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

488 lines
18 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 服务
*
* 核心业务:批量生成座位级 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],
];
}
}