SiteConfigurationService.php 4.9 KB

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