架构深化审查 — CRM Backend

2026-08-13
module seam leakage dead code deep module

热点来源:最近 10 次提交集中在 crm-lead 模块(线索全生命周期)。审查范围:crm-lead / crm-base / crm-file。

1. 将线索状态机坍缩为深模块

Strong in-process
Files
crm-lead/.../service/impl/LeadServiceImpl.java (738 行)
crm-lead/.../job/LeadRecycleJob.java
crm-lead/.../job/LeadExpireJob.java
crm-lead/.../event/PoolChangedEventListener.java

Before — 状态机散落 9 处

flowchart TD subgraph SVC["LeadServiceImpl (738行)"] CL["claimLead()"] --> CAS1["casUpdate()"] AS["assignToPool()"] --> CAS1 AU["assignToUser()"] --> CAS2["inline CAS"] FB["submitFeedback()"] --> CAS2 CV["convertToOpportunity()"] --> CAS1 RL["releaseLead()"] --> CAS1 AC["activateLead()"] --> CAS2 end subgraph JOB["定时任务 (绕过 service)"] RJ["LeadRecycleJob"] --> MAGIC1["magic: status IN 3,4"] EJ["LeadExpireJob"] --> MAGIC2["magic: NOT IN 5,6"] end CAS1 --> DB[(lead table)] CAS2 --> DB MAGIC1 --> DB MAGIC2 --> DB style CAS2 fill:#fef3c7 style MAGIC1 fill:#fecaca style MAGIC2 fill:#fecaca style SVC fill:#f8fafc style JOB fill:#fff7ed
  • • 7 方法各自 if-else 校验状态、构建 wrapper
  • • 2 Job 绕过 service,用魔法数字硬编码状态
  • • CAS 调用不一致:4 处用 casUpdate(),3 处内联

After — 一个深模块收拢全部迁移

flowchart TD subgraph SVC["LeadServiceImpl (瘦身)"] CL["claimLead()"] FB["submitFeedback()"] RL["releaseLead()"] OTHER["...其他方法"] end subgraph SM["LeadTransition (深模块)"] RULES["迁移规则表\n(from, action) to (to, sideEffects)"] EXEC["execute(leadId, action, ctx)\nCAS + side-effects + history"] end subgraph JOB["定时任务"] RJ["LeadRecycleJob"] EJ["LeadExpireJob"] end CL --> SM FB --> SM RL --> SM OTHER --> SM RJ --> SM EJ --> SM EXEC --> DB[(lead table)] style SM fill:#0f172a,color:#e2e8f0 style RULES fill:#1e293b,color:#e2e8f0 style EXEC fill:#1e293b,color:#e2e8f0
  • • 9 个调用点 to 1 个 interface
  • • 状态规则集中声明,魔法数字消除
  • • 纯逻辑可零 mock 测试

Problem

7 态状态机的迁移规则隐式散落在 7 个 service 方法 + 2 个定时任务中。理解"从已领取能走到哪"需要读 5 个方法。Job 用魔法数字 3,4 替代 STATUS_CLAIMED。CAS 调用方式不统一——4 处走 casUpdate() helper,3 处内联 baseMapper.update()

Solution

提取 LeadTransition 深模块:声明全部 (from, action) to (to, sideEffects) 规则,暴露 execute(leadId, action) 一个方法做 CAS + 副作用 + history。Service 和 Job 都调它。

Wins

locality: 迁移 bug 集中到一个模块 leverage: 9 调用点 to 1 interface interface 缩窄;implementation 吸收 wrapper 零 mock 测试迁移规则 消除魔法数字

2. 删除遗留的 DataScopeHelper 影子上下文

Strong dead code
Files
crm-base/.../security/DataScopeHelper.java (141 行)
crm-lead/.../service/impl/LeadServiceImpl.java (line 14 死 import)

Before — 两套并行上下文

DataScopeHelper
141行 死代码

initContext() 无人调
shouldSkip() 无人调
registerTable() 无人调

DataVisibilityContext

load() / clear()
currentScope(module)
ADR-0018

读者困惑:该用哪个?

After — 一套上下文

DataVisibilityContext
唯一真相源

