# designer-blog **Repository Path**: rainterm/designer-blog ## Basic Information - **Project Name**: designer-blog - **Description**: 前后台分离的个人博客系统:后端 Solon + MyBatis-Plus,前端 Vue 3 + Naive UI / Vite。博客业务以独立模块 designer-module-blog 接入 Designer Admin 脚手架。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-14 - **Last Updated**: 2026-10-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Designer Blog [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![JDK](https://img.shields.io/badge/JDK-25-orange.svg)](https://adoptium.net/temurin/releases/) [![Solon](https://img.shields.io/badge/Solon-4.1.0-16A34A.svg)](https://solon.noear.org/) [![Vue](https://img.shields.io/badge/Vue-3.5-42b883.svg)](https://vuejs.org/) [![Nuxt](https://img.shields.io/badge/Nuxt-4-00DC82.svg)](https://nuxt.com/) ## 简介 一套**前后台分离**的个人博客系统,基于 **[Designer Admin](https://gitee.com/rainterm/designer-admin)** 后台脚手架开发: - **博客前台**(`designer-ui/blog-web`):Nuxt 4 SSR,面向读者与搜索引擎,正文随首屏 HTML 返回; - **管理后台**(`designer-ui/admin-web/src/modules/blog`):在脚手架提供的登录、权限、日志、任务与文件存储之上管理博客内容; - **后端**:博客业务作为独立模块 `designer-module-blog` 接入,只依赖 `designer-common` / `designer-framework`,由 `designer-server` 装配。 登录、RBAC、文件存储、操作日志、定时任务等基础能力均来自脚手架,本项目**不重复实现**,也不修改脚手架既有能力。 功能范围:文章 / 分类 / 标签 / 评论 / 个人链接 / 站点设置 / 访问统计,外加 SEO 所需的 `sitemap.xml`、`robots.txt` 与真 404。 **为什么前台用 SSR** 1. **搜索引擎收录**:百度等对 JS 渲染的抓取能力弱,纯客户端渲染的文章常常长时间不被收录。 2. **分享卡片不为空**:微信 / QQ / Twitter / Facebook / Slack 的爬虫**完全不执行 JS**,只能读到首屏 HTML 里的 `og:*`;没有 SSR 就只剩一张没有标题与缩略图的白卡片。 3. **真正的 404**:slug 不存在时返回 HTTP 404,而不是「200 + 空壳」——后者会被搜索引擎当成正常页收录,浪费抓取配额。 代价是多一个 Node 常驻进程(基线约 100–200MB),因此 SSR 页面在 nginx 层缓存 60 秒,命中时完全不经过 Node。 ## 界面预览 ### 博客前台 ![image-20261020164248246](docs/images/image-20261020164248246.png) ![image-20261020164331375](docs/images/image-20261020164331375.png) ![image-20261020164352520](docs/images/image-20261020164352520.png) ### 管理后台 ![image-20261020164846637](docs/images/image-20261020164846637.png) ![image-20261020164910344](docs/images/image-20261020164910344.png) ![image-20261020164936993](docs/images/image-20261020164936993.png) ![image-20261020164956362](docs/images/image-20261020164956362.png) ![image-20261020165013478](docs/images/image-20261020165013478.png) ## 功能特性 | 模块 | 能力 | |---|---| | 内容管理 | Markdown 文章、草稿 / 发布、置顶、分类与标签、个人链接、站点设置 | | 评论 | 匿名评论、两级开关、默认待审、验证码 + 频控防刷、置顶与博主回复 | | 访问统计 | 服务端推导的埋点、五类图表、去重与配额、定期清理 | | SEO | 页面级 meta / OG / JSON-LD、`sitemap.xml`、`robots.txt`、真 404 | | 阅读数 | Redis 累加 + 定时合并回写,读路径不产生数据库写 | ### 内容管理 - **文章**:Markdown 正文(`md-editor-v3`),草稿 / 发布两态,`slug` 即前台地址 `/post/{slug}`;支持置顶,管理端可按标题 / 正文关键字与分类 / 标签 / 状态筛选,前台顶栏另有搜索(`?q=`)。 - **分类与标签**:一篇文章一个分类、多个标签(`blog_post_tag`),两者都有独立前台页 `/category/{slug}`、`/tag/{slug}`;列表页的标签查询固定 2 条,避免按篇数放大请求。 - **个人链接**:站长的社交 / 外部链接(GitHub、Gitee、邮箱等),后台维护名称、图标、排序与启停;前台页头以一行图标展示,点击新开标签页。 - **站点设置**:站点名 / 副标题 / Logo(含圆角)/ 页脚版权 / ICP 备案号 / 公安网安备案号 / 站长署名 / 保留昵称 / SEO 描述与关键词 / 主题色,一处配置前台全站生效;页脚版权、两类备案号留空则对应行不渲染。 - **阅读数**:读路径只写 Redis(`HINCRBY`),`BlogViewFlushJob` 每 30 秒合并回写 MySQL,并用 Redis 锁保证多实例只回写一次;展示值 = 库里已落库 + 未回写增量。 ### 评论 - **匿名评论**:读者无需注册;提交默认**待审**,后台审核通过才公开。 - **防刷三层**:滑块验证码(AJ-Captcha,一次性凭据)+ 分 IP 频控(`@RateLimit`:30 秒 1 条、24 小时 20 条)+ 默认待审。 - **两级开关**:站点总开关(站点设置)与单篇「允许评论」为**与**关系,都开启才可评论;关闭后已发布评论仍可读。 - **回复层级压平为 2 层**:存储上 `parent_id` 记录真实回复对象(链可任意深),展示层统一上溯到顶级评论,最多两层,第二层标出「某某 回复 某某」。 - **我的评论**:读者凭浏览器本地密钥(请求头 `X-Commenter-Key`,服务端只存 SHA-256)查看自己的待审评论、删除自己的评论。 - **后台运营**:审核(通过 / 拒绝)、批量删除、置顶、博主回复(`author_type=1`,署名固定取站点设置里的站长署名);待审批数量在侧栏菜单上以红点提示。 - **防冒充**:保留昵称可配(默认 `博主,站长,管理员,admin`),访客昵称包含其中任一词即拒绝;博主回复不接受昵称入参。 - **IP 归属地**:离线 ip2region 解析到**省级**,前台与后台均展示,不向第三方泄露访客 IP。 ### 访问统计 前台每次路由切换上报一次埋点,后台「访问统计」页聚合成图表。 - **埋点只接收路径**:IP / UA / 来源 / 设备 / 页面归属分类**全部由服务端从请求推导**,前端无法伪造统计。 - **五类图表**:PV / UV 趋势(按天补齐空缺日期)、分类访问分布、来源 Top、设备占比、访客地域分布(Top 10 省份)。 - **防刷与去重**:同访客同路径 60 秒去重、单 IP 每日 3000 条上限,超限静默丢弃。 - **日志清理**:内置任务 `blogLogTask.cleanVisitLog(90)`(默认暂停),保留 90 天。 ### SEO - `app/composables/useBlogSeo.js` 是页面级 SEO 的唯一入口:`title` / `description` / `keywords` / `og:*` / `twitter:*` / `canonical` / JSON-LD(文章页为 `BlogPosting`)。 - `canonical` 保留 `?page=`、剥掉 `?q=`:分页是不同内容必须收录,搜索词组合必须收敛。 - `server/routes/` 动态生成 `/robots.txt` 与 `/sitemap.xml`(首页 + 分类 + 标签 + 全部已发布文章),绝对地址由请求的 `Host` / `X-Forwarded-Proto` 推导,**改域名不用改代码**。 - slug / 分类不存在时抛 `BizException(NOT_FOUND)`,前台返回真正的 HTTP 404。 ## 设计 ### 技术栈 | 层 | 选型 | 版本 | 说明 | |---|---|---|---| | 运行时 | JDK | 25 | 脚手架要求 | | 后端框架 | Solon | 4.1.0 | 来自脚手架 | | ORM | MyBatis-Plus(Solon 插件) | 3.5.17 | 来自脚手架 | | 多表查询 | MyBatis-Plus-Join | 1.5.9 | 来自脚手架 | | 鉴权 | Sa-Token | 1.46.0 | 来自脚手架,后台登录与接口权限 | | 数据库 | MySQL | 8.0.13+ | 表结构依赖 8.0.13 起支持的函数索引 | | 缓存 / 会话 | Redis | 7 | 会话、验证码、限流、阅读数、公开接口缓存 | | IP 归属地 | ip2region(离线 xdb) | 2.7.0 | 不请求第三方,不泄露访客 IP | | 博客前台 | Nuxt | 4.6 | SSR 直出,页面与接口同源 | | 管理后台 | Vue + Naive UI | 3.5 / 2.45 | 来自脚手架 | | 构建 | Nitro / Vite | 随 Nuxt / 8 | 前台产物是 Node 服务;两前端构建期 minify 并预生成 `.gz`(见「前端构建压缩」) | | Markdown | md-editor-v3 | 7 | 后台编辑、前台渲染共用一套语法与样式 | | 图表 | ECharts | 5.6 | 访问统计 | > `md-editor-v3` 底层依赖 Vditor 的运行时资源(lute 引擎 / 代码高亮 / 图标 / 语言包),默认从 unpkg 动态拉取。 > 后台的 Vite 插件(`plugins/copyVditorAssets.js`)在启动 / 构建时把这些资源复制到 `public/vditor`, > 编辑器因此**不依赖外部 CDN**。 ### 目录结构 ``` designer-blog/ ├── designer-module-blog/ # 博客业务模块 │ ├── blog-api/ # ★ 对外契约(当前按需预留) │ └── blog-service/ # 文章 / 分类 / 标签 / 评论 / 链接 / 设置 / 统计 ├── designer-ui/ │ ├── blog-web/ # ★ 博客前台(Nuxt 4 SSR) │ ├── admin-web/ │ │ └── src/modules/blog/ # ★ 后台博客页面:文章 / 分类 / 标签 / 评论 / 链接 / 设置 / 统计 │ └── shared/captcha/ # ★ 前后台共用的滑块验证码源码 ├── designer-server/ │ └── src/main/resources/ │ ├── app.yml # 博客配置(验证码、限流、缓存等) │ └── ip2region.xdb # IP 归属地离线数据(约 11MB,随包) ├── sql/ │ ├── init/mysql/ │ │ ├── 00-run-init.sh # 入口:_base → system → demo → blog → upgrade │ │ └── blog/ # ★ 博客表结构与种子数据 │ └── upgrade/mysql/ # 增量升级脚本(V{版本}__{说明}.sql,幂等) ├── deploy/ # 三容器部署(server + 博客 Node + nginx) ├── docs/ # 开发规范、SSO └── designer-common/ · designer-framework/ · designer-module-system/ · designer-module-demo/ # 脚手架模块,与上游保持一致 ``` ### 文档说明 | 文档 | 说明 | | ------------------------------------ | ------------------------------------ | | [deploy/README.md](deploy/README.md) | 三容器部署、域名分流、SSR 缓存与限流 | | [docs/开发规范.md](docs/开发规范.md) | 脚手架的前后端编码约定与模块依赖方向 | ### 博客设计 #### 性能与缓存 - **SSR 页面缓存在 nginx 层**:`proxy_cache` 缓存整段 HTML,TTL 60 秒,命中时完全不走 Node;缓存键含 `$host`,404 也缓存,冷 URL 并发只回源一次。 - 公开读接口(站点设置 / 分类 / 标签 / 外链)在应用层有 1 小时缓存,写侧主动失效,后台改完立刻生效。 - 阅读数读路径只碰 Redis,30 秒合并回写,热文章不会产生行锁竞争。 - `PostCard` 关闭 Nuxt 链接预取(`:prefetch="false"`):首页 5 张卡片不值得提前发 5 次请求。 - 读路径限流全部在 nginx(页面 60r/m、`/app-api/` 120r/m、sitemap 6r/m),被拦下的请求不消耗 Node 与数据库连接。 - 静态资源在构建期压缩并预生成 `.gz`,见下方「前端构建压缩」。 #### 前端构建压缩 两个前端的生产产物都在**构建期**完成压缩,运行时与 nginx 只负责挑合适的文件下发,不在请求路径上现压。 | 项目 | 内容压缩(去换行 / 空格) | gzip 预压缩 | 下发方式 | |---|---|---|---| | 管理后台(Vite) | `build.minify: 'oxc'` 压 JS、`build.cssMinify: true` 压 CSS;`index.html` 由 `plugins/compressAssets.js` 的 `minifyHtml` 压 | `plugins/compressAssets.js` 的 `compressAssets` 对 `dist/**` 生成同名 `.gz`(`gzip -9`) | nginx `gzip_static on` | | 博客前台(Nuxt) | Vite 默认 minify(客户端)、Nitro 压服务端产物 | `nitro.compressPublicAssets` 把 `.output/public/**` 预压成 `.gz` | Nitro 运行时按 `Accept-Encoding` 直出 `.gz` | **管理后台**(`designer-ui/admin-web`): - `build.minify` / `build.cssMinify` 显式写在 `vite.config.js` 里,防止后续被改成 `false` 导致体积翻倍。 - `index.html` 是 Vite 唯一不压的产物(换行、缩进、注释原样输出,内联 `