hl-api-changelog/changelogs/2026-04/2026-04-18_mp-order-service-detail-and-vo-upgrade.md

575 行
17 KiB
Markdown

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

# 小程序订单 - 服务详情弹窗接口 + MpOrderDetailVO 字段强类型化
- **日期**: 2026-04-18
- **PR**:
- [#818](https://git.1814.love:8443/wx/HL/pulls/818) MpOrderDetailVO 6 字段强类型化
- [#853](https://git.1814.love:8443/wx/HL/pulls/853) 补 6 个服务详情透传
- [#856](https://git.1814.love:8443/wx/HL/pulls/856) 独立 Controller + Swagger 分组
- **类型**: FEATURE + REFACTOR
- **状态**: 已合并到 dev + 测试环境部署中
- **服务**: hl-mp-service + hl-order-service-v2
---
## 一、本次变更汇总
### 1. 订单详情 VO 字段强类型化(🔵 前端体验提升,无 Breaking
`MpOrderDetailVO`(下单 / 详情 / 编辑 三接口共用返回结构)中的 6 个字段,**字段名完全不变**,仅 JSON Schema 由 `Map<String, Object>` / `List<Map<String, Object>>` 升级为**强类型 VO**。前端 TS 类型提示/自动补全可直接受益。
| 字段 | 旧类型 | 新类型 |
|------|--------|--------|
| `priceBreakdown` | `object` (Map) | `MpPriceBreakdownVO` |
| `serviceItems` | `object[]` (List<Map>) | `MpServiceItemVO[]` |
| `tripProgress` | `object` (Map) | `MpTripProgressVO` |
| `refundProgress` | `object` (Map) | `MpRefundProgressVO` |
| `travelers` | `object[]` | `MpTravelerVO[]` |
| `todos` | `object[]` | `MpOrderTodoVO[]` |
### 2. 新增 6 个服务详情弹窗接口(🟢 新功能)
订单详情页 `serviceItems` 列表里每个条目点击后的弹窗数据源,此前前端调不到,现已对外开放。
独立 **Swagger 分组**`C端 - 订单接口 - 服务包含`knife4j 左侧菜单)。
---
## 二、变更接口清单
| # | 方法 | 路径 | 说明 | 鉴权 | 缓存 |
|---|------|------|------|------|------|
| 1 | POST | `/mp/order/create` | 创建订单(返回结构升级) | REQUIRED | 否 |
| 2 | GET | `/mp/order/{orderId}` | 订单详情(返回结构升级) | REQUIRED | 否 |
| 3 | PUT | `/mp/order/{orderId}/edit` | 用户编辑订单(返回结构升级) | REQUIRED | 否 |
| 4 | GET | `/mp/order/{orderId}/tickets` | 【新增】门票清单 | REQUIRED | 否 |
| 5 | GET | `/mp/order/{orderId}/hotels` | 【新增】住宿清单 | REQUIRED | 否 |
| 6 | GET | `/mp/order/{orderId}/meals` | 【新增】餐饮清单 | REQUIRED | 否 |
| 7 | GET | `/mp/order/{orderId}/vehicle` | 【新增】用车详情 | REQUIRED | 否 |
| 8 | GET | `/mp/order/{orderId}/guide` | 【新增】领队详情 | REQUIRED | 否 |
| 9 | GET | `/mp/order/{orderId}/photographer` | 【新增】摄影师详情 | REQUIRED | 否 |
---
## 三、订单创建接口(返回结构升级)
```
POST /mp/order/create
```
### 请求(未变化)
```json
{
"productId": "1760000000000001",
"departureDate": "2026-05-01",
"groupBatchId": "1760000000000002",
"tierSeq": 1,
"adultCount": 2,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"childNeedBed": false,
"roomCount": 1,
"contactName": "张三",
"contactPhone": "13800138000",
"remark": "希望安排靠窗",
"sharerOpenid": "wx_xxxxx"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `productId` | String | 是 | 产品ID |
| `departureDate` | String(yyyy-MM-dd) | 否 | 出发日期;GROUP 产品可不传(从团期取) |
| `groupBatchId` | String | 条件必填 | GROUP 产品必填 |
| `tierSeq` | Integer | 是 | 档位序号,≥1 |
| `adultCount` | Integer | 是 | 成人数,默认 1,≥1 |
| `childCount` | Integer | 否 | 儿童数,≥0 |
| `youngChildCount` | Integer | 否 | 小童数,≥0 |
| `babyCount` | Integer | 否 | 幼童数,≥0 |
| `childNeedBed` | Boolean | 否 | 儿童是否需要床位 |
| `roomCount` | Integer | 否 | 房间数GROUP 默认 1 |
| `contactName` | String | 是 | 联系人姓名,≤50 字 |
| `contactPhone` | String | 是 | 联系人电话,≤20 字 |
| `remark` | String | 否 | 备注,≤500 字 |
| `sharerOpenid` | String | 否 | 分享人 openid,≤64 字 |
### 返回(结构升级)
> **⚠️ 关键**`POST /mp/order/create`、`GET /mp/order/{id}`、`PUT /mp/order/{id}/edit` 三个接口返回结构**完全一致**,均为 `MpOrderDetailVO`。前端可共用一套类型定义 + 渲染组件。
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "2045450312813043713",
"orderNo": "HL20260418183211-6560",
"groupCode": "6560",
"productId": "2045424500610125825",
"productName": "E2E-核心版本-V1",
"productSubtitle": "去草原看日出",
"productCoverUrl": "https://cdn.example.com/cover.jpg",
"productType": "CORE",
"productTypeLabel": "核心产品",
"departureDate": "2026-05-10",
"returnDate": "2026-05-12",
"tripDays": 3,
"tripNights": 2,
"adultCount": 1,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"createTime": "2026-04-18 18:32:12",
"status": "PENDING_PAY",
"statusLabel": "待支付",
"displayStatus": "PENDING_PAY",
"displayStatusLabel": "待支付",
"expiryTime": "2026-04-18 20:32:12",
"totalPrice": 2401280.00,
"paidAmount": 0.00,
"depositAmount": 500.00,
"balanceAmount": 2400780.00,
"paymentType": "DEPOSIT",
"paymentTypeLabel": "订金+尾款",
"payMethodLabel": "微信支付",
"balancePayMethodLabel": "落地后由领队收取",
"priceBreakdown": {
"items": [
{ "label": "成人", "unitPrice": 2300.00, "quantity": 1, "amount": 2300.00 }
],
"subtotal": 2300.00,
"discounts": [],
"totalDiscount": 0.00,
"totalPayable": 2300.00
},
"serviceItems": [
{ "type": "HOTEL", "title": "住宿", "summary": "2 晚精选当地酒店", "clickable": true },
{ "type": "MEAL", "title": "餐饮", "summary": "含早餐 2 次 + 特色正餐 1 次", "clickable": true },
{ "type": "VEHICLE", "title": "用车", "summary": "小蒙马车 1 辆", "clickable": true }
],
"notIncluded": "往返呼伦贝尔大交通、个人消费",
"tripProgress": null,
"refundProgress": null,
"travelers": [],
"todos": [],
"contractStatus": "NONE",
"contractStatusLabel": "无合同",
"insuranceStatus": "NONE",
"insuranceStatusLabel": "无保险",
"invoiceStatus": "AFTER_TRIP",
"invoiceStatusLabel": "出行后开",
"reviewed": false,
"customizerId": "1001",
"customizerName": "admin",
"customizerAvatarUrl": null,
"customizerQrUrl": null,
"refundPolicy": {
"policyId": 1,
"policyName": "标准退款",
"rules": []
}
},
"success": true
}
```
### `MpOrderDetailVO` 重点字段类型说明
#### priceBreakdown - 价格明细
```typescript
interface MpPriceBreakdownVO {
items: MpPriceLineItemVO[]; // 按人群类型的单价明细
subtotal: number; // 小计(优惠前)
discounts: MpDiscountLineItemVO[];// 优惠明细列表
totalDiscount: number; // 已优惠总额(负数)
totalPayable: number; // 应付总额
}
interface MpPriceLineItemVO {
label: string; // "成人" / "儿童" 等
unitPrice: number;
quantity: number;
amount: number;
}
interface MpDiscountLineItemVO {
tag: string; // "早鸟"
description: string; // "提前30天"
amount: number; // 负数
}
```
#### serviceItems - 服务包含摘要
```typescript
interface MpServiceItemVO {
type: 'HOTEL' | 'MEAL' | 'VEHICLE' | 'GUIDE' | 'PHOTOGRAPHER' | 'TICKET' | 'INSURANCE';
title: string; // "住宿"
summary: string; // "2 晚精选当地酒店"
clickable: boolean; // 是否可点击查看详情弹窗(对应下面 6 个弹窗接口)
}
```
> 当 `clickable === true` 时,点击该条目调用对应的 `/mp/order/{orderId}/{tickets|hotels|meals|vehicle|guide|photographer}` 接口拉取弹窗详情。映射关系:
> - HOTEL → `/hotels`
> - MEAL → `/meals`
> - VEHICLE → `/vehicle`
> - GUIDE → `/guide`
> - PHOTOGRAPHER → `/photographer`
> - TICKET → `/tickets`
#### tripProgress - 行程进度
```typescript
interface MpTripProgressVO {
currentDay: number; // 当前第几天
totalDays: number; // 总天数
progressPercent: number; // 进度百分比0-100
todayDestination: string; // 今日目的地
}
```
> **仅行程中状态有值**,其他状态该字段为 `null`(且因 `@JsonInclude(NON_NULL)` 过滤,可能不在 JSON 中出现)。
#### refundProgress - 退款进度
```typescript
interface MpRefundProgressVO {
steps: MpRefundStepVO[]; // 4 步时间线
currentStep: number; // 当前活跃步骤索引0-based
refundAmount: number;
refundMethod: string; // 例 "原路返回·微信"
refundArrivalTime: string | null; // 已到账才有值
}
interface MpRefundStepVO {
title: string; // "提交申请"
description: string;
status: 'COMPLETED' | 'ACTIVE' | 'PENDING';
time: string | null; // PENDING 时为 null
}
```
#### travelers - 出行人列表
```typescript
interface MpTravelerVO {
travelerId: number;
name: string;
travelerType: string; // ADULT/CHILD/YOUNG_CHILD/BABY
travelerTypeLabel: string; // "成人" 等中文
idCardType: string; // ID_CARD/PASSPORT
idCardTypeLabel: string; // "身份证" 等中文
idCardNo: string;
gender: string;
birthday: string;
phone: string;
nationality: string;
emergencyContact: string;
emergencyPhone: string;
email: string;
}
```
#### todos - 待办列表
```typescript
interface MpOrderTodoVO {
todoId: number;
todoType: string;
todoTypeLabel: string;
todoLabel: string;
sequence: number;
status: string;
assigneeRoleKey: string;
assigneeRoleLabel: string;
completedAt: string | null;
createTime: string;
blocked: boolean;
blockedReason: string | null;
}
```
---
## 四、服务详情弹窗接口6 个新增)
所有接口签名一致:
- **方法**: GET
- **鉴权**: `Authorization: Bearer {mp-token}`(用户登录态)
- **Path Var**: `orderId` (Long)
- **Query**: 无
- **失败降级**: 订单服务不可用时返回 `{"code":500, "message":"订单服务不可用,请稍后重试"}`
### 1. 门票清单
```
GET /mp/order/{orderId}/tickets
```
```json
{
"code": 200,
"data": {
"items": [
{
"seq": 1,
"scenicName": "莫日格勒河",
"coverUrl": "https://cdn.example.com/scenic/1.jpg",
"dayNumber": 2,
"originalPrice": 80.00,
"included": true
}
],
"scenicCount": 14,
"totalValue": 340.00
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `items[]` | MpTicketItemVO[] | 门票列表 |
| `items[].seq` | Integer | 序号 |
| `items[].scenicName` | String | 景点名称 |
| `items[].coverUrl` | String | 景点封面图 |
| `items[].dayNumber` | Integer | 第几天 |
| `items[].originalPrice` | BigDecimal | 门票原价/人 |
| `items[].included` | Boolean | 是否已包含在套餐内 |
| `scenicCount` | Integer | 景点数量 |
| `totalValue` | BigDecimal | 门票总价值/人(快照有 price 才有值) |
### 2. 住宿清单
```
GET /mp/order/{orderId}/hotels
```
```json
{
"code": 200,
"data": {
"items": [
{
"hotelId": "2023714929877450753",
"hotelName": "呼伦贝尔香格里拉大酒店",
"coverUrl": "https://cdn.example.com/hotel/1.jpg",
"assignmentDate": "2026-05-10",
"roomType": "大床房"
}
],
"nightCount": 2,
"roomTypeStats": [
{ "roomType": "大床房", "count": 1 }
]
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `items[]` | MpHotelItemVO[] | 每晚酒店列表(按入住日期升序) |
| `items[].hotelId` | Long | 酒店ID |
| `items[].hotelName` | String | 酒店名称 |
| `items[].coverUrl` | String | 酒店封面(产品服务扩展后可用) |
| `items[].assignmentDate` | String(yyyy-MM-dd) | 入住日期 |
| `items[].roomType` | String | 房型 |
| `nightCount` | Integer | 总晚数 |
| `roomTypeStats[]` | MpRoomTypeStatVO[] | 按房型统计 |
| `roomTypeStats[].roomType` | String | 房型 |
| `roomTypeStats[].count` | Integer | 房间数 |
### 3. 餐饮清单
```
GET /mp/order/{orderId}/meals
```
```json
{
"code": 200,
"data": {
"specialMeals": [
{
"dayNumber": 2,
"mealName": "蒙古包午餐·手把肉宴",
"mealType": "午餐",
"description": "含奶茶/手把肉/烤包子"
}
],
"stats": {
"breakfastCount": 2,
"specialDinnerCount": 1,
"regularDinnerCount": 3
}
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `specialMeals[]` | MpMealItemVO[] | 特色正餐列表(含描述的餐) |
| `specialMeals[].dayNumber` | Integer | 第几天 |
| `specialMeals[].mealName` | String | 餐名 |
| `specialMeals[].mealType` | String | 餐型:早餐/午餐/晚餐 |
| `specialMeals[].description` | String | 菜品描述 |
| `stats.breakfastCount` | Integer | 早餐数量 |
| `stats.specialDinnerCount` | Integer | 特色正餐数量(有描述的) |
| `stats.regularDinnerCount` | Integer | 日常正餐数量 |
### 4. 用车详情
```
GET /mp/order/{orderId}/vehicle
```
```json
{
"code": 200,
"data": {
"vehicleType": "小蒙马车",
"vehicleCount": 1,
"coverUrl": "https://cdn.example.com/vehicle/xiaomma.jpg",
"plateNumber": "蒙E12345",
"driverName": "张师傅",
"driverPhone": "138****5678",
"matched": true
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `vehicleType` | String | 车型名称 |
| `vehicleCount` | Integer | 车辆数 |
| `coverUrl` | String | 车型封面(产品服务扩展后可用) |
| `plateNumber` | String | 车牌号(**仅出行前后可见**,其他时间为空) |
| `driverName` | String | 司机姓名 |
| `driverPhone` | String | 司机电话(脱敏) |
| `matched` | Boolean | 是否已匹配车辆 |
### 5. 领队详情
```
GET /mp/order/{orderId}/guide
```
```json
{
"code": 200,
"data": {
"staffId": "5001",
"name": "巴特尔",
"avatarUrl": "https://cdn.example.com/avatar/5001.jpg",
"role": "LEADER",
"roleLabel": "领队",
"phone": "138****5678",
"remark": "10 年草原线路经验,会蒙语",
"assigned": true
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `staffId` | Long | 员工ID |
| `name` | String | 姓名 |
| `avatarUrl` | String | 头像(员工档案扩展后可用) |
| `role` | String | 角色代码 |
| `roleLabel` | String | 角色中文 |
| `phone` | String | 电话(脱敏) |
| `remark` | String | 备注/简介 |
| `assigned` | Boolean | 是否已分配 |
### 6. 摄影师详情
```
GET /mp/order/{orderId}/photographer
```
响应字段与领队详情完全一致,`role``"PHOTOGRAPHER"``roleLabel``"摄影师"`
---
## 五、前端接入建议
### Swagger 入口
- **mp-service 独立**: http://localhost:8085/doc.html (本地开发)
- **网关聚合**: http://localhost:8080/doc.html → 切到 `hl-mp-service`
- **测试环境**: https://api.test.1814.love/doc.html → 切到 `hl-mp-service`
左侧菜单新增分组:**「C端 - 订单接口 - 服务包含」**,6 个弹窗接口在该分组下。
### 订单详情页渲染建议
```javascript
async function openOrderDetail(orderId) {
const detail = await request.get(`/mp/order/${orderId}`);
// 1. 渲染基础信息orderNo/产品/金额/状态/时间轴)
renderHeader(detail.data);
// 2. 价格明细 - 用 priceBreakdown.items / discounts 渲染
renderPriceBreakdown(detail.data.priceBreakdown);
// 3. 服务包含列表
detail.data.serviceItems.forEach(item => {
renderServiceCard(item, {
onClick: item.clickable ? () => openServiceDetail(orderId, item.type) : null
});
});
}
// 类型到接口路径的映射
const SERVICE_DETAIL_API = {
HOTEL: 'hotels', MEAL: 'meals', VEHICLE: 'vehicle',
GUIDE: 'guide', PHOTOGRAPHER: 'photographer', TICKET: 'tickets'
};
async function openServiceDetail(orderId, type) {
const path = SERVICE_DETAIL_API[type];
if (!path) return; // 非 clickable 类型(如 INSURANCE
const data = await request.get(`/mp/order/${orderId}/${path}`);
showServiceDialog(type, data.data);
}
```
---
## 六、回归验证
测试环境部署完成后,用 mp token 依次调用:
```bash
# 1. 拿个已绑定的订单详情,确认 6 个强类型字段结构
curl "https://api.test.1814.love/mp/order/{orderId}" \
-H "Authorization: Bearer {mp-token}"
# 2. 6 个弹窗接口(对应 serviceItems 里 clickable=true 的条目)
for path in tickets hotels meals vehicle guide photographer; do
curl "https://api.test.1814.love/mp/order/{orderId}/$path" \
-H "Authorization: Bearer {mp-token}"
done
```
**预期**
- 订单详情返回的 `priceBreakdown` 是对象而非无结构 Map,`serviceItems` 是结构化数组
- 6 个弹窗接口均返回 `code=200` + 对应 VO 结构,不再出现 403 或路径不存在
---
## 七、不兼容变更
**无**。字段名与语义保持不变,仅 JSON Schema 类型由 Map 升级为结构化对象;Jackson 反序列化对旧的"对象不定键"消费方式零影响。