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 }