vr-shopxo-plugin/docs/VR_GOODS_CONFIG_SPEC.md

9.4 KiB
Raw Blame History

vr_goods_config JSON 规格说明

版本v3.0 | 日期2026-04-20 | 状态:已确认,待实现 关联提交:待实现


⚠️ 重要v3.0 vs 旧版本的区别

v2.0(旧)rooms/sections/seats 存放在 vr_seat_templates.seat_map JSON 里,前端需要跨表查询。

v3.0(新):完整快照直接嵌入 goods.vr_goods_config,前端完全不跨表。

破坏性变更selected_sections对象格式 {"room_id": ["A","B"]} 改为数组格式 ["A","B"](每个房间一个选中分区列表)。


一、vr_goods_config 完整结构

[
  {
    "version": 1.0,
    "template_id": 4,
    "selected_rooms": ["room_id_1776341371905", "room_id_1776341444657"],
    "selected_sections": ["A", "B"],
    "sessions": [
      { "start": "15:00", "end": "16:59" },
      { "start": "18:00", "end": "21:59" }
    ],
    "venue": {
      "name": "测试 2",
      "address": "测试地址",
      "location": { "lng": "", "lat": "" },
      "images": []
    },
    "rooms": [
      {
        "id": "room_id_1776341371905",
        "name": "1号放映室VV",
        "map": [
          "AAAAB__BBB_BAAAA",
          "AAAAB__BBB_BAAAA",
          "AAAAB__BBB_BAAAA"
        ],
        "sections": [
          { "char": "A", "name": "VIP区",  "price": 100, "color": "#f06292" },
          { "char": "B", "name": "看台区", "price": 50,  "color": "#4fc3f7" }
        ],
        "seats": {
          "A": { "char": "A", "name": "VIP区",  "price": 100, "color": "#f06292" },
          "B": { "char": "B", "name": "看台区", "price": 50,  "color": "#4fc3f7" }
        }
      }
    ]
  }
]

字段说明

字段 类型 必填 说明
version float 协议版本,当前 1.0,用于前向兼容判断
template_id int 来源场馆模板 ID溯源用不用于查询
selected_rooms string[] 本商品启用的房间 ID 列表(决定渲染哪些房间)
selected_sections string[] 本商品选中的分区字符列表(当前房间的分区,["A","B"]
sessions object[] 本商品场次列表
venue object 场馆基本信息快照
rooms object[] 房间完整数据快照(直接复制自 vr_seat_templates.rooms

二、selected_sections 格式说明

格式string[] — 选中分区的字符列表,应用于 selected_rooms 中的当前选中房间

为什么不是对象{"room_id": ["A","B"]}v2.0 旧格式)改为 ["A","B"]

简化理由:用户在后端编辑商品时,一次只能编辑一个房间的分区选择。所以 selected_sections 是"当前房间的选中分区",不需要跨房间存储。

// AdminGoodsSaveHandle 中的转换(旧格式 → 新格式)
// 旧格式(已废弃):
//   $config['selected_sections'] = { "room_id_xxx": ["A","B"] }

// 新格式v3.0
//   前端传入 selected_sections: ["A","B"]
//   直接存储,不做任何转换
$selectedSections = $config['selected_sections'] ?? [];

// BatchGenerate 适配(详见第五章)

三、spec_base_id_map 生成与存储

3.1 当前断路问题

BatchGenerate() → 生成 GoodsSpecBase.id
               → 从未写入 spec_base_id_map  ← 断路

Admin.php Save → 从 form 存旧格式 spec_base_id_map空或无效
GetGoodsViewData → 读 vr_seat_templates.spec_base_id_map旧路径
前端 JS → 用 "A_3" 格式查 → key 不匹配 → 永远查不到

3.2 解决方案:使用 goods_spec_base.extends 字段

ShopXO 原生 goods_spec_base 表有 extends 字段JSON 扩展数据),我们的 BatchGenerate 每次都重建全量 specdelete + insert所以写入 extends 不会被覆盖。

存储时BatchGenerate 写入 GoodsSpecBase

$extends = json_encode([
    'seat_key' => $roomId . '_' . $rowLabel . '_' . $col  // 例:"room_id_xxx_A_3"
], JSON_UNESCAPED_UNICODE);

Db::name('GoodsSpecBase')->insertGetId([
    'goods_id'       => $goodsId,
    'price'          => $seatPrice,
    'inventory'      => 1,
    // ... 其他字段
    'extends'        => $extends,  // ← 新增
]);

读取时GetGoodsViewData 动态构建 spec_base_id_map

$specs = Db::name('GoodsSpecBase')
    ->where('goods_id', $goodsId)
    ->where('inventory', '>', 0)
    ->select();

$specBaseIdMap = [];
foreach ($specs as $spec) {
    $ext = json_decode($spec['extends'] ?? '{}', true);
    if (!empty($ext['seat_key'])) {
        $specBaseIdMap[$ext['seat_key']] = intval($spec['id']);
    }
}

spec_base_id_map 最终格式

{
  "room_id_1776341371905_A_3": 2001,
  "room_id_1776341371905_B_5": 2002,
  "room_id_1776341444657_A_1": 2050
}

