找回密码
 立即注册
首页 业界区 业界 用户只需要知道「怎么办」,不需要知道「为什么炸了」 ...

用户只需要知道「怎么办」,不需要知道「为什么炸了」

琦谓 昨天 07:03
大家好,我是晓凡。
写在前面

一到月初或者月末(某些业务操作大规模爆发的时候),手机狂震,生产告警狂轰滥炸:xxx接口超时、用户中心 CPU 飙到 98%……
运维在群里疯狂 @ 你,你却只能回一句“我本地是好的”。
别问,问就是接口设计欠下的技术债。
下面,晓凡总结成 18 条可落地的接口设计“军规”。每条都配上“作死写法”与“保命写法”。
军规 1:路径必须永久不变

反面教材
  1. @RestController
  2. @RequestMapping("/getUserInfoByIdV2.3_beta")
  3. public class UserController { ... }
复制代码
产品说“V2.3_beta”只是临时版本,结果半年后,死活不敢下线。
正面写法
  1. @RestController
  2. @RequestMapping("/users")
  3. public class UserController {
  4.     @GetMapping("/{uid}")
  5.     public UserDTO get(@PathVariable Long uid) { ... }
  6. }
复制代码
版本号放到 Header:Accept: application/vnd.myapp.v2+json
路由一旦上线,就是“墓碑”,永远不许动,哪怕老板喊重构。
军规 2:命名只准用名词,禁止动词

反面教材
  1. @PostMapping("/createOrder")
  2. @PostMapping("/addOrder")
  3. @PostMapping("/insertOrder")
复制代码
同一个业务三个入口,新人入职三天就开始迷路。
正面写法
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody CreateOrderCommand cmd) { ... }
复制代码
HTTP 动词已经表达“创建”语义,别再动词叠 buff。
军规 3:统一用复数

反面教材
  1. @GetMapping("/order/{id}")
  2. @GetMapping("/orders")
复制代码
单复数混用,前端拼接 URL 得写 if/else,特别容易出错。
正面写法
  1. @GetMapping("/orders/{id}")
  2. @GetMapping("/orders")
复制代码
集合与成员保持一致,前端直接模板字符串 ${host}/orders/${id},代码干净又整洁。
军规 4:分页参数必须“三件套”

反面教材
  1. @GetMapping("/orders")
  2. public List<Order> list(@RequestParam int offset,
  3.                         @RequestParam int limit) { ... }
复制代码
参数名随心所欲,前端封装 不知道骂了你多少次。
正面写法
  1. @GetMapping("/orders")
  2. public PageResult<OrderDTO> list(
  3.         @RequestParam(defaultValue = "1") @Min(1) int page,
  4.         @RequestParam(defaultValue = "20") @Min(1) @Max(100) int perPage) {
  5.     long total = orderMapper.count();
  6.     List<OrderDTO> data = orderMapper.selectPage((page - 1) * perPage, perPage);
  7.     return PageResult.<OrderDTO>builder()
  8.             .data(data)
  9.             .totalCount(total)
  10.             .hasNext(page * perPage < total)
  11.             .build();
  12. }
复制代码
返回统一包装:
  1. @Data
  2. @Builder
  3. public class PageResult<T> {
  4.     private List<T> data;
  5.     private long totalCount;
  6.     private boolean hasNext;
  7. }
复制代码
军规 5:字段命名一律小写加下划线

反面教材
  1. {"userName":"Jack","userAge":18}
复制代码
前端 axios 自动把下划线转小驼峰,结果文档对不上,联调 2 小时。
正面写法
  1. @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
  2. public class UserDTO {
  3.     private String userName;
  4.     private Integer userAge;
  5. }
复制代码
返回即:
  1. {"user_name":"Jack","user_age":18}
复制代码
前后端一人一把尺子,永远对得上。
军规 6:枚举值禁止用魔法数字

反面教材
  1. if (order.getStatus() == 3) { ... }   // 3 代表啥?鬼知道