DataVisibility (值对象)
VisibilityScope (过滤结果)
DataScopeTables (表声明)

Problem

ADR-0018 将数据权限从单档位改为按模块配置时,引入了 DataVisibilityContext + DataVisibility,但旧的 DataScopeHelper(141 行,6 个 ThreadLocal)未被清除。代码中零调用——唯一引用是 LeadServiceImpl 的一个死 import。

Solution

删除 DataScopeHelper,移除 LeadServiceImpl 的死 import。deletion test:删除它不移动任何复杂度——它已经是死的。

Wins

locality: 一套上下文系统 删除 141 行死代码 + 1 死 import 新读者不再面临虚假选择

3. 缩略图读取逻辑从 FileApiImpl 分离

Worth exploring in-process
Files
crm-file/.../service/impl/FileApiImpl.java (551 行)

Before — 两个职责挤在一个模块

FileApiImpl (551行)
文件CRUD
upload/download
preview/delete
分片上传
缩略图读取
PENDING轮询
READY读取
FAILED占位

~100行轮询逻辑与文件CRUD混在一起

After — 缩略图读取独立深模块

FileApiImpl
~400行
文件CRUD
ThumbnailReader
getThumbnail(fileId)
轮询/占位/缓存

interface: 1 方法
implementation: 吸收轮询逻辑

Problem

FileApiImpl 同时承担文件 CRUD 和缩略图读取两个职责。缩略图读取有独立的状态机(PENDING to READY/FAILED/UNSUPPORTED)和同步兜底轮询逻辑(~100行),与文件上传/下载无业务关联。

Solution

提取 ThumbnailReader 模块,暴露 getThumbnail(fileId) 一个方法。FileApiImpl 委托给它。ADR-0013 的架构决策不被推翻——只是把读取侧从文件模块中拆出。

Wins

locality: 缩略图状态逻辑集中 FileApiImpl 从 551 行瘦身至 ~400 缩略图读取可独立测试

4. 线索操作日志写入收拢为统一模块

Worth exploring in-process
Files
crm-lead/.../service/impl/LeadServiceImpl.java (writeHistory + buildDetail)
crm-lead/.../job/LeadRecycleJob.java (inline history insert)
crm-lead/.../job/LeadExpireJob.java (inline history insert)

Before — 3 处重复写入

LeadServiceImpl
writeHistory()
+ buildDetail()
RecycleJob
手拼JSON
inline insert
ExpireJob
手拼JSON
inline insert

同一模式 x 3;Job 手拼 JSON 绕过 ObjectMapper

After — 统一 LeadHistoryWriter

LeadServiceImpl
RecycleJob
ExpireJob

all delegate down

LeadHistoryWriter
write(leadId, type, userId, detail)
统一 JSON 序列化

Problem

操作日志写入模式在 3 处重复。两个 Job 手拼 JSON 字符串("{\"ownerUserIdBefore\":" + lead.getOwnerUserId())绕过 ObjectMapper,与 Service 的 buildDetail() 不一致。若 Candidate 1 先做,此问题部分消解。

Solution

提取 LeadHistoryWriter 模块:write(leadId, type, userId, detail) 一个方法,内部统一用 ObjectMapper 序列化 detail。3 个调用点委托给它。

Wins

locality: 日志格式集中 消除手拼 JSON 3 调用点 to 1 interface
注意:若先做 Candidate 1(状态机坍缩),Job 的 history 写入会被状态机模块收拢,本候选项的大部分价值会被吸收。建议在 Candidate 1 之后评估是否仍需独立提取。

Top recommendation

先做 Candidate 2(删除死代码),再做 Candidate 1(状态机坍缩)

Candidate 2 是零风险的即时收益——删 141 行死代码 + 1 个死 import,不改变任何运行时行为,立即可做。

Candidate 1 是本审查的核心深化机会。LeadServiceImpl 是最近 3 次提交的热点文件,738 行且仍在增长。状态机规则散落在 9 个调用点(含 2 个绕过 service 的定时任务),是测试难度和 bug 风险的主要来源。坍缩为深模块后,985 行的测试文件可以大幅瘦身——迁移规则从"10 个 mock 的集成测试"变为"纯函数单元测试"。

回到候选项列表