// +---------------------------------------------------------------------- declare(strict_types=1); namespace addon\haonav\controller; use ywxapp\controller\FrontendBase; use think\facade\Cache; use addon\haonav\model\Configure as ConfigureModel; /** * 首页小组件:实时热搜榜 + 天气(服务端代理 + 缓存,规避跨域并保护第三方接口) * * @author ywxapp */ class Widget extends FrontendBase { protected $noNeedLogin = ['*']; protected $noNeedVerify = ['*']; protected function initialize() { } /** * 实时热搜榜(缓存 10 分钟) * GET /haonav/widget/hot?source=baidu|weibo|zhihu */ public function hot() { if ((int)ConfigureModel::getVal('enable_hot_api', '0') !== 1) { return $this->result->success([], '未开启'); } $source = $this->request->param('source', ConfigureModel::getVal('hot_api_source', 'baidu')); $source = in_array($source, ['baidu', 'weibo', 'zhihu'], true) ? $source : 'baidu'; $cacheKey = 'haonav_hot_' . $source; $data = Cache::get($cacheKey); if (is_array($data)) { return $this->result->success($data, '缓存'); } $map = [ 'baidu' => 'https://api.vvhan.com/api/hotlist/baiduRD', 'weibo' => 'https://api.vvhan.com/api/hotlist/wbHot', 'zhihu' => 'https://api.vvhan.com/api/hotlist/zhihuHot', ]; $list = $this->fetchHot($map[$source]); // 即便为空也短缓存,避免频繁打第三方 Cache::set($cacheKey, $list, $list ? 600 : 120); return $this->result->success($list); } /** * 天气(缓存 30 分钟) * GET /haonav/widget/weather?city=北京 */ public function weather() { if ((int)ConfigureModel::getVal('enable_weather', '0') !== 1) { return $this->result->success([], '未开启'); } $city = trim((string)$this->request->param('city', ConfigureModel::getVal('weather_city', ''))); $cacheKey = 'haonav_weather_' . ($city !== '' ? md5($city) : 'auto'); $data = Cache::get($cacheKey); if (is_array($data) && $data) { return $this->result->success($data, '缓存'); } $info = $this->fetchWeather($city); if ($info) { Cache::set($cacheKey, $info, 1800); return $this->result->success($info); } return $this->result->success([], '暂无数据'); } /** * 拉取并归一化热搜列表 -> [ {title,url,hot}, ... ] */ protected function fetchHot(string $api): array { $json = $this->httpGet($api); if (!$json) { return []; } $arr = json_decode($json, true); if (!is_array($arr)) { return []; } $rows = $arr['data'] ?? ($arr['result'] ?? []); if (!is_array($rows)) { return []; } $out = []; foreach ($rows as $r) { if (!is_array($r)) { continue; } $title = $r['title'] ?? ($r['name'] ?? ''); if ($title === '') { continue; } $out[] = [ 'title' => (string)$title, 'url' => (string)($r['url'] ?? ($r['mobil_url'] ?? '')), 'hot' => (string)($r['hot'] ?? ($r['num'] ?? '')), ]; if (count($out) >= 20) { break; } } return $out; } /** * 拉取并归一化天气 -> {city,type,low,high,tips,...} */ protected function fetchWeather(string $city) { $api = 'https://api.vvhan.com/api/weather'; if ($city !== '') { $api .= '?city=' . urlencode($city); } $json = $this->httpGet($api); if (!$json) { return false; } $arr = json_decode($json, true); if (!is_array($arr) || empty($arr['success'])) { return false; } $today = $arr['data'][0] ?? []; return [ 'city' => (string)($arr['city'] ?? $city), 'date' => (string)($today['date'] ?? ''), 'type' => (string)($today['type'] ?? ''), 'low' => (string)($today['low'] ?? ''), 'high' => (string)($today['high'] ?? ''), 'fx' => (string)($today['fengxiang'] ?? ''), 'tips' => (string)($arr['tip'] ?? ''), ]; } /** * 轻量 GET(优先 Guzzle,失败回退 file_get_contents),失败返回 '' */ protected function httpGet(string $url): string { try { if (class_exists(\GuzzleHttp\Client::class)) { $client = new \GuzzleHttp\Client(['timeout' => 5, 'verify' => false]); $resp = $client->get($url, ['headers' => ['Member-Agent' => 'Mozilla/5.0']]); return (string)$resp->getBody(); } $ctx = stream_context_create(['http' => ['timeout' => 5, 'header' => "Member-Agent: Mozilla/5.0\r\n"]]); $body = @file_get_contents($url, false, $ctx); return $body === false ? '' : (string)$body; } catch (\Throwable $e) { return ''; } } }