复制代码
DB 改个值,线上直接 500。
正面写法
  1. public enum OrderStatus {
  2.     CREATED(10),
  3.     PAID(20),
  4.     SHIPPED(30),
  5.     DONE(40);
  6.     private final int code;
  7.     OrderStatus(int code) { this.code = code; }
  8.     public int getCode() { return code; }
  9. }
复制代码
实体与数据库均存 code:
  1. @Converter(autoApply = true)
  2. public class OrderStatusConverter implements AttributeConverter<OrderStatus, Integer> {
  3.     public Integer convertToDatabaseColumn(OrderStatus s) { return s.getCode(); }
  4.     public OrderStatus convertToEntityAttribute(Integer c) {
  5.         return Arrays.stream(OrderStatus.values())
  6.                      .filter(e -> e.getCode() == c)
  7.                      .findFirst()
  8.                      .orElseThrow(() -> new IllegalArgumentException("unknown code " + c));
  9.     }
  10. }
复制代码
代码里只有枚举,没有魔法数字。
军规 7:接收参数必须 DTO,禁止 Map

反面教材
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody Map<String,Object> map) {
  3.     Integer skuId = (Integer) map.get("skuId");  // 强转爆炸
  4. }
复制代码
Map 一把梭,编译期 0 提示,运行时 ClassCastException 随机出现。
正面写法
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody @Valid CreateOrderCommand cmd) { ... }
  3. @Data
  4. public class CreateOrderCommand {
  5.     @NotNull
  6.     private Long skuId;
  7.     @NotNull @Min(1)
  8.     private Integer quantity;
  9. }
复制代码
校验失败自动 400,错误信息一目了然:
  1. {"field":"quantity","message":"must be greater than or equal to 1"}
复制代码
军规 8:统一返回包装,禁止裸奔

反面教材
  1. @GetMapping("/orders/{id}")
  2. public OrderDTO get(@PathVariable Long id) { ... }
复制代码
成功返回对象,失败返回字符串,前端得写三行 if 判断类型。
正面写法
  1. @RestControllerAdvice
  2. public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
  3.     public boolean supports(MethodParameter returnType, Class converterType) { return true; }
  4.     public Object beforeBodyWrite(Object body, MethodParameter returnType,
  5.                                   MediaType selectedContentType, Class selectedConverterType,
  6.                                   ServerHttpRequest request, ServerHttpResponse response) {
  7.         if (body instanceof CommonResult) return body;   // 避免二次包装
  8.         return CommonResult.success(body);
  9.     }
  10. }
  11. public class CommonResult<T> {
  12.     private int code;
  13.     private String msg;
  14.     private T data;
  15.     public static <T> CommonResult<T> success(T data) {
  16.         return new CommonResult<>(0, "ok", data);
  17.     }
  18. }
复制代码
前端唯一判断 code === 0,其余按错误弹窗。
军规 9:错误码必须分段

反面教材
  1. new RuntimeException("订单不存在");
复制代码
日志里只有一行文字,定位靠天意。
正面写法
  1. @Getter
  2. @AllArgsConstructor
  3. public enum ErrorEnum {
  4.     ORDER_NOT_FOUND(20001, "订单不存在"),
  5.     SKU_NOT_AVAILABLE(20002, "商品库存不足");
  6.     private final int code;
  7.     private final String message;
  8. }
复制代码
全局异常处理:
  1. @RestControllerAdvice
  2. public class GlobalExceptionHandler {
  3.     @ExceptionHandler(BizException.class)
  4.     public CommonResult<Void> handle(BizException ex) {
  5.         return CommonResult.fail(ex.getErrorEnum().getCode(),
  6.                                  ex.getErrorEnum().getMessage());
  7.     }
  8. }
复制代码
前端按码弹窗,20001 跳转“订单列表”,20002 跳转“商品详情”。
军规 10:接口必须幂等

