package handler import ( "bytes" "context" "fmt" "html/template" "net/http" "strings" "bj_power_mes/common/httpx" "bj_power_mes/ent" "bj_power_mes/ent/associationtrace" "bj_power_mes/ent/processstep" "bj_power_mes/ent/station" "bj_power_mes/ent/stepdata" "bj_power_mes/ent/torquerecord" "bj_power_mes/ent/workorder" "bj_power_mes/ent/workpiece" "bj_power_mes/ent/workpieceprocess" "bj_power_mes/internal/svc" ) // ProcessCardHandler GET /api/v1/process-card?sn=xxx // 按 SN 生成可打印流程卡。数据经工单联查(产品名称/数量),工序时间线补全操作人与完成时间, // 拧紧数据(如有拧紧步骤)渲染到卡尾。 // 带 ?check=1 时返回 JSON {missing:[缺失项…]} 供前端"打印前预检"弹窗,不渲染 HTML。 func ProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sn := strings.TrimSpace(r.URL.Query().Get("sn")) if sn == "" { httpx.BadRequest(w, "缺少 sn 参数") return } card, missing, err := buildCard(r.Context(), svcCtx, sn) if err != nil { httpx.Fail(w, 2401, err.Error()) return } if r.URL.Query().Get("check") == "1" { httpx.Ok(w, map[string]any{"sn": sn, "missing": missing}) return } html, err := renderProcessCard(card) if err != nil { httpx.Fail(w, 2402, "流程卡生成失败:"+err.Error()) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(html)) } } type cardProcess struct { ID int Code string Name string Station string Operator string Status string Result string DoneTime string Steps []cardStep } type cardStep struct { Name string Value string OK bool } type cardTorque struct { ScrewNo string Torque string Angle string Result string Operator string Time string } type cardTmpl struct { Sn string OrderNo string Product string Quantity int CreatedAt string Processes []cardProcess Materials []string Operator string Inspector string DoneTime string Result string NeedTorque bool TorqueRows []cardTorque } // buildCard 汇总流程卡所需全部数据(ent 实体直查),并计算缺失项。 // missing 非空时表示存在数据不完整,前端应提示"是否继续打印"。 func buildCard(ctx context.Context, svcCtx *svc.ServiceContext, sn string) (*cardTmpl, []string, error) { client := svcCtx.EntClient wp, err := client.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx) if err != nil { return nil, nil, fmt.Errorf("该SN无追溯数据") } t := &cardTmpl{Sn: sn, Result: "待完工", NeedTorque: false} missing := []string{} if wp.OrderNo == "" { missing = append(missing, "工单号") } else { t.OrderNo = wp.OrderNo } t.CreatedAt = wp.CreatedAt.Format("2006-01-02 15:04") // 产品信息:工件 → 工单 联查(修复 wp.productType 恒空 bug) var wo *ent.WorkOrder if t.OrderNo != "" { wo, _ = client.WorkOrder.Query().Where(workorder.WorkOrderNo(t.OrderNo)).First(ctx) } if wo != nil { t.Product = wo.ProductName if wo.ProductName == "" { t.Product = wo.ProductCode } t.Quantity = wo.Quantity } if t.Product == "" { missing = append(missing, "产品名称") } if wo == nil || t.Quantity <= 0 { missing = append(missing, "产品数量") } // 工序时间线(操作人 + 完成时间) procs, _ := client.WorkpieceProcess.Query(). Where(workpieceprocess.Sn(sn)).Order(workpieceprocess.ByProcessCode()).All(ctx) for _, p := range procs { doneAt := "" if p.EndedAt != nil { doneAt = p.EndedAt.Format("2006-01-02 15:04") } if p.Operator == "" { missing = append(missing, fmt.Sprintf("工序%s报工记录(缺操作人)", itoaCard(p.ProcessCode))) } if p.EndedAt == nil { missing = append(missing, fmt.Sprintf("工序%s报工记录(缺完成时间)", itoaCard(p.ProcessCode))) } cp := cardProcess{ ID: p.ID, Code: fmt.Sprintf("%d", p.ProcessCode), Name: p.ProcessName, Station: p.StationNo, Operator: p.Operator, Status: p.Status, Result: p.Result, DoneTime: doneAt, } if p.Result == "NG" { t.Result = "NG" } t.Processes = append(t.Processes, cp) } if len(procs) == 0 { missing = append(missing, "工序报工记录") } // 步骤考核数据按工序实绩 ID 归组,回填到对应工序行的"采集参数" steps, _ := client.StepData.Query().Where(stepdata.Sn(sn)).All(ctx) stepByProc := map[int][]cardStep{} for _, s := range steps { stepByProc[s.ProcessId] = append(stepByProc[s.ProcessId], cardStep{Name: s.StepName, Value: s.ValueText, OK: s.IsOK}) } for i := range t.Processes { t.Processes[i].Steps = stepByProc[t.Processes[i].ID] } // 拧紧数据:工件所属流程含 isTorque 步骤时判定"需要拧紧数据" needTorque := hasTorqueStep(ctx, client, sn, wp) t.NeedTorque = needTorque torque, _ := client.TorqueRecord.Query(). Where(torquerecord.Sn(sn)).Order(ent.Desc(torquerecord.FieldTime)).Limit(200).All(ctx) for _, q := range torque { t.TorqueRows = append(t.TorqueRows, cardTorque{ ScrewNo: q.ScrewNo, Torque: fmt.Sprintf("%.2f", q.Torque), Angle: fmt.Sprintf("%.2f", q.Angle), Result: q.Result, Operator: q.Operator, Time: q.Time.Format("2006-01-02 15:04:05"), }) } if needTorque && len(t.TorqueRows) == 0 { missing = append(missing, "拧紧数据") } // 关联物料(批次/电气件SN) assoc, _ := client.AssociationTrace.Query(). Where(associationtrace.FinishedSn(sn)).First(ctx) if assoc != nil { t.Operator = assoc.Operator for _, b := range assoc.BatchItems { t.Materials = append(t.Materials, b) } for _, s := range assoc.SerialItems { t.Materials = append(t.Materials, s) } } return t, missing, nil } // hasTorqueStep 判断该工件加工路径上是否存在"拧紧采集"步骤(经 工位→流程→步骤 链) func hasTorqueStep(ctx context.Context, client *ent.Client, sn string, wp *ent.Workpiece) bool { procCodes := parseCardSeq(wp.ProcessSeq) if len(procCodes) == 0 { return false } stations, err := client.Station.Query(). Where(station.StationNoIn(procCodes...), station.FlowIdGT(0)).All(ctx) if err != nil || len(stations) == 0 { return false } flowIds := make([]int, 0, len(stations)) for _, st := range stations { flowIds = append(flowIds, st.FlowId) } n, err := client.ProcessStep.Query(). Where(processstep.FlowIdIn(flowIds...), processstep.IsTorque(true)).Count(ctx) if err != nil { return false } return n > 0 } // parseCardSeq 解析工单/工件工序组合 "1,3,5" → 工位号列表 func parseCardSeq(s string) []int { out := []int{} for _, p := range strings.Split(s, ",") { p = strings.TrimSpace(p) if p == "" { continue } var n int if _, err := fmt.Sscanf(p, "%d", &n); err == nil && n >= 1 && n <= 12 { out = append(out, n) } } return out } func itoaCard(n int) string { return fmt.Sprintf("%d", n) } func renderProcessCard(t *cardTmpl) (string, error) { const tpl = ` 流程卡 {{.Sn}}

