Java RESTful接口开发:从入门到精通

在这里插入图片描述

文章目录


在这里插入图片描述

一、为什么选择Spring Boot:极速开发的秘密

作为.NET转Java的开发者,您会发现Spring Boot与ASP.NET Core在理念上惊人的相似,但Spring Boot的“约定优于配置”哲学将其发挥到极致。Spring Boot可以:

  • 30秒内启动一个可运行的REST API
  • 通过自动配置减少80%的配置工作
  • 提供生产就绪的功能(监控、健康检查、指标)
  • 拥有全球最大的Java生态支持

二、极速启动:三步创建第一个REST接口

2.1 项目初始化

使用Spring Initializr(start.spring.io)或IDE内置工具创建项目,只需选择:

  • Spring Web:RESTful支持
  • Spring Data JPA:数据库操作(可选)
  • Validation:数据验证(可选)

2.2 基础代码示例

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@Valid @RequestBody UserDTO userDTO) {
        return userService.create(userDTO);
    }
    
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, 
                          @Valid @RequestBody UserDTO userDTO) {
        return userService.update(id, userDTO);
    }
    
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

三、Spring Boot RESTful核心详解

3.1 控制器层最佳实践

3.1.1 RESTful资源设计原则
// 好的RESTful设计示例
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    // GET /api/v1/orders - 获取所有订单
    @GetMapping
    public List<Order> getAllOrders(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size) {
        return orderService.getOrders(page, size);
    }
    
    // GET /api/v1/orders/{id} - 获取特定订单
    @GetMapping("/{id}")
    public Order getOrderById(@PathVariable Long id) {
        return orderService.getById(id);
    }
    
    // GET /api/v1/orders/{id}/items - 获取订单项(子资源)
    @GetMapping("/{id}/items")
    public List<OrderItem> getOrderItems(@PathVariable Long id) {
        return orderService.getOrderItems(id);
    }
    
    // POST /api/v1/orders - 创建订单
    @PostMapping
    public ResponseEntity<Order> createOrder(@Valid @RequestBody OrderDTO dto) {
        Order created = orderService.create(dto);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }
}
3.1.2 高级请求处理技巧
// 1. 多条件查询参数处理
@GetMapping("/search")
public List<User> searchUsers(
    @RequestParam(required = false) String name,
    @RequestParam(required = false) String email,
    @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
    @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
    @RequestParam(defaultValue = "createdAt") String sortBy,
    @RequestParam(defaultValue = "desc") String direction) {
    
    Specification<User> spec = UserSpecifications.search(name, email, startDate, endDate);
    Sort sort = direction.equalsIgnoreCase("desc") 
        ? Sort.by(sortBy).descending() 
        : Sort.by(sortBy).ascending();
    
    return userService.search(spec, sort);
}

// 2. 文件上传处理
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
    @RequestParam("file") MultipartFile file,
    @RequestParam("category") String category) {
    
    if (file.isEmpty()) {
        throw new BadRequestException("文件不能为空");
    }
    
    String filePath = fileStorageService.store(file, category);
    return ResponseEntity.ok("文件上传成功: " + filePath);
}

// 3. 请求/响应体压缩
@PostMapping("/compress")
public ResponseEntity<byte[]> handleCompressedData(
    @RequestBody byte[] compressedData) throws IOException {
    
    byte[] decompressed = CompressionUtils.decompress(compressedData);
    // 处理数据...
    
    byte[] responseData = "处理成功".getBytes();
    byte[] compressedResponse = CompressionUtils.compress(responseData);
    
    return ResponseEntity.ok()
        .header("Content-Encoding", "gzip")
        .body(compressedResponse);
}

3.2 服务层设计与实现

3.2.1 服务层架构模式
// 1. 基础服务接口
public interface UserService {
    UserDTO createUser(UserCreateDTO dto);
    UserDTO updateUser(Long id, UserUpdateDTO dto);
    UserDTO getUserById(Long id);
    Page<UserDTO> getUsers(UserQueryDTO query, Pageable pageable);
    void deleteUser(Long id);
}

// 2. 服务实现(使用@Transactional)
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;
    private final UserMapper userMapper;
    private final CacheService cacheService;
    private final EmailService emailService;
    
    // 构造器注入(推荐方式)
    public UserServiceImpl(UserRepository userRepository,
                          UserMapper userMapper,
                          CacheService cacheService,
                          EmailService emailService) {
        this.userRepository = userRepository;
        this.userMapper = userMapper;
        this.cacheService = cacheService;
        this.emailService = emailService;
    }
    
    @Override
    public UserDTO createUser(UserCreateDTO dto) {
        log.info("创建用户: {}", dto.getEmail());
        
        // 验证逻辑
        if (userRepository.existsByEmail(dto.getEmail())) {
            throw new BusinessException("邮箱已存在");
        }
        
        // DTO转实体
        User user = userMapper.toEntity(dto);
        user.setStatus(UserStatus.ACTIVE);
        user.setCreatedAt(LocalDateTime.now());
        
        // 保存到数据库
        User savedUser = userRepository.save(user);
        
        // 清理缓存
        cacheService.evict("users", savedUser.getId());
        
        // 发送欢迎邮件(异步)
        emailService.sendWelcomeEmail(savedUser.getEmail());
        
        // 返回DTO
        return userMapper.toDTO(savedUser);
    }
    
    @Override
    @Transactional(readOnly = true)
    public UserDTO getUserById(Long id) {
        // 先尝试从缓存获取
        String cacheKey = "user:" + id;
        UserDTO cached = cacheService.get(cacheKey, UserDTO.class);
        
        if (cached != null) {
            return cached;
        }
        
        // 缓存未命中,查询数据库
        User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("用户不存在"));
        
        UserDTO dto = userMapper.toDTO(user);
        
        // 放入缓存
        cacheService.put(cacheKey, dto, Duration.ofMinutes(30));
        
        return dto;
    }
    
    @Override
    @Transactional(readOnly = true)
    public Page<UserDTO> getUsers(UserQueryDTO query, Pageable pageable) {
        // 构建查询条件
        Specification<User> spec = buildSpecification(query);
        
        // 执行分页查询
        Page<User> userPage = userRepository.findAll(spec, pageable);
        
        // 转换为DTO
        return userPage.map(userMapper::toDTO);
    }
    
    private Specification<User> buildSpecification(UserQueryDTO query) {
        return (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            if (StringUtils.hasText(query.getName())) {
                predicates.add(criteriaBuilder.like(
                    root.get("name"), "%" + query.getName() + "%"
                ));
            }
            
            if (query.getStatus() != null) {
                predicates.add(criteriaBuilder.equal(
                    root.get("status"), query.getStatus()
                ));
            }
            
            if (query.getStartDate() != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(
                    root.get("createdAt"), query.getStartDate()
                ));
            }
            
            if (query.getEndDate() != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(
                    root.get("createdAt"), query.getEndDate()
                ));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}
