Spring MVC RESTful API设计教程

目录

  1. REST API设计原则
  2. 资源与HATEOAS
  3. HTTP状态码应用
  4. API版本控制
  5. 错误处理标准化
  6. 分页与过滤
  7. 缓存策略
  8. API安全实现
  9. 性能优化
  10. 测试策略
  11. API文档化
  12. 总结

REST API设计原则

REST架构约束

Roy Fielding的REST约束

  1. 客户端-服务器(Client-Server):分离关注点
  2. 无状态(Stateless):请求独立,服务器不保存客户端状态
  3. 缓存(Cacheable):响应可以被缓存
  4. 统一接口(Uniform Interface):标准化接口设计
  5. 分层系统(Layered System):分层架构
  6. 按需编码(Code on Demand):可选约束

资源命名规范

资源命名最佳实践

// ✅ 好的设计
GET    /api/v1/users                 // 获取用户列表
GET    /api/v1/users/{id}           // 获取特定用户
POST   /api/v1/users                 // 创建用户
PUT    /api/v1/users/{id}           // 完整更新用户
PATCH  /api/v1/users/{id}           // 部分更新用户
DELETE /api/v1/users/{id}           // 删除用户

GET    /api/v1/users/{id}/orders    // 获取用户的订单
POST   /api/v1/users/{id}/orders    // 为用户创建订单
GET    /api/v1/users/{id}/orders/{orderId} // 获取特定订单

// ❌ 不好的设计
POST   /api/v1/getUsers             // 动词在URL中
GET    /api/v1/usersList           // 不必要的词汇
POST   /api/v1/user/create         // 资源和方法混淆
GET    /api/v1/userById/123        // 路径参数命名不规范

统一响应格式

API响应封装

// 统一响应结果类
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private String errorCode;
    private long timestamp;
    private Map<String, Object> metadata;
    
    // 成功响应
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = true;
        response.data = data;
        response.timestamp = System.currentTimeMillis();
        return response;
    }
    
    public static <T> ApiResponse<T> success(String message, T data) {
        ApiResponse<T> response = success(data);
        response.message = message;
        return response;
    }
    
    // 错误响应
    public static <T> ApiResponse<T> error(String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = false;
        response.message = message;
        response.timestamp = System.currentTimeMillis();
        return response;
    }
    
    public static <T> ApiResponse<T> error(String errorCode, String message) {
        ApiResponse<T> response = error(message);
        response.errorCode = errorCode;
        return response;
    }
    
    // getters and setters
}

// REST控制器实现
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserMapper userMapper;
    
    @GetMapping
    public ResponseEntity<ApiResponse<PageResult<UserDto>>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "createdDate") String sortBy,
        @RequestParam(defaultValue = "desc") String sortDir,
        @RequestParam(required = false) String search) {
        
        Pageable pageable = PageRequest.of(page, size, 
            Sort.by(Sort.Direction.fromString(sortDir), sortBy));
        
        PageResult<User> users = userService.getUsers(search, pageable);
        PageResult<UserDto> userDtos = userMapper.toPageResult(users);
        
        return ResponseEntity.ok(ApiResponse.success("用户列表获取成功", userDtos));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDto>> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        UserDto userDto = userMapper.toDto(user);
        
        return ResponseEntity.ok(ApiResponse.success("用户信息获取成功", userDto));
    }
    
    @PostMapping
    public ResponseEntity<ApiResponse<UserDto>> createUser(
        @Valid @RequestBody UserCreateRequest request) {
        
        User user = userMapper.toEntity(request);
        User savedUser = userService.createUser(user);
        UserDto userDto = userMapper.toDto(savedUser);
        
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(ApiResponse.success("用户创建成功", userDto));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDto>> updateUser(
        @PathVariable Long id,
        @Valid @RequestBody UserUpdateRequest request) {
        
        User user = userService.findById(id);
        userMapper.updateEntity(user, request);
        User updatedUser = userService.updateUser(user);
        UserDto userDto = userMapper.toDto(updatedUser);
        
        return ResponseEntity.ok(ApiResponse.success("用户更新成功", userDto));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok(ApiResponse.success("用户删除成功", null));
    }
}

