From a25201357382f005a8ebba7caaa05fcfecaf90ee Mon Sep 17 00:00:00 2001 From: yueli Date: Wed, 16 Sep 2026 16:44:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(perm):=20=E9=80=86=E5=90=91=E7=89=A9?= =?UTF-8?q?=E6=B5=81=E4=B8=93=E7=94=A8=E6=9D=83=E9=99=90=E7=A0=81=E4=B8=8E?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8E=88=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为逆向物流的每个动作拆出独立权限码,终止此前复用其它模块权限的做法。 权限码一览(全部为无冒号形式,见下方说明): outbound_return 出库退回 defective_list 不良品台账查看 defective_restock 不良品回库 defective_scrap 不良品报废 stock_change_status 库存状态变更 outbound_return_list 退回记录查看 另补建两个菜单:defective_goods(不良品在管台账)、outbound_returns(退回记录), 前者原先只有前端路由、sys_menu 中无对应行,导致页面在权限管理界面不可见、 其下权限也无从授予(sys_element.menu_code 有指向 sys_menu 的外键)。 默认授予:SUPER_ADMIN、SUPERVISOR、WAREHOUSE_MGR。 刻意不含 OUTBOUND —— 业务确认出库员只负责正向拣货发货,不参与逆向物流。 ★ 为什么全部用无冒号形式,而不是 :: 后端 _expand_operation_perms() 对带冒号的权限码做**前缀桥接** —— 只要用户 持有该菜单下任一以 :operation/:edit/:delete/... 结尾的权限就被放行。 实测验证过这个放大效应: outbound_list:return <- 持有 outbound_list:operation -> True outbound_return <- 持有 outbound_list:operation -> False 且前端 hasPermission() 是精确匹配,用冒号码会出现「接口能调、按钮却看不到」 的错位。无冒号码不触发桥接,前后端判定完全一致。 ★ add_return_view_support.sql 另含一处 schema 变更:trans_return 补 company_name 快照列。退回流水的多租户隔离原先只能靠 join 链推 (trans_return → trans_outbound → 库存表 → 物料主表),而库存行会被入库 模块物理删除(实测 1077 条出库记录中已有 7 条悬空),链路一断该记录就会 对普通用户静默消失。审计视图静默丢数据不可接受,故落快照。 表当前为空,无需回填。 三个脚本均为幂等,含预检、回滚段与执行后核对。 --- .../add_defective_operation_perms.sql | 121 +++++++++++++++++ db_migrations/add_outbound_return_perm.sql | 84 ++++++++++++ db_migrations/add_return_view_support.sql | 125 ++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 db_migrations/add_defective_operation_perms.sql create mode 100644 db_migrations/add_outbound_return_perm.sql create mode 100644 db_migrations/add_return_view_support.sql diff --git a/db_migrations/add_defective_operation_perms.sql b/db_migrations/add_defective_operation_perms.sql new file mode 100644 index 0000000..48985c7 --- /dev/null +++ b/db_migrations/add_defective_operation_perms.sql @@ -0,0 +1,121 @@ +-- ============================================================================= +-- 逆向物流动作权限码(拒绝搭便车) +-- +-- 背景 +-- 退回接口已拆出专用码 outbound_return(见 add_outbound_return_perm.sql)。 +-- 余下三个逆向动作此前仍在复用 inventory_stocktake:operation(盲盘作业), +-- 职责错配、审计不合规,此处一并收口: +-- +-- code 名称 宿主菜单 用途 +-- -------------------- -------------- ---------------- ------------------ +-- defective_restock 不良品回库 defective_goods 修好后加回原库存 +-- defective_scrap 不良品报废 defective_goods 确认无法维修后销毁 +-- stock_change_status 库存状态变更 inventory_mgmt 冻结 / 标不良 / 恢复 +-- +-- ★ 三者均为**无冒号**形式,刻意的。原因见 add_outbound_return_perm.sql: +-- 形如 : 的权限码会命中 _expand_operation_perms() 的 +-- 前缀桥接 —— 例如 inventory_stocktake 下已有 inventory_stocktake:operation, +-- 若改用 inventory_stocktake:restock,则所有持有盘点操作权的角色会被自动 +-- 放行,授权面失控。无冒号码不触发桥接,与前端 hasPermission 的精确匹配一致。 +-- +-- ★ 顺带补建 defective_goods 菜单:二期只加了前端路由(侧边栏由路由驱动, +-- 页面本就能访问),但 sys_menu 中没有对应行,导致该页面在权限管理界面 +-- 不可见、其下权限也无法授予。sys_element.menu_code 有指向 sys_menu(code) +-- 的外键,故必须先把菜单建出来。 +-- +-- 默认授予:SUPER_ADMIN(超管)、SUPERVISOR(主管)、WAREHOUSE_MGR(库管) +-- company_name 置 NULL = 全局模板权限,对所有公司生效(与既有授权一致)。 +-- ★ 刻意**不含** OUTBOUND:业务确认出库员只负责正向拣货发货, +-- 不参与任何逆向物流操作。 +-- +-- 幂等:全部带存在性判断,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +-- --------------------------------------------------------------------------- +-- 1) 补建「不良品在管台账」菜单(挂在 入库管理 inventory_mgmt 下) +-- --------------------------------------------------------------------------- +INSERT INTO sys_menu (parent_id, name, code, path, sort_order, is_visible) +SELECT (SELECT id FROM sys_menu WHERE code = 'inventory_mgmt'), + '不良品在管台账', 'defective_goods', '/inventory/defective', 6, TRUE +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE code = 'defective_goods'); + +-- --------------------------------------------------------------------------- +-- 2) 注册三个权限元素 +-- --------------------------------------------------------------------------- +INSERT INTO sys_element (menu_code, name, code, element_type) +SELECT v.menu_code, v.name, v.code, 'operation' +FROM (VALUES + ('defective_goods', '不良品回库', 'defective_restock'), + ('defective_goods', '不良品报废', 'defective_scrap'), + ('inventory_mgmt', '库存状态变更', 'stock_change_status') +) AS v(menu_code, name, code) +WHERE NOT EXISTS (SELECT 1 FROM sys_element e WHERE e.code = v.code); + +-- --------------------------------------------------------------------------- +-- 3) 默认授予三个角色(菜单 + 元素) +-- --------------------------------------------------------------------------- +-- 3.1 菜单 +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT v.role_code, 'defective_goods', 'menu', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS v(role_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission r + WHERE r.role_code = v.role_code AND r.target_code = 'defective_goods' + AND r.type = 'menu' +); + +-- 3.2 元素 +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT r.role_code, v.target_code, 'element', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS r(role_code) +CROSS JOIN (VALUES + ('defective_restock'), ('defective_scrap'), ('stock_change_status') +) AS v(target_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission x + WHERE x.role_code = r.role_code AND x.target_code = v.target_code + AND x.type = 'element' +); + +-- --------------------------------------------------------------------------- +-- 4) 执行后核对 +-- --------------------------------------------------------------------------- +\echo '--- 菜单已建立 ---' +SELECT code, name, path FROM sys_menu WHERE code = 'defective_goods'; + +\echo '--- 三个权限元素(应 3 行)---' +SELECT e.code, e.name, e.menu_code, e.element_type + FROM sys_element e + WHERE e.code IN ('defective_restock', 'defective_scrap', 'stock_change_status') + ORDER BY e.code; + +\echo '--- 授权明细(应 3 菜单 + 9 元素 = 12 行)---' +SELECT target_code, type, string_agg(role_code, ', ' ORDER BY role_code) AS roles + FROM sys_role_permission + WHERE target_code IN ('defective_goods', 'defective_restock', + 'defective_scrap', 'stock_change_status') + GROUP BY target_code, type ORDER BY type, target_code; + +\echo '--- 确认 OUTBOUND 未获得任何逆向权限(应为 0 行)---' +SELECT target_code FROM sys_role_permission + WHERE role_code = 'OUTBOUND' + AND target_code IN ('outbound_return', 'defective_goods', 'defective_restock', + 'defective_scrap', 'stock_change_status'); + +COMMIT; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DELETE FROM sys_role_permission +-- WHERE target_code IN ('defective_goods','defective_restock', +-- 'defective_scrap','stock_change_status'); +-- DELETE FROM sys_element +-- WHERE code IN ('defective_restock','defective_scrap','stock_change_status'); +-- DELETE FROM sys_menu WHERE code = 'defective_goods'; +-- COMMIT; diff --git a/db_migrations/add_outbound_return_perm.sql b/db_migrations/add_outbound_return_perm.sql new file mode 100644 index 0000000..fe04815 --- /dev/null +++ b/db_migrations/add_outbound_return_perm.sql @@ -0,0 +1,84 @@ +-- ============================================================================= +-- 出库退回权限码(库管 SOP) +-- +-- 背景 +-- 退回是实物交接动作,按仓库管理规范只能由具备「库管」职责的人员执行, +-- 不能对所有领料员工开放。原先该接口复用的是 inventory_stocktake:operation +-- (盲盘作业),语义不符且粒度太粗。此处拆出专用权限码。 +-- +-- 权限码设计 +-- code = 'outbound_return',注册在 menu_code = 'outbound_list'(出库记录)下, +-- element_type = 'operation'。 +-- +-- ★ 为什么用**无冒号**形式而不是 outbound_list:return: +-- 1) 本系统对「动作类权限」的既有约定就是无冒号码 —— scrap_apply / +-- scrap_execute / scrap_approval 均为此形式; +-- 2) 更关键的是后端 _expand_operation_perms() 的**前缀桥接**: +-- 形如 : 的权限码,只要用户持有该菜单下任一以 +-- :operation/:edit/:delete/... 结尾的权限就会被放行。 +-- outbound_list 下已存在 outbound_list:operation, +-- 若用 outbound_list:return,则所有持有 outbound_list:operation 的角色 +-- (OUTBOUND / SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR)都会自动获得 +-- 退回权 —— 授权面被放大,且与前端 hasPermission 的**精确匹配**不一致, +-- 会出现「接口能调、按钮却看不到」的错位。 +-- 无冒号码不触发桥接,前后端判定完全一致。 +-- +-- 3) 为什么不用 stock:return:sys_element.menu_code 有指向 sys_menu 的 +-- **外键**,而系统中不存在 code='stock' 的菜单,该权限无法注册。 +-- +-- 默认授予:SUPER_ADMIN(超管)、SUPERVISOR(主管)、WAREHOUSE_MGR(库管) +-- company_name 置 NULL = 全局模板权限,对所有公司生效(与既有授权一致)。 +-- +-- ⚠ 注意:改为专用码后,OUTBOUND 角色将**失去**退回权。 +-- 该系统把 OUTBOUND 视为库管操作角色(它同时持有 scrap_execute、 +-- inventory_stocktake:operation)。如业务上出库管理员也应能退回, +-- 只需在下方 VALUES 中追加 ('OUTBOUND')。 +-- +-- 幂等:全部带 WHERE NOT EXISTS,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +-- --------------------------------------------------------------------------- +-- 1) 注册权限元素 +-- --------------------------------------------------------------------------- +INSERT INTO sys_element (menu_code, name, code, element_type) +SELECT 'outbound_list', '出库退回(库管)', 'outbound_return', 'operation' +WHERE NOT EXISTS ( + SELECT 1 FROM sys_element WHERE code = 'outbound_return' +); + +-- --------------------------------------------------------------------------- +-- 2) 默认授予三个角色 +-- --------------------------------------------------------------------------- +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT v.role_code, 'outbound_return', 'element', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS v(role_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission r + WHERE r.role_code = v.role_code + AND r.target_code = 'outbound_return' + AND r.type = 'element' +); + +-- --------------------------------------------------------------------------- +-- 3) 执行后核对 +-- --------------------------------------------------------------------------- +\echo '--- 权限元素已注册 ---' +SELECT code, name, menu_code, element_type FROM sys_element WHERE code = 'outbound_return'; + +\echo '--- 授权角色(应为 3 行)---' +SELECT role_code, target_code, COALESCE(company_name, '') AS company_name + FROM sys_role_permission WHERE target_code = 'outbound_return' ORDER BY role_code; + +COMMIT; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DELETE FROM sys_role_permission WHERE target_code = 'outbound_return'; +-- DELETE FROM sys_element WHERE code = 'outbound_return'; +-- COMMIT; diff --git a/db_migrations/add_return_view_support.sql b/db_migrations/add_return_view_support.sql new file mode 100644 index 0000000..4f4f4e7 --- /dev/null +++ b/db_migrations/add_return_view_support.sql @@ -0,0 +1,125 @@ +-- ============================================================================= +-- 退回看板支撑:权限码 + 菜单 + 公司快照列 +-- +-- 内容分三段: +-- 1) trans_return 补 company_name 快照列(Schema) +-- 2) 两个新权限码:defective_list / outbound_return_list(权限) +-- 3) 新建「退回记录」菜单并挂到出库管理下(菜单) +-- +-- --------------------------------------------------------------------------- +-- 1) 为什么补 company_name +-- 退回流水的多租户隔离原先只能靠 join 链推: +-- trans_return → trans_outbound → (source_table, stock_id) → 库存表 → 物料主表 +-- 但库存行会被入库模块**物理删除**(实测 1077 条出库记录中已有 7 条悬空)。 +-- 一旦源库存行被删,链路断裂 → 公司无法判定 → 该条退回记录对普通用户 +-- **静默消失**。审计视图里静默丢数据是不可接受的。 +-- 故在本表直接落一份公司快照,隔离判定不再依赖任何 join。 +-- 与 trans_defective_goods.company_name 的处理保持一致。 +-- +-- 本表当前为空,无需回填。 +-- +-- 2) 为什么两个权限码都是无冒号形式 +-- 理由见 add_outbound_return_perm.sql:形如 : 的码会命中 +-- _expand_operation_perms() 的前缀桥接,导致持有同菜单下 :operation 之类 +-- 权限的角色被自动放行。无冒号码不触发桥接,与前端精确匹配一致。 +-- +-- 3) 默认授予:SUPER_ADMIN(超管)、SUPERVISOR(主管)、WAREHOUSE_MGR(库管) +-- 不含 OUTBOUND —— 出库员只负责正向拣货发货,不参与逆向物流。 +-- +-- 幂等:全部带存在性判断,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +-- --------------------------------------------------------------------------- +-- 1) trans_return 公司快照列 +-- --------------------------------------------------------------------------- +ALTER TABLE trans_return + ADD COLUMN IF NOT EXISTS company_name varchar(255); + +COMMENT ON COLUMN trans_return.company_name IS + '退回发生时的所属公司快照;用于行级隔离,避免源库存行被删后隔离判定失效'; + +CREATE INDEX IF NOT EXISTS ix_trans_return_company + ON trans_return (company_name); + +-- --------------------------------------------------------------------------- +-- 2) 新建「退回记录」菜单(挂在 出库管理 outbound_mgmt 下) +-- --------------------------------------------------------------------------- +INSERT INTO sys_menu (parent_id, name, code, path, sort_order, is_visible) +SELECT (SELECT id FROM sys_menu WHERE code = 'outbound_mgmt'), + '退回记录', 'outbound_returns', '/outbound/returns', 5, TRUE +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE code = 'outbound_returns'); + +-- --------------------------------------------------------------------------- +-- 3) 注册两个权限元素 +-- --------------------------------------------------------------------------- +INSERT INTO sys_element (menu_code, name, code, element_type) +SELECT v.menu_code, v.name, v.code, 'operation' +FROM (VALUES + ('defective_goods', '不良品台账查看', 'defective_list'), + ('outbound_returns', '退回记录查看', 'outbound_return_list') +) AS v(menu_code, name, code) +WHERE NOT EXISTS (SELECT 1 FROM sys_element e WHERE e.code = v.code); + +-- --------------------------------------------------------------------------- +-- 4) 默认授予三个角色(菜单 + 元素) +-- --------------------------------------------------------------------------- +-- 4.1 菜单:退回记录 +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT v.role_code, 'outbound_returns', 'menu', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS v(role_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission r + WHERE r.role_code = v.role_code AND r.target_code = 'outbound_returns' + AND r.type = 'menu' +); + +-- 4.2 元素:两个查看权 +INSERT INTO sys_role_permission (role_code, target_code, type, company_name) +SELECT r.role_code, v.target_code, 'element', NULL +FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS r(role_code) +CROSS JOIN (VALUES ('defective_list'), ('outbound_return_list')) AS v(target_code) +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_permission x + WHERE x.role_code = r.role_code AND x.target_code = v.target_code + AND x.type = 'element' +); + +-- --------------------------------------------------------------------------- +-- 5) 执行后核对 +-- --------------------------------------------------------------------------- +\echo '--- 1) trans_return 新列 ---' +SELECT column_name, data_type FROM information_schema.columns + WHERE table_name = 'trans_return' AND column_name = 'company_name'; + +\echo '--- 2) 菜单 ---' +SELECT code, name, path FROM sys_menu WHERE code = 'outbound_returns'; + +\echo '--- 3) 权限码与授权(应 2 元素 + 1 菜单 = 3 组)---' +SELECT target_code, type, string_agg(role_code, ', ' ORDER BY role_code) AS roles + FROM sys_role_permission + WHERE target_code IN ('defective_list', 'outbound_return_list', 'outbound_returns') + GROUP BY target_code, type ORDER BY type DESC, target_code; + +\echo '--- 4) OUTBOUND 仍无任何逆向权限(应 0 行)---' +SELECT target_code FROM sys_role_permission + WHERE role_code = 'OUTBOUND' + AND target_code IN ('outbound_return', 'outbound_return_list', 'defective_list', + 'defective_restock', 'defective_scrap', 'stock_change_status', + 'defective_goods', 'outbound_returns'); + +COMMIT; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DELETE FROM sys_role_permission WHERE target_code IN +-- ('defective_list','outbound_return_list','outbound_returns'); +-- DELETE FROM sys_element WHERE code IN ('defective_list','outbound_return_list'); +-- DELETE FROM sys_menu WHERE code = 'outbound_returns'; +-- ALTER TABLE trans_return DROP COLUMN IF EXISTS company_name; +-- COMMIT;