package logic import ( "context" "errors" "strconv" "strings" "time" "bj_power_mes/ent" "bj_power_mes/ent/dailyplan" "bj_power_mes/ent/workorder" ) // parseDate 解析 yyyy-MM-dd 或标准时间 func parseDate(v string) (time.Time, error) { if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil { return t, nil } return time.ParseInLocation(time.RFC3339, v, time.Local) } type DailyPlanReq struct { Id int `json:"id"` OrderNo string `json:"orderNo"` PlanDate string `json:"planDate"` PlanQty int `json:"planQty"` CompletedQty int `json:"completedQty"` Status string `json:"status"` DockCodes []string `json:"dockCodes"` } // SaveDailyPlan 创建/更新日排产(同一 工单+日期 唯一) // 排产状态枚举统一为:PENDING(待执行)/PROCESSING(执行中)/DONE(已完成)/CANCELLED(已取消) func (s *Service) SaveDailyPlan(ctx context.Context, req DailyPlanReq, operator string) error { if req.OrderNo == "" || req.PlanDate == "" { return errors.New("工单号和排产日期必填") } // 校验:该工单所有日排产计划数量累计不得超过工单总数量(保存时合计,改小可、改大不超总量) wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx) if err != nil { return errors.New("工单不存在") } if wo.Status == "DONE" || wo.Status == "CANCELLED" { return errors.New("该工单已完成或已取消,不能排产") } if wo.Status == "PAUSED" { return errors.New("该工单已暂停,恢复执行前不能新增排产") } cur, err := s.ctx.EntClient.DailyPlan.Query(). Where(dailyplan.OrderNo(req.OrderNo)).All(ctx) if err != nil { return err } // 计算合计:本次 req.PlanQty 恒计入;若「工单+日期」已有记录,则原值作废只算新值(避免旧值+新值重复累计) total := req.PlanQty for _, p := range cur { if p.PlanDate == req.PlanDate { if p.CompletedQty > 0 { return errors.New("该排产日已有完工数量,不能修改计划数量") } continue } total += p.PlanQty } if total > wo.Quantity { return errors.New("排产合计数量超过工单总数量,无法保存") } exist, err := s.ctx.EntClient.DailyPlan.Query(). Where(dailyplan.OrderNo(req.OrderNo), dailyplan.PlanDate(req.PlanDate)).First(ctx) status := req.Status switch status { case "", "PENDING", "PROCESSING", "DONE", "CANCELLED": if status == "" { status = "PENDING" } default: return errors.New("非法的排产状态") } if err == nil && exist != nil { _, err = s.ctx.EntClient.DailyPlan.UpdateOneID(exist.ID). SetOrderNo(req.OrderNo).SetPlanDate(req.PlanDate).SetPlanQty(req.PlanQty).SetStatus(status). SetDockCodes(dockCodes(req.DockCodes)).Save(ctx) if err != nil { return err } } else { _, err = s.ctx.EntClient.DailyPlan.Create(). SetOrderNo(req.OrderNo).SetPlanDate(req.PlanDate).SetPlanQty(req.PlanQty). SetStatus(status).SetOperator(operator).SetDockCodes(dockCodes(req.DockCodes)).Save(ctx) if err != nil { return err } } // 记录该工单本次使用的接驳台到工单 context,供下次排产默认推荐 if len(dockCodes(req.DockCodes)) > 0 { s.rememberOrderDocks(ctx, req.OrderNo, dockCodes(req.DockCodes)) } s.ctx.EventLog.Write(ctx, "daily.plan.save", req.OrderNo, operator, "daily_plan", req.OrderNo, "保存日排产", map[string]any{"planDate": req.PlanDate, "planQty": req.PlanQty, "dockCodes": req.DockCodes}) s.notifyDashboard() return nil } // dockCodes 规范化接驳台列表:仅保留 DOCK01..DOCK20 / DOCK21,去重、保持顺序 func dockCodes(codes []string) []string { seen := map[string]bool{} out := []string{} for _, c := range codes { c = strings.TrimSpace(c) if !validDockCode(c) || seen[c] { continue } seen[c] = true out = append(out, c) } return out } // validDockCode DOCK01..DOCK21 func validDockCode(c string) bool { if len(c) != 6 || !strings.HasPrefix(c, "DOCK") { return false } n, err := strconv.Atoi(c[4:]) return err == nil && n >= 1 && n <= 21 } // rememberOrderDocks 把工单「上次使用的接驳台」写入 work_order.context,供下次排产默认推荐 func (s *Service) rememberOrderDocks(ctx context.Context, orderNo string, docks []string) { if len(docks) == 0 { return } wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).Only(ctx) if err != nil { return } c := wo.Context if c == nil { c = map[string]any{} } c["lastDockCodes"] = docks _ = s.ctx.EntClient.WorkOrder.UpdateOneID(wo.ID).SetContext(c).Exec(ctx) } // SuggestedDockCodes 查某工单「上次使用的接驳台」,供排产默认推荐 func (s *Service) SuggestedDockCodes(ctx context.Context, orderNo string) []string { wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).Only(ctx) if err != nil { return nil } if v, ok := wo.Context["lastDockCodes"].([]any); ok { out := []string{} for _, x := range v { if s2, ok := x.(string); ok { out = append(out, s2) } } return out } return nil } // syncPlansByOrderStatus 工单状态联动日排产: // - 工单 → IN_PROGRESS:该工单 PENDING 排产置 PROCESSING(开始执行) // - 工单 → DONE:已足额完工(completedQty>=planQty)的排产置 DONE;其余未完成排产置 CANCELLED // - 工单 → CANCELLED:该工单未完成(PENDING/PROCESSING)排产全部置 CANCELLED func (s *Service) syncPlansByOrderStatus(ctx context.Context, orderNo, woStatus, operator string) { if orderNo == "" { return } plans, err := s.ctx.EntClient.DailyPlan.Query(). Where(dailyplan.OrderNo(orderNo), dailyplan.StatusIn("PENDING", "PROCESSING")).All(ctx) if err != nil { return } changed := 0 to := "" for _, p := range plans { switch woStatus { case "IN_PROGRESS": to = "PROCESSING" case "DONE": if p.CompletedQty >= p.PlanQty { to = "DONE" } else { to = "CANCELLED" } case "CANCELLED": to = "CANCELLED" default: return } if p.Status == to { continue } if _, err := s.ctx.EntClient.DailyPlan.UpdateOneID(p.ID).SetStatus(to).Save(ctx); err == nil { changed++ } } if changed > 0 { s.ctx.EventLog.Write(ctx, "daily.plan.status_sync", orderNo, operator, "daily_plan", orderNo, "工单状态联动排产", map[string]any{"woStatus": woStatus, "changed": changed}) } } // ListDailyPlans 按工单/日期查询日排产 func (s *Service) ListDailyPlans(ctx context.Context, orderNo, planDate string) ([]*ent.DailyPlan, error) { q := s.ctx.EntClient.DailyPlan.Query() if orderNo != "" { q = q.Where(dailyplan.OrderNo(orderNo)) } if planDate != "" { q = q.Where(dailyplan.PlanDate(planDate)) } return q.Order(ent.Desc(dailyplan.FieldPlanDate)).All(ctx) } // DeleteDailyPlan 删除一条日排产记录 func (s *Service) DeleteDailyPlan(ctx context.Context, id int) error { return s.ctx.EntClient.DailyPlan.DeleteOneID(id).Exec(ctx) }