293 lines
11 KiB
Go
293 lines
11 KiB
Go
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 || stationNo > 10 {
|
|||
|
|
return 0, errors.New("仅工位 1~10 可发起叫料(11/12 为末端下料检测位,无终端无接驳台)")
|
|||
|
|
}
|
|||
|
|
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 {
|
|||
|
|
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
|
|||
|
|
}
|