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

375 lines
14 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, 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, sn string) (*ent.Workpiece, *ent.WorkOrder, error) {
wp, err := s.ctx.EntClient.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 := s.ctx.EntClient.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, productCode, bomName string, processCode int) ([]bindTarget, error) {
items, err := s.ctx.EntClient.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, 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, 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, _ := s.ctx.EntClient.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, _ := s.ctx.EntClient.WorkpieceBind.Query().
Where(workpiecebind.Sn(sn), workpiecebind.MaterialCode(mc), workpiecebind.BindValue(bv)).Exist(ctx)
if exists {
continue
}
// 电气件 SN 全局防重
if bi.ManageMode == "2" {
other, _ := s.ctx.EntClient.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 := s.ctx.EntClient.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, sn string, processCode int) (string, error) {
_, wo, err := s.workpieceProduct(ctx, sn)
if err != nil {
return "", err
}
targets, err := s.stationBindTargets(ctx, wo.ProductCode, "", processCode)
if err != nil {
return "", err
}
if len(targets) == 0 {
return "", nil // 该工序未配置装配物料 → 不校验
}
// 已绑定明细:按 物料编码 → 绑定值集合
binds, _ := s.ctx.EntClient.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, sn string) (string, error) {
wp, _, err := s.workpieceProduct(ctx, sn)
if err != nil {
return "", err
}
missing := []string{}
for _, code := range ParseProcessSeq(wp.ProcessSeq) {
msg, err := s.validateStationBinds(ctx, 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"`
}
// 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, sn)
if err != nil {
return nil, err
}
return &BindTraceVO{Binds: binds, Complete: missing == "", Missing: missing}, nil
}
// 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, sn)
if err != nil {
return nil, err
}
targets, err := s.stationBindTargets(ctx, 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, 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
}