KioskService.php 13.2 KB
<?php
declare(strict_types=1);

namespace app\service;

use app\common\KioskDefaults;
use think\facade\Db;

class KioskService
{
    /** 知识库日期:兼容 2026/05/06、时间戳字符串等,统一为 MySQL DATE */
    protected function normalizeKnowledgeDate(mixed $d): ?string
    {
        if ($d === null || $d === '') {
            return null;
        }
        $s = is_string($d) ? trim($d) : trim((string) $d);
        if ($s === '') {
            return null;
        }
        $s = str_replace(['/', '.'], '-', $s);
        if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $s, $m)) {
            return sprintf('%04d-%02d-%02d', (int) $m[1], (int) $m[2], (int) $m[3]);
        }
        $ts = strtotime($s);

        return $ts !== false ? date('Y-m-d', $ts) : null;
    }

    /** 仅允许三种类型,避免 NOT NULL / 长度错误 */
    protected function normalizeKnowledgeType(mixed $t): string
    {
        $s = is_string($t) ? trim($t) : '';
        if (in_array($s, ['文字', '图片', '视频'], true)) {
            return $s;
        }

        return '文字';
    }

    public function ensureSeed(): void
    {
        $this->seedKioskKv();
        $this->seedKnowledge();
        $this->seedObservatoryHistory();
        $this->seedRealtimeImages();
        $this->seedObservationStatus();
        $this->seedObservationDaily();
    }

    protected function seedKioskKv(): void
    {
        $now = date('Y-m-d H:i:s');
        foreach ([
            KioskDefaults::KEY_HOME_BG      => KioskDefaults::homeBackgrounds(),
            KioskDefaults::KEY_CAROUSEL_BG  => KioskDefaults::carouselBackgrounds(),
            KioskDefaults::KEY_WELCOME      => KioskDefaults::welcome(),
            KioskDefaults::KEY_GUIDE      => KioskDefaults::guide(),
            KioskDefaults::KEY_VIDEO_SOURCE => KioskDefaults::videoSource(),
        ] as $key => $val) {
            $exists = Db::name('kiosk_kv')->where('config_key', $key)->find();
            if (!$exists) {
                Db::name('kiosk_kv')->insert([
                    'config_key'   => $key,
                    'config_value' => json_encode($val, JSON_UNESCAPED_UNICODE),
                    'updated_at'   => $now,
                ]);
            }
        }
    }

    protected function seedKnowledge(): void
    {
        if (Db::name('knowledge')->count() > 0) {
            return;
        }
        $now = date('Y-m-d H:i:s');
        $order = 0;
        foreach (KioskDefaults::knowledge() as $row) {
            Db::name('knowledge')->insert([
                'id'          => $row['id'],
                'type'        => $row['type'],
                'title'       => $row['title'],
                'content'     => $row['content'] ?? '',
                'entry_date'  => $row['date'] ?? null,
                'tags'        => json_encode($row['tags'] ?? [], JSON_UNESCAPED_UNICODE),
                'image'       => $row['image'] ?? null,
                'video_url'   => $row['videoUrl'] ?? null,
                'sort_order'  => $order++,
                'created_at'  => $now,
                'updated_at'  => $now,
            ]);
        }
    }

    protected function seedObservatoryHistory(): void
    {
        if (Db::name('observatory_history')->count() > 0) {
            return;
        }
        $order = 0;
        foreach (KioskDefaults::observatoryHistory() as $row) {
            Db::name('observatory_history')->insert([
                'kind'       => $row['kind'],
                'title'      => $row['title'],
                'summary'    => $row['summary'],
                'date_str'   => $row['date_str'],
                'thumb'      => $row['thumb'],
                'sort_order' => $order++,
            ]);
        }
    }

    protected function seedRealtimeImages(): void
    {
        if (Db::name('realtime_image')->count() > 0) {
            return;
        }
        $order = 0;
        foreach (KioskDefaults::realtimeImages() as $row) {
            Db::name('realtime_image')->insert([
                'name'       => $row['name'],
                'time_str'   => $row['time_str'],
                'telescope'  => $row['telescope'],
                'exposure'   => $row['exposure'],
                'image_url'  => $row['image_url'],
                'sort_order' => $order++,
            ]);
        }
    }

    protected function seedObservationStatus(): void
    {
        $row = Db::name('observation_status')->where('id', 1)->find();
        if ($row) {
            return;
        }
        $s = KioskDefaults::observationStatus();
        Db::name('observation_status')->insert([
            'id'             => 1,
            'weather_label'  => $s['weather_label'],
            'seeing'         => $s['seeing'],
            'transparency'   => $s['transparency'],
            'moon_phase'     => $s['moon_phase'],
        ]);
    }

    protected function seedObservationDaily(): void
    {
        if (Db::name('observation_daily')->count() > 0) {
            return;
        }
        foreach (KioskDefaults::observationDaily() as $row) {
            Db::name('observation_daily')->insert([
                'record_date'  => $row['record_date'],
                'observations' => $row['observations'],
                'quality'      => $row['quality'],
                'weather'      => $row['weather'],
            ]);
        }
    }

    public function getJsonKv(string $key, mixed $default): mixed
    {
        $row = Db::name('kiosk_kv')->where('config_key', $key)->find();
        if (!$row || $row['config_value'] === null || $row['config_value'] === '') {
            return $default;
        }
        $decoded = json_decode((string) $row['config_value'], true);
        return is_array($decoded) ? $decoded : $default;
    }

    public function setJsonKv(string $key, mixed $data): void
    {
        $now = date('Y-m-d H:i:s');
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        $exists = Db::name('kiosk_kv')->where('config_key', $key)->find();
        if ($exists) {
            Db::name('kiosk_kv')->where('config_key', $key)->update([
                'config_value' => $json,
                'updated_at'     => $now,
            ]);
        } else {
            Db::name('kiosk_kv')->insert([
                'config_key'   => $key,
                'config_value' => $json,
                'updated_at'   => $now,
            ]);
        }
    }

    public function getKnowledgeList(): array
    {
        $rows = Db::name('knowledge')->order('sort_order', 'asc')->order('entry_date', 'desc')->select()->toArray();
        $out = [];
        foreach ($rows as $r) {
            $tags = $r['tags'];
            if (is_string($tags)) {
                $tags = json_decode($tags, true) ?: [];
            }
            if (!is_array($tags)) {
                $tags = [];
            }
            $out[] = [
                'id'       => $r['id'],
                'type'     => $r['type'],
                'title'    => $r['title'],
                'content'  => $r['content'] ?? '',
                'date'     => $r['entry_date'] ? substr((string) $r['entry_date'], 0, 10) : '',
                'tags'     => $tags,
                'image'    => $r['image'] ?: null,
                'videoUrl' => $r['video_url'] ?: null,
            ];
        }
        return $out;
    }

    public function replaceKnowledge(array $entries): void
    {
        Db::startTrans();
        try {
            Db::name('knowledge')->delete(true);
            $now = date('Y-m-d H:i:s');
            $order = 0;
            foreach ($entries as $e) {
                if (empty($e['id']) || empty($e['title'])) {
                    continue;
                }
                $tags = $e['tags'] ?? [];
                if (!is_array($tags)) {
                    $tags = [];
                }
                $tags = array_values(array_filter($tags, fn ($x) => is_string($x) && $x !== ''));
                $entryDate = $this->normalizeKnowledgeDate($e['date'] ?? null);
                $type = $this->normalizeKnowledgeType($e['type'] ?? '文字');
                $image = isset($e['image']) && is_string($e['image']) && $e['image'] !== '' ? $e['image'] : null;
                $videoUrl = isset($e['videoUrl']) && is_string($e['videoUrl']) && $e['videoUrl'] !== '' ? $e['videoUrl'] : null;
                Db::name('knowledge')->insert([
                    'id'          => (string) $e['id'],
                    'type'        => $type,
                    'title'       => (string) $e['title'],
                    'content'     => (string) ($e['content'] ?? ''),
                    'entry_date'  => $entryDate,
                    'tags'        => json_encode($tags, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
                    'image'       => $image,
                    'video_url'   => $videoUrl,
                    'sort_order'  => $order++,
                    'created_at'  => $now,
                    'updated_at'  => $now,
                ]);
            }
            Db::commit();
        } catch (\Throwable $ex) {
            Db::rollback();
            throw $ex;
        }
    }

    public function getBundle(): array
    {
        $this->ensureSeed();
        $home = $this->getJsonKv(KioskDefaults::KEY_HOME_BG, KioskDefaults::homeBackgrounds());
        if (!is_array($home) || $home === []) {
            $home = KioskDefaults::homeBackgrounds();
        }
        $welcome = $this->getJsonKv(KioskDefaults::KEY_WELCOME, KioskDefaults::welcome());
        if (!is_array($welcome)) {
            $welcome = KioskDefaults::welcome();
        }
        $guide = $this->getJsonKv(KioskDefaults::KEY_GUIDE, []);
        if (!is_array($guide)) {
            $guide = [];
        }

        $carousel = $this->getJsonKv(KioskDefaults::KEY_CAROUSEL_BG, KioskDefaults::carouselBackgrounds());
        if (!is_array($carousel) || $carousel === []) {
            $carousel = KioskDefaults::carouselBackgrounds();
        }

        $video = $this->getJsonKv(KioskDefaults::KEY_VIDEO_SOURCE, KioskDefaults::videoSource());
        if (!is_array($video)) {
            $video = KioskDefaults::videoSource();
        }
        $proto = (string) ($video['protocol'] ?? 'HLS');
        if (!in_array($proto, ['HLS', 'WebRTC', 'RTSP'], true)) {
            $proto = 'HLS';
        }

        return [
            'homeBackgrounds' => array_values(array_filter($home, fn ($u) => is_string($u) && $u !== '')),
            'carouselBackgrounds' => array_values(array_filter($carousel, fn ($u) => is_string($u) && $u !== '')),
            'welcome'         => [
                'zh-CN' => (string) ($welcome['zh-CN'] ?? KioskDefaults::welcome()['zh-CN']),
                'en'    => (string) ($welcome['en'] ?? KioskDefaults::welcome()['en']),
                'bo'    => (string) ($welcome['bo'] ?? KioskDefaults::welcome()['bo']),
            ],
            'guide'           => $guide,
            'knowledge'       => $this->getKnowledgeList(),
            'videoSource'     => [
                'protocol' => $proto,
                'url'      => (string) ($video['url'] ?? ''),
                'note'     => (string) ($video['note'] ?? ''),
            ],
        ];
    }

    public function getDataDisplay(): array
    {
        $this->ensureSeed();
        $imgs = Db::name('realtime_image')->order('sort_order', 'asc')->select()->toArray();
        $realtime = [];
        foreach ($imgs as $r) {
            $realtime[] = [
                'id'         => (int) $r['id'],
                'name'       => $r['name'],
                'time'       => $r['time_str'],
                'telescope'  => $r['telescope'],
                'exposure'   => $r['exposure'],
                'image'      => $r['image_url'],
            ];
        }
        $st = Db::name('observation_status')->where('id', 1)->find();
        if (!$st) {
            $st = KioskDefaults::observationStatus();
        }
        $status = [
            'weather'       => (string) ($st['weather_label'] ?? '—'),
            'seeing'        => (string) ($st['seeing'] ?? '—'),
            'transparency'  => (string) ($st['transparency'] ?? '—'),
            'moonPhase'     => (string) ($st['moon_phase'] ?? '—'),
        ];
        $dailyRows = Db::name('observation_daily')->order('record_date', 'desc')->limit(30)->select()->toArray();
        $historical = [];
        foreach ($dailyRows as $d) {
            $historical[] = [
                'date'          => substr((string) $d['record_date'], 0, 10),
                'observations'  => (int) $d['observations'],
                'quality'       => (string) $d['quality'],
                'weather'       => (string) $d['weather'],
            ];
        }

        return [
            'realtimeImages'  => $realtime,
            'status'          => $status,
            'historicalData'  => $historical,
        ];
    }

    public function getObservatoryHistory(): array
    {
        $this->ensureSeed();
        $rows = Db::name('observatory_history')->order('sort_order', 'asc')->select()->toArray();
        $items = [];
        foreach ($rows as $r) {
            $items[] = [
                'id'      => (int) $r['id'],
                'kind'    => $r['kind'],
                'title'   => $r['title'],
                'summary' => $r['summary'],
                'date'    => $r['date_str'],
                'thumb'   => $r['thumb'],
            ];
        }
        return ['items' => $items];
    }
}