侧边栏壁纸
  • 累计撰写 21 篇文章
  • 累计收到 29 条评论

ERP-WMS-MES P0 级稳定性与正确性补齐

2026-8-29 / 0 评论 / 134 阅读

ERP-WMS-MES P0 级稳定性与正确性补齐 - 产品需求文档(spec.md)

Overview

  • Summary: 落地 P0-1 至 P0-7 共 7 项高优先级修复与能力补齐:入库 FEFO 建议、MES 领料出库自动拣货、出库过期批次硬拦截、批次建议用户点选回填、2 处忽略错误加固、库存预留(Reservation)模型 + 销售占用、用户手动拆分回填不被自动拣货覆盖。

  • Purpose: 基于已落地的 S1 FIFO/FEFO 拣货能力,补齐其上下游流程中的"可用性"与"正确性"缺口——尤其是批次相关的最后一道防线(过期拦截、用户调整被覆盖)和业务流转断点(MES 领料一直是手选批次,库存预留在多订单并发场景下缺防线)。

  • Target Users: 仓库管理员(出入库/批次/盘点)、生产调度员(工单/领料)、销售订单管理员、系统运维/审计人员。

Goals

  • 出库时对任何expiry_date < today的批次段一律拦截,并给出中文错误,回滚事务。

  • MES 领料确认(ConfirmPickingOrder + 生成 WMS 出库单)自动走BuildPickingPlan,无需手选批次;成功则把拆段计划写回领料明细。

  • 入库 FEFO 优先复用已有可用批次(同产品同批号已存在则增量加库存并记录,而非默认创建新批号),并在入库单新增/编辑页给出"批号建议"按钮。

  • 桌面端批次建议对话框新增复选框,允许用户点选部分分段回填;回填成功后只对已勾选行写BatchID/LocationID/Remark,不影响未勾选行。

  • database.go:255、controller/wms/batch.go:528两处_ = err全部替换为可观测的日志(logger.Warnf)+ 上层错误返回(能返回则返回)。

  • 新增StockReservation表与一组接口:销售订单 Confirm/Unconfirm/Void 自动 reserve/release;BuildPickingPlan在分配库存时减去同产品已预留未出库数量。

  • 出库明细Extra字段新增 JSON 字段,序列化用户已确认的拆段计划;ApplyConfirmOutbound检测到 Extra 内有manual_picking_plan时,跳过自动拣货,直接按用户拆分执行扣减与校验,绝不重算。

Non-Goals

  • 不引入 P1/P2:如盘点差异自动生成出入库、调拨单 FIFO 拆段、MRP 计算、APS 排产、财务总账、多租户等均不在本次范围。

  • 不改数据库账号密码策略:config.yaml密码已清空,本 P0 不触碰环境变量体系。

  • 不改动鸿蒙/小程序 UI:批次建议用户勾选与入库 FEFO 建议只做桌面端;三端 UI 一致性留给后续 P1/P2。

  • 不改 SN 绑定:SN + 批次联动属于 P1-4,不做。

  • 不改打印模板:打印对齐问题属于 P2-7,不做。

Background & Context

  • 项目已具备完整 FIFO/FEFO 纯函数能力(buildPlanFromStockBatches、BuildPickingPlan、ApplyConfirmOutbound),并通过 6 个集成测试。

  • 桌面端/wms/batches/picking-suggestAPI + 批次建议可视化表格 + 一键回填已上线。

  • 已知错误忽略 2 处:controller/wms/batch.go:528 吞掉BuildPickingPlan的错误(目前接口是"suggest"语义,错误会导致 plan=nil 直接返回,用户看不到真实原因);database/database.go:255 吞掉初始化/迁移错误。

  • ConfirmOutboundOrder中只有"库存不足"的拦截,没有"选到的批次已过期但仍允许出库"的硬检查;若CheckExpiryTask因调度周期错过(刚入库就出库),过期品会流出。

  • 目前桌面"一键回填"是全量的,用户无法在 dialog 中勾选"只取 FEFO 第二段"做部分回填——这是业务常见需求(比如客户拒绝临期品,只想要更远期的批次)。

  • 销售订单 Confirm 时不预留库存,销售订单 A(100)+ B(100)都显示"有货",但实际库存只有 120;第二个出库 Confirm 才会报错。这是交付冲突的直接根因。

  • 用户在出库对话框里已经手动调整好 BatchID/LocationID 后,ConfirmOutboundOrder 会被自动拣货重新覆盖 plan,导致用户的调整丢失——这是之前 S1 落地时故意留的简化假设,现在必须修正。

