feat(dept): CUSTOM 自定义部门与统一部门目录契约(双来源部门二期)
build / build-push (push) Successful in 59s

- 三表 ub_custom_dept_category/ub_custom_department/ub_custom_department_member(DDL 入 service/sql,160 待人工执行)
- 实体/仓库/服务:学校隔离(AccessScopeService 新增 requireSchoolInScope/resolveWritableSchoolId)、
  del_marker=id 软删让位 uk、成员=本校教职工黑名单口径(排除学生组 type=8 与禁用账号)
- 管理端 /api/deptcategory、/api/customdept 共 17 端点(含 CandidateUsers 候选口)
- 开放端 /oapi/departments 四口:目录/分类/resolve-batch/成员(scope 缺省=select 严档),
  应用×学校授权走 sys_group_app_rel(仅验签不放行跨校)
- SysGroupService.changeIncludeChildren 补学校组类型校验
- 新增单测 83 例全绿(部门26/分类12/目录25/schoolguard12/includeChildren8)
This commit is contained in:
chenx
2026-10-22 09:35:37 +08:00
parent bf3f3344c1
commit b1b769a1b8
36 changed files with 4156 additions and 1 deletions
@@ -0,0 +1,38 @@
-- =====================================================================
-- 部门管理 ·「包含子组」落库(方案:部门管理-包含子组落库方案.md)
-- sys_group 增列 include_children:学校节点上的一个开关,
-- 1 = 部门列表连同下级部门一起列出(默认)/ 0 = 只列一级部门
-- 交付文件,不在本机执行;由人工在 160 执行
-- 执行前请备份;JPA 实体已同名映射,执行后重启后端即可生效
-- =====================================================================
-- ---------------------------------------------------------------------------
-- 1. 增列 include_children
-- 粒度:1 学校 : 1 值,与 status 同为节点自身属性,故直接挂在 sys_group,不另建表
-- 默认 1:与升级前的页面表现一致,存量学校无需回填
-- ⚠ application.yml 是 ddl-auto=update,后端先发也会自动加列,但**不带** NOT NULL/DEFAULT/COMMENT,
-- 存量会留 NULL。因此本脚本建议先执行,保证列定义可控;即使漏执行,前端已按 null = 含子组 兜底
-- ---------------------------------------------------------------------------
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sys_group'
AND COLUMN_NAME = 'include_children');
SET @sql := IF(@col = 0,
'ALTER TABLE sys_group ADD COLUMN include_children TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''包含子组:1=部门列表含下级部门,0=仅本级'' AFTER status',
'SELECT ''include_children already exists'' AS msg');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------------------
-- 2. 核验(可选)
-- ---------------------------------------------------------------------------
-- SHOW COLUMNS FROM sys_group LIKE 'include_children';
-- SELECT COUNT(*) AS total, SUM(include_children = 1) AS include_on FROM sys_group;
-- ---------------------------------------------------------------------------
-- 回滚(仅当确认不再需要该能力时执行;只加列不改列,代码回滚无需删列)
-- ---------------------------------------------------------------------------
-- ALTER TABLE sys_group DROP COLUMN include_children;
@@ -0,0 +1,102 @@
-- =====================================================================
-- UserBase 双来源部门 · CUSTOM 侧建表(任务书 USERBASE_DEPARTMENT_QODER_TASK.md T1 / 规格书 UB-1)
-- 三张新表:ub_custom_dept_category(自定义分类)/ ub_custom_department(自定义部门)
-- / ub_custom_department_member(部门成员关系)
-- 交付文件,不在本机执行;由人工在 160 执行
-- 执行前请备份;JPA 实体与列同名映射,执行后重启后端即可生效
-- ⚠ application.yml 是 ddl-auto=update,后端先发也会自动建表,但**不带** NOT NULL/DEFAULT/COMMENT/唯一键,
-- 且 del_marker 之类参与唯一约束的列必须由本脚本定义,故务必先执行本脚本
-- 口径:school_id = sys_group 中 type=2(学校组)节点的 id,与 AccessScopeService.scopeGroupIds() 同域
-- =====================================================================
-- ---------------------------------------------------------------------------
-- 1. 自定义分类(学校的容器层,本身无启停、无成员)
-- 名称唯一:del_marker 技巧——MySQL 的 uk 对 NULL 不去重,deleted_at 进 uk 挡不住「软删后再激活同名」,
-- 故软删时把 del_marker 置为该行 id,未删行恒为 0,同校同名只能有一条**有效**行;
-- name 列永不改写(软删保留原名,保证历史解析拿到原名、旧引用不指向新对象)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS ub_custom_dept_category (
id INT NOT NULL AUTO_INCREMENT COMMENT '主键(不可变稳定 ID)',
school_id INT NOT NULL COMMENT '学校组 id(sys_group.type=2 的节点 id)',
name VARCHAR(64) NOT NULL COMMENT '分类名称,学校自定;永不改写',
sort_no INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越小越靠前',
is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删标记:0=有效 / 1=已软删(不是业务状态)',
del_marker BIGINT NOT NULL DEFAULT 0 COMMENT '软删时置为该行 id;参与唯一键,保证同名只挡有效行',
deleted_at DATETIME NULL COMMENT '软删时间',
deleted_by INT NULL COMMENT '软删操作人 sys_user.id',
create_user INT NOT NULL COMMENT '创建人 sys_user.id(BaseEntity)',
create_time DATETIME NOT NULL COMMENT '创建时间(BaseEntity)',
update_user INT NULL COMMENT '修改人 sys_user.id(BaseEntity)',
update_time DATETIME NULL COMMENT '修改时间(BaseEntity)',
PRIMARY KEY (id),
UNIQUE KEY uk_school_name_marker (school_id, name, del_marker),
KEY idx_school_sort (school_id, is_deleted, sort_no)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '自定义部门分类(学校级容器,非可选部门)';
-- ---------------------------------------------------------------------------
-- 2. 自定义部门(可配成员、可启停的实际可选对象)
-- 唯一键=同校同分类下未软删的部门名不重复;跨分类、SYSTEM 与 CUSTOM 之间允许重名(A06)
-- enabled 语义直读 1=启用/0=停用,**与 sys_group.status(0=启用/1=停用)方向相反,严禁混用**
-- 软删不级联删成员:成员关系保留在库,由查询侧按部门 is_deleted 过滤(A12)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS ub_custom_department (
id INT NOT NULL AUTO_INCREMENT COMMENT '主键(CUSTOM 侧独立 ID,与 SYSTEM 组 id 不同域,引用须带 departmentType)',
school_id INT NOT NULL COMMENT '学校组 id(冗余自分类,写口服务端复核)',
category_id INT NOT NULL COMMENT '所属分类 ub_custom_dept_category.id,必须同校',
name VARCHAR(64) NOT NULL COMMENT '部门名称,学校自定;永不改写',
sort_no INT NOT NULL DEFAULT 0 COMMENT '同分类内展示顺序,越小越靠前',
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启停:1=启用 / 0=停用(新建默认启用)',
is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删标记:0=有效 / 1=已软删(删除不是第三种业务状态)',
del_marker BIGINT NOT NULL DEFAULT 0 COMMENT '软删时置为该行 id;参与唯一键',
deleted_at DATETIME NULL COMMENT '软删时间',
deleted_by INT NULL COMMENT '软删操作人 sys_user.id',
create_user INT NOT NULL COMMENT '创建人 sys_user.id(BaseEntity)',
create_time DATETIME NOT NULL COMMENT '创建时间(BaseEntity)',
update_user INT NULL COMMENT '修改人 sys_user.id(BaseEntity)',
update_time DATETIME NULL COMMENT '修改时间(BaseEntity)',
PRIMARY KEY (id),
UNIQUE KEY uk_school_category_name_marker (school_id, category_id, name, del_marker),
KEY idx_category_list (category_id, is_deleted, sort_no),
KEY idx_school_list (school_id, is_deleted, enabled)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '学校自定义部门(与 SYSTEM 组织树完全分离)';
-- ---------------------------------------------------------------------------
-- 3. 自定义部门成员关系(引用 sys_user.id,不复制账号)
-- 移除成员=物理删该关系行(本仓无软删惯例,增删审计靠 @OperationLog),不得残留有效成员
-- uk 挡不住并发重复插入之外的场景(如软删部门后重新激活)由服务端逐个复核兜底
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS ub_custom_department_member (
id INT NOT NULL AUTO_INCREMENT COMMENT '主键',
school_id INT NOT NULL COMMENT '学校组 id(与部门同校,写口服务端复核)',
department_id INT NOT NULL COMMENT 'ub_custom_department.id',
user_id INT NOT NULL COMMENT 'sys_user.id(本校教职工,排除学生组成员)',
create_user INT NOT NULL COMMENT '添加人 sys_user.id(BaseEntity)',
create_time DATETIME NOT NULL COMMENT '添加时间(BaseEntity)',
update_user INT NULL COMMENT '修改人 sys_user.id(BaseEntity)',
update_time DATETIME NULL COMMENT '修改时间(BaseEntity)',
PRIMARY KEY (id),
UNIQUE KEY uk_department_user (department_id, user_id),
KEY idx_department (department_id),
KEY idx_school (school_id),
KEY idx_user (user_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '自定义部门成员关系(一个用户可属同校多个部门)';
-- ---------------------------------------------------------------------------
-- 4. 核验(可选,执行后逐条确认)
-- ---------------------------------------------------------------------------
-- SHOW CREATE TABLE ub_custom_dept_category;
-- SHOW CREATE TABLE ub_custom_department;
-- SHOW CREATE TABLE ub_custom_department_member;
-- SELECT TABLE_NAME, TABLE_COMMENT FROM information_schema.TABLES
-- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'ub_custom%';
-- 唯一键生效自检(手工插入应报 1062):
-- INSERT INTO ub_custom_dept_category (school_id, name, create_user, create_time) VALUES (1, '自检分类', 0, NOW());
-- INSERT INTO ub_custom_dept_category (school_id, name, create_user, create_time) VALUES (1, '自检分类', 0, NOW()); -- 期望失败
-- DELETE FROM ub_custom_dept_category WHERE school_id = 1 AND name = '自检分类';
-- ---------------------------------------------------------------------------
-- 回滚(本期未上线,回滚即删表;CUSTOM 数据不与 sys_group 有任何写耦合)
-- ---------------------------------------------------------------------------
-- DROP TABLE IF EXISTS ub_custom_department_member;
-- DROP TABLE IF EXISTS ub_custom_department;
-- DROP TABLE IF EXISTS ub_custom_dept_category;