Files
xiaoqu_mcp_service/xiaoqu-franchisee/doc/stock-checkout-switch-dev.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

259 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发文档:对账开关操作页(库存结账开关 v2 · 代理级)
> **需求**:库存结账开关按代理维度管理,一个代理一个开关状态;本页面提供代理列表 + 行按钮「开启/关闭」,关账前强制校验该代理名下无进行中单据。
> **设计文档**:`material-in-out-detail-design.md` §11(v2 代理级);需求确认记录 `stock-checkout-switch-requirements.md` §8。
> **环境**:dev `192.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`)
```sql
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 即可。
```sql
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. 上线执行顺序
1. **DEV(`xiaoqu_franchisee_d` + `xiaoqu_comples_d`)**
- 业务库执行 §1.1(建表 `warehouse_stock_checkout`)
- 权限库执行 §1.2(事务)
- 后端:`xiaoqu-publicbeanandmap` 先 `mvn clean install -DskipTests` → `xiaoqu-franchisee` `mvn clean package -DskipTests` → 部署
- 前端:`yarn dev` / 构建部署
2. **DEV 冒烟**
- 左侧菜单出现「出入库管理 > 对账开关操作页」,列表显示拥有仓库的代理,状态全为「开账中」
- 「权限组管理 → 配置页面内容权限」可见 7 个字段
- 造一张该代理的进行中入库单 → 点「关闭」→ 弹单号列表、不可关账;结单后再关 → 成功,状态变「已关账(操作人 时间)」
- 关账代理:新增/审核/确认/作废入库单、出库单、调拨单均被拦并提示「{代理名称}库存已结账…」;其他代理不受影响
- 跨公司调拨:任一方代理关账即拦
- 「开启」后全部恢复
3. **TEST(业务库同 dev 库名 `xiaoqu_franchisee_d`,表已存在则跳过 §1.1)**
- 权限库 `USE test;` 执行 §1.2(SQL 完全不改)
4. **PRD 上线前 sanity check**
```sql
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';
```
若 prod 的 parent id ≠ 150,替换 §1.2 中所有 `150` 即可,其余照跑。
5. **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),人工低频可接受 |
| 自动任务不拦 | 关账期间自动出库等仍动库存 | 已确认口径(确认记录 ⑧),关账≠库存绝对静止 |