简介:这是一套基于Spring Boot开发的前后台分离博客系统,专为计算机专业本科生毕业设计提供完整参考方案,涵盖系统实现、技术选型与设计报告全流程。资源包含可直接运行的源码、MySQL数据库脚本及配套文档,适用于Java Web课程设计、毕设快速启动与Shiro权限管理实践学习。压缩包共2000个文件,44.8MB,其中Java类文件371个(含Controller、Service、Mapper层结构清晰),HTML模板485个(Thymeleaf渲染页)、JS脚本698个(支撑前台交互与后台管理功能),CSS样式186个(含Semantic UI、Element UI、Bootstrap等多框架整合),另有XML配置、SQL脚本与Properties配置文件,体现典型企业级分层架构。目前已有75人学习下载,读者可获得完整角色权限体系(含用户/管理员双端菜单与导航配置)、分类标签管理模块、友情链接前台展示能力,以及Quartz定时任务、Druid连接池、Fastjson工具集成等进阶实践细节,具备良好的教学示范性与工程复用价值。
1. 这不是又一个“Hello World”博客,而是一套能过答辩、可上线、带完整权限链的 Spring Boot 毕业设计系统
你手头正赶着计算机专业毕业设计 deadline,导师要求“有前后台、有用户角色、有数据库设计、能跑通、最好还能写进设计报告”,但网上搜到的所谓“Spring Boot 博客系统”要么只有前台静态页面,要么后台连登录校验都靠 session 硬编码,更别说菜单动态加载、分类/标签树形管理、友情链接热更新这些答辩常问点。这套资源不是 Demo,而是按真实项目结构组织的可交付系统:它用 Apache Shiro 实现 RBAC 权限模型(管理员/编辑/游客三级),用 MyBatis-Plus 自动生成 CRUD 并支持逻辑删除,Thymeleaf 模板层与后台 Controller 完全解耦,连style.min.css和semantic.min.css都做了版本分离——说明前端资源已走构建流程。它不教你怎么配 JDK,而是直接给出mysql5.7+下可执行的建表 SQL 和初始化数据;不讲 Spring Boot 基础概念,而是把Quartz定时任务嵌在博客统计模块里,每天凌晨自动刷新热门文章排行。适合正在写开题报告、做 ER 图、画系统流程图、填功能模块表的应届生,也适合需要快速验证权限控制逻辑的初级开发。
2. 权限体系落地:Shiro + 自定义 Realm + 动态菜单加载的三重实现
2.1 为什么选 Shiro 而非 Spring Security?
在毕业设计场景下,Shiro 的轻量级和显式权限控制逻辑更利于答辩展示。Spring Security 的自动配置虽强,但@PreAuthorize("hasRole('ADMIN')")这类注解背后是复杂的代理机制和 AOP 切面,学生调试时容易卡在AccessDeniedException却找不到拦截点;而 Shiro 的Subject.isPermitted("blog:delete")是直白的方法调用,配合自定义Realm可清晰看到“用户 → 角色 → 权限字符串”的映射链条。本系统中,ShiroConfig.java显式配置了SecurityManager、ShiroFilterFactoryBean和CredentialsMatcher,其中ShiroFilterFactoryBean的filterChainDefinitionMap定义了/admin/**=authc,roles[admin]这类路径级权限,比注解方式更易在设计报告中画出权限矩阵表。
2.2 自定义 Realm 的核心逻辑与数据库关联
BlogRealm.java继承AuthorizingRealm,重写doGetAuthenticationInfo和doGetAuthorizationInfo。前者通过UserMapper.selectByUsername(username)查询用户,用ByteSource.Util.bytes(salt)处理密码盐值,返回SimpleAuthenticationInfo;后者调用RoleMapper.selectRolesByUserId(userId)获取角色列表,再通过PermissionMapper.selectPermissionsByRoleId(roleId)关联查询权限字符串(如"blog:list"、"menu:edit")。关键点在于:权限字符串不是硬编码,而是从数据库sys_permission表读取,该表字段含permission_code(唯一标识)、permission_name(中文名)、url_pattern(匹配路径)和parent_id(支持菜单层级)。这样答辩时可演示“新增一个‘标签管理’权限 → 分配给编辑角色 → 前台菜单实时刷新”的完整闭环。
// BlogRealm.java 中 doGetAuthorizationInfo 方法节选 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 1. 查询用户角色 List<Role> roles = roleMapper.selectRolesByUserName(username); Set<String> roleSet = roles.stream().map(Role::getRoleCode).collect(Collectors.toSet()); info.addRoles(roleSet); // 2. 根据角色查权限(支持多角色叠加) Set<String> permissionSet = new HashSet<>(); for (Role role : roles) { List<Permission> perms = permissionMapper.selectPermissionsByRoleId(role.getId()); permissionSet.addAll(perms.stream().map(Permission::getPermissionCode).collect(Collectors.toList())); } info.addStringPermissions(permissionSet); return info; }提示:
permission_code必须与 Controller 方法上的@RequiresPermissions("blog:publish")严格一致,大小写敏感。若数据库中存的是BLOG:PUBLISH,而代码写成blog:publish,Shiro 将静默拒绝访问,日志中仅显示UnauthorizedException,需检查logback-spring.xml中org.apache.shiro日志级别设为DEBUG才能看到权限校验失败详情。
2.3 前台动态菜单渲染:Thymeleaf + Shiro 标签的协同
菜单数据不写死在 HTML 里,而是由MenuController.java的getMenuTree()方法返回 JSON 树结构,前端通过th:each遍历。关键在于使用 Shiro 的shiro:hasPermission标签控制菜单可见性:
<!-- admin/layout.html --> <div class="ui vertical menu" th:fragment="sidebar"> <a href="/admin" class="item" th:classappend="${#strings.equals(menuPath,'/admin')} ? 'active' : ''"> <i class="home icon"></i> 首页 </a> <!-- 遍历后台查询的菜单列表 --> <div th:each="menu : ${menus}" th:if="${#strings.length(menu.permissionCode) > 0}"> <!-- 仅当用户有该权限才渲染此菜单项 --> <shiro:hasPermission name="${menu.permissionCode}"> <a th:href="@{${menu.url}}" class="item" th:classappend="${#strings.contains(menuPath, menu.url)} ? 'active' : ''"> <i th:class="${menu.icon}"></i> <span th:text="${menu.name}">菜单名</span> </a> </shiro:hasPermission> </div> </div>MenuService.java中的buildMenuTree()方法递归组装树形结构,其 SQL 使用 MyBatis-Plus 的QueryWrapper构建条件:wrapper.eq("status", 1).orderByAsc("sort_order"),确保启用状态且按排序序号升序。sys_menu表设计包含id,parent_id,name,url,icon,permission_code,sort_order,status字段,其中parent_id=0表示一级菜单,status=1表示启用。这种设计让答辩时可现场修改数据库sys_menu.status字段,立即看到前台菜单消失或出现,直观证明权限控制有效性。
3. 博客核心模块:MyBatis-Plus 分页 + 文章富文本存储 + 分类/标签树形管理
3.1 文章表设计与富文本处理策略
blog_article表采用content(LONGTEXT)存储 Markdown 源码,content_html(LONGTEXT)存储渲染后的 HTML,避免每次请求都调用CommonMark解析器。create_time和update_time字段由 MyBatis-Plus 的@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)自动填充,is_top(置顶)、is_recommend(推荐)、status(草稿/发布/删除)字段支撑后台审核流。关键技巧:ArticleMapper.xml中的selectListWithCategoryAndTag查询使用<resultMap>映射关联的Category和Tag对象,而非简单JOIN,防止 N+1 查询问题:
<!-- ArticleMapper.xml --> <resultMap id="ArticleWithCategoryTagMap" type="com.example.blog.entity.Article"> <id property="id" column="id"/> <result property="title" column="title"/> <result property="content" column="content"/> <association property="category" javaType="com.example.blog.entity.Category"> <id property="id" column="category_id"/> <result property="name" column="category_name"/> </association> <collection property="tags" ofType="com.example.blog.entity.Tag"> <id property="id" column="tag_id"/> <result property="name" column="tag_name"/> </collection> </resultMap> <select id="selectListWithCategoryAndTag" resultMap="ArticleWithCategoryTagMap"> SELECT a.*, c.id as category_id, c.name as category_name, t.id as tag_id, t.name as tag_name FROM blog_article a LEFT JOIN sys_category c ON a.category_id = c.id LEFT JOIN blog_article_tag at ON a.id = at.article_id LEFT JOIN sys_tag t ON at.tag_id = t.id WHERE a.status = 1 ORDER BY a.create_time DESC </select>3.2 分类与标签的树形结构与多对多关系
sys_category表支持无限级分类:id,parent_id,name,code,sort_order,status。CategoryService.java的listCategoryTree()方法用递归查询构建树,SQL 中WITH RECURSIVE(MySQL 8.0+)或 Java 层递归(兼容 MySQL 5.7)均可实现。标签sys_tag是扁平表,但通过blog_article_tag关联表实现文章与标签多对多。后台TagController提供saveBatch接口,接收articleId和tagNames数组,先根据tagNames查询或创建标签,再批量插入关联记录:
// TagController.java @PostMapping("/batchSave") @ResponseBody public Result batchSave(@RequestParam Long articleId, @RequestBody List<String> tagNames) { List<Tag> tags = new ArrayList<>(); for (String tagName : tagNames) { Tag tag = tagMapper.selectOne(new QueryWrapper<Tag>().eq("name", tagName)); if (tag == null) { tag = new Tag(); tag.setName(tagName); tag.setStatus(1); tagMapper.insert(tag); } tags.add(tag); } // 清空旧关联 articleTagMapper.delete(new QueryWrapper<ArticleTag>().eq("article_id", articleId)); // 插入新关联 List<ArticleTag> articleTags = tags.stream() .map(t -> { ArticleTag at = new ArticleTag(); at.setArticleId(articleId); at.setTagId(t.getId()); return at; }) .collect(Collectors.toList()); articleTagMapper.insertBatchSomeColumn(articleTags); return Result.success(); }注意:
articleTagMapper.insertBatchSomeColumn()是 MyBatis-Plus 3.4+ 的批量插入方法,比循环insert性能提升 5 倍以上。若使用旧版 MyBatis-Plus,需改用executeBatch或foreach标签。
3.3 前台分页与搜索的性能优化
前台/blog/list接口使用Page<Article>分页,但ArticleMapper.selectListWithCategoryAndTag在大数据量下会因LEFT JOIN导致慢查询。解决方案:先分页查blog_article主表 ID,再用IN子查询关联分类和标签:
// ArticleService.java public IPage<Article> pageWithCategoryAndTag(IPage<Article> page, QueryWrapper<Article> wrapper) { // 第一步:分页查主表ID IPage<Long> idPage = articleMapper.selectPage(page, wrapper.select("id")); // 第二步:查出ID列表 List<Long> ids = idPage.getRecords(); if (ids.isEmpty()) return new Page<>(page.getCurrent(), page.getSize()); // 第三步:用IN查询完整数据(避免JOIN) QueryWrapper<Article> fullWrapper = new QueryWrapper<>(); fullWrapper.in("id", ids); List<Article> articles = articleMapper.selectList(fullWrapper); // 第四步:批量查询分类和标签(N+1优化) Map<Long, Category> categoryMap = categoryMapper.selectBatchIds( articles.stream().map(Article::getCategoryId).filter(Objects::nonNull).collect(Collectors.toList()) ).stream().collect(Collectors.toMap(Category::getId, c -> c)); // 返回分页对象(手动设置records) Page<Article> resultPage = new Page<>(page.getCurrent(), page.getSize(), idPage.getTotal()); resultPage.setRecords(articles); return resultPage; }4. 前台展示与 SEO 适配:Thymeleaf 模板继承 + 静态资源分离 + 友情链接热更新
4.1 Thymeleaf 模板继承体系与 SEO 元信息注入
templates/layout.html定义基础布局,templates/blog/index.html通过th:replace="~{layout :: content}"继承,并在head中使用th:fragment="seo"注入动态元信息:
<!-- layout.html --> <head th:fragment="head"> <meta charset="UTF-8"/> <title th:text="${title} ?: '我的博客'">博客标题</title> <meta name="keywords" th:content="${keywords} ?: 'java,springboot,毕业设计'"/> <meta name="description" th:content="${description} ?: '基于Spring Boot的个人博客系统'"/> <link rel="stylesheet" th:href="@{/css/semantic.min.css}"/> <link rel="stylesheet" th:href="@{/css/style.min.css}"/> </head><!-- blog/index.html --> <div th:replace="~{layout :: head(title='首页', keywords='springboot博客,java毕业设计', description='基于Spring Boot开发的前后台博客系统,含完整权限管理与数据库设计')}"></div>这种写法让每页可独立设置title/keywords/description,满足答辩对 SEO 基础能力的要求。style.min.css和semantic.min.css分离存放,便于在application.yml中配置spring.resources.static-locations=classpath:/static/,file:/opt/blog/static/,实现生产环境静态资源外挂。
4.2 友情链接的缓存与热更新机制
友情链接数据存在sys_friend_link表,字段含name,url,logo,sort_order,status。为避免每次请求都查库,FriendLinkService.java使用@Cacheable(value = "friendLinks", key = "'all'")缓存全部启用链接。关键点在于@CacheEvict的触发时机:当后台执行updateStatus或save时,必须清除缓存:
@Service public class FriendLinkService { @Cacheable(value = "friendLinks", key = "'all'") public List<FriendLink> listAll() { return friendLinkMapper.selectList(new QueryWrapper<FriendLink>().eq("status", 1).orderByAsc("sort_order")); } @CacheEvict(value = "friendLinks", key = "'all'") public void updateStatus(Long id, Integer status) { FriendLink link = new FriendLink(); link.setId(id); link.setStatus(status); friendLinkMapper.updateById(link); } @CacheEvict(value = "friendLinks", key = "'all'") public void save(FriendLink link) { friendLinkMapper.insert(link); } }前台blog/sidebar.html通过th:each="link : ${@friendLinkService.listAll()}"直接调用服务方法,Thymeleaf 在渲染时自动触发缓存逻辑。答辩时可演示:修改数据库sys_friend_link.status为0→ 刷新前台侧边栏 → 链接消失;再改回1→ 再次刷新 → 链接重现,全程无需重启应用。
4.3 导航配置的 JSON 驱动与前端解析
导航栏内容不写死,而是由sys_nav_config表存储 JSON 字符串,如{"home":"首页","about":"关于","contact":"联系"}。NavConfigService.java提供getNavConfig()方法解析 JSON:
public Map<String, String> getNavConfig() { NavConfig config = navConfigMapper.selectOne(new QueryWrapper<NavConfig>().eq("code", "main_nav")); if (config != null && StringUtils.isNotBlank(config.getConfigJson())) { try { return new ObjectMapper().readValue(config.getConfigJson(), new TypeReference<Map<String, String>>() {}); } catch (JsonProcessingException e) { log.error("解析导航配置JSON失败", e); } } return Collections.emptyMap(); }前台模板用th:each="entry : ${@navConfigService.getNavConfig().entrySet()}"遍历,<a th:href="@{/${entry.key}}">生成链接。这种设计让答辩老师提问“如何快速增加‘作品集’导航项”时,你只需在数据库sys_nav_config.config_json字段追加,"portfolio":"作品集",无需改代码、无需重启,立刻生效。
5. 毕业设计交付包实操:数据库初始化、IDEA 配置要点与设计报告素材提取
5.1 数据库初始化脚本执行与常见报错修复
提供的blog_db.sql文件需在 MySQL 5.7+ 执行。常见错误及修复:
- 错误
ERROR 1071 (42000): Specified key was too long:因utf8mb4字符集下索引长度超限。解决:在my.cnf中添加innodb_large_prefix=ON,并确保innodb_file_format=Barracuda。 - 错误
Unknown column 'create_time' in 'field list':SQL 文件中blog_article表缺少create_time datetime DEFAULT CURRENT_TIMESTAMP字段。手动补全:ALTER TABLE `blog_article` ADD COLUMN `create_time` datetime DEFAULT CURRENT_TIMESTAMP, ADD COLUMN `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; - Shiro 登录后跳转 404:检查
ShiroConfig.java中shiroFilterFactoryBean.setLoginUrl("/login")是否与LoginController.login()的@RequestMapping("/login")匹配,且login.html模板位于templates/login.html。
5.2 IDEA 项目导入关键配置
- JDK 版本:File → Project Structure → Project → SDK 设置为
1.8(不可用 11+,因 Shiro 1.4.x 不完全兼容)。 - Maven 依赖:
pom.xml中spring-boot-starter-thymeleaf版本需与 Spring Boot 2.3.x 匹配(本系统用2.3.12.RELEASE),若自动下载失败,手动在 Maven Repositories 中添加阿里云镜像:<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>Aliyun Maven</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> - 运行配置:Edit Configurations → Spring Boot → Main class 选择
BlogApplication.java,VM options 添加-Dfile.encoding=UTF-8,Environment variables 设置SPRING_PROFILES_ACTIVE=dev。
5.3 设计报告可直接复用的技术点表格
| 报告章节 | 可直接引用的系统实现细节 | 对应源码位置 |
|---|---|---|
| 系统架构设计 | 采用 B/S 架构,后端 Spring Boot 2.3.x + Shiro 1.4.x + MyBatis-Plus 3.4.x,前端 Thymeleaf + Semantic UI | pom.xml,ShiroConfig.java |
| 数据库设计 | blog_article表含content(Markdown) 和content_html(渲染后) 字段,支持富文本双存储策略 | blog_article.sql,Article.java |
| 权限模块设计 | RBAC 模型:用户-角色-权限三级映射,权限字符串(如blog:delete)存于sys_permission表 | sys_permission.sql,BlogRealm.java |
| 功能模块描述 | 友情链接支持缓存(@Cacheable)与热更新(@CacheEvict),修改数据库即生效 | FriendLinkService.java,sys_friend_link.sql |
| 系统测试用例 | 登录测试:输入错误密码返回login?error;权限测试:编辑角色访问/admin/menu返回 403 | LoginController.java,ShiroConfig.java |
提示:答辩 PPT 中“关键技术难点”页,可截图
BlogRealm.java的doGetAuthorizationInfo方法,圈出info.addStringPermissions(permissionSet)行,标注“权限字符串动态加载,非硬编码,支持后台配置”。
5.4 定时任务 Quartz 的实际应用场景
BlogStatisticsJob.java实现每日凌晨 2 点执行的博客统计任务,更新blog_article.view_count和sys_category.article_count。其@Scheduled(cron = "0 0 0 2 * ?")注解需配合@EnableScheduling启用。关键点在于:任务中使用@Transactional保证统计更新原子性,且查询时加FOR UPDATE锁防止并发更新:
@Component public class BlogStatisticsJob { @Scheduled(cron = "0 0 0 2 * ?") // 每天凌晨2点 @Transactional public void updateStatistics() { // 更新热门文章(按阅读量TOP10) List<Article> topArticles = articleMapper.selectList( new QueryWrapper<Article>().orderByDesc("view_count").last("LIMIT 10") ); // 更新分类文章数(子查询效率高) articleMapper.updateCategoryCount(); } }articleMapper.updateCategoryCount()对应 XML 中的UPDATE sys_category c SET article_count = (SELECT COUNT(*) FROM blog_article a WHERE a.category_id = c.id)。此任务在答辩中可作为“系统自动化能力”的佐证,说明系统具备后台运维思维,不止于 CRUD。
本文还有配套的精品资源,点击获取