vr-shopxo-plugin/docs/【plan】260514-seatmap-api-po...

8.5 KiB
Raw Permalink Blame History

【plan】260514 — seatmap API + 轮询库存实时同步

标签seatmap-api | realtime | inventory | polling | uniapp 日期2026-05-14 状态:待执行


一、背景与目标

背景

  • UniApp 票务页面需要实时显示座位是否已售(灰色)
  • vr_goods_config 有座位图结构(rooms[].map),但没有座位级库存
  • 库存数据在 vrt_goods_spec_base.inventoryShopXO 标准 goods/detail API 不返回
  • ShopXO 无 SSE/WebSocket 基础设施SSE 需改 PHP 基础设施层(成本高)

目标

  1. 新增 seatmap API返回 seatSpecMap(含每个座位的 inventory
  2. UniApp 每 10 秒轮询,实时灰化已售座位
  3. 订单支付成功后 cache 失效,下一次 poll 自动拿到新库存
  4. 完全隔离:不动任何现有 ShopXO 代码,所有改动在插件内部

二、架构设计

2.1 API 接口

GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=seatmap&goods_id=118

响应:

{
  "code": 0,
  "data": {
    "seatSpecMap": {
      "69e5b802-c71e-4cc2-437f-2f1ef5f6afad_A_1": {
        "inventory": 1,
        "price": 999,
        "spec": [
          { "type": "$vr-场次",   "value": "08:00-23:59" },
          { "type": "$vr-场馆",   "value": "一个测试场馆信息" },
          { "type": "$vr-演播室", "value": "主要展厅" },
          { "type": "$vr-分区",   "value": "一个测试场馆信息-主要展厅-A" },
          { "type": "$vr-座位号", "value": "一个测试场馆信息-主要展厅-A-A1" }
        ],
        "section": { "char": "A", "name": "VIP区", "color": "#ff4d4f" },
        "roomId": "69e5b802-c71e-4cc2-437f-2f1ef5f6afad",
        "roomName": "主要展厅",
        "rowLabel": "A",
        "colNum": 1
      },
      "69e5b802-c71e-4cc2-437f-2f1ef5f6afad_A_2": {
        "inventory": 0,   // 已售!
        "price": 999,
        "spec": [...],
        "section": { "char": "A", "name": "VIP区", "color": "#ff4d4f" },
        ...
      }
    },
    "goods_spec_data": [
      { "spec_name": "08:00-23:59", "price": 299, "min_price": 299 }
    ]
  }
}

seatSpecMap key 格式{roomId}_{rowLabel}_{colNum} (与 H5 ticket_detail.htmlseatKey 完全一致,保证 UI 一致性)

2.2 数据流

订单支付成功
    ↓
TicketService::onOrderPaid() 末尾
    → Cache::delete('vr_seatmap_' . goodsId)  ← 1行改动
    ↓
UniApp 端轮询每10秒
    → GET seatmap API
    → 用 seatSpecMap 灰化已售座位inventory ≤ 0
    → renderSeatMap() 重新渲染

2.3 缓存策略

数据 缓存策略 TTL
template_snapshot(座位图结构) ShopXO Cachefile 或 Redis 60s
seatSpecMapinventory 不缓存(实时读 DB
goods_spec_data(场次+最低价) 从 seatSpecMap 实时合并

理由

  • 模板快照不变,缓存减少 DB 查询
  • inventory 必须实时,否则可能出现超售后 UI 仍显示可售

2.4 Realtime 推送方案

最终选择:轮询(轻量,无额外基础设施)

方案 结论
SSE长连接 ShopXO 无 Swoole/WorkermanPHP-FPM 不支持长连
Redis pub/sub + daemon 需并行跑 daemon 进程,改基础设施
轮询(推荐) GET /seatmap 无 CDN 延迟,每 10 秒刷新
ShopXO Realtime addon ⚠️ 未装 addon不依赖

轮询 vs 即时推送的差距 < 10 秒,用户感知弱。支付时 FOR UPDATE SKIP LOCKED 兜底拒超售。


三、新增文件清单

3.1 service/SeatMapService.php(新建)

namespace app\plugins\vr_ticket\service;

class SeatMapService
{
    /**
     * 获取座位图完整数据(含实时库存)
     *
     * @param int $goodsId
     * @return array [
     *   'seatSpecMap' => [...],
     *   'goods_spec_data' => [...],
     * ]
     */
    public static function GetSeatMap(int $goodsId): array

    /**
     * 清除座位图缓存
     */
    public static function ClearCache(int $goodsId): void
}

实现要点

  • 复用 SeatSkuService::buildSeatSpecMap($goodsId, $seatTemplate) 的核心逻辑
  • 模板快照走 Cache::get('vr_seatmap_' . $goodsId)miss 时从 DB 读并写入
  • seatSpecMap 不缓存,直接调 buildSeatSpecMap()

3.2 api/Goods.php(已有空壳,加 action

// 新增 action
public function seatmap()
{
    $goodsId = input('goods_id', 0, 'intval');
    if ($goodsId <= 0) {
        return self::error('参数错误');
    }
    $data = SeatMapService::GetSeatMap($goodsId);
    return self::success($data);
}

3.3 Hook 改动1行TicketService.php

// TicketService::onOrderPaid() 末尾
// 清除缓存,触发下一次 poll 拿到新库存
SeatMapService::ClearCache($goodsId);

四、UniApp 接入vr-shopxo-uniapp

4.1 轮询时机

购票弹窗打开openTicketPopup
    ↓ 立即调一次 seatmap API拿到初始状态
    ↓ 每 10 秒 setInterval
        → 调 seatmap API
        → 比对本地 seatSpecMap找出 inventory=0 的座位
        → renderSeatMap() 灰化已售
购票弹窗关闭closePopup
    ↓ clearInterval停止轮询

4.2 UniApp 端改动goods-vr-ticket.vue

data() {
  return {
    seatSpecMap: {},        // ← 新增
    seatmapTimer: null,     // ← 新增
  }
},

methods: {
  openTicketPopup() {
    // 现有逻辑...
    this.loadSeatMap();          // ← 新增:立即加载
    this.seatmapTimer = setInterval(() => {
      this.loadSeatMap();
    }, 10000);                   // ← 新增10秒轮询
  },

  closePopup() {
    if (this.seatmapTimer) {
      clearInterval(this.seatmapTimer);
      this.seatmapTimer = null;
    }
    // 现有逻辑...
  },

  loadSeatMap() {
    uni.request({
      url: 'http://shopxo.test/api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=goods&pluginsaction=seatmap&goods_id=' + this.goodsId,
      method: 'GET',
      success: (res) => {
        if (res.data.code === 0) {
          this.seatSpecMap = res.data.data.seatSpecMap;
          this.renderSeatMap();   // 用 seatSpecMap 灰化已售座位
        }
      }
    });
  },

  renderSeatMap() {
    // 参考 H5 ticket_detail.html 的 renderSeatMap 逻辑
    // 遍历 mapData对每字符
    //   - char === '_' → 空白
    //   - seatSpecMap[seatKey].inventory > 0 → 可选(带颜色)
    //   - seatSpecMap[seatKey].inventory <= 0 → 已售(灰色)
  }
}

4.3 API URL 配置

ShopXO 部署到不同环境时,shopxo.test 需要换成实际域名。 建议在 main.js 或全局配置里统一管理:

// main.js
Vue.prototype.$shopxoBaseUrl = 'http://shopxo.test';

五、执行步骤

Step 1SeatMapService.php

  1. 创建 service/SeatMapService.php
  2. 复用 SeatSkuService::buildSeatSpecMap() 逻辑
  3. 模板快照走 CacheTTL 60s
  4. seatSpecMap 实时读 DB
  5. ClearCache($goodsId) 实现

验证curl 访问 API返回正确的 seatSpecMap JSON

Step 2api/Goods.php 加 action

  1. api/Goods.php(已有空壳)加 seatmap() action
  2. 调用 SeatMapService::GetSeatMap()
  3. 返回格式 { code: 0, data: {...} }

验证:浏览器直接访问 seatmap URL

Step 3TicketService Hook 加 1 行

  1. TicketService::onOrderPaid() 末尾加一行:
    SeatMapService::ClearCache($goodsId);
    

验证:购买一张票后,确认下一次 poll 拿到新库存

Step 4UniApp 接入

  1. goods-vr-ticket.vue 实现 loadSeatMap()renderSeatMap()
  2. openTicketPopup 时启动轮询,弹窗关闭时停止
  3. 本地开发测试localhost联调时配置实际 API URL

验证

  1. seatmap API 响应正确
  2. 轮询后已售座位变灰
  3. 购买后 poll 自动刷新

六、风险与备选

风险 概率 应对
轮询间隔内用户选到刚被买的座位 FOR UPDATE SKIP LOCKED 支付时兜底拒单
多 PHP-FPM 实例 file cache 不一致 低(单服) 未来迁移 Redis
API 响应慢影响轮询体验 设置 5 秒 timeout超时跳过

七、相关文档

  • docs/VR_GOODS_CONFIG_SPEC.md — vr_goods_config v3.0 协议
  • docs/PLAN_5DIM_REFACTOR.md — 5维 SPEC_DIMS 说明
  • docs/09_SHOPXO_HOOKS_REFERENCE.md — ShopXO Hook 钩子参考
  • service/SeatSkuService.php — buildSeatSpecMap 核心逻辑