架构深化审查 — CRM Backend(第二轮)

2026-08-13

热点来源:最近 7 次提交集中在 crm-auth(ADR-0017/0018 重构:SystemController、RoleController、PermissionSeeder)和 crm-rule(公海池新增模块)。 第一轮已完成:LeadTransition 深模块 ✓、DataScopeHelper 删除 ✓。

Strong Worth exploring Speculative

1. 将模块档位逻辑从 SysRoleServiceImpl 中抽离

Strong
Files
crm-auth/.../service/impl/SysRoleServiceImpl.java (247 行)
crm-auth/.../domain/entity/SysRoleDataScope.java
crm-auth/.../domain/entity/SysDataScopeModule.java

Before — 数据可见性关注点渗入角色管理

SysRoleServiceImpl
5 个 mapper 注入
角色 CRUD
saveRole()
deleteRoleCascade()
模块档位管理
saveRoleScopes()
校验 + 持久化
50 行
资源分配
assignResources()

模块档位是 ADR-0018 引入的数据权限关注点
不属于「角色是什么」

getRoleDetail() 需串联 5 个 mapper:
SysRoleMapper + SysRoleMenuMapper + SysRoleDataScopeMapper + SysDataScopeModuleMapper + SysUserRoleMapper

After — RoleScopeStore 深模块接管档位

SysRoleServiceImpl
角色 CRUD
资源分配

delegates scope ops ↓

RoleScopeStore(深模块)
interface: 3 methods
save(roleId, scopes)
load(roleId): List<ModuleScopeDTO>
delete(roleId)

implementation 吸收:module code 合法性校验
dataScope 枚举校验 · 重复 code 检查 · 批量写入

Problem

ADR-0018 引入「每模块数据档位」后,SysRoleServiceImpl 承担了 两个不同演化轴:角色的基本属性(名称/编码/内置保护)和模块档位的校验规则(哪些 module code 合法、 dataScope 枚举范围)。saveRoleScopes 校验逻辑(50行)比 saveRole 本体更长。getRoleDetail 为了拼装档位展示名需串查 5 个 mapper。新增一个业务模块只需改 ADR-0018 的注册表, 但角色服务的测试也随之需要额外 mock。

Solution

提取 RoleScopeStore 深模块:接口暴露 save / load / delete 三个方法, implementation 内部持有 SysRoleDataScopeMapper + SysDataScopeModuleMapper, 完整吸收档位校验与持久化。SysRoleServiceImpl 只需注入 RoleScopeStore 而非两个 mapper, 测试时一个 mock 替代两个。

Wins

locality: 档位规则集中在 RoleScopeStore leverage: SysRoleServiceImpl 从 5 mapper → 3 mapper 新增业务模块时无需改角色测试 deletion test 通过:提取集中复杂度

2. 公海池多值表管理(区域 + 人员)从 LeadPoolServiceImpl 分离

Strong
Files
crm-rule/.../service/impl/LeadPoolServiceImpl.java (342 行)
crm-rule/.../service/ILeadPoolService.java
mapper: LeadPoolProvinceMapper / LeadPoolRegionMapper / LeadPoolMemberMapper

Before — 3 个关注点挤在 342 行

LeadPoolServiceImpl
342 行 · 5 个 mapper
① 公海池 CRUD
~80 行(pagePools, savePool, delete...)
② 省市区域多值表
~90 行(province + region 全量替换)
③ 成员管理(负责人+协作人)
~60 行(member 全量替换 + roleType)
④ 展示字段填充
~60 行(用户名 + 部门名)
⑤ 事件发布(PoolChangedEvent)
~20 行

①是公海池本质;②③因不同业务规则演化

After — 区域与人员管理各自深化

LeadPoolServiceImpl
CRUD + 事件 + 展示填充
~160 行

delegate ↓

PoolGeographyStore
load(poolId)
replace(poolId, regions)

province +
city region

delegate ↓

PoolMemberStore
load(poolId)
replace(poolId, members)

owner +
collaborators

接口各 2 个方法 · 各自持有专属 mapper
LeadPoolServiceImpl 从 5 mapper → 2 mapper

Problem

LeadPoolServiceImpl 接口语义是「管理公海池」, 但实现承担了两个完全独立的多值关联表管理:省市区域(lead_pool_province + lead_pool_region)和成员(lead_pool_member)。 这两类数据的修改规则(全量替换语义)完全相同,但业务含义不同。结果是 5 个 mapper 注入, 342 行代码,理解「保存公海池」需要追踪 4 个关注点。

Solution

提取 PoolGeographyStorePoolMemberStore 两个深模块,各自暴露 load / replace 两个方法, 内部管理对应的 mapper 和全量替换逻辑。 LeadPoolServiceImpl 只保留公海池本体 CRUD、事件发布、展示字段填充, 注入依赖从 5 个降到 2 个(IAuthUserService + ISysDeptService,用于展示字段填充)。

Wins

locality: 区域规则变更 → 只改 PoolGeographyStore leverage: 5 mapper → 2 个注入 342 行 → ~160 行核心 多值表 load/replace 逻辑零散注释消失

