账权模块独立服务化方案

基于账权平台 service-master 源码分析 · 2026-08-05

目录
  1. 现状分析:账权模块的边界与耦合
  2. 可行性判断:能不能拆?该不该拆?
  3. 目标架构设计
  4. 核心技术方案
  5. 数据库拆分策略
  6. 解耦改造详细方案
  7. 迁移路径与实施计划
  8. 风险评估与应对
  9. 结论与建议

1 现状分析:账权模块的边界与耦合

1.1 账权模块的功能边界

通过对源码的全面扫描,当前单体应用中"账权"相关功能可归纳为以下五大域:

功能域核心实体核心 Service核心 Controller
用户认证UserInfo, SsoConfigUserInfoServiceImpl, SsoServiceImpl, Sso2ServiceImplUserController
权限管理SysRole, SysPermission, SysMenu, SysRolePermission, SysMemberRolePermissionServiceImplPermissionController
租户管理CustomerInfo, CustomerUser, SysOrganizationCustomerInfoServiceImpl, CustomerUserServiceImpl, SysOrganizationServiceImplCustomerController, SysOrganizationController
套餐配额SysPackagePlan, SysPackagePlanPermission, SysQuota, CustomerPackage, CustomerPackageQuotaSysPackagePlanServiceImpl, CustomerPackageServiceImpl, SysQuotaServiceImplSysPackagePlanController, CustomerPackageController
开放认证SysAccessTokenSysAccessTokenServiceImplAccessTokenController

1.2 耦合度量化

52
文件直接引用 AuthContext
111
AuthContext 调用总次数
11
文件引用 OperationLogUtil
0
业务 Service 直接 import 账权 Service
关键发现

溯源(Trace*)和过磅(Weight*)等业务模块 不直接 import 任何账权 Service,它们仅通过 AuthContext.getLoginInfo().getCustomer().getId() 获取当前租户 ID。这意味着业务模块与账权模块的耦合是通过共享上下文(ThreadLocal + Redis)间接耦合,而非直接代码调用——这为服务化拆分提供了极好的基础。

1.3 耦合关系详解

① AuthContext(ThreadLocal)— 全局耦合

AuthContext 来自外部库 apes-commons,是整个系统的"认证上下文总线"。所有业务模块通过它获取当前登录用户和租户信息:

// 典型使用模式(出现 52 个文件中)
LoginCustomer customer = AuthContext.getLoginInfo().getCustomer();
Long customerId = customer.getId();
// 然后在查询中添加 WHERE customer_id = customerId

② OperationLogUtil — 静态工具耦合

OperationLogUtil 内部依赖 AuthContext 获取操作人信息,被 11 个文件调用。它通过 @PostConstruct 注入静态 SysOperationLogService,形成了"静态方法 → Spring Bean → AuthContext"的隐式依赖链。

③ 账权模块内部 — Service 间直接调用

调用方被调用方调用内容
UserInfoServiceImplPermissionServiceImplgetUserRoles()
UserInfoServiceImplCustomerUserServiceImplgetCustomers(), getCustomerListByUserIds()
UserInfoServiceImplCustomerInfoServiceImplgetById()
PermissionServiceImplSsoServiceImpl / Sso2ServiceImplgetUserConfig(), getPageUrl()
PermissionServiceImplUserInfoMapperselectById()
PermissionServiceImplCustomerPackageMapperselectList()
PermissionServiceImplSysPackagePlanMapperselectById()
AccessTokenFilterCustomerInfoServiceImplgetById()

④ Redis 共享会话 — 天然分布式基础

登录态存储在 Redis 中(Key = 加密 Token,Value = 用户 JSON;Key = Token:customer,Value = 企业 JSON),TTL 1 天。这是拆分的最有利条件——任何服务只要能访问同一个 Redis,就能读取登录态。

⑤ apes-commons 库 — 认证框架黑盒

核心认证逻辑(LoginAspect AOP 拦截、AuthContext ThreadLocal 管理、@Login 注解)封装在外部 JAR 包 apes-commons:0.0.7-SNAPSHOT 中,源码不在项目内。当前所有 Controller 通过 @Login 注解声明需要登录校验,由 LoginAspect 从 Redis 读取 Token 并填充 AuthContext

