fundplatform/doc/资金服务平台 FundPlatform 架构设计文档.md

84 KiB
Raw Blame History

资金服务平台 (FundPlatform) - 架构设计文档

文档版本: v1.0
创建日期: 2026-02-13
项目名称: 资金服务平台
项目代号: fundplatform


一、架构设计概述

1.1 设计目标

目标 描述
高可用性 系统可用性≥99.5%,支持故障自动恢复
高性能 支持≥100人并发接口响应<3秒
可扩展性 微服务架构,支持水平扩展
安全性 数据加密传输,完善的权限控制
可维护性 模块化设计,代码结构清晰
多租户 支持一库多租户和一库一租户两种模式
可观测性 全链路日志跟踪,支持 Head 日志追踪

1.2 架构风格

采用 微服务架构 + 前后端分离 + 多租户架构 模式:

  • 后端Spring Cloud Alibaba 微服务框架
  • 前端Vue 3 + UniApp 多端应用
  • 数据层MySQL + Redis 缓存(支持多租户隔离)
  • 基础设施Nacos 服务治理、Nginx 负载均衡
  • 可观测性Head 日志追踪 + 全链路监控

二、多租户架构设计

2.1 多租户架构概述

系统支持两种多租户模式,可根据业务需求灵活选择:

模式 说明 适用场景
一库多租户 所有租户共享一个数据库,通过 tenant_id 字段隔离 中小租户、数据量较小、成本敏感
一库一租户 每个租户独立数据库,物理隔离 大客户、数据量大、安全要求高

2.2 一库多租户模式

2.2.1 架构设计

┌─────────────────────────────────────────────────────────────┐
│                        应用服务层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │  fund-sys   │  │  fund-cust  │  │  fund-proj  │         │
│  │  (多租户)    │  │  (多租户)    │  │  (多租户)    │         │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘         │
└─────────┼────────────────┼────────────────┼─────────────────┘
          │                │                │
          └────────────────┴────────────────┘
                          │
          ┌───────────────┴───────────────┐
          │      TenantContextHolder      │
          │   (线程级租户上下文存储)        │
          └───────────────┬───────────────┘
                          │
┌─────────────────────────┼─────────────────────────────────────┐
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                    MySQL 数据库                          │ │
│  │  ┌─────────────────────────────────────────────────┐   │ │
│  │  │              fund_platform 库                    │   │ │
│  │  │  ┌───────────┬───────────┬───────────┬─────────┐ │   │ │
│  │  │  │ tenant_id │ user_id   │ username  │  ...    │ │   │ │
│  │  │  ├───────────┼───────────┼───────────┼─────────┤ │   │ │
│  │  │  │    1      │    1      │  admin    │  ...    │ │   │ │
│  │  │  │    1      │    2      │  user1    │  ...    │ │   │ │
│  │  │  │    2      │    1      │  admin    │  ...    │ │   │ │
│  │  │  └───────────┴───────────┴───────────┴─────────┘ │   │ │
│  │  │  所有表包含 tenant_id 字段用于数据隔离            │   │ │
│  │  └─────────────────────────────────────────────────┘   │ │
│  └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘

2.2.2 技术实现

1. 租户识别

/**
 * 租户上下文持有者
 */
public class TenantContextHolder {
    private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
    
    public static void setTenantId(Long tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static Long getTenantId() {
        return CURRENT_TENANT.get();
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

2. 租户拦截器

/**
 * 租户拦截器 - 从请求头或JWT中提取租户ID
 */
@Component
public class TenantInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) {
        // 从请求头获取租户ID
        String tenantId = request.getHeader("X-Tenant-Id");
        
        // 或从JWT Token中解析
        if (StringUtils.isEmpty(tenantId)) {
            String token = request.getHeader("Authorization");
            tenantId = JwtUtil.getTenantId(token);
        }
        
        if (StringUtils.isNotEmpty(tenantId)) {
            TenantContextHolder.setTenantId(Long.valueOf(tenantId));
        }
        
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        TenantContextHolder.clear();
    }
}

3. MyBatis-Plus 租户插件

/**
 * 多租户 SQL 拦截器
 */
@Component
public class TenantLineInnerInterceptor implements InnerInterceptor {
    
    @Override
    public void beforeQuery(Executor executor, 
                          MappedStatement ms, 
                          Object parameter, 
                          RowBounds rowBounds,
                          ResultHandler resultHandler, 
                          BoundSql boundSql) {
        // 获取当前租户ID
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            return;
        }
        
        // 获取SQL
        String originalSql = boundSql.getSql();
        
        // 忽略特定表(如租户表本身)
        if (isIgnoreTable(ms.getId())) {
            return;
        }
        
        // 添加租户条件
        String tenantSql = addTenantCondition(originalSql, tenantId);
        
        // 重写SQL
        reflectSetField(boundSql, "sql", tenantSql);
    }
    
    private String addTenantCondition(String sql, Long tenantId) {
        // 使用 JSqlParser 解析并修改 SQL
        try {
            Statement statement = CCJSqlParserUtil.parse(sql);
            if (statement instanceof Select) {
                Select select = (Select) statement;
                PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
                
                // 添加 WHERE 条件
                EqualsTo equalsTo = new EqualsTo();
                equalsTo.setLeftExpression(new Column("tenant_id"));
                equalsTo.setRightExpression(new LongValue(tenantId));
                
                Expression where = plainSelect.getWhere();
                if (where == null) {
                    plainSelect.setWhere(equalsTo);
                } else {
                    AndExpression and = new AndExpression(where, equalsTo);
                    plainSelect.setWhere(and);
                }
                
                return select.toString();
            }
        } catch (JSQLParserException e) {
            log.error("SQL解析失败", e);
        }
        return sql;
    }
}

4. 实体基类

/**
 * 多租户实体基类
 */
@Data
public abstract class TenantBaseEntity extends BaseEntity {
    
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
}

2.2.3 数据隔离策略

隔离级别 实现方式 说明
行级隔离 tenant_id 字段过滤 默认方式,所有业务表包含 tenant_id
schema隔离 不同租户使用不同schema 可选,适用于数据量大的租户
缓存隔离 Redis Key 添加租户前缀 tenant:{id}:user:info
文件隔离 COS 目录按租户划分 /tenant/{id}/files/

