个人博客系统源码首跑部署与二次开发指南
简介:这份个人博客系统源码面向Java Web课程设计、毕业设计及SSH2框架学习者,基于Spring、Struts2、Hibernate3与Oracle数据库实现,可用于搭建一个支持日志发布、浏览、评论和后台维护的简单博客站点。压缩包采用rar格式,共159个文件,约1.51MB,包含Java源文件、JSP页面、XML配置、class文件,以及图片、jar包、标签库、脚本和属性配置等,分别承担业务代码、页面展示、框架配置、资源支撑与部署运行等用途。系统功能覆盖首页展示、日志评论、随机验证码登录,以及后台日志查询、新增、修改、删除和分页管理,能够帮助读者对照理解SSH2分层结构、实体映射、Action跳转与数据库访问流程;对于缺少完整项目经验的学习者,也可借此熟悉从页面到持久层的调用关系。已有1164人学习,适合用来梳理模块边界、参考配置写法、排查部署问题,并在源码基础上完成二次开发或课程项目改造。
1. 个人博客系统源码下载:解压前先弄清这套代码能跑成什么样
朋友半夜发来一句「博客系统源码下载下来了,跑不起来」,我远程看了二十分钟,问题不在代码,而在他把用户端和管理端当成了一个工程。这套源码是典型的前后端分离结构:用户端、管理端两套前端加一个后端服务,靠 HTTP 接口通信,数据落在 MySQL,登录态走 JWT,图片写本地目录。它能解决的是「从零搭一个可长期维护、能自己改主题、能挂自己域名的博客」,而不是装个插件就完事的那种。适合手里有服务器、愿意读代码、打算做技术沉淀站点的人;只想发两篇文章就走的,这套东西反而重。下面按我实际跑通的顺序拆:目录怎么对、配置改哪几行、三条主链路怎么验证、哪些地方一定会出问题。
2. 源码结构与依赖对位:四个目录、三份配置、一张表看懂
拿到压缩包先别急着 install。我一般的动作是解压后只看一层目录,把每个目录的职责和它依赖的运行时对上,再决定装什么。这一步花十分钟,能省掉后面两小时的报错。
2.1 目录职责与技术栈对照
这套源码的顶层目录大致是下面这个样子,名字可能略有差异,但职责划分基本一致:
| 目录 | 职责 | 运行时/技术栈 | 能否裁掉 |
|---|---|---|---|
blog-web | 用户端,文章列表、详情、标签、评论 | Vue 3 + Vite + TypeScript | 不能,主站 |
blog-admin | 管理端,发文、分类、评论审核、用户 | Vue 3 + Vite + Element Plus | 不能,但可延迟部署 |
blog-server | 后端服务,REST 接口、鉴权、上传 | Java 17 + Spring Boot 3 + MyBatis | 不能 |
blog-sql | 建表脚本与初始数据 | MySQL 8 | 不能,首跑必需 |
deploy | Nginx 配置、启动脚本、Dockerfile | Nginx / Shell | 可以先不看 |
两个前端是独立工程,各有自己的package.json和node_modules,必须分别安装依赖、分别起 dev server。这是新手第一个卡点:在根目录敲npm install什么都不会发生,因为根目录压根没有package.json。
后端是单体 jar,启动后监听一个端口,所有接口都在这一个进程里。前后端之间只认接口,所以你可以只起后端 + 用户端,管理端等你需要发文时再起,不影响浏览。
提示:
deploy目录里的 Nginx 配置是给生产环境用的,本地开发阶段不用它,别一开始就去改。
2.2 版本对位:Java 17 和 Node 18 是两个硬门槛
看一眼源码用了什么语法,就知道版本能不能降。Spring Boot 3 要求 Java 17 起步,Vite 5 要求 Node 18 起步,这两个是硬约束,不是建议。
java -version # 必须 17+,用 8 或 11 会在编译期直接失败 node -v # 建议 18.18+ 或 20 LTS,16 装依赖会报 engines 不匹配 npm -v # 9 以上 mvn -v # 3.8+,版本太低解析不了新的 pom 结构 mysql --version # 8.0+,脚本里用了 utf8mb4_0900_ai_ci 就必须 8.0四个版本都对上之后再装依赖。注意前端用npm ci而不是npm install:
# 后端:跳过测试先打包,第一次跑不需要跑单测 cd blog-server mvn -q -DskipTests clean package # 用户端:ci 严格按 lock 文件装,避免依赖漂移导致的玄学报错 cd ../blog-web npm ci --registry=https://registry.npmmirror.com # 管理端同理 cd ../blog-admin npm ci --registry=https://registry.npmmirror.commvn -DskipTests的意义在于先确认「能不能编译过」,单测里的数据库依赖往往在首跑阶段跑不通,没必要混在一起排查。npm ci会删掉已有的node_modules重装,如果你的 lock 文件和package.json版本对不上(源码作者改过依赖没更新 lock),它会直接报错——这个报错是好事,比装出一棵错乱的依赖树强。
Maven 那边如果卡在下载,改~/.m2/settings.xml加镜像即可,这是环境问题不是代码问题,别去动 pom。
2.3 配置文件只改这几个键
后端配置在blog-server/src/main/resources/application-dev.yml,需要改的地方集中在数据源、Redis、上传目录和 JWT:
server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: blog password: 换成你自己的 data: redis: host: 127.0.0.1 port: 6379 database: 2 servlet: multipart: max-file-size: 5MB max-request-size: 20MB blog: jwt: secret: 换成至少32位的随机字符串 expire: 7200 upload: path: /data/blog/upload url-prefix: /upload几个参数值得单独说,因为它们几乎每年都有人踩:
serverTimezone=Asia/Shanghai不写,文章发布时间会整体差 8 小时,评论区会出现「来自未来」的时间戳。allowPublicKeyRetrieval=true是 MySQL 8 默认caching_sha2_password插件下的必需项,不写会报 Public Key Retrieval is not allowed,这个报错看起来很吓人,其实加一个参数就完事。database: 2是给这个项目单独分一个 Redis 库,避免和你服务器上别的项目 key 撞车——撞了之后清缓存会把别人的登录态一起清掉。multipart的两级限制要和你后面 Nginx 的client_max_body_size对齐,只改一边等于没改。jwt.secret长度不足 32 字节,HS256 签名时会抛 WeakKeyException,随便写个123456是过不去的。upload.path必须是绝对路径,且运行 jar 的那个用户对该目录有写权限。
2.4 建库、导脚本、核对表数量
SQL 脚本不能直接source进一个不存在的库:
# 先建库,字符集一次到位,不要依赖默认值 mysql -uroot -p -e "CREATE DATABASE blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;" # 再导入,注意顺序:库在前,脚本在后 mysql -uroot -p blog < blog-sql/blog.sql # 验证:表数量、初始账号、字符集 mysql -uroot -p blog -e "SHOW TABLES; SELECT id,username,status FROM sys_user;"导入后做三件事:数一下表数量和你看到的 Mapper 数量是否大致对得上(少表通常是脚本中途报错但你是用-f忽略错误跑的);确认sys_user里有初始管理员,密码是脚本里的哈希值,第一次登录后立刻改;确认库和表的字符集都是utf8mb4,混合字符集在插入 emoji 或生僻字时会整条失败。
如果 SQL 是从某个旧库导出再改的,可能带DEFINER子句,导入到没有对应账号的实例会报错,把DEFINER=xxx@xxx去掉再导。
3. 三条主链路跑通:登录鉴权、发文、评论的接口自测顺序
页面报错的信息量太低,先打接口。我的习惯是:后端起来 → curl 打通登录 → 拿到 token 打通一个需要鉴权的列表 → 再开前端。这三步任何一步失败,问题范围立刻收敛。
3.1 后端启动与登录接口自测
# 用 dev profile 起 jar,日志直接看控制台 java -jar blog-server/target/blog-server.jar --spring.profiles.active=dev # 另开一个终端,先打登录 curl -s -X POST http://127.0.0.1:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}' | tee /tmp/login.json # 从返回里抠 token,再打一个必须鉴权的接口 TOKEN=$(python3 -c "import json;print(json.load(open('/tmp/login.json'))['data']['token'])") curl -s -i "http://127.0.0.1:8080/api/admin/article?page=1&size=10" \ -H "Authorization: Bearer $TOKEN"这里的重点是先看状态码再看 body。登录返回 200 且 body 里有data.token,说明数据源、密码校验、JWT 签发这一整条通了。带 token 的那个请求如果返回 401,问题在拦截器或 token 解析;返回 403,说明 token 有效但权限不足,去查角色和权限表;返回 500,看后端堆栈,通常是某个字段映射对不上。
字段路径要按实际返回值改,data.token只是常见结构,有的源码是data.accessToken,先cat /tmp/login.json看一眼再写脚本,别照抄。
3.2 前端代理与跨域,开发期别去后端加 CORS
前端工程里配代理,比让后端开@CrossOrigin("*")干净得多:
// blog-web/vite.config.ts export default defineConfig({ server: { port: 5173, proxy: { // 接口代理,开发期靠它绕开跨域 '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true // 后端没有 /api 前缀时打开下面这行 // rewrite: (p) => p.replace(/^\/api/, '') }, // 图片静态映射也要代理,否则编辑器里图片是裂的 '/upload': { target: 'http://127.0.0.1:8080', changeOrigin: true } } } })配套的.env.development里写VITE_API_BASE_URL=/api,让 axios 的 baseURL 走相对路径。这样浏览器请求的是同源地址,CORS 根本不会触发,Cookie 的域也不会出问题。
rewrite和后端的server.servlet.context-path是二选一的关系:后端如果已经配了/api作为 context-path,你再 rewrite 去掉前缀,就会变成/api/api/auth/login,404 但不报错,纯排查噩梦。判断方法很简单——直接 curl 一下http://127.0.0.1:8080/api/auth/login,通就是后端带前缀,不通就把 rewrite 去掉试。
注意:源码里后端多半配了全局
@CrossOrigin("*"),本地图省事没问题,上线前收窄成你的域名,否则等于把接口开放给任何站点。
3.3 发文链路:上传、Markdown 渲染与状态机
发文是这套系统里涉及面最广的一条链路,串了上传、富文本、权限、写库四件事。管理端的上传调用大致是这样:
// 上传图片并把 Markdown 语法插回编辑器 const fd = new FormData() fd.append('file', file) // 字段名必须是后端约定的 file const { data } = await http.post('/api/admin/upload/image', fd, { headers: { 'Content-Type': 'multipart/form-data' } }) // data.data.url 是后端拼好的可访问地址,形如 /upload/2025/06/xxxx.png editor.insertText(``)后端在这个接口上应该做四件事,缺一个后面都会出问题:扩展名白名单(jpg/png/webp/gif,不要用 Content-Type 判断,它可以伪造);按yyyy/MM分目录存,单目录文件过万之后ls都卡;文件名用 UUID 重写,保留原始文件名会带来中文乱码和路径穿越两个隐患;返回url-prefix + 相对路径,让前端拿到就能直接渲染。
正文渲染这条线要格外小心。Markdown 转 HTML 之后直接v-html注入,等于把 XSS 大门敞开。常见做法是转完之后过一遍白名单过滤(前端 DOMPurify 或后端统一 sanitize),只放行常规标签和class,把on*事件属性和javascript:协议全部拦掉。代码高亮的样式文件是独立的 CSS,很多人渲染出来代码块没颜色,就是因为忘了在入口import 'highlight.js/styles/github.css'之类。
数据层面有两个细节容易被忽略:文章状态机要有「草稿 / 已发布」两态,发布时才写published_at,列表接口按状态过滤;slug加唯一索引,重名就追加数字后缀,否则详情页 URL 会互相覆盖。列表接口只返回摘要字段,别把正文大字段一起 select 出来,十篇长文能把接口拖到两秒以上。阅读数用 Redisincr累加再定时回写 MySQL,别每次浏览都update主表。
4. 首跑翻车排查:五个高频现象的原因与改法
下面五条是我在不同机器上反复遇到的,按「现象 → 原因 → 解决」写,遇到时直接对号入座。
4.1 启动阶段:数据源与时间
现象:启动直接退出,日志顶部Failed to configure a DataSource: 'url' attribute is not specified。
原因:多环境配置文件命名不匹配。源码里可能是application-dev.yml,而你启动时没带--spring.profiles.active=dev,主配置里又没有默认 url。
解决:启动命令显式带 profile,或者把spring.profiles.active: dev写进application.yml。别去新建一个application.yml把 dev 的内容复制一遍,那样升级源码时会覆盖丢失。
现象:文章列表时间全部差 8 小时,评论显示「3 小时前」但实际刚发。
原因:连接串缺serverTimezone,或者 JVM 时区和数据库时区不一致,LocalDateTime落库时被按 UTC 解释。
解决:连接串补serverTimezone=Asia/Shanghai,同时在启动参数里加-Duser.timezone=Asia/Shanghai。两处都设置,只设一处仍可能在容器里翻车。
4.2 鉴权与上传:401 和 413
现象:登录成功,跳转首页正常,一刷新就退回登录页。
原因:token 只存在了 Pinia/Vuex 的内存里,没落 localStorage;路由守卫在刷新后读不到 token,直接判定未登录。
解决:登录成功后localStorage.setItem('token', ...),axios 请求拦截器统一从 localStorage 取并拼Authorization: Bearer xxx,响应拦截器里遇到 401 清 token 并跳登录。三处要一起改,只改一处会出现「有时能有时不能」的玄学。
现象:上传图片报 413,或者转半天返回 500。
原因:三层限制只改了一层。Nginx 的client_max_body_size默认 1M,Spring 的multipart.max-file-size默认 1MB,前端可能还有自己的 size 校验。
解决:三处同时放大到同一个数量级,比如都按 5MB 单文件、20MB 单请求。改完必须重启后端和 reload Nginx,client_max_body_size改完不 reload 是不生效的。
4.3 部署与静态资源:刷新 404
现象:本地一切正常,部署后首页能开,点进任意文章详情再按 F5 就白屏或 404。
原因:前端用的是 history 路由,服务器收到/article/12这个路径后去找同名文件,找不到就 404。开发时 Vite 帮你兜住了,Nginx 不会。
解决:Nginx 里加try_files $uri $uri/ /index.html;,把非静态资源请求全部交给入口文件。同一类问题还有一个变体:构建产物放在子目录(比如/blog/),但vite.config.ts里的base没改,结果 JS/CSS 全部 404,页面白屏但控制台只有资源报错——这时候先看 Network 面板的资源路径前缀对不对。
现象:接口在浏览器里 404,但 curl 同一个路径是通的。
原因:生产包用的是.env.production里的VITE_API_BASE_URL,源码里可能写死成了http://localhost:8080,浏览器在用户机器上访问 localhost 当然不通。
解决:.env.production里改成/api走同域反代,重新npm run build。环境变量是构建期注入的,改完不重新构建等于没改——这一点吃过一次亏就记住了。
5. 二次开发改造:主题变量、详情页 SEO 与后台权限三个落点
源码能跑只是起点,真正要用起来,改的无非三处:长什么样、被不被搜到、谁能改。
5.1 主题改造:动 CSS 变量,别动组件内部
这套源码的主题层一般抽了一层 CSS 变量,在src/styles/theme.css或者类似的入口里:
:root { --color-primary: #2f6fed; /* 主色,按钮/链接/高亮共用 */ --color-text: #1f2328; /* 正文色 */ --color-text-secondary: #6b7280; --color-bg: #ffffff; --color-bg-soft: #f6f8fa; /* 卡片、代码块底色 */ --radius-base: 8px; --content-width: 760px; /* 正文最大宽度,改这一个值影响全站排版 */ } /* 暗色模式只覆盖变量,不动任何组件选择器 */ [data-theme='dark'] { --color-text: #e6edf3; --color-bg: #0d1117; --color-bg-soft: #161b22; }改主题只做两件事:改变量值,以及在根节点上切>-- 权限码挂在菜单表上,前端和后端共用同一套字符串 INSERT INTO sys_menu (name, path, perms, type) VALUES ('文章管理', '/article', 'article:list', 'menu'), ('删除文章', '', 'article:remove', 'button'); -- 角色与菜单的关联,改权限只需动这张表 INSERT INTO sys_role_menu (role_id, menu_id) VALUES (2, 101), (2, 102);
前端路由用meta.perms: 'article:list'控制菜单显隐,后端接口用@PreAuthorize("hasAuthority('article:remove')")做真实校验。两边用的是同一个字符串常量,改的时候一起改,否则会出现「菜单看得见但点进去 403」或者更糟的「菜单看不见但接口能打」。
6. 上线前的验证与留手:Nginx 反代、缓存头与一份可回滚的备份
上线这一步,配置本身不难,难在「改完怎么确认真的生效」。我的做法是先写一份最小的 Nginx 配置,只保留必须的几条,跑通之后再往上加缓存和安全头:
server { listen 443 ssl http2; server_name blog.example.com; root /srv/blog-web/dist; client_max_body_size 20m; # 必须和 Spring 的 multipart 对齐 # history 路由回落,缺这行刷新就 404 location / { try_files $uri $uri/ /index.html; } # 注意 proxy_pass 结尾的斜杠:有斜杠会剥掉 /api 前缀 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传目录直接由 Nginx 出,别让 Java 进程读文件流 location /upload/ { alias /data/blog/upload/; expires 30d; } # 带 hash 的静态资源可以长期缓存 location ~* \.(js|css|woff2|png|jpg|webp)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; } # 入口文件绝对不缓存,否则用户永远拿到旧版本 location = /index.html { add_header Cache-Control "no-store"; } }proxy_pass末尾那个斜杠是最容易翻车的地方:带斜杠,/api/auth/login转发到后端会变成/auth/login;不带,就是原样转发。和后端 context-path 必须配套,配错了表现为全站接口 404,而 curl 内网端口又是通的。
缓存策略的核心就一句:入口文件不缓存,带 hash 的静态资源长期缓存。这两条配反了,用户会一直看到旧页面,你改十次他都觉得没生效。
备份我不信任任何「自动」的东西,只信一个能手动跑、能看到文件大小的脚本:
#!/bin/bash # 每天凌晨 3 点跑,保留 7 天 set -e D=$(date +%F) mysqldump -uroot -p"$DB_PASS" --single-transaction blog | gzip > /backup/db-$D.sql.gz tar -czf /backup/upload-$D.tar.gz -C /data/blog upload find /backup -name "*-$D.*" -size -1k -delete # 删掉明显异常的小文件 find /backup -type f -mtime +7 -delete--single-transaction保证导出时不锁表;-size -1k那一行是后悔药,磁盘满或者权限错时导出的空文件必须自动清掉,否则七天后你发现七个备份全是 0 字节。备份文件我每周会手动gunzip -t验一次完整性,解压不出来的备份等于没有。
上线后的验证清单,我固定跑这五项:
| 检查项 | 期望结果 | 不合格说明 |
|---|---|---|
| 详情页直接 F5 | 200,内容完整 | 路由回落没配 |
| 图片地址直连 | 200,Content-Type 为图片 | upload 映射或权限错 |
| 未登录访问后台接口 | 401 | 鉴权拦截器漏配 |
| 移动端首屏 | 2.5 秒内可见正文 | 入口文件被缓存或包过大 |
| 备份文件大小 | 与库体积同量级 | 备份脚本没跑成功 |
从那以后我每次拿到一份博客源码,都先按第 2 章那张目录表核对一遍运行时版本,再按第 3 章的顺序打三个接口,最后才开浏览器——这个顺序帮我省掉的深夜排查时间,比任何调试技巧都值。希望帮到你。
本文还有配套的精品资源,点击获取