docs(business): 生成业务契约文档并重构MES客户端代码

- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
This commit is contained in:
SunYF
2026-09-20 18:25:44 +08:00
parent 5f68e77d5f
commit 8e6e8e1d20
46 changed files with 1052 additions and 3954 deletions
+1 -65
View File
@@ -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
}
+11 -25
View File
@@ -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、
// 不叫料、不进入产线组合排序;仅做上下线登记,不参与物理工位计数。
+60
View File
@@ -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 != "" {