Java实现SaaS架构案例

wen java案例 4

本文目录导读:

Java实现SaaS架构案例

  1. 项目整体架构
  2. 核心代码实现
  3. 测试和验证
  4. 关键特性总结

我提供一个完整的Java实现SaaS架构案例,包含多租户管理、数据隔离和计费管理等核心功能。

项目整体架构

saas-architecture/
├── pom.xml
├── src/main/java/com/example/saas/
│   ├── SaasApplication.java
│   ├── config/
│   │   ├── TenantConfig.java
│   │   ├── DatabaseConfig.java
│   │   └── SecurityConfig.java
│   ├── tenant/
│   │   ├── TenantContext.java
│   │   ├── TenantInterceptor.java
│   │   ├── TenantResolver.java
│   │   └── Tenant.java
│   ├── security/
│   │   ├── JwtTokenProvider.java
│   │   ├── UserPrincipal.java
│   │   └── CurrentUser.java
│   ├── controller/
│   │   ├── AuthController.java
│   │   ├── UserController.java
│   │   ├── OrderController.java
│   │   └── SubscriptionController.java
│   ├── service/
│   │   ├── UserService.java
│   │   ├── OrderService.java
│   │   ├── TenantService.java
│   │   └── SubscriptionService.java
│   ├── repository/
│   │   ├── UserRepository.java
│   │   ├── OrderRepository.java
│   │   └── TenantRepository.java
│   ├── entity/
│   │   ├── User.java
│   │   ├── Order.java
│   │   └── Tenant.java
│   ├── dto/
│   │   ├── LoginRequest.java
│   │   ├── LoginResponse.java
│   │   ├── OrderRequest.java
│   │   └── TenantRegistrationRequest.java
│   └── exception/
│       ├── GlobalExceptionHandler.java
│       └── CustomException.java
└── src/main/resources/
    ├── application.yml
    └── db/
        └── migration/

核心代码实现

1 多租户配置

// TenantContext.java - 租户上下文
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
    private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<>();
    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }
    public static void setUserId(Long userId) {
        CURRENT_USER_ID.set(userId);
    }
    public static Long getUserId() {
        return CURRENT_USER_ID.get();
    }
    public static void clear() {
        CURRENT_TENANT.remove();
        CURRENT_USER_ID.remove();
    }
}
// TenantInterceptor.java - 租户拦截器
@Component
public class TenantInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 白名单路径直接放行
        String path = request.getRequestURI();
        if (path.contains("/api/auth/") || path.contains("/api/tenants/register")) {
            return true;
        }
        // 从Header获取租户信息
        String tenantId = request.getHeader("X-Tenant-ID");
        String token = request.getHeader("Authorization");
        if (tenantId == null && token != null) {
            // 从JWT中提取租户信息
            tenantId = extractTenantFromToken(token);
        }
        if (tenantId == null) {
            throw new CustomException("缺少租户信息", HttpStatus.BAD_REQUEST);
        }
        // 验证租户是否存在且有效
        if (!validateTenant(tenantId)) {
            throw new CustomException("租户不存在或已过期", HttpStatus.UNAUTHORIZED);
        }
        TenantContext.setTenantId(tenantId);
        // 动态切换数据源
        DynamicDataSource.setCurrentDataSource(tenantId);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        TenantContext.clear();
        DynamicDataSource.clear();
    }
}
// TenantConfig.java - 租户配置
@Configuration
@ConfigurationProperties(prefix = "saas")
public class TenantConfig {
    private List<TenantProperties> tenants = new ArrayList<>();
    private int maxTenants;
    private boolean allowSelfRegistration;
    @Data
    public static class TenantProperties {
        private String id;
        private String name;
        private String database;
        private String schema;
        private String status;
        private Integer userLimit;
        private Integer storageLimit;
    }
    public Optional<TenantProperties> getTenant(String tenantId) {
        return tenants.stream()
            .filter(t -> t.getId().equals(tenantId))
            .findFirst();
    }
}

2 动态数据源实现

