334 lines
11 KiB
PHP
334 lines
11 KiB
PHP
<?php
|
||
namespace app\plugins\vr_ticket\service;
|
||
|
||
use think\facade\Db;
|
||
|
||
/**
|
||
* VR票务 - 座位图服务(UniApp seatmap API 专用)
|
||
*
|
||
* 提供 seatSpecMap(含实时库存)+ goods_spec_data
|
||
* 模板快照走 ShopXO Cache(TTL 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 Cache,TTL 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. 构建 seatSpecMap:seatKey → 完整规格
|
||
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;
|
||
}
|
||
} |