diff --git a/changelogs-v2/2026-09/17_7491_Lock4j抢锁失败统一返100503-修改接口-管理后台.md b/changelogs-v2/2026-09/17_7491_Lock4j抢锁失败统一返100503-修改接口-管理后台.md new file mode 100644 index 00000000..8616d316 --- /dev/null +++ b/changelogs-v2/2026-09/17_7491_Lock4j抢锁失败统一返100503-修改接口-管理后台.md @@ -0,0 +1,234 @@ +--- +schema: "hl-changelog/v2" +ticket: "7491" +title: "所有 @Lock4j 抢锁失败统一返回 100503(不再返回 500)" +consumer: "multiple" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-17" +status_note: "抢锁失败分支的 code 由 500 改为 100503,HTTP 仍为 200;前端应把「可重试」提示挂到 100503 上。" +updated_at: "2026-09-17" +base: "dev-v3" +--- + +# common:所有 @Lock4j 抢锁失败统一返回 100503 + +> **服务**: 全部使用 `hl-starter-protection` 的 `@Lock4j` 后端服务(order-v3、fleet、mp、product-v2、resource、user、finance 等) +> **PR**: #7885(目标 dev-v3) +> **Issue**: #7491 +> **日期**: 2026-09-17 +> **影响范围**: 所有带 `@Lock4j` 的写接口在被别的请求占锁时的失败响应(code / message / 是否带 traceId) + +--- + +## ⚠️ 关键变化 + +所有带 `@Lock4j` 的端点在**抢锁失败**(等满 acquire-timeout 仍未拿到锁)时: + +- `code`:`500`(系统繁忙请稍后重试或联系客服) → **`100503`**(资源被占用,请稍后重试) +- `message`:`系统繁忙请稍后重试或联系客服` → **`资源被占用,请稍后重试`** +- HTTP 状态:仍为 **200**(业务失败仍走 HTTP 200 包络,未变为 4xx/5xx) +- `traceId`:该分支**不再返回** `traceId` 字段(此前 500 分支会带 traceId) + +**前端必须知道的一句话**:以前抢锁失败看起来像系统故障(500 + traceId),现在是一个明确的业务提示(100503 + 中文文案);请把「稍后重试」类提示挂到 **100503** 上,不要再按 500 走系统异常兜底。 + +--- + +## 一、背景 + +`hl-starter-protection` 的 `LockFailureExceptionHandler`(`@ExceptionHandler(LockException.class)`)从来没有被命中过:Spring MVC 先按 advice 顺序挑选 `ControllerAdviceBean`,再在该 bean 内部匹配异常类型,**不会跨 advice 比较哪个异常类型更具体**。`hl-common-log` 的 `GlobalExceptionHandler`(`@ExceptionHandler(Exception.class)`)注册在前,于是全部 `@Lock4j` 抢锁失败都以 `code=500` 的系统异常兜底返回,错误码表里的 `100503` 从未对外生效(由 #7324 的 AC-59 实测发现并转结本单)。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 团期按日订房(H4,示例端点) | POST | `/v3/admin/house/group-batches/{groupBatchId}/room-plans` | 修改响应 | 占锁时由 500 改为 100503 | +| 2 | 其余全部 `@Lock4j` 端点 | 任意 | 各服务 `@Lock4j` 写口 | 修改响应 | 同一共享 starter 行为,抢锁失败一律 100503 | + +--- + +## 三、接口详情 + +### 1. 团期按日订房 `POST /v3/admin/house/group-batches/{groupBatchId}/room-plans` + +**VO**: `GroupBatchRoomPlanSaveReqVO → Result>` + +#### 使用场景 + +房务按日提交订房计划。该端点带团期级 `@Lock4j`(锁键 `lock4j:house:gb:room#{groupBatchId}`,acquire-timeout 默认 3s)。当同一团期已有请求持锁且超过 3s 未释放时,后到请求返回 100503。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 正整数 | 团期主订单 ID | +| items | Body | List | ✅ | 1-200 行 | 订房行(stayDate / hotelId / roomTypeId / roomCount) | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | Integer | 抢锁失败时为 100503 | +| message | String | 抢锁失败时为 `资源被占用,请稍后重试` | +| data | null | 抢锁失败时为 null | +| traceId | — | **抢锁失败分支不再返回该字段** | +| success | Boolean | false | + +#### 请求示例 + +```http +POST /v3/admin/house/group-batches/999999100000021/room-plans +Authorization: Bearer *** +Content-Type: application/json + +{ "items": [ { "stayDate": "2027-07-01", "hotelId": 1, "roomTypeId": 1, "roomCount": 1 } ] } +``` + +#### 响应示例(占锁,抢锁失败) + +```json +{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "traceId": null, "success": false } +``` + +#### 空数据 / 降级响应 + +抢锁失败不是降级:请求**没有进入业务逻辑**(不写库、不扣库存)。释放锁后重试即可正常进入业务处理。 + +#### 错误响应 + +```json +{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "traceId": null, "success": false } +``` + +| 场景 | code | message | +|------|------|---------| +| 抢锁失败(本单变化点) | 100503 | 资源被占用,请稍后重试 | +| 3 秒内重复提交(幂等拦截,行为不变) | 100502 | 订房计划提交处理中,请勿重复提交 | +| 锁已释放后业务校验失败(行为不变) | 业务码(如 808691 / 589500 等) | 对应业务文案 | + +#### 业务边界 + +- 只在「等满 acquire-timeout(默认 3s)仍抢不到锁」时返回 100503;正常拿到锁的请求行为完全不变。 +- 抢锁失败不进入业务逻辑:不写库、不扣库存、不产生状态流转。 +- 幂等窗口(3s)内的重复提交仍返回 100502,与抢锁失败明确区分。 +- 锁释放后重试同一请求可立即进入业务处理(实测返回业务码而非 500/100503)。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误做法对照 + +| 场景 | 调用方行为 | +|------|------------| +| ✅ 收到 100503 | 按「资源被占用」给用户可重试提示;如需重试请间隔后再发,并保证重试请求体/幂等键语义正确 | +| ✅ 判断成功 | 只看 `success` / `code`,不要依赖 HTTP 状态码区分业务失败 | +| ❌ 把 100503 当系统故障 | 不要再按 500 走「系统繁忙/联系客服」兜底文案 | +| ❌ 依赖 traceId | 抢锁失败分支不再返回 traceId;需要排查请让后端按时间窗查日志 | + +### 前端需要做的动作 + +- 在错误码映射中把 **100503** 映射为「资源被占用,请稍后重试」的可重试提示。 +- 若此前对 500 做了「系统异常」特殊处理,抢锁失败场景将不再走该分支。 + +--- + +## 五、数据库行为 + +- 抢锁失败分支:**零数据库写入**——请求在锁层即被拒绝,不产生任何行变更、状态流转或日志写入。 +- 正常抢到锁的请求:数据库行为与本次修改前完全一致,无字段、无表结构变化。 + +--- + +## 六、边界行为 + +- 鉴权、路由、请求字段、必填性、响应结构均未变。 +- HTTP 状态仍为 200(业务失败约定不变;鉴权 401/403 与未知系统异常 500 不受影响)。 +- 用户自定义 `LockFailureStrategy` 时默认策略自动退让,行为由自定义实现决定。 +- 只在「等满 acquire-timeout 仍抢不到锁」时返回 100503;正常拿到锁的请求行为完全不变。 + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| code | 500 | 100503 | +| message | 系统繁忙请稍后重试或联系客服 | 资源被占用,请稍后重试 | +| traceId | 返回(非空) | 该分支不返回 | +| data | null | null(不变) | +| HTTP 状态 | 200 | 200(不变) | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 抢锁失败 | 通用异常兜底,日志 ERROR `Unexpected error` | 业务异常,日志 WARN `Business error [common.100503]` | +| 是否进入业务逻辑 | 否 | 否(不变) | +| 锁释放后重试 | 正常进入业务 | 正常进入业务(不变) | + +## 六.7、影响评估 + +- **是否破坏向后兼容**:code/message 属行为纠正,前端若硬编码 500 文案需同步调整为 100503;响应结构与 HTTP 状态未变。 +- **前端是否必须同步上线**:建议同步(把可重试提示挂到 100503);未同步时表现为文案不精确,不会导致功能性失败。 +- **后端覆盖范围**:`hl-starter-protection` 是共享 starter,所有依赖它的服务(order-v3、fleet、mp、product-v2、resource、user、finance 等)行为同时变化,需一起滚动部署。 + +## 七、不影响范围 + +- **仅影响**:`@Lock4j` 抢锁失败分支的 code / message 与该分支的 traceId 字段。 +- **零影响**: + - 幂等拦截(100502)、鉴权(401/403)、参数校验(HTTP 200 + 业务码)等既有分支 + - 正常抢到锁的请求:入参、出参、业务规则、库存扣减行为 + - 数据库结构与存量数据 + - 非 `@Lock4j` 异常(业务码、未知系统异常 500) + +--- + +## 八、测试环境已验证 + +- 部署:order-v3 / fleet / mp / product-v2 / resource / user 六个进程滚动到 `fix/7491-lock-failure-handler @65715c2f1`,`deploy-status.sh` 逐行 `STATE=ok`、`BEHIND 0/N`;合并提交 `716cd17656c05850ff5fc476c91e48032c63a187`。 +- 网关实测(占锁 60s 后调用): + +``` +POST /v3/admin/house/group-batches/999999100000021/room-plans +(锁键 lock4j:house:gb:room#999999100000021 已被占) +→ HTTP 200 +→ {"code":100503,"message":"资源被占用,请稍后重试","data":null,"traceId":null,"success":false} ✓ + +DEL 锁键 → EXISTS 0 → 同一请求再次调用 +→ HTTP 200 {"code":589500,"message":"团期不存在"} ✓ +(业务码,非 500/100503,证明锁释放后立即穿透业务逻辑) +``` + +- 跨实例:4 次占锁调用分别落在 order-v3 `:8086`(2 次)与 `:8186`(2 次),两实例结论一致;`Unexpected error` 计数均为 0。 +- 兼容性结论:抢锁失败分支已稳定返回 100503,HTTP 200 与响应结构保持不变。 + +--- + +## 十、相关文档 + +- Issue: [wx/HL#7491](https://git.1814.love:8443/wx/HL/issues/7491) +- PR: [wx/HL#7885](https://git.1814.love:8443/wx/HL/pulls/7885) +- Merge commit: [716cd17656c05850ff5fc476c91e48032c63a187](https://git.1814.love:8443/wx/HL/commit/716cd17656c05850ff5fc476c91e48032c63a187) +- 规范联动: [wx/hl-workflow#28](https://git.1814.love:8443/wx/hl-workflow/pulls/28) +- 上游线索: [wx/HL#7324](https://git.1814.love:8443/wx/HL/issues/7324) AC-59 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7491](https://git.1814.love:8443/wx/HL/issues/7491) +- **PR**: [#7885](https://git.1814.love:8443/wx/HL/pulls/7885) +- **Merge commit**: [716cd17656c05850ff5fc476c91e48032c63a187](https://git.1814.love:8443/wx/HL/commit/716cd17656c05850ff5fc476c91e48032c63a187) + +### 联系人 + +- **后端负责人**: @wx