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.

398 lines
29 KiB

16 hours ago
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>架构审查 · crm-backend-matt · 2026-09-06</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'neutral', themeVariables: { fontSize: '13px' } });
</script>
<style>
body { font-family: "Segoe UI", "Microsoft YaHei", sans-serif; }
.badge-strong { background:#059669; }
.badge-explore { background:#d97706; }
.badge-spec { background:#6b7280; }
.mono { font-family: Consolas, "Courier New", monospace; }
</style>
</head>
<body class="bg-slate-100 text-slate-800">
<!-- ================= HEADER ================= -->
<header class="bg-slate-900 text-white">
<div class="max-w-6xl mx-auto px-8 py-10">
<div class="text-sm text-slate-400 mb-2">/improve-codebase-architecture · 深化机会扫描</div>
<h1 class="text-3xl font-bold mb-3">架构审查报告:热点集中在 crm-customer 返工区</h1>
<p class="text-slate-300 leading-relaxed max-w-4xl">
扫描范围由提交历史决定:上次审查(2026-08-17,候选 #1/#2 已落地为 ADR-0030/0031,#3/#4/#5 已否决并记录在 ADR-0022 §4 / ADR-0030,本次不再重提)之后的全部改动。
热点 = <b>客户模块返工(票 01-12)</b>、商机 board / 建档字段、字典两级树、crm-rule 客户规则子域。领域词汇取自各模块 <span class="mono">CONTEXT.md</span>
架构词汇按 <span class="mono">module / interface / depth / seam / adapter / leverage / locality</span> 口径使用。
</p>
<div class="mt-5 flex flex-wrap gap-2 text-xs">
<span class="bg-emerald-600 px-3 py-1 rounded-full">2 × Strong</span>
<span class="bg-amber-600 px-3 py-1 rounded-full">1 × Worth exploring</span>
<span class="bg-slate-500 px-3 py-1 rounded-full">1 × Speculative(与既有拍板冲突)</span>
</div>
</div>
</header>
<!-- ================= SUMMARY ================= -->
<section class="max-w-6xl mx-auto px-8 py-8">
<div class="grid md:grid-cols-2 gap-4">
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-bold text-lg mb-2">#1 客户三 workspace 查询口径三处复制 <span class="badge-strong text-white text-xs px-2 py-0.5 rounded ml-1">Strong</span></h2>
<p class="text-sm text-slate-600">同一份 WHERE(workspace × 内置视图 × 筛选)在 <span class="mono">CustomerMapper</span> 里手写了三份,board 与列表已静默分叉。商机侧同场景已有单一事实源范式可对照。</p>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-bold text-lg mb-2">#2 H2 测试 schema 的 12 份手工副本 <span class="badge-strong text-white text-xs px-2 py-0.5 rounded ml-1">Strong</span></h2>
<p class="text-sm text-slate-600">12 个集成测试类各自内联整套 DDL + MyBatis 自举,其中一份已漂移。加一列要改 12 个文件,crm-opportunity 的表结构变更也会波及客户侧测试。</p>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-bold text-lg mb-2">#3 crm-rule 权限种子 6 个同构 Initializer <span class="badge-explore text-white text-xs px-2 py-0.5 rounded ml-1">Worth exploring</span></h2>
<p class="text-sm text-slate-600">6 个 45 行的 CommandLineRunner 各包装一条纯数据(菜单名/路径/排序)。@Order 全局手工编号已出现两处撞号。</p>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h2 class="font-bold text-lg mb-2">#4 crm-preference 三同构偏好栈 <span class="badge-spec text-white text-xs px-2 py-0.5 rounded ml-1">Speculative</span></h2>
<p class="text-sm text-slate-600">列偏好 / 自定义视图 / 形态偏好三套平行 entity+mapper+service+controller。<b>与 CONTEXT.md 既有拍板「不做全平台通用化」冲突</b>,仅在前端出现第 4 种偏好时才值得重开。</p>
</div>
</div>
</section>
<!-- ================= CANDIDATE 1 ================= -->
<section class="max-w-6xl mx-auto px-8 pb-12">
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="bg-slate-800 text-white px-8 py-5 flex items-center justify-between">
<div>
<div class="text-xs text-slate-400 mb-1">候选 #1</div>
<h2 class="text-2xl font-bold">客户三 workspace 查询:一份口径,三处手写,已经漂移</h2>
</div>
<span class="badge-strong text-white text-sm px-3 py-1 rounded-lg">Strong</span>
</div>
<div class="px-8 py-6 space-y-6">
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">涉及文件</h3>
<ul class="space-y-1 text-slate-700">
<li class="mono text-xs bg-slate-50 rounded p-1.5">crm-customer/.../mapper/CustomerMapper.java<br><span class="text-slate-500">pageWorkspace L152-313 · boardSummary L323-423</span></li>
<li class="mono text-xs bg-slate-50 rounded p-1.5">crm-customer/.../CustomerWorkspaceServiceImpl.java</li>
<li class="mono text-xs bg-slate-50 rounded p-1.5">对照:crm-opportunity/.../OpportunityViewFilter.java</li>
</ul>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">问题(deletion test:会集中复杂度)</h3>
<p class="text-slate-700 leading-relaxed">
「客户总览 / 我的客户 / 客户公海」三个 workspace 的数据集条件(内置视图 ASSIGNED/COLLABORATING/FOLLOW_UP_DUE/FOCUSED/RECENT × 公共筛选 × saved-view 条件)在
<b>同一张 Mapper 里逐字写三遍</b><span class="mono">pageWorkspace</span> 的 overview 分支、mine/pool 分支、以及 <span class="mono">boardSummary</span>
javadoc 自述「改动须两处同步」——实际是三处。删掉三份换成一份共享片段,复杂度会集中而不是搬家。
</p>
</div>
</div>
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-sm">
<div class="font-bold text-red-800 mb-1">⚠ 已发生的口径漂移(本次审查实测)</div>
<p class="text-red-700 leading-relaxed">
<span class="mono">boardSummary</span> 相对 <span class="mono">pageWorkspace</span> <b>静默缺少 3 个条件</b><span class="mono">FOLLOW_UP_DUE</span>(待跟进视图 EXISTS)、
<span class="mono">industryCode</span><span class="mono">provinceCode</span>。而 <span class="mono">boardCards</span> 走的是 <span class="mono">pageWorkspace</span>(带全这些条件)。
结果:看板列头汇总数 ≠ 同参数下卡片数。javadoc 声称「WHERE 口径与 pageWorkspace 完全一致」——代码 disagrees。
无任何测试覆盖该一致性(返工票 03 只测了 pool 拒绝)。这正是「同一份列表数据三种渲染、同一查询」约定(商机侧票 06 D-15 明文化:<b>summary 与 cards 必须同口径,视图语义不得各写各的</b>)在客户侧的违例。
</p>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2 text-sm">Before / After</h3>
<div class="grid md:grid-cols-2 gap-4">
<!-- BEFORE -->
<div class="border-2 border-red-300 rounded-xl p-4 bg-red-50/50">
<div class="text-xs font-bold text-red-700 mb-3">BEFORE · 三份手写 WHERE(同色块 = 逐字相同)</div>
<div class="space-y-2 text-xs">
<div class="bg-white rounded border border-slate-300 p-2 mono">pageWorkspace / overview 分支<br><span class="text-emerald-700">✓ FOLLOW_UP_DUE</span> <span class="text-emerald-700">✓ industry</span> <span class="text-emerald-700">✓ province</span> <span class="text-emerald-700">✓ savedView</span></div>
<div class="bg-white rounded border border-slate-300 p-2 mono">pageWorkspace / mine+pool 分支<br><span class="text-emerald-700">✓ FOLLOW_UP_DUE</span> <span class="text-emerald-700">✓ industry</span> <span class="text-emerald-700">✓ province</span> <span class="text-emerald-700">✓ savedView</span></div>
<div class="bg-white rounded border-2 border-red-400 p-2 mono">boardSummary(看板汇总)<br><span class="text-red-600">✗ FOLLOW_UP_DUE</span> <span class="text-red-600">✗ industry</span> <span class="text-red-600">✗ province</span> <span class="text-slate-400">— savedView(有意不接)</span></div>
<div class="text-center text-red-600 font-bold">↑ 同一约定,第 3 份已漂移且 javadoc 与代码矛盾</div>
</div>
</div>
<!-- AFTER -->
<div class="border-2 border-emerald-300 rounded-xl p-4 bg-emerald-50/50">
<div class="text-xs font-bold text-emerald-700 mb-3">AFTER · 一份共享 SQL 片段(XML mapper)</div>
<div class="space-y-2 text-xs">
<div class="bg-white rounded border border-emerald-400 p-2 mono">&lt;sql id="workspaceCriteria"&gt;<br>内置视图 × 筛选 × savedView 唯一事实源</div>
<div class="flex gap-2">
<div class="bg-white rounded border border-slate-300 p-2 mono flex-1">pageWorkspace<br><span class="text-slate-500">&lt;include&gt; ×2 分支</span></div>
<div class="bg-white rounded border border-slate-300 p-2 mono flex-1">boardSummary<br><span class="text-slate-500">&lt;include&gt;(排除 groupValue)</span></div>
</div>
<div class="text-center text-emerald-700 font-bold">↑ 新增筛选条件 = 改一处;漂移在结构上不可能</div>
</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">方案(plain English)</h3>
<p class="text-slate-700 leading-relaxed">
<span class="mono">pageWorkspace</span><span class="mono">boardSummary</span> 两条注解 SQL 迁入同 namespace 的 XML mapper 文件,
公共 WHERE 提成 <span class="mono">&lt;sql id&gt;</span> 片段,各处 <span class="mono">&lt;include&gt;</span>
手写 SQL 本身是正当的(子查询包裹绕 <span class="mono">DataScopeInterceptor</span>、RECENT 标量子查询列、saved-view 白名单列 <span class="mono">${}</span> 都需要它)——要收敛的不是「写 SQL」,是「写三遍」。
顺手裁决(grilling 议题):<span class="mono">/api/customer/page</span>(基础分页,wrapper 版)与 <span class="mono">/api/customer/workspace/page</span> 同挂三个页面 tag 的双轨是否还需要。
</p>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">收益(locality / leverage / 测试面)</h3>
<ul class="text-slate-700 space-y-1.5 leading-relaxed list-disc pl-5">
<li><b>locality</b>:workspace 语义住一处。改口径不再需要「找到三份并保持一致」。</li>
<li><b>leverage</b>:下一个筛选字段(如行业二级联动)一处生效于列表 + 看板汇总 + 取卡。</li>
<li><b>测试面 = interface</b>:H2 集成测试直接打唯一口径;可新增「boardSummary 总数 == pageWorkspace 同参数计数」的守恒断言,把 D-15 约定变成可执行契约。</li>
<li>顺手修复一个潜伏缺陷(FOLLOW_UP_DUE / industry / province 看板计数失真)。</li>
</ul>
</div>
</div>
<div class="text-xs text-slate-500 border-t pt-3">
不与任何 ADR 冲突;反而落实商机侧已明文化的对称约定(票 06 D-15)。风险低:纯 SQL 组织方式变化,方法签名与调用方不变。
</div>
</div>
</div>
</section>
<!-- ================= CANDIDATE 2 ================= -->
<section class="max-w-6xl mx-auto px-8 pb-12">
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="bg-slate-800 text-white px-8 py-5 flex items-center justify-between">
<div>
<div class="text-xs text-slate-400 mb-1">候选 #2</div>
<h2 class="text-2xl font-bold">H2 测试 schema:12 份手工副本,一份已经漂移</h2>
</div>
<span class="badge-strong text-white text-sm px-3 py-1 rounded-lg">Strong</span>
</div>
<div class="px-8 py-6 space-y-6">
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">涉及文件</h3>
<ul class="space-y-1 text-slate-700 text-xs mono">
<li class="bg-slate-50 rounded p-1.5">crm-customer/src/test/**/ 12 个集成测试类<br><span class="text-slate-500">每类自带 @BeforeAll 自举 + 内联 DDL(合计 6155 行测试代码)</span></li>
<li class="bg-slate-50 rounded p-1.5">漂移证据:CustomerWorkspaceSavedViewIntegrationTest<br><span class="text-red-600">customer 表缺 is_biz_negotiated 列(D-05 新增),其余 11 份都有</span></li>
<li class="bg-slate-50 rounded p-1.5">跨模块复制:CustomerWorkspaceOwnershipIntegrationTest<br><span class="text-slate-500">把 opportunity / opportunity_customer 表 DDL 也复制进客户测试</span></li>
</ul>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">问题(浅模块:脚手架比业务断言还厚)</h3>
<p class="text-slate-700 leading-relaxed">
每个集成测试类都手搓同一套 130 行自举(JdbcDataSource + MybatisConfiguration + 分页/乐观锁拦截器 + MetaObjectFillHandler + execute DDL),
再各自内联一份 <span class="mono">CREATE TABLE customer</span>(55+ 列)及各子表。这份自举本身是一个<b>没人拥有的 module</b>:12 个副本、零测试(它是测试)、无单一事实源。
<b>deletion test</b>:把 12 份脚手架删掉换成 1 个测试基建类,复杂度大幅集中——这正是信号本身。
</p>
</div>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 text-sm">
<div class="font-bold text-amber-800 mb-1">代价实测</div>
<p class="text-amber-800 leading-relaxed">
客户表加一列(如 D-05 的 <span class="mono">is_biz_negotiated</span>)要同步改 12 个测试文件——本次已漏改 1 个。
更隐蔽的是<b>反向依赖</b>:CustomerMapper.pageWorkspace 的 opportunity_count 标量子查询 join 了 crm-opportunity 的表,商机表结构变更时
客户侧测试的复制 DDL 必须同步改(javadoc 已自认「商机表结构变更须同步本处与 H2 测试 SCHEMA」)。测试面不是深 module 的 interface,而是 12 个各说各话的影子 schema。
</p>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2 text-sm">Before / After</h3>
<div class="grid md:grid-cols-2 gap-4">
<div class="border-2 border-red-300 rounded-xl p-4 bg-red-50/50">
<div class="text-xs font-bold text-red-700 mb-3">BEFORE · 每类一份全套脚手架</div>
<div class="flex items-end justify-center gap-1 h-40 text-xs">
<div class="flex flex-col items-center gap-1"><div class="w-14 bg-slate-300 rounded-t" style="height:75%"></div><span class="mono">DDL+自举</span></div>
<div class="flex flex-col items-center gap-1"><div class="w-14 bg-slate-300 rounded-t" style="height:75%"></div><span class="mono">DDL+自举</span></div>
<div class="flex flex-col items-center gap-1"><div class="w-14 bg-slate-300 rounded-t" style="height:75%"></div><span class="mono">DDL+自举</span></div>
<div class="flex flex-col items-center gap-1"><div class="w-14 bg-red-400 rounded-t border-2 border-red-600" style="height:75%"></div><span class="mono text-red-600">已漂移</span></div>
<div class="flex flex-col items-center gap-1"><span class="text-slate-400 text-2xl"></span></div>
<div class="flex flex-col items-center gap-1"><div class="w-14 bg-slate-300 rounded-t" style="height:75%"></div><span class="mono">×12</span></div>
</div>
<div class="text-center text-red-600 font-bold text-xs mt-2">加一列 = 改 12 处,漏一处 = 假绿或半夜炸</div>
</div>
<div class="border-2 border-emerald-300 rounded-xl p-4 bg-emerald-50/50">
<div class="text-xs font-bold text-emerald-700 mb-3">AFTER · 一个测试基建 module</div>
<div class="flex items-end justify-center gap-2 h-40 text-xs">
<div class="flex flex-col items-center gap-1"><div class="w-24 bg-emerald-400 rounded-t" style="height:88%"></div><span class="mono">schema.sql(单一事实源)<br>+ H2Harness(自举)</span></div>
<div class="flex flex-col items-center gap-1 justify-end"><div class="flex gap-1"><div class="w-8 bg-slate-300 rounded" style="height:30%"></div><div class="w-8 bg-slate-300 rounded" style="height:30%"></div><div class="w-8 bg-slate-300 rounded" style="height:30%"></div></div><span class="mono">测试类只写断言</span></div>
</div>
<div class="text-center text-emerald-700 font-bold text-xs mt-2">加一列 = 改 1 处;漂移在结构上不可能</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">方案(plain English)</h3>
<p class="text-slate-700 leading-relaxed">
<span class="mono">crm-customer/src/test/resources/</span> 落一份 <span class="mono">schema.sql</span>(customer 域全部表 + 按 H2 方言),
加一个 <span class="mono">CustomerH2Harness</span> 测试基建类持有自举(DataSource / MybatisConfiguration / 拦截器 / 建表执行),12 个测试类改为继承/组合它。
只做客户域,不动 lead/opportunity 的既有测试形态(那边没有内联 DDL 问题)。crm-opportunity 表的 DDL 是否也抽进 harness(跨域耦合已既成事实)作为 grilling 议题。
</p>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">收益(locality / leverage / 测试面)</h3>
<ul class="text-slate-700 space-y-1.5 leading-relaxed list-disc pl-5">
<li><b>locality</b>:客户域表结构的测试投影住一个文件。</li>
<li><b>leverage</b>:下次加列(D-05 式迁移必然再来)改一处;顺带修复 SavedView 测试的隐性漂移。</li>
<li><b>测试面</b>:测试类的 interface 收敛为「业务断言 + 需要哪些表」,新集成测试的边际成本从 ~130 行降到 ~0。</li>
<li>顺手消灭一个类 bug:漂移副本在某些插入路径下会假绿/假红。</li>
</ul>
</div>
</div>
<div class="text-xs text-slate-500 border-t pt-3">
不与任何 ADR 冲突。注:脚手架代码量大但机械,适合一次票清掉;与候选 #1 修完口径后再做此件收益更直观(守恒断言直接写进新 harness 的测试)。
</div>
</div>
</div>
</section>
<!-- ================= CANDIDATE 3 ================= -->
<section class="max-w-6xl mx-auto px-8 pb-12">
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="bg-slate-700 text-white px-8 py-5 flex items-center justify-between">
<div>
<div class="text-xs text-slate-300 mb-1">候选 #3</div>
<h2 class="text-2xl font-bold">crm-rule 权限种子:6 个同构 CommandLineRunner 包装 6 条数据</h2>
</div>
<span class="badge-explore text-white text-sm px-3 py-1 rounded-lg">Worth exploring</span>
</div>
<div class="px-8 py-6 space-y-6">
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">涉及文件</h3>
<ul class="space-y-1 text-slate-700 text-xs mono">
<li class="bg-slate-50 rounded p-1.5">crm-rule/config/:RulePermissionInitializer(@Order 11)<br>OpportunityRulePermissionInitializer(13) · OpportunityStageTemplatePermissionInitializer(14)<br>CustomerReminderRulePermissionInitializer(14 ⚠撞号) · OpportunitySchemeCardTemplatePermissionInitializer(16)<br>CustomerDedupRulePermissionInitializer(19)</li>
<li class="bg-slate-50 rounded p-1.5">另撞号:crm-lead RulePermissionInitializer(12) vs crm-rule RegionDataInitializer(12)</li>
</ul>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">问题(浅模块的教科书形态)</h3>
<p class="text-slate-700 leading-relaxed">
每个类 45 行,其中 40 行是 javadoc/样板,<b>信息量 = 一个 PermissionModuleDescriptor 常量</b>(菜单名/路径/组件/排序/角色)。
interface 与 implementation 几乎一样宽——典型的 shallow module。新增一个规则菜单 = 再抄一个类 + 手挑一个全局 @Order 号。
@Order 编号是全工程手工分配的隐式协议,现已两处撞号(14×2、12×2)——撞号后种子顺序不确定。
</p>
</div>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2 text-sm">Before / After</h3>
<div class="grid md:grid-cols-2 gap-4">
<div class="border-2 border-amber-300 rounded-xl p-4 bg-amber-50/50">
<div class="text-xs font-bold text-amber-700 mb-3">BEFORE · 6 类 × 45 行</div>
<div class="space-y-1 text-xs mono">
<div class="bg-white rounded border border-slate-300 px-2 py-1">@Order(11) 线索池配置 → seedModule(公海池配置)</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">@Order(13) 商机规则 → seedModule(公海规则)</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">@Order(14) 阶段模板 → seedModule(阶段模板)</div>
<div class="bg-white rounded border border-red-400 px-2 py-1 text-red-700">@Order(14) 客户提醒 → seedModule(超期提醒) ⚠与上一行撞号</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">@Order(16) 方案卡模板 → seedModule(方案卡)</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">@Order(19) 查重设置 → seedModule(查重设置)</div>
<div class="text-center text-amber-700 font-bold mt-2">每类 = 5 行数据 + 40 行仪式</div>
</div>
</div>
<div class="border-2 border-emerald-300 rounded-xl p-4 bg-emerald-50/50">
<div class="text-xs font-bold text-emerald-700 mb-3">AFTER · 一个 Initializer + N 行声明</div>
<div class="space-y-1 text-xs mono">
<div class="bg-white rounded border border-emerald-400 px-2 py-1">RulePermissionSeedInitializer(唯一种子 runner)</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">List.of(descriptor(线索池…), descriptor(商机规则…), …)</div>
<div class="bg-white rounded border border-slate-300 px-2 py-1">顺序 = 列表顺序(不再有全局 @Order 撞号面)</div>
<div class="text-center text-emerald-700 font-bold mt-2">新菜单 = 加一行数据</div>
</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">方案(plain English)</h3>
<p class="text-slate-700 leading-relaxed">
crm-rule 内 6 个种子类合并为一个 <span class="mono">RulePermissionSeedInitializer</span>,持有 List&lt;PermissionModuleDescriptor&gt;
顺序即列表顺序。各域 javadoc 里的决策注释随数据归位。可顺带把撞号的 @Order 清掉(合并后 crm-rule 只占 1 个号)。
dict / auth / lead 各自的 Initializer 形态不同(有真实逻辑),不动。
</p>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">收益 / 保留意见</h3>
<ul class="text-slate-700 space-y-1.5 leading-relaxed list-disc pl-5">
<li><b>leverage</b>:第 7 个规则菜单(客户规则族还会长)边际成本从 45 行降到 1 行。</li>
<li>消灭全局 @Order 撞号这个隐式协议的局部实例。</li>
<li><b>保留意见</b>:每个类的 javadoc 承载了「为什么菜单挂这个目录」的领域决策——合并时注释须跟着数据走,不能丢。</li>
<li>不解决跨模块 @Order 问题本身(那是另一个更大的话题,此处不展开)。</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- ================= CANDIDATE 4 ================= -->
<section class="max-w-6xl mx-auto px-8 pb-12">
<div class="bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="bg-slate-600 text-white px-8 py-5 flex items-center justify-between">
<div>
<div class="text-xs text-slate-300 mb-1">候选 #4</div>
<h2 class="text-2xl font-bold">crm-preference 三套同构偏好栈(列偏好 / 自定义视图 / 形态偏好)</h2>
</div>
<span class="badge-spec text-white text-sm px-3 py-1 rounded-lg">Speculative</span>
</div>
<div class="px-8 py-6 space-y-6">
<div class="bg-orange-50 border-l-4 border-orange-500 rounded p-4 text-sm">
<div class="font-bold text-orange-800 mb-1">⚠ 与既有拍板冲突 — 不足以重开</div>
<p class="text-orange-800 leading-relaxed">
crm-preference/CONTEXT.md 明文:「本次只做到<b>够用的最小通用深度</b>,不做全平台通用化打磨」。本候选与该拍板冲突,
但摩擦尚未真实化(三栈各自稳定、消费方按 scope 接入无改动诉求)。列出仅为完整性 + 标注重开条件:<b>前端出现第 4 种偏好(如卡片密度/排序方案)时再议</b>
</p>
</div>
<div class="grid md:grid-cols-2 gap-6 text-sm">
<div>
<h3 class="font-bold text-slate-900 mb-2">现状事实(rule of three 计数器:2)</h3>
<ul class="space-y-1 text-slate-700 text-xs mono">
<li class="bg-slate-50 rounded p-1.5">user_column_preference → ColumnPreferenceServiceImpl(81 行)</li>
<li class="bg-slate-50 rounded p-1.5">user_saved_view → SavedViewServiceImpl(146 行)</li>
<li class="bg-slate-50 rounded p-1.5">user_view_form → ViewFormServiceImpl(59 行)</li>
<li class="bg-slate-50 rounded p-1.5">三套 entity/mapper/service/controller 平行,行为差异真实存在:<br>列偏好无校验 · 形态白名单校验(68xxx) · 自定义视图 filter_json + 默认互斥</li>
</ul>
</div>
<div>
<h3 class="font-bold text-slate-900 mb-2">为什么现在不做</h3>
<p class="text-slate-700 leading-relaxed">
三栈的「同构」只到 CRUD 骨架层;各自领域规则(保存才落库 / 值域白名单 / 默认视图互斥 + 翻译器归业务方)不同且已各自内聚。
抽公共泛型基座会把三种差异塞进一个宽 interface——正是上次审查否决 DisplayNameEnricher 的同款理由(宽接口薄实现 = shallow module)。
<b>deletion test 不通过</b>:删掉任何一栈,复杂度只是搬家到另外两栈的特判里。
</p>
</div>
</div>
</div>
</div>
</section>
<!-- ================= TOP RECOMMENDATION ================= -->
<section class="max-w-6xl mx-auto px-8 pb-16">
<div class="bg-gradient-to-r from-emerald-700 to-emerald-600 text-white rounded-2xl shadow-lg px-8 py-8">
<div class="text-xs text-emerald-200 mb-2">TOP RECOMMENDATION</div>
<h2 class="text-2xl font-bold mb-3">先做 #1:客户 workspace 查询口径收敛</h2>
<p class="leading-relaxed text-emerald-50 max-w-4xl">
理由:(a) 它已经是一个<b>活的缺陷</b>(看板汇总 vs 卡片计数不一致,且 javadoc 撒谎说一致);(b) 修复面窄、风险低(SQL 重组,签名不变);
(c) 商机侧 <span class="mono">OpportunityViewFilter</span> 已验证同款深化路径,等于有参照实现;(d) 它是 #2 的前置——口径唯一后,#2 的 harness 里可以直接写
「boardSummary == pageWorkspace 计数」守恒断言,把「三种渲染同一数据」从注释变成可执行契约。做完 #1 顺手补 D-15 守恒测试,再清 #2。
#3 独立小票随时可做;#4 挂起重开条件,不投入。
</p>
<div class="mt-4 text-xs text-emerald-200">建议顺序:#1(含守恒断言)→ #2(测试基建收敛)→ #3(独立小票)· #4(挂起)</div>
</div>
</section>
<footer class="bg-slate-900 text-slate-400 text-xs">
<div class="max-w-6xl mx-auto px-8 py-6">
生成于 2026-09-06 · /improve-codebase-architecture · 报告文件:.scratch/architecture-review-20260906-102629.html(不入侵 src/)
</div>
</footer>
</body>
</html>