反面教材
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody CreateOrderCommand cmd) {
  3.     return orderService.create(cmd);   // 每次调用都插新订单
  4. }
复制代码
用户狂点按钮,瞬间 5 单,客服哭晕。
正面写法
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody @Valid CreateOrderCommand cmd,
  3.                        HttpServletRequest request) {
  4.     String idempotencyKey = request.getHeader("Idempotency-Key");
  5.     if (idempotencyKey == null) throw new BizException(ErrorEnum MISSING_KEY);
  6.     return idempotencyService.execute(idempotencyKey, () -> orderService.create(cmd));
  7. }
复制代码
Redis 缓存 24h 唯一 KEY,重复请求直接返回第一次结果,0 重复订单。
军规 11:日期格式只准 ISO8601

反面教材
  1. {"createTime":"06/18/2025 09:05:12"}
复制代码
万一有国外项目,同事一脸懵:这是 6 月还是 18 月?
正面写法
  1. @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "UTC")
  2. private OffsetDateTime createTime;
复制代码
返回即:
  1. {"create_time":"2025-06-18T09:05:12+00:00"}
复制代码
前端 new Date('2025-06-18T09:05:12+00:00') 直接解析,时区 0 歧义。
军规 12:Long 主键后端转 String

反面教材
  1. {"orderId":9223372036854775807}
复制代码
JS 最大安全整数 2^53-1,订单号精度丢失,用户 A 的订单跑到用户 B。
正面写法
  1. @JsonSerialize(using = ToStringSerializer.class)
  2. private Long orderId;
复制代码
返回即:
  1. {"order_id":"9223372036854775807"}
复制代码
前端字符串传,精度不丢失。
军规 13:批量接口必须限制数量

反面教材
  1. @PostMapping("/orders/batch")
  2. public List<OrderDTO> batch(@RequestBody List<Long> ids) { ... }
复制代码
对方一次丢 10w 个 id,线程池直接拉满。
正面写法
  1. @PostMapping("/orders/batch")
  2. public List<OrderDTO> batch(@RequestBody @Size(max = 100) List<Long> ids) { ... }
复制代码
超过 100 直接 400,爱用不用。
军规 14:文件上传必须预签名

反面教材
  1. @PostMapping("/upload")
  2. public String upload(MultipartFile file) { ... }
复制代码
1G 视频直接把带宽打爆,Tomcat OOM。
正面写法
  1. @GetMapping("/upload/token")
  2. public UploadTokenDTO token(@RequestParam String suffix) {
  3.     String key = "private/" + UUID.randomUUID() + suffix;
  4.     String uploadUrl = ossClient.generatePresignedUrl(key,  ExpirationEnum.TEN_MINUTES);
  5.     return new UploadTokenDTO(uploadUrl, key);
  6. }
复制代码
前端拿到直传 OSS,服务端只存 key,流量 0 占用。
军规15:禁止把「内部错误码」直接抛给前端

反面教材
  1. catch (Exception e) {
  2.     log.error("RPC失败", e);
  3.     return CommonResult.fail(999, e.getMessage());   // 999 是什么?只有我自己懂
  4. }
复制代码
结果:
前端拿到 {code:999, msg:"Read timed out executing POST http://stock-service/lock"},直接把超时堆栈展示给用户,页面弹出「Read timed out…」——用户一脸懵,黑客倒开心,内网地址全暴露。
正面写法
1.对外错误码只保留「用户可理解」枚举,统一收敛:
  1. @AllArgsConstructor
  2. public enum FrontEndErrorEnum {
  3.     STOCK_UNAVAILABLE(5100, "商品库存不足"),
  4.     SYSTEM_BUSY(5101, "系统繁忙,请稍后重试"),
  5.     UNKNOWN_ERROR(5999, "网络走神了,稍后再试");
  6.     final int code;
  7.     final String message;
  8. }
