news 2026/10/24 1:00:49

个人博客系统源码实战:前后端分离、核心链路与部署排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
个人博客系统源码实战:前后端分离、核心链路与部署排查

简介:这份个人博客系统源码面向Java Web入门学习者与课程设计人群,基于SSH2三大框架整合Oracle数据库开发,提供SmartBlog博客的首页展示、日志评论、用户登录和后台管理等功能,可用于理解框架协作方式与博客系统模块划分。压缩包为rar格式,共159个文件、约1.51MB;其中Java源文件与class字节码对应业务实现,JSP页面承担视图展示,XML、properties及框架配置用于装配,另含GIF、PNG、CSS、JS等静态资源和少量JAR依赖。资源已有1164人学习,功能覆盖分类日志分页浏览、评论发布与显示、随机验证码登录,以及后台日志查询、新增、修改、删除和分页管理。读者可参考实体类、Action与业务实现的组织方式,查看Oracle数据库下的持久层映射和页面交互结构,并体会登录校验、评论关联和后台数据维护的常见做法,为课程设计、框架整合练习或同类博客开发提供源码参照。

1. 从一个周末的折腾说起:个人博客系统源码到底能替你省下什么

有位朋友把两年的读书笔记全放在某内容社区,某天后台改版,导出只剩一个残缺的 CSV,几百篇带代码块的文章全变成了纯文本。他找我时说了句话挺扎心:内容写了两年,最后能带走的只有一段一段往上复制。后来我帮他找了一份个人博客系统源码来部署,前后花了一个周末,换来的是一套放在自己服务器上、数据在自己数据库里的博客源码,改主题、加字段、迁服务器都不用看任何人的脸色。这类源码包适合两类人:一类是想摆脱平台束缚、又不想从零写 CRUD 的内容作者;一类是刚学完一门后端语言、需要一个有真实业务闭环(登录、文章、分类、评论、分页)来练手的开发者。它不复杂,但每一层都是真东西,读一遍比看十篇教程管用。

2. 项目结构:一套前后端分离的博客系统是怎么拼起来的

拿到源码包之后的第一件事不是急着npm install,而是先把目录和表结构翻一遍。个人博客这种业务,功能边界其实很窄:写文章、发文章、看文章、评论,外加一个后台。窄的好处是整条链路你能完整吃下来,坏处是很多源码包为了显得“完整”,塞了一堆用不上的东西,读的时候要会挑。我一般先确认三件事:前后端是不是分离的、数据模型有哪几张表、鉴权是怎么做的。这三点定下来,剩下的都是填空。

2.1 功能边界:文章、分类、标签、评论、后台

一个能跑的博客系统,最小功能集大概是这样几块。前台负责展示:首页文章列表(分页)、文章详情页(Markdown 渲染 + 代码高亮)、归档页(按年月分组)、分类页和标签页、站内搜索、评论列表与提交。后台负责写入:登录、文章的新增/编辑/删除(最好带草稿和发布两种状态)、分类和标签的维护、评论审核、以及一点基本的统计(文章数、评论数)。

这里有个容易被忽略的点:草稿态。很多练手级源码把所有文章都当已发布处理,结果你在线编辑到一半,读者刷新首页就看到了半截内容。合格的实现是post表带一个status字段,前台查询永远带WHERE status = 'published',后台列表才查全量。这一条看着小,但它把“内容可见性”这件事从业务代码里剥离到了数据层面,后面加定时发布、加私密文章都是同一套逻辑往下延。

评论部分我建议保留parent_id字段做楼中楼,即便前端暂时只渲染一层。源码包里如果没这个字段,二次开发时加它就等于改表 + 改树形组装,成本比一开始留着高得多。

2.2 技术栈与目录结构:每个目录对应一条请求链路

