"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) { $seatId = $roomId . '_' . $rowLabel . '_' . $col . '_' . md5($sessionStr); $seatsToInsert[$seatId] = [ 'price' => $seatPrice, '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, ]); if (!$baseId) { throw new \Exception("GoodsSpecBase 写入失败 (seat: {$seatId})"); } // 4 条 GoodsSpecValue,每条对应一个维度 foreach ($s['spec_values'] as $specVal) { $valueBatch[] = [ 'goods_id' => $goodsId, 'goods_spec_base_id' => $baseId, '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' => [...]] */ 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' => [], 'goods_config' => null]; } // 取第一个配置块(单模板模式) $config = $vrGoodsConfig[0]; $templateId = intval($config['template_id'] ?? 0); if ($templateId <= 0) { return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null]; } // 读取座位模板(包含 seat_map 和 spec_base_id_map) $seatTemplate = \think\facade\Db::name(self::table('seat_templates')) ->where('id', $templateId) ->find(); // 模板不存在时(硬删除场景): // - 将 template_id 置 null,让前端选单显示为空 // - 同时清掉 template_snapshot,下次保存时整块 config 干净地失效 if (empty($seatTemplate)) { $config['template_id'] = null; $config['template_snapshot'] = null; \think\facade\Db::name('Goods')->where('id', $goodsId)->update([ 'vr_goods_config' => json_encode([$config], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); return [ 'vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => $config, ]; } // 解码 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; } } // 解码 spec_base_id_map JSON if (!empty($seatTemplate['spec_base_id_map'])) { $decoded = json_decode($seatTemplate['spec_base_id_map'], true); if (json_last_error() === JSON_ERROR_NONE) { $seatTemplate['spec_base_id_map'] = $decoded; } } // 构建场次列表(goods_spec_data) $sessions = $config['sessions'] ?? []; $goodsSpecData = []; foreach ($sessions as $session) { $start = $session['start'] ?? ''; $end = $session['end'] ?? ''; $timeRange = $start && $end ? "{$start}-{$end}" : ($start ?: $end); // 查找该场次对应的 spec_base_id $specValue = \think\facade\Db::name('goods_spec_value') ->alias('sv') ->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id') ->where('sv.goods_id', $goodsId) ->where('sv.value', $timeRange) ->where('sb.price', '>', 0) ->find(); $goodsSpecData[] = [ 'spec_id' => $specValue['goods_spec_base_id'] ?? 0, 'spec_name' => $timeRange, 'price' => $specValue['price'] ?? floatval($goods['price'] ?? 0), ]; } // 如果没有从配置读取到场次,尝试从数据库直接读取场次类规格值 if (empty($goodsSpecData)) { $sessionValues = \think\facade\Db::name('goods_spec_value') ->alias('sv') ->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id') ->field('sv.goods_spec_base_id as spec_id, sv.value as spec_name, sb.price') ->where('sv.goods_id', $goodsId) ->where('sb.price', '>', 0) ->order('sb.id asc') ->select()->toArray(); foreach ($sessionValues as $sv) { if (preg_match('/^\d{2}:\d{2}-\d{2}:\d{2}$/', $sv['spec_name'])) { $goodsSpecData[] = [ 'spec_id' => $sv['spec_id'], 'spec_name' => $sv['spec_name'], 'price' => floatval($sv['price']), ]; } } } return [ 'vr_seat_template' => $seatTemplate ?: null, 'goods_spec_data' => $goodsSpecData, 'goods_config' => $config, ]; } }