资源与HATEOAS

HATEOAS实现

Spring HATEOAS集成

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
    <version>1.1.5</version>
</dependency>

HATEOAS资源表示

// HATEOAS资源类
@Relation(collectionRelation = "users")
public class UserResource extends EntityModel<User> {
    
    public UserResource(User user) {
        super(user);
        add(linkTo(UserController.class).withRel("collection"));
        add(linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel());
        add(linkTo(methodOn(UserController.class).updateUser(user.getId(), null)).withRel("edit"));
        add(linkTo(methodOn(UserController.class).deleteUser(user.getId())).withRel("delete"));
        
        // 相关的资源链接
        if (user.getOrders() != null && !user.getOrders().isEmpty()) {
            add(linkTo(methodOn(OrderController.class).getUserOrders(user.getId()))
                .withRel("orders"));
        }
    }
}

// 控制器中的HATEOAS应用
@RestController
@RequestMapping("/api/v1/users")
public class UserHateoasController {
    
    @GetMapping(value = "/{id}", produces = MediaTypes.HAL_JSON_VALUE)
    public ResponseEntity<UserResource> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        UserResource userResource = new UserResource(user);
        return ResponseEntity.ok(userResource);
    }
    
    @GetMapping(produces = MediaTypes.HAL_JSON_VALUE)
    public ResponseEntity<PagedModel<EntityModel<User>>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<User> users = userService.getUsers(pageable);
        
        List<EntityModel<User>> userModels = users.getContent().stream()
            .map(this::toUserModel)
            .collect(Collectors.toList());
        
        Link[] links = {
            linkTo(methodOn(UserHateoasController.class).getUsers(0, size)).withRel("first"),
            selfLink()
        };
        
        if (users.hasNext()) {
            links = Arrays.copyOf(links, links.length + 1);
            links[links.length - 1] = linkTo(methodOn(UserHateoasController.class)
                .getUsers(users.next().getPageNumber(), size)).withRel("next");
        }
        
        PagedModel<EntityModel<User>> pagedModel = PagedModel.of(
            userModels,
            new PagedModel.PageMetadata(users.getSize(), users.getNumber(),
                users.getTotalElements(), users.getTotalPages())
        );
        
        pagedModel.forEach(user -> {
            user.add(linkTo(UserHateoasController.class).slash(user.getContent().getId())
                .withSelfRel());
        });
        
        return ResponseEntity.ok(pagedModel);
    }
    
    private EntityModel<User> toUserModel(User user) {
        return EntityModel.of(user,
            linkTo(UserHateoasController.class).slash(user.getId()).withSelfRel(),
            linkTo(UserHateoasController.class).withRel("collection"));
    }
}

资源链接构建

链接构建服务

@Component
public class LinkBuilderService {
    
    public void addSelfLinks(RepresentationModel<?> model, Class<?> controllerClass, Object... pathVariables) {
        model.add(WebMvcLinkBuilder.linkTo(controllerClass).slash(pathVariables).withSelfRel());
    }
    
    public void addCollectionLinks(RepresentationModel<?> model, Class<?> controllerClass) {
        model.add(WebMvcLinkBuilder.linkTo(controllerClass).withRel("collection"));
    }
    
    public <T extends RepresentatioModel<?>> T addUserLinks(T userModel, Long userId) {
        userModel.add(linkTo(UserController.class).slash(userId).withSelfRel());
        userModel.add(linkTo(UserController.class).slash(userId).slash("edit").withRel("edit"));
        userModel.add(linkTo(UserController.class).slash(userId).slash("orders").withRel("orders"));
        return userModel;
    }
    
    public <T extends RepresentationModel<?>> T addOrderLinks(T orderModel, Long orderId, Long userId) {
        orderModel.add(linkTo(OrderController.class).slash(orderId).withSelfRel());
        orderModel.add(linkTo(UserController.class).slash(userId).withRel("user"));
        orderModel.add(linkTo(OrderController.class).slash(orderId).slash("items").withRel("items"));
        return orderModel;
    }
}

HTTP状态码应用

标准状态码使用

状态码应用规范

@RestController
public class HttpStatusController {
    