// DynamicDataSource.java - 动态数据源
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();
    @Autowired
    private DataSourceConfig dataSourceConfig;
    @Override
    protected Object determineCurrentLookupKey() {
        return CURRENT_DATA_SOURCE.get();
    }
    public static void setCurrentDataSource(String tenantId) {
        CURRENT_DATA_SOURCE.set("tenant_" + tenantId);
    }
    public static void clear() {
        CURRENT_DATA_SOURCE.remove();
    }
    @Override
    public void afterPropertiesSet() {
        // 初始化默认数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("default", dataSourceConfig.getDefaultDataSource());
        // 动态注册租户数据源
        for (Tenant tenant : tenantRepository.findAll()) {
            DataSource dataSource = dataSourceConfig.createDataSource(tenant);
            targetDataSources.put("tenant_" + tenant.getId(), dataSource);
        }
        this.setDefaultTargetDataSource(dataSourceConfig.getDefaultDataSource());
        this.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
}
// DatabaseConfig.java - 数据库配置
@Configuration
public class DatabaseConfig {
    @Bean
    public DataSource defaultDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/saas_platform");
        config.setUsername("saas_admin");
        config.setPassword("secret");
        config.setMaximumPoolSize(20);
        config.setConnectionTimeout(30000);
        return new HikariDataSource(config);
    }
    public DataSource createDataSource(Tenant tenant) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(tenant.getDatabaseUrl());
        config.setUsername(tenant.getDatabaseUsername());
        config.setPassword(tenant.getDatabasePassword());
        config.setMaximumPoolSize(10);
        config.setConnectionTimeout(30000);
        // 设置连接池名称便于监控
        config.setPoolName("tenant-" + tenant.getId());
        return new HikariDataSource(config);
    }
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource() {
        return new DynamicDataSource();
    }
    @Bean
    @Primary
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

3 实体类设计

// Tenant.java
@Entity
@Table(name = "tenants")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Tenant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    private String tenantId;
    @Column(nullable = false)
    private String name;
    @Column(nullable = false)
    private String plan;
    @Column(nullable = false)
    private String status;
    private String databaseUrl;
    private String databaseUsername;
    private String databasePassword;
    @Column(nullable = false)
    private Integer maxUsers;
    @Column(nullable = false)
    private Integer maxStorage;
    @Column(nullable = false)
    private Integer currentUsers;
    private LocalDateTime expiryDate;
    private LocalDateTime createdAt;
    @OneToMany(mappedBy = "tenant", cascade = CascadeType.ALL)
    private List<TenantUser> users;
}
// User.java
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    private String username;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String fullName;
    @Column(nullable = false)
    private String role;
    private boolean enabled;
    private LocalDateTime lastLogin;
    // 租户级应用
    @Column(name = "tenant_id")
    private String tenantId;
    @Version
    private Integer version;
}
// Order.java - 业务实体
@Entity
@Table(name = "orders")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String orderNumber;
    @Column(nullable = false)
    private Long customerId;
    @Column(nullable = false)
    private BigDecimal totalAmount;
    @Column(nullable = false)
    private String status;
    private String tenantId;
    @CreatedDate
    private LocalDateTime createdDate;
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> items;
}

4 多租户Repository实现

