Files
xiaoqu_mcp_service/xiaoqu-franchisee/doc/stock-checkout-switch-requirements.md
T
xiaoqu b6d4bf6126 导入三个参考项目: xiaoqu-franchisee / xiaoqu-publicbeanandmap / elasticsearchpublic
作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合,
仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
2026-09-01 23:00:47 +08:00

13 KiB
Raw Blame History

需求文档:对账开关(库存结账开关 v2 · 代理级)

状态:已定稿(2026-08-04 需求整理;2026-08-04 待确认事项按建议方案全部确认,见 §8) 前置文档:material-in-out-detail-design.md §11(库存结账开关 v1 · 全局单开关,未实施,本文档整体替代 §11;设计文档 §11 已按本文档重构为 v2) 核心变更:开关从「全局单开关」改为「代理级开关」,一个代理一个开关状态;开关操作从收发明细页迁出,新增独立页面「对账开关操作页」,开启/关闭放在行按钮里。


1. 背景与变更点

# 维度 v1(原 §11,废弃) v2(本文档)
1 开关粒度 全局单行(id=1),一关全停 代理级,一个代理一个开关状态
2 拦截判定 查全局单行状态 解析操作涉及的仓库 → 仓库所属公司(warehouse.asset_owner_id → 代理)→ 查该代理开关
3 关账前置校验 全库查进行中单据 按代理维度查该代理名下进行中单据
4 开关操作入口 收发明细页顶部按钮 新增独立页面「对账开关操作页」,列表一行一个代理,开启/关闭为行按钮
5 拦截接口清单 AOP 注解拦截约 17 个写入口 接口清单不变,仅切面内部判定逻辑改变

不变的约定(沿用 v1 已确认结论):

  • 关 = 全量冻结:关账代理名下仓库的三类单据(入库单、出库单、调拨/划转单)所有写操作一律拦截,只读接口不拦。
  • 自动任务不拦截:AutoOutWarehouseTask、工单自动出入库照常;关账语义是「冻结手工单据操作」。
  • 驳回单(order_state=1)算进行中,未处理完不允许关账。
  • 装机工单只拦配发协同(/installOrder/distributeAndCooperate),取货工单拦入库确认(/pickup/pickupInWarehouseConfirm);工单状态不参与关账前置校验。

2. 核心语义

  • 开关维度 = 代理(agent)。关联路径:warehouse.asset_owner_id = 代理 id(即页面上的「仓库所属公司」)。
  • 开(默认):该代理一切如常。
  • 关:该代理名下所有仓库(asset_owner_id = 该代理)的入库单、出库单、调拨/划转单的全部写操作被拦截;已有单据也不允许继续审核/确认/撤销/作废(全量冻结)。
  • 对其他代理无任何影响,各代理开关互不影响。

3. 拦截逻辑(接口不变,逻辑变)

3.1 拦截入口清单(与 v1 §11.1 一致,不变)

单据 被拦截的写操作入口
入库单 /myWarehouseIn /add、/audit、/cancel、/remove、/warehouseConfirm
出库单 /myWarehouseOut /add、/audit、/cancel、/remove、/warehouseConfirm
调拨/划转单 /warehouseAllocation /addOrUpdateWarehouseAllocation、/addOrUpdateWarehouseAllocationTransfer、/addOrUpdateWarehouseAllocationAudit、/warehouseAllocationCommint、/warehouseAllocationTransferCommint、/repealWarehouseAllocation
装机工单 /installOrder /distributeAndCooperate(配发协同)
取货工单 /pickup /pickupInWarehouseConfirm(入库确认)

仍采用 @StockCheckoutGuard 注解 + AOP 切面方案,注解点位不变。

3.2 切面判定逻辑(本次变更核心)

切面不再查全局单行,改为:

1. 解析本次操作涉及的仓库集合 warehouseIds(解析方式按入口类型,见下表)
2. 查 warehouse 表得 asset_owner_id 集合(涉及哪些代理)
3. 逐个查 warehouse_stock_checkout 中该代理的开关状态
4. 任一涉及代理为「关」→ 拦截,返回失败提示

各入口的仓库解析方式:

