Files
bj_power/bj_power_mes/internal/handler/processcard.go
T
SunYF 0ab21b1234 feat&refactor: 完成多模块功能迭代与配置优化
本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
2026-09-10 16:59:15 +08:00

343 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/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 = `<!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
}