diff --git a/bj_power_mes/_build_r1.txt b/bj_power_mes/_build_r1.txt deleted file mode 100644 index e69de29..0000000 diff --git a/bj_power_mes/_fe_build_r1.txt b/bj_power_mes/_fe_build_r1.txt deleted file mode 100644 index df814a3..0000000 --- a/bj_power_mes/_fe_build_r1.txt +++ /dev/null @@ -1,65 +0,0 @@ - -> bj_power_mes@0.1.0 build -> vite build - -vite v6.4.3 building for production... -transforming... -鉁?[39m 1717 modules transformed. -rendering chunks... -computing gzip size... -../public/index.html  0.93 kB 鈹?gzip: 0.64 kB -../public/assets/AccountManage-C2vuxiJn.css  0.04 kB 鈹?gzip: 0.06 kB -../public/assets/ProcessFlow-3N9njrbS.css  0.07 kB 鈹?gzip: 0.09 kB -../public/assets/DailyPlan-BDdQczMH.css  0.10 kB 鈹?gzip: 0.10 kB -../public/assets/WorkOrder-D22Hpzah.css  0.22 kB 鈹?gzip: 0.17 kB -../public/assets/RoleManage-CiKSkw8Z.css  0.23 kB 鈹?gzip: 0.17 kB -../public/assets/Login-CjLneD_r.css  0.28 kB 鈹?gzip: 0.22 kB -../public/assets/Performance-apDxO0GG.css  0.29 kB 鈹?gzip: 0.17 kB -../public/assets/Wip-B2fSsftz.css  0.34 kB 鈹?gzip: 0.20 kB -../public/assets/ProcessCard-CZr2KBMZ.css  0.38 kB 鈹?gzip: 0.20 kB -../public/assets/Inspect-B9iD55ul.css  1.29 kB 鈹?gzip: 0.47 kB -../public/assets/help-LjjRk3EO.css  2.05 kB 鈹?gzip: 0.71 kB -../public/assets/index-BDJS_YlO.css  363.39 kB 鈹?gzip: 48.74 kB -../public/assets/perm-DCTK2KrM.js  0.25 kB 鈹?gzip: 0.21 kB -../public/assets/stations-Dzv88YhI.js  0.30 kB 鈹?gzip: 0.23 kB -../public/assets/print-DTtcch1s.js  0.38 kB 鈹?gzip: 0.33 kB -../public/assets/format-Cv489sdL.js  0.42 kB 鈹?gzip: 0.27 kB -../public/assets/Login-DmCBxrQq.js  1.71 kB 鈹?gzip: 1.01 kB -../public/assets/ChangePassword-s6p1SZDQ.js  2.24 kB 鈹?gzip: 1.04 kB -../public/assets/Attachment-BK7vM8A6.js  2.78 kB 鈹?gzip: 1.45 kB -../public/assets/Wip-BhtA64Wf.js  4.93 kB 鈹?gzip: 2.13 kB -../public/assets/PlcSend-BALwsPXD.js  5.26 kB 鈹?gzip: 2.27 kB -../public/assets/AlertCenter-BNTEHWwa.js  5.26 kB 鈹?gzip: 2.31 kB -../public/assets/EventLog-BbNjMdbZ.js  5.65 kB 鈹?gzip: 2.41 kB -../public/assets/MaterialRequest-DW2mD0ap.js  6.38 kB 鈹?gzip: 2.62 kB -../public/assets/RoleManage-D8JUrrkM.js  6.98 kB 鈹?gzip: 3.03 kB -../public/assets/ProductType-CDHoO9KA.js  7.71 kB 鈹?gzip: 2.86 kB -../public/assets/Trace-gVCxZhMx.js  7.77 kB 鈹?gzip: 3.05 kB -../public/assets/Performance-COUMijf6.js  8.52 kB 鈹?gzip: 2.63 kB -../public/assets/ProcessCard-VdAX3Ctb.js  8.65 kB 鈹?gzip: 3.00 kB -../public/assets/AccountManage-B4B5mcv2.js  9.34 kB 鈹?gzip: 3.61 kB -../public/assets/Scan-uubBizpO.js  9.65 kB 鈹?gzip: 3.57 kB -../public/assets/Torque-CWfv13GG.js  10.36 kB 鈹?gzip: 3.54 kB -../public/assets/Bom-C24oLlJ1.js  10.45 kB 鈹?gzip: 3.66 kB -../public/assets/Station-DXlw9Iq6.js  12.20 kB 鈹?gzip: 4.40 kB -../public/assets/QualityInspection-DerZOZPD.js  13.23 kB 鈹?gzip: 4.16 kB -../public/assets/DailyPlan-CJJ7RH3O.js  13.84 kB 鈹?gzip: 4.98 kB -../public/assets/ProcessFlow-Cx8d2It_.js  15.57 kB 鈹?gzip: 5.19 kB -../public/assets/WorkOrder-CCYT-qD3.js  16.84 kB 鈹?gzip: 5.25 kB -../public/assets/Inspect-DiIlkSed.js  22.21 kB 鈹?gzip: 6.30 kB -../public/assets/help--svnxhd4.js  49.44 kB 鈹?gzip: 18.03 kB -../public/assets/index-Bt2-rnMJ.js 1,126.46 kB 鈹?gzip: 372.37 kB -npm.cmd :  -所在位置 行:448 字符: 1 -+ & "C:\Program Files\nodejs\npm.cmd" run build 2>&1 | Out-File -Encodi ... -+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + CategoryInfo : NotSpecified: (:String) [], RemoteException - + FullyQualifiedErrorId : NativeCommandError - -(!) Some chunks are larger than 500 kB after minification. Consider: -- Using dynamic import() to code-split the application -- Use build.rollupOptions.output.manualChunks to improve chunking: https://roll -upjs.org/configuration-options/#output-manualchunks -- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.[39 -m -鉁?built in 6.88s diff --git a/bj_power_mes/_fix_station.py b/bj_power_mes/_fix_station.py deleted file mode 100644 index 8f18904..0000000 --- a/bj_power_mes/_fix_station.py +++ /dev/null @@ -1,95 +0,0 @@ -p='internal/logic/processflow.go' -lines=open(p,encoding='utf-8').read().split('\n') -# find function start -start=None -for i,l in enumerate(lines): - if l.startswith('func (s *Service) StationTask('): - start=i - break -assert start is not None, 'func not found' -# find matching close brace -depth=0 -end=None -for j in range(start,len(lines)): - depth+=lines[j].count('{') - depth-=lines[j].count('}') - if depth==0 and j>start: - end=j - break -assert end is not None, 'no end' -func='''func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]any, error) { - st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx) - if err != nil { - return nil, errors.New("工位不存在或未配置") - } - var flow *ent.ProcessFlow - if st.FlowId > 0 { - flow, _ = s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId) - } - steps := []*ProcessStepTemplate{} - if flow != nil && flow.Status == "ACTIVE" { - steps = s.flowSteps(ctx, flow.ID) - } - orderNos := []string{} - wos, _ := s.ctx.EntClient.WorkOrder.Query(). - Where(workorder.StatusIn("CREATED", "RELEASED", "IN_PROGRESS")). - Order(ent.Asc(workorder.FieldID)).Limit(10).All(ctx) - for _, w := range wos { - orderNos = append(orderNos, w.WorkOrderNo) - } - // M4:工位终端按"工艺路线段"驱动(段=并行工位组,谁空停谁)。 - // 解析活跃工单的 routeSnapshot,找出本工位所属的路线段,供终端按段执行流程。 - routeSegments := []map[string]any{} - for _, w := range wos { - if len(w.RouteSnapshot) == 0 { - continue - } - for _, seg := range w.RouteSnapshot { - stations, _ := seg["stations"].([]any) - inSeg := false - for _, sv := range stations { - switch n := sv.(type) { - case float64: - if int(n) == stationNo { - inSeg = true - } - case int: - if n == stationNo { - inSeg = true - } - } - if inSeg { - break - } - } - if !inSeg { - continue - } - flowId := 0 - if fv, ok := seg["flowId"].(float64); ok { - flowId = int(fv) - } else if fv, ok := seg["flowId"].(int); ok { - flowId = fv - } - routeSegments = append(routeSegments, map[string]any{ - "orderNo": w.WorkOrderNo, - "seq": seg["seq"], - "segmentType": seg["segmentType"], - "flowId": flowId, - "stations": stations, - }) - } - } - return map[string]any{ - "stationNo": st.StationNo, - "stationName": st.Name, - "flow": flow, - "flowActive": flow != nil && flow.Status == "ACTIVE", - "steps": steps, - "orderNos": orderNos, - "routeSegments": routeSegments, - }, nil -}''' -out=lines[:start]+func.split('\n')+lines[end+1:] -open(p,'w',encoding='utf-8').write('\n'.join(out)) -print("OK StationTask rewritten") diff --git a/bj_power_mes/bj_power_mes.exe.bak_120023 b/bj_power_mes/bj_power_mes.exe.bak_120023 deleted file mode 100644 index 9a4e164..0000000 Binary files a/bj_power_mes/bj_power_mes.exe.bak_120023 and /dev/null differ diff --git a/bj_power_mes/bj_power_mes.exe.bak_144517 b/bj_power_mes/bj_power_mes.exe.bak_144517 deleted file mode 100644 index 4617ee2..0000000 Binary files a/bj_power_mes/bj_power_mes.exe.bak_144517 and /dev/null differ diff --git a/案例.md b/案例.md index dc70e2a..dc681c3 100644 --- a/案例.md +++ b/案例.md @@ -357,6 +357,851 @@ - 无。 +## 场景 W3:入库登记 +### 前置条件 +- WMS 物料档案已存在结构件物料 M-A(按批次管理)、电气件物料 M-B(按序列号管理)(W17)。 +- 已存在一张来料检验单号(用于入库时填写),结论为合格。 +### 操作步骤 +- 库管员登录 WMS,进「入库管理」,点「新增入库」,填:图号 M-A、入库类型「采购入库」、区域 Z1、检测单号 INSP-001、数量 100、批次号 BA001、归属类型「工单编号」、归属单号 WO20260920、操作人「张三」,点「保存」。 +### 系统反应 +- 系统先校验图号必填、区域必填、检测单号必填,再查物料档案确认 M-A 存在。 +- 质量门禁:若该批次此前被判「不合格」,拒绝入库。本例批次无不合格记录,放行。 +- 按检测单号带出质量状态(合格/不合格/部分/未关联)。 +- 结构件按批次建库存行、写入库明细,回写入库单数量。 +### 数据变化 +- inbound_order 新增一行:inbound_no=IBxxxx、material_code=M-A、manage_mode=1、batch_no=BA001、zone_code=Z1、quantity=100、inspection_no=INSP-001、ownership_type=workorder、ownership_no=WO20260920、quality_status=合格、voided=false。 +- inventories 新增一行:manage_mode=1、category=1、material_code=M-A、batch_no=BA001、quantity=100、locked_qty=0、quality_status=未检、status=在库、zone_code=Z1、inbound_no=IBxxxx。 +- inbound_detail 新增一行:inbound_no=IBxxxx、batch_no=BA001、material_code=M-A、quantity=100。 +### 验证点 +- 入库管理列表出现该单,数量 100,质量状态「合格」,未作废。 +- 库存查询(W8)批次 BA001 在库 100、可用 100。 +- 数量等式:inventory.quantity = inbound_order.quantity = inbound_detail.quantity = 100;可用 = quantity − locked_qty = 100。 +### 对不上的地方 +- 无。(同批次再次入库为「追加」,quantity 累加;盘点锁定中的批次禁止追加入库——属前置校验。) + +## 场景 W4:入库作废 +### 前置条件 +- 入库单 IBxxxx(M-A 批次 BA001 数量 100)已登记且未作废(W3);该批次未被出库锁定、未被消耗。 +### 操作步骤 +- 库管员在「入库管理」找到该单,点「作废」,填作废原因「录入重复」,点「确认作废」。 +### 系统反应 +- 系统校验作废原因必填、单据未重复作废。 +- 结构件按本单明细批次号扣减对应批次库存,扣到 0 删除该库存行;电气件则按本单 SN 精确删除库存行。 +- 安全门禁:目标库存已被锁定或数量不足,拒绝作废。 +- 单据标记作废,保留记录供追溯。 +### 数据变化 +- inventories:批次 BA001 行 quantity 100→0,行被删除。 +- inbound_order(IBxxxx):voided=false→true、void_reason=录入重复、voided_by=当前登录用户、voided_at=当前时间。 +### 验证点 +- 入库管理按「已作废」筛选可查到该单,库存查询不再有 BA001。 +- 数据库断言:inbound_order inbound_no=IBxxxx 的 voided=true;inventories 中 batch_no=BA001 的行数=0。 +- 数量等式:作废回滚量 rolledQty = 本单明细数量合计 = 100。 +### 对不上的地方 +- 无。(批次已被出库锁定 locked_qty>0、或在库量已小于本单数量时,作废被拒——属前置校验。) + +## 场景 W6:通用出库 +### 前置条件 +- 结构件物料 M-A 批次 BA002 合格在库、quantity=80、locked_qty=0(W3 入库后并经检验合格)。 +### 操作步骤 +- 库管员在「出库管理」选「通用出库」,填:图号 M-A、批次号 BA002、数量 30、出库类别「发货」、操作人「李四」,点「确认出库」。 +### 系统反应 +- 系统校验图号必填、批次或 SN 至少一个、出库类别必选(类别为「其他」时原因必填)。 +- 盘点冻结守卫:批次被进行中盘点锁定则拒绝。 +- 质量门禁:发货/样品/其他类出库仅允许「合格」品;退货/报废/退料类豁免。 +- 校验可用量充足后扣减批次库存、写出库明细、回写出库单实际数量。出库单创建即「已出库」。 +### 数据变化 +- outbound_order 新增一行:outbound_no=OBxxxx、outbound_type=general、status=已出库、material_code=M-A、batch_no=BA002、manage_mode=1、outbound_category=发货、quantity=30。 +- outbound_detail 新增一行:outbound_no=OBxxxx、batch_no=BA002、material_code=M-A、quantity=30。 +- inventories:批次 BA002 quantity 80→50。 +### 验证点 +- 出库管理列表出现该单,类型「通用出库」、状态「已出库」、数量 30。 +- 数据库断言:inventories batch_no=BA002 的 quantity=50;outbound_detail 该单 quantity=30。 +- 数量等式:出库前 quantity − 出库数量 = 出库后 quantity(80−30=50);可用 = quantity − locked_qty ≥ 0。 +### 对不上的地方 +- 无。(批次质量状态非「合格」且出库类别为发货/样品/其他时,出库被拒;可用量不足时被拒——属前置校验。) + +## 场景 W7:退库确认 +### 前置条件 +- 工位已发起退库,WMS 存在一张待确认退库单 THxxxx:order_no=WO20260920、station_no=1、material_code=M-A、qty=20、status=PENDING(由工位终端 S11 经内部接口预建)。 +- 工单台账 order_material_ledger(WO20260920, M-A) 存在:total_qty=20、out_qty=20、returned_qty=0、status=领料完结。 +### 操作步骤 +- 库管员在「退库确认」找到退库单 THxxxx,点「确认收货」,选退库类型「剩余物料」,选择退回库位区域 Z1,结构件填批次号 BA001,点「确认」。 +### 系统反应 +- 系统校验退库单未重复收货,按物料档案确定管理粒度。 +- 退库类型决定质量状态:剩余物料置「未检」,问题物料置「不合格」。 +- 结构件按数量加回(匹配同料同质量状态同货位的在库行追加,否则新建行);电气件按 SN 逐个扫码加回,扫码数须与退库数一致。 +- 盘点冻结守卫:目标批次/SN 被盘点锁定则拒绝加回。 +- 回写台账退回量,并把「领料完结」按净额拉回「进行中」。 +### 数据变化 +- return_order(THxxxx):status PENDING→RECEIVED、receive_by=当前登录用户、receive_at=当前时间。 +- inventories:M-A 批次 BA001(未检、Z1)quantity 加回 20(若无匹配行则新建 quantity=20 的行,inbound_no=RET:THxxxx)。 +- order_material_ledger(WO20260920, M-A):returned_qty 0→20;因 out_qty−returned_qty=0 < total_qty=20,status 领料完结→进行中。 +### 验证点 +- 退库确认列表该单状态「已收货」;库存查询 M-A 未检在库 +20。 +- 数量等式:net_out = out_qty − returned_qty = 20 − 20 = 0,且 net_out ≤ total_qty;电气件退库扫码数 = 退库数 ro.qty。 +### 对不上的地方 +- 无。(电气件扫码 SN 数与退库数量不符、目标被盘点锁定时,收货被拒——属前置校验。) + +## 场景 W9:来料检验 +### 前置条件 +- 结构件物料 M-A 批次 BA003 已入库、在库、quality_status=未检(W3)。 +### 操作步骤 +- 检验员在「来料检验」,填:检验类型「来料检验」、批次号 BA003、结论「不合格」、检验数 100、合格数 0、记录人「王五」,点「保存」。 +### 系统反应 +- 系统校验结论只能为合格/不合格,按批次号定位唯一库存行。 +- 写检验记录,并按结论+处置同步库存质量状态:合格→合格;让步接收→合格;退货→待退;其余不合格→不合格。 +### 数据变化 +- inspection_record 新增一行:target_type=BATCH、target_id=BA003、material_code=M-A、inspection_type=来料检验、status=不合格、inspect_qty=100、pass_qty=0、disposal_type=NONE。 +- inventories:批次 BA003 quality_status 未检→不合格(隔离,禁止出库上线)。 +### 验证点 +- 来料检验列表出现 BA003 不合格记录;库存查询 BA003 质量状态「不合格」。 +- 数据库断言:inventories batch_no=BA003 的 quality_status='不合格'。 +- 数量等式:pass_qty ≤ inspect_qty(0 ≤ 100)。 +### 对不上的地方 +- 无。 + +## 场景 W9-处置:不合格品处置(退货) +### 前置条件 +- 检验记录 BA003 结论为「不合格」(W9),记录 id 记为 :recId。 +### 操作步骤 +- 检验员在「来料检验」对 BA003 记录点「处置」,选处置方式「退货」,填货运单号 SF123456,点「确认处置」。 +### 系统反应 +- 系统校验仅「不合格」记录可处置、退货处置必须填货运单号。 +- 按处置方式翻转库存质量状态:退货→待退(保持数量、隔离);报废→不合格且生命周期报废;部分入库→按合格数拆行(仅批次管理);返修→不合格;让步接收→合格。 +### 数据变化 +- inspection_record(:recId):disposal_type NONE→RETURN、disposal_remark、return_tracking_no=SF123456。 +- inventories:批次 BA003 quality_status 不合格→待退。 +### 验证点 +- 检验记录处置方式显示「退货」,货运单号 SF123456;库存查询 BA003 质量状态「待退」。 +- 数据库断言:inventories batch_no=BA003 的 quality_status='待退';quantity 不变。 +### 对不上的地方 +- 无。 + +## 场景 W10:过程检验 +### 前置条件 +- 电气件物料 M-B 某 SN(记为 SNB0001)已入库、在库、quality_status=未检(W3)。 +### 操作步骤 +- 检验员在「过程检验」,填:检验类型「过程检验」、SN SNB0001、结论「合格」、检验数 1、合格数 1、记录人「王五」,点「保存」。 +### 系统反应 +- 系统按 SN 定位库存行(manage_mode=2),写检验记录并把库存质量状态同步为合格。 +### 数据变化 +- inspection_record 新增一行:target_type=SN、target_id=SNB0001、material_code=M-B、inspection_type=过程检验、status=合格、inspect_qty=1、pass_qty=1。 +- inventories:SN SNB0001 quality_status 未检→合格。 +### 验证点 +- 过程检验列表出现 SNB0001 合格记录;库存查询 SNB0001 质量状态「合格」。 +- 数据库断言:inventories sn_code=SNB0001 的 quality_status='合格'。 +### 对不上的地方 +- 无。 + +## 场景 W11:完工检验 +### 前置条件 +- 成品 SN(记为 SN0001,category=3)已由 MES 完工回流入 WMS、在库、quality_status=未检(S5)。 +### 操作步骤 +- 检验员在「完工检验」,填:检验类型「完工检验」、SN SN0001、结论「合格」、检验数 1、合格数 1、记录人「王五」,点「保存」。 +### 系统反应 +- 系统按 SN 定位成品库存行,写检验记录并把质量状态同步为合格。 +### 数据变化 +- inspection_record 新增一行:target_type=SN、target_id=SN0001、inspection_type=完工检验、status=合格、inspect_qty=1、pass_qty=1。 +- inventories:成品 SN0001 quality_status 未检→合格。 +### 验证点 +- 完工检验列表出现 SN0001 合格记录;库存查询成品口径 SN0001 质量状态「合格」。 +- 数据库断言:inventories sn_code=SN0001 且 category=3 的 quality_status='合格'。 +### 对不上的地方 +- 无。 + +## 场景 W12:库存盘点 +### 前置条件 +- 结构件物料 M-A 批次 BA004 在库、quantity=50、locked_qty=0、未被其他盘点单锁定。 +### 操作步骤 +- 库管员在「库存盘点」点「发起盘点」,限定物料 M-A(区域可空),点「确认发起」;进扫码区扫批次 BA004,填实盘数 48,点「录入」;点「完成盘点」,勾选「差异写回」、未盘项处理选「按账面保留」,点「确认完成」。 +### 系统反应 +- 发起:冻结当前库存快照并锁定范围内库存(打盘点单号),算出目标数量,状态「进行中」。 +- 扫码录入:电气件实盘数恒为 1,结构件按填入数量;记差异=实盘−账面。 +- 完成写回:仅按差异增减库存量、锁定量不动、可用量重算;写回后校验库存量≥锁定量,否则整体回滚;解锁库存,生成盘点调整单号,状态「已写回」。 +### 数据变化 +- stocktake_order 新增一行:stocktake_no=STxxxx、status 进行中→已写回、total_targets=覆盖批次/SN 数、diff_count=1、adjust_no=ADJxxxx、adjusted=true。 +- stocktake_item 新增/更新:target_id=BA004、target_type=BATCH、book_qty=50、scanned_qty=48、diff_qty=-2、counted=true。 +- inventories:发起时 BA004 stocktake_no=STxxxx(锁定),完成写回后 quantity 50→48、stocktake_no 清空(解锁)。 +### 验证点 +- 盘点单状态「已写回」,差异项数 1;库存查询 BA004 在库 48。 +- 数量等式:库存量 = 可用量 + 锁定量(48 = 48 + 0);diff_qty = scanned_qty − book_qty = 48 − 50 = -2;写回后 quantity ≥ locked_qty。 +- 数据库断言:inventories batch_no=BA004 的 quantity=48 且 stocktake_no=''。 +### 对不上的地方 +- 无。(盘亏后库存量将小于锁定量时写回被拒并整体回滚;仅「进行中」可取消——属前置校验/状态机约束。) + +## 场景 W13:半成品入库/出库 +### 前置条件 +- 半成品 SN(记为 SEMI0001)尚未入 WMS 半成品库。 +### 操作步骤 +- 库管员在「半成品管理」点「半成品入库」,填:SN SEMI0001、图号 M-A、品类「半成品」、已完成工序「1,2」、区域 Z1、工单号 WO20260920,点「保存」;再对该 SN 点「半成品出库」,选出库原因「继续生产」,点「确认」。 +### 系统反应 +- 入库:SN 防重(同品类同 SN 只允许一条),建统一库存行(category=2 半成品),状态在库、质量未检。 +- 出库:出库原因必选,校验在库且未被盘点锁定,置状态「出库」并记出库原因。 +### 数据变化 +- inventories 新增一行:manage_mode=2、category=2、sn_code=SEMI0001、material_code=M-A、quantity=1、quality_status=未检、status=在库、order_no=WO20260920、completed_process=1,2。 +- 出库后:该行 status 在库→出库、outbound_reason=继续生产。 +### 验证点 +- 半成品管理列表出现 SEMI0001,入库后状态「在库」,出库后状态「出库」、原因「继续生产」。 +- 数据库断言:inventories sn_code=SEMI0001 且 category=2 的行 status='出库'、quantity=1。 +- 数量等式:半成品按 SN 单件管理,quantity 恒为 1。 +### 对不上的地方 +- 无。(SN 重复入库、非在库出库、盘点锁定中出库均被拒——属前置校验。) + +## 场景 W8:库存查询 +### 前置条件 +- 已有若干在库库存(结构件批次、电气件 SN)。 +### 操作步骤 +- 库管员在「库存查询」,选口径「按物料汇总」,可填图号/名称/区域/质量状态筛选,点「查询」。 +### 系统反应 +- 系统只统计在库/锁定状态的库存(出库/使用/报废不计),结构件隐藏数量为 0 的行,按物料聚合出在库量/锁定量/可用量。 +- 另两个口径:按区域汇总、明细(逐批次/逐 SN)。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 页面按物料展示在库量/锁定量/可用量。 +- 数量等式:可用量 = 在库量 − 锁定量;仅合格品计入可用(未检/不合格/待退不计)。 +### 对不上的地方 +- 无。 + +## 场景 W14:工单备料台账 +### 前置条件 +- 工单 WO20260920 已经备料同步与出库(M4),order_material_ledger 存在 M-A、M-B 行。 +### 操作步骤 +- 库管员在「工单备料台账」,填工单号 WO20260920,点「查询」。 +### 系统反应 +- 系统按工单+物料展示四段量(需求总量/已出库/已退回/已消耗),并实时算出可用库存与备料四态。 +- 四态优先级:未到货(可用=0 且已出库=0)> 预警(可用<未来5天需求)> 未齐套(0<可用<需求)> 齐套(可用≥需求)。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 页面展示 M-A:需求 20 / 已出库 20 / 已退回 0 / 已消耗(完工后)。 +- 数量等式:net_out = out_qty − returned_qty ≤ total_qty;缺口 short_qty = max(0, total_qty − 可用量)。 +### 对不上的地方 +- 无。 + +## 场景 W15:AGV 配送 +### 前置条件 +- MES 已有待配送备料单行(含目标接驳台 target_dock);接驳台主数据已预置。 +### 操作步骤 +- 仓管员在「AGV配送」,拉取待发料行,勾选一批(同一目标接驳台),点「下发」。 +### 系统反应 +- 系统按目标接驳台聚合生成搬运任务,先落库为待下发,再调海康调度下发;下发成功置「已下发」并回写 MES 备料单为「配送中」;下发失败置「失败」。 +- 轮询刷新:到位后置「已到位」,关联出库单置「已送达」,回写 MES 备料单「已完成」。 +- 取消:已下发的先通知调度取消,本地置「已取消」,关联备料单退回待发料。 +### 数据变化 +- agv_task 新增一行:task_no=AGVxxxx、source_dock=DOCK21、target_dock=目标接驳台、materials_json=料行清单、status PENDING→DISPATCHED、operator、dispatched_at、hik_task_code。 +- 到位后:status DISPATCHED→DONE、arrived_at=当前时间;关联 outbound_order status→已送达。 +### 验证点 +- AGV任务列表出现该任务,状态「已下发」(模拟模式下刷新后「已到位」)。 +- 数据库断言:agv_task task_no=AGVxxxx 的 status 与海康回写一致;取消后 status='CANCELLED'。 +### 对不上的地方 +- 无。(模拟模式(未接真实海康 RCS)下任务会立即标记为已到位,属环境配置差异,非业务链路断裂。) + +## 场景 W16:区域维护 +### 前置条件 +- 已存在区域 Z1(level1)与其下货架 S1(level2)。 +### 操作步骤 +- 库管员在「区域维护」选「批量生成」,选区域 Z1、货架 S1,层起 1、层止 2,位起 1、位止 3,先点「预览」再点「提交生成」。 +### 系统反应 +- 系统按层×位范围生成位置节点(并自动确保层节点存在),已存在的节点跳过不重复建;一次最多 2000 个。 +### 数据变化 +- zone 新增多行:level=4、zone_code=Z1、shelf_no=S1、layer_no=1或2、position_no=1~3、code=位号、name=“S1架X层Y位”、status=启用;同时确保 level=3 层节点存在。 +### 验证点 +- 区域维护位置号 Tab 出现 6 个新位置(2层×3位)。 +- 数量等式:created + skipped = 层数 × 位数(6);重复提交时 created=0、skipped=6。 +### 对不上的地方 +- 无。 + +## 场景 W17:物料档案维护 +### 前置条件 +- 登录用户拥有物料新增权限。 +### 操作步骤 +- 库管员在「物料档案」点「新增」,填:图号 M-C、名称“结构件C”、规格型号“C-100”、单位“个”、品类“原材料”、类型“结构件”、安全库存 10,点「保存」。 +### 系统反应 +- 系统校验除「说明」外全部必填,图号唯一;其他类物料强制按数量管理(不做 SN)。 +### 数据变化 +- material 新增一行:code=M-C、name=结构件C、spec=C-100、unit=个、item_type=1、manage_mode=1、is_batch_managed=true、is_serial_managed=false、safety_stock=10。 +### 验证点 +- 物料档案列表出现 M-C,类型「结构件」、品类「原材料」。 +- 数据库断言:material code='M-C' 的 manage_mode=1、is_batch_managed=true。 +- 数量等式:manage_mode=1 时 is_batch_managed=true 且 is_serial_managed=false(互斥)。 +### 对不上的地方 +- 无。(图号重复新增被拒;除说明外缺任一必填项被拒——属前置校验。) + +## 场景 W18:接驳台维护 +### 前置条件 +- 系统已预置接驳台主数据(产线/库房/其他)。 +### 操作步骤 +- 库管员在「接驳台维护」,按类型/编码/名称/状态筛选,点「查询」。 +### 系统反应 +- 接驳台为系统预置只读主数据,仅展示不提供新增/修改/删除;产线类型与工位 1:1 绑定。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 页面展示接驳台编码/名称/类型/绑定工位号/状态/是否有托盘。 +- 数据库断言:dock 表中 dock_type='line' 的行 station_no 与工位一一对应。 +### 对不上的地方 +- 无。 + +## 场景 W19:账号管理 +### 前置条件 +- 登录用户拥有账号新建权限;角色已存在(W20)。 +### 操作步骤 +- 管理员在「账号管理」点「新建账号」,填:用户名 wms01、密码 123456、姓名“库管一”、选角色(roleId)、部门“库房”,点「保存」。 +### 系统反应 +- 系统校验用户名/密码必填、密码至少 6 位、用户名唯一;密码加密存储;角色优先按角色编号关联并回填角色编码。 +### 数据变化 +- user 新增一行:username=wms01、password=bcrypt 密文、real_name=库管一、role=角色编码、role_id=角色ID、dept=库房。 +### 验证点 +- 账号列表出现 wms01,可用该账号登录。 +- 数据库断言:user username='wms01' 存在且 password 为密文(非明文 123456)。 +### 对不上的地方 +- 无。(用户名重复、密码不足 6 位均被拒——属前置校验。) + +## 场景 W20:角色管理 +### 前置条件 +- 登录用户拥有角色新建权限。 +### 操作步骤 +- 管理员在「角色管理」点「新建角色」,填:角色名称“库管”、编码 warehouse、勾选权限(如 inbound:create、stocktake:start),点「保存」。 +### 系统反应 +- 系统校验名称与编码必填、编码唯一;权限以编码数组存储。 +### 数据变化 +- role 新增一行:name=库管、code=warehouse、remark、permission_codes=["inbound:create","stocktake:start"]。 +### 验证点 +- 角色列表出现 warehouse,权限数=勾选数。 +- 数据库断言:role code='warehouse' 的 permission_codes 含已勾选编码。 +### 对不上的地方 +- 无。(角色编码重复被拒;内置管理员角色权限不允许修改、内置三角色不允许删除——属前置校验。) + +## 场景 W21:操作日志 +### 前置条件 +- 系统已产生若干业务操作(入库/出库/盘点等均写日志)。 +### 操作步骤 +- 管理员在「操作日志」,按时间/操作人/业务类型筛选,点「查询」。 +### 系统反应 +- 系统分页返回操作留痕(谁、何时、对哪个对象、做了什么)。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 页面展示日志条目,含操作人/时间/业务对象/描述。 +- 数据库断言:event_log 中每条业务写入均有对应日志行。 +### 对不上的地方 +- 无。 + +## 场景 W22:附件中心 +### 前置条件 +- 存在一个业务对象(如某入库单),登录用户拥有附件查看权限。 +### 操作步骤 +- 库管员在业务页上传一份附件(选文件),或在「附件中心」集中查看/归档/清理各业务附件。 +### 系统反应 +- 上传校验业务类型/业务对象必填、文件类型白名单、大小上限;流式落盘并算 MD5,同业务对象同 MD5 秒传不重复落盘。 +### 数据变化 +- attachment 新增一行:biz_type、biz_id、file_name、file_path、file_type、file_ext、file_md5、file_size、uploaded_by、deleted=false。 +### 验证点 +- 附件中心可查到该附件并可预览/下载。 +- 数据库断言:attachment biz_id=业务对象 的行 deleted=false;重复上传同文件不新增行(file_md5 相同)。 +### 对不上的地方 +- 无。 + +## 场景 W23:修改密码 +### 前置条件 +- 当前用户已登录。 +### 操作步骤 +- 用户在「修改密码」,填旧密码、新密码(至少 6 位)、确认新密码,点「保存」。 +### 系统反应 +- 系统校验旧密码正确、新密码至少 6 位,重新加密存储。 +### 数据变化 +- user(当前用户):password 更新为新密码的 bcrypt 密文。 +### 验证点 +- 用新密码可登录,旧密码失效。 +- 数据库断言:user 该行 password 已变更且为密文。 +### 对不上的地方 +- 无。(旧密码不正确、新密码不足 6 位均被拒——属前置校验。) + +## 场景 W24:库存大屏 +### 前置条件 +- 库存/入出库已有数据。 +### 操作步骤 +- 展示屏免登录打开库存大屏页面,自动拉取概览数据全屏展示。 +### 系统反应 +- 系统汇总结构件批次总量/锁定量、电气件在库/出库 SN 数、半成品/成品在库数、装箱数、当日入/出库单数、按区域+质量状态明细。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 大屏展示批次数/总量/锁定量/SN在库数/半成品在库数/成品在库数/今日入库单数/今日出库单数。 +- 数据库断言:大屏各计数与 inventories/inbound_order/outbound_order 对应条件计数一致(如 totalQty = 结构件 quantity 求和)。 +### 对不上的地方 +- 无。 + +## 场景 W1:登录 +### 前置条件 +- 账号已在库房系统创建并启用、密码已加密存储。 +### 操作步骤 +- 库管员/管理员在登录页输入用户名、密码,点登录。 +### 系统反应 +- 校验用户名密码必填→查用户→账号停用被拒→比对密码→更新最后登录时间→签发访问令牌→记登录操作日志。返回令牌、过期时间、用户信息(含权限码列表)、弱密码标记(纯数字密码仅提示不拦截)。 +### 数据变化 +- user.last_login_at 更新为当前时间戳;event_log 增一条登录日志(auth.login、含 IP)。 +### 验证点 +- 用户名或密码错误提示「用户名或密码错误」;账号停用提示「账号已停用」;用户名/密码为空提示「用户名和密码必填」;纯数字密码登录成功但弱密码标记为真;返回权限码为该用户角色权限。 +### 对不上的地方 +- 无。 + +## 场景 W2:工作台 +### 前置条件 +- 用户已登录。 +### 操作步骤 +- 用户登录后进入工作台首页。 +### 系统反应 +- 首页自动拉取库存概览与缺货预警:概览汇总在库总量、锁定量、SN在库数、半成品在库数、装箱出库数、批次数、今日入库单数、今日出库单数;缺货预警列出可用量低于安全库存的物料(图号、名称、规格、安全库存、可用量、缺口);并提供入库/备料出库/库存查询/质量检验/盘点/半成品/台账/区域/物料档案/大屏等快捷入口。概览接口免登录。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 概览各计数与库存/入出库表对应条件计数一致;缺货预警仅列可用量小于安全库存的物料;缺货接口异常时工作台不报错、预警区留空。 +### 对不上的地方 +- 无。 + +## 场景 W5:备料出库 +### 前置条件 +- 工单 BOM 已同步到库房形成工单物料台账;目标批次库存充足且质量状态为合格;未在盘点冻结期。 +### 操作步骤 +- 库管在备料出库页选工单号、物料、批次号(或序列号列表)、数量、目标工位/接驳台,可填装箱与库位信息,点出库。 +### 系统反应 +- 强校验工单物料台账存在(否则提示先同步 BOM 或改用通用出库)、台账非「领料完结」、批次或序列号至少提供一个、盘点期间冻结的批次禁止出库;在一个事务内创建出库单(类型备料出库、状态已出库、按需叫 AGV)+按批次原子扣减库存(库存量不小于出库量才扣、回收锁定量)+写出库明细+更新工单台账,任一环节失败整体回滚;批次质量状态非合格被拦截(仅合格品可领料上线);累计出库超 BOM 需求时需填超领原因否则拦截。 +### 数据变化 +- outbound_order 增出库单(出库单号 OB+uuid、类型备料出库、状态已出库、是否需 AGV、工单号、物料、批次号、库位、目标接驳台、操作人);inventories 对应批次 quantity 减少、locked_qty 回收;outbound_detail 增明细;order_material_ledger 累计出库量增加(达需求量置「领料完结」)。 +### 验证点 +- 台账不存在被拒;台账「领料完结」后再出库被拒;批次可用量不足提示「批次库存不足」;批次非合格提示仅合格品可领料上线;缺批次号与序列号提示至少提供一个;盘点冻结批次禁止出库;扣减为原子条件更新不产生负库存。 +### 对不上的地方 +- 无。 + +## 场景 M1-状态:工单状态流转 +### 前置条件 +- 工单 WO20260920 已创建(状态「已创建」);产线上无其他「执行中/已暂停」的在产工单。 +### 操作步骤 +- 计划员在 MES「工单管理」页找到 WO20260920,点「下发」(可填变更原因);之后在列表对该工单依次点「开工」「暂停」「恢复」「完工」或「取消」。 +### 系统反应 +- 系统校验流转是否合法:已创建→已下发/已取消;已下发→执行中/已取消;执行中→已暂停/已完成;已暂停→执行中/已取消;已完成、已取消为终态不再流转。下发或开工前校验产线互斥:已有其他在产工单则拒绝。完工时写入完成时间。每次变更记操作日志(原状态/新状态/原因),并联动排产:完工或取消时把该工单未完成排产批量取消,开工时排产置执行中。 +### 数据变化 +- work_order.status 变为目标状态(已下发/执行中/已暂停/已完成/已取消);置「已完成」时 completed_at=当前时间;event_log 增一条 event_type=work.order.status(payload 含 from/to/reason);daily_plan 随工单状态联动。 +### 验证点 +- 非法流转(如 已创建→已完成)被拒并提示「不允许从「已创建」变更为「已完成」」;产线已有在产工单时下发第二张被拒;相同状态重复提交为幂等成功不报错。 +### 对不上的地方 +- 无。 + +## 场景 M3:产品物料清单(BOM 维护) +### 前置条件 +- 产品型号 CP001 已存在;准备 BOM 条目(M-A 结构件 单台用量2 工序1;M-B 电气件 单台用量4 工序2)。 +### 操作步骤 +- 计划员在 MES「产品物料清单」页选产品编码 CP001、填 BOM 名称(留空即「默认」),逐行录入物料编码/名称/规格/单位/单台用量/损耗率/装配工序/相关标准,点「保存」;也可点「导入」上传 Excel(列序:产品编号、清单名称、图号、物料名称、规格、单位、单台用量、损耗率、装配工序、相关标准);对多余条目点「移除」。 +### 系统反应 +- 保存为覆盖式写入,唯一键=产品编码+BOM名称+物料编码,存在则更新、不存在则新建;管理方式留空默认按序列号。导入按「产品编号+清单名称」分组逐组保存,跳过表头行,缺产品编号或图号的行忽略,全为空则报「未解析到有效物料清单行」。移除条目时若该型号已被任一工单选用则拒绝。 +### 数据变化 +- bom_item 新增/更新行:product_code=CP001、bom_name=默认、material_code、unit_qty、loss_rate、process_code、related_standard、manage_mode(默认2);移除则删该 bom_item 行;event_log 记 bom.save / bom.item.delete。 +### 验证点 +- 同一(型号+BOM名+物料)重复保存不产生重复行;导入返回 count=有效行数;已被工单选用的型号移除条目被拒。 +### 对不上的地方 +- 无。 + +## 场景 M5:数量不符处理 +### 前置条件 +- 工位已上报一条待处理数量不符记录(应发>实收为少发/差异为正;应发<实收为多发/差异为负)。 +### 操作步骤 +- 库管/计划员在 MES「数量不符处理」页按状态/工位/物料/工单查询,选中一条,选处理方式(补发/退库/调整)并填说明,点「处理」。 +### 系统反应 +- 仅「待处理/处理中」可处理,已处理拒绝重复。补发仅少发可用,生成来源=补料、需 AGV、关联差异单号的备料单,后续走自动出库→AGV→接料;退库仅多发可用,调 WMS 建退库单,WMS 确认收货后库存加回;调整任意方向、必须填说明、直接关闭不补不退。处理后把对应「数量不符」未读预警置已读并记日志。 +### 数据变化 +- material_qty_report.status 变为 已补发/已退库/已关闭,写 handle_type/handle_doc_no/handle_note/handled_by/handled_at;补发生成 material_request(source=REFILL,req_qty=差异量,status=PENDING);退库在 WMS 生成退库单;alert(type=qty_diff) 对应行 status=已读;event_log 记 qty_report.handle。 +### 验证点 +- 少发选退库被拒「本单为「少发」,不能走退库」;多发选补发被拒;调整不填说明被拒;已处理记录再处理被拒;处理后统计概览待处理数减一。 +### 对不上的地方 +- 无。 + +## 场景 M6:工位组合下发(PLC) +### 前置条件 +- 今日已配置工位派工(工位↔工序);工单 WO20260920 未完工未取消;工件 SN0001 已进线登记且未完工;无未完成的上一条下发或上一条已收到完成信号。 +### 操作步骤 +- 计划员在 MES「工位组合下发」页选工单号、填/扫工件 SN、选工位号,点「下发」。 +### 系统反应 +- 路线由今日工位派工派生(未配置则拒「今日未配置工位派工,无法下发 PLC 工序码」)。校验 SN 属于该工单且已进线、工件未完工、工单未完工/取消。若存在上一条状态=已下发的记录,先查完成信号,未收到则拒「上一条工序尚未收到PLC完成信号,不能下发下一条」,收到则把上一条置完成再下发。下发写日志并模拟下行,数秒后由系统模拟回传完成信号把本条置完成。 +### 数据变化 +- plc_send_log 新增行:order_no/sn/station_no/process_combination/process_codes/status=已下发(SENT)/send_time/operator;握手完成后 status=已完成(DONE)/done_time/ack=true;event_log 记 plc.send。 +### 验证点 +- SN 不属于工单被拒;工件已完工被拒;上一条未收到完成信号时下发下一条被拒;下发日志可按工单号/状态查询。 +### 对不上的地方 +- 完成信号为系统内模拟(下发后延时数秒自动置完成),非真实 PLC 握手;对接真实 PLC 时该握手以现场信号为准。 + +## 场景 M7:拧紧查询 +### 前置条件 +- 已有拧紧记录(含 SN/工单号/工位/螺丝编号/扭矩/角度/结果);对应工位绑定的工艺流程含「扭矩」考核标准与拧紧步骤。 +### 操作步骤 +- 查询员在 MES「拧紧查询」页按 SN/工单号/工位/日期区间查明细;切「按组汇总」看应拧vs实拧;对某条记录点「审核签字」填备注;点「修改留痕」看审计历史。 +### 系统反应 +- 明细四条件取交集(大小写不敏感),封顶500条。组汇总按 工单号+SN+工位 分组,应拧数=该工位工艺流程拧紧步骤数,满足判定=应拧>0且实拧>=应拧且无不合格;组内任一条未审核则整组未审核。审核签字仅未审核记录可审,重复审核被拒,写审核人/时间并记留痕。上报时扭矩超出「扭矩」范围考核标准判不合格,写步骤数据并触发拧紧不合格预警。 +### 数据变化 +- torque_record(strain/torque/angle/result/audit_by/audit_at);step_data(按范围判定 is_ok);torque_audit_log 新增 action=审核 行;不合格时 alert(type=torque_ng)。 +### 验证点 +- 组汇总 satisfied 与 应拧/实拧/不合格数一致;已审核记录再审被拒「该记录已审核,不可重复审核」;修改留痕按记录倒序可查。 +### 对不上的地方 +- 无。 + +## 场景 M10:绩效报表 +### 前置条件 +- 已有报工记录(含操作人/工位/工序/结果/作业时长/日期)。 +### 操作步骤 +- 管理员在 MES「绩效报表」页按操作人/工位/工单/日期区间查询,切「按人/按工位/明细」三口径查看;点「导出」下载 Excel(可指定起止日期)。 +### 系统反应 +- 以报工记录为源,按 操作人+工位+日期+工序 聚合完成数、合格数、不合格数、累计与平均作业时长、效率(件/小时)。三口径分别按人、按工位、明细汇总。导出把三口径写成 Excel 多表;日期区间非法时报错。 +### 数据变化 +- 只读统计,无写入(导出仅生成文件)。 +### 验证点 +- 完成数=报工记录条数;合格+不合格=完成数;按人/按工位汇总数与明细一致。 +### 对不上的地方 +- 无。 + +## 场景 M11:巡检终端 +### 前置条件 +- 登录用户持对应巡检按钮权限(签到/点检/过程/完工/异常各一权限);过程巡检需工单与工序步骤。 +### 操作步骤 +- 巡检员在 MES「巡检终端」页选记录类型(签到/点检/过程巡检/完工确认/异常),填工位/班次/工单/SN/结果/点检项/实测值/文字说明,可「上传照片」,点「提交」;对某工单点「一键生成工序间检验记录」。 +### 系统反应 +- 记录类型必填且须合法(签到/点检/过程/完工/异常,及质量检验三类)。按类型校验按钮权限(质量检验三类持质检录入权限放行)。结果留空默认合格。异常类型同时推看板报警。工序间检验记录汇总该工单全部过程巡检(按步骤维度),工单不存在则拒。 +### 数据变化 +- inspection_record 新增行:category/station_no/shift/order_no/sn/result/items/photo/remark/operator/process_code/step_id/step_name/measured_value;event_log 记 inspection.<类型>;异常时看板报警缓存失效。 +### 验证点 +- 非法类型被拒;无权限类型被拒(无操作权限);工序间检验记录条数=该工单过程巡检条数。 +### 对不上的地方 +- 无。 + +## 场景 M12:质量检验 +### 前置条件 +- 已有检验记录(过程检/完工检/来料检);BOM 中有所需物料。 +### 操作步骤 +- 质检员在 MES「质量检验」页按物料编码/名称/图号关键字检索 BOM 料并带出;登记检验结论;对不合格记录选处置类型(退货/返修/退换)填处置说明点「处置」;点「生成不合格单」下载预填 Excel。 +### 系统反应 +- 按关键字模糊检索 BOM 料(编码或名称,封顶200)。处置类型仅限 退货/返修/退换,非法被拒,写处置说明并记日志。不合格单按记录取数生成 Excel(报检单号/图号/物料名称/生产厂家/检验单号/结论/处置方式/处置说明/记录时间),缺记录标识被拒。 +### 数据变化 +- inspection_record.disposal_type、disposal_remark 更新;event_log 记 inspection.disposal;不合格单为导出文件不写库。 +### 验证点 +- 处置类型非 退货/返修/退换 被拒「处置类型仅支持 退货/返修/退换」;不合格单缺 id 被拒。 +### 对不上的地方 +- 无。 + +## 场景 M16:生产流程卡 +### 前置条件 +- 工件 SN0001 已上线并有报工/拧紧/关联物料数据。 +### 操作步骤 +- 计划员在 MES「生产流程卡」页填/扫 SN,点「打印前预检」看缺失项,点「打印」出卡;或打开「型号全景流程卡」按型号查看聚合视图。 +### 系统反应 +- 按 SN 联查工件→工单(产品名称/数量)、工序时间线(操作人/完成时间)、步骤采集参数、拧紧数据(该路径含拧紧步骤才要求)、关联物料批次/SN,计算缺失项(工单号/产品名称/数量/工序报工/拧紧数据等)。带预检参数时只返回缺失项不渲染卡片。SN 无追溯数据被拒。 +### 数据变化 +- 只读,无写入(渲染打印)。 +### 验证点 +- 预检返回的缺失项与实际数据缺口一致;无任何报工时缺失项含「工序报工记录」;需拧紧但无拧紧数据时缺失项含「拧紧数据」。 +### 对不上的地方 +- 无。 + +## 场景 M18:预警中心 +### 前置条件 +- 管理员登录 MES。 +### 操作步骤 +- 管理员在「预警中心」页新增预警规则(名称/类型/阈值/接收人/启用),可改可删;在预警列表按状态/类型/工单/时间查询,对某条点「标记已读」;现场或前端点「应急呼叫」填工位/工单/说明发起。 +### 系统反应 +- 规则名称与类型必填。预警列表支持分页与筛选。标记已读把该预警置已读。业务事件(如拧紧不合格、数量不符)按启用规则评估命中阈值则生成预警(库存偏低为小于等于阈值命中,其余为大于等于)。应急呼叫落一条类型=应急呼叫的未读预警,缺工位号被拒。 +### 数据变化 +- alert_rule 增/改/删;alert 新增(type/title/content/ref_type/ref_id/order_no/receiver/status=未读)或 status=已读;应急呼叫 alert.type=emergency_call、ref_type=station、ref_id=工位号;event_log 记 station.emergency。 +### 验证点 +- 规则名称/类型为空被拒;标记已读后未读数减一;应急呼叫缺工位号被拒「缺少工位号」。 +### 对不上的地方 +- 无。 + +## 场景 M8:工艺流程 +### 前置条件 +- 工位主数据已存在(工位1、工位2);准备工序步骤与考核标准。 +### 操作步骤 +- 工艺员在 MES「工艺流程」页新建流程,填流程名称、绑定工位(可多选)、上传工艺 PDF、设状态(启用/停用)、逐条录入工序步骤(序号/名称/采集方式/是否拧紧/是否需检测确认/备注/附件)与每步考核标准(名称/单位/判定逻辑/目标值/上下限),点「保存」;对已有流程可「启用/停用」「删除」。 +### 系统反应 +- 流程名称必填,状态仅启用/停用。绑定工位必须已存在(不自动创建),一个工位同时只绑一个流程,保存时所选工位整体改绑到本流程、原绑该流程但不在列表内的工位解绑。工序步骤与考核标准覆盖式重建(先删后建),考核逻辑为空或无的标准跳过。删除流程先解绑所有工位再删步骤与流程。 +### 数据变化 +- process_flow 增/改(name/pdf_file/status/remark);station.flow_id 指向该流程或置0;process_step 覆盖重建(flow_id/seq/name/collect_type/is_torque/need_check/remark/attachment);step_criterion 覆盖重建(step_id/name/unit/logic/target/min/max);event_log 记 process.flow.save / process.flow.delete。 +### 验证点 +- 流程名为空被拒;绑定不存在工位报「工位 X 不存在」;非法状态被拒;保存后该流程步骤数=提交步骤数;删除后工位 flow_id 归0。 +### 对不上的地方 +- 无。 + +## 场景 M13:在制品 +### 前置条件 +- 已有工件进线未完工(状态 已进线/在工位/返修)。 +### 操作步骤 +- 管理员在 MES「在制品」页查看线上在制工件,可按工位号与工单号过滤。 +### 系统反应 +- 列出停在各工位尚未完工的工件,按进线时间倒序。当前工位推算:已进线在上线位(0),在工位/返修按已完成工序推算,已到末端且在工位状态显示下线位(13)。展示产品编码/名称、当前工位名、状态、进线时间、停留时长、已完成工序数/总工序数(总工序由今日工位派工派生)。工位号>0只返回停在该工位的;工单号非空按模糊过滤。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 仅返回状态为 已进线/在工位/返修 的工件;完工工件不出现;已完成工序数=该SN状态为已完成的报工行数。 +### 对不上的地方 +- 无。 + +## 场景 M14:手动报工 +### 前置条件 +- 工单 WO20260920 存在;工件 SN0001 需手动上线/补报;对应工位绑定启用工艺流程(含考核标准,可能有需检测确认步骤与拧紧步骤)。 +### 操作步骤 +- 操作员在 MES「手动报工」页选工位带出工序步骤,扫/填工件 SN 点「上线」建档;按工序填采集数据(含检测确认勾选)、装机绑定物料,点「报工」;末道点「完工」填结构件批次/电气件SN。 +### 系统反应 +- 上线以 SN 建工件档案状态=已进线。报工强校验:仅「已进线/在工位」可报工,且必须按工序顺序(本工位=已完成工序+1),禁止跳站/漏站/重复;先应用装机绑定并校验本工序物料齐套(电气件按单台用量逐颗SN、结构件至少一个批次),缺料拒绝并返回缺料清单;标记需检测确认的步骤必须逐条勾选,否则拒绝;据考核标准判每步合格与否,任一步不合格则本工序结果=不合格。报工为整体事务,任一步失败全回滚。完工兜底校验全部工序物料齐套后置工件=已完工,生成/更新成品关联追溯,联动工单进度与日排产完成数,成品回流 WMS 库存并按整份 BOM 反冲扣料(WMS 不可达仅记日志不阻塞)。 +### 数据变化 +- workpiece 新增/更新(sn/order_no/status 已进线ONLINE→在工位PROCESSING→已完工DONE/current_process/online_at/done_at);workpiece_process 新增报工行(process_code/station_no/result/started_at/ended_at/duration_sec);step_data 逐步判定 is_ok;association_trace 成品关联(finished_sn/batch_items/serial_items);work_order 进度、daily_plan 完成数联动;event_log 记 workpiece.online/workpiece.process.report/workpiece.done。 +### 验证点 +- 跳站报工被拒「工序顺序不符」;非在制状态报工被拒;缺料报工被拒并返回缺料清单;需检测确认未勾选被拒;完工后 workpiece.status=DONE 且 association_trace 有该 SN 行。 +### 对不上的地方 +- 完工反冲的 WMS 侧只累加工单备料台账已耗量、不实际扣减库存也不生成出库单,与此处「按整份 BOM 反冲扣料、生成反冲出库单」的描述不一致(业务链路可跑通,仅反冲扣账口径与预期不符;同 S5 已标注问题的 MES 手动报工入口复现)。 + +## 场景 M17:物料档案(MES) +### 前置条件 +- WMS 可达(编码主数据单一来源=WMS 成品档案)。 +### 操作步骤 +- 计划员在 MES「物料档案」页按编码/名称/类别/启用状态分页查询产品型号;新增/修改填编码/名称/规格/单位/类别/启用;删除某型号;可导出 Excel。 +### 系统反应 +- 本页实时代理 WMS(成品档案),WMS 不可达时降级读本地只读缓存(前端以 wmsOnline 标识提示),写操作必须 WMS 可用。新增/修改名称、规格、单位必填,写请求转发 WMS。删除保护:被工单或 BOM 引用过的型号禁删(提示改用停用/下架),否则转发 WMS 删除。导出转发 WMS 生成 xlsx。 +### 数据变化 +- 主数据落在 WMS(materials item_type=3);MES 本地 product_types 仅作降级只读缓存;删除/新增/修改经 WMS 内部接口。 +### 验证点 +- 名称/规格/单位为空被拒;被工单引用的型号删除被拒「该物料档案已被 N 张工单引用」;WMS 不可达时列表降级(wmsOnline=false)且写操作被拒。 +### 对不上的地方 +- 无。 + +## 场景 M19:附件中心(MES) +### 前置条件 +- 已有各业务上传的附件。 +### 操作步骤 +- 管理员在 MES「附件中心」按文件类型/业务类型/业务号/文件名/日期/是否含已删除分页查询;查看磁盘状态;对某附件下载/预览、逻辑删除;点「归档」(可先试运行)把超期附件移到归档目录。 +### 系统反应 +- 上传时业务类型与业务号必填,流式落盘算 MD5,同业务对象同 MD5 秒传(删刚落盘文件返回既有记录)。中心返回分页列表+磁盘状态。下载按记录标识提供(支持断点续传、dl=1 强制下载、中文名 RFC5987)。删除为逻辑删除(库内标记,物理文件交归档任务)。归档把早于本地保留年数的附件移到归档目录并标记已归档(归档目录为空只标记不移动,试运行只返回将归档数量)。 +### 数据变化 +- attachment 增/改(biz_type/biz_id/file_type/file_name/file_path/file_md5/file_size/uploader/archived/deleted/deleted_by);event_log 记 attachment.upload/attachment.delete/attachment.archive。 +### 验证点 +- 业务类型或业务号为空上传被拒;同业务同 MD5 重复上传不新增行;逻辑删除后 deleted=true 且默认列表不含;归档试运行返回待归档数不移动文件。 +### 对不上的地方 +- 无。 + +## 场景 M20:操作日志(MES) +### 前置条件 +- 系统已有操作留痕;当前用户已登录。 +### 操作步骤 +- 用户在 MES「操作日志」页按工单号/事件类型/操作人/时间范围分页查询操作留痕。 +### 系统反应 +- 普通用户仅能查看自己的操作日志(用户名+中文名任一命中,忽略显式操作人筛选);管理员(SUPER_ADMIN)可查全部并按工单号/事件类型/操作人/时间范围筛选。返回事件类型/工单号/操作人(映射中文名,缺档显原用户名)/实体类型/实体标识/描述/时间。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 普通用户查询只返回自己名下日志;isAdmin 标识与当前用户角色一致;每条含非空创建时间与事件类型。 +### 对不上的地方 +- 无。 + +## 场景 M21:账号管理(MES) +### 前置条件 +- 管理员登录;已有角色。 +### 操作步骤 +- 管理员在 MES「账号管理」页查账号列表;新增账号填用户名/密码/姓名/角色/状态/是否可登录工位终端(及工位终端密码、允许操作工位);修改;删除。 +### 系统反应 +- 密码强度校验(至少6位且不能纯数字),bcrypt 存储;状态默认启用。可登录工位终端时设工位终端密码(留空默认复用登录密码,同样强度校验)。内置管理员账号(admin 或 SUPER_ADMIN 角色)不允许修改/删除。新增后按所选工位重建用户-工位关联。 +### 数据变化 +- user 增/改(username/password/name/role_id/status/can_login_workstation/workstation_password);user_station 关联重建。 +### 验证点 +- 纯数字或不足6位密码被拒;删除 admin 被拒「系统内置管理员账号不允许删除」;修改 admin 被拒。 +### 对不上的地方 +- 无。 + +## 场景 M22:角色管理(MES) +### 前置条件 +- 管理员登录;已有权限项。 +### 操作步骤 +- 管理员在 MES「角色管理」页查角色;新增角色填名称/编码/备注/权限勾选;修改;删除。 +### 系统反应 +- 新增写名称/编码/备注/权限码集合。修改时内置管理员角色(SUPER_ADMIN)不允许改权限,角色编码创建后不可修改。删除保护:内置角色(SUPER_ADMIN/OPERATOR/INSPECTOR)不可删;仍有关联用户的角色不可删。 +### 数据变化 +- role 增/改/删(name/code/remark/permission_codes)。 +### 验证点 +- 改内置管理员角色权限被拒;改角色编码被拒「角色编码创建后不可修改」;删内置角色被拒;删有用户的角色被拒「该角色下仍有用户」。 +### 对不上的地方 +- MES 新增角色未显式校验编码重复(直接落库),与 WMS 侧新增角色「编码已存在」的显式拦截不一致;若数据库无唯一约束则可能产生重复编码。 + +## 场景 M23:修改密码(MES) +### 前置条件 +- 当前用户已登录。 +### 操作步骤 +- 用户在 MES「修改密码」页填旧密码、新密码,点提交。 +### 系统反应 +- 未登录被拒;旧密码与新密码必填;新密码至少6位且不能纯数字;校验旧密码正确(bcrypt 比对),错误被拒;通过后更新为新密文。 +### 数据变化 +- user.password 更新为新 bcrypt 密文。 +### 验证点 +- 旧密码错误被拒「旧密码不正确」;新密码不足6位或纯数字被拒;改后新密码可登录、旧密码失效。 +### 对不上的地方 +- 无。 + +## 场景 S1:工位登录/登出 +### 前置条件 +- 账号已在产线服务开通工位终端登录权限、设置了工位终端密码、并分配了可操作工位;工位终端已配置固定工位号。 +### 操作步骤 +- 操作员在工位终端主界面输入用户名、工位终端密码(工位号由终端配置固定带入),点「登录」;下班点「登出」。 +### 系统反应 +- 终端把登录请求转给产线服务校验:账号处于启用、已开通工位终端登录权限、工位终端密码正确、且该账号拥有此工位的操作权限(未单独配工位权限时兜底授权全部物理工位,不含虚拟上下线位),通过后签发登录令牌并返回可操作工位清单。 +- 登出转产线服务补记一条「工位终端退出」操作日志;产线服务不可达时静默忽略,不阻塞本地退出。 +### 数据变化 +- 登录成功返回令牌、用户信息、可操作工位号列表;登出在产线服务 event_log 增一条工位退出日志。 +### 验证点 +- 密码错误、账号禁用、未开通工位登录权限、无此工位权限分别被拒并给中文提示;工位号由终端配置固定,终端不可修改。 +### 对不上的地方 +- 无。 + +## 场景 S2:当前任务 +### 前置条件 +- 工位已在产线服务配置并绑定生效(ACTIVE)的工艺流程;存在处于已创建/已下发/执行中的工单。 +### 操作步骤 +- 操作员登录后,终端主界面自动加载「当前任务」。 +### 系统反应 +- 终端按固定工位号向产线服务取本工位任务:返回工位名、绑定的工艺流程及其步骤清单、当前可生产的工单号列表(取状态为已创建/已下发/执行中的工单,最多10条)。本工位要做哪些步骤完全由绑定的流程图决定,工单只决定走哪些工位。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 工位不存在提示「工位不存在或未配置」;绑定流程非生效状态时步骤清单为空。 +### 对不上的地方 +- 无。 + +## 场景 S6:面板绑定 +### 前置条件 +- 工件已上线并关联工单;工单锁定的产品清单在本工序配置了装配物料。 +### 操作步骤 +- 操作员在工作面板扫工件序列号拉取「应装/已装」清单,逐件扫物料绑定(结构件扫批次号、电气件扫序列号);误绑可在报工前撤销。 +### 系统反应 +- 拉面板返回本工序应装物料(含需绑数量、已绑列表、齐套状态)。 +- 绑定时校验:物料属于该产品清单且装配工序为本工序;电气件序列号全局防重(已被别的工件绑定则拒);同工件同料同值重复扫码静默去重。撤销仅允许该工序未报工前,已报工不可撤销。 +### 数据变化 +- workpiece_bind 增绑定记录(工件序列号、工单号、工序号、工位号、物料编码/名称/规格/单位、管理方式、绑定值、绑定类型:结构件为批次、电气件为序列号、操作人);撤销删除对应记录;event_log 记装机绑定/撤销绑定。 +### 验证点 +- 料不在清单被拒;料配在别的工序被拒;电气件序列号重复被拒并提示已绑到哪个工件;已完工/报废工件不能绑;已报工工序撤销被拒。 +### 对不上的地方 +- 无。 + +## 场景 S7:移料/叫料 +### 前置条件 +- 产线点位台账已初始化(上料位、工位、缓存位、末端下料检测位、接驳台);移料时起点位当前占用为该工件、目标点位空闲;叫料时所选接驳台属于本工位。 +### 操作步骤 +- 操作员在移动面板:① 点位上料/下料记账(选点位、扫工件序列号、选上料或下料);② 下发移料指令(选起点位、目标点位、工件序列号);③ 工件到位确认(指令号);④ 叫料(选本工位接驳台、填叫料明细的物料与数量)。 +### 系统反应 +- 点位记账:上料把工件记到点位(点位被别的工件占用则拒、同工件幂等放行),下料清空点位(传工件时校验点位当前就是该工件)。 +- 移料:校验起点位存在且当前是该工件、目标点位存在且空闲、起终点不同,生成移料指令(状态已下发);到位时回写台账(起点清空、目标写入该工件)并闭环指令。 +- 叫料:校验工位号有效、所选接驳台属于本工位、有明细,逐条生成备料需求(来源为补发、需 AGV 配送、目标为所选接驳台、状态待处理、写入本工位号)。 +### 数据变化 +- line_point 更新在位工件与占用标记;plc_move_cmd 增移料指令(指令号、工件、工单、起点位、目标点位、状态:已下发→到位→闭环、操作人);material_request 增补料单(备料单号、工单号、计划日期为今日、物料、需求数量、状态待处理、工位号、来源补发、需 AGV、目标接驳台、操作人);event_log 记移料下发/到位、点位上料/下料、叫料。 +### 验证点 +- 起点位非该工件、目标点位被占用、起终点相同均被拒;接驳台不属于本工位被拒「该工位无此接驳台」;叫料明细物料编码或数量缺失的行被忽略,全部无效时被拒。 +### 对不上的地方 +- 无。 + +## 场景 S9:巡检 +### 前置条件 +- 操作员已登录工位终端;巡检记录类型合法(签到/点检/过程巡检/完工确认/异常等)。 +### 操作步骤 +- 操作员在巡检面板选记录类型、填工位号/工单号/工件序列号/班次/结果/检验项/实测值/备注,可拍照上传,点提交。 +### 系统反应 +- 拍照把图片传产线服务解码落盘到上传目录的日期子目录,返回系统文件名。提交巡检复用与巡检终端相同的落库逻辑:记录类型必填且合法,结果留空默认合格。工位终端无按钮级权限概念,仅按记录类型合法性校验。 +### 数据变化 +- inspection_record 增记录(记录类型、工位号、工单号、工件序列号、班次、结果、检验项、照片、备注、工序号、步骤、实测值、操作人);照片落盘到上传目录日期子目录。 +### 验证点 +- 记录类型为空提示「记录类型 category 必填」;非法类型被拒;照片数据为空提示「图片数据为空」、解码失败报错;提交成功返回「巡检记录已提交」。 +### 对不上的地方 +- 无。 + +## 场景 S10:数量不符上报 +### 前置条件 +- 操作员已登录;工位收到的物料数量与下发数量不一致。 +### 操作步骤 +- 操作员在物料面板填工位号/工单号/工件序列号/物料编码/物料名称/应发数/实收数/原因,点提交。 +### 系统反应 +- 校验工位号与物料编码必填;落数量不符单(差异=应发-实收,状态待处理),同时生成预警中心消息(类型数量不符、关联号为差异单业务号、状态未读)并推大屏。后续处理在产线服务侧(见场景 M7)。 +### 数据变化 +- material_qty_report 增记录(工位号、工单号、工件序列号、物料编码/名称、应发数、实收数、差异数、原因、状态待处理、操作人);alert 增预警(类型数量不符、标题「数量不符预警」、关联差异单业务号、状态未读);event_log 记数量不符上报。 +### 验证点 +- 缺工位号或物料编码被拒;差异数=应发数-实收数;上报后预警中心出现未读的数量不符预警。 +### 对不上的地方 +- 无。 + +## 场景 S11:退料 +### 前置条件 +- 操作员已登录;工位有余料需退回库房。 +### 操作步骤 +- 操作员在物料面板填工单号/工位号/工件序列号/物料编码/名称/规格/退库数量/单位/原因,点提交。 +### 系统反应 +- 校验工位号、物料编码、退库数量(大于0)必填;落一条事件日志,并调库房服务预建「待确认退库单」(库管确认收货后库存才加回);通知库房服务失败则报错。 +### 数据变化 +- MES event_log 记工位退料;库房侧 return_order 预建一条待确认退库单(含物料、数量、来源工位、原因)。 +### 验证点 +- 退库数量小于等于0被拒「退库数量必须大于 0」;物料编码为空被拒;提交成功返回「退库单已提交」;退库单在库房确认收货前库存不变。 +### 对不上的地方 +- 无。 + +## 场景 S12:应急呼叫 +### 前置条件 +- 操作员已登录工位终端。 +### 操作步骤 +- 操作员在主界面点「应急呼叫」,可填工单号、备注,确认发送。 +### 系统反应 +- 校验工位号;落预警中心消息(类型应急呼叫、标题「应急呼叫」、内容为「工位N发起应急呼叫(工单X):备注」、状态未读),推大屏,巡检终端顶部轮询提示。纯软件方案,不依赖安灯硬件。 +### 数据变化 +- alert 增记录(类型应急呼叫、关联类型工位、关联号为工位号、状态未读);event_log 记工位应急呼叫。 +### 验证点 +- 缺工位号被拒「缺少工位号」;呼叫成功返回「应急呼叫已发送」;预警中心出现未读的应急呼叫。 +### 对不上的地方 +- 无。 + +## 场景 S13:工艺文件查看 +### 前置条件 +- 工序对应的工艺文件(PDF)已上传到产线服务文件库。 +### 操作步骤 +- 操作员在主界面或工序步骤点「查看工艺文件」,终端内嵌或新窗口打开 PDF。 +### 系统反应 +- 内嵌/新窗口打开无法携带登录令牌,终端提供免鉴权直链,内部仍走产线服务文件接口(内部令牌保护)。启用本地预缓存后首次访问回源下载并落盘,后续直接读本地缓存(断网也能查看);文件流原样转发,并校验文件名防路径穿越(禁上级目录、反斜杠、绝对路径、非法字符,段数有上限)。 +### 数据变化 +- 本地 PDF 缓存目录首次回源时新增缓存文件;无数据库写入。 +### 验证点 +- 文件名为空提示「缺少文件名」;非法文件名提示「非法文件名」;命中缓存与回源分别标记;产线服务不可达提示「工艺文件获取失败」。 +### 对不上的地方 +- 无。 + +## 场景 S14:工作量查询 +### 前置条件 +- 操作员已登录;有历史报工记录(工件工序实绩)。 +### 操作步骤 +- 操作员在主界面点「我的工作量」,可选日期区间查询。 +### 系统反应 +- 终端按当前登录人(可带工位号、日期区间)向产线服务取工作量:以工件工序实绩为源,按 操作人+工位+日期+工序 聚合完成数/合格数/不合格数/累计作业时长/平均作业时长/效率(件每小时);工位号留空表示统计该操作人全部工位。 +### 数据变化 +- 只读,无写入。 +### 验证点 +- 聚合口径为工件工序实绩按操作人+工位+日期+工序;平均作业时长=累计时长/完成数;效率=完成数/(累计时长/3600);结果按操作人、工位排序。 +### 对不上的地方 +- 无。 + +## 场景 S15:离线同步 +### 前置条件 +- 工位终端本地库可用;产线服务临时不可达(断网)。 +### 操作步骤 +- 断网时操作员照常报工/暂存/上线/完工/拧紧,终端本地暂存;网络恢复后终端每15秒自动回传,或操作员手动点「立即同步」;可在主界面查看同步状态(待同步条数)。 +### 系统反应 +- 报工/暂存/上线/完工:先尽力冲刷一轮历史积压,再实时上报产线服务;产线服务不可达则写入本地队列(工序完成/暂存/上线建档/下线完工四类)并返回「已排队」稍后自动重传;产线服务业务拒绝(如装配未绑齐、电气件序列号重复)直接失败回前端,绝不入离线队。 +- 拧紧结果单独存本地表,同步时把接驳台编码换算成工位号后上报。 +- 后台每15秒冲刷一轮(先队列后拧紧),成功标记已同步、失败保持待同步并累计重试次数;手动同步立即执行一轮并返回本次成功条数;同步状态返回待同步拧紧数与待同步队列数。 +### 数据变化 +- 本地 report_queue(类型、载荷、是否已同步、重试次数、创建时间)、torque_results(拧紧结果、是否已同步)、event_log(本地操作留痕);同步成功后对应记录标记为已同步。 +### 验证点 +- 断网报工返回已排队且待同步队列数加1;恢复后自动或手动同步成功则标记已同步、待同步数归0;产线服务业务拒绝不入队直接失败;手动同步返回本轮成功条数。 +### 对不上的地方 +- 无。 + + # 文档3:回归测试 > 可断言用例,按场景分组。断言为具体数值/状态/条数。MES 接口前缀 /api/v1(需 JWT);WMS 接口前缀 /api(需 JWT);内部接口带 X-API-TOKEN。 @@ -495,6 +1340,785 @@ ### 清理步骤 - 无写入,无需清理。 +## 测试场景 W3:入库登记 +### 前置数据准备(SQL/API) +- WMS 物料档案:POST /api/material/create 建 M-A(manage_mode=1 结构件)、M-B(manage_mode=2 电气件)。 +- 先建一张合格来料检验单号 INSP-001(可先走 W9 合格检验,或直接 INSERT inspection_record(inspection_no='INSP-001',status='合格'))。 +### 执行步骤(API) +- POST /api/inbound/create,body:{"inboundType":"purchase","materialCode":"M-A","batchNo":"BA001","quantity":100,"zoneCode":"Z1","inspectionNo":"INSP-001","ownershipType":"workorder","ownershipNo":"WO20260920","operator":"张三"} +### 预期结果 +- inbound_order:新增一行 material_code='M-A'、batch_no='BA001'、quantity=100、inspection_no='INSP-001'、voided=false(inbound_no 由系统生成,从响应体取)。 +- inventories:新增 batch_no='BA001'、manage_mode=1、category=1、quantity=100、locked_qty=0、quality_status='未检'、status='在库'。 +- inbound_detail:新增 batch_no='BA001'、quantity=100。 +### 数量等式断言 +- SELECT quantity FROM inventories WHERE batch_no='BA001' → 100;SELECT sum(quantity) FROM inbound_detail WHERE inbound_no='<返回单号>' → 100;两者相等。 +- 可用量 = quantity − locked_qty = 100 − 0 = 100。 +### 清理步骤 +- DELETE FROM inbound_detail WHERE inbound_no='<返回单号>'; DELETE FROM inbound_order WHERE inbound_no='<返回单号>'; DELETE FROM inventories WHERE batch_no='BA001'; + +## 测试场景 W4:入库作废 +### 前置数据准备 +- 入库单 IBxxxx(M-A 批次 BA001 数量 100)已登记(W3),且该批次 locked_qty=0、quantity=100。 +### 执行步骤(API) +- POST /api/inbound/void,body:{"inboundNo":"","reason":"录入重复"} +### 预期结果 +- inbound_order(IBxxxx):voided=true、void_reason='录入重复'、voided_by 非空、voided_at>0。 +- inventories:batch_no='BA001' 的行被删除(quantity 100→0 后删行)。 +- 响应体 rolledQty=100。 +### 数量等式断言 +- SELECT count(*) FROM inventories WHERE batch_no='BA001' → 0。 +- 负用例:对已作废单再次 POST /api/inbound/void → 409(重复作废)。 +### 清理步骤 +- 随 W3 清理(本用例已删除 BA001 库存行);DELETE FROM inbound_order WHERE inbound_no=''。 + +## 测试场景 W6:通用出库 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status,zone_code) VALUES(1,1,'M-A','BA002',80,0,'合格','在库','Z1'); +### 执行步骤(API) +- POST /api/outbound/general,body:{"materialCode":"M-A","batchNo":"BA002","qty":30,"outboundCategory":"delivery","operator":"李四"} +### 预期结果 +- outbound_order:新增 outbound_type='general'、status='已出库'、batch_no='BA002'、outbound_category='delivery'、quantity=30。 +- outbound_detail:新增 batch_no='BA002'、quantity=30。 +- inventories:batch_no='BA002' quantity 80→50。 +### 数量等式断言 +- SELECT quantity FROM inventories WHERE batch_no='BA002' → 50(= 80 − 30)。 +- 负用例:对 quality_status='未检' 的批次以 delivery 出库 → 409(仅合格品);qty 大于可用量 → 409。 +### 清理步骤 +- DELETE FROM outbound_detail WHERE batch_no='BA002'; DELETE FROM outbound_order WHERE outbound_no='<返回单号>'; DELETE FROM inventories WHERE batch_no='BA002'; + +## 测试场景 W7:退库确认 +### 前置数据准备(SQL/API) +- 预建退库单:POST /api/internal/return-order(带 X-API-TOKEN),body:{"orderNo":"WO20260920","materialCode":"M-A","qty":20,"stationNo":1,"reason":"用剩退回","operator":"工位1"},取回 returnNo 记为 THxxxx。 +- 台账:INSERT INTO order_material_ledger(order_no,material_code,total_qty,out_qty,returned_qty,status) VALUES('WO20260920','M-A',20,20,0,'领料完结') ON CONFLICT 更新为同值。 +- 库存:INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status,zone_code) VALUES(1,1,'M-A','BA001',0,0,'未检','在库','Z1'); +### 执行步骤(API) +- POST /api/return-order/confirm,body:{"returnNo":"","returnType":"surplus","zoneCode":"Z1","batchNo":"BA001"} +### 预期结果 +- return_order(THxxxx):status='RECEIVED'、receive_by 非空、receive_at>0。 +- inventories:M-A 批次 BA001(未检、Z1)quantity 加回 20。 +- order_material_ledger(WO20260920,M-A):returned_qty=20、status='进行中'。 +### 数量等式断言 +- net_out = out_qty − returned_qty = 20 − 20 = 0,且 0 ≤ total_qty(20)。 +- SELECT quantity FROM inventories WHERE batch_no='BA001' AND quality_status='未检' → 含加回的 20。 +- 负用例:对已 RECEIVED 单再次 confirm → 409。 +### 清理步骤 +- DELETE FROM return_order WHERE return_no=''; DELETE FROM order_material_ledger WHERE order_no='WO20260920' AND material_code='M-A'; DELETE FROM inventories WHERE batch_no='BA001'; + +## 测试场景 W9:来料检验 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status,zone_code) VALUES(1,1,'M-A','BA003',100,0,'未检','在库','Z1'); +### 执行步骤(API) +- POST /api/inspection/create,body:{"inspectionType":"来料检验","targetId":"BA003","status":"不合格","inspectQty":100,"passQty":0,"inspector":"王五"}(取回记录 id 记为 :recId) +### 预期结果 +- inspection_record(:recId):inspection_type='来料检验'、status='不合格'、inspect_qty=100、pass_qty=0。 +- inventories:batch_no='BA003' quality_status 由'未检'置'不合格';quantity=100。 +### 数量等式断言 +- pass_qty ≤ inspect_qty(0 ≤ 100);SELECT quality_status FROM inventories WHERE batch_no='BA003' → '不合格'。 +- 负用例:status 传非合格/不合格值 → 400;targetId 无对应库存行 → 400。 +### 清理步骤 +- DELETE FROM inspection_record WHERE target_id='BA003'; DELETE FROM inventories WHERE batch_no='BA003'; + +## 测试场景 W9-处置:不合格品处置(退货) +### 前置数据准备(SQL) +- 已有 W9 产生的不合格来料检验记录 inspection_record(:recId)(status='不合格';批次 BA003 quality_status='不合格'、quantity=100)。 +### 执行步骤(API) +- POST /api/inspection/dispose,body:{"recordId":,"disposalType":"RETURN","returnTrackingNo":"SF123456","operator":"王五"} +### 预期结果 +- inspection_record(:recId):disposal_type='RETURN'、return_tracking_no='SF123456'。 +- inventories:batch_no='BA003' quality_status 由'不合格'置'待退';quantity 仍=100。 +### 数量等式断言 +- SELECT quality_status FROM inventories WHERE batch_no='BA003' → '待退';SELECT quantity FROM inventories WHERE batch_no='BA003' → 100(退货处置不减数量)。 +- 负用例:对 status='合格' 的记录调 dispose → 400(仅不合格可处置);disposalType='RETURN' 不填 returnTrackingNo → 400。 +### 清理步骤 +- DELETE FROM inspection_record WHERE target_id='BA003'; DELETE FROM inventories WHERE batch_no='BA003'; + +## 测试场景 W10:过程检验 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,sn_code,quantity,locked_qty,quality_status,status) VALUES(2,1,'M-B','SNB0001',1,0,'未检','在库'); +### 执行步骤(API) +- POST /api/inspection/create,body:{"inspectionType":"过程检验","targetId":"SNB0001","status":"合格","inspectQty":1,"passQty":1,"inspector":"王五"} +### 预期结果 +- inspection_record:新增 target_id='SNB0001'、inspection_type='过程检验'、status='合格'。 +- inventories:sn_code='SNB0001' quality_status 由'未检'置'合格'。 +### 数量等式断言 +- SELECT quality_status FROM inventories WHERE sn_code='SNB0001' → '合格';负用例:status 传非合格/不合格值 → 400;targetId 无对应库存行 → 400。 +### 清理步骤 +- DELETE FROM inspection_record WHERE target_id='SNB0001'; DELETE FROM inventories WHERE sn_code='SNB0001'; + +## 测试场景 W11:完工检验 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,sn_code,quantity,locked_qty,quality_status,status,order_no) VALUES(2,3,'CP001','SN0001',1,0,'未检','在库','WO20260920'); +### 执行步骤(API) +- POST /api/inspection/create,body:{"inspectionType":"完工检验","targetId":"SN0001","status":"合格","inspectQty":1,"passQty":1,"inspector":"王五"} +### 预期结果 +- inspection_record:新增 target_id='SN0001'、inspection_type='完工检验'、status='合格'。 +- inventories:sn_code='SN0001' 且 category=3 的 quality_status 由'未检'置'合格'。 +### 数量等式断言 +- SELECT quality_status FROM inventories WHERE sn_code='SN0001' AND category=3 → '合格';负用例:status 传非合格/不合格值 → 400;targetId 无对应库存行 → 400。 +### 清理步骤 +- DELETE FROM inspection_record WHERE target_id='SN0001'; DELETE FROM inventories WHERE sn_code='SN0001'; + +## 测试场景 W12:库存盘点 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status,zone_code,stocktake_no) VALUES(1,1,'M-A','BA004',50,0,'合格','在库','Z1',''); +### 执行步骤(API) +- POST /api/stocktake/start,body:{"operator":"张三","materialCodes":["M-A"]}(取回 stocktakeNo 记为 STxxxx) +- POST /api/stocktake/record,body:{"stocktakeNo":"","targetId":"BA004","scanQty":48} +- POST /api/stocktake/finish(需 stocktake:writeback 权限),body:{"stocktakeNo":"","adjust":true,"uncountedMode":"keep"} +### 预期结果 +- stocktake_order(STxxxx):start 后 status='进行中'、total_targets≥1;finish 后 status='已写回'、adjusted=true、adjust_no 非空、diff_count=1。 +- stocktake_item(BA004):book_qty=50、scanned_qty=48、diff_qty=-2、counted=true。 +- inventories(BA004):start 后 stocktake_no=STxxxx;finish 后 quantity=48、stocktake_no=''。 +### 数量等式断言 +- diff_qty = scanned_qty − book_qty = 48 − 50 = -2。 +- 库存量 = 可用量 + 锁定量(48 = 48 + 0);写回后 quantity(48) ≥ locked_qty(0)。 +- SELECT quantity,stocktake_no FROM inventories WHERE batch_no='BA004' → 48, ''。 +### 清理步骤 +- DELETE FROM stocktake_item WHERE stocktake_no=''; DELETE FROM stocktake_order WHERE stocktake_no=''; DELETE FROM inventories WHERE batch_no='BA004'; + +## 测试场景 W13:半成品入库/出库 +### 前置数据准备 +- 确保 SEMI0001 在 category=2 下无库存行。 +### 执行步骤(API) +- POST /api/semi/inbound,body:{"sn":"SEMI0001","materialCode":"M-A","category":2,"doneProcessCodes":[1,2],"zoneCode":"Z1","orderNo":"WO20260920"} +- POST /api/semi/outbound,body:{"sn":"SEMI0001","category":2,"reason":"继续生产"} +### 预期结果 +- inventories:新增 sn_code='SEMI0001'、manage_mode=2、category=2、quantity=1、quality_status='未检'、status 先'在库'、completed_process='1,2';出库后 status='出库'、outbound_reason='继续生产'。 +### 数量等式断言 +- SELECT quantity FROM inventories WHERE sn_code='SEMI0001' AND category=2 → 1(SN 单件恒为1)。 +- SELECT status FROM inventories WHERE sn_code='SEMI0001' AND category=2 → '出库'。 +- 负用例:重复入库同 SN → 409;reason 空出库 → 400;非在库出库 → 409。 +### 清理步骤 +- DELETE FROM inventories WHERE sn_code='SEMI0001' AND category=2; + +## 测试场景 W8:库存查询 +### 前置数据准备(SQL) +- INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status,zone_code) VALUES(1,1,'M-A','BA010',60,10,'合格','在库','Z1'); +### 执行步骤(API) +- GET /api/stock/query?materialCode=M-A +- GET /api/stock/material-summary?materialCode=M-A +- GET /api/stock/zone-summary?zoneCode=Z1 +### 预期结果 +- 返回体含 BA010 在库行;按物料汇总 M-A 在库 60、锁定 10、可用 50(仅统计在库/锁定)。 +### 数量等式断言 +- 可用 = 在库 − 锁定 = 60 − 10 = 50。 +- 负用例:将 quality_status 置'不合格'后,可用量汇总不计入该批次。 +### 清理步骤 +- DELETE FROM inventories WHERE batch_no='BA010'; + +## 测试场景 W14:工单备料台账 +### 前置数据准备(SQL) +- INSERT INTO order_material_ledger(order_no,material_code,total_qty,out_qty,returned_qty,consumed_qty,status) VALUES('WO20260920','M-A',20,20,0,0,'领料完结'); +- INSERT INTO inventories(manage_mode,category,material_code,batch_no,quantity,locked_qty,quality_status,status) VALUES(1,1,'M-A','BA011',5,0,'合格','在库'); +### 执行步骤(API) +- GET /api/ledger/query?orderNo=WO20260920 +### 预期结果 +- 返回 M-A 行:total_qty=20、out_qty=20、returned_qty=0、availableQty=5、shortQty=15、state='未齐套'(0<5<20)。 +### 数量等式断言 +- net_out = out_qty − returned_qty = 20 ≤ total_qty=20;short_qty = max(0, 20 − 5) = 15。 +### 清理步骤 +- DELETE FROM order_material_ledger WHERE order_no='WO20260920' AND material_code='M-A'; DELETE FROM inventories WHERE batch_no='BA011'; + +## 测试场景 W15:AGV 配送 +### 前置数据准备 +- MES 存在待配送备料单行(requestNo 记为 REQ001,targetDock=DOCK01);接驳台 DOCK21、DOCK01 已预置。 +### 执行步骤(API) +- POST /api/agv/submit,body:{"sourceDock":"DOCK21","rows":[{"requestNo":"REQ001","materialCode":"M-A","qty":20,"targetDock":"DOCK01"}]} +- POST /api/agv/refresh(模拟模式下拉取到位) +- POST /api/agv/cancel,body:{"taskNo":"<返回任务号>","reason":"测试取消"} +### 预期结果 +- agv_task:新增 task_no='AGVxxxx'、source_dock='DOCK21'、target_dock='DOCK01'、status 先'DISPATCHED';refresh 后'DONE'、arrived_at>0;cancel 后'CANCELLED'。 +- 回写:MES 备料单 REQ001 submit 后'DELIVERING'、到位后'DONE'、取消后'WAIT_SEND'。 +### 数量等式断言 +- SELECT count(*) FROM agv_task WHERE target_dock='DOCK01' → ≥1;响应体 created=1。 +### 清理步骤 +- DELETE FROM agv_task WHERE target_dock='DOCK01'; + +## 测试场景 W16:区域维护(批量生成) +### 前置数据准备(SQL) +- INSERT INTO zones(level,code,name,parent_code,zone_code,zone_name,status) VALUES(1,'Z1','区域1','','Z1','区域1','启用'); +- INSERT INTO zones(level,code,name,parent_code,zone_code,shelf_no,status) VALUES(2,'S1','货架1','Z1','Z1','S1','启用'); +### 执行步骤(API) +- POST /api/zone/batch-generate,body:{"zoneCode":"Z1","shelfNo":"S1","layerStart":1,"layerEnd":2,"posStart":1,"posEnd":3,"level":4,"preview":false} +### 预期结果 +- zones:新增 6 行 level=4、zone_code='Z1'、shelf_no='S1'、layer_no∈{1,2}、position_no∈{1,2,3}、status='启用';并确保 level=3 层节点 2 行。 +- 响应体 created=6、skipped=0、total=6。 +### 数量等式断言 +- SELECT count(*) FROM zones WHERE zone_code='Z1' AND shelf_no='S1' AND level=4 → 6;重复提交后 created=0、skipped=6。 +### 清理步骤 +- DELETE FROM zones WHERE zone_code='Z1'; + +## 测试场景 W17:物料档案维护 +### 前置数据准备 +- 确保 material 中无 code='M-C'。 +### 执行步骤(API) +- POST /api/material/create,body:{"code":"M-C","name":"结构件C","spec":"C-100","unit":"个","itemType":1,"manageMode":1,"safetyStock":10} +### 预期结果 +- material:新增 code='M-C'、name='结构件C'、spec='C-100'、unit='个'、item_type=1、manage_mode=1、is_batch_managed=true、is_serial_managed=false、safety_stock=10。 +### 数量等式断言 +- SELECT count(*) FROM material WHERE code='M-C' → 1;manage_mode=1 时 is_batch_managed=true 且 is_serial_managed=false。 +- 负用例:重复 code 新增 → 409;缺 name/spec/unit 任一 → 400。 +### 清理步骤 +- DELETE FROM material WHERE code='M-C'; + +## 测试场景 W18:接驳台维护(只读) +### 前置数据准备 +- dock 表已由种子预置(含 dock_type='line' 行)。 +### 执行步骤(API) +- GET /api/docks?dockType=line +### 预期结果 +- 返回 dock_type='line' 的接驳台列表,每行含 dock_code/name/station_no/status/has_pallet。 +### 数量等式断言 +- 返回条数 = SELECT count(*) FROM docks WHERE dock_type='line';无新增/修改/删除接口(只读)。 +### 清理步骤 +- 无写入,无需清理。 + +## 测试场景 W19:账号管理 +### 前置数据准备 +- 角色已存在(W20,取其 id 记为 :roleId);确保 user 中无 username='wms01'。 +### 执行步骤(API) +- POST /api/user/create,body:{"username":"wms01","password":"123456","realName":"库管一","roleId":,"dept":"库房"} +### 预期结果 +- user:新增 username='wms01'、password 为 bcrypt 密文(非'123456')、real_name='库管一'、role_id=、dept='库房'。 +### 数量等式断言 +- SELECT count(*) FROM users WHERE username='wms01' → 1;SELECT password FROM users WHERE username='wms01' ≠ '123456'。 +- 负用例:重复 username → 409;password 不足 6 位 → 400。 +### 清理步骤 +- DELETE FROM users WHERE username='wms01'; + +## 测试场景 W20:角色管理 +### 前置数据准备 +- 确保 role 中无 code='warehouse'。 +### 执行步骤(API) +- POST /api/roles,body:{"name":"库管","code":"warehouse","permissionCodes":["inbound:create","stocktake:start"]} +### 预期结果 +- role:新增 name='库管'、code='warehouse'、permission_codes=["inbound:create","stocktake:start"]。 +### 数量等式断言 +- SELECT count(*) FROM roles WHERE code='warehouse' → 1;permission_codes 数组长度=2。 +- 负用例:重复 code → 409;对 code='admin' 调 /api/roles/update 改权限 → 400。 +### 清理步骤 +- DELETE FROM roles WHERE code='warehouse'; + +## 测试场景 W21:操作日志(只读) +### 前置数据准备 +- 先执行一次写入操作(如 W17 新增物料)以产生日志。 +### 执行步骤(API) +- GET /api/event/logs?page=1&pageSize=20 +### 预期结果 +- 返回分页日志列表,含刚产生的 material.create 条目(操作人/时间/对象/描述)。 +### 数量等式断言 +- 返回 total ≥ 1;列表每条含非空 created_at 与 action。 +### 清理步骤 +- 无写入,无需清理(日志为审计留痕,不随业务数据回滚)。 + +## 测试场景 W22:附件上传 +### 前置数据准备 +- 准备一份小于上限的 PDF/图片文件;bizType='inbound'、bizId='<某入库单号>'。 +### 执行步骤(API) +- POST /api/attachments/upload(multipart/form-data),字段:bizType=inbound、bizId=<入库单号>、file=<文件> +- GET /api/attachments?bizType=inbound&bizId=<入库单号> +### 预期结果 +- attachment:新增 biz_type='inbound'、biz_id=<入库单号>、file_md5 非空、file_size>0、deleted=false。 +- 重复上传同一文件(同 MD5)不新增行,返回已有记录。 +### 数量等式断言 +- SELECT count(*) FROM attachments WHERE biz_id=<入库单号> AND file_md5=<该文件MD5> AND deleted=false → 1(传两次仍为1)。 +### 清理步骤 +- DELETE FROM attachments WHERE biz_id=<入库单号>(并清理落盘文件)。 + +## 测试场景 W23:修改密码 +### 前置数据准备 +- 用户 wms01 已存在(W19),密码 123456;以 wms01 登录取 token。 +### 执行步骤(API) +- POST /api/user/change-password(带 wms01 的 Authorization),body:{"oldPassword":"123456","newPassword":"654321"} +### 预期结果 +- user(wms01):password 更新为新密文;用 654321 可登录,123456 失效。 +### 数量等式断言 +- 负用例:oldPassword 错误 → 400(旧密码不正确);newPassword 不足 6 位 → 400。 +### 清理步骤 +- 随 W19 清理删除 wms01。 + +## 测试场景 W24:库存大屏(只读) +### 前置数据准备(SQL) +- 确保 inventories 有若干结构件在库行(如 BA010 quantity=60)。 +### 执行步骤(API) +- GET /api/display/overview(免登录) +### 预期结果 +- 返回体含 batchCount、totalQty、lockedQty、snInStock、semiInStock、finishedInStock、inToday、outToday、zoneDetail。 +### 数量等式断言 +- totalQty = SELECT sum(quantity) FROM inventories WHERE manage_mode=1;batchCount = SELECT count(*) FROM inventories WHERE manage_mode=1。 +- semiInStock = SELECT count(*) FROM inventories WHERE category=2 AND status='在库'。 +### 清理步骤 +- 无写入,无需清理。 + +## 测试场景 W1:登录(WMS) +### 前置数据准备(SQL) +- 确保用户 wms01:INSERT INTO "user"(username,password,real_name,role,is_active) VALUES('wms01', bcrypt('wms123456'), '库管员', 'OPERATOR', true) ON CONFLICT DO NOTHING。 +### 执行步骤(API) +- POST /api/auth/login body {"username":"wms01","password":"wms123456"} +### 预期结果 +- code=0;data.token 非空;data.expireAt>当前;data.user.username='wms01';data.user.permissionCodes 为数组;data.weakPassword=false。 +### 数量等式断言 +- 负用例:password 错 → 401「用户名或密码错误」;is_active=false → 403「账号已停用」;username 或 password 空 → 400「用户名和密码必填」;纯数字密码(如 '123456')登录成功但 weakPassword=true。 +- 登录后 user.last_login_at 被更新;event_log 新增1条 event_type='auth.login'。 +### 清理步骤 +- DELETE FROM "user" WHERE username='wms01'; DELETE FROM event_log WHERE event_type='auth.login' AND operator='wms01'; + +## 测试场景 W2:工作台(只读) +### 前置数据准备(SQL) +- 确保 inventories 有若干结构件在库行(如 BA010 quantity=60);至少一条物料可用量低于安全库存(触发缺货预警)。 +### 执行步骤(API) +- GET /api/display/overview(免登录);GET /api/stock/shortage(带 token) +### 预期结果 +- overview 返回体含 totalQty、lockedQty、snInStock、semiInStock、packageCount、batchCount、inToday、outToday;shortage 返回 data.list,每项含 materialCode、materialName、spec、safetyStock、availQty、shortQty。 +### 数量等式断言 +- totalQty = SELECT sum(quantity) FROM inventories WHERE manage_mode=1;shortage 仅列 availQtyavailable)→ 409「批次库存不足」;批次 quality_status非'合格' → 409「仅合格品可领料上线」;盘点冻结批次 → 被拦截。 +### 清理步骤 +- DELETE FROM outbound_detail WHERE outbound_no=:obNo; DELETE FROM outbound_order WHERE outbound_no=:obNo; UPDATE inventories SET quantity=30, locked_qty=0 WHERE batch_no='BA001'; + +## 测试场景 M1-状态:工单状态流转 +### 前置数据准备(SQL) +- INSERT INTO work_order(work_order_no,product_type_id,product_code,quantity,status) VALUES('WO20260920',:ptId,'CP001',10,'CREATED');(取回 id=:woId) +- 清场:UPDATE work_order SET status='DONE' WHERE status IN('IN_PROGRESS','PAUSED') AND work_order_no<>'WO20260920'; +### 执行步骤(API) +- POST /api/v1/work-orders/status body {"id":":woId","status":"RELEASED","reason":"下发"} +- POST /api/v1/work-orders/status body {"id":":woId","status":"IN_PROGRESS"} +- POST /api/v1/work-orders/status body {"id":":woId","status":"DONE"} +### 预期结果 +- work_order(:woId).status 依次 RELEASED→IN_PROGRESS→DONE;DONE 后 completed_at 非空。 +- 负用例:新建一张 CREATED 工单直接 POST status=DONE → 错误「不允许从「已创建」变更为「已完成」」。 +### 数量等式断言 +- SELECT status FROM work_order WHERE id=:woId → 'DONE'。 +- SELECT count(*) FROM event_log WHERE event_type='work.order.status' AND work_order_no='WO20260920' → >=3。 +### 清理步骤 +- DELETE FROM work_order WHERE work_order_no='WO20260920'; DELETE FROM event_log WHERE work_order_no='WO20260920'; + +## 测试场景 M3:产品物料清单(BOM 维护) +### 前置数据准备(SQL) +- 确保产品型号 CP001 存在;清场 DELETE FROM bom_item WHERE product_code='CP001'; +### 执行步骤(API) +- PUT /api/v1/bom body {"productCode":"CP001","bomName":"","items":[{"materialCode":"M-A","materialName":"结构件","unit":"件","unitQty":2,"processCode":1},{"materialCode":"M-B","materialName":"电气件","unit":"件","unitQty":4,"processCode":2}]} +- 重复 PUT 同一 body(验证 upsert 不重行) +### 预期结果 +- bom_item 中 CP001/默认 共 2 行;M-A unit_qty=2、process_code=1;M-B unit_qty=4、process_code=2;manage_mode 默认 '2'。 +- 重复保存后行数仍为 2(不新增)。 +### 数量等式断言 +- SELECT count(*) FROM bom_item WHERE product_code='CP001' AND bom_name='默认' → 2(传两次仍 2)。 +- 负用例:对已被工单选用的型号 POST /api/v1/bom/item/delete {"id":":bomItemId"} → 错误(已被工单选用)。 +### 清理步骤 +- DELETE FROM bom_item WHERE product_code='CP001'; + +## 测试场景 M5:数量不符处理 +### 前置数据准备(API) +- 工位上报少发:POST /api/internal/station/qty-report(X-API-TOKEN)body {"stationNo":1,"orderNo":"WO20260920","materialCode":"M-A","planQty":10,"actualQty":7,"cause":"少发"}(差异=3,取回记录 id=:qrId) +### 执行步骤(API) +- POST /api/v1/qty-reports/handle body {"id":":qrId","handleType":"REPLENISH","note":"补发"} +### 预期结果 +- material_qty_report(:qrId).status='REPLENISHED'、handle_type='REPLENISH'、handle_doc_no 非空、handled_at>0。 +- 新增 material_request:source='REFILL'、req_qty=3、status='PENDING'、order_no='WO20260920'。 +- alert(type='qty_diff',ref_id='QR:qrId') 由 UNREAD 置 READ。 +### 数量等式断言 +- 负用例:对少发单 handleType='RETURN' → 错误「本单为「少发」,不能走退库」;handleType='ADJUST' 不填 note → 错误「调整关闭必须填写处理说明」。 +- 幂等:已 REPLENISHED 后再 handle → 错误「该差异已处理」。 +- SELECT req_qty FROM material_request WHERE source='REFILL' AND order_no='WO20260920' → 3。 +### 清理步骤 +- DELETE FROM material_qty_report WHERE id=:qrId; DELETE FROM material_request WHERE source='REFILL' AND order_no='WO20260920'; DELETE FROM alert WHERE type='qty_diff' AND ref_id='QR:qrId'; + +## 测试场景 M6:工位组合下发(PLC) +### 前置数据准备(SQL/API) +- 今日已配工位派工;工单 WO20260920 status 非 DONE/CANCELLED;工件 SN0001 已进线(workpiece 存 sn='SN0001',order_no='WO20260920',status<>'DONE')。 +### 执行步骤(API) +- POST /api/v1/plc/send-process body {"orderNo":"WO20260920","sn":"SN0001","stationNo":1} +- GET /api/v1/plc/send-logs?orderNo=WO20260920 +### 预期结果 +- plc_send_log 新增一行:order_no='WO20260920'、sn='SN0001'、status='SENT'、send_time 非空、process_combination 非空。 +- 负用例:sn='NOT_EXIST' → 错误「该 SN 不属于当前工单或未进线登记」;今日无工位派工时 → 错误「今日未配置工位派工」。 +### 数量等式断言 +- SELECT count(*) FROM plc_send_log WHERE order_no='WO20260920' AND sn='SN0001' → >=1。 +- 等待数秒后该行 status 由 SENT 转 DONE、ack=true(模拟完成信号)。 +### 清理步骤 +- DELETE FROM plc_send_log WHERE order_no='WO20260920'; + +## 测试场景 M7:拧紧查询 +### 前置数据准备(API) +- 上报拧紧:POST /api/v1/torque/report body {"sn":"SN0001","workOrderNo":"WO20260920","stationNo":"1","screwNo":"S1","strain":12.5,"angle":90,"result":"OK"}(重复上报至应拧数,取回某行 id=:tqId) +### 执行步骤(API) +- GET /api/v1/torque/records?sn=SN0001&stationNo=1 +- GET /api/v1/torque/groups?sn=SN0001 +- POST /api/v1/torque/audit body {"recordId":":tqId","remark":"已核"} +### 预期结果 +- torque_record 写入 strain=torque=12.5、angle=90、result='OK'。 +- groups 返回该组 required=工位拧紧步骤数、actual=记录数、satisfied 与(required>0且actual>=required且ngCount=0)一致。 +- 审核后 torque_record(:tqId).audit_by 非空、audit_at 非空;torque_audit_log 新增 action='审核' 行。 +### 数量等式断言 +- 负用例:对已审核行再次 POST /torque/audit → 错误「该记录已审核,不可重复审核」。 +- SELECT count(*) FROM torque_audit_log WHERE torque_record_id=:tqId → 1。 +### 清理步骤 +- DELETE FROM torque_audit_log WHERE torque_record_id=:tqId; DELETE FROM torque_record WHERE sn='SN0001'; DELETE FROM step_data WHERE sn='SN0001'; + +## 测试场景 M10:绩效报表 +### 前置数据准备(SQL) +- 确保 workpiece_process 有若干行(operator='张三',station_no='1',result='OK'/'NG',duration_sec,created_at)。 +### 执行步骤(API) +- GET /api/v1/performance?operator=张三&from=2026-09-01&to=2026-09-30 +- GET /api/v1/performance/export?startDate=2026-09-01&endDate=2026-09-30 +### 预期结果 +- 返回 byOperator/byStation/detail 三口径;导出返回 xlsx 附件流。 +### 数量等式断言 +- 任一口径行:doneCount = okCount + ngCount。 +- byOperator 某操作人 doneCount = SELECT count(*) FROM workpiece_process WHERE operator='张三' AND created_at 在区间内。 +### 清理步骤 +- 无写入,无需清理。 + +## 测试场景 M11:巡检终端 +### 前置数据准备(SQL) +- 工单 WO20260920 存在;登录用户持 sys.inspect:process 权限。 +### 执行步骤(API) +- POST /api/v1/inspections body {"category":"PROCESS","stationNo":"1","orderNo":"WO20260920","sn":"SN0001","result":"OK","stepId":1,"stepName":"工序1","measuredValue":"10.2"} +- POST /api/v1/inspections/generate-inter-process body {"orderNo":"WO20260920"} +### 预期结果 +- inspection_record 新增:category='PROCESS'、order_no='WO20260920'、result='OK'、step_name='工序1'、measured_value='10.2'。 +- generate-inter-process 返回列表条数 = 该工单 category='PROCESS' 记录数。 +### 数量等式断言 +- 负用例:category='XXX' → 错误「记录类型不合法」;无权限用户提交 PROCESS → 403 无操作权限。 +- 返回条数 = SELECT count(*) FROM inspection_record WHERE category='PROCESS' AND order_no='WO20260920'。 +### 清理步骤 +- DELETE FROM inspection_record WHERE order_no='WO20260920' AND category='PROCESS'; + +## 测试场景 M12:质量检验 +### 前置数据准备(SQL) +- 已有一条检验记录 inspection_record(id=:insId,含 report_no/material_code/result='NG')。 +### 执行步骤(API) +- GET /api/v1/quality/bom-material?keyword=M-A +- POST /api/v1/quality/disposal body {"id":":insId","disposalType":"退货","disposalRemark":"来料不良"} +- GET /api/v1/quality/unqualified-report?id=:insId +### 预期结果 +- bom-material 返回 material_code 或 name 命中 M-A 的行(<=200)。 +- inspection_record(:insId).disposal_type='退货'、disposal_remark='来料不良'。 +- unqualified-report 返回 xlsx 附件流(表头含报检单号/图号/结论/处置方式)。 +### 数量等式断言 +- 负用例:disposalType='报废' → 错误「处置类型仅支持 退货/返修/退换」;unqualified-report 缺 id → 错误「缺少 id」。 +### 清理步骤 +- UPDATE inspection_record SET disposal_type='',disposal_remark='' WHERE id=:insId;(或随检验数据一并清理) + +## 测试场景 M16:生产流程卡 +### 前置数据准备(SQL) +- 工件 SN0001 已上线,workpiece_process 有报工行;(可选)torque_record 有拧紧行。 +### 执行步骤(API) +- GET /api/v1/process-card?sn=SN0001&check=1 +- GET /api/v1/process-card?sn=SN0001 +### 预期结果 +- check=1 返回 JSON {sn,missing:[...]};无 check 返回 text/html 流程卡。 +- SN 无追溯数据时返回错误「该SN无追溯数据」。 +### 数量等式断言 +- 无任何报工时 missing 含「工序报工记录」;路径需拧紧但无拧紧数据时 missing 含「拧紧数据」。 +### 清理步骤 +- 无写入,无需清理。 + +## 测试场景 M18:预警中心 +### 前置数据准备(SQL) +- 无(直接建规则)。 +### 执行步骤(API) +- POST /api/v1/alert/rule body {"name":"拧紧不合格","type":"torque_ng","threshold":1,"receiver":"班组长","enabled":true}(取回 id=:ruleId) +- POST /api/v1/alert/emergency body {"stationNo":1,"orderNo":"WO20260920","remark":"设备异常"} +- GET /api/v1/alerts?status=UNREAD(取回应急呼叫预警 id=:alertId) +- POST /api/v1/alert/read body {"id":":alertId"} +### 预期结果 +- alert_rule 新增一行 name='拧紧不合格'、type='torque_ng'、enabled=true。 +- alert 新增一行 type='emergency_call'、ref_type='station'、ref_id='1'、status='UNREAD'。 +- 标记已读后 alert(:alertId).status='READ'。 +### 数量等式断言 +- 负用例:POST /alert/rule {"name":"","type":""} → 错误「规则名称与类型必填」;POST /alert/emergency {"stationNo":0} → 错误「缺少工位号」。 +- SELECT count(*) FROM alert WHERE type='emergency_call' AND ref_id='1' AND status='UNREAD' → 标记已读前 1、后 0。 +### 清理步骤 +- DELETE FROM alert WHERE type='emergency_call' AND ref_id='1'; DELETE FROM alert_rule WHERE id=:ruleId; + +## 测试场景 M8:工艺流程 +### 前置数据准备(SQL) +- 确保工位存在:INSERT INTO station(station_no,name,status,is_builtin,has_dock) VALUES(1,'工位1','ENABLED',true,true) ON CONFLICT DO NOTHING; +### 执行步骤(API) +- POST /api/v1/process-flows body {"name":"测试流程","stations":[1],"status":"ACTIVE","steps":[{"seq":1,"name":"工序1","collectType":"MANUAL","isTorque":true,"criteria":[{"name":"扭矩","unit":"N·m","logic":"RANGE","min":10,"max":15}]}]} +- GET /api/v1/process-flows?stationNo=1(取回 flowId=:flowId) +### 预期结果 +- process_flow 新增 name='测试流程'、status='ACTIVE';station(station_no=1).flow_id=:flowId。 +- process_step 新增 1 行 flow_id=:flowId、is_torque=true;step_criterion 新增 1 行 logic='RANGE'、min=10、max=15。 +### 数量等式断言 +- SELECT count(*) FROM process_step WHERE flow_id=:flowId → 1(与提交步骤数一致)。 +- 负用例:POST /process-flows {"name":"","stations":[]} → 错误「流程名称必填」;stations:[9999](不存在)→ 错误「工位 9999 不存在」。 +### 清理步骤 +- DELETE FROM step_criterion WHERE step_id IN(SELECT id FROM process_step WHERE flow_id=:flowId); DELETE FROM process_step WHERE flow_id=:flowId; UPDATE station SET flow_id=0 WHERE station_no=1; DELETE FROM process_flow WHERE id=:flowId; + +## 测试场景 M13:在制品 +### 前置数据准备(SQL) +- INSERT INTO workpiece(sn,order_no,status,online_at) VALUES('SN0001','WO20260920','ONLINE',now()); +### 执行步骤(API) +- GET /api/v1/workpieces/in-process?orderNo=WO20260920 +### 预期结果 +- 返回列表含 sn='SN0001'、status='ONLINE'、currentStation=0、currentStationName='上线位(虚拟)'。 +### 数量等式断言 +- 返回条数 = SELECT count(*) FROM workpiece WHERE order_no='WO20260920' AND status IN('ONLINE','PROCESSING','REPAIR')。 +- 完工(status='DONE')后该 SN 不再出现在列表。 +### 清理步骤 +- DELETE FROM workpiece WHERE sn='SN0001'; + +## 测试场景 M14:手动报工 +### 前置数据准备(SQL/API) +- 工单 WO20260920 存在;工位1绑定启用流程(含工序1考核标准);清场 DELETE FROM workpiece WHERE sn='SN0001'; +### 执行步骤(API) +- POST /api/v1/workpiece/online body {"sn":"SN0001","orderNo":"WO20260920"} +- POST /api/v1/workpiece/process/report body {"sn":"SN0001","processCode":1,"stationNo":1,"steps":[],"binds":[本工序应绑物料]} +- POST /api/v1/workpiece/done body {"sn":"SN0001","batchItems":["BA001"],"serialItems":["SNB0001"]} +### 预期结果 +- 上线后 workpiece(sn='SN0001').status='ONLINE'、online_at 非空。 +- 报工后 workpiece_process 新增 process_code=1、result 根据考核判定;workpiece.status='PROCESSING'、current_process=1。 +- 完工后 workpiece.status='DONE'、done_at 非空;association_trace 新增 finished_sn='SN0001'。 +### 数量等式断言 +- 负用例:未完工序1直接报 processCode=2 → 错误「工序顺序不符」;缺料报工 → 错误含缺料清单。 +- SELECT status FROM workpiece WHERE sn='SN0001' → 'DONE';SELECT count(*) FROM association_trace WHERE finished_sn='SN0001' → 1。 +### 清理步骤 +- DELETE FROM association_trace WHERE finished_sn='SN0001'; DELETE FROM step_data WHERE sn='SN0001'; DELETE FROM workpiece_process WHERE sn='SN0001'; DELETE FROM workpiece WHERE sn='SN0001'; + +## 测试场景 M17:物料档案(MES) +### 前置数据准备 +- WMS 可达;管理员 token。 +### 执行步骤(API) +- POST /api/v1/product-types body {"code":"CP002","name":"测试产品2","spec":"S","unit":"台","category":"成品","isActive":true} +- GET /api/v1/product-types/page?code=CP002 +### 预期结果 +- WMS materials 新增 item_type=3、code='CP002' 行(经内部接口);分页查询返回该型号且 wmsOnline=true。 +### 数量等式断言 +- 负用例:POST /product-types {"name":"","spec":"","unit":""} → 错误「名称为必填项」;DELETE 被工单引用的型号 → 错误「该物料档案已被 N 张工单引用」。 +### 清理步骤 +- 经 WMS 删除 CP002(或 DELETE FROM materials WHERE code='CP002')。 + +## 测试场景 M19:附件中心(MES) +### 前置数据准备 +- 准备一份小于上限的文件;bizType='work_order'、bizId='WO20260920'。 +### 执行步骤(API) +- POST /api/v1/attachment/upload(multipart)字段 bizType=work_order、bizId=WO20260920、file=<文件> +- GET /api/v1/attachment/all?bizType=work_order&bizId=WO20260920 +- POST /api/v1/attachment/delete body {"id":":attId"} +### 预期结果 +- attachment 新增 biz_type='work_order'、biz_id='WO20260920'、file_md5 非空、deleted=false;中心返回含 disk 磁盘状态。 +- 逻辑删除后 attachment(:attId).deleted=true。 +### 数量等式断言 +- 同业务同 MD5 重复上传:SELECT count(*) FROM attachment WHERE biz_id='WO20260920' AND file_md5= AND deleted=false → 1(传两次仍1)。 +- 负用例:bizType 或 bizId 为空上传 → 错误「bizType 与 bizId 必填」。 +### 清理步骤 +- DELETE FROM attachment WHERE biz_id='WO20260920'(并清理落盘文件)。 + +## 测试场景 M20:操作日志(MES) +### 前置数据准备 +- 以普通用户(非 SUPER_ADMIN)登录取 token;该用户名下有至少一条 event_log。 +### 执行步骤(API) +- GET /api/v1/event-logs?page=1&pageSize=20(带普通用户 token) +### 预期结果 +- 返回 list/total/isAdmin;isAdmin=false 时 list 仅含当前用户(operator 为本人用户名或中文名)的日志。 +### 数量等式断言 +- 非管理员时:list 中每条 operator ∈ {当前用户 username, name};显式传 operator=他人 仍只返回本人日志。 +### 清理步骤 +- 无写入,无需清理。 + +## 测试场景 M21:账号管理(MES) +### 前置数据准备(SQL) +- 取一非内置角色 id=:roleId;清场 DELETE FROM user WHERE username='mes01'; +### 执行步骤(API) +- POST /api/v1/users body {"username":"mes01","password":"abc123","name":"测试员","roleId":":roleId","status":"ENABLED"} +### 预期结果 +- user 新增 username='mes01'、password 为 bcrypt 密文(非明文)、role_id=:roleId、status='ENABLED'。 +### 数量等式断言 +- 负用例:password='123456'(纯数字)→ 错误「密码不能是纯数字」;password='abc'(<6)→ 错误「密码长度至少 6 位」。 +- 内置保护:DELETE /api/v1/users/:adminId(admin)→ 错误「系统内置管理员账号不允许删除」。 +### 清理步骤 +- DELETE FROM user_station WHERE user_id=(SELECT id FROM user WHERE username='mes01'); DELETE FROM user WHERE username='mes01'; + +## 测试场景 M22:角色管理(MES) +### 前置数据准备(SQL) +- 清场 DELETE FROM role WHERE code='TEST_ROLE'; +### 执行步骤(API) +- POST /api/v1/roles body {"name":"测试角色","code":"TEST_ROLE","remark":"","permissionCodes":["workorder:view"]}(取回 id=:roleId) +### 预期结果 +- role 新增 name='测试角色'、code='TEST_ROLE'、permission_codes 含 'workorder:view'。 +### 数量等式断言 +- 负用例:PUT /api/v1/roles 改 SUPER_ADMIN 权限 → 错误「内置管理员角色不允许修改其权限」;改 code → 错误「角色编码创建后不可修改」。 +- DELETE 内置角色(SUPER_ADMIN/OPERATOR/INSPECTOR)→ 错误「内置角色不允许删除」;DELETE 有用户的角色 → 错误「该角色下仍有用户」。 +### 清理步骤 +- DELETE FROM role WHERE id=:roleId; + +## 测试场景 M23:修改密码(MES) +### 前置数据准备(SQL) +- 用户 mes01 已存在(M21),密码 abc123;以 mes01 登录取 token。 +### 执行步骤(API) +- POST /api/v1/user/change-password(带 mes01 的 Authorization)body {"oldPassword":"abc123","newPassword":"xyz789"} +### 预期结果 +- user(mes01).password 更新为新密文;用 xyz789 可登录,abc123 失效。 +### 数量等式断言 +- 负用例:oldPassword 错误 → 错误「旧密码不正确」;newPassword='123456'(纯数字)→ 错误「密码不能是纯数字」;newPassword 不足6位 → 错误「新密码长度至少 6 位」。 +### 清理步骤 +- 随 M21 清理删除 mes01。 + +> 工位终端(前缀 /api):登录免鉴权 POST /api/auth/login 取 token,其余 /api/* 带 Authorization: Bearer 。终端多为代理,断言落在 MES(PostgreSQL)与终端本地库(SQLite)两侧。 + +## 测试场景 S1:工位登录/登出(工位终端) +### 前置数据准备(SQL) +- MES 侧确保用户 ws01:INSERT/UPDATE "user" SET status='ENABLED', can_login_workstation=true, workstation_password=bcrypt('ws123456') WHERE username='ws01'(取回其 id 记为 :uid);user_station 授权工位1。 +### 执行步骤(API) +- POST /api/auth/login body {"username":"ws01","password":"ws123456","stationNo":1} +### 预期结果 +- code=0;data.accessToken 非空;data.user.username='ws01';data.stations 含 1;data.stationNo=1。 +### 数量等式断言 +- 负用例:password 错 → 「用户名或密码错误」;status='DISABLED' → 「账号已禁用」;can_login_workstation=false → 「该账号未开通工位终端登录权限」;stationNo=99(未授权)→ 「该账号无此工位的操作权限」。 +- POST /api/station/logout(带 token)body {"username":"ws01","stationNo":1} → code=0;MES event_log 新增1条工位退出日志。 +### 清理步骤 +- DELETE FROM user_station WHERE user_id=:uid; DELETE FROM "user" WHERE username='ws01'; + +## 测试场景 S2:当前任务(工位终端) +### 前置数据准备(SQL) +- 工位1存在且 station.flow_id 指向一个 status='ACTIVE' 的 process_flow;工单 WO20260920.status ∈ {CREATED,RELEASED,IN_PROGRESS}。 +### 执行步骤(API) +- GET /api/task/current?stationNo=1(带 ws01 token) +### 预期结果 +- code=0;data.stationNo=1;data.stationName 非空;data.flowActive=true;data.steps 为该流程步骤数组;data.orderNos 含 'WO20260920'。 +### 数量等式断言 +- GET /api/task/current?stationNo=999 → 错误「工位不存在或未配置」;stationNo 缺失 → 400「缺少 stationNo 参数」;工单全部为 DONE/CANCELLED 时 data.orderNos 长度=0;data.orderNos 长度≤10。 +### 清理步骤 +- 无(只读)。 + +## 测试场景 S6:面板绑定(工位终端) +### 前置数据准备(SQL) +- 工件 SN0001 已上线且关联 WO20260920(workpiece.work_order_id>0, status='ONLINE');bom_item(product_code='CP001', bom_name='默认') 工序1配 M-A(manage_mode='1', unit_qty=2)、工序2配 M-B(manage_mode='2', unit_qty=4)。 +### 执行步骤(API) +- GET /api/bind/panel?sn=SN0001&processCode=1(带 token) +- POST /api/bind/record body {"sn":"SN0001","orderNo":"WO20260920","processCode":1,"stationNo":"1","operator":"ws01","items":[{"materialCode":"M-A","bindValue":"BA001"}]} +### 预期结果 +- panel:data.enabled=true,required 含 M-A(need=2、manageMode='1');record:code=0,data.bound=1,data.panel.required[M-A].boundCount=1、complete=true(结构件至少绑1个批次即齐套)。 +### 数量等式断言 +- COUNT(workpiece_bind WHERE sn='SN0001' AND process_code=1 AND material_code='M-A')=1。 +- 电气件工序2:绑 M-B 四颗不同SN(SNB0001..SNB0004)后 panel M-B boundCount=4、complete=true;只绑3颗 complete=false、missing 含「需绑定4颗SN,已绑3颗」。 +- 负用例:绑 M-C(不在BOM) → 「物料 M-C 不在该产品的物料清单(BOM)中,不能绑定」;工序1绑 M-B(配在工序2) → 「物料 M-B 配置在工序2装配,本工位(工序1)不可绑定」;M-B SN 已被别的工件绑 → 「电气件SN xxx 已绑定到工件 yyy」;同工件同料同值重绑 → 静默去重 data.bound=0。 +- 撤销:POST /api/bind/remove body {"sn":"SN0001","processCode":1,"materialCode":"M-A","bindValue":"BA001","operator":"ws01"} → code=0,COUNT(workpiece_bind …M-A…)=0;该工序已报工后撤销 → 「该工序已报工,不能撤销装配绑定」。 +### 清理步骤 +- DELETE FROM workpiece_bind WHERE sn='SN0001'; + +## 测试场景 S7:移料/叫料(工位终端) +### 前置数据准备(SQL) +- 点位台账已初始化(line_point 含 point_no 'IN'/'1'/'2'/'1-R1' 等);UPDATE line_point SET occupied=true, current_sn='SN0001', order_no='WO20260920' WHERE point_no='1';UPDATE line_point SET occupied=false, current_sn='' WHERE point_no='2'。 +### 执行步骤(API) +- POST /api/line/occupy body {"pointNo":"IN","sn":"SN0001","orderNo":"WO20260920","action":"IN"} +- POST /api/line/move body {"sn":"SN0001","fromPoint":"1","toPoint":"2","orderNo":"WO20260920"}(记返回 data.cmdNo=:cmdNo) +- POST /api/line/move/arrive body {"cmdNo":":cmdNo"} +- POST /api/line/call-material body {"stationNo":1,"dock":"1-R1","orderNo":"WO20260920","items":[{"materialCode":"M-A","materialName":"结构件","unit":"件","manageMode":"1","qty":5}]} +### 预期结果 +- move:data.cmdNo 以 'MV' 开头、data.status='ISSUED';arrive 后 line_point '1' occupied=false current_sn=''、'2' occupied=true current_sn='SN0001';plc_move_cmd(:cmdNo).status='CLOSED';call-material:data=1(创建条数),material_request 新增1条 source='REFILL'、need_agv=true、target_dock='1-R1'、station_no=1、status='PENDING'、req_qty=5。 +### 数量等式断言 +- COUNT(material_request WHERE source='REFILL' AND station_no=1 AND target_dock='1-R1')=1。 +- 负用例:move fromPoint=toPoint → 「起点位与目标点位相同」;起点位非该工件 → 「起点位当前不是该工件,无法移出」;目标被占用 → 「目标点位被占用:X」;occupy 点位被别的工件占用 → 「点位 X 已被工件 Y 占用」;call-material dock='2-R1'(属工位2)→ 「该工位无此接驳台:2-R1」;items 空 → 「请添加叫料明细」;明细 materialCode 空或 qty<=0 被忽略,全无效 → 「没有有效的叫料明细(物料编码与数量必填)」。 +### 清理步骤 +- DELETE FROM plc_move_cmd WHERE cmd_no=:cmdNo; DELETE FROM material_request WHERE request_no=:reqNo; UPDATE line_point SET occupied=false, current_sn='', order_no='' WHERE point_no IN ('1','2','IN'); + +## 测试场景 S9:巡检(工位终端) +### 前置数据准备(SQL) +- ws01 已登录取 token。 +### 执行步骤(API) +- POST /api/inspection/upload body {"filename":"chk.png","data":""}(记 data.filename=:photo) +- POST /api/inspection/submit body {"category":"POINT","stationNo":1,"orderNo":"WO20260920","sn":"SN0001","shift":"白班","result":"OK","remark":"点检正常","photo":":photo","operator":"ws01"} +### 预期结果 +- upload:code=0,data.filename 形如 '日期/chk.png',data.url='/api/v1/files/日期/chk.png';submit:code=0,message='巡检记录已提交';inspection_record 新增1条 category='POINT'、station_no='1'、order_no='WO20260920'、result='OK'。 +### 数量等式断言 +- COUNT(inspection_record WHERE order_no='WO20260920' AND category='POINT')=1。 +- 负用例:category 空 → 400「记录类型 category 必填」;非法 category(如 'XXX')→ 被拒;upload data 空 → 「图片数据为空」;result 留空 → 默认 'OK'。 +### 清理步骤 +- DELETE FROM inspection_record WHERE order_no='WO20260920' AND category='POINT'; + +## 测试场景 S10:数量不符上报(工位终端) +### 前置数据准备(SQL) +- ws01 已登录;工位1收到 M-A 应发10实收8。 +### 执行步骤(API) +- POST /api/qty-report body {"stationNo":1,"orderNo":"WO20260920","sn":"SN0001","materialCode":"M-A","materialName":"结构件","planQty":10,"actualQty":8,"cause":"运输损耗","operator":"ws01"} +### 预期结果 +- code=0,message='数量不符已上报';material_qty_report 新增1条 plan_qty=10、actual_qty=8、diff_qty=2、status='PENDING';alert 新增1条 type='qty_diff'、ref_id='QR{新单id}'、status='UNREAD'。 +### 数量等式断言 +- diff_qty = plan_qty - actual_qty = 10 - 8 = 2;COUNT(alert WHERE type='qty_diff' AND ref_id='QR{id}')=1。 +- 负用例:materialCode 空 → 400「物料编码必填」;stationNo<=0 或 materialCode 空(MES侧)→ 「工位号与物料编码必填」。 +### 清理步骤 +- DELETE FROM material_qty_report WHERE id=:id; DELETE FROM alert WHERE type='qty_diff' AND ref_id='QR{id}'; + +## 测试场景 S11:退料(工位终端) +### 前置数据准备(SQL) +- ws01 已登录;工位1有 M-A 余料3件退库;WMS 服务可达。 +### 执行步骤(API) +- POST /api/return-material body {"orderNo":"WO20260920","stationNo":1,"sn":"SN0001","materialCode":"M-A","materialName":"结构件","spec":"S1","qty":3,"unit":"件","reason":"工序余料","operator":"ws01"} +### 预期结果 +- code=0,message='退库单已提交';MES event_log 新增1条 event_type='station.return_material';WMS return_order 预建1条待确认退库单(material_code='M-A'、qty=3、待确认状态)。 +### 数量等式断言 +- 负用例:qty<=0 → 400「退库数量必须大于 0」;materialCode 空 → 400「物料编码必填」;MES侧 stationNo<=0 或 qty<=0 或 materialCode 空 → 「工位号、物料编码、退库数量必填」;退库单确认收货前 WMS 库存不变(仅预建待确认)。 +### 清理步骤 +- WMS:删除预建退库单(return_order WHERE …);MES:DELETE FROM event_log WHERE event_type='station.return_material' AND entity_id='M-A'; + +## 测试场景 S12:应急呼叫(工位终端) +### 前置数据准备(SQL) +- ws01 已登录。 +### 执行步骤(API) +- POST /api/emergency/call body {"stationNo":1,"orderNo":"WO20260920","remark":"设备异响","operator":"ws01"} +### 预期结果 +- code=0,message='应急呼叫已发送';alert 新增1条 type='emergency_call'、title='应急呼叫'、ref_type='station'、ref_id='1'、status='UNREAD'、content 含「工位 1 发起应急呼叫(工单 WO20260920):设备异响」;event_log 新增1条 event_type='station.emergency'。 +### 数量等式断言 +- COUNT(alert WHERE type='emergency_call' AND ref_id='1' AND status='UNREAD')=1;负用例:stationNo<=0 → 「缺少工位号」。 +### 清理步骤 +- DELETE FROM alert WHERE type='emergency_call' AND ref_id='1'; + +## 测试场景 S13:工艺文件查看(工位终端) +### 前置数据准备(SQL) +- 工艺 PDF 已上传产线服务文件库(如 2026-09-16/xxx.pdf);ws01 已登录。 +### 执行步骤(API) +- GET /api/pdf?name=2026-09-16/xxx.pdf(带 token);或免鉴权 GET /pdfview?name=2026-09-16/xxx.pdf +### 预期结果 +- HTTP 200,Content-Type=application/pdf,响应头 X-Original-Filename 为文件名;首次 X-PDF-Cache=miss(回源),二次 X-PDF-Cache=hit(本地缓存)。 +### 数量等式断言 +- 负用例:name 空 → 400「缺少文件名」;name='../../etc/passwd' 或含反斜杠 → 400「非法文件名」;name 段数>8 → 「非法文件名」;产线服务不可达 → 502「工艺文件获取失败:…」。 +### 清理步骤 +- 删除本地 PDF 缓存文件(可选)。 + +## 测试场景 S14:工作量查询(工位终端) +### 前置数据准备(SQL) +- workpiece_process 插入 ws01 在工位1 于 2026-09-20 的报工实绩:工序1 完成5件(result OK 4条/NG 1条)、duration_sec 合计3600。 +### 执行步骤(API) +- GET /api/workload?operator=ws01&stationNo=1&from=2026-09-20&to=2026-09-20(带 token) +### 预期结果 +- code=0;聚合行(operator=ws01,stationNo=1,date=2026-09-20,processCode=1):doneCount=5、okCount=4、ngCount=1、totalDurationSec=3600、avgDurationSec=720、efficiency=5.0。 +### 数量等式断言 +- avgDurationSec = totalDurationSec/doneCount = 3600/5 = 720;efficiency = doneCount/(totalDurationSec/3600) = 5/(3600/3600) = 5.0;stationNo 空 = 统计该操作人全部工位;from/to 按 created_at 过滤(to 含当日)。 +### 清理步骤 +- DELETE FROM workpiece_process WHERE operator='ws01' AND order_no='WO20260920'; + +## 测试场景 S15:离线同步(工位终端) +### 前置数据准备(SQL) +- 工位终端本地库(SQLite)可用;模拟产线服务不可达(关停 MES 或断网)。 +### 执行步骤(API) +- 断网时 POST /api/report/process-done body {"sn":"SN0001","orderNo":"WO20260920","processCode":1,"stationNo":1,"operator":"ws01","steps":[]} +- GET /api/sync/stats +- 恢复产线服务后 POST /api/sync/flush +### 预期结果 +- 断网报工:code=0,data.queued=true;本地 report_queue 新增1条 kind='process_done'、synced=0;sync/stats:data.pendingQueue≥1。恢复后 sync/flush:code=0,data.count=本次成功条数(≥1),report_queue 该条 synced=1,sync/stats pendingQueue 归0。 +### 数量等式断言 +- MES 不可达 → report_queue 待同步数 +1、queued=true;MES 业务拒绝(HTTP200+code!=0,如装配未绑齐)→ HTTP409 直接失败、report_queue 不新增(不入队)。 +- 拧紧离线:torque_results synced=0 计数=pendingTorque;同步时 dock_code 'DOCK01' 换算 station_no='1' 上报 /api/internal/torque/report 成功后 synced=1。 +- flush 后全部成功时 pendingQueue=0 且 pendingTorque=0;失败条 retry_count +1 且 synced 保持0。 +### 清理步骤 +- 本地 SQLite:DELETE FROM report_queue WHERE kind='process_done'; DELETE FROM torque_results WHERE sn='SN0001'; +