SiteConfigurationService.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Validator;
  5. use Illuminate\Validation\ValidationException;
  6. class SiteConfigurationService
  7. {
  8. public function get(): array
  9. {
  10. return DB::transaction(fn () => $this->format(DB::table('site_settings')->where('id', 1)->sharedLock()->first()));
  11. }
  12. public function save(array $input): array
  13. {
  14. $data = Validator::make($input, [
  15. 'translations' => ['present', 'array:zh,en,vi'],
  16. 'translations.*' => ['array:content'],
  17. 'translations.*.content' => ['array', 'max:100'],
  18. 'translations.*.content.*' => ['nullable', 'string', 'max:2000'],
  19. 'domains' => ['present', 'array', 'max:100'],
  20. 'domains.*' => ['required', 'string', 'max:2048'],
  21. ], [
  22. 'translations.present' => '请提交三语言跑马灯内容', 'translations.array' => '跑马灯仅支持zh/en/vi三种语言',
  23. 'translations.*.array' => '每种语言请提交content对象', 'translations.*.content.array' => '跑马灯内容必须为数组',
  24. 'translations.*.content.max' => '每种语言的跑马灯最多100条',
  25. 'translations.*.content.*.string' => '每条跑马灯内容必须为文本',
  26. 'translations.*.content.*.max' => '每条跑马灯最多2000个字符',
  27. 'domains.present' => '请提交域名数组', 'domains.array' => '域名必须为数组', 'domains.max' => '域名最多100个',
  28. 'domains.*.required' => '域名不能为空', 'domains.*.string' => '每个域名必须为字符串', 'domains.*.max' => '每个域名最多2048个字符',
  29. ])->validate();
  30. if (array_values($data['domains']) !== $data['domains']) {
  31. throw ValidationException::withMessages(['domains' => '域名必须为顺序数组']);
  32. }
  33. $domains = [];
  34. foreach ($data['domains'] as $index => $domain) {
  35. $url = $this->domain($domain, $index);
  36. if (!in_array($url, $domains, true)) $domains[] = $url;
  37. }
  38. $translations = [];
  39. foreach (['zh', 'en', 'vi'] as $language) {
  40. $content = $data['translations'][$language]['content'] ?? [];
  41. if (!is_array($content) || array_values($content) !== $content) {
  42. throw ValidationException::withMessages(['translations.' . $language . '.content' => '跑马灯内容必须为顺序数组']);
  43. }
  44. $translations[$language] = ['content' => array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $content), static fn ($item) => $item !== ''))];
  45. }
  46. $payload = ['translations' => $translations, 'domains' => $domains];
  47. return DB::transaction(function () use ($payload) {
  48. DB::table('site_settings')->insertOrIgnore(['id' => 1, 'domains' => '[]', 'created_at' => now(), 'updated_at' => now()]);
  49. $before = $this->format(DB::table('site_settings')->where('id', 1)->lockForUpdate()->first());
  50. if ($before !== $payload) {
  51. DB::table('site_settings')->where('id', 1)->update([
  52. 'domains' => json_encode($payload['domains'], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), 'updated_at' => now()]);
  53. DB::table('site_setting_translations')->where('site_setting_id', 1)->delete();
  54. foreach ($payload['translations'] as $language => $text) {
  55. if ($text['content'] !== []) DB::table('site_setting_translations')->insert([
  56. 'site_setting_id' => 1, 'language' => $language, 'content' => $text['content'][0],
  57. 'contents' => json_encode($text['content'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)]);
  58. }
  59. OperationAuditService::record('site_configuration', 1, 'update', $before, $payload);
  60. }
  61. return $payload;
  62. }, 3);
  63. }
  64. private function format(?object $row): array
  65. {
  66. $domains = $row ? json_decode($row->domains, true, 512, JSON_THROW_ON_ERROR) : [];
  67. if (!is_array($domains) || array_values($domains) !== $domains || count(array_filter($domains, 'is_string')) !== count($domains)) {
  68. throw new \RuntimeException('站点域名配置格式异常');
  69. }
  70. $translations = ['zh' => ['content' => []], 'en' => ['content' => []], 'vi' => ['content' => []]];
  71. if ($row) foreach (DB::table('site_setting_translations')->where('site_setting_id', 1)->get(['language', 'content', 'contents']) as $text) {
  72. if (isset($translations[$text->language])) $translations[$text->language] = ['content' => $this->contents($text->contents, (string) $text->content)];
  73. }
  74. return ['translations' => $translations, 'domains' => $domains];
  75. }
  76. private function contents(?string $value, string $legacy): array
  77. {
  78. if ($value === null) return trim($legacy) === '' ? [] : [trim($legacy)];
  79. $items = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  80. if (!is_array($items) || array_values($items) !== $items || count(array_filter($items, 'is_string')) !== count($items)) {
  81. throw new \RuntimeException('跑马灯配置格式异常');
  82. }
  83. return array_values(array_filter(array_map('trim', $items), static fn ($item) => $item !== ''));
  84. }
  85. private function domain(string $value, int $index): string
  86. {
  87. $url = trim($value);
  88. if (!preg_match('~^[a-z][a-z0-9+.-]*://~i', $url)) $url = 'https://' . $url;
  89. $parts = parse_url($url);
  90. if (!filter_var($url, FILTER_VALIDATE_URL) || !$parts
  91. || !in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true)
  92. || empty($parts['host']) || isset($parts['user']) || isset($parts['pass'])
  93. || isset($parts['query']) || isset($parts['fragment'])
  94. || !in_array($parts['path'] ?? '', ['', '/'], true)
  95. || (isset($parts['port']) && $parts['port'] < 1)) {
  96. throw ValidationException::withMessages(['domains.' . $index => '域名仅支持HTTP/HTTPS站点地址,不带账号、路径、查询参数或片段']);
  97. }
  98. return strtolower($parts['scheme']) . '://' . strtolower($parts['host']) . (isset($parts['port']) ? ':' . $parts['port'] : '');
  99. }
  100. }