2.3 一库一租户模式

2.3.1 架构设计

┌─────────────────────────────────────────────────────────────┐
│                        应用服务层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │  fund-sys   │  │  fund-cust  │  │  fund-proj  │         │
│  │  (多租户)    │  │  (多租户)    │  │  (多租户)    │         │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘         │
└─────────┼────────────────┼────────────────┼─────────────────┘
          │                │                │
          └────────────────┴────────────────┘
                          │
          ┌───────────────┴───────────────┐
          │      DynamicDataSource        │
          │      (动态数据源路由)          │
          └───────────────┬───────────────┘
                          │
          ┌───────────────┼───────────────┐
          │               │               │
          ▼               ▼               ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│  tenant_1_db    │ │  tenant_2_db    │ │  tenant_3_db    │
│  (租户1数据库)   │ │  (租户2数据库)   │ │  (租户3数据库)   │
│                 │ │                 │ │                 │
│ • sys_user      │ │ • sys_user      │ │ • sys_user      │
│ • customer      │ │ • customer      │ │ • customer      │
│ • project       │ │ • project       │ │ • project       │
│ • expense       │ │ • expense       │ │ • expense       │
└─────────────────┘ └─────────────────┘ └─────────────────┘

2.3.2 技术实现

1. 动态数据源配置

/**
 * 动态数据源上下文
 */
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
    
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

/**
 * 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

2. 数据源管理器

/**
 * 租户数据源管理器
 */
@Component
public class TenantDataSourceManager {
    
    @Autowired
    private DynamicDataSource dynamicDataSource;
    
    @Autowired
    private TenantDataSourceConfigRepository configRepository;
    
    /**
     * 添加租户数据源
     */
    public void addDataSource(Long tenantId) {
        TenantDataSourceConfig config = configRepository.findByTenantId(tenantId);
        
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(config.getDriverClassName());
        dataSource.setUrl(config.getJdbcUrl());
        dataSource.setUsername(config.getUsername());
        dataSource.setPassword(config.getPassword());
        dataSource.setInitialSize(5);
        dataSource.setMaxActive(20);
        
        // 添加到动态数据源
        dynamicDataSource.addDataSource("tenant_" + tenantId, dataSource);
    }
    
    /**
     * 切换租户数据源
     */
    public void switchDataSource(Long tenantId) {
        String key = "tenant_" + tenantId;
        
        // 检查数据源是否存在
        if (!dynamicDataSource.hasDataSource(key)) {
            addDataSource(tenantId);
        }
        
        DynamicDataSourceContextHolder.setDataSourceKey(key);
    }
}

3. 租户数据源拦截器

/**
 * 租户数据源切换拦截器
 */
@Component
public class TenantDataSourceInterceptor implements HandlerInterceptor {
    
    @Autowired
    private TenantDataSourceManager dataSourceManager;
    
    @Override
    public boolean preHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler) {
        // 获取租户ID
        String tenantId = request.getHeader("X-Tenant-Id");
        
        if (StringUtils.isNotEmpty(tenantId)) {
            // 切换数据源
            dataSourceManager.switchDataSource(Long.valueOf(tenantId));
        }
        
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        DynamicDataSourceContextHolder.clear();
    }
}

2.4 租户管理

2.4.1 租户表设计

CREATE TABLE sys_tenant (
    tenant_id       BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID',
    tenant_code     VARCHAR(50) NOT NULL UNIQUE COMMENT '租户编码',
    tenant_name     VARCHAR(100) NOT NULL COMMENT '租户名称',
    tenant_type     TINYINT DEFAULT 1 COMMENT '租户类型1-一库多租户2-一库一租户',
    db_config       JSON COMMENT '数据库配置(一库一租户模式使用)',
    status          TINYINT DEFAULT 1 COMMENT '状态0-禁用1-启用',
    expire_time     DATETIME COMMENT '过期时间',
    created_time    DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time    DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_code (tenant_code),
    INDEX idx_status (status)
) COMMENT='租户表';

2.4.2 租户创建流程

┌─────────┐     创建租户      ┌─────────┐
│  管理员  │ ───────────────> │  系统   │
└─────────┘                  └────┬────┘
                                  │
                    ┌─────────────┼─────────────┐
                    │             │             │
                    ▼             ▼             ▼
            ┌───────────┐ ┌───────────┐ ┌───────────┐
            │ 保存租户   │ │ 初始化数据 │ │ 创建数据源 │
            │ 基本信息   │ │ (菜单/角色)│ │ (一库一租户)│
            └───────────┘ └───────────┘ └───────────┘

2.5 一库一租户路由方案对比

2.5.1 方案对比DynamicDataSource vs Feign 动态路由