入口类型 仓库来源
入库单/出库单 /add(新增/修改) input 明细行的 warehouseId(一单可多行多仓,全部解析)
入库单/出库单 /audit、/cancel、/remove、/warehouseConfirm 按单据 id 查明细行 warehouse_id 集合
调拨/划转 /addOrUpdate* input 的 warehouseOutId(调出仓)+ warehouseInId(调入仓)
调拨/划转 审核/确认/撤销 按单据 id 查 warehouse_allocation.warehouse_out_id / warehouse_in_id
装机工单配发协同 配发仓库(工单/配发明细中的仓库)
取货工单入库确认 入库仓库

性能:状态读取按 agent_id 直查单行(唯一索引,毫秒级),不上 Redis,避免缓存不一致导致放行/误拦(沿用 v1 结论)。

3.3 拦截提示文案

拦截提示文案带代理名,便于定位:「**{代理名称}**库存已结账,所有出入库及调拨划转单据暂停操作,请联系管理员开账」。(已确认 ③)

4. 关账前置校验(按代理)

关闭某代理开关时,校验该代理名下是否存在进行中单据,存在即拒绝关闭,必须全部结单(完成或作废)后才可关账。

单据 表 「进行中」状态 代理归属判定
入库单 warehouse_in + 明细 order_state ∈ (0 待审核, 1 驳回, 2 待入库) 明细行仓库的 asset_owner_id = 该代理
出库单 warehouse_out + 明细 order_state ∈ (0 待审核, 1 驳回, 2 待出库) 同上
调拨/划转单 warehouse_allocation order_state ∈ (0 待审核, 1 被驳回, 2 待出库, 3 待入库) 调出仓 或 调入仓的 asset_owner_id = 该代理

统一 soft_delete = 0。驳回(order_state = 1)算进行中(沿用已确认结论)。

口径说明:入/出库单主单无 warehouse_id,仓库在明细行,故按明细行仓库判定归属;一单涉及多仓时,任一明细仓属于该代理即计入。调拨单跨公司时任一仓属于该代理即计入。

校验失败返回分类明细(每类单号最多 20 条):

{
  "canClose": false,
  "details": [
    { "docType": "入库单", "count": 2, "orderNos": ["232026...", "232026..."] },
    { "docType": "调拨单", "count": 1, "orderNos": ["252026..."] }
  ]
}

并发:关账操作用 Redisson 锁 lock:stockCheckout:{agentId}(按代理加锁,互不影响)包住「校验 + 置位」;switch 接口服务端强制再跑一次前置校验(防前端绕过);重复提交同状态幂等返回当前状态。(沿用 v1 结论,锁粒度变细)

5. 数据存储

warehouse_stock_checkout 表由「全局单行」改为「代理维度多行」(实体在公共模块 beans/b2):

