with one click
api-design-safety
// 当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。
// 当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。
| name | api-design-safety |
| description | 当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。 |
当设计或修改 REST API 响应结构时,防止常见的设计缺陷。
场景: 返回类型为 String 时,Java 重载解析可能匹配错误的方法
Java 方法重载解析时,String 类型参数会优先匹配 success(String message) 而非 success(T data),导致数据进入错误的字段。
// ApiResponse 有两个重载:
public static <T> ApiResponse<T> success(T data)
public static <T> ApiResponse<T> success(String message, T data)
// ❌ 错误: String 类型匹配到 success(String message)
String avatarUrl = "http://example.com/avatar.jpg";
return ApiResponse.success(avatarUrl);
// 结果: {"code":200, "message":"http://...", "data":null}
// 前端 data.data 拿到 null,导致功能异常
// ✅ 方案1: 明确指定 message 参数(推荐)
return ApiResponse.success("上传成功", avatarUrl);
// 结果: {"code":200, "message":"上传成功", "data":"http://..."}
// ✅ 方案2: 使用泛型明确类型
return ApiResponse.<String>success(avatarUrl);
// ✅ 方案3: 包装为 DTO(复杂场景推荐)
return ApiResponse.success(new UploadResult(avatarUrl));
data 字段(而非 message)场景: message 和 data 字段职责混淆
| 字段 | 用途 | 类型 | 示例 |
|---|---|---|---|
code | 业务状态码 | int | 200, 400, 500 |
message | 用户可读的提示信息 | String | "上传成功", "参数错误" |
data | 业务数据 | T | {"url": "..."}, [...] |
timestamp | 响应时间戳 | String | ISO 8601 格式 |
// ❌ 错误: 把业务数据放在 message
return ApiResponse.success("avatars/2026-04/xxx.jpeg");
// ❌ 错误: message 包含技术细节
return ApiResponse.error("NullPointerException at line 42");
// ✅ message 是用户提示,data 是业务数据
return ApiResponse.success("上传成功", avatarUrl);
// ✅ 错误信息对用户友好
return ApiResponse.error("文件格式不支持,请上传 JPG/PNG 格式");
场景: 无数据时返回 null、{}、[] 不统一
| 场景 | 推荐返回 | 说明 |
|---|---|---|
| 单个对象不存在 | data: null | 前端判断 if (!data) |
| 列表为空 | data: [] | 前端可直接遍历 |
| 分页数据为空 | data: {list: [], total: 0} | 保持结构一致 |
// ❌ 错误: 有时返回 null,有时返回空对象
if (user == null) {
return ApiResponse.success(null); // 不一致
}
return ApiResponse.success(new UserVO());
// ✅ 统一返回 null 表示不存在
if (user == null) {
return ApiResponse.success(null);
}
return ApiResponse.success(userVO);
// ✅ 列表统一返回空数组
List<UserVO> users = userService.list();
return ApiResponse.success(users); // 永远不返回 null
场景: 业务失败时返回 HTTP 500
| 场景 | HTTP 状态码 | 业务 code | 说明 |
|---|---|---|---|
| 成功 | 200 | 200 | 正常响应 |
| 参数错误 | 200 | 400 | 业务层校验失败 |
| 未授权 | 401 | - | 认证失败 |
| 无权限 | 403 | - | 授权失败 |
| 资源不存在 | 200 | 404 | 业务资源不存在 |
| 服务器错误 | 500 | - | 代码异常 |
// ❌ 错误: 业务失败返回 HTTP 500
if (user == null) {
throw new RuntimeException("用户不存在"); // HTTP 500
}
// ✅ 业务失败返回 HTTP 200 + 业务 code
if (user == null) {
return ApiResponse.error(404, "用户不存在"); // HTTP 200
}
// ✅ 只有代码异常才返回 HTTP 500
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleException(Exception e) {
log.error("服务器错误", e);
return ResponseEntity.status(500)
.body(ApiResponse.error("服务器错误,请稍后重试"));
}
场景: 导出 Excel/CSV/PDF、生成对账文件、推送第三方接口、批量通知等"产生下游消费产物"的功能,关键字段缺失时用空字符串/null/默认值兜底,文件能生成、接口能返回 200,但下游消费方(微信导入、对账系统、第三方平台)会静默失败或拒收。
业务方关注"接口返回 200"和"文件生成成功",但真实成功的判断标准在下游消费方:
mchId 和 transactionId 必须存在,缺失会拒收订单号 / 金额 / 时间,缺一项整批驳回快递公司 和 快递单号 都存在,否则物流端报错如果生成时缺值用 "" / null / 0 兜底,调用方会以为成功,等下游消费失败时才回头排查,浪费时间且影响业务。
// ❌ 错误: 关键字段缺失时静默用空值兜底,Excel 能生成但微信导入失败
public byte[] export(Long tenantId, List<Long> orderIds) {
String mchId = miniAppConfigRepository.findByTenantId(tenantId)
.map(TenantMiniAppConfig::getMchId)
.orElse("");
Map<Long, PaymentRecord> paymentMap = loadPaymentMap(tenantId, orders);
for (TradeOrder order : orders) {
PaymentRecord payment = paymentMap.get(order.getId());
String transactionId = payment != null ? payment.getTransactionId() : "";
String company = packages.stream().map(ShippingPackage::getCompany)
.filter(StringUtils::hasText).distinct().collect(joining(";"));
// transactionId 为空仍会写入 Excel,微信发货模板按位置导入时会拒收
writeRow(sheet, order, mchId, transactionId, company, ...);
}
return workbook.toByteArray();
}
「必填字段一次画全 + 生成前预校验 + 缺失列出业务 ID + 整体失败」
public byte[] export(Long tenantId, List<Long> orderIds) {
TenantMiniAppConfig config = miniAppConfigRepository.findByTenantId(tenantId)
.orElseThrow(() -> new BusinessException("未配置小程序"));
if (!StringUtils.hasText(config.getMchId())) {
throw new BusinessException("小程序未配置微信支付商户号,无法导出");
}
Map<Long, PaymentRecord> paymentMap = loadPaymentMap(tenantId, orders);
validateRequiredFields(orders, paymentMap);
return buildWorkbook(orders, paymentMap, config.getMchId());
}
private void validateRequiredFields(List<TradeOrder> orders, Map<Long, PaymentRecord> paymentMap) {
List<String> missingTxn = new ArrayList<>();
List<String> missingPackage = new ArrayList<>();
List<String> missingCompany = new ArrayList<>();
for (TradeOrder order : orders) {
PaymentRecord payment = paymentMap.get(order.getId());
if (payment == null || !StringUtils.hasText(payment.getTransactionId())) {
missingTxn.add(order.getOrderNumber());
}
List<ShippingPackage> packages = resolvePackages(order);
if (packages.isEmpty()) {
missingPackage.add(order.getOrderNumber());
} else if (packages.stream().anyMatch(p -> !StringUtils.hasText(p.getCompany()))) {
missingCompany.add(order.getOrderNumber());
}
}
if (!missingTxn.isEmpty()) {
throw new BusinessException("以下订单缺少微信支付交易单号,无法导出: " + String.join("、", missingTxn));
}
if (!missingPackage.isEmpty()) {
throw new BusinessException("以下订单缺少快递单号,无法导出: " + String.join("、", missingPackage));
}
if (!missingCompany.isEmpty()) {
throw new BusinessException("以下订单缺少快递公司,无法导出: " + String.join("、", missingCompany));
}
}
下手前先列完整清单,否则 review 一轮发现一个,会出现"修了 mchId/transactionId,下轮才发现快递公司/单号"的反复返工:
| 层次 | 含义 | 例子 |
|---|---|---|
| 业务字段 | 每条业务记录自身必填 | 订单号、金额、数量、订单状态 |
| 依赖配置 | 整个产物生成所需的全局配置 | mchId、API 凭证、模板 ID、签名密钥 |
| 外键关联 | 业务记录必须关联到的其他实体 | 关联的支付记录(且交易号非空)、物流单(且公司+单号都在)、收件地址 |
判断"必填"的依据来自下游消费方文档(微信开放平台导入文档、对账规范、第三方推送 API spec),不是来自源数据是否方便填。
完整的 Java(POI)/ Go(excelize)/ TypeScript(exceljs)实现示例见 references/multi-lang-examples.md。
返回值设计:
message 字段是否只包含用户可读的提示信息data 字段是否只包含业务数据状态码设计:
code 字段前后端协议:
data 字段data: null 的情况产物完整性(生成 Excel/CSV/PDF/对账文件/批量推送时,参见陷阱 #5):
场景:在 Service 里用 throw new IllegalArgumentException("系统分类不可用") 表达业务错误,前端拿到 500 + 一坨堆栈。
Spring 默认全局异常处理把 IllegalArgumentException / IllegalStateException / RuntimeException 等框架/JDK 异常都归类为"代码 bug",返回 HTTP 500 + 通用错误。
项目通常有自定义的 BusinessException(或 BizException),全局 handler 把它处理成 HTTP 200 + 业务 code 4xx + 用户友好的 message。
用错异常类型 → 前端拿不到正确的 message、用户看不到原因、监控告警被业务错刷屏。
// ❌ 框架异常被全局 handler 处理成 500
public void updateMapping(Long id, Long categoryId) {
ProductCategory category = repository.findById(categoryId)
.orElseThrow(() -> new IllegalArgumentException("系统分类不存在"));
if (category.getStatus() != ACTIVE) {
throw new IllegalArgumentException("系统分类不可用"); // 500 + 堆栈
}
}
// ✅ 用项目自定义业务异常
public void updateMapping(Long id, Long categoryId) {
ProductCategory category = repository.findById(categoryId)
.orElseThrow(() -> new BusinessException("系统分类不存在"));
if (category.getStatus() != ACTIVE) {
throw new BusinessException("系统分类不可用"); // 200 + code 4xx + 友好 message
}
}
| 场景 | 抛什么异常 | 全局 handler 处理 | HTTP 状态 |
|---|---|---|---|
| 业务规则不满足(金额超额、状态不允许) | BusinessException | 200 + code 4xx + 业务 message | 200 |
| 参数格式错误(手动校验) | BusinessException 或 @Valid 触发的 MethodArgumentNotValidException | 200 + code 400 + 字段提示 | 200 |
| 资源不存在 | BusinessException 或自定义 NotFoundException | 200 + code 404 + 业务 message | 200 |
| 无权限 | AccessDeniedException(Spring Security 处理) | 403 | 403 |
| 未认证 | AuthenticationException(Spring Security 处理) | 401 | 401 |
| 代码 bug(不可能 null、断言失败) | IllegalStateException / AssertionError | 500 + 通用错误 + 告警 | 500 |
| 外部依赖故障(远程 API 5xx、DB 断连) | 让框架异常冒泡,全局 handler 转 500 | 500 + 通用错误 + 告警 | 500 |
核心原则:
BusinessException(用户可理解、不该告警)代码评审看到以下任一立即怀疑:
throw new IllegalArgumentException(...) 或 throw new RuntimeException(...) 包业务错orElseThrow(() -> new IllegalArgumentException(...)) 表达"找不到资源"BusinessException 的分支BusinessException(不是 IllegalArgumentException)orElseThrow 是否用业务异常BusinessException(200 + code 4xx) vs 其他异常(500)> 📋 本回复遵循:`api-design-safety` - API 设计安全规范
一键安装 cc-use-exp 配置体系到 Codex CLI
结构化 Codex 配置与任务状态检查工作流,适用于显式 status、配置诊断、同步结果核对或任务盘点场景;聚焦 Codex 配置与项目内 .codex 任务状态。
当 API/任务可能执行超过 10 秒(批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合)时触发。防止同步接口被网关 30s 超时切断、用户重复点击触发并发、状态缓存内存泄漏等问题。提供异步任务状态机标准模板。
Bash 脚本与系统命令规范。禁止行尾注释,强制使用 tee/heredoc 写入,提升命令执行的可维护性。
当编写新模块、设计接口、重构代码或代码审查时触发。提供经典模块化六原则检查清单(大小适中/调用深度/扇入扇出/边界清晰/作用域内聚/可预测性),适用于 PR/Review/新模块设计场景。
当重构涉及字段映射(dataIndex、枚举映射、类型转换)时触发。防止字段名推测错误,确保字段映射的正确性。