diff --git a/changelogs/2026-05/23_feat_product_itinerary_day_reorder.md b/changelogs/2026-05/23_feat_product_itinerary_day_reorder.md new file mode 100644 index 0000000..6b75507 --- /dev/null +++ b/changelogs/2026-05/23_feat_product_itinerary_day_reorder.md @@ -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) 评论区留言。