后端API接口规范设计与实践指南

📅 2026/7/3 11:35:56 👁️ 阅读次数 📝 编程学习
后端API接口规范设计与实践指南

1. 为什么我们需要规范的后端API接口?

作为一名经历过多个企业级项目的老码农,我深刻体会到API接口规范的重要性。记得刚入行时参与的一个电商项目,由于缺乏统一的接口规范,前后端联调时各种问题层出不穷:返回数据结构不一致、错误码混乱、参数校验缺失...这些"技术债"最终导致项目延期三个月上线。

1.1 混乱接口的典型症状

先来看几个常见的反面案例:

// 反例1:随意返回数据 @GetMapping("/user") public Object getUser() { if(Math.random() > 0.5) { return userService.list(); // 返回List } else { return new HashMap<>(); // 返回Map } } // 反例2:裸抛异常 @PostMapping("/order") public String createOrder(@RequestBody OrderDTO dto) { if(dto.getAmount() == null) { throw new RuntimeException("金额不能为空"); // 直接抛出RuntimeException } return orderService.create(dto); } // 反例3:参数校验缺失 @GetMapping("/product") public ProductVO getProduct(Integer id) { // 没有校验id是否为空 return productService.getById(id); }

这些代码的问题在于:

  • 返回数据结构不可预期
  • 错误处理方式粗暴
  • 参数校验完全依赖业务代码
  • 没有统一的异常处理机制

1.2 规范接口的核心价值

规范的API接口应该具备以下特征:

  1. 一致性:所有接口遵循相同的结构和约定
  2. 可预测性:调用方可以准确预知接口行为
  3. 健壮性:能够优雅处理各种异常情况
  4. 自描述性:通过接口本身就能理解其用途
// 正例:规范的接口示例 @GetMapping("/users/{id}") public Result<UserVO> getUser(@PathVariable @Min(1) Long id) { return Result.success(userService.getUserById(id)); }

这样的接口:

  • 使用Result统一包装返回结果
  • 通过@Min注解明确参数校验规则
  • 路径命名符合RESTful规范
  • 返回类型明确是UserVO

2. 构建规范API的核心组件

2.1 统一返回结构设计

统一的返回结构应该包含三个基本要素:

  1. 状态码:明确操作结果
  2. 消息:可读的提示信息
  3. 数据:实际业务数据
@Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private Integer code; private String message; private T data; // 成功响应 public static <T> Result<T> success(T data) { return new Result<>(200, "成功", data); } // 失败响应 public static <T> Result<T> fail(Integer code, String message) { return new Result<>(code, message, null); } }

实际项目中,我们可以进一步优化这个结构:

  1. 使用枚举管理状态码
