// +---------------------------------------------------------------------- declare(strict_types=1); namespace addon\haonav\controller\backend; use think\facade\Db; use ywxapp\model\BaseModel; use addon\haonav\model\Links as LinksModel; use addon\haonav\model\Category as CategoryModel; /** * 后台数据概览仪表盘 * * @author ywxapp */ class Dashboard extends HaonavBackend { protected $noNeedVerify = ['*']; protected function initialize() { } public function index() { if ($this->request->isAjax()) { return $this->stats(); } // 顺带为存量链接回填拼音搜索串(每次最多 300 条,幂等、失败静默) LinksModel::backfillPinyin(300); $this->view->assign('title', '数据概览'); return $this->view->fetch('dashboard/index'); } /** * 汇总统计 */ protected function stats() { // 基础计数 $summary = [ 'links' => LinksModel::count(), 'online' => LinksModel::where('status', 1)->count(), 'pending' => LinksModel::where('status', 2)->count(), 'disabled' => LinksModel::where('status', 0)->count(), 'categories' => CategoryModel::count(), 'hot' => LinksModel::where('is_hot', 1)->count(), 'recommend' => LinksModel::where('is_recommend', 1)->count(), 'clicks' => (int)LinksModel::sum('click_count'), ]; // Top 分类(按链接数) $cates = CategoryModel::field('id,title,icon')->select(); $catStat = []; foreach ($cates as $c) { $cnt = LinksModel::where('cid', $c->id)->count(); $catStat[] = ['title' => $c->title, 'icon' => $c->icon, 'count' => $cnt]; } usort($catStat, function ($a, $b) { return $b['count'] <=> $a['count']; }); $topCategories = array_slice($catStat, 0, 10); // Top 点击 $topClicks = LinksModel::field('id,title,url,click_count,cid') ->where('status', 1) ->order('click_count', 'desc') ->limit(10) ->select(); // 死链概览(status_code 已检测且非 2xx/3xx,或为 0) $deadCount = LinksModel::whereNotNull('status_code') ->where(function ($q) { $q->where('status_code', 0)->whereOr('status_code', '>=', 400); }) ->count(); $deadList = LinksModel::field('id,title,url,status_code,last_check_at') ->whereNotNull('status_code') ->where(function ($q) { $q->where('status_code', 0)->whereOr('status_code', '>=', 400); }) ->limit(20) ->select(); $lastCheckAt = (int)LinksModel::max('last_check_at'); // 分类点击热度(基于 links.click_count 聚合,零额外表) $catHeat = []; $allCates = CategoryModel::field('id,title,icon')->select(); foreach ($allCates as $c) { $catHeat[] = [ 'cid' => $c->id, 'title' => $c->title, 'icon' => $c->icon, 'clicks' => (int)LinksModel::where('cid', $c->id)->sum('click_count'), ]; } usort($catHeat, function ($a, $b) { return $b['clicks'] <=> $a['clicks']; }); // 访问时段热力图(星期×小时),取最近 30 天,按 date 归并星期;表不存在或查询失败均静默兜底 $timeHeat = []; for ($d = 0; $d < 7; $d++) { $timeHeat[$d] = array_fill(0, 24, 0); } $timeTotal = 0; $timeRange = date('Y-m-d', strtotime('-30 days')) . ' ~ ' . date('Y-m-d'); try { if (BaseModel::tableExists('wxapp_haonav_click_stats')) { $rows = Db::name('haonav_click_stats') ->where('date', '>=', date('Y-m-d', strtotime('-30 days'))) ->field('date,hour,clicks') ->select(); foreach ($rows as $r) { $w = (int)date('w', strtotime($r['date'])); // 0=周日..6=周六 $h = (int)$r['hour']; if ($w >= 0 && $w < 7 && $h >= 0 && $h < 24) { $timeHeat[$w][$h] += (int)$r['clicks']; $timeTotal += (int)$r['clicks']; } } } } catch (\Throwable $e) { // 统计查询失败不阻断概览 } return $this->result->success([ 'summary' => $summary, 'topCategories' => $topCategories, 'topClicks' => $topClicks, 'dead' => ['count' => $deadCount, 'list' => $deadList, 'last_check_at' => $lastCheckAt ? date('Y-m-d H:i', $lastCheckAt) : ''], 'heatmap' => [ 'category' => $catHeat, 'time' => $timeHeat, 'timeRange' => $timeRange, 'timeTotal' => $timeTotal, ], ]); } }