    // 2xx 成功状态码
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        UserDto userDto = userMapper.toDto(user);
        
        return ResponseEntity.ok(userDto); // 200 OK
    }
    
    @PostMapping("/users")
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserCreateRequest request) {
        User user = userService.createUser(userMapper.toEntity(request));
        UserDto userDto = userMapper.toDto(user);
        
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(userDto); // 201 Created
    }
    
    @PutMapping("/users/{id}")
    public ResponseEntity<Void> updateUser(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
        userService.updateUser(id, request);
        
        return ResponseEntity.noContent().build(); // 204 No Content
    }
    
    // 3xx 重定向状态码
    @GetMapping("/users/{id}/profile")
    public ResponseEntity<Void> redirectToProfile(@PathVariable Long id) {
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(id)
            .toUri();
            
        return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
            .location(location)
            .build(); // 301 Moved Permanently
    }
    
    // 4xx 客户端错误状态码
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
        try {
            User user = userService.findById(id);
            UserDto userDto = userMapper.toDto(user);
            return ResponseEntity.ok(userDto);
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build(); // 404 Not Found
        } catch (AccessDeniedException e) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); // 403 Forbidden
        }
    }
    
    @PostMapping("/users")
    public ResponseEntity<?> createUserWithValidation(@Valid @RequestBody UserCreateRequest request) {
        if (userService.existsByEmail(request.getEmail())) {
            ApiError error = new ApiError(HttpStatus.BAD_REQUEST, "邮箱已被使用");
            return ResponseEntity.badRequest().body(error); // 400 Bad Request
        }
        
        User user = userService.createUser(userMapper.toEntity(request));
        UserDto userDto = userMapper.toDto(user);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(userDto);
    }
    
    // 5xx 服务器错误状态码
    @PostMapping("/users/{id}/process")
    public ResponseEntity<Void> processUser(@PathVariable Long id) {
        try {
            userService.processUserData(userId);
            return ResponseEntity.ok().build();
        } catch (DatabaseException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); // 500 Internal Server Error
        } catch (ServiceUnavailableException e) {
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); // 503 Service Unavailable
        }
    }
}

自定义状态码

业务特定状态码

// 自定义业务异常
public class BusinessException extends RuntimeException {
    private final HttpStatus httpStatus;
    private final String errorCode;
    
    public BusinessException(String message, HttpStatus httpStatus, String errorCode) {
        super(message);
        this.httpStatus = httpStatus;
        this.errorCode = errorCode;
    }
    
    // getters
}