对比维度 DynamicDataSource Feign 动态路由
架构层级 数据层 服务层
隔离级别 数据源级别 服务实例级别
资源占用 高(连接池 × N租户 中(服务实例 × N租户
扩展性 差(单服务多数据源) 优(独立扩容)
故障隔离 差(单点故障影响多租户) 优(租户间互不影响)
跨租户查询 困难 可通过聚合服务实现
服务治理 简单 需要完善的服务发现
部署复杂度
私有化部署 困难 简单(整体迁移)
运维成本

2.5.2 推荐方案Feign 动态路由

推荐理由:

  1. 真正的租户隔离:每个租户拥有独立的服务实例和数据库
  2. 弹性扩容:单个租户可独立水平扩展
  3. 故障隔离:单个租户故障不影响其他租户
  4. 私有化友好:便于大客户私有化部署
  5. 符合云原生:与 Kubernetes 等容器平台配合良好

2.6 Feign 动态路由详细设计

2.6.1 架构设计

┌─────────────────────────────────────────────────────────────────────────────┐
│                          Feign 动态路由架构                                   │
│                                                                             │
│  ┌─────────┐                                                               │
│  │  客户端  │  Header: X-Tenant-Id: tenant_001                              │
│  └────┬────┘                                                               │
│       │                                                                     │
│       ▼                                                                     │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                         Gateway (网关)                               │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │  TenantRoutingFilter                                        │   │   │
│  │  │  • 解析 X-Tenant-Id                                         │   │   │
│  │  │  • 根据租户ID路由到对应服务组                                │   │   │
│  │  │  • 添加 X-Tenant-Route 标记                                 │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  └────────────────────────────────┬───────────────────────────────────┘   │
│                                   │                                         │
│       ┌───────────────────────────┼───────────────────────────┐            │
│       │                           │                           │            │
│       ▼                           ▼                           ▼            │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      Nacos 服务注册中心                              │   │
│  │                                                                     │   │
│  │  服务分组设计:                                                      │   │
│  │  • fund-sys:DEFAULT (共享服务组)                                    │   │
│  │  • fund-sys:TENANT_001 (租户001专属)                                │   │
│  │  • fund-sys:TENANT_002 (租户002专属)                                │   │
│  │  • fund-cust:TENANT_001                                             │   │
│  │  • ...                                                              │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                   │                                         │
│       ┌───────────────────────────┼───────────────────────────┐            │
│       │                           │                           │            │
│       ▼                           ▼                           ▼            │
│  ┌──────────────┐          ┌──────────────┐          ┌──────────────┐     │
│  │ 租户001服务组 │          │ 租户002服务组 │          │ 租户003服务组 │     │
│  │              │          │              │          │              │     │
│  │ ┌──────────┐ │          │ ┌──────────┐ │          │ ┌──────────┐ │     │
│  │ │fund-sys  │ │          │ │fund-sys  │ │          │ │fund-sys  │ │     │
│  │ │:8100     │ │          │ │:8101     │ │          │ │:8102     │ │     │
│  │ └──────────┘ │          │ └──────────┘ │          │ └──────────┘ │     │
│  │ ┌──────────┐ │          │ ┌──────────┐ │          │ ┌──────────┐ │     │
│  │ │fund-cust │ │          │ │fund-cust │ │          │ │fund-cust │ │     │
│  │ │:8110     │ │          │ │:8111     │ │          │ │:8112     │ │     │
│  │ └──────────┘ │          │ └──────────┘ │          │ └──────────┘ │     │
│  │ ┌──────────┐ │          │ ┌──────────┐ │          │ ┌──────────┐ │     │
│  │ │fund-proj │ │          │ │fund-proj │ │          │ │fund-proj │ │     │
│  │ │:8120     │ │          │ │:8121     │ │          │ │:8122     │ │     │
│  │ └──────────┘ │          │ └──────────┘ │          │ └──────────┘ │     │
│  └──────┬───────┘          └──────┬───────┘          └──────┬───────┘     │
│         │                         │                         │             │
│         └─────────────────────────┼─────────────────────────┘             │
│                                   │                                       │
│       ┌───────────────────────────┼───────────────────────────┐          │
│       │                           │                           │          │
│       ▼                           ▼                           ▼          │
│  ┌──────────┐              ┌──────────┐              ┌──────────┐       │
│  │tenant_001│              │tenant_002│              │tenant_003│       │
│  │  _db     │              │  _db     │              │  _db     │       │
│  └──────────┘              └──────────┘              └──────────┘       │
└─────────────────────────────────────────────────────────────────────────┘

2.6.2 核心组件实现

1. 租户路由元数据管理

/**
 * 租户路由元数据
 */
@Data
@Component
@ConfigurationProperties(prefix = "tenant.routing")
public class TenantRoutingProperties {
    
    /** 是否启用租户路由 */
    private boolean enabled = true;
    
    /** 租户ID请求头 */
    private String tenantHeader = "X-Tenant-Id";
    
    /** 服务组分隔符 */
    private String groupSeparator = "TENANT_";
    
    /** 共享服务列表(不区分租户) */
    private List<String> sharedServices = Arrays.asList("fund-gateway", "fund-report");
    
    /** 租户服务配置 */
    private Map<String, TenantServiceConfig> tenantConfigs = new HashMap<>();
}

/**
 * 租户服务配置
 */
@Data
public class TenantServiceConfig {
    /** 租户ID */
    private String tenantId;
    
    /** 服务实例列表 */
    private Map<String, ServiceInstanceConfig> services = new HashMap<>();
    
    /** 数据库配置 */
    private DatabaseConfig database;
}

/**
 * 服务实例配置
 */
@Data
public class ServiceInstanceConfig {
    /** 服务名 */
    private String serviceName;
    
    /** 端口号 */
    private int port;
    
    /** 实例数 */
    private int replicas = 1;
}

2. Gateway 租户路由过滤器

/**
 * Gateway 租户路由过滤器
 * 根据 X-Tenant-Id 路由到对应租户的服务组
 */
@Component
@Order(-50)
public class TenantRoutingGatewayFilter implements GlobalFilter {
    
    @Autowired
    private TenantRoutingProperties routingProperties;
    
    @Autowired
    private NacosServiceManager nacosServiceManager;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String tenantId = request.getHeaders().getFirst(routingProperties.getTenantHeader());
        
        // 未指定租户或共享服务,使用默认路由
        if (StringUtils.isEmpty(tenantId) || isSharedService(exchange)) {
            return chain.filter(exchange);
        }
        
        // 构建租户服务组名称
        String tenantGroup = buildTenantGroup(tenantId);
        
        // 将租户路由信息存入 Exchange 属性
        exchange.getAttributes().put("tenantId", tenantId);
        exchange.getAttributes().put("tenantGroup", tenantGroup);
        
        // 添加到请求头,传递给下游服务
        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-Tenant-Group", tenantGroup)
            .header("X-Tenant-Id", tenantId)
            .build();
        
        log.info("[TenantRouting] Route request to tenant group: {}, URI: {}", 
            tenantGroup, request.getURI());
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
    
    /**
     * 构建租户服务组名称
     */
    private String buildTenantGroup(String tenantId) {
        return routingProperties.getGroupSeparator() + tenantId.toUpperCase();
    }
    
    /**
     * 判断是否为共享服务
     */
    private boolean isSharedService(ServerWebExchange exchange) {
        String path = exchange.getRequest().getURI().getPath();
        return routingProperties.getSharedServices().stream()
            .anyMatch(path::contains);
    }
}

3. 自定义负载均衡器(按租户路由)

/**
 * 租户感知的负载均衡器
 */
public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    
    private final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
    
    public TenantAwareLoadBalancer(String serviceId, 
            ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
        this.serviceId = serviceId;
        this.supplierProvider = supplierProvider;
    }
    
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 从请求上下文获取租户ID
        String tenantId = getTenantIdFromRequest(request);
        String tenantGroup = buildTenantGroup(tenantId);
        
        ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
        
        return supplier.get().next()
            .map(instances -> filterByTenantGroup(instances, tenantGroup))
            .map(this::getInstanceResponse);
    }
    
    /**
     * 根据租户组过滤服务实例
     */
    private List<ServiceInstance> filterByTenantGroup(
            List<ServiceInstance> instances, String tenantGroup) {
        
        // 优先选择租户专属实例
        List<ServiceInstance> tenantInstances = instances.stream()
            .filter(inst -> tenantGroup.equals(inst.getMetadata().get("tenant-group")))
            .collect(Collectors.toList());
        
        if (!tenantInstances.isEmpty()) {
            return tenantInstances;
        }
        
        // 回退到默认实例
        return instances.stream()
            .filter(inst -> !inst.getMetadata().containsKey("tenant-group"))
            .collect(Collectors.toList());
    }
    
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        // 轮询选择
        int index = ThreadLocalRandom.current().nextInt(instances.size());
        return new DefaultResponse(instances.get(index));
    }
}

