refactor: 重构工艺工序相关命名与数据模型

1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
This commit is contained in:
SunYF
2026-09-22 11:32:29 +08:00
parent 932700ca65
commit d609ff8a9e
185 changed files with 8623 additions and 4501 deletions
+80 -35
View File
@@ -3,6 +3,7 @@ package logic
import (
"context"
"errors"
"fmt"
"math"
"strconv"
"strings"
@@ -22,13 +23,6 @@ import (
)
func itoa(n int) string { return strconv.Itoa(n) }
func processName(code int) string {
names := map[int]string{1: "装配一", 2: "装配二", 3: "装配三", 4: "装配四", 5: "装配五", 6: "装配六", 7: "装配七", 8: "装配八", 9: "装配九", 10: "装配十", 11: "装配十一", 12: "装配十二"}
if n, ok := names[code]; ok {
return n
}
return "工位" + itoa(code)
}
// evalLogic 依据考核逻辑判定
func evalLogic(c *ent.StepCriterion, v float64) bool {
@@ -73,7 +67,7 @@ type OnlineReq struct {
}
// OnlineWorkpiece 工件进线登记
// 路线由「工位派工(station_process)」按日派生,工件不再冗余存储路线串
// 执行路线 = 工单工艺组合(唯一路线源头),工件只需按组合逐工位报工推进
func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator string) error {
if req.Sn == "" {
return errors.New("sn 不能为空")
@@ -95,11 +89,11 @@ func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator s
}
type ReportProcessReq struct {
Sn string `json:"sn"`
ProcessCode int `json:"processCode"`
StationNo int `json:"stationNo"`
Steps []StepDataReq `json:"steps"`
Binds []BindItemReq `json:"binds"` // 装机绑定(本工序装配物料:结构件批次/电气件SN),随报工原子提交
Sn string `json:"sn"`
FlowId int `json:"flowId"` // 报工工艺(缺省回退工位当前绑定的工艺)
StationNo int `json:"stationNo"`
Steps []StepDataReq `json:"steps"`
Binds []BindItemReq `json:"binds"` // 装机绑定(本工序装配物料:结构件批次/电气件SN),随报工原子提交
// 作业时长采集(方案 M / P0-3):工位终端在「上料到本工位」时采 startedAt、点「报工」时采 endedAt
// 两端时间戳同源于终端时钟(ISO8601),离线重传也不失真;缺失则回退服务端当前时刻(时长=0)。
StartedAt string `json:"startedAt"`
@@ -122,25 +116,53 @@ func parseClientTime(s string) (time.Time, error) {
}
// ReportProcess 工位报工:先应用装机绑定并做强校验(齐套才放行),再写报工实绩 + 步骤考核。
// 绑料是"落到工艺流程"的强约束:产品 BOM 中标记为本工序(processCode)的物料必须绑齐,
// 绑料是"落到工艺"的强约束:工艺物料清单(flow_material)中要求装配的物料必须绑齐,
// 电气件按单台用量逐颗SN、结构件至少一个批次号;缺料直接拒绝报工并返回缺料清单。
// 顺序校验(工单工艺组合为唯一路线源头):报工工位必须在组合中、工艺匹配,且不可倒序(可跳过未选工位)。
func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, operator string) error {
if req.Sn == "" || req.ProcessCode == 0 {
return errors.New("sn 与 processCode 必填")
if req.Sn == "" || req.StationNo <= 0 {
return errors.New("sn 与 stationNo 必填")
}
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
if err != nil {
return errors.New("工件未进线登记")
}
// 严格校验(业务口径已确认):仅“在制”工件可报工,且必须按工序顺序流转,禁止跳站/漏站/重复报同一站
// 在制 = ONLINE(已进线) / PROCESSING(在工位)DONE/REPAIR/SCRAPPED 一律拒绝。
// 严格校验(业务口径已确认):仅“在制”工件可报工DONE/REPAIR/SCRAPPED 一律拒绝
if wp.Status != "ONLINE" && wp.Status != "PROCESSING" {
return errors.New("工件当前状态为「" + wp.Status + "」,非在制,不允许报工")
}
// 顺序流转:currentProcess 语义为“已报工完成的工序”(进线时=0),本工位必须是其下一道,即 processCode == currentProcess+1。
if req.ProcessCode != wp.CurrentProcess+1 {
return errors.New("工序顺序不符:工件已完成工序" + itoa(wp.CurrentProcess) +
",应在工位" + itoa(wp.CurrentProcess+1) + "报工,当前请求工位" + itoa(req.ProcessCode))
// 工艺解析:优先取报工传入 flowId;缺省回退工位当前绑定的工艺
flowId := req.FlowId
if flowId <= 0 {
if st, e := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx); e == nil {
flowId = st.FlowId
}
}
if flowId <= 0 {
return errors.New("无法确定报工工艺:请传 flowId 或确认工位已绑定工艺")
}
// 组合校验:报工条目必须在工单工艺组合中且工艺匹配;不可倒序(工位号即顺序号)
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
return errors.New("工件所属工单不存在")
}
items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo)
matched := false
for _, it := range items {
if it.StationNo == req.StationNo {
matched = true
if it.FlowId != flowId {
return fmt.Errorf("工位%d当前工艺与工单工艺组合不匹配", req.StationNo)
}
break
}
}
if !matched {
return fmt.Errorf("工位%d不在该工单的工艺组合中,不可报工", req.StationNo)
}
if req.StationNo <= wp.CurrentStationNo {
return errors.New("不可倒序报工:工件已报工到工位" + itoa(wp.CurrentStationNo) +
",不能在工位" + itoa(req.StationNo) + "重复/回退报工")
}
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 U3):
// 任一步失败全部回滚,杜绝"实绩已建但工件状态没推进/结果没落"的半成品脏数据;
@@ -161,10 +183,10 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
if req.StationNo > 0 {
stationNo = itoa(req.StationNo)
}
if _, err := s.applyBinds(ctx, client, req.Sn, wp.OrderNo, req.ProcessCode, stationNo, operator, req.Binds); err != nil {
if _, err := s.applyBinds(ctx, client, req.Sn, wp.OrderNo, flowId, stationNo, operator, req.Binds); err != nil {
return err
}
if miss, err := s.validateStationBinds(ctx, client, req.Sn, req.ProcessCode); err != nil {
if miss, err := s.validateStationBinds(ctx, client, req.Sn, flowId); err != nil {
return err
} else if miss != "" {
return errors.New(miss)
@@ -190,9 +212,9 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
endedAt = startedAt
}
proc, err := client.WorkpieceProcess.Create().
SetSn(req.Sn).SetProcessCode(req.ProcessCode).
SetSn(req.Sn).SetFlowId(flowId).
SetStationNo(itoa(req.StationNo)).SetOrderNo(wp.OrderNo).
SetProcessName(processName(req.ProcessCode)).
SetProcessName(s.flowNameById(ctx, flowId)).
SetStatus("DONE").SetOperator(operator).SetResult("OK").
SetStartedAt(startedAt).SetEndedAt(endedAt).SetDurationSec(durationSec).Save(ctx)
if err != nil {
@@ -223,7 +245,7 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
return err
}
if _, err := client.Workpiece.UpdateOneID(wp.ID).
SetCurrentProcess(req.ProcessCode).SetStatus("PROCESSING").Save(ctx); err != nil {
SetCurrentStationNo(req.StationNo).SetStatus("PROCESSING").Save(ctx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
@@ -232,7 +254,7 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
committed = true
s.ctx.EventLog.Write(ctx, "workpiece.process.report", wp.OrderNo, operator, "workpiece", req.Sn,
"工位报工", map[string]any{"processCode": req.ProcessCode, "result": result})
"工位报工", map[string]any{"flowId": flowId, "stationNo": req.StationNo, "result": result})
s.notifyDashboard()
return nil
}
@@ -316,8 +338,12 @@ func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator strin
if err != nil {
return errors.New("工件不存在")
}
// 兜底强校验:工艺路线中每个工的装配物料都须绑齐(BOM 未配置工序时自动放行)
if miss, err := s.validateAllProcessBinds(ctx, s.ctx.EntClient, req.Sn); err != nil {
// 兜底强校验:工单工艺组合中每个工的装配物料都须绑齐(工艺未配置物料清单时自动放行)
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
return errors.New("工件所属工单不存在")
}
if miss, err := s.validateAllFlows(ctx, s.ctx.EntClient, req.Sn, wo); err != nil {
return err
} else if miss != "" {
return errors.New("完工前必须绑齐全部装配物料:" + miss)
@@ -332,7 +358,10 @@ func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator strin
_, _ = s.ctx.EntClient.AssociationTrace.UpdateOneID(existID).
SetBatchItems(req.BatchItems).SetSerialItems(req.SerialItems).SetOperator(operator).Save(ctx)
} else {
routeStr, _ := RouteStationsStr(ctx, s.ctx.EntClient, TodayStr(), "")
routeStr := ""
if items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo); len(items) > 0 {
routeStr = RouteStationsStrOfWO(items)
}
_, err = s.ctx.EntClient.AssociationTrace.Create().
SetFinishedSn(req.Sn).SetOrderNo(wp.OrderNo).SetProcessCode(routeStr).
SetBatchItems(req.BatchItems).SetSerialItems(req.SerialItems).
@@ -420,7 +449,7 @@ func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error)
return nil, errors.New("该SN无追溯数据")
}
procList, _ := s.ctx.EntClient.WorkpieceProcess.Query().
Where(workpieceprocess.Sn(sn)).Order(ent.Asc(workpieceprocess.FieldProcessCode)).All(ctx)
Where(workpieceprocess.Sn(sn)).Order(ent.Asc(workpieceprocess.FieldFlowId), ent.Asc(workpieceprocess.FieldID)).All(ctx)
steps, _ := s.ctx.EntClient.StepData.Query().
Where(stepdata.Sn(sn)).Order(ent.Desc(stepdata.FieldID)).All(ctx)
assoc, _ := s.ctx.EntClient.AssociationTrace.Query().
@@ -484,9 +513,25 @@ func (s *Service) WorkpiecesInProcess(ctx context.Context, stationNo int, orderN
return nil, err
}
out := make([]InProcessItem, 0, len(list))
woCache := map[int]*ent.WorkOrder{}
getWO := func(wp *ent.Workpiece) *ent.WorkOrder {
if wp.WorkOrderId <= 0 {
return nil
}
if wo, ok := woCache[wp.WorkOrderId]; ok {
return wo
}
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil {
wo = nil
}
woCache[wp.WorkOrderId] = wo
return wo
}
for _, wp := range list {
// 路线由今日工位派工(station_process)派生;未派工则无路线(maxStation=0
seq, _ := RouteStations(ctx, s.ctx.EntClient, TodayStr(), "")
// 路线 = 工单工艺组合(唯一路线源头);未配置组合则无路线(maxStation=0
items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, getWO(wp))
seq := RouteStationsOfWO(items)
maxStation := 0
for _, c := range seq {
if c > maxStation {
@@ -496,7 +541,7 @@ func (s *Service) WorkpiecesInProcess(ctx context.Context, stationNo int, orderN
// 推算当前物理工位:ONLINE 在 上线位(0)PROCESSING 且已到末端 → 下线位(13)
cur := 0
if wp.Status == "PROCESSING" || wp.Status == "REPAIR" {
cp := wp.CurrentProcess
cp := wp.CurrentStationNo
if cp <= 0 {
cp = maxStation
}