docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
@@ -2,56 +2,13 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/dailyplan"
|
||||
"bj_power_mes/ent/scanrecord"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/ent/workpiece"
|
||||
)
|
||||
|
||||
type ScanReq struct {
|
||||
Station string `json:"station"`
|
||||
Sn string `json:"sn"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
Type string `json:"type"` // ONLINE / PROCESS / DONE / TEMP_STORE
|
||||
ProcessCode int `json:"processCode"`
|
||||
Operator string `json:"operator"`
|
||||
}
|
||||
|
||||
// ReportScan 扫码报工:记录 scan_record,并更新工单进度
|
||||
func (s *Service) ReportScan(ctx context.Context, req ScanReq, operator string) error {
|
||||
if req.Sn == "" {
|
||||
return errors.New("sn 不能为空")
|
||||
}
|
||||
scanType := req.Type
|
||||
if scanType == "" {
|
||||
scanType = "PROCESS"
|
||||
}
|
||||
if req.Operator == "" {
|
||||
req.Operator = operator
|
||||
}
|
||||
_, err := s.ctx.EntClient.ScanRecord.Create().
|
||||
SetStation(req.Station).SetSn(req.Sn).SetOrderNo(req.OrderNo).
|
||||
SetType(scanType).SetProcessCode(req.ProcessCode).SetOperator(req.Operator).
|
||||
SetTime(time.Now()).Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "scan.report", req.OrderNo, operator, "scan_record", req.Sn, "扫码报工", map[string]any{"station": req.Station, "type": scanType, "processCode": req.ProcessCode})
|
||||
|
||||
// 更新工单进度:调高 finished 进度(去重按 sn+工序)
|
||||
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
|
||||
if err == nil && wp != nil && wp.WorkOrderId > 0 {
|
||||
s.bumpWorkOrderProgress(ctx, wp.OrderNo, wp.Sn)
|
||||
}
|
||||
s.notifyDashboard()
|
||||
return nil
|
||||
}
|
||||
|
||||
// bumpWorkOrderProgress 增加工单已完成数量(由完工动作驱动;扫码报工仅流转状态不重复计数)
|
||||
// bumpWorkOrderProgress 增加工单已完成数量(由完工动作驱动)
|
||||
func (s *Service) bumpWorkOrderProgress(ctx context.Context, orderNo, sn string) {
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).First(ctx)
|
||||
if err != nil || wo == nil {
|
||||
@@ -90,24 +47,3 @@ func (s *Service) bumpDailyPlanCompleted(ctx context.Context, orderNo string, t
|
||||
SetCompletedQty(newQty).SetStatus(st).Save(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ListScanRecords 查询扫码报工记录
|
||||
func (s *Service) ListScanRecords(ctx context.Context, sn, orderNo string, page, pageSize int) ([]*ent.ScanRecord, int, error) {
|
||||
q := s.ctx.EntClient.ScanRecord.Query()
|
||||
if sn != "" {
|
||||
q = q.Where(scanrecord.Sn(sn))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(scanrecord.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
if sn != "" {
|
||||
q = q.Where(scanrecord.SnEqualFold(sn))
|
||||
}
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := q.Order(ent.Desc(scanrecord.FieldID)).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Seed 初始化:超级管理员、默认角色、默认菜单权限、12 道工序与工位派工
|
||||
// Seed 初始化:超级管理员、默认角色、默认菜单权限、产线工位(内置只读主数据;工艺流程由用户自建不预置)
|
||||
func (s *Service) Seed(ctx context.Context) error {
|
||||
// 1. 默认角色(已存在则合并权限码,保证新按钮权限能落到已有角色上)
|
||||
roles := []struct{ name, code string }{
|
||||
@@ -168,37 +168,23 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
// 清掉存量角色里的旧码与旧菜单定义行(幂等);新菜单 produce.qty 在 1. 的 seedMenus 中定义。
|
||||
migrateLegacyQtyReport(ctx, s)
|
||||
|
||||
// 4. 初始化工位与默认工艺流程(工位绑定流程,流程承载步骤)
|
||||
seedFlowsAndStations(ctx, s)
|
||||
// 4. 初始化产线工位(内置只读主数据);工艺流程/工序步骤由用户在「工艺流程」页自建,不预置
|
||||
seedStations(ctx, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFlowsAndStations(ctx context.Context, s *Service) {
|
||||
// 初始数据:12 个工位(工位数量以数据库为准,这里只做首次初始化;要 13 个就往 station 表插一行)
|
||||
// 初始化的工位一律为「内置工位」:需连 PLC、必须按工位号顺序流转、不可改类型、不可删除;
|
||||
// 其中 1~10 号工位对应实体接驳台,11/12 为产线末端无接驳台。
|
||||
func seedStations(ctx context.Context, s *Service) {
|
||||
// 工位是产线固定物理工位,一律「内置工位」:需连 PLC、按工位号顺序流转、不可改类型、不可删除、仅可改名,
|
||||
// 属系统内置只读主数据(同 WMS 接驳台),故由种子初始化。12 个装配工位(1~10 带接驳台,11/12 产线末端无接驳台);
|
||||
// 工位数量以数据库为准,要 13 个就往 station 表插一行。
|
||||
// 工艺流程/工序步骤不在此预置:流程可在「工艺流程」页新建/编辑/删除/启停,是用户可维护的业务数据,
|
||||
// 由实施按产品工艺自建、并在「关联工位」页绑定到工位(station.flow_id),不硬编码进种子。
|
||||
for i := 1; i <= 12; i++ {
|
||||
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
|
||||
if err != nil {
|
||||
st, _ = s.ctx.EntClient.Station.Create().
|
||||
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx); err != nil {
|
||||
_, _ = s.ctx.EntClient.Station.Create().
|
||||
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").
|
||||
SetIsBuiltin(true).SetHasDock(i <= 10).Save(ctx)
|
||||
}
|
||||
if st == nil || st.FlowId > 0 {
|
||||
continue
|
||||
}
|
||||
flow, err := s.ctx.EntClient.ProcessFlow.Create().
|
||||
SetName("工位" + strconv.Itoa(i) + "_默认").
|
||||
SetStatus("ACTIVE").SetRemark("默认工艺流程").Save(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := s.ctx.EntClient.ProcessStep.Create().
|
||||
SetFlowId(flow.ID).SetSeq(1).SetName("装配完成").SetCollectType("NONE").Save(ctx); err == nil && i == 3 {
|
||||
_, _ = s.ctx.EntClient.ProcessStep.Create().
|
||||
SetFlowId(flow.ID).SetSeq(2).SetName("拧紧扭矩").SetCollectType("AUTO").SetIsTorque(true).Save(ctx)
|
||||
}
|
||||
_, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flow.ID).Save(ctx)
|
||||
}
|
||||
// 0 号(上线位) / 13 号(下线位) 虚拟工位:OFFLINE 类型,用 PAD 操作,不连 PLC、
|
||||
// 不叫料、不进入产线组合排序;仅做上下线登记,不参与物理工位计数。
|
||||
|
||||
@@ -132,6 +132,16 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
|
||||
if err != nil {
|
||||
return errors.New("工件未进线登记")
|
||||
}
|
||||
// 严格校验(业务口径已确认):仅“在制”工件可报工,且必须按工序顺序流转,禁止跳站/漏站/重复报同一站。
|
||||
// 在制 = ONLINE(已进线) / PROCESSING(在工位);DONE/REPAIR/SCRAPPED 一律拒绝。
|
||||
if wp.Status != "ONLINE" && wp.Status != "PROCESSING" {
|
||||
return errors.New("工件当前状态为「" + wp.Status + "」,非在制,不允许报工")
|
||||
}
|
||||
// 顺序流转:currentProcess 语义为“已报工完成的工序”(进线时=0),本工位必须是其下一道,即 processCode == currentProcess+1。
|
||||
if req.ProcessCode != wp.CurrentProcess+1 {
|
||||
return errors.New("工序顺序不符:工件已完成工序" + itoa(wp.CurrentProcess) +
|
||||
",应在工位" + itoa(wp.CurrentProcess+1) + "报工,当前请求工位" + itoa(req.ProcessCode))
|
||||
}
|
||||
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 U3):
|
||||
// 任一步失败全部回滚,杜绝"实绩已建但工件状态没推进/结果没落"的半成品脏数据;
|
||||
// 校验(齐套/needCheck)也走同一 tx client,才能读到本事务内刚写入的绑定。
|
||||
@@ -429,6 +439,56 @@ func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListProcessRecords 报工记录列表(分页):直接查真实报工表 workpiece_process,
|
||||
// 手动报工与工位终端报工都在此可见,形成报工闭环(取代已废弃的 scan_record 查询)。
|
||||
func (s *Service) ListProcessRecords(ctx context.Context, sn, orderNo string, stationNo, page, pageSize int) ([]map[string]any, int, error) {
|
||||
q := s.ctx.EntClient.WorkpieceProcess.Query()
|
||||
if sn != "" {
|
||||
q = q.Where(workpieceprocess.SnEqualFold(sn))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(workpieceprocess.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
if stationNo > 0 {
|
||||
q = q.Where(workpieceprocess.StationNo(itoa(stationNo)))
|
||||
}
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
rows, err := q.Order(ent.Desc(workpieceprocess.FieldID)).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
list := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ended := ""
|
||||
if r.EndedAt != nil {
|
||||
ended = r.EndedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"sn": r.Sn,
|
||||
"orderNo": r.OrderNo,
|
||||
"stationNo": r.StationNo,
|
||||
"processCode": r.ProcessCode,
|
||||
"processName": r.ProcessName,
|
||||
"status": r.Status,
|
||||
"result": r.Result,
|
||||
"operator": r.Operator,
|
||||
"durationSec": r.DurationSec,
|
||||
"endedAt": ended,
|
||||
})
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListWorkpieces(ctx context.Context, sn, orderNo string) ([]*ent.Workpiece, error) {
|
||||
q := s.ctx.EntClient.Workpiece.Query()
|
||||
if sn != "" {
|
||||
|
||||
Reference in New Issue
Block a user