/**
 * 租户负载均衡配置
 */
@Configuration
public class TenantLoadBalancerConfig {
    
    @Bean
    public ReactorLoadBalancer<ServiceInstance> tenantAwareLoadBalancer(
            Environment environment,
            LoadBalancerClientFactory loadBalancerClientFactory) {
        
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        
        return new TenantAwareLoadBalancer(name,
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
    }
}

4. Feign 租户路由拦截器

/**
 * Feign 租户路由拦截器
 * 在服务间调用时保持租户上下文
 */
@Component
public class FeignTenantRoutingInterceptor implements RequestInterceptor {
    
    @Autowired
    private TenantRoutingProperties routingProperties;
    
    @Override
    public void apply(RequestTemplate template) {
        // 从当前线程上下文获取租户ID
        String tenantId = TenantContextHolder.getTenantId();
        
        if (StringUtils.isNotEmpty(tenantId)) {
            // 添加租户ID到请求头
            template.header(routingProperties.getTenantHeader(), tenantId);
            
            // 添加租户组到请求头(用于目标服务的路由)
            String tenantGroup = buildTenantGroup(tenantId);
            template.header("X-Tenant-Group", tenantGroup);
            
            log.debug("[FeignTenantRouting] Add tenant header: {}, group: {}", 
                tenantId, tenantGroup);
        }
    }
    
    private String buildTenantGroup(String tenantId) {
        return routingProperties.getGroupSeparator() + tenantId.toUpperCase();
    }
}

5. Nacos 服务注册(带租户标记)

/**
 * Nacos 租户服务注册配置
 */
@Configuration
public class NacosTenantRegistrationConfig {
    
    @Value("${tenant.id:}")
    private String tenantId;
    
    @Bean
    public NacosRegistrationCustomizer tenantNacosRegistrationCustomizer() {
        return registration -> {
            if (StringUtils.isNotEmpty(tenantId)) {
                // 添加租户标记到服务元数据
                registration.getMetadata().put("tenant-id", tenantId);
                registration.getMetadata().put("tenant-group", "TENANT_" + tenantId.toUpperCase());
                
                // 修改服务分组
                registration.setGroup("TENANT_" + tenantId.toUpperCase());
                
                log.info("[NacosRegistration] Register service with tenant: {}", tenantId);
            }
        };
    }
}

6. 租户上下文管理

/**
 * 租户上下文持有者
 */
public class TenantContextHolder {
    
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
    private static final ThreadLocal<String> CURRENT_TENANT_GROUP = new ThreadLocal<>();
    
    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }
    
    public static void setTenantGroup(String tenantGroup) {
        CURRENT_TENANT_GROUP.set(tenantGroup);
    }
    
    public static String getTenantGroup() {
        return CURRENT_TENANT_GROUP.get();
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
        CURRENT_TENANT_GROUP.remove();
    }
}

/**
 * 租户上下文过滤器
 */
@Component
public class TenantContextFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        try {
            // 从请求头获取租户信息
            String tenantId = httpRequest.getHeader("X-Tenant-Id");
            String tenantGroup = httpRequest.getHeader("X-Tenant-Group");
            
            if (StringUtils.isNotEmpty(tenantId)) {
                TenantContextHolder.setTenantId(tenantId);
                TenantContextHolder.setTenantGroup(tenantGroup);
                
                // 设置到 MDC 用于日志
                MDC.put("tenantId", tenantId);
            }
            
            chain.doFilter(request, response);
        } finally {
            TenantContextHolder.clear();
            MDC.remove("tenantId");
        }
    }
}

2.6.3 配置示例

# application-tenant.yml
spring:
  cloud:
    nacos:
      discovery:
        # 动态服务组,由代码设置
        group: ${TENANT_GROUP:DEFAULT}
        metadata:
          tenant-id: ${TENANT_ID:}
          tenant-group: ${TENANT_GROUP:DEFAULT}
    loadbalancer:
      configurations: tenant-aware

tenant:
  routing:
    enabled: true
    tenant-header: X-Tenant-Id
    group-separator: TENANT_
    # 共享服务(所有租户共用)
    shared-services:
      - fund-gateway
      - fund-report
      - fund-file
    # 租户配置
    tenant-configs:
      tenant_001:
        tenant-id: tenant_001
        services:
          fund-sys:
            service-name: fund-sys
            port: 8100
            replicas: 2
          fund-cust:
            service-name: fund-cust
            port: 8110
            replicas: 1
        database:
          url: jdbc:mysql://localhost:3306/tenant_001_db
          username: tenant_001
          password: xxx
      tenant_002:
        tenant-id: tenant_002
        services:
          fund-sys:
            service-name: fund-sys
            port: 8101
            replicas: 1
          fund-cust:
            service-name: fund-cust
            port: 8111
            replicas: 1
        database:
          url: jdbc:mysql://localhost:3306/tenant_002_db
          username: tenant_002
          password: xxx

2.6.4 部署架构