这类博客源码最常见的组合是 Vue 3 + Vite 做前端,Node.js + Express 做后端,MySQL 存数据,Markdown 渲染交给markdown-it,代码高亮交给highlight.js,登录态用 JWT。选这套的理由很实际:前端构建快、后端没有编译步骤、MySQL 随便一台 1 核 2G 的机器就能跑。如果你拿到的包是 Spring Boot + Vue,逻辑一模一样,只是把routes换成Controller、把service换成Service层,下面讲的链路完全通用。

目录结构大致长这样,我按“一条请求怎么走”的顺序标注了每个文件的职责:

blog/ ├── blog-server/ # 后端 │ ├── app.js # 入口:挂中间件、挂路由、监听端口 │ ├── config/index.js # 集中读取 .env,别在业务里到处 process.env │ ├── db.js # mysql2 连接池,全局一个实例 │ ├── middleware/ │ │ ├── auth.js # JWT 校验,挂在 /api/admin 前面 │ │ └── error.js # 统一异常出口,避免每个路由都写 try/catch 返回体 │ ├── routes/ │ │ ├── auth.js # POST /api/login │ │ ├── public.js # GET /api/posts、/api/posts/:slug │ │ └── admin.js # /api/admin/*,必须过 auth │ ├── service/ │ │ ├── post.js # 文章的增删改查,路由只做参数提取 │ │ └── comment.js │ ├── utils/ │ │ ├── markdown.js # md 渲染 + 高亮 │ │ └── pagination.js # 把 page/size 归一化成 limit/offset │ └── sql/init.sql # 建表 + 一条种子数据 └── blog-web/ # 前端 ├── vite.config.js # 开发期把 /api 代理到 3000 └── src/ ├── api/index.js # axios 实例,统一塞 token、统一处理 401 ├── router/index.js # 前台路由 + 后台路由 + 路由守卫 ├── views/ # Home / Post / Archive / Admin └── components/

这个分层的关键在于routes只做三件事:取参数、调 service、返回统一格式。真正的 SQL 全在service里。很多新手拿到源码后图省事,直接在路由里写 SQL,改一个字段要翻五个文件,血泪经验就是这么来的。

2.3 数据模型:五张表撑起一个能用的博客

不用去看源码里的建表语句,先记住这几张表的关系,读代码时就有地图了:

表名作用关键字段
user后台账号id、username、password_hash、role
post文章id、title、slug、content、summary、category_id、status、created_at
category分类id、name、slug、sort
tag/post_tag标签与多对多关联post_tag(post_id, tag_id)联合唯一
comment评论id、post_id、parent_id、nickname、content、status

slug是这套模型里最值得单独说一嘴的字段:它是文章详情页的 URL 标识,比如/post/why-i-write。用 slug 而不是自增 id 有两个好处,一是地址可读,二是迁移数据时 id 会变、slug 不会。代价是它必须有唯一索引,且每次保存都要处理冲突,这一块在第 4 章的保存接口里会具体写。

post.content存原文(Markdown 源码),不存渲染后的 HTML。这一点务必确认,如果源码包里存的是渲染结果,你后面想换个渲染器、想加目录锚点、想做全文检索都会很难受。渲染放在读取时做,配合页面级缓存,性能完全够用。

3. 跑起来:从空数据库到浏览器里出现第一篇博客

环境这块没什么玄学,Node 18 或 20 的 LTS 版本、MySQL 5.7 以上,基本就能跑。真正让人卡住的往往不是装依赖,而是配置项对不上、字符集不对、前端代理没生效。我按实际执行的顺序把每一步和它背后的含义写清楚,照着走一遍,中间出问题也知道该看哪。

3.1 环境准备与依赖安装

先确认三样东西的版本,版本不对后面全是白忙:

node -v # 18.x / 20.x 都行,低于 16 直接换 npm -v mysql --version # 5.7+ 或 8.0 cd blog-server && npm install cd ../blog-web && npm install

前端装依赖慢是常态,可以用npm config set registry换成国内镜像源。如果npm install报ERESOLVE could not resolve,多半是某个依赖的 peer 版本冲突,先别急着加--force,把报错里提到的那个包名记下来,去package.json里看它被谁依赖,通常会有一个明确的版本区间可以调。--force装上去看着成功,跑起来缺模块才是真的折腾。