// TenantAwareRepository.java - 租户感知的Repository基类
@NoRepositoryBean
public interface TenantAwareRepository<T, ID> extends JpaRepository<T, ID> {
    // 自动注入租户过滤条件
    @Query("select e from #{#entityName} e where e.tenantId = :tenantId")
    List<T> findByTenantId(@Param("tenantId") String tenantId);
    // 带分页查询
    @Query("select e from #{#entityName} e where e.tenantId = :tenantId")
    Page<T> findByTenantId(@Param("tenantId") String tenantId, Pageable pageable);
    // 计数
    @Query("select count(e) from #{#entityName} e where e.tenantId = :tenantId")
    long countByTenantId(@Param("tenantId") String tenantId);
}
// UserRepository.java
@Repository
public interface UserRepository extends TenantAwareRepository<User, Long> {
    Optional<User> findByUsernameAndTenantId(String username, String tenantId);
    Optional<User> findByEmailAndTenantId(String email, String tenantId);
    List<User> findByTenantIdAndRole(String tenantId, String role);
    @Query("select u from User u where u.tenantId = :tenantId and " +
           "(u.username like %:keyword% or u.fullName like %:keyword%)")
    Page<User> searchUsers(@Param("tenantId") String tenantId, 
                           @Param("keyword") String keyword, 
                           Pageable pageable);
}
// OrderRepository.java
@Repository
public interface OrderRepository extends TenantAwareRepository<Order, Long> {
    Optional<Order> findByOrderNumberAndTenantId(String orderNumber, String tenantId);
    List<Order> findByCustomerIdAndTenantId(Long customerId, String tenantId);
    @Query("select sum(o.totalAmount) from Order o where o.tenantId = :tenantId and " +
           "o.status = :status")
    BigDecimal sumTotalAmountByStatus(@Param("tenantId") String tenantId, 
                                      @Param("status") String status);
}

5 服务层实现

// TenantService.java
@Service
@Slf4j
public class TenantService {
    @Autowired
    private TenantRepository tenantRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TenantManagementClient tenantManagementClient;
    public Tenant registerTenant(TenantRegistrationRequest request) {
        // 检查租户ID是否唯一
        if (tenantRepository.existsByTenantId(request.getTenantId())) {
            throw new CustomException("租户ID已存在", HttpStatus.CONFLICT);
        }
        // 创建租户
        Tenant tenant = Tenant.builder()
            .tenantId(request.getTenantId())
            .name(request.getCompanyName())
            .plan(request.getPlan() != null ? request.getPlan() : "free")
            .status("active")
            .maxUsers(calculateMaxUsers(request.getPlan()))
            .maxStorage(calculateMaxStorage(request.getPlan()))
            .currentUsers(0)
            .expiryDate(LocalDateTime.now().plusDays(30))
            .createdAt(LocalDateTime.now())
            .build();
        // 创建数据库和用户
        tenantManagementClient.createTenantDatabase(tenant);
        // 创建默认管理员
        createDefaultAdmin(tenant, request);
        // 保存租户信息
        tenant = tenantRepository.save(tenant);
        log.info("New tenant registered: {}", tenant.getTenantId());
        return tenant;
    }
    private void createDefaultAdmin(Tenant tenant, TenantRegistrationRequest request) {
        // 在独立事务中执行,避免影响主租户
        TenantContext.setTenantId(tenant.getTenantId());
        User admin = User.builder()
            .username("admin")
            .password(passwordEncoder.encode(request.getAdminPassword()))
            .email(request.getAdminEmail())
            .fullName("System Administrator")
            .role("ROLE_ADMIN")
            .tenantId(tenant.getTenantId())
            .enabled(true)
            .build();
        try {
            userRepository.save(admin);
        } finally {
            TenantContext.clear();
        }
    }
    private int calculateMaxUsers(String plan) {
        switch (plan) {
            case "free": return 10;
            case "basic": return 50;
            case "pro": return 200;
            case "enterprise": return Integer.MAX_VALUE;
            default: return 10;
        }
    }
}
// UserService.java
@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TenantLimiterService tenantLimiterService;
    public User createUser(RegisterUserRequest request) {
        String tenantId = TenantContext.getTenantId();
        // 检查租户用户数限制
        tenantLimiterService.checkUserLimit(tenantId);
        // 检查用户名唯一性
        if (userRepository.findByUsernameAndTenantId(request.getUsername(), tenantId).isPresent()) {
            throw new CustomException("用户名已存在", HttpStatus.CONFLICT);
        }
        User user = User.builder()
            .username(request.getUsername())
            .password(passwordEncoder.encode(request.getPassword()))
            .email(request.getEmail())
            .fullName(request.getFullName())
            .role("ROLE_USER")
            .tenantId(tenantId)
            .enabled(true)
            .build();
        user = userRepository.save(user);
        // 更新租户用户计数
        tenantLimiterService.updateUserCount(tenantId, 1);
        log.info("User created: {} in tenant: {}", user.getUsername(), tenantId);
        return user;
    }
    public Page<User> getUsers(int page, int size) {
        String tenantId = TenantContext.getTenantId();
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdDate").descending());
        return userRepository.findByTenantId(tenantId, pageable);
    }
    public void deleteUser(Long userId) {
        String tenantId = TenantContext.getTenantId();
        User user = userRepository.findByIdAndTenantId(userId, tenantId)
            .orElseThrow(() -> new CustomException("用户不存在", HttpStatus.NOT_FOUND));
        userRepository.delete(user);
        // 更新租户用户计数
        tenantLimiterService.updateUserCount(tenantId, -1);
    }
}
// OrderService.java
@Service
@Slf4j
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private OrderItemRepository orderItemRepository;
    @Autowired
    private TenantLimiterService tenantLimiterService;
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        String tenantId = TenantContext.getTenantId();
        Long userId = TenantContext.getUserId();
        // 检查租户限额
        tenantLimiterService.checkOrderLimit(tenantId);
        // 创建订单
        Order order = Order.builder()
            .orderNumber(generateOrderNumber())
            .customerId(request.getCustomerId())
            .totalAmount(BigDecimal.ZERO)
            .status("PENDING")
            .tenantId(tenantId)
            .build();
        order = orderRepository.save(order);
        // 添加订单项
        BigDecimal total = BigDecimal.ZERO;
        for (OrderItemRequest itemRequest : request.getItems()) {
            OrderItem item = OrderItem.builder()
                .orderId(order.getId())
                .productId(itemRequest.getProductId())
                .quantity(itemRequest.getQuantity())
                .price(itemRequest.getPrice())
                .tenantId(tenantId)
                .build();
            item = orderItemRepository.save(item);
            total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
            order.getItems().add(item);
        }
        // 更新订单总额
        order.setTotalAmount(total);
        order = orderRepository.save(order);
        log.info("Order created: {} in tenant: {}", order.getOrderNumber(), tenantId);
        return order;
    }
    @Transactional
    public void processOrder(Long orderId) {
        String tenantId = TenantContext.getTenantId();
        Order order = orderRepository.findByIdAndTenantId(orderId, tenantId)
            .orElseThrow(() -> new CustomException("订单不存在", HttpStatus.NOT_FOUND));
        // 处理订单逻辑
        order.setStatus("PROCESSING");
        // 发送消息队列通知
        kafkaTemplate.send("order-processing", order.getId());
        orderRepository.save(order);
    }
}

