6.7 KiB
ADR-0016: PermissionSeeder seam — 消除跨模块权限种子化的影子实体
Status
Accepted
Context
crm-dict 的 DictPermissionInitializer 需要将 10 个 dict:* 权限点种子化到权限资源树(sys_menu 物理表)并绑定到内置管理员角色。ADR-0015 禁止 crm-dict 编译期依赖 crm-auth("crm-dict 只依赖 crm-base,不 import crm-auth 的任何类")。
为绕过这一约束,crm-dict 创建了三个影子实体——SysMenuSeed、SysRoleSeed、SysRoleMenuSeed——用 @TableName("sys_menu") 等直接映射 crm-auth 的物理表,通过自己的 Mapper 写入。这导致:
- Schema 泄漏:crm-dict 知道
sys_menu的全部字段名(menuType、parentId、perms、denyBehavior、status、apiUrl、visible)、枚举值(menuType=3表示 button)、默认值(denyBehavior="hide"、status="enabled")。crm-auth 改 schema 时静默破坏 crm-dict 的种子化。 - 逻辑重复:
DictPermissionInitializer与 crm-auth 的DataInitializer有几乎相同的辅助方法(findMenuByName、insertButtonIfAbsent、bindIfAbsent),各 ~100 行。 - ADR-0015 意图被违反:ADR 说"权限码字符串是与 auth 体系的唯一契约",但影子实体把整个
sys_menu物理结构泄漏了过来——契约远不止是"字符串"。
Considered Options
- 影子实体(现状):schema 泄漏 + 逻辑重复,但编译期和运行期都不依赖 crm-auth。否决——schema 泄漏是核心摩擦。
- SQL 迁移脚本(Flyway/Liquibase):项目用 JPA
ddl-auto: update,无迁移框架。引入 Flyway 仅为此需改变全项目的 schema 管理策略,成本不成比例。否决。 @Autowired(required=false):注入PermissionSeeder时找不到实现就跳过种子化。生产环境会出现"种子化偶尔不跑"的幽灵问题。否决。- PermissionSeeder seam(采纳):在 crm-base 提取 interface,crm-auth 提供实现。编译期只依赖 crm-base(interface 所在地),运行期依赖 crm-auth(通过 crm-app 聚合 classpath 提供
PermissionSeederImpl)。
Decision
1. Interface 位置与形状
PermissionSeeder interface 放在 crm-base 的 service 包,PermissionModuleDescriptor 和 ButtonSeed record 放在 crm-base 的 domain.dto 包。
// crm-base
public interface PermissionSeeder {
void seedModule(PermissionModuleDescriptor descriptor);
}
public record PermissionModuleDescriptor(
String catalogName, // 目录名(如"系统管理")
String menuName, // 菜单名(如"数据字典")
String menuPath, // 前端路由(如"/system/dict")
String menuComponent, // 前端组件路径(如"system/dict/index")
int menuSort, // 菜单排序
List<ButtonSeed> buttons, // 按钮权限点(可为空列表)
List<String> bindToRoleCodes // 绑定的内置角色编码(如"ROLE_ADMIN")
) {}
public record ButtonSeed(
String name, // 按钮名称
String perms, // 权限码(如"dict:group:list")
String apiUrl, // 接口 URL(ApiPermissionInterceptor 按此匹配)
int sort // 排序
) {}
声明式:调用方只声明意图("我有这些权限点、挂在这个菜单下、绑给这个角色"),实现方吸收所有 sys_menu schema 细节。
2. 实现位置
PermissionSeederImpl 放在 crm-auth 的 service.impl 包,@Service 注解,直接使用 SysMenuMapper、SysRoleMapper、SysRoleMenuMapper。它拥有:
- 目录节点(type=1)的 find-or-create:按
name + parentId=0查,不存在则建 - 菜单节点(type=2)的 find-or-create:按
name + parentId=catalogId查,不存在则建 - 按钮节点(type=3)的 find-or-create:按
perms查(幂等键),不存在则建 - 角色绑定:按
roleCode查sys_role,找到则bindIfAbsent,找不到 warn 跳过(保留当前行为)
硬编码默认值:denyBehavior="hide"、status="enabled"、visible=true、menuType 枚举值——调用方不感知这些字段。
3. 一次一个菜单
描述符 = 一个 catalog + 一个 menu + 一组 buttons。DataInitializer 调 3 次 seedModule(角色管理 / 菜单管理 / 部门管理),catalog 幂等确保不重复创建。不做多菜单嵌套——当前规模(最多 3 个菜单)不值得引入 List<MenuSeed> 的嵌套结构。
4. 角色创建留在 DataInitializer
seam 只管菜单/按钮/绑定,不管角色创建。DataInitializer 仍负责创建 ROLE_ADMIN(含 builtin=true、dataScope=ALL),然后调 seedModule。DictPermissionInitializer 不再创建角色——它只调 seedModule,seam 按 roleCode 查找角色。
5. 启动顺序
DataInitializer 标 @Order(1),DictPermissionInitializer 标 @Order(10)。确保 ROLE_ADMIN 先创建,后续模块的绑定一次到位。
6. 运行期依赖
crm-dict 编译期只依赖 crm-base(PermissionSeeder interface)。运行期 PermissionSeederImpl 由 crm-auth 提供,通过 crm-app 聚合 classpath 注入。实际系统只通过 crm-app 启动——crm-auth 和 crm-dict 总在同一 classpath 上。
7. 删除的影子实体
crm-dict/domain/entity/SysMenuSeed.javacrm-dict/domain/entity/SysRoleSeed.javacrm-dict/domain/entity/SysRoleMenuSeed.javacrm-dict/mapper/SysMenuSeedMapper.javacrm-dict/mapper/SysRoleSeedMapper.javacrm-dict/mapper/SysRoleMenuSeedMapper.java
DictPermissionInitializer 和 DataInitializer 中的辅助方法(findMenuByName、insertMenu、insertButtonIfAbsent、bindIfAbsent)由 PermissionSeederImpl 吸收,两个初始化器变为声明式薄调用方。
Consequences
- ADR-0015 编译期约束保持:crm-dict 仍只 import crm-base 的类,不 import crm-auth。
- 运行期依赖新增:crm-dict 的
DictPermissionInitializer运行期需要 crm-auth 的PermissionSeederImpl。实际部署通过 crm-app 聚合,不影响生产。crm-dict 独立跑DictApplication时需 crm-auth 在 classpath 上。 - 权限体系可换性更好:换权限引擎只需换
PermissionSeederImpl,crm-dict 代码一行不改。影子实体方案反而硬编码了sys_menu物理结构,更不可换。 - Schema 变更不再跨模块破坏:crm-auth 改
sys_menu字段只影响PermissionSeederImpl,crm-dict 无感。 - 未来模块复用:crm-rule、crm-audit 等新模块种子化权限点时,只需调
seedModule,不需要再造影子实体。 - crm-dict 测试瘦身:
DictPermissionInitializerTest从 H2 集成测试改为 mockPermissionSeeder的单元测试,不再需要在 H2 中建sys_menu/sys_role/sys_role_menu表。