// 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ApiError> handleUserNotFound(UserNotFoundException ex) {
        ApiError error = new ApiError(HttpStatus.NOT_FOUND, ex.getMessage(), "USER_NOT_FOUND");
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ApiError> handleValidationException(ValidationException ex) {
        ApiError error = new ApiError(HttpStatus.BAD_REQUEST, ex.getMessage(), "VALIDATION_ERROR");
        return ResponseEntity.badRequest().body(error);
    }
    
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiError> handleBusinessException(BusinessException ex) {
        ApiError error = new ApiError(ex.getHttpStatus(), ex.getMessage(), ex.getErrorCode());
        return ResponseEntity.status(ex.getHttpStatus()).body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleGenericException(Exception ex) {
        logger.error("未处理的异常", ex);
        
        ApiError error = new ApiError(
            HttpStatus.INTERNAL_SERVER_ERROR,
            "内部服务器错误",
            "INTERNAL_ERROR");
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

API版本控制

URL路径版本控制

路径版本控制实现

// 版本1控制器
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserV1Controller {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV1Dto> getUserV1(@PathVariable Long id) {
        User user = userService.findById(id);
        UserV1Dto dto = UserV1Mapper.INSTANCE.toDtoV1(user);
        
        return ResponseEntity.ok(dto);
    }
    
    @PostMapping
    public ResponseEntity<UserV1Dto> createUserV1(@Valid @RequestBody UserCreateV1Request request) {
        User user = userService.createUser(UserV1Mapper.INSTANCE.toEntity(request));
        UserV1Dto dto = UserV1Mapper.INSTANCE.toDtoV1(user);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(dto);
    }
}

// 版本2控制器
@RestController
@RequestMapping("/api/v2/users")
@Validated
public class UserV2Controller {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV2Dto> getUserV2(@PathVariable Long id) {
        User user = userService.findById(id);
        UserV2Dto dto = UserV2Mapper.INSTANCE.toDtoV2(user);
        
        // V2版本增加了更多字段
        return ResponseEntity.ok(dto);
    }
    
    @PostMapping
    public ResponseEntity<UserV2Dto> createUserV2(@Valid @RequestBody UserCreateV2Request request) {
        User user = userService.createUser(UserV2Mapper.INSTANCE.toEntity(request));
        UserV2Dto dto = UserV2Mapper.INSTANCE.toDtoV2(user);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(dto);
    }
}

请求头版本控制

Accept版本控制

@RestController
@RequestMapping("/api/users")
public class UserVersionController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping(value = "/{id}", produces = {
        "application/vnd.company.app-v1+json",
        "application/vnd.company.app-v2+json"
    })
    public ResponseEntity<?> getUser(@PathVariable Long id, HttpServletRequest request) {
        User user = userService.findById(id);
        
        String acceptHeader = request.getHeader("Accept");
        
        if (acceptHeader.contains("v1")) {
            UserV1Dto dto = UserV1Mapper.INSTANCE.toDtoV1(user);
            return ResponseEntity.ok()
                .contentType(MediaType.valueOf("application/vnd.company.app-v1+json"))
                .body(dto);
        } else if (acceptHeader.contains("v2")) {
            UserV2Dto dto = UserV2Mapper.INSTANCE.toDtoV2(user);
            return ResponseEntity.ok()
                .contentType(MediaType.valueOf("application/vnd.company.app-v2+json"))
                .body(dto);
        } else {
            // 默认返回最新版本
            UserV2Dto dto = UserV2Mapper.INSTANCE.toDtoV2(user);
            return ResponseEntity.ok(dto);
        }
    }
}

版本管理策略

版本生命周期管理

@Service
public class ApiVersionService {
    
    private static final Map<String, String> VERSION_DEPRECATION_INFO = Map.of(
        "v1", "2024-12-31", // V1版本将于2024年12月31日废弃
        "v2", "2025-06-30"  // V2版本将于2025年6月30日废弃
    );
    
    public void addDeprecationHeaders(HttpServletResponse response, String version) {
        if (VERSION_DEPRECATION_INFO.containsKey(version)) {
            response.setHeader("Deprecation", "true");
            response.setHeader("Sunset", VERSION_DEPRECATION_INFO.get(version));
            response.setHeader("Link", "<https://api.example.com/docs/deprecation>; rel=\"deprecation\"");
        }
    }
    
    public boolean isVersionDeprecated(String version) {
        return VERSION_DEPRECATION_INFO.containsKey(version);
    }
    
    public String getLatestVersion() {
        return "v3";
    }
}

// 版本拦截器
@Component
public class ApiVersionInterceptor implements HandlerInterceptor {
    
    @Autowired
    private ApiVersionService versionService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestURI = request.getRequestURI();
        String version = extractVersionFromUri(requestURI);
        
        if (version != null && versionService.isVersionDeprecated(version)) {
            versionService.addDeprecationHeaders(response, version);
        }
        
        return true;
    }
    
    private String extract_versionFromUri(String uri) {
        Pattern pattern = Pattern.compile("/api/v(\\d+)/");
        Matcher matcher = pattern.matcher(uri);
        return matcher.find() ? matcher.group(1) : null;
    }
}

API安全实现

API密钥认证

API密钥管理

@Component
public class ApiKeyAuthenticationFilter extends GenericFilterBean {
    
    @Autowired
    private ApiKeyService apiKeyService;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String apiKey = request.getHeader("X-API-Key");
        
        if (apiKey != null) {
            try {
                ApiKeyInfo keyInfo = apiKeyService.validateApiKey(apiKey);
                
                if (keyInfo != null) {
                    // 设置安全上下文
                    SecurityContext context = SecurityContextHolder.createEmptyContext();
                    context.setAuthentication(new ApiKeyAuthentication(keyInfo));
                    SecurityContextHolder.setContext(context);
                    
                    // 设置请求限制
                    applyRateLimiting(keyInfo, httpRequest);
                    
                    chain.doFilter(request, response);
                    return;
                }
            } catch (Exception e) {
                logger.error("API密钥验证失败", e);
            }
        }
        
