Files
bj_power/bj_power_mes/internal/logic/workpiece_bind.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

426 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
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 logic
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strings"
"bj_power_mes/ent"
"bj_power_mes/ent/flowmaterial"
"bj_power_mes/ent/workpiece"
"bj_power_mes/ent/workpiecebind"
"bj_power_mes/ent/workpieceprocess"
)
// RemoveBind 撤销一条绑定(仅允许未报工前的误扫纠正;报工后不可撤销)
func (s *Service) RemoveBind(ctx context.Context, sn string, flowId int, materialCode, bindValue, operator string) error {
if sn == "" || materialCode == "" || bindValue == "" {
return errors.New("sn/materialCode/bindValue 必填")
}
// 该工艺已报工(存在实绩)则不允许撤销
done, err := s.ctx.EntClient.WorkpieceProcess.Query().
Where(workpieceprocess.Sn(sn), workpieceprocess.FlowId(flowId)).Exist(ctx)
if err != nil {
return err
}
if done {
return errors.New("该工艺已报工,不能撤销装配绑定")
}
n, err := s.ctx.EntClient.WorkpieceBind.Delete().
Where(workpiecebind.Sn(sn), workpiecebind.FlowId(flowId),
workpiecebind.MaterialCode(materialCode), workpiecebind.BindValue(bindValue)).Exec(ctx)
if err != nil {
return err
}
if n == 0 {
return errors.New("未找到该绑定记录")
}
s.ctx.EventLog.Write(ctx, "workpiece.bind.remove", "", operator, "workpiece_bind", sn,
"撤销装配绑定", map[string]any{"flowId": flowId, "materialCode": materialCode, "bindValue": bindValue})
return nil
}
// BindWorkpiece 独立绑定接口:工人在工位扫一件绑一件(报工前的逐条扫码/防错),
// 与报工携带 binds 等价,落库后立即返回结果供前端即时反馈(SN 重复/料不匹配当场报错)。
func (s *Service) BindWorkpiece(ctx context.Context, sn, orderNo string, flowId int, stationNo string, operator string, items []BindItemReq) (map[string]any, error) {
bound, err := s.applyBinds(ctx, s.ctx.EntClient, sn, orderNo, flowId, stationNo, operator, items)
if err != nil {
return nil, err
}
if bound > 0 {
s.ctx.EventLog.Write(ctx, "workpiece.bind", orderNo, operator, "workpiece_bind", sn,
"装机绑定物料", map[string]any{"flowId": flowId, "count": bound})
s.notifyDashboard()
}
// 返回最新面板,前端据此刷新齐套状态
panel, err := s.StationBindPanelData(ctx, sn, flowId)
if err != nil {
return map[string]any{"bound": bound}, nil
}
return map[string]any{"bound": bound, "panel": panel}, nil
}
// ---------- 装机绑定(工位装配物料追溯 + 工艺物料清单强校验) ----------
// BindItemReq 单条装机绑定请求(报工时随 steps 一并提交)
type BindItemReq struct {
MaterialCode string `json:"materialCode"`
BindValue string `json:"bindValue"` // 结构件=批次号;电气件=SN
}
// bindTarget 某工艺要求装配的物料定义(源自工艺物料清单 flow_material
type bindTarget struct {
MaterialCode string
MaterialName string
Spec string
Unit string
ManageMode string // 1 批次(结构件) / 2 SN(电气件)
UnitQty float64
}
// workpieceProduct 解析工件所属产品编码与工单(sn → workpiece → workOrder
func (s *Service) workpieceProduct(ctx context.Context, client *ent.Client, sn string) (*ent.Workpiece, *ent.WorkOrder, error) {
wp, err := client.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx)
if err != nil {
return nil, nil, errors.New("工件未进线登记")
}
if wp.WorkOrderId <= 0 {
return wp, nil, errors.New("工件未关联工单")
}
wo, err := client.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
return wp, nil, errors.New("工件所属工单不存在")
}
return wp, wo, nil
}
// stationBindTargets 取某工艺(flowId)要求装配的物料清单(工艺物料清单 flow_material)。
func (s *Service) stationBindTargets(ctx context.Context, client *ent.Client, flowId int) ([]bindTarget, error) {
items, err := client.FlowMaterial.Query().
Where(flowmaterial.FlowId(flowId)).All(ctx)
if err != nil {
return nil, err
}
out := make([]bindTarget, 0, len(items))
for _, it := range items {
out = append(out, bindTarget{
MaterialCode: it.MaterialCode, MaterialName: it.MaterialName, Spec: it.Spec,
Unit: it.Unit, ManageMode: it.ManageMode, UnitQty: it.UnitQty,
})
}
return out, nil
}
// applyBinds 报工时应用装机绑定(落库 + 防错校验):
// ① 料必须配置在该工位的工艺物料清单(flow_material)中;
// ② 电气件 SN 全局防重:已被其他工件绑定则拒绝(防错拿/防重复装机);
// ③ 同一工件同一料同一绑定值幂等(重复扫码不报错,静默去重)。
// 返回绑定成功条数。
func (s *Service) applyBinds(ctx context.Context, client *ent.Client, sn, orderNo string, flowId int, stationNo string, operator string, items []BindItemReq) (int, error) {
if len(items) == 0 {
return 0, nil
}
wp, _, err := s.workpieceProduct(ctx, client, sn)
if err != nil {
return 0, err
}
if wp.Status == "DONE" || wp.Status == "SCRAPPED" {
return 0, errors.New("已完工/报废工件不能再绑定物料")
}
// 本工艺物料清单全集:materialCode → 定义(校验料合法性 + 取物料快照)
fmMap := map[string]*ent.FlowMaterial{}
targets, err := s.stationBindTargets(ctx, client, flowId)
if err != nil {
return 0, err
}
for i := range targets {
fmMap[targets[i].MaterialCode] = &ent.FlowMaterial{
MaterialCode: targets[i].MaterialCode, MaterialName: targets[i].MaterialName,
Spec: targets[i].Spec, Unit: targets[i].Unit,
ManageMode: targets[i].ManageMode, UnitQty: targets[i].UnitQty,
}
}
bound := 0
for _, it := range items {
mc := strings.TrimSpace(it.MaterialCode)
bv := strings.TrimSpace(it.BindValue)
if mc == "" || bv == "" {
return bound, errors.New("物料编码与绑定值不能为空")
}
fm, ok := fmMap[mc]
if !ok {
return bound, fmt.Errorf("物料 %s 未配置在工艺「%s」的物料清单,不能绑定", mc, s.flowNameById(ctx, flowId))
}
manageMode := fm.ManageMode
if manageMode == "" {
manageMode = "1"
}
// 同工件同料同值幂等
exists, _ := client.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.MaterialCode(mc), workpiecebind.BindValue(bv)).Exist(ctx)
if exists {
continue
}
// 电气件 SN 全局防重
if manageMode == "2" {
other, _ := client.WorkpieceBind.Query().
Where(workpiecebind.BindValue(bv), workpiecebind.BindType("SN"), workpiecebind.SnNEQ(sn)).First(ctx)
if other != nil {
return bound, fmt.Errorf("电气件SN %s 已绑定到工件 %s(工艺「%s」),不能重复装机",
bv, other.Sn, s.flowNameById(ctx, other.FlowId))
}
}
bindType := "BATCH"
if manageMode == "2" {
bindType = "SN"
}
if err := client.WorkpieceBind.Create().
SetSn(sn).SetOrderNo(orderNo).SetFlowId(flowId).
SetStationNo(stationNo).SetMaterialCode(mc).SetMaterialName(fm.MaterialName).
SetSpec(fm.Spec).SetUnit(fm.Unit).SetManageMode(manageMode).
SetBindValue(bv).SetBindType(bindType).SetOperator(operator).
Exec(ctx); err != nil {
return bound, err
}
bound++
}
return bound, nil
}
// validateStationBinds 校验某工件在某工艺的装配物料是否绑齐(齐套强校验核心):
// 返回缺料明细(空串=齐套)。判定:电气件(SN) 绑定的不同SN数 ≥ 单台用量;结构件(批次) 至少绑定1个批次号。
// 该工艺未配置物料清单时视为不启用绑定校验,放行。
func (s *Service) validateStationBinds(ctx context.Context, client *ent.Client, sn string, flowId int) (string, error) {
if _, _, err := s.workpieceProduct(ctx, client, sn); err != nil {
return "", err
}
targets, err := s.stationBindTargets(ctx, client, flowId)
if err != nil {
return "", err
}
if len(targets) == 0 {
return "", nil // 该工艺未配置装配物料 → 不校验
}
// 已绑定明细:按 物料编码 → 绑定值集合
binds, _ := client.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.FlowId(flowId)).All(ctx)
boundMap := map[string]map[string]bool{}
for _, b := range binds {
if boundMap[b.MaterialCode] == nil {
boundMap[b.MaterialCode] = map[string]bool{}
}
boundMap[b.MaterialCode][b.BindValue] = true
}
missing := []string{}
for _, t := range targets {
set := boundMap[t.MaterialCode]
boundCount := len(set)
need := int(math.Ceil(t.UnitQty))
if need < 1 {
need = 1
}
if t.ManageMode == "2" { // 电气件 SN:需绑满单台用量
if boundCount < need {
missing = append(missing, fmt.Sprintf("%s(%s):需绑定%d颗SN,已绑%d颗", t.MaterialCode, t.MaterialName, need, boundCount))
}
} else { // 结构件批次:至少绑定1个批次号
if boundCount < 1 {
missing = append(missing, fmt.Sprintf("%s(%s):未绑定批次号", t.MaterialCode, t.MaterialName))
}
}
}
if len(missing) == 0 {
return "", nil
}
return "装配物料未绑齐:" + strings.Join(missing, ""), nil
}
// validateAllFlows 完工兜底:按工单工艺组合(唯一路线源头)逐工艺校验装配物料齐套。
// 同一工艺绑多工位时只校验一次(按 FlowId 去重)。
func (s *Service) validateAllFlows(ctx context.Context, client *ent.Client, sn string, wo *ent.WorkOrder) (string, error) {
if _, _, err := s.workpieceProduct(ctx, client, sn); err != nil {
return "", err
}
items, _ := RouteStationsFromWO(ctx, client, wo)
missing := []string{}
seen := map[int]bool{}
for _, it := range items {
if seen[it.FlowId] {
continue
}
seen[it.FlowId] = true
msg, err := s.validateStationBinds(ctx, client, sn, it.FlowId)
if err != nil {
return "", err
}
if msg != "" {
missing = append(missing, fmt.Sprintf("工艺「%s」:%s", s.flowNameById(ctx, it.FlowId), msg))
}
}
if len(missing) == 0 {
return "", nil
}
return strings.Join(missing, ""), nil
}
// ListWorkpieceBinds 查询工件的装机绑定明细
func (s *Service) ListWorkpieceBinds(ctx context.Context, sn string) ([]*ent.WorkpieceBind, error) {
q := s.ctx.EntClient.WorkpieceBind.Query()
if sn != "" {
q = q.Where(workpiecebind.Sn(sn))
}
return q.Order(ent.Asc(workpiecebind.FieldFlowId), ent.Asc(workpiecebind.FieldID)).Limit(2000).All(ctx)
}
// BindTraceVO 追溯页:装机绑定明细 + 每工艺齐套状态 + 物料组成树
type BindTraceVO struct {
Binds []*ent.WorkpieceBind `json:"binds"`
Complete bool `json:"complete"`
Missing string `json:"missing"`
BomTree []BindNode `json:"bomTree"` // 递归展开成品零部件到叶子的物料组成树
}
// BindNode 追溯用的物料组成节点(递归展开成品零部件,不依赖 WMS)
type BindNode struct {
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"` // 1 批次(结构件) / 2 SN(电气件)
Qty float64 `json:"qty"` // 折算到单台成品的用量
Level int `json:"level"` // 0=顶层装配物料,1+=下级
Children []BindNode `json:"children,omitempty"`
}
// BuildBindTrace 组装某工件装机绑定追溯(含全工艺齐套状态 + 物料组成树,供成品档案/追溯页展示)
func (s *Service) BuildBindTrace(ctx context.Context, sn string) (*BindTraceVO, error) {
binds, err := s.ListWorkpieceBinds(ctx, sn)
if err != nil {
return nil, err
}
_, wo, err := s.workpieceProduct(ctx, s.ctx.EntClient, sn)
if err != nil {
return nil, err
}
missing, err := s.validateAllFlows(ctx, s.ctx.EntClient, sn, wo)
if err != nil {
return nil, err
}
// 物料组成树:递归展开成品零部件到叶子(不依赖 WMS),让追溯页能展示完整物料组成,
// 避免成品作零部件被引用时只显示上层成品而漏下层料、追溯断链。
var tree []BindNode
tree = s.expandBindBom(ctx, wo.ProductCode, "", 1, 0, map[string]bool{})
return &BindTraceVO{Binds: binds, Complete: missing == "", Missing: missing, BomTree: tree}, nil
}
// expandBindBom 递归展开某产品的物料组成树(供追溯页/成品档案展示完整物料构成)。
//
// R12026-09-19):递归骨架统一到 bom_expand.go 的 expandBomTree,与备料算料共用同一份实现;
// 下钻判定由 localBomExistsResolver 注入(本地存在下级清单才继续展开)——不依赖 WMS,
// 保证 WMS 不可达时追溯仍能展示完整物料组成、不断链。
func (s *Service) expandBindBom(ctx context.Context, productCode, bomName string, qty float64, depth int, path map[string]bool) []BindNode {
return bomTreeToBindNodes(s.expandBomTree(ctx, productCode, bomNameOrDefault(bomName), qty, depth, path, s.localBomExistsResolver))
}
// BindRequiredItem 应绑清单条目(工位终端展示引导 + 前端齐套计算)
type BindRequiredItem struct {
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"`
UnitQty float64 `json:"unitQty"`
Need int `json:"need"` // 需绑数量
Bound []string `json:"bound"` // 已绑定的批次号/SN 列表
BoundCount int `json:"boundCount"`
Complete bool `json:"complete"`
}
// StationBindPanel 某工件在某工艺的装配绑定面板数据(应绑清单 + 已绑情况)
type StationBindPanel struct {
Enabled bool `json:"enabled"` // 本工艺是否启用装配物料绑定
Required []BindRequiredItem `json:"required"`
Complete bool `json:"complete"`
Missing string `json:"missing"`
StationNo int `json:"stationNo"` // 工位号(从工单工艺组合该 flow 条目派生)
FlowId int `json:"flowId"`
LastBinds []*ent.WorkpieceBind `json:"lastBinds"` // 最近10条绑定记录
StationNames map[int]string `json:"-"`
}
// StationBindPanelData 供工位终端在报工前拉取:本工件本工艺该装什么、已装什么
func (s *Service) StationBindPanelData(ctx context.Context, sn string, flowId int) (*StationBindPanel, error) {
_, wo, err := s.workpieceProduct(ctx, s.ctx.EntClient, sn)
if err != nil {
return nil, err
}
targets, err := s.stationBindTargets(ctx, s.ctx.EntClient, flowId)
if err != nil {
return nil, err
}
// 工位号从工单工艺组合该 flow 的条目派生(组合是唯一路线源头)
stationNo := 0
if items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo); len(items) > 0 {
for _, it := range items {
if it.FlowId == flowId {
stationNo = it.StationNo
break
}
}
}
panel := &StationBindPanel{Enabled: len(targets) > 0, FlowId: flowId, StationNo: stationNo}
// 全量绑定(齐套判定必须按全量统计,不能用 limit 截断,否则多料/多SN会误判缺料)
allBinds, _ := s.ctx.EntClient.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.FlowId(flowId)).All(ctx)
boundMap := map[string]map[string]bool{}
for _, b := range allBinds {
if boundMap[b.MaterialCode] == nil {
boundMap[b.MaterialCode] = map[string]bool{}
}
boundMap[b.MaterialCode][b.BindValue] = true
}
for _, t := range targets {
need := int(math.Ceil(t.UnitQty))
if need < 1 {
need = 1
}
bound := make([]string, 0, len(boundMap[t.MaterialCode]))
for v := range boundMap[t.MaterialCode] {
bound = append(bound, v)
}
sort.Strings(bound)
item := BindRequiredItem{
MaterialCode: t.MaterialCode, MaterialName: t.MaterialName, Spec: t.Spec, Unit: t.Unit,
ManageMode: t.ManageMode, UnitQty: t.UnitQty, Need: need, Bound: bound,
BoundCount: len(bound),
}
if t.ManageMode == "2" {
item.Complete = len(bound) >= need
} else {
item.Complete = len(bound) >= 1
}
panel.Required = append(panel.Required, item)
}
allComplete := true
for _, r := range panel.Required {
if !r.Complete {
allComplete = false
break
}
}
panel.Complete = allComplete
if !allComplete {
panel.Missing, _ = s.validateStationBinds(ctx, s.ctx.EntClient, sn, flowId)
}
// 最近绑定(仅展示用)
sort.Slice(allBinds, func(i, j int) bool { return allBinds[i].ID > allBinds[j].ID })
if len(allBinds) > 10 {
allBinds = allBinds[:10]
}
panel.LastBinds = allBinds
return panel, nil
}