3.2.2 业务逻辑与事务管理
// 复杂业务事务管理示例
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final NotificationService notificationService;
    
    /**
     * 创建订单的复杂业务流程
     * 使用@Transactional管理事务边界
     */
    @Transactional(rollbackFor = Exception.class)
    public OrderDTO createOrder(OrderCreateDTO dto) {
        // 1. 验证库存
        inventoryService.checkStock(dto.getItems());
        
        // 2. 扣减库存(独立事务)
        inventoryService.deductStock(dto.getItems());
        
        try {
            // 3. 创建订单
            Order order = createOrderEntity(dto);
            order = orderRepository.save(order);
            
            // 4. 调用支付(外部系统,需要补偿机制)
            paymentService.processPayment(order);
            
            // 5. 更新订单状态
            order.setStatus(OrderStatus.PAID);
            orderRepository.save(order);
            
            // 6. 发送通知(异步,不影响主事务)
            notificationService.sendOrderConfirmation(order);
            
            return convertToDTO(order);
            
        } catch (PaymentException e) {
            // 支付失败,恢复库存
            inventoryService.restoreStock(dto.getItems());
            throw new BusinessException("支付失败: " + e.getMessage());
        }
    }
    
    /**
     * 使用编程式事务处理特殊场景
     */
    public void batchUpdateOrderStatus(List<Long> orderIds, OrderStatus status) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        
        transactionTemplate.execute(status -> {
            for (Long orderId : orderIds) {
                Order order = orderRepository.findById(orderId)
                    .orElseThrow(() -> new ResourceNotFoundException("订单不存在"));
                
                // 记录状态变更历史
                order.addStatusHistory(order.getStatus(), status, "批量更新");
                
                // 更新状态
                order.setStatus(status);
                orderRepository.save(order);
                
                // 每处理100条记录提交一次,防止事务过大
                if (orderIds.indexOf(orderId) % 100 == 0) {
                    entityManager.flush();
                    entityManager.clear();
                }
            }
            return null;
        });
    }
}

3.3 数据传输对象设计

3.3.1 DTO模式实现
// 1. 请求DTO(验证注解)
@Data
public class UserCreateDTO {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$", 
             message = "密码必须至少8位,包含字母和数字")
    private String password;
    
    @NotNull(message = "角色不能为空")
    private UserRole role;
    
    @PhoneNumber // 自定义验证注解
    private String phone;
}

// 2. 响应DTO(嵌套对象)
@Data
@Builder
public class UserDetailDTO {
    private Long id;
    private String username;
    private String email;
    private UserStatus status;
    private LocalDateTime createdAt;
    private List<UserAddressDTO> addresses;
    private UserProfileDTO profile;
}

// 3. 查询参数DTO
@Data
public class UserQueryDTO {
    private String username;
    private String email;
    private UserStatus status;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    private String sortField;
    private Sort.Direction sortDirection;
    
    public Pageable toPageable() {
        if (sortField != null && sortDirection != null) {
            return PageRequest.of(0, 20, Sort.by(sortDirection, sortField));
        }
        return PageRequest.of(0, 20);
    }
}
3.3.2 MapStruct映射器
// 1. 映射器接口
@Mapper(componentModel = "spring", 
        uses = {AddressMapper.class, ProfileMapper.class},
        unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
    
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    
    // 简单映射
    User toEntity(UserCreateDTO dto);
    UserDTO toDTO(User entity);
    
    // 更新映射(忽略空值)
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void updateEntity(UserUpdateDTO dto, @MappingTarget User entity);
    
    // 列表映射
    List<UserDTO> toDTOList(List<User> entities);
    
    // 分页映射
    default Page<UserDTO> toDTOPage(Page<User> page) {
        return page.map(this::toDTO);
    }
    
    // 自定义映射方法
    @AfterMapping
    default void afterMapping(UserCreateDTO dto, @MappingTarget User user) {
        if (user.getPassword() != null) {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
        }
        user.setCreatedAt(LocalDateTime.now());
    }
}

// 2. 复杂映射配置
@Mapper(componentModel = "spring")
public interface OrderMapper {
    
    @Mapping(source = "customer.id", target = "customerId")
    @Mapping(source = "customer.name", target = "customerName")
    @Mapping(source = "items", target = "orderItems")
    @Mapping(target = "totalAmount", expression = "java(calculateTotal(order))")
    OrderDTO toDTO(Order order);
    
    default BigDecimal calculateTotal(Order order) {
        return order.getItems().stream()
            .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

四、全局处理与高级特性

4.1 全局异常处理机制

4.1.1 统一异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // 处理验证异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());
        
        ErrorResponse response = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.BAD_REQUEST.value())
            .error("验证失败")
            .message("请求参数无效")
            .errors(errors)
            .path(getRequestPath())
            .build();
        
        return ResponseEntity.badRequest().body(response);
    }
    
    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
            BusinessException ex, HttpServletRequest request) {
        
        ErrorResponse response = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.CONFLICT.value())
            .error("业务错误")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
    }
    
    // 处理资源不存在异常
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFoundException(
            ResourceNotFoundException ex, HttpServletRequest request) {
        
        ErrorResponse response = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.NOT_FOUND.value())
            .error("资源未找到")
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
    
    // 处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(
            Exception ex, HttpServletRequest request) {
        
        log.error("未处理的异常: ", ex);
        
        ErrorResponse response = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error("服务器内部错误")
            .message("系统繁忙,请稍后重试")
            .path(request.getRequestURI())
            .build();
        
        return ResponseEntity.internalServerError().body(response);
    }
    
    private String getRequestPath() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes instanceof ServletRequestAttributes) {
            return ((ServletRequestAttributes) attributes).getRequest().getRequestURI();
        }
        return null;
    }
}

// 错误响应DTO
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private List<String> errors;
    private String path;
}
4.1.2 自定义异常体系
// 基础业务异常
public abstract class BaseException extends RuntimeException {
    private final String code;
    private final Map<String, Object> data;
    
    public BaseException(String code, String message) {
        super(message);
        this.code = code;
        this.data = new HashMap<>();
    }
    
    public BaseException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.data = new HashMap<>();
    }
    
    public BaseException withData(String key, Object value) {
        this.data.put(key, value);
        return this;
    }
    
    public abstract HttpStatus getHttpStatus();
}

