- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统 - 为BomItem实体添加relatedStandard字段及相关CRUD方法 - 为InspectionRecord实体添加reportNo、materialCode、materialName等字段 - 更新ent schema确保新字段的验证和默认值设置 - 添加必要的数据库迁移和字段映射逻辑
294 lines
11 KiB
Go
294 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 {
|
||
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
|
||
}
|