Functional Requirements

FR-1 出库过期批次硬拦截

  • FR-1.1: 在ApplyConfirmOutbound每一个批次段分配前,比较批次expiry_date与time.Now()。

  • FR-1.2: 若expiry_date != NULL && expiry_date.Before(today)(含日粒度相等?不,相等即当天未过期,是<才拦截),必须返回错误并回滚事务,错误信息必须包含:批次号、到期日、数量。

  • FR-1.3: 拦截优先级高于"库存不足/负库存"检查,即先报错"批次已过期"。

  • FR-1.4: 如果系统设置AllowNegativeStock = 1,这条拦截仍然生效(过期品不允许出与是否负库存是两个维度)。

FR-2 MES 领料出库自动走 FIFO/FEFO

  • FR-2.1:ConfirmPickingOrder调用BuildPickingPlan(传入领料明细产品、库位、数量)为每一条领料明细生成 plan。

  • FR-2.2: 把 plan 的每段 qty/batch_id/location_id 写回PickingOrderItem:单段直写,多段首段 batch_id/location_id + 其余写到备注(与 S1 桌面回填约定一致)。

  • FR-2.3: 已有手工批次的领料明细,若 BatchID>0 则跳过自动拣货,不覆盖用户选择。

  • FR-2.4:GenerateOutboundFromPicking在生成 WMS 出库单时,把领料单已计算好的 plan 原样序列化到出库明细的 Extra,让ApplyConfirmOutbound直接按用户拆分路径执行。

  • FR-2.5: 当策略计算失败(不足/无批次),或RequireBatchTracking=1但无 stock_batch 行,ConfirmPickingOrder直接返回错误,不把数据写入。

FR-3 入库 FEFO 批次复用建议

  • FR-3.1: 新增/wms/batches/inbound-suggest接口(参数:warehouse_id, product_id, production_date),返回"已存在的同产品最早未过期批次";若没有,返回创建建议(新批次号 = 自动编码规则生成)。

  • FR-3.2: 桌面端入库单新增/编辑页工具栏增加"批号建议"按钮,点击调用 inbound-suggest,用户确认后把 BatchID/BatchNo 回填到选中行。

  • FR-3.3: 回填后如果选的是已有批次,在ConfirmInboundOrder中按(warehouse_id, location_id, product_id, batch_id)做 upsert,已有的 StockBatch 数量 += 入库数量,不创建新 StockBatch。

FR-4 批次建议对话框:用户点选部分段回填

  • FR-4.1: 在 desktopshowPickingSuggestDialog的"建议拆分明细"表里增加第一列:复选框(默认全勾选)。

  • FR-4.2: 新增两个按钮:「全选」「反选」。

  • FR-4.3: 修改"一键回填"逻辑(重命名为"按选中回填"),只对勾选的 seg 写入 BatchID/LocationID/Remark 到出库行;未勾选段保持用户之前的手工设置不变。

  • FR-4.4: 如果勾选的段合计 qty < 出库需求,回填完成时弹警告(例如"您勾选的分段合计 4.00,还缺 3.00 未分配")但不阻断。

FR-5 两处_ = err替换为可观测错误

  • FR-5.1: database.go:255:改为logger.Warnf("db setup step warning: %v", err),且如果 err 会导致后续 DB 不可用(例如 InitDB 迁移失败)则必须直接 panic 返回给启动流程,不能吞掉。

  • FR-5.2: batch.go:528(GetPickingSuggest场景吞掉 BuildPickingPlan error):改为util.Error返回,把错误信息写入响应的msg字段;此时 plan 字段可置空,但必须让客户端看到错误原因而非空列表。

