package logic import ( "context" "errors" "fmt" "sort" "time" "bj_power_mes/ent" "bj_power_mes/ent/processflow" "bj_power_mes/ent/processstep" "bj_power_mes/ent/station" "bj_power_mes/ent/stepcriterion" "bj_power_mes/ent/workorder" "bj_power_mes/ent/workpieceprocess" ) // ---------- 工艺流程(块3) ---------- // FlowReq 工艺流程保存请求 // StationNo:主工序编号 1..12(流程承载的工序号,沿用既有语义); // Stations:绑定工位列表(可多选,一个流程可绑多个工位;不传时默认只绑 StationNo)。 type FlowReq struct { Id int `json:"id"` Name string `json:"name"` StationNo int `json:"stationNo"` Stations []int `json:"stations"` PdfFile string `json:"pdfFile"` Status string `json:"status"` Remark string `json:"remark"` Steps []StepItemReq `json:"steps"` } // FlowVO 工艺流程视图(含工序步骤 + 绑定该流程的工位号) type FlowVO struct { Id int `json:"id"` Name string `json:"name"` StationNo int `json:"stationNo"` PdfFile string `json:"pdfFile"` Status string `json:"status"` Remark string `json:"remark"` Stations []int `json:"stations"` Steps []*ProcessStepTemplate `json:"steps"` CreatedAt time.Time `json:"createdAt"` } // ListFlows 查询工艺流程(可按工位号过滤:命中绑定该工位或主工序号的流程) // page>0 时真分页返回 map{total,list,page,pageSize}(管理页用);page=0 全量数组(工位终端兼容) func (s *Service) ListFlows(ctx context.Context, stationNo, page, pageSize int) (any, error) { q := s.ctx.EntClient.ProcessFlow.Query(). Order(ent.Asc(processflow.FieldProcessCode), ent.Asc(processflow.FieldID)) flows, err := q.All(ctx) if err != nil { return nil, err } stations, _ := s.ctx.EntClient.Station.Query().All(ctx) flowToStations := map[int][]int{} for _, st := range stations { if st.FlowId > 0 { flowToStations[st.FlowId] = append(flowToStations[st.FlowId], st.StationNo) } } out := make([]*FlowVO, 0, len(flows)) for _, f := range flows { // 过滤:指定工位号时,只返回"绑定列表或主工序号"命中该工位的流程 if stationNo > 0 { hit := false for _, st := range flowToStations[f.ID] { if st == stationNo { hit = true break } } if !hit && f.ProcessCode != stationNo { continue } } vo := &FlowVO{ Id: f.ID, Name: f.Name, StationNo: f.ProcessCode, PdfFile: f.PdfFile, Status: f.Status, Remark: f.Remark, Stations: []int{}, Steps: s.flowSteps(ctx, f.ID, f.ProcessCode), CreatedAt: f.CreatedAt, } if list, ok := flowToStations[f.ID]; ok { vo.Stations = list } out = append(out, vo) } if page > 0 { total := len(out) start := (page - 1) * pageSize if start > total { start = total } end := start + pageSize if end > total { end = total } return map[string]any{"total": total, "list": out[start:end], "page": page, "pageSize": pageSize}, nil } return out, nil } // SaveFlow 保存工艺流程(可一次绑定多个工位)。 // 约束:① 工位号/绑定工位 1..12;② 一个工位同时只能绑定一个"启用"流程(保存时校验冲突); // ③ 停用流程不再自动解绑工位(绑定关系由本保存接口显式维护,启停只切流程状态)。 func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error { if req.Name == "" { return errors.New("流程名称必填") } // 工序编号与物理工位解耦:工序是流程步骤编号(1~999,流程内唯一即可), // 工位对应关系由「关联工位」页维护(2026-09-08) if req.StationNo < 1 || req.StationNo > 999 { return errors.New("主工序编号应为 1~999 的整数") } // 解析绑定工位列表:显式 stations 优先,否则回退到 StationNo(兼容旧调用) bindStations := req.Stations if len(bindStations) == 0 { bindStations = []int{req.StationNo} } seen := map[int]bool{} stations := []int{} for _, no := range bindStations { if no < 1 || no > 12 || seen[no] { continue } seen[no] = true stations = append(stations, no) } if len(stations) == 0 { return errors.New("请至少选择一个绑定工位") } status := req.Status if status == "" { status = "ACTIVE" } if status != "ACTIVE" && status != "INACTIVE" { return errors.New("非法状态") } // 绑定语义(2026-09-08 定稿):直接更换——一个工位只有一条关联数据, // 保存时所选工位一律改绑到本流程(覆盖旧绑定),不再报"已被启用流程绑定"冲突。 var flowID int if req.Id > 0 { upd := s.ctx.EntClient.ProcessFlow.UpdateOneID(req.Id). SetName(req.Name).SetProcessCode(req.StationNo).SetStatus(status).SetRemark(req.Remark) if req.PdfFile != "" { upd.SetPdfFile(req.PdfFile) } if _, err := upd.Save(ctx); err != nil { return err } flowID = req.Id } else { f, err := s.ctx.EntClient.ProcessFlow.Create(). SetName(req.Name).SetProcessCode(req.StationNo).SetPdfFile(req.PdfFile). SetStatus(status).SetRemark(req.Remark).Save(ctx) if err != nil { return err } flowID = f.ID } // 绑定同步:把绑定列表整体指向该流程(旧绑定该流程但不在列表内的工位解绑) boundNow, _ := s.ctx.EntClient.Station.Query().Where(station.FlowId(flowID)).All(ctx) for _, st := range boundNow { if !seen[st.StationNo] { _, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(0).Save(ctx) } } for _, no := range stations { s.syncStationFlow(ctx, no, flowID) } // 覆盖式重建该流程的工序步骤及考核标准 _, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(flowID)).Exec(ctx) for _, st := range req.Steps { if st.Name == "" { continue } rec, err := s.ctx.EntClient.ProcessStep.Create(). SetFlowId(flowID).SetProcessCode(req.StationNo).SetSeq(st.Seq).SetName(st.Name). SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetRemark(st.Remark).Save(ctx) if err != nil { return err } _, _ = s.ctx.EntClient.StepCriterion.Delete().Where(stepcriterion.StepId(rec.ID)).Exec(ctx) for _, c := range st.Criteria { if c.Name == "" || c.Logic == "" || c.Logic == "NONE" { continue } b := s.ctx.EntClient.StepCriterion.Create(). SetStepId(rec.ID).SetName(c.Name).SetUnit(c.Unit).SetLogic(c.Logic).SetTarget(c.Target) if c.Min != nil { b = b.SetMin(*c.Min) } if c.Max != nil { b = b.SetMax(*c.Max) } if err := b.Exec(ctx); err != nil { return err } } } s.ctx.EventLog.Write(ctx, "process.flow.save", "", operator, "process_flow", "", "维护工艺流程", map[string]any{"flowId": flowID, "processCode": req.StationNo, "stations": stations, "steps": len(req.Steps)}) return nil } // syncStationFlow 同步工位绑定:工位 stationNo 绑定到 flowID(无工位记录时自动创建) func (s *Service) syncStationFlow(ctx context.Context, stationNo, flowID int) { st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx) if err != nil { _, _ = s.ctx.EntClient.Station.Create(). SetStationNo(stationNo).SetName(fmt.Sprintf("工位%d", stationNo)). SetFlowId(flowID).Save(ctx) return } _, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flowID).Save(ctx) } func (s *Service) DeleteFlow(ctx context.Context, id int, operator string) error { // 删除前先解除所有工位绑定,再清步骤与流程本身 _, _ = s.ctx.EntClient.Station.Update(). Where(station.FlowId(id)). SetFlowId(0).Save(ctx) _, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(id)).Exec(ctx) s.ctx.EventLog.Write(ctx, "process.flow.delete", "", operator, "process_flow", "", "删除工艺流程", map[string]any{"flowId": id}) return s.ctx.EntClient.ProcessFlow.DeleteOneID(id).Exec(ctx) } // flowSteps 查询某流程的工序步骤(含考核标准) func (s *Service) flowSteps(ctx context.Context, flowId, processCode int) []*ProcessStepTemplate { steps, _ := s.ctx.EntClient.ProcessStep.Query(). Where(processstep.FlowId(flowId)).Order(ent.Asc(processstep.FieldSeq)).All(ctx) out := make([]*ProcessStepTemplate, 0, len(steps)) for _, st := range steps { t := &ProcessStepTemplate{ ID: st.ID, ProcessCode: st.ProcessCode, Seq: st.Seq, Name: st.Name, CollectType: st.CollectType, IsTorque: st.IsTorque, Remark: st.Remark, Criteria: []CriterionVO{}, } crits, _ := s.ctx.EntClient.StepCriterion.Query(). Where(stepcriterion.StepId(st.ID)).All(ctx) for _, c := range crits { t.Criteria = append(t.Criteria, CriterionVO{ ID: c.ID, Name: c.Name, Unit: c.Unit, Logic: c.Logic, Target: c.Target, Min: c.Min, Max: c.Max, }) } out = append(out, t) } return out } // ---------- 工位主数据(块3) ---------- type StationReq struct { Id int `json:"id"` StationNo int `json:"stationNo"` Name string `json:"name"` FlowId int `json:"flowId"` } type StationVO struct { Id int `json:"id"` StationNo int `json:"stationNo"` Name string `json:"name"` FlowId int `json:"flowId"` FlowName string `json:"flowName"` ProcessCode int `json:"processCode"` Status string `json:"status"` } func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) { stas, err := s.ctx.EntClient.Station.Query().Order(ent.Asc(station.FieldStationNo)).All(ctx) if err != nil { return nil, err } flows, _ := s.ctx.EntClient.ProcessFlow.Query().All(ctx) flowMap := map[int]*ent.ProcessFlow{} for _, f := range flows { flowMap[f.ID] = f } out := make([]*StationVO, 0, len(stas)) for _, st := range stas { vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, Status: st.Status} if f, ok := flowMap[st.FlowId]; ok { vo.FlowName = f.Name vo.ProcessCode = f.ProcessCode } out = append(out, vo) } return out, nil } func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error { if req.StationNo <= 0 || req.StationNo > 12 { return errors.New("工位号必须在 1~12") } st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx) if err != nil { if _, err := s.ctx.EntClient.Station.Create(). SetStationNo(req.StationNo).SetName(req.Name).Save(ctx); err != nil { return err } } else { // 仅改名称:flowId 不传时保留原绑定 upd := s.ctx.EntClient.Station.UpdateOneID(st.ID).SetName(req.Name) if req.FlowId > 0 { flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, req.FlowId) if fErr != nil { return errors.New("所选工艺流程不存在") } if flow.Status != "ACTIVE" { return errors.New("该工艺流程已停用,请先启用后再绑定工位") } upd.SetFlowId(req.FlowId) } if _, err := upd.Save(ctx); err != nil { return err } } s.ctx.EventLog.Write(ctx, "station.save", "", operator, "station", "", "维护工位", map[string]any{"stationNo": req.StationNo}) return nil } // SetFlowStatus 工艺流程启用/停用。 // 新语义:绑定关系由「保存流程」显式维护(一个流程可绑多工位),启停只切换流程状态, // 不再自动解绑/绑定工位;停用流程对已绑定工位不可用(工位取步骤时按 ACTIVE 过滤)。 func (s *Service) SetFlowStatus(ctx context.Context, id int, status, operator string) error { if id <= 0 { return errors.New("缺少 id") } if status != "ACTIVE" && status != "INACTIVE" { return errors.New("非法状态") } flow, err := s.ctx.EntClient.ProcessFlow.Get(ctx, id) if err != nil { return errors.New("工艺流程不存在") } if flow.Status == status { return nil } // 启用无需额外动作:绑定由保存流程时校验(一个工位不会被两个启用流程占用) if err := s.ctx.EntClient.ProcessFlow.UpdateOneID(id).SetStatus(status).Exec(ctx); err != nil { return err } action := "停用工艺流程" if status == "ACTIVE" { action = "启用工艺流程" } s.ctx.EventLog.Write(ctx, "process.flow.status", "", operator, "process_flow", "", action, map[string]any{"flowId": id, "status": status, "name": flow.Name, "processCode": flow.ProcessCode}) return nil } // ---------- 工位任务(块4/5:工位终端拉取) ---------- func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]any, error) { st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx) if err != nil { return nil, errors.New("工位不存在或未配置") } var flow *ent.ProcessFlow if st.FlowId > 0 { flow, _ = s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId) } steps := []*ProcessStepTemplate{} if flow != nil && flow.Status == "ACTIVE" { steps = s.flowSteps(ctx, flow.ID, flow.ProcessCode) } orderNos := []string{} wos, _ := s.ctx.EntClient.WorkOrder.Query(). Where(workorder.StatusIn("CREATED", "RELEASED", "IN_PROGRESS")). Order(ent.Asc(workorder.FieldID)).Limit(10).All(ctx) for _, w := range wos { orderNos = append(orderNos, w.WorkOrderNo) } return map[string]any{ "stationNo": st.StationNo, "stationName": st.Name, "flow": flow, "flowActive": flow != nil && flow.Status == "ACTIVE", "steps": steps, "orderNos": orderNos, }, nil } // ---------- 工作量/绩效报表(块6) ---------- type WorkloadRow struct { Operator string `json:"operator"` StationNo string `json:"stationNo"` Date string `json:"date"` ProcessCode int `json:"processCode"` ProcessName string `json:"processName"` DoneCount int `json:"doneCount"` OkCount int `json:"okCount"` NgCount int `json:"ngCount"` } func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to string, opPage, opSize, stPage, stSize, detPage, detSize int) (map[string]any, error) { q := s.ctx.EntClient.WorkpieceProcess.Query() if operator != "" { q = q.Where(workpieceprocess.Operator(operator)) } if stationNo != "" { q = q.Where(workpieceprocess.StationNo(stationNo)) } if from != "" { if f, err := time.Parse("2006-01-02", from); err == nil { q = q.Where(workpieceprocess.CreatedAtGTE(f)) } } if to != "" { if t, err := time.Parse("2006-01-02", to); err == nil { q = q.Where(workpieceprocess.CreatedAtLT(t.Add(24 * time.Hour))) } } rows, err := q.All(ctx) if err != nil { return nil, err } type key struct{ op, station, date string; code int } agg := map[key]*WorkloadRow{} for _, r := range rows { k := key{r.Operator, r.StationNo, r.CreatedAt.Format("2006-01-02"), r.ProcessCode} row, ok := agg[k] if !ok { row = &WorkloadRow{Operator: r.Operator, StationNo: r.StationNo, Date: k.date, ProcessCode: r.ProcessCode, ProcessName: r.ProcessName} agg[k] = row } row.DoneCount++ if r.Result == "OK" { row.OkCount++ } else { row.NgCount++ } } list := make([]*WorkloadRow, 0, len(agg)) for _, v := range agg { list = append(list, v) } sort.Slice(list, func(i, j int) bool { if list[i].Operator != list[j].Operator { return list[i].Operator < list[j].Operator } if list[i].StationNo != list[j].StationNo { return list[i].StationNo < list[j].StationNo } if list[i].Date != list[j].Date { return list[i].Date < list[j].Date } return list[i].ProcessCode < list[j].ProcessCode }) // 三个视图分别聚合(2026-09-08 绩效报表改造:按人/按工位/明细,各自真分页) byOpAgg := map[string]*WorkloadRow{} byStAgg := map[string]*WorkloadRow{} for _, v := range list { opKey := v.Operator if o, ok := byOpAgg[opKey]; ok { o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount } else { byOpAgg[opKey] = &WorkloadRow{Operator: v.Operator, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount} } stKey := v.StationNo if o, ok := byStAgg[stKey]; ok { o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount } else { byStAgg[stKey] = &WorkloadRow{StationNo: v.StationNo, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount} } } paginateRows := func(all []*ent.WorkpieceProcess, page, size int) (int, []*ent.WorkpieceProcess) { total := len(all) start := (page - 1) * size if start > total { start = total } end := start + size if end > total { end = total } return total, all[start:end] } paginate := func(all []*WorkloadRow, page, size int) (int, []*WorkloadRow) { total := len(all) start := (page - 1) * size if start > total { start = total } end := start + size if end > total { end = total } return total, all[start:end] } byOpList := make([]*WorkloadRow, 0, len(byOpAgg)) for _, v := range byOpAgg { byOpList = append(byOpList, v) } sort.Slice(byOpList, func(i, j int) bool { return byOpList[i].Operator < byOpList[j].Operator }) byStList := make([]*WorkloadRow, 0, len(byStAgg)) for _, v := range byStAgg { byStList = append(byStList, v) } sort.Slice(byStList, func(i, j int) bool { return byStList[i].StationNo < byStList[j].StationNo }) opTotal, byOpPage := paginate(byOpList, opPage, opSize) stTotal, byStPage := paginate(byStList, stPage, stSize) detTotal, detRowsPage := paginateRows(rows, detPage, detSize) return map[string]any{ "byOperator": map[string]any{"total": opTotal, "list": byOpPage}, "byStation": map[string]any{"total": stTotal, "list": byStPage}, "detail": map[string]any{"total": detTotal, "list": detRowsPage}, }, nil }