ensureConfigDefaults(); $this->ensureHelpArticlesTable(); $this->seedHelpArticles(); $this->ensurePolicyHelpArticles(); self::$bootstrapped = true; } public function getHomeConfig(): array { $this->bootstrapDefaults(); $defaults = $this->defaultHomeConfig(); $configMap = Db::name('system_configs') ->where('config_group', self::HOME_GROUP) ->column('config_value', 'config_key'); return [ 'banners' => $this->decodeJsonConfig($configMap['banners_json'] ?? '', $defaults['banners']), 'page_visuals' => $this->normalizeObjectConfig( $this->decodeJsonObjectConfig($configMap['page_visuals_json'] ?? '', $defaults['page_visuals']), ['order_background_image_url', 'report_background_image_url'], $defaults['page_visuals'] ), 'service_entries' => $this->decodeJsonConfig($configMap['service_entries_json'] ?? '', $defaults['service_entries']), 'category_visuals' => $this->decodeJsonConfig($configMap['category_visuals_json'] ?? '', $defaults['category_visuals']), 'quick_entries' => $this->decodeJsonConfig($configMap['quick_entries_json'] ?? '', $defaults['quick_entries']), 'trust_metrics' => $this->decodeJsonConfig($configMap['trust_metrics_json'] ?? '', $defaults['trust_metrics']), 'trust_points' => $this->decodeJsonConfig($configMap['trust_points_json'] ?? '', $defaults['trust_points']), 'faqs' => $this->decodeJsonConfig($configMap['faqs_json'] ?? '', $defaults['faqs']), ]; } public function getPolicyConfig(): array { $this->bootstrapDefaults(); $defaults = $this->defaultPolicyConfig(); $configMap = Db::name('system_configs') ->where('config_group', self::POLICY_GROUP) ->column('config_value', 'config_key'); return [ 'legal_entries' => $this->hydratePolicyItems( $this->decodeJsonConfig($configMap['legal_entries_json'] ?? '', $defaults['legal_entries']) ), 'appraisal_agreements' => $this->hydratePolicyItems( $this->decodeJsonConfig($configMap['appraisal_agreements_json'] ?? '', $defaults['appraisal_agreements']) ), ]; } public function getMetaConfig(): array { $this->bootstrapDefaults(); $defaults = $this->defaultMetaConfig(); $configMap = Db::name('system_configs') ->where('config_group', self::META_GROUP) ->column('config_value', 'config_key'); return [ 'help_categories' => $this->decodeJsonConfig($configMap['help_categories_json'] ?? '', $defaults['help_categories']), 'report_risk_defaults' => $this->decodeJsonConfig($configMap['report_risk_defaults_json'] ?? '', $defaults['report_risk_defaults']), 'ticket_types' => $this->decodeJsonConfig($configMap['ticket_types_json'] ?? '', $defaults['ticket_types']), 'ticket_statuses' => $this->decodeJsonConfig($configMap['ticket_statuses_json'] ?? '', $defaults['ticket_statuses']), 'message_events' => $this->decodeJsonConfig($configMap['message_events_json'] ?? '', $defaults['message_events']), 'message_page_copy' => $this->decodeJsonObjectConfig($configMap['message_page_copy_json'] ?? '', $defaults['message_page_copy']), ]; } public function saveHomeConfig(array $payload): void { $this->bootstrapDefaults(); $defaults = $this->defaultHomeConfig(); $existing = $this->getHomeConfig(); $categoryVisuals = array_key_exists('category_visuals', $payload) ? $payload['category_visuals'] : $existing['category_visuals']; $normalized = [ 'banners_json' => json_encode($this->normalizeArrayItems($payload['banners'] ?? [], ['title', 'subtitle', 'description', 'background_image_url'], $defaults['banners']), JSON_UNESCAPED_UNICODE), 'page_visuals_json' => json_encode($this->normalizeObjectConfig($payload['page_visuals'] ?? [], ['order_background_image_url', 'report_background_image_url'], $defaults['page_visuals']), JSON_UNESCAPED_UNICODE), 'service_entries_json' => json_encode($this->normalizeArrayItems($payload['service_entries'] ?? [], ['service_provider', 'title', 'tag', 'description', 'meta'], $defaults['service_entries']), JSON_UNESCAPED_UNICODE), 'category_visuals_json' => json_encode($this->normalizeArrayItems($categoryVisuals, ['category_name', 'category_code', 'image_url'], $defaults['category_visuals']), JSON_UNESCAPED_UNICODE), 'quick_entries_json' => json_encode($this->normalizeArrayItems($payload['quick_entries'] ?? [], ['code', 'title', 'desc'], $defaults['quick_entries']), JSON_UNESCAPED_UNICODE), 'trust_metrics_json' => json_encode($this->normalizeArrayItems($payload['trust_metrics'] ?? [], ['value', 'label'], $defaults['trust_metrics']), JSON_UNESCAPED_UNICODE), 'trust_points_json' => json_encode($this->normalizeArrayItems($payload['trust_points'] ?? [], ['title', 'desc'], $defaults['trust_points']), JSON_UNESCAPED_UNICODE), 'faqs_json' => json_encode($this->normalizeStringList($payload['faqs'] ?? [], $defaults['faqs']), JSON_UNESCAPED_UNICODE), ]; $now = date('Y-m-d H:i:s'); foreach ($normalized as $configKey => $configValue) { $this->upsertSystemConfig(self::HOME_GROUP, $configKey, $configValue, $now); } } public function savePolicyConfig(array $payload): void { $this->bootstrapDefaults(); $defaults = $this->defaultPolicyConfig(); $normalized = [ 'legal_entries_json' => json_encode($this->normalizePolicyItems($payload['legal_entries'] ?? [], $defaults['legal_entries']), JSON_UNESCAPED_UNICODE), 'appraisal_agreements_json' => json_encode($this->normalizePolicyItems($payload['appraisal_agreements'] ?? [], $defaults['appraisal_agreements']), JSON_UNESCAPED_UNICODE), ]; $now = date('Y-m-d H:i:s'); foreach ($normalized as $configKey => $configValue) { $this->upsertSystemConfig(self::POLICY_GROUP, $configKey, $configValue, $now); } } public function saveMetaConfig(array $payload): void { $this->bootstrapDefaults(); $defaults = $this->defaultMetaConfig(); $normalized = [ 'help_categories_json' => json_encode($this->normalizeArrayItems($payload['help_categories'] ?? [], ['code', 'title', 'desc'], $defaults['help_categories']), JSON_UNESCAPED_UNICODE), 'report_risk_defaults_json' => json_encode($this->normalizeArrayItems($payload['report_risk_defaults'] ?? [], ['report_type', 'title', 'text'], $defaults['report_risk_defaults']), JSON_UNESCAPED_UNICODE), 'ticket_types_json' => json_encode($this->normalizeArrayItems($payload['ticket_types'] ?? [], ['code', 'title', 'hint', 'quick_desc'], $defaults['ticket_types']), JSON_UNESCAPED_UNICODE), 'ticket_statuses_json' => json_encode($this->normalizeArrayItems($payload['ticket_statuses'] ?? [], ['code', 'title', 'desc'], $defaults['ticket_statuses']), JSON_UNESCAPED_UNICODE), 'message_events_json' => json_encode($this->normalizeArrayItems($payload['message_events'] ?? [], ['event_code', 'title', 'desc'], $defaults['message_events']), JSON_UNESCAPED_UNICODE), 'message_page_copy_json' => json_encode($this->normalizeObjectConfig($payload['message_page_copy'] ?? [], ['title', 'desc'], $defaults['message_page_copy']), JSON_UNESCAPED_UNICODE), ]; $now = date('Y-m-d H:i:s'); foreach ($normalized as $configKey => $configValue) { $this->upsertSystemConfig(self::META_GROUP, $configKey, $configValue, $now); } } public function getHelpArticles(bool $enabledOnly = false): array { $this->bootstrapDefaults(); $query = Db::name(self::HELP_TABLE)->order('sort_order', 'asc')->order('id', 'asc'); if ($enabledOnly) { $query->where('is_enabled', 1); } return array_map(function (array $item) { return [ 'id' => (int)$item['id'], 'category' => (string)$item['category'], 'category_text' => $this->categoryText((string)$item['category']), 'title' => (string)$item['title'], 'summary' => (string)$item['summary'], 'keywords' => $this->decodeJsonConfig($item['keywords_json'] ?? '', []), 'updated_at' => (string)$item['updated_at'], 'is_recommended' => (bool)$item['is_recommended'], 'is_enabled' => (bool)$item['is_enabled'], 'sort_order' => (int)$item['sort_order'], 'content_blocks' => $this->decodeJsonConfig($item['content_blocks_json'] ?? '', []), ]; }, $query->select()->toArray()); } public function getHelpCategories(): array { $meta = $this->getMetaConfig(); return $meta['help_categories']; } public function getTicketTypes(): array { $meta = $this->getMetaConfig(); return $meta['ticket_types']; } public function getTicketStatuses(): array { $meta = $this->getMetaConfig(); return $meta['ticket_statuses']; } public function getMessageEvents(): array { $meta = $this->getMetaConfig(); return $meta['message_events']; } public function getMessagePageCopy(): array { $meta = $this->getMetaConfig(); return $meta['message_page_copy']; } public function getHelpArticle(int $id): ?array { $items = $this->getHelpArticles(true); foreach ($items as $item) { if ((int)$item['id'] === $id) { return $item; } } return null; } public function saveHelpArticle(array $payload): int { $this->bootstrapDefaults(); $id = (int)($payload['id'] ?? 0); $category = trim((string)($payload['category'] ?? 'service')); $title = trim((string)($payload['title'] ?? '')); $summary = trim((string)($payload['summary'] ?? '')); $keywords = $this->normalizeStringList($payload['keywords'] ?? [], []); $contentBlocks = $this->normalizeStringList($payload['content_blocks'] ?? [], []); $isRecommended = !empty($payload['is_recommended']) ? 1 : 0; $isEnabled = array_key_exists('is_enabled', $payload) ? (!empty($payload['is_enabled']) ? 1 : 0) : 1; $sortOrder = (int)($payload['sort_order'] ?? 0); if ($title === '' || $summary === '') { throw new \RuntimeException('文章标题和摘要不能为空'); } if (!$contentBlocks) { throw new \RuntimeException('请至少填写一段文章正文'); } if (!in_array($category, ['service', 'report', 'shipping', 'support'], true)) { throw new \RuntimeException('文章分类不合法'); } $now = date('Y-m-d H:i:s'); $data = [ 'category' => $category, 'title' => $title, 'summary' => $summary, 'keywords_json' => json_encode($keywords, JSON_UNESCAPED_UNICODE), 'content_blocks_json' => json_encode($contentBlocks, JSON_UNESCAPED_UNICODE), 'is_recommended' => $isRecommended, 'is_enabled' => $isEnabled, 'sort_order' => $sortOrder, 'updated_at' => $now, ]; if ($id > 0) { $exists = Db::name(self::HELP_TABLE)->where('id', $id)->find(); if (!$exists) { throw new \RuntimeException('帮助文章不存在'); } Db::name(self::HELP_TABLE)->where('id', $id)->update($data); return $id; } $data['created_at'] = $now; return (int)Db::name(self::HELP_TABLE)->insertGetId($data); } public function deleteHelpArticle(int $id): void { $this->bootstrapDefaults(); if ($id <= 0) { throw new \RuntimeException('文章 ID 不能为空'); } $exists = Db::name(self::HELP_TABLE)->where('id', $id)->find(); if (!$exists) { throw new \RuntimeException('帮助文章不存在'); } $reference = $this->findPolicyReferenceByArticleId($id); if ($reference !== null) { throw new \RuntimeException(sprintf('该文章已被“%s”引用,请先在内容中心的协议与说明中解绑', $reference)); } Db::name(self::HELP_TABLE)->where('id', $id)->delete(); } public function categoryText(string $category): string { foreach ($this->getHelpCategories() as $item) { if (($item['code'] ?? '') === $category) { return (string)($item['title'] ?? $category); } } return $category; } public function getReportRiskNotice(string $reportType): string { $meta = $this->getMetaConfig(); foreach ($meta['report_risk_defaults'] as $item) { if (($item['report_type'] ?? '') === $reportType) { return (string)($item['text'] ?? ''); } } return ''; } public function ticketTypeText(string $type): string { foreach ($this->getTicketTypes() as $item) { if (($item['code'] ?? '') === $type) { return (string)($item['title'] ?? $type); } } return $type; } public function ticketStatusText(string $status): string { foreach ($this->getTicketStatuses() as $item) { if (($item['code'] ?? '') === $status) { return (string)($item['title'] ?? $status); } } return $status; } private function ensureConfigDefaults(): void { $homeDefaults = $this->defaultHomeConfig(); $defaults = [ self::HOME_GROUP => [ 'banners_json' => json_encode($homeDefaults['banners'], JSON_UNESCAPED_UNICODE), 'page_visuals_json' => json_encode($homeDefaults['page_visuals'], JSON_UNESCAPED_UNICODE), 'service_entries_json' => json_encode($homeDefaults['service_entries'], JSON_UNESCAPED_UNICODE), 'category_visuals_json' => json_encode($homeDefaults['category_visuals'], JSON_UNESCAPED_UNICODE), 'quick_entries_json' => json_encode($homeDefaults['quick_entries'], JSON_UNESCAPED_UNICODE), 'trust_metrics_json' => json_encode($homeDefaults['trust_metrics'], JSON_UNESCAPED_UNICODE), 'trust_points_json' => json_encode($homeDefaults['trust_points'], JSON_UNESCAPED_UNICODE), 'faqs_json' => json_encode($homeDefaults['faqs'], JSON_UNESCAPED_UNICODE), ], self::POLICY_GROUP => [ 'legal_entries_json' => json_encode($this->defaultPolicyConfig()['legal_entries'], JSON_UNESCAPED_UNICODE), 'appraisal_agreements_json' => json_encode($this->defaultPolicyConfig()['appraisal_agreements'], JSON_UNESCAPED_UNICODE), ], self::META_GROUP => [ 'help_categories_json' => json_encode($this->defaultMetaConfig()['help_categories'], JSON_UNESCAPED_UNICODE), 'report_risk_defaults_json' => json_encode($this->defaultMetaConfig()['report_risk_defaults'], JSON_UNESCAPED_UNICODE), 'ticket_types_json' => json_encode($this->defaultMetaConfig()['ticket_types'], JSON_UNESCAPED_UNICODE), 'ticket_statuses_json' => json_encode($this->defaultMetaConfig()['ticket_statuses'], JSON_UNESCAPED_UNICODE), 'message_events_json' => json_encode($this->defaultMetaConfig()['message_events'], JSON_UNESCAPED_UNICODE), 'message_page_copy_json' => json_encode($this->defaultMetaConfig()['message_page_copy'], JSON_UNESCAPED_UNICODE), ], ]; $groupCodes = array_keys($defaults); $existingRows = Db::name('system_configs') ->field(['config_group', 'config_key']) ->whereIn('config_group', $groupCodes) ->select() ->toArray(); $existingMap = []; foreach ($existingRows as $item) { $existingMap[($item['config_group'] ?? '') . '.' . ($item['config_key'] ?? '')] = true; } $now = date('Y-m-d H:i:s'); $insertRows = []; foreach ($defaults as $groupCode => $items) { foreach ($items as $configKey => $configValue) { $mapKey = $groupCode . '.' . $configKey; if (isset($existingMap[$mapKey])) { continue; } $insertRows[] = [ 'config_group' => $groupCode, 'config_key' => $configKey, 'config_value' => $configValue, 'remark' => '内容配置', 'created_at' => $now, 'updated_at' => $now, ]; } } if ($insertRows) { Db::name('system_configs')->insertAll($insertRows); } } private function ensureHelpArticlesTable(): void { $exists = Db::query(sprintf("SHOW TABLES LIKE '%s'", self::HELP_TABLE)); if ($exists) { return; } Db::execute(sprintf( "CREATE TABLE %s ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, category VARCHAR(32) NOT NULL DEFAULT 'service', title VARCHAR(255) NOT NULL DEFAULT '', summary VARCHAR(500) NOT NULL DEFAULT '', keywords_json LONGTEXT NULL, content_blocks_json LONGTEXT NULL, is_recommended TINYINT(1) NOT NULL DEFAULT 0, is_enabled TINYINT(1) NOT NULL DEFAULT 1, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_help_articles_category (category), KEY idx_help_articles_enabled (is_enabled), KEY idx_help_articles_sort (sort_order) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='帮助中心文章'", self::HELP_TABLE )); } private function ensurePolicyHelpArticles(): void { $defaults = $this->defaultPolicyConfig(); $configMap = Db::name('system_configs') ->where('config_group', self::POLICY_GROUP) ->column('config_value', 'config_key'); $policy = [ 'legal_entries' => $this->decodeJsonConfig($configMap['legal_entries_json'] ?? '', $defaults['legal_entries']), 'appraisal_agreements' => $this->decodeJsonConfig($configMap['appraisal_agreements_json'] ?? '', $defaults['appraisal_agreements']), ]; $changed = false; foreach (['legal_entries', 'appraisal_agreements'] as $section) { $items = []; foreach (($policy[$section] ?? []) as $item) { if (!is_array($item)) { continue; } $normalizedItem = $item; $articleId = (int)($normalizedItem['article_id'] ?? 0); if ($articleId <= 0) { $articleId = $this->extractHelpArticleIdFromTargetUrl((string)($normalizedItem['target_url'] ?? '')); } if ($articleId > 0) { $article = Db::name(self::HELP_TABLE)->field(['id'])->where('id', $articleId)->find(); if (!$article) { $articleId = 0; } } if ($articleId <= 0) { $articleId = $this->resolveOrCreatePolicyArticleId((string)($normalizedItem['code'] ?? '')); } if ($articleId > 0) { $nextTargetUrl = $this->buildHelpArticleTargetUrl($articleId); if ((int)($normalizedItem['article_id'] ?? 0) !== $articleId || (string)($normalizedItem['target_url'] ?? '') !== $nextTargetUrl) { $changed = true; } $normalizedItem['article_id'] = $articleId; $normalizedItem['target_url'] = $nextTargetUrl; } $items[] = $normalizedItem; } $policy[$section] = $items; } if (!$changed) { return; } $now = date('Y-m-d H:i:s'); $this->upsertSystemConfig(self::POLICY_GROUP, 'legal_entries_json', json_encode($policy['legal_entries'], JSON_UNESCAPED_UNICODE), $now); $this->upsertSystemConfig(self::POLICY_GROUP, 'appraisal_agreements_json', json_encode($policy['appraisal_agreements'], JSON_UNESCAPED_UNICODE), $now); } private function seedHelpArticles(): void { $count = (int)Db::name(self::HELP_TABLE)->count(); if ($count > 0) { return; } $now = date('Y-m-d H:i:s'); foreach ($this->defaultHelpArticles() as $index => $item) { Db::name(self::HELP_TABLE)->insert([ 'category' => $item['category'], 'title' => $item['title'], 'summary' => $item['summary'], 'keywords_json' => json_encode($item['keywords'], JSON_UNESCAPED_UNICODE), 'content_blocks_json' => json_encode($item['content_blocks'], JSON_UNESCAPED_UNICODE), 'is_recommended' => !empty($item['is_recommended']) ? 1 : 0, 'is_enabled' => 1, 'sort_order' => $item['sort_order'] ?? ($index + 1) * 10, 'created_at' => $item['updated_at'] ?? $now, 'updated_at' => $item['updated_at'] ?? $now, ]); } } private function upsertSystemConfig(string $groupCode, string $configKey, string $configValue, string $now): void { $exists = Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $configKey) ->find(); $payload = [ 'config_group' => $groupCode, 'config_key' => $configKey, 'config_value' => $configValue, 'remark' => '内容配置', 'updated_at' => $now, ]; if ($exists) { Db::name('system_configs')->where('id', $exists['id'])->update($payload); return; } $payload['created_at'] = $now; Db::name('system_configs')->insert($payload); } private function decodeJsonConfig(string $value, array $default): array { if ($value === '') { return $default; } $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : $default; } private function decodeJsonObjectConfig(string $value, array $default): array { if ($value === '') { return $default; } $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : $default; } private function normalizeArrayItems(mixed $value, array $keys, array $default): array { if (!is_array($value)) { return $default; } $normalized = []; foreach ($value as $item) { if (!is_array($item)) { continue; } $row = []; foreach ($keys as $key) { $row[$key] = trim((string)($item[$key] ?? '')); } if (implode('', $row) === '') { continue; } $normalized[] = $row; } return $normalized ?: $default; } private function normalizePolicyItems(mixed $value, array $default): array { if (!is_array($value)) { return $this->hydratePolicyItems($default); } $normalized = []; foreach ($value as $item) { if (!is_array($item)) { continue; } $code = trim((string)($item['code'] ?? '')); $title = trim((string)($item['title'] ?? '')); $desc = trim((string)($item['desc'] ?? '')); $targetUrl = trim((string)($item['target_url'] ?? '')); $articleId = (int)($item['article_id'] ?? 0); if ($articleId <= 0) { $articleId = $this->extractHelpArticleIdFromTargetUrl($targetUrl); } if ($articleId > 0) { $article = Db::name(self::HELP_TABLE)->field(['id'])->where('id', $articleId)->find(); if (!$article) { throw new \RuntimeException(sprintf('绑定文章 #%d 不存在,请重新选择', $articleId)); } $targetUrl = $this->buildHelpArticleTargetUrl($articleId); } if ($code === '' && $title === '' && $desc === '' && $targetUrl === '' && $articleId <= 0) { continue; } $normalized[] = [ 'code' => $code, 'title' => $title, 'desc' => $desc, 'target_url' => $targetUrl, 'article_id' => $articleId, ]; } return $normalized ?: $this->hydratePolicyItems($default); } private function hydratePolicyItems(array $items): array { $normalized = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $articleId = (int)($item['article_id'] ?? 0); $targetUrl = trim((string)($item['target_url'] ?? '')); if ($articleId <= 0) { $articleId = $this->extractHelpArticleIdFromTargetUrl($targetUrl); } if ($articleId > 0) { $targetUrl = $this->buildHelpArticleTargetUrl($articleId); } $normalized[] = [ 'code' => trim((string)($item['code'] ?? '')), 'title' => trim((string)($item['title'] ?? '')), 'desc' => trim((string)($item['desc'] ?? '')), 'target_url' => $targetUrl, 'article_id' => $articleId, ]; } return $normalized; } private function extractHelpArticleIdFromTargetUrl(string $targetUrl): int { if ($targetUrl === '') { return 0; } if (!preg_match('/\/pages\/help\/detail\?id=(\d+)/', $targetUrl, $matches)) { return 0; } return (int)($matches[1] ?? 0); } private function buildHelpArticleTargetUrl(int $articleId): string { return sprintf('/pages/help/detail?id=%d', $articleId); } private function findPolicyReferenceByArticleId(int $articleId): ?string { $policy = $this->getPolicyConfig(); foreach (['legal_entries' => '设置页说明入口', 'appraisal_agreements' => '下单确认协议'] as $section => $sectionName) { foreach (($policy[$section] ?? []) as $item) { $itemArticleId = (int)($item['article_id'] ?? 0); if ($itemArticleId <= 0) { $itemArticleId = $this->extractHelpArticleIdFromTargetUrl((string)($item['target_url'] ?? '')); } if ($itemArticleId !== $articleId) { continue; } $title = trim((string)($item['title'] ?? '')); return $title !== '' ? sprintf('%s / %s', $sectionName, $title) : $sectionName; } } return null; } private function normalizeObjectConfig(mixed $value, array $keys, array $default): array { if (!is_array($value)) { return $default; } $normalized = []; foreach ($keys as $key) { $normalized[$key] = trim((string)($value[$key] ?? '')); } return implode('', $normalized) === '' ? $default : $normalized; } private function normalizeStringList(mixed $value, array $default): array { if (!is_array($value)) { return $default; } $normalized = array_values(array_filter(array_map( fn ($item) => trim((string)$item), $value ), fn ($item) => $item !== '')); return $normalized ?: $default; } private function defaultHomeConfig(): array { return [ 'banners' => [ [ 'title' => '安心验', 'subtitle' => '独立第三方鉴定服务平台', 'description' => '专业鉴定高价值商品,报告可验真,流程可追踪。', 'background_image_url' => '', ], ], 'page_visuals' => [ 'order_background_image_url' => '', 'report_background_image_url' => '', ], 'service_entries' => [ [ 'service_provider' => 'anxinyan', 'title' => '实物鉴定', 'tag' => '标准服务', 'description' => '由安心验提供标准实物鉴定服务,适合正式结果交付场景。', 'meta' => '预计 48 小时内出结果 | 报告可验真', ], [ 'service_provider' => 'zhongjian', 'title' => '中检鉴定', 'tag' => '更高规格机构', 'description' => '由更高规格机构提供实物鉴定服务,适合更高要求场景。', 'meta' => '流程一致 | 出具机构不同 | 价格与时效有差异', ], ], 'category_visuals' => [ ['category_name' => '奢侈品箱包', 'category_code' => 'luxury_bag', 'image_url' => ''], ['category_name' => '潮流鞋类', 'category_code' => 'sneaker', 'image_url' => ''], ['category_name' => '首饰配饰', 'category_code' => 'jewelry', 'image_url' => ''], ['category_name' => '高端美妆', 'category_code' => 'beauty', 'image_url' => ''], ['category_name' => '腕表', 'category_code' => 'watch', 'image_url' => ''], ['category_name' => '服饰', 'category_code' => 'clothing', 'image_url' => ''], ['category_name' => '3C 数码', 'category_code' => 'digital', 'image_url' => ''], ['category_name' => '古董文玩', 'category_code' => 'antique', 'image_url' => ''], ], 'quick_entries' => [ ['code' => 'start', 'title' => '发起鉴定', 'desc' => '进入送检流程'], ['code' => 'orders', 'title' => '我的订单', 'desc' => '查看当前进度'], ['code' => 'reports', 'title' => '我的报告', 'desc' => '查看结果凭证'], ['code' => 'messages', 'title' => '消息中心', 'desc' => '查看服务提醒与结果通知'], ], 'trust_metrics' => [ ['value' => '1280+', 'label' => '累计鉴定申请'], ['value' => '48h', 'label' => '标准结果时效'], ['value' => '100%', 'label' => '正式报告可验真'], ], 'trust_points' => [ ['title' => '独立第三方', 'desc' => '保持中立判断,不参与买卖立场。'], ['title' => '报告可验真', 'desc' => '每份正式报告均支持编号与状态验证。'], ['title' => '流程可追踪', 'desc' => '从下单到出报告,关键节点一目了然。'], ['title' => '标准化作业', 'desc' => '按模板采集资料,单次鉴定后出具报告。'], ], 'faqs' => [ '实物鉴定和中检鉴定有什么区别?', '一般多久可以出结果?', '报告如何验证真伪?', ], ]; } private function defaultPolicyConfig(): array { return [ 'legal_entries' => [ [ 'code' => 'privacy_policy', 'title' => '隐私说明', 'desc' => '了解平台如何处理您的订单与联系方式信息', 'target_url' => '/pages/help/index?q=%E9%9A%90%E7%A7%81', 'article_id' => 0, ], [ 'code' => 'service_notice', 'title' => '服务与通知说明', 'desc' => '了解消息提醒、工单回复与服务相关通知逻辑', 'target_url' => '/pages/help/index?q=%E6%9C%8D%E5%8A%A1', 'article_id' => 0, ], ], 'appraisal_agreements' => [ [ 'code' => 'service_agreement', 'title' => '服务协议', 'desc' => '下单前请确认服务边界、报告用途与责任说明。', 'target_url' => '/pages/help/index?q=%E6%9C%8D%E5%8A%A1', 'article_id' => 0, ], [ 'code' => 'appraisal_notice', 'title' => '鉴定须知', 'desc' => '了解资料要求、流程节点与补资料处理规则。', 'target_url' => '/pages/help/index?q=%E9%89%B4%E5%AE%9A', 'article_id' => 0, ], [ 'code' => 'privacy_policy', 'title' => '隐私政策', 'desc' => '了解平台如何处理联系方式、地址和订单信息。', 'target_url' => '/pages/help/index?q=%E9%9A%90%E7%A7%81', 'article_id' => 0, ], ], ]; } private function defaultMetaConfig(): array { return [ 'help_categories' => [ ['code' => 'all', 'title' => '全部', 'desc' => '查看全部帮助文章'], ['code' => 'service', 'title' => '服务流程', 'desc' => '了解下单、寄送、鉴定流程'], ['code' => 'report', 'title' => '报告验真', 'desc' => '了解报告查看、下载与验真'], ['code' => 'shipping', 'title' => '寄送物流', 'desc' => '了解寄送、运单和签收说明'], ['code' => 'support', 'title' => '售后支持', 'desc' => '了解补资料、工单和客服协助'], ], 'report_risk_defaults' => [ [ 'report_type' => 'appraisal', 'title' => '正式鉴定报告', 'text' => '本报告基于送检商品及当前提交资料出具。若商品状态或所附资料发生变化,报告结论可能不再适用。', ], [ 'report_type' => 'inspection', 'title' => '后台补录检查单', 'text' => '本检查单为后台补录结果,请结合扫码查看的正式页面与现场实物信息综合判断。', ], ], 'ticket_types' => [ [ 'code' => 'pre_consultation', 'title' => '鉴定前咨询', 'hint' => '适合流程、服务说明类问题', 'quick_desc' => '下单前流程、服务说明咨询', ], [ 'code' => 'order_issue', 'title' => '订单问题', 'hint' => '适合订单状态、支付、进度问题', 'quick_desc' => '进度、状态、支付相关', ], [ 'code' => 'upload_issue', 'title' => '上传问题', 'hint' => '适合拍摄上传、补图协助问题', 'quick_desc' => '拍摄、上传、补图协助', ], [ 'code' => 'report_issue', 'title' => '报告问题', 'hint' => '适合报告结论、验真、估值问题', 'quick_desc' => '结论、估值、验真咨询', ], [ 'code' => 'after_sales', 'title' => '售后问题', 'hint' => '适合服务反馈与后续处理', 'quick_desc' => '服务反馈与后续处理', ], [ 'code' => 'recheck', 'title' => '结果咨询', 'hint' => '适合咨询报告结论或补充说明', 'quick_desc' => '报告结论与说明咨询', ], ], 'ticket_statuses' => [ ['code' => 'pending', 'title' => '待处理', 'desc' => '工单已提交,客服尚未正式开始处理。'], ['code' => 'processing', 'title' => '处理中', 'desc' => '客服正在跟进问题,您可继续补充说明或截图。'], ['code' => 'waiting_user', 'title' => '待您反馈', 'desc' => '客服需要您补充更多信息后才能继续处理。'], ['code' => 'resolved', 'title' => '已解决', 'desc' => '当前问题已处理完成,如仍有疑问可继续留言。'], ['code' => 'closed', 'title' => '已关闭', 'desc' => '工单已关闭,如需继续处理可重新发起工单。'], ], 'message_events' => [ ['event_code' => 'order_created', 'title' => '下单成功', 'desc' => '用户成功创建鉴定订单后触发。'], ['event_code' => 'supplement_required', 'title' => '待补资料', 'desc' => '鉴定师发起补资料要求后触发。'], ['event_code' => 'report_published', 'title' => '报告已出具', 'desc' => '正式报告发布成功后触发。'], ['event_code' => 'return_shipped', 'title' => '物品已寄回', 'desc' => '平台登记回寄物流后触发。'], ['event_code' => 'return_received', 'title' => '回寄商品已签收', 'desc' => '用户签收回寄物品后触发。'], ['event_code' => 'ticket_reply', 'title' => '工单有新回复', 'desc' => '客服回复用户工单后触发。'], ['event_code' => 'ticket_waiting_user', 'title' => '工单待用户反馈', 'desc' => '后台将工单状态改为待用户反馈时触发。'], ['event_code' => 'ticket_resolved', 'title' => '工单已解决', 'desc' => '后台将工单状态改为已解决时触发。'], ['event_code' => 'ticket_closed', 'title' => '工单已关闭', 'desc' => '后台将工单状态改为已关闭时触发。'], ], 'message_page_copy' => [ 'title' => '服务提醒与处理进度', 'desc' => '这里会统一展示订单流转、补资料、报告出具和工单回复等关键通知,方便您集中查看。', ], ]; } private function defaultHelpArticles(): array { return [ [ 'category' => 'service', 'title' => '实物鉴定和中检鉴定有什么区别?', 'summary' => '两种服务的核心流程一致,差异主要体现在出具机构、时效与价格上。', 'keywords' => ['实物鉴定', '中检鉴定', '服务区别'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => true, 'sort_order' => 10, 'content_blocks' => [ '实物鉴定和中检鉴定都会经过下单、填写信息、上传资料、寄送商品、鉴定和查看报告这几个核心步骤。', '两者最大的区别在于出具机构不同。实物鉴定由安心验提供标准实物鉴定服务;中检鉴定由更高规格合作机构提供服务,适合对机构资质有更高要求的场景。', '中检鉴定通常价格更高、时效也会略长一些。下单前建议先根据您的使用场景、预算和时效要求选择合适服务。', ], ], [ 'category' => 'service', 'title' => '一般多久可以出结果?', 'summary' => '标准版通常 48 小时左右,具体取决于服务类型、资料完整度和物流节点。', 'keywords' => ['时效', '出结果', '多久'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => true, 'sort_order' => 20, 'content_blocks' => [ '安心验标准版通常在 48 小时左右完成处理,中检鉴定因机构流程要求更高,时效会相对更长。', '如果您上传的资料不完整、需要补图,或者商品物流尚未签收,整体时效会顺延。', '建议您在订单详情和消息中心关注关键节点,一旦有补资料要求或报告出具通知,系统会第一时间提醒。', ], ], [ 'category' => 'report', 'title' => '报告如何验证真伪?', 'summary' => '正式报告出具后,可通过报告详情页或验真页输入编号进行验证。', 'keywords' => ['报告', '验真', '验证真伪'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => true, 'sort_order' => 30, 'content_blocks' => [ '正式报告发布后,您可以在报告中心进入报告详情,再点击“去验真”进入验真页面。', '验真页会展示报告编号、机构、商品摘要和结论摘要。请以验真页显示的结果为准。', '如果您对报告内容或验真结果仍有疑问,可以直接通过客服工单联系人工支持。', ], ], [ 'category' => 'shipping', 'title' => '商品寄出后还需要做什么?', 'summary' => '寄出商品后,请尽快回到“查看寄送”页填写快递公司和运单号。', 'keywords' => ['寄送', '运单', '物流'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => false, 'sort_order' => 40, 'content_blocks' => [ '寄出商品后,请保留寄件凭证,并尽快在订单详情或寄送页填写快递公司和运单号。', '提交运单后,订单会显示“已提交运单”,后续签收和处理节点也会继续同步。', '贵重商品建议选择可追踪快递,并在包裹内附上订单号或鉴定单号。', ], ], [ 'category' => 'support', 'title' => '收到补资料提醒后该怎么处理?', 'summary' => '收到补资料提醒后,请进入订单详情或补资料页,按要求重新上传指定资料。', 'keywords' => ['补资料', '补图', '上传'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => false, 'sort_order' => 50, 'content_blocks' => [ '如果鉴定师认为现有资料还不足以完成判断,系统会推送补资料通知到消息中心。', '您可以直接点击消息进入补资料页,按要求上传缺失资料,再提交补资料。', '提交完成后,订单会重新进入鉴定流程。如仍有疑问,也可以通过客服工单寻求协助。', ], ], [ 'category' => 'support', 'title' => '如何联系客服并查看处理进度?', 'summary' => '您可以从订单详情、验真页、“我的”页等入口发起工单,并在工单详情查看客服回复。', 'keywords' => ['客服', '工单', '处理进度'], 'updated_at' => '2026-04-21 09:00:00', 'is_recommended' => false, 'sort_order' => 60, 'content_blocks' => [ '目前用户端已支持发起工单、继续留言、查看客服回复和附件。', '客服回复后,消息中心会收到提醒,点击即可进入对应工单详情。', '如果工单状态变成“待您反馈”或“已解决”,系统也会同步推送状态通知。', ], ], ]; } private function defaultPolicyHelpArticles(): array { return [ 'privacy_policy' => [ 'category' => 'service', 'title' => '隐私政策', 'summary' => '说明平台如何处理联系方式、地址、订单等个人信息,以及相关使用边界。', 'keywords' => ['隐私政策', '个人信息', '联系方式', '地址', '订单信息'], 'is_recommended' => false, 'sort_order' => 70, 'content_blocks' => [ '平台会在您下单、填写寄回地址、提交运单和联系客服等场景中收集必要的信息,仅用于完成鉴定服务、订单履约、结果通知和售后支持。', '联系方式、地址、订单编号、商品资料和服务记录会用于鉴定流程推进、物流寄回、消息提醒和问题追踪,不会用于与本次服务无关的处理场景。', '如需了解或更正相关信息,可通过设置页、地址管理、订单详情或客服工单入口进行处理,我们会按平台规则提供协助。', ], ], 'service_notice' => [ 'category' => 'service', 'title' => '服务与通知说明', 'summary' => '说明消息提醒、工单回复、补资料和结果通知的触发方式与查看入口。', 'keywords' => ['服务通知', '消息提醒', '工单回复', '补资料', '结果通知'], 'is_recommended' => false, 'sort_order' => 80, 'content_blocks' => [ '订单创建、补资料、报告出具、物品寄回和工单回复等关键节点都会通过消息中心统一提醒,方便您集中查看当前处理进度。', '若鉴定师需要补充资料,系统会推送待补资料通知,您可直接进入订单详情或补资料页继续上传。', '若对通知内容有疑问,可通过客服工单继续追问,客服回复后也会再次触发站内提醒。', ], ], 'service_agreement' => [ 'category' => 'service', 'title' => '服务协议', 'summary' => '说明鉴定服务边界、报告用途、结果交付方式与相关责任说明。', 'keywords' => ['服务协议', '鉴定服务', '报告用途', '责任说明'], 'is_recommended' => false, 'sort_order' => 90, 'content_blocks' => [ '平台提供的是独立第三方鉴定服务,服务结果基于送检商品、提交资料及实际履约节点综合判断,并以正式页面或报告展示内容为准。', '不同服务类型在出具机构、价格、时效和交付形式上可能存在差异,下单前请结合自身需求确认所选服务方案。', '若因资料缺失、物流未签收、商品状态变化或其他需补充核验的情况影响处理进度,平台会通过补资料或消息通知继续提示您后续操作。', ], ], 'appraisal_notice' => [ 'category' => 'service', 'title' => '鉴定须知', 'summary' => '说明资料要求、流程节点、补资料处理规则与常见注意事项。', 'keywords' => ['鉴定须知', '资料要求', '流程节点', '补资料'], 'is_recommended' => false, 'sort_order' => 100, 'content_blocks' => [ '请尽量按模板上传清晰、完整的商品资料,并在寄送实物后及时填写物流信息,这会直接影响鉴定效率与处理时效。', '若当前资料不足以支持判断,鉴定师会发起补资料要求,订单会暂停在待补资料节点,待您补齐后再继续流转。', '正式报告仅对当前送检商品及本次服务资料负责,如商品状态、附件情况或所附证明材料发生变化,相关结论可能需要重新核验。', ], ], ]; } private function resolveOrCreatePolicyArticleId(string $code): int { $code = trim($code); if ($code === '') { return 0; } $definitions = $this->defaultPolicyHelpArticles(); $definition = $definitions[$code] ?? null; if (!$definition) { return 0; } $existing = Db::name(self::HELP_TABLE) ->where('title', $definition['title']) ->find(); if ($existing) { return (int)$existing['id']; } $now = date('Y-m-d H:i:s'); return (int)Db::name(self::HELP_TABLE)->insertGetId([ 'category' => $definition['category'], 'title' => $definition['title'], 'summary' => $definition['summary'], 'keywords_json' => json_encode($definition['keywords'], JSON_UNESCAPED_UNICODE), 'content_blocks_json' => json_encode($definition['content_blocks'], JSON_UNESCAPED_UNICODE), 'is_recommended' => !empty($definition['is_recommended']) ? 1 : 0, 'is_enabled' => 1, 'sort_order' => (int)$definition['sort_order'], 'created_at' => $now, 'updated_at' => $now, ]); } }