FR-6 库存预留模型 + 销售订单占用

  • FR-6.1: 新增模型StockReservation:ID, WarehouseID, ProductID, BatchID, LocationID, SourceType(sales_order/picking_order/other), SourceID, Quantity, ReservedByAdminUserID, ExpiresAt, CreatedAt;唯一键(source_type, source_id, product_id, batch_id)或单条 source/product 的更细粒度。

  • FR-6.2:system_setting增加EnableStockReservation枚举开关(0=off, 1=on,默认 on)。

  • FR-6.3: 销售订单Confirm时,按明细 (warehouse, product, qty) 调用 BuildPickingPlan 生成预留 plan,并把每段写入 StockReservation;Unconfirm/Void按 source_id 做 delete。

  • FR-6.4:BuildPickingPlan内部对每段 stock_batch.qty 减去同 (product, batch, location) 的已预留未过期数量(SUM(quantity) WHERE expiry_at > NOW()),之后才做分配。

  • FR-6.5:CheckExpiryTask或新增定时任务清理过期 StockReservation(ExpiresAt 超过即 delete)。

  • FR-6.6: ApplyConfirmOutbound 确认出库后,按明细的 source(销售订单)把对应 StockReservation 标记为"已消耗"或直接删除(优先按 batch/location 精确匹配)。

FR-7 用户手动拆分回填(OutboundOrderItem.Extra 手动计划)不被自动拣货覆盖

  • FR-7.1: 扩展 OutboundOrderItem 的 Extra JSON 字段,新增 schema:{"manual_picking_plan":[{"batch_id":uint,"location_id":uint,"quantity":float64,"batch_no":"string","expiry_date":"YYYY-MM-DD"}]};字段名约定为manual_picking_plan_v1。

  • FR-7.2: ApplyConfirmOutbound 先检查 item.Extra 反序列化后是否含 non-empty manual_picking_plan_v1:若有,跳过 BuildPickingPlan 调用,直接按 manual plan 逐段分配与校验(库存充足性、批次过期拦截仍生效)。

  • FR-7.3: 成功后把 manual plan 写入 item.Remark 的前缀[已按手动拆分执行],保留溯源。

  • FR-7.4: MESGenerateOutboundFromPicking(FR-2.4)也要写入 manual_picking_plan_v1,保证领料计划不被出库 Confirm 重新分配。

  • FR-7.5: 桌面端"按选中回填"(FR-4)回填时,把勾选的分段写入 OutboundOrderItem.Extra 对应的 manual_picking_plan_v1;用户手工修改 BatchID 或 LocationID 后,若 Extra 已含 manual plan,要把行级的 batch/location 变化同步更新 Extra(即"用户直接改文本列"也生效,不用强制走 dialog 勾选路径)。

Non-Functional Requirements

  • NFR-1 (正确性): 每一项 FR 都至少有一个单测(纯函数 level)或集成测试(数据库 level)覆盖关键路径;失败场景必须有负面测试。

  • NFR-2 (可观测): 所有新增分支有logger.Infof/Warnf级日志;拦截场景日志要包含批次号/订单号/数量/原因。

  • NFR-3 (性能): BuildPickingPlan 减去 reserve 时使用一条聚合 SQL 而不是逐段查询(避免 N+1)。

  • NFR-4 (兼容性): 不破坏任何现有 API 字段;新增字段可选,不影响旧桌面客户端(旧客户端不传 Extra 的 manual plan,仍走自动拣货旧路径)。

  • NFR-5 (防御性): 关键风控闸门(过期拦截、reserve 双重扣减一致性、manual plan sum 校验)使用"保守失败默认值"——信息不足时一律视为失败并抛出清晰错误,不做静默兜底。

  • NFR-6 (构建):go build ./...全程 0 错误。

  • NFR-7 (测试): 数据库测试跑DB_PASSWORD=root go test ./controller/wms/ -run TestApplyConfirm -v必须 6/6 PASS;新增集成测试用例数量 >= FR 数量。

Constraints

  • Technical:

    • 继续使用现有技术栈:Go + Gin + GORM(MySQL)+ walk(桌面端)。

    • 新增表和字段必须在database/models.go中定义;迁移/初始化走database/database.go的 AutoMigrate。

    • 所有 GORM 调用必须使用参数化 Where/Update,禁止拼接 SQL。

    • 枚举 / 系统设置必须继续通过util/system_setting.go的现有机制(添加 enum 常量 + Getter + 缓存 override helpers)。

  • Business:

    • 中文错误信息,使用与现有风格一致("批次 XXX 的库存不足…")。

    • 保留现有"策略名"命名:FIFO / FEFO / 手工 / Auto。

  • Dependencies:

    • 依赖已有BuildPickingPlan与ApplyConfirmOutbound纯事务函数;不对它们的签名做大改(如果一定需要,追加参数而非更改)。

    • 依赖桌面端现有TableModel/CheckBoxColumn机制。

