作为 xiaoqu-agent-service(MCP 服务) 的开发参考。新系统与旧系统零代码耦合, 仅照搬其行级数据权限规则与数据源结构。设计方案见 xiaoqu-franchisee/doc/agent-mcp-service-design.md
259 lines
17 KiB
Markdown
259 lines
17 KiB
Markdown
# 开发文档:对账开关操作页(库存结账开关 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),人工低频可接受 |
|
||
| 自动任务不拦 | 关账期间自动出库等仍动库存 | 已确认口径(确认记录 ⑧),关账≠库存绝对静止 |
|