
简介这份资源是面向计算机相关专业在校学生与JavaWeb初学者的一套个人博客系统课程设计完整方案可用于期末大作业、课程设计或毕业设计参考。项目基于JavaWeb技术栈实现涵盖用户登录注册、文章发布与管理、头像修改、收藏与评论等典型博客功能代码经过实际运行测试答辩评审平均分达到96分。压缩包共128个文件约12.38MB包含17个java源文件、17个class编译文件、13个css样式、12个js脚本以及sql建库脚本、pdf报告文档和md说明文件另附jar依赖与图片字体等静态资源结构完整、层次清晰。目前已有560人学习下载。读者可据此获得一套可直接运行的博客系统源码、数据库脚本与配套文档既能对照学习JavaWeb分层开发与数据库操作思路也便于在此基础上修改扩展功能适合作为课设、毕设或项目立项演示的参考模板。1. 个人博客系统从课程设计到能写进简历的完整交付物每年期末总有一批 JavaWeb 课程设计的选题落在「个人博客系统」上。原因很直接它覆盖了用户登录注册、文章增删改查、分类标签、评论互动、分页搜索这几块 Web 开发的核心动作又不像电商那样牵扯订单、库存、支付等复杂状态机。但真正动手时多数人卡在同一个地方——代码能跑但说不清为什么这么分层SQL 建了表但字段类型和索引全凭感觉文档和报告更是最后两天拼凑出来的。这篇笔记就围绕「个人博客系统 源代码 文档说明 SQL 报告 PDF」这套完整交付物把从环境搭建、数据库设计、分层实现到文档撰写的路径拆开讲。适合正在做 JavaWeb 课程设计的学生也适合想用一个完整案例复习 Servlet/JSP/Spring Boot 的开发者。下面说的每一步都是我带过几届课设后觉得最省事、也最经得起答辩追问的做法。2. 技术选型与工程骨架Servlet/JSP 还是 Spring Boot2.1 两种路线的取舍判断课程设计最常见的两种技术栈一种是纯 Servlet JSP JDBC另一种是 Spring Boot MyBatis Thymeleaf。选哪个不取决于哪个更「高级」而取决于你的验收标准和答辩老师关注什么。如果课程大纲只讲到 Servlet 和 JSP用纯 Servlet 路线反而更稳。老师能看到你手写doGet/doPost、手动管理HttpSession、自己封装 JDBC 工具类这些是课程知识点的直接体现。缺点是代码量大一个博客系统的 CRUD 写下来DAO 层加 Servlet 层轻松超过两千行。如果课程允许或鼓励用框架Spring Boot 路线能把开发时间压缩到三分之一。spring-boot-starter-web加mybatis-spring-boot-starter再配一个application.yml项目骨架就起来了。但要注意答辩时老师很可能追问「自动配置帮你做了什么」如果答不上来反而扣分。我一般建议课程大纲覆盖到哪就用哪条路线。想稳拿分选 Servlet/JSP想省时间且能讲清原理选 Spring Boot。两条路线的数据库设计、SQL 脚本、文档结构是通用的后面几章的内容对两者都适用。2.2 用 Maven 搭出可运行的工程骨架不管选哪条路线工程结构都建议按分层来组织。下面是一个 Maven 项目的pom.xml核心依赖配置以 Spring Boot 路线为例!-- pom.xml 核心依赖Spring Boot 2.7.x MyBatis MySQL -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 层内嵌 Tomcat Spring MVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 持久层MyBatis 与 Spring Boot 整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL 驱动注意 8.x 版本驱动类名带 cj -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 模板引擎用于服务端渲染页面 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency /dependencies这段配置里三个参数值得说明。spring-boot-starter-parent的版本决定了整个依赖树的版本对齐课设环境用 2.7.x 比 3.x 稳因为 3.x 要求 JDK 17 且部分教程还没跟上。MyBatis starter 的版本要和 Spring Boot 版本匹配2.3.x 对应 Boot 2.7。MySQL 驱动 8.0.33 是 8.x 系列里兼容性较好的一个驱动类名是com.mysql.cj.jdbc.Driver不是老教程里的com.mysql.jdbc.Driver写错会直接报ClassNotFoundException。对应的application.yml数据源配置# application.yml 数据源与 MyBatis 配置 spring: datasource: url: jdbc:mysql://localhost:3306/blog_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml # XML 映射文件位置 type-aliases-package: com.example.blog.entity # 实体类别名包 configuration: map-underscore-to-camel-case: true # 下划线字段自动映射驼峰属性serverTimezoneAsia/Shanghai这个参数不加MySQL 8 会报时区错误。map-underscore-to-camel-case打开后数据库的create_time能自动映射到实体类的createTime省掉大量resultMap配置。这两个是新手最容易漏、又最容易排查半天的点。2.3 包结构怎么分才经得起追问分层不是形式主义答辩时老师看包结构就能判断你是否理解 MVC。推荐这样分com.example.blog ├── controller // 接收请求参数校验返回视图或 JSON ├── service // 业务逻辑事务边界 │ └── impl ├── mapper // 数据访问接口对应 XML ├── entity // 数据库实体与表一一对应 ├── dto // 数据传输对象用于接收前端参数 ├── vo // 视图对象用于返回给页面 └── config // 拦截器、跨域等配置entity和dto分开是关键。很多课设把前端传的参数直接塞进实体类结果注册接口把id、createTime也暴露出去被老师问「用户能不能自己传 id 覆盖」就答不上来。dto只包含前端该传的字段entity只对应数据库表职责清晰。3. 数据库设计五张表撑起一个博客系统3.1 表结构与字段类型选择个人博客系统的核心表不多五张就够用户表、文章表、分类表、标签表、评论表。下面给出建表 SQL以 MySQL 8 为例-- 用户表存储登录信息与基本资料 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码存加密后值, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 文章表博客核心内容 CREATE TABLE article ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 标题, content TEXT NOT NULL COMMENT 正文支持较长内容, user_id INT NOT NULL COMMENT 作者ID, category_id INT DEFAULT NULL COMMENT 分类ID, view_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章表; -- 分类表 CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分类表; -- 标签表 CREATE TABLE tag ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_tag_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT标签表; -- 文章标签关联表多对多 CREATE TABLE article_tag ( article_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY (article_id, tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章标签关联表; -- 评论表 CREATE TABLE comment ( id INT NOT NULL AUTO_INCREMENT, article_id INT NOT NULL COMMENT 所属文章, user_id INT NOT NULL COMMENT 评论人, content VARCHAR(500) NOT NULL COMMENT 评论内容, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_article_id (article_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;几个字段选择需要解释。content用TEXT而不是VARCHAR(5000)因为博客正文可能超过 VARCHAR 的实用长度TEXT 类型在 InnoDB 里存储更合理。password给到 100 长度是因为如果做 MD5 或 BCrypt 加密密文长度会超过原始密码留足空间。article表的user_id和category_id都建了普通索引因为列表页按作者或分类筛选是高频查询。article_tag用联合主键而不是自增 id因为关联表本身不需要独立标识联合主键天然防重复。3.2 索引与查询性能的取舍课设数据量小索引的效果看不出来但答辩时老师可能问「如果文章上万篇列表页怎么优化」。这时候能说出索引设计思路就是加分项。文章列表页最常见的查询是「按创建时间倒序分页」。如果create_time没有索引MySQL 需要全表扫描再排序。加一个索引-- 为文章列表的时间倒序查询加索引 ALTER TABLE article ADD INDEX idx_create_time (create_time);但要注意如果查询条件是「先按分类筛选再按时间排序」单列索引idx_create_time帮助有限因为 MySQL 会先走idx_category_id过滤再排序。更优的做法是建联合索引-- 联合索引分类 创建时间覆盖「分类下按时间倒序」的查询 ALTER TABLE article ADD INDEX idx_category_time (category_id, create_time);联合索引遵循最左前缀原则category_id在前、create_time在后正好匹配「where category_id ? order by create_time desc」这种查询模式。这个点在文档里写清楚比单纯贴建表语句更能体现设计能力。3.3 初始化数据与 SQL 脚本组织交付物里的 SQL 文件建议分成两个schema.sql放建表语句data.sql放初始化数据。初始化数据至少包含一个管理员账号和几条示例文章方便验收时直接登录演示。-- data.sql 初始化数据 INSERT INTO user (username, password, nickname, email) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, adminexample.com); -- 上面密码是 123456 的 MD5 值演示用实际项目应加盐 INSERT INTO category (name) VALUES (技术笔记), (生活随笔), (读书摘要); INSERT INTO article (title, content, user_id, category_id) VALUES (第一篇博客, 这是初始化示例文章内容。, 1, 1), (Servlet 学习笔记, 记录 Servlet 生命周期与请求处理流程。, 1, 1);密码存 MD5 值而不是明文这是文档里必须体现的安全意识。虽然 MD5 本身已不够安全但课设层面能说出「不存明文、用哈希」就已经到位。如果想让文档更完整可以补一句「生产环境应使用 BCrypt 加盐哈希」。4. 核心功能实现登录、文章 CRUD 与分页4.1 登录与 Session 管理登录功能是博客系统的入口也是答辩必看的地方。核心逻辑是接收用户名密码查库比对成功则写入 Session失败返回错误提示。// UserController.java 登录接口Spring Boot 路线 PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpSession session, Model model) { // 对前端传来的明文密码做 MD5与库中密文比对 String md5Pwd DigestUtils.md5DigestAsHex(password.getBytes()); User user userService.findByUsernameAndPassword(username, md5Pwd); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; // 返回登录页携带错误信息 } // 登录成功用户信息写入 Session后续拦截器据此判断登录态 session.setAttribute(loginUser, user); return redirect:/article/list; }这段代码有三个关键点。第一密码比对在服务端做 MD5前端传的是明文但传输层如果没上 HTTPS明文在网络中仍可见课设层面至少要在文档里提到这一点。第二Session 里存的是整个User对象后续页面可以直接取loginUser.nickname显示。第三登录失败返回同一个视图并带error属性而不是抛异常这样页面能友好提示。配套的登录拦截器// LoginInterceptor.java 拦截未登录请求 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { // 未登录重定向到登录页 response.sendRedirect(request.getContextPath() /login); return false; } return true; // 已登录放行 } }拦截器注册时排除登录页、注册页和静态资源路径否则登录页本身也会被拦截形成死循环。这个坑几乎每届都有人踩。4.2 文章发布与富文本处理文章发布涉及表单提交和内容存储。如果只是纯文本textarea提交即可。如果想支持富文本课设层面常用的是引入一个轻量编辑器提交 HTML 内容。// ArticleController.java 发布文章 PostMapping(/article/save) public String saveArticle(ModelAttribute ArticleDTO dto, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); Article article new Article(); article.setTitle(dto.getTitle()); article.setContent(dto.getContent()); article.setUserId(loginUser.getId()); article.setCategoryId(dto.getCategoryId()); articleService.save(article); // service 层处理事务与标签关联 return redirect:/article/list; }ArticleDTO只包含title、content、categoryId三个字段userId从 Session 取而不是前端传防止越权。articleService.save里如果涉及标签关联需要在一个事务里完成文章插入和关联表插入否则可能出现文章存了但标签没存的不一致状态。// ArticleServiceImpl.java 保存文章并关联标签 Transactional public void save(Article article) { articleMapper.insert(article); // 插入后 article.id 由主键回填 if (article.getTagIds() ! null) { for (Integer tagId : article.getTagIds()) { articleTagMapper.insert(article.getId(), tagId); } } }Transactional保证两步操作要么都成功要么都回滚。MyBatis 的useGeneratedKeystrue配置能让插入后自动回填主键到实体这样关联表才能拿到article.getId()。4.3 分页查询与列表展示分页是列表页的核心。课设层面有两种做法手动LIMIT offset, size加总数查询或用 PageHelper 插件。手动方式更能体现对 SQL 的理解推荐在文档里写清楚。-- 分页查询查当前页数据 SELECT a.id, a.title, a.view_count, a.create_time, u.nickname, c.name AS category_name FROM article a LEFT JOIN user u ON a.user_id u.id LEFT JOIN category c ON a.category_id c.id ORDER BY a.create_time DESC LIMIT #{offset}, #{pageSize}; -- 查总数用于计算总页数 SELECT COUNT(*) FROM article;offset的计算方式是(pageNum - 1) * pageSize。LEFT JOIN保证即使作者或分类被删文章仍能显示。Service 层把这两个查询结果组装成一个分页对象// PageResult.java 分页结果封装 public class PageResultT { private ListT list; // 当前页数据 private int total; // 总记录数 private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int totalPages; // 总页数由 total 和 pageSize 计算 public PageResult(ListT list, int total, int pageNum, int pageSize) { this.list list; this.total total; this.pageNum pageNum; this.pageSize pageSize; this.totalPages (total pageSize - 1) / pageSize; // 向上取整 } // getter/setter 省略 }总页数用(total pageSize - 1) / pageSize向上取整避免最后一页数据被截断。这个公式在文档里写出来比直接调插件更能说明你理解分页原理。5. 避坑与排查课设里最容易翻车的五个点5.1 中文乱码从数据库到页面的全链路排查现象文章标题存进数据库变成问号或者页面显示乱码。原因乱码可能出现在三个环节——数据库字符集、连接 URL 字符集、页面编码。MySQL 8 默认字符集是utf8mb4但建库时如果没指定可能继承成latin1。连接 URL 没加characterEncodingutf8也会导致传输乱码。JSP 页面没写% page contentTypetext/html;charsetUTF-8 %同样会乱。解决建库时显式指定CREATE DATABASE blog_db DEFAULT CHARSET utf8mb4;连接 URL 加useUnicodetruecharacterEncodingutf8JSP 页面头部声明 UTF-8。三处都对齐后乱码基本消失。如果还有问题用SHOW VARIABLES LIKE character%;检查数据库实际字符集。5.2 静态资源 404拦截器把 CSS 和 JS 也拦了现象登录页能打开但样式全丢F12 看到 CSS 和 JS 请求返回 404 或重定向到登录页。原因登录拦截器配置时只写了拦截路径/**没排除静态资源路径。浏览器请求/css/style.css时被拦截器判定为未登录重定向到登录页导致样式加载失败。解决注册拦截器时用excludePathPatterns排除静态资源目录// WebConfig.java 拦截器注册排除静态资源与登录注册页 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /css/**, /js/**, /images/**);Spring Boot 默认静态资源放在src/main/resources/static/下对应 URL 路径就是/css/**这种。排除后样式正常加载。5.3 事务不生效方法内部调用绕过了代理现象文章保存时标签关联失败但文章本身存进去了数据不一致。原因在同一个 Service 类里一个非事务方法直接调用了本类的事务方法Spring AOP 代理没生效Transactional形同虚设。解决把事务方法拆到另一个 Service 类或者通过注入自身代理调用。课设层面最简单的做法是确保Transactional标注的方法是被 Controller 直接调用的入口方法不在类内部嵌套调用。5.4 分页最后一页数据重复或丢失现象翻到最后一页数据条数和预期不符或者和上一页有重复。原因offset计算错误或者排序字段有相同值导致 MySQL 返回顺序不稳定。解决确认offset (pageNum - 1) * pageSize。排序时如果create_time有相同值加一个次级排序字段ORDER BY create_time DESC, id DESC保证顺序确定。5.5 密码明文存储与 SQL 注入现象数据库里能看到明文密码登录框输入 OR 11能绕过验证。原因密码没加密直接存SQL 用字符串拼接而不是预编译参数。解决密码存 MD5 或 BCrypt 哈希值。所有 SQL 用#{}占位符MyBatis或PreparedStatement不要用${}拼接用户输入。这两点在文档的「安全设计」小节里写清楚答辩时是明确的加分项。6. 文档与报告让交付物经得起翻看6.1 文档说明该写什么课程设计交付物里的「文档说明」不是 README 的复制粘贴而是一份能让老师快速理解项目结构、运行方式、功能清单的说明文件。建议包含这几块项目简介一段话说清做了什么、技术栈列出框架和版本、环境要求JDK、MySQL、Maven 版本、部署步骤从导入 SQL 到启动项目的完整命令、功能清单按模块列出已实现功能、目录结构说明每个包负责什么。部署步骤要写到「复制粘贴就能跑」的程度# 1. 创建数据库并导入 SQL mysql -u root -p -e CREATE DATABASE blog_db DEFAULT CHARSET utf8mb4; mysql -u root -p blog_db schema.sql mysql -u root -p blog_db data.sql # 2. 修改 application.yml 中的数据库密码 # 3. Maven 构建并启动 mvn clean package -DskipTests java -jar target/blog-0.0.1-SNAPSHOT.jar # 4. 浏览器访问 # http://localhost:8080/login 默认账号 admin / 123456这段命令里-DskipTests是跳过测试加速构建课设项目通常没有测试用例加上能省时间。默认账号密码写在文档里方便验收。6.2 报告 PDF 的结构与写法报告 PDF 通常有固定模板但内容质量差距很大。常见结构是需求分析、系统设计、数据库设计、功能实现、测试、总结。其中「系统设计」和「数据库设计」是老师重点看的部分。系统设计里画一张分层架构图用文字描述或表格代替图形说明 Controller、Service、Mapper 各层职责。数据库设计里放 E-R 关系描述和表结构表格表名字段数主键外键/索引说明user6iduk_username用户信息article8ididx_user_id, idx_category_id文章内容category2iduk_name分类tag2iduk_tag_name标签article_tag2(article_id, tag_id)联合主键多对多关联comment5ididx_article_id评论功能实现部分不要贴大段代码而是用「流程描述 关键代码片段」的方式。比如登录功能写清楚「接收参数 → 加密比对 → 写入 Session → 重定向」再附上核心方法签名即可。测试部分列出测试用例和预期结果比如「输入错误密码预期提示用户名或密码错误实际结果一致」。6.3 答辩时最容易被追问的三个问题第一个「你的项目里哪些地方用了事务为什么」答文章保存时文章表和关联表要一起成功或一起失败所以用Transactional。第二个「密码为什么存哈希用的什么算法」答避免明文泄露课设用 MD5生产环境应换 BCrypt 加盐。第三个「如果文章量很大列表页怎么优化」答加联合索引(category_id, create_time)分页用LIMIT避免全表扫描。这三个问题提前准备好答辩基本稳。我自己的习惯是文档写完后再通读一遍把每个「为什么这么设计」的地方都标出来这些就是老师可能追问的点。希望帮到你。本文还有配套的精品资源点击获取