┌─────────────────────────────────────────────────────────────────────────────┐
│                         Kubernetes 部署示例                                  │
│                                                                             │
│  apiVersion: apps/v1                                                        │
│  kind: Deployment                                                           │
│  metadata:                                                                  │
│    name: fund-sys-tenant-001                                                │
│    labels:                                                                  │
│      app: fund-sys                                                          │
│      tenant: tenant-001                                                     │
│  spec:                                                                      │
│    replicas: 2                                                              │
│    selector:                                                                │
│      matchLabels:                                                           │
│        app: fund-sys                                                        │
│        tenant: tenant-001                                                   │
│    template:                                                                │
│      metadata:                                                              │
│        labels:                                                              │
│          app: fund-sys                                                      │
│          tenant: tenant-001                                                 │
│      spec:                                                                  │
│        containers:                                                          │
│        - name: fund-sys                                                     │
│          image: fundplatform/fund-sys:1.0.0                                 │
│          env:                                                               │
│          - name: TENANT_ID                                                  │
│            value: "tenant_001"                                              │
│          - name: TENANT_GROUP                                               │
│            value: "TENANT_001"                                              │
│          - name: DB_URL                                                     │
│            value: "jdbc:mysql://mysql:3306/tenant_001_db"                   │
└─────────────────────────────────────────────────────────────────────────────┘

2.6.5 两种方案最终对比

场景 推荐方案 原因
中小租户 (< 100) DynamicDataSource 资源利用率高,运维简单
大客户/私有化 Feign 动态路由 物理隔离,独立扩容
金融/政务行业 Feign 动态路由 安全合规要求高
快速原型/MVP DynamicDataSource 快速上线,成本低
长期运营 SaaS Feign 动态路由 可持续演进,租户自治

三、Head 日志追踪设计

3.1 概述

Head 日志追踪Header-based Logging是一种基于请求头的全链路日志追踪方案通过在每个请求中传递唯一的 Trace ID实现跨服务的调用链追踪。

3.2 架构设计

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Head 日志追踪架构                                │
│                                                                             │
│  ┌─────────┐                                                               │
│  │  客户端  │  X-Trace-Id: abc123                                           │
│  └────┬────┘                                                               │
│       │                                                                     │
│       ▼                                                                     │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                         Gateway (网关)                               │   │
│  │  ┌─────────────────────────────────────────────────────────────┐   │   │
│  │  │  TraceIdFilter                                              │   │   │
│  │  │  • 生成/接收 TraceId                                        │   │   │
│  │  │  • 写入 MDC (Mapped Diagnostic Context)                      │   │   │
│  │  │  • 传递 TraceId 到下游服务                                   │   │   │
│  │  └─────────────────────────────────────────────────────────────┘   │   │
│  └────────────────────────────────┬───────────────────────────────────┘   │
│                                   │                                         │
│       ┌───────────────────────────┼───────────────────────────┐            │
│       │                           │                           │            │
│       ▼                           ▼                           ▼            │
│  ┌─────────┐               ┌─────────┐               ┌─────────┐          │
│  │fund-sys │               │fund-cust│               │fund-proj│          │
│  │         │               │         │               │         │          │
│  │• 接收   │               │• 接收   │               │• 接收   │          │
│  │  TraceId│               │  TraceId│               │  TraceId│          │
│  │• 写入   │               │• 写入   │               │• 写入   │          │
│  │  MDC    │               │  MDC    │               │  MDC    │          │
│  │• 记录   │               │• 记录   │               │• 记录   │          │
│  │  日志   │               │  日志   │               │  日志   │          │
│  └────┬────┘               └────┬────┘               └────┬────┘          │
│       │                         │                         │               │
│       └─────────────────────────┼─────────────────────────┘               │
│                                 │                                         │
│                                 ▼                                         │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                         ELK / Loki 日志平台                          │   │
│  │  • 日志收集 → 解析 → 存储 → 检索 → 可视化                            │   │
│  │  • 支持 TraceId 全链路查询                                          │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

3.3 技术实现

3.3.1 TraceId 生成与传递

/**
 * TraceId 工具类
 */
public class TraceIdUtil {
    
    public static final String TRACE_ID_HEADER = "X-Trace-Id";
    public static final String TRACE_ID_MDC_KEY = "traceId";
    public static final String SPAN_ID_MDC_KEY = "spanId";
    
    /**
     * 生成 TraceId
     */
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
    
    /**
     * 生成 SpanId
     */
    public static String generateSpanId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    }
    
    /**
     * 获取当前 TraceId
     */
    public static String getCurrentTraceId() {
        return MDC.get(TRACE_ID_MDC_KEY);
    }
    
    /**
     * 设置 TraceId 到 MDC
     */
    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID_MDC_KEY, traceId);
    }
    
    /**
     * 清除 MDC
     */
    public static void clear() {
        MDC.clear();
    }
}

3.3.2 Gateway TraceId 过滤器

/**
 * Gateway TraceId 过滤器
 */
@Component
@Order(-100)
public class TraceIdGatewayFilter implements GlobalFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        
        // 获取或生成 TraceId
        String traceId = headers.getFirst(TraceIdUtil.TRACE_ID_HEADER);
        if (StringUtils.isEmpty(traceId)) {
            traceId = TraceIdUtil.generateTraceId();
        }
        
        // 生成 SpanId
        String spanId = TraceIdUtil.generateSpanId();
        
        // 添加到 MDC
        TraceIdUtil.setTraceId(traceId);
        MDC.put(TraceIdUtil.SPAN_ID_MDC_KEY, spanId);
        
        // 添加到请求头,传递给下游服务
        ServerHttpRequest mutatedRequest = request.mutate()
            .header(TraceIdUtil.TRACE_ID_HEADER, traceId)
            .header("X-Span-Id", spanId)
            .header("X-Parent-Span-Id", MDC.get(TraceIdUtil.SPAN_ID_MDC_KEY))
            .build();
        
        // 记录请求日志
        log.info("[Gateway] Request: {} {}, TraceId: {}", 
            request.getMethod(), 
            request.getURI(),
            traceId);
        
        final String finalTraceId = traceId;
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build())
            .doFinally(signalType -> {
                // 记录响应日志
                log.info("[Gateway] Response: {}, TraceId: {}", 
                    exchange.getResponse().getStatusCode(),
                    finalTraceId);
                TraceIdUtil.clear();
            });
    }
}