// 具体业务异常
public class BusinessException extends BaseException {
    public BusinessException(String message) {
        super("BUSINESS_ERROR", message);
    }
    
    public BusinessException(String code, String message) {
        super(code, message);
    }
    
    @Override
    public HttpStatus getHttpStatus() {
        return HttpStatus.CONFLICT;
    }
}

public class ValidationException extends BaseException {
    public ValidationException(String message) {
        super("VALIDATION_ERROR", message);
    }
    
    @Override
    public HttpStatus getHttpStatus() {
        return HttpStatus.BAD_REQUEST;
    }
}

public class AuthenticationException extends BaseException {
    public AuthenticationException(String message) {
        super("AUTH_ERROR", message);
    }
    
    @Override
    public HttpStatus getHttpStatus() {
        return HttpStatus.UNAUTHORIZED;
    }
}

4.2 数据验证高级技巧

4.2.1 自定义验证注解
// 1. 自定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
    String message() default "手机号码格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String region() default "CN"; // 支持不同地区
}

// 2. 验证器实现
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    
    private String region;
    private static final Map<String, Pattern> REGION_PATTERNS = new HashMap<>();
    
    static {
        // 中国大陆手机号
        REGION_PATTERNS.put("CN", Pattern.compile("^1[3-9]\\d{9}$"));
        // 美国手机号
        REGION_PATTERNS.put("US", Pattern.compile("^\\(?([0-9]{3})\\)?[-.\\s]?([0-9]{3})[-.\\s]?([0-9]{4})$"));
        // 香港手机号
        REGION_PATTERNS.put("HK", Pattern.compile("^[569]\\d{3}\\d{4}$"));
    }
    
    @Override
    public void initialize(PhoneNumber constraintAnnotation) {
        this.region = constraintAnnotation.region();
    }
    
    @Override
    public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
        if (phoneNumber == null) {
            return true; // 使用@NotNull处理空值
        }
        
        Pattern pattern = REGION_PATTERNS.get(region);
        if (pattern == null) {
            throw new IllegalArgumentException("不支持的地区: " + region);
        }
        
        return pattern.matcher(phoneNumber).matches();
    }
}

// 3. 使用自定义注解
@Data
public class ContactDTO {
    @NotBlank(message = "姓名不能为空")
    private String name;
    
    @PhoneNumber(region = "CN", message = "请输入有效的中国大陆手机号")
    private String phone;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    // 跨字段验证
    @AssertTrue(message = "至少提供一种联系方式")
    public boolean isContactInfoProvided() {
        return StringUtils.hasText(phone) || StringUtils.hasText(email);
    }
}
4.2.2 分组验证
// 1. 定义验证组
public interface ValidationGroups {
    interface Create {}
    interface Update {}
    interface Patch {}
}

// 2. 在DTO中使用分组
@Data
public class UserDTO {
    
    @Null(groups = Create.class, message = "ID必须为空")
    @NotNull(groups = {Update.class, Patch.class}, message = "ID不能为空")
    private Long id;
    
    @NotBlank(groups = Create.class, message = "用户名不能为空")
    @Size(min = 3, max = 50, groups = {Create.class, Update.class})
    private String username;
    
    @Email(groups = {Create.class, Update.class})
    private String email;
    
    @NotBlank(groups = Create.class, message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).{8,}$", 
             groups = Create.class)
    private String password;
    
    @NotNull(groups = Create.class)
    private UserRole role;
}

// 3. 在控制器中使用分组验证
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<UserDTO> createUser(
            @Validated(ValidationGroups.Create.class) 
            @RequestBody UserDTO userDTO) {
        // 创建逻辑
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(
            @PathVariable Long id,
            @Validated(ValidationGroups.Update.class)
            @RequestBody UserDTO userDTO) {
        // 更新逻辑
    }
    
    @PatchMapping("/{id}")
    public ResponseEntity<UserDTO> patchUser(
            @PathVariable Long id,
            @Validated(ValidationGroups.Patch.class)
            @RequestBody UserDTO userDTO) {
        // 部分更新逻辑
    }
}

五、安全与认证授权

5.1 Spring Security集成

5.1.1 安全配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final UserDetailsService userDetailsService;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用CSRF(REST API通常不需要)
            .csrf().disable()
            
            // 会话管理(无状态)
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            
            // 异常处理
            .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
                .and()
            
            // 授权配置
            .authorizeHttpRequests(auth -> auth
                // 公开接口
                .requestMatchers(
                    "/api/auth/**",
                    "/swagger-ui/**",
                    "/v3/api-docs/**",
                    "/actuator/health"
                ).permitAll()
                
                // 需要认证的接口
                .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                
                // 其他接口需要认证
                .anyRequest().authenticated()
            )
            
            // 添加JWT过滤器
            .addFilterBefore(jwtAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class)
            
            // 记住我功能(可选)
            .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(86400) // 24小时
                .and()
            
            // 安全头配置
            .headers(headers -> headers
                .contentSecurityPolicy("default-src 'self'")
                .frameOptions().sameOrigin()
            );
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }
}
5.1.2 JWT认证实现
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        try {
            // 从请求头获取Token
            String token = extractToken(request);
            
            if (token != null && tokenProvider.validateToken(token)) {
                // 从Token中获取用户名
                String username = tokenProvider.getUsernameFromToken(token);
                
                // 加载用户详情
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 创建认证对象
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                
                // 设置详情
                authentication.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request));
                
                // 设置安全上下文
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("无法设置用户认证", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

@Component
public class JwtTokenProvider {
    
    @Value("${app.jwt.secret}")
    private String secret;
    
    @Value("${app.jwt.expiration}")
    private Long expiration;
    
    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (SignatureException ex) {
            logger.error("无效的JWT签名");
        } catch (MalformedJwtException ex) {
            logger.error("无效的JWT令牌");
        } catch (ExpiredJwtException ex) {
            logger.error("JWT令牌已过期");
        } catch (UnsupportedJwtException ex) {
            logger.error("不支持的JWT令牌");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims字符串为空");
        }
        return false;
    }
}

5.2 方法级安全控制

