vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/service/SeatMapService.php

334 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace app\plugins\vr_ticket\service;
use think\facade\Db;
/**
* VR票务 - 座位图服务UniApp seatmap API 专用)
*
* 提供 seatSpecMap含实时库存+ goods_spec_data
* 模板快照走 ShopXO CacheTTL 60s库存实时读 DB
*
* @package vr_ticket\service
*/
class SeatMapService
{
/**
* 缓存 key 前缀
*/
const CACHE_KEY_PREFIX = 'vr_seatmap_';
/**
* 模板快照缓存 TTL
*/
const CACHE_TTL = 60;
/**
* 获取座位图完整数据(含实时库存)
*
* @param int $goodsId
* @return array [
* 'seatSpecMap' => [...], // seatKey → {inventory, price, spec, ...}
* 'goods_spec_data' => [...], // 场次列表(含最低价)
* ]
*/
public static function GetSeatMap(int $goodsId): array
{
// 1. 读取 vr_goods_config数组取第一项
$vrConfigRaw = Db::name('Goods')
->where('id', $goodsId)
->value('vr_goods_config');
$configs = json_decode($vrConfigRaw ?? '', true);
if (empty($configs) || !is_array($configs)) {
return ['seatSpecMap' => [], 'goods_spec_data' => []];
}
$config = $configs[0];
$templateId = intval($config['template_id'] ?? 0);
if ($templateId <= 0) {
return ['seatSpecMap' => [], 'goods_spec_data' => []];
}
// 2. 获取座位模板(含 rooms[] / sections[] / map[]
$seatTemplate = self::getSeatTemplate($templateId);
if (empty($seatTemplate)) {
return ['seatSpecMap' => [], 'goods_spec_data' => []];
}
// 3. 构建 seatSpecMap含 inventory实时读 DB包含已售
$seatSpecMap = self::buildSeatSpecMap($goodsId, $seatTemplate);
// 4. 构建场次列表(从 sessions[] + seatSpecMap 合并最低价)
$goodsSpecData = self::buildGoodsSpecData($config['sessions'] ?? [], $seatSpecMap);
return [
'seatSpecMap' => $seatSpecMap,
'goods_spec_data' => $goodsSpecData,
];
}
/**
* 清除座位图缓存(订单支付成功后调用)
*
* @param int $goodsId
* @return bool
*/
public static function ClearCache(int $goodsId): bool
{
return \think\facade\Cache::delete(self::CACHE_KEY_PREFIX . $goodsId);
}
// ─────────────────────────────────────────────────────────
// 私有方法
// ─────────────────────────────────────────────────────────
/**
* 获取座位模板(走 ShopXO CacheTTL 60s
*
* @param int $templateId
* @return array|null
*/
private static function getSeatTemplate(int $templateId)
{
$cacheKey = self::CACHE_KEY_PREFIX . $templateId;
$cached = \think\facade\Cache::get($cacheKey);
if ($cached !== null) {
return $cached;
}
$row = Db::name('vr_seat_templates')->find($templateId);
if (empty($row)) {
return null;
}
$seatMap = json_decode($row['seat_map'] ?? '{}', true);
if (empty($seatMap)) {
return null;
}
// 缓存TTL = self::CACHE_TTL
\think\facade\Cache::set($cacheKey, $seatMap, self::CACHE_TTL);
return $seatMap;
}
/**
* 构建座位规格映射表(含 inventory实时读 DB
*
* 遍历所有 GoodsSpecBase含 inventory=0 的已售座位),
* 与 GoodsSpecType + GoodsSpecValue 关联,
* 输出 seatSpecMap。
*
* @param int $goodsId
* @param array $seatTemplate seat_map JSON已解析
* @return array seatSpecMap
*/
private static function buildSeatSpecMap(int $goodsId, array $seatTemplate): array
{
$seatSpecMap = [];
// 1. 查询当前商品所有 GoodsSpecBase不过滤 inventory获取所有座位含已售
$specs = Db::name('GoodsSpecBase')
->where('goods_id', $goodsId)
->select()
->toArray();
if (empty($specs)) {
return $seatSpecMap;
}
// 2. 查询 GoodsSpecType 获取维度映射name → index
$specTypes = Db::name('GoodsSpecType')
->where('goods_id', $goodsId)
->order('id', 'asc')
->select()
->toArray();
$dimIndexByName = [];
$dimValuesByName = []; // name → [value1, value2, ...]
foreach ($specTypes as $idx => $type) {
$dimName = $type['name'] ?? '';
if (!empty($dimName)) {
$dimIndexByName[$dimName] = $idx;
$values = json_decode($type['value'] ?? '[]', true);
$dimValuesByName[$dimName] = [];
foreach ($values as $v) {
if (isset($v['name'])) {
$dimValuesByName[$dimName][] = $v['name'];
}
}
}
}
// 3. 查询每个 spec_base_id 对应的 GoodsSpecValue
$specBaseIds = array_column($specs, 'id');
$specValues = Db::name('GoodsSpecValue')
->whereIn('goods_spec_base_id', $specBaseIds)
->select()
->toArray();
// 4. 按 spec_base_id 分组,通过值匹配找到维度名
$specByBaseId = [];
foreach ($specValues as $sv) {
$baseId = $sv['goods_spec_base_id'];
$value = $sv['value'] ?? '';
$dimName = '';
foreach ($dimValuesByName as $name => $values) {
if (in_array($value, $values)) {
$dimName = $name;
break;
}
}
$specByBaseId[$baseId][] = [
'type' => $dimName,
'value' => $value,
];
}
// 5. 解析座位模板中的 room 信息(用于提取 rowLabel, colNum, section 等)
$rooms = $seatTemplate['rooms'] ?? [];
$roomSeatInfo = []; // roomId → [rowLabel_colNum → [...]]
foreach ($rooms as $rIdx => $room) {
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
$sections = $room['sections'] ?? [];
$map = $room['map'] ?? [];
$seatsData = $room['seats'] ?? [];
foreach ($map as $rowIndex => $rowStr) {
$rowLabel = chr(65 + $rowIndex);
$chars = preg_split('//u', $rowStr, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as $colIndex => $char) {
if ($char === '_' || $char === '-' || !isset($seatsData[$char])) {
continue;
}
$colNum = $colIndex + 1;
$sectionInfo = null;
foreach ($sections as $sec) {
if (($sec['char'] ?? '') === $char) {
$sectionInfo = $sec;
break;
}
}
$roomSeatInfo[$roomId][$rowLabel . '_' . $colNum] = [
'rowLabel' => $rowLabel,
'colNum' => $colNum,
'section' => $sectionInfo,
'char' => $char,
];
}
}
}
// 6. 构建 seatSpecMapseatKey → 完整规格
foreach ($specs as $spec) {
$extends = json_decode($spec['extends'] ?? '{}', true);
$seatKey = $extends['seat_key'] ?? '';
if (empty($seatKey)) continue;
// 解析 seatKey 格式roomId_rowLabel_colNum
$parts = explode('_', $seatKey);
if (count($parts) < 3) continue;
$roomId = $parts[0];
$rowLabel = $parts[1];
$colNum = intval($parts[2]);
// 提取各维度值
$venueName = '';
$sectionName = '';
$seatName = '';
$sessionName = '';
$roomName = '';
foreach ($specByBaseId[$spec['id']] ?? [] as $specItem) {
$specType = $specItem['type'] ?? '';
$specVal = $specItem['value'] ?? '';
switch ($specType) {
case '$vr-场次':
$sessionName = $specVal;
break;
case '$vr-场馆':
$venueName = $specVal;
break;
case '$vr-演播室':
$roomName = $specVal;
break;
case '$vr-分区':
$sectionName = $specVal;
break;
case '$vr-座位号':
$seatName = $specVal;
break;
}
}
$seatMeta = $roomSeatInfo[$roomId][$rowLabel . '_' . $colNum] ?? [
'rowLabel' => $rowLabel,
'colNum' => $colNum,
'section' => null,
'char' => '',
];
$seatSpecMap[$seatKey] = [
'spec_base_id' => intval($spec['id']),
'price' => floatval($spec['price']),
'inventory' => intval($spec['inventory']), // ← 关键字段0=已售)
'spec' => $specByBaseId[$spec['id']] ?? [],
'rowLabel' => $seatMeta['rowLabel'],
'colNum' => $seatMeta['colNum'],
'roomId' => $roomId,
'roomName' => $roomName,
'section' => $seatMeta['section'],
'venueName' => $venueName,
'sectionName' => $sectionName,
'seatName' => $seatName,
'sessionName' => $sessionName,
];
}
return $seatSpecMap;
}
/**
* 构建 goods_spec_data场次列表含最低价
*
* @param array $sessions vr_goods_config.sessions[]
* @param array $seatSpecMap
* @return array
*/
private static function buildGoodsSpecData(array $sessions, array $seatSpecMap): array
{
if (empty($sessions)) {
return [];
}
$result = [];
foreach ($sessions as $session) {
$specName = ($session['start'] ?? '') . '-' . ($session['end'] ?? '');
$minPrice = PHP_FLOAT_MAX;
// 从 seatSpecMap 中找该场次的最低价
foreach ($seatSpecMap as $info) {
if (($info['sessionName'] ?? '') === $specName) {
$p = $info['price'] ?? PHP_FLOAT_MAX;
if ($p < $minPrice) {
$minPrice = $p;
}
}
}
$result[] = [
'spec_name' => $specName,
'price' => $minPrice < PHP_FLOAT_MAX ? $minPrice : 0,
'start' => $session['start'] ?? '',
'end' => $session['end'] ?? '',
];
}
return $result;
}
}