3.3.3 服务间 TraceId 传递

/**
 * Feign 请求拦截器 - 传递 TraceId
 */
@Component
public class FeignTraceIdInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        String traceId = TraceIdUtil.getCurrentTraceId();
        if (StringUtils.isNotEmpty(traceId)) {
            template.header(TraceIdUtil.TRACE_ID_HEADER, traceId);
        }
    }
}

/**
 * 服务端 TraceId 过滤器
 */
@Component
public class TraceIdFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 从请求头获取 TraceId
        String traceId = httpRequest.getHeader(TraceIdUtil.TRACE_ID_HEADER);
        if (StringUtils.isEmpty(traceId)) {
            traceId = TraceIdUtil.generateTraceId();
        }
        
        // 设置到 MDC
        TraceIdUtil.setTraceId(traceId);
        MDC.put(TraceIdUtil.SPAN_ID_MDC_KEY, TraceIdUtil.generateSpanId());
        
        try {
            chain.doFilter(request, response);
        } finally {
            TraceIdUtil.clear();
        }
    }
}

3.3.4 日志配置

<!-- logback-spring.xml -->
<configuration>
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 文件输出JSON格式便于ELK收集 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <maxFileSize>100MB</maxFileSize>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeContext>true</includeContext>
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>tenantId</includeMdcKeyName>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

3.4 日志格式规范

3.4.1 标准日志格式

[时间] [线程] [TraceId] [SpanId] [租户ID] [级别] [类名] - [消息]

示例:
2026-02-13 14:30:25.123 [http-nio-8100-exec-1] [abc123def456] [span789] [tenant_1] INFO c.f.s.c.UserController - 用户登录成功: admin

3.4.2 JSON 日志格式

{
  "@timestamp": "2026-02-13T14:30:25.123+08:00",
  "level": "INFO",
  "logger_name": "com.fundplatform.sys.controller.UserController",
  "message": "用户登录成功: admin",
  "thread_name": "http-nio-8100-exec-1",
  "traceId": "abc123def456",
  "spanId": "span789",
  "tenantId": "tenant_1",
  "service": "fund-sys",
  "host": "192.168.1.100"
}

3.5 日志收集与分析

3.5.1 ELK Stack 架构

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  应用服务    │────>│  Filebeat   │────>│  Logstash   │────>│ Elasticsearch│
│  (日志文件)  │     │  (日志收集)  │     │  (日志处理)  │     │  (日志存储)  │
└─────────────┘     └─────────────┘     └─────────────┘     └──────┬──────┘
                                                                    │
                                                                    ▼
                                                            ┌─────────────┐
                                                            │   Kibana    │
                                                            │  (日志查询)  │
                                                            └─────────────┘

3.5.2 Kibana 查询示例

# 根据 TraceId 查询全链路日志
traceId: "abc123def456"

# 根据租户ID查询日志
tenantId: "tenant_1"

# 查询特定服务的错误日志
service: "fund-sys" AND level: "ERROR"

# 查询特定用户的操作日志
message: "admin" AND tenantId: "tenant_1"

3.6 调用链追踪增强

3.6.1 与 SkyWalking 集成

/**
 * SkyWalking 插件配置
 */
@Component
public class SkyWalkingConfig {
    
    /**
     * 自定义 SkyWalking 标签
     */
    @TraceContext
    public void addCustomTags() {
        // 添加租户ID标签
        String tenantId = TenantContextHolder.getTenantId();
        if (tenantId != null) {
            Tags.ofKey("tenant.id").set(tenantId);
        }
        
        // 添加用户ID标签
        String userId = SecurityUtils.getCurrentUserId();
        if (userId != null) {
            Tags.ofKey("user.id").set(userId);
        }
    }
}

3.6.2 性能指标采集

/**
 * 接口性能监控
 */
@Aspect
@Component
public class PerformanceAspect {
    
    private static final Logger perfLog = LoggerFactory.getLogger("PERFORMANCE");
    
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long start = System.currentTimeMillis();
        String method = point.getSignature().toShortString();
        String traceId = TraceIdUtil.getCurrentTraceId();
        
        try {
            return point.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            perfLog.info("[Performance] Method: {}, Cost: {}ms, TraceId: {}", 
                method, cost, traceId);
        }
    }
}

四、系统整体架构

2.1 架构全景图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              接入层 (Access Layer)                           │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│  │   Web端     │  │   移动端    │  │   小程序    │  │   H5页面    │        │
│  │  (Vue3)     │  │  (UniApp)   │  │  (UniApp)   │  │  (UniApp)   │        │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘        │
└─────────┼────────────────┼────────────────┼────────────────┼───────────────┘
          │                │                │                │
          └────────────────┴────────────────┴────────────────┘
                                     │
┌────────────────────────────────────┼────────────────────────────────────────┐
│                         网关层 (Gateway Layer)                               │
│  ┌─────────────────────────────────┴─────────────────────────────────────┐   │
│  │                         Nginx 负载均衡                                 │   │
│  │                    SSL终止 / 静态资源 / 反向代理                        │   │
│  └─────────────────────────────────┬─────────────────────────────────────┘   │
│                                    │                                         │
│  ┌─────────────────────────────────┴─────────────────────────────────────┐   │
│  │                    Spring Cloud Gateway                               │   │
│  │         路由转发 / 限流熔断 / 鉴权认证 / 日志记录                        │   │
│  └─────────────────────────────────┬─────────────────────────────────────┘   │
└────────────────────────────────────┼────────────────────────────────────────┘
                                     │