Assumptions

  • 生产环境 MySQL 版本至少 5.7 以上,支持 JSON 字段和事务隔离级别 READ COMMITTED(项目已依赖)。

  • StockReservation 在启用后可能占用少量内存/磁盘;现有库量级下(单客户百万级 stock_batch)完全可接受。

  • 用户接受"用户手动拆分确认后,系统不再重算"的业务语义(这是 FR-7 的假设,否则我们还需提供"重置自动拣货"按钮)。

  • 桌面端 walk TableView 能支持 CheckBox 列(现有项目已有inventory_check.go等使用 CheckBox 列,可行)。

Open Questions

  • OQ-1(P0-3 过期拦截粒度):expiry_date = today算不算过期?默认"不算(当天仍可用)"。若你需要"到期当日即拦截",请指定。

  • OQ-2(P0-6 StockReservation ExpiresAt):销售订单默认多长时间不确认出库即自动释放预留?默认 7 天 + 可在 SystemSetting 中调。

  • OQ-3(P0-6 订单拆分后释放):销售订单部分发货后,剩余预留怎么处理?默认"按已确认出库的 qty 按比例释放"。

  • OQ-4(P0-7 手动拆分 UI 入口):是否需要"重置自动拣货"按钮把 manual plan 清空?默认要加(在批次建议 dialog 里放一个"重置"按钮)。

  • OQ-5(并发一致性):StockReservation 写入是否要求加分布式 Redis 锁?默认"仅用 MySQL 事务 + 唯一键保证幂等(同 source 重复 Confirm 不重复 reserve)"即可,暂不引入 Redis 锁。

以上问题若你有不同意见,在审批 spec 时一并指出即可,默认按我的假设落地。


Acceptance Criteria

AC-1: 出库过期批次段拦截(rule)

  • Type:rule

  • Given: DB 中存在产品 P 的两个批次:B1(qty=5,expiry=2024-01-01 已过期)、B2(qty=10,expiry=2099-12-31),stock 总 qty=15;系统设置 FEFO,出库需求 7

  • When: 调用ApplyConfirmOutbound,且 FEFO 排序把 B1 排第一(它更早到期但已过期)

  • Then: 函数返回 error,error message 中包含 B1 的 batch_no 关键字 + "已过期";事务不提交,StockBatch 表数据不变(B1 仍 5,B2 仍 10)

  • Pass Condition: 集成测试TestApplyConfirm_FEFO_ExpiredBatch_Blocked返回 PASS;sql 查询SELECT * FROM stock_batches显示 B1/B2 数量未变化

  • Evidence:DB_PASSWORD=root go test ./controller/wms/ -run TestApplyConfirm_FEFO_ExpiredBatch_Blocked -v输出 + SQL 后查

AC-2: 非过期 FEFO 正确出 B2,不触发拦截(rule)

  • Type:rule

  • Given: 同上 fixture,把 B1 过期改为 expiry=2025-01-01(比 B2 早但未过期),FEFO 应出 B1=5 + B2=2

  • When: 调用 ApplyConfirmOutbound(qty=7)

  • Then: 不抛错,B1 变 0、B2 变 8;order.Status=1

  • Pass Condition: 集成测试TestApplyConfirm_FEFO_NotExpired_Splits通过且数量断言正确

  • Evidence: go test output

AC-3: AllowNegativeStock=1 时过期批次仍被拦截(rule)

  • Type:rule

  • Given: 只有 B1(已过期 qty=1),stock qty=1,AllowNegativeStock=1,需求 2

  • When: ApplyConfirmOutbound

  • Then: 返回 error(含"已过期"),不进入负库存分支

  • Pass Condition:TestApplyConfirm_AllowNegStock_ExpiryStillBlocked通过

  • Evidence: go test output

