package logic import ( "context" "errors" "fmt" "strconv" "strings" "time" "bj_power_mes/ent" "bj_power_mes/ent/dailyplan" "bj_power_mes/ent/materialrequest" "bj_power_mes/ent/workorder" "bj_power_mes/ent/workpiece" ) type WorkOrderReq struct { Id int `json:"id"` WorkOrderNo string `json:"workOrderNo"` ProductTypeId int `json:"productTypeId"` RouteId int `json:"routeId"` ProductCode string `json:"productCode"` ProductName string `json:"productName"` Quantity int `json:"quantity"` ProcessSeq string `json:"processSeq"` Status string `json:"status"` PlanStart string `json:"planStart"` PlanEnd string `json:"planEnd"` } // CreateWorkOrder 创建工单(状态恒 CREATED,后续用状态流转按钮推进) func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operator string) error { if req.WorkOrderNo == "" { return errors.New("工单号不能为空") } if req.ProductTypeId == 0 { return errors.New("请选择产品型号") } if req.Quantity <= 0 { req.Quantity = 1 } status := req.Status if status == "" { status = "CREATED" } if status != "CREATED" { status = "CREATED" // 创建恒为已创建;不允许直接创建到后续状态 } routeSnapshot := []map[string]any{} if req.RouteId > 0 { route, rerr := s.GetRoute(ctx, req.RouteId) if rerr != nil { return rerr } snapshot, lineStations := s.BuildRouteSnapshot(route) routeSnapshot = snapshot if len(lineStations) > 0 { parts := make([]string, 0, len(lineStations)) for _, no := range lineStations { parts = append(parts, strconv.Itoa(no)) } req.ProcessSeq = strings.Join(parts, ",") } else { req.ProcessSeq = "" } } else { if req.ProcessSeq == "" { req.ProcessSeq = FullProcessSeq } else if err := ValidateProcessSeq(req.ProcessSeq); err != nil { return err } req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq) } b := s.ctx.EntClient.WorkOrder.Create(). SetWorkOrderNo(req.WorkOrderNo). SetProductTypeId(req.ProductTypeId). SetProductCode(req.ProductCode). SetProductName(req.ProductName). SetQuantity(req.Quantity). SetProcessSeq(req.ProcessSeq).SetRouteId(req.RouteId).SetRouteSnapshot(routeSnapshot). SetStatus(status) if req.PlanStart != "" { if t, err := parseDate(req.PlanStart); err == nil { b.SetPlanStart(t) } } if req.PlanEnd != "" { if t, err := parseDate(req.PlanEnd); err == nil { b.SetPlanEnd(t) } } if _, err := b.Save(ctx); err != nil { return err } s.ctx.EventLog.Write(ctx, "work.order.create", req.WorkOrderNo, operator, "work_order", req.WorkOrderNo, "创建工单", map[string]any{"quantity": req.Quantity}) s.notifyDashboard() return nil } // UpdateWorkOrder 编辑工单。仅「已创建」状态允许编辑(已下发/执行中/已暂停只能流转,已完成/已取消只读)。 func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operator string) error { if req.Id == 0 { return errors.New("缺少 id") } old, err := s.ctx.EntClient.WorkOrder.Get(ctx, req.Id) if err != nil { return errors.New("工单不存在") } if old.Status != "CREATED" && old.Status != "PENDING" { return errors.New("仅「已创建」状态的工单可编辑;生产中或已结束的工单只能做状态流转") } if req.ProcessSeq != "" { if err := ValidateProcessSeq(req.ProcessSeq); err != nil { return err } req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq) } u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).SetProcessSeq(req.ProcessSeq) if req.Quantity > 0 { u.SetQuantity(req.Quantity) } if req.ProductCode != "" { u.SetProductCode(req.ProductCode) } if req.ProductName != "" { u.SetProductName(req.ProductName) } if req.PlanStart != "" { if t, err := parseDate(req.PlanStart); err == nil { u.SetPlanStart(t) } } if req.PlanEnd != "" { if t, err := parseDate(req.PlanEnd); err == nil { u.SetPlanEnd(t) } } if err := u.Exec(ctx); err != nil { return err } wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, req.Id) if err == nil { s.ctx.EventLog.Write(ctx, "work.order.update", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "更新工单", nil) } s.notifyDashboard() return nil } // ListWorkOrders 查询工单(支持工单号/状态/产品编码/产品名称 过滤) func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status, productCode, productName string) ([]*ent.WorkOrder, error) { q := s.ctx.EntClient.WorkOrder.Query() if orderNo != "" { q = q.Where(workorder.WorkOrderNoContains(orderNo)) } if status != "" { q = q.Where(workorder.Status(status)) } if productCode != "" { q = q.Where(workorder.ProductCodeContains(productCode)) } if productName != "" { q = q.Where(workorder.ProductNameContains(productName)) } return q.Order(ent.Desc(workorder.FieldCreatedAt), ent.Desc(workorder.FieldID)).All(ctx) } func (s *Service) GetWorkOrder(ctx context.Context, id int) (*ent.WorkOrder, error) { if id > 0 { return s.ctx.EntClient.WorkOrder.Get(ctx, id) } return s.ctx.EntClient.WorkOrder.Query().Order(ent.Desc(workorder.FieldID)).First(ctx) } // 工单状态白名单(统一枚举:已创建/已下发/执行中/已暂停/已完成/已取消) var workOrderStatuses = map[string]bool{ "CREATED": true, "RELEASED": true, "IN_PROGRESS": true, "PAUSED": true, "DONE": true, "CANCELLED": true, } // workOrderTransitions 工单状态流转表:from → {to...} // CREATED 可下发/取消;RELEASED 可开工/取消;IN_PROGRESS 可暂停/完工; // PAUSED 可恢复/取消;DONE/CANCELLED 为终态,不再流转。 var workOrderTransitions = map[string]map[string]bool{ "CREATED": {"RELEASED": true, "CANCELLED": true}, "RELEASED": {"IN_PROGRESS": true, "CANCELLED": true}, "IN_PROGRESS": {"PAUSED": true, "DONE": true}, "PAUSED": {"IN_PROGRESS": true, "CANCELLED": true}, "DONE": {}, "CANCELLED": {}, // 兼容存量旧枚举:PENDING(待排产)≈已创建可发布,CONFIRMED(已确认)≈已下发可开工 "PENDING": {"RELEASED": true, "CANCELLED": true}, "CONFIRMED": {"IN_PROGRESS": true, "CANCELLED": true}, } // SetWorkOrderStatus 工单状态流转(发布/开工/暂停/恢复/完工/取消)。 // 校验流转合法性;写 eventlog 记录 原状态/新状态/原因;终态联动处理(见 cancelPlansOfOrder)。 func (s *Service) SetWorkOrderStatus(ctx context.Context, id int, status, reason, operator string) error { if id <= 0 { return errors.New("缺少 id") } if !workOrderStatuses[status] { return errors.New("非法的工单状态") } wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id) if err != nil { return errors.New("工单不存在") } if wo.Status == status { return nil // 幂等:状态相同直接成功 } if !workOrderTransitions[wo.Status][status] { return fmt.Errorf("不允许从「%s」变更为「%s」", statusTextCN(wo.Status), statusTextCN(status)) } // 单工单互斥:一条产线同时只能有一个在产工单(下发/开工前校验) if status == "RELEASED" || status == "IN_PROGRESS" { others, _ := s.ctx.EntClient.WorkOrder.Query(). Where(workorder.StatusIn("IN_PROGRESS", "PAUSED")). All(ctx) for _, o := range others { if o.ID != id { return fmt.Errorf("产线上已有在产工单「%s」(%s),完成或暂停后方可下发/开工新工单", o.WorkOrderNo, statusTextCN(o.Status)) } } } upd := s.ctx.EntClient.WorkOrder.UpdateOneID(id).SetStatus(status) if status == "DONE" { now := time.Now() upd.SetCompletedAt(now) } if err := upd.Exec(ctx); err != nil { return err } // 状态变更操作日志:原状态/新状态/原因 s.ctx.EventLog.Write(ctx, "work.order.status", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "变更工单状态", map[string]any{"from": wo.Status, "to": status, "reason": reason}) // 联动:DONE/CANCELLED → 该工单未完成排产批量取消;IN_PROGRESS → 排产置执行中 s.syncPlansByOrderStatus(ctx, wo.WorkOrderNo, status, operator) s.notifyDashboard() return nil } // statusTextCN 状态枚举中文文案 func statusTextCN(s string) string { m := map[string]string{ "CREATED": "已创建", "RELEASED": "已下发", "IN_PROGRESS": "执行中", "PAUSED": "已暂停", "DONE": "已完成", "CANCELLED": "已取消", } if v, ok := m[s]; ok { return v } return s } // DeleteWorkOrder 删除工单。 // 安全条件:① 状态仅限 已创建(CREATED,兼容存量 PENDING/CONFIRMED 未开始生产);② 无日排产记录; // ③ 未生成备料单;④ 无任何工件生产数据。已下发/执行中/已暂停/已完成/已取消一律不可删除。 func (s *Service) DeleteWorkOrder(ctx context.Context, id int, operator string) error { if id <= 0 { return errors.New("缺少 id") } wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id) if err != nil { return errors.New("工单不存在") } if wo.Status != "CREATED" && wo.Status != "PENDING" && wo.Status != "CONFIRMED" { return errors.New("仅「已创建」状态的工单可删除;已下发、执行中或已结束的工单不能删除") } if n, err := s.ctx.EntClient.DailyPlan.Query().Where(dailyplan.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 { return errors.New("该工单已有日排产记录,请先删除对应排产后再删除工单") } if n, err := s.ctx.EntClient.MaterialRequest.Query().Where(materialrequest.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 { return errors.New("该工单已生成备料单,不能删除") } if n, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 { return errors.New("该工单已有工件生产数据,不能删除") } if err := s.ctx.EntClient.WorkOrder.DeleteOneID(id).Exec(ctx); err != nil { return err } s.ctx.EventLog.Write(ctx, "work.order.delete", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "删除工单", map[string]any{"status": wo.Status}) return nil }