后端这边我一般会额外确认一下package.json里的scripts,看开发命令是nodemon app.js还是node --watch app.js,这决定了你改代码后要不要手动重启。前者改完自动生效,后者在 Node 18 以上也支持热重载,两个都没有的话,自己加一个nodemon依赖最省事。

3.2 数据库初始化与配置项填写

建库、导表、建账号,三行命令解决:

# 用 root 登录,创建一个专用账号,别拿 root 跑应用 mysql -uroot -p -e "CREATE DATABASE blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -uroot -p blog < blog-server/sql/init.sql # 建一个只对 blog 库有权限的应用账号 mysql -uroot -p -e "CREATE USER 'blog'@'127.0.0.1' IDENTIFIED BY 'change_me';" mysql -uroot -p -e "GRANT ALL ON blog.* TO 'blog'@'127.0.0.1'; FLUSH PRIVILEGES;"

字符集这里必须显式写utf8mb4,不能只写utf8。前者是 4 字节,能存下 emoji 和部分生僻字,后者在很多版本里是 3 字节的别名,存 emoji 会直接报Incorrect string value。这个坑我见过太多次,表建好了才发现,改起来要动列、动连接、动表默认值,不如一开始就写对。

配置写在.env里,config/index.js只负责读它,业务代码一律不碰process.env:

# blog-server/.env PORT=3000 DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=blog DB_PASSWORD=change_me DB_NAME=blog JWT_SECRET=换成一串足够长的随机字符串 JWT_EXPIRES=7d

JWT_SECRET一定要换。源码包里通常会带一个默认值方便你开箱即用,但那意味着任何人拿到这份源码都能伪造你后台的 token。生成一个随机串:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))",粘进去就行。

3.3 启动前后端与首次登录后台

两边分别是这样起的:

# 终端 1:后端 cd blog-server npm run dev # 监听 http://127.0.0.1:3000 # 终端 2:前端 cd blog-web npm run dev # 默认 http://127.0.0.1:5173

前端开发服务器会把/api开头的请求代理到 3000 端口,配置在vite.config.js的server.proxy里。所以浏览器里你访问的是 5173,但接口请求实际打到 3000,这时候不存在跨域问题。只有当你把前端打包成静态文件、和后端分开部署时,跨域才会真正出现,处理方式在 3.4 说。

后端起来后,先用curl打一下健康检查或者列表接口,确认服务真的通了再去看页面:

curl -i http://127.0.0.1:3000/api/posts?page=1&size=10

返回 200 且data.list是个数组,说明数据库连接、SQL、序列化这条链路都没问题。返回 500 就去看后端终端的报错栈,返回 401 说明这个接口被误挂到了鉴权中间件后面——公开列表接口不该要登录。

后台账号一般由init.sql里的种子数据提供,密码是哈希后的。如果源码包里没写明默认密码,去看init.sql里user表的插入语句注释,或者用 Node 现算一个 bcrypt 哈希替换进去:

// 生成一个 bcrypt 哈希,替换 init.sql 里的 password_hash const bcrypt = require('bcryptjs') bcrypt.hash('你的新密码', 10).then(console.log)

登录成功后,token 一般存在localStorage,api/index.js里的 axios 拦截器会自动把它塞进Authorization头。第一次登录如果跳回登录页,八成是拦截器里读的 key 和存的时候写的 key 不一致,这种小问题搜一下两个字符串就能定位。

3.4 打包部署:Nginx 反代与静态资源

本地跑通只是开始,真正上线要把前端打包成静态文件交给 Nginx,后端用进程守护跑在后台:

cd blog-web && npm run build # 产物在 dist/ cd ../blog-server && npm install --production # 用 pm2 之类的进程守护启动,崩了能自动拉起

Nginx 配置有两个地方必须写对,否则就是经典的白屏和 404:

