9cbb1f7695
- docs/: 源码分析报告、代码深度解析、项目说明书、独立服务化方案 - backend/: 账权模块提取说明(提取原则、文件清单、耦合度验证) - frontend/: 前端剥离方案、剥离历史与说明、框架修改详情
1144 lines
58 KiB
HTML
1144 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>账权模块独立服务化方案</title>
|
||
<style>
|
||
:root {
|
||
--c-bg: #0d1117;
|
||
--c-surface: #161b22;
|
||
--c-surface2: #21262d;
|
||
--c-border: #30363d;
|
||
--c-text: #e6edf3;
|
||
--c-text-dim: #8b949e;
|
||
--c-accent: #58a6ff;
|
||
--c-green: #3fb950;
|
||
--c-orange: #d29922;
|
||
--c-red: #f85149;
|
||
--c-purple: #bc8cff;
|
||
--c-teal: #39d0d8;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
background: var(--c-bg);
|
||
color: var(--c-text);
|
||
line-height: 1.7;
|
||
font-size: 15px;
|
||
}
|
||
.container { max-width: 980px; margin: 0 auto; padding: 40px 24px 80px; }
|
||
h1 {
|
||
font-size: 28px; font-weight: 700; margin-bottom: 8px;
|
||
background: linear-gradient(135deg, var(--c-accent), var(--c-purple));
|
||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||
}
|
||
.subtitle { color: var(--c-text-dim); font-size: 14px; margin-bottom: 40px; }
|
||
h2 {
|
||
font-size: 22px; font-weight: 600; margin: 48px 0 16px;
|
||
padding-bottom: 8px; border-bottom: 1px solid var(--c-border);
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
h2 .num {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
width: 28px; height: 28px; border-radius: 6px;
|
||
background: var(--c-accent); color: var(--c-bg);
|
||
font-size: 14px; font-weight: 700;
|
||
}
|
||
h3 { font-size: 17px; font-weight: 600; margin: 28px 0 12px; color: var(--c-accent); }
|
||
h4 { font-size: 15px; font-weight: 600; margin: 20px 0 8px; color: var(--c-text); }
|
||
p { margin-bottom: 12px; color: var(--c-text); }
|
||
ul, ol { margin: 8px 0 16px 20px; }
|
||
li { margin-bottom: 6px; }
|
||
code {
|
||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||
background: var(--c-surface2); padding: 2px 6px; border-radius: 4px;
|
||
font-size: 13px; color: var(--c-teal);
|
||
}
|
||
pre {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 16px; overflow-x: auto;
|
||
margin: 12px 0; font-size: 13px; line-height: 1.6;
|
||
}
|
||
pre code { background: none; padding: 0; color: var(--c-text); }
|
||
table {
|
||
width: 100%; border-collapse: collapse; margin: 12px 0;
|
||
font-size: 13px;
|
||
}
|
||
th, td {
|
||
border: 1px solid var(--c-border); padding: 8px 12px; text-align: left;
|
||
}
|
||
th { background: var(--c-surface2); font-weight: 600; color: var(--c-accent); }
|
||
tr:nth-child(even) { background: var(--c-surface); }
|
||
.callout {
|
||
border-left: 3px solid var(--c-accent); background: var(--c-surface);
|
||
padding: 12px 16px; border-radius: 0 8px 8px 0; margin: 16px 0;
|
||
}
|
||
.callout.warn { border-left-color: var(--c-orange); }
|
||
.callout.danger { border-left-color: var(--c-red); }
|
||
.callout.ok { border-left-color: var(--c-green); }
|
||
.callout-title { font-weight: 600; margin-bottom: 4px; }
|
||
.badge {
|
||
display: inline-block; padding: 2px 8px; border-radius: 12px;
|
||
font-size: 12px; font-weight: 600;
|
||
}
|
||
.badge-green { background: rgba(63,185,80,0.15); color: var(--c-green); }
|
||
.badge-orange { background: rgba(210,153,34,0.15); color: var(--c-orange); }
|
||
.badge-red { background: rgba(248,81,73,0.15); color: var(--c-red); }
|
||
.badge-blue { background: rgba(88,166,255,0.15); color: var(--c-accent); }
|
||
.metric-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 12px; margin: 16px 0;
|
||
}
|
||
.metric-card {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 16px;
|
||
}
|
||
.metric-value { font-size: 28px; font-weight: 700; color: var(--c-accent); }
|
||
.metric-label { font-size: 13px; color: var(--c-text-dim); margin-top: 4px; }
|
||
.toc {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 20px 24px; margin-bottom: 40px;
|
||
}
|
||
.toc-title { font-weight: 600; margin-bottom: 12px; color: var(--c-accent); }
|
||
.toc ol { margin-left: 20px; }
|
||
.toc a { color: var(--c-text-dim); text-decoration: none; }
|
||
.toc a:hover { color: var(--c-accent); }
|
||
.diagram {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 24px; margin: 16px 0; text-align: center;
|
||
}
|
||
.diagram svg { max-width: 100%; height: auto; }
|
||
.compare-grid {
|
||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 16px 0;
|
||
}
|
||
.compare-card {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 16px;
|
||
}
|
||
.compare-card h4 { margin-top: 0; }
|
||
.phase-card {
|
||
background: var(--c-surface); border: 1px solid var(--c-border);
|
||
border-radius: 8px; padding: 20px; margin: 12px 0;
|
||
}
|
||
.phase-header {
|
||
display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
|
||
}
|
||
.phase-num {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
width: 32px; height: 32px; border-radius: 50%;
|
||
font-weight: 700; font-size: 14px;
|
||
}
|
||
.phase-num.p1 { background: rgba(248,81,73,0.2); color: var(--c-red); }
|
||
.phase-num.p2 { background: rgba(210,153,34,0.2); color: var(--c-orange); }
|
||
.phase-num.p3 { background: rgba(88,166,255,0.2); color: var(--c-accent); }
|
||
.phase-num.p4 { background: rgba(63,185,80,0.2); color: var(--c-green); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<h1>账权模块独立服务化方案</h1>
|
||
<p class="subtitle">基于账权平台 service-master 源码分析 · 2026-08-05</p>
|
||
|
||
<div class="toc">
|
||
<div class="toc-title">目录</div>
|
||
<ol>
|
||
<li><a href="#s1">现状分析:账权模块的边界与耦合</a></li>
|
||
<li><a href="#s2">可行性判断:能不能拆?该不该拆?</a></li>
|
||
<li><a href="#s3">目标架构设计</a></li>
|
||
<li><a href="#s4">核心技术方案</a></li>
|
||
<li><a href="#s5">数据库拆分策略</a></li>
|
||
<li><a href="#s6">解耦改造详细方案</a></li>
|
||
<li><a href="#s7">迁移路径与实施计划</a></li>
|
||
<li><a href="#s8">风险评估与应对</a></li>
|
||
<li><a href="#s9">结论与建议</a></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- ===================== 1. 现状分析 ===================== -->
|
||
<h2 id="s1"><span class="num">1</span> 现状分析:账权模块的边界与耦合</h2>
|
||
|
||
<h3>1.1 账权模块的功能边界</h3>
|
||
<p>通过对源码的全面扫描,当前单体应用中"账权"相关功能可归纳为以下五大域:</p>
|
||
|
||
<table>
|
||
<tr><th>功能域</th><th>核心实体</th><th>核心 Service</th><th>核心 Controller</th></tr>
|
||
<tr><td><strong>用户认证</strong></td><td>UserInfo, SsoConfig</td><td>UserInfoServiceImpl, SsoServiceImpl, Sso2ServiceImpl</td><td>UserController</td></tr>
|
||
<tr><td><strong>权限管理</strong></td><td>SysRole, SysPermission, SysMenu, SysRolePermission, SysMemberRole</td><td>PermissionServiceImpl</td><td>PermissionController</td></tr>
|
||
<tr><td><strong>租户管理</strong></td><td>CustomerInfo, CustomerUser, SysOrganization</td><td>CustomerInfoServiceImpl, CustomerUserServiceImpl, SysOrganizationServiceImpl</td><td>CustomerController, SysOrganizationController</td></tr>
|
||
<tr><td><strong>套餐配额</strong></td><td>SysPackagePlan, SysPackagePlanPermission, SysQuota, CustomerPackage, CustomerPackageQuota</td><td>SysPackagePlanServiceImpl, CustomerPackageServiceImpl, SysQuotaServiceImpl</td><td>SysPackagePlanController, CustomerPackageController</td></tr>
|
||
<tr><td><strong>开放认证</strong></td><td>SysAccessToken</td><td>SysAccessTokenServiceImpl</td><td>AccessTokenController</td></tr>
|
||
</table>
|
||
|
||
<h3>1.2 耦合度量化</h3>
|
||
|
||
<div class="metric-grid">
|
||
<div class="metric-card">
|
||
<div class="metric-value">52</div>
|
||
<div class="metric-label">文件直接引用 AuthContext</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value">111</div>
|
||
<div class="metric-label">AuthContext 调用总次数</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value">11</div>
|
||
<div class="metric-label">文件引用 OperationLogUtil</div>
|
||
</div>
|
||
<div class="metric-card">
|
||
<div class="metric-value">0</div>
|
||
<div class="metric-label">业务 Service 直接 import 账权 Service</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout ok">
|
||
<div class="callout-title">关键发现</div>
|
||
<p>溯源(Trace*)和过磅(Weight*)等业务模块 <strong>不直接 import 任何账权 Service</strong>,它们仅通过 <code>AuthContext.getLoginInfo().getCustomer().getId()</code> 获取当前租户 ID。这意味着业务模块与账权模块的耦合是<strong>通过共享上下文(ThreadLocal + Redis)间接耦合</strong>,而非直接代码调用——这为服务化拆分提供了极好的基础。</p>
|
||
</div>
|
||
|
||
<h3>1.3 耦合关系详解</h3>
|
||
|
||
<h4>① AuthContext(ThreadLocal)— 全局耦合</h4>
|
||
<p><code>AuthContext</code> 来自外部库 <code>apes-commons</code>,是整个系统的"认证上下文总线"。所有业务模块通过它获取当前登录用户和租户信息:</p>
|
||
<pre><code>// 典型使用模式(出现 52 个文件中)
|
||
LoginCustomer customer = AuthContext.getLoginInfo().getCustomer();
|
||
Long customerId = customer.getId();
|
||
// 然后在查询中添加 WHERE customer_id = customerId</code></pre>
|
||
|
||
<h4>② OperationLogUtil — 静态工具耦合</h4>
|
||
<p><code>OperationLogUtil</code> 内部依赖 <code>AuthContext</code> 获取操作人信息,被 11 个文件调用。它通过 <code>@PostConstruct</code> 注入静态 <code>SysOperationLogService</code>,形成了"静态方法 → Spring Bean → AuthContext"的隐式依赖链。</p>
|
||
|
||
<h4>③ 账权模块内部 — Service 间直接调用</h4>
|
||
<table>
|
||
<tr><th>调用方</th><th>被调用方</th><th>调用内容</th></tr>
|
||
<tr><td>UserInfoServiceImpl</td><td>PermissionServiceImpl</td><td>getUserRoles()</td></tr>
|
||
<tr><td>UserInfoServiceImpl</td><td>CustomerUserServiceImpl</td><td>getCustomers(), getCustomerListByUserIds()</td></tr>
|
||
<tr><td>UserInfoServiceImpl</td><td>CustomerInfoServiceImpl</td><td>getById()</td></tr>
|
||
<tr><td>PermissionServiceImpl</td><td>SsoServiceImpl / Sso2ServiceImpl</td><td>getUserConfig(), getPageUrl()</td></tr>
|
||
<tr><td>PermissionServiceImpl</td><td>UserInfoMapper</td><td>selectById()</td></tr>
|
||
<tr><td>PermissionServiceImpl</td><td>CustomerPackageMapper</td><td>selectList()</td></tr>
|
||
<tr><td>PermissionServiceImpl</td><td>SysPackagePlanMapper</td><td>selectById()</td></tr>
|
||
<tr><td>AccessTokenFilter</td><td>CustomerInfoServiceImpl</td><td>getById()</td></tr>
|
||
</table>
|
||
|
||
<h4>④ Redis 共享会话 — 天然分布式基础</h4>
|
||
<p>登录态存储在 Redis 中(Key = 加密 Token,Value = 用户 JSON;Key = Token:customer,Value = 企业 JSON),TTL 1 天。<strong>这是拆分的最有利条件</strong>——任何服务只要能访问同一个 Redis,就能读取登录态。</p>
|
||
|
||
<h4>⑤ apes-commons 库 — 认证框架黑盒</h4>
|
||
<p>核心认证逻辑(<code>LoginAspect</code> AOP 拦截、<code>AuthContext</code> ThreadLocal 管理、<code>@Login</code> 注解)封装在外部 JAR 包 <code>apes-commons:0.0.7-SNAPSHOT</code> 中,源码不在项目内。当前所有 Controller 通过 <code>@Login</code> 注解声明需要登录校验,由 <code>LoginAspect</code> 从 Redis 读取 Token 并填充 <code>AuthContext</code>。</p>
|
||
|
||
<div class="diagram">
|
||
<svg viewBox="0 0 860 400" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L9,3 z" fill="#8b949e"/>
|
||
</marker>
|
||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L9,3 z" fill="#f85149"/>
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- Title -->
|
||
<text x="430" y="24" text-anchor="middle" fill="#e6edf3" font-size="14" font-weight="600">当前单体架构 — 账权耦合关系图</text>
|
||
|
||
<!-- Monolith box -->
|
||
<rect x="20" y="40" width="820" height="340" rx="12" fill="none" stroke="#30363d" stroke-width="2" stroke-dasharray="6,4"/>
|
||
<text x="40" y="60" fill="#8b949e" font-size="12">单体应用 (service-master)</text>
|
||
|
||
<!-- apes-commons -->
|
||
<rect x="280" y="80" width="300" height="60" rx="8" fill="#21262d" stroke="#bc8cff" stroke-width="2"/>
|
||
<text x="430" y="105" text-anchor="middle" fill="#bc8cff" font-size="13" font-weight="600">apes-commons (外部JAR)</text>
|
||
<text x="430" y="125" text-anchor="middle" fill="#8b949e" font-size="11">AuthContext · @Login · LoginAspect · UserSession</text>
|
||
|
||
<!-- Auth modules -->
|
||
<rect x="40" y="170" width="200" height="140" rx="8" fill="#161b22" stroke="#58a6ff" stroke-width="2"/>
|
||
<text x="140" y="192" text-anchor="middle" fill="#58a6ff" font-size="12" font-weight="600">账权模块</text>
|
||
<text x="140" y="210" text-anchor="middle" fill="#8b949e" font-size="10">UserInfoService</text>
|
||
<text x="140" y="225" text-anchor="middle" fill="#8b949e" font-size="10">PermissionService</text>
|
||
<text x="140" y="240" text-anchor="middle" fill="#8b949e" font-size="10">CustomerInfoService</text>
|
||
<text x="140" y="255" text-anchor="middle" fill="#8b949e" font-size="10">AccessTokenService</text>
|
||
<text x="140" y="270" text-anchor="middle" fill="#8b949e" font-size="10">PackagePlanService</text>
|
||
<text x="140" y="285" text-anchor="middle" fill="#8b949e" font-size="10">SsoService</text>
|
||
|
||
<!-- Business modules -->
|
||
<rect x="300" y="170" width="200" height="140" rx="8" fill="#161b22" stroke="#3fb950" stroke-width="2"/>
|
||
<text x="400" y="192" text-anchor="middle" fill="#3fb950" font-size="12" font-weight="600">业务模块</text>
|
||
<text x="400" y="210" text-anchor="middle" fill="#8b949e" font-size="10">TraceProductService</text>
|
||
<text x="400" y="225" text-anchor="middle" fill="#8b949e" font-size="10">WeightTicketService</text>
|
||
<text x="400" y="240" text-anchor="middle" fill="#8b949e" font-size="10">TraceCodeService</text>
|
||
<text x="400" y="255" text-anchor="middle" fill="#8b949e" font-size="10">ExtLinkService</text>
|
||
<text x="400" y="270" text-anchor="middle" fill="#8b949e" font-size="10">CustomerUiConfigService</text>
|
||
<text x="400" y="285" text-anchor="middle" fill="#8b949e" font-size="10">FeiFertigationService</text>
|
||
|
||
<!-- LLM/Agent modules -->
|
||
<rect x="560" y="170" width="200" height="140" rx="8" fill="#161b22" stroke="#d29922" stroke-width="2"/>
|
||
<text x="660" y="192" text-anchor="middle" fill="#d29922" font-size="12" font-weight="600">AI/Agent 模块</text>
|
||
<text x="660" y="210" text-anchor="middle" fill="#8b949e" font-size="10">ChatSessionService</text>
|
||
<text x="660" y="225" text-anchor="middle" fill="#8b949e" font-size="10">ToolService</text>
|
||
<text x="660" y="240" text-anchor="middle" fill="#8b949e" font-size="10">AgentService</text>
|
||
<text x="660" y="255" text-anchor="middle" fill="#8b949e" font-size="10">KnowledgeService</text>
|
||
|
||
<!-- Redis -->
|
||
<ellipse cx="430" cy="350" rx="80" ry="22" fill="#21262d" stroke="#f85149" stroke-width="2"/>
|
||
<text x="430" y="355" text-anchor="middle" fill="#f85149" font-size="12" font-weight="600">Redis (共享会话)</text>
|
||
|
||
<!-- Arrows: AuthContext dependency -->
|
||
<line x1="140" y1="170" x2="350" y2="110" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
|
||
<line x1="400" y1="170" x2="430" y2="140" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
|
||
<line x1="660" y1="170" x2="510" y2="110" stroke="#bc8cff" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
|
||
<text x="250" y="145" fill="#bc8cff" font-size="9">AuthContext</text>
|
||
<text x="555" y="145" fill="#bc8cff" font-size="9">AuthContext</text>
|
||
|
||
<!-- Arrows: Internal auth calls -->
|
||
<line x1="170" y1="200" x2="170" y2="200" stroke="#58a6ff" stroke-width="1"/>
|
||
|
||
<!-- Arrows: Redis -->
|
||
<line x1="140" y1="310" x2="380" y2="335" stroke="#f85149" stroke-width="1.5" marker-end="url(#arrow-red)" stroke-dasharray="4,3"/>
|
||
<line x1="430" y1="310" x2="430" y2="328" stroke="#f85149" stroke-width="1.5" marker-end="url(#arrow-red)" stroke-dasharray="4,3"/>
|
||
<text x="200" y="330" fill="#f85149" font-size="9">读写 Token</text>
|
||
<text x="440" y="325" fill="#f85149" font-size="9">读 Token</text>
|
||
|
||
<!-- No direct call label -->
|
||
<rect x="225" y="225" width="90" height="22" rx="4" fill="#0d1117" stroke="#3fb950" stroke-width="1"/>
|
||
<text x="270" y="240" text-anchor="middle" fill="#3fb950" font-size="9" font-weight="600">无直接调用 ✓</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- ===================== 2. 可行性判断 ===================== -->
|
||
<h2 id="s2"><span class="num">2</span> 可行性判断:能不能拆?该不该拆?</h2>
|
||
|
||
<h3>2.1 能不能拆?—— 技术可行性</h3>
|
||
|
||
<div class="compare-grid">
|
||
<div class="compare-card">
|
||
<h4 style="color:var(--c-green)">✓ 有利因素</h4>
|
||
<ul>
|
||
<li><strong>Redis 共享会话</strong>:登录态天然存储在 Redis,任何服务都能读取,无需迁移认证机制</li>
|
||
<li><strong>业务模块零直接依赖</strong>:Trace*/Weight* 不 import 账权 Service,仅通过 AuthContext 获取租户 ID</li>
|
||
<li><strong>无数据库外键约束</strong>:所有表关联为逻辑关联,数据库拆分无物理障碍</li>
|
||
<li><strong>AccessToken 机制已具备开放能力</strong>:已有外部系统认证的基础设施</li>
|
||
<li><strong>无跨服务远程调用</strong>:当前无 Feign/RestTemplate,不需要重构已有远程调用</li>
|
||
<li><strong>数据源已配置多库</strong>:dynamic-datasource 框架已就位,多库支持基础已有</li>
|
||
</ul>
|
||
</div>
|
||
<div class="compare-card">
|
||
<h4 style="color:var(--c-orange)">⚠ 主要障碍</h4>
|
||
<ul>
|
||
<li><strong>AuthContext 深度耦合</strong>:52 个文件、111 处调用,需统一替换为新的上下文传递机制</li>
|
||
<li><strong>apes-commons 黑盒</strong>:认证核心逻辑在外部 JAR,需替换或重构认证框架</li>
|
||
<li><strong>OperationLogUtil 静态依赖</strong>:11 个文件通过静态方法调用,内部依赖 AuthContext</li>
|
||
<li><strong>账权 Service 间内部调用</strong>:UserInfoService → PermissionService → SsoService 等直接注入</li>
|
||
<li><strong>共享数据库</strong>:所有表在同一 MySQL 库,拆分需数据库迁移</li>
|
||
<li><strong>AOP 执行顺序依赖</strong>:DataSourceHeaderAspect(Order=1) 与 LoginAspect 有隐式协作</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout ok">
|
||
<div class="callout-title">结论:技术上可行</div>
|
||
<p>核心判断依据是<strong>"业务模块与账权模块之间不存在直接代码调用"</strong>。耦合仅发生在 AuthContext(ThreadLocal 上下文)层面,这种耦合可以通过<strong>网关解析 Token + HTTP Header 传递</strong>的方式完全消除。改造工作量主要集中在替换 AuthContext 和解耦 apes-commons,而非重构业务代码。</p>
|
||
</div>
|
||
|
||
<h3>2.2 该不该拆?—— 业务驱动力分析</h3>
|
||
|
||
<table>
|
||
<tr><th>维度</th><th>当前痛点</th><th>独立后的收益</th><th>优先级</th></tr>
|
||
<tr>
|
||
<td><strong>多产品复用</strong></td>
|
||
<td>账权逻辑锁在单体中,新产品(如独立 AI Agent 平台、移动端)要么整体依赖单体,要么重复实现</td>
|
||
<td>一套账权服务支撑多个前端产品线,避免重复建设</td>
|
||
<td><span class="badge badge-red">高</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>独立演进</strong></td>
|
||
<td>账权变更需整体重新部署,业务模块也跟着重启</td>
|
||
<td>账权服务独立发布、独立扩缩容,不影响业务服务</td>
|
||
<td><span class="badge badge-orange">中</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>开放平台</strong></td>
|
||
<td>AccessToken 机制已在单体内,但第三方接入需走单体 API,无法独立管控</td>
|
||
<td>独立 OAuth2 服务,支持第三方应用授权、API 网关统一鉴权</td>
|
||
<td><span class="badge badge-orange">中</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>安全合规</strong></td>
|
||
<td>用户密码、Token 等敏感数据与业务数据混在同一库,权限边界模糊</td>
|
||
<td>账权数据库独立,可实施更严格的安全策略(网络隔离、审计、加密)</td>
|
||
<td><span class="badge badge-blue">低</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>性能隔离</strong></td>
|
||
<td>大量登录/鉴权请求与业务请求混在同一进程,相互影响</td>
|
||
<td>账权服务可独立扩容,登录高峰不影响业务处理</td>
|
||
<td><span class="badge badge-blue">低</span></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="callout warn">
|
||
<div class="callout-title">建议:有条件地拆</div>
|
||
<p>如果团队有<strong>多产品线复用账权</strong>的需求(如独立 AI Agent 平台、移动端 App、开放平台),则拆分收益明确,值得投入。如果当前只有一个前端产品且近期无扩展计划,则优先在单体内做<strong>模块化隔离</strong>(包结构拆分 + 接口抽象)即可,暂不需微服务化。</p>
|
||
</div>
|
||
|
||
<!-- ===================== 3. 目标架构 ===================== -->
|
||
<h2 id="s2-3" id="s3"><span class="num">3</span> 目标架构设计</h2>
|
||
|
||
<h3>3.1 整体架构</h3>
|
||
|
||
<div class="diagram">
|
||
<svg viewBox="0 0 860 520" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<marker id="arrow2" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||
<path d="M0,0 L0,6 L9,3 z" fill="#8b949e"/>
|
||
</marker>
|
||
</defs>
|
||
|
||
<text x="430" y="24" text-anchor="middle" fill="#e6edf3" font-size="14" font-weight="600">目标架构 — 账权服务独立化</text>
|
||
|
||
<!-- Client -->
|
||
<rect x="300" y="44" width="260" height="36" rx="6" fill="#21262d" stroke="#8b949e" stroke-width="1.5"/>
|
||
<text x="430" y="67" text-anchor="middle" fill="#e6edf3" font-size="12">Web 前端 / 移动端 / 第三方应用</text>
|
||
|
||
<!-- Gateway -->
|
||
<rect x="260" y="110" width="340" height="50" rx="8" fill="#21262d" stroke="#58a6ff" stroke-width="2"/>
|
||
<text x="430" y="132" text-anchor="middle" fill="#58a6ff" font-size="13" font-weight="600">API Gateway (Spring Cloud Gateway)</text>
|
||
<text x="430" y="148" text-anchor="middle" fill="#8b949e" font-size="10">Token 解析 → 注入 X-User-Id / X-Customer-Id / X-Roles Header</text>
|
||
|
||
<!-- Line to gateway -->
|
||
<line x1="430" y1="80" x2="430" y2="110" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arrow2)"/>
|
||
|
||
<!-- Auth Service -->
|
||
<rect x="30" y="200" width="240" height="160" rx="10" fill="#161b22" stroke="#bc8cff" stroke-width="2"/>
|
||
<text x="150" y="222" text-anchor="middle" fill="#bc8cff" font-size="13" font-weight="600">Identity Service (账权服务)</text>
|
||
<line x1="50" y1="232" x2="250" y2="232" stroke="#30363d" stroke-width="1"/>
|
||
<text x="150" y="250" text-anchor="middle" fill="#e6edf3" font-size="11">· 用户注册/登录/密码管理</text>
|
||
<text x="150" y="266" text-anchor="middle" fill="#e6edf3" font-size="11">· 角色/权限/菜单管理</text>
|
||
<text x="150" y="282" text-anchor="middle" fill="#e6edf3" font-size="11">· 租户(企业)/组织架构</text>
|
||
<text x="150" y="298" text-anchor="middle" fill="#e6edf3" font-size="11">· 套餐/配额管理</text>
|
||
<text x="150" y="314" text-anchor="middle" fill="#e6edf3" font-size="11">· AccessToken / OAuth2</text>
|
||
<text x="150" y="330" text-anchor="middle" fill="#e6edf3" font-size="11">· SSO 对接</text>
|
||
<text x="150" y="346" text-anchor="middle" fill="#8b949e" font-size="10">Spring Boot · 独立部署 · 独立数据库</text>
|
||
|
||
<!-- Business Service -->
|
||
<rect x="310" y="200" width="240" height="160" rx="10" fill="#161b22" stroke="#3fb950" stroke-width="2"/>
|
||
<text x="430" y="222" text-anchor="middle" fill="#3fb950" font-size="13" font-weight="600">Business Service (业务服务)</text>
|
||
<line x1="330" y1="232" x2="530" y2="232" stroke="#30363d" stroke-width="1"/>
|
||
<text x="430" y="250" text-anchor="middle" fill="#e6edf3" font-size="11">· 溯源管理 (Trace*)</text>
|
||
<text x="430" y="266" text-anchor="middle" fill="#e6edf3" font-size="11">· 自助过磅 (Weight*)</text>
|
||
<text x="430" y="282" text-anchor="middle" fill="#e6edf3" font-size="11">· 智慧农业 (FeiFertigation*)</text>
|
||
<text x="430" y="298" text-anchor="middle" fill="#e6edf3" font-size="11">· 外部链接 (ExtLink*)</text>
|
||
<text x="430" y="314" text-anchor="middle" fill="#e6edf3" font-size="11">· UI 配置 (CustomerUiConfig*)</text>
|
||
<text x="430" y="346" text-anchor="middle" fill="#8b949e" font-size="10">Spring Boot · 独立部署 · 独立数据库</text>
|
||
|
||
<!-- AI/Agent Service -->
|
||
<rect x="590" y="200" width="240" height="160" rx="10" fill="#161b22" stroke="#d29922" stroke-width="2"/>
|
||
<text x="710" y="222" text-anchor="middle" fill="#d29922" font-size="13" font-weight="600">AI Agent Service (智能服务)</text>
|
||
<line x1="610" y1="232" x2="810" y2="232" stroke="#30363d" stroke-width="1"/>
|
||
<text x="710" y="250" text-anchor="middle" fill="#e6edf3" font-size="11">· 对话管理 (ChatSession*)</text>
|
||
<text x="710" y="266" text-anchor="middle" fill="#e6edf3" font-size="11">· Function Calling</text>
|
||
<text x="710" y="282" text-anchor="middle" fill="#e6edf3" font-size="11">· 知识库 (Knowledge*)</text>
|
||
<text x="710" y="298" text-anchor="middle" fill="#e6edf3" font-size="11">· Agent 编排</text>
|
||
<text x="710" y="346" text-anchor="middle" fill="#8b949e" font-size="10">Spring Boot · 独立部署 · 独立数据库</text>
|
||
|
||
<!-- Gateway to services -->
|
||
<line x1="340" y1="160" x2="150" y2="200" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arrow2)"/>
|
||
<line x1="430" y1="160" x2="430" y2="200" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arrow2)"/>
|
||
<line x1="520" y1="160" x2="710" y2="200" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arrow2)"/>
|
||
<text x="220" y="185" fill="#8b949e" font-size="9">/auth/** /user/**</text>
|
||
<text x="440" y="185" fill="#8b949e" font-size="9">/trace/** /weight/**</text>
|
||
<text x="600" y="185" fill="#8b949e" font-size="9">/ai/** /agent/**</text>
|
||
|
||
<!-- Auth DB -->
|
||
<ellipse cx="150" cy="410" rx="80" ry="22" fill="#21262d" stroke="#bc8cff" stroke-width="1.5"/>
|
||
<text x="150" y="415" text-anchor="middle" fill="#bc8cff" font-size="11" font-weight="600">auth_db (MySQL)</text>
|
||
|
||
<!-- Business DB -->
|
||
<ellipse cx="430" cy="410" rx="80" ry="22" fill="#21262d" stroke="#3fb950" stroke-width="1.5"/>
|
||
<text x="430" y="415" text-anchor="middle" fill="#3fb950" font-size="11" font-weight="600">business_db (MySQL)</text>
|
||
|
||
<!-- AI DB -->
|
||
<ellipse cx="710" cy="410" rx="80" ry="22" fill="#21262d" stroke="#d29922" stroke-width="1.5"/>
|
||
<text x="710" y="415" text-anchor="middle" fill="#d29922" font-size="11" font-weight="600">ai_db (MySQL)</text>
|
||
|
||
<!-- DB connections -->
|
||
<line x1="150" y1="360" x2="150" y2="388" stroke="#bc8cff" stroke-width="1" stroke-dasharray="3,3"/>
|
||
<line x1="430" y1="360" x2="430" y2="388" stroke="#3fb950" stroke-width="1" stroke-dasharray="3,3"/>
|
||
<line x1="710" y1="360" x2="710" y2="388" stroke="#d29922" stroke-width="1" stroke-dasharray="3,3"/>
|
||
|
||
<!-- Redis -->
|
||
<ellipse cx="430" cy="470" rx="90" ry="22" fill="#21262d" stroke="#f85149" stroke-width="1.5"/>
|
||
<text x="430" y="475" text-anchor="middle" fill="#f85149" font-size="11" font-weight="600">Redis (共享会话 + 缓存)</text>
|
||
|
||
<!-- Redis connections -->
|
||
<line x1="150" y1="432" x2="360" y2="460" stroke="#f85149" stroke-width="1" stroke-dasharray="3,3"/>
|
||
<line x1="430" y1="432" x2="430" y2="448" stroke="#f85149" stroke-width="1" stroke-dasharray="3,3"/>
|
||
<line x1="710" y1="432" x2="500" y2="460" stroke="#f85149" stroke-width="1" stroke-dasharray="3,3"/>
|
||
|
||
<!-- Auth SDK -->
|
||
<rect x="610" y="440" width="180" height="36" rx="6" fill="#21262d" stroke="#39d0d8" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||
<text x="700" y="463" text-anchor="middle" fill="#39d0d8" font-size="11">auth-sdk (共享 JAR)</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<h3>3.2 架构要点说明</h3>
|
||
|
||
<table>
|
||
<tr><th>组件</th><th>职责</th><th>技术选型</th></tr>
|
||
<tr>
|
||
<td><strong>API Gateway</strong></td>
|
||
<td>统一入口,路由转发,Token 解析与校验,将用户信息注入 HTTP Header 传递给下游服务</td>
|
||
<td>Spring Cloud Gateway</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Identity Service</strong></td>
|
||
<td>用户/权限/租户/套餐/AccessToken 的全部管理,Token 签发与校验</td>
|
||
<td>Spring Boot + MyBatis Plus</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Business Service</strong></td>
|
||
<td>溯源/过磅/农业等业务功能,通过 Header 接收用户上下文</td>
|
||
<td>原有单体业务代码剥离</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>auth-sdk</strong></td>
|
||
<td>轻量级 SDK JAR,供 Business/AI 服务引入,提供:Header 解析、AuthContext 替代品、权限校验注解</td>
|
||
<td>纯 Java JAR,无 Spring 依赖</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Redis</strong></td>
|
||
<td>共享会话存储(Token → 用户/租户信息),所有服务共用</td>
|
||
<td>Redisson(沿用现有)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>消息队列(可选)</strong></td>
|
||
<td>用户/租户信息变更时广播通知,减少跨服务实时查询</td>
|
||
<td>RabbitMQ / RocketMQ</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<!-- ===================== 4. 核心技术方案 ===================== -->
|
||
<h2 id="s4"><span class="num">4</span> 核心技术方案</h2>
|
||
|
||
<h3>4.1 认证上下文传递机制改造</h3>
|
||
|
||
<p><strong>核心变更</strong>:用"网关解析 Token → HTTP Header 传递"替代当前的"每个服务各自从 Redis 读取 Token → 填充 ThreadLocal"。</p>
|
||
|
||
<h4>改造前(当前架构)</h4>
|
||
<pre><code>请求 → AccessTokenFilter → DataSourceHeaderAspect(clean AuthContext)
|
||
→ LoginAspect(从Redis读Token → 填充AuthContext)
|
||
→ Controller(@Login) → Service(AuthContext.getLoginInfo())</code></pre>
|
||
|
||
<h4>改造后(目标架构)</h4>
|
||
<pre><code>请求 → API Gateway
|
||
├─ 从 Header 提取 Token
|
||
├─ 调用 Identity Service 的 /auth/verify 接口(或直接读 Redis)
|
||
├─ 解析出 userId, customerId, roles, permissions
|
||
└─ 注入 HTTP Header: X-User-Id, X-Customer-Id, X-User-Name, X-Roles
|
||
→ 路由到下游服务
|
||
├─ auth-sdk 的 AuthFilter 解析 Header → 填充 ServiceAuthContext
|
||
└─ Controller → Service(ServiceAuthContext.getUserId())</code></pre>
|
||
|
||
<h4>auth-sdk 设计</h4>
|
||
<pre><code>// auth-sdk 提供的核心类(替代 apes-commons 的 AuthContext)
|
||
|
||
public class ServiceAuthContext {
|
||
private static final ThreadLocal<AuthInfo> CONTEXT = new ThreadLocal<>();
|
||
|
||
public static void set(AuthInfo info) { CONTEXT.set(info); }
|
||
public static AuthInfo get() { return CONTEXT.get(); }
|
||
public static void clear() { CONTEXT.remove(); }
|
||
|
||
// 便捷方法
|
||
public static Long getUserId() { return get() != null ? get().getUserId() : null; }
|
||
public static Long getCustomerId() { return get() != null ? get().getCustomerId() : null; }
|
||
public static String getUserName() { return get() != null ? get().getUserName() : null; }
|
||
public static List<String> getRoles() { return get() != null ? get().getRoles() : Collections.emptyList(); }
|
||
}
|
||
|
||
public class AuthInfo {
|
||
private Long userId;
|
||
private Long customerId;
|
||
private String userName;
|
||
private String userPhone;
|
||
private List<String> roles;
|
||
private List<String> permissions;
|
||
// getters/setters...
|
||
}
|
||
|
||
// 网关注入的 Header → ThreadLocal 的 Filter
|
||
@Component
|
||
@Order(1)
|
||
public class AuthHeaderFilter implements Filter {
|
||
@Override
|
||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
|
||
HttpServletRequest request = (HttpServletRequest) req;
|
||
AuthInfo info = new AuthInfo();
|
||
info.setUserId(parseLong(request.getHeader("X-User-Id")));
|
||
info.setCustomerId(parseLong(request.getHeader("X-Customer-Id")));
|
||
info.setUserName(request.getHeader("X-User-Name"));
|
||
info.setRoles(parseList(request.getHeader("X-Roles")));
|
||
ServiceAuthContext.set(info);
|
||
try {
|
||
chain.doFilter(req, res);
|
||
} finally {
|
||
ServiceAuthContext.clear();
|
||
}
|
||
}
|
||
}</code></pre>
|
||
|
||
<div class="callout">
|
||
<div class="callout-title">兼容性策略</div>
|
||
<p>为降低改造成本,<code>auth-sdk</code> 可提供一个 <code>AuthContext</code> 兼容适配器,使其 API 与 <code>apes-commons.AuthContext</code> 保持一致(<code>getLoginInfo().getUser().getId()</code> 等),这样业务代码只需将 import 从 <code>cn.apes.commons.auth.AuthContext</code> 改为 <code>cn.apes.sdk.auth.AuthContext</code>,方法调用不变。</p>
|
||
</div>
|
||
|
||
<h3>4.2 Token 机制升级</h3>
|
||
|
||
<p>当前使用 RSA 加密的随机 ID 作为 Token,存储在 Redis 中。建议升级为 <strong>JWT + Redis 混合方案</strong>:</p>
|
||
|
||
<table>
|
||
<tr><th>方案</th><th>说明</th><th>优缺点</th></tr>
|
||
<tr>
|
||
<td><strong>方案 A:JWT(推荐)</strong></td>
|
||
<td>Token 本身携带 userId, customerId, roles 等信息,网关直接解析,无需查 Redis</td>
|
||
<td>✓ 无状态,性能最优 ✓ 网关可独立解析<br>✗ Token 无法主动失效(需配合 Redis 黑名单)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>方案 B:Redis Token(兼容现有)</strong></td>
|
||
<td>保持现有机制,网关从 Redis 读取 Token 信息后注入 Header</td>
|
||
<td>✓ 改造成本最低 ✓ Token 可主动失效<br>✗ 每次请求需查 Redis ✗ 网关依赖 Redis</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>方案 C:JWT + Redis 黑名单</strong></td>
|
||
<td>JWT 无状态解析 + Redis 存储已注销的 Token(黑名单)</td>
|
||
<td>✓ 性能好 ✓ 可主动失效<br>✗ 实现略复杂</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p><strong>推荐方案 C</strong>,JWT Payload 设计:</p>
|
||
<pre><code>{
|
||
"sub": "userId", // 用户ID
|
||
"cid": "customerId", // 租户ID
|
||
"cname": "customerName", // 租户名称
|
||
"uname": "nickName", // 用户昵称
|
||
"phone": "138****1234", // 脱敏手机号
|
||
"roles": ["admin", "user"],// 角色列表
|
||
"perms": ["trace:view", "weight:edit"], // 权限码列表
|
||
"iat": 1691234567, // 签发时间
|
||
"exp": 1691320967 // 过期时间(24h)
|
||
}</code></pre>
|
||
|
||
<h3>4.3 权限校验机制升级</h3>
|
||
|
||
<p>当前系统<strong>仅校验登录态,不校验具体权限</strong>(无 <code>@PreAuthorize</code>、无 Shiro 注解),权限控制依赖前端菜单过滤。服务化后建议增加后端权限校验:</p>
|
||
|
||
<pre><code>// auth-sdk 提供的权限注解 + AOP
|
||
|
||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||
@Retention(RetentionPolicy.RUNTIME)
|
||
public @interface RequiresPermission {
|
||
String[] value(); // 权限码,如 "trace:product:add"
|
||
Logical logical() default Logical.AND;
|
||
}
|
||
|
||
// AOP 切面
|
||
@Aspect
|
||
@Component
|
||
public class PermissionAspect {
|
||
@Around("@annotation(requiresPermission)")
|
||
public Object check(ProceedingJoinPoint pjp, RequiresPermission requiresPermission) throws Throwable {
|
||
List<String> userPerms = ServiceAuthContext.get().getPermissions();
|
||
String[] required = requiresPermission.value();
|
||
boolean hasPermission = requiresPermission.logical() == Logical.AND
|
||
? Arrays.asList(required).stream().allMatch(userPerms::contains)
|
||
: Arrays.asList(required).stream().anyMatch(userPerms::contains);
|
||
if (!hasPermission) {
|
||
throw new ForbiddenException("权限不足");
|
||
}
|
||
return pjp.proceed();
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
@RestController
|
||
@RequestMapping("/trace/product")
|
||
public class TraceProductController {
|
||
@RequiresPermission("trace:product:add")
|
||
@PostMapping
|
||
public Res add(@RequestBody TraceProductDTO dto) { ... }
|
||
|
||
@RequiresPermission("trace:product:delete")
|
||
@DeleteMapping("/{id}")
|
||
public Res delete(@PathVariable Long id) { ... }
|
||
}</code></pre>
|
||
|
||
<h3>4.4 跨服务数据查询方案</h3>
|
||
|
||
<p>业务服务偶尔需要查询用户/租户信息(如显示操作人姓名)。方案:</p>
|
||
|
||
<table>
|
||
<tr><th>方案</th><th>适用场景</th><th>实现</th></tr>
|
||
<tr>
|
||
<td><strong>JWT Payload 携带</strong></td>
|
||
<td>仅需 userId, customerId, userName 等基础信息</td>
|
||
<td>直接从 ServiceAuthContext 读取,零网络开销</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Identity Service API</strong></td>
|
||
<td>需要完整用户/租户信息</td>
|
||
<td>Feign Client 调用 <code>GET /auth/user/{id}</code>、<code>GET /auth/customer/{id}</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Redis 缓存 + MQ 同步</strong></td>
|
||
<td>高频查询的场景</td>
|
||
<td>用户信息变更时通过 MQ 广播,业务服务更新本地 Redis 缓存</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<pre><code>// Feign Client 示例
|
||
@FeignClient(name = "identity-service", url = "${service.identity.url}")
|
||
public interface IdentityFeignClient {
|
||
|
||
@GetMapping("/auth/user/{userId}")
|
||
Res<UserDTO> getUser(@PathVariable("userId") Long userId);
|
||
|
||
@GetMapping("/auth/customer/{customerId}")
|
||
Res<CustomerDTO> getCustomer(@PathVariable("customerId") Long customerId);
|
||
|
||
@GetMapping("/auth/user/{userId}/permissions")
|
||
Res<List<String>> getUserPermissions(@PathVariable("userId") Long userId,
|
||
@RequestParam("customerId") Long customerId);
|
||
|
||
@GetMapping("/auth/user/{userId}/menus")
|
||
Res<List<MenuDTO>> getUserMenus(@PathVariable("userId") Long userId,
|
||
@RequestParam("customerId") Long customerId);
|
||
}</code></pre>
|
||
|
||
<h3>4.5 OperationLogUtil 改造</h3>
|
||
|
||
<p>当前 <code>OperationLogUtil</code> 静态方法内部依赖 <code>AuthContext</code>,改造为依赖 <code>ServiceAuthContext</code>:</p>
|
||
|
||
<pre><code>// 改造前
|
||
public static void log(String module, String action, String description) {
|
||
LoginUser user = AuthContext.getLoginInfo().getUser(); // 依赖 apes-commons
|
||
LoginCustomer customer = AuthContext.getLoginInfo().getCustomer();
|
||
// ...
|
||
}
|
||
|
||
// 改造后
|
||
public static void log(String module, String action, String description) {
|
||
AuthInfo auth = ServiceAuthContext.get(); // 依赖 auth-sdk
|
||
if (auth == null) return;
|
||
logService.log(auth.getUserId(), auth.getUserName(),
|
||
auth.getCustomerId(), auth.getCustomerName(),
|
||
module, action, maskDescription(description));
|
||
}</code></pre>
|
||
|
||
<div class="callout ok">
|
||
<div class="callout-title">零侵入改造</div>
|
||
<p>由于 <code>OperationLogUtil</code> 的 <code>log()</code> 方法签名不变,11 个调用方文件<strong>无需修改任何代码</strong>——只需更新 <code>OperationLogUtil</code> 自身的实现(从 <code>AuthContext</code> 切换到 <code>ServiceAuthContext</code>),并将 <code>OperationLogUtil</code> 迁移到 <code>auth-sdk</code> 中。</p>
|
||
</div>
|
||
|
||
<!-- ===================== 5. 数据库拆分 ===================== -->
|
||
<h2 id="s5"><span class="num">5</span> 数据库拆分策略</h2>
|
||
|
||
<h3>5.1 表归属划分</h3>
|
||
|
||
<table>
|
||
<tr><th>归属</th><th>表名</th><th>说明</th></tr>
|
||
<tr>
|
||
<td rowspan="14"><strong>auth_db</strong><br>(Identity Service)</td>
|
||
<td><code>user_info</code></td>
|
||
<td>用户基础信息</td>
|
||
</tr>
|
||
<tr><td><code>customer_info</code></td><td>租户(企业)信息</td></tr>
|
||
<tr><td><code>customer_user</code></td><td>用户-租户关联</td></tr>
|
||
<tr><td><code>sys_role</code></td><td>角色</td></tr>
|
||
<tr><td><code>sys_permission</code></td><td>权限</td></tr>
|
||
<tr><td><code>sys_menu</code></td><td>菜单</td></tr>
|
||
<tr><td><code>sys_role_permission</code></td><td>角色-权限关联</td></tr>
|
||
<tr><td><code>sys_member_role</code></td><td>用户-角色关联(含租户ID)</td></tr>
|
||
<tr><td><code>sys_organization</code></td><td>组织架构</td></tr>
|
||
<tr><td><code>sys_access_token</code></td><td>AccessToken</td></tr>
|
||
<tr><td><code>sys_package_plan</code></td><td>套餐计划</td></tr>
|
||
<tr><td><code>sys_package_plan_permission</code></td><td>套餐-权限关联</td></tr>
|
||
<tr><td><code>sys_quota</code> / <code>sys_package_plan_quota</code></td><td>配额模板</td></tr>
|
||
<tr><td><code>sso_config</code></td><td>SSO 配置</td></tr>
|
||
<tr>
|
||
<td rowspan="4"><strong>auth_db</strong><br>(扩展)</td>
|
||
<td><code>customer_package</code></td><td>客户套餐关联</td>
|
||
</tr>
|
||
<tr><td><code>customer_package_quota</code></td><td>客户配额实例</td></tr>
|
||
<tr><td><code>customer_quota_change_log</code></td><td>配额变更日志</td></tr>
|
||
<tr><td><code>customer_package_extend_apply</code></td><td>套餐延期申请</td></tr>
|
||
<tr>
|
||
<td rowspan="3"><strong>business_db</strong><br>(Business Service)</td>
|
||
<td><code>trace_*</code> (20+ 表)</td><td>溯源业务表</td>
|
||
</tr>
|
||
<tr><td><code>weight_*</code> (10+ 表)</td><td>过磅业务表</td></tr>
|
||
<tr><td><code>customer_ui_config</code> / <code>ext_link</code> 等</td><td>业务配置表</td></tr>
|
||
<tr>
|
||
<td><strong>ai_db</strong><br>(AI Service)</td>
|
||
<td><code>chat_session</code> / <code>chat_message</code> / <code>tool_*</code> 等</td>
|
||
<td>AI 对话相关表</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<h3>5.2 跨库查询处理</h3>
|
||
|
||
<p>当前系统中,业务表通过 <code>customer_id</code> 关联 <code>customer_info</code>。拆库后:</p>
|
||
|
||
<ul>
|
||
<li><strong>常规场景</strong>:业务表只需要 <code>customer_id</code> 作为外键标识,不需要 JOIN 查询 <code>customer_info</code>——租户名称等信息从 JWT Payload 或 <code>ServiceAuthContext</code> 获取</li>
|
||
<li><strong>列表展示场景</strong>:需要显示租户名称时,先查业务表获取 <code>customer_id</code> 列表,再批量调用 Identity Service API 补全名称(或使用 Redis 缓存)</li>
|
||
<li><strong>操作日志表</strong>:<code>sys_operation_log</code> 表已冗余存储 <code>user_name</code> 和 <code>customer_name</code>,可放入 business_db 或独立日志库,不依赖 auth_db</li>
|
||
</ul>
|
||
|
||
<h3>5.3 套餐配额的跨服务消费</h3>
|
||
|
||
<p>套餐配额(如溯源码生成数量限制)需要在业务服务中检查。方案:</p>
|
||
<pre><code>// 业务服务通过 Feign 调用 Identity Service 检查配额
|
||
@PostMapping("/trace/code/generate")
|
||
public Res generateCode(@RequestBody GenerateDTO dto) {
|
||
// 1. 检查配额
|
||
QuotaCheckResult result = identityFeignClient.checkQuota(
|
||
ServiceAuthContext.getCustomerId(), "trace_code", dto.getCount()
|
||
);
|
||
if (!result.isAllowed()) {
|
||
return Res.fail("配额不足,剩余: " + result.getRemaining());
|
||
}
|
||
// 2. 执行业务
|
||
traceCodeService.generate(dto);
|
||
// 3. 扣减配额(Identity Service 内部处理)
|
||
identityFeignClient.consumeQuota(
|
||
ServiceAuthContext.getCustomerId(), "trace_code", dto.getCount()
|
||
);
|
||
return Res.ok();
|
||
}</code></pre>
|
||
|
||
<!-- ===================== 6. 解耦改造详细方案 ===================== -->
|
||
<h2 id="s6"><span class="num">6</span> 解耦改造详细方案</h2>
|
||
|
||
<h3>6.1 改造影响范围矩阵</h3>
|
||
|
||
<table>
|
||
<tr><th>改造项</th><th>影响文件数</th><th>改造方式</th><th>难度</th></tr>
|
||
<tr><td>AuthContext → ServiceAuthContext</td><td>52</td><td>替换 import(兼容 API)</td><td><span class="badge badge-green">低</span></td></tr>
|
||
<tr><td>OperationLogUtil 实现</td><td>1(自身)</td><td>内部实现替换,迁移到 auth-sdk</td><td><span class="badge badge-green">低</span></td></tr>
|
||
<tr><td>OperationLogUtil 调用方</td><td>11</td><td>更新 import(方法签名不变)</td><td><span class="badge badge-green">低</span></td></tr>
|
||
<tr><td>apes-commons 替换</td><td>全局</td><td>Maven 依赖替换为 auth-sdk</td><td><span class="badge badge-orange">中</span></td></tr>
|
||
<tr><td>@Login 注解替换</td><td>62(所有Controller)</td><td>替换注解 import 或保留兼容</td><td><span class="badge badge-orange">中</span></td></tr>
|
||
<tr><td>DataSourceHeaderAspect</td><td>1</td><td>重构或移除(网关接管数据源路由)</td><td><span class="badge badge-orange">中</span></td></tr>
|
||
<tr><td>AccessTokenFilter</td><td>1</td><td>迁移到网关层</td><td><span class="badge badge-orange">中</span></td></tr>
|
||
<tr><td>账权 Service 间调用</td><td>~8</td><td>保持同服务内调用(在 Identity Service 内部)</td><td><span class="badge badge-green">低</span></td></tr>
|
||
<tr><td>业务 Service → 账权 Service</td><td>0</td><td>无需改造(本来就没有直接调用)</td><td><span class="badge badge-green">无</span></td></tr>
|
||
<tr><td>数据库拆分</td><td>—</td><td>导出 auth 相关表到新库</td><td><span class="badge badge-orange">中</span></td></tr>
|
||
</table>
|
||
|
||
<h3>6.2 AuthContext 兼容适配方案(最小化改动)</h3>
|
||
|
||
<p>为了将 52 个文件的改动降到最低,<code>auth-sdk</code> 提供 API 完全兼容的 <code>AuthContext</code> 替代品:</p>
|
||
|
||
<pre><code>// auth-sdk 中的兼容 AuthContext(包名可保持一致或通过 Maven relocation)
|
||
package cn.apes.sdk.auth;
|
||
|
||
// 保持与 apes-commons.AuthContext 完全相同的 API
|
||
public class AuthContext {
|
||
private static final ThreadLocal<UserSession> HOLDER = new ThreadLocal<>();
|
||
|
||
public static UserSession getLoginInfo() {
|
||
return HOLDER.get();
|
||
}
|
||
|
||
public static void setLoginInfo(UserSession session) {
|
||
HOLDER.set(session);
|
||
}
|
||
|
||
public static void clean() {
|
||
HOLDER.remove();
|
||
}
|
||
}
|
||
|
||
// UserSession 保持字段结构一致
|
||
public class UserSession {
|
||
private LoginUser user;
|
||
private LoginCustomer customer;
|
||
private String token;
|
||
// getters/setters...
|
||
}
|
||
|
||
// AuthHeaderFilter 从 HTTP Header 构建 UserSession
|
||
@Component
|
||
@Order(1)
|
||
public class AuthHeaderFilter implements Filter {
|
||
@Override
|
||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||
throws IOException, ServletException {
|
||
HttpServletRequest request = (HttpServletRequest) req;
|
||
|
||
// 从网关注入的 Header 构建 UserSession
|
||
LoginUser user = new LoginUser();
|
||
user.setId(parseLong(request.getHeader("X-User-Id")));
|
||
user.setNickName(request.getHeader("X-User-Name"));
|
||
user.setPhone(request.getHeader("X-User-Phone"));
|
||
|
||
LoginCustomer customer = new LoginCustomer();
|
||
customer.setId(parseLong(request.getHeader("X-Customer-Id")));
|
||
customer.setCustomerName(request.getHeader("X-Customer-Name"));
|
||
|
||
UserSession session = new UserSession();
|
||
session.setUser(user);
|
||
session.setCustomer(customer);
|
||
session.setToken(request.getHeader("X-Token"));
|
||
|
||
AuthContext.setLoginInfo(session);
|
||
try {
|
||
chain.doFilter(req, res);
|
||
} finally {
|
||
AuthContext.clean();
|
||
}
|
||
}
|
||
}</code></pre>
|
||
|
||
<p>这样业务代码的改动仅为<strong>一行 import 替换</strong>:</p>
|
||
<pre><code>// 改前
|
||
import cn.apes.commons.auth.AuthContext;
|
||
|
||
// 改后
|
||
import cn.apes.sdk.auth.AuthContext;
|
||
|
||
// 方法调用完全不变
|
||
AuthContext.getLoginInfo().getCustomer().getId(); // ✓</code></pre>
|
||
|
||
<h3>6.3 @Login 注解兼容方案</h3>
|
||
|
||
<p>当前 62 个 Controller 类/方法使用 <code>@Login</code> 注解。在 <code>auth-sdk</code> 中提供同名注解,并由 <code>AuthHeaderFilter</code> 替代 <code>LoginAspect</code> 的功能:</p>
|
||
|
||
<pre><code>// auth-sdk 中提供 @Login 注解(标记需要登录的接口)
|
||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||
@Retention(RetentionPolicy.RUNTIME)
|
||
public @interface Login {
|
||
boolean required() default true;
|
||
}
|
||
|
||
// auth-sdk 中的 LoginAspect(仅校验 AuthContext 是否有值,不查 Redis)
|
||
@Aspect
|
||
@Component
|
||
@Order(2)
|
||
public class LoginAspect {
|
||
@Around("@within(login) || @annotation(login)")
|
||
public Object check(ProceedingJoinPoint pjp, Login login) throws Throwable {
|
||
if (login.required() && AuthContext.getLoginInfo() == null) {
|
||
throw new UnauthorizedException("未登录");
|
||
}
|
||
return pjp.proceed();
|
||
}
|
||
}</code></pre>
|
||
|
||
<div class="callout">
|
||
<div class="callout-title">更进一步:可使用 Maven jar-shade 插件</div>
|
||
<p>通过 Maven 的 <code>maven-shade-plugin</code> 的 <code>relocation</code> 功能,可以将 <code>cn.apes.commons.auth</code> 包名重定向到 <code>cn.apes.sdk.auth</code>,这样所有业务代码的 <code>import</code> 语句<strong>完全不需要修改</strong>——只需在 <code>pom.xml</code> 中将 <code>apes-commons</code> 依赖替换为 <code>auth-sdk</code>。</p>
|
||
</div>
|
||
|
||
<!-- ===================== 7. 迁移路径 ===================== -->
|
||
<h2 id="s7"><span class="num">7</span> 迁移路径与实施计划</h2>
|
||
|
||
<h3>7.1 分阶段迁移策略</h3>
|
||
|
||
<div class="phase-card">
|
||
<div class="phase-header">
|
||
<span class="phase-num p1">1</span>
|
||
<h4 style="margin:0">Phase 1:模块化隔离(单体内)</h4>
|
||
</div>
|
||
<p><strong>目标</strong>:在单体应用内将账权代码与业务代码做包级隔离,建立清晰边界。</p>
|
||
<ul>
|
||
<li>将账权相关代码迁移到 <code>cn.apes.cloud.auth</code> 包下(Controller/Service/Entity/Mapper/Config)</li>
|
||
<li>将业务代码迁移到 <code>cn.apes.cloud.business</code> 包下</li>
|
||
<li>提取账权模块的对外接口为 <code>AuthFacade</code> 接口,业务模块只依赖接口</li>
|
||
<li>统一 <code>AuthContext</code> 的使用规范,消除不规范的直接 Mapper 调用</li>
|
||
<li><strong>不拆数据库</strong>,不引入网关,不影响部署</li>
|
||
</ul>
|
||
<p><span class="badge badge-green">风险低</span> <span class="badge badge-blue">可在 1-2 周内完成</span> <span class="badge badge-orange">需全量回归测试</span></p>
|
||
</div>
|
||
|
||
<div class="phase-card">
|
||
<div class="phase-header">
|
||
<span class="phase-num p2">2</span>
|
||
<h4 style="margin:0">Phase 2:auth-sdk 抽取 + 认证机制改造</h4>
|
||
</div>
|
||
<p><strong>目标</strong>:抽取 <code>auth-sdk</code> JAR,替换 <code>apes-commons</code>,引入网关。</p>
|
||
<ul>
|
||
<li>创建 <code>auth-sdk</code> Maven 项目,包含:兼容 AuthContext、AuthHeaderFilter、@Login 注解、LoginAspect、OperationLogUtil</li>
|
||
<li>使用 Maven shade relocation 保持 <code>cn.apes.commons.auth</code> 包名兼容</li>
|
||
<li>引入 Spring Cloud Gateway,配置 Token 解析 → Header 注入逻辑</li>
|
||
<li>将单体应用注册到网关(此时单体仍包含账权代码,但认证由网关处理)</li>
|
||
<li><strong>不拆数据库</strong>,单体仍访问同一 MySQL</li>
|
||
</ul>
|
||
<p><span class="badge badge-orange">风险中</span> <span class="badge badge-blue">2-3 周</span> <span class="badge badge-orange">需重点测试认证流程</span></p>
|
||
</div>
|
||
|
||
<div class="phase-card">
|
||
<div class="phase-header">
|
||
<span class="phase-num p3">3</span>
|
||
<h4 style="margin:0">Phase 3:Identity Service 独立部署</h4>
|
||
</div>
|
||
<p><strong>目标</strong>:将账权代码从单体中剥离,部署为独立微服务。</p>
|
||
<ul>
|
||
<li>创建 <code>identity-service</code> Maven 项目,迁移 Phase 1 隔离的账权代码</li>
|
||
<li>拆分数据库:导出 auth 相关表到 <code>auth_db</code></li>
|
||
<li>单体(现 Business Service)移除账权代码,通过 Feign 调用 Identity Service</li>
|
||
<li>网关路由配置:<code>/auth/**</code> → Identity Service,<code>/trace/**</code> 等 → Business Service</li>
|
||
<li>Token 签发逻辑迁移到 Identity Service,网关通过 API 校验 Token</li>
|
||
</ul>
|
||
<p><span class="badge badge-red">风险高</span> <span class="badge badge-blue">3-4 周</span> <span class="badge badge-red">需数据库迁移 + 全量回归</span></p>
|
||
</div>
|
||
|
||
<div class="phase-card">
|
||
<div class="phase-header">
|
||
<span class="phase-num p4">4</span>
|
||
<h4 style="margin:0">Phase 4:完善与优化</h4>
|
||
</div>
|
||
<p><strong>目标</strong>:完善权限校验、配额管理、开放平台等高级能力。</p>
|
||
<ul>
|
||
<li>后端权限注解 <code>@RequiresPermission</code> 全量覆盖</li>
|
||
<li>JWT + Redis 黑名单替换 RSA Token 机制</li>
|
||
<li>OAuth2 授权码模式支持第三方应用接入</li>
|
||
<li>用户/租户信息变更 MQ 广播 + Redis 缓存</li>
|
||
<li>AI Agent Service 独立拆分(可选)</li>
|
||
</ul>
|
||
<p><span class="badge badge-orange">风险中</span> <span class="badge badge-blue">持续迭代</span></p>
|
||
</div>
|
||
|
||
<h3>7.2 迁移时间线</h3>
|
||
|
||
<div class="diagram">
|
||
<svg viewBox="0 0 860 200" xmlns="http://www.w3.org/2000/svg">
|
||
<text x="430" y="20" text-anchor="middle" fill="#e6edf3" font-size="13" font-weight="600">迁移时间线(建议)</text>
|
||
|
||
<!-- Timeline line -->
|
||
<line x1="40" y1="80" x2="820" y2="80" stroke="#30363d" stroke-width="2"/>
|
||
|
||
<!-- Phase 1 -->
|
||
<circle cx="120" cy="80" r="12" fill="#f85149" stroke="#0d1117" stroke-width="3"/>
|
||
<text x="120" y="55" text-anchor="middle" fill="#f85149" font-size="11" font-weight="600">Phase 1</text>
|
||
<text x="120" y="105" text-anchor="middle" fill="#8b949e" font-size="10">模块化隔离</text>
|
||
<text x="120" y="120" text-anchor="middle" fill="#8b949e" font-size="10">1-2 周</text>
|
||
<rect x="60" y="140" width="120" height="40" rx="6" fill="#161b22" stroke="#f85149" stroke-width="1"/>
|
||
<text x="120" y="158" text-anchor="middle" fill="#f85149" font-size="9">包结构隔离</text>
|
||
<text x="120" y="172" text-anchor="middle" fill="#f85149" font-size="9">接口抽象</text>
|
||
|
||
<!-- Phase 2 -->
|
||
<circle cx="340" cy="80" r="12" fill="#d29922" stroke="#0d1117" stroke-width="3"/>
|
||
<text x="340" y="55" text-anchor="middle" fill="#d29922" font-size="11" font-weight="600">Phase 2</text>
|
||
<text x="340" y="105" text-anchor="middle" fill="#8b949e" font-size="10">SDK + 网关</text>
|
||
<text x="340" y="120" text-anchor="middle" fill="#8b949e" font-size="10">2-3 周</text>
|
||
<rect x="280" y="140" width="120" height="40" rx="6" fill="#161b22" stroke="#d29922" stroke-width="1"/>
|
||
<text x="340" y="158" text-anchor="middle" fill="#d29922" font-size="9">auth-sdk 抽取</text>
|
||
<text x="340" y="172" text-anchor="middle" fill="#d29922" font-size="9">网关引入</text>
|
||
|
||
<!-- Phase 3 -->
|
||
<circle cx="560" cy="80" r="12" fill="#58a6ff" stroke="#0d1117" stroke-width="3"/>
|
||
<text x="560" y="55" text-anchor="middle" fill="#58a6ff" font-size="11" font-weight="600">Phase 3</text>
|
||
<text x="560" y="105" text-anchor="middle" fill="#8b949e" font-size="10">服务独立</text>
|
||
<text x="560" y="120" text-anchor="middle" fill="#8b949e" font-size="10">3-4 周</text>
|
||
<rect x="500" y="140" width="120" height="40" rx="6" fill="#161b22" stroke="#58a6ff" stroke-width="1"/>
|
||
<text x="560" y="158" text-anchor="middle" fill="#58a6ff" font-size="9">数据库拆分</text>
|
||
<text x="560" y="172" text-anchor="middle" fill="#58a6ff" font-size="9">独立部署</text>
|
||
|
||
<!-- Phase 4 -->
|
||
<circle cx="760" cy="80" r="12" fill="#3fb950" stroke="#0d1117" stroke-width="3"/>
|
||
<text x="760" y="55" text-anchor="middle" fill="#3fb950" font-size="11" font-weight="600">Phase 4</text>
|
||
<text x="760" y="105" text-anchor="middle" fill="#8b949e" font-size="10">完善优化</text>
|
||
<text x="760" y="120" text-anchor="middle" fill="#8b949e" font-size="10">持续</text>
|
||
<rect x="700" y="140" width="120" height="40" rx="6" fill="#161b22" stroke="#3fb950" stroke-width="1"/>
|
||
<text x="760" y="158" text-anchor="middle" fill="#3fb950" font-size="9">权限注解</text>
|
||
<text x="760" y="172" text-anchor="middle" fill="#3fb950" font-size="9">JWT/OAuth2</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- ===================== 8. 风险评估 ===================== -->
|
||
<h2 id="s8"><span class="num">8</span> 风险评估与应对</h2>
|
||
|
||
<table>
|
||
<tr><th>风险</th><th>影响</th><th>概率</th><th>应对措施</th></tr>
|
||
<tr>
|
||
<td><strong>认证中断</strong><br>迁移过程中 Token 机制变更导致用户掉线</td>
|
||
<td>高</td>
|
||
<td>中</td>
|
||
<td>Phase 2 保持 Redis Token 机制不变,仅改上下文传递方式;JWT 升级推迟到 Phase 4;支持新旧 Token 并存期</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>跨服务调用延迟</strong><br>业务服务需远程调用 Identity Service 查询用户/权限信息</td>
|
||
<td>中</td>
|
||
<td>高</td>
|
||
<td>JWT Payload 携带基础信息减少远程调用;Redis 缓存热点用户/租户信息;MQ 广播变更</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>数据库拆分数据不一致</strong><br>迁移过程中 auth_db 与 business_db 数据不同步</td>
|
||
<td>高</td>
|
||
<td>低</td>
|
||
<td>Phase 3 先双写(同时写旧库和新库),验证一致后切换读新库,最后停止写旧库</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>apes-commons 兼容问题</strong><br>shade relocation 可能遗漏某些内部类或反射调用</td>
|
||
<td>中</td>
|
||
<td>中</td>
|
||
<td>Phase 2 充分测试;保留 apes-commons 作为 fallback 依赖直到 Phase 3 完成后移除</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>网关单点故障</strong><br>所有请求经过网关,网关宕机导致全站不可用</td>
|
||
<td>高</td>
|
||
<td>低</td>
|
||
<td>网关集群部署 + 负载均衡;网关无状态化设计;配置降级策略(网关故障时直接路由)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>配额检查性能</strong><br>业务操作前需远程检查配额,增加请求延迟</td>
|
||
<td>中</td>
|
||
<td>中</td>
|
||
<td>Redis 缓存配额信息(TTL 5 分钟);批量操作时一次性检查;允许短时超卖(最终一致)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>团队学习成本</strong><br>微服务架构对团队提出更高要求</td>
|
||
<td>中</td>
|
||
<td>高</td>
|
||
<td>Phase 1-2 不引入复杂中间件(无注册中心/配置中心);先用 Nginx 做网关,后续再引入 Spring Cloud Gateway</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<!-- ===================== 9. 结论 ===================== -->
|
||
<h2 id="s9"><span class="num">9</span> 结论与建议</h2>
|
||
|
||
<div class="callout ok">
|
||
<div class="callout-title">核心结论</div>
|
||
<p><strong>账权模块可以抽象为独立服务。</strong>核心依据是当前架构中业务模块与账权模块之间<strong>不存在直接代码调用</strong>(0 个业务 Service import 账权 Service),耦合仅通过 <code>AuthContext</code> ThreadLocal 间接发生,且登录态已存储在 Redis 中天然支持分布式。通过"网关解析 Token → HTTP Header 传递 → auth-sdk 解析"的方式,可以将 52 个文件的 AuthContext 耦合以<strong>最小改动(import 替换)</strong>的方式消除。</p>
|
||
</div>
|
||
|
||
<h3>建议的行动路径</h3>
|
||
|
||
<ol>
|
||
<li><strong>立即启动 Phase 1</strong>(模块化隔离):在单体内做包结构隔离和接口抽象,风险最低、收益明确,为后续拆分打下基础</li>
|
||
<li><strong>评估多产品需求</strong>:如果近期有 AI Agent 平台独立部署、移动端 App 接入、开放平台等需求,则推进 Phase 2-3;否则停留在 Phase 1 即可</li>
|
||
<li><strong>引入网关是关键转折点</strong>:Phase 2 引入网关后,认证机制从"各服务自行查 Redis"变为"网关统一解析",这是微服务化的基础设施前提</li>
|
||
<li><strong>不急于拆数据库</strong>:Phase 3 才拆数据库。Phase 1-2 可保持共享数据库,降低风险。数据库拆分必须配合双写过渡期</li>
|
||
<li><strong>JWT 推迟到 Phase 4</strong>:前三个阶段保持现有 Redis Token 机制不变,避免认证机制和架构拆分同时变更</li>
|
||
</ol>
|
||
|
||
<h3>改造工作量估算</h3>
|
||
|
||
<table>
|
||
<tr><th>阶段</th><th>核心工作量</th><th>预估人天</th></tr>
|
||
<tr><td>Phase 1</td><td>包迁移 + 接口抽象 + 回归测试</td><td>5-10 人天</td></tr>
|
||
<tr><td>Phase 2</td><td>auth-sdk 开发 + 网关搭建 + shade 配置 + 全量测试</td><td>10-15 人天</td></tr>
|
||
<tr><td>Phase 3</td><td>服务拆分 + 数据库迁移 + 双写过渡 + Feign 接入</td><td>15-20 人天</td></tr>
|
||
<tr><td>Phase 4</td><td>JWT + 权限注解 + OAuth2 + 持续优化</td><td>10+ 人天</td></tr>
|
||
<tr><td><strong>合计</strong></td><td></td><td><strong>40-55 人天</strong></td></tr>
|
||
</table>
|
||
|
||
<p style="color: var(--c-text-dim); font-size: 13px; margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--c-border);">
|
||
本方案基于 service-master 源码静态分析编制。实际实施前建议结合运行时监控数据(API 调用频率、AuthContext 使用热点)做进一步验证。
|
||
</p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|