feat(role,scope)!: 角色与授权可配 + 需求收敛(4 角色 / 10 菜单 / 删两模块)
角色与授权(交接摘要 §一·三十五): - 权限判定合并两个来源:内置角色按代码(BuiltinRole)、自定义角色按库 (sys_role_permission)。实现放 module 层的 DbRoleGrantSource,security 只依赖 接口 RoleGrantSource —— 否则角色服务要调 permissionChecker,两个包迟早成环 - 权限判定必须加缓存:此前是零 IO,而压测基线是"SQL 条数恒定"。缓存放 Redis, 空结果也缓存(否则已删角色的 token 会把库打穿)、Redis 挂了退化成直读库、TTL 只兜底 - V18 建 sys_role.user_type(账号的 user_type 一直从角色反推,自定义角色不在代码映射里) 与 sys_role_menu;8 个接口 + 错误码 40045;契约 119 -> 129 - 内置角色只读(权限码是前后端契约,做成可改 = 一次误操作就能把管理员锁在系统外); 平台级能力两道防线(界面禁用 + 服务端 40301);防自锁;改完立即生效(写授权清缓存) - RoleProvisioner 从"已存在就 continue"改成幂等回填 + 每日 03:20 定时任务 —— 修的是"代码改了权限、已开通租户永远停在发放那一刻",这种漂移不报错也没测试会红 - scripts/e2e-m10-role.py 77/77 需求收敛(交接摘要 §一·三十六,本节的数字为准): - 删副班主任 / 审核员两个内置角色(6 -> 4)、历史整理器(module/cleanup)、 加入申请(module/join + 学生端 pages/join)、collect_task_org.deputy_user_ids 整列; 审核范围三级回退 -> 两级,连带 6 个 mapper 的 LIKE '%,id,%' 分支 - V19 迁移处理存量:先解 sys_user_role 绑定再软删角色;账号只在"除这两个角色外没有 别的角色"时才软删。必须迁而不能只删代码 —— 角色码删掉后 mergePermissions 返回空集, 存量账号会"能登录、处处 40301",不抛异常不打日志 - 契约 129 -> 119、错误码 59 -> 54、权限码 30 -> 28、PC 菜单 12 -> 10、 小程序页 10 -> 9、原型 32 -> 29 屏、E2E 39 -> 37 个(断言 1695 条) - UserService.list 对不认识的角色码由"静默丢掉过滤条件"改成失败关闭 (之前传个不存在的 roleCode 会返回全校账号,且看起来完全正常) - 品牌元信息同步改名「深学在线」:pom 名称、/doc.html 标题、配置与启动类注释、 存储自检探针内容;学生端 app 名与全局导航栏标题
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 数据信息填报系统 · 主配置
|
||||
# 深学在线 · 主配置
|
||||
# 环境差异全部放 application-{dev,test,prod}.yml
|
||||
# ⚠️ 生产密钥一律走环境变量,不写在这里
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
-- 角色与授权(二期):① sys_role 补 user_type;② 新建 sys_role_menu。
|
||||
--
|
||||
-- 为什么现在才补:一期只有 6 个**内置**角色,权限由代码决定(BuiltinRole),
|
||||
-- 于是有两件事一直由代码隐式表达、库里没有:
|
||||
-- ① 这个角色对应哪一类用户(sys_user.user_type 是 NOT NULL,而它一直是"从角色反推"的);
|
||||
-- ② 这个角色能看见哪些菜单(一直只有 BuiltinRole 里的一个字符串集合)。
|
||||
-- 二期开放自定义角色之后,这两件事必须能由租户自己配 —— 所以要落到库里。
|
||||
--
|
||||
-- ⚠️ 内置角色在这两处的**权威仍然是代码**(见 PermissionChecker 与 RoleProvisioner 的注释):
|
||||
-- 库里的行是为了「看得见、可审计、可同步」,不是生效依据。
|
||||
-- 否则代码收紧一条权限、而库里没同步,就会出现"界面显示一套、实际生效另一套"。
|
||||
|
||||
-- ── 1) sys_role.user_type ───────────────────────────────────────────
|
||||
-- 不加这一列的后果很具体:给账号发一个自定义角色时,服务端要反推 user_type
|
||||
--(UserService.assertRoleMatchesUserType),而自定义角色不在代码映射里 → 直接 40001,
|
||||
-- 也就是"角色建得出来、却发不出去"。
|
||||
ALTER TABLE sys_role ADD COLUMN user_type smallint;
|
||||
|
||||
-- 回填:此刻库里只有内置角色(自定义角色的界面还没上线),按 BuiltinRole 的映射逐行写。
|
||||
-- 刻意写成 role_code 条件而不是全表刷一个值:映射关系就在代码里,这里只是把它落到行上。
|
||||
UPDATE sys_role SET user_type = 1 WHERE role_code = 'PLATFORM_ADMIN';
|
||||
UPDATE sys_role SET user_type = 2 WHERE role_code IN ('SCHOOL_ADMIN', 'AUDITOR');
|
||||
UPDATE sys_role SET user_type = 3 WHERE role_code IN ('HEAD_TEACHER', 'DEPUTY_TEACHER');
|
||||
UPDATE sys_role SET user_type = 4 WHERE role_code = 'STUDENT';
|
||||
|
||||
-- 兜底:万一有上面没覆盖到的行(例如某个租户手工插过自定义角色),
|
||||
-- 给最保守的一类(学生=只能操作本人那一份),而不是留 NULL 让 NOT NULL 失败。
|
||||
UPDATE sys_role SET user_type = 4 WHERE user_type IS NULL;
|
||||
|
||||
ALTER TABLE sys_role ALTER COLUMN user_type SET NOT NULL;
|
||||
COMMENT ON COLUMN sys_role.user_type IS '角色对应的用户类型 1平台运营 2学校管理员 3班主任 4学生(契约 §2.7)';
|
||||
|
||||
-- ── 2) sys_role_menu 角色可见菜单 ───────────────────────────────────
|
||||
-- 菜单**不做成权限的附属**:一个菜单可能由多个权限共同决定可见性
|
||||
--(「审核」菜单初审与终审权限都能看到),所以单独一张表,与 sys_role_permission 同构。
|
||||
CREATE TABLE sys_role_menu (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
tenant_id bigint NOT NULL,
|
||||
role_id bigint NOT NULL,
|
||||
menu_key varchar(64) NOT NULL,
|
||||
create_by bigint,
|
||||
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by bigint,
|
||||
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted smallint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_rolemenu_role_key ON sys_role_menu (role_id, menu_key) WHERE deleted = 0;
|
||||
CREATE INDEX idx_rolemenu_tenant_role ON sys_role_menu (tenant_id, role_id);
|
||||
COMMENT ON TABLE sys_role_menu IS '角色与菜单键的关联(菜单键定义在代码 BuiltinRole,与前端 XtMenu.key 逐字一致)';
|
||||
COMMENT ON COLUMN sys_role_menu.menu_key IS '菜单键,如 task;只影响"看得见哪些入口",不代表有权限操作';
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
-- V19:需求收敛(2026-10 客户确认)
|
||||
--
|
||||
-- 删掉四样东西,一次做完:
|
||||
-- ① 内置角色「副班主任 DEPUTY_TEACHER」「审核员 AUDITOR」——连同**只挂这两个角色的账号**
|
||||
-- ② collect_task_org.deputy_user_ids("协办人"列)——审核范围由三级回退收窄为两级
|
||||
-- ③ 「历史整理器」表(collect_cleanup_job)
|
||||
-- ④ 「加入申请」表(collect_join_request)
|
||||
--
|
||||
-- ⚠️ 三条刻意的写法,别"顺手改成更干净的写法":
|
||||
-- 1. **账号是软删(deleted=1 + status=0),不是物理 DELETE。**
|
||||
-- 本系统的"删除"一律是软删;而历史填报单、审核记录、操作日志里都留着他们的 user_id,
|
||||
-- 物理删会让那些行变成"指向一个不存在的用户"(审核时间轴显示不出人)。
|
||||
-- 2. **只删"除这两个角色外没有别的角色"的账号。** 一个班主任如果同时被挂了副班主任,
|
||||
-- 删掉他等于把一个还在用的人踢出系统 —— 那种错在库里看不出来,只有当事人来问。
|
||||
-- 3. **顺序:先处理绑定关系,再软删角色行。** 反过来的话,后面的语句按 role_code 找不到角色
|
||||
-- (本迁移里所有查询都带 deleted = 0),会静默什么都不做。
|
||||
--
|
||||
-- ⚠️ 若某租户的**唯一管理账号**恰好是审核员(异常情况,正常开通租户时必定有一个学校管理员),
|
||||
-- 删掉后该租户会没有人能进后台。上线前用这条查一遍:
|
||||
-- SELECT r.tenant_id FROM sys_role r WHERE r.role_code = 'AUDITOR' AND r.deleted = 0;
|
||||
|
||||
-- ── ① 找出"只挂被删角色"的账号 ──────────────────────────────
|
||||
CREATE TEMP TABLE tmp_dead_user ON COMMIT DROP AS
|
||||
SELECT u.id AS user_id
|
||||
FROM sys_user u
|
||||
WHERE u.deleted = 0
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_user_role ur
|
||||
JOIN sys_role r ON r.id = ur.role_id
|
||||
WHERE ur.user_id = u.id AND ur.deleted = 0 AND r.deleted = 0
|
||||
AND r.role_code IN ('DEPUTY_TEACHER', 'AUDITOR'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_user_role ur2
|
||||
JOIN sys_role r2 ON r2.id = ur2.role_id
|
||||
WHERE ur2.user_id = u.id AND ur2.deleted = 0 AND r2.deleted = 0
|
||||
AND r2.role_code NOT IN ('DEPUTY_TEACHER', 'AUDITOR'));
|
||||
|
||||
-- 软删这些账号(顺手停用:软删后仍可能被历史记录引用,停用让它不可能再登录)
|
||||
UPDATE sys_user
|
||||
SET deleted = 1, status = 0, update_time = CURRENT_TIMESTAMP
|
||||
WHERE id IN (SELECT user_id FROM tmp_dead_user);
|
||||
|
||||
-- ── ② 解除这两个角色的全部绑定(含"多角色用户"身上的那一条)──────
|
||||
UPDATE sys_user_role
|
||||
SET deleted = 1, update_time = CURRENT_TIMESTAMP
|
||||
WHERE deleted = 0
|
||||
AND role_id IN (SELECT id FROM sys_role
|
||||
WHERE deleted = 0 AND role_code IN ('DEPUTY_TEACHER', 'AUDITOR'));
|
||||
|
||||
UPDATE sys_role_permission
|
||||
SET deleted = 1, update_time = CURRENT_TIMESTAMP
|
||||
WHERE deleted = 0
|
||||
AND role_id IN (SELECT id FROM sys_role
|
||||
WHERE deleted = 0 AND role_code IN ('DEPUTY_TEACHER', 'AUDITOR'));
|
||||
|
||||
UPDATE sys_role_menu
|
||||
SET deleted = 1, update_time = CURRENT_TIMESTAMP
|
||||
WHERE deleted = 0
|
||||
AND role_id IN (SELECT id FROM sys_role
|
||||
WHERE deleted = 0 AND role_code IN ('DEPUTY_TEACHER', 'AUDITOR'));
|
||||
|
||||
-- ── ③ 软删两个内置角色本身 ─────────────────────────────────
|
||||
UPDATE sys_role
|
||||
SET deleted = 1, status = 0, update_time = CURRENT_TIMESTAMP
|
||||
WHERE deleted = 0 AND role_code IN ('DEPUTY_TEACHER', 'AUDITOR');
|
||||
|
||||
-- ── ④ 删掉「协办人」列 ─────────────────────────────────────
|
||||
-- 该列存的是首尾带逗号的 id 串(,3,7,),被 6 个模块的 SQL 用来做"这份单归不归我"的判定。
|
||||
-- 删掉后审核范围只剩两级:collect_task_org.head_user_id → sys_org.leader_user_id。
|
||||
ALTER TABLE collect_task_org DROP COLUMN IF EXISTS deputy_user_ids;
|
||||
|
||||
-- ── ⑤ 删掉两个模块的表 ─────────────────────────────────────
|
||||
DROP TABLE IF EXISTS collect_cleanup_job;
|
||||
DROP TABLE IF EXISTS collect_join_request;
|
||||
|
||||
-- ── ⑥ 两处注释对齐(值本身仍在历史行里,注释别说谎)────────────
|
||||
-- collect_file.source = 4 曾经是"整理器导入";整理器下线后这个值不会再产生,
|
||||
-- 但历史行里可能还有,所以保留在注释里说明它是历史值。
|
||||
COMMENT ON COLUMN collect_file.source IS
|
||||
'1本人上传 2班主任代传 3管理员代传 4整理器导入(历史值:整理器已下线,不再产生新值)';
|
||||
|
||||
-- 操作日志的对象类型:JOIN_REQUEST 已不再产生新记录(历史行保留)
|
||||
COMMENT ON COLUMN sys_operation_log.target_type IS
|
||||
'被操作对象的类型:ROSTER/TASK/SUBMISSION/FILE/EXPORT/USER/ROLE(历史值另有 JOIN_REQUEST)';
|
||||
Reference in New Issue
Block a user