You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

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 创建了三个影子实体——SysMenuSeedSysRoleSeedSysRoleMenuSeed——用 @TableName("sys_menu") 等直接映射 crm-auth 的物理表,通过自己的 Mapper 写入。这导致:

  1. Schema 泄漏:crm-dict 知道 sys_menu 的全部字段名(menuTypeparentIdpermsdenyBehaviorstatusapiUrlvisible)、枚举值(menuType=3 表示 button)、默认值(denyBehavior="hide"status="enabled")。crm-auth 改 schema 时静默破坏 crm-dict 的种子化。
  2. 逻辑重复DictPermissionInitializer 与 crm-auth 的 DataInitializer 有几乎相同的辅助方法(findMenuByNameinsertButtonIfAbsentbindIfAbsent),各 ~100 行。
  3. 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-baseservice 包,PermissionModuleDescriptorButtonSeed 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-authservice.impl 包,@Service 注解,直接使用 SysMenuMapperSysRoleMapperSysRoleMenuMapper。它拥有:

  • 目录节点(type=1)的 find-or-create:按 name + parentId=0 查,不存在则建
  • 菜单节点(type=2)的 find-or-create:按 name + parentId=catalogId 查,不存在则建
  • 按钮节点(type=3)的 find-or-create:按 perms 查(幂等键),不存在则建
  • 角色绑定:按 roleCodesys_role,找到则 bindIfAbsent,找不到 warn 跳过(保留当前行为)

硬编码默认值:denyBehavior="hide"status="enabled"visible=truemenuType 枚举值——调用方不感知这些字段。

3. 一次一个菜单

描述符 = 一个 catalog + 一个 menu + 一组 buttons。DataInitializer 调 3 次 seedModule(角色管理 / 菜单管理 / 部门管理),catalog 幂等确保不重复创建。不做多菜单嵌套——当前规模(最多 3 个菜单)不值得引入 List<MenuSeed> 的嵌套结构。

4. 角色创建留在 DataInitializer

seam 只管菜单/按钮/绑定,不管角色创建。DataInitializer 仍负责创建 ROLE_ADMIN(含 builtin=truedataScope=ALL),然后调 seedModuleDictPermissionInitializer 不再创建角色——它只调 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.java
  • crm-dict/domain/entity/SysRoleSeed.java
  • crm-dict/domain/entity/SysRoleMenuSeed.java
  • crm-dict/mapper/SysMenuSeedMapper.java
  • crm-dict/mapper/SysRoleSeedMapper.java
  • crm-dict/mapper/SysRoleMenuSeedMapper.java

DictPermissionInitializerDataInitializer 中的辅助方法(findMenuByNameinsertMenuinsertButtonIfAbsentbindIfAbsent)由 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 集成测试改为 mock PermissionSeeder 的单元测试,不再需要在 H2 中建 sys_menu/sys_role/sys_role_menu 表。