5.2.1 基于注解的权限控制
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    // 基于角色的访问控制
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping
    public ProductDTO createProduct(@Valid @RequestBody ProductCreateDTO dto) {
        return productService.create(dto);
    }
    
    // 基于权限的访问控制
    @PreAuthorize("hasAuthority('PRODUCT_READ')")
    @GetMapping("/{id}")
    public ProductDTO getProduct(@PathVariable Long id) {
        return productService.getById(id);
    }
    
    // 基于表达式的复杂权限控制
    @PreAuthorize("hasRole('ADMIN') or @productSecurity.isOwner(#id, authentication)")
    @PutMapping("/{id}")
    public ProductDTO updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody ProductUpdateDTO dto) {
        return productService.update(id, dto);
    }
    
    // 方法调用后权限检查
    @PostAuthorize("returnObject.status != 'DELETED'")
    @GetMapping("/secure/{id}")
    public ProductDTO getSecureProduct(@PathVariable Long id) {
        return productService.getById(id);
    }
    
    // 基于过滤器的权限控制
    @PreFilter("filterObject.ownerId == authentication.principal.id")
    @PostMapping("/batch")
    public List<ProductDTO> createProducts(
            @RequestBody List<ProductCreateDTO> products) {
        return productService.createBatch(products);
    }
    
    // 方法调用后过滤
    @PostFilter("filterObject.price > 100")
    @GetMapping("/expensive")
    public List<ProductDTO> getExpensiveProducts() {
        return productService.getAll();
    }
}

// 自定义安全表达式处理器
@Component("productSecurity")
public class ProductSecurity {
    
    private final ProductRepository productRepository;
    
    public boolean isOwner(Long productId, Authentication authentication) {
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }
        
        String username = authentication.getName();
        Optional<Product> product = productRepository.findById(productId);
        
        return product.isPresent() && 
               product.get().getCreatedBy().equals(username);
    }
    
    public boolean canView(Product product, Authentication authentication) {
        // 复杂的业务逻辑判断
        if (product.isPublic()) {
            return true;
        }
        
        if (authentication == null) {
            return false;
        }
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return product.getOwners().contains(userDetails.getUsername()) ||
               userDetails.getAuthorities().stream()
                   .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    }
}
5.2.2 权限缓存与性能优化
@Configuration
@EnableCaching
public class SecurityCacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setCacheNames(Arrays.asList(
            "userDetails",
            "permissions",
            "aclCache"
        ));
        return cacheManager;
    }
}

@Service
public class CachingUserDetailsService implements UserDetailsService {
    
    private final UserDetailsService delegate;
    private final CacheManager cacheManager;
    
    @Cacheable(value = "userDetails", key = "#username")
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return delegate.loadUserByUsername(username);
    }
    
    @CacheEvict(value = "userDetails", key = "#username")
    public void evictUserCache(String username) {
        // 缓存清除
    }
}

// 权限缓存服务
@Service
public class PermissionCacheService {
    
    @Cacheable(value = "permissions", key = "#userId + ':' + #resource")
    public boolean hasPermission(Long userId, String resource, String action) {
        // 从数据库查询权限
        return permissionRepository.existsByUserIdAndResourceAndAction(
            userId, resource, action);
    }
    
    @CacheEvict(value = "permissions", allEntries = true)
    public void clearAllPermissionCache() {
        // 清除所有权限缓存
    }
}

六、API文档与测试

6.1 OpenAPI/Swagger集成

6.1.1 Springdoc OpenAPI配置
@Configuration
@OpenAPIDefinition(
    info = @Info(
        title = "订单管理系统API",
        version = "1.0.0",
        description = "订单管理系统REST API文档",
        contact = @Contact(
            name = "技术支持",
            email = "support@example.com",
            url = "https://example.com"
        ),
        license = @License(
            name = "Apache 2.0",
            url = "https://www.apache.org/licenses/LICENSE-2.0"
        ),
        termsOfService = "https://example.com/terms"
    ),
    servers = {
        @Server(
            url = "http://localhost:8080",
            description = "开发环境"
        ),
        @Server(
            url = "https://api.example.com",
            description = "生产环境"
        )
    },
    security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
    name = "bearerAuth",
    type = SecuritySchemeType.HTTP,
    bearerFormat = "JWT",
    scheme = "bearer"
)
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .components(new Components()
                .addSchemas("ErrorResponse", new Schema<ErrorResponse>()
                    .type("object")
                    .addProperty("timestamp", new Schema<String>()
                        .type("string")
                        .format("date-time"))
                    .addProperty("status", new Schema<Integer>()
                        .type("integer"))
                    .addProperty("error", new Schema<String>()
                        .type("string"))
                    .addProperty("message", new Schema<String>()
                        .type("string"))
                    .addProperty("path", new Schema<String>()
                        .type("string")))
                .addSecuritySchemes("bearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")))
            .externalDocs(new ExternalDocumentation()
                .description("更多文档")
                .url("https://docs.example.com"));
    }
    
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
            .group("public")
            .pathsToMatch("/api/**")
            .build();
    }
    
    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
            .group("admin")
            .pathsToMatch("/api/admin/**")
            .build();
    }
}
6.1.2 控制器文档注解
@RestController
@RequestMapping("/api/orders")
@Tag(name = "订单管理", description = "订单相关操作")
public class OrderController {
    
    @Operation(
        summary = "获取订单列表",
        description = "分页获取订单列表,支持多种查询条件",
        parameters = {
            @Parameter(name = "page", description = "页码,从0开始", example = "0"),
            @Parameter(name = "size", description = "每页大小", example = "20"),
            @Parameter(name = "status", description = "订单状态"),
            @Parameter(name = "startDate", description = "开始日期", example = "2024-01-01"),
            @Parameter(name = "endDate", description = "结束日期", example = "2024-12-31")
        }
    )
    @ApiResponses({
        @ApiResponse(
            responseCode = "200",
            description = "成功获取订单列表",
            content = @Content(
                mediaType = "application/json",
                array = @ArraySchema(schema = @Schema(implementation = OrderDTO.class))
            )
        ),
        @ApiResponse(
            responseCode = "401",
            description = "未授权访问"
        ),
        @ApiResponse(
            responseCode = "403",
            description = "权限不足"
        )
    })
    @GetMapping
    @PreAuthorize("hasRole('USER')")
    public Page<OrderDTO> getOrders(
            @ParameterObject Pageable pageable,
            @ParameterObject OrderQueryDTO query) {
        return orderService.getOrders(query, pageable);
    }
    
