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], ]; } }