fundplatform/doc/开发问题清单.md
zhangjf 10eca3fb35 feat: 实现多租户架构完整能力
## 新增功能

### 1. 多租户核心组件
- TenantRoutingProperties: 租户路由配置属性
- TenantAwareLoadBalancer: 租户感知负载均衡器
- TenantLineHandlerImpl: MyBatis Plus 租户插件
- TenantIgnoreHelper: 忽略租户过滤工具类
- NacosMetadataConfig: Nacos 元数据自动注册

### 2. Gateway 租户过滤器
- TenantGatewayFilter: 从 JWT 提取租户信息写入请求头
- 透传 X-Tenant-Id、X-Tenant-Group、X-User-Id、X-Username

### 3. 支持的部署模式
- 一库多租户(SaaS 模式): 通过 tenant_id 字段隔离
- 一库一租户(私有化): 独立服务实例和数据库
- 混合模式: VIP 租户专属实例 + 普通租户共享实例

### 4. Nacos 3.0 适配
- 所有业务模块添加 username/password 认证配置
- 服务实例自动注册租户标签

## 问题修复
- #8: FeignClient 硬编码 URL 导致 Nacos 服务发现失效
- #9: Nacos 3.0 客户端缺少 username/password 认证配置
- fund-exp expenseType 字段类型从 Integer 改为 Long

## 测试
- TenantAwareLoadBalancerTest: 负载均衡器单元测试
- 混合模式集成测试脚本
2026-02-19 18:10:16 +08:00

23 KiB
Raw Blame History

资金服务平台 - 开发问题清单

文档版本: v1.0
创建日期: 2026-02-13
记录时段: 2026-02-12 09:00 ~ 2026-02-13 当前时间


问题清单概览

序号 问题分类 问题描述 严重程度 状态
1 前后端接口 /auth/info接口返回400 Bad Request 已解决
2 前端路由 个人中心和系统设置菜单点击无反应 已解决
3 前端路由 刷新页面显示404 已解决
4 后端序列化 AOP日志LocalDateTime序列化失败 已解决
5 后端接口 新增支出expenseType字段雪花ID超出Integer范围 已解决
6 OpenFeign配置 FeignChainInterceptor未注册为Spring Bean 已解决
7 全链路追踪 TraceContextHolder未在HTTP入口初始化导致链路断裂 已解决
8 OpenFeign配置 FeignClient硬编码URL导致Nacos服务发现失效 已解决
9 Nacos 配置 Nacos 3.0 客户端缺少 username/password 认证配置 已解决

问题详情与解决方案

问题1/auth/info接口返回400 Bad Request

问题现象

Request URL: http://localhost:3002/sys/api/v1/auth/info
Request Method: GET
Status Code: 400 Bad Request

用户登录成功后,调用 /auth/info 获取用户信息时返回400错误。

问题原因

后端接口需要 X-User-Id 请求头来识别当前用户但前端未在请求中传递该header。

解决方案

1. 修改前端请求拦截器 (fund-admin/src/api/request.ts)

// 在请求头中添加 X-User-Id
request.headers['X-User-Id'] = localStorage.getItem('userId') || ''

2. 修改用户登录逻辑 (fund-admin/src/stores/user.ts)

// 登录成功后保存 userId 和 tenantId 到 localStorage
localStorage.setItem('userId', String(res.data.userId))
localStorage.setItem('tenantId', String(res.data.tenantId))

经验总结

  • 前后端接口对接时,需确认所有必需的请求头
  • 用户信息userId、tenantId应在登录成功后立即持久化存储

问题2个人中心和系统设置菜单点击无反应

问题现象

点击顶部下拉菜单中的"个人中心"和"系统设置"选项后,页面无任何跳转反应。

问题原因

MainLayout.vue 中的 handleCommand 函数对 profilesettings 两个命令的处理为空实现。

