feat: 价格日历加单房差字段 (PR #1901)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
父节点
ea6e629d52
当前提交
d240ef0e96
@ -0,0 +1,252 @@
|
|||||||
|
# 价格日历加单房差字段:成人=1 时计入总价
|
||||||
|
|
||||||
|
> **服务**: hl-product-service-v2(8093)+ hl-order-service-v2(8094)
|
||||||
|
> **PR**: #1901
|
||||||
|
> **Issue**: #1900
|
||||||
|
> **日期**: 2026-05-08
|
||||||
|
> **影响范围**: 管理端价格日历(所有产品类型)+ GROUP 班期编辑器 + 小程序订单确认页价格 / 明细面板 + 管理端订单详情明细面板
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 关键变化(管理端 + 小程序必读)
|
||||||
|
|
||||||
|
**价格日历 / 班期新增 `singleRoomDiff` 必填字段(单房差)**:
|
||||||
|
|
||||||
|
- 管理端"编辑价格区间"弹窗 + GROUP"班期编辑"弹窗:**4 个价格输入框旁加一列"单房差 ¥xxx"必填输入框**(0 = 不收)
|
||||||
|
- 小程序订单确认页:**`adultCount == 1` 触发**(严格 == 1,与儿童/小童/幼童数量无关),后端把 `singleRoomDiff` 累加到 `totalPrice`
|
||||||
|
- 订单详情明细面板:`singleRoomSurcharge > 0` 时多出一行"单房差 ¥xxx",= 0 或老订单缺字段时不展示
|
||||||
|
- **定金不变**:比例定金按 `baseTotal`(不含单房差)算;固定/人定金不动
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
1 个成人单住一房时,实际成本高于双人均摊 — 管理员需要为此场景配置单房差。本期实现:
|
||||||
|
|
||||||
|
1. 价格日历(`product_price_calendar`)+ 班期表(`group_tour_batch`)各加 `single_room_diff DECIMAL(12,2) NOT NULL DEFAULT 0`
|
||||||
|
2. 算价时**严格** `adultCount == 1` 触发,累加到 `totalPrice` + 写入 `priceBreakdown` JSON
|
||||||
|
3. 订单明细多一行展示
|
||||||
|
4. 定金算法不变(具体见下文 D2/D3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、变更接口清单
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|------|------|------|----------|------|
|
||||||
|
| 1 | 管理端批量保存价格日历 | POST | `/admin/product/{id}/price-calendar/batch` | 请求体加必填字段 | `singleRoomDiff` 必填(@NotNull @DecimalMin("0.00")) |
|
||||||
|
| 2 | 管理端查询价格日历 | GET | `/admin/product/{id}/price-calendar` | 响应加字段 | 每条记录回传 `singleRoomDiff` |
|
||||||
|
| 3 | 管理端 GROUP 班期保存 | POST/PUT | (Schedule 系列) | 请求体加必填字段 | `singleRoomDiff` 必填 |
|
||||||
|
| 4 | 管理端 GROUP 班期查询 | GET | (Schedule 系列) | 响应加字段 | 回传 `singleRoomDiff` |
|
||||||
|
| 5 | 内部简单报价 | GET | `/internal/product/{productId}/simple-quote` | 响应加 2 字段 | `singleRoomDiff`(配置值)+ `singleRoomSurcharge`(本次实际加价,0 或 = diff) |
|
||||||
|
| 6 | 内部 GROUP 报价 | GET | `/internal/product/{productId}/group-quote` | 响应加 2 字段 | 同上 |
|
||||||
|
| 7 | 小程序订单详情 | GET | `/mp/order/{id}` | **行为变更** | `priceBreakdown.items[]` 当 `singleRoomSurcharge > 0` 时多出一行 "单房差" |
|
||||||
|
| 8 | 管理端订单详情 | GET | `/admin/order/{id}` | 无变化 | admin 端不读 priceBreakdown,只读汇总字段 |
|
||||||
|
| 9 | 创建订单(C 端 / admin) | POST | `/mp/order/create` 等 | **行为变更** | `OrderInfo.totalPrice` 已包含单房差;`OrderInfo.priceBreakdown` JSON 包含 `singleRoomDiff` + `singleRoomSurcharge` 字段 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、决策点
|
||||||
|
|
||||||
|
### D1 触发条件(严格)
|
||||||
|
|
||||||
|
```
|
||||||
|
if (adultCount == 1) {
|
||||||
|
singleRoomSurcharge = singleRoomDiff;
|
||||||
|
} else {
|
||||||
|
singleRoomSurcharge = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**只看 adultCount 是否严格等于 1**,与儿童 / 小童 / 幼童数量无关。`adultCount == 0` 不触发(下单本来就要求至少 1 成人,@Min(1));`adultCount >= 2` 不触发。
|
||||||
|
|
||||||
|
### D2 比例定金基数(不含单房差)
|
||||||
|
|
||||||
|
```
|
||||||
|
比例定金 = baseTotal × ratio / 100
|
||||||
|
其中 baseTotal = totalPrice - singleRoomSurcharge
|
||||||
|
```
|
||||||
|
|
||||||
|
**保证"定金计算逻辑不变"**:不含单房差的基础价 × 比例 = 定金;尾款 = totalPrice - 定金(自然就含了单房差)。
|
||||||
|
|
||||||
|
### D3 固定/人定金不动
|
||||||
|
|
||||||
|
```
|
||||||
|
固定定金 = depositAmount/人 × 定金人头(成人 + 儿童 + 小童)
|
||||||
|
```
|
||||||
|
|
||||||
|
与单房差完全解耦。
|
||||||
|
|
||||||
|
### D7 落库归属(不进 surcharge_amount)
|
||||||
|
|
||||||
|
| 字段 / 表 | 是否含单房差 |
|
||||||
|
|---|---|
|
||||||
|
| `order_info.total_price` | **是**(含单房差) |
|
||||||
|
| `order_info.price_breakdown` JSON | **是**(`singleRoomSurcharge` 字段) |
|
||||||
|
| `order_info.surcharge_amount` | **否**(语义=后期附加费,与单房差解耦) |
|
||||||
|
| `order_surcharge` 表 | **否**(不写单房差行) |
|
||||||
|
|
||||||
|
### D9 修订单 `adminEditOrder` 改 4 count **不重算**单房差
|
||||||
|
|
||||||
|
⚠ **管理端"编辑订单"弹窗改成人/儿童/小童/幼童数后,后端不会重新计算 totalPrice / depositAmount / 单房差**(维持现有契约,Line 140 的 manualPrice 注释已说明:"调价统一走加优惠/加附加费接口")。
|
||||||
|
|
||||||
|
**影响场景**:
|
||||||
|
- 用户原下单成人=1 → 含单房差 200,定制师改为成人=2 → totalPrice 仍含 200(应不含),客户多付 200
|
||||||
|
- 反之改为成人=1,totalPrice 缺 200(应含),少收 200
|
||||||
|
|
||||||
|
**应对**:定制师改人数后,如需调整单房差,**通过"加附加费"接口**手动落 `order_surcharge` 一笔(正负皆可)。本期不开发自动重算路径,后续单独工单再做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、接口详情
|
||||||
|
|
||||||
|
### 1. 管理端批量保存价格日历 `POST /admin/product/{id}/price-calendar/batch`
|
||||||
|
|
||||||
|
**VO**: `PriceCalendarBatchReqVO`
|
||||||
|
|
||||||
|
#### 入参变化
|
||||||
|
|
||||||
|
新增字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"startDate": "2026-04-01",
|
||||||
|
"endDate": "2026-04-30",
|
||||||
|
"tierSeq": 1,
|
||||||
|
"priceType": "NORMAL",
|
||||||
|
"adultSellPrice": 4105,
|
||||||
|
"childSellPrice": 1865,
|
||||||
|
"toddlerDiscount": -500,
|
||||||
|
"infantPrice": 0,
|
||||||
|
"singleRoomDiff": 200,
|
||||||
|
"dailyStock": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 必填 | 校验 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `singleRoomDiff` | **必填** | @NotNull @DecimalMin("0.00") | 不传 → 400;< 0 → 400 |
|
||||||
|
|
||||||
|
### 5. 内部简单报价 `GET /internal/product/{productId}/simple-quote`
|
||||||
|
|
||||||
|
**响应 VO 新增字段**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"adultPrice": 4105,
|
||||||
|
"childPrice": 1865,
|
||||||
|
...,
|
||||||
|
"adultTotal": 4105,
|
||||||
|
...,
|
||||||
|
"singleRoomDiff": 200,
|
||||||
|
"singleRoomSurcharge": 200,
|
||||||
|
"totalPrice": 4305,
|
||||||
|
"depositAmount": 100,
|
||||||
|
"stock": 99
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `singleRoomDiff` — 价格日历配置的单房差(无论 adultCount 是多少都返回)
|
||||||
|
- `singleRoomSurcharge` — 本次报价**实际加价金额**:`adultCount == 1 ? singleRoomDiff : 0`
|
||||||
|
- `totalPrice` 已经包含 `singleRoomSurcharge`
|
||||||
|
- `depositAmount`(比例定金时)按 `(totalPrice - singleRoomSurcharge) × ratio` 算
|
||||||
|
|
||||||
|
### 7. 小程序订单详情 `GET /mp/order/{id}` 价格明细行为变更
|
||||||
|
|
||||||
|
**`priceBreakdown.items[]` 数组结构**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"label": "成人", "unitPrice": 4105, "quantity": 1, "amount": 4105},
|
||||||
|
{"label": "儿童", "unitPrice": 1865, "quantity": 0, "amount": 0},
|
||||||
|
{"label": "小童", "unitPrice": 1365, "quantity": 0, "amount": 0},
|
||||||
|
{"label": "幼童", "unitPrice": 0, "quantity": 0, "amount": 0},
|
||||||
|
{"label": "单房差", "unitPrice": null, "quantity": null, "amount": 200}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**单房差行的特殊处理**:
|
||||||
|
- `unitPrice` / `quantity` 为 `null`(整笔费用,不按人头)
|
||||||
|
- `amount` 即为本次单房差金额
|
||||||
|
- **仅当 `singleRoomSurcharge > 0` 时才出现此行**;= 0 或老订单缺字段时不展示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、前端改动建议(mmg)
|
||||||
|
|
||||||
|
### 管理端
|
||||||
|
|
||||||
|
#### 价格日历 - "编辑价格区间"弹窗
|
||||||
|
|
||||||
|
参考用户截图布局,在"幼童"输入框右侧加一列必填输入框:
|
||||||
|
|
||||||
|
```
|
||||||
|
[成人 *] [儿童 *] [小童优惠额] [幼童] [单房差 *]
|
||||||
|
```
|
||||||
|
|
||||||
|
- placeholder: `¥ 必填,0=不收`
|
||||||
|
- 提示文案 / hover tip:"成人=1 人下单时叠加到总价"
|
||||||
|
- 校验:必填 + 不能为负数
|
||||||
|
|
||||||
|
#### GROUP 班期编辑
|
||||||
|
|
||||||
|
同上,在班期价格区域加"单房差"必填输入框。
|
||||||
|
|
||||||
|
### 小程序
|
||||||
|
|
||||||
|
#### 订单确认页
|
||||||
|
|
||||||
|
- 总价 / 定金:**直接展示后端返回的 `totalPrice` / `depositAmount`**,前端**不要自己加单房差**(防止双计)
|
||||||
|
- 明细面板:从后端返回的 `priceBreakdown.items[]` 直接渲染,如有 `label === "单房差"` 一行,展示 `amount`,无 `unitPrice` / `quantity` 列
|
||||||
|
|
||||||
|
#### 不需要改造
|
||||||
|
|
||||||
|
- 选择人数控件:不需要改(adultCount = 1 时由后端自动加单房差,前端无感)
|
||||||
|
- 定金 / 尾款显示:无变化
|
||||||
|
|
||||||
|
### 管理端订单详情
|
||||||
|
|
||||||
|
- 不读 `priceBreakdown`,只读 `totalPrice` 汇总字段,无需改造
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、向后兼容
|
||||||
|
|
||||||
|
- **老 calendar / batch 数据**:`single_room_diff` 列 DEFAULT 0,行为=关闭单房差,与原逻辑一致
|
||||||
|
- **老订单**:`priceBreakdown` JSON 缺 `singleRoomSurcharge` 字段时,解析端 `isMissingNode()` 跳过,不报错不展示空行
|
||||||
|
- **老 App 仍传 `singleRoomDiff` 不会**(本来就没此字段)
|
||||||
|
- **新建/编辑必填**:管理员每次保存价格区间或班期都得显式填(@NotNull),不允许默认 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、@mmg 关注点
|
||||||
|
|
||||||
|
1. 管理端价格日历"编辑价格区间"弹窗加单房差必填输入框
|
||||||
|
2. 管理端 GROUP 班期编辑器加同款输入框
|
||||||
|
3. 小程序订单确认页明细面板渲染"单房差"行(条件出现)
|
||||||
|
4. **D9 注意**:管理端"编辑订单"弹窗改成人/儿童/小童/幼童数后,后端**不会**重算单房差。建议在该弹窗加文案提示:"如改人数导致单房差变化,请通过『加附加费』接口手动调整"(可选,本期不强制)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、错误码
|
||||||
|
|
||||||
|
无新增错误码。校验失败统一走 Spring Validation 默认 400 message。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、影响范围(经核确认无影响)
|
||||||
|
|
||||||
|
| 系统 | 是否影响 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 早鸟优惠 | 否 | 落 `order_discount` 表减尾款,与定金无关;`min(totalPrice)` 上限保护 |
|
||||||
|
| 微信支付 | 否 | 公式 `totalPrice - discount + surcharge` 自洽 |
|
||||||
|
| 退款 | 否 | refundAmount 是 admin 手输入,代码层不依赖 totalPrice 拆分 |
|
||||||
|
| 起步价 / 列表"¥xxx 起" | 否 | 不算单房差(展示用) |
|
||||||
|
| 保险费 | 否 | 按人头 × 保单单价 |
|
||||||
|
| 12301 上报 | 否 | 团费按 totalPrice 上报,自洽 |
|
||||||
|
| 合同 PDF | 否 | 团费金额 = totalPrice |
|
||||||
|
| C 端价格日历视图 | 否 | 只展示成人/儿童/小童/幼童 4 价 |
|
||||||
|
| 兜底对账 | 否 | `totalPrice - discount + surcharge` 公式自洽 |
|
||||||
|
| `order_surcharge` 表 / 后期附加费 | 否 | 单房差不进此表(D7) |
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户