┌────────────────────────────────────┼────────────────────────────────────────┐
│                         服务层 (Service Layer)                               │
│                                    │                                         │
│  ┌─────────────────────────────────┴─────────────────────────────────────┐   │
│  │                      Nacos 服务注册与配置中心                            │   │
│  └───────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │  系统服务    │ │  客户中心    │ │  项目中心    │ │  需求中心    │        │
│  │ fund-sys     │ │ fund-cust    │ │ fund-proj    │ │ fund-req     │        │
│  │              │ │              │ │              │ │              │        │
│  │ • 用户管理   │ │ • 客户管理   │ │ • 项目管理   │ │ • 需求工单   │        │
│  │ • 权限管理   │ │ • 联系人管理 │ │ • 成员管理   │ │ • 应收款管理 │        │
│  │ • 部门管理   │ │              │ │              │ │              │        │
│  │ • 日志管理   │ │              │ │              │ │              │        │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘        │
│         │                │                │                │                │
│  ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐        │
│  │  支出中心    │ │  收款中心    │ │  报表中心    │ │  文件中心    │        │
│  │ fund-exp     │ │ fund-receipt │ │ fund-report  │ │ fund-file    │        │
│  │              │ │              │ │              │ │              │        │
│  │ • 支出类型   │ │ • 收款记录   │ │ • 收支统计   │ │ • 文件上传   │        │
│  │ • 支出申请   │ │ • 收款凭证   │ │ • 趋势分析   │ │ • 文件存储   │        │
│  │ • 付款管理   │ │ • 账期管理   │ │ • 数据导出   │ │ • 文件下载   │        │
│  │ • 统计分析   │ │              │ │              │ │              │        │
│  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘        │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
                                     │
┌────────────────────────────────────┼────────────────────────────────────────┐
│                         数据层 (Data Layer)                                  │
│                                    │                                         │
│  ┌─────────────────────────────────┴─────────────────────────────────────┐   │
│  │                      Redis 缓存集群                                    │   │
│  │         会话缓存 / 热点数据 / 分布式锁 / 限流计数器                      │   │
│  └───────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │                      MySQL 8.0 主从集群                              │     │
│  │         用户库 / 业务库 / 日志库 / 报表库                             │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │                      腾讯COS 对象存储                                │     │
│  │         文件存储 / 图片存储 / 凭证附件                                │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

2.2 服务划分

服务名 服务编码 职责 端口范围
网关服务 fund-gateway 路由、鉴权、限流 8080
系统服务 fund-sys 用户、权限、部门、日志 8100-8109
客户中心 fund-cust 客户、联系人管理 8110-8119
项目中心 fund-proj 项目、成员管理 8120-8129
需求中心 fund-req 需求工单、应收款 8130-8139
支出中心 fund-exp 支出类型、支出管理 8140-8149
收款中心 fund-receipt 收款记录、账期管理 8150-8159
报表中心 fund-report 统计分析、数据导出 8160-8169
文件中心 fund-file 文件上传、存储管理 8170-8179

三、技术架构

3.1 技术栈选型

3.1.1 后端技术栈

层级 技术选型 版本 用途
基础框架 Spring Boot 3.2.x 应用基础框架
微服务框架 Spring Cloud Alibaba 2022.x 微服务治理
服务注册 Nacos 3.0.0 服务注册与配置中心
服务网关 Spring Cloud Gateway 4.x API网关
负载均衡 Spring Cloud LoadBalancer 4.x 客户端负载均衡
服务调用 OpenFeign 4.x 声明式HTTP客户端
服务容错 Sentinel 1.8.x 限流、熔断、降级
ORM框架 MyBatis-Plus 3.5.x 数据访问层
数据库连接池 Druid 1.2.x 连接池、监控
缓存框架 Spring Data Redis 3.x Redis操作
安全框架 Spring Security 6.x 认证授权
JWT令牌 jjwt 0.12.x Token生成与验证
API文档 Knife4j 4.x Swagger增强
对象存储 腾讯云COS SDK 5.x 文件存储
定时任务 XXL-JOB 2.4.x 分布式任务调度
日志框架 Logback + SLF4J - 日志记录
工具类库 Hutool 5.8.x 常用工具

3.1.2 前端技术栈

技术选型 版本 用途
管理后台 Vue 3.4.x 前端框架
TypeScript 5.x 类型安全
Element Plus 2.5.x UI组件库
Pinia 2.x 状态管理
Vue Router 4.x 路由管理
Axios 1.x HTTP客户端
ECharts 5.x 图表库
Vite 5.x 构建工具
移动端 UniApp 3.x 跨端框架
Vue 3.x 前端框架
uView UI 2.x 移动端UI库

3.2 架构分层

┌─────────────────────────────────────────────────────────────┐
│                        表现层 (Presentation)                 │
│  Controller / DTO / VO / 参数校验 / 异常处理                  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                        业务层 (Business)                     │
│  Service / 业务逻辑 / 事务管理 / 权限校验                      │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                        数据访问层 (Data Access)              │
│  Mapper / DAO / MyBatis-Plus / 数据库操作                     │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                        基础设施层 (Infrastructure)           │
│  Redis / MySQL / COS / MQ / 缓存 / 消息队列                   │
└─────────────────────────────────────────────────────────────┘

四、数据架构

4.1 数据库设计

4.1.1 数据库划分

数据库 用途 主要表
fund_sys 系统数据 sys_user, sys_dept, sys_role, sys_menu, sys_log
fund_biz 业务数据 customer, project, requirement, expense, receivable, receipt
fund_file 文件数据 file_record, file_chunk

4.1.2 分库分表策略

表名 策略 分片键 说明
sys_operation_log 按时间分表 created_time 每月一张表
expense 按时间分表 expense_date 每季度一张表
receipt 按时间分表 receipt_date 每季度一张表

4.2 缓存设计

4.2.1 缓存策略

缓存类型 Key前缀 过期时间 说明
用户会话 session: 30分钟 JWT Token 黑名单
用户信息 user:info: 1小时 用户基本信息缓存
字典数据 dict: 24小时 系统字典数据
热点数据 hot: 10分钟 频繁访问数据
限流计数 limit: 1分钟 API限流计数
分布式锁 lock: 30秒 并发控制

4.2.2 缓存更新策略

┌─────────────┐     查询      ┌─────────────┐
│   业务请求   │ ───────────> │   查询缓存   │
└─────────────┘              └──────┬──────┘
                                    │
                    ┌───────────────┼───────────────┐
                    │ 命中          │ 未命中         │
                    ▼               ▼               │
            ┌─────────────┐   ┌─────────────┐      │
            │  返回缓存    │   │  查询数据库  │      │
            │  数据       │   │             │      │
            └─────────────┘   └──────┬──────┘      │
                                     │             │
                                     ▼             │
                             ┌─────────────┐      │
                             │  写入缓存    │<─────┘
                             │  设置过期    │
                             └─────────────┘

五、安全架构

5.1 认证授权

5.1.1 JWT认证流程