// 问题代码
const handleCommand = async (command: string) => {
  switch (command) {
    case 'profile':
      // 空实现,无任何代码
      break
    case 'settings':
      // 空实现,无任何代码
      break
    case 'logout':
      await userStore.logout()
      router.push('/login')
      break
  }
}

解决方案

修改 fund-admin/src/layouts/MainLayout.vue

const handleCommand = async (command: string) => {
  switch (command) {
    case 'profile':
      router.push('/profile')
      break
    case 'settings':
      router.push('/system/config')
      break
    case 'logout':
      await userStore.logout()
      router.push('/login')
      break
  }
}

同时在侧边栏菜单中添加"参数设置"入口。

经验总结

  • 开发新功能时需确保所有UI交互都有对应的业务实现
  • 代码提交前应进行基础功能测试

问题3刷新页面显示404

问题现象

登录后进入业务页面刷新浏览器后页面显示404

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
type=Not Found, status=404

问题原因

Vite 开发服务器的代理配置中,/sys 路径模糊匹配了前端路由 /system,导致前端路由请求被错误代理到后端服务。

// 问题配置
proxy: {
  '/sys': {  // 会匹配 /sys 开头的所有路径,包括 /system
    target: 'http://localhost:8100',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/sys/, '')
  }
}

解决方案

修改 fund-admin/vite.config.ts

proxy: {
  '/sys/': {  // 添加斜杠,精确匹配 /sys/ 开头的路径
    target: 'http://localhost:8100',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/sys/, '')
  }
}

同时改进路由守卫逻辑,更好地处理页面刷新场景。

经验总结

  • 代理路径配置应使用精确匹配,避免模糊匹配导致的路由冲突
  • 前端路由命名应与后端API路径有明显区分

问题4AOP日志LocalDateTime序列化失败

问题现象

为 fund-sys 添加 AOP 日志功能后,日志输出时报错:

Java 8 date/time type 'java.time.LocalDateTime' not supported by default: 
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" 
to enable handling

问题原因

Jackson 默认不支持 Java 8 的日期时间类型LocalDateTime、LocalDate等的序列化需要额外配置。

解决方案

1. 添加 Maven 依赖 (fund-sys/pom.xml)

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

2. 配置 ObjectMapper (ApiLogAspect.java)

private static final ObjectMapper objectMapper = new ObjectMapper();

static {
    // 注册JavaTimeModule以支持Java 8日期时间类型
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

3. 序列化异常兜底处理

try {
    logInfo.put("responseBody", objectMapper.writeValueAsString(result));
} catch (Exception e) {
    // 序列化失败时使用toString()兜底
    logInfo.put("responseBody", result.toString());
}

经验总结

  • 使用 Jackson 序列化 Java 8 日期时间类型时,必须注册 JavaTimeModule
  • 对于复杂的序列化场景,应添加异常兜底处理,避免日志功能影响业务
  • Spring Boot 项目使用 AOP 需引入 spring-boot-starter-aop 依赖

问题5新增支出expenseType字段雪花ID超出Integer范围

问题现象

POST http://localhost:8080/exp/api/v1/exp/expense
Status Code: 400 Bad Request

移动端新增支出时,请求返回 400 错误,无任何响应内容。

问题原因

后端 FundExpenseDTO.expenseType 字段类型为 Integer,最大值为 2147483647。而前端传递的是雪花ID2023686600025919489),远超 Integer 范围,导致 JSON 解析失败。

解决方案

修改 fund-exp/src/main/java/com/fundplatform/exp/dto/FundExpenseDTO.java

// 修改前
@NotNull(message = "支出类型不能为空")
private Integer expenseType;

public Integer getExpenseType() { return expenseType; }
public void setExpenseType(Integer expenseType) { this.expenseType = expenseType; }

// 修改后
@NotNull(message = "支出类型不能为空")
private Long expenseType;

public Long getExpenseType() { return expenseType; }
public void setExpenseType(Long expenseType) { this.expenseType = expenseType; }

