Files
bj_power/bj_power_mes/internal/logic/workpiece_bind.go
T
SunYF 37f10d3a13 refactor: 重构产线工位与备料流程,移除冗余字段
1. 移除工单、工件、日排产中的冗余工位组合/接驳台字段
2. 新增工位接驳台编码字段与唯一约束,关联WMS接驳台主数据
3. 重构日排产为工位产量分配模式,替代原接驳台列表
4. 新增备料单工位级字段与分批出库支持
5. 移除定时同步可生产数量,改为WMS实时拉取接口
6. 过滤操作日志,排除登录/退出相关日志
7. 调整仪表盘工序展示逻辑为今日派工路线
8. 新增接驳台维护菜单与基础数据
2026-09-19 07:50:21 +08:00

436 lines
17 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/bomitem"
"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, processCode 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.ProcessCode(processCode)).Exist(ctx)
if err != nil {
return err
}
if done {
return errors.New("该工序已报工,不能撤销装配绑定")
}
n, err := s.ctx.EntClient.WorkpieceBind.Delete().
Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode),
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{"processCode": processCode, "materialCode": materialCode, "bindValue": bindValue})
return nil
}
// BindWorkpiece 独立绑定接口:工人在工位扫一件绑一件(报工前的逐条扫码/防错),
// 与报工携带 binds 等价,落库后立即返回结果供前端即时反馈(SN 重复/料不匹配当场报错)。
func (s *Service) BindWorkpiece(ctx context.Context, sn, orderNo string, processCode int, stationNo string, operator string, items []BindItemReq) (map[string]any, error) {
bound, err := s.applyBinds(ctx, s.ctx.EntClient, sn, orderNo, processCode, 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{"processCode": processCode, "count": bound})
s.notifyDashboard()
}
// 返回最新面板,前端据此刷新齐套状态
panel, err := s.StationBindPanelData(ctx, sn, processCode)
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 由 BOM 解析出的某工序应装物料定义
type bindTarget struct {
MaterialCode string
MaterialName string
Spec string
Unit string
ManageMode string // 1 批次(结构件) / 2 SN(电气件)
UnitQty float64
}
// workpieceProduct 解析工件所属产品编码与工单锁定的 BOM 名称(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 取某工单(产品+BOM)在某工序(processCode>0)要求装配的物料清单。
// 一型号多份 BOM 并存,必须按工单锁定的 bomName 过滤,否则不同厂家料单互相串。
func (s *Service) stationBindTargets(ctx context.Context, client *ent.Client, productCode, bomName string, processCode int) ([]bindTarget, error) {
items, err := client.BomItem.Query().
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomNameOrDefault(bomName)), bomitem.ProcessCode(processCode)).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 报工时应用装机绑定(落库 + 防错校验):
// ① 料必须属于该产品 BOM 且装配工序配置为本工序(processCode==当前工序);
// ② 电气件 SN 全局防重:已被其他工件绑定则拒绝(防错拿/防重复装机);
// ③ 同一工件同一料同一绑定值幂等(重复扫码不报错,静默去重)。
// 返回绑定成功条数。
func (s *Service) applyBinds(ctx context.Context, client *ent.Client, sn, orderNo string, processCode int, stationNo string, operator string, items []BindItemReq) (int, error) {
if len(items) == 0 {
return 0, nil
}
wp, wo, err := s.workpieceProduct(ctx, client, sn)
if err != nil {
return 0, err
}
productCode := wo.ProductCode
if wp.Status == "DONE" || wp.Status == "SCRAPPED" {
return 0, errors.New("已完工/报废工件不能再绑定物料")
}
// 工单锁定 BOM 的全量索引:materialCode → target(校验料合法性 + 取物料快照)
bomMap := map[string]*ent.BomItem{}
allBom, _ := client.BomItem.Query().
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomNameOrDefault(""))).All(ctx)
for _, b := range allBom {
bomMap[b.MaterialCode] = b
}
bound := 0
for _, it := range items {
mc := strings.TrimSpace(it.MaterialCode)
bv := strings.TrimSpace(it.BindValue)
if mc == "" || bv == "" {
return bound, errors.New("物料编码与绑定值不能为空")
}
bi, ok := bomMap[mc]
if !ok {
return bound, fmt.Errorf("物料 %s 不在该产品的物料清单(BOM)中,不能绑定", mc)
}
if bi.ProcessCode != processCode {
return bound, fmt.Errorf("物料 %s 配置在工序%d装配,本工位(工序%d)不可绑定", mc, bi.ProcessCode, processCode)
}
// 同工件同料同值幂等
exists, _ := client.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.MaterialCode(mc), workpiecebind.BindValue(bv)).Exist(ctx)
if exists {
continue
}
// 电气件 SN 全局防重
if bi.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(工序%d),不能重复装机", bv, other.Sn, other.ProcessCode)
}
}
bindType := "BATCH"
if bi.ManageMode == "2" {
bindType = "SN"
}
if err := client.WorkpieceBind.Create().
SetSn(sn).SetOrderNo(orderNo).SetProcessCode(processCode).
SetStationNo(stationNo).SetMaterialCode(mc).SetMaterialName(bi.MaterialName).
SetSpec(bi.Spec).SetUnit(bi.Unit).SetManageMode(bi.ManageMode).
SetBindValue(bv).SetBindType(bindType).SetOperator(operator).
Exec(ctx); err != nil {
return bound, err
}
bound++
}
return bound, nil
}
// validateStationBinds 校验某工件在某工序的装配物料是否绑齐(工艺流程强校验核心):
// 返回缺料明细(空串=齐套)。判定:电气件(SN) 绑定的不同SN数 ≥ 单台用量;结构件(批次) 至少绑定1个批次号。
// 若该产品该工序未配置任何装配物料(processCode未配置),视为不启用绑定校验,放行。
func (s *Service) validateStationBinds(ctx context.Context, client *ent.Client, sn string, processCode int) (string, error) {
_, wo, err := s.workpieceProduct(ctx, client, sn)
if err != nil {
return "", err
}
targets, err := s.stationBindTargets(ctx, client, wo.ProductCode, "", processCode)
if err != nil {
return "", err
}
if len(targets) == 0 {
return "", nil // 该工序未配置装配物料 → 不校验
}
// 已绑定明细:按 物料编码 → 绑定值集合
binds, _ := client.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode)).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
}
// validateAllProcessBinds 完工兜底:校验工件工艺路线中每个工序的装配物料齐套
func (s *Service) validateAllProcessBinds(ctx context.Context, client *ent.Client, sn string) (string, error) {
if _, _, err := s.workpieceProduct(ctx, client, sn); err != nil {
return "", err
}
// 工艺路线由 station_process(按今日派工)派生:遍历今日启用的全部工序编号,逐一校验装配物料齐套
codes, err := RouteProcessCodes(ctx, client, TodayStr(), "")
if err != nil {
return "", err
}
missing := []string{}
for _, code := range codes {
msg, err := s.validateStationBinds(ctx, client, sn, code)
if err != nil {
return "", err
}
if msg != "" {
missing = append(missing, fmt.Sprintf("工序%d%s", code, 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.FieldProcessCode), 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
}
missing, err := s.validateAllProcessBinds(ctx, s.ctx.EntClient, sn)
if err != nil {
return nil, err
}
// 物料组成树:递归展开成品零部件到叶子(不依赖 WMS),让追溯页能展示完整物料组成,
// 避免成品作零部件被引用时只显示上层成品而漏下层料、追溯断链。
var tree []BindNode
if _, wo, perr := s.workpieceProduct(ctx, s.ctx.EntClient, sn); perr == nil && wo != nil {
tree = s.expandBindBom(ctx, s.ctx.EntClient, wo.ProductCode, "", 1, 0, map[string]bool{})
}
return &BindTraceVO{Binds: binds, Complete: missing == "", Missing: missing, BomTree: tree}, nil
}
// expandBindBom 递归展开某产品的物料组成树(成品/装配件若存在下级 BOM 则继续展开,叶子终止)。
// 不依赖 WMS:用 BomItem 是否存在 productCode 记录判断是否有下级 BOM,避免 WMS 不可达时不展开。
// 防环:同一条展开路径出现重复编码视为配置错误,跳过该分支。
func (s *Service) expandBindBom(ctx context.Context, client *ent.Client, productCode, bomName string, qty float64, depth int, path map[string]bool) []BindNode {
const maxDepth = 10
if depth > maxDepth || productCode == "" {
return nil
}
if path[productCode] {
s.ctx.EventLog.Write(ctx, "bom.expand.cycle", "", "", "product_bom", productCode,
"产品物料清单存在循环引用,已跳过", map[string]any{"depth": depth})
return nil
}
items, err := client.BomItem.Query().
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomNameOrDefault(bomName))).All(ctx)
if err != nil || len(items) == 0 {
return nil
}
path[productCode] = true
defer delete(path, productCode)
out := make([]BindNode, 0, len(items))
for _, it := range items {
if it.MaterialCode == "" {
continue
}
node := BindNode{
MaterialCode: it.MaterialCode, MaterialName: it.MaterialName, Spec: it.Spec,
Unit: it.Unit, ManageMode: it.ManageMode, Qty: it.UnitQty * qty, Level: depth,
}
// 该物料自身是否还有下级 BOM(成品/装配件):递归展开其子件
if has, _ := client.BomItem.Query().Where(bomitem.ProductCode(it.MaterialCode)).Exist(ctx); has {
node.Children = s.expandBindBom(ctx, client, it.MaterialCode, bomName, it.UnitQty*qty, depth+1, path)
}
out = append(out, node)
}
return out
}
// 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"`
ProcessCode int `json:"processCode"`
LastBinds []*ent.WorkpieceBind `json:"lastBinds"` // 最近10条绑定记录
StationNames map[int]string `json:"-"`
}
// StationBindPanelData 供工位终端/手动报工页在报工前拉取:本工件本工序该装什么、已装什么
func (s *Service) StationBindPanelData(ctx context.Context, sn string, processCode 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, wo.ProductCode, "", processCode)
if err != nil {
return nil, err
}
panel := &StationBindPanel{Enabled: len(targets) > 0, ProcessCode: processCode, StationNo: processCode}
// 全量绑定(齐套判定必须按全量统计,不能用 limit 截断,否则多料/多SN会误判缺料)
allBinds, _ := s.ctx.EntClient.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode)).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, processCode)
}
// 最近绑定(仅展示用)
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
}