docs(changelog): #7378 团期行程逐日汇总与逐户下钻(GB-ADM-018/019,新增接口)
changelog-filename-gate / validate (push) Successful in 3s

这个提交包含在:
jw
2026-09-09 17:57:34 +08:00
父节点 d1e45d6d2e
当前提交 c95ab182dc
@@ -0,0 +1,476 @@
---
schema: "hl-changelog/v2"
ticket: "7378"
title: "新增团期行程只读端点 GB-ADM-018/019:逐日汇总 + 某项逐户下钻"
consumer: "admin"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-09"
status_note: "团期详情新增「行程」区块的两个只读端点:一次 GET 拿到全团逐日行程,每项标好「全团一致 / 都有但价量不同 / 只有部分户有」,点某项再 GET 一次下钻到逐户。后端已部署 TEST 并经真实网关逐条实测(含 55 户团压 N+1、差异造数后已全部回滚)。前端三条必看:① 本日合计 dayTotalAmount 只含 consistency=ALL_SAME 的项,每项有 countedInDayTotal、天级有 excludedNodeCount,界面必须提示,否则运营会把它当成整团实际金额(那是核团的事);② ALL_SAME 读 unitPrice/quantity/totalAmount,VALUE_DIFF/PARTIAL 读 unitPriceMin~Max / quantityMin~Max,两边字段互斥;③ nodeKey 不含天,同一景区第 1、2 天各排一次很常见,点哪张卡就把那张卡的 dayNumber 带上下钻,不带会跨天返回、一户多行。团期层无任何行程写口,要改行程用下钻行里的 orderId 跳到该户订单行程页。⚠️ 本需求无原型(2026-08-31 jw 口述新增),界面形态需 jw 与 mmg 另定,后端不阻塞。"
updated_at: "2026-09-09"
base: "dev-v3"
---
# 团期行程逐日汇总与逐户下钻
## 一、给前端的一句话
团期详情现在能按天看整团行程了。**一次 GET 拿到全团逐日行程**,每一项都标好了
「全团一致 / 都有但价量不同 / 只有部分户有」,点某一项再 GET 一次能下钻到**逐户**明细。
两个都是纯新增只读端点,不动任何既有接口,**团期层没有任何行程写口**——
要改行程还是去那户的订单行程页(下钻每行都带 `orderId` 供跳转)。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 团期行程逐日汇总 | GET | `/v3/admin/order/group-batch/:groupBatchId/itinerary` | 新增 | 按天返回全团行程,每项带一致性标记与本日合计 |
| 2 | 团期行程某项逐户下钻 | GET | `/v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey` | 新增 | 某一项的逐户明细,含没有该项的户 |
## 三、接口详情
### 1. 团期行程逐日汇总 `GET /v3/admin/order/group-batch/:groupBatchId/itinerary`
**VO**: `GroupBatchItineraryRespVO`
#### 使用场景
团期详情页新增的「行程」区块。运营出团前要一眼看出「整团第 3 天去哪」以及
「哪几户跟大部队不一样」。行程是**订单级**的:同一期客人产品相同、出发日相同,
创单事务内从产品快照展开固化,骨架本来一样;下单之后每户可以单独加项、改时间、调价。
这个接口把全团各户的行程按天叠在一起,标出哪里已经不一样了。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | path | Long | 是 | 雪花 ID,按字符串传 | 团期 ID |
无 query、无 body。**不分页**——单团期的天数与每天的项数是有限的行程结构,不是无界列表。
#### 出参
`Result<GroupBatchItineraryRespVO>`。
| 字段 | 类型 | 说明 |
|---|---|---|
| `groupBatchId` | String | 团期 ID(雪花) |
| `totalHouseholds` | Integer | 参与统计的活跃户数(分母 M);已取消 / 已退单户不计入 |
| `dayCountConsistent` | Boolean | 各户行程天数是否一致 |
| `dayCount` | Integer | **多数户**的行程天数;无行程时 `0` |
| `dayCountOutliers` | Array | 天数与多数户不同的户;一致时为 `[]` |
| `dayCountOutliers[].orderId` | String | 子订单 ID(雪花) |
| `dayCountOutliers[].orderNo` | String | 子订单编号 |
| `dayCountOutliers[].contactName` | String | 联系人 |
| `dayCountOutliers[].dayCount` | Integer | 该户的行程天数(该户完全没有行程时为 `0`) |
| `days` | Array | 逐日汇总,按 `dayNumber` 升序 |
| `days[].dayNumber` | Integer | 第几天,从 1 起 |
| `days[].dayDate` | String | `yyyy-MM-dd`;各户不一致时取多数户的值 |
| `days[].dayTitle` | String | 该天标题;取多数户的值 |
| `days[].dayTotalAmount` | Number | **本日合计——只含 `ALL_SAME` 项** |
| `days[].excludedNodeCount` | Integer | 未计入本日合计的项数 |
| `days[].nodes` | Array | 该天的行程项 |
| `days[].nodes[].nodeKey` | String | 归组键,下钻接口原样回传 |
| `days[].nodes[].nodeName` | String | 行程项名称 |
| `days[].nodes[].nodeType` | String | 行程项类型(`SCENIC` / `RESTAURANT` / `ACTIVITY` / `SERVICE` / `CUSTOM`) |
| `days[].nodes[].resourceType` | String | 资源类型(`SCENIC_SPOT` / `RESTAURANT` / `ACTIVITY`;自定义项可能为 `null`) |
| `days[].nodes[].resourceId` | String | 资源 ID(雪花);自定义项为 `null` |
| `days[].nodes[].resourceName` | String | 资源名称快照 |
| `days[].nodes[].startTime` | String | 精确时间 `HH:mm`;各户不一致时取多数户的值 |
| `days[].nodes[].timePeriod` | String | 时段说明(上午 / 下午 / 全天);取多数户的值 |
| `days[].nodes[].consistency` | String | `ALL_SAME` / `VALUE_DIFF` / `PARTIAL` |
| `days[].nodes[].householdCount` | Integer | 有该项的户数 N |
| `days[].nodes[].totalHouseholds` | Integer | 参与统计的总户数 M |
| `days[].nodes[].unitPrice` | Number | 单价;**仅 `ALL_SAME` 有值**,否则 `null` |
| `days[].nodes[].unitPriceMin` / `unitPriceMax` | Number | 单价区间;**仅非 `ALL_SAME` 有值** |
| `days[].nodes[].quantity` | Integer | 数量;仅 `ALL_SAME` 有值 |
| `days[].nodes[].quantityMin` / `quantityMax` | Integer | 数量区间;仅非 `ALL_SAME` 有值 |
| `days[].nodes[].totalAmount` | Number | 小计;仅 `ALL_SAME` 有值 |
| `days[].nodes[].countedInDayTotal` | Boolean | 是否计入本日合计——**只有 `ALL_SAME` 才是 `true`** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2097498512387104770/itinerary
Authorization: Bearer <admin token>
```
#### 响应示例
TEST 实测(3 户团,节选第 1 天的两项 —— 一项全团一致、一项只有一户有):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"totalHouseholds": 3,
"dayCountConsistent": false,
"dayCount": 3,
"dayCountOutliers": [
{
"orderId": "2097498673159028737",
"orderNo": "HL20260909093347891",
"contactName": "容量验收客3",
"dayCount": 4
}
],
"days": [
{
"dayNumber": 1,
"dayDate": "2026-11-20",
"dayTitle": "抵达与草原初见",
"dayTotalAmount": 545.00,
"excludedNodeCount": 2,
"nodes": [
{
"nodeKey": "e735c79b9a59a955",
"nodeName": "中俄边境公路(卡线)",
"nodeType": "SCENIC",
"resourceType": "SCENIC_SPOT",
"resourceId": "3001000000000000019",
"resourceName": "中俄边境公路(卡线)",
"startTime": null,
"timePeriod": "MORNING",
"consistency": "ALL_SAME",
"householdCount": 3,
"totalHouseholds": 3,
"unitPrice": 80.00,
"unitPriceMin": null,
"unitPriceMax": null,
"quantity": 1,
"quantityMin": null,
"quantityMax": null,
"totalAmount": 80.00,
"countedInDayTotal": true
},
{
"nodeKey": "3f2d83708b6957db",
"nodeName": "驯鹿拉车体验",
"nodeType": "ACTIVITY",
"resourceType": "ACTIVITY",
"resourceId": "2029926122787946497",
"resourceName": "驯鹿拉车体验",
"startTime": null,
"timePeriod": "MORNING",
"consistency": "VALUE_DIFF",
"householdCount": 3,
"totalHouseholds": 3,
"unitPrice": null,
"unitPriceMin": 60.00,
"unitPriceMax": 90.00,
"quantity": null,
"quantityMin": 1,
"quantityMax": 1,
"totalAmount": null,
"countedInDayTotal": false
}
]
}
]
}
}
```
#### 空数据 / 降级响应
团期存在但**没有活跃子订单**(或活跃户都没有行程)时回空天数组,不报错:
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2089667627927437313",
"totalHouseholds": 0,
"dayCountConsistent": true,
"dayCount": 0,
"dayCountOutliers": [],
"days": []
}
}
```
单项降级:`total_amount`(订单侧的成交小计,签单 / 退款口径下才冻结)为空时,
小计按 `unitPrice × quantity` 现算;两个都缺(如无价的「早餐」)时 `totalAmount` 回 `null`,
**不当 0 计入本日合计**。`startTime` / `timePeriod` / `resourceId` 为空的项照常返回。
#### 错误响应
团期不存在(**与团期详情同码,不是回空数组**):
```json
{ "code": 589500, "message": "团期不存在", "data": null, "success": false }
```
当前角色没有 `group-batch:view` 权限:
```json
{ "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false }
```
缺少或无效的 token 由网关拦截,返回 `401`。本端点不产生其它业务错误码。
#### 业务边界
- 只读端点,不改任何数据;**团期层没有行程写口**。
- 一张表都不建、不给行程表加团期列——实时汇总在团活跃子订单的行程。
- 活跃集与团期其余聚合同口径:已取消 / 已退单 / 已软删户不出现,**也不计入分母 M**。
- 返回全量,不分页。
- `dayDate` / `dayTitle` / `startTime` / `timePeriod` 各户不一致时取**多数户**的值。
- `dayCount` 取多数户的天数;与多数不同的户进 `dayCountOutliers`(完全无行程的户以 `dayCount=0` 出现)。
- 本日合计只含 `ALL_SAME` 项;`80` 与 `80.00` 判为同一个价(按数值比,不比标度)。
- 权限码 `group-batch:view`,与团期看板、六芯片明细、时间线一致。
### 2. 团期行程某项逐户下钻 `GET /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey`
**VO**: `GroupBatchItineraryNodeDetailVO`
#### 使用场景
汇总页看到某项标着「2/3 户」或「¥80~150」时点进来,看清楚**是哪几户不一样、各自多少**。
每一行都带 `orderId`,据此跳到那户的订单行程页去改——团期层没有任何行程编辑入口。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `groupBatchId` | path | Long | 是 | 雪花 ID,按字符串传 | 团期 ID |
| `nodeKey` | path | String | 是 | 原样取自汇总响应 | 归组键 |
| `dayNumber` | query | Integer | 否 | 从 1 起 | 只看第几天;不传 = 跨天全看 |
无 body。
#### 出参
`Result<GroupBatchItineraryNodeDetailVO>`。
| 字段 | 类型 | 说明 |
|---|---|---|
| `groupBatchId` | String | 团期 ID(雪花) |
| `nodeKey` | String | 原样回显请求里的归组键 |
| `dayNumber` | Integer | 原样回显请求里的天;未传时 `null` |
| `nodeName` | String | 行程项名称;无任何户有该项时 `null` |
| `consistency` | String | `ALL_SAME` / `VALUE_DIFF` / `PARTIAL`,与汇总同口径 |
| `householdCount` | Integer | 有该项的户数 N |
| `totalHouseholds` | Integer | 参与统计的总户数 M |
| `items` | Array | 逐户明细,**含没有该项的户** |
| `items[].orderId` | String | 子订单 ID(雪花)——前端据此跳转到该户订单行程 |
| `items[].orderNo` | String | 子订单编号 |
| `items[].contactName` | String | 联系人 |
| `items[].has` | Boolean | 该户有没有这一项 |
| `items[].dayNumber` | Integer | 第几天;`has=false` 时 `null` |
| `items[].unitPrice` | Number | 单价;`has=false` 时 `null` |
| `items[].quantity` | Integer | 数量;`has=false` 时 `null` |
| `items[].totalAmount` | Number | 小计;`has=false` 时 `null` |
| `items[].startTime` | String | 精确时间 `HH:mm` |
| `items[].timePeriod` | String | 时段说明 |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2097498512387104770/itinerary/nodes/3f2d83708b6957db?dayNumber=1
Authorization: Bearer <admin token>
```
#### 响应示例
TEST 实测(该项三户都有,其中一户把单价从 60 改成了 90):
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"nodeKey": "3f2d83708b6957db",
"dayNumber": 1,
"nodeName": "驯鹿拉车体验",
"consistency": "VALUE_DIFF",
"householdCount": 3,
"totalHouseholds": 3,
"items": [
{
"orderId": "2097498512223526914",
"orderNo": "HL20260909093309393",
"contactName": "容量验收客",
"has": true,
"dayNumber": 1,
"unitPrice": 60.00,
"quantity": 1,
"totalAmount": 60.00,
"startTime": null,
"timePeriod": "MORNING"
},
{
"orderId": "2097498671368003585",
"orderNo": "HL20260909093347283",
"contactName": "容量验收客2",
"has": true,
"dayNumber": 1,
"unitPrice": 90.00,
"quantity": 1,
"totalAmount": 90.00,
"startTime": null,
"timePeriod": "MORNING"
},
{
"orderId": "2097498673159028737",
"orderNo": "HL20260909093347891",
"contactName": "容量验收客3",
"has": true,
"dayNumber": 1,
"unitPrice": 60.00,
"quantity": 1,
"totalAmount": 60.00,
"startTime": null,
"timePeriod": "MORNING"
}
]
}
}
```
#### 空数据 / 降级响应
`nodeKey` 在本团期没有任何户命中(包括拼错的键、带了 `dayNumber` 但那天没有该项)时,
返回**零命中的完整户清单**而不是错误——前端拿到的仍是「谁有谁没有」这张表:
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"nodeKey": "deadbeefdeadbeef",
"dayNumber": null,
"nodeName": null,
"consistency": "PARTIAL",
"householdCount": 0,
"totalHouseholds": 3,
"items": [
{ "orderId": "2097498512223526914", "orderNo": "HL20260909093309393", "contactName": "容量验收客", "has": false, "dayNumber": null, "unitPrice": null, "quantity": null, "totalAmount": null, "startTime": null, "timePeriod": null }
]
}
}
```
团期无活跃子订单时 `items` 为 `[]`、`totalHouseholds=0`,同样不报错。
`has=false` 的行除身份字段外全部为 `null`,前端按「未参加」渲染即可。
#### 错误响应
团期不存在:
```json
{ "code": 589500, "message": "团期不存在", "data": null, "success": false }
```
当前角色没有 `group-batch:view` 权限:
```json
{ "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false }
```
缺少或无效的 token 由网关拦截,返回 `401`。**未知 `nodeKey` 不是错误**,见上一节。
#### 业务边界
- 只读端点;`items` 里每行的 `orderId` 就是「去哪改」的跳转依据。
- `items` **含没有该项的户**(`has=false`),顺序与汇总接口的户序一致(按 `orderId` 升序)。
- 不带 `dayNumber` 时跨天返回该项的全部出现,同一户可能多行;带 `dayNumber` 则只看那一天。
- `consistency` 与汇总同口径,但取值集合随 `dayNumber` 收窄而变——只看第 1 天可能 `ALL_SAME`,跨天看可能 `VALUE_DIFF`。
- 同一户同一天重复出现同一项时只取第一条,保证 `householdCount <= totalHouseholds`。
- 已取消 / 已退单 / 已软删户既不出现在 `items` 里,也不计入 `totalHouseholds`。
- 权限码 `group-batch:view`。
## 四、契约约束与正确调用方式
1. **`nodeKey` 只能原样回传,不要自己拼。** 它是 `(nodeName, resourceType, resourceId)` 的
稳定哈希(SHA-256 前 8 字节十六进制),服务端实时算、**不落库**。同一三元组在任何时候、
任何请求里都得到同一个键;但它是服务端实现细节,前端从汇总响应里取到就行。
2. **点哪张卡就把那张卡的 `dayNumber` 带上。** `nodeKey` **不含天**——同一个景区在第 1 天和
第 2 天各排一次是很常见的。不带 `dayNumber` 时下钻会跨天返回该项的全部出现,
同一户可能出现多行(各行带自己的 `dayNumber`),与你点的那张卡就对不上了。
3. **本日合计 `dayTotalAmount` 只含 `consistency=ALL_SAME` 的项。** 每项有
`countedInDayTotal` 标明是否计入,天级有 `excludedNodeCount` 给出被排除的项数——
请在界面上明确提示,否则运营会把这个数当成「整团实际金额」。整团实际金额是核团的事。
4. **`ALL_SAME` 与非 `ALL_SAME` 取值字段不同**:`ALL_SAME` 用 `unitPrice` / `quantity` /
`totalAmount`(此时区间字段为 `null`);`VALUE_DIFF` / `PARTIAL` 用
`unitPriceMin`~`unitPriceMax` / `quantityMin`~`quantityMax`(此时确定值字段为 `null`)。
别只读一边。
5. **团期层没有任何行程写口。** 要改行程,用下钻每行带的 `orderId` 跳到那户的订单行程页去改。
6. **雪花 ID 一律按字符串处理**(`groupBatchId` / `orderId` / `resourceId` 都以字符串下发)。
7. 权限码 `group-batch:view`,与团期看板、六芯片明细、时间线同码。
## 六、边界行为
| 场景 | 行为 |
|---|---|
| 团期不存在 | `589500`(与团期详情同码,非空数组——这两个端点要先确认团期存在) |
| 团期存在但无活跃子订单 | `code=200`,`days=[]`、`totalHouseholds=0`、`dayCount=0`、`dayCountConsistent=true` |
| 有活跃户但都没有行程 | 同上,`days=[]`,不报错 |
| 某户没有行程 | 该户不参与逐项统计,但**计入分母 M**,且在 `dayCountOutliers` 里以 `dayCount=0` 出现 |
| 已取消 / 已退单 / 已软删户 | 不出现在汇总与下钻中,也**不计入分母 M** |
| 不存在的 `nodeKey` | `code=200`,`householdCount=0`、`nodeName=null`、`consistency=PARTIAL`,`items` 里每户 `has=false`,**不报错** |
| `dayNumber` 传了但该天没有该项 | 同上:零命中的空清单,不报错 |
| 各户天数不同 | `dayCountConsistent=false`,`dayCount` 取**多数户**的天数,`dayCountOutliers` 列出与多数不同的户 |
| 同一户同一天重复出现同一项 | 只取第一条;不替订单侧去重,也不因此把该户算成两户(保证 `householdCount <= totalHouseholds`) |
| `80` 与 `80.00` | 判为**同一个价**(按数值比较,不比标度),不会误报成 `VALUE_DIFF` |
| 单价 / 数量为 `null` | 参与一致性判定(都为 `null` 算一致);区间取值时 `null` 不参与 min/max |
| 无 `group-batch:view` 权限 | `589507` |
## 七、不影响范围
- 不改任何既有接口的路径、入参、出参、错误码。
- 不改订单侧行程的任何读写口。
- **无表变更、无 Flyway、无数据迁移、无新增错误码。**
- 网关路由不新增(`/v3/admin/**` 已覆盖)。
- 只滚 `hl-order-service-v3` 一个服务。
## 八、测试环境已验证
TEST(`api.test.1814.love`)真实网关逐条取证。主用团期 `2097498512387104770`(3 户 · 3 天 · 真实产品展开),
另用 `2096412454643802114`(**55 户**)压 N+1、`2089667627927437313`(0 户)压空态。
差异场景由 SQL 造数,**已全部回滚**(回滚后复测:3 户 / 3 天 / 全一致 / 第 1 天合计 `605.00` / `excludedNodeCount=0`,与造数前逐字段一致)。
| 验收项 | 实测 |
|---|---|
| 按天返回全团行程,逐项含名称 / 时间 / 单价 × 数量 = 小计 / 本日合计 | ✅ 3 天,合计 `605.00` / `385.00` / `730.01` |
| 各户完全一致时全为全团项,本日合计 = 单户本日合计 | ✅ 25 项全 `ALL_SAME`;第 1 天 30+80+60+35+60+160+100+80 = `605.00` |
| 有户改价 → 该项返回区间,下钻看到每户各自的价 | ✅ `VALUE_DIFF`,`unitPriceMin/Max = 60/90`;下钻三行 60 / 90 / 60 |
| 有户加项 → 标 N/M,下钻列出是哪几户加的 | ✅ `PARTIAL` `1/3`,下钻 `has=false/false/true` |
| 本日合计只含全团一致项,且标出未计入的项 | ✅ 合计由 `605.00` 降为 `545.00`,`excludedNodeCount=2`,两项 `countedInDayTotal=false` |
| 天数不一致 → 预警 + 列出是哪几户 | ✅ `dayCountConsistent=false`,`dayCount=3`,`dayCountOutliers` 指名到 `HL20260909093347891`(4 天) |
| 下钻每行带 `orderId` 供跳转;团期层无写口 | ✅ 每行均带;本次只新增两个 GET |
| `nodeKey` 稳定、不落库 | ✅ 两次请求 25 个 `nodeKey` 完全一致;库中无该列 |
| 已取消户不出现在汇总与下钻,也不计入分母 | ✅ 置 `CANCELLED` 后分母 3→2,其加项与多出的第 4 天一并消失,天数预警自动解除 |
| 空态与未知 `nodeKey` | ✅ 0 户团回 `days: []`;未知键回零命中清单;团期不存在回 `589500` |
| 权限 | ✅ `CUSTOMIZER` 角色两个端点均 `589507`;无 token 由网关拦 `401` |
| 查询次数与户数无关(禁 N+1) | ✅ `performance_schema` 计数:3 户与 55 户**均恰好 2 条** `order_itinerary_*` 查询;55 户耗时 88ms |
**跨天项实测**(`dayNumber` 存在的理由):「巴尔虎蒙古部落」在第 1、2 天各排一次——
不带 `dayNumber` 下钻回 6 行(3 户 × 2 天,各带自己的 `dayNumber`),带 `dayNumber=1` 回 3 行,与第 1 天那张卡一致。
**小计兜底实测**:该团 `total_amount` 全为空、`unit_price` 有值。兜底前本日合计恒为 `0`,
兜底后 `605.00`;无价的「早餐」小计仍为 `null`,不当 0 计入。
## 十、相关文档
- 实施单编号:GB-ADM-018 / GB-ADM-019(新增),实施单 `docs/group/实施单/04-团期行程安排.html`
- 需求:《团期模块详细设计 v3.0》§3.11「只读汇总、不建表」;SRS §0.13;《数据模型》§A.9D「归组键不落库」
- ⚠️ **前端形态无原型可依**:《需求逐屏对照》的团期详情 12 屏里没有「行程」屏,本需求是 2026-08-31 jw 口述新增,契约与验收要点齐备但没有画面。界面长什么样需 jw 与 mmg 另定。
## 关联 / 联系人
- 工单:https://git.1814.love:8443/wx/HL/issues/7378
- PR:https://git.1814.love:8443/wx/HL/pulls/7397
- 后端:jw;前端:mmg