diff --git a/app/Services/VenueImportService.php b/app/Services/VenueImportService.php index 12b8df2..1b4b1a0 100644 --- a/app/Services/VenueImportService.php +++ b/app/Services/VenueImportService.php @@ -30,6 +30,9 @@ class VenueImportService /** @var array|null 主题:item_label => item_value(与数据字典 venue_type 同步,按请求缓存) */ private ?array $themeLabelToValueMapCache = null; + /** @var array|null 开放模式:item_label => item_value(与数据字典 venue_open_mode 同步,按请求缓存) */ + private ?array $openModeLabelToValueMapCache = null; + private const TICKET_TYPE_LABEL = [ '免费' => 'free', '收费' => 'paid', @@ -46,12 +49,6 @@ class VenueImportService '个人不需预约团队需预约' => 'team_required', ]; - private const OPEN_MODE_LABEL = [ - '常态化全时开放' => 'fulltime', - '常态化定时开放' => 'scheduled', - '预约开放' => 'appointment', - ]; - /** * @return array 中文标签 => item_value */ @@ -73,6 +70,27 @@ class VenueImportService return $this->themeLabelToValueMapCache; } + /** + * @return array 中文标签 => item_value(与后台数据字典「场馆开放模式」一致,勿硬编码以免改文案后导入失败) + */ + private function openModeLabelToValueMap(): array + { + if ($this->openModeLabelToValueMapCache !== null) { + return $this->openModeLabelToValueMapCache; + } + + $this->openModeLabelToValueMapCache = DictItem::query() + ->where('dict_type', 'venue_open_mode') + ->where('is_active', true) + ->orderBy('sort') + ->orderBy('id') + ->get(['item_label', 'item_value']) + ->mapWithKeys(fn ($r) => [trim((string) $r->item_label) => (string) $r->item_value]) + ->all(); + + return $this->openModeLabelToValueMapCache; + } + public function buildTemplateSpreadsheet(): Spreadsheet { $spreadsheet = new Spreadsheet(); @@ -153,9 +171,12 @@ class VenueImportService $opt->setCellValue('E4', '个人不需预约团队需预约'); $opt->setCellValue('F1', '开放模式'); - $opt->setCellValue('F2', '常态化全时开放'); - $opt->setCellValue('F3', '常态化定时开放'); - $opt->setCellValue('F4', '预约开放'); + $fr = 2; + foreach (array_keys($this->openModeLabelToValueMap()) as $openLabel) { + $opt->setCellValue('F' . $fr, $openLabel); + $fr++; + } + $lastF = max(2, $fr - 1); $opt->setCellValue('G1', '是否'); $opt->setCellValue('G2', '是'); @@ -167,7 +188,7 @@ class VenueImportService $this->applyListValidation($sheet, 'D2:D5000', '=选项!$D$2:$D$3'); // 预约类型 $this->applyListValidation($sheet, 'E2:E5000', '=选项!$C$2:$C$3'); // 门票类型 $this->applyListValidation($sheet, 'F2:F5000', '=选项!$E$2:$E$4'); // 预约模式 - $this->applyListValidation($sheet, 'G2:G5000', '=选项!$F$2:$F$4'); // 开放模式 + $this->applyListValidation($sheet, 'G2:G5000', '=选项!$F$2:$F$' . $lastF); // 开放模式(与字典 label 一致) $this->applyListValidation($sheet, 'O2:O5000', '=选项!$G$2:$G$3'); // 启用 $this->applyListValidation($sheet, 'P2:P5000', '=选项!$G$2:$G$3'); // 纳入人数统计 @@ -272,7 +293,8 @@ class VenueImportService $ticketType = $rawTicket === '' ? null : (self::TICKET_TYPE_LABEL[$rawTicket] ?? null); $appointmentType = $rawApp === '' ? null : (self::APPOINTMENT_LABEL[$rawApp] ?? null); $bookingMode = $rawBookingMode === '' ? null : (self::BOOKING_MODE_LABEL[$rawBookingMode] ?? null); - $openMode = $rawOpen === '' ? null : (self::OPEN_MODE_LABEL[$rawOpen] ?? null); + $openModeMap = $this->openModeLabelToValueMap(); + $openMode = $rawOpen === '' ? null : ($openModeMap[$rawOpen] ?? null); $lng = $this->parseFloat($g(17)); $lat = $this->parseFloat($g(18)); @@ -295,8 +317,9 @@ class VenueImportService 'consultation_hours' => $this->squishPlain($g(11) === '' ? null : $g(11)), 'contact_phone' => $this->squishPlain($g(12) === '' ? null : $g(12)), 'sort' => $sort, - 'is_active' => $this->parseBool($g(14)), - 'is_included_in_stats' => $this->parseBool($g(15)), + // 启用:留空按「是」与常见新建习惯;纳入人数统计:留空须为「否」,否则全表空单元格会全部被 parse 成 true + 'is_active' => $this->parseYesNoWithEmptyDefault($g(14), true), + 'is_included_in_stats' => $this->parseYesNoWithEmptyDefault($g(15), false), 'address' => $this->squishPlain($g(16) === '' ? null : $g(16)), 'lng' => $lng, 'lat' => $lat, @@ -339,7 +362,7 @@ class VenueImportService $errors[] = '预约类型须为「仅团队」「个人团队均可」或留空'; } if (($data['_raw_open_mode_label'] ?? '') !== '' && ($data['open_mode'] ?? null) === null) { - $errors[] = '开放模式须为三种选项之一或留空'; + $errors[] = '开放模式须与数据字典「场馆开放模式」中的选项名称一致,或留空(可重新下载模板查看当前选项)'; } if (($data['_raw_booking_mode_label'] ?? '') !== '' && ($data['booking_mode'] ?? null) === null) { $errors[] = '预约模式须为三种选项之一或留空'; @@ -485,16 +508,22 @@ class VenueImportService return is_finite($n) ? $n : null; } - private function parseBool(string $s): bool + /** + * 解析「是/否」类单元格。空串或无法识别时由 $defaultWhenEmpty 决定,避免与「启用」不同列共用同一套默认导致误纳入统计。 + */ + private function parseYesNoWithEmptyDefault(string $s, bool $defaultWhenEmpty): bool { - $t = mb_strtolower(trim($s)); - if ($t === '' || in_array($t, ['1', '是', 'y', 'yes', 'true', '启用'], true)) { + $t = mb_strtolower(trim($s), 'UTF-8'); + if ($t === '') { + return $defaultWhenEmpty; + } + if (in_array($t, ['1', '是', 'y', 'yes', 'true', '启用'], true)) { return true; } if (in_array($t, ['0', '否', 'n', 'no', 'false', '禁用'], true)) { return false; } - return true; + return $defaultWhenEmpty; } }