    @Operation(
        summary = "创建订单",
        description = "创建新订单,需要商品信息和收货地址"
    )
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<OrderDTO> createOrder(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "订单创建信息",
                required = true,
                content = @Content(
                    schema = @Schema(implementation = OrderCreateDTO.class)
                )
            )
            @Valid @RequestBody OrderCreateDTO dto) {
        
        OrderDTO created = orderService.createOrder(dto);
        
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        
        return ResponseEntity.created(location).body(created);
    }
    
    @Operation(
        summary = "获取订单详情",
        description = "根据ID获取订单详细信息"
    )
    @GetMapping("/{id}")
    public OrderDTO getOrder(
            @Parameter(description = "订单ID", required = true, example = "123")
            @PathVariable Long id) {
        return orderService.getOrderById(id);
    }
    
    @Operation(
        summary = "更新订单状态",
        description = "更新订单状态,支持取消、完成等操作"
    )
    @PatchMapping("/{id}/status")
    public OrderDTO updateOrderStatus(
            @Parameter(description = "订单ID", required = true)
            @PathVariable Long id,
            @RequestBody OrderStatusUpdateDTO dto) {
        
        return orderService.updateStatus(id, dto);
    }
    
    @Operation(
        summary = "导出订单",
        description = "导出订单数据为Excel文件"
    )
    @GetMapping("/export")
    public void exportOrders(
            @ParameterObject OrderQueryDTO query,
            HttpServletResponse response) throws IOException {
        
        List<OrderDTO> orders = orderService.exportOrders(query);
        
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment; filename=orders.xlsx");
        
        exportService.exportToExcel(orders, response.getOutputStream());
    }
}

6.2 全面测试策略

6.2.1 单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private PasswordEncoder passwordEncoder;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserServiceImpl userService;
    
    private UserMapper userMapper = Mappers.getMapper(UserMapper.class);
    
    @Test
    void createUser_ShouldReturnUserDTO_WhenValidInput() {
        // 准备测试数据
        UserCreateDTO createDTO = new UserCreateDTO();
        createDTO.setUsername("testuser");
        createDTO.setEmail("test@example.com");
        createDTO.setPassword("password123");
        
        User user = new User();
        user.setId(1L);
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        
        // 设置Mock行为
        when(userRepository.existsByEmail(anyString())).thenReturn(false);
        when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
        when(userRepository.save(any(User.class))).thenReturn(user);
        
        // 执行测试
        UserDTO result = userService.createUser(createDTO);
        
        // 验证结果
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("testuser", result.getUsername());
        assertEquals("test@example.com", result.getEmail());
        
        // 验证交互
        verify(userRepository).existsByEmail("test@example.com");
        verify(passwordEncoder).encode("password123");
        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail("test@example.com");
    }
    
    @Test
    void createUser_ShouldThrowException_WhenEmailExists() {
        // 准备测试数据
        UserCreateDTO createDTO = new UserCreateDTO();
        createDTO.setEmail("existing@example.com");
        
        // 设置Mock行为
        when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
        
        // 执行测试并验证异常
        BusinessException exception = assertThrows(
            BusinessException.class,
            () -> userService.createUser(createDTO)
        );
        
        assertEquals("邮箱已存在", exception.getMessage());
    }
    
    @Test
    @DisplayName("根据ID获取用户 - 用户存在")
    void getUserById_ShouldReturnUser_WhenUserExists() {
        // 准备测试数据
        Long userId = 1L;
        User user = new User();
        user.setId(userId);
        user.setUsername("testuser");
        
        // 设置Mock行为
        when(userRepository.findById(userId))
            .thenReturn(Optional.of(user));
        
        // 执行测试
        UserDTO result = userService.getUserById(userId);
        
        // 验证结果
        assertNotNull(result);
        assertEquals(userId, result.getId());
        assertEquals("testuser", result.getUsername());
    }
    
    @Test
    @DisplayName("根据ID获取用户 - 用户不存在")
    void getUserById_ShouldThrowException_WhenUserNotFound() {
        // 设置Mock行为
        when(userRepository.findById(anyLong()))
            .thenReturn(Optional.empty());
        
        // 执行测试并验证异常
        ResourceNotFoundException exception = assertThrows(
            ResourceNotFoundException.class,
            () -> userService.getUserById(1L)
        );
        
        assertEquals("用户不存在", exception.getMessage());
    }
    
    @ParameterizedTest
    @CsvSource({
        "1, admin, ADMIN",
        "2, user, USER",
        "3, manager, MANAGER"
    })
    void getUserById_WithDifferentUsers_ShouldReturnCorrectUser(
            Long id, String username, UserRole role) {
        
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        user.setRole(role);
        
        when(userRepository.findById(id)).thenReturn(Optional.of(user));
        
        UserDTO result = userService.getUserById(id);
        
        assertEquals(id, result.getId());
        assertEquals(username, result.getUsername());
        assertEquals(role, result.getRole());
    }
    
    @Test
    void updateUser_ShouldUpdateAndReturnUser() {
        // 准备测试数据
        Long userId = 1L;
        UserUpdateDTO updateDTO = new UserUpdateDTO();
        updateDTO.setUsername("updateduser");
        updateDTO.setEmail("updated@example.com");
        
        User existingUser = new User();
        existingUser.setId(userId);
        existingUser.setUsername("olduser");
        existingUser.setEmail("old@example.com");
        
        User updatedUser = new User();
        updatedUser.setId(userId);
        updatedUser.setUsername("updateduser");
        updatedUser.setEmail("updated@example.com");
        
        // 设置Mock行为
        when(userRepository.findById(userId))
            .thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class)))
            .thenReturn(updatedUser);
        
        // 执行测试
        UserDTO result = userService.updateUser(userId, updateDTO);
        
        // 验证结果
        assertEquals("updateduser", result.getUsername());
        assertEquals("updated@example.com", result.getEmail());
        
        // 验证交互
        verify(userRepository).findById(userId);
        verify(userRepository).save(existingUser);
        assertEquals("updateduser", existingUser.getUsername());
        assertEquals("updated@example.com", existingUser.getEmail());
    }
}
6.2.2 集成测试
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
@Transactional
class UserControllerIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    void createUser_ShouldReturnCreatedUser() throws Exception {
        // 准备测试数据
        UserCreateDTO createDTO = new UserCreateDTO();
        createDTO.setUsername("integrationtest");
        createDTO.setEmail("integration@test.com");
        createDTO.setPassword("Password123!");
        createDTO.setRole(UserRole.USER);
        
        // 执行请求
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
                
            // 验证响应
            .andExpect(status().isCreated())
            .andExpect(header().exists("Location"))
            .andExpect(jsonPath("$.id").exists())
            .andExpect(jsonPath("$.username").value("integrationtest"))
            .andExpect(jsonPath("$.email").value("integration@test.com"))
            .andExpect(jsonPath("$.role").value("USER"));
        
