feat(product-v2): 行程天数拖拽重排
PR #2926 / Closes #2925 新增 POST /admin/product/item/{productId}/itinerary/reorder - items: [{dayId, newDayNumber}, ...] - 状态校验 (EDITABLE_STATUSES), @Idempotent 5s 防抖 - 子表 product_day_hotel 同步 day_number 测试服真测 T1-T5 全绿,单测 49/49 全绿。 前端 mmg 需实现拖动 UI + 调端点。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
父节点
2d38dcd139
当前提交
704881d3c4
@ -0,0 +1,153 @@
|
||||
# feat(product-v2): 产品行程天数支持拖拽重排
|
||||
|
||||
> **仓库**: HL (后端 hl-product-service-v2)
|
||||
> **关联 PR/Issue**: PR #2926, Closes #2925
|
||||
> **日期**: 2026-05-23
|
||||
> **影响范围**: 新增 admin 端点,前端需新增拖动 UI 交互
|
||||
> **接收方**: mmg (前端)
|
||||
> **前端**: **需要实现拖动 UI**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 业务背景
|
||||
|
||||
产品设计页 Step2「行程」标签,目前每一天的内容只能整体保存,**没法拖动整天重排**。
|
||||
本次后端新增专用 reorder 端点,前端实现拖动后调用即可。
|
||||
|
||||
典型场景:
|
||||
- 原 D1 和 D2 整天对调 (D1↔D2)
|
||||
- 原 D1 拖到第 3 天位置 (D1→D3, 原 D2/D3 自动上移)
|
||||
- 6 天行程整体倒序 (D1↔D6, D2↔D5, D3↔D4)
|
||||
|
||||
---
|
||||
|
||||
## 一、新增 endpoint
|
||||
|
||||
### `POST /admin/product/item/{productId}/itinerary/reorder`
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"dayId": 2043611284271677442, "newDayNumber": 1},
|
||||
{"dayId": 2043611284271677441, "newDayNumber": 2},
|
||||
{"dayId": 2043611284267483139, "newDayNumber": 3},
|
||||
{"dayId": 2043611284267483138, "newDayNumber": 4},
|
||||
{"dayId": 2043611284263288834, "newDayNumber": 5},
|
||||
{"dayId": 2043611284259094529, "newDayNumber": 6}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段约束** (前端必须遵守,否则后端返业务错码):
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `items` | array | ✅ | 重排清单,**必须覆盖该产品当前所有未删除的天**(少传/多传都拒绝) |
|
||||
| `items[].dayId` | Long | ✅ | 行程天 ID (从 `GET /admin/product/item/{id}/itinerary` 接口取) |
|
||||
| `items[].newDayNumber` | int | ✅ | 新天序号 (1-based),所有 newDayNumber 必须组成 1..N 连续整数无重 |
|
||||
|
||||
**响应**:
|
||||
|
||||
| 场景 | code | message |
|
||||
|------|------|---------|
|
||||
| 成功 | 200 | `行程天数重排成功` |
|
||||
| 产品状态不允许 (如 PUBLISHED/PENDING_REVIEW) | **430110** | `当前状态不允许行程重排, 请先下架或撤回审批` |
|
||||
| items 与当前行程不匹配 (size 不等 / dayId 不全) | **430111** | `重排清单与当前行程不匹配, 请刷新页面后重试` |
|
||||
| newDayNumber 不是 1..N 连续整数 (跳号/重复) | **430112** | `新天序号必须为 1..N 连续无重` |
|
||||
| 5 秒内同用户重复点击 | 100502 | `请勿重复提交` (`@Idempotent` 防抖) |
|
||||
|
||||
---
|
||||
|
||||
## 🚨 前端需要做的事
|
||||
|
||||
### 1. 拖动 UI
|
||||
|
||||
参考"待办列表拖动"的常见库 (Vue 端 `vuedraggable` / React 端 `react-beautiful-dnd` 等),把行程按天列出,允许整天上下拖动。
|
||||
|
||||
### 2. 调 reorder 端点
|
||||
|
||||
拖完一次就调一次后端,**不需要等用户点保存按钮**(后端已有 5s 幂等防抖)。
|
||||
|
||||
```ts
|
||||
const items = orderedDays.map((day, idx) => ({
|
||||
dayId: day.dayId,
|
||||
newDayNumber: idx + 1 // 拖动后新序号 = 数组索引 + 1
|
||||
}));
|
||||
|
||||
await axios.post(`/admin/product/item/${productId}/itinerary/reorder`, { items });
|
||||
// 成功后刷新当前页 itinerary,展示新序号
|
||||
```
|
||||
|
||||
### 3. 错码处理
|
||||
|
||||
| code | 用户提示 |
|
||||
|------|---------|
|
||||
| 430110 | 当前状态不允许行程重排,请先下架或撤回审批 |
|
||||
| 430111 | 重排清单与当前行程不匹配,请刷新页面后重试 |
|
||||
| 430112 | (理论上前端不会出这个,如果出说明前端拖动逻辑有 bug) |
|
||||
| 100502 | 操作太快,请稍后再试 |
|
||||
|
||||
### 4. UX 建议
|
||||
|
||||
- **乐观更新**:用户拖完先在本地更新天序号,再调后端,失败时回滚
|
||||
- **不要每次拖动都调后端**:用户连续拖几次可能撞 5s 幂等,可以本地 debounce 500ms 再调
|
||||
- **状态判断**:产品状态非可编辑时禁用拖动手势 (颜色置灰/拖不动)
|
||||
|
||||
### 5. 哪些状态可以拖
|
||||
|
||||
后端通过 `EDITABLE_STATUSES = {DRAFT, REJECTED, UNPUBLISHED, COMPLETED}` 判定,**PUBLISHED 等其他状态会被 430110 拒绝**。前端最好提前判断 product.status 决定是否启用拖动手势,避免无谓 API 调用。
|
||||
|
||||
---
|
||||
|
||||
## 二、后端核心实现 (仅供 mmg 了解,前端无需关心)
|
||||
|
||||
### Service 主流程 `ProductItineraryService.reorderDays`
|
||||
|
||||
1. 状态校验 (复用 `checkEditable`)
|
||||
2. `@Idempotent(timeout=5)` 5 秒防同用户重复点击
|
||||
3. 三道参数校验 (items 数量 / dayId 集合 / newDayNumber 连续无重)
|
||||
4. 事务两阶段更新避免索引冲突:
|
||||
- Step1: 临时把所有 `day_number` 改为负数 (`1..N → -1..-N`)
|
||||
- Step2: 按 items 映射改回正数 newDayNumber
|
||||
5. **同步冗余字段** `product_day_hotel.day_number` (内部聚合接口靠它分组)
|
||||
6. 写 admin 操作日志,`changed_fields="itinerary_order"`
|
||||
|
||||
### 子表关联
|
||||
|
||||
- `product_itinerary_node` / `product_route_point` 通过 `day_id` 关联,**零影响**
|
||||
- `product_day_hotel` 同步 `day_number`
|
||||
|
||||
---
|
||||
|
||||
## 三、测试服真测记录
|
||||
|
||||
部署 product-v2 双实例 (rolling-deploy 2026-05-23 09:15) 后真测:
|
||||
|
||||
| # | 场景 | 实测 |
|
||||
|---|------|------|
|
||||
| T1 | DRAFT 6 天产品 (`2043595307274436609`) D1↔D6/D2↔D5/D3↔D4 全倒序 | ✅ 200 + DB 验证 day_id 不动 day_number 完美倒序 |
|
||||
| T2 | PUBLISHED 产品 (`2043604279016443905`) 调 reorder | ✅ 430110 状态拒绝 |
|
||||
| T3 | DRAFT 5 天产品 items 只传 4 个 | ✅ 430111 数量不匹配 |
|
||||
| T4 | DRAFT 5 天产品 newDayNumber 跳号 (1,3,4,5,6 没 2) | ✅ 430112 新天序号必须为 1..N 连续无重 |
|
||||
| T5 | DRAFT 6 天 + 5 hotel 产品 (`2044280877378134017`) 全倒序 | ✅ itinerary day_number 全反 + hotel day_number 完美同步 |
|
||||
|
||||
单测:`ProductItineraryServiceReorderTest` 10 个 + 既有 `ProductItineraryServiceTest` 39 个 = **49 / 49 全绿**
|
||||
|
||||
---
|
||||
|
||||
## 四、不在本期范围
|
||||
|
||||
- UNIQUE INDEX `(product_id, day_number)` — 等数据 audit 无重复后单独 PR (本期事务两步法已能容忍未来加这个索引)
|
||||
- 快照刷新 — 拖动只影响编辑态,订单/小程序看的是订单快照,无影响
|
||||
- 前端 UI 实现 — mmg 实现
|
||||
|
||||
---
|
||||
|
||||
## 五、联系人
|
||||
|
||||
后端: wx (呼籁旅行)
|
||||
前端: mmg
|
||||
|
||||
如有疑问可在 [#2925](https://git.1814.love:8443/wx/HL/issues/2925) 评论区留言。
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户