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

483 lines
19 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
/**
* VR票务插件 - 座位 SKU 服务
*
* ShopXO 规格表结构:
* GoodsSpecType : id, goods_id, name, value(JSON数组), add_time
* GoodsSpecBase : id, goods_id, price, original_price, inventory, ...
* GoodsSpecValue : id, goods_id, goods_spec_base_id, value, md5_key, add_time
*
* 列对应关系由 GoodsEditSpecifications 决定:
* 它把每个 GoodsSpecValue.value 逐个在 GoodsSpecType.value(JSON) 的 name 字段中搜索,
* 匹配成功才显示在对应列。
* 因此GoodsSpecType.value JSON 里必须包含我们写入的所有 value 字符串。
*
* @package vr_ticket\service
*/
namespace app\plugins\vr_ticket\service;
use think\facade\Db;
class SeatSkuService extends BaseService
{
const BATCH_SIZE = 200;
/**
* VR 规格维度名(顺序固定)
*/
const SPEC_DIMS = ['$vr-场馆', '$vr-分区', '$vr-座位号', '$vr-场次'];
/**
* 批量生成座位级 SKU
*
* @param int $goodsId
* @param int $seatTemplateId
* @param array $selectedRooms 要生成的厅 id 列表,空=全部
* @param array $selectedSections 按 roomId 为 key 的分区 char 数组,空=全部
* @param array $sessions 场次数组 e.g. [["start"=>"08:00","end"=>"23:59"]]
*/
public static function BatchGenerate(
int $goodsId,
int $seatTemplateId,
array $selectedRooms = [],
array $selectedSections = [],
array $sessions = []
): array {
if ($goodsId <= 0 || $seatTemplateId <= 0) {
return ['code' => -1, 'msg' => '参数错误goodsId 或 seatTemplateId 无效'];
}
// 1. 加载座位模板
$template = Db::name(self::table('seat_templates'))
->where('id', $seatTemplateId)
->find();
if (empty($template)) {
return ['code' => -2, 'msg' => "座位模板 {$seatTemplateId} 不存在"];
}
// 2. 解析 seat_map
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
$rooms = $seatMap['rooms'] ?? [];
if (empty($rooms)) {
return ['code' => -3, 'msg' => '座位模板 seat_map 无效rooms 为空)'];
}
// 使用模板表的短名称
$venueName = $template['name'] ?? '未命名场馆';
// 3. 场次处理(默认兜底)
if (empty($sessions)) {
$sessions = [['start' => '08:00', 'end' => '23:59']];
}
$sessionStrings = [];
foreach ($sessions as $s) {
$start = is_array($s) ? ($s['start'] ?? '08:00') : '08:00';
$end = is_array($s) ? ($s['end'] ?? '23:59') : '23:59';
$sessionStr = "{$start}-{$end}";
if (!in_array($sessionStr, $sessionStrings)) {
$sessionStrings[] = $sessionStr;
}
}
// 4. 确保 4 个 VR spec type 维度存在,同时收集要写入的所有唯一 value
// 我们先遍历座位图,收集全部规格值,再一次性写入 type.value JSON
$selectedRooms = array_values(array_filter($selectedRooms));
$selectedSections = array_filter($selectedSections);
// 按维度收集唯一值(用 有序列表 + 去重)
$dimUniqueValues = [
'$vr-场馆' => [],
'$vr-分区' => [],
'$vr-座位号' => [],
'$vr-场次' => [],
];
// 5. 遍历地图,收集所有座位信息
$seatsToInsert = [];
foreach ($rooms as $rIdx => $room) {
// 与前端 PHP 预处理保持一致id 缺失时用 'room_{index}'
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
$roomName = $room['name'] ?? '默认放映室';
if (!empty($selectedRooms) && !in_array($roomId, $selectedRooms)) {
continue;
}
// char → price 映射
$sectionPrices = [];
foreach (($room['sections'] ?? []) as $sec) {
if (!empty($sec['char'])) {
$sectionPrices[$sec['char']] = floatval($sec['price'] ?? 0);
}
}
$map = $room['map'] ?? [];
$seatsData = $room['seats'] ?? [];
foreach ($map as $rowIndex => $rowStr) {
$rowLabel = chr(65 + $rowIndex);
$chars = mb_str_split($rowStr);
foreach ($chars as $colIndex => $char) {
if ($char === '_' || $char === '-' || !isset($seatsData[$char])) {
continue;
}
if (!empty($selectedSections[$roomId])
&& !in_array($char, $selectedSections[$roomId])) {
continue;
}
// 价格三级 fallback
$seatInfo = $seatsData[$char];
$seatPrice = floatval($seatInfo['price'] ?? 0);
if ($seatPrice == 0 && isset($sectionPrices[$char])) {
$seatPrice = $sectionPrices[$char];
}
if ($seatPrice == 0) {
foreach (($room['sections'] ?? []) as $sec) {
if (($sec['char'] ?? '') === $char) {
$seatPrice = floatval($sec['price'] ?? 0);
break;
}
}
}
$col = $colIndex + 1;
// 维度值字符串(确保非空)
$val_venue = $venueName;
$val_section = $venueName . '-' . $roomName . '-' . $char;
$val_seat = $venueName . '-' . $roomName . '-' . $char . '-' . $rowLabel . $col;
foreach ($sessionStrings as $sessionStr) {
$seatId = $roomId . '_' . $rowLabel . '_' . $col . '_' . md5($sessionStr);
$seatsToInsert[$seatId] = [
'price' => $seatPrice,
'spec_values' => [
$val_venue,
$val_section,
$val_seat,
$sessionStr,
],
];
// 收集唯一维度值(保持首次出现顺序)
if (!in_array($val_venue, $dimUniqueValues['$vr-场馆'])) {
$dimUniqueValues['$vr-场馆'][] = $val_venue;
}
if (!in_array($val_section, $dimUniqueValues['$vr-分区'])) {
$dimUniqueValues['$vr-分区'][] = $val_section;
}
if (!in_array($val_seat, $dimUniqueValues['$vr-座位号'])) {
$dimUniqueValues['$vr-座位号'][] = $val_seat;
}
if (!in_array($sessionStr, $dimUniqueValues['$vr-场次'])) {
$dimUniqueValues['$vr-场次'][] = $sessionStr;
}
}
}
}
}
if (empty($seatsToInsert)) {
return ['code' => -4, 'msg' => '无有效座位可生成'];
}
// 6. 保证 4 个 VR spec type 存在,并把已收集的 value 写入 type.value JSON
// 这样 GoodsEditSpecifications 的 name 匹配才能命中每条 GoodsSpecValue
self::ensureAndFillVrSpecTypes($goodsId, $dimUniqueValues);
// 7. 写入 GoodsSpecBase + GoodsSpecValue
$now = time();
$generatedCount = 0;
$valueBatch = [];
foreach ($seatsToInsert as $seatId => $s) {
$baseId = Db::name('GoodsSpecBase')->insertGetId([
'goods_id' => $goodsId,
'price' => $s['price'],
'original_price' => 0,
'inventory' => 1,
'buy_min_number' => 1,
'buy_max_number' => 1,
'weight' => 0,
'volume' => 0,
'coding' => '',
'barcode' => '',
'add_time' => $now,
]);
if (!$baseId) {
throw new \Exception("GoodsSpecBase 写入失败 (seat: {$seatId})");
}
// 4 条 GoodsSpecValue每条对应一个维度
foreach ($s['spec_values'] as $specVal) {
$valueBatch[] = [
'goods_id' => $goodsId,
'goods_spec_base_id' => $baseId,
'value' => (string)$specVal,
'md5_key' => md5((string)$specVal),
'add_time' => $now,
];
}
$generatedCount++;
if (count($valueBatch) >= self::BATCH_SIZE) {
Db::name('GoodsSpecValue')->insertAll($valueBatch);
$valueBatch = [];
}
}
if (!empty($valueBatch)) {
Db::name('GoodsSpecValue')->insertAll($valueBatch);
}
return [
'code' => 0,
'msg' => '生成成功',
'data' => [
'total' => count($seatsToInsert),
'generated' => $generatedCount,
],
];
}
/**
* 幂等确保 4 个 VR 维度存在,并把本次收集的所有唯一值合并写入 type.value JSON
*
* 关键GoodsEditSpecifications 通过 type.value JSON 里的 name 做匹配,
* 所以每条 GoodsSpecValue.value 都必须在对应 type.value 的某个 name 里找到。
*
* @param int $goodsId
* @param array $dimUniqueValues ['$vr-场馆' => [...], '$vr-分区' => [...], ...]
*/
public static function ensureAndFillVrSpecTypes(int $goodsId, array $dimUniqueValues = []): void
{
$now = time();
// 读取已存在的 VR 维度(按顺序)
$existing = Db::name('GoodsSpecType')
->where('goods_id', $goodsId)
->whereIn('name', self::SPEC_DIMS)
->order('id', 'asc')
->select()
->toArray();
$existingByName = array_column($existing, null, 'name');
foreach (self::SPEC_DIMS as $dimName) {
// 构建该维度的 value JSON把所有唯一值合并含已有值
$newItems = [];
$existingItems = [];
if (isset($existingByName[$dimName])) {
$existingItems = json_decode($existingByName[$dimName]['value'] ?? '[]', true);
if (!is_array($existingItems)) $existingItems = [];
}
// 把现有 JSON 中的 name 提取出来
$existingNames = array_column($existingItems, 'name');
// 合并本次新增的值
$toAdd = $dimUniqueValues[$dimName] ?? [];
foreach ($toAdd as $val) {
if (!in_array($val, $existingNames)) {
$existingItems[] = ['name' => (string)$val, 'images' => ''];
$existingNames[] = $val;
}
}
$valueJson = json_encode($existingItems, JSON_UNESCAPED_UNICODE);
if (isset($existingByName[$dimName])) {
// 更新已有维度的 value JSON
Db::name('GoodsSpecType')
->where('id', $existingByName[$dimName]['id'])
->update(['value' => $valueJson]);
} else {
// 插入缺失维度(保持 SPEC_DIMS 顺序ID 递增)
Db::name('GoodsSpecType')->insert([
'goods_id' => $goodsId,
'name' => $dimName,
'value' => $valueJson,
'add_time' => $now,
]);
}
}
}
/**
* 重新计算商品基础信息(价格区间、总库存)
*/
public static function refreshGoodsBase(int $goodsId): array
{
$bases = Db::name('GoodsSpecBase')
->where('goods_id', $goodsId)
->select()
->toArray();
if (empty($bases)) {
return ['code' => -1, 'msg' => '无 GoodsSpecBase'];
}
$prices = array_column($bases, 'price');
$minPrice = min($prices);
$maxPrice = max($prices);
$inventory = array_sum(array_column($bases, 'inventory'));
$priceDisplay = ($minPrice != $maxPrice && $maxPrice > 0)
? $minPrice . '-' . $maxPrice : $minPrice;
Db::name('Goods')->where('id', $goodsId)->update([
'min_price' => $minPrice,
'max_price' => $maxPrice,
'price' => $priceDisplay,
'min_original_price' => 0,
'max_original_price' => 0,
'original_price' => 0,
'inventory' => $inventory,
'is_exist_many_spec' => 1,
'buy_min_number' => 1,
'buy_max_number' => 1,
'upd_time' => time(),
]);
return ['code' => 0];
}
/**
* 获取商品前端展示数据(供 ticket_detail.html 模板使用)
*
* @param int $goodsId
* @return array ['vr_seat_template' => [...], 'goods_spec_data' => [...]]
*/
public static function GetGoodsViewData(int $goodsId): array
{
// 读取 vr_goods_config
$goods = \think\facade\Db::name('goods')->find($goodsId);
$vrGoodsConfig = json_decode($goods['vr_goods_config'] ?? '', true);
if (empty($vrGoodsConfig) || !is_array($vrGoodsConfig)) {
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}
// 过滤有效配置块(多模板模式)
$validConfigs = [];
foreach ($vrGoodsConfig as $cfg) {
$tid = intval($cfg['template_id'] ?? 0);
if ($tid <= 0) continue;
$tpl = \think\facade\Db::name(self::table('seat_templates'))->where('id', $tid)->find();
if (!empty($tpl)) {
$validConfigs[] = $cfg;
}
}
if (empty($validConfigs)) {
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}
// 取第一个有效配置块用于前端展示
$config = $validConfigs[0];
$templateId = intval($config['template_id'] ?? 0);
if ($templateId <= 0) {
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}
// 读取座位模板(包含 seat_map 和 spec_base_id_map
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
->where('id', $templateId)
->find();
// 模板不存在时(硬删除场景):清理无效配置块,写回有效配置
if (empty($seatTemplate)) {
$validConfigs = array_filter($validConfigs, function ($cfg) use ($templateId) {
$tid = intval($cfg['template_id'] ?? 0);
if ($tid <= 0 || $tid === $templateId) return false;
$tpl = \think\facade\Db::name(self::table('seat_templates'))->where('id', $tid)->find();
return !empty($tpl);
});
$validConfigs = array_values($validConfigs);
if (!empty($validConfigs)) {
\think\facade\Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => json_encode($validConfigs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
} else {
\think\facade\Db::name('Goods')->where('id', $goodsId)->update(['vr_goods_config' => '']);
}
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}
// 解码 seat_map JSON存储时是 JSON 字符串)
if (!empty($seatTemplate['seat_map'])) {
$decoded = json_decode($seatTemplate['seat_map'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$seatTemplate['seat_map'] = $decoded;
}
}
// 解码 spec_base_id_map JSON
if (!empty($seatTemplate['spec_base_id_map'])) {
$decoded = json_decode($seatTemplate['spec_base_id_map'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$seatTemplate['spec_base_id_map'] = $decoded;
}
}
// 构建场次列表goods_spec_data
$sessions = $config['sessions'] ?? [];
$goodsSpecData = [];
foreach ($sessions as $session) {
$start = $session['start'] ?? '';
$end = $session['end'] ?? '';
$timeRange = $start && $end ? "{$start}-{$end}" : ($start ?: $end);
// 查找该场次对应的 spec_base_id
$specValue = \think\facade\Db::name('goods_spec_value')
->alias('sv')
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
->where('sv.goods_id', $goodsId)
->where('sv.value', $timeRange)
->where('sb.price', '>', 0)
->find();
$goodsSpecData[] = [
'spec_id' => $specValue['goods_spec_base_id'] ?? 0,
'spec_name' => $timeRange,
'price' => $specValue['price'] ?? floatval($goods['price'] ?? 0),
];
}
// 如果没有从配置读取到场次,尝试从数据库直接读取场次类规格值
if (empty($goodsSpecData)) {
$sessionValues = \think\facade\Db::name('goods_spec_value')
->alias('sv')
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
->field('sv.goods_spec_base_id as spec_id, sv.value as spec_name, sb.price')
->where('sv.goods_id', $goodsId)
->where('sb.price', '>', 0)
->order('sb.id asc')
->select()->toArray();
foreach ($sessionValues as $sv) {
if (preg_match('/^\d{2}:\d{2}-\d{2}:\d{2}$/', $sv['spec_name'])) {
$goodsSpecData[] = [
'spec_id' => $sv['spec_id'],
'spec_name' => $sv['spec_name'],
'price' => floatval($sv['price']),
];
}
}
}
return [
'vr_seat_template' => $seatTemplate ?: null,
'goods_spec_data' => $goodsSpecData,
'goods_config' => $config,
];
}
}