Files
bj_power/bj_power_mes/internal/logic/line.go
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

299 lines
11 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"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/linepoint"
"bj_power_mes/ent/plcmovecmd"
)
// ============ 产线点位台账(产线布局说明·修正版) ============
// 布局:上料位 IN(工位1/2间主线,人工投料)→ [1,2] → C1,C2 → [3,4] → C3,C4 → [5,6] → C5,C6 → [7,8] → C7,C8 → [9,10] → 上排末端11 / 下排末端12(下料检测位)
// 接驳台:工位1~10 各 2 个(R1=1号AGV、R2=2号AGV),属 AGV 点位,PLC 只读在位信号
// 上料位:工位1/2 之间的主传送带,人工投料,带传感器,PLC 上报在位信号
type pointDef struct {
pointType string
pointNo string
label string
seq int
stationNo int
}
// InitLinePoints 初始化点位台账(幂等:只补缺失,不覆盖已占用点位)
func (s *Service) InitLinePoints(ctx context.Context) error {
defs := []pointDef{}
seq := 10
add := func(pt, no, label string, stationNo int) {
seq += 10
defs = append(defs, pointDef{pt, no, label, seq, stationNo})
}
add("FEED", "IN", "上料位(工位1/2间主线)", 0)
for i := 1; i <= 10; i += 2 {
add("STATION", fmt.Sprint(i), fmt.Sprintf("工位%d", i), i)
add("STATION", fmt.Sprint(i+1), fmt.Sprintf("工位%d", i+1), i+1)
if i <= 7 {
add("CACHE", fmt.Sprintf("C%d", i), fmt.Sprintf("缓存位C%d", i), 0)
add("CACHE", fmt.Sprintf("C%d", i+1), fmt.Sprintf("缓存位C%d", i+1), 0)
}
}
add("END", "END-U", "上排末端下料检测位(工位11)", 11)
add("END", "END-D", "下排末端下料检测位(工位12)", 12)
for st := 1; st <= 10; st++ {
for agv := 1; agv <= 2; agv++ {
add("DOCK", fmt.Sprintf("%d-R%d", st, agv), fmt.Sprintf("工位%d %d号AGV接驳台", st, agv), st)
}
}
for _, d := range defs {
exist, _ := s.ctx.EntClient.LinePoint.Query().
Where(linepoint.PointType(d.pointType), linepoint.PointNo(d.pointNo)).First(ctx)
if exist != nil {
continue
}
_, _ = s.ctx.EntClient.LinePoint.Create().
SetPointType(d.pointType).SetPointNo(d.pointNo).SetLabel(d.label).
SetSeq(d.seq).SetStationNo(d.stationNo).Save(ctx)
}
return nil
}
// ListPoints 查询点位台账(按传送带顺序)
func (s *Service) ListPoints(ctx context.Context) ([]map[string]any, error) {
pts, err := s.ctx.EntClient.LinePoint.Query().Order(ent.Asc(linepoint.FieldSeq)).All(ctx)
if err != nil {
return nil, err
}
out := make([]map[string]any, 0, len(pts))
for _, p := range pts {
out = append(out, map[string]any{
"id": p.ID, "pointType": p.PointType, "pointNo": p.PointNo, "label": p.Label,
"seq": p.Seq, "stationNo": p.StationNo, "currentSn": p.CurrentSn,
"orderNo": p.OrderNo, "occupied": p.Occupied, "updatedAt": p.UpdatedAt,
})
}
return out, nil
}
// IssueMove 上位机下发移料指令(PLC 只执行,不判断)
func (s *Service) IssueMove(ctx context.Context, sn, fromPoint, toPoint, orderNo, operator string) (map[string]any, error) {
if sn == "" || fromPoint == "" || toPoint == "" {
return nil, errors.New("工件SN、起点位、目标点位必填")
}
if fromPoint == toPoint {
return nil, errors.New("起点位与目标点位相同")
}
from, err := s.ctx.EntClient.LinePoint.Query().Where(linepoint.PointNo(fromPoint)).First(ctx)
if err != nil {
return nil, errors.New("起点位不存在:" + fromPoint)
}
to, err := s.ctx.EntClient.LinePoint.Query().Where(linepoint.PointNo(toPoint)).First(ctx)
if err != nil {
return nil, errors.New("目标点位不存在:" + toPoint)
}
if !from.Occupied || from.CurrentSn != sn {
return nil, errors.New("起点位当前不是该工件,无法移出")
}
if to.Occupied {
return nil, errors.New("目标点位被占用:" + toPoint)
}
if operator == "" {
operator = "SYSTEM"
}
cmdNo := fmt.Sprintf("MV%d", time.Now().UnixNano())
cmd, err := s.ctx.EntClient.PlcMoveCmd.Create().
SetCmdNo(cmdNo).SetSn(sn).SetOrderNo(orderNo).
SetFromPoint(fromPoint).SetToPoint(toPoint).
SetStatus("ISSUED").SetOperator(operator).Save(ctx)
if err != nil {
return nil, err
}
s.ctx.EventLog.Write(ctx, "line.move.issue", sn, operator, "plc_move_cmd", cmdNo,
"下发移料指令 "+fromPoint+" → "+toPoint, map[string]any{"sn": sn, "orderNo": orderNo})
return map[string]any{"id": cmd.ID, "cmdNo": cmd.CmdNo, "status": cmd.Status}, nil
}
// AckMove PLC 应答指令
func (s *Service) AckMove(ctx context.Context, cmdNo string) error {
cmd, err := s.ctx.EntClient.PlcMoveCmd.Query().Where(plcmovecmd.CmdNo(cmdNo)).First(ctx)
if err != nil {
return errors.New("指令不存在")
}
if cmd.Status != "ISSUED" {
return errors.New("当前状态不允许应答:" + cmd.Status)
}
return s.ctx.EntClient.PlcMoveCmd.UpdateOneID(cmd.ID).SetStatus("ACK").Exec(ctx)
}
// ArriveMove 工件到位:回写点位台账(起点清空、目标写入),指令闭环
func (s *Service) ArriveMove(ctx context.Context, cmdNo string) error {
cmd, err := s.ctx.EntClient.PlcMoveCmd.Query().Where(plcmovecmd.CmdNo(cmdNo)).First(ctx)
if err != nil {
return errors.New("指令不存在")
}
if cmd.Status != "ISSUED" && cmd.Status != "ACK" {
return errors.New("当前状态不允许到位:" + cmd.Status)
}
tx, err := s.ctx.EntClient.Tx(ctx)
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
_, _ = tx.LinePoint.Update().Where(linepoint.PointNo(cmd.FromPoint)).
SetCurrentSn("").SetOrderNo("").SetOccupied(false).Save(ctx)
_, _ = tx.LinePoint.Update().Where(linepoint.PointNo(cmd.ToPoint)).
SetCurrentSn(cmd.Sn).SetOrderNo(cmd.OrderNo).SetOccupied(true).Save(ctx)
if err := tx.PlcMoveCmd.UpdateOneID(cmd.ID).SetStatus("ARRIVED").Exec(ctx); err != nil {
return err
}
_ = tx.PlcMoveCmd.UpdateOneID(cmd.ID).SetStatus("CLOSED").Exec(ctx)
if err := tx.Commit(); err != nil {
return err
}
committed = true
s.ctx.EventLog.Write(ctx, "line.move.arrive", cmd.Sn, cmd.Operator, "plc_move_cmd", cmdNo,
"移料到位 "+cmd.FromPoint+" → "+cmd.ToPoint, map[string]any{"sn": cmd.Sn, "orderNo": cmd.OrderNo})
s.notifyDashboard()
return nil
}
// ListMoveCmds 查询移料指令(可按状态/工件过滤)
func (s *Service) ListMoveCmds(ctx context.Context, status, sn string, limit int) ([]map[string]any, error) {
q := s.ctx.EntClient.PlcMoveCmd.Query().Order(ent.Desc(plcmovecmd.FieldID))
if status != "" {
q = q.Where(plcmovecmd.Status(status))
}
if sn != "" {
q = q.Where(plcmovecmd.Sn(sn))
}
if limit <= 0 || limit > 200 {
limit = 50
}
cmds, err := q.Limit(limit).All(ctx)
if err != nil {
return nil, err
}
out := make([]map[string]any, 0, len(cmds))
for _, c := range cmds {
out = append(out, map[string]any{
"id": c.ID, "cmdNo": c.CmdNo, "sn": c.Sn, "orderNo": c.OrderNo,
"fromPoint": c.FromPoint, "toPoint": c.ToPoint, "status": c.Status,
"operator": c.Operator, "message": c.Message, "createdAt": c.CreatedAt,
})
}
return out, nil
}
// CallMaterialItem 叫料明细
type CallMaterialItem struct {
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"`
Qty float64 `json:"qty"`
}
// CallMaterial 工位终端叫料:操作员选定接驳台(R1/R2)后生成备料需求,物料送达该接驳台。
// 规则(产线布局修正版):叫料/退料均由工位终端发起,日排产不发起;接驳台属 AGV 点位。
func (s *Service) CallMaterial(ctx context.Context, stationNo int, dock, orderNo, operator string, items []CallMaterialItem) (int, error) {
if stationNo < 1 {
return 0, errors.New("工位号无效")
}
if dock == "" {
return 0, errors.New("请选择接驳台(R1/R2")
}
_, err := s.ctx.EntClient.LinePoint.Query().
Where(linepoint.PointType("DOCK"), linepoint.PointNo(dock), linepoint.StationNo(stationNo)).First(ctx)
if err != nil {
// 0/13 为 OFFLINE 虚拟上下线位,11/12 为末端无接驳台;一律按「无此接驳台」拒绝,不写死工位范围
return 0, errors.New("该工位无此接驳台:" + dock)
}
if len(items) == 0 {
return 0, errors.New("请添加叫料明细")
}
created := 0
planDate := time.Now().Format("2006-01-02")
for _, it := range items {
if it.MaterialCode == "" || it.Qty <= 0 {
continue
}
reqNo := fmt.Sprintf("MR%d", time.Now().UnixNano()+int64(created))
// 真 bug 修复(2026-09-19):必须写 stationNo,否则工位终端「待接料」按
// stationNo=本工位 查询时看不见自己叫的料(只有扫码接料能撞上,隐蔽)。
// 补料单 source=REFILL:优先于排产备料,且默认叫 AGV 配送到该工位接驳台。
if _, err := s.ctx.EntClient.MaterialRequest.Create().
SetRequestNo(reqNo).SetOrderNo(orderNo).SetPlanDate(planDate).
SetMaterialCode(it.MaterialCode).SetMaterialName(it.MaterialName).SetUnit(it.Unit).
SetManageMode(it.ManageMode).SetReqQty(it.Qty).SetStatus("PENDING").
SetStationNo(stationNo).
SetSource("REFILL").SetNeedAgv(true).
SetTargetDock(dock).SetOperator(operator).Save(ctx); err != nil {
return created, err
}
created++
}
if created == 0 {
return 0, errors.New("没有有效的叫料明细(物料编码与数量必填)")
}
s.ctx.EventLog.Write(ctx, "line.call_material", orderNo, operator, "material_request", dock,
fmt.Sprintf("工位%d 叫料 %d 项 → 接驳台 %s", stationNo, created, dock),
map[string]any{"stationNo": stationNo, "dock": dock, "count": created})
return created, nil
}
// OccupyPoint 点位人工记账(唯一事实源维护入口)。
// action=IN :上料/占位,把工件 SN 记到该点位(点位被其它工件占用时报错;同一工件幂等放行)。
// action=OUT :下料/释放,清空该点位(传入 sn 时校验点位当前就是该工件)。
// 用途:产线首件上线、上料位(IN)投料、末端下料检测位(11/12)下料、未联调 PLC 时的人工记账。
func (s *Service) OccupyPoint(ctx context.Context, pointNo, sn, orderNo, operator, action string) error {
pointNo = strings.TrimSpace(pointNo)
sn = strings.TrimSpace(sn)
if pointNo == "" {
return errors.New("点位编号必填")
}
pt, err := s.ctx.EntClient.LinePoint.Query().Where(linepoint.PointNo(pointNo)).First(ctx)
if err != nil {
return errors.New("点位不存在:" + pointNo)
}
if operator == "" {
operator = "SYSTEM"
}
if action == "OUT" {
if pt.Occupied && sn != "" && pt.CurrentSn != sn {
return errors.New("点位 " + pointNo + " 当前不是该工件,无法下料")
}
if _, err := s.ctx.EntClient.LinePoint.UpdateOneID(pt.ID).
SetCurrentSn("").SetOrderNo("").SetOccupied(false).Save(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "line.point.out", pt.CurrentSn, operator, "line_point", pointNo,
"点位下料释放 "+pointNo, map[string]any{"sn": pt.CurrentSn, "orderNo": pt.OrderNo})
s.notifyDashboard()
return nil
}
if sn == "" {
return errors.New("上料必须提供工件SN")
}
if pt.Occupied && pt.CurrentSn != sn {
return errors.New("点位 " + pointNo + " 已被工件 " + pt.CurrentSn + " 占用")
}
if _, err := s.ctx.EntClient.LinePoint.UpdateOneID(pt.ID).
SetCurrentSn(sn).SetOrderNo(orderNo).SetOccupied(true).Save(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "line.point.in", sn, operator, "line_point", pointNo,
"点位上料占位 "+pointNo, map[string]any{"sn": sn, "orderNo": orderNo})
s.notifyDashboard()
return nil
}