CREATE TABLE `warehouse_stock_checkout` (
  `id`                bigint(8)   NOT NULL AUTO_INCREMENT COMMENT '主键',
  `agent_id`          bigint(8)   NOT NULL                COMMENT '代理id(=warehouse.asset_owner_id)',
  `agent_name`        varchar(100) DEFAULT NULL           COMMENT '代理名称(冗余,展示用)',
  `close_state`       int(4)      NOT NULL DEFAULT '0'    COMMENT '结账状态(0:开;1:关)',
  `operate_user_id`   bigint(8)   DEFAULT NULL            COMMENT '最近操作人id',
  `operate_user_name` varchar(50)  DEFAULT NULL           COMMENT '最近操作人姓名',
  `operate_time`      datetime    DEFAULT NULL            COMMENT '最近操作时间',
  `memo`              varchar(200) DEFAULT NULL           COMMENT '操作备注(如关账原因)',
  `create_id`         bigint(8)   DEFAULT '0',
  `create_time`       datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `modify_id`         bigint(8)   DEFAULT '0',
  `modify_time`       datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `soft_delete`       int(4)      DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_agent` (`agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存结账开关(代理维度)';
  • 无记录视为「开」(懒创建:首次对该代理执行关账/开账操作时插入行),新增代理无需任何数据初始化。
  • 单行只保留最近一次操作人/时间;完整开关账历史日志表本期不做(沿用 v1 结论)。

6. 新页面:对账开关操作页

6.1 菜单位置

  • 菜单位置:仓库管理 → 「对账开关操作页」,webpage.path 最后一段 stockCheckoutSwitch(前端路由 name / 组件 name 保持一致)。
  • 按权限 SQL 铁律新增 webpage / webpage_authority / page_field / field_webpage_authority / button / button_webpage_authority(动态并集兄弟菜单授权,LAST_INSERT_ID() 串联,事务 + 校验段)。

6.2 页面形态

列表页(standard-table,page_field 元数据驱动列),一行一个代理:

列 来源
代理名称 agent.name
渠道编号 agent.channel_no
当前状态 开账中 / 已关账(开关表无记录 = 开账中)
最近操作人 operate_user_name
最近操作时间 operate_time
备注 memo
操作 行按钮:开启 / 关闭(button 表配 stockOpen / stockClose 两条行按钮,按当前状态互斥显示,先例:mixin.js 的 rowButtons + rowButtonClick)
  • 列表数据范围:只列拥有仓库的代理(warehouse.asset_owner_id 去重),LEFT JOIN 开关表取状态;无仓库的代理开关无意义,不列出。(已确认 ④)
  • 数据权限:沿用 agent.channel_no Z/F 模式(Z 全量、F 按管理仓库过滤)。(已确认 ⑤)
  • 搜索区:代理名称/渠道编号模糊、状态筛选。

6.3 关账操作流程(前端)

  1. 点行按钮「关闭」→ 调 precheck(agentId);
  2. 有进行中单据 → 弹窗列出分类计数 + 单号(不可继续);
  3. 无 → $confirm 二次确认:「关账后将暂停**{代理名称}**所有出入库及调拨划转单据的新增、审核、确认、作废操作,确认关账?」可填备注;
  4. 调 switch(服务端强制再校验,被拦时同样弹单号列表);
  5. 「开启」直接 $confirm 后调 switch。

6.4 开关接口(新 Controller,不再挂收发明细 Controller)

接口 方法 说明
/stockCheckout/listByPage POST 代理列表 + 开关状态(支持名称/编号/状态筛选)
/stockCheckout/precheck POST 关账前置校验:{agentId} → {canClose, details:[...]}
/stockCheckout/switch POST 切换:{agentId, closeState, memo?};关账时服务端强制再校验;幂等

precheck / switch 必须接 PermissionInterceptor 按钮级校验(webId + button.enName),不能只靠前端按钮显隐(沿用 v1 漏洞审查结论)。

6.5 收发明细页联动调整

原 §10.2 布局中搜索区的「库存关账」按钮与「当前状态」文本、§11.5 前端内容整体移除,收发明细页回归纯报表。

7. 改动清单(预估,实施前细化)

层 改动
DB warehouse_stock_checkout 建表(代理维度,m2 业务库,三环境各一次)
公共模块 beans/b2/WarehouseStockCheckout 实体 + Mapper + Service 接口/Impl(先 mvn clean install)
本仓库 @StockCheckoutGuard 切面改为按仓库→代理解析判定;各入口仓库解析适配(新增单查 input、状态变更单查库);新 StockCheckoutController(listByPage / precheck / switch);按代理 Redisson 锁
权限库 新页面 4 张权限表 + 行按钮 stockOpen / stockClose
前端 新页面 stockCheckoutSwitch.vue(列表 + 行按钮 + 关账预校验弹窗 + 确认弹窗);路由;api.js + service;收发明细页移除关账按钮/状态文本

8. 业务确认记录(2026-08-04 全部确认,按建议方案落定)

# 事项 结论
① 跨公司调拨/划转单:调出仓属 A 代理(开)、调入仓属 B 代理(关)时 任一方代理关账即拦截(与「关 = 全量冻结」语义一致,避免关账代理库存被调入/调出动作改变)
② 关账前置校验中「该代理下的单据」口径 按仓库所属公司(明细行仓库 asset_owner_id)判定,与拦截口径完全一致;不按录单代理 agent_id
③ 拦截提示文案 带代理名:「{代理名称}库存已结账,所有出入库及调拨划转单据暂停操作,请联系管理员开账」
④ 对账开关操作页列表范围 只列拥有仓库的代理(warehouse.asset_owner_id 去重),无仓库的代理开关无意义
⑤ 该页面数据权限 沿用 agent.channel_no Z/F 数据范围模式(Z 见全部、F 按管理仓库过滤)
⑥ 行按钮显示方式 按当前状态互斥显示「开启」/「关闭」一个按钮(button 表配两条,前端 v-show 互斥)
⑦ 开关操作是否需要完整历史日志 本期不做,单行只留最近操作人/时间/备注,后续按需另立迭代
⑧ 装机工单配发协同的仓库解析 实施时以代码实配发仓库为准(配发明细/工单上的仓库字段),归属判定同 ②