3.3 字段不冲突说明

extends 字段是 ShopXO 的标准扩展字段ShopXO 自己的 GoodsSave 更新 spec 时只修改核心字段price/inventory 等),不碰 extends。而我们的 BatchGenerate 在商品保存时全量重建 specextends 由我们写入,不存在被覆盖的风险。


四、前端数据结构GetGoodsViewData 输出)

[
  'vr_seat_template' => [
    'venue'             => $config['venue'],      // 场馆信息
    'rooms'            => $config['rooms'],       // 房间快照
    'sessions'         => $config['sessions'],    // 场次列表
    'selected_rooms'    => $config['selected_rooms'],
    'selected_sections'=> $config['selected_sections'],
    'spec_base_id_map' => $specBaseIdMap,          // 动态构建见3.2
  ],
  'goods_spec_data'   => [...],   // 场次+价格(用于前端场次卡片)
  'goods_config'      => $config   // 原始 vr_goods_config[0]
]

goods_spec_data 生成逻辑

goods_spec_base 按场次维度聚合(每个座位 = 一条 GoodsSpecBase

// 取每个场次的最低价格作为卡片显示价
$specs = Db::name('GoodsSpecBase')
    ->where('goods_id', $goodsId)
    ->where('inventory', '>', 0)
    ->select();

$sessionPrices = [];  // sessionStr => minPrice
$sessionSpecs  = [];  // sessionStr => first spec_base_id

foreach ($specs as $spec) {
    // 从 goods_spec_value 找到场次维度值
    $sessionValue = Db::name('GoodsSpecValue')
        ->where('goods_spec_base_id', $spec['id'])
        ->where('value', 'like', '%-%:%')
        ->find();

    if ($sessionValue) {
        $sessionStr = $sessionValue['value'];
        if (!isset($sessionPrices[$sessionStr]) || $spec['price'] < $sessionPrices[$sessionStr]) {
            $sessionPrices[$sessionStr] = floatval($spec['price']);
        }
        if (!isset($sessionSpecs[$sessionStr])) {
            $sessionSpecs[$sessionStr] = intval($spec['id']);
        }
    }
}

// 构建 goods_spec_data
$goodsSpecData = [];
foreach ($sessions as $s) {
    $start = $s['start'] ?? '';
    $end   = $s['end']   ?? '';
    $sessionStr = $start && $end ? "{$start}-{$end}" : ($start ?: $end);
    $goodsSpecData[] = [
        'spec_id'   => $sessionSpecs[$sessionStr] ?? 0,
        'spec_name' => $sessionStr,
        'price'     => $sessionPrices[$sessionStr] ?? floatval($goods['price'] ?? 0),
    ];
}

前端 JS 使用方式

// ticket_detail.html JS
var specBaseIdMap = <?php echo json_encode($vr_seat_template['spec_base_id_map'] ?? []); ?>;

// submit() 时:根据选中座位查 spec_base_id
var seatKey = seat.roomId + '_' + seat.rowLabel + '_' + seat.colNum;
// 例:"room_id_1776341371905_A_3"
var specBaseId = specBaseIdMap[seatKey] || 0;

// goods_params 格式(每座一行)
{
  goods_id: goodsId,
  spec_base_id: specBaseId,   // ← 用 seatKey 查到
  stock: 1,
  extension_data: JSON.stringify({ attendee, seat: {...} })
}

五、需要修改的文件

文件 改动
SeatSkuService::BatchGenerate() 写入 extends JSONseat_key
SeatSkuService::GetGoodsViewData() 动态从 extends 构建 spec_base_id_map;适配 selected_sections 数组格式
ticket_detail.html JS seatKey 格式改为 roomId + '_' + rowLabel + '_' + colNum
AdminGoodsSaveHandle selected_sections 透传(已经是数组格式,无需改动)

六、数据库迁移(如需要)

如后续需要在 goods_spec_base 加专用列(非必须,extends 已够用):

ALTER TABLE `{{prefix}}goods_spec_base` ADD COLUMN `seat_key` VARCHAR(100) DEFAULT NULL COMMENT '座位键roomId_row_col';

七、版本判断(降级兼容)

$config = json_decode($goods['vr_goods_config'] ?? '', true);
if (empty($config)) {
    return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}

$config = $config[0];

if (empty($config['rooms'])) {
    // v1/v2 旧格式:降级读 vr_seat_templates 表
    return self::GetGoodsViewDataLegacy($goodsId, $config);
}

// v3.0 新格式
$version = $config['version'] ?? 1.0;
// ...

八、已确认的设计决策

决策 结论
selected_sections 格式 数组 ["A","B"],不是对象
spec_base_id_map 存储 不入库GetGoodsViewData 动态构建
seat_key 存储位置 goods_spec_base.extends->seat_keyJSON
seat_key 格式 {roomId}_{rowLabel}_{colNum}(无 MD5
goods_spec_data.price 取该场次所有座位中的最低价(用于卡片显示)
降级兼容 version 字段判断v1/v2 走旧 vr_seat_templates 表路径