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

17 KiB
Raw Blame History

开发文档:对账开关操作页(库存结账开关 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)

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. 上线执行顺序

  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
    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),人工低频可接受
自动任务不拦 关账期间自动出库等仍动库存 已确认口径(确认记录 ⑧),关账≠库存绝对静止