        // 验证数据库
        Optional<User> savedUser = userRepository.findByEmail("integration@test.com");
        assertTrue(savedUser.isPresent());
        assertEquals("integrationtest", savedUser.get().getUsername());
    }
    
    @Test
    void createUser_ShouldReturnBadRequest_WhenInvalidInput() throws Exception {
        // 准备无效的测试数据
        UserCreateDTO createDTO = new UserCreateDTO();
        createDTO.setEmail("invalid-email");
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
                
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errors").exists());
    }
    
    @Test
    void getUser_ShouldReturnUser_WhenUserExists() throws Exception {
        // 准备测试数据
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("encodedPassword");
        user.setRole(UserRole.USER);
        user = userRepository.save(user);
        
        // 执行请求
        mockMvc.perform(get("/api/users/{id}", user.getId()))
                
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(user.getId()))
            .andExpect(jsonPath("$.username").value("testuser"))
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }
    
    @Test
    void getUser_ShouldReturnNotFound_WhenUserNotExists() throws Exception {
        mockMvc.perform(get("/api/users/{id}", 999))
            .andExpect(status().isNotFound());
    }
    
    @Test
    void updateUser_ShouldUpdateUser() throws Exception {
        // 创建测试用户
        User user = new User();
        user.setUsername("olduser");
        user.setEmail("old@example.com");
        user.setPassword("encodedPassword");
        user.setRole(UserRole.USER);
        user = userRepository.save(user);
        
        // 准备更新数据
        UserUpdateDTO updateDTO = new UserUpdateDTO();
        updateDTO.setUsername("updateduser");
        updateDTO.setEmail("updated@example.com");
        
        // 执行更新请求
        mockMvc.perform(put("/api/users/{id}", user.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateDTO)))
                
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.username").value("updateduser"))
            .andExpect(jsonPath("$.email").value("updated@example.com"));
        
        // 验证数据库更新
        Optional<User> updatedUser = userRepository.findById(user.getId());
        assertTrue(updatedUser.isPresent());
        assertEquals("updateduser", updatedUser.get().getUsername());
        assertEquals("updated@example.com", updatedUser.get().getEmail());
    }
    
    @Test
    void deleteUser_ShouldDeleteUser() throws Exception {
        // 创建测试用户
        User user = new User();
        user.setUsername("tobedeleted");
        user.setEmail("delete@example.com");
        user.setPassword("encodedPassword");
        user = userRepository.save(user);
        
        // 执行删除请求
        mockMvc.perform(delete("/api/users/{id}", user.getId()))
            .andExpect(status().isNoContent());
        
        // 验证用户已删除
        assertFalse(userRepository.existsById(user.getId()));
    }
    
    @Test
    void getUsers_ShouldReturnPaginatedUsers() throws Exception {
        // 创建测试数据
        for (int i = 1; i <= 25; i++) {
            User user = new User();
            user.setUsername("user" + i);
            user.setEmail("user" + i + "@example.com");
            user.setPassword("password" + i);
            userRepository.save(user);
        }
        
        // 执行分页查询
        mockMvc.perform(get("/api/users")
                .param("page", "0")
                .param("size", "10")
                .param("sort", "username,asc"))
                
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content").isArray())
            .andExpect(jsonPath("$.content.length()").value(10))
            .andExpect(jsonPath("$.totalPages").value(3))
            .andExpect(jsonPath("$.totalElements").value(25))
            .andExpect(jsonPath("$.first").value(true))
            .andExpect(jsonPath("$.last").value(false));
    }
    
    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void adminEndpoint_ShouldBeAccessible_ForAdminUser() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    void adminEndpoint_ShouldBeForbidden_ForNonAdminUser() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
    }
}

七、部署与监控

7.1 Docker容器化部署

7.1.1 Dockerfile配置
# 构建阶段
FROM maven:3.8.4-openjdk-17-slim AS build

# 设置工作目录
WORKDIR /app

# 复制项目文件
COPY pom.xml .
COPY src ./src

# 下载依赖并构建(利用Docker层缓存)
RUN mvn dependency:go-offline
RUN mvn clean package -DskipTests

# 运行时阶段
FROM openjdk:17-jdk-slim

