# 小程序订单 - 服务详情弹窗接口 + 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` / `List>` 升级为**强类型 VO**。前端 TS 类型提示/自动补全可直接受益。 | 字段 | 旧类型 | 新类型 | |------|--------|--------| | `priceBreakdown` | `object` (Map) | `MpPriceBreakdownVO` | | `serviceItems` | `object[]` (List) | `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 反序列化对旧的"对象不定键"消费方式零影响。