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

488 lines
18 KiB
PHP
Raw Normal View History

<?php
/**
* VR票务插件 - 座位 SKU 服务
*
* 核心业务:批量生成座位级 SKUspec_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],
];
}
}