┌─────────┐                              ┌─────────┐
│  客户端  │                              │  服务端  │
└────┬────┘                              └────┬────┘
     │                                         │
     │  1. 登录请求 (username/password)        │
     │ ──────────────────────────────────────> │
     │                                         │
     │                              ┌─────────┴─────────┐
     │                              │  验证用户名密码    │
     │                              │  生成JWT Token    │
     │                              └─────────┬─────────┘
     │                                         │
     │  2. 返回 Token                          │
     │ <────────────────────────────────────── │
     │                                         │
     │  3. 业务请求 (Header: Authorization)    │
     │ ──────────────────────────────────────> │
     │                              ┌─────────┴─────────┐
     │                              │  验证Token有效性  │
     │                              │  解析用户信息     │
     │                              │  权限校验         │
     │                              └─────────┬─────────┘
     │                                         │
     │  4. 返回业务数据                        │
     │ <────────────────────────────────────── │

5.1.2 权限模型

采用 RBAC (Role-Based Access Control) 模型:

用户 (User) ──N:M──> 角色 (Role) ──N:M──> 权限 (Permission)
                         │
                         └─N:M──> 菜单/按钮 (Menu)

5.2 数据安全

安全措施 实现方式
传输加密 HTTPS/TLS 1.3
密码加密 BCrypt (强度10)
敏感数据 AES-256 加密存储
SQL注入 MyBatis预编译语句
XSS攻击 前端转义 + 后端过滤
CSRF攻击 Token验证 + SameSite Cookie

六、部署架构

6.1 生产环境部署

┌─────────────────────────────────────────────────────────────────┐
│                         负载均衡层                               │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              Nginx (主) + Nginx (备)                     │   │
│  │              Keepalived VIP: 192.168.1.10                │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────────┐
│                         应用服务层                               │
│                                                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│  │  Gateway-1  │  │  Gateway-2  │  │  Gateway-3  │             │
│  │   :8080     │  │   :8080     │  │   :8080     │             │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘             │
│         │                │                │                     │
│  ┌──────┴────────────────┴────────────────┴──────┐              │
│  │              Nacos 集群 (3节点)                 │              │
│  │           8848 / 9848 / 7848                   │              │
│  └───────────────────────────────────────────────┘              │
│                                                                 │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐          │
│  │fund-sys  │ │fund-cust │ │fund-proj │ │fund-req  │          │
│  │  x 2     │ │  x 2     │ │  x 2     │ │  x 2     │          │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘          │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐          │
│  │fund-exp  │ │fund-rec  │ │fund-rpt  │ │fund-file │          │
│  │  x 2     │ │  x 2     │ │  x 2     │ │  x 2     │          │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────────┐
│                         数据存储层                               │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              Redis 集群 (6节点3主3从)                   │   │
│  │              6379 / 6380 / 6381                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              MySQL 主从集群                               │   │
│  │         Master: 3306 ──> Slave: 3306 (x2)               │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 容器化部署

6.2.1 Docker Compose 配置

version: '3.8'
services:
  # Nacos
  nacos:
    image: nacos/nacos-server:v3.0.0
    ports:
      - "8848:8848"
      - "9848:9848"
    environment:
      - MODE=standalone
    
  # Redis
  redis:
    image: redis:8.0.2
    ports:
      - "6379:6379"
    command: redis-server --requirepass zjf@123456
    
  # MySQL
  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=zjf@123456
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
      
  # Gateway
  gateway:
    image: fundplatform/fund-gateway:1.0.0
    ports:
      - "8080:8080"
    environment:
      - NACOS_SERVER=nacos:8848
      
  # Services
  fund-sys:
    image: fundplatform/fund-sys:1.0.0
    deploy:
      replicas: 2
    environment:
      - NACOS_SERVER=nacos:8848
      - DB_HOST=mysql
      - REDIS_HOST=redis

七、监控与运维

7.1 监控体系

监控类型 工具 监控内容
应用监控 Prometheus + Grafana JVM、接口响应、错误率
链路追踪 SkyWalking 调用链、性能瓶颈
日志收集 ELK Stack 业务日志、错误日志
告警通知 AlertManager 钉钉/邮件/短信告警

7.2 运维脚本

# 服务启动脚本
#!/bin/bash
SERVICES=("gateway" "sys" "cust" "proj" "req" "exp" "receipt" "report" "file")
for service in "${SERVICES[@]}"; do
    docker-compose up -d fund-${service}
    sleep 5
done

# 健康检查脚本
#!/bin/bash
curl -f http://localhost:8080/actuator/health || exit 1

八、开发规范

8.1 代码结构

fund-sys/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/fundplatform/sys/
│   │   │       ├── SysApplication.java
│   │   │       ├── config/          # 配置类
│   │   │       ├── controller/      # 控制器层
│   │   │       ├── service/         # 业务层
│   │   │       │   └── impl/
│   │   │       ├── mapper/          # 数据访问层
│   │   │       ├── entity/          # 实体类
│   │   │       ├── dto/             # 数据传输对象
│   │   │       ├── vo/              # 视图对象
│   │   │       ├── enums/           # 枚举类
│   │   │       ├── utils/           # 工具类
│   │   │       └── handler/         # 处理器
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-dev.yml
│   │       ├── application-prod.yml
│   │       └── mapper/              # XML映射文件
│   └── test/
├── Dockerfile
├── pom.xml
└── README.md

8.2 API 设计规范

规范项 要求
URL /api/{service}/{version}/{resource}/{action}
HTTP方法 GET(查)、POST(增)、PUT(改)、DELETE(删)
响应格式 { "code": 200, "message": "", "data": {} }
分页 pageNum, pageSize, total, list
版本 v1, v2 在URL中体现

九、附录

9.1 端口分配表

服务 端口 说明
Nginx 80/443 负载均衡
Gateway 8080 API网关
Nacos 8848/9848 注册配置中心
Redis 6379 缓存服务
MySQL 3306 数据库
XXL-JOB 8081 任务调度中心

9.2 文档修订记录

版本 修订日期 修订内容 修订人
v1.0 2026-02-13 初始版本 zhangjf
v1.1 2026-02-13 补充多租户架构(一库多租户/一库一租户)和 Head 日志追踪设计 zhangjf

文档结束