6 租户限额和计费管理

// TenantLimiterService.java
@Service
public class TenantLimiterService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private TenantRepository tenantRepository;
    private final Map<String, Map<String, Integer>> quotas = new ConcurrentHashMap<>();
    public void checkUserLimit(String tenantId) {
        // 查询缓存
        String key = "tenant:" + tenantId + ":users";
        String count = redisTemplate.opsForValue().get(key);
        if (count != null) {
            int currentCount = Integer.parseInt(count);
            Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
            if (currentCount >= tenant.getMaxUsers()) {
                throw new CustomException("达到租户用户数上限", HttpStatus.BAD_REQUEST);
            }
        }
    }
    public void updateUserCount(String tenantId, int delta) {
        String key = "tenant:" + tenantId + ":users";
        redisTemplate.opsForValue().increment(key, delta);
    }
    public void checkStorage(String tenantId, long fileSize) {
        Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
        long currentUsage = getStorageUsage(tenantId);
        long newUsage = currentUsage + fileSize;
        if (newUsage > tenant.getMaxStorage() * 1024L * 1024L) {
            throw new CustomException("超出存储配额", HttpStatus.BAD_REQUEST);
        }
    }
    public void checkOrderLimit(String tenantId) {
        // 检查每日订单数
        String today = LocalDate.now().toString();
        String key = "tenant:" + tenantId + ":orders:" + today;
        long count = redisTemplate.opsForValue().increment(key);
        if (count > getDailyOrderLimit(tenantId)) {
            throw new CustomException("超出每日订单限额", HttpStatus.BAD_REQUEST);
        }
        // 设置过期时间(24小时后)
        redisTemplate.expire(key, 24, TimeUnit.HOURS);
    }
    private int getDailyOrderLimit(String tenantId) {
        Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
        switch (tenant.getPlan()) {
            case "free": return 100;
            case "basic": return 1000;
            case "pro": return 10000;
            case "enterprise": return Integer.MAX_VALUE;
            default: return 100;
        }
    }
}
// SubscriptionService.java - 订阅管理
@Service
@Slf4j
public class SubscriptionService {
    @Autowired
    private TenantRepository tenantRepository;
    @Autowired
    private PaymentService paymentService;
    public SubscriptionPlan upgradePlan(String tenantId, NewPlanRequest request) {
        Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
        // 计算费用
        BigDecimal amount = calculateUpgradeCost(tenant.getPlan(), request.getNewPlan());
        // 处理支付
        PaymentResult payment = paymentService.processPayment(tenant, amount, request.getPaymentMethod());
        // 更新租户计划
        tenant.setPlan(request.getNewPlan());
        tenant.setMaxUsers(calculateMaxUsers(request.getNewPlan()));
        tenantRepository.save(tenant);
        // 更新配额
        updateQuotas(tenant);
        log.info("Tenant {} upgraded to plan: {}", tenantId, request.getNewPlan());
        return SubscriptionPlan.builder()
            .tenantId(tenantId)
            .plan(request.getNewPlan())
            .amount(amount)
            .effectiveDate(LocalDateTime.now())
            .build();
    }
    public void renewSubscription(String tenantId) {
        Tenant tenant = tenantRepository.findByTenantId(tenantId).orElseThrow();
        BigDecimal amount = calculateRenewalCost(tenant.getPlan());
        PaymentResult payment = paymentService.processPayment(tenant, amount, null);
        // 续期30天
        tenant.setExpiryDate(tenant.getExpiryDate().plusDays(30));
        tenantRepository.save(tenant);
    }
}