产品流程卡

SN:{{.Sn}} 工单号:{{.OrderNo}} 产品型号:{{.Product}} 数量:{{.Quantity}} 登记时间:{{.CreatedAt}} 判定:{{if eq .Result "NG"}}NG{{else}}{{.Result}}{{end}}
{{if .Processes}}

工序流转记录

{{range .Processes}} {{end}}
工序工序名称工位操作人完成时间采集参数结果
工序{{.Code}}{{.Name}}{{.Station}}{{.Operator}}{{.DoneTime}} {{if .Steps}}{{range .Steps}}{{.Name}}={{.Value}}({{if .OK}}OK{{else}}NG{{end}}) {{end}}{{else}}-{{end}} {{if eq .Result "NG"}}NG{{else}}OK{{end}}
{{else}}

暂无工序报工记录。

{{end}} {{if .NeedTorque}}

拧紧数据{{if not .TorqueRows}}(无数据){{end}}

{{if .TorqueRows}} {{range .TorqueRows}} {{end}}
螺丝编号扭矩(N·m)角度(°)结果操作人时间
{{.ScrewNo}}{{.Torque}}{{.Angle}} {{if eq .Result "NG"}}NG{{else}}OK{{end}} {{.Operator}}{{.Time}}
{{end}} {{end}} {{if .Materials}}

关联物料/批次:{{range .Materials}}{{.}} {{end}}

{{end}}
操作人签字:
检验员签字:
` tmpl, err := template.New("card").Parse(tpl) if err != nil { return "", err } var buf bytes.Buffer if err := tmpl.Execute(&buf, t); err != nil { return "", err } return buf.String(), nil }