文件
hl-api-changelog/changelogs-v2/2026-09/17_7491_Lock4j抢锁失败统一返100503-修改接口-管理后台.md

235 行
11 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-09-17"
status_note: "抢锁失败分支的 code 由 500 改为 100503,HTTP 仍为 200;前端应把「可重试」提示挂到 100503 上。前端 2026-09-17 复核:grep 实证无 100503/500 抢锁硬编码,业务错误统一由拦截器透 body.message 弹出(request.js:465),新码文案「资源被占用,请稍后重试」原样可达用户,同 #7529 先例不建错误码字典,判 not_required(无业务改动)。"
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<List<GroupBatchRoomPlanRespVO>>`
#### 使用场景
房务按日提交订房计划。该端点带团期级 `@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<List<GroupBatchRoomPlanRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| 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