server { listen 80; server_name your-domain.com; root /var/www/blog-web/dist; index index.html; location / { # 前端用的是 history 路由,刷新 /post/xxx 时磁盘上没有这个文件 # 必须回退到 index.html,交给前端路由处理 try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

try_files那一行是前端路由的保命符。少了它,首页能打开,点进文章详情再刷新就是 404,很多人以为是后端接口挂了,其实是 Nginx 在磁盘上找不到/post/xxx这个路径。另外proxy_pass结尾的斜杠会影响路径拼接:写http://127.0.0.1:3000/会把/api/posts里的/api剥掉再转发;不写斜杠则原样转发。这个差异必须和后端路由前缀对上,对不上就是一片 404。

4. 核心链路拆解:一篇 Markdown 从保存到渲染发生了什么

这一章是整套源码里最值得逐行读的部分。文章是这个系统的主体,它的写入、渲染、查询三件事串起来,就是整个博客的业务主干。看懂这条主干,加字段、加接口、改主题都是在这个骨架上挂东西。

4.1 文章保存接口:校验、slug 生成与事务

保存接口最容易写糙:不校验、不做事务、slug 冲突直接 500。下面这个版本是我在实际项目里会用的写法,把几个关键点都收进去了:

// blog-server/routes/admin.js 里的文章保存 const express = require('express') const slugify = require('slugify') const db = require('../db') const auth = require('../middleware/auth') const router = express.Router() router.post('/api/admin/posts', auth, async (req, res) => { const { title, content, categoryId, tags = [], status = 'draft' } = req.body // 1. 入参校验:缺字段直接打回,别让数据库的 NOT NULL 去兜底 if (!title || !content) { return res.status(400).json({ code: 400, msg: '标题和正文不能为空' }) } if (!['draft', 'published'].includes(status)) { return res.status(400).json({ code: 400, msg: '状态值不合法' }) } // 2. slug 生成:中文标题经 slugify 会得到空串,必须兜底 let slug = slugify(title, { lower: true, strict: true }) if (!slug) slug = `post-${Date.now()}` // 3. 摘要:优先用前端传的,否则从正文剥离 Markdown 标记后截 120 字 const summary = req.body.summary || content.replace(/[#>*`\-\[\]()!]/g, '').replace(/\s+/g, ' ').slice(0, 120) const conn = await db.getConnection() try { await conn.beginTransaction() const [result] = await conn.execute( `INSERT INTO post (title, slug, content, summary, category_id, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`, [title, slug, content, summary, categoryId || null, status] ) const postId = result.insertId // 4. 标签是多对多,先删后插保证幂等(编辑时同样适用) if (tags.length) { const values = tags.map(tagId => [postId, tagId]) await conn.query('INSERT INTO post_tag (post_id, tag_id) VALUES ?', [values]) } await conn.commit() res.json({ code: 0, data: { id: postId, slug } }) } catch (e) { await conn.rollback() // slug 有唯一索引,撞了就是重复键错误,给用户明确提示 if (e.code === 'ER_DUP_ENTRY') { return res.status(409).json({ code: 409, msg: 'slug 已存在,换个标题或手动指定' }) } console.error('[post.save]', e) res.status(500).json({ code: 500, msg: '保存失败' }) } finally { conn.release() // 连接必须归还,否则连接池很快耗尽 } })

逐段说一下。第 1 段的入参校验放在最前面,是因为后面的所有逻辑都依赖title和content存在;status用白名单校验而不是随便接受字符串,能防住前端传个奇怪的值进来把草稿逻辑搞乱。第 2 段的 slug 兜底是这份源码里很关键的一处细节:slugify处理纯中文标题会返回空字符串,如果不兜底,第一次发中文文章就会插入一个空 slug,第二次再发就撞唯一索引。用时间戳兜底虽然不好看,但绝不会冲突,后续可以在后台加一个“自定义 URL”输入框来覆盖。

第 3 段的正则剥离只是粗略清洗,目的是让列表页有个像样的摘要,不需要精确。第 4 段的标签关联用INSERT ... VALUES ?批量插入,配合事务,文章和标签要么一起成功,要么一起回滚。finally里的conn.release()千万别漏,连接池默认就几十个连接,漏几次服务就卡死不响应了——这种问题在日志里看不出任何异常,只有连接数一直涨,非常像玄学。

4.2 Markdown 渲染:为什么渲染放在读取时而不是写入时

渲染器的封装很短,但两个参数决定了安全性:

// blog-server/utils/markdown.js const MarkdownIt = require('markdown-it') const hljs = require('highlight.js') const md = new MarkdownIt({ html: false, // 关键:不允许正文里夹带原始 HTML linkify: true, // 裸链接自动转 <a> breaks: true, // 单个换行渲染成 <br>,更符合中文书写习惯 highlight(str, lang) { if (lang && hljs.getLanguage(lang)) { try { const out = hljs.highlight(str, { language: lang }).value return `<pre class="hljs"><code class="language-${lang}">${out}</code></pre>` } catch (_) { /* 高亮失败就退回纯文本 */ } } return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>` } }) module.exports = (raw) => md.render(raw || '')

html: false是这套渲染配置里最重要的一行。打开它,作者就能在正文里写<script>,而博客的后台作者往往就是站长自己,一旦账号泄露,攻击者可以在文章里挂任意脚本,影响所有访客。关掉它之后,Markdown 里的 HTML 标签会被转义成文本显示,安全边界清晰得多。真有需要嵌视频、嵌图表,用自定义容器或者白名单插件单独开,别整个放开。

highlight回调里有个细节值得注意:高亮失败时必须escapeHtml再输出。因为代码块内容是用户输入的,如果直接拼进<code>,里面的<和>会破坏页面结构。这个兜底分支平时不触发,一旦触发就是救命的。

渲染放在读取时做的代价是每次请求都要跑一遍解析。实际做法是在service/post.js里给详情页加一层缓存,键用post:${slug}:${updated_at},文章更新时这个键自然失效,不需要手动清理。列表页不渲染全文,只返回summary,所以压力主要在详情页,加缓存后单机扛住日常流量毫无问题。

4.3 列表查询与分页:别让 SELECT * 拖垮首页

首页是访问量最大的页面,它的查询写法直接决定你以后要不要加机器:

-- 列表页只取列表需要的字段,正文 content 一个字都不要带 SELECT p.id, p.title, p.slug, p.summary, p.created_at, c.name AS category_name FROM post p LEFT JOIN category c ON c.id = p.category_id WHERE p.status = 'published' ORDER BY p.created_at DESC LIMIT ? OFFSET ?; -- 总数单独查,和列表分开,方便各自加缓存 SELECT COUNT(*) AS total FROM post WHERE status = 'published';

SELECT *在文章表上是很危险的习惯:content字段动辄几万字,一次查 10 条就是几百 KB 的传输,白白占带宽和内存,前端还用不上。只取需要的列之后,首页接口的响应体通常能小一个数量级。

分页参数要在入口处归一化,不能让前端传什么就用什么:

// blog-server/utils/pagination.js function normalize(query) { const page = Math.max(parseInt(query.page, 10) || 1, 1) const size = Math.min(Math.max(parseInt(query.size, 10) || 10, 1), 50) return { page, size, limit: size, offset: (page - 1) * size } } module.exports = { normalize }

size上限卡在 50 是硬性要求。不卡的话,有人传size=100000,你这一条 SQL 就会把整张表拉进内存,服务直接假死。分页还有一个常见写法问题:用OFFSET翻到很深的页会越来越慢,因为数据库要跳过前面所有行。个人博客文章量通常几百到几千,OFFSET完全够用;真到几万篇,再换成基于created_at的游标分页,改动量也就一个查询函数。

详情页和列表页都可以加一层短 TTL 的缓存,比如 60 秒。博客内容更新频率低,60 秒的延迟完全可以接受,换来的是数据库压力骤降。缓存键记得带上文章的updated_at,不然改了文章要等缓存过期才生效,调试时会以为保存没成功。

4.4 登录鉴权:JWT 中间件与前后端约定

后台的所有写接口都必须过鉴权。中间件本身很短,但错误分支要分清楚:

// blog-server/middleware/auth.js const jwt = require('jsonwebtoken') const { JWT_SECRET } = require('../config') module.exports = function auth(req, res, next) { const header = req.headers.authorization || '' const token = header.startsWith('Bearer ') ? header.slice(7) : '' if (!token) { return res.status(401).json({ code: 401, msg: '未登录' }) } try { // payload 约定为 { id, username, role } req.user = jwt.verify(token, JWT_SECRET) next() } catch (e) { // 过期和伪造要分开提示,前端才能决定是跳登录还是提示重新登录 const msg = e.name === 'TokenExpiredError' ? '登录已过期' : '登录态无效' res.status(401).json({ code: 401, msg }) } }

前端那边用 axios 拦截器统一处理 401,避免每个页面都写一遍判断:

// blog-web/src/api/index.js import axios from 'axios' const api = axios.create({ baseURL: '/api', timeout: 10000 }) api.interceptors.request.use(cfg => { const token = localStorage.getItem('blog_token') if (token) cfg.headers.Authorization = `Bearer ${token}` return cfg }) api.interceptors.response.use( res => res.data, err => { if (err.response?.status === 401) { localStorage.removeItem('blog_token') // 用 replace 而不是 push,避免用户点返回又回到需要登录的页面 location.replace('/admin/login') } return Promise.reject(err) } ) export default api

这套约定的核心是:后端只负责判断 token 能不能用,前端只负责根据 401 决定去哪。不要把权限判断散落在各个页面里,否则新增一个后台页面就可能忘加守卫。路由守卫在router/index.js里统一配一次,meta.requiresAuth标记哪些路由需要登录,进入前检查 token 是否存在即可。注意前端守卫只是体验优化,真正的安全边界永远在后端的auth中间件上,两者缺一不可。

5. 上线排查:五个把我卡到半夜的坑

下面这几条都是我在部署这类博客系统时真实遇到过的,按“现象 → 原因 → 解决”整理,遇到问题时可以直接对照着查。

5.1 启动阶段:端口占用、模块缺失与 Node 版本

现象:npm run dev报EADDRINUSE,或者起来后立刻退出、日志里只有一行Cannot find module 'xxx'。

原因:端口被上一次没杀干净的进程占着;模块缺失多半是依赖装在了错误的目录,比如在仓库根目录执行了npm install,而package.json其实在后端子目录里。Node 版本过低则会在启动时抛语法错误,比如用了顶层await或较新的 API,报错信息往往指向一个看起来完全正常的文件。

解决:查端口用lsof -i:3000找到 PID 后kill -9,或者干脆在.env里把PORT改成 3001。模块缺失先确认当前工作目录下有node_modules,没有就cd到正确目录重新装。版本问题用nvm use 20切一下,再rm -rf node_modules package-lock.json && npm install重装一遍,比逐个排查依赖省时间。

5.2 数据阶段:字符集、时区与分页总数对不上

现象:插入带 emoji 的文章报Incorrect string value;文章发布时间比实际早了或晚了 8 小时;分页显示“共 100 篇”但翻到最后一页是空的。

原因:字符集只写了utf8(3 字节);数据库连接没有指定时区,Node 驱动按 UTC 处理DATETIME;总数查询和列表查询用了不同的WHERE条件,比如总数忘了加status = 'published'。

解决:库、表、列三处都确认是utf8mb4,连接池配置里加charset: 'utf8mb4'和timezone: '+08:00'。总数和列表的WHERE子句抽成同一个buildWhere()函数,两处共用,从根上避免条件不一致。这两个问题都属于“看起来是玄学,其实是配置”的典型,查一遍配置比读十遍业务代码有用。

5.3 部署阶段:刷新 404、跨域报错与静态资源 404

现象:本地一切正常,部署后首页能开,点进文章详情刷新就 404;或者接口返回CORS policy报错;或者页面能开但样式全丢。

原因:Nginx 少了try_files回退,history 路由在服务端找不到对应文件;前后端不同域又没有配跨域头;root指向的目录和npm run build的产物目录不一致,或者assets的公共路径配置成了绝对路径但部署在子目录下。

解决:location /里补上try_files $uri $uri/ /index.html;。跨域优先用 Nginx 反代把前后端放到同一个域下,从根上消除跨域,比在后端加一堆 CORS 头干净。静态资源 404 就打开浏览器网络面板,看请求的实际路径和服务器上的文件路径差在哪,通常是多了一层或少了一层目录,改root或构建配置里的base即可。

5.4 安全配置:默认密钥与后台路径裸奔

现象:部署上线几天后,后台出现莫名其妙的草稿文章,或者user表里多了一个不认识的账号。

原因:.env里的JWT_SECRET还是源码包自带的默认值,任何人拿到这份源码都能签出一个合法 token;后台入口路径是/admin,被扫描器扫到后直接撞库;数据库账号用了 root,或者允许远程任意 IP 连接。

解决:上线前强制换掉JWT_SECRET,换成一串 32 字节以上的随机值;后台登录接口加频率限制(比如同一 IP 每分钟 5 次),Nginx 层用limit_req就能做;数据库账号只授予blog库的权限,绑定127.0.0.1,不开放公网。这几条都不是“高级安全”,而是上线的最低门槛,漏一条就等于把门虚掩着。

5.5 性能排查:首页加载慢到底是哪一环慢

现象:首页要三四秒才出来,服务器 CPU 和内存都不高,看不出瓶颈。

原因:常见有三处。一是列表查询带了content字段导致传输量大;二是每篇文章的标签是循环里单独查一次,10 篇文章就是 11 次查询;三是前端把整份 Markdown 渲染库和全部高亮语言包都打进了首屏 bundle。

解决:列表 SQL 去掉content;标签改成一次WHERE post_id IN (...)查出来再在内存里按post_id分组,查询次数从 N+1 降到 2;前端把高亮语言包按需引入,只注册常用的几种,或者把详情页的渲染组件改成异步加载。排查顺序建议先看网络面板的请求瀑布图,哪个请求耗时最长就先动哪个,别凭感觉优化。

6. 二次开发:在骨架上挂自己的东西

拿到一套能跑的博客源码之后,多数人第一件想做的事是换皮。改主题的风险最低,改src/views/Home.vue和样式变量就行,注意别把分页和列表渲染的逻辑一起删了。第二件想做的事往往是加字段,比如给文章加封面图。这时候按“表 → 接口 → 前端”的顺序走,一步都别跳:

-- 1. 先加列,给默认值和注释,方便以后的人看懂 ALTER TABLE post ADD COLUMN cover_url VARCHAR(255) DEFAULT NULL COMMENT '封面图地址' AFTER summary;
// 2. 列表和详情查询同步补字段,只加表不改接口是最常见的翻车点 const [rows] = await db.query( `SELECT id, title, slug, summary, cover_url, created_at FROM post WHERE status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?`, [limit, offset] )

保存接口的INSERT语句也要同步加列,漏了这一步,前端传了封面图也存不进去,表现是“保存成功但刷新就没了”。改造时我习惯用一张小表把影响面列清楚,避免漏改:

改造点涉及位置影响面验证方式
加封面图post表、保存接口、列表/详情 SQL、后台表单、前台卡片中新建一篇带图文章,列表和详情都能看到
换主题src/views、样式变量、index.html低首页、详情、归档三个页面走一遍
加评论审核comment.status、后台列表接口、前台查询条件中提交一条评论,前台不显示,后台通过后显示
接站内搜索新增/api/search、post表加全文索引高搜标题和正文关键词,结果按相关度排序

验证这块有个我踩过的坑:只测“新增”,不测“编辑”和“删除”。加字段时新增走通了,编辑接口的UPDATE语句忘了带上新列,改完保存还是旧值,用户以为没保存成功,反复点。所以每加一个字段,新增、编辑、详情、列表四条路径都要各走一遍,一条都不能少。

最后说个习惯。从那以后我每次从零部署一套陌生源码,都会先把.env里的密钥、数据库账号、后台路径这三样改完,再去动业务代码,并且把改动记在一个CHANGELOG.md里。看起来是多余的一步,但等到三个月后要迁服务器、要交接给别人时,这份记录能省下大量回忆成本。这套博客系统源码不大,正好适合拿来练这套流程,跑通一遍、改一个字段、换一次主题,你对“一个 Web 项目从代码到线上”的完整认知就建立起来了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 0:18:10

Android学生信息管理系统:SQLite+Java/Kotlin完整课设工程

简介&#xff1a;本资源是一份面向高校Android开发初学者与课程设计学生的完整安卓应用实战项目&#xff0c;聚焦学生信息管理系统的全流程实现&#xff0c;帮助学习者系统掌握Android Studio开发核心技能。压缩包共1426个文件&#xff0c;涵盖338个XML布局与资源文件、280个PN…

作者头像 李华
网站建设 2026/10/24 0:11:26

Spotify押注AI代理:让音乐服务成为智能体首选

当生成式AI从聊天窗口走向能替用户执行任务的智能体&#xff0c;数字服务的入口正在发生迁移。TechCrunch报道&#xff0c;Spotify联合CEO Gustav Sderstrm近日解释了公司当前的AI判断&#xff1a;AI代理的崛起&#xff0c;可能对音乐服务是个好消息。Spotify真正押注的&#x…

作者头像 李华
网站建设 2026/10/24 0:10:28

知识付费+虚拟商城:PHP全开源源码部署与二次开发详解

简介&#xff1a;这是一份2023全新彩虹晴天多功能系统源码&#xff0c;集知识付费、虚拟商城、实物商城与虚拟卡网于一身&#xff0c;面向需要快速搭建商业运营网站的开发者、站长与创业者。作者在修复更新后发布&#xff0c;随包带1800余条初始数据&#xff0c;国内外服务器、…

作者头像 李华
网站建设 2026/10/24 0:09:53

STM32+Proteus智能小车仿真:PWM调速与电机控制实战

简介&#xff1a;这份资源面向嵌入式初学者与电子工程相关专业学生&#xff0c;提供一套基于STM32F103C6与Proteus的智能小车仿真工程&#xff0c;用于在无实物硬件的情况下验证电机控制代码与电路设计。项目通过按键控制两个直流电机的正反转&#xff0c;并借助PWM调节转速&am…

作者头像 李华
网站建设 2026/10/24 0:08:08

C# WinForms计算器源码解析:从事件分发到状态机设计

简介&#xff1a;这份资源是一套基于C#与Windows Forms开发的桌面计算器完整源代码&#xff0c;面向正在学习C#窗体编程的初学者与进阶者&#xff0c;帮助读者通过一个可运行的小型项目理解GUI界面搭建、控件使用与事件驱动编程的核心思路。压缩包共56个文件&#xff0c;约1.21…

作者头像 李华
网站建设 2026/10/23 23:34:31

电力系统潮流计算通用程序:四表建模、三种解法与结果校验

简介&#xff1a;面向电力系统分析与潮流计算学习者的C工具包&#xff0c;聚焦稳态网络电压、电流与功率分布的求解&#xff0c;适合课程设计、科研复现及工程入门者参考。压缩包共46个文件、约2.23MB&#xff0c;以18个txt算例与结果数据、3个xlsx表格、2个cpp源文件和2个h头文…

作者头像 李华