"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) { $seatKey = $roomId . '_' . $rowLabel . '_' . $col; $seatId = $seatKey . '_' . md5($sessionStr); $seatsToInsert[$seatId] = [ 'price' => $seatPrice, 'seat_key' => $seatKey, // ← 用于前端映射 'extends' => json_encode(['seat_key' => $seatKey], JSON_UNESCAPED_UNICODE), '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, 'extends' => $s['extends'] ?? null, ]); if (!$baseId) { throw new \Exception("GoodsSpecBase 写入失败 (seat: {$seatId})"); } // 4 条 GoodsSpecValue,每条对应一个维度 foreach ($s['spec_values'] as $idx => $specVal) { $valueBatch[] = [ 'goods_id' => $goodsId, 'goods_spec_base_id' => $baseId, 'type' => self::SPEC_DIMS[$idx] ?? '', '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' => [], 'seatSpecMap' => [...]] */ 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' => [], 'seatSpecMap' => [], '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' => [], 'seatSpecMap' => [], 'goods_config' => null]; } // 取第一个有效配置块用于前端展示 $config = $validConfigs[0]; $templateId = intval($config['template_id'] ?? 0); if ($templateId <= 0) { return ['vr_seat_template' => null, 'goods_spec_data' => [], 'seatSpecMap' => [], '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' => [], 'seatSpecMap' => [], '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; } } // ========== 新增:构建 seatSpecMap ========== $seatSpecMap = self::buildSeatSpecMap($goodsId, $seatTemplate); // ========== 构建场次列表(goods_spec_data)========== $sessions = $config['sessions'] ?? []; $goodsSpecData = []; foreach ($sessions as $session) { $start = $session['start'] ?? ''; $end = $session['end'] ?? ''; $timeRange = $start && $end ? "{$start}-{$end}" : ($start ?: $end); // 查找该场次对应的最低价格(从 seatSpecMap 中获取) $sessionPrice = null; foreach ($seatSpecMap as $seatKey => $info) { foreach ($info['spec'] as $specItem) { $specType = $specItem['type'] ?? ''; $specValue = $specItem['value'] ?? ''; if ($specType === '$vr-场次' && $specValue === $timeRange) { if ($sessionPrice === null || $info['price'] < $sessionPrice) { $sessionPrice = $info['price']; } break 2; } } } $goodsSpecData[] = [ 'spec_id' => 0, // 不再需要 spec_id,前端用 seatSpecMap 'spec_name' => $timeRange, 'price' => $sessionPrice ?? floatval($goods['price'] ?? 0), 'start' => $start, 'end' => $end, ]; } // 如果没有从配置读取到场次,尝试从 seatSpecMap 提取唯一场次 if (empty($goodsSpecData)) { $sessionMap = []; foreach ($seatSpecMap as $info) { foreach ($info['spec'] as $specItem) { $specType = $specItem['type'] ?? ''; if ($specType === '$vr-场次') { $sessionMap[$specItem['value'] ?? ''] = $specItem['value'] ?? ''; } } } foreach ($sessionMap as $timeRange) { $goodsSpecData[] = [ 'spec_id' => 0, 'spec_name' => $timeRange, 'price' => floatval($goods['price'] ?? 0), 'start' => '', 'end' => '', ]; } } return [ 'vr_seat_template' => $seatTemplate ?: null, 'goods_spec_data' => $goodsSpecData, 'seatSpecMap' => $seatSpecMap, 'goods_config' => $config, ]; } /** * 构建座位规格映射表(seatSpecMap) * * @param int $goodsId * @param array $seatTemplate * @return array seatSpecMap */ private static function buildSeatSpecMap(int $goodsId, array $seatTemplate): array { $seatSpecMap = []; // 1. 查询当前商品所有 GoodsSpecBase(含 extends.seat_key) $specs = \think\facade\Db::name('GoodsSpecBase') ->where('goods_id', $goodsId) ->where('inventory', '>', 0) ->select() ->toArray(); if (empty($specs)) { return $seatSpecMap; } // 2. 查询每个 spec_base_id 对应的 4 维 GoodsSpecValue $specBaseIds = array_column($specs, 'id'); $specValues = \think\facade\Db::name('GoodsSpecValue') ->whereIn('goods_spec_base_id', $specBaseIds) ->select() ->toArray(); // 3. 按 spec_base_id 分组,构建 4 维 spec 数组 $specByBaseId = []; foreach ($specValues as $sv) { $specByBaseId[$sv['goods_spec_base_id']][] = [ 'type' => $sv['type'] ?? '', 'value' => $sv['value'] ?? '', ]; } // 4. 解析座位模板中的 room 信息(用于提取 rowLabel, colNum 等) $rooms = $seatTemplate['seat_map']['rooms'] ?? []; $roomSeatInfo = []; // roomId => [rowLabel_colNum => ['rowLabel' => 'A', 'colNum' => 3, 'section' => [...]]] foreach ($rooms as $rIdx => $room) { $roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx); $sections = $room['sections'] ?? []; $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; } $colNum = $colIndex + 1; // 查找分区信息 $sectionInfo = null; foreach ($sections as $sec) { if (($sec['char'] ?? '') === $char) { $sectionInfo = $sec; break; } } $roomSeatInfo[$roomId][$rowLabel . '_' . $colNum] = [ 'rowLabel' => $rowLabel, 'colNum' => $colNum, 'section' => $sectionInfo, 'char' => $char, ]; } } } // 5. 构建 seatSpecMap:seat_key → 完整规格 foreach ($specs as $spec) { $extends = json_decode($spec['extends'] ?? '{}', true); $seatKey = $extends['seat_key'] ?? ''; if (empty($seatKey)) continue; // 解析 seatKey 格式:roomId_rowLabel_colNum $parts = explode('_', $seatKey); if (count($parts) < 3) continue; $roomId = $parts[0]; $rowLabel = $parts[1]; $colNum = intval($parts[2]); // 提取场馆名(从 $vr-场馆 维度) $venueName = ''; $sectionName = ''; $seatName = ''; $sessionName = ''; foreach ($specByBaseId[$spec['id']] ?? [] as $specItem) { $specType = $specItem['type'] ?? ''; $specVal = $specItem['value'] ?? ''; switch ($specType) { case '$vr-场馆': $venueName = $specVal; break; case '$vr-分区': $sectionName = $specVal; break; case '$vr-座位号': $seatName = $specVal; break; case '$vr-场次': $sessionName = $specVal; break; } } // 获取座位元信息 $seatMeta = $roomSeatInfo[$roomId][$rowLabel . '_' . $colNum] ?? [ 'rowLabel' => $rowLabel, 'colNum' => $colNum, 'section' => null, 'char' => '', ]; $seatSpecMap[$seatKey] = [ 'spec_base_id' => intval($spec['id']), 'price' => floatval($spec['price']), 'inventory' => intval($spec['inventory']), 'spec' => $specByBaseId[$spec['id']] ?? [], 'rowLabel' => $seatMeta['rowLabel'], 'colNum' => $seatMeta['colNum'], 'roomId' => $roomId, 'section' => $seatMeta['section'], 'venueName' => $venueName, 'sectionName' => $sectionName, 'seatName' => $seatName, 'sessionName' => $sessionName, ]; } return $seatSpecMap; } }