# 安装必要的工具
RUN apt-get update && apt-get install -y \
    curl \
    tzdata \
    && rm -rf /var/lib/apt/lists/*

# 设置时区
ENV TZ=Asia/Shanghai

# 创建非root用户
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring

# 设置工作目录
WORKDIR /app

# 从构建阶段复制JAR文件
COPY --from=build /app/target/*.jar app.jar

# 暴露端口
EXPOSE 8080

# JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp"

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# 启动命令
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app/app.jar"]
7.1.2 Docker Compose配置
version: '3.8'

services:
  # 主应用服务
  app:
    build: .
    container_name: spring-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/appdb
      - SPRING_DATASOURCE_USERNAME=appuser
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
      - SPRING_REDIS_HOST=redis
      - SPRING_REDIS_PORT=6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # PostgreSQL数据库
  postgres:
    image: postgres:15-alpine
    container_name: app-postgres
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-db:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: app-redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Nginx反向代理
  nginx:
    image: nginx:alpine
    container_name: app-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    networks:
      - backend
    restart: unless-stopped

  # 监控系统 (Prometheus + Grafana)
  prometheus:
    image: prom/prometheus:latest
    container_name: app-prometheus
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - backend
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: app-grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    ports:
      - "3000:3000"
    networks:
      - backend
    restart: unless-stopped

networks:
  backend:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  prometheus_data:
  grafana_data:

7.2 性能监控与指标

7.2.1 Spring Boot Actuator配置
# application-prod.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
      cors:
        allowed-origins: "*"
        allowed-methods: GET,POST
    
  endpoint:
    health:
      show-details: when_authorized
      roles: ADMIN
      probes:
        enabled: true
      groups:
        liveness:
          include: livenessState
        readiness:
          include: readinessState
    
    metrics:
      enabled: true
    
    prometheus:
      enabled: true
  
  metrics:
    export:
      prometheus:
        enabled: true
        step: 1m
    distribution:
      percentiles-histogram:
        "[http.server.requests]": true
      sla:
        "[http.server.requests]": 10ms, 50ms, 100ms, 200ms, 500ms, 1s, 2s
  
  info:
    env:
      enabled: true
    java:
      enabled: true
    os:
      enabled: true
  
  tracing:
    sampling:
      probability: 0.1
  
  server:
    port: 8081 # 单独的监控端口
7.2.2 自定义健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    private final DataSource dataSource;
    private final JdbcTemplate jdbcTemplate;
    
    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    @Override
    public Health health() {
        try {
            // 检查数据库连接
            Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
            
            if (result != null && result == 1) {
                // 检查数据库性能
                Map<String, Object> details = new HashMap<>();
                
                // 获取连接池信息
                if (dataSource instanceof HikariDataSource) {
                    HikariDataSource hikari = (HikariDataSource) dataSource;
                    details.put("activeConnections", hikari.getHikariPoolMXBean().getActiveConnections());
                    details.put("idleConnections", hikari.getHikariPoolMXBean().getIdleConnections());
                    details.put("totalConnections", hikari.getHikariPoolMXBean().getTotalConnections());
                }
                
                return Health.up()
                    .withDetails(details)
                    .build();
            }
            
            return Health.down()
                .withDetail("error", "数据库查询返回异常结果")
                .build();
            
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .withDetail("error", "数据库连接失败: " + e.getMessage())
                .build();
        }
    }
}

@Component
public class CacheHealthIndicator implements HealthIndicator {
    
    private final CacheManager cacheManager;
    
    public CacheHealthIndicator(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
    
    @Override
    public Health health() {
        Map<String, Object> details = new HashMap<>();
        
        for (String cacheName : cacheManager.getCacheNames()) {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache != null && cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
                @SuppressWarnings("unchecked")
                com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache = 
                    (com.github.benmanes.caffeine.cache.Cache<Object, Object>) cache.getNativeCache();
                
                Map<String, Object> cacheStats = new HashMap<>();
                cacheStats.put("estimatedSize", caffeineCache.estimatedSize());
                cacheStats.put("stats", caffeineCache.stats());
                
                details.put(cacheName, cacheStats);
            }
        }
        
        return Health.up()
            .withDetails(details)
            .build();
    }
}

@Component
public class ExternalServiceHealthIndicator extends AbstractHealthIndicator {
    
    private final RestTemplate restTemplate;
    private final String serviceUrl;
    
    public ExternalServiceHealthIndicator(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder
            .setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(10))
            .build();
        this.serviceUrl = "https://api.external-service.com/health";
    }
    
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        try {
            ResponseEntity<String> response = restTemplate.getForEntity(serviceUrl, String.class);
            
            if (response.getStatusCode().is2xxSuccessful()) {
                builder.up()
                    .withDetail("status", response.getStatusCode().value())
                    .withDetail("responseTime", "正常");
            } else {
                builder.down()
                    .withDetail("status", response.getStatusCode().value())
                    .withDetail("error", "外部服务返回异常状态码");
            }
            
        } catch (ResourceAccessException e) {
            builder.down()
                .withException(e)
                .withDetail("error", "连接外部服务超时");
        } catch (Exception e) {
            builder.down()
                .withException(e)
                .withDetail("error", "检查外部服务健康状态时发生异常");
        }
    }
}

八、性能优化与最佳实践

8.1 数据库性能优化

8.1.1 连接池配置
# application-prod.yml
spring:
  datasource:
    hikari:
      # 连接池配置
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      
      # 性能优化
      auto-commit: false
      connection-test-query: SELECT 1
      validation-timeout: 5000
      leak-detection-threshold: 60000
      
      # 连接属性
      data-source-properties:
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        cachePrepStmts: true
        useServerPrepStmts: true
        useLocalSessionState: true
        rewriteBatchedStatements: true
        cacheResultSetMetadata: true
        cacheServerConfiguration: true
        elideSetAutoCommits: true
        maintainTimeStats: false
8.1.2 JPA性能优化
@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.repository",
    repositoryBaseClass = CustomJpaRepositoryImpl.class
)
@EnableTransactionManagement
public class JpaConfig {
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            DataSource dataSource) {
        
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.jdbc.batch_size", 50);
        properties.put("hibernate.order_inserts", true);
        properties.put("hibernate.order_updates", true);
        properties.put("hibernate.batch_versioned_data", true);
        properties.put("hibernate.query.in_clause_parameter_padding", true);
        properties.put("hibernate.default_batch_fetch_size", 16);
        properties.put("hibernate.max_fetch_depth", 3);
        properties.put("hibernate.jdbc.fetch_size", 100);
        
        // 生产环境禁用DDL自动更新
        properties.put("hibernate.hbm2ddl.auto", "validate");
        
        return builder
            .dataSource(dataSource)
            .packages("com.example.entity")
            .persistenceUnit("default")
            .properties(properties)
            .build();
    }
    
    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

// 自定义Repository实现
@NoRepositoryBean
public class CustomJpaRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> 
    implements CustomJpaRepository<T, ID> {
    
    private final EntityManager entityManager;
    
    public CustomJpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, 
                                  EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<T> findAllWithPagination(int offset, int limit, Sort sort) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = cb.createQuery(getDomainClass());
        Root<T> root = query.from(getDomainClass());
        
        query.select(root);
        
        if (sort != null) {
            List<Order> orders = new ArrayList<>();
            sort.forEach(order -> {
                if (order.isAscending()) {
                    orders.add(cb.asc(root.get(order.getProperty())));
                } else {
                    orders.add(cb.desc(root.get(order.getProperty())));
                }
            });
            query.orderBy(orders);
        }
        
        return entityManager.createQuery(query)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
    }
    
    @Override
    @Transactional
    public int batchInsert(List<T> entities) {
        int batchSize = 50;
        int count = 0;
        
        for (int i = 0; i < entities.size(); i++) {
            entityManager.persist(entities.get(i));
            
            if (i % batchSize == 0 && i > 0) {
                entityManager.flush();
                entityManager.clear();
                count += batchSize;
            }
        }
        
        entityManager.flush();
        entityManager.clear();
        
        return count + (entities.size() % batchSize);
    }
}

8.2 缓存策略优化

8.2.1 多级缓存配置
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        
        // 全局缓存配置
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(30))
            .maximumSize(10000)
            .recordStats());
        
        // 自定义缓存配置
        Map<String, Caffeine<Object, Object>> cacheConfigs = new HashMap<>();
        
        // 用户缓存 - 较短时间,高频访问
        cacheConfigs.put("users", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(10))
            .maximumSize(1000)
            .recordStats());
        
        // 商品缓存 - 较长时间,低频更新
        cacheConfigs.put("products", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofHours(2))
            .maximumSize(5000)
            .recordStats());
        
        // 配置缓存 - 永不过期,手动刷新
        cacheConfigs.put("config", Caffeine.newBuilder()
            .maximumSize(100)
            .recordStats());
        
        cacheManager.setCacheSpecification(cacheConfigs);
        
        return cacheManager;
    }
    
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .disableCachingNullValues()
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        // 不同缓存不同配置
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        cacheConfigs.put("users", config.entryTtl(Duration.ofMinutes(30)));
        cacheConfigs.put("products", config.entryTtl(Duration.ofHours(2)));
        
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigs)
            .transactionAware()
            .build();
    }
    
    @Bean
    public CacheManager multiLevelCacheManager(
            CacheManager localCacheManager,
            CacheManager redisCacheManager) {
        
        return new MultiLevelCacheManager(localCacheManager, redisCacheManager);
    }
}

// 多级缓存实现
public class MultiLevelCacheManager implements CacheManager {
    
    private final CacheManager localCacheManager;  // L1: Caffeine
    private final CacheManager redisCacheManager;  // L2: Redis
    
    public MultiLevelCacheManager(CacheManager localCacheManager, 
                                  CacheManager redisCacheManager) {
        this.localCacheManager = localCacheManager;
        this.redisCacheManager = redisCacheManager;
    }
    
    @Override
    public Cache getCache(String name) {
        Cache localCache = localCacheManager.getCache(name);
        Cache remoteCache = redisCacheManager.getCache(name);
        
        return new MultiLevelCache(name, localCache, remoteCache);
    }
    
    @Override
    public Collection<String> getCacheNames() {
        Set<String> names = new HashSet<>();
        names.addAll(localCacheManager.getCacheNames());
        names.addAll(redisCacheManager.getCacheNames());
        return names;
    }
}

class MultiLevelCache implements Cache {
    
    private final String name;
    private final Cache localCache;
    private final Cache remoteCache;
    
    public MultiLevelCache(String name, Cache localCache, Cache remoteCache) {
        this.name = name;
        this.localCache = localCache;
        this.remoteCache = remoteCache;
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    @Override
    public Object getNativeCache() {
        return remoteCache.getNativeCache();
    }
    
    @Override
    public ValueWrapper get(Object key) {
        // 先查本地缓存
        ValueWrapper value = localCache.get(key);
        if (value != null) {
            return value;
        }
        
        // 本地缓存未命中,查Redis
        value = remoteCache.get(key);
        if (value != null) {
            // 回写到本地缓存
            localCache.put(key, value.get());
        }
        
        return value;
    }
    
    @Override
    public <T> T get(Object key, Class<T> type) {
        // 实现类似get方法
        T value = localCache.get(key, type);
        if (value != null) {
            return value;
        }
        
        value = remoteCache.get(key, type);
        if (value != null) {
            localCache.put(key, value);
        }
        
        return value;
    }
    
    @Override
    public void put(Object key, Object value) {
        // 同时写入两级缓存
        localCache.put(key, value);
        remoteCache.put(key, value);
    }
    
    @Override
    public void evict(Object key) {
        // 同时清除两级缓存
        localCache.evict(key);
        remoteCache.evict(key);
    }
    
    @Override
    public void clear() {
        localCache.clear();
        remoteCache.clear();
    }
}

九、生产环境最佳实践

9.1 应用配置管理

9.1.1 多环境配置
# application.yml (基础配置)
spring:
  application:
    name: order-service
  
  profiles:
    active: @spring.profiles.active@

# 日志配置
logging:
  level:
    com.example: INFO
    org.springframework.web: INFO
    org.hibernate.SQL: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/application.log
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
# application-dev.yml (开发环境)
server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  
  h2:
    console:
      enabled: true
      path: /h2-console

logging:
  level:
    com.example: DEBUG
    org.springframework.web: DEBUG
# application-prod.yml (生产环境)
server:
  port: 8080
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
    min-response-size: 1024
  tomcat:
    max-connections: 10000
    max-threads: 200
    min-spare-threads: 10
    connection-timeout: 5000

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
    
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
  
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

# 生产环境日志配置
logging:
  level:
    com.example: INFO
    org.springframework.web: WARN
    org.hibernate: WARN
  logstash:
    enabled: true
    host: ${LOGSTASH_HOST}
    port: ${LOGSTASH_PORT}

9.2 监控告警配置

9.2.1 Prometheus监控配置
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093

rule_files:
  - "alert_rules.yml"

scrape_configs:
  - job_name: 'spring-boot-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app:8080']
        labels:
          application: 'order-service'
          environment: 'production'
    
  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']
    
  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']
9.2.2 告警规则配置
# alert_rules.yml
groups:
  - name: spring_boot_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(http_server_requests_seconds_count{status=~"5..",uri!~".*actuator.*"}[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "高错误率报警"
          description: "{{ $labels.instance }}的错误率超过5% (当前值: {{ $value }})"
      
      - alert: HighResponseTime
        expr: histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "高响应时间报警"
          description: "{{ $labels.instance }}的95%响应时间超过1秒 (当前值: {{ $value }}s)"
      
      - alert: ServiceDown
        expr: up{job="spring-boot-app"} == 0
        for: 1m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "服务宕机报警"
          description: "{{ $labels.instance }}服务已宕机"
      
      - alert: HighMemoryUsage
        expr: (sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"})) > 0.8
        for: 5m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "高内存使用率报警"
          description: "{{ $labels.instance }}内存使用率超过80% (当前值: {{ $value }})"
      
      - alert: HighCPULoad
        expr: system_cpu_usage > 0.8
        for: 5m
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "高CPU使用率报警"
          description: "{{ $labels.instance }}CPU使用率超过80% (当前值: {{ $value }})"

十、学习路径规划

10.1 初学者入门路径(1-2周)

  1. 掌握Spring Boot基础

    • 理解Spring Boot自动配置原理
    • 掌握RESTful API设计原则
    • 学习Spring MVC注解使用
  2. 完成第一个CRUD项目

    • 创建用户管理系统
    • 实现增删改查接口
    • 添加数据验证

10.2 进阶提升路径(3-4周)

  1. 深入Spring生态

    • 学习Spring Security实现安全控制
    • 掌握Spring Data JPA高级特性
    • 了解Spring Cache缓存机制
  2. 项目实战

    • 实现电商系统核心模块
    • 集成第三方服务(支付、短信)
    • 添加API文档和单元测试

10.3 专家精通路径(2-3个月)

  1. 性能优化

    • JVM调优实践
    • 数据库查询优化
    • 缓存策略设计
  2. 微服务架构

    • Spring Cloud学习
    • 服务注册与发现
    • 分布式事务处理
  3. 生产实践

    • Docker容器化部署
    • CI/CD流水线搭建
    • 监控告警系统建设

通过以上系统学习路径,可以从Spring Boot新手逐步成长为RESTful API开发专家,掌握企业级应用开发的全套技能栈。

在这里插入图片描述

评论 43
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百锦再@新空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值