7 安全配置

// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private RestAuthenticationEntryPoint authenticationEntryPoint;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .cors().and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/tenants/register").permitAll()
                .anyRequest().authenticated()
            .and()
            .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint);
        // 添加JWT过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
                           UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
// JwtTokenProvider.java
@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String jwtSecret;
    @Value("${jwt.expiration}")
    private Long jwtExpiration;
    public String generateToken(UserPrincipal userPrincipal) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        return Jwts.builder()
            .setSubject(userPrincipal.getUsername())
            .claim("userId", userPrincipal.getId())
            .claim("tenantId", userPrincipal.getTenantId())
            .claim("role", userPrincipal.getRole())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        return Long.parseLong(claims.get("userId").toString());
    }
    public String getTenantFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        return claims.get("tenantId").toString();
    }
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new CustomException("无效的JWT令牌", HttpStatus.UNAUTHORIZED);
        }
    }
}

8 Controller层

// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtTokenProvider tokenProvider;
    @Autowired
    private UserService userService;
    @PostMapping("/login")
    public LoginResponse login(@Valid @RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        String token = tokenProvider.generateToken(userPrincipal);
        TenantContext.setTenantId(userPrincipal.getTenantId());
        TenantContext.setUserId(userPrincipal.getId());
        return LoginResponse.builder()
            .token(token)
            .tokenType("Bearer")
            .expiresIn(3600)
            .user(generateUserInfo(userPrincipal))
            .build();
    }
    @PostMapping("/refresh")
    public TokenResponse refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
        // token刷新逻辑
    }
}
// OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    @Autowired
    private OrderService orderService;
    @PostMapping
    @PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
    public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(order);
    }
    @GetMapping("/{orderId}")
    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER', 'USER')")
    public Order getOrder(@PathVariable Long orderId) {
        return orderService.getOrder(orderId);
    }
    @GetMapping
    public Page<Order> getOrders(
            @RequestParam(required = false) String status,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return orderService.getOrders(status, page, size);
    }
    @PatchMapping("/{orderId}/cancel")
    @PreAuthorize("hasRole('ADMIN')")
    public Order cancelOrder(@PathVariable Long orderId) {
        return orderService.cancelOrder(orderId);
    }
}

