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.

513 lines
28 KiB

3 weeks ago
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>Architecture review — crm-backend-matt (round 2)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true, theme: "neutral", securityLevel: "loose" });
</script>
<style>
.badge-strong { background: #059669; color: #fff; }
.badge-explore { background: #d97706; color: #fff; }
.badge-spec { background: #64748b; color: #fff; }
.badge-done { background: #2563eb; color: #fff; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.label-tag { font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.05em; }
.mod-box { border: 1.5px solid #94a3b8; border-radius: 6px; padding: 6px 10px; text-align: center; font-size: 0.7rem; }
.mod-shallow { border-color: #f59e0b; border-width: 2px; }
.mod-deep { border-color: #0f172a; border-width: 3px; background: #f8fafc; }
.mod-dead { border-color: #dc2626; border-style: dashed; opacity: 0.6; }
.arrow { font-size: 0.65rem; color: #64748b; }
.seam-line { border-top: 2px dashed #94a3b8; }
.leak-line { border-top: 2px solid #dc2626; }
.concern-box { border-radius: 4px; padding: 4px 8px; font-size: 0.65rem; text-align: center; }
</style>
</head>
<body class="bg-stone-50 text-slate-900 font-sans">
<main class="max-w-5xl mx-auto px-6 py-12 space-y-12">
<header class="space-y-3">
<div class="flex items-baseline justify-between">
<h1 class="text-2xl font-bold tracking-tight">架构深化审查 — CRM Backend(第二轮)</h1>
<span class="text-sm text-slate-500">2026-08-13</span>
</div>
<p class="text-sm text-slate-600">
热点来源:最近 7 次提交集中在 <span class="mono">crm-auth</span>(ADR-0017/0018 重构:SystemController、RoleController、PermissionSeeder)和 <span class="mono">crm-rule</span>(公海池新增模块)。
第一轮已完成:LeadTransition 深模块 ✓、DataScopeHelper 删除 ✓。
</p>
<div class="flex flex-wrap gap-3 text-xs">
<span class="badge-strong label-tag px-2 py-0.5 rounded">Strong</span>
<span class="badge-explore label-tag px-2 py-0.5 rounded">Worth exploring</span>
<span class="badge-spec label-tag px-2 py-0.5 rounded">Speculative</span>
</div>
</header>
<section class="space-y-10">
<!-- ═══════════════ CANDIDATE 1 ═══════════════ -->
<article class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-6 space-y-5">
<div class="flex items-center justify-between flex-wrap gap-2">
<h2 class="text-lg font-semibold">1. 将模块档位逻辑从 SysRoleServiceImpl 中抽离</h2>
<span class="badge-strong label-tag px-2 py-0.5 rounded">Strong</span>
</div>
<div class="text-sm text-slate-600 space-y-1">
<span class="label-tag text-slate-400">Files</span><br>
<span class="mono text-xs">crm-auth/.../service/impl/SysRoleServiceImpl.java (247 行)</span><br>
<span class="mono text-xs">crm-auth/.../domain/entity/SysRoleDataScope.java</span><br>
<span class="mono text-xs">crm-auth/.../domain/entity/SysDataScopeModule.java</span>
</div>
<!-- Before / After -->
<div class="grid grid-cols-2 gap-4 mt-2">
<!-- Before -->
<div class="space-y-2">
<p class="label-tag text-amber-600 font-semibold">Before — 数据可见性关注点渗入角色管理</p>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 space-y-2" style="min-height:260px">
<div class="mod-box mod-shallow" style="width:100%">
SysRoleServiceImpl<br>
<span class="text-xs text-slate-400">5 个 mapper 注入</span>
</div>
<div class="flex gap-2 mt-2">
<div class="concern-box bg-blue-100 text-blue-800 flex-1">
角色 CRUD<br><span class="text-xs">saveRole()<br>deleteRoleCascade()</span>
</div>
<div class="concern-box bg-orange-100 text-orange-800 flex-1" style="border: 1.5px solid #f97316">
模块档位管理<br><span class="text-xs">saveRoleScopes()<br>校验 + 持久化<br>50 行</span>
</div>
<div class="concern-box bg-purple-100 text-purple-800 flex-1">
资源分配<br><span class="text-xs">assignResources()</span>
</div>
</div>
<p class="text-xs text-orange-600 mt-2 text-center">模块档位是 ADR-0018 引入的数据权限关注点<br>不属于「角色是什么」</p>
<div class="mt-2 rounded bg-red-50 border border-red-200 p-2 text-xs text-red-700">
getRoleDetail() 需串联 5 个 mapper:<br>
SysRoleMapper + SysRoleMenuMapper + SysRoleDataScopeMapper + SysDataScopeModuleMapper + SysUserRoleMapper
</div>
</div>
</div>
<!-- After -->
<div class="space-y-2">
<p class="label-tag text-emerald-600 font-semibold">After — RoleScopeStore 深模块接管档位</p>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 space-y-2" style="min-height:260px">
<div class="flex gap-2">
<div class="mod-box flex-1 bg-white">
SysRoleServiceImpl<br>
<span class="text-xs text-slate-400">角色 CRUD<br>资源分配</span>
</div>
</div>
<p class="text-center arrow">delegates scope ops ↓</p>
<div class="mod-box mod-deep" style="width:100%">
RoleScopeStore(深模块)<br>
<span class="text-xs text-slate-500">interface: 3 methods<br>
save(roleId, scopes)<br>
load(roleId): List&lt;ModuleScopeDTO&gt;<br>
delete(roleId)</span>
</div>
<p class="text-xs text-slate-400 text-center mt-1">
implementation 吸收:module code 合法性校验<br>
dataScope 枚举校验 · 重复 code 检查 · 批量写入
</p>
</div>
</div>
</div>
<!-- Problem / Solution -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="label-tag text-slate-400">Problem</p>
<p class="mt-1">
ADR-0018 引入「每模块数据档位」后,<span class="mono">SysRoleServiceImpl</span> 承担了
两个不同演化轴:角色的基本属性(名称/编码/内置保护)和模块档位的校验规则(哪些 module code 合法、
dataScope 枚举范围)。<span class="mono">saveRoleScopes</span> 校验逻辑(50行)比
<span class="mono">saveRole</span> 本体更长。<span class="mono">getRoleDetail</span>
为了拼装档位展示名需串查 5 个 mapper。新增一个业务模块只需改 ADR-0018 的注册表,
但角色服务的测试也随之需要额外 mock。
</p>
</div>
<div>
<p class="label-tag text-slate-400">Solution</p>
<p class="mt-1">
提取 <span class="mono">RoleScopeStore</span> 深模块:接口暴露
<span class="mono">save / load / delete</span> 三个方法,
implementation 内部持有 <span class="mono">SysRoleDataScopeMapper</span> +
<span class="mono">SysDataScopeModuleMapper</span>
完整吸收档位校验与持久化。<span class="mono">SysRoleServiceImpl</span> 只需注入
<span class="mono">RoleScopeStore</span> 而非两个 mapper,
测试时一个 mock 替代两个。
</p>
</div>
</div>
<div>
<p class="label-tag text-slate-400">Wins</p>
<div class="mt-1 flex flex-wrap gap-2 text-xs">
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">locality: 档位规则集中在 RoleScopeStore</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">leverage: SysRoleServiceImpl 从 5 mapper → 3 mapper</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">新增业务模块时无需改角色测试</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">deletion test 通过:提取集中复杂度</span>
</div>
</div>
</div>
</article>
<!-- ═══════════════ CANDIDATE 2 ═══════════════ -->
<article class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-6 space-y-5">
<div class="flex items-center justify-between flex-wrap gap-2">
<h2 class="text-lg font-semibold">2. 公海池多值表管理(区域 + 人员)从 LeadPoolServiceImpl 分离</h2>
<span class="badge-strong label-tag px-2 py-0.5 rounded">Strong</span>
</div>
<div class="text-sm text-slate-600 space-y-1">
<span class="label-tag text-slate-400">Files</span><br>
<span class="mono text-xs">crm-rule/.../service/impl/LeadPoolServiceImpl.java (342 行)</span><br>
<span class="mono text-xs">crm-rule/.../service/ILeadPoolService.java</span><br>
<span class="mono text-xs">mapper: LeadPoolProvinceMapper / LeadPoolRegionMapper / LeadPoolMemberMapper</span>
</div>
<!-- Before / After mass diagram -->
<div class="grid grid-cols-2 gap-4 mt-2">
<div class="space-y-2">
<p class="label-tag text-amber-600 font-semibold">Before — 3 个关注点挤在 342 行</p>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4" style="min-height:280px">
<div class="flex justify-center">
<div class="w-52">
<div class="mod-box mod-shallow text-center mb-2">
LeadPoolServiceImpl<br>
<span class="text-red-500 text-xs font-bold">342 行 · 5 个 mapper</span>
</div>
<!-- 质量图 -->
<div class="rounded overflow-hidden border border-slate-200">
<div class="bg-blue-100 text-blue-800 text-xs px-2 py-1.5">
① 公海池 CRUD<br>
<span class="text-slate-500">~80 行(pagePools, savePool, delete...)</span>
</div>
<div class="bg-orange-100 text-orange-800 text-xs px-2 py-1.5 border-t border-orange-200">
② 省市区域多值表<br>
<span class="text-slate-500">~90 行(province + region 全量替换)</span>
</div>
<div class="bg-purple-100 text-purple-800 text-xs px-2 py-1.5 border-t border-purple-200">
③ 成员管理(负责人+协作人)<br>
<span class="text-slate-500">~60 行(member 全量替换 + roleType)</span>
</div>
<div class="bg-slate-100 text-slate-600 text-xs px-2 py-1.5 border-t border-slate-200">
④ 展示字段填充<br>
<span class="text-slate-500">~60 行(用户名 + 部门名)</span>
</div>
<div class="bg-green-100 text-green-800 text-xs px-2 py-1.5 border-t border-green-200">
⑤ 事件发布(PoolChangedEvent)<br>
<span class="text-slate-500">~20 行</span>
</div>
</div>
<p class="text-xs text-amber-600 mt-2 text-center">
①是公海池本质;②③因不同业务规则演化
</p>
</div>
</div>
</div>
</div>
<div class="space-y-2">
<p class="label-tag text-emerald-600 font-semibold">After — 区域与人员管理各自深化</p>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4" style="min-height:280px">
<div class="flex flex-col items-center gap-2">
<div class="mod-box bg-white" style="width:180px">
LeadPoolServiceImpl<br>
<span class="text-xs text-slate-400">CRUD + 事件 + 展示填充<br>~160 行</span>
</div>
<div class="flex gap-3">
<div class="text-center">
<p class="arrow">delegate ↓</p>
<div class="mod-box mod-deep" style="width:120px">
PoolGeographyStore<br>
<span class="text-xs text-slate-500">
load(poolId)<br>
replace(poolId, regions)
</span>
</div>
<p class="text-xs text-slate-400 mt-1">province +<br>city region</p>
</div>
<div class="text-center">
<p class="arrow">delegate ↓</p>
<div class="mod-box mod-deep" style="width:120px">
PoolMemberStore<br>
<span class="text-xs text-slate-500">
load(poolId)<br>
replace(poolId, members)
</span>
</div>
<p class="text-xs text-slate-400 mt-1">owner +<br>collaborators</p>
</div>
</div>
<p class="text-xs text-slate-500 mt-2 text-center">
接口各 2 个方法 · 各自持有专属 mapper<br>
LeadPoolServiceImpl 从 5 mapper → 2 mapper
</p>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="label-tag text-slate-400">Problem</p>
<p class="mt-1">
<span class="mono">LeadPoolServiceImpl</span> 接口语义是「管理公海池」,
但实现承担了两个完全独立的多值关联表管理:省市区域(<span class="mono">lead_pool_province</span>
+ <span class="mono">lead_pool_region</span>)和成员(<span class="mono">lead_pool_member</span>)。
这两类数据的修改规则(全量替换语义)完全相同,但业务含义不同。结果是 5 个 mapper 注入,
342 行代码,理解「保存公海池」需要追踪 4 个关注点。
</p>
</div>
<div>
<p class="label-tag text-slate-400">Solution</p>
<p class="mt-1">
提取 <span class="mono">PoolGeographyStore</span><span class="mono">PoolMemberStore</span>
两个深模块,各自暴露 <span class="mono">load / replace</span> 两个方法,
内部管理对应的 mapper 和全量替换逻辑。
<span class="mono">LeadPoolServiceImpl</span> 只保留公海池本体 CRUD、事件发布、展示字段填充,
注入依赖从 5 个降到 2 个(IAuthUserService + ISysDeptService,用于展示字段填充)。
</p>
</div>
</div>
<div>
<p class="label-tag text-slate-400">Wins</p>
<div class="mt-1 flex flex-wrap gap-2 text-xs">
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">locality: 区域规则变更 → 只改 PoolGeographyStore</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">leverage: 5 mapper → 2 个注入</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">342 行 → ~160 行核心</span>
<span class="bg-emerald-50 text-emerald-700 px-2 py-1 rounded">多值表 load/replace 逻辑零散注释消失</span>
</div>
</div>
</div>
</article>
<!-- ═══════════════ CANDIDATE 3 ═══════════════ -->
<article class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-6 space-y-5">
<div class="flex items-center justify-between flex-wrap gap-2">
<h2 class="text-lg font-semibold">3. ResourceServiceImpl 图标上传提取为 IconRepository 端口</h2>
<span class="badge-explore label-tag px-2 py-0.5 rounded">Worth exploring</span>
</div>
<div class="text-sm text-slate-600 space-y-1">
<span class="label-tag text-slate-400">Files</span><br>
<span class="mono text-xs">crm-auth/.../service/impl/ResourceServiceImpl.java (268 行)</span>
</div>
<div class="grid grid-cols-2 gap-4 mt-2">
<div class="space-y-2">
<p class="label-tag text-amber-600 font-semibold">Before — 两种依赖混在一个类</p>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4" style="min-height:220px">
<div class="flex flex-col items-center gap-2">
<div class="mod-box mod-shallow" style="width:200px">
ResourceServiceImpl<br>
<span class="text-xs text-slate-400">注入: ISysMenuService + SysRoleMenuMapper<br>+ FileApi + ApiPermissionCache</span>
</div>
<div class="flex gap-3 mt-2">
<div class="mod-box bg-blue-50" style="width:80px">
资源树 CRUD<br>
<span class="text-xs text-slate-400">listAll<br>save<br>delete<br>~130行</span>
</div>
<div class="mod-box bg-orange-50" style="width:100px; border-color:#f97316; border-width:2px">
图标上传<br>
<span class="text-xs text-orange-600">格式/大小校验<br>FileApi 调用<br>~40行</span>
</div>
</div>
<p class="text-xs text-orange-600 mt-1 text-center">
uploadIcon 是唯一使用 FileApi 的方法<br>
资源树 CRUD 不需要文件存储依赖
</p>
</div>
</div>
</div>
<div class="space-y-2">
<p class="label-tag text-emerald-600 font-semibold">After — 存储端口隔离</p>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4" style="min-height:220px">
<div class="flex flex-col items-center gap-2">
<div class="mod-box bg-white" style="width:180px">
ResourceServiceImpl<br>
<span class="text-xs text-slate-400">ISysMenuService + SysRoleMenuMapper<br>+ IconRepository</span>
</div>
<p class="arrow">delegates store ↓</p>
<div class="mod-box mod-deep" style="width:180px">
IconRepository(端口)<br>
<span class="text-xs text-slate-500">
store(file, name, contentType)<br>: FileInfoDTO
</span>
</div>
<p class="arrow">↓ adapts</p>
<div class="mod-box bg-slate-100" style="width:180px">
IconRepositoryImpl<br>
<span class="text-xs text-slate-400">
格式校验 + 大小校验<br>
FileApi.upload("resources/icon")
</span>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="label-tag text-slate-400">Problem</p>
<p class="mt-1">
<span class="mono">ResourceServiceImpl</span> 的主要职责是管理权限资源树(菜单 catalog/menu/button 的 CRUD)。
<span class="mono">uploadIcon</span> 把文件存储关注点(扩展名白名单、500KB 限制、FileApi 调用)
引入同一个类。这两个关注点有独立的演化轴:资源树结构变化不影响图标格式规则,
但目前修改其中一个需要理解整个 268 行类。
</p>
</div>
<div>
<p class="label-tag text-slate-400">Solution</p>
<p class="mt-1">
提取 <span class="mono">IconRepository</span> 端口(1 方法),由
<span class="mono">IconRepositoryImpl</span> 适配 <span class="mono">FileApi</span>
内部持有格式/大小校验逻辑。<span class="mono">ResourceServiceImpl</span> 依赖端口而非
<span class="mono">FileApi</span>——一个 mock 取代两个依赖(FileApi + 校验逻辑),
且测试不再需要引入 crm-file 模块的 bean。
</p>
</div>
</div>
<div>
<p class="label-tag text-slate-400">Wins</p>
<div class="mt-1 flex flex-wrap gap-2 text-xs">
<span class="bg-amber-50 text-amber-700 px-2 py-1 rounded">一个真实 seam(资源树 vs 文件存储)</span>
<span class="bg-amber-50 text-amber-700 px-2 py-1 rounded">ResourceServiceImpl 测试不再需要 FileApi mock</span>
<span class="bg-amber-50 text-amber-700 px-2 py-1 rounded">图标格式规则集中在 IconRepositoryImpl</span>
</div>
</div>
</div>
</article>
<!-- ═══════════════ CANDIDATE 4 ═══════════════ -->
<article class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-6 space-y-5">
<div class="flex items-center justify-between flex-wrap gap-2">
<h2 class="text-lg font-semibold">4. RoleController 的 JSON 解析违反 ADR-0017</h2>
<span class="badge-spec label-tag px-2 py-0.5 rounded">Speculative</span>
</div>
<div class="text-sm text-slate-600 space-y-1">
<span class="label-tag text-slate-400">Files</span><br>
<span class="mono text-xs">crm-auth/.../controller/RoleController.java (120 行)</span><br>
<span class="mono text-xs">crm-auth/.../domain/param/RoleParam.java</span>
</div>
<div class="grid grid-cols-2 gap-4 mt-2">
<div class="space-y-2">
<p class="label-tag text-amber-600 font-semibold">Before — ObjectMapper 泄漏到 Controller</p>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 font-mono text-xs space-y-1" style="min-height:200px">
<p class="text-slate-400">// RoleController.java</p>
<p><span class="text-purple-600">private final</span> ObjectMapper objectMapper;</p>
<p class="mt-2"><span class="text-slate-400">// @RequestParam String moduleScopes</span></p>
<p><span class="text-blue-600">@PostMapping</span>("/saveOrUpdate")</p>
<p><span class="text-green-600">public</span> Result saveOrUpdate(</p>
<p class="pl-4">...params,</p>
<p class="pl-4"><span class="text-orange-600">@RequestParam</span> String moduleScopes</p>
<p>) {</p>
<p class="pl-4">List&lt;ModuleScopeDTO&gt; scopes =</p>
<p class="pl-8">parseModuleScopes(moduleScopes); <span class="text-red-500">// 私有方法</span></p>
<p>}</p>
<div class="mt-2 p-2 rounded bg-red-100 text-red-700 text-xs">
Controller 手工调用 objectMapper.readValue()<br>
ADR-0017 要求:收参用 @RequestParam + Param 对象
</div>
</div>
</div>
<div class="space-y-2">
<p class="label-tag text-emerald-600 font-semibold">After — Param 对象承接解析职责</p>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 font-mono text-xs space-y-1" style="min-height:200px">
<p class="text-slate-400">// RoleParam.java(或专用 SaveRoleParam)</p>
<p><span class="text-blue-600">@Getter @Setter</span></p>
<p><span class="text-purple-600">public class</span> SaveRoleParam {</p>
<p class="pl-4"><span class="text-blue-600">@JsonDeserialize</span></p>
<p class="pl-4">List&lt;ModuleScopeDTO&gt; moduleScopes;</p>
<p>}</p>
<p class="mt-2 text-slate-400">// Controller:</p>
<p><span class="text-blue-600">@PostMapping</span>("/saveOrUpdate")</p>
<p><span class="text-green-600">public</span> Result saveOrUpdate(</p>
<p class="pl-4"><span class="text-orange-600">SaveRoleParam</span> param</p>
<p>) { ... }</p>
<div class="mt-2 p-2 rounded bg-green-100 text-green-700 text-xs">
Controller 无 ObjectMapper 依赖<br>
与 ADR-0017 对齐
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="label-tag text-slate-400">Problem</p>
<p class="mt-1">
<span class="mono">RoleController</span> 注入了 <span class="mono">ObjectMapper</span> 专门用于
<span class="mono">parseModuleScopes</span>——把 JSON 字符串反序列化为
<span class="mono">List&lt;ModuleScopeDTO&gt;</span>
ADR-0017 明确「写操作 POST + @RequestParam,收参对象化」。
Controller 层承担序列化关注点是其余 Controller 没有的例外。
</p>
</div>
<div>
<p class="label-tag text-slate-400">Solution</p>
<p class="mt-1">
引入 <span class="mono">SaveRoleParam</span>(或给 <span class="mono">RoleParam</span> 补字段)绑定
<span class="mono">moduleScopes</span>,使用 Spring MVC 的 <span class="mono">@RequestBody</span>(如接受切换为 JSON body)或
自定义 <span class="mono">@InitBinder</span> 解析。Controller 不再持有 ObjectMapper。
</p>
</div>
</div>
<div class="rounded-lg bg-amber-50 border border-amber-200 px-3 py-2 text-xs text-amber-800">
<strong>注意:</strong>ADR-0017 决策前端已按「表单字段」集成,切换为 JSON body 需前后端协调。
标记为 Speculative 的原因是:收益(移除一个 ObjectMapper 依赖)较小,
但若前后端本次有协调窗口则顺手可改。
</div>
<div>
<p class="label-tag text-slate-400">Wins</p>
<div class="mt-1 flex flex-wrap gap-2 text-xs">
<span class="bg-slate-100 text-slate-700 px-2 py-1 rounded">Controller 无序列化依赖</span>
<span class="bg-slate-100 text-slate-700 px-2 py-1 rounded">与 ADR-0017 完全对齐</span>
<span class="bg-slate-100 text-slate-700 px-2 py-1 rounded">Param 对象可独立校验测试</span>
</div>
</div>
</div>
</article>
</section>
<!-- ═══════════════ TOP RECOMMENDATION ═══════════════ -->
<section class="bg-slate-900 text-slate-100 rounded-xl p-8 space-y-4">
<p class="label-tag text-slate-400">Top recommendation</p>
<h2 class="text-xl font-bold">先做 Candidate 1(RoleScopeStore),再做 Candidate 2(公海池多值表)</h2>
<div class="space-y-3 text-sm text-slate-300">
<p>
<strong class="text-emerald-400">Candidate 1</strong> 是 ADR-0018 遗留的直接后果——
「模块档位」是全新引入的数据权限关注点,但它的校验和持久化逻辑遗留在角色管理服务中。
热点(<span class="mono">SysRoleServiceImpl</span> 在最近 3 次提交中被改动)表明这个文件仍在增长。
提取 <span class="mono">RoleScopeStore</span> 是 ADR-0018 的「清账」动作,接口极小(3 方法),
风险低,收益直接——角色测试从 5 个 mapper mock 降到 3 个。
</p>
<p>
<strong class="text-emerald-400">Candidate 2</strong><span class="mono">crm-rule</span> 模块的头号深化机会。
公海池是业务核心规则的载体,预期还会增加区域规则和成员规则。342 行的现状已经让
「改区域验证逻辑」和「改成员角色逻辑」需要在同一文件里定位。提取两个深模块后,
LeadPoolServiceImpl 可以缩到约 160 行,且区域规则与人员规则的测试各自独立。
</p>
<p class="text-slate-400 text-xs">
Candidate 3(IconRepository)值得在 Candidate 1/2 之后考虑;Candidate 4 留到前后端有协调窗口时处理。
</p>
</div>
</section>
</main>
</body>
</html>