作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
17 KiB
开发文档:对账开关操作页(库存结账开关 v2 · 代理级)
需求:库存结账开关按代理维度管理,一个代理一个开关状态;本页面提供代理列表 + 行按钮「开启/关闭」,关账前强制校验该代理名下无进行中单据。 设计文档:
material-in-out-detail-design.md§11(v2 代理级);需求确认记录stock-checkout-switch-requirements.md§8。 环境:dev192.168.1.181:3306;prd 上线需按 §4 sanity check 后重跑。 涉及库:xiaoqu_comples_d(菜单/字段/按钮/权限)+xiaoqu_franchisee_d(开关表)。
1. 数据库变更
1.1 业务表 — xiaoqu_franchisee_d(dataSource2 / m2;test 同库名,prod 为 xiaoqu_franchisee)
USE xiaoqu_franchisee_d; -- dev/test;prod 改 USE xiaoqu_franchisee;
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='库存结账开关(代理维度)';
无记录视为「开」(懒创建:首次对该代理执行开关操作时插入行),无需初始化数据,新增代理零成本。
1.2 菜单 + 字段 + 按钮 + 权限(权限库)
包含 6 张表:webpage / webpage_authority / page_field / field_webpage_authority / button / button_webpage_authority,在一个事务里完成。
环境对应的权限 schema:dev →
xiaoqu_comples_d;test →test;prod →xiaoqu_comples_p。切环境只需改第一行USE。 授权铁律:webpage_authority/field_webpage_authority/button_webpage_authority全部动态复用同 parent(出入库管理,dev id=150)下兄弟菜单的授权并集,不硬编码 authority_id、不硬编码自增主键。 ⚠️ 本段 SQL 不是幂等的,每个环境只跑一次:重复执行会再插入一整套菜单/字段/按钮记录(webpage 出现重复菜单)。执行前先查SELECT id FROM webpage WHERE path='warehouseInOutManage/stockCheckoutSwitch';,有记录就不要再跑;误重复执行后,把多余 web_id 在 6 张表(button_webpage_authority → button → field_webpage_authority → page_field → webpage_authority → webpage)里整套 DELETE 即可。
USE xiaoqu_comples_d; -- dev;test 改 USE test; prod 改 USE xiaoqu_comples_p;
START TRANSACTION;
-- ─────────────────────────────────────────────────────────────
-- ① webpage:菜单本体(挂到「出入库管理」id=150 之下,仓库管理的兄弟节点)
-- ─────────────────────────────────────────────────────────────
INSERT INTO webpage
(title, param, icon, description, order_by, parent_id, `type`, `status`, `path`, is_table_show)
VALUES
('对账开关操作页', '', '', '', 10, 150, 0, 1, 'warehouseInOutManage/stockCheckoutSwitch', 1);
SET @new_web_id := LAST_INSERT_ID();
SELECT @new_web_id AS new_webpage_id;
-- ─────────────────────────────────────────────────────────────
-- ② webpage_authority:菜单授权(动态复用同 parent 下兄弟菜单的授权 authority 并集)
-- ─────────────────────────────────────────────────────────────
INSERT INTO webpage_authority
(web_id, authority_id, title, `path`, parent_id, check_status, `type`, is_table_show, order_by)
SELECT
@new_web_id, sa.authority_id, '对账开关操作页', 'warehouseInOutManage/stockCheckoutSwitch', 150, 1, 0, 1, 10
FROM (
SELECT DISTINCT authority_id
FROM webpage_authority
WHERE web_id IN (SELECT id FROM webpage WHERE parent_id = 150 AND id <> @new_web_id)
AND soft_delete = 0
) sa;
-- ─────────────────────────────────────────────────────────────
-- ③ page_field:页面字段(对应 StockCheckoutListOutput)
-- 搜索区前端手写(非元数据驱动),故 nav_select 全 0;
-- closeState 必须 field_type='customdom' + custom_name='closeState'(前端状态 tag 插槽依赖)
-- ─────────────────────────────────────────────────────────────
INSERT INTO page_field
(web_id, cn_name, en_name, class_name, field_type, order_by,
is_select, nav_select, is_table_show, param, custom_name)
VALUES
(@new_web_id, '代理id', 'agentId', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'string', 1, 1, 0, 0, '', NULL),
(@new_web_id, '代理名称', 'agentName', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'string', 2, 1, 0, 1, '', NULL),
(@new_web_id, '渠道编号', 'channelNo', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'string', 3, 1, 0, 1, '', NULL),
(@new_web_id, '当前状态', 'closeState', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'customdom', 4, 1, 0, 1, '', 'closeState'),
(@new_web_id, '最近操作人', 'operateUserName','me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'string', 5, 1, 0, 1, '', NULL),
(@new_web_id, '最近操作时间', 'operateTime', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'date', 6, 1, 0, 1, '', NULL),
(@new_web_id, '备注', 'memo', 'me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput', 'string', 7, 1, 0, 1, '', NULL);
-- ─────────────────────────────────────────────────────────────
-- ④ field_webpage_authority:字段级权限(跟随 ② 的 authority 并集 CROSS JOIN)
-- ─────────────────────────────────────────────────────────────
INSERT INTO field_webpage_authority
(web_id, authority_id, cn_name, en_name, is_select, nav_select,
field_type, field_id, order_by, check_status, param, is_table_show)
SELECT
pf.web_id, wa.authority_id, pf.cn_name, pf.en_name, pf.is_select, pf.nav_select,
pf.field_type, pf.id, pf.order_by, 1, pf.param, pf.is_table_show
FROM page_field pf
CROSS JOIN (
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id = @new_web_id AND soft_delete = 0
) wa
WHERE pf.web_id = @new_web_id;
-- ─────────────────────────────────────────────────────────────
-- ⑤ button ×2:行按钮(type=0 → 前端 rowButtons)「关闭」/「开启」
-- ─────────────────────────────────────────────────────────────
INSERT INTO `button`
(web_id, cn_name, en_name, icon, class_name, function_path,
`type`, `status`, order_by, theme, memo, app_en_name)
VALUES
(@new_web_id, '关闭', 'stockClose', '', '', '', 0, 1, 1, 'danger', '库存关账(该代理)', ''),
(@new_web_id, '开启', 'stockOpen', '', '', '', 0, 1, 2, 'success', '库存开账(该代理)', '');
-- ─────────────────────────────────────────────────────────────
-- ⑥ button_webpage_authority:按钮权限(跟随 ② 的 authority 并集 CROSS JOIN)
-- ─────────────────────────────────────────────────────────────
INSERT INTO button_webpage_authority
(web_id, authority_id, cn_name, en_name, button_id,
order_by, check_status, param, icon, `type`, class_name, theme, memo)
SELECT
b.web_id, wa.authority_id, b.cn_name, b.en_name, b.id,
b.order_by, 1, '', b.icon, b.`type`, b.class_name, b.theme, b.memo
FROM `button` b
CROSS JOIN (
SELECT DISTINCT authority_id FROM webpage_authority
WHERE web_id = @new_web_id AND soft_delete = 0
) wa
WHERE b.web_id = @new_web_id;
-- ─────────────────────────────────────────────────────────────
-- ⑦ 校验
-- ─────────────────────────────────────────────────────────────
SELECT id, parent_id, title, path, order_by FROM webpage WHERE id = @new_web_id;
SELECT COUNT(*) AS authority_grants FROM webpage_authority WHERE web_id = @new_web_id;
SELECT COUNT(*) AS field_count FROM page_field WHERE web_id = @new_web_id;
SELECT COUNT(*) AS field_grants_total FROM field_webpage_authority WHERE web_id = @new_web_id;
-- 期待 field_grants_total = field_count(7) × authority_grants
SELECT id, cn_name, en_name, `type`, theme FROM `button` WHERE web_id = @new_web_id;
SELECT COUNT(*) AS button_grants_total FROM button_webpage_authority WHERE web_id = @new_web_id;
-- 期待 button_grants_total = 2 × authority_grants
COMMIT;
-- 失败请改 ROLLBACK
各环境授权范围预期:
- dev:出入库管理(150)下兄弟菜单授权数 1~2(实测 2026-08-04:多数 2,仓库类型管理 1)→ 并集预计 2
- test/prod:上线前用 §4 校验脚本现查兄弟菜单授权并集大小
已执行情况(2026-08-04):
- dev 业务库(
xiaoqu_franchisee_d):§1.1 建表完成(含uk_agent唯一索引)- dev 权限库(
xiaoqu_comples_d):webpage.id=391,webpage_authority=2 条(并集 authority 1+102),page_field=7 条,field_webpage_authority=14 条(7×2),button.id=794(stockClose)/795(stockOpen,均 type=0 行按钮),button_webpage_authority=4 条(2×2)——校验全部符合预期
1.3 字段配置说明
| 字段 (en_name) | order_by | 列表 | field_type | 说明 |
|---|---|---|---|---|
| agentId | 1 | ✗ | string | 行 key,开关操作传参用,不展示 |
| agentName | 2 | ✓ | string | 代理名称(agent.name) |
| channelNo | 3 | ✓ | string | 渠道编号(agent.channel_no) |
| closeState | 4 | ✓ | customdom | 当前状态;前端插槽渲染 tag(0 开账中 / 1 已关账),custom_name 必须为 'closeState' |
| operateUserName | 5 | ✓ | string | 最近操作人 |
| operateTime | 6 | ✓ | date | 最近操作时间 |
| memo | 7 | ✓ | string | 操作备注 |
| 按钮 (en_name) | type | theme | 说明 |
|---|---|---|---|
| stockClose | 0(行按钮) | danger | 关闭:precheck → 确认(可填备注)→ switch |
| stockOpen | 0(行按钮) | success | 开启:确认 → switch |
前端按行
closeState互斥只显示其中一个按钮(customRowButton 插槽实现);precheck/switch接口由PermissionInterceptor按webId + button.enName做按钮级校验。
2. 后端改动 — xiaoqu-franchisee(已实现)
xiaoqu-publicbeanandmap/src/main/java/me/iiv/iivframework/
├── beans/b2/WarehouseStockCheckout.java # @TableName("warehouse_stock_checkout")
├── mapper/m2/WarehouseStockCheckoutMapper.java (+ .xml)
└── service/s2/
├── WarehouseStockCheckoutService.java # getByAgentId(Long)
└── Impl/WarehouseStockCheckoutServiceImpl.java
xiaoqu-franchisee/src/main/java/me/iiv/iivframework/
├── aop/
│ ├── StockCheckoutGuard.java # 注解 scene()
│ ├── GuardScene.java # 7 类场景
│ ├── StockCheckoutGuardAspect.java # 唯一拦截判定(fail-open)
│ └── checkout/ # CheckoutWarehouseResolver + 7 实现
└── controller/warehouse/
├── StockCheckoutController.java # /stockCheckout listByPage|precheck|switch
├── input/StockCheckoutListInput.java
├── input/StockCheckoutPrecheckInput.java
├── input/StockCheckoutSwitchInput.java
└── output/StockCheckoutListOutput.java # ⚠️ 全限定类名与 page_field.class_name 严格一致
└── output/StockCheckoutPrecheckOutput.java # switch 复用(StockCheckoutSwitchOutput extends)
拦截点位:18 个写操作入口已加 @StockCheckoutGuard(入库单 5 + 出库单 5 + 调拨/划转 6 + 装机配发协同 1 + 取货入库确认 1),详见设计文档 §11.3.3。
3. 前端改动 — vue-manage-system(已实现)
- 路由:
src/router/module/warehouseInOutRouter.js追加/warehouseInOutManage/stockCheckoutSwitch(name = 组件 name =stockCheckoutSwitch)。 - 页面:
src/components/page/warehouseInOutManage/stockCheckoutSwitch.vue(standTableMixin;搜索区手写:代理名称/渠道编号模糊 + 状态下拉;状态列 customdom 插槽 tag;行按钮 customRowButton 插槽按 closeState 互斥;关账两弹窗流程)。 - API:
src/api/api.js+src/api/warehouseService.js的stockCheckoutService(list/precheck/switch)。
4. 上线执行顺序
- DEV(
xiaoqu_franchisee_d+xiaoqu_comples_d)- 业务库执行 §1.1(建表
warehouse_stock_checkout) - 权限库执行 §1.2(事务)
- 后端:
xiaoqu-publicbeanandmap先mvn clean install -DskipTests→xiaoqu-franchiseemvn clean package -DskipTests→ 部署 - 前端:
yarn dev/ 构建部署
- 业务库执行 §1.1(建表
- DEV 冒烟
- 左侧菜单出现「出入库管理 > 对账开关操作页」,列表显示拥有仓库的代理,状态全为「开账中」
- 「权限组管理 → 配置页面内容权限」可见 7 个字段
- 造一张该代理的进行中入库单 → 点「关闭」→ 弹单号列表、不可关账;结单后再关 → 成功,状态变「已关账(操作人 时间)」
- 关账代理:新增/审核/确认/作废入库单、出库单、调拨单均被拦并提示「{代理名称}库存已结账…」;其他代理不受影响
- 跨公司调拨:任一方代理关账即拦
- 「开启」后全部恢复
- TEST(业务库同 dev 库名
xiaoqu_franchisee_d,表已存在则跳过 §1.1)- 权限库
USE test;执行 §1.2(SQL 完全不改)
- 权限库
- PRD 上线前 sanity check
若 prod 的 parent id ≠ 150,替换 §1.2 中所有
USE xiaoqu_comples_p; -- ① 「出入库管理」parent id 是否仍是 150 SELECT id FROM webpage WHERE title='出入库管理' AND parent_id=0; -- ② 兄弟菜单授权并集大小 SELECT COUNT(DISTINCT authority_id) FROM webpage_authority WHERE web_id IN (SELECT id FROM webpage WHERE parent_id=150) AND soft_delete=0; -- ③ 防重复执行 SELECT id, title, path FROM webpage WHERE path='warehouseInOutManage/stockCheckoutSwitch';150即可,其余照跑。 - PRD 上线:业务库
USE xiaoqu_franchisee;执行 §1.1;权限库USE xiaoqu_comples_p;执行 §1.2。
5. 风险与注意
| 项 | 风险 | 处理 |
|---|---|---|
parent_id=150 硬编码 |
PRD 不一定一致 | §4 ① sanity check 取真实 id |
page_field.class_name 全限定类名 |
DTO 路径必须严格一致 | 已固定 me.iiv.iivframework.controller.warehouse.output.StockCheckoutListOutput |
closeState 列 |
不配 customdom+custom_name='closeState' 则前端显示原始 0/1 |
§1.2 ③ 已按此配置,勿改 |
| 关账拦截 fail-open | guard 异常/解析失败会放行 | 设计决策(§11.3.5),日志关键字「库存结账开关校验异常」需关注 |
| 并发窗口 | 校验→置位毫秒级窗口、在途事务不拦 | 如实记档(§11.3.6/§11.3.7),人工低频可接受 |
| 自动任务不拦 | 关账期间自动出库等仍动库存 | 已确认口径(确认记录 ⑧),关账≠库存绝对静止 |