3. ResourceServiceImpl 图标上传提取为 IconRepository 端口

Worth exploring
Files
crm-auth/.../service/impl/ResourceServiceImpl.java (268 行)

Before — 两种依赖混在一个类

ResourceServiceImpl
注入: ISysMenuService + SysRoleMenuMapper
+ FileApi + ApiPermissionCache
资源树 CRUD
listAll
save
delete
~130行
图标上传
格式/大小校验
FileApi 调用
~40行

uploadIcon 是唯一使用 FileApi 的方法
资源树 CRUD 不需要文件存储依赖

After — 存储端口隔离

ResourceServiceImpl
ISysMenuService + SysRoleMenuMapper
+ IconRepository

delegates store ↓

IconRepository(端口)
store(file, name, contentType)
: FileInfoDTO

↓ adapts

IconRepositoryImpl
格式校验 + 大小校验
FileApi.upload("resources/icon")

Problem

ResourceServiceImpl 的主要职责是管理权限资源树(菜单 catalog/menu/button 的 CRUD)。 但 uploadIcon 把文件存储关注点(扩展名白名单、500KB 限制、FileApi 调用) 引入同一个类。这两个关注点有独立的演化轴:资源树结构变化不影响图标格式规则, 但目前修改其中一个需要理解整个 268 行类。

Solution

提取 IconRepository 端口(1 方法),由 IconRepositoryImpl 适配 FileApi, 内部持有格式/大小校验逻辑。ResourceServiceImpl 依赖端口而非 FileApi——一个 mock 取代两个依赖(FileApi + 校验逻辑), 且测试不再需要引入 crm-file 模块的 bean。

Wins

一个真实 seam(资源树 vs 文件存储) ResourceServiceImpl 测试不再需要 FileApi mock 图标格式规则集中在 IconRepositoryImpl

4. RoleController 的 JSON 解析违反 ADR-0017

Speculative
Files
crm-auth/.../controller/RoleController.java (120 行)
crm-auth/.../domain/param/RoleParam.java

Before — ObjectMapper 泄漏到 Controller

// RoleController.java

private final ObjectMapper objectMapper;

// @RequestParam String moduleScopes

@PostMapping("/saveOrUpdate")

public Result saveOrUpdate(

...params,

@RequestParam String moduleScopes

) {

List<ModuleScopeDTO> scopes =

parseModuleScopes(moduleScopes); // 私有方法

}

Controller 手工调用 objectMapper.readValue()
ADR-0017 要求:收参用 @RequestParam + Param 对象

After — Param 对象承接解析职责

// RoleParam.java(或专用 SaveRoleParam)

@Getter @Setter

public class SaveRoleParam {

@JsonDeserialize

List<ModuleScopeDTO> moduleScopes;

}

// Controller:

@PostMapping("/saveOrUpdate")

public Result saveOrUpdate(

SaveRoleParam param

) { ... }

Controller 无 ObjectMapper 依赖
与 ADR-0017 对齐

Problem

RoleController 注入了 ObjectMapper 专门用于 parseModuleScopes——把 JSON 字符串反序列化为 List<ModuleScopeDTO>。 ADR-0017 明确「写操作 POST + @RequestParam,收参对象化」。 Controller 层承担序列化关注点是其余 Controller 没有的例外。

Solution

引入 SaveRoleParam(或给 RoleParam 补字段)绑定 moduleScopes,使用 Spring MVC 的 @RequestBody(如接受切换为 JSON body)或 自定义 @InitBinder 解析。Controller 不再持有 ObjectMapper。

注意:ADR-0017 决策前端已按「表单字段」集成,切换为 JSON body 需前后端协调。 标记为 Speculative 的原因是:收益(移除一个 ObjectMapper 依赖)较小, 但若前后端本次有协调窗口则顺手可改。

Wins

Controller 无序列化依赖 与 ADR-0017 完全对齐 Param 对象可独立校验测试

Top recommendation

先做 Candidate 1(RoleScopeStore),再做 Candidate 2(公海池多值表)

Candidate 1 是 ADR-0018 遗留的直接后果—— 「模块档位」是全新引入的数据权限关注点,但它的校验和持久化逻辑遗留在角色管理服务中。 热点(SysRoleServiceImpl 在最近 3 次提交中被改动)表明这个文件仍在增长。 提取 RoleScopeStore 是 ADR-0018 的「清账」动作,接口极小(3 方法), 风险低,收益直接——角色测试从 5 个 mapper mock 降到 3 个。

Candidate 2crm-rule 模块的头号深化机会。 公海池是业务核心规则的载体,预期还会增加区域规则和成员规则。342 行的现状已经让 「改区域验证逻辑」和「改成员角色逻辑」需要在同一文件里定位。提取两个深模块后, LeadPoolServiceImpl 可以缩到约 160 行,且区域规则与人员规则的测试各自独立。

Candidate 3(IconRepository)值得在 Candidate 1/2 之后考虑;Candidate 4 留到前后端有协调窗口时处理。