当前单体架构 — 账权耦合关系图 单体应用 (service-master) apes-commons (外部JAR) AuthContext · @Login · LoginAspect · UserSession 账权模块 UserInfoService PermissionService CustomerInfoService AccessTokenService PackagePlanService SsoService 业务模块 TraceProductService WeightTicketService TraceCodeService ExtLinkService CustomerUiConfigService FeiFertigationService AI/Agent 模块 ChatSessionService ToolService AgentService KnowledgeService Redis (共享会话) AuthContext AuthContext 读写 Token 读 Token 无直接调用 ✓

2 可行性判断:能不能拆?该不该拆?

2.1 能不能拆?—— 技术可行性

✓ 有利因素

  • Redis 共享会话:登录态天然存储在 Redis,任何服务都能读取,无需迁移认证机制
  • 业务模块零直接依赖:Trace*/Weight* 不 import 账权 Service,仅通过 AuthContext 获取租户 ID
  • 无数据库外键约束:所有表关联为逻辑关联,数据库拆分无物理障碍
  • AccessToken 机制已具备开放能力:已有外部系统认证的基础设施
  • 无跨服务远程调用:当前无 Feign/RestTemplate,不需要重构已有远程调用
  • 数据源已配置多库:dynamic-datasource 框架已就位,多库支持基础已有

⚠ 主要障碍

  • AuthContext 深度耦合:52 个文件、111 处调用,需统一替换为新的上下文传递机制
  • apes-commons 黑盒:认证核心逻辑在外部 JAR,需替换或重构认证框架
  • OperationLogUtil 静态依赖:11 个文件通过静态方法调用,内部依赖 AuthContext
  • 账权 Service 间内部调用:UserInfoService → PermissionService → SsoService 等直接注入
  • 共享数据库:所有表在同一 MySQL 库,拆分需数据库迁移
  • AOP 执行顺序依赖:DataSourceHeaderAspect(Order=1) 与 LoginAspect 有隐式协作
结论:技术上可行

核心判断依据是"业务模块与账权模块之间不存在直接代码调用"。耦合仅发生在 AuthContext(ThreadLocal 上下文)层面,这种耦合可以通过网关解析 Token + HTTP Header 传递的方式完全消除。改造工作量主要集中在替换 AuthContext 和解耦 apes-commons,而非重构业务代码。

2.2 该不该拆?—— 业务驱动力分析

维度当前痛点独立后的收益优先级
多产品复用 账权逻辑锁在单体中,新产品(如独立 AI Agent 平台、移动端)要么整体依赖单体,要么重复实现 一套账权服务支撑多个前端产品线,避免重复建设
独立演进 账权变更需整体重新部署,业务模块也跟着重启 账权服务独立发布、独立扩缩容,不影响业务服务
开放平台 AccessToken 机制已在单体内,但第三方接入需走单体 API,无法独立管控 独立 OAuth2 服务,支持第三方应用授权、API 网关统一鉴权
安全合规 用户密码、Token 等敏感数据与业务数据混在同一库,权限边界模糊 账权数据库独立,可实施更严格的安全策略(网络隔离、审计、加密)
性能隔离 大量登录/鉴权请求与业务请求混在同一进程,相互影响 账权服务可独立扩容,登录高峰不影响业务处理
建议:有条件地拆

如果团队有多产品线复用账权的需求(如独立 AI Agent 平台、移动端 App、开放平台),则拆分收益明确,值得投入。如果当前只有一个前端产品且近期无扩展计划,则优先在单体内做模块化隔离(包结构拆分 + 接口抽象)即可,暂不需微服务化。

3 目标架构设计

3.1 整体架构

