146 lines
5.4 KiB
PHP
146 lines
5.4 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
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 <admin@ywxapp.cn>
|
||
*/
|
||
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,
|
||
],
|
||
]);
|
||
}
|
||
}
|