public enum ResultCode { SUCCESS(200, "操作成功"), BAD_REQUEST(400, "参数错误"), UNAUTHORIZED(401, "未授权"), FORBIDDEN(403, "禁止访问"), NOT_FOUND(404, "资源不存在"), SERVER_ERROR(500, "服务器错误"); private final Integer code; private final String message; // constructor and getters }
  1. 支持链式调用
public Result<T> code(Integer code) { this.code = code; return this; } public Result<T> message(String message) { this.message = message; return this; }
  1. 添加扩展字段
private Map<String, Object> extra; // 额外信息 public Result<T> addExtra(String key, Object value) { if(extra == null) { extra = new HashMap<>(); } extra.put(key, value); return this; }

2.2 自动统一包装

手动包装每个返回结果既繁琐又容易遗漏,我们可以利用Spring的ResponseBodyAdvice实现自动包装:

@RestControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Object> { // 排除不需要包装的返回类型 @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return !returnType.getParameterType().equals(Result.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 处理String类型特殊处理 if(body instanceof String) { return JSON.toJSONString(Result.success(body)); } // 已经包装过的直接返回 if(body instanceof Result) { return body; } return Result.success(body); } }

注意事项

  1. 需要单独处理String类型返回值,因为String有专门的HttpMessageConverter
  2. 可以通过注解排除特定接口的自动包装
  3. 对于文件下载等特殊接口需要额外处理

2.3 完善的参数校验

参数校验是API可靠性的第一道防线。Spring提供了强大的校验机制:

2.3.1 声明式校验
@Data public class UserDTO { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20个字符") private String username; @NotNull(message = "年龄不能为空") @Min(value = 1, message = "年龄最小1岁") @Max(value = 150, message = "年龄最大150岁") private Integer age; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式不正确") private String mobile; }
2.3.2 分组校验

不同场景可能需要不同的校验规则:

public interface CreateGroup {} // 创建时校验组 public interface UpdateGroup {} // 更新时校验组 @Data public class ProductDTO { @Null(groups = CreateGroup.class, message = "创建时ID必须为空") @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空") private Long id; @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}) private String name; } // 使用分组校验 @PostMapping public Result create(@Validated(CreateGroup.class) @RequestBody ProductDTO dto) { // ... }
2.3.3 自定义校验

当内置注解不能满足需求时,可以自定义校验规则:

@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EnumValueValidator.class) public @interface EnumValue { String message() default "值不在指定范围内"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<? extends Enum<?>> enumClass(); // 枚举类 String enumMethod() default "name"; // 校验方法 } public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> { private Class<? extends Enum<?>> enumClass; private String enumMethod; @Override public void initialize(EnumValue constraintAnnotation) { enumClass = constraintAnnotation.enumClass(); enumMethod = constraintAnnotation.enumMethod(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if(value == null) return true; try { // 反射调用枚举的校验方法 Method method = enumClass.getMethod(enumMethod); for(Enum<?> enumVal : enumClass.getEnumConstants()) { if(value.equals(method.invoke(enumVal))) { return true; } } return false; } catch (Exception e) { throw new RuntimeException(e); } } }

使用自定义注解:

public enum Gender { MALE, FEMALE; } @Data public class PersonDTO { @EnumValue(enumClass = Gender.class, message = "性别只能是MALE或FEMALE") private String gender; }

2.4 异常处理机制

良好的异常处理应该做到:

  1. 业务异常与系统异常分离
  2. 异常信息友好可读
  3. 异常处理集中管理
2.4.1 自定义异常体系
// 基础业务异常 public class BusinessException extends RuntimeException { private final Integer code; public BusinessException(Integer code, String message) { super(message); this.code = code; } public BusinessException(ResultCode resultCode) { super(resultCode.getMessage()); this.code = resultCode.getCode(); } } // 具体业务异常 public class UserNotFoundException extends BusinessException { public UserNotFoundException() { super(ResultCode.USER_NOT_FOUND); } } public class PermissionDeniedException extends BusinessException { public PermissionDeniedException() { super(ResultCode.PERMISSION_DENIED); } }
2.4.2 全局异常处理
@RestControllerAdvice public class GlobalExceptionHandler { // 处理业务异常 @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { log.error("业务异常: {}", e.getMessage(), e); return Result.fail(e.getCode(), e.getMessage()); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return Result.fail(ResultCode.BAD_REQUEST.getCode(), message); } // 处理系统异常 @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常: {}", e.getMessage(), e); return Result.fail(ResultCode.SERVER_ERROR); } }
2.4.3 异常处理最佳实践
  1. 异常分类处理:不同类型的异常应该有不同的处理方式
  2. 异常信息国际化:根据请求头Accept-Language返回对应语言的错误信息
  3. 异常日志记录:关键业务异常需要记录详细日志
  4. 异常重试机制:对于可重试的异常自动重试
// 带重试机制的异常处理示例 @Retryable(value = {RemoteAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public Result<String> callRemoteService() { // 调用远程服务 return remoteService.call(); } @Recover public Result<String> recover(RemoteAccessException e) { return Result.fail(ResultCode.REMOTE_SERVICE_ERROR); }

3. 高级API设计技巧

3.1 接口版本管理

随着业务发展,API可能需要变更。良好的版本管理策略可以平滑过渡:

  1. URL路径版本控制
/api/v1/users /api/v2/users
  1. 请求头版本控制
Accept: application/vnd.myapi.v1+json
  1. 实现方案
@RestController @RequestMapping("/api/v{version}/users") public class UserController { @GetMapping public Result<List<UserVO>> getUsers( @PathVariable String version, @RequestParam(required = false) String name) { if("1".equals(version)) { // v1版本逻辑 } else if("2".equals(version)) { // v2版本逻辑 } // ... } }

3.2 接口文档自动化

手动维护接口文档容易过时,推荐使用自动化工具:

  1. Swagger集成
@Configuration @EnableOpenApi public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.OAS_30) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("API文档") .description("系统接口文档") .version("1.0") .build(); } }
  1. 接口注释规范
@Operation(summary = "获取用户列表") @ApiResponses({ @ApiResponse(responseCode = "200", description = "成功"), @ApiResponse(responseCode = "500", description = "服务器错误") }) @GetMapping("/users") public Result<List<UserVO>> getUsers( @Parameter(description = "用户名模糊查询") @RequestParam(required = false) String name, @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) { // ... }

3.3 接口安全设计

  1. 认证与授权
@GetMapping("/users/me") public Result<UserVO> getCurrentUser(@AuthenticationPrincipal UserDetails user) { // 获取当前登录用户信息 return Result.success(userService.getById(user.getUsername())); }
  1. 接口限流
@RateLimiter(value = 10, key = "#userId") // 每秒10次 @GetMapping("/users/{userId}/orders") public Result<List<OrderVO>> getUserOrders(@PathVariable String userId) { // ... }
  1. 敏感数据脱敏
@Data public class UserVO { private String username; @JsonSerialize(using = SensitiveSerializer.class) private String mobile; // 手机号脱敏 @JsonSerialize(using = SensitiveSerializer.class) private String idCard; // 身份证脱敏 } public class SensitiveSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { // 实现脱敏逻辑 gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); } }

3.4 性能优化技巧

  1. 响应数据过滤
@JsonFilter("userFilter") @Data public class UserDetailVO { private String username; private String email; private String mobile; // ... } @GetMapping("/users/{id}") public MappingJacksonValue getUser(@PathVariable Long id, @RequestParam(required = false) String fields) { UserDetailVO user = userService.getDetailById(id); MappingJacksonValue result = new MappingJacksonValue(Result.success(user)); if(StringUtils.hasText(fields)) { FilterProvider filters = new SimpleFilterProvider() .addFilter("userFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fields.split(","))); result.setFilters(filters); } return result; }
  1. 批量接口设计
@PostMapping("/users/batch") public Result<BatchResult> batchCreateUsers(@RequestBody List<@Valid UserDTO> users) { // 批量处理逻辑 return Result.success(userService.batchCreate(users)); } @Data public class BatchResult { private int successCount; private int failCount; private List<ErrorItem> errors; @Data @AllArgsConstructor public static class ErrorItem { private int index; private String message; } }
  1. 异步接口设计
@PostMapping("/report") public Result<String> generateReport() { String taskId = UUID.randomUUID().toString(); CompletableFuture.runAsync(() -> reportService.generate(taskId)); return Result.success(taskId); } @GetMapping("/report/{taskId}") public Result<ReportVO> getReport(@PathVariable String taskId) { return Result.success(reportService.getResult(taskId)); }

4. 实战:完整API设计示例

4.1 用户管理API

@RestController @RequestMapping("/api/v1/users") @Tag(name = "用户管理", description = "用户相关操作接口") public class UserController { @Autowired private UserService userService; @Operation(summary = "获取用户列表") @GetMapping public Result<PageResult<UserVO>> listUsers( @Parameter(description = "用户名") @RequestParam(required = false) String username, @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer pageSize) { PageQuery query = new PageQuery(pageNum, pageSize); PageResult<UserVO> result = userService.listUsers(username, query); return Result.success(result); } @Operation(summary = "获取用户详情") @GetMapping("/{userId}") public Result<UserDetailVO> getUserDetail( @Parameter(description = "用户ID") @PathVariable @Min(1) Long userId) { return Result.success(userService.getDetailById(userId)); } @Operation(summary = "创建用户") @PostMapping public Result<Long> createUser( @Parameter(description = "用户信息") @Valid @RequestBody UserCreateDTO dto) { return Result.success(userService.createUser(dto)); } @Operation(summary = "更新用户") @PutMapping("/{userId}") public Result<Void> updateUser( @Parameter(description = "用户ID") @PathVariable @Min(1) Long userId, @Parameter(description = "用户信息") @Valid @RequestBody UserUpdateDTO dto) { userService.updateUser(userId, dto); return Result.success(); } @Operation(summary = "删除用户") @DeleteMapping("/{userId}") public Result<Void> deleteUser( @Parameter(description = "用户ID") @PathVariable @Min(1) Long userId) { userService.deleteUser(userId); return Result.success(); } }

4.2 相关DTO定义

@Data @EqualsAndHashCode(callSuper = true) public class PageResult<T> extends Result<List<T>> { private Long total; private Integer pageNum; private Integer pageSize; public PageResult(List<T> data, Long total, Integer pageNum, Integer pageSize) { super(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); this.total = total; this.pageNum = pageNum; this.pageSize = pageSize; } } @Data public class UserCreateDTO { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20个字符") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度6-20个字符") private String password; @NotNull(message = "年龄不能为空") @Min(value = 1, message = "年龄最小1岁") @Max(value = 150, message = "年龄最大150岁") private Integer age; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式不正确") private String mobile; } @Data public class UserUpdateDTO { @NotNull(message = "年龄不能为空") @Min(value = 1, message = "年龄最小1岁") @Max(value = 150, message = "年龄最大150岁") private Integer age; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式不正确") private String mobile; }

4.3 服务层实现

@Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @Transactional(readOnly = true) public PageResult<UserVO> listUsers(String username, PageQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<User> users = userMapper.selectByUsername(username); PageInfo<User> pageInfo = new PageInfo<>(users); List<UserVO> userVOs = users.stream() .map(this::convertToVO) .collect(Collectors.toList()); return new PageResult<>(userVOs, pageInfo.getTotal(), query.getPageNum(), query.getPageSize()); } @Override @Transactional(readOnly = true) public UserDetailVO getDetailById(Long userId) { User user = userMapper.selectById(userId); if(user == null) { throw new UserNotFoundException(); } return convertToDetailVO(user); } @Override @Transactional public Long createUser(UserCreateDTO dto) { if(userMapper.existsByUsername(dto.getUsername())) { throw new BusinessException(ResultCode.USER_EXISTS); } User user = convertToEntity(dto); userMapper.insert(user); return user.getId(); } @Override @Transactional public void updateUser(Long userId, UserUpdateDTO dto) { User user = userMapper.selectById(userId); if(user == null) { throw new UserNotFoundException(); } BeanUtils.copyProperties(dto, user); userMapper.updateById(user); } @Override @Transactional public void deleteUser(Long userId) { if(!userMapper.existsById(userId)) { throw new UserNotFoundException(); } userMapper.deleteById(userId); } // 转换方法省略... }

5. 常见问题与解决方案

5.1 参数校验常见问题

问题1:校验注解不生效

可能原因:

  1. 没有在Controller方法参数前加@Valid或@Validated注解
  2. 校验的DTO类没有使用@Data或没有getter/setter方法
  3. 校验的字段是private且没有提供getter方法

解决方案

@PostMapping public Result create(@Valid @RequestBody UserDTO dto) { // 确保有@Valid或@Validated // ... } @Data // 确保有@Data或手动实现getter/setter public class UserDTO { @NotBlank private String username; // 确保不是final字段 }

问题2:国际化消息不生效

配置步骤:

  1. 创建ValidationMessages.properties文件
  2. 配置MessageSource
  3. 在注解中使用{}引用消息key
# ValidationMessages.properties user.name.notblank=用户名不能为空 user.name.size=用户名长度必须在{min}到{max}之间
@Data public class UserDTO { @NotBlank(message = "{user.name.notblank}") @Size(min = 2, max = 20, message = "{user.name.size}") private String username; }

5.2 异常处理常见问题

问题1:自定义异常没有被全局处理器捕获

可能原因:

  1. 异常没有被抛出到Controller层(在Service内部被捕获)
  2. 全局异常处理器没有扫描到对应的包
  3. 异常类型不匹配

解决方案

// 确保异常抛出到Controller @Service public class UserService { public User getById(Long id) { User user = userRepository.findById(id); if(user == null) { throw new UserNotFoundException(); // 不要捕获这个异常 } return user; } } // 确保全局处理器扫描正确包 @RestControllerAdvice(basePackages = "com.example.controller") public class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) public Result handleUserNotFound(UserNotFoundException e) { // ... } }

问题2:异常信息暴露敏感数据

错误做法:

try { // 调用第三方服务 } catch (Exception e) { throw new BusinessException("调用XX服务失败:" + e.getMessage()); // 暴露底层错误 }

正确做法:

try { // 调用第三方服务 } catch (Exception e) { log.error("调用XX服务失败", e); // 记录完整日志 throw new BusinessException(ResultCode.THIRD_PARTY_ERROR); // 返回友好提示 }

5.3 性能优化常见问题

问题1:N+1查询问题

典型场景:

public List<OrderVO> getUserOrders(Long userId) { List<Order> orders = orderMapper.selectByUserId(userId); // 1次查询 return orders.stream() .map(order -> { User user = userMapper.selectById(order.getUserId()); // N次查询 return convertToVO(order, user); }) .collect(Collectors.toList()); }

解决方案

  1. 使用JOIN查询:
SELECT o.*, u.name as user_name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.user_id = #{userId}
  1. 使用MyBatis的关联查询:
<resultMap id="orderWithUser" type="OrderVO"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <association property="user" javaType="UserVO"> <id property="id" column="user_id"/> <result property="name" column="user_name"/> </association> </resultMap> <select id="selectWithUser" resultMap="orderWithUser"> SELECT o.*, u.name as user_name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.user_id = #{userId} </select>
  1. 使用批量查询:
public List<OrderVO> getUserOrders(Long userId) { List<Order> orders = orderMapper.selectByUserId(userId); List<Long> userIds = orders.stream() .map(Order::getUserId) .distinct() .collect(Collectors.toList()); Map<Long, User> userMap = userMapper.selectByIds(userIds).stream() .collect(Collectors.toMap(User::getId, Function.identity())); return orders.stream() .map(order -> convertToVO(order, userMap.get(order.getUserId()))) .collect(Collectors.toList()); }

问题2:大结果集内存溢出

错误做法:

@GetMapping("/export") public List<User> exportAllUsers() { return userMapper.selectAll(); // 可能返回百万条数据 }

解决方案

  1. 使用分页查询
  2. 使用流式处理
  3. 使用异步导出
@GetMapping("/export") public void exportAllUsers(HttpServletResponse response) throws IOException { response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=users.csv"); try(OutputStream out = response.getOutputStream(); CSVPrinter printer = new CSVPrinter(new OutputStreamWriter(out), CSVFormat.DEFAULT)) { int pageNum = 1; int pageSize = 1000; PageResult<User> page; do { page = userService.listUsers(null, new PageQuery(pageNum, pageSize)); for(User user : page.getData()) { printer.printRecord(user.getId(), user.getName(), user.getEmail()); } pageNum++; } while(page.getData().size() == pageSize); } }

6. 接口设计最佳实践

6.1 RESTful设计原则

  1. 资源命名

    • 使用名词复数形式:/users 而不是 /getUsers
    • 避免动词:/users/{id}/activation 而不是 /activateUser
  2. HTTP方法使用

    • GET:获取资源
    • POST:创建资源
    • PUT:全量更新资源
    • PATCH:部分更新资源
    • DELETE:删除资源
  3. 状态码使用

    • 200 OK:成功请求
    • 201 Created:资源创建成功
    • 204 No Content:成功但无返回内容
    • 400 Bad Request:客户端错误
    • 401 Unauthorized:未认证
    • 403 Forbidden:无权限
    • 404 Not Found:资源不存在
    • 500 Internal Server Error:服务器错误

6.2 分页查询规范

标准分页响应结构:

{ "code": 200, "message": "成功", "data": [ {"id": 1, "name": "用户1"}, {"id": 2, "name": "用户2"} ], "total": 100, "pageNum": 1, "pageSize": 10 }

实现方案:

@Data public class PageQuery { private Integer pageNum; private Integer pageSize; public PageQuery(Integer pageNum, Integer pageSize) { this.pageNum = pageNum == null || pageNum < 1 ? 1 : pageNum; this.pageSize = pageSize == null || pageSize < 1 ? 10 : pageSize; } public <E> Page<E> toPage() { return Page.of(pageNum, pageSize); } } public interface Page<T> extends List<T> { static <T> Page<T> of(int pageNum, int pageSize) { return PageHelper.startPage(pageNum, pageSize); } }

6.3 批量操作设计

批量创建

@PostMapping("/batch") public Result<BatchResult> batchCreate(@Valid @RequestBody List<@Valid UserCreateDTO> dtos) { return Result.success(userService.batchCreate(dtos)); }

批量更新

@PutMapping("/batch") public Result<BatchResult> batchUpdate(@Valid @RequestBody List<@Valid UserUpdateDTO> dtos) { return Result.success(userService.batchUpdate(dtos)); }

批量删除

@DeleteMapping("/batch") public Result<Void> batchDelete(@RequestParam List<Long> ids) { userService.batchDelete(ids); return Result.success(); }

6.4 接口幂等性设计

方案1:Token机制

@GetMapping("/token") public Result<String> createToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES); return Result.success(token); } @PostMapping("/order") public Result<Long> createOrder(@RequestHeader("X-Idempotent-Token") String token) { if(!redisTemplate.delete(token)) { throw new BusinessException(ResultCode.REPEAT_REQUEST); } return Result.success(orderService.create()); }

方案2:唯一索引

@Data public class OrderCreateDTO { @NotBlank private String orderNo; // 唯一订单号 // 其他字段 } @Service public class OrderService { @Transactional public Long create(OrderCreateDTO dto) { if(orderMapper.existsByOrderNo(dto.getOrderNo())) { throw new BusinessException(ResultCode.ORDER_EXISTS); } // 创建订单 } }

方案3:状态机

@Service public class OrderService { @Transactional public void pay(Long orderId) { Order order = orderMapper.selectById(orderId); if(order.getStatus() != OrderStatus.CREATED) { throw new BusinessException(ResultCode.ORDER_STATUS_ERROR); } order.setStatus(OrderStatus.PAID); orderMapper.updateById(order); } }

7. 接口测试策略

7.1 单元测试

Controller层测试:

@WebMvcTest(UserController.class) @AutoConfigureMockMvc class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void getUserById() throws Exception { UserVO user = new UserVO(1L, "test"); when(userService.getById(1L)).thenReturn(user); mockMvc.perform(get("/users/1") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.id").value(1)) .andExpect(jsonPath("$.data.username").value("test")); } }

7.2 集成测试

@SpringBootTest @AutoConfigureMockMvc class UserIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserMapper userMapper; @Test @Transactional @Rollback void createUser() throws Exception { UserCreateDTO dto = new UserCreateDTO(); dto.setUsername("test"); dto.setPassword("123456"); dto.setAge(20); mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(JSON.toJSONString(dto))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isNumber()); } }

7.3 契约测试

使用Pact进行契约测试:

  1. 消费者端测试
@RunWith(PactRunner.class) @Provider("userService") @Consumer("orderService") public class UserContractTest { @Pact(consumer = "orderService") public RequestResponsePact getUserById(PactDslWithProvider builder) { return builder .given("user exists") .uponReceiving("get user by id") .path("/users/1") .method("GET") .willRespondWith() .status(200) .body(new PactDslJsonBody() .integerType("code", 200) .stringType("message", "成功") .object("data") .integerType("id", 1) .stringType("username", "test") .closeObject()) .toPact(); } @Test @PactVerification(fragment = "getUserById") public void testGetUserById() { UserClient client = new UserClient("http://localhost:8080"); User user = client.getUserById(1L); assertThat(user.getId()).isEqualTo(1L); assertThat(user.getUsername()).isEqualTo("test"); } }
  1. 提供者端验证
@RunWith(PactRunner.class) @Provider("userService") @PactFolder("pacts") public class UserProviderTest { @TestTarget public final Target target = new HttpTarget(8080); @State("user exists") public void userExists() { // 准备测试数据 User user = new User(1L, "test"); userRepository.save(user); } }

7.4 性能测试

使用JMeter进行性能测试:

  1. 测试计划
  • 线程组:模拟并发