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

17 KiB

小程序订单 - 服务详情弹窗接口 + MpOrderDetailVO 字段强类型化

  • 日期: 2026-04-18
  • PR:
    • #818 MpOrderDetailVO 6 字段强类型化
    • #853 补 6 个服务详情透传
    • #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) 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

请求(未变化)

{
  "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/createGET /mp/order/{id}PUT /mp/order/{id}/edit 三个接口返回结构完全一致,均为 MpOrderDetailVO。前端可共用一套类型定义 + 渲染组件。

{
  "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 - 价格明细

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 - 服务包含摘要

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 - 行程进度

interface MpTripProgressVO {
  currentDay: number;        // 当前第几天
  totalDays: number;         // 总天数
  progressPercent: number;   // 进度百分比0-100
  todayDestination: string;  // 今日目的地
}

仅行程中状态有值,其他状态该字段为 null(且因 @JsonInclude(NON_NULL) 过滤,可能不在 JSON 中出现)。

refundProgress - 退款进度

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 - 出行人列表

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 - 待办列表

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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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 入口

左侧菜单新增分组:「C端 - 订单接口 - 服务包含」,6 个弹窗接口在该分组下。

订单详情页渲染建议

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 依次调用:

# 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 反序列化对旧的"对象不定键"消费方式零影响。