MMyBlog
← 返回博客
博客管理后台升级:仪表盘、访客地图与系统日志
13 分钟·5,074 ·36 次阅读

博客管理后台升级:仪表盘、访客地图与系统日志

博客管理后台大升级:从"能用"到"好用"

为什么要升级后台?

博客搭好之后,后台管理一直是个"能用就行"的状态——文章列表、编辑、删除,基本功能都有。但用了一段时间之后,我越来越觉得不够用:

  • 每次想看博客整体数据,得一篇篇点开文章看阅读量
  • 完全不知道读者都分布在哪些城市
  • 服务器出问题了,只能 SSH 上去看日志

这次升级就是为了解决这三个痛点。目标很明确:让后台从"能用"变成"好用"


仪表盘:一眼看清博客全貌

仪表盘主界面

数据卡片

登录后台第一眼看到的就是5个统计卡片:文章总数、分类数量、总浏览量、项目数、技能数。

这里用了一个小优化——三个数据库查询是并行执行的:

const [stats, categories, totalViews] = await Promise.all([
  getAdminStats(),
  getCategories(),
  db.blogPost.aggregate({ _sum: { viewCount: true } }),
]);

这三个查询互不依赖,串行执行要等三次数据库往返,并行只要一次。对于仪表盘这种"首屏加载"的场景,这个优化很有意义。

最近更新 + 阅读排行

页面中间是两栏布局——左边是最近更新的文章,右边是阅读排行榜。

排行榜的实现比较简单,就是按 viewCount 降序查询:

const topPosts = await db.blogPost.findMany({
  where: { status: "published" },
  orderBy: { viewCount: "desc" },
  take: 8,
});

数据库排序比内存排序快很多,而且利用了索引。前3名用金银铜徽章标识,虽然只是个颜色区分,但看到自己的文章排在前面,还是挺有成就感的。


访客地图:知道读者在哪里

这是我最想要的功能。之前只有阅读量这个数字,完全没有地理维度——读者在哪里、哪个城市最多、有没有海外访问,一概不知。地图就嵌在仪表盘里,上面那张截图的中间部分就是它。

追踪原理

整体流程分三步:

第一步:用户访问博客时,前端自动发送位置请求。

// sendBeacon 在页面关闭时也能发送,不会阻塞页面加载
if (navigator.sendBeacon) {
  navigator.sendBeacon("/api/track", blob);
}

为什么用 sendBeacon 而不是 fetch?因为用户可能快速切换页面甚至直接关闭,普通的 fetch 会被浏览器取消,sendBeacon 不会。

第二步:后端拿到 IP,调用 ip-api.com 查询地理位置,写入数据库。

const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
const res = await fetch(`http://ip-api.com/json/${ip}?lang=zh-CN`);
const { city, lat, lon } = await res.json();

await db.visitLog.create({
  data: { ip, path, city, latitude: lat, longitude: lon }
});

第三步:仪表盘地图组件从数据库读取访客数据,用 Leaflet 渲染标记。

地图选型

地图用的是 Leaflet 渲染。之前试过高德和 OpenStreetMap,都有问题——高德瓦片偶尔白块,OpenStreetMap 国内加载慢,一度换到 CartoDB 的 CDN 稳定了一阵(后来它也开始要 API Key 了,见踩坑记录)。

标记按访问次数分三种颜色:蓝色(1次)、绿色(2-5次)、橙色(5次以上)。点击标记可以看该城市的详细访问记录。


系统日志:运维不再靠 SSH

系统日志

这个功能主要是为了方便日常维护。之前服务器出问题,只能 SSH 上去手动看日志,现在在管理后台就能看到。

系统信息

页面顶部有4个卡片,实时显示服务器状态:CPU、内存、运行时间、Node.js 版本。

数据用的是 Node.js 内置的 os 模块,每30秒自动刷新:

const cpus = os.cpus();
const totalMem = os.totalmem();
const freeMem = os.freemem();