经验总结

  • 使用雪花ID作为主键的实体其外键字段类型必须使用 Long 而非 Integer
  • Java Integer 范围:-2147483648 ~ 2147483647约21亿
  • 雪花ID范围通常为19位数字远超 Integer 范围

问题6FeignChainInterceptor未注册为Spring Bean

问题现象

OpenFeign 调用下游服务时租户ID、用户信息、TraceId 等上下文信息未透传,导致下游服务无法获取当前请求的租户和用户信息。

问题原因

FeignChainInterceptor 虽然实现了 RequestInterceptor 接口,但缺少 @Component 注解,未被 Spring 注册为 Bean导致 OpenFeign 调用时拦截器不生效。

// 问题代码
public class FeignChainInterceptor implements RequestInterceptor {
    // 没有 @Component 注解
}

解决方案

1. 添加 Bean 注解 (fund-common/src/main/java/com/fundplatform/common/feign/FeignChainInterceptor.java)

@Component  // 添加此注解
public class FeignChainInterceptor implements RequestInterceptor {
    // ...
}

2. 配置组件扫描 (fund-report/src/main/java/com/fundplatform/report/ReportApplication.java)

@SpringBootApplication(scanBasePackages = {"com.fundplatform.report", "com.fundplatform.common"})
@EnableDiscoveryClient
@EnableFeignClients
public class ReportApplication {
    // ...
}

经验总结

  • Spring 组件Service、Component、Repository 等)必须添加相应注解才能被容器管理
  • 跨模块的组件需确保使用该组件的模块配置了正确的 scanBasePackages
  • Feign 拦截器是实现微服务间上下文透传的关键,必须确保正确注册

问题 7TraceContextHolder 未在 HTTP 入口初始化导致链路断裂

问题现象

Gateway 层已生成 TraceId 并写入请求头 X-Trace-Id,但业务服务中调用 TraceContextHolder.getTraceId() 返回 nullFeign 调用时会重新生成新的 TraceId导致全链路追踪断裂。

问题原因

ContextInterceptor 只提取了租户 ID 和用户 ID未从请求头中提取 Gateway 传递的 TraceId 并设置到 TraceContextHolder,导致:

  1. 业务逻辑无法获取 Gateway 生成的 TraceId
  2. Feign 调用时通过 getOrCreateTraceId() 生成新的 TraceId
  3. 日志系统中出现多个不同的 TraceId无法串联完整调用链
// 问题代码 - ContextInterceptor.preHandle
// 缺少 TraceId 的提取逻辑
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    // 只设置了 TenantId 和 UserId
    TenantContextHolder.setTenantId(tenantId);
    UserContextHolder.setUserId(userId);
    // 缺失TraceContextHolder.setTraceId(traceId);
    return true;
}

解决方案

修改 fund-common/src/main/java/com/fundplatform/common/web/ContextInterceptor.java

  1. 添加导入和常量定义
import com.fundplatform.common.context.TraceContextHolder;

public static final String HEADER_TRACE_ID = "X-Trace-Id";
  1. 在 preHandle 中提取 TraceId
// 提取 TraceId如不存在后续会在需要时自动生成
String traceId = request.getHeader(HEADER_TRACE_ID);
if (traceId != null && !traceId.isEmpty()) {
    TraceContextHolder.setTraceId(traceId);
}
  1. 在 afterCompletion 中清理 TraceContextHolder
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                           Object handler, Exception ex) {
    TenantContextHolder.clear();
    UserContextHolder.clear();
    TraceContextHolder.clear();  // 新增
}

修复后的完整链路

Gateway (GlobalLogFilter)
  ↓ 生成 TraceId: abc123 并写入 X-Trace-Id 请求头
业务服务 A (ContextInterceptor.preHandle)
  ↓ 读取 X-Trace-Id: abc123 并设置到 TraceContextHolder
业务逻辑 A
  ↓ 使用 TraceContextHolder.getTraceId() → abc123
FeignChainInterceptor.apply
  ↓ 从 TraceContextHolder 获取 abc123 并透传到下游