复制代码
2.全局异常层做「内外翻译」——任何底层异常都不准穿透:
  1. @RestControllerAdvice
  2. public class ErrorTranslator {
  3.     @ExceptionHandler(Exception.class)
  4.     public CommonResult<Void> handle(Exception ex) {
  5.         log.error("Fetal error", ex);          // 详细堆栈只写日志
  6.         if (ex instanceof FeignException) {    // 下游超时
  7.             return CommonResult.fail(FrontEndErrorEnum.SYSTEM_BUSY);
  8.         }
  9.         return CommonResult.fail(FrontEndErrorEnum.UNKNOWN_ERROR);
  10.     }
  11. }
复制代码
3.前端拿到的是:
  1. {"code":5100,"msg":"商品库存不足"}
复制代码
既安全又友好,还方便做国际化——以后想换提示语,只改枚举即可
用户只需要知道「怎么办」,不需要知道「为什么炸了」。把堆栈留在日志,把尊严留给产品。
军规 16:对外暴露 Swagger,对内必须加注解

反面教材
  1. @RestController
  2. public class OrderController {
  3.     @PostMapping("/orders")
  4.     public OrderDTO create(CreateOrderCommand cmd) { ... }
  5. }
复制代码
文档靠口口相传,字段一旦改名,测试小姐姐提刀来找。
正面写法
  1. @Tag(name = "订单模块")
  2. @RestController
  3. public class OrderController {
  4.     @Operation(summary = "创建订单")
  5.     @ApiResponse(responseCode = "200", description = "成功")
  6.     @PostMapping("/orders")
  7.     public OrderDTO create(@RequestBody @Valid CreateOrderCommand cmd) { ... }
  8. }
复制代码
启动后 http://localhost:8080/swagger-ui.html 实时可见,改字段即报错,0 沟通成本。
军规 17:关键接口必须打印入参出参

反面教材
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody CreateOrderCommand cmd) {
  3.     return orderService.create(cmd);
  4. }
复制代码
线上出错,日志只有一行“NullPointerException”,想复现?随缘。
正面写法
  1. @PostMapping("/orders")
  2. public OrderDTO create(@RequestBody CreateOrderCommand cmd) {
  3.     log.info("create order req: {}", cmd);
  4.     OrderDTO dto = orderService.create(cmd);
  5.     log.info("create order rsp: {}", dto);
  6.     return dto;
  7. }
复制代码
配合 Logback 异步 + 脱敏:
  1.     <queueSize>2048</queueSize>
  2.    
  3. </appender>
复制代码
性能损耗 < 5%,问题排查速度大大提升。
军规 18:发版前必须做向后兼容扫描

反面教材
  1. // V1
  2. public class UserDTO { private String name; }
  3. // V2 直接把 name 改成 username
  4. public class UserDTO { private String username; }
复制代码
旧应用直接解析失败,用户体验非常不好
正面写法

  • 加新字段,不动旧字段:
  1. public class UserDTO {
  2.     private String name;        //  deprecated
  3.     private String username;    //  新字段
  4. }
复制代码

  • 使用 @Deprecated 注解,Swagger 自动标灰。
  • 配套单元测试:
  1. @Test
  2. void v1ClientShouldStillSeeNameField() throws Exception {
  3.     mockMvc.perform(get("/users/1")
  4.                    .header("Accept", "application/vnd.myapp.v1+json"))
  5.            .andExpect(jsonPath("$.name").exists())
  6.            .andExpect(jsonPath("$.username").doesNotExist());
  7. }
复制代码

  • 上线后观察 7 日,旧字段无调用再下线。
小结

接口设计不是炫技,而是写“半年后看了自己之前写的代码,还敢重构的代码勇气”。
这 18 条军规,一半来自我的踩坑,一半来自“别人踩过的坑”。
别嫌啰嗦,真正上线 0 告警的那天,你会来感谢我。
愿下次手机响起,只是外卖到了,不是 502。
我是晓凡,再小的帆也能远航
我们下期再见ヾ(•ω•`)o (●'◡'●)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册