AC-4: MES ConfirmPickingOrder 自动拣货,结果写回领料明细(rule)

  • Type:rule

  • Given: 产品 P 有 B1(qty=5)/B2(qty=10),系统设置 FEFO;创建一张领料单 PickingOrder,1 行,product=P, qty=7,BatchID=0

  • When: POST/mes/picking-orders/:id/confirm

  • Response & After: 返回 200 success;PickingOrderItem.BatchID == B1;PickingOrderItem.Remark 含 "B2×2" 多段摘要(FEFO 顺序 B1=5 + B2=2);数据库 StockBatch B1=0, B2=8(或仅 PickingOrder 本身被更新,取决实现:若确认领料只更新内部领料单状态、库存扣减在出库单,则断言 PickingOrderItem 的 BatchID/Remark)

  • Pass Condition:TestMESConfirmPicking_AutoFEFO_WriteBackItem集成测试通过

  • Evidence: go test -run TestMES + 代码路径 review

AC-5: MES GenerateOutboundFromPicking 写入 manual_picking_plan_v1,出库不再重算(rule)

  • Type:rule

  • Given: AC-4 已生成 PickingOrder,其 Item.Remark 里有 FEFO 拆段

  • When: 调用GenerateOutboundFromPicking→ 生成 OutboundOrder 及 Item;再调用 ApplyConfirmOutbound

  • Then: 扣减结果与 FEFO 计划完全一致(B1 0 / B2 8),且 OutboundOrderItem.Extra JSON 中含键manual_picking_plan_v1,长度=2 段

  • Pass Condition:TestMES_PickingToOutbound_ManualPlan_NoRebuild通过

  • Evidence: go test output

AC-6: 手工已填批次的领料单,自动拣货跳过(rule)

  • Type:rule

  • Given: B1(qty=5)/B2(qty=10),领料单 qty=7 且手工设 BatchID=B2, LocationID=X

  • When: ConfirmPickingOrder + 系统 FEFO 设置

  • Then: 最终仍按 B2 扣(B2 → 3),B1 保持 5,不重算

  • Pass Condition:TestMES_ManualBatch_NotOverridden通过

  • Evidence: go test output

AC-7: InboundSuggest 返回已有最早批次并能回填(rule)

  • Type:rule

  • Given: 入库单新增产品 P,warehouse=W,已存在 B-old(stock 10) / B-new(stock 3),B-old.created_at < B-new.created_at

  • When: 调/wms/batches/inbound-suggest?warehouse_id=W&product_id=P

  • Response: HTTP 200,batch_id == B-old.ID,reuse == true,reason == "FEFO 复用已有未过期最早批次"

  • Pass Condition: 单元测试TestInboundSuggest_ReuseOldest通过

  • Evidence: go test -run TestInboundSuggest

AC-8: ConfirmInboundOrder 对复用批次做 StockBatch upsert,不新建(rule)

  • Type:rule

  • Given: B-old 已有 stock_batch.qty=10,入库单填了 B-old,入库 qty=5

  • When: ConfirmInboundOrder

  • Then:SELECT COUNT(*) FROM stock_batches WHERE batch_id = B-old== 1,qty == 15;stock_batches 没有新增 row

  • Pass Condition:TestConfirmInbound_ReuseBatch_UpsertQty通过

  • Evidence: go test output

AC-9: 桌面批次建议 dialog 支持勾选回填(rubric:可用性)

  • Type:rubric

  • Dimension: 用户交互体验——能否只勾选部分段做精确回填

  • Scale: 1-5

  • Anchors: 1=无勾选,只能全量;3=勾选存在但只支持单段;5=复选框列(默认全勾选)+ 全选/反选 + 勾选合计<需求时弹警告 + 未勾选行不受影响

  • Pass Threshold: >= 4

  • Evidence: 桌面端构建go build ./desktop/...通过 + dialog UI 代码中可见 CheckBoxColumn + Selection 计数逻辑 + warning dialog 分支

AC-10: BuildPickingPlan error 不吞,返回 500 带 msg(rule)

  • Type:rule

  • Given: 调用/wms/batches/picking-suggest时构造"无 product_id 参数"的错误请求(或 BuildPickingPlan 内部因参数越界返回 err)

  • When: handler 执行到BuildPickingPlanerror 分支

  • Then: HTTP 响应 code == 500/400(按语义),body JSON 中msg字段非空且包含真实错误 reason;logger.Warnf 有对应日志

  • Pass Condition: 单测TestGetPickingSuggest_PropagateBuildErr通过

  • Evidence: go test output + handler 代码 review

