docs(order-v3): #7531 下线 Mock 的行程整体保存端点 + #7530 团期 staff 去重与报账人扇出限定本团
changelog-filename-gate / validate (push) Successful in 3s
changelog-filename-gate / validate (push) Successful in 3s
#7531(删除接口):POST /v3/admin/order/{orderId}/itinerary/save 下线。
该端点自 2026-05-12 落地起就是 Mock,只回显入参 id、从不落库;
下线前已穷举 mmg/hl-ui 全部 4 条远端分支确认零调用(每条带阳性对照),
故前端预期无需改动。整体保存的真实入口是 POST /v3/admin/order/{id}/adjustment/submit。
错误码 583030-583034 号位保留未释放。
#7530(修改接口,两个端点):
1. PUT /v3/admin/group-batch/{productBatchId}/staff 新增 589582——
staffList 内 staffId 重复即拒绝、整批不保存,同一员工不能在同一团期占两个配置位。
前端需提交前去重或直接透出文案。
2. PUT .../staff/{staffId}/reporter-rank 行为收敛——
报账人等级的订单副本同步范围由「全库」收窄为「本团活跃订单」,
响应结构与错误码不变;存量重复行不再抛裸 500。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,420 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7530"
|
||||
title: "团期 staff 配置 staffId 去重(新增 589582)+ 报账人订单副本扇出收窄到本团"
|
||||
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-12"
|
||||
status_note: "两处变化都需要前端知晓:① staffList 内 staffId 重复将被 589582 拒绝、整批不保存,前端需提交前去重或直接透出文案;② 报账人等级的订单副本同步范围由「全库」收窄为「本团活跃订单」,响应结构与错误码不变。"
|
||||
updated_at: "2026-09-12"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 团期 staff 配置 staffId 去重 + 报账人扇出限定本团
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #7571
|
||||
**Issue**: #7530
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **`staffList` 内 `staffId` 重复会被拒绝(新增 `589582`),整批不保存。** 同一员工**不能在同一团期占两个配置位**(比如既当导游又当摄影)。前端需要在提交前自行去重,或把 589582 的文案直接透出给运营。
|
||||
|
||||
> **为什么是拒绝而不是静默去重**:重复的两条 item 可以携带**不同的** `staffRole` / `sortOrder` / `remark`,静默去重等于服务端替业务随机挑一条,没有正确答案。
|
||||
|
||||
🔴 **改前这个重复会把该团期的报账人设置入口打成永久 500。** 重复的两条会原样写两行,之后调 `reporter-rank` 时那个「按团期+员工查单行」的查询命中多行,抛出未转译的 `TooManyResultsException` —— 运营看到的是裸 500,没有可理解的提示,也没有自助恢复手段。本单同时把读取侧改成容错(见下)。
|
||||
|
||||
🔴 **报账人等级的「订单副本」同步范围由「全库」收窄为「本团活跃订单」。** 改前给 A 团设主报账人,会把与 A 团毫无关系的 B、C 团的订单副本一起改掉;改后只动本团。**响应结构与错误码不变**,前端若此前依赖过跨团联动(不应存在),需自查。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
团期报账人(`PRIMARY` 主报账人 / `SECONDARY` 次报账人)决定团期预支与结算时款项打给谁 —— 财务候选接口就是按 `reporterRank` 排序并把 `PRIMARY` 标为默认收款人。
|
||||
|
||||
本单修两个**互相独立、落在同一批文件**的缺陷:
|
||||
|
||||
1. **保存不校验重复**:`staffList` 内两条相同 `staffId` 会原样落两行,之后报账人设置入口永久 500。
|
||||
2. **扇出越界**:设置报账人时,团级行的更新已限定本团,但紧接着两次订单副本写入**没有任何团期或订单维度条件**,会横扫全库所有团期共享来源的副本行。后果是**跨团数据污染** —— 被打坏的团里,团级表说乙是主报账人、订单副本说乙是 `NONE`,两张表长期不一致且**不报任何错**。团期越多、配主报账人的动作越频繁,被打坏的团越多。
|
||||
|
||||
两处不是同一个根因:缺陷 2 与是否重复无关,缺陷 1 即使修完,扇出照样越界。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 团期 staff 全量保存 | PUT | `/v3/admin/group-batch/{productBatchId}/staff` | 修改 | **新增错误码 589582**:`staffList` 内 `staffId` 重复即拒绝,整批不保存 |
|
||||
| 2 | 设置团期报账人等级 | PUT | `/v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank` | 修改 | 行为收敛:订单副本同步范围由「全库」收窄为「本团活跃订单」;存量重复行不再 500 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 团期 staff 全量保存 `PUT /v3/admin/group-batch/{productBatchId}/staff`
|
||||
|
||||
**VO**: `BatchStaffConfigReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
hl-ui 管理后台团期详情页「配置人员」,全量覆盖该团期的共享 staff 配置,并异步扇出到团内活跃订单。**本单起,请求体内 `staffId` 重复会被拒绝。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productBatchId | Path | Long | ✅ | 产品侧排期 ID | **不是**运营团期主键 `groupBatchId` |
|
||||
| staffList | Body | Array | ✅ | 全量覆盖语义 | 显式传 `[]` = 清空;缺字段 / 传 `null` → 400(**行为不变**) |
|
||||
| staffList[].staffId | Body | Long | ✅ | **本单新增约束:列表内不得重复**,重复即 589582 | 员工 ID |
|
||||
| staffList[].staffRole | Body | String | ✅ | `LEADER\|GUIDE\|DRIVER\|PHOTOGRAPHER\|OTHER\|GUIDE_ASSISTANT\|STUDY_TEACHER\|LIFE_TEACHER` | 角色(**行为不变**) |
|
||||
| staffList[].sortOrder | Body | Integer | ❌ | 默认 0 | 展示排序(**行为不变**) |
|
||||
| staffList[].remark | Body | String | ❌ | ≤500 | 备注(**行为不变**) |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| staffList | Array | **结构完全不变**(`id` / `staffId` / `staffRole` / `staffName` / `staffPhone` / `avatarUrl` / `sortOrder` / `remark`) |
|
||||
| affectedOrderCount | Integer | **语义完全不变**,本团活跃订单数 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"staffList": [
|
||||
{ "staffId": 1005, "staffRole": "LEADER", "sortOrder": 0 },
|
||||
{ "staffId": 1008, "staffRole": "LEADER", "sortOrder": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"staffList": [
|
||||
{ "id": "2098606570236481538", "staffId": 1005, "staffRole": "LEADER", "staffName": "刘大山", "staffPhone": "138****1005", "avatarUrl": null, "sortOrder": 0, "remark": null },
|
||||
{ "id": "2098606570240675841", "staffId": 1008, "staffRole": "LEADER", "staffName": "萨仁高娃", "staffPhone": "139****1008", "avatarUrl": null, "sortOrder": 1, "remark": null }
|
||||
],
|
||||
"affectedOrderCount": 2
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
显式传 `{"staffList": []}` 仍是**清空语义**(**不会**误报 589582):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": { "staffList": [], "affectedOrderCount": 2 },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| **589582** | `BATCH_STAFF_DUPLICATED` | `staffList` 内出现重复 `staffId` | 🆕 **新增** |
|
||||
| 400 | — | 缺 `staffList` 字段 / 传 `null` | 不变 |
|
||||
| 582114 | — | 角色与人员类型不符 | 不变 |
|
||||
| 589552 / 589553 | — | 团期未成团 / 未建团 | 不变 |
|
||||
| 589507 | `GROUP_BATCH_PERMISSION_DENIED` | 无 `group-batch:manage` 权限 | 不变 |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589582,
|
||||
"message": "同一员工在本次团期人员配置中重复出现(staffId=1005),请去重后重试",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
文案里的 staffId 是**原样的数字字符串**(`1005`),**不带千位分隔符**,可直接拿去页面上定位那个人。
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **拒绝时零写入、零 Feign**:校验在人员反查(Feign)之前,必然失败的请求不会打任何下游、不会软删旧配置、不会插任何行 —— 调用前后 `order_batch_staff` 逐字节一致(行数、主键、`update_time` 全等),扇出副本也一格未动。
|
||||
- **`staffId` 为 `null` 的元素跳过不判**:`@NotNull` 已在参数校验层挡住,不在这里重复造错误语义。
|
||||
- **同一员工不能在同一团期占两个配置位**。若业务确有「一人两位」的诉求,属新需求,不在本单。
|
||||
- **存量重复行本单不清理**:`order_batch_staff` 目前没有 `(product_batch_id, staff_id)` 唯一索引,本单也不加(无 DDL 序号)。存量重复由接口 2 的读取侧兜底,数据订正与建索引列入后续工单。
|
||||
|
||||
---
|
||||
|
||||
### 2. 设置团期报账人等级 `PUT /v3/admin/group-batch/{productBatchId}/staff/{staffId}/reporter-rank`
|
||||
|
||||
**VO**: `SetReporterRankReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
hl-ui 管理后台把团期内某位 staff 设为主报账人 / 次报账人 / 无。该等级决定团期预支与结算时款项打给谁。**本单起,订单副本的同步范围收窄为本团活跃订单。**
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| productBatchId | Path | Long | ✅ | 产品侧排期 ID | **入参形态完全不变** |
|
||||
| staffId | Path | Long | ✅ | 员工 ID,须已在该团期已派列表内 | **入参形态完全不变** |
|
||||
| reporterRank | Body | String | ✅ | `PRIMARY` / `SECONDARY` / `NONE` | **入参形态完全不变** |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| data | null | **响应结构完全不变**,成功即 `{"code":200,"data":null,"success":true}` |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"reporterRank": "PRIMARY"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
**本团没有任何活跃订单**(订单全为已完成 / 已取消,或该团零订单)时,**仍返 200**,团级行照常更新,只是**不执行**任何订单副本写入:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**本单未新增、未调整任何错误码。**
|
||||
|
||||
| 码 | 符号 | 触发 | 本单 |
|
||||
|----|------|------|------|
|
||||
| 589508 | `GROUP_BATCH_REPORTER_NOT_ASSIGNED` | `staffId` 不在该团期已派列表 | 不变 |
|
||||
| 589507 | `GROUP_BATCH_PERMISSION_DENIED` | 无 `group-batch:manage` 权限 | 不变 |
|
||||
| 589552 / 589553 | — | 团期未成团 / 未建团 | 不变 |
|
||||
| ~~裸 500~~ | — | ~~存量重复行导致 `TooManyResultsException`~~ | ✅ **本单消除** |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589508,
|
||||
"message": "该员工不是本团期的已派人员,无法设置报账人",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **报账人唯一性的作用域是「团内」**,不是全局:设新主时,**只有本团**的旧主降级为 `NONE`;别的团不受任何影响。
|
||||
- **跨团同人**:同一位员工同时在 A、B 两团时,在 A 团把他设为主报账人,**不会**改动 B 团里他的副本。
|
||||
- **`source=ORDER` 的人员行永不被触碰**:本端点只同步团期共享来源(`GROUP_BATCH`)的副本。
|
||||
- **已完成 / 已取消订单的副本保持历史值**:活跃订单口径排除这两类,它们的副本不再随团期设置更新,停在历史值。这是**已知且接受**的口径收窄 —— 已完成订单的人员行本就是归档快照,且改前它反而会被**别的团**的操作随机改坏。
|
||||
- **存量重复行不再 500**:读取侧改为「取 `batch_staff_id` 最小的一行(最早插入的那行)」,返回 200 而不是裸 500。存量重复时另一行的等级保持旧值,由后续的数据订正单一次性清理。
|
||||
- **并发双主窗口本单不修**:两个管理员同时把甲、乙设为主报账人,仍可能留下双主。这是既有缺口,与本单两个缺陷无因果关系,已列后续工单。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **提交 staff 配置前请自行去重**,或把 589582 的文案直接透出。判断依据是 `code === 589582`,文案里已含出事的 `staffId`。
|
||||
- **不要把 589582 当成可重试错误**:原样重发一定还是 589582,必须先去重。
|
||||
- **不要依赖跨团联动**:在 A 团设报账人**不会**、也**不应该**改动 B 团。若有页面逻辑建立在「改一个团会同步别的团」之上,那个假设改前就是缺陷的表现,不是契约。
|
||||
- **团期 staff 列表接口看不到报账人等级**:`BatchStaffConfigRespVO.BatchStaffItemVO` **不含** `reporterRank` 字段。要读报账人等级请用:
|
||||
- 团级:`GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates`(注意这里是**运营团期主键** `groupBatchId`,不是 `productBatchId`)
|
||||
- 订单副本:`GET /v3/admin/order/{orderId}/staff` 的 `reporterRank`
|
||||
- **两个 path 参数的 ID 空间不同**:本单两个端点用的是**产品侧排期 ID** `productBatchId`;财务候选接口用的是**运营团期主键** `groupBatchId`。不要混用。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
**本单零数据库变更**:无新增/修改表、无新增列、**无新增索引**、**无 Flyway 迁移脚本**。
|
||||
|
||||
| 表 | 本单行为 |
|
||||
|---|---|
|
||||
| `order_batch_staff` | 读写,**无结构变更**。写入侧多了一道请求体去重校验(拒绝时零写入);读取侧由「查唯一一条」改为「取 `batch_staff_id` 最小的一条」,对存量重复行容错 |
|
||||
| `order_staff_assignment` | 写,**无结构变更**。两次报账人等级同步的 `WHERE` 条件**各多了一个订单集合限定**(`order_id IN (本团活跃订单)`),由全表收窄为本团 |
|
||||
| `order_main` | **只读**,经既有服务契约取本团活跃订单 ID(排除已完成 / 已取消) |
|
||||
|
||||
**存量数据本单不订正**(明确列出,避免误以为已修):
|
||||
- `order_batch_staff` 中已产生的重复行**不清理**;
|
||||
- 因原缺陷被跨团清成 `NONE` 的 `order_staff_assignment` 副本**不回填**。
|
||||
|
||||
两项均已列入后续工单(数据订正 → 再加唯一索引,顺序不能反)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| `staffList` 内两条相同 `staffId`(`staffRole` 不同) | `code=589582`,**整批不保存**,零 Feign、零写入 |
|
||||
| `staffList` 内两条相同 `staffId`(完全相同) | 同上,`code=589582` |
|
||||
| `staffList` 为 `[]` | **200**,清空语义,不误报 589582 |
|
||||
| 缺 `staffList` 字段 | **400**「staff 配置列表不能缺失;确要清空请显式传空数组 []」 |
|
||||
| `staffId` 互不相同 | **200**,结构与 `affectedOrderCount` 与改前一致 |
|
||||
| 库里已有同一员工的两行(存量脏数据),调 reporter-rank | **200**(改前是裸 500),更新的是 `batch_staff_id` 较小的那一行 |
|
||||
| A 团设主报账人,B 团有同等级的人 | B 团团级与订单副本**一格不动**(改前 B 团副本会被清成 `NONE`) |
|
||||
| 同一员工同时在 A、B 两团,在 A 团设为主报账人 | B 团里他的副本**保持原值**(改前会被一并置为 `PRIMARY`) |
|
||||
| 本团零活跃订单,设报账人 | **200**,团级行照常更新,**零副本写入** |
|
||||
| 本团有已完成 / 已取消订单 | 它们的副本**保持历史值**,不再被更新(已知且接受) |
|
||||
| 订单里 `source=ORDER` 的人员行 | **永不被触碰** |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `PUT .../staff` 传重复 `staffId` | **200**,原样写两行 | **589582**,整批不保存 |
|
||||
| 重复写入后再调 `reporter-rank` | **裸 500**(`TooManyResultsException`,非业务码),该团期入口永久不可用 | **200**,取最早插入的那一行 |
|
||||
| `reporter-rank` 的订单副本同步范围 | **全库**所有团期共享来源的副本 | **本团活跃订单** |
|
||||
| 在 A 团设主报账人对 B 团的影响 | B 团同等级副本被清成 `NONE`、同人副本被置为 `PRIMARY`(与 B 团团级表长期不一致,且不报错) | **零影响** |
|
||||
| 本团零活跃订单时 | 仍执行全库更新 | **零副本写入**,团级行照常更新 |
|
||||
| 已完成 / 已取消订单的副本 | 会被(别的团的操作随机)改动 | **保持历史值**,不再被更新 |
|
||||
| `source=ORDER` 的人员行 | 不受影响 | **不受影响**(不变) |
|
||||
| 两个端点的路径 / 方法 / 请求体结构 | — | **完全不变** |
|
||||
| 两个端点的响应体结构 | — | **完全不变** |
|
||||
| `reporter-rank` 的错误码 | — | **无新增、无调整** |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:
|
||||
- 端点 1 **新增一个失败分支**。原先能通过的「同一人配两个位」的请求会从 200 变成 589582。本次调查未发现前端存在该姿势,但归属方是 hl-ui,**请 mmg 自查**。其余请求形态行为完全不变。
|
||||
- 端点 2 **响应结构与错误码完全不变**,只是副作用范围收窄。正常使用方无感知。
|
||||
- **需要前端动的**:
|
||||
1. 提交 staff 配置前去重,或把 589582 文案透出;
|
||||
2. 自查是否有依赖跨团联动的逻辑(不应存在)。
|
||||
- **数据影响**:本单只止血,**不订正存量**。已被打坏的副本需要等后续的数据订正单,在那之前团级表与订单副本可能仍不一致。
|
||||
- **性能**:`reporter-rank` 每次多一条本团活跃订单的主键查询(本服务内 DB 查询,非 Feign);换来的是两条 `UPDATE` 由**全表扫描**收窄为按订单集合命中,代价远小于改前。单团扇出量通常 <20 个订单,`IN` 列表不会膨胀到需要分批。
|
||||
- **不影响任何其它服务**:本单只改 order-v3,不动网关路由、不动 `hl-common`、不动其它微服务。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **`GET /v3/admin/group-batch/{productBatchId}/staff`(查配置)与 `/staff/candidates`(候选列表)零改动。**
|
||||
- **`GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates`(财务候选)零改动** —— 它是本单验收时的观测出口,本身未被修改。
|
||||
- **`GET /v3/admin/order/{orderId}/staff`(订单人员)零改动** —— 同上,仅作为观测出口。
|
||||
- **`source=ORDER` / `source=FLEET` 的人员行零影响**(实测 `FLEET` 524 行 / 2 个主报账人在全程各次动作后数字未变)。
|
||||
- **权限口径零改动**:两个写口仍是 `group-batch:manage`,拒绝时零写入。
|
||||
- **零改动**:Controller 方法体、Entity、Flyway、网关配置、`hl-common`、`hl-mp-service`、`hl-fleet-service`、小程序端。
|
||||
- **既有错误码零调整**:589507 / 589508 / 589552 / 589553 / 582114 的号码、文案、触发条件全部不变。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署**:`hl-order-service-v3` @ `fix/7530-staff-dedupe-fanout` / `4bc6f4855`(已合并 dev-v3 = `d9ed89e55`),双实例滚动重启完成(8086 / 8186)。**全部实测经真实网关 `api.test.1814.love:9443` + Bearer 鉴权**。
|
||||
|
||||
造数用了 3 个**既有**团期(未新建团期、未下新订单):**A 团** 2 个活跃订单、**B 团** 2 个活跃订单 + 1 个已取消订单、**C 团** 零订单。员工取资源域真人:甲 1005 / 乙 1007 / 丙 1008 / 丁 1009。测试后现场已完全恢复(两张表的活跃行都回到测前的 0 行,3 条手工夹具行已硬删)。
|
||||
|
||||
### 去重(端点 1)
|
||||
|
||||
```
|
||||
PUT /v3/admin/group-batch/2096494989130260482/staff
|
||||
body: {"staffList":[{"staffId":1005,"staffRole":"LEADER"},{"staffId":1008,"staffRole":"LEADER"},{"staffId":1005,"staffRole":"GUIDE"}]}
|
||||
-> {"code":589582,"message":"同一员工在本次团期人员配置中重复出现(staffId=1005),请去重后重试"}
|
||||
调用前后 order_batch_staff 逐字节一致(行数 / 主键 / update_time 全等);扇出副本 update_time 也未变
|
||||
```
|
||||
|
||||
正常路径与边界:`staffId` 互不相同 → **200**,`affectedOrderCount=2`,结构与改前一致;`{}` → **400**「staff 配置列表不能缺失」;`{"staffList":[]}` → **200** 清空语义,不误报 589582。
|
||||
|
||||
### 存量重复行不再 500(端点 2)
|
||||
|
||||
手工插入第二行同 `(团期, 1005)`,`batch_staff_id=9200000000000000001`(**大于**既有行 `2098605826007609346`):
|
||||
|
||||
```
|
||||
PUT /v3/admin/group-batch/2096494989130260482/staff/1005/reporter-rank body:{"reporterRank":"SECONDARY"}
|
||||
-> {"code":200,"message":"成功","success":true} ← 改前这里是 TooManyResultsException 裸 500
|
||||
|
||||
2098605826007609346 1005 SECONDARY ← 被更新的是 batch_staff_id 较小的那一行
|
||||
9200000000000000001 1005 NONE ← 重复行保持旧值
|
||||
```
|
||||
|
||||
### 两团隔离(核心)
|
||||
|
||||
初始态:A 团 丙1008=PRIMARY,B 团 乙1007=PRIMARY(团级与两团各自的活跃订单副本均已就位)。核心动作:
|
||||
|
||||
```
|
||||
PUT /v3/admin/group-batch/2096494989130260482/staff/1005/reporter-rank body:{"reporterRank":"PRIMARY"}
|
||||
-> {"code":200,"success":true}
|
||||
```
|
||||
|
||||
| 观测出口 | 结果 |
|
||||
|---|---|
|
||||
| A 团 `payee-candidates` | 甲1005 `PRIMARY`(`isDefault:true`)、丙1008 `NONE` ✅ |
|
||||
| A 团**每一个**活跃订单 `GET /v3/admin/order/{id}/staff` | 甲1005 `PRIMARY`、丙1008 `NONE` ✅(2/2 单) |
|
||||
| **B 团** `payee-candidates` | 乙1007 **仍 `PRIMARY`**、甲1005 **仍 `NONE`** ✅ |
|
||||
| **B 团每一个**活跃订单 | 乙1007 **仍 `PRIMARY`**、甲1005 **仍 `NONE`** ✅(2/2 单,改前此处会变成 `NONE`) |
|
||||
| A 团订单里 `source=ORDER` 的丁1009 | **仍 `PRIMARY`**,未被触碰 ✅ |
|
||||
| B 团**已取消**订单上的副本 | **仍 `PRIMARY`**(保持历史值,已知且接受) ✅ |
|
||||
|
||||
### 零活跃订单(且是对扇出收窄的第二条、更强的证明)
|
||||
|
||||
C 团全团零订单,且甲1005 此刻在 A、B 两团都有副本。若扇出仍是全局的,在一个**零订单**的团里把甲设为主报账人会把全库所有甲的副本刷成 `PRIMARY`、并把所有别人的 `PRIMARY` 清成 `NONE`:
|
||||
|
||||
```
|
||||
PUT /v3/admin/group-batch/2089667254789484545/staff/1005/reporter-rank body:{"reporterRank":"PRIMARY"}
|
||||
-> {"code":200,"success":true}
|
||||
团级行正确更新为 PRIMARY
|
||||
全库团期共享副本:动作前后 COUNT(*)=9、MAX(update_time)=10:55:02 完全不变 → 零副本写入 ✅
|
||||
```
|
||||
|
||||
返回 200 而不是 SQL 语法错误,也证明空集合确实没有进入 `IN` 条件。
|
||||
|
||||
### 本地全量单测
|
||||
|
||||
`mvn -o -pl hl-order-service-v3 -am test` → **`Tests run: 9951, Failures: 0, Errors: 13, Skipped: 49`**
|
||||
(数字对账:dev-v3 基线 9942 + 本单新增 9 例 = 9951)。
|
||||
13 个 Errors 全部是 Testcontainers 找不到 Docker(跑全量前停了本机 colima 腾内存),与本单零交集。
|
||||
门禁:`RedLineArchTest` 12/12、`MapperBoundaryArchTest` 26/26、`ErrorCodeUniquenessGuardTest` 3/3 全绿。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love:8443/wx/HL/issues/7530
|
||||
- PR:https://git.1814.love:8443/wx/HL/pulls/7571
|
||||
- 新增错误码:`GroupBatchErrorCode.BATCH_STAFF_DUPLICATED = 589582`(团期段 589500-589599)
|
||||
- 报账人等级枚举与「团期内唯一」口径:`hl-order-service-v3/src/main/java/com/hulalv/order/assignment/enums/ReporterRank.java`
|
||||
- 团级报账人观测出口:`GET /v3/admin/order/group-batch/{groupBatchId}/advance/payee-candidates`
|
||||
- 订单副本观测出口:`GET /v3/admin/order/{orderId}/staff`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端(hl-ui 管理后台):mmg
|
||||
@@ -0,0 +1,277 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "7531"
|
||||
title: "下线 Mock 的行程整体保存端点 POST /v3/admin/order/{orderId}/itinerary/save"
|
||||
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-12"
|
||||
status_note: "该端点自 2026-05-12 落地至今是 Mock,从未落库一行;下线前已穷举 hl-ui 全部 4 条远端分支确认无人调用,故前端预期无需改动。行程整体保存的真实入口是 POST /v3/admin/order/{id}/adjustment/submit。"
|
||||
updated_at: "2026-09-12"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# order-v3: 下线 Mock 的行程整体保存端点 `POST /v3/admin/order/{orderId}/itinerary/save`
|
||||
|
||||
**服务**: hl-order-service-v3
|
||||
**PR**: #7570
|
||||
**Issue**: #7531
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
🔴 **这个端点从来就没有真的保存过任何东西。** 它自 2026-05-12(`e0fc9de5d`,「6 接口空骨架 + Mock ServiceImpl」)落地起,`ItineraryService#batchSave` 就是 Mock —— **只把请求里的 id 原样回显**,`order_itinerary_day` / `order_itinerary_node` **一行都不写**。也就是说:
|
||||
|
||||
| 你以为 | 实际 |
|
||||
|---|---|
|
||||
| 调了它,行程改动保存成功了 | 返回 200,**库里一个字没变**,刷新页面即回到改前 |
|
||||
| `updatedNodeIds` 是被更新的节点 | 是**请求里传进来的 id 原样回显**,不代表任何写入 |
|
||||
|
||||
🔴 **下线不是「功能被砍」,是「假功能被撤掉」。** 行程整体保存**一直**只有一个真实入口:
|
||||
**`POST /v3/admin/order/{id}/adjustment/submit`**(订单调整提交)。
|
||||
|
||||
🔴 **本次下线前已确认无人调用。** 经 Gitea API 枚举 `mmg/hl-ui` 的**远端分支全集 = 4 条**(`v2.1` / `master` / `v1` / `v2`),**4 条全部检索**,`itinerary/save` 与其字符串拼接形态 `itinerary/${` 均 **0 命中**;每条 ref 都带阳性对照(见「八、测试环境已验证」)。因此**前端预期无需任何改动**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
order-v3 的 §5 行程域在 2026-05-12 以「6 接口空骨架 + Mock ServiceImpl」的形式落地,其中 `batchSave`(整体保存)留了 Mock 实现,类 javadoc 里写着「batchSave 仍为 Mock,下期落地。」。
|
||||
|
||||
此后行程的真实写入能力走的是**订单调整**链路(`adjustment/submit`),`itinerary/save` 这条路再没被真实化过。留着它的代价是:**它长得像一个可用的保存接口,返 200、返 id,但实际是空操作** —— 任何一次误用都会以「保存成功」的外观丢掉用户的全部编辑。
|
||||
|
||||
本单据此下线该端点,并把「为什么下线 / 替代入口是谁」写进 Controller 类 javadoc 存档。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 行程整体保存(Mock) | POST | `/v3/admin/order/{orderId}/itinerary/save` | 删除 | 端点整体下线;改前是 Mock,从不落库。替代入口 `POST /v3/admin/order/{id}/adjustment/submit` |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 行程整体保存(Mock) `POST /v3/admin/order/{orderId}/itinerary/save`
|
||||
|
||||
**VO**: `ItinerarySaveReqVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
**已下线,不再有使用场景。** 改前的设计意图是「订单行程的整体保存」(一次提交多天、多节点的叙事字段改动),但实现始终是 Mock,从未落库。
|
||||
|
||||
需要改订单行程的,一律走 **`POST /v3/admin/order/{id}/adjustment/submit`**(订单调整提交)—— 它是行程改动**唯一**真实落库的入口,改前改后都是。
|
||||
|
||||
#### 入参字段表
|
||||
|
||||
**已下线,不再接受任何入参。** 以下是改前的形态,仅供核对你手上的调用代码是否属于本端点:
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| orderId | Path | Long | ✅ | 订单 ID | **已下线** |
|
||||
| days | Body | Array | ✅ | 行程天列表 | **已下线**。改前传什么都不会落库 |
|
||||
| days[].id | Body | Long | ✅ | 行程天主键 | **已下线**。改前原样回显在 `updatedNodeIds` 里 |
|
||||
|
||||
#### 出参字段表
|
||||
|
||||
**已下线,不再有响应体。** 改前的形态:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| addedDayIds | Array | **已下线**。改前恒为空数组 |
|
||||
| updatedNodeIds | Array | **已下线**。改前是**请求入参的原样回显**,不代表任何写入 |
|
||||
| deletedDayIds | Array | **已下线**。改前恒为空数组 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
**端点已下线。** 以下是改前的请求形态,用于识别需要迁移的调用点:
|
||||
|
||||
```http
|
||||
POST /v3/admin/order/{orderId}/itinerary/save
|
||||
Authorization: Bearer <admin token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"days": [
|
||||
{ "id": 7700000000003, "dayNumber": 1, "title": "首日行程" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**迁移到**:`POST /v3/admin/order/{orderId}/adjustment/submit`。
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**端点已下线,现在的真实响应是 404:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/60001/itinerary/save",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
**不适用** —— 端点已不存在,任何请求(无论 body 是什么、orderId 是否真实存在)都统一返 `code=404`,不存在空数据或降级分支。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
改前该端点声明的 5 个错误码 **583030 ~ 583034** 号位**全部保留、一个未删**(号码与符号原样),只在 javadoc 上标注「端点已于 2026-09-12 随 #7531 下线,号位保留待整体保存真实化时启用」。它们**现在不会再被任何路径抛出**。
|
||||
|
||||
| 码 | 触发(改前) | 现在 |
|
||||
|----|------|------|
|
||||
| 583030 ~ 583034 | 改前该端点的各校验分支 | **不再可达**;号位保留,未释放给他人 |
|
||||
|
||||
现在唯一的响应就是路由未命中:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 404,
|
||||
"message": "接口不存在: POST /v3/admin/order/2098381549387882497/itinerary/save",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **下线是纯删除,不改变任何其它端点的行为。** 同 Controller 的其它行程端点(如 `GET /v3/admin/order/{id}/itinerary`)照常工作。
|
||||
- **替代入口 `POST /v3/admin/order/{id}/adjustment/submit` 本单未做任何改动**,其行为、入参、错误码与改前完全一致。
|
||||
- **不存在「数据迁移」问题**:被下线的端点从未写过库,所以没有它产生的数据需要清理或订正。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
- **不要重试、不要降级兜底**:`POST .../itinerary/save` 现在恒为 `code=404`,重试不会变好。
|
||||
- **行程整体保存请调 `POST /v3/admin/order/{orderId}/adjustment/submit`**。
|
||||
- **不要把改前的 200 当成保存成功的历史证据**:它是 Mock 返回的 200,与落库无关。若有基于「调过 save 所以行程已改」的假设的页面逻辑或数据判断,那个假设改前就是错的。
|
||||
- **错误码 583030-583034 号位不要复用**:它们保留给「整体保存真实化」的后续工单,本单未释放。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
**本单零数据库变更**:无新增/修改表、无新增列、无新增索引、**无 Flyway 迁移脚本**、无 Mapper 改动。
|
||||
|
||||
被下线的端点**改前就不写库**(Mock 实现),因此下线**不产生任何数据影响**:
|
||||
- `order_itinerary_day` / `order_itinerary_node` 的既有数据**不受影响**,一行不动;
|
||||
- 没有「该端点写出来的数据」需要清理或订正 —— 它从未写出过任何数据。
|
||||
|
||||
清单里虽是 POST(写方法),但其**改前的实际写库行为为零**,下线后同样为零。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 调 `POST .../itinerary/save`,orderId 真实存在 | `code=404`「接口不存在」 |
|
||||
| 调 `POST .../itinerary/save`,orderId 不存在 | `code=404`「接口不存在」(与上一行**同样的响应**,不因 id 是否存在而不同) |
|
||||
| body 传空 / 传错 / 不传 | 一律 `code=404`(路由层就没命中,不会走到参数校验) |
|
||||
| 调同 Controller 的其它行程端点 | **照常 200**,不受影响 |
|
||||
| 调替代入口 `adjustment/submit` | **照常工作**,本单未改动 |
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
| 维度 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `POST /v3/admin/order/{orderId}/itinerary/save` | 路由存在,返 `code=200` | **端点不存在**,返 `code=404` |
|
||||
| 该端点的**落库行为** | **零**(Mock,只回显入参 id) | 零(端点已不存在) |
|
||||
| `updatedNodeIds` 的含义 | 请求入参的原样回显 | — |
|
||||
| `ItinerarySaveReqVO` / `ItinerarySaveRespVO` | 存在 | **已删除** |
|
||||
| 错误码 583030-583034 | 声明在该端点上 | **号码与符号原样保留**,仅不再可达 |
|
||||
| `POST /v3/admin/order/{id}/adjustment/submit` | 行程改动的真实入口 | **完全不变** |
|
||||
| 同 Controller 其它行程端点 | — | **完全不变** |
|
||||
| 网关路由配置 | — | **未改动**(`/v3/admin/**` 通配,无需删路由行) |
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **兼容性**:下线前已穷举确认 `mmg/hl-ui` **4 条远端分支全部 0 命中**(含字符串拼接形态),**前端预期无需任何改动**。若某个未纳入这 4 条 ref 的本地分支仍在调用,它会从「200 但什么也没保存」变成「404」—— **后者更诚实**,且迁移目标明确(`adjustment/submit`)。
|
||||
- **数据影响**:**零**。该端点从未落库,下线不丢任何数据、不需要任何数据订正。
|
||||
- **需要前端动的**:正常情况下无。若发现有调用点,改调 `POST /v3/admin/order/{orderId}/adjustment/submit`。
|
||||
- **性能**:无影响(删除的是一段空操作代码路径)。
|
||||
- **不影响任何其它服务**:本单只改 order-v3,不动网关、不动 hl-common、不动其它微服务。
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **`POST /v3/admin/order/{id}/adjustment/submit`(订单调整提交)一行未改** —— 行程改动的真实入口,行为与错误码完全不变。
|
||||
- **`ItineraryAdminController` 的其它行程端点全部不动**(仅删了 `batchSave` 一个方法,并在类 javadoc 追加下线备注)。
|
||||
- **`order_itinerary_day` / `order_itinerary_node` 两张表零变更**,既有数据一行不动。
|
||||
- **`ItineraryErrorCode` 的常量号码与符号一个未改**,只加 javadoc 说明。
|
||||
- **网关路由零改动**、**Flyway 零新增**、**hl-common / hl-mp-service / hl-fleet-service 零改动**。
|
||||
- **小程序端(mp 域)零影响**。
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
**部署**:`hl-order-service-v3` @ `fix/7531-itinerary-save-offline` / `54d9cbe64`(已合并 dev-v3 = `2bd01e6bc`),双实例滚动重启完成。**全部实测经真实网关 `api.test.1814.love:9443` + Bearer 鉴权**。
|
||||
|
||||
### 端点已下线(两条,覆盖「id 不存在」与「id 真实存在」)
|
||||
|
||||
```
|
||||
POST /v3/admin/order/60001/itinerary/save
|
||||
body: {"days":[{"id":7700000000003,"dayNumber":1,"title":"x"}]}
|
||||
-> {"code":404,"message":"接口不存在: POST /v3/admin/order/60001/itinerary/save","success":false}
|
||||
|
||||
POST /v3/admin/order/2098381549387882497/itinerary/save ← 真实存在的订单 id
|
||||
-> {"code":404,"message":"接口不存在: POST /v3/admin/order/2098381549387882497/itinerary/save","success":false}
|
||||
```
|
||||
|
||||
### 阳性对照(证明不是整个 Controller 或服务挂了、也不是网关路由整体丢失)
|
||||
|
||||
```
|
||||
GET /v3/admin/order/2098381549387882497/itinerary -> code=200 同 Controller 的其它端点仍在
|
||||
POST /v3/admin/order/.../adjustment/submit -> code=587002 替代入口路由仍在,且走到了业务层
|
||||
```
|
||||
|
||||
### 前端调用面调查(AC-1,每条 ref 都带阳性对照)
|
||||
|
||||
| ref | commit | 日期 | `itinerary/save` | `itinerary/${` | `adjustment/submit`(阳性对照) |
|
||||
|---|---|---|---|---|---|
|
||||
| `origin/v2.1` | `36c9064a` | 2026-09-12 | **0** | 0 | 6 文件 ✅ |
|
||||
| `origin/master` | `68075168` | 2026-08-15 | **0** | 0 | 1 文件 ✅ |
|
||||
| `origin/v1` | `fb0abc68` | 2026-08-15 | **0** | 0 | 1 文件 ✅ |
|
||||
| `origin/v2` | `157ba864` | 2026-04-30 | **0** | 0 | 0 ⚠️ 见下 |
|
||||
|
||||
`origin/v2` 的阳性对照为 0,孤立的零结果没有分辨力,故补两条独立判据:① 后端端点诞生于 2026-05-12,比该 ref 冻结的 2026-04-30 **晚 12 天**,不可能被它调用;② 该 ref 上 `/v3/admin/order` **0 个文件**命中(对照 `http.post` 命中 53 个文件,证明 grep 在该 ref 上工作正常)。
|
||||
|
||||
### 本地全量单测
|
||||
|
||||
`mvn -o -pl hl-order-service-v3 -am test` → **`Tests run: 9941, Failures: 0, Errors: 13, Skipped: 49`**。
|
||||
13 个 Errors 全部是 Testcontainers 找不到 Docker(跑全量前停了本机 colima 腾内存),与本单零交集。
|
||||
门禁:`RedLineArchTest` 12/12 绿、`MapperBoundaryArchTest` 26/26 绿。
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- Issue:https://git.1814.love:8443/wx/HL/issues/7531
|
||||
- PR:https://git.1814.love:8443/wx/HL/pulls/7570
|
||||
- 端点诞生提交:`e0fc9de5d`(2026-05-12,「6 接口空骨架 + Mock ServiceImpl」#2011)
|
||||
- 下线备注存档:`hl-order-service-v3/src/main/java/com/hulalv/order/itinerary/controller/admin/ItineraryAdminController.java` 类 javadoc
|
||||
- 替代入口:`POST /v3/admin/order/{id}/adjustment/submit`
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
- 后端:jw
|
||||
- 前端(hl-ui 管理后台):mmg
|
||||
在新工单中引用
屏蔽一个用户