文件
hl-api-changelog/changelogs-v2/2026-09/20_7980_出行人删改与发票申请三组写口补同名锁新增100503冲突码触达C端-修改接口-管理后台.md
T
2026-09-20 23:35:03 +08:00

881 行
46 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "7980"
title: "出行人删除/批量编辑与发票申请三组写口补同一把 @Lock4j name,新增可重试冲突码 100503(会传导至小程序 C 端)"
consumer: "multiple"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8049 已 squash 合并 dev-v3(合并提交 43c53c4c9)。2026-09-20 23:24 已部署测试服 hl-order-service-v3:滚动更新两实例,部署前 COMMIT=c35251b07、部署后 COMMIT=ba8aab3ab,8186 与 8086 各 12 秒内起监听。`git merge-base --is-ancestor 43c53c4c9 ba8aab3ab` 返回 ANCESTOR_YES。gateway_status=not_required:端点路径/方法零变化,本次与网关路由无关,不是漏验证。🔴 本次未在测试服做功能调用取证,这是有意为之而不是遗漏:本组端点是删除出行人与批量改出行人,调用它们会真实删改订单下的出行人行;测试服订单数据被多个会话共用,删除撤不回来,副作用会落在一个不知情的人头上。互斥本身由落库级并发 IT `Lock4jSharedNameConcurrencyIT` 覆盖(3 条用例,含专为自调用入口 `deleteByInternal` 单写的一条),不变量是「订单至少留 1 位成人」;另有 `Lock4jSharedNameContractTest` 按 SpEL 反查全类、钉死三组的成员集合,能抓住日后新加的第 N 个方法。⚠️ `100503` 在测试服上一次都没有触发过——不是「测过、不会触发」,是没测;临界区短不等于永不超时,更长事务/更大载荷/生产数据量下仍可能出现。mmg 前端实证 2026-09-20:admin 侧调用方 TravelerInfoEditor.vue(:550/863/868)、FunItemAdjustModal.vue(adjustment/submit 经 index.vue)、ApplyModal.vue(:298/329/372)的 catch 均仅复位 loading 并注释「业务码由 request.js 拦截器统一 toast」,成功失败一律读 body code 非 HTTP 状态;100503(HTTP 200+业务码)经拦截器透 message 原样展示,可重试/不静默/不当系统异常三条全满足,故 admin 端零改动,翻 not_required。mp 侧出行人补全/删除与申请发票传导 C 端属 changelogs-v2-mp 系列,由 mp loop 消费,本仓不越权。"
updated_at: "2026-09-20"
base: "dev-v3"
---
# order-v3: 出行人删除/批量编辑与发票申请三组写口补同一把 @Lock4j name,新增可重试冲突码 100503(会传导至小程序 C 端)
> **存放目录**:
> - 一期(v2,无 `order-v3` 标签的工单)→ `changelogs/{YYYY-MM}/`
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/{YYYY-MM}/`
>
> **服务**: hl-order-service-v3(另涉 hl-mp-service 四个透传/BFF 入口)
> **PR**: #8049
> **Issue**: #7980(AC-7 第一/三/四组)
> **日期**: 2026-09-20
> **影响范围**: 管理后台「订单出行人」删除/批量编辑、「订单调整」提交(出行人 tab)、「发票管理」申请开票 + 小程序 C 端「出行人补全/删除」「申请发票」
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- 三组写口的 `@Lock4j` 都写了**逐字相同**的 `keys`,却都**没写 `name`**——lock4j 2.2.7 缺省 `name` 时用「类全限定名 + 方法名」当默认值拼进 Redis 键,于是每组内的多个方法**各持一把互不相干的锁**,可以并发互相踩踏:
1. **`traveler:delete:order`**(3 处):`TravelerService.deleteTraveler`(admin)/ `mpDeleteTraveler`(mp)/ `deleteByInternal`(internal Feign,且它经 `this.deleteTraveler(...)` 自调用,不走 Spring 代理,之前**一次都没触发过**锁)。「最后 1 位成人禁删」是内存计数的 check-then-act,`order_traveler` 无 `@Version`、无行锁复读,没有第二道锁兜底——两个并发删除请求各读到「还有 2 个成人」的旧快照就会把成人删到 0。
2. **`traveler:batch-edit`**(3 处):`TravelerService.batchEditTravelers`(admin 直接批量编辑)/ `batchEditTravelersForAdjustment`(订单调整提交里的出行人 tab)/ `mpBatchEditTravelers`(mp)。「基于内存合并的最终生效集合先校验后写库」同样是 check-then-act,没有第二道锁兜底。
3. **`order-invoice:apply`**(2 处):`InvoiceAdminService.adminApplyInvoice`(admin)/ `InvoiceMpService.mpApplyInvoice`(mp)。两者前置条件相同(同一个 `validateApplyEligibility`,含「一单一票」existsActive 检查),`order_invoice` 无 `UNIQUE`、`OrderInvoice` 无 `@Version`,补 `name` 之前 admin 申请 ∥ mp 申请可各读到 existsActive 旧快照并各插一条申请单。
- 本次给三组各自补上**同一个**显式 `name`(分别为 `"traveler:delete:order"` / `"traveler:batch-edit"` / `"order-invoice:apply"`),组内互斥才真正生效。**接口路径、请求/响应字段、既有业务错误码全部零增删**,唯一契约变化是新增一个可能返回的失败码 **`100503`**,**HTTP 状态码仍是 200**。
- 🔴 **新增的失败路径会到达小程序 C 端**:出行人补全/删除、申请发票均是 mp 端用户自助操作,命中锁冲突时前端会等待最多约 3 秒后收到 `code=100503`("资源被占用,请稍后重试")。**传输层仍是 HTTP 200**,失败信息在响应体的 `code` 字段里——前端如果只看 HTTP 状态码会把这次失败误判为成功。
- 🔴 **本 PR 只做 AC-7 里判定为「要修」的三组**。同一份判定还覆盖了另外三组,均**不修**(不在本文档「二、变更接口清单」范围内,行为未变,仅供背景参考):
- `order-ins:` 组(保险 `purchaseCore`):判定为「有意不加 name」——`purchaseCore` 本身没有按单去重逻辑,补锁对「同单不重复出单」零贡献;真正的缺口在工单未列出的 `autoPurchase`/`autoPurchaseByScheme`(连 `@Lock4j` 都没有),本轮只订正了两处描述这把锁效果的假注释,完整修复另起工单,本文档不涉及。
- `material:tag:name:`(resource 服务):有 `uk_tag_name` 唯一索引兜底,判定不修。
- `fleet:msg-tpl:default:`(fleet 服务):有 InnoDB 行锁兜底,判定不修。
---
## 一、背景
工单 #7980 第四节原文对以下几处描述与代码不符,本 PR 已订正(不影响本次「要不要补锁」的结论,只是让文档准确):① 第 5 组「同名标签双插(无唯一索引)」——`material_tag` 实际**有** `UNIQUE KEY uk_tag_name`;② 第 3 组两处行号已漂移(AC-4 改动注释所致),实际为 `TravelerService.java:126`/`:150`(现已改为本次改动后的真实行号);③ 第 3 组组大小实际是 **3** 个方法(`deleteTraveler`/`mpDeleteTraveler`/`deleteByInternal`)不是 2 个,其中 `deleteByInternal` 因自调用问题此前**从未触发过**锁,是最危险的一处。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 出行人软删(admin) | POST | `/v3/admin/order/{id}/traveler/{travelerId}/delete` | 新增可能返回的错误码 | 与 §2/§3 共用 `traveler:delete:order` 锁 |
| 2 | 客户删除出行人(mp internal) | POST | `/v3/internal/mp/order/{id}/traveler/{travelerId}/delete` | 新增可能返回的错误码 | 经 hl-mp-service 到达小程序 C 端 |
| 3 | 跨服务删除出行人(internal Feign) | DELETE | `/v3/internal/order/orders/{orderId}/travelers/{travelerId}` | 新增可能返回的错误码 | 当前零跨服务调用方,见业务边界 |
| 4 | 批量 upsert 出行人(admin) | POST | `/v3/admin/order/{id}/traveler/batch-edit` | 新增可能返回的错误码 | 与 §5/§6 共用 `traveler:batch-edit` 锁 |
| 5 | 调整订单统一提交(含出行人 tab) | POST | `/v3/admin/order/{id}/adjustment/submit` | 新增可能返回的错误码(条件触发) | 仅当请求体含出行人增删改时才会命中本组锁 |
| 6 | 客户出行人补全(mp internal) | POST | `/v3/internal/mp/order/{id}/traveler/batch-edit` | 新增可能返回的错误码 | 经 hl-mp-service 到达小程序 C 端 |
| 7 | 定制师代客申请发票(admin) | POST | `/v3/admin/order/{orderId}/invoice/apply` | 新增可能返回的错误码 | 与 §8 共用 `order-invoice:apply` 锁 |
| 8 | 用户申请开票(mp internal) | POST | `/v3/internal/mp/order/{orderId}/invoice/apply` | 新增可能返回的错误码 | 经 hl-mp-service 到达小程序 C 端 |
---
## 三、接口详情
### 1. 出行人软删(admin) `POST /v3/admin/order/{id}/traveler/{travelerId}/delete`
**VO**: `无 ReqVO(纯路径参数,无请求体) → Boolean`
#### 使用场景
管理后台订单详情页,定制师/客服删除某个出行人(已签电子合同禁删;订单最后 1 位成人禁删)。本次改动不涉及入参/出参,只新增一条「被本组另外 2 个写口占用同一把锁」的失败分支。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| travelerId | Path | Long | ✅ | - | 出行人 ID |
#### 出参 `Result<Boolean>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | 恒为 `true`(失败走异常,不会返回 `false`) |
#### 请求示例
```http
POST /v3/admin/order/1234567890123456789/traveler/70123456789012/delete
Authorization: Bearer {token}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": true, "success": true }
```
#### 空数据 / 降级响应
本接口无「空数据」/降级概念(成功恒返回 `true`,失败一律抛业务异常)。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 401 | 未登录(网关拦截) |
| 581102 | 订单不存在 |
| 581110 | 出行人 ID 不属于该订单 |
| 581106 | 已签电子合同,禁止删除出行人 |
| 581107 | 出行人是订单最后 1 位成人,禁止删除 |
| 100502 | 3 秒幂等窗口内重复提交(键=id+travelerId) |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §2 mp 删除、§3 internal Feign 删除共用同一把 `traveler:delete:order` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent`(`keyPrefix=traveler:delete`)3 秒窗口,键含订单 ID + 出行人 ID。
- 抢锁失败(100503)时该次请求未进入方法体,零写入——「最后 1 位成人」的计数不会被并发破坏。
- 软删:级联软删桥接表 `order_transport_plan_traveler`,并写 `order_status_log` 流水(`TRAVELER_DELETE`)。
- 已签约阶段删人会触发"作废旧合同/保单 + 重新签约/投保"事件(按订单状态白名单过滤),不受本次改动影响。
---
### 2. 客户删除出行人(mp internal) `POST /v3/internal/mp/order/{id}/traveler/{travelerId}/delete`
**VO**: `无 ReqVO(纯路径参数,无请求体) → Void`
#### 使用场景
小程序 C 端用户在「我的订单」自助删除出行人,仅订单确认前(PENDING_PAY/CUSTOMIZING)放行,且只能删自己名下订单的出行人。**本端点是 order-v3 的 internal 路径,C 端不直接访问它**,而是经 hl-mp-service 的公网入口 `DELETE /mp/order/{orderId}/traveler/{travelerId}`(`MpOrderTravelerController`)透传调用。本次改动不涉及入参/出参,只新增一条抢锁超时的失败分支。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| travelerId | Path | Long | ✅ | - | 出行人 ID |
(`userId` 从 JWT 解析,前端不传,由网关透传 `X-User-Id`。)
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | null | 无返回数据 |
#### 请求示例
```http
DELETE /mp/order/1234567890123456789/traveler/70123456789012
Authorization: Bearer {token}
```
(以上为小程序 C 端实际调用的公网地址;order-v3 侧 internal 路径见本节标题。)
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": null, "success": true }
```
#### 空数据 / 降级响应
本接口无「空数据」/降级概念。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 581122 | 订单不属于当前用户(越权) |
| 581145 | 订单已确认,出行人信息不可再经小程序修改(须走 admin) |
| 581102/581110/581106/581107 | 同 §1(复用 `deleteTraveler` 全部业务规则) |
| 100502 | 3 秒幂等窗口内重复提交 |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §1 admin 删除、§3 internal Feign 删除共用同一把 `traveler:delete:order` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent` 3 秒窗口(键=订单 ID)。
- 越权校验:`order.userId == 当前登录 userId`,不满足统一返 581122,不额外区分"不存在"与"无权限"。
- 状态门禁:仅 `PENDING_PAY`/`CUSTOMIZING` 放行,订单确认后(`PENDING_DEPARTURE` 起)及已完成/已取消一律禁止,防止客户改动触发合同作废重签/退保重投保。
- 抢锁失败(100503)时零写入,业务规则全部委托 `deleteTraveler`,见 §1。
---
### 3. 跨服务删除出行人(internal Feign) `DELETE /v3/internal/order/orders/{orderId}/travelers/{travelerId}`
**VO**: `无 ReqVO(纯路径参数,无请求体) → Boolean`
#### 使用场景
供其他微服务通过 Feign 跨服务删除出行人(无 JWT 鉴权,依赖网络隔离,Gateway 不暴露 `/internal/**` 至公网)。🔴 全仓 grep 确认**当前零跨服务调用方**——没有任何 `@FeignClient` 接口声明指向本路径,是本组三处里唯一没有现存调用方的一处。本次改动不涉及入参/出参,只是把此前**完全裸奔**(因自调用问题一次都没触发过锁)的这条链路纳入本组互斥。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 订单 ID |
| travelerId | Path | Long | ✅ | - | 出行人 ID |
#### 出参 `Result<Boolean>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | 恒为 `true`(委托 `deleteTraveler`,失败走异常) |
#### 请求示例
```http
DELETE /v3/internal/order/orders/1234567890123456789/travelers/70123456789012
```
(跨服务 Feign 调用,无鉴权头。)
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": true, "success": true }
```
#### 空数据 / 降级响应
本接口无「空数据」/降级概念,也无当前真实流量。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 581102/581110/581106/581107 | 同 §1(委托 `deleteTraveler`,`adminId` 传 `null`) |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §1 admin 删除、§2 mp 删除共用同一把 `traveler:delete:order` 锁),**可重试** |
#### 业务边界
- 无 `@Idempotent`(internal Feign 端点通常由调用方自行保证幂等)。
- 🔴 **本次改动前,本端点的调用链(`deleteByInternal` → `this.deleteTraveler(...)`)是自调用,不经 Spring 代理,`deleteTraveler` 上的 `@Lock4j` 在这条链路上此前一次都不会触发**——`deleteByInternal` 方法本身现已补上独立的 `@Lock4j(name="traveler:delete:order")`,才真正纳入互斥;这不是"补 name"本身的效果,而是本次一并修的一个更深的问题(自调用丢锁)。
- 当前零调用方,不代表本条不需要修:与 A/B/C 三份 changelog 里 C 份(库存扣减)同类判定——回归面可实证为零,但闸口本身要补齐。
- 抢锁失败(100503)时零写入。
---
### 4. 批量 upsert 出行人(admin) `POST /v3/admin/order/{id}/traveler/batch-edit`
**VO**: `TravelerBatchEditReqVO → TravelerBatchEditRespVO`
#### 使用场景
管理后台订单详情页,定制师/客服批量新增/更新出行人(id=null 新增,id 非 null 更新,整体事务,≤30 行)。本次改动不涉及入参/出参字段,只新增一条「被本组另外 2 个写口占用同一把锁」的失败分支。
#### 入参
路径参数 + Body:
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| travelers | Body | List\<Item\> | ✅ | `@NotEmpty`,≤30 | 待修改出行人数组 |
| travelers[].id | Body | Long | - | null=新增/非 null=更新 | 更新时必须属于该 orderId |
| travelers[].name | Body | String | - | - | 姓名 |
| travelers[].gender | Body | String | - | 1=男/2=女/0=未知 | 性别 |
| travelers[].birthday | Body | LocalDate | ✅ | - | 出生日期,后端按年龄自动派生出行人类型 |
| travelers[].idType | Body | String | - | 字典 id_card_type | 证件类型 |
| travelers[].idNo | Body | String | - | 格式按证件类型校验 | 证件号(明文传,DB 加密) |
| travelers[].nationality | Body | String | - | 不可空字符串 | 国籍(12301 必报字段) |
| travelers[].race | Body | String | - | 不可空字符串 | 民族(12301 必报字段) |
| travelers[].phone | Body | String | - | 11 位数字 | 出行人手机(明文传,DB 加密) |
| travelers[].email | Body | String | - | 宽松格式 | 电子邮箱 |
| travelers[].emergencyContact | Body | String | - | - | 紧急联系人姓名 |
| travelers[].emergencyPhone | Body | String | - | - | 紧急联系人电话 |
| travelers[].roomGroupNo | Body | Integer | - | 不超家庭数上限 | 同住分组号 |
#### 出参 `Result<TravelerBatchEditRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| createdCount | Integer | 本次新增的出行人数(id=null 行) |
| updatedCount | Integer | 本次更新的出行人数(id 非 null 行) |
| completedCount | Integer | 本次操作后变为 COMPLETED 的行数 |
| pendingCount | Integer | 仍为 PENDING 的行数 |
| allCompleted | Boolean | 该订单所有出行人是否已完善 |
#### 请求示例
```json
{
"travelers": [
{
"id": null,
"name": "张三",
"gender": "1",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idNo": "220103198508121234",
"nationality": "中国",
"race": "汉族",
"phone": "13812342046"
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": { "createdCount": 1, "updatedCount": 0, "completedCount": 1, "pendingCount": 0, "allCompleted": true },
"success": true
}
```
#### 空数据 / 降级响应
本接口无「空数据」概念(`travelers` 要求非空),无降级路径。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 401 | 未登录(网关拦截) |
| 581105 | 批量大小超限(>30) |
| 581102 | 订单不存在 |
| 581119 | 出行人证件号重复(同批) |
| 581101 | 12301 必报字段缺失(国籍/民族为空字符串) |
| 581112/581113/581114 | 证件号/手机号格式非法、出生日期晚于今天 |
| 581103/581104 | 性别编码不合法、同住分组号超出上限 |
| 581118 | 新增项缺少必填信息(姓名/证件类型/证件号至少一项) |
| 581110 | 更新项不属于该订单 |
| 581111 | 已签电子合同后禁止修改证件号 |
| 581136 | 成人手机号硬门禁(至少一名成人须填手机号) |
| 100502 | 3 秒幂等窗口内重复提交(键=订单 ID) |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §5 订单调整提交、§6 mp 出行人补全共用同一把 `traveler:batch-edit` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent` 3 秒窗口(键=订单 ID)。
- 抢锁失败(100503)时零写入——所有校验(配额、去重、必填矩阵)都在事务内完成,锁竞争失败不会产生任何部分写入。
- 校验顺序:批量大小 → 订单存在 → 身份证去重 → 姓名硬拦截 → 12301 字段 → 格式类字段 → 新增/更新分支各自的必填与归属校验 → 成人手机号硬门禁(基于内存合并的最终生效集合),任一失败整事务回滚。
- 老数据兼容:无字段变化。
---
### 5. 调整订单统一提交(含出行人 tab) `POST /v3/admin/order/{id}/adjustment/submit`
**VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO`
#### 使用场景
⚠️ 本接口是订单调整弹窗的统一提交入口,覆盖人数/行程/住宿/用车等多个子领域,**本节只描述与本次改动相关的「出行人 tab」部分**,其余子领域(改期/行程编辑/房需求/车需求)字段与行为均**不在本次改动范围内、未变化**,完整入参见既有产品/接口文档。当请求体 `updates.people.travelers`(或兼容旧契约的 `updates.travelers`)含新增/更新/删除任一项时,后端内部会调用与 §4 admin 批量编辑**同名**的锁(`traveler:batch-edit`),本次改动前该调用完全不受这把锁保护——补 `name` 之后,本接口在处理出行人变更时会真的把 §4/§6 挡在门外。
#### 入参(仅出行人相关部分,其余字段见既有文档)
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| updates.people.travelers.add | Body | List\<TravelerEditVO\> | - | - | 新增出行人列表 |
| updates.people.travelers.update | Body | List\<TravelerEditVO\> | - | 含 id 字段 | 更新出行人列表 |
| updates.people.travelers.remove | Body | List\<Long\> | - | - | 删除出行人 id 列表 |
| updates.people.travelers.add[].name | Body | String | - | - | 姓名 |
| updates.people.travelers.add[].idType | Body | String | - | - | 证件类型 |
| updates.people.travelers.add[].idNo | Body | String | - | - | 证件号(脱敏回显,编辑回传按未修改处理规则见 §4 同款字段) |
| updates.people.travelers.add[].birthday | Body | LocalDate | 条件必填 | 新增必填 | 出生日期,用于派生出行人类型 |
| updates.people.travelers.add[].travelerType | Body | String | - | ADULT/CHILD | 出行人类型 |
| updates.people.travelers.add[].remark | Body | String | - | - | 备注 |
| updates(其余字段:schedule/itinerary/hotelRequirement/vehicleRequirement/transferRequirement) | Body | - | - | 见既有文档 | 与本次改动无关,未变化 |
#### 出参 `Result<AdjustmentSubmitRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| success | Boolean | 提交是否成功(极简响应,失败统一走全局异常处理器返回 `Result{code,message}`) |
#### 请求示例(仅出行人 tab 片段)
```json
{
"updates": {
"people": {
"travelers": {
"add": [
{ "name": "李四", "idType": "ID_CARD", "idNo": "110101198801011234", "birthday": "1988-01-01" }
],
"remove": [70123456789013]
}
}
}
}
```
#### 响应示例
```json
{ "code": 200, "message": "成功", "data": { "success": true }, "success": true }
```
#### 空数据 / 降级响应
`updates` 各子域均可为 null(按需填),但不能全 null(否则 `AdjustmentErrorCode.ADJUST_NO_EFFECTIVE_CHANGE`)。不含出行人变更时本次改动完全不生效,走原有逻辑。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 401 | 未登录(网关拦截) |
| (出行人相关)581101/581103/581104/581110-581114/581118/581119/581136 | 同 §4 校验矩阵,按调整后目标人数校验(区别于 §4 按数据库当前声明人数校验) |
| **100503(本次新增,仅当本次提交含出行人变更时可能触发)** | 抢锁等待超过 3 秒(与 §4 admin 批量编辑、§6 mp 出行人补全共用同一把 `traveler:batch-edit` 锁),**可重试** |
| (其余子领域错误码) | 与本次改动无关,未变化,不在本文档范围 |
#### 业务边界
- 无 `@Idempotent`(本端点覆盖多子领域,幂等策略由前端弹窗层面控制,不在本次改动范围)。
- **配额校验口径与 §4 不同**:本入口按"本次调整的最终人数"校验(`targetDeclaration`),并在校验时排除同一事务中待删除的旧出行人;§4 普通编辑入口按数据库中的当前声明人数校验。
- 抢锁失败(100503)**只影响本次请求整体**——本接口是单一事务原子应用所有子领域改动,出行人锁竞争失败会导致整个 `adjustment/submit` 请求失败回滚,不会出现"其他子域改完、出行人没改完"的部分成功。
- 老数据兼容:无字段变化。
---
### 6. 客户出行人补全(mp internal) `POST /v3/internal/mp/order/{id}/traveler/batch-edit`
**VO**: `TravelerBatchEditReqVO → TravelerBatchEditRespVO`
#### 使用场景
小程序 C 端用户自助补全/修改出行人信息,仅订单确认前(`PENDING_PAY`/`CUSTOMIZING`)放行,且只能改自己名下订单。**本端点是 order-v3 的 internal 路径**,C 端经 hl-mp-service 的三个公网入口间接调用:`POST /mp/order/{orderId}/traveler`(新增单个出行人)、`PUT /mp/order/{orderId}/traveler/{travelerId}`(修改单个出行人)、`POST /mp/v3/order/{id}/traveler/batch-edit`(批量补全)。本次改动不涉及入参/出参字段,只新增一条抢锁超时的失败分支。
#### 入参
同 §4(`TravelerBatchEditReqVO`,字段表见上节,不重复列出)。`userId` 从 JWT 解析,前端不传。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 订单 ID |
| travelers | Body | List\<Item\> | ✅ | `@NotEmpty`,≤30 | 字段结构同 §4 |
#### 出参 `Result<TravelerBatchEditRespVO>`
字段结构同 §4:
| 字段 | 类型 | 说明 |
|------|------|------|
| createdCount | Integer | 本次新增的出行人数(id=null 行) |
| updatedCount | Integer | 本次更新的出行人数(id 非 null 行) |
| completedCount | Integer | 本次操作后变为 COMPLETED 的行数 |
| pendingCount | Integer | 仍为 PENDING 的行数 |
| allCompleted | Boolean | 该订单所有出行人是否已完善 |
#### 请求示例
```json
{
"travelers": [
{ "id": null, "name": "王五", "idType": "ID_CARD", "idNo": "310101199001011234", "birthday": "1990-01-01" }
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": { "createdCount": 1, "updatedCount": 0, "completedCount": 1, "pendingCount": 0, "allCompleted": true },
"success": true
}
```
#### 空数据 / 降级响应
本接口无「空数据」概念(`travelers` 要求非空),无降级路径。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 581122 | 订单不属于当前用户(越权) |
| 581145 | 订单已确认,出行人信息不可再经小程序修改 |
| 581101/581105/581110-581114/581118/581119/581136 | 同 §4 校验矩阵 |
| 100502 | 3 秒幂等窗口内重复提交 |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §4 admin 批量编辑、§5 订单调整提交共用同一把 `traveler:batch-edit` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent` 3 秒窗口(键=订单 ID)。
- 越权与状态门禁同 §2。
- 🔴 **本 PR 尤其重要的一处**:`mpBatchEditTravelers` 的方法体最终委托 `this.batchEditTravelers(...)`(自调用),此前该调用同样有绕过代理丢锁的历史问题,本次连同 §4/§5 一并补齐同一 `name`,三个入口现在才真正互斥——补锁前,客户在小程序自助编辑与定制师在 admin 后台同时编辑同一订单的出行人可能互不阻塞(内存计算的配额各按各的旧快照算,静默丢更新)。
- 抢锁失败(100503)时零写入。
- 老数据兼容:无字段变化。
---
### 7. 定制师代客申请发票(admin) `POST /v3/admin/order/{orderId}/invoice/apply`
**VO**: `AdminInvoiceApplyReqVO → AdminInvoiceApplyRespVO`
#### 使用场景
管理后台「发票管理」,定制师代客户申请开票(订单状态须 ∈ {CUSTOMIZING, PENDING_DEPARTURE, TRAVELLING, COMPLETED},或已取消且净实收>0;一单一票)。本次改动不涉及入参/出参字段,只新增一条「被 §8 mp 申请占用同一把锁」的失败分支。
#### 入参
路径参数 + Body:
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 订单 ID |
| invoiceType | Body | String | ✅ | `@NotBlank`,VAT_NORMAL/VAT_SPECIAL | 发票类型 |
| titleType | Body | String | ✅ | `@NotBlank`,COMPANY/PERSONAL | 抬头类型,专票只能开给单位 |
| titleName | Body | String | ✅ | `@NotBlank`,≤200 字符 | 抬头名称 |
| taxNo | Body | String | 条件必填 | ≤64 字符 | 税号(titleType=COMPANY 或 invoiceType=VAT_SPECIAL 必填) |
| bankName | Body | String | 条件必填 | ≤128 字符 | 开户行(invoiceType=VAT_SPECIAL 必填) |
| bankAccount | Body | String | 条件必填 | ≤64 字符 | 开户账号(同上) |
| registAddress | Body | String | 条件必填 | ≤500 字符 | 注册地址(同上) |
| registPhone | Body | String | 条件必填 | ≤32 字符 | 注册电话(同上) |
| email | Body | String | ✅ | `@Email`,≤128 字符 | 收件邮箱 |
| remark | Body | String | - | ≤500 字符 | 备注/货物或应税劳务名称 |
#### 出参 `Result<AdminInvoiceApplyRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String(雪花,序列化为字符串) | 发票 ID |
| orderId | String | 订单 ID |
| status | String | 发票状态(REQUESTED=待开票) |
| amount | BigDecimal | 开票金额(申请阶段为 null,开票时才计算写入) |
| requestedAt | LocalDateTime | 申请时间 |
| requestedBy | String | 申请人(定制师真实姓名) |
#### 请求示例
```json
{
"invoiceType": "VAT_NORMAL",
"titleType": "COMPANY",
"titleName": "上海呼籁旅行科技有限公司",
"taxNo": "91310115MA1K48XXXX",
"email": "finance@hulalv.com"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2100889866400104450",
"orderId": "1234567890123456789",
"status": "REQUESTED",
"amount": null,
"requestedAt": "2026-09-20T14:30:25",
"requestedBy": "李定制"
},
"success": true
}
```
#### 空数据 / 降级响应
本接口无「空数据」概念(成功恒返回完整响应),无降级路径。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 401 | 未登录(网关拦截) |
| 581511 | 该订单已存在有效发票(一单一票) |
| 581524 | 订单当前状态不可申请开票 |
| 581513 | 发票类型非法 |
| 581523 | 增值税专用发票只能开给单位 |
| 581514 | 公司抬头或专票必须填写税号 |
| 581515 | 增值税专用发票须填写银行及注册信息 |
| 581516 | 收件邮箱不能为空 |
| 100502 | 5 秒幂等窗口内重复提交(键=订单+发票类型+抬头名称,抬头或类型不同即不拦) |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §8 mp 申请共用同一把 `order-invoice:apply` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent` 5 秒窗口,键含订单+发票类型+抬头名称——**抬头或类型不同即不拦**,替代不了这把锁(两个不同抬头的并发申请仍会分别通过幂等但撞上一单一票)。
- 抢锁失败(100503)时该次请求零写入,`existsActive` 检查与 INSERT 之间不会被并发穿透。
- 开票金额本次申请不写,由后续「完成开票」(另一 PR #8043 的 `issueInvoice`)自动计算写入,与本次改动无关。
- 老数据兼容:无字段变化。
---
### 8. 用户申请开票(mp internal) `POST /v3/internal/mp/order/{orderId}/invoice/apply`
**VO**: `MpInvoiceApplyReqVO → MpInvoiceApplyRespVO`
#### 使用场景
小程序 C 端用户自助申请开票(仅本人订单,状态门槛与一单一票规则与 §7 相同)。**本端点是 order-v3 的 internal 路径**,C 端经 hl-mp-service 的公网入口 `POST /mp/v3/order/{orderId}/invoice/apply`(`MpV3CollabInvoiceApplyController`)透传调用。本次改动不涉及入参/出参字段,只新增一条「被 §7 admin 申请占用同一把锁」的失败分支。
#### 入参
路径参数 + Body:
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | - | 订单 ID |
| invoiceType | Body | String | ✅ | `@NotBlank`,VAT_NORMAL/VAT_SPECIAL | 发票类型 |
| titleType | Body | String | ✅ | `@NotBlank`,COMPANY/PERSONAL | 抬头类型 |
| titleName | Body | String | ✅ | `@NotBlank`,≤200 字符 | 抬头名称 |
| taxNo | Body | String | 条件必填 | ≤64 字符 | 税号 |
| bankName | Body | String | 条件必填 | ≤128 字符 | 开户行 |
| bankAccount | Body | String | 条件必填 | ≤64 字符 | 开户账号 |
| registAddress | Body | String | 条件必填 | ≤500 字符 | 注册地址 |
| registPhone | Body | String | 条件必填 | ≤32 字符 | 注册电话 |
| email | Body | String | ✅ | `@Email`,≤128 字符 | 收件邮箱 |
| applyReason | Body | String | - | ≤500 字符 | 申请说明 |
(`userId` 从 JWT 解析,前端不传。)
#### 出参 `Result<MpInvoiceApplyRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 发票主键 |
| orderId | Long | 订单 ID |
| status | String | 状态:实际写入值为 `REQUESTED`(与 hl-mp-service 侧 VO 注释所写"固定 APPLIED"不一致,以 order-v3 源码 `InvoiceMpService.mpApplyInvoice` 实际赋值为准) |
| amount | BigDecimal | 申请金额(申请阶段为 null) |
| applyAt | LocalDateTime | 申请时间 |
| triggerPushLogIds | List\<Long\> | 触发的通知推送日志 ID 列表(当前恒为空数组) |
#### 请求示例
```json
{
"invoiceType": "VAT_NORMAL",
"titleType": "PERSONAL",
"titleName": "张三",
"email": "zhangsan@example.com"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 2100889866400104460,
"orderId": 1234567890123456789,
"status": "REQUESTED",
"amount": null,
"applyAt": "2026-09-20T14:35:10",
"triggerPushLogIds": []
},
"success": true
}
```
#### 空数据 / 降级响应
本接口无「空数据」概念,无降级路径。
#### 错误响应
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "success": false }
```
| code | 触发条件 |
|------|----------|
| 581470 | 订单不存在或不属于当前用户 |
| 581511/581524/581513/581523/581514/581515/581516 | 同 §7 校验矩阵(复用 `InvoiceAdminService.validateApplyEligibility` / `validateInvoiceFields`) |
| 100502 | 5 秒幂等窗口内重复提交 |
| **100503(本次新增)** | 抢锁等待超过 3 秒(与 §7 admin 申请共用同一把 `order-invoice:apply` 锁),**可重试** |
#### 业务边界
- 幂等:`@Idempotent` 5 秒窗口,口径同 §7。
- 抢锁失败(100503)时零写入。
- `requestedBy` 落库为用户 ID 字符串(区别于 admin 端落真实姓名),字段本身不在本次改动范围。
- 老数据兼容:无字段变化。
---
## 四、契约约束与正确调用方式(接口类必写)
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### 正确 / 错误的客户端处理方式
本次不涉及请求体字段的互斥/联动规则变化,契约约束集中在"如何处理新失败码":
| 场景 | 处理方式 |
|------|----------|
| ✅ 收到 `code=100503` | 提示"另一个操作正在进行,请稍后重试",**允许用户重试**,不当系统错误上报、不静默吞掉 |
| ✅ 判断成功/失败 | 一律读响应体 `code`(`===200` 才算成功),不能只看 HTTP 状态码 |
| ❌ 只看 HTTP 状态码 | `100503` 场景下 HTTP 仍是 200,只看状态码会把失败误判为成功,界面会"假装成功"但后端实际零写入 |
| ❌ 把 `100503` 当致命错误弹全屏报错 | 应视为可重试的低频并发退避,不要引导用户联系客服或上报崩溃监控 |
### 三组锁各自的互斥范围(不要混淆)
| 锁 name | 互斥范围 | 不在范围内 |
|---------|----------|------------|
| `traveler:delete:order` | §1 admin 删除 / §2 mp 删除 / §3 internal Feign 删除,三者对**同一 orderId** | 出行人的新增/编辑(属于 `traveler:batch-edit` 组) |
| `traveler:batch-edit` | §4 admin 批量编辑 / §5 订单调整提交(仅含出行人变更时)/ §6 mp 出行人补全,三者对**同一 orderId** | 出行人的删除(属于 `traveler:delete:order` 组);§5 的非出行人子域(改期/行程/住宿/用车)与本锁无关 |
| `order-invoice:apply` | §7 admin 申请 / §8 mp 申请,两者对**同一 orderId** | 「完成开票」`issueInvoice`、「重新上传」`reuploadInvoice`、「申请重开」`reissueInvoice`(那三者共用**另一把**锁 `order-invoice:issue`,已在 PR #8043 / changelog `20_7980_发票签发重传重开三写口补同一把锁新增100503冲突码触达C端-修改接口-管理后台.md` 中处理,与本文档不是同一组) |
### 幂等与并发冲突的区别
- 请求体**逐字节相同**且在幂等窗口内重复提交 → 幂等拦截,返回 `100502`(请勿重复提交),这是**另一个已存在的错误码**,本次不变。
- 请求体**不同**或已过幂等窗口,但撞上组内另一写口正在执行的同订单请求 → 本次新增的 `100503`。
---
## 五、数据库行为(涉及写操作时必写)
本次不改变任何一次**成功**写操作实际写入的行数或内容——8 个端点各自原有的写入逻辑(软删/桥接表级联/批量 upsert/发票 INSERT)逐字节不变。变化的只是"谁能在同一时刻对同一 orderId 执行本组写入":
| 组 | 改动前 | 改动后 |
|----|--------|--------|
| `traveler:delete:order` | admin/mp/internal 各持一把锁(internal 因自调用问题实际从未持锁),可并发互相踩踏「最后 1 位成人」内存计数 | 三者共享同一把锁,串行执行 |
| `traveler:batch-edit` | admin/adjustment/mp 各持一把锁,可并发各读旧快照后各自写库 | 三者共享同一把锁,串行执行 |
| `order-invoice:apply` | admin/mp 各持一把锁,可并发各读 `existsActive` 旧快照后各插一条申请 | 两者共享同一把锁,串行执行,一单一票才真正生效 |
| 抢锁失败时是否有部分写入 | N/A(此前不会因锁而失败) | 否,零写入(`@Lock4j` 方法级环绕拦截,拿不到锁直接抛异常) |
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在/不属于当前用户 → 581102/581110/581122/581470(各端点适用情况见上)
- 出行人删除相关业务闸口(已签合同/最后成人)→ 581106/581107,不受本次改动影响
- 出行人批量编辑校验矩阵(12301/格式/配额)→ 581101/581103/581104/581112-581114/581118/581119/581136,不受本次改动影响
- mp 端状态门禁(订单确认后禁止自助改动)→ 581145,不受本次改动影响
- 发票一单一票/状态门槛 → 581511/581524,不受本次改动影响
- **抢锁超时(本次新增)→ 100503,HTTP 200,可重试,零写入**
- 幂等窗口内重复提交(请求体相同)→ 100502,与本次改动无关,行为不变
- 老数据兼容:本次不涉及字段增删,存量数据无需迁移
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
### 出行人性别(gender,出现于 §4/§6)
**所属字段**: `travelers[].gender` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `1` | 男 | - |
| `2` | 女 | - |
| `0` | 未知 | - |
### 发票类型(invoiceType,出现于 §7/§8)
**所属字段**: `invoiceType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `VAT_NORMAL` | 增值税普通发票 | 默认场景 |
| `VAT_SPECIAL` | 增值税专用发票 | 要求 `titleType=COMPANY`,且税号/银行/注册信息 5 字段全必填 |
### 发票抬头类型(titleType,出现于 §7/§8)
**所属字段**: `titleType` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `COMPANY` | 单位 | 抬头为公司名,`taxNo` 必填 |
| `PERSONAL` | 个人 | 抬头为个人姓名,`taxNo` 非必填 |
### 发票状态(status,出现于 §7/§8 出参)
**所属字段**: `AdminInvoiceApplyRespVO.status` / `MpInvoiceApplyRespVO.status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `REQUESTED` | 待开票 | 申请成功后的初始状态(admin/mp 两端申请落库值一致,均为 `REQUESTED`) |
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| (无)| 8 个接口的请求字段、响应字段逐字节不变 | 同左 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| `traveler:delete:order` 组三入口并发 | 各持一把锁(internal 一支因自调用问题实际从未持锁),可并发破坏「最后 1 位成人」计数 | 三者共用同一把锁,串行执行 |
| `traveler:batch-edit` 组三入口并发 | 各持一把锁,可并发各按旧快照写库 | 三者共用同一把锁,串行执行 |
| `order-invoice:apply` 组两入口并发 | 各持一把锁,一单一票检查可被并发穿透 | 两者共用同一把锁,串行执行 |
| 抢锁失败的响应 | 不存在这条路径 | 路径真实存在(返回 100503,HTTP 200,业务失败,可重试);本次改动未部署测试服,无实测触发频率数据 |
| 请求/响应字段、既有业务错误码 | 不变 | 不变 |
## 六.7、影响评估(修改/删除类必写)
- **是否破坏向后兼容**: 否——不改字段、不改既有错误码语义;但**新增了一条此前不存在的失败路径**(100503),且该路径会传导至小程序 C 端。
- **前端是否必须同步上线**: 建议同步——尤其是小程序 C 端出行人补全/删除、发票申请三个入口,命中该分支时若前端无兜底会把失败误判为成功(HTTP 200)。**触发频率本文档未做任何测试服实测**,不可援引本工单其他 changelog(fleet 价格日历 #8035、发票签发 #8043)测试服实测出的"频率极低"结论——那是各自独立测量的结果,本组三把锁粒度均为单订单,与被测的价格日历/发票签发粒度不同,机制相同不代表频率相同。
- **前端 workaround 清理点**: 无(本次是新增失败路径,不是清理旧 workaround)。
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 上述 8 个端点,抢锁失败时的响应体
- **零影响**:
- 出行人列表查询(`GET /v3/admin/order/{id}/traveler/list`)、出行人校验(`GET .../traveler/validate`)
- 单个出行人新增(`POST /v3/admin/order/{id}/traveler/add`,走另一把独立锁 `order:traveler:add:{id}`,本次未涉及)
- 大交通批次相关接口
- 发票的「完成开票」「重新上传」「申请重开」三个写口(属于另一把锁 `order-invoice:issue`,已在 #8043 处理,非本文档范围)
- 发票详情/列表/下载/企业信息自动回填等只读接口
- `order-ins:`(保险)/ `material:tag:name:`(resource)/ `fleet:msg-tpl:default:`(fleet)三组,本轮判定不修,行为未变
- 8 个端点的请求字段、响应字段结构
- 既有业务错误码的触发条件与文案
- 幂等拦截(100502)的行为
- 单个请求在**无并发**时的行为(延迟、返回值、写入内容全部不变)
- hl-gateway 路由配置——8 个端点路径/方法零变化
---
## 八、测试环境已验证
**部署读数(2026-09-20 23:24)**:hl-order-service-v3 滚动更新两实例,部署前 `c35251b07` → 部署后 `ba8aab3ab`,8186 与 8086 各 12 秒内起监听,`deploy-backend.sh` 报 `rolling deploy complete`。`git merge-base --is-ancestor 43c53c4c9 ba8aab3ab` 返回 **ANCESTOR_YES**。
🔴 本次**未在测试服做功能调用取证,这是有意为之而不是遗漏**:本组端点是删除出行人与批量改出行人,调用它们会**真实删改订单下的出行人行**;测试服订单数据被多个会话共用,删除**撤不回来**,副作用会落在一个不知情的人头上。
互斥本身由**落库级并发 IT** `Lock4jSharedNameConcurrencyIT` 覆盖(3 条用例,含专为自调用入口 `deleteByInternal` 单写的一条),不变量是「订单至少留 1 位成人」;另有 `Lock4jSharedNameContractTest` 按 SpEL 反查全类、钉死三组的成员集合,**能抓住日后新加的第 N 个方法**。
⚠️ **`100503` 在测试服上一次都没有触发过**——不是「测过、不会触发」,是**没测**。
---
## 十、相关文档
- 关联 Issue: [wx/HL#7980](https://git.1814.love:8443/wx/HL/issues/7980)(AC-7)
- 关联 PR: [wx/HL#8049](https://git.1814.love:8443/wx/HL/pulls/8049)
- 相关(另一组锁,非本文档范围): `changelogs-v2/2026-09/20_7980_发票签发重传重开三写口补同一把锁新增100503冲突码触达C端-修改接口-管理后台.md`(PR #8043,`order-invoice:issue` 锁)
## 关联 / 联系人
### 链接
- **Issue**: [#7980](https://git.1814.love:8443/wx/HL/issues/7980)
- **PR**: [#8049](https://git.1814.love:8443/wx/HL/pulls/8049)
- **Merge commit**: [43c53c4c9](https://git.1814.love:8443/wx/HL/commit/43c53c4c9)(合并后回填,backend_status 待部署后翻转)
### 联系人
- **后端负责人**: @wx