业务服务 B (ContextInterceptor.preHandle)
  ↓ 读取 X-Trace-Id: abc123 并设置到 TraceContextHolder
业务逻辑 B
  ↓ 使用 TraceContextHolder.getTraceId() → abc123
afterCompletion
  ↓ 清理所有 ThreadLocal

经验总结

  • Gateway 生成的 TraceId 必须在业务服务入口处提取并设置到 ThreadLocal
  • Feign 拦截器应从 ThreadLocal 获取 TraceId 并透传,而不是重新生成
  • 所有 ThreadLocal 使用后必须在 finally 或 afterCompletion 中清理,防止内存泄漏
  • 全链路追踪需要每个环节都正确传递 TraceId任何一环断裂都会导致追踪失败

问题 8FeignClient 硬编码 URL 导致 Nacos 服务发现失效

问题现象

已配置 Nacos 作为服务注册中心,但各 FeignClient 仍使用 url = "${feign.xxx.url:http://localhost:xxxx}" 硬编码地址,导致:

  1. Nacos 服务注册中心形同虚设
  2. Feign 不会通过 Nacos 进行服务发现和负载均衡
  3. 无法实现服务动态扩缩容和高可用
// 问题代码 - ExpenseFeignClient.java
@FeignClient(name = "fund-exp", url = "${feign.fund-exp.url:http://localhost:8140}")
public interface ExpenseFeignClient {
    // ...
}

问题原因

架构设计不一致

  • 已在 application.yml 中配置 Nacos 服务发现
  • 但 FeignClient 仍指定了固定的 URL 地址

可能的历史原因

  1. 开发环境过渡方案:本地开发时 Nacos 未部署,临时使用直连方式
  2. 迁移遗留问题:从单体架构迁移到微服务时的过渡配置
  3. 理解偏差:误以为 FeignClient 必须指定 url

解决方案