9 异常处理和配置文件

// GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiError handleValidationException(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return new ApiError(HttpStatus.BAD_REQUEST.value(), "Validation failed", errors);
    }
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ApiError> handleCustomException(CustomException ex) {
        return ResponseEntity.status(ex.getStatus())
            .body(new ApiError(ex.getStatus().value(), ex.getMessage()));
    }
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiError handleGenericException(Exception ex) {
        log.error("Unexpected error", ex);
        return new ApiError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal server error");
    }
}

10 配置文件

# application.yml
spring:
  application:
    name: saas-architecture
  datasource:
    platform-url: jdbc:mysql://localhost:3306/saas_platform
    platform-username: saas_admin
    platform-password: ${PLATFORM_DB_PASSWORD}
  redis:
    host: localhost
    port: 6379
    timeout: 5000
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: saas-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
saas:
  allow-self-registration: true
  max-tenants: 100
  tenants:
    - id: t001
      name: "Tenant A"
      database: saas_tenant_a
      status: active
    - id: t002
      name: "Tenant B"
      database: saas_tenant_b
      status: active
jwt:
  secret: ${JWT_SECRET:your-secret-key-should-be-long-and-secure}
  expiration: 86400000  # 24小时
server:
  port: 8080
  servlet:
    context-path: /api
logging:
  level:
    com.example.saas: DEBUG

测试和验证

// 集成测试示例
@SpringBootTest
@AutoConfigureMockMvc
public class SaasServiceTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @Test
    public void testTenantRegistration() throws Exception {
        // 1. 租户注册
        TenantRegistrationRequest request = TenantRegistrationRequest.builder()
            .tenantId("test-tenant")
            .companyName("Test Company")
            .adminEmail("admin@test.com")
            .adminPassword("password123")
            .plan("basic")
            .build();
        mockMvc.perform(post("/api/tenants/register")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isOk());
        // 2. 租户登录
        LoginRequest loginRequest = LoginRequest.builder()
            .username("admin")
            .password("password123")
            .build();
        mockMvc.perform(post("/api/auth/login")
                .header("X-Tenant-ID", "test-tenant")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(loginRequest)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.token").exists());
    }
    @Test
    public void testMultiTenantDataIsolation() throws Exception {
        // 验证不同租户之间的数据隔离
        String tenantA = "tenant-a";
        String tenantB = "tenant-b";
        // 创建两个租户的数据
        createOrder(tenantA, "ORDER-001");
        createOrder(tenantB, "ORDER-002");
        // 查询租户A的订单,不应包含租户B的数据
        mockMvc.perform(get("/api/orders")
                .header("Authorization", getToken(tenantA))
                .header("X-Tenant-ID", tenantA))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content[0].orderNumber").value("ORDER-001"))
            .andExpect(jsonPath("$.content", hasSize(1)));
    }
}

关键特性总结

  1. 多租户架构:支持数据库级隔离,每个租户独立数据库
  2. 动态数据源:根据租户上下文动态切换数据库连接
  3. 安全认证:JWT+租户上下文双重验证
  4. 租户管理:完整的租户生命周期管理
  5. 限额控制:基于Redis的实时配额管理
  6. 计费管理:支持订阅计划升级和续费
  7. 数据隔离:确保租户间数据安全隔离
  8. 可扩展性:支持横向扩展和性能优化

这个架构实现了完整的SaaS多租户支持,能够满足大多数企业级SaaS应用的需求,可以通过增加更多功能模块、优化性能参数来根据实际业务情况进行调整。

抱歉,评论功能暂时关闭!