AC-11: database.go 迁移错误不再吞(rule)

  • Type:rule

  • Given: 模拟 AutoMigrate 返回的 err 非空(注入 mock 或用条件分支)

  • When: 初始化 DB

  • Then: 直接 log.fatal/panic 退出启动流程(或返回 error 给 main 决定退出),不能出现_ = err吞掉

  • Pass Condition:go vet ./database/无 warning,代码 review 确认无_ = err

  • Evidence: grep_ = err仅 0 处剩余;go vet output

AC-12: StockReservation 在销售订单 Confirm 时写入、出库后删除(rule)

  • Type:rule

  • Given: stock P 总 15;创建销售订单 A qty=10 + 创建销售订单 B qty=10

  • When: 分别 Confirm A、Confirm B

  • Then:

    • A Confirm 后 stock_reservation 有 1+ 条 row,sum(qty)=10;BuildPickingPlan分配给 A 的 plan 合计=10

    • B Confirm 时 stock 总 15 - 已预留 10,剩 5 可用 → BuildPickingPlan 返回 qty=10 不足 → 应报"可用(扣减预留后) 5 < 10"错误;B Confirm 应失败并回滚,不写入 B 的 reservation

  • Pass Condition: 集成测试TestStockReservation_TwoOrders_SecondFails通过

  • Evidence: go test -run TestStockReservation

AC-13: StockReservation ExpiresAt 过期自动清理(rule)

  • Type:rule

  • Given: 写一条 StockReservation,ExpiresAt = now-1s(已过期);运行 Scheduler 的CleanupExpiredReservations

  • When: 执行一次调度

  • Then: 该 reservation 被删除,log 有清理记录"清理 N 条过期预留"

  • Pass Condition: scheduler 单测TestCleanupExpiredReservations通过

  • Evidence: go test -run TestCleanup output

AC-14: OutboundOrderItem.Extra 含 manual_picking_plan_v1 时自动拣货被跳过(rule)

  • Type:rule

  • Given: B1=5/B2=10,FEFO;出库行填 Extra={"manual_picking_plan_v1":[{"batch_id":B2,"quantity":7}]}(用户只想要 B2 的 7,跳过 B1 的 FEFO 优先)

  • When: ApplyConfirmOutbound

  • Then: B2 → 3,B1 → 5(B1 不动);item.Remark 前缀含[已按手动拆分执行]

  • Pass Condition: 集成测试TestApplyConfirm_ManualPlan_SkipBuild通过

  • Evidence: go test output

AC-15: manual plan sum 不等于需求数量时抛错并回滚(rule)

  • Type:rule

  • Given: 需求 7,但 manual plan 合计 6(少 1)

  • When: ApplyConfirmOutbound

  • Then: 返回错误 "手动拆分合计 6.00 ≠ 需求 7.00,差额 1.00",库存不扣

  • Pass Condition:TestApplyConfirm_ManualPlan_SumMismatch_Blocked通过

  • Evidence: go test output

AC-16: 全部 P0 改动不影响既有测试回归(rubric:回归稳定性)

  • Type:rubric

  • Dimension: 回归测试通过率与既有 behavior 一致性

  • Scale: 1-5

  • Anchors: 1=至少一个既有 AC 失败;3=出现 1-2 个无关 warning(已确认不影响);5=go build ./...+go test ./util/ ./config/ ./controller/wms/ -short+DB_PASSWORD=root go test ./controller/wms/ -run TestApplyConfirm全部通过,与上一轮结果数量一致

  • Pass Threshold: 5

  • Evidence: 最终命令输出

AC-17: 关键闸门失败默认值"保守失败"一致性(rubric:防御一致性)

  • Type:rubric

  • Dimension: 失败时的防御性默认值是否在所有闸门一致——例如 manual plan 缺字段一律中断,expiry 字段一律先判空再判 before,reserve 失败一律回滚订单,等等

  • Scale: 1-5

  • Anchors: 1=存在至少一个闸门使用"空=放行"的默认;3=闸门一致但日志/错误语义含糊;5=6 个关键闸门(expiry 拦截/MES plan 失败/错误不吞/reserve 扣减/manual plan sum/过期 reserve 清理)全部使用"失败=阻塞/回滚/记录日志",无任何静默放行

  • Pass Threshold: >= 4

  • Evidence: 代码 review 结果 + 对应负面测试存在(例如每个闸门一个 TestApplyConfirm_*_Blocked / *_Fails / *_Cleanup)

评论一下?

OωO
取消