作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
13 KiB
需求文档:对账开关(库存结账开关 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_noZ/F 模式(Z 全量、F 按管理仓库过滤)。(已确认 ⑤) - 搜索区:代理名称/渠道编号模糊、状态筛选。
6.3 关账操作流程(前端)
- 点行按钮「关闭」→ 调
precheck(agentId); - 有进行中单据 → 弹窗列出分类计数 + 单号(不可继续);
- 无 →
$confirm二次确认:「关账后将暂停**{代理名称}**所有出入库及调拨划转单据的新增、审核、确认、作废操作,确认关账?」可填备注; - 调
switch(服务端强制再校验,被拦时同样弹单号列表); - 「开启」直接
$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 互斥) |
| ⑦ | 开关操作是否需要完整历史日志 | 本期不做,单行只留最近操作人/时间/备注,后续按需另立迭代 |
| ⑧ | 装机工单配发协同的仓库解析 | 实施时以代码实配发仓库为准(配发明细/工单上的仓库字段),归属判定同 ② |