目标架构 — 账权服务独立化 Web 前端 / 移动端 / 第三方应用 API Gateway (Spring Cloud Gateway) Token 解析 → 注入 X-User-Id / X-Customer-Id / X-Roles Header Identity Service (账权服务) · 用户注册/登录/密码管理 · 角色/权限/菜单管理 · 租户(企业)/组织架构 · 套餐/配额管理 · AccessToken / OAuth2 · SSO 对接 Spring Boot · 独立部署 · 独立数据库 Business Service (业务服务) · 溯源管理 (Trace*) · 自助过磅 (Weight*) · 智慧农业 (FeiFertigation*) · 外部链接 (ExtLink*) · UI 配置 (CustomerUiConfig*) Spring Boot · 独立部署 · 独立数据库 AI Agent Service (智能服务) · 对话管理 (ChatSession*) · Function Calling · 知识库 (Knowledge*) · Agent 编排 Spring Boot · 独立部署 · 独立数据库 /auth/** /user/** /trace/** /weight/** /ai/** /agent/** auth_db (MySQL) business_db (MySQL) ai_db (MySQL) Redis (共享会话 + 缓存) auth-sdk (共享 JAR)

3.2 架构要点说明

组件职责技术选型
API Gateway 统一入口,路由转发,Token 解析与校验,将用户信息注入 HTTP Header 传递给下游服务 Spring Cloud Gateway
Identity Service 用户/权限/租户/套餐/AccessToken 的全部管理,Token 签发与校验 Spring Boot + MyBatis Plus
Business Service 溯源/过磅/农业等业务功能,通过 Header 接收用户上下文 原有单体业务代码剥离
auth-sdk 轻量级 SDK JAR,供 Business/AI 服务引入,提供:Header 解析、AuthContext 替代品、权限校验注解 纯 Java JAR,无 Spring 依赖
Redis 共享会话存储(Token → 用户/租户信息),所有服务共用 Redisson(沿用现有)
消息队列(可选) 用户/租户信息变更时广播通知,减少跨服务实时查询 RabbitMQ / RocketMQ

4 核心技术方案

4.1 认证上下文传递机制改造

核心变更:用"网关解析 Token → HTTP Header 传递"替代当前的"每个服务各自从 Redis 读取 Token → 填充 ThreadLocal"。

改造前(当前架构)

请求 → AccessTokenFilter → DataSourceHeaderAspect(clean AuthContext)
     → LoginAspect(从Redis读Token → 填充AuthContext)
     → Controller(@Login) → Service(AuthContext.getLoginInfo())

改造后(目标架构)

请求 → 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())

auth-sdk 设计

// 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();
        }
    }
}
兼容性策略

为降低改造成本,auth-sdk 可提供一个 AuthContext 兼容适配器,使其 API 与 apes-commons.AuthContext 保持一致(getLoginInfo().getUser().getId() 等),这样业务代码只需将 import 从 cn.apes.commons.auth.AuthContext 改为 cn.apes.sdk.auth.AuthContext,方法调用不变。

4.2 Token 机制升级

当前使用 RSA 加密的随机 ID 作为 Token,存储在 Redis 中。建议升级为 JWT + Redis 混合方案

方案说明优缺点
方案 A:JWT(推荐) Token 本身携带 userId, customerId, roles 等信息,网关直接解析,无需查 Redis ✓ 无状态,性能最优 ✓ 网关可独立解析
✗ Token 无法主动失效(需配合 Redis 黑名单)
方案 B:Redis Token(兼容现有) 保持现有机制,网关从 Redis 读取 Token 信息后注入 Header ✓ 改造成本最低 ✓ Token 可主动失效
✗ 每次请求需查 Redis ✗ 网关依赖 Redis
方案 C:JWT + Redis 黑名单 JWT 无状态解析 + Redis 存储已注销的 Token(黑名单) ✓ 性能好 ✓ 可主动失效
✗ 实现略复杂

推荐方案 C,JWT Payload 设计:

{
  "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)
}

4.3 权限校验机制升级

当前系统仅校验登录态,不校验具体权限(无 @PreAuthorize、无 Shiro 注解),权限控制依赖前端菜单过滤。服务化后建议增加后端权限校验:

// 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) { ... }
}

4.4 跨服务数据查询方案

业务服务偶尔需要查询用户/租户信息(如显示操作人姓名)。方案:

方案适用场景实现
JWT Payload 携带 仅需 userId, customerId, userName 等基础信息 直接从 ServiceAuthContext 读取,零网络开销
Identity Service API 需要完整用户/租户信息 Feign Client 调用 GET /auth/user/{id}GET /auth/customer/{id}
Redis 缓存 + MQ 同步 高频查询的场景 用户信息变更时通过 MQ 广播,业务服务更新本地 Redis 缓存
// 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);
}

4.5 OperationLogUtil 改造

当前 OperationLogUtil 静态方法内部依赖 AuthContext,改造为依赖 ServiceAuthContext

// 改造前
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));
}
零侵入改造

由于 OperationLogUtillog() 方法签名不变,11 个调用方文件无需修改任何代码——只需更新 OperationLogUtil 自身的实现(从 AuthContext 切换到 ServiceAuthContext),并将 OperationLogUtil 迁移到 auth-sdk 中。

5 数据库拆分策略

5.1 表归属划分

归属表名说明
auth_db
(Identity Service)
user_info 用户基础信息
customer_info租户(企业)信息
customer_user用户-租户关联
sys_role角色
sys_permission权限
sys_menu菜单
sys_role_permission角色-权限关联
sys_member_role用户-角色关联(含租户ID)
sys_organization组织架构
sys_access_tokenAccessToken
sys_package_plan套餐计划
sys_package_plan_permission套餐-权限关联
sys_quota / sys_package_plan_quota配额模板
sso_configSSO 配置
auth_db
(扩展)
customer_package客户套餐关联
customer_package_quota客户配额实例
customer_quota_change_log配额变更日志
customer_package_extend_apply套餐延期申请
business_db
(Business Service)
trace_* (20+ 表)溯源业务表
weight_* (10+ 表)过磅业务表
customer_ui_config / ext_link业务配置表
ai_db
(AI Service)
chat_session / chat_message / tool_* AI 对话相关表

5.2 跨库查询处理

当前系统中,业务表通过 customer_id 关联 customer_info。拆库后:

5.3 套餐配额的跨服务消费

套餐配额(如溯源码生成数量限制)需要在业务服务中检查。方案:

// 业务服务通过 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();
}

6 解耦改造详细方案

6.1 改造影响范围矩阵

改造项影响文件数改造方式难度
AuthContext → ServiceAuthContext52替换 import(兼容 API)
OperationLogUtil 实现1(自身)内部实现替换,迁移到 auth-sdk
OperationLogUtil 调用方11更新 import(方法签名不变)
apes-commons 替换全局Maven 依赖替换为 auth-sdk
@Login 注解替换62(所有Controller)替换注解 import 或保留兼容
DataSourceHeaderAspect1重构或移除(网关接管数据源路由)
AccessTokenFilter1迁移到网关层
账权 Service 间调用~8保持同服务内调用(在 Identity Service 内部)
业务 Service → 账权 Service0无需改造(本来就没有直接调用)
数据库拆分导出 auth 相关表到新库

6.2 AuthContext 兼容适配方案(最小化改动)

为了将 52 个文件的改动降到最低,auth-sdk 提供 API 完全兼容的 AuthContext 替代品:

// 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();
        }
    }
}

这样业务代码的改动仅为一行 import 替换

// 改前
import cn.apes.commons.auth.AuthContext;

// 改后
import cn.apes.sdk.auth.AuthContext;

// 方法调用完全不变
AuthContext.getLoginInfo().getCustomer().getId();  // ✓

6.3 @Login 注解兼容方案

当前 62 个 Controller 类/方法使用 @Login 注解。在 auth-sdk 中提供同名注解,并由 AuthHeaderFilter 替代 LoginAspect 的功能:

// 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();
    }
}
更进一步:可使用 Maven jar-shade 插件

通过 Maven 的 maven-shade-pluginrelocation 功能,可以将 cn.apes.commons.auth 包名重定向到 cn.apes.sdk.auth,这样所有业务代码的 import 语句完全不需要修改——只需在 pom.xml 中将 apes-commons 依赖替换为 auth-sdk

7 迁移路径与实施计划

7.1 分阶段迁移策略

1

Phase 1:模块化隔离(单体内)

目标:在单体应用内将账权代码与业务代码做包级隔离,建立清晰边界。

风险低 可在 1-2 周内完成 需全量回归测试

2

Phase 2:auth-sdk 抽取 + 认证机制改造

目标:抽取 auth-sdk JAR,替换 apes-commons,引入网关。

风险中 2-3 周 需重点测试认证流程

3

Phase 3:Identity Service 独立部署

目标:将账权代码从单体中剥离,部署为独立微服务。

风险高 3-4 周 需数据库迁移 + 全量回归

4

Phase 4:完善与优化

目标:完善权限校验、配额管理、开放平台等高级能力。

风险中 持续迭代

7.2 迁移时间线

迁移时间线(建议) Phase 1 模块化隔离 1-2 周 包结构隔离 接口抽象 Phase 2 SDK + 网关 2-3 周 auth-sdk 抽取 网关引入 Phase 3 服务独立 3-4 周 数据库拆分 独立部署 Phase 4 完善优化 持续 权限注解 JWT/OAuth2

8 风险评估与应对

风险影响概率应对措施
认证中断
迁移过程中 Token 机制变更导致用户掉线
Phase 2 保持 Redis Token 机制不变,仅改上下文传递方式;JWT 升级推迟到 Phase 4;支持新旧 Token 并存期
跨服务调用延迟
业务服务需远程调用 Identity Service 查询用户/权限信息
JWT Payload 携带基础信息减少远程调用;Redis 缓存热点用户/租户信息;MQ 广播变更
数据库拆分数据不一致
迁移过程中 auth_db 与 business_db 数据不同步
Phase 3 先双写(同时写旧库和新库),验证一致后切换读新库,最后停止写旧库
apes-commons 兼容问题
shade relocation 可能遗漏某些内部类或反射调用
Phase 2 充分测试;保留 apes-commons 作为 fallback 依赖直到 Phase 3 完成后移除
网关单点故障
所有请求经过网关,网关宕机导致全站不可用
网关集群部署 + 负载均衡;网关无状态化设计;配置降级策略(网关故障时直接路由)
配额检查性能
业务操作前需远程检查配额,增加请求延迟
Redis 缓存配额信息(TTL 5 分钟);批量操作时一次性检查;允许短时超卖(最终一致)
团队学习成本
微服务架构对团队提出更高要求
Phase 1-2 不引入复杂中间件(无注册中心/配置中心);先用 Nginx 做网关,后续再引入 Spring Cloud Gateway

9 结论与建议

核心结论

账权模块可以抽象为独立服务。核心依据是当前架构中业务模块与账权模块之间不存在直接代码调用(0 个业务 Service import 账权 Service),耦合仅通过 AuthContext ThreadLocal 间接发生,且登录态已存储在 Redis 中天然支持分布式。通过"网关解析 Token → HTTP Header 传递 → auth-sdk 解析"的方式,可以将 52 个文件的 AuthContext 耦合以最小改动(import 替换)的方式消除。

建议的行动路径

  1. 立即启动 Phase 1(模块化隔离):在单体内做包结构隔离和接口抽象,风险最低、收益明确,为后续拆分打下基础
  2. 评估多产品需求:如果近期有 AI Agent 平台独立部署、移动端 App 接入、开放平台等需求,则推进 Phase 2-3;否则停留在 Phase 1 即可
  3. 引入网关是关键转折点:Phase 2 引入网关后,认证机制从"各服务自行查 Redis"变为"网关统一解析",这是微服务化的基础设施前提
  4. 不急于拆数据库:Phase 3 才拆数据库。Phase 1-2 可保持共享数据库,降低风险。数据库拆分必须配合双写过渡期
  5. JWT 推迟到 Phase 4:前三个阶段保持现有 Redis Token 机制不变,避免认证机制和架构拆分同时变更

改造工作量估算

阶段核心工作量预估人天
Phase 1包迁移 + 接口抽象 + 回归测试5-10 人天
Phase 2auth-sdk 开发 + 网关搭建 + shade 配置 + 全量测试10-15 人天
Phase 3服务拆分 + 数据库迁移 + 双写过渡 + Feign 接入15-20 人天
Phase 4JWT + 权限注解 + OAuth2 + 持续优化10+ 人天
合计40-55 人天

本方案基于 service-master 源码静态分析编制。实际实施前建议结合运行时监控数据(API 调用频率、AuthContext 使用热点)做进一步验证。