488 lines
18 KiB
PHP
488 lines
18 KiB
PHP
<?php
|
||
/**
|
||
* VR票务插件 - 座位 SKU 服务
|
||
*
|
||
* 核心业务:批量生成座位级 SKU(spec_base + spec_value)
|
||
* 旁路 GoodsSpecificationsInsert(),直接 SQL INSERT
|
||
*
|
||
* @package vr_ticket\service
|
||
*/
|
||
|
||
namespace app\plugins\vr_ticket\service;
|
||
|
||
class SeatSkuService extends BaseService
|
||
{
|
||
/** @var int 分批处理每批条数 */
|
||
const BATCH_SIZE = 500;
|
||
|
||
/**
|
||
* 批量生成座位级 SKU
|
||
*
|
||
* 遍历座位模板的 seat_map,为每个座位生成:
|
||
* 1. goods_spec_base 行(inventory=1,价格从 zone.price 获取)
|
||
* 2. goods_spec_value 行(4维度 × N座位 = 4N行)
|
||
*
|
||
* 幂等:已存在的座位(spec_value 中已有关联)不重复生成
|
||
*
|
||
* @param int $goodsId 商品ID
|
||
* @param int $seatTemplateId 座位模板ID
|
||
* @return array ['code' => 0, 'msg' => '...', 'data' => ['total' => N, 'generated' => N, 'spec_base_id_map' => ['seatId' => spec_base_id, ...]]]
|
||
*
|
||
* spec_base_id_map 格式:前端 ticket_detail.html 使用 seatKey(如 "A_1")作为 key,
|
||
* 期望 value 为整数 spec_base_id(如 2001)。
|
||
*/
|
||
public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
|
||
{
|
||
$goodsId = intval($goodsId);
|
||
$seatTemplateId = intval($seatTemplateId);
|
||
|
||
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);
|
||
if (empty($seatMap['map']) || empty($seatMap['seats'])) {
|
||
return ['code' => -3, 'msg' => '座位模板 seat_map 数据无效'];
|
||
}
|
||
|
||
// 3. 获取/确认 VR 规格类型ID($vr-场馆 / $vr-分区 / $vr-时段 / $vr-座位号)
|
||
$specTypeIds = self::ensureVrSpecTypes($goodsId);
|
||
if ($specTypeIds['code'] !== 0) {
|
||
return $specTypeIds;
|
||
}
|
||
$typeVenue = $specTypeIds['data']['$vr-场馆'];
|
||
$typeZone = $specTypeIds['data']['$vr-分区'];
|
||
$typeTime = $specTypeIds['data']['$vr-时段'];
|
||
$typeSeat = $specTypeIds['data']['$vr-座位号'];
|
||
|
||
// 4. 构建 section → price 映射(从 seat_map.sections 读)
|
||
// 格式:section['name'] => section['price'](默认 0)
|
||
$sectionPrices = [];
|
||
foreach (($seatMap['sections'] ?? []) as $section) {
|
||
$sectionPrices[$section['name'] ?? ''] = floatval($section['price'] ?? 0);
|
||
}
|
||
|
||
// 5. 收集所有座位数据
|
||
$seats = []; // [seatId => ['row' => int, 'col' => int, 'char' => string, 'label' => string, 'price' => float, 'zone' => string]]
|
||
$map = $seatMap['map'];
|
||
$rowLabels = $seatMap['row_labels'] ?? [];
|
||
$seatsData = $seatMap['seats'] ?? [];
|
||
|
||
foreach ($map as $rowIndex => $rowStr) {
|
||
$rowLabel = $rowLabels[$rowIndex] ?? chr(65 + $rowIndex);
|
||
$chars = mb_str_split($rowStr);
|
||
foreach ($chars as $colIndex => $char) {
|
||
if ($char === '_' || $char === '-' || !isset($seatsData[$char])) {
|
||
continue; // 跳过空座/通道/无效
|
||
}
|
||
$seatInfo = $seatsData[$char];
|
||
$zoneName = $seatInfo['zone'] ?? ($seatInfo['section'] ?? '默认区');
|
||
|
||
// 价格:优先用 seat_info.zone.price,没有则用 sectionPrices,最后用 seat_info.price
|
||
$seatPrice = floatval($seatInfo['price'] ?? 0);
|
||
if ($seatPrice == 0 && isset($sectionPrices[$zoneName])) {
|
||
$seatPrice = $sectionPrices[$zoneName];
|
||
}
|
||
|
||
$seatId = $rowLabel . '_' . ($colIndex + 1); // 唯一座位标识,与前端 specBaseIdMap key 格式一致(如 "A_1")
|
||
$seats[$seatId] = [
|
||
'row' => $rowIndex,
|
||
'col' => $colIndex,
|
||
'char' => $char,
|
||
'label' => $seatInfo['label'] ?? ($rowLabel . '排' . ($colIndex + 1) . '座'),
|
||
'price' => $seatPrice,
|
||
'zone' => $zoneName,
|
||
'row_label' => $rowLabel,
|
||
'col_num' => $colIndex + 1,
|
||
'seat_key' => $seatId,
|
||
];
|
||
}
|
||
}
|
||
|
||
if (empty($seats)) {
|
||
return ['code' => -4, 'msg' => '座位模板中未找到有效座位'];
|
||
}
|
||
|
||
// 6. 找出已存在的 spec_base_id(幂等:只处理新座位)
|
||
$existingMap = self::getExistingSpecBaseIds($goodsId, $typeSeat);
|
||
$newSeats = [];
|
||
foreach ($seats as $seatId => $seat) {
|
||
if (!isset($existingMap[$seatId])) {
|
||
$newSeats[$seatId] = $seat;
|
||
}
|
||
}
|
||
|
||
if (empty($newSeats)) {
|
||
return [
|
||
'code' => 0,
|
||
'msg' => '所有座位 SKU 已存在,无需重复生成',
|
||
'data' => [
|
||
'total' => count($seats),
|
||
'generated' => 0,
|
||
'batch' => 0,
|
||
'spec_base_id_map' => $existingMap,
|
||
],
|
||
];
|
||
}
|
||
|
||
// 7. 分批插入 goods_spec_base + goods_spec_value
|
||
$now = time();
|
||
$newSeatIds = array_keys($newSeats);
|
||
$totalBatches = ceil(count($newSeatIds) / self::BATCH_SIZE);
|
||
$generatedCount = 0;
|
||
$specBaseIdMap = $existingMap; // 合并已存在和新生成的
|
||
|
||
for ($batch = 0; $batch < $totalBatches; $batch++) {
|
||
$batchSeatIds = array_slice($newSeatIds, $batch * self::BATCH_SIZE, self::BATCH_SIZE);
|
||
$baseInsertData = [];
|
||
$valueInsertData = [];
|
||
|
||
foreach ($batchSeatIds as $seatId) {
|
||
$seat = $newSeats[$seatId];
|
||
|
||
// 1行 goods_spec_base
|
||
$baseInsertData[] = [
|
||
'goods_id' => $goodsId,
|
||
'price' => $seat['price'],
|
||
'original_price' => $seat['price'],
|
||
'inventory' => 1,
|
||
'buy_min_number' => 1,
|
||
'buy_max_number' => 1,
|
||
'weight' => 0.00,
|
||
'volume' => 0.00,
|
||
'coding' => '',
|
||
'barcode' => '',
|
||
'inventory_unit' => '座',
|
||
'extends' => json_encode([
|
||
'seat_id' => $seatId,
|
||
'seat_char' => $seat['char'],
|
||
'row_label' => $seat['row_label'],
|
||
'zone' => $seat['zone'],
|
||
'label' => $seat['label'],
|
||
], JSON_UNESCAPED_UNICODE),
|
||
'add_time' => $now,
|
||
];
|
||
}
|
||
|
||
// 批量插入 spec_base,获取自增ID
|
||
$specBaseIds = self::batchInsertSpecBase($baseInsertData);
|
||
|
||
// 构建并批量插入 spec_value(每个 base_id × 4维度)
|
||
foreach ($specBaseIds as $idx => $specBaseId) {
|
||
$seatId = $batchSeatIds[$idx];
|
||
$seat = $newSeats[$seatId];
|
||
|
||
// $vr-场馆
|
||
$valueInsertData[] = [
|
||
'goods_id' => $goodsId,
|
||
'goods_spec_base_id' => $specBaseId,
|
||
'spec_type_id' => $typeVenue,
|
||
'value' => '国家体育馆',
|
||
'md5_key' => md5('国家体育馆'),
|
||
'add_time' => $now,
|
||
];
|
||
// $vr-分区(zone 名称)
|
||
$valueInsertData[] = [
|
||
'goods_id' => $goodsId,
|
||
'goods_spec_base_id' => $specBaseId,
|
||
'spec_type_id' => $typeZone,
|
||
'value' => $seat['zone'],
|
||
'md5_key' => md5($seat['zone']),
|
||
'add_time' => $now,
|
||
];
|
||
// $vr-时段(placeholder,后续由 UpdateSessionSku 替换)
|
||
$valueInsertData[] = [
|
||
'goods_id' => $goodsId,
|
||
'goods_spec_base_id' => $specBaseId,
|
||
'spec_type_id' => $typeTime,
|
||
'value' => '待选场次',
|
||
'md5_key' => md5('待选场次'),
|
||
'add_time' => $now,
|
||
];
|
||
// $vr-座位号
|
||
$valueInsertData[] = [
|
||
'goods_id' => $goodsId,
|
||
'goods_spec_base_id' => $specBaseId,
|
||
'spec_type_id' => $typeSeat,
|
||
'value' => $seat['label'],
|
||
'md5_key' => md5($seat['label']),
|
||
'add_time' => $now,
|
||
];
|
||
|
||
$specBaseIdMap[$seatId] = $specBaseId;
|
||
$generatedCount++;
|
||
}
|
||
|
||
// 批量插入 spec_value
|
||
if (!empty($valueInsertData)) {
|
||
self::batchInsertSpecValue($valueInsertData);
|
||
}
|
||
}
|
||
|
||
// 8. 更新座位模板的 spec_base_id_map 字段
|
||
self::updateTemplateSpecMap($seatTemplateId, $specBaseIdMap);
|
||
|
||
self::log('BatchGenerate: done', [
|
||
'goods_id' => $goodsId,
|
||
'template_id'=> $seatTemplateId,
|
||
'total' => count($seats),
|
||
'generated' => $generatedCount,
|
||
'batches' => $totalBatches,
|
||
]);
|
||
|
||
return [
|
||
'code' => 0,
|
||
'msg' => "生成完成,共 {$generatedCount} 个座位 SKU(分 {$totalBatches} 批)",
|
||
'data' => [
|
||
'total' => count($seats),
|
||
'generated' => $generatedCount,
|
||
'batch' => $totalBatches,
|
||
'spec_base_id_map' => $specBaseIdMap,
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 确保 VR 规格类型存在
|
||
*
|
||
* @param int $goodsId
|
||
* @return array
|
||
*/
|
||
private static function ensureVrSpecTypes(int $goodsId): array
|
||
{
|
||
$now = time();
|
||
$specTypeNames = ['$vr-场馆', '$vr-分区', '$vr-时段', '$vr-座位号'];
|
||
$defaultValues = [
|
||
'$vr-场馆' => '[{"name":"国家体育馆","images":""}]',
|
||
'$vr-分区' => '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]',
|
||
'$vr-时段' => '[{"name":"待选场次","images":""}]',
|
||
'$vr-座位号' => '[{"name":"待选座位","images":""}]',
|
||
];
|
||
|
||
$typeIds = [];
|
||
foreach ($specTypeNames as $name) {
|
||
$existing = \Db::name('GoodsSpecType')
|
||
->where('goods_id', $goodsId)
|
||
->where('name', $name)
|
||
->find();
|
||
|
||
if (!empty($existing)) {
|
||
$typeIds[$name] = intval($existing['id']);
|
||
} else {
|
||
$id = \Db::name('GoodsSpecType')->insertGetId([
|
||
'goods_id' => $goodsId,
|
||
'name' => $name,
|
||
'value' => $defaultValues[$name],
|
||
'add_time' => $now,
|
||
]);
|
||
$typeIds[$name] = $id;
|
||
}
|
||
}
|
||
|
||
// 确保商品启用多规格
|
||
\Db::name('Goods')->where('id', $goodsId)->update([
|
||
'is_exist_many_spec' => 1,
|
||
'upd_time' => $now,
|
||
]);
|
||
|
||
return ['code' => 0, 'data' => $typeIds];
|
||
}
|
||
|
||
/**
|
||
* 批量插入 goods_spec_base,返回自增ID列表
|
||
*
|
||
* @param array $data 二维数组
|
||
* @return array 自增ID列表
|
||
*/
|
||
private static function batchInsertSpecBase(array $data): array
|
||
{
|
||
if (empty($data)) {
|
||
return [];
|
||
}
|
||
|
||
$table = \Db::name('GoodsSpecBase')->getTable();
|
||
$columns = array_keys($data[0]);
|
||
$placeholders = implode(',', array_fill(0, count($data), '(' . implode(',', array_fill(0, count($columns), '?')) . ')'));
|
||
$values = [];
|
||
foreach ($data as $row) {
|
||
foreach ($columns as $col) {
|
||
$values[] = $row[$col];
|
||
}
|
||
}
|
||
|
||
$sql = "INSERT INTO {$table} (" . implode(',', $columns) . ") VALUES {$placeholders}";
|
||
\Db::execute($sql, $values);
|
||
|
||
// 获取本批插入的自增ID
|
||
$lastId = (int) \Db::query("SELECT LAST_INSERT_ID()")[0]['LAST_INSERT_ID()'] ?? 0;
|
||
$count = count($data);
|
||
$ids = [];
|
||
for ($i = 0; $i < $count; $i++) {
|
||
$ids[] = $lastId + $i;
|
||
}
|
||
return $ids;
|
||
}
|
||
|
||
/**
|
||
* 批量插入 goods_spec_value
|
||
*
|
||
* @param array $data 二维数组
|
||
*/
|
||
private static function batchInsertSpecValue(array $data): void
|
||
{
|
||
if (empty($data)) {
|
||
return;
|
||
}
|
||
|
||
$table = \Db::name('GoodsSpecValue')->getTable();
|
||
$columns = array_keys($data[0]);
|
||
$placeholders = implode(',', array_fill(0, count($data), '(' . implode(',', array_fill(0, count($columns), '?')) . ')'));
|
||
$values = [];
|
||
foreach ($data as $row) {
|
||
foreach ($columns as $col) {
|
||
$values[] = $row[$col];
|
||
}
|
||
}
|
||
|
||
$sql = "INSERT INTO {$table} (" . implode(',', $columns) . ") VALUES {$placeholders}";
|
||
\Db::execute($sql, $values);
|
||
}
|
||
|
||
/**
|
||
* 获取已存在的座位 spec_base_id 映射(幂等用)
|
||
*
|
||
* @param int $goodsId
|
||
* @param int $typeSeatId $vr-座位号 spec_type_id
|
||
* @return array [seatId => spec_base_id]
|
||
*/
|
||
private static function getExistingSpecBaseIds(int $goodsId, int $typeSeatId): array
|
||
{
|
||
// 从 goods_spec_value 中找 $vr-座位号 的记录
|
||
// value 字段存储的是 seat_label(如 "A排1座"),从中解析出 seatId(如 "A_1")
|
||
$rows = \Db::name('GoodsSpecValue')
|
||
->where('goods_id', $goodsId)
|
||
->where('spec_type_id', $typeSeatId)
|
||
->column('goods_spec_base_id', 'value');
|
||
|
||
if (empty($rows)) {
|
||
return [];
|
||
}
|
||
|
||
$seatIdMap = [];
|
||
foreach ($rows as $seatLabel => $baseId) {
|
||
// 从 seat_label 解析 seatId(如 "A排1座" → "A_1")
|
||
// 格式: "{rowLabel}排{colNum}座"
|
||
// Bug fix: 原正则 `^([A-Za-z]+)(\d+)排(\d)座$` 第二个 `\d+` 会吞掉 colNum 的高位数字,
|
||
// 例如 "A排10座" 匹配为 rowLabel="A" colNum=1(错误),应为 colNum=10
|
||
if (preg_match('/^([A-Za-z]+)排(\d+)座$/', $seatLabel, $m)) {
|
||
$rowLabel = $m[1];
|
||
$colNum = intval($m[2]);
|
||
$seatId = $rowLabel . '_' . $colNum;
|
||
$seatIdMap[$seatId] = intval($baseId);
|
||
}
|
||
}
|
||
|
||
return $seatIdMap;
|
||
}
|
||
|
||
/**
|
||
* 更新座位模板的 spec_base_id_map 字段
|
||
*
|
||
* @param int $templateId
|
||
* @param array $specBaseIdMap
|
||
*/
|
||
private static function updateTemplateSpecMap(int $templateId, array $specBaseIdMap): void
|
||
{
|
||
\Db::name(self::table('seat_templates'))
|
||
->where('id', $templateId)
|
||
->update([
|
||
'spec_base_id_map' => json_encode($specBaseIdMap, JSON_UNESCAPED_UNICODE),
|
||
'upd_time' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 按场次更新座位 SKU 的 $vr-时段 维度
|
||
*
|
||
* 当用户选择具体场次后,将所有座位的"待选场次"替换为实际场次时间
|
||
*
|
||
* @param int $goodsId 商品ID
|
||
* @param int $seatTemplateId 座位模板ID
|
||
* @param string $sessionName 场次名称(如 "2026-05-01 19:00")
|
||
* @param float $sessionPrice 场次价格(可选,用于替换价格)
|
||
* @return array
|
||
*/
|
||
public static function UpdateSessionSku(int $goodsId, int $seatTemplateId, string $sessionName, float $sessionPrice = 0.0): array
|
||
{
|
||
$goodsId = intval($goodsId);
|
||
$seatTemplateId = intval($seatTemplateId);
|
||
|
||
// 获取 $vr-时段 type_id
|
||
$timeType = \Db::name('GoodsSpecType')
|
||
->where('goods_id', $goodsId)
|
||
->where('name', '$vr-时段')
|
||
->find();
|
||
if (empty($timeType)) {
|
||
return ['code' => -1, 'msg' => '$vr-时段 规格类型不存在,请先调用 BatchGenerate()'];
|
||
}
|
||
$typeTimeId = intval($timeType['id']);
|
||
|
||
// 找出所有"待选场次"的 spec_value 行
|
||
$待选Rows = \Db::name('GoodsSpecValue')
|
||
->where('goods_id', $goodsId)
|
||
->where('spec_type_id', $typeTimeId)
|
||
->where('value', '待选场次')
|
||
->select()
|
||
->toArray();
|
||
|
||
if (empty($待选Rows)) {
|
||
return ['code' => 0, 'msg' => '没有需要更新的场次', 'data' => ['updated' => 0]];
|
||
}
|
||
|
||
$now = time();
|
||
$updatedCount = 0;
|
||
foreach ($待选Rows as $row) {
|
||
\Db::name('GoodsSpecValue')
|
||
->where('id', $row['id'])
|
||
->update([
|
||
'value' => $sessionName,
|
||
'md5_key' => md5($sessionName),
|
||
'add_time' => $now,
|
||
]);
|
||
$updatedCount++;
|
||
}
|
||
|
||
// 如果提供了场次价格,更新对应 spec_base 的价格
|
||
if ($sessionPrice > 0) {
|
||
$待选BaseIds = array_column($待选Rows, 'goods_spec_base_id');
|
||
\Db::name('GoodsSpecBase')
|
||
->whereIn('id', $待选BaseIds)
|
||
->update([
|
||
'price' => $sessionPrice,
|
||
'original_price' => $sessionPrice,
|
||
]);
|
||
}
|
||
|
||
self::log('UpdateSessionSku: done', [
|
||
'goods_id' => $goodsId,
|
||
'session' => $sessionName,
|
||
'updated' => $updatedCount,
|
||
]);
|
||
|
||
return [
|
||
'code' => 0,
|
||
'msg' => "更新 {$updatedCount} 个座位的场次信息",
|
||
'data' => ['updated' => $updatedCount],
|
||
];
|
||
}
|
||
}
|