Files
bj_power/bj_power_mes/internal/handler/processcard.go
T
SunYF d609ff8a9e refactor: 重构工艺工序相关命名与数据模型
1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
2026-09-22 11:32:29 +08:00

340 lines
11 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/logic"
"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(ent.Asc(workpieceprocess.FieldFlowId), ent.Asc(workpieceprocess.FieldID)).All(ctx)
for _, p := range procs {
doneAt := ""
if p.EndedAt != nil {
doneAt = p.EndedAt.Format("2006-01-02 15:04")
}
flowLabel := p.ProcessName
if flowLabel == "" {
flowLabel = "工艺" + itoaCard(p.FlowId)
}
if p.Operator == "" {
missing = append(missing, fmt.Sprintf("工艺「%s」报工记录(缺操作人)", flowLabel))
}
if p.EndedAt == nil {
missing = append(missing, fmt.Sprintf("工艺「%s」报工记录(缺完成时间)", flowLabel))
}
cp := cardProcess{
ID: p.ID,
Code: p.StationNo,
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 {
if wp.WorkOrderId <= 0 {
return false
}
wo, err := client.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil {
return false
}
items, _ := logic.RouteStationsFromWO(ctx, client, wo)
if len(items) == 0 {
return false
}
stations, err := client.Station.Query().
Where(station.StationNoIn(logic.RouteStationsOfWO(items)...), 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
}
func itoaCard(n int) string { return fmt.Sprintf("%d", n) }
func renderProcessCard(t *cardTmpl) (string, error) {
const tpl = `<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>流程卡 {{.Sn}}</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:"Microsoft YaHei",sans-serif; color:#1f2937; padding:24px; }
.card { max-width:900px; margin:0 auto; border:2px solid #333; border-radius:8px; padding:20px 24px; }
h1 { font-size:22px; text-align:center; border-bottom:2px solid #333; padding-bottom:10px; margin-bottom:14px; letter-spacing:4px; }
.meta { display:flex; flex-wrap:wrap; gap:8px 32px; font-size:14px; margin-bottom:14px; }
.meta b { font-weight:700; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { border:1px solid #999; padding:6px 8px; text-align:center; }
th { background:#f3f4f6; }
.ok { color:#15803d; font-weight:700; }
.ng { color:#dc2626; font-weight:700; }
.sub td { text-align:left; color:#374151; background:#fafafa; }
.sign { display:flex; justify-content:space-between; margin-top:22px; font-size:14px; }
.sign div { width:45%; border-top:1px solid #333; padding-top:6px; text-align:center; }
h2 { font-size:15px; margin:14px 0 6px; border-left:4px solid #1f2937; padding-left:8px; }
@media print { body { padding:0; } .card { border-color:#000; box-shadow:none; } .no-print { display:none; } }
</style>
</head>
<body>
<div class="card">
<h1>产品流程卡</h1>
<div class="meta">
<span>SN<b>{{.Sn}}</b></span>
<span>工单号:<b>{{.OrderNo}}</b></span>
<span>产品型号:<b>{{.Product}}</b></span>
<span>数量:<b>{{.Quantity}}</b></span>
<span>登记时间:<b>{{.CreatedAt}}</b></span>
<span>判定:<b>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}{{.Result}}{{end}}</b></span>
</div>
{{if .Processes}}
<h2>工艺流转记录</h2>
<table>
<thead><tr><th>工位号</th><th>工艺名称</th><th>工位</th><th>操作人</th><th>完成时间</th><th>采集参数</th><th>结果</th></tr></thead>
<tbody>
{{range .Processes}}
<tr>
<td>{{.Code}}</td><td>{{.Name}}</td><td>{{.Station}}</td><td>{{.Operator}}</td><td>{{.DoneTime}}</td>
<td>{{if .Steps}}{{range .Steps}}{{.Name}}={{.Value}}({{if .OK}}<span class="ok">OK</span>{{else}}<span class="ng">NG</span>{{end}}) {{end}}{{else}}-{{end}}</td>
<td>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}OK{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p style="color:#b45309;">暂无报工记录。</p>
{{end}}
{{if .NeedTorque}}
<h2>拧紧数据{{if not .TorqueRows}}(无数据){{end}}</h2>
{{if .TorqueRows}}
<table>
<thead><tr><th>螺丝编号</th><th>扭矩(N·m)</th><th>角度(°)</th><th>结果</th><th>操作人</th><th>时间</th></tr></thead>
<tbody>
{{range .TorqueRows}}
<tr>
<td>{{.ScrewNo}}</td><td>{{.Torque}}</td><td>{{.Angle}}</td>
<td>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}<span class="ok">OK</span>{{end}}</td>
<td>{{.Operator}}</td><td>{{.Time}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
{{end}}
{{if .Materials}}
<p style="margin-top:10px;font-size:13px;">关联物料/批次:{{range .Materials}}{{.}} {{end}}</p>
{{end}}
<div class="sign">
<div>操作人签字:</div>
<div>检验员签字:</div>
</div>
<div style="text-align:center;margin-top:14px;">
<button class="no-print" onclick="window.print()" style="padding:8px 28px;font-size:15px;">打 印</button>
</div>
</div>
</body>
</html>`
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
}