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.
374 lines
24 KiB
374 lines
24 KiB
|
3 weeks ago
|
<!doctype html>
|
||
|
|
<html lang="zh">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<title>Architecture review — crm-backend-matt</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>
|
||
|
|
.seam { stroke-dasharray: 4 4; }
|
||
|
|
.leak { stroke: #dc2626; }
|
||
|
|
.deep { background: linear-gradient(135deg, #0f172a, #1e293b); color: #e2e8f0; }
|
||
|
|
.mod-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
||
|
|
.bar-if { background: #f59e0b; }
|
||
|
|
.bar-impl { background: #334155; }
|
||
|
|
</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 ============ -->
|
||
|
|
<header class="space-y-3">
|
||
|
|
<p class="text-xs uppercase tracking-widest text-slate-400">architecture review</p>
|
||
|
|
<h1 class="text-3xl font-serif font-semibold">crm-backend-matt</h1>
|
||
|
|
<p class="text-sm text-slate-500">2026-08-17 · 热点区域:crm-auth(资源树 / 登录态 / 数据权限)、crm-file</p>
|
||
|
|
<div class="flex flex-wrap gap-4 pt-2 text-xs text-slate-600">
|
||
|
|
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 border-2 border-slate-500 rounded-sm"></span>module</span>
|
||
|
|
<span class="flex items-center gap-1.5"><svg width="24" height="6"><line x1="0" y1="3" x2="24" y2="3" class="seam" stroke="#64748b" stroke-width="2"/></svg>seam</span>
|
||
|
|
<span class="flex items-center gap-1.5"><svg width="24" height="6"><line x1="0" y1="3" x2="24" y2="3" class="leak" stroke-width="2"/></svg>leakage</span>
|
||
|
|
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 deep rounded-sm"></span>deep module</span>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<section id="candidates" class="space-y-12">
|
||
|
|
|
||
|
|
<!-- ============ CANDIDATE 1 ============ -->
|
||
|
|
<article id="c1" class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 space-y-6">
|
||
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||
|
|
<h2 class="text-xl font-serif font-semibold">1 · 拆分 TokenService:三个概念从宽接口里各归其位</h2>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-emerald-100 text-emerald-800">Strong</span>
|
||
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">local-substitutable</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="text-sm font-mono text-slate-600 space-y-0.5">
|
||
|
|
<p>crm-auth/.../security/TokenService.java(212 行,12 个公开方法)</p>
|
||
|
|
<p>crm-auth/.../service/impl/AuthServiceImpl.java · security/JwtAuthenticationFilter.java · controller/DebugTokenController.java</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- mass diagram: before/after -->
|
||
|
|
<div class="grid grid-cols-2 gap-6">
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-3">Before — 一个宽接口扛三个概念</p>
|
||
|
|
<div class="flex items-end gap-6 justify-center h-56">
|
||
|
|
<div class="flex flex-col justify-end items-center gap-1">
|
||
|
|
<div class="bar-if w-24 h-40 rounded-t border border-amber-600"></div>
|
||
|
|
<p class="mod-label text-slate-500">interface<br/>12 方法</p>
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-col justify-end items-center gap-1">
|
||
|
|
<div class="bar-impl w-24 h-48 rounded-t"></div>
|
||
|
|
<p class="mod-label text-slate-500">implementation<br/>212 行</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-slate-500 mt-2 text-center">JWT 密码学 + 单端在线会话索引 + 预登录票 + 被顶下线标记同居一类;<br/>接口几乎与实现等宽 —— shallow</p>
|
||
|
|
</div>
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-3">After — 三个深模块</p>
|
||
|
|
<div class="flex items-end gap-3 justify-center h-56">
|
||
|
|
<div class="flex flex-col justify-end items-center gap-1">
|
||
|
|
<div class="bar-if w-12 h-6 rounded-t border border-amber-600"></div>
|
||
|
|
<div class="deep w-16 h-20 rounded-b flex items-center justify-center text-center text-[10px] px-1">sign / parseJti</div>
|
||
|
|
<p class="mod-label text-slate-500">JwtCodec</p>
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-col justify-end items-center gap-1">
|
||
|
|
<div class="bar-if w-14 h-8 rounded-t border border-amber-600"></div>
|
||
|
|
<div class="deep w-20 h-36 rounded-b flex items-center justify-center text-center text-[10px] px-1">单端在线不变量独占于此</div>
|
||
|
|
<p class="mod-label text-slate-500">SessionStore</p>
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-col justify-end items-center gap-1">
|
||
|
|
<div class="bar-if w-12 h-4 rounded-t border border-amber-600"></div>
|
||
|
|
<div class="deep w-16 h-14 rounded-b flex items-center justify-center text-center text-[10px] px-1">issue / consume</div>
|
||
|
|
<p class="mod-label text-slate-500">TicketStore</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-slate-500 mt-2 text-center">橙色 = interface,深色 = implementation;接口收窄,实现变深</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6 text-sm">
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Problem</p>
|
||
|
|
<p>「每个 (userId, 端类型) 至多一个活会话」这条核心安全不变量没有一个专属的 module 负责——它分散在 12 个平铺方法之间,靠调用方的调用顺序维系。</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Solution</p>
|
||
|
|
<p>按概念拆成三个深模块:无状态 JwtCodec、独占单端在线不变量的 SessionStore、独立的预登录票 store;编排上移到本就该知道顺序的调用方。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ul class="text-sm text-slate-700 space-y-1">
|
||
|
|
<li>• locality:单端在线不变量有一个明确的家和专属测试</li>
|
||
|
|
<li>• JwtCodec 无状态不碰 Redis,纯密码学单测即可覆盖</li>
|
||
|
|
<li>• 每请求热路径(JwtAuthenticationFilter)只依赖它真正需要的那一小片 interface</li>
|
||
|
|
<li>• 登录编排测试不再 mock 一个 12 方法的门面</li>
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
<div class="rounded-md bg-amber-50 border border-amber-200 px-4 py-2.5 text-sm text-amber-900">
|
||
|
|
已有成稿 spec:<span class="font-mono text-xs">.scratch/token-service-split/spec.md</span>(status: ready-for-agent),纯重构、零行为变更——本卡片是确认它的优先级,而非重新提案。
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- ============ CANDIDATE 2 ============ -->
|
||
|
|
<article id="c2" class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 space-y-6">
|
||
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||
|
|
<h2 class="text-xl font-serif font-semibold">2 · 收拢权限资源树:变更路径与规则失效收进一个深模块</h2>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-emerald-100 text-emerald-800">Strong</span>
|
||
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">in-process</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="text-sm font-mono text-slate-600 space-y-0.5">
|
||
|
|
<p>crm-auth/.../service/impl/ResourceServiceImpl.java(229 行)· service/IResourceService.java</p>
|
||
|
|
<p>crm-auth/.../security/ApiPermissionCache.java · security/ApiPermissionInterceptor.java · service/impl/PermissionSeederImpl.java</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- mermaid before -->
|
||
|
|
<div class="grid grid-cols-2 gap-6">
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-2">Before — 失效义务挂在调用方</p>
|
||
|
|
<pre class="mermaid">
|
||
|
|
flowchart TB
|
||
|
|
RC[ResourceController] --> RS[ResourceServiceImpl]
|
||
|
|
SD[PermissionSeederImpl<br/>启动种子化] --> SM[(sys_menu)]
|
||
|
|
RS --> SM
|
||
|
|
RS -->|"手动 invalidate()"| AC[ApiPermissionCache]
|
||
|
|
AC --> AI[ApiPermissionInterceptor]
|
||
|
|
SD -.->|"不失效 ⚠ 靠启动顺序侥幸"| AC
|
||
|
|
RS --> FA[FileApi<br/>图标上传]
|
||
|
|
classDef leak stroke:#dc2626,stroke-width:2px,color:#dc2626;
|
||
|
|
class SD leak
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-2">After — 变更收进深模块,失效藏在 seam 后</p>
|
||
|
|
<pre class="mermaid">
|
||
|
|
flowchart TB
|
||
|
|
RC[ResourceController] --> RT
|
||
|
|
SD[PermissionSeederImpl] --> RT
|
||
|
|
subgraph RT["权限资源树 module(深)"]
|
||
|
|
direction TB
|
||
|
|
M1[节点校验 / 层级约束]
|
||
|
|
M2[sys_menu 读写]
|
||
|
|
M3[规则缓存失效]
|
||
|
|
end
|
||
|
|
RT --> AI[ApiPermissionInterceptor]
|
||
|
|
IC[图标上传校验] --> FAPI[FileApi]
|
||
|
|
classDef deep fill:#0f172a,color:#e2e8f0,stroke:#0f172a;
|
||
|
|
class RT deep
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6 text-sm">
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Problem</p>
|
||
|
|
<p>「资源树变更 → API 权限规则缓存失效」这条不变量由调用方手动履行:ResourceServiceImpl 记得调 invalidate(),PermissionSeederImpl 写 sys_menu 却不调——目前只因种子化跑在首个请求之前而没出事。图标上传(扩展名白名单、大小上限)也挂错了 module,撑宽了资源树的 interface。</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Solution</p>
|
||
|
|
<p>把所有 sys_menu 写路径(save / delete / seed)收进一个权限资源树深模块,规则缓存失效藏在它的 seam 之后;图标上传挪到它该在的地方(FileApi 侧的上传策略或独立小 module)。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ul class="text-sm text-slate-700 space-y-1">
|
||
|
|
<li>• locality:缓存失效义务从 N 个调用方收进 1 个 module,新增写路径不再可能漏失效</li>
|
||
|
|
<li>• interface 收窄:资源树 module 只剩「树 CRUD」,不再混入文件上传机制</li>
|
||
|
|
<li>• 测试打在资源树的 interface 上:save/delete 用例顺带断言规则一致性</li>
|
||
|
|
<li>• 「enabled」过滤谓词现在散落三处(Resolver / Cache / Seeder),收拢后只写一遍</li>
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
<div class="rounded-md bg-amber-50 border border-amber-200 px-4 py-2.5 text-sm text-amber-900">
|
||
|
|
须守住 ADR-0016 的 PermissionSeeder seam(跨模块种子化的唯一出口)——深模块化的是 crm-auth 内部的写路径,不改变 seed 契约。
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- ============ CANDIDATE 3 ============ -->
|
||
|
|
<article id="c3" class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 space-y-6">
|
||
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||
|
|
<h2 class="text-xl font-serif font-semibold">3 · 抽出「用户定位器」:登录流程的定位规则与组织准入归一</h2>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-amber-100 text-amber-800">Worth exploring</span>
|
||
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">in-process</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="text-sm font-mono text-slate-600 space-y-0.5">
|
||
|
|
<p>crm-auth/.../service/impl/AuthServiceImpl.java(249 行,self-injection + 两次身份查询)</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- cross-section before/after -->
|
||
|
|
<div class="grid grid-cols-2 gap-6">
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-3">Before — 定位规则横跨事务边界两次执行</p>
|
||
|
|
<div class="space-y-1.5 text-xs">
|
||
|
|
<div class="h-9 border-l-4 border-amber-500 bg-white px-3 flex items-center">login() 前置校验 + 端类型解析</div>
|
||
|
|
<div class="h-9 border-l-4 border-amber-500 bg-white px-3 flex items-center">authCode → 三方用户信息(adapter)</div>
|
||
|
|
<div class="h-9 border-l-4 border-red-500 bg-red-50 px-3 flex items-center">事务外探测:查身份 ① + locateUser + 组织准入</div>
|
||
|
|
<div class="h-9 border-l-4 border-red-500 bg-red-50 px-3 flex items-center">事务内 resolveUser:查身份 ② + locateUser(重复)</div>
|
||
|
|
<div class="h-9 border-l-4 border-slate-400 bg-white px-3 flex items-center">@Lazy self 注入绕 AOP 代理进事务</div>
|
||
|
|
<div class="h-9 border-l-4 border-slate-400 bg-white px-3 flex items-center">单端在线检测 → 签发 / 预登录票</div>
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-slate-500 mt-2">身份绑定 → 手机号匹配 → 首登注册这条定位规则,为事务边界切成了两段互相参照的代码。</p>
|
||
|
|
</div>
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-3">After — 定位规则收进一个深模块</p>
|
||
|
|
<div class="space-y-1.5 text-xs">
|
||
|
|
<div class="h-9 border-l-4 border-slate-400 bg-white px-3 flex items-center">login() 编排</div>
|
||
|
|
<div class="deep h-28 rounded px-3 py-2 flex flex-col justify-center gap-0.5">
|
||
|
|
<span class="font-semibold">用户定位器 module</span>
|
||
|
|
<span class="text-slate-300">身份绑定 → 手机号匹配 → 组织准入 → 首登注册</span>
|
||
|
|
<span class="text-slate-300">孤儿身份重定向 · 事务边界在内部处理</span>
|
||
|
|
</div>
|
||
|
|
<div class="h-9 border-l-4 border-slate-400 bg-white px-3 flex items-center">单端在线检测 → 签发 / 预登录票</div>
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-slate-500 mt-2">AuthServiceImpl 只剩登录编排;定位规则一处写、一处测。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6 text-sm">
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Problem</p>
|
||
|
|
<p>「身份绑定 → 手机号匹配 → 首登注册」被事务边界切成事务外探测与事务内重放两段(身份查询执行两次、locateUser 被迫共用防漂移),还靠 @Lazy self-injection 才能走进事务代理。</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Solution</p>
|
||
|
|
<p>把定位规则连同组织准入门禁收进一个用户定位器深模块,自持事务边界;login() 只消费定位结果,孤儿身份重定向也藏进 seam 之后。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ul class="text-sm text-slate-700 space-y-1">
|
||
|
|
<li>• locality:孤儿身份、组织准入、首登注册的 bug 集中在一个 module</li>
|
||
|
|
<li>• 身份查询从两次降为一次,探测与定位不再各自维护一份规则</li>
|
||
|
|
<li>• self-injection 消失,事务边界由定位器自己声明</li>
|
||
|
|
<li>• 测试打在定位器 interface 上,不必再对整条登录链路做 mock</li>
|
||
|
|
</ul>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- ============ CANDIDATE 4 ============ -->
|
||
|
|
<article id="c4" class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 space-y-6">
|
||
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||
|
|
<h2 class="text-xl font-serif font-semibold">4 · 数据权限机制:理解一个档位要跳五个地方</h2>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-200 text-slate-700">Speculative</span>
|
||
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">in-process</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="text-sm font-mono text-slate-600 space-y-0.5">
|
||
|
|
<p>crm-base/.../security/(DataVisibility · DataScopeLevel · DataVisibilityContext · VisibilityScope)</p>
|
||
|
|
<p>crm-auth/.../security/PermissionResolverImpl.java · DataScopeInterceptor.java · service/impl/RoleScopeStoreImpl.java</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6">
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-2">Before — 一条机制横跨两个 module 五处代码</p>
|
||
|
|
<pre class="mermaid">
|
||
|
|
flowchart TB
|
||
|
|
RS[RoleScopeStore<br/>档位持久化] --> DB[(sys_role_data_scope)]
|
||
|
|
DB --> PR[PermissionResolverImpl<br/>按模块取最宽]
|
||
|
|
PR --> DV[DataVisibility<br/>crm-base]
|
||
|
|
DV --> CTX[DataVisibilityContext<br/>请求线程传递]
|
||
|
|
CTX --> DI[DataScopeInterceptor<br/>翻译成 SQL 条件]
|
||
|
|
</pre>
|
||
|
|
</div>
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4 flex flex-col justify-center gap-3">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400">After — 假设性收拢</p>
|
||
|
|
<div class="deep rounded p-4 text-xs space-y-1">
|
||
|
|
<p class="font-semibold">数据权限 module</p>
|
||
|
|
<p class="text-slate-300">档位存储 · 折算(多角色每模块取最宽)· 部门集合 · SQL 翻译</p>
|
||
|
|
</div>
|
||
|
|
<p class="text-xs text-slate-500">但 crm-base 必须提供注解与上下文给所有业务模块——seam 怎么摆是真正的难题。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6 text-sm">
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Problem</p>
|
||
|
|
<p>「一个角色的数据范围档位如何变成 SQL 条件」要依次读 RoleScopeStore → PermissionResolverImpl → DataVisibility → DataVisibilityContext → DataScopeInterceptor,且横跨 crm-base / crm-auth 两个 module。</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Solution</p>
|
||
|
|
<p>评估把「档位折算 + 可见范围判定」收进 crm-auth 的一个深模块,crm-base 只留注解与上下文这两样真正的跨模块契约。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ul class="text-sm text-slate-700 space-y-1">
|
||
|
|
<li>• locality:档位折算规则集中一处,「一律不可见」的永假判定不再分散</li>
|
||
|
|
<li>• AI 可导航性:理解数据权限模块不必跨 module 跳读</li>
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
<div class="rounded-md bg-amber-50 border border-amber-200 px-4 py-2.5 text-sm text-amber-900">
|
||
|
|
ADR-0018 刚落地、DataScopeIntegrationTest 覆盖良好——除非后续模块(商机 / 客户)接入时再次感到摩擦,否则不值得现在动它。
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
<!-- ============ CANDIDATE 5 ============ -->
|
||
|
|
<article id="c5" class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 space-y-6">
|
||
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||
|
|
<h2 class="text-xl font-serif font-semibold">5 · FileApi 门面里的透传方法</h2>
|
||
|
|
<div class="flex gap-2">
|
||
|
|
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-200 text-slate-700">Speculative</span>
|
||
|
|
<span class="text-xs px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">in-process</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="text-sm font-mono text-slate-600 space-y-0.5">
|
||
|
|
<p>crm-file/.../api/FileApi.java(12 方法)· service/impl/FileApiImpl.java · service/MultipartUploader.java · service/ThumbnailResolver.java</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6">
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-3">Before — 12 方法门面,其中 4 个纯透传</p>
|
||
|
|
<div class="space-y-1 text-xs font-mono">
|
||
|
|
<div class="h-7 bg-slate-700 text-slate-100 rounded px-3 flex items-center">upload / uploadDirect / getInfo / download / delete / getPreviewUrl</div>
|
||
|
|
<div class="h-7 bg-slate-300 text-slate-600 rounded px-3 flex items-center">initMultipart → MultipartUploader(一行透传)</div>
|
||
|
|
<div class="h-7 bg-slate-300 text-slate-600 rounded px-3 flex items-center">uploadChunk → MultipartUploader(一行透传)</div>
|
||
|
|
<div class="h-7 bg-slate-300 text-slate-600 rounded px-3 flex items-center">completeMultipart → MultipartUploader(一行透传)</div>
|
||
|
|
<div class="h-7 bg-slate-300 text-slate-600 rounded px-3 flex items-center">getThumbnail → ThumbnailResolver(一行透传)</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4 flex flex-col justify-center gap-3">
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400">After — 门面对准「fileId 生命周期」</p>
|
||
|
|
<div class="deep rounded p-3 text-xs">FileApi:上传 / 详情 / 下载 / 删除 / 预览</div>
|
||
|
|
<p class="text-xs text-slate-500">分片三段式与缩略图由各自的深模块直接暴露给 Controller——但删除测试:透传消失后,MultipartUploader 成了第二个对外 seam,值不值取决于它是否真会独立演化。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid grid-cols-2 gap-6 text-sm">
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Problem</p>
|
||
|
|
<p>FileApi 把「文件生命周期」和「上传会话」两套概念装进同一个 12 方法 interface,其中 4 个方法一行透传、不增不减——调用方学完整张门面才能用其中一小片。</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p class="text-xs uppercase tracking-wider text-slate-400 mb-1">Solution</p>
|
||
|
|
<p>门面只留 fileId 生命周期;分片上传会话与缩略图各自成为独立 seam(若它们确有独立演化)。</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ul class="text-sm text-slate-700 space-y-1">
|
||
|
|
<li>• interface 收窄,leverage 按概念各归各位</li>
|
||
|
|
<li>• 风险:一个 adapter 只是假设性 seam——若分片协议不再演化,拆了反而多一层</li>
|
||
|
|
</ul>
|
||
|
|
</article>
|
||
|
|
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<!-- ============ TOP RECOMMENDATION ============ -->
|
||
|
|
<section id="top-recommendation" class="deep rounded-xl p-8 space-y-3">
|
||
|
|
<p class="text-xs uppercase tracking-widest text-slate-400">top recommendation</p>
|
||
|
|
<h2 class="text-2xl font-serif font-semibold text-white">先做 <a href="#c1" class="underline decoration-emerald-400 underline-offset-4">候选 1 · 拆分 TokenService</a></h2>
|
||
|
|
<p class="text-slate-300 text-sm leading-relaxed">
|
||
|
|
spec 已 ready-for-agent、纯重构零行为变更、且 TokenService 是最近提交里最热的文件区域(单端在线 / 顶号刚落地,改动还在持续)。
|
||
|
|
拆完之后再回头看候选 2:资源树收拢需要动到的调用方会更少、seam 更干净。
|
||
|
|
</p>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
</main>
|
||
|
|
</body>
|
||
|
|
</html>
|