Files
bj_power/bj_power_mes/internal/logic/line.go
T
SunYF 1cc93795ce feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
2026-09-17 12:49:07 +08:00

294 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))
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").
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
}