docs(changelog): 团期正式派车司机投影到人员配置表,DRIVER 改系统托管(#8653 #8654)
changelog-filename-gate / validate (push) Failing after 2s

PR #8669 已合入 dev-v3(merge commit 5c50782717d6),order-v3 已部署测试服
(jar 5c5078271)并逐条取证通过。

三条对外契约变化:
1. 同一批数据两个读口口径不同——saveConfig 的响应回显与 GROUP_BATCH 扇出副本
   不含司机行(走 selectManualConfigByProductBatchId,带 ne(staff_role, DRIVER)),
   而 getConfig / getConfigWithLiveStaffInfo 含司机行(走 selectByProductBatchId,
   无该谓词)。前端不要假设保存响应即全量。
2. order_batch_staff 现在会出现系统写入的 DRIVER 行,由车务派车回调投影维护,
   不提供人工编辑/删除入口。
3. 新错误码 582120:saveConfig 的 staffList 含 staffRole=DRIVER,或 scopeRoles
   声明 DRIVER,均拒绝整批保存且零写入(守卫排在软删与插入之前)。

Refs #8653
Refs #8654

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-30 22:34:15 +08:00
共同撰写人 Claude Opus 5
父节点 0e8500103a
当前提交 a29c0873e3
@@ -0,0 +1,485 @@
---
schema: "hl-changelog/v2"
ticket: "8654"
title: "团期正式派车司机投影到人员配置表,DRIVER 角色改系统托管"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-09-30"
status_note: ""
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期人员配置: 正式派车司机投影到人员配置表,DRIVER 改系统托管
> **存放目录**: `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3
> **PR**: #8669
> **Issue**: #8653, #8654
> **日期**: 2026-09-30
> **影响范围**: 管理后台团期人员配置页、团期核单按团看人的名册读取
---
## ⚠️ 关键变化
**团期人员配置表 `order_batch_staff` 现在会有系统自动生成的 DRIVER 行(司机),前端必须适配两个关键变化:**
1. **同一批数据的两个读口口径不同**: 保存接口响应里**不含**司机行,但查询接口(`getConfig` / 名册读取)**含**司机行。前端不要假设响应即全量。
2. **司机行不可编辑**: 这些 DRIVER 行由车务派车自动投影产生,不是运营配置的,前端不要在人员编辑表单提供编辑/删除入口。
3. **新的拒绝错误**(582120):保存请求的 `staffList` 或 `scopeRoles` 里出现 DRIVER,后端会拒绝**整批保存、零写入**。
---
## 一、背景
工单 #8653 与 #8654 合并的功能:**把车务派车产生的司机自动投影到团期人员配置表**,供核单时按团看人、按天算账。
此前司机事实分散在每一户订单的用车需求上,现在统一投影到团期维度,简化核单逻辑。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存团期 staff 配置 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 新增错误码 + 响应内容变化 | DRIVER 行系统管理,拒绝人工写入 |
---
## 三、接口详情
### 1. 保存团期 staff 配置 `PUT /v3/admin/group-batch/{productBatchId}/staff`
**VO**: `BatchStaffConfigReqVO → BatchStaffConfigRespVO`
#### 使用场景
管理后台团期人员配置页,点「保存」按钮时调用此接口保存本期的团队成员(导游、摄影等)。支持按配置位(导游位 GUIDE+LEADER / 摄影位 PHOTOGRAPHER)分范围保存,避免一个弹窗保存时把另一个弹窗的既有数据清空。
保存成功后异步扇出到团内所有活跃订单的人员分配表 `order_staff_assignment`(source=GROUP_BATCH)。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| productBatchId | Path | Long | ✅ | 必须是有效的产品侧排期 ID | 团期所属产品侧班期 ID(非运营团期主键,由 group_tour_batch.batch_id 对应) |
| staffList | Body | List | ✅ | 非 null;显式传 [] 表示清空,不能省略 | 本次保存覆盖范围内的最终成员列表。**禁止含 staffRole=DRIVER 的行**(582120)。若覆盖范围是导游位,必须同时列出 GUIDE 与 LEADER 两个角色成员(工单 #8122)。 |
| staffList[].staffId | Body | Long | ✅ | 有效的员工 ID | 用户域员工 ID |
| staffList[].staffRole | Body | String | ✅ | 取值: LEADER / GUIDE / PHOTOGRAPHER / OTHER / GUIDE_ASSISTANT / STUDY_TEACHER / LIFE_TEACHER;**不可含 DRIVER**(582120) | 员工角色。司机行由车务派车自动投影,一律不由本接口写入。 |
| staffList[].sortOrder | Body | Integer | ❌ | 缺省 0 | 显示排序值(升序排列) |
| staffList[].remark | Body | String | ❌ | ≤500 字符;null 表示保留原值,传空串清空 | 备注,仅供参考 |
| staffList[].serviceStartDate | Body | LocalDate | ❌ | yyyy-MM-dd;不传时保留下来的人沿用原值、新选人员跟随团期;传值若与团期出发日相同则存 null(即跟随团期) | 有效服务开始日(#8468)。若自定义则必须在团期出发日到结束日之间,否则 582119 拒绝。 |
| staffList[].serviceEndDate | Body | LocalDate | ❌ | yyyy-MM-dd;规则同 serviceStartDate;对照团期结束日 | 有效服务结束日(#8468)。 |
| scopeRoles | Body | List<String> | ❌ | 元素不能为 null / 空串 / 纯空白;传了必须 size ≥ 1 | 本次保存覆盖的角色范围。不传 = 整期全量覆盖(历史行为);传了 = 只覆盖这些角色,范围外既有行不动(工单 #8006)。导游位必须同时传 GUIDE 与 LEADER(工单 #8122),只传一个拒绝 582116 且零写入。staffList 里出现范围外角色拒绝 582115 且零写入。**禁止传 DRIVER**(582120)。 |
#### 出参 `Result<BatchStaffConfigRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data.productBatchId | Long | 路径参数回显 |
| data.groupBatchId | Long | 运营团期 ID(order_group_batch 主键) |
| data.staffList | List | 本次保存后**保留下来的整期非司机成员**的最新快照。**不含 DRIVER 行**(工单 #8654)。 |
| data.staffList[].id | Long | 记录 ID(batch_staff_id) |
| data.staffList[].staffId | Long | 员工 ID |
| data.staffList[].staffRole | String | 员工角色(LEADER / GUIDE / PHOTOGRAPHER 等,响应侧**不含 DRIVER**) |
| data.staffList[].staffRoleName | String | 员工角色中文名(「导游」「领队」等;取值不在字典内时回落原 code) |
| data.staffList[].staffName | String | 员工姓名(配置时快照) |
| data.staffList[].staffPhone | String | 员工手机(脱敏:前 3 后 4,如 `138****6677`) |
| data.staffList[].avatarUrl | String | 头像 URL(配置时快照) |
| data.staffList[].sortOrder | Integer | 显示排序值 |
| data.staffList[].remark | String | 备注 |
| data.staffList[].reporterRank | String | 报账人等级:PRIMARY(主) / SECONDARY(次) / NONE(非报账人) |
| data.staffList[].reporterRankName | String | 报账人等级中文名 |
| data.staffList[].serviceStartDate | LocalDate | 有效服务开始日(未自定义时 = 团期出发日,团期改期后跟着变;#8468) |
| data.staffList[].serviceEndDate | LocalDate | 有效服务结束日(规则同上) |
| data.staffList[].serviceDateCustom | Boolean | 是否自定义过服务日期;true 时开始/结束至少一个有自定义值 |
| data.staffList[].baseDailyWage | BigDecimal | 基础日薪(选人时从人员档案快照带出,团期内不可改;仅供参考;单位元;金额以字符串格式返回) |
| data.staffList[].occupancyStatus | String | 占用状态(#8468):FREE / PARTIAL / FULL;按本人有效服务日期段比对其他团期与直派订单;本团未建或日期不完整时为 null;仅提示,不拦截 |
| data.staffList[].occupiedDays | Integer | 被占天数(两端都算);null 时为 0 |
| data.staffList[].freeRanges | List<String> | 可派日期段列表(本人有效日期段内未被占的连续区间);恒非 null;全程占用时为 [] |
| data.staffList[].occupancies | List | 占用明细数组;恒非 null;空闲时为 [] |
| data.affectedOrderCount | Integer | 扇出影响的订单数(已触发异步写入的活跃子订单数) |
#### 请求示例
```http
PUT /v3/admin/group-batch/80001/staff HTTP/1.1
Host: admin-api.test.example.com
Authorization: Bearer {token}
Content-Type: application/json
{
"scopeRoles": ["GUIDE", "LEADER"],
"staffList": [
{
"staffId": 40001,
"staffRole": "LEADER",
"sortOrder": 0,
"remark": "首席领队",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06"
},
{
"staffId": 40002,
"staffRole": "GUIDE",
"sortOrder": 1,
"remark": null,
"serviceStartDate": null,
"serviceEndDate": null
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [
{
"id": 770001,
"staffId": 40001,
"staffRole": "LEADER",
"staffRoleName": "领队",
"staffName": "刘领队",
"staffPhone": "138****6677",
"avatarUrl": "https://example.com/avatar/40001.jpg",
"sortOrder": 0,
"remark": "首席领队",
"reporterRank": "PRIMARY",
"reporterRankName": "主报账人",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06",
"serviceDateCustom": true,
"baseDailyWage": "600.00",
"occupancyStatus": "PARTIAL",
"occupiedDays": 2,
"freeRanges": ["2026-10-01~2026-10-03", "2026-10-05~2026-10-06"],
"occupancies": [
{
"groupBatchId": 90212,
"groupBatchName": "十一国庆游 V2 期",
"conflictDates": ["2026-10-02", "2026-10-04"]
}
]
},
{
"id": 770002,
"staffId": 40002,
"staffRole": "GUIDE",
"staffRoleName": "导游",
"staffName": "王导游",
"staffPhone": "138****5678",
"avatarUrl": "https://example.com/avatar/40002.jpg",
"sortOrder": 1,
"remark": null,
"reporterRank": "NONE",
"reporterRankName": "非报账人",
"serviceStartDate": "2026-10-01",
"serviceEndDate": "2026-10-06",
"serviceDateCustom": false,
"baseDailyWage": "500.00",
"occupancyStatus": "FREE",
"occupiedDays": 0,
"freeRanges": ["2026-10-01~2026-10-06"],
"occupancies": []
}
],
"affectedOrderCount": 3
},
"success": true
}
```
#### 空数据 / 降级响应
整期清空后的响应(`staffList=[]` 加 `scopeRoles` 不传):
```json
{
"code": 200,
"message": "成功",
"data": {
"productBatchId": 80001,
"groupBatchId": 90211,
"staffList": [],
"affectedOrderCount": 3
},
"success": true
}
```
#### 错误响应
**错误码 582120**(司机行系统管理,拒绝人工写入):
```json
{
"code": 200,
"message": "司机由车务派车自动带入团期人员,不能在这里新增或删除;如需调整请到车务派单中维护",
"success": false,
"data": null
}
```
**错误码 582116**(导游位只传半个配置位):
```json
{
"code": 200,
"message": "团期人员配置位 GUIDE 成员缺失,导游位(导游+领队)须同时声明两个角色",
"success": false,
"data": null
}
```
**错误码 582115**(staffList 包含 scopeRoles 范围外的角色):
```json
{
"code": 200,
"message": "员工角色 PHOTOGRAPHER 不在覆盖范围 [GUIDE,LEADER] 内,请修正请求",
"success": false,
"data": null
}
```
**错误码 582119**(服务日期校验失败):
```json
{
"code": 200,
"message": "刘领队 的服务日期(2026-10-06 至 2026-10-05)不合法:开始日期不能晚于结束日期,且须在团期日期(2026-10-01 至 2026-10-10)之内",
"success": false,
"data": null
}
```
#### 业务边界
- **权限**: 接口接 `GroupBatchPermissionGuard.PERMISSION_MANAGE`,需要团期管理权限;权限校验在 Controller 层,拒绝时零写入。
- **幂等性**: 同一请求重复提交视为覆盖保存,第二次提交时无新改动则库表无变化、响应同样 200。
- **扇出并发**: 保存成功后异步扇出到团内全部活跃订单的 `order_staff_assignment(source=GROUP_BATCH)`,前端无需等待此异步过程即可收到 200。
- **DRIVER 行系统管理**: 司机行由车务派车通过 `GroupBatchDriverProjectionService` 自动投影维护,人工配置侧严禁涉及。staffList 或 scopeRoles 里出现 DRIVER 一律拒绝(582120),**整批保存零写入**,连软删都不执行。
- **两个读口口径不同**:
- 本接口(PUT)保存成功后返回的 `staffList` **不含司机行**(只含人工配置的非 DRIVER 角色)。
- 查询接口 `GET /v3/admin/group-batch/{productBatchId}/staff` 与单人查询 `GET .../staff/{staffId}` **含司机行**。
- 前端不要假设响应即全量,名册读取时必须从查询接口获取完整名单(含司机)。
- **团期状态检查**: 入口第一步检查团期成团状态(未建团 589553 / 已确认或已取消 589598);不满足直接拒,代码不走到保存逻辑。
- **服务日期有效期**: serviceStartDate 与 serviceEndDate 必须满足:
- 开始日 ≤ 结束日
- 双双在团期出发日到结束日之间
- 违反任一条拒绝 582119,**整批零写入**。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 导游位全量覆盖 | `{ "scopeRoles": ["GUIDE","LEADER"], "staffList": [{staffId:40001, staffRole:"LEADER"}, {staffId:40002, staffRole:"GUIDE"}] }` | 200,只有这两人留在 GUIDE 和 LEADER 行,其他既有行不动 |
| ✅ 清空整期 | `{ "staffList": [] }` (不传 scopeRoles) | 200,整期人员全清,库表此后仅含司机行 |
| ✅ 导演位自定义服务日期 | `{ "staffList": [{staffId:40001, staffRole:"LEADER", serviceStartDate:"2026-10-01", serviceEndDate:"2026-10-05"}] }` | 200 |
| ❌ 导游位只传半个配置位 | `{ "scopeRoles": ["GUIDE"], "staffList": [...] }` | 拒绝 582116、零写入(缺少 LEADER 配角) |
| ❌ staffList 包含 DRIVER | `{ "staffList": [{staffId:40001, staffRole:"DRIVER"}] }` | 拒绝 582120、零写入(司机由车务派车投影) |
| ❌ scopeRoles 包含 DRIVER | `{ "scopeRoles": ["DRIVER"], "staffList": [] }` | 拒绝 582120、零写入 |
| ❌ 服务开始日晚于结束日 | `{ "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-10-05", serviceEndDate:"2026-10-01"}] }` | 拒绝 582119、零写入 |
| ❌ 服务日期越出团期 | `{ "staffList": [{staffId:40001, staffRole:"GUIDE", serviceStartDate:"2026-09-30"}] }` (团期出发日 2026-10-01) | 拒绝 582119、零写入 |
### scopeRoles 的语义
- **不传**: 整期全量覆盖。staffList 即为团期的最终全量人员配置(除去司机),其他所有角色的既有行会被软删。
- **传了**: 仅覆盖声明的角色。比如只想更新导演位不动摄影位,传 `["GUIDE","LEADER"]` 即可;摄影位的既有行保持不变。
- **导游位特殊性**: 因为导游位同时收 GUIDE 与 LEADER 两个角色,声明导游位必须两个都传。只传其中一个会被拒(582116)——因为「少那一个」意味着覆盖范围不完整,不能正确表达"我只想改导游位"的意图。
- **DRIVER 禁令**: scopeRoles 里出现 DRIVER 拒绝 582120。虽然 DRIVER 在结构上不属于任何配置位(系统管理),但这条禁令是恒定的——不能通过改 scopeRoles 来迂回删除司机行。
---
## 五、数据库行为
保存请求成功后的库表变化:
| 操作 | 对象 | 行为 |
|------|------|------|
| 软删 | `order_batch_staff` | 按 scopeRoles(若不传则整期)清除人工行,**不删司机行**(502120 保护) |
| 插入 | `order_batch_staff` | 按 staffList 新增非 DRIVER 行 |
| 异步扇出 | `order_staff_assignment(source=GROUP_BATCH)` | 更新团内活跃订单的 GROUP_BATCH 副本,内容为最新的整期非司机成员 |
**核心不变量**: 人工配置(GroupBatchStaffConfigService)**只碰非 DRIVER 行**;司机投影(GroupBatchDriverProjectionService)**只碰 DRIVER 行**。两边各管各的子集,交集为空。
---
## 六、边界行为
- **未登录** → 401(网关拦截)
- **无团期管理权限** → 403(权限校验)
- **团期不存在** → 589553(未建团)或 589598(已确认/已取消)
- **下游服务(人员服务、图片服务)降级** → 快照字段(姓名、手机、头像)回落配置时快照,接口仍 200;不阻断保存流程
- **并发冲突** → 保存成功(最后提交的版本胜出,不走 CAS),查询时可能看到中间态
- **司机行来自** → 由 `fleet-service` 派车回调、经 `GroupBatchDriverProjectionService` 投影维护,非人工配置
---
## 六.5、枚举
### staffRole(员工角色)
**所属字段**: `staffList[].staffRole` (请求) / `data.staffList[].staffRole` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `LEADER` | 领队 | 导游位成员之一 |
| `GUIDE` | 导游 | 导游位成员之一(与 LEADER 并收) |
| `PHOTOGRAPHER` | 摄影 | 摄影位成员 |
| `GUIDE_ASSISTANT` | 导游助理 | 其他配置位成员 |
| `STUDY_TEACHER` | 研学老师 | 其他配置位成员 |
| `LIFE_TEACHER` | 生活老师 | 其他配置位成员 |
| `OTHER` | 其他 | 杂项角色(不属于任何标准配置位) |
| `DRIVER` | 司机 | **本接口禁止人工写入**(582120);由车务派车自动投影;查询时可见 |
### reporterRank(报账人等级)
**所属字段**: `data.staffList[].reporterRank` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PRIMARY` | 主报账人 | 团期内唯一;预支款打给此人 |
| `SECONDARY` | 次报账人 | 团期内唯一;备选收款人 |
| `NONE` | 非报账人 | 缺省值;不参与结算 |
### occupancyStatus(占用状态)
**所属字段**: `data.staffList[].occupancyStatus` (响应) | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `FREE` | 空闲 | 有效服务日期段内无其他团期或订单占用 |
| `PARTIAL` | 部分占用 | 有效日期段内某些天被占,某些天可派 |
| `FULL` | 全程占用 | 有效日期段全部被占,无可派日期 |
| `null` | - | 团期未建成或服务日期不完整时为 null;仅提示,不拦截提交 |
---
## 六.6、修改前后对比
### 接口逻辑变化
| 维度 | 改前 | 改后 |
|------|------|------|
| **司机行管理** | 团期人员配置由运营全手工维护,无系统投影 | 司机行由车务派车自动投影,人工配置侧严禁涉及 |
| **保存响应** | 返回整期人员全量(包含一切手工配置) | 返回**非司机成员**快照(DRIVER 行被筛除) |
| **查询响应** | 同保存响应 | **含司机行**(与保存响应口径不同) |
| **错误码新增** | 无 DRIVER 相关拒绝 | 新增 582120:司机行拒绝码,整批零写入 |
| **权限校验** | 无 | 新增 Controller 层权限校验(PERMISSION_MANAGE),拒绝时零写入 |
### 字段级变化
| 字段 | 改前状态 | 改后状态 |
|------|----------|----------|
| `serviceStartDate` / `serviceEndDate` | 无此字段 | 新增可选字段;支持按人自定义服务有效期 |
| `serviceDateCustom` | 无 | 新增,标记是否自定义过服务日期 |
| `baseDailyWage` | 无 | 新增,人员档案快照(配置时带出,不可修改) |
| `occupancyStatus` / `occupiedDays` / `freeRanges` | 无 | 新增,占用查询结果(#8468 D8,仅提示不拦截) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 是(一定程度)
- 响应体新增字段:`serviceStartDate` / `serviceEndDate` / `serviceDateCustom` / `baseDailyWage` / `occupancyStatus` / `occupiedDays` / `freeRanges` / `occupancies`,但字段全可空,字段层兼容。
- 响应 `staffList` 内容变化:**不再包含司机行**(之前如果有系统产生的司机行会出现,现在被筛除)。前端若依赖"返回即全量"会破损。
- 新增错误码 582120:拒绝 DRIVER 行,整批零写入——改动前的正常请求可能现在被拒。
- **前端是否必须同步上线**: 是
- 如果前端在"人员名册""核单名单"等读取页面会显示司机信息,必须从查询接口(GET)而非缓存保存接口的响应来获取;否则缺司机行。
- 如果前端在人员编辑表单提供了 DRIVER 行的编辑/删除入口,必须移除,改为呈现"这是由车务派车产生的"提示。
- UI 需要适配新字段(服务日期、日薪、占用)的显示。
- **前端 workaround 清理点**:
- 若前端之前硬编码了"司机只能通过XX页面配置",现在这句不再对。
- 若前端假设"保存响应即全量名册"来渲染名册卡片,需改为调查询接口。
- 若前端在人员编辑表单里有 DRIVER 选项,需删除;前端用户无法(也不应该)在这里新增/删除司机。
---
## 七、不影响范围
- **仅影响**: 管理后台「团期人员配置」页面与「团期核单」按团看人的名册展示
- **零影响**:
- 订单侧人员分配(仍独立维护 `order_staff_assignment(source=ORDER)`)
- 车务派车流程(按 fleet 业务正常发车、自动投影)
- 人员候选列表查询(`GET .../staff/candidates`)的返回格式
- 团期总体状态、成团判断、团期改期逻辑
- 后端其他模块对人员表的现有查询(如财务核对)
---
## 八、测试环境已验证
以 productBatchId=80001(groupBatchId=90211)为例,真实接口测试:
```
PUT /v3/admin/group-batch/80001/staff
Request: scopeRoles=["GUIDE","LEADER"], staffList=[{staffId:40001, staffRole:"LEADER"}]
→ 200 OK ✓
GET /v3/admin/group-batch/80001/staff
→ 200 OK,staffList 含导游位成员及系统投影司机行 ✓
PUT /v3/admin/group-batch/80001/staff
Request: staffList=[{staffId:40001, staffRole:"DRIVER"}]
→ 拒绝 582120(司机由车务派车自动带入...)✓
PUT /v3/admin/group-batch/80001/staff
Request: scopeRoles=["GUIDE"], staffList=[...](缺 LEADER)
→ 拒绝 582116(导游位成员缺失...)✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|-----------|
| #8669 | #8653, #8654 | 本次变更:司机投影落地,DRIVER 改系统管理 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [#8653](https://git.1814.love:8443/wx/HL/issues/8653), [#8654](https://git.1814.love:8443/wx/HL/issues/8654)
- 关联 PR: [#8669](https://git.1814.love:8443/wx/HL/pulls/8669)
- Merge commit: [5c50782717d6](https://git.1814.love:8443/wx/HL/commit/5c50782717d6)
---
## 关联 / 联系人
### 链接
- **Issue**: [#8653](https://git.1814.love:8443/wx/HL/issues/8653) / [#8654](https://git.1814.love:8443/wx/HL/issues/8654)
- **PR**: [#8669](https://git.1814.love:8443/wx/HL/pulls/8669)
- **Merge commit**: [5c50782717d6](https://git.1814.love:8443/wx/HL/commit/5c50782717d6)
### 联系人
- **后端负责人**: @wx