酒店预订页(C端)对接独立酒店域 · 实施补丁(交小齐)

后端(bobo)已上线独立「酒店住宿域」,与通用套餐模型 /api/miniapp/orders 完全解耦。
本文件给出 miniprogram-v2/pages/hotel-booking/hotel-booking.ts 改为调用独立端点的可套用代码。
端点详细契约见 docs/hotel-c-end-contract.md(本文件只给落地代码,不重复列字段)。
请求层:apiGet(path, query) / apiPost(path, data),base 已含 /api/miniapp,调用写全路径即可。

0. 前置:merchantId 从哪来

独立酒店域按「商户」组织房型,C端必须带 merchantId。

(也可从全局选中的酒店商户上下文读取,按小齐现有导航结构定。)


1. data 字段调整

data: {
  toast: '', toastShow: false, toastYielding: false,
  t: {} as Record<string, string>,
  acting: false,
  form: { name: '', phone: '', note: '' },        // 联系人/电话(不变)
  merchantId: 0,                                   // 新增:酒店商户 ID
  roomTypes: [] as Array<{                          // 新增:房型列表
    id: number; name: string; bed_type: string;
    max_guests: number; total_rooms: number;
    base_price: number; weekend_price: number; holiday_price: number
  }>,
  selectedRoomId: 0,                                // 新增:选中房型
  roomsCount: 1,                                    // 新增:间数
  checkIn: '',                                      // 入住日(原 stayDate 改名,语义更准)
  checkOut: '',                                     // 离店日(新增)
  availability: null as any,                         // 新增:逐晚余量+总价
  cfg: {} as Record<string, string>
}
删除原 room: '山景木屋'、roomPrice、stayDate 等硬编码字段。

2. onLoad:拉房型列表 + 移除硬编码价

onLoad(options: any) {
  this.setData({ t: pageTexts('hotel-booking') })
  if (options && options.merchantId) this.setData({ merchantId: Number(options.merchantId) })
  try { this.revealCtx = revealInPage(this) } catch (e) {}
  // 拉该商户在售房型(替代原 loadContent 硬编码 A端配置价)
  if (this.data.merchantId) {
    apiGet('/api/miniapp/hotel/room-types', { merchantId: this.data.merchantId })
      .then((d: any) => this.setData({ roomTypes: (d && d.room_types) || [] }))
      .catch(() => this.notify('房型加载失败'))
  } else {
    this.notify('该酒店暂未接入预订')
  }
  const stop = skeletonDelay(this, 600)
  setTimeout(() => { stop() }, 900)
}
移除原 loadContent(['hotel.room.mountain.price', ...]) 调用(金额不再写死,取自接口)。

3. 房型选择 / 间数 / 日期(替代原 onPickRoom / onPickDate)

onPickRoom(e: any) {
  const id = Number((e.currentTarget.dataset as any).id || 0)
  this.setData({ selectedRoomId: id, availability: null })
}
incRooms() { const c = (this.data as any).roomsCount; if (c < 9) this.setData({ roomsCount: c + 1, availability: null }) }
decRooms() { const c = (this.data as any).roomsCount; if (c > 1) this.setData({ roomsCount: c - 1, availability: null }) }
onPickDate(e: any) {
  const ds: any = e.currentTarget.dataset || {}
  if (ds.k === 'in') this.setData({ checkIn: ds.date, availability: null })
  else if (ds.k === 'out') this.setData({ checkOut: ds.date, availability: null })
}

4. 查余量(提交前必做,拿到总价 + 校验可订)

async loadAvailability(): Promise<boolean> {
  const d: any = this.data
  if (!d.selectedRoomId || !d.checkIn || !d.checkOut) { this.notify('请选择房型与日期'); return false }
  try {
    const r: any = await apiGet('/api/miniapp/hotel/availability', {
      roomTypeId: d.selectedRoomId, checkIn: d.checkIn, checkOut: d.checkOut, roomsCount: d.roomsCount,
    })
    const nights = (r && r.nights) || []
    if (nights.some((n: any) => !n.available)) { this.notify('所选日期含满房/维护日,请调整'); return false }
    this.setData({ availability: r })
    return true
  } catch (err: any) {
    this.notify((err && err.message) || '余量查询失败'); return false
  }
}

5. submitAction:改用独立下单端点

submitAction() {
  const c: any = this as any
  if (c._submitting || this.data.acting) return
  c._submitting = true
  const f: any = this.data.form
  if (!isLoggedIn()) { c._submitting = false; this.notify('请先登录后预订'); return }
  if (!f.name || !f.phone) { c._submitting = false; this.notify('请填写联系人与电话'); return }
  this.setData({ confirmOpen: false, acting: true })
  this.loadAvailability().then((ok) => {
    if (!ok) { c._submitting = false; this.setData({ acting: false }); return }
    const d: any = this.data
    apiPost('/api/miniapp/hotel/bookings', {
      roomTypeId: d.selectedRoomId,
      checkIn: d.checkIn,
      checkOut: d.checkOut,
      roomsCount: d.roomsCount,
      guests: d.roomsCount,                 // 住客人数:按间数或另加选择器,按业务定
      guestName: f.name,
      guestPhone: f.phone,
      contactName: f.name,
      contactPhone: f.phone,
      paymentType: 'offline',                // offline=到店付;online=线上支付(走现有支付链路)
    })
      .then((r: any) => {
        c._submitting = false
        this.setData({ acting: false, submitted: true })
        const bid = (r && r.bookingId) || ''
        this.notify('已提交,等待主理人确认')
        setTimeout(() => {
          if (bid) wx.redirectTo({ url: '/pages/order-detail/order-detail?id=' + String(bid) })
          else wx.redirectTo({ url: '/pages/orders/orders' })
        }, 800)
      })
      .catch((err: any) => {
        c._submitting = false
        this.setData({ acting: false })
        this.notify((err && err.message) || '提交失败,请稍后再试')
      })
  })
}
关键变更:apiPost('/api/miniapp/orders', {productId,...}) → apiPost('/api/miniapp/hotel/bookings', {...独立字段})。
订单详情/状态时间线复用现有 orders 域(order_type='room'),无需新页面。

6. wxml 改动要点(hotel-booking.wxml)

  1. 房型区:<radio-group> / 卡片列表改成 wx:for="{{roomTypes}}",data-id="{{item.id}}" bindtap="onPickRoom",显示 item.base_price 等三价。
  2. 日期区:两个 picker mode="date"(入住/离店),data-k="in|out" bindchange="onPickDate"。
  3. 间数:±按钮 bindtap="incRooms"/"decRooms",显示 roomsCount。
  4. 确认抽屉:展示 availability.total_price 与逐晚价,提交按钮 bindtap="submitAction"。
  5. 删除原写死的「山景木屋」「A端配置价」相关节点。

7. 验收(小齐侧)

接口已全部在 prod 验证可用(房型列表/余量/下单/取消)。B端后台(b.kailashi.cn)房型与房价、房态、预订管理、入住台均已上线,可作对照。