feat:多项功能优化
This commit is contained in:
@@ -120,6 +120,15 @@ var schemaPatchSQL = []string{
|
||||
`ALTER TABLE work_order ADD COLUMN IF NOT EXISTS created_by varchar(64) NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE work_order ADD COLUMN IF NOT EXISTS due_date timestamp`,
|
||||
`ALTER TABLE workpiece_process ADD COLUMN IF NOT EXISTS duration_sec integer NOT NULL DEFAULT 0`,
|
||||
|
||||
// 工位内置标识 + 是否有接驳台(实体位置由内置数据给定,页面不可手改)
|
||||
`ALTER TABLE station ADD COLUMN IF NOT EXISTS is_builtin boolean NOT NULL DEFAULT false`,
|
||||
`ALTER TABLE station ADD COLUMN IF NOT EXISTS has_dock boolean NOT NULL DEFAULT false`,
|
||||
// 存量数据回填:种子初始化的工位号(0/1..13)一律为内置工位;
|
||||
// 其中 1~10 号工位对应实体接驳台(R1/R2),0/11/12/13 无接驳台。
|
||||
// 页面新增的工位号从 14 起,不受本回填影响。
|
||||
`UPDATE station SET is_builtin = true WHERE station_no BETWEEN 0 AND 13`,
|
||||
`UPDATE station SET has_dock = true WHERE station_no BETWEEN 1 AND 10`,
|
||||
}
|
||||
|
||||
// EnsureSchema ent 建表 + 幂等列补丁(只增,不删数据)
|
||||
|
||||
@@ -31,8 +31,7 @@ var pathPermMap = map[string]string{
|
||||
"POST:/api/v1/process-flows/upload": "produce.processflow:upload",
|
||||
"POST:/api/v1/process-steps": "produce.processflow:edit",
|
||||
"POST:/api/v1/stations": "produce.station:edit",
|
||||
// 数量不符上报(菜单 produce.qtyreport 下新增)
|
||||
"POST:/api/v1/qty-reports": "produce.qtyreport",
|
||||
"DELETE:/api/v1/stations/*": "produce.station:delete",
|
||||
// 过程巡检汇总生成《工序间检验记录》(巡检终端操作域)
|
||||
"POST:/api/v1/inspections/generate-inter-process": "sys.inspect:process",
|
||||
// 质量检验处置(过程检/完工检不合格处置 退货/返修/退换)
|
||||
|
||||
@@ -112,6 +112,17 @@ func SaveStationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStationHandler DELETE /stations/:id(删除工位:内置工位不允许删除)
|
||||
func DeleteStationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := logic.New(svcCtx).DeleteStation(r.Context(), pathId(r), operator(r, "")); err != nil {
|
||||
httpx.Fail(w, 2208, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "工位已删除", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFlowStatusHandler POST /process-flows/status(工艺流程启用/停用)
|
||||
func SetFlowStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -81,16 +81,29 @@ func ListAlertRulesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ListAlertsHandler GET /alerts?status= 预警消息收件箱
|
||||
// ListAlertsHandler GET /alerts?status=&type=&from=&to=&page=&pageSize=
|
||||
// 预警消息收件箱:支持 状态/类型/时间范围(YYYY-MM-DD) 筛选;带 page 返回分页对象,否则返回全量数组(顶部铃铛未读数复用)。
|
||||
func ListAlertsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
data, err := logic.New(svcCtx).ListAlerts(r.Context(), status)
|
||||
q := r.URL.Query()
|
||||
query := logic.AlertQuery{
|
||||
Status: q.Get("status"),
|
||||
Type: q.Get("type"),
|
||||
From: q.Get("from"),
|
||||
To: q.Get("to"),
|
||||
Page: atoiDefault(q.Get("page"), 0),
|
||||
PageSize: atoiDefault(q.Get("pageSize"), 20),
|
||||
}
|
||||
list, total, err := logic.New(svcCtx).ListAlerts(r.Context(), query)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3305, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
if query.Page > 0 {
|
||||
httpx.Ok(w, map[string]any{"list": list, "total": total, "page": query.Page, "pageSize": query.PageSize})
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, list)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ func ExportProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("产品型号.xlsx"))
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("物料档案.xlsx"))
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodGet, Path: "/torque/groups", Handler: production.TorqueGroupsHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/torque/stat", Handler: production.TorqueStatHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/torque/audit", Handler: production.TorqueAuditHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPost, Path: "/scan/report", Handler: production.ScanReportHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/scan/records", Handler: production.ScanRecordsHandler(serverCtx)},
|
||||
@@ -217,6 +217,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodPost, Path: "/process-flows/upload", Handler: UploadPdfHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/stations", Handler: ListStationsHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/stations", Handler: SaveStationHandler(serverCtx)},
|
||||
{Method: http.MethodDelete, Path: "/stations/:id", Handler: DeleteStationHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/line/points", Handler: ListPointsHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/line/move", Handler: IssueMoveHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/line/move/ack", Handler: AckMoveHandler(serverCtx)},
|
||||
@@ -235,10 +236,6 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodGet, Path: "/quality/unqualified-report", Handler: UnqualifiedReportHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/inspections/generate-inter-process", Handler: GenerateInterProcessInspectionHandler(serverCtx)},
|
||||
|
||||
// ---------- 数量不符上报(第三批) ----------
|
||||
{Method: http.MethodPost, Path: "/qty-reports", Handler: CreateQtyReportHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/qty-reports", Handler: ListQtyReportsHandler(serverCtx)},
|
||||
|
||||
// ---------- 应急呼叫(D3,MES 前端亦可发起) ----------
|
||||
{Method: http.MethodPost, Path: "/alert/emergency", Handler: EmergencyCallHandler(serverCtx)},
|
||||
|
||||
|
||||
@@ -170,36 +170,6 @@ func EmergencyCallHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateQtyReportHandler POST /api/v1/qty-reports
|
||||
func CreateQtyReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req logic.QtyReportReq
|
||||
if err := httpx.ParseJSON(r, &req); err != nil {
|
||||
httpx.BadRequest(w, "请求体解析失败")
|
||||
return
|
||||
}
|
||||
req.Operator = operator(r, req.Operator)
|
||||
if err := logic.New(svcCtx).CreateQtyReport(r.Context(), req); err != nil {
|
||||
httpx.Fail(w, 2402, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "数量不符已上报", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ListQtyReportsHandler GET /api/v1/qty-reports?status=
|
||||
func ListQtyReportsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).ListQtyReports(r.Context(),
|
||||
r.URL.Query().Get("status"), r.URL.Query().Get("orderNo"), r.URL.Query().Get("materialCode"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2404, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateInterProcessInspectionHandler POST /api/v1/inspections/generate-inter-process
|
||||
// 一键生成《工序间检验记录》:汇总该工单 PROCESS 类巡检记录(按步骤维度)。
|
||||
func GenerateInterProcessInspectionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
@@ -3,6 +3,7 @@ package logic
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/alert"
|
||||
@@ -48,13 +49,48 @@ func (s *Service) ListAlertRules(ctx context.Context) ([]*ent.AlertRule, error)
|
||||
Order(ent.Desc(alertrule.FieldCreatedAt), ent.Desc(alertrule.FieldID)).All(ctx)
|
||||
}
|
||||
|
||||
// ListAlerts 查询预警消息(status 空=全部)
|
||||
func (s *Service) ListAlerts(ctx context.Context, status string) ([]*ent.Alert, error) {
|
||||
q := s.ctx.EntClient.Alert.Query()
|
||||
if status != "" {
|
||||
q = q.Where(alert.Status(status))
|
||||
// AlertQuery 预警查询条件:status/type 空=不限;from/to 为 YYYY-MM-DD(空=不限);page<=0 表示不分页。
|
||||
type AlertQuery struct {
|
||||
Status string
|
||||
Type string
|
||||
From string
|
||||
To string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// ListAlerts 查询预警消息(支持状态/类型/时间范围筛选 + 分页),返回当前页数据与命中总数
|
||||
func (s *Service) ListAlerts(ctx context.Context, q AlertQuery) ([]*ent.Alert, int, error) {
|
||||
query := s.ctx.EntClient.Alert.Query()
|
||||
if q.Status != "" {
|
||||
query = query.Where(alert.Status(q.Status))
|
||||
}
|
||||
return q.Order(ent.Desc(alert.FieldCreatedAt), ent.Desc(alert.FieldID)).All(ctx)
|
||||
if q.Type != "" {
|
||||
query = query.Where(alert.Type(q.Type))
|
||||
}
|
||||
if q.From != "" {
|
||||
if f, err := time.Parse("2006-01-02", q.From); err == nil {
|
||||
query = query.Where(alert.CreatedAtGTE(f))
|
||||
}
|
||||
}
|
||||
if q.To != "" {
|
||||
if t, err := time.Parse("2006-01-02", q.To); err == nil {
|
||||
query = query.Where(alert.CreatedAtLT(t.Add(24 * time.Hour)))
|
||||
}
|
||||
}
|
||||
total, err := query.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if q.Page > 0 {
|
||||
size := q.PageSize
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
query = query.Offset((q.Page - 1) * size).Limit(size)
|
||||
}
|
||||
list, err := query.Order(ent.Desc(alert.FieldCreatedAt), ent.Desc(alert.FieldID)).All(ctx)
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// UnreadAlertCount 未读预警数
|
||||
|
||||
@@ -255,6 +255,8 @@ type StationVO struct {
|
||||
FlowName string `json:"flowName"`
|
||||
StationType string `json:"stationType"`
|
||||
Status string `json:"status"`
|
||||
IsBuiltin bool `json:"isBuiltin"`
|
||||
HasDock bool `json:"hasDock"`
|
||||
}
|
||||
|
||||
func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
|
||||
@@ -269,7 +271,7 @@ func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
|
||||
}
|
||||
out := make([]*StationVO, 0, len(stas))
|
||||
for _, st := range stas {
|
||||
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, StationType: st.StationType, Status: st.Status}
|
||||
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, StationType: st.StationType, Status: st.Status, IsBuiltin: st.IsBuiltin, HasDock: st.HasDock}
|
||||
if f, ok := flowMap[st.FlowId]; ok {
|
||||
vo.FlowName = f.Name
|
||||
}
|
||||
@@ -283,6 +285,9 @@ func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
|
||||
// - 工位号不存在 → 新增工位;新增的工位号由用户自定义,允许扩展 14、15…(虚拟/物理均可)
|
||||
//
|
||||
// 工位数量以数据库为准,前端下拉随 station 表自动增减;虚拟工位(0/13/14...)不连 PLC、仅记录。
|
||||
// 内置工位(种子/初始化数据,is_builtin=true)需连 PLC 并必须按工位号顺序流转,
|
||||
// 不允许手动更改工位类型;页面新增的工位一律为非内置,可改类型、可删除。
|
||||
// 「是否有接驳台」对应实体位置,由内置数据给定,不接收页面入参。
|
||||
func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error {
|
||||
if req.StationNo < 0 {
|
||||
return errors.New("工位号非法(需 ≥ 0)")
|
||||
@@ -304,7 +309,10 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
|
||||
}
|
||||
}
|
||||
cr := s.ctx.EntClient.Station.Create().
|
||||
SetStationNo(req.StationNo).SetName(req.Name).SetStationType(stype).SetStatus("ENABLED")
|
||||
SetStationNo(req.StationNo).SetName(req.Name).SetStationType(stype).SetStatus("ENABLED").
|
||||
// 页面「新增工位」创建的是非内置工位(可改类型、可删除);内置工位只能由种子/初始化数据产生。
|
||||
// 是否有接驳台对应实体位置,由内置数据给定,页面新增时一律为 false 且不可手改。
|
||||
SetIsBuiltin(false).SetHasDock(false)
|
||||
if req.FlowId != nil && *req.FlowId > 0 {
|
||||
flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, *req.FlowId)
|
||||
if fErr != nil {
|
||||
@@ -330,7 +338,13 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
|
||||
if t != "LINE" && t != "OFFLINE" {
|
||||
return errors.New("工位类型仅支持 LINE/OFFLINE")
|
||||
}
|
||||
upd.SetStationType(t)
|
||||
// 内置工位对应实体产线固定位置(含 PLC 通讯与顺序流转),工位类型不可手动更改。
|
||||
if st.IsBuiltin && t != st.StationType {
|
||||
return errors.New("内置工位不允许修改工位类型")
|
||||
}
|
||||
if !st.IsBuiltin {
|
||||
upd.SetStationType(t)
|
||||
}
|
||||
}
|
||||
if req.FlowId != nil {
|
||||
fid := *req.FlowId
|
||||
@@ -355,6 +369,27 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStation 删除工位:内置工位不允许删除(对应实体产线位置,需连 PLC 且必须按顺序流转);
|
||||
// 非内置工位(页面新增的)允许删除。
|
||||
func (s *Service) DeleteStation(ctx context.Context, id int, operator string) error {
|
||||
if id <= 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
st, err := s.ctx.EntClient.Station.Get(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("工位不存在")
|
||||
}
|
||||
if st.IsBuiltin {
|
||||
return errors.New("内置工位不允许删除")
|
||||
}
|
||||
if err := s.ctx.EntClient.Station.DeleteOneID(st.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "station.delete", "", operator, "station", "", "删除工位",
|
||||
map[string]any{"stationNo": st.StationNo, "name": st.Name})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFlowStatus 工艺流程启用/停用。
|
||||
// 新语义:绑定关系由「保存流程」显式维护(一个流程可绑多工位),启停只切换流程状态,
|
||||
// 不再自动解绑/绑定工位;停用流程对已绑定工位不可用(工位取步骤时按 ACTIVE 过滤)。
|
||||
|
||||
@@ -56,11 +56,11 @@ func (s *Service) DeleteProductType(ctx context.Context, id int) error {
|
||||
}
|
||||
woCnt, _ := s.ctx.EntClient.WorkOrder.Query().Where(workorder.ProductCode(row.Code)).Count(ctx)
|
||||
if woCnt > 0 {
|
||||
return fmt.Errorf("该产品型号已被 %d 张工单引用,不可删除;建议停用(下架)", woCnt)
|
||||
return fmt.Errorf("该物料档案已被 %d 张工单引用,不可删除;建议停用(下架)", woCnt)
|
||||
}
|
||||
bomCnt, _ := s.ctx.EntClient.BomItem.Query().Where(bomitem.ProductCode(row.Code)).Count(ctx)
|
||||
if bomCnt > 0 {
|
||||
return fmt.Errorf("该产品型号在物料清单中有 %d 条配置,不可删除;建议停用(下架)", bomCnt)
|
||||
return fmt.Errorf("该物料档案在物料清单中有 %d 条配置,不可删除;建议停用(下架)", bomCnt)
|
||||
}
|
||||
if err := s.ctx.Wms.DeleteProductType(ctx, id); err != nil {
|
||||
return errors.New("WMS 不可达或拒绝删除:" + err.Error())
|
||||
@@ -112,12 +112,12 @@ func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.Produ
|
||||
return &list[i], nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("产品型号不存在")
|
||||
return nil, errors.New("物料档案不存在")
|
||||
}
|
||||
// 降级:本地缓存
|
||||
pt, err := s.ctx.EntClient.ProductType.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New("产品型号不存在(WMS 不可达,无法校验引用)")
|
||||
return nil, errors.New("物料档案不存在(WMS 不可达,无法校验引用)")
|
||||
}
|
||||
return &wmsclient.ProductTypeRow{ID: pt.ID, Code: pt.Code, Name: pt.Name}, nil
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
"produce.trace", "produce.wip", "produce.torque", "produce.plc", "produce.product",
|
||||
"produce.processflow", "produce.station", "produce.performance", "produce.processcard",
|
||||
// 按钮级权限(块2)
|
||||
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
|
||||
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
|
||||
"produce.qtyreport", "produce.quality", "produce.quality:edit",
|
||||
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
|
||||
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
|
||||
"produce.quality", "produce.quality:edit",
|
||||
"produce.plc:send", "produce.product:add", "produce.product:edit", "produce.product:delete",
|
||||
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
|
||||
"produce.processflow:upload", "produce.station:edit", "produce.station:add",
|
||||
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
|
||||
"produce.processflow:upload", "produce.station:edit", "produce.station:add", "produce.station:delete",
|
||||
}
|
||||
} else if r.code == "INSPECTOR" {
|
||||
codes = []string{"produce.trace", "produce.wip", "produce.torque", "produce.performance", "sys.eventlog",
|
||||
@@ -74,13 +74,12 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.plc", "工位组合下发", "MENU", "/plc-send"},
|
||||
{"produce.torque", "拧紧查询", "MENU", "/torque"},
|
||||
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
|
||||
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
|
||||
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
|
||||
{"produce.scan", "手动报工", "MENU", "/scan"},
|
||||
{"produce.trace", "工件追溯", "MENU", "/trace"},
|
||||
{"produce.wip", "在制品", "MENU", "/wip"},
|
||||
{"produce.processcard", "生产流程卡", "MENU", "/process-card"},
|
||||
{"produce.product", "产品型号", "MENU", "/product-type"},
|
||||
{"produce.qtyreport", "数量不符上报", "MENU", "/qty-report"},
|
||||
{"produce.product", "物料档案", "MENU", "/product-type"},
|
||||
{"produce.quality", "质量检验", "MENU", "/quality"},
|
||||
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
|
||||
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
|
||||
@@ -107,15 +106,16 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.bom:edit", "物料清单-维护", "BUTTON", "", "produce.bom"},
|
||||
{"produce.material:generate", "备料单-生成", "BUTTON", "", "produce.material"},
|
||||
{"produce.plc:send", "工位组合-下发", "BUTTON", "", "produce.plc"},
|
||||
{"produce.product:add", "产品型号-新增", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:edit", "产品型号-编辑", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:delete", "产品型号-删除", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:add", "物料档案-新增", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:edit", "物料档案-编辑", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:delete", "物料档案-删除", "BUTTON", "", "produce.product"},
|
||||
{"produce.processflow:add", "工艺流程-新增", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.processflow:edit", "工艺流程-编辑", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.processflow:upload", "工艺流程-上传图纸", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.station:edit", "工位-绑定与改名", "BUTTON", "", "produce.station"},
|
||||
{"produce.station:add", "工位-新增", "BUTTON", "", "produce.station"},
|
||||
{"produce.station:delete", "工位-删除", "BUTTON", "", "produce.station"},
|
||||
{"sys.account:add", "账号-新增", "BUTTON", "", "sys.account"},
|
||||
{"sys.account:edit", "账号-编辑", "BUTTON", "", "sys.account"},
|
||||
{"sys.account:delete", "账号-删除", "BUTTON", "", "sys.account"},
|
||||
@@ -151,6 +151,10 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
// 3.3 工艺路线模块已整块删除(D1):清掉存量角色里的路线权限码与旧菜单/按钮定义行(幂等)
|
||||
migrateLegacyRoute(ctx, s)
|
||||
|
||||
// 3.4 MES「数量不符上报」菜单已下线(改为仅预警,明细由 WMS 处理):
|
||||
// 清掉存量角色里的 produce.qtyreport 权限码与旧菜单定义行(幂等)
|
||||
migrateLegacyQtyReport(ctx, s)
|
||||
|
||||
// 4. 初始化工位与默认工艺流程(工位绑定流程,流程承载步骤)
|
||||
seedFlowsAndStations(ctx, s)
|
||||
return nil
|
||||
@@ -158,11 +162,14 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
|
||||
func seedFlowsAndStations(ctx context.Context, s *Service) {
|
||||
// 初始数据:12 个工位(工位数量以数据库为准,这里只做首次初始化;要 13 个就往 station 表插一行)
|
||||
// 初始化的工位一律为「内置工位」:需连 PLC、必须按工位号顺序流转、不可改类型、不可删除;
|
||||
// 其中 1~10 号工位对应实体接驳台,11/12 为产线末端无接驳台。
|
||||
for i := 1; i <= 12; i++ {
|
||||
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
|
||||
if err != nil {
|
||||
st, _ = s.ctx.EntClient.Station.Create().
|
||||
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").Save(ctx)
|
||||
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").
|
||||
SetIsBuiltin(true).SetHasDock(i <= 10).Save(ctx)
|
||||
}
|
||||
if st == nil || st.FlowId > 0 {
|
||||
continue
|
||||
@@ -191,7 +198,8 @@ func seedFlowsAndStations(ctx context.Context, s *Service) {
|
||||
} {
|
||||
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(v.no)).First(ctx); err != nil {
|
||||
_, _ = s.ctx.EntClient.Station.Create().
|
||||
SetStationNo(v.no).SetName(v.name).SetStationType("OFFLINE").SetStatus("ENABLED").Save(ctx)
|
||||
SetStationNo(v.no).SetName(v.name).SetStationType("OFFLINE").
|
||||
SetIsBuiltin(true).SetHasDock(false).SetStatus("ENABLED").Save(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,3 +287,27 @@ func migrateLegacyRoute(ctx context.Context, s *Service) {
|
||||
Where(permission.Code(code)).Exec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// migrateLegacyQtyReport 清理已下线的「数量不符上报」菜单(幂等,可重复执行):
|
||||
// MES 侧对数量不符只做预警(预警中心 + 右上角铃铛),不再提供独立菜单页;
|
||||
// 从存量角色的 permissionCodes 摘除 produce.qtyreport 并删除其权限定义行。
|
||||
func migrateLegacyQtyReport(ctx context.Context, s *Service) {
|
||||
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
|
||||
for _, rl := range roleList {
|
||||
kept := make([]string, 0, len(rl.PermissionCodes))
|
||||
changed := false
|
||||
for _, c := range rl.PermissionCodes {
|
||||
if c == "produce.qtyreport" {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
if changed {
|
||||
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
|
||||
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
|
||||
}
|
||||
}
|
||||
_, _ = s.ctx.EntClient.Permission.Delete().
|
||||
Where(permission.Code("produce.qtyreport")).Exec(ctx)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/inspectionrecord"
|
||||
"bj_power_mes/ent/materialqtyreport"
|
||||
"bj_power_mes/ent/workorder"
|
||||
)
|
||||
|
||||
@@ -51,8 +50,8 @@ type QtyReportReq struct {
|
||||
StationNo int `json:"stationNo"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
Sn string `json:"sn"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
PlanQty int `json:"planQty"`
|
||||
ActualQty int `json:"actualQty"`
|
||||
Cause string `json:"cause"`
|
||||
@@ -60,6 +59,8 @@ type QtyReportReq struct {
|
||||
}
|
||||
|
||||
// CreateQtyReport 数量不符上报:落 material_qty_report + 预警中心消息(type=qty_diff)。
|
||||
// MES 侧仅预警,不承载明细处理流程:相关内容融合为一段文字描述写入预警内容,
|
||||
// 由预警中心与右上角铃铛统一呈现;明细与闭环由 WMS 负责。
|
||||
func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
|
||||
if req.StationNo <= 0 || req.MaterialCode == "" {
|
||||
return errors.New("工位号与物料编码必填")
|
||||
@@ -81,12 +82,27 @@ func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := fmt.Sprintf("工位 %d 数量不符:物料 %s 应发 %d / 实收 %d / 差异 %d。原因:%s",
|
||||
req.StationNo, req.MaterialCode, req.PlanQty, req.ActualQty, diff, req.Cause)
|
||||
// 融合成单段文字描述,直接写入预警「内容」(即预警原因)。
|
||||
cause := req.Cause
|
||||
if cause == "" {
|
||||
cause = "未填写"
|
||||
}
|
||||
content := fmt.Sprintf("工位%d 数量不符:物料 %s", req.StationNo, req.MaterialCode)
|
||||
if req.MaterialName != "" {
|
||||
content += "(" + req.MaterialName + ")"
|
||||
}
|
||||
content += fmt.Sprintf(";应发 %d,实收 %d,差异 %d", req.PlanQty, req.ActualQty, diff)
|
||||
if req.OrderNo != "" {
|
||||
content += ";工单 " + req.OrderNo
|
||||
}
|
||||
if req.Sn != "" {
|
||||
content += ";序列号 " + req.Sn
|
||||
}
|
||||
content += ";上报原因:" + cause
|
||||
_, _ = s.ctx.EntClient.Alert.Create().
|
||||
SetRuleId(0).
|
||||
SetType("qty_diff").
|
||||
SetTitle("数量不符上报").
|
||||
SetTitle("数量不符预警").
|
||||
SetContent(content).
|
||||
SetRefType("material_qty_report").
|
||||
SetRefId(req.MaterialCode).
|
||||
@@ -99,21 +115,6 @@ func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListQtyReports 数量不符上报列表
|
||||
func (s *Service) ListQtyReports(ctx context.Context, status, orderNo, materialCode string) ([]*ent.MaterialQtyReport, error) {
|
||||
q := s.ctx.EntClient.MaterialQtyReport.Query()
|
||||
if status != "" {
|
||||
q = q.Where(materialqtyreport.Status(status))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(materialqtyreport.OrderNoContainsFold(orderNo))
|
||||
}
|
||||
if materialCode != "" {
|
||||
q = q.Where(materialqtyreport.MaterialCodeContainsFold(materialCode))
|
||||
}
|
||||
return q.Order(ent.Desc(materialqtyreport.FieldCreatedAt), ent.Desc(materialqtyreport.FieldID)).All(ctx)
|
||||
}
|
||||
|
||||
// StationReturnMaterial 工位退料回库房:落事件日志 + 调 WMS 预建待确认退库单。
|
||||
// 退库单在 WMS 侧由库管确认收货后库存加回(链路封闭,问题记录 L491)。
|
||||
func (s *Service) StationReturnMaterial(ctx context.Context, orderNo string, stationNo int, sn, materialCode, materialName, spec, reason, operator, unit string, qty int) error {
|
||||
|
||||
Reference in New Issue
Block a user