内存使用率超过80%会变红,一眼就能看出服务器是不是快要扛不住了。

日志面板

下方是终端风格的日志面板——黑底白字、等宽字体,开发者一看就懂。

日志支持三种来源:开发环境日志、PM2 日志、宝塔日志。系统会自动检测并合并显示,不用手动切换。错误日志标红,警告标黄,普通信息标绿,一眼就能找到问题。


后续迭代:简历管理与数据库管理

后台上线之后并没有停下来——用着用着就会长出新的需求。后来又加了两个大模块,一并写在这篇里。

简历管理:改简历不用再进数据库

简历管理

简历站的全部内容(个人信息、项目、技能分类、技能、成就、荣誉与资质)都存在数据库里。以前想改一条项目描述,得打开数据库客户端手动 UPDATE,心里总有点发慌。

现在后台里有一个「简历管理」页面:七个标签页分区管理,拖住圆点图标就能拖动排序,点眼睛图标控制这条内容在前台显不显示。最新上线的「荣誉与资质」模块也一样——后台增删改查,前台即时生效。

数据库管理:把表结构画出来

数据库管理

这个页面是「学习 + 运维」两用的。当初设计博客表结构的时候,表和表之间的关系全靠脑子记,时间一长就忘了谁关联谁。

现在「数据库管理」页面把全部数据表按「认证系统」「博客系统」分组画成关系图,每张表列出关键字段和关联指向,再配上建表语句、数据浏览和一键导出 Excel。查数据再也不用登服务器敲 SQL 了,点开就能看。


文章管理优化

移动端适配

之前文章列表在手机上有个问题:操作按钮(编辑、删除)是 hover 才显示的,手机没有 hover,按钮就看不见。

解决方案是移动端改用卡片布局,操作按钮始终显示在卡片底部:

@media (min-width: 768px) {
  .md\:hidden { display: none; }
  .hidden.md\:block { display: block; }
}

桌面端保持表格,移动端自动切换卡片——一个组件,两种布局。

置顶功能

给文章加了 isPinned 字段,置顶文章在所有列表中排最前面:

orderBy: [
  { isPinned: "desc" },
  { publishedAt: "desc" },
]

踩坑记录

Leaflet 地图一片灰

试了三个瓦片源才找到能用的。OpenStreetMap 国内加载慢,高德偶尔白块,最后用了 CartoDB 的 CDN 才稳定下来。教训是:国内环境选瓦片源,一定要实际测试

CartoDB 瓦片也开始要 API Key

地图稳定跑了没几个月,CartoDB 的瓦片开始大面积出现 "API KEY REQUIRED" 水印——免费瓦片源的政策说变就变。教训升级了一版:依赖免费第三方服务,就要接受它随时变卦;要么自建瓦片,要么把切换瓦片源做成一分钟就能改完的事。(这次的修复过程,下一篇更新里写。)

NEXTAUTH_SECRET 丢失

Next.js 的 standalone 模式不会自动读取 .env 文件。部署后登录页面一直报 MissingSecret,排查了半天才发现是环境变量没注入。最后在构建脚本里自动注入 dotenv 到 server.js 才解决。

地图初始化报错

Leaflet 初始化时如果 DOM 还没准备好,会报 _leaflet_pos 错误。加了个 200ms 的延迟就好了——虽然有点 hack,但在实际使用中没问题。


技术栈总结

  • 前端:Next.js + Tailwind CSS + Leaflet
  • 后端:Next.js API Routes + Prisma ORM + MySQL
  • 地图:Leaflet + 免费瓦片源(选型随政策变动,保留切换余地)
  • 部署:宝塔 PM2 + Nginx 反代

这次升级没有引入新的重型依赖,Leaflet 和 ip-api.com 都是免费的,纯 CSS 实现了排行榜图表。保持了项目的轻量性。


初稿 2026年8月26日 · 9 月更新:修复配图、新增简历管理与数据库管理模块

文章链接: