svc = new KioskService(); } public function bundle(): Response { try { $data = $this->svc->getBundle(); return json(['code' => 0, 'data' => $data]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } /** 天文知识库专用列表(与 bundle 内 knowledge 同源,便于页面独立刷新) */ public function knowledge(): Response { try { return json(['code' => 0, 'data' => ['entries' => $this->svc->getKnowledgeList()]]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function dataDisplay(): Response { try { return json(['code' => 0, 'data' => $this->svc->getDataDisplay()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function observatoryHistory(): Response { try { return json(['code' => 0, 'data' => $this->svc->getObservatoryHistory()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function saveHomeBackgrounds(): Response { $urls = $this->request->post('urls'); if (!is_array($urls)) { return json(['code' => 400, 'msg' => 'urls must be array'], 400); } $clean = array_values(array_filter($urls, fn ($u) => is_string($u) && $u !== '')); try { $this->svc->setJsonKv(\app\common\KioskDefaults::KEY_HOME_BG, $clean); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function saveWelcome(): Response { $w = $this->request->post(); $payload = [ 'zh-CN' => (string) ($w['zh-CN'] ?? ''), 'en' => (string) ($w['en'] ?? ''), 'bo' => (string) ($w['bo'] ?? ''), ]; try { $this->svc->setJsonKv(\app\common\KioskDefaults::KEY_WELCOME, $payload); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function saveGuide(): Response { $g = $this->request->post('guide'); if (!is_array($g)) { return json(['code' => 400, 'msg' => 'guide must be object'], 400); } try { $this->svc->setJsonKv(\app\common\KioskDefaults::KEY_GUIDE, $g); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } /** 共享天文台视频源(HLS/WebRTC/RTSP + url + note) */ public function saveVideoSource(): Response { $body = $this->request->post(); $protocol = (string) ($body['protocol'] ?? 'HLS'); if (!in_array($protocol, ['HLS', 'WebRTC', 'RTSP'], true)) { return json(['code' => 400, 'msg' => 'protocol must be HLS, WebRTC or RTSP'], 400); } $payload = [ 'protocol' => $protocol, 'url' => (string) ($body['url'] ?? ''), 'note' => (string) ($body['note'] ?? ''), ]; try { $this->svc->setJsonKv(\app\common\KioskDefaults::KEY_VIDEO_SOURCE, $payload); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } /** 轮播背景 URL 列表(与首页背景分开存储) */ public function saveCarouselBackgrounds(): Response { $urls = $this->request->post('urls'); if (!is_array($urls)) { return json(['code' => 400, 'msg' => 'urls must be array'], 400); } $clean = array_values(array_filter($urls, fn ($u) => is_string($u) && $u !== '')); try { $this->svc->setJsonKv(\app\common\KioskDefaults::KEY_CAROUSEL_BG, $clean); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } public function saveKnowledge(): Response { $entries = $this->request->post('entries'); if (!is_array($entries)) { return json(['code' => 400, 'msg' => 'entries must be array'], 400); } try { $this->svc->replaceKnowledge($entries); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } /** 知识库封面 / 视频 multipart 上传,返回 `/uploads/knowledge/...` 供 sync 写入库表 */ public function uploadKnowledgeMedia(): Response { /** @var UploadedFile|null $file */ $file = $this->request->file('file'); if (!$file) { return json([ 'code' => 400, 'msg' => '未收到文件(大文件请调大 PHP post_max_size、upload_max_filesize 与 Nginx client_max_body_size)', ], 400); } $maxBytes = 2147483648; // 2GiB,约可覆盖多数 10 分钟以内压缩视频 if (!$file->isValid()) { return json([ 'code' => 400, 'msg' => '上传未成功(常见原因:超过 PHP upload_max_filesize / post_max_size)', ], 400); } if ($file->getSize() > $maxBytes) { return json(['code' => 400, 'msg' => '文件超过 2GB 上限'], 400); } $ext = strtolower($file->getOriginalExtension()); if ($ext === 'jpeg') { $ext = 'jpg'; } if ($ext === '') { $mime = strtolower($file->getOriginalMime()); $ext = match ($mime) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/quicktime' => 'mov', 'video/x-matroska' => 'mkv', default => '', }; } $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'webm', 'mov', 'mkv']; if (!in_array($ext, $allowed, true)) { return json(['code' => 400, 'msg' => '不支持的文件类型'], 400); } $dir = public_path('uploads/knowledge'); if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { return json(['code' => 500, 'msg' => '无法创建上传目录'], 500); } $name = bin2hex(random_bytes(12)) . '.' . $ext; try { $file->move($dir, $name); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } $url = '/uploads/knowledge/' . $name; return json(['code' => 0, 'msg' => 'ok', 'data' => ['url' => $url]]); } public function seedKnowledge(): Response { $entries = \app\common\KioskDefaults::knowledge(); if ($entries === []) { return json([ 'code' => 400, 'msg' => '演示知识种子已关闭,请使用 knowledge/sync 同步真实条目,勿调用本接口清空库表', ], 400); } try { $this->svc->replaceKnowledge($entries); return json(['code' => 0, 'msg' => 'ok']); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()], 500); } } }