修复前(

@FeignClient(name = "fund-exp", url = "${feign.fund-exp.url:http://localhost:8140}")
// ↓ 直接访问固定地址Nacos 不生效

修复后(

@FeignClient(name = "fund-exp")
// ↓ 通过 Nacos 服务发现 + Ribbon 负载均衡

涉及的文件修改

  1. ExpenseFeignClient.java - 移除 url 属性
  2. ReceivableFeignClient.java - 移除 url 属性
  3. ProjectFeignClient.java - 移除 url 属性
  4. SysServiceClient.java - 移除 url 属性

修复后的架构优势

工作流程:

Fund-Report (调用方)
  ↓ FeignClient: fund-exp
  ↓ Spring Cloud OpenFeign + Nacos Discovery
  ↓ 从 Nacos 查询 fund-exp 服务的所有实例
  ↓ Ribbon/LoadBalancer 进行负载均衡选择实例
  ↓ 发起 HTTP 请求到选中的实例

带来的好处:

特性 修复前 修复后
服务发现 固定地址 自动感知上下线
负载均衡 单点 多实例轮询
高可用 故障无法切换 自动切换健康实例
弹性扩缩容 需修改配置 新增实例无需配置
架构一致性 Nacos 形同虚设 充分发挥作用

正确的 FeignClient 配置规范

场景 配置方式 示例
有注册中心(推荐) 只指定 name @FeignClient(name = "fund-exp")
无注册中心 指定 name + url @FeignClient(name = "xxx", url = "http://localhost:8080")
需要降级 指定 name + fallback @FeignClient(name = "fund-exp", fallback = ExpFallback.class)

必备配置要求

1. application.yml 配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  # Nacos 地址
        namespace: fund-platform     # 命名空间
        group: DEFAULT_GROUP         # 分组

2. 启动类注解

@SpringBootApplication
@EnableDiscoveryClient  // 启用服务发现
@EnableFeignClients     // 启用 Feign 客户端
public class ReportApplication {
    // ...
}

经验总结

  • 有注册中心时必须移除 url 属性,让 Feign 通过注册中心发现服务
  • url 属性仅用于开发测试或特殊场景(如直连第三方服务)
  • Nacos 的价值在于服务发现与负载均衡,硬编码 URL 会让这些能力失效
  • 微服务架构应充分利用注册中心,避免回到点对点调用的老路

问题 9Nacos 3.0 客户端缺少 username/password 认证配置

问题现象

Nacos 3.0 版本升级后,各业务服务启动时无法注册到 Nacos控制台报错

com.alibaba.nacos.api.exception.NacosException: 
Error Code: 403, Error Msg: authorization failed

或日志中出现:

WARN  com.alibaba.nacos.client.naming - [NA] failed to call server
com.alibaba.nacos.api.exception.NacosException: 
Client need token, but token is empty.

问题原因

Nacos 3.0 安全策略变更

  • Nacos 2.x 版本:客户端可以匿名访问服务端
  • Nacos 3.0 版本:强制要求客户端提供用户名和密码进行认证

默认凭证

  • 用户名:nacos
  • 密码:nacos

影响范围 所有配置了 Nacos 服务发现的模块都需要添加认证信息。

解决方案

修复前(

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: fund-platform
        group: DEFAULT_GROUP
        # 缺少 username 和 password

修复后(

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: fund-platform
        group: DEFAULT_GROUP
        username: nacos      # Nacos 3.0 必需
        password: nacos      # Nacos 3.0 必需

涉及的所有模块

模块 服务名 配置文件位置
fund-sys fund-sys fund-sys/src/main/resources/application.yml
fund-cust fund-cust fund-cust/src/main/resources/application.yml
fund-exp fund-exp fund-exp/src/main/resources/application.yml
fund-proj fund-proj fund-proj/src/main/resources/application.yml
fund-req fund-req fund-req/src/main/resources/application.yml
fund-receipt fund-receipt fund-receipt/src/main/resources/application.yml
fund-file fund-file fund-file/src/main/resources/application.yml
fund-report fund-report fund-report/src/main/resources/application.yml

验证方法

1. 查看服务注册状态 访问 Nacos 控制台:http://localhost:8048/

  • 登录账号:nacos / nacos
  • 进入「服务管理」→「服务列表」
  • 应该能看到所有服务已成功注册

2. 检查服务日志 启动日志中应该出现:

INFO  c.a.cloud.nacos.registry.NacosServiceRegistry - 
nacos registry, DEFAULT_GROUP fund-sys 10.244.21.185:8100 register finished

3. 测试服务调用

curl http://localhost:8000/sys/api/v1/sys/health
# 应该能正常返回健康检查结果

Nacos 3.0 vs 2.x 主要变化

特性 Nacos 2.x Nacos 3.0 影响
客户端认证 不需要 强制要求 必须配置 username/password
Token 机制 JWT Token 客户端需携带有效 Token
权限控制 强化 支持细粒度 RBAC 权限管理
审计日志 基础 完整 记录所有敏感操作

经验总结

  • Nacos 3.0 升级后必须修改客户端配置,否则无法正常注册
  • 默认用户名和密码都是 nacos,生产环境建议修改为强密码
  • Gateway 不需要配置 discovery,但需要通过 @EnableDiscoveryClient 启用服务发现
  • 认证失败会报 403 错误,而不是连接失败的错误

预防措施清单

前后端接口对接

检查项 说明
请求头确认 确认所有必需的请求头X-User-Id、X-Tenant-Id、Authorization等
响应格式确认 统一使用 Result<T> 包装响应,确认字段命名一致
错误码定义 明确各类错误码的含义和处理方式

前端路由配置

检查项 说明
代理路径精确匹配 使用 /api/ 带斜杠的形式,避免模糊匹配
路由命名规范 前端路由与后端API路径应使用不同的前缀区分
刷新兼容性 SPA 应用需考虑页面刷新时的路由状态恢复

后端开发规范

检查项 说明
AOP 依赖引入 使用 AOP 需添加 spring-boot-starter-aop 依赖
Jackson 日期配置 序列化 Java 8 日期类型需注册 JavaTimeModule
日志独立输出 使用 Logger name 隔离,配置独立的 appender
外键字段类型 使用雪花 ID 的外键字段必须使用 Long 类型
Feign 拦截器注册 Feign 拦截器必须添加@Component 注解并配置组件扫描
TraceId 传递 ContextInterceptor 必须提取并设置 TraceId
FeignClient 配置 有注册中心时必须移除 url 属性,仅保留 name
Nacos 3.0 认证 客户端必须配置 username 和 password默认都是 nacos

功能开发流程

检查项 说明
UI交互完整性 所有菜单、按钮的点击事件都应有对应实现
自测覆盖 代码提交前进行基础功能自测
刷新测试 重点测试页面刷新后的状态是否正常

相关文件索引

本次问题修复涉及的文件

后端文件:

  • fund-sys/pom.xml - 添加 AOP 和 Jackson 依赖
  • fund-sys/src/main/java/com/fundplatform/sys/aop/ApiLogAspect.java - AOP 日志切面
  • fund-sys/src/main/resources/logback-spring.xml - 日志配置
  • fund-exp/src/main/java/com/fundplatform/exp/dto/FundExpenseDTO.java - 支出 DTO expenseType 字段类型修复
  • fund-common/src/main/java/com/fundplatform/common/feign/FeignChainInterceptor.java - Feign 拦截器添加@Component 注解
  • fund-report/src/main/java/com/fundplatform/report/ReportApplication.java - 配置组件扫描路径
  • fund-common/src/main/java/com/fundplatform/common/web/ContextInterceptor.java - 添加 TraceId 提取与清理逻辑
  • fund-report/src/main/java/com/fundplatform/report/feign/ExpenseFeignClient.java - 移除硬编码 URL使用 Nacos 服务发现
  • fund-report/src/main/java/com/fundplatform/report/feign/ReceivableFeignClient.java - 移除硬编码 URL使用 Nacos 服务发现
  • fund-report/src/main/java/com/fundplatform/report/feign/ProjectFeignClient.java - 移除硬编码 URL使用 Nacos 服务发现
  • fund-cust/src/main/java/com/fundplatform/cust/feign/SysServiceClient.java - 移除硬编码 URL使用 Nacos 服务发现
  • fund-*/src/main/resources/application.yml - 为所有业务模块添加 Nacos 3.0 username/password 认证配置

前端文件:

  • fund-admin/vite.config.ts - 代理配置修复
  • fund-admin/src/router/index.ts - 路由配置
  • fund-admin/src/layouts/MainLayout.vue - 菜单事件处理
  • fund-admin/src/api/request.ts - 请求拦截器
  • fund-admin/src/stores/user.ts - 用户状态管理

附录:常见错误速查

错误信息 可能原因 解决方案
400 Bad Request 缺少必需请求头 检查X-User-Id、X-Tenant-Id等
404 Not Found (刷新后) 代理路径冲突 使用精确匹配,区分前后端路由
Java 8 date/time type not supported Jackson未配置日期模块 注册JavaTimeModule
ClassNotFoundException: Aspect 未引入AOP依赖 添加spring-boot-starter-aop
JSON parse error 字段类型不匹配Integer vs Long 雪花ID外键必须使用Long类型
Feign 调用未透传上下文 拦截器未注册为 Bean 添加@Component 注解并配置扫描
TraceId 链路断裂 ContextInterceptor 未提取 TraceId 在 preHandle 中添加 TraceId 设置逻辑
服务发现失效 FeignClient 硬编码 URL 移除 url 属性,仅保留 name 使用 Nacos 发现
Nacos 注册失败 (认证错误) 缺少 username/password 配置 在 discovery 节点添加 username 和 password

备注: 本文档将根据后续开发过程中的问题持续更新。