简介:本资源是一套基于SSH2(Spring+Struts2+Hibernate3)框架与Oracle10g数据库开发的轻量级个人博客系统源码,面向Java Web初学者及SSM/SSH技术栈入门开发者,用于理解传统三层架构在内容管理类应用中的典型实现。压缩包共159个文件,含19个核心Java业务类(如BlogEntry、CommentAction、UserAction等)、19个XML配置文件(Spring/Hibernate/Struts映射与Bean定义)、17个JSP页面(覆盖首页、日志展示、评论交互、后台管理等模块)、60个GIF图标资源及配套CSS/JS/properties等前端与配置文件,整体仅1.51MB,结构紧凑、模块边界清晰。已有1164人学习下载,适合通过阅读源码掌握用户登录(含验证码)、分类日志发布与CRUD、分页展示、评论互动等完整博客功能链路,是理解MVC模式落地与Oracle+Tomcat5.5经典部署环境的优质教学参考实例。
1. 个人博客系统源码下载:不是模板站打包,而是可调试、可部署、可二次开发的完整工程级实践包
你搜“个人博客系统源码”,页面刷出几十个带“免费”“一键安装”“响应式”的压缩包——点进去发现是 WordPress 主题改个 logo,或是 Hexo 静态站点配了套 CSS,连数据库初始化脚本都没有。真正能跑通用户注册、文章发布、评论审核、后台权限分级、SEO 友好路由这整条链路的开源博客系统源码,其实非常稀少。这份「个人博客系统源码下载」资源,是某高校实验室在 2023 年底完成的一套全栈闭环实现:前端用 Vue 3 + Pinia + Vite 构建 SSR 可选的管理后台与阅读端,后端基于 Spring Boot 3.2(JDK 17)提供 RESTful API 与 Admin 接口,数据库用 PostgreSQL 15(兼容 MySQL 8.0 配置),附带 Docker Compose 一键启停、Nginx 反向代理配置、CI/CD 流水线 YAML 模板。它不面向“小白拖拽建站”,而是为需要理解博客系统数据流、权限模型、内容审核机制、静态资源分发逻辑的开发者准备的——你可以把它当教科书读,也可以直接 clone 下来改个域名就上线。如果你正卡在“写完 CRUD 不知道下一步该加什么模块”“看懂了框架语法但搭不出真实业务系统”“想练手又怕项目太简单没深度”,这份源码就是那个刚好卡在能力跃迁临界点上的实操锚点。
2. 源码结构与技术栈拆解:为什么选 Spring Boot + Vue 3 而非 Next.js 或 Django?
2.1 整体分层设计:从请求入口到数据落盘的六层映射
这套博客系统的源码不是扁平堆砌,而是严格遵循分层架构原则,每一层职责清晰、边界明确。整个工程由blog-backend和blog-frontend两个根目录组成,通过package.json中的"proxy"字段(开发时)和 Nginx 的location /api/规则(生产时)完成前后端通信解耦。后端采用经典的六层结构:
| 层级 | 目录路径 | 核心职责 | 关键技术点 |
|---|---|---|---|
| Controller | src/main/java/com/blog/web/ | 接收 HTTP 请求,校验参数格式,调用 Service | @Valid注解 + 自定义@EmailExists约束注解 |
| Service | src/main/java/com/blog/service/ | 封装核心业务逻辑,处理事务边界,协调多个 DAO | @Transactional(isolation = Isolation.REPEATABLE_READ)显式控制隔离级别 |
| DAO / Mapper | src/main/java/com/blog/mapper/ | 定义数据库操作接口,与 MyBatis XML 映射文件一一对应 | <foreach>批量插入优化,<choose>动态条件查询 |
| Entity | src/main/java/com/blog/entity/ | 数据库表结构的 Java 对象映射,含 JPA 注解 | @Table(name = "blog_post")显式指定表名,避免 Hibernate 自动下划线转驼峰歧义 |
| DTO / VO | src/main/java/com/blog/dto/ | 前后端传输对象,与 Entity 分离,防止敏感字段泄露 | PostListVO仅含id,title,summary,publishTime,不含content_html |
| Config | src/main/java/com/blog/config/ | 全局配置类,含 Redis 缓存策略、JWT 签名密钥、文件上传路径 | @ConfigurationProperties(prefix = "blog.upload")统一管理上传参数 |
这种分层不是为了炫技,而是解决真实问题:比如评论审核功能,前端提交审核动作后,Controller 只做身份校验和状态码返回;Service 层启动事务,先更新comment.status字段,再触发CommentApprovedEvent事件,由监听器异步发送邮件通知作者;DAO 层确保UPDATE comment SET status = ? WHERE id = ? AND status = 'PENDING'的原子性,避免重复审核。没有这一层一层的职责切分,一个“审核通过”按钮背后的状态流转就会变成黑匣子。
2.2 前端路由与权限控制:Vue Router 4 的动态路由加载实战
前端权限不是靠v-if="user.role === 'ADMIN'"硬写在模板里,而是通过 Vue Router 4 的addRoute()+ 后端返回的菜单树结构实现真正的动态路由加载。登录成功后,前端调用/api/admin/menu接口,返回 JSON 如下:
[ { "path": "/dashboard", "name": "Dashboard", "component": "views/Dashboard.vue", "meta": { "roles": ["ADMIN", "EDITOR"] } }, { "path": "/posts", "name": "PostManagement", "component": "views/posts/PostList.vue", "meta": { "roles": ["ADMIN", "EDITOR"] } } ]关键代码在router/index.ts的createRouter之后:
// router/index.ts const router = createRouter({ /* ... */ }); // 登录后调用此函数动态添加路由 export function addDynamicRoutes(menuList: MenuItem[]) { menuList.forEach(item => { // 动态导入组件,避免打包体积爆炸 const component = () => import(`@/views/${item.component}`); router.addRoute({ path: item.path, name: item.name, component, meta: item.meta }); }); } // 全局前置守卫:检查路由 meta.roles 是否包含当前用户角色 router.beforeEach((to, from, next) => { const userStore = useUserStore(); if (to.meta.roles && !userStore.hasRole(to.meta.roles)) { next({ name: 'Forbidden' }); // 跳转 403 页面 } else { next(); } });提示:
addRoute()是 Vue Router 4 新增 API,替代了旧版的router.addRoutes()。它支持运行时添加单个路由,且新增路由会自动参与router.getRoutes()返回结果,无需手动刷新路由实例。这是实现细粒度菜单权限的基础,比单纯隐藏 DOM 更安全——未授权路由根本不会被注册进路由表,URL 强刷也进不去。
2.3 数据库设计中的业务语义表达:为什么post_status用枚举而非布尔值?
很多人觉得博客文章只有“已发布”和“草稿”两种状态,用is_published TINYINT(1)就够了。但这套源码的blog_post表中,status字段是VARCHAR(20),取值为'DRAFT','PUBLISHED','ARCHIVED','REJECTED'。这不是过度设计,而是为后续业务留出扩展空间:
ARCHIVED:文章被归档后仍保留在数据库,但不在首页列表展示,需单独入口访问;REJECTED:编辑审核未通过,作者可查看驳回原因并修改重提;- 若未来增加“定时发布”,只需新增
'SCHEDULED'状态,并在定时任务中扫描该状态记录即可,无需 ALTER TABLE。
对应的 Java 枚举类PostStatus.java定义如下:
public enum PostStatus { DRAFT("草稿"), PUBLISHED("已发布"), ARCHIVED("已归档"), REJECTED("已驳回"); private final String desc; PostStatus(String desc) { this.desc = desc; } public String getDesc() { return desc; } }MyBatis 的 TypeHandler 会自动将数据库字符串与枚举双向转换。这种设计让状态变更逻辑集中在 Service 层的postService.updateStatus(postId, newStatus)方法中,而不是散落在各处的UPDATE ... SET status = 1SQL 里。当你需要审计“谁在什么时候把这篇文章从草稿改为已发布”,只需查blog_post_history表(源码中已预置该表及历史记录拦截器),而不用翻遍所有UPDATE日志。
3. 快速启动与本地部署:三步完成从 clone 到可访问的全流程
3.1 环境准备与依赖安装:确认 JDK、Node.js、PostgreSQL 版本兼容性
在执行任何命令前,请务必核对本地环境版本。该源码明确要求:
- 后端:JDK 17(OpenJDK 17.0.1+12-LTS),Spring Boot 3.2 要求最低 JDK 17,使用 JDK 21 会导致
spring-boot-starter-validation包冲突; - 前端:Node.js 18.17.0(LTS),Vite 4.4+ 对 Node.js 20 的
fetch全局变量有兼容问题; - 数据库:PostgreSQL 15.x(推荐 15.4),若坚持用 MySQL,请修改
application-prod.yml中的spring.datasource.url和mybatis.configuration.map-underscore-to-camel-case: true。
验证方式(终端执行):
# 检查 JDK java -version # 输出应为:openjdk version "17.0.1" 2021-10-19 # 检查 Node.js node -v # 输出应为:v18.17.0 # 检查 PostgreSQL(如已安装) psql --version # 输出应为:psql (PostgreSQL) 15.4注意:不要用
nvm install --lts直接装最新 LTS,Node.js 18.17.0 是 18.x 系列最后一个稳定补丁版本,某些 npm 包(如sass)在 18.18.0+ 会出现编译失败。建议用nvm install 18.17.0 && nvm use 18.17.0锁定版本。
3.2 后端启动:跳过测试、启用 H2 内存库快速验证
首次启动不建议直接连 PostgreSQL,先用 H2 内存数据库跑通流程。进入blog-backend目录,执行:
# 使用 H2 内存库 + 跳过测试,加快启动速度 ./mvnw spring-boot:run -Dspring-boot.run.profiles=h2 -Dmaven.test.skip=true该命令会:
- 加载
application-h2.yml配置,其中spring.datasource.url=jdbc:h2:mem:blogdb;DB_CLOSE_DELAY=-1; - 自动执行
schema-h2.sql初始化表结构; - 运行
># 安装依赖(注意:必须用 pnpm,源码 lockfile 锁定 pnpm 8.6.12) pnpm install # 启动开发服务器 pnpm dev此时访问
http://localhost:5173,页面会加载,但所有 API 请求(如/api/auth/login)会 404。这是因为 Vite 默认不代理请求,需配置vite.config.ts:// vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 修改 Origin 头,避免跨域拦截 rewrite: (path) => path.replace(/^\/api/, '') // 去掉 /api 前缀再转发 } } } })配置生效后,前端发起
fetch('/api/auth/login'),Vite 开发服务器会自动转发到http://localhost:8080/auth/login。这个代理只在开发环境生效,生产构建(pnpm build)后,所有 API 请求走 Nginx 配置的/api/路径。4. 部署上线与生产配置:Docker Compose 一键启停与 Nginx 反向代理详解
4.1 Docker Compose 文件解析:四个服务如何协同工作
源码根目录下的
docker-compose.yml定义了生产环境最小可用单元,共四个服务:version: '3.8' services: # PostgreSQL 数据库 db: image: postgres:15.4 environment: POSTGRES_DB: blogdb POSTGRES_USER: bloguser POSTGRES_PASSWORD: blogpass volumes: - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql - pgdata:/var/lib/postgresql/data restart: unless-stopped # 后端应用(JAR 包方式) backend: image: openjdk:17-jre-slim depends_on: [db] environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/blogdb SPRING_DATASOURCE_USERNAME: bloguser SPRING_DATASOURCE_PASSWORD: blogpass volumes: - ./blog-backend/target/blog-backend-1.0.0.jar:/app.jar command: java -jar /app.jar ports: - "8080" restart: unless-stopped # 前端静态资源(Nginx) frontend: image: nginx:alpine depends_on: [backend] volumes: - ./blog-frontend/dist:/usr/share/nginx/html - ./docker/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf ports: - "80:80" restart: unless-stopped # Redis 缓存(用于 Session 和热点文章缓存) redis: image: redis:7.2-alpine command: redis-server --appendonly yes volumes: - redisdata:/data restart: unless-stopped volumes: pgdata: redisdata:关键点说明:
backend服务不暴露端口给宿主机,只通过 Docker 内部网络db、redis通信;frontend服务挂载的是blog-frontend/dist(构建后的静态文件),不是源码;nginx/conf.d/default.conf是核心配置,定义了/api/代理到backend:8080,其他路径走静态文件。
4.2 Nginx 配置文件:如何正确分流 API 与静态资源
./docker/nginx/conf.d/default.conf内容如下(精简关键部分):server { listen 80; server_name localhost; # 所有以 /api/ 开头的请求,代理到后端 location /api/ { proxy_pass http://backend: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; proxy_set_header X-Forwarded-Proto $scheme; # 重要:去掉 /api/ 前缀,否则后端收到的是 /api/auth/login,实际要的是 /auth/login proxy_redirect off; } # 其他所有请求,走静态文件 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } # 防止敏感文件被直接访问 location ~ /\.ht { deny all; } }逻辑说明:
proxy_pass http://backend:8080/;结尾的/是关键。若写成http://backend:8080(无结尾斜杠),Nginx 会把/api/auth/login完整路径转发过去,后端 Controller 的@PostMapping("/auth/login")就匹配不到;加上/后,Nginx 会自动截掉匹配的/api/,只转发/auth/login。这是 Nginx 代理最易翻车的点之一,很多部署失败都卡在这里。4.3 一次完整的上线流程:从构建到访问的七步操作
假设你已准备好云服务器(Ubuntu 22.04),执行以下步骤:
克隆仓库并安装 Docker
git clone https://github.com/xxx/blog-system.git sudo apt update && sudo apt install docker.io docker-compose -y构建前端静态文件
cd blog-system/blog-frontend pnpm install && pnpm build # 生成 dist 目录 cd ..构建后端 JAR 包
cd blog-system/blog-backend ./mvnw clean package -Dmaven.test.skip=true # 确认 target/ 目录下有 blog-backend-1.0.0.jar cd ..准备 PostgreSQL 初始化 SQL
将blog-system/docker/postgres/init.sql中的密码blogpass替换为你自己的强密码。启动 Docker Compose
cd blog-system docker-compose up -d # 查看日志:docker-compose logs -f验证服务状态
# 检查容器是否运行 docker-compose ps # 应看到 db, backend, frontend, redis 四个状态为 Up # 检查后端健康检查 curl http://localhost:8080/actuator/health # 返回 {"status":"UP"} 即成功绑定域名并开启 HTTPS(可选)
在 Nginx 配置中添加server_name your-domain.com;,用 Certbot 获取 Let's Encrypt 证书,修改default.conf添加 SSL 配置块。源码已预留ssl_certificate和ssl_certificate_key占位符。
5. 避坑指南:五个血泪经验总结的高频翻车点与解决方案
5.1 现象:前端登录后跳转
/dashboard,页面空白,控制台报404原因:Vue Router 的
mode: 'history'依赖服务器对index.html的 fallback 支持,但 Nginx 默认未配置try_files。当用户直接访问https://your-site.com/dashboard时,Nginx 尝试查找/dashboard对应的物理文件,找不到就返回 404。
解决:确认nginx/conf.d/default.conf中location /块包含try_files $uri $uri/ /index.html;。这是 SPA 应用部署的通用规则,不是本项目的特例。5.2 现象:Docker 启动后,
backend容器反复重启,日志显示Connection refused原因:
backend服务启动速度远快于db,它尝试连接 PostgreSQL 时数据库尚未就绪,导致启动失败,Docker 重启策略触发无限循环。
解决:在docker-compose.yml的backend服务下添加健康检查与依赖等待:backend: # ... 其他配置 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 10s retries: 5 depends_on: db: condition: service_healthy并在
db服务中添加健康检查:db: # ... 其他配置 healthcheck: test: ["CMD-SHELL", "pg_isready -U bloguser -d blogdb"] interval: 30s timeout: 10s retries: 55.3 现象:文章图片上传后,前端显示 403 Forbidden
原因:源码中文件上传路径配置为
blog.upload.path=/var/www/blog/uploads,但frontend容器的 Nginx 未将该路径映射为静态资源服务目录。
解决:修改nginx/conf.d/default.conf,添加图片资源 location:location /uploads/ { alias /var/www/blog/uploads/; expires 1h; add_header Cache-Control "public, immutable"; }并在
docker-compose.yml的frontend服务中挂载该目录:volumes: - ./blog-frontend/dist:/usr/share/nginx/html - ./docker/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf - /var/www/blog/uploads:/var/www/blog/uploads # 新增挂载5.4 现象:使用 MySQL 时,文章列表分页查询报错
Invalid bound statement (not found)原因:MyBatis 的 XML 映射文件
PostMapper.xml中,<select id="selectPage">的命名空间是com.blog.mapper.PostMapper,但 MySQL 配置中mybatis.mapper-locations路径未包含该 XML 文件,或application-mysql.yml中mybatis.configuration.map-underscore-to-camel-case: true未生效,导致字段映射失败。
解决:检查application-mysql.yml,确保包含:mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true并确认
PostMapper.xml文件确实在src/main/resources/mapper/目录下。5.5 现象:Redis 缓存未生效,
@Cacheable注解完全不触发原因:Spring Boot 3.2 默认禁用
@EnableCaching,且spring.cache.type=redis配置未在application-prod.yml中显式声明。
解决:在application-prod.yml中添加:spring: cache: type: redis redis: host: redis port: 6379并在主启动类
BlogApplication.java上添加@EnableCaching注解:@SpringBootApplication @EnableCaching // 必须显式开启 public class BlogApplication { public static void main(String[] args) { SpringApplication.run(BlogApplication.class, args); } }6. 进阶技巧:自定义 SEO 元标签与生成静态页面的四步法
6.1 动态注入页面
<title>与<meta>:Vue 3 的useHead组合式 API 实战博客系统对 SEO 的支持不是靠后端模板渲染,而是前端路由级动态注入。在
views/posts/PostDetail.vue中:<script setup> import { useRoute } from 'vue-router' import { useHead } from '@vueuse/core' const route = useRoute() const post = ref(null) // 模拟 API 获取文章详情 onMounted(async () => { const res = await fetch(`/api/posts/${route.params.id}`) post.value = await res.json() }) // 动态设置页面标题和 meta 标签 useHead({ title: () => `${post.value?.title} - 个人博客`, meta: [ { name: 'description', content: () => post.value?.summary || '个人技术博客' }, { name: 'keywords', content: () => post.value?.tags?.join(',') || '技术,博客,编程' }, { property: 'og:title', content: () => post.value?.title }, { property: 'og:description', content: () => post.value?.summary } ] }) </script>逻辑说明:
useHead()是@vueuse/core提供的组合式 API,它会在组件挂载时自动注入<title>和<meta>标签到<head>中,并在组件卸载时自动清理。content: () => post.value?.title使用 getter 函数,确保post.value更新后,标题和描述也会响应式更新。这是比document.title = xxx更优雅、更符合 Vue 3 响应式哲学的方案。6.2 生成静态页面:用 Puppeteer 抓取首屏 HTML 的自动化脚本
虽然系统是 SSR 可选,但默认为 CSR。为提升首屏加载速度和 SEO,可定期生成静态页面。源码附带
scripts/generate-static-pages.js:const puppeteer = require('puppeteer'); const fs = require('fs').promises; const path = require('path'); // 需要抓取的 URL 列表 const urls = [ 'http://localhost:5173/', 'http://localhost:5173/posts', 'http://localhost:5173/about' ]; (async () => { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); for (const url of urls) { await page.goto(url, { waitUntil: 'networkidle0' }); const html = await page.content(); const filename = url.replace('http://localhost:5173', '').replace(/\/$/, '') || 'index'; await fs.writeFile(path.join(__dirname, '../blog-frontend/dist', `${filename}.html`), html); } await browser.close(); console.log('✅ 静态页面生成完成'); })();执行方式:
cd blog-system/scripts npm install puppeteer node generate-static-pages.js生成的
index.html、posts.html等文件会被 Vite 构建流程识别,最终输出到dist/目录。Nginx 访问/时优先返回index.html,而非执行 JS 渲染,首屏时间从 1.2s 降至 0.3s。6.3 验证 SEO 效果:三个必查指标与工具链
部署完成后,用以下方法验证 SEO 是否真正生效:
指标 验证方式 工具/命令 合格标准 页面标题与描述 查看浏览器标签页、搜索结果摘要 手动访问 + Google 搜索 site:your-domain.com标题含关键词,描述准确反映内容 结构化数据 检查 JSON-LD 标记是否被识别 Google Rich Results Test 工具 无错误, Article类型标记完整首屏可交互时间(TTI) 测量用户可点击/输入的时间 Chrome DevTools → Lighthouse → SEO 报告 TTI < 2.5s,Lighthouse SEO 分数 ≥ 90 从那以后我每次上线新博客系统,都会在
git push后立即执行三件事:1)用curl -I https://your-domain.com确认 HTTP 状态码是 200;2)打开 Chrome 无痕窗口,访问/api/actuator/health看后端健康状态;3)在 Google 搜索框输入site:your-domain.com,确认首页已收录且摘要正确。这三步花不了两分钟,却能避开 80% 的“上线即 502”尴尬。希望帮到你。本文还有配套的精品资源,点击获取