        // 无有效的API密钥
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("{\"error\":\"无效的API密钥\"}");
    }
    
    private void applyRateLimiting(ApiKeyInfo keyInfo, HttpServletRequest request) {
        // 根据API密钥类型应用不同的请求限制
        RateLimiter rateLimiter = getRateLimiterForKey(keyInfo);
        if (!rateLimiter.tryAcquire()) {
            throw new TooManyRequestsException("请求频率超限");
        }
    }
}

@Service
public class ApiKeyService {
    
    @Autowired
    private ApiKeyRepository apiKeyRepository;
    
    public ApiKeyInfo validateApiKey(String key) {
        Optional<ApiKey> apiKey = apiKeyRepository.findByKeyHash(hashKey(key));
        
        if (apiKey.isEmpty() || !apiKey.get().isActive()) {
            return null;
        }
        
        // 检查密钥是否过期
        ApiKey keyEntity = apiKey.get();
        if (keyEntity.getExpiresAt() != null && 
            keyEntity.getExpiresAt().isBefore(LocalDateTime.now())) {
            return null;
        }
        
        // 更新最后使用时间
        keyEntity.setLastUsedAt(LocalDateTime.now());
        apiKeyRepository.save(keyEntity);
        
        return mapToInfo(keyEntity);
    }
    
    public ApiKeyInfo createApiKey(String owner, ApiKeyType type) {
        String keyValue = generateApiKey();
        String keyHash = hashKey(keyValue);
        
        ApiKey apiKey = new ApiKey();
        apiKey.setOwner(owner);
        apiKey.setType(type);
        apiKey.setKeyHash(keyHash);
        apiKey.setActive(true);
        apiKey.setCreatedAt(LocalDateTime.now());
        apiKey.setExpiresAt(LocalDateTime.now().plusDays(365)); // 1年有效期
        
        ApiKey savedKey = apiKeyRepository.save(apiKey);
        savedKey.setKeyValue(keyValue); // 只在创建时返回明文密钥
        
        return mapToInfo(savedKey);
    }
    
    private String generateApiKey() {
        return "sk-" + UUID.randomUUID().toString().replace("-", "");
    }
    
    private String hashKey(String key) {
        return DigestUtils.sha256Hex(key + "salt"); // 加盐哈希
    }
    
    private ApiKeyInfo mapToInfo(ApiKey apiKey) {
        return ApiKeyInfo.builder()
            .id(apiKey.getId())
            .owner(apiKey.getOwner())
            .type(apiKey.getType())
            .keyValue(apiKey.getKeyValue()) // 注意:这里只在创建时设置
            .active(apiKey.isActive())
            .expiresAt(apiKey.getExpiresAt())
            .build();
    }
}

总结

Spring MVC RESTful API设计教程涵盖了现代API开发的核心要素:

核心技术要点

  1. REST设计原则 - 遵循REST架构约束和最佳实践
  2. HATEOAS实现 - 提供资源间的导航链接
  3. HTTP状态码 - 正确使用状态码表达API状态
  4. 版本控制 - 支持API演进和向后兼容
  5. 错误处理 - 统一的错误响应格式
  6. 安全实现 - API密钥认证和权限控制
  7. 性能优化 - 缓存策略和分页机制

企业级API价值

  • 可维护性:清晰的API结构和版本管理
  • 可扩展性:模块化的API设计支持功能扩展
  • 用户体验:一致的响应格式和错误处理
  • 开发者友好:完善的文档和示例代码

设计最佳实践

  1. 资源导向:以资源为核心的API设计
  2. 无状态:每个请求都是独立的
  3. 缓存友好:设计可缓存的响应
  4. 版本稳定:保持向后兼容性
  5. 错误明确:提供清晰的错误信息

通过这些教程的深入学习,您将掌握现代REST API的完整开发技能,为构建高质量的企业级Web服务打下坚实基础!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员小凯

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

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

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

打赏作者

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

抵扣说明:

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

余额充值