refactor: 重构产线工位与备料流程,移除冗余字段

1. 移除工单、工件、日排产中的冗余工位组合/接驳台字段
2. 新增工位接驳台编码字段与唯一约束,关联WMS接驳台主数据
3. 重构日排产为工位产量分配模式,替代原接驳台列表
4. 新增备料单工位级字段与分批出库支持
5. 移除定时同步可生产数量,改为WMS实时拉取接口
6. 过滤操作日志,排除登录/退出相关日志
7. 调整仪表盘工序展示逻辑为今日派工路线
8. 新增接驳台维护菜单与基础数据
This commit is contained in:
SunYF
2026-09-19 07:50:21 +08:00
parent 543655d441
commit 37f10d3a13
156 changed files with 4489 additions and 1285 deletions
+51 -15
View File
@@ -3,6 +3,7 @@ package logic
import (
"context"
"errors"
"math"
"strconv"
"strings"
"time"
@@ -17,6 +18,7 @@ import (
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpiece"
"bj_power_mes/ent/workpieceprocess"
"bj_power_mes/internal/wmsclient"
)
func itoa(n int) string { return strconv.Itoa(n) }
@@ -68,30 +70,22 @@ type OnlineReq struct {
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
WorkOrderId int `json:"workOrderId"`
ProcessSeq string `json:"processSeq"`
}
// OnlineWorkpiece 工件进线登记
// 路线由「工位派工(station_process)」按日派生,工件不再冗余存储路线串
func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator string) error {
if req.Sn == "" {
return errors.New("sn 不能为空")
}
seq := req.ProcessSeq
if seq == "" {
seq = FullProcessSeq
}
if req.WorkOrderId == 0 {
if wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx); err == nil {
req.WorkOrderId = wo.ID
if req.ProcessSeq == "" && wo.ProcessSeq != "" {
seq = wo.ProcessSeq
}
}
}
seq = NormalizeProcessSeq(seq)
err := s.ctx.EntClient.Workpiece.Create().
SetSn(req.Sn).SetOrderNo(req.OrderNo).SetWorkOrderId(req.WorkOrderId).
SetProcessSeq(seq).SetStatus("ONLINE").SetOnlineAt(time.Now()).Exec(ctx)
SetStatus("ONLINE").SetOnlineAt(time.Now()).Exec(ctx)
if err != nil {
return err
}
@@ -328,8 +322,9 @@ 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(), "")
_, err = s.ctx.EntClient.AssociationTrace.Create().
SetFinishedSn(req.Sn).SetOrderNo(wp.OrderNo).SetProcessCode(wp.ProcessSeq).
SetFinishedSn(req.Sn).SetOrderNo(wp.OrderNo).SetProcessCode(routeStr).
SetBatchItems(req.BatchItems).SetSerialItems(req.SerialItems).
SetOperator(operator).Save(ctx)
if err != nil {
@@ -343,10 +338,53 @@ func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator strin
// 成品 SN = 工件 SN(进线登记即系统发号,全程唯一,保证对号可追溯)。
// WMS 不可达/校验失败时降级:记 eventlog 不阻塞完工,与备料 WMS 校验降级策略同模式。
s.retainFinishedToWms(ctx, wp, operator)
// 反冲扣账:物料在当道工序被消耗,完工即按整份 BOM 反冲扣 WMS 库存。
// 失败仅记 eventlog 不阻塞完工(与成品回流同降级策略)。
s.backflushWorkpiece(ctx, wp, operator)
s.notifyDashboard()
return nil
}
// backflushWorkpiece 反冲扣账:按工单产品整份 BOM 算「单件消耗(含损耗)」,
// 调 WMS 内部反冲接口 FIFO 扣库存 + 生成 backflush 出库单。
// 整份 BOM 一次性下发(完工即全部物料已消耗),WMS 不可达/校验失败仅记日志。
func (s *Service) backflushWorkpiece(ctx context.Context, wp *ent.Workpiece, operator string) {
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
s.ctx.EventLog.Write(ctx, "wms.backflush.failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲失败:工单不存在", map[string]any{"workOrderId": wp.WorkOrderId})
return
}
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(""))
if err != nil || len(bom) == 0 {
s.ctx.EventLog.Write(ctx, "wms.backflush.skip", wp.OrderNo, operator, "workpiece", wp.Sn,
"无BOM物料,跳过反冲", map[string]any{"productCode": wo.ProductCode})
return
}
items := make([]wmsclient.BackflushItem, 0, len(bom))
for _, it := range bom {
if it.MaterialCode == "" {
continue
}
// 单件消耗 = 单台用量 ×(1+损耗率),四舍五入;≤0 跳过
qty := int(math.Round(it.UnitQty * (1 + it.LossRate/100)))
if qty <= 0 {
continue
}
items = append(items, wmsclient.BackflushItem{MaterialCode: it.MaterialCode, Qty: qty})
}
if len(items) == 0 {
return
}
if wmsErr := s.ctx.Wms.Backflush(ctx, wp.OrderNo, 0, 0, operator, items); wmsErr != nil {
s.ctx.EventLog.Write(ctx, "wms.backflush.failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲扣账WMS失败(库存未扣,请人工核查)", map[string]any{"error": wmsErr.Error(), "productCode": wo.ProductCode})
return
}
s.ctx.EventLog.Write(ctx, "wms.backflush", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲扣账已下发WMS", map[string]any{"productCode": wo.ProductCode, "items": len(items)})
}
// retainFinishedToWms 完工品回流 WMS 库存。失败仅记日志(wms.finished.retain_failed),
// 不回滚完工状态——仓管可在 WMS 侧人工补录,避免 WMS 抖动卡死产线。
func (s *Service) retainFinishedToWms(ctx context.Context, wp *ent.Workpiece, operator string) {
@@ -433,10 +471,8 @@ func (s *Service) WorkpiecesInProcess(ctx context.Context, stationNo int) ([]InP
}
out := make([]InProcessItem, 0, len(list))
for _, wp := range list {
seq := ParseProcessSeq(wp.ProcessSeq)
if len(seq) == 0 {
seq = ParseProcessSeq(FullProcessSeq)
}
// 路线由今日工位派工(station_process)派生;未派工则无路线(maxStation=0
seq, _ := RouteStations(ctx, s.ctx.EntClient, TodayStr(), "")
maxStation := 0
for _, c := range seq {
if c > maxStation {