Files
apes-Authon-history/docs/账权模块独立服务化方案.html
T
figmar 9cbb1f7695 init: apes-Authon-history — 剥离历史与源码分析归档
- docs/: 源码分析报告、代码深度解析、项目说明书、独立服务化方案
- backend/: 账权模块提取说明(提取原则、文件清单、耦合度验证)
- frontend/: 前端剥离方案、剥离历史与说明、框架修改详情
2026-08-09 13:04:04 +08:00

1144 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>① AuthContextThreadLocal)— 全局耦合</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 = 加密 TokenValue = 用户 JSONKey = Token:customerValue = 企业 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>。耦合仅发生在 AuthContextThreadLocal 上下文)层面,这种耦合可以通过<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&lt;AuthInfo&gt; CONTEXT = new ThreadLocal&lt;&gt;();
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&lt;String&gt; 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&lt;String&gt; roles;
private List&lt;String&gt; 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>方案 AJWT(推荐)</strong></td>
<td>Token 本身携带 userId, customerId, roles 等信息,网关直接解析,无需查 Redis</td>
<td>✓ 无状态,性能最优 ✓ 网关可独立解析<br>✗ Token 无法主动失效(需配合 Redis 黑名单)</td>
</tr>
<tr>
<td><strong>方案 BRedis Token(兼容现有)</strong></td>
<td>保持现有机制,网关从 Redis 读取 Token 信息后注入 Header</td>
<td>✓ 改造成本最低 ✓ Token 可主动失效<br>✗ 每次请求需查 Redis ✗ 网关依赖 Redis</td>
</tr>
<tr>
<td><strong>方案 CJWT + 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&lt;String&gt; 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&lt;UserDTO&gt; getUser(@PathVariable("userId") Long userId);
@GetMapping("/auth/customer/{customerId}")
Res&lt;CustomerDTO&gt; getCustomer(@PathVariable("customerId") Long customerId);
@GetMapping("/auth/user/{userId}/permissions")
Res&lt;List&lt;String&gt;&gt; getUserPermissions(@PathVariable("userId") Long userId,
@RequestParam("customerId") Long customerId);
@GetMapping("/auth/user/{userId}/menus")
Res&lt;List&lt;MenuDTO&gt;&gt; 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&lt;UserSession&gt; HOLDER = new ThreadLocal&lt;&gt;();
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 2auth-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 3Identity 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>