Spring Boot + MyBatis 构建带审核流的内容社交平台
简介基于SpringBoot的社交平台设计与实现文档是一份完整的毕业设计论文资料适合计算机相关专业学生及初学SpringBoot生态的开发者参考。文档围绕以用户为中心的社交平台展开覆盖用户内容发布、管理员审核、用户资料管理等核心模块并阐述了从系统目标确立、需求分析、概要/详细设计到功能实现、测试运行的全流程。技术上采用SpringBoot整合MyBatis作为持久层搭配MySQL存储数据MD5加密保障安全同时利用Maven管理依赖SpringBoot自带Tomcat完成前后端交互。资源体积仅1.21MB包含1个docx文档内容结构完整含摘要、目录、系统分析、设计实现等章节便于对照学习或作为毕业设计写作蓝本。已有164人学习下载对希望快速搭建社交平台项目或完成同类课题的同学具有直接的参考价值。1. 带审核流的 Spring Boot 社交平台先审后发才是核心做过带用户生成内容的站点最后都会面对同一个问题用户量上来之后垃圾帖和违规评论开始淹没正常内容。这个社交平台的思路是不堆复杂功能把发布和管理员审核拆成两条清晰主线——用户内容先落在待审区管理员通过后才进入其他用户的浏览流。先审后发是这套设计里最值得拿走的部分。技术上它选的是 Spring Boot MyBatis MySQLMaven 管依赖Docker 起环境。文字、图片、视频三块内容区独立展示后台围绕审核做管理整体不大但不单薄。适合想搭内容社区雏形的开发者也适合正在做社区类课设或毕设的人。后面按搭建、建模、实现、验证的顺序把 Docker、表结构、验证码登录、发布审核和压测参数逐段拆开。2. Docker 起环境Maven 管依赖Spring Boot MyBatis 的工程骨架2.1 这一套组合解决什么问题Spring Boot 在这里解决的是从零搭工程的问题。它把 Spring 的基础配置做成自动装配引入一个 spring-boot-starter-web 就自动带出 DispatcherServlet 和内嵌 Tomcat开发阶段不用再打 war 包丢到外部容器。对于登录、发布、点赞、审核这类接口来说最终交付就是一个 java -jar 跑起来的进程这就是小型社区项目最省事的形态。MyBatis 存在的理由更直接这个平台的查询条件非常碎。首页要按审核状态过滤后台要按用户查内容好友列表要做分页这些用动态 SQL 拼条件比 JPA 自动生成查询更可控。Maven 负责把 Spring Boot、MyBatis、MySQL 驱动的版本固定下来避免依赖冲突。原文里提到的 JDK 1.8 和 MySQL 5.7 是当年常见的稳定组合Spring Boot 版本建议锁在 2.7.x这个版本对 JDK 1.8 的支持最完整。原文的前端页面用的是 JSP如果复现时想把前端换成 VueController 里返回 ModelAndView 的地方改成返回 JSON 就行业务层和数据层基本不用动。这也是 Spring Boot 做前后端分离时最舒服的一点接口写好了前端怎么换都行。2.2 Docker 起 MySQL 5.7 和 Redis原文的部署方案里用了 Docker我复现时的做法是把 MySQL 5.7 和 Redis 都放进容器开发机不装原生数据库。Redis 在概要设计里明确写了“为了缓存后台产生的数据帮助提高系统性能”所以这里一并起起来。# MySQL 5.7数据目录挂到宿主机防止容器销毁丢数据 docker run -d \ --name social-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e TZAsia/Shanghai \ -v /data/mysql:/var/lib/mysql \ mysql:5.7 # Redis 6保存点赞状态和推荐结果 docker run -d \ --name social-redis \ -p 6379:6379 \ redis:6两个命令的关键参数都一样-d 后台运行-p 端口映射-e 传环境变量。MySQL 这里必须设置 TZAsia/Shanghai否则容器默认 UTC 时间写入 addtime 字段会和本地差 8 个小时。另一点要注意MySQL 5.7 的 JDBC 驱动类是 com.mysql.jdbc.Driver如果本地用的是 MySQL 8要换成 com.mysql.cj.jdbc.Driver 并把 mysql-connector-java 版本升高否则启动会报驱动加载失败。2.3 application.yml 与 pom.xml 的关键配置数据库和中间件起来之后工程配置集中在 application.yml。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/social?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123456 driver-class-name: com.mysql.jdbc.Driver redis: host: localhost port: 6379 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这段配置里几个参数要单独说。server.port 是内嵌 Tomcat 的监听端口和 Docker 映射出来的宿主机端口不是一回事本地 8080 被占用时可以直接改 8081。url 里的 serverTimezone 与容器启动时设置的 TZ 要一致两个地方都处理了时间字段读取才不偏移。multipart 的两个上限对应图片、视频上传10MB 是单文件上限50MB 是单次请求总上限。表 2-1 配置参数说明参数作用注意点server.port内嵌 Tomcat 监听端口与 Docker 映射端口区分spring.datasource.url数据库地址和字符集serverTimezone 与容器 TZ 一致spring.servlet.multipart.max-file-size单文件上传上限图片视频场景调到 10MB 以上mybatis.map-underscore-to-camel-case下划线转驼峰依赖表字段命名规范mybatis.mapper-locationsMapper XML 扫描路径XML 需要打包进 classpathpom.xml 里最核心的依赖是这几项dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency依赖这块最常翻车的是 mybatis-spring-boot-starter 的版本。2.2.2 配合 Spring Boot 2.7.x 是经过验证的组合如果把 Spring Boot 升到 3.xMyBatis 官方 starter 的版本策略和包名都会变JDK 也要升到 17改动量不小。原来工程若用 Lombok还需要加带 annotationProcessor 配置的依赖否则 IDE 编译报找不到 getter/setter。2.4 启动前要检查的两个点第一次启动如果报找不到 WrittingsMapper 对应的 XML先检查两处一是 target/classes 里有没有 mapper 目录Maven 默认不会把放到 java 目录下的 XML 打进去需要把 XML 放在 src/main/resources/mapper 下二是 mybatis.mapper-locations 路径是否匹配 classpath:mapper/*.xml。另一个常见情况是容器 MySQL 起来了客户端连接却报 Access denied确认 MySQL 5.7 默认 root 的远程登录权限简单做法是建一个专用账号并只授权 social 库。提示数据卷挂载不是可选项。不挂 -vdocker rm 容器后数据库内容全部丢失重新建表会浪费大量时间。3. 数据模型与 MyBatis 映射从 E-R 图到内容表的取舍3.1 七张表怎么对上业务原文的 E-R 设计把业务拆成七个实体用户、好友、收藏、评论、文字内容、图片内容、视频内容。落表之后对应 Users、Friends、Collects、Comments、Writtings、Pictures、Videos。其中 Users 是主表保存用户名、真实姓名、性别、出生年月、电话和创建时间Friends 存的是关注关系里面同时记录添加人的 usersid 和 concerned所以它天然是单向关注Collects 只存链接和标题说明收藏功能把三类内容统一抽象成了标题加 URL 的形式。表 3-1 核心表结构与用途表名核心字段说明Usersuid, name, password, realname, phonenumber用户主表登录态与个人资料Friendsfid, usersid, concerned关注关系表单向关注Collectscolid, usersid, url, title收藏内容统一记录链接Commentscomid, commentator, contents评论内容需要补 target_idWrittingswid, title, contents, likes, isverify文字内容带审核状态Picturespid, title, contents, likes, isverify图片内容Videosvid, title, contents, likes, isverify视频内容这里有一个值得展开的选型内容为什么不放一张表加 type 字段而是拆成三张结构几乎一样的表。原文写了“文字为一个区域视频为一个区域图片为一个区域”说明信息流首页是按内容类型分区的。拆表之后查文字流就是单表 where 条件不需要先过滤 type 再拼装对象但之后如果要做混合时间线三张表就得用 union all 把字段对齐这是拆表的代价。对一个以内容分区展示为主的平台拆表是更直接的选择。3.2 建表 SQL 与审核状态字段字段设计里有几个细节值得照抄这里举三个有代表性的建表语句。CREATE TABLE Users ( uid INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(49) NOT NULL COMMENT 登录用户名, password VARCHAR(64) NOT NULL COMMENT MD5后的密码, realname VARCHAR(50) DEFAULT NULL, sex VARCHAR(4) DEFAULT NULL, birthtime VARCHAR(100) DEFAULT NULL, phonenumber VARCHAR(45) DEFAULT NULL, addtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE Comments ( comid INT PRIMARY KEY AUTO_INCREMENT, commentator VARCHAR(50) NOT NULL, contents VARCHAR(255) NOT NULL, addtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE Writtings ( wid INT PRIMARY KEY AUTO_INCREMENT, uid INT NOT NULL, title VARCHAR(50) NOT NULL, contents VARCHAR(255) NOT NULL, likes INT DEFAULT 0, isverify VARCHAR(10) DEFAULT 0 COMMENT 0待审核 1通过 2驳回, addtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_verify (isverify) );字段设计上password 按 MD5 输出的 32 位十六进制预留 VARCHAR(64)为以后换加密算法留空间birthtime 用 VARCHAR(100) 而不是 DATE因为前端日期选择器传回来的是字符串直接存可以省去格式转换还能兼容不完整日期。isverify 用 VARCHAR(10) 存状态虽然 TINYINT 更省空间但字符串在前后端交互时不容易被框架自动转成 boolean实际操作里反而少踩坑。Comments 表有一个隐患原文表结构里评论没有关联到具体内容 ID。实际开发时评论至少要有一个 target_id 标明评论的是哪条文字、哪张图或哪个视频再有一个 type 字段区分内容类型。我复现时会补上这两个字段否则评论列表没法按内容反查。3.3 Mapper XML 的动态 SQL 怎么写MyBatis 在这个项目里的核心价值体现在动态 SQL。后台审核页面要按状态过滤用户中心要看自己发的内容首页要只看审核通过的内容这些查询结构相同、条件不同。用 where 加 if 可以一套 SQL 覆盖所有场景。select idlistWritings resultTypecom.social.entity.Writings SELECT wid, uid, title, contents, likes, isverify, addtime FROM Writtings where if testisverify ! null and isverify ! AND isverify #{isverify} /if if testuid ! null AND uid #{uid} /if /where ORDER BY addtime DESC LIMIT #{offset}, #{limit} /select update idupdateVerify UPDATE Writtings SET isverify #{isverify} WHERE wid #{wid} /update执行逻辑是传入 isverify 就按审核状态过滤传入 uid 就按发布人过滤两个都传就是“某个用户某种状态下的内容”两个都不传就是首页全量列表。LIMIT 直接在 SQL 里分页#{offset} 是起始行号#{limit} 是每页条数PageHelper 的 pageNum 在 Service 层换算成 offset 后传入。where 标签会自动去掉第一个多余的 AND这是 MyBatis 动态 SQL 里最容易写错的地方手写 WHERE AND 开头时 MySQL 会直接报语法错误。3.4 点赞计数的更新策略原文设计里 Writtings、Pictures、Videos 各自带 likes 字段点赞时直接加一。单机小流量下这么写没问题但有一个明显缺陷同一个用户对同一条内容可以无限点赞前端不刷新就能重复触发。常见做法是加一张点赞流水表用(user_id, target_id, type)建唯一索引。CREATE TABLE Likes ( id INT PRIMARY KEY AUTO_INCREMENT, uid INT NOT NULL, target_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1文字 2图片 3视频, addtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_uid_target (uid, target_id, type) );点赞接口先 INSERT IGNORE 流水影响行数为 1 才更新 likes 字段这样重复点击不会重复计数。取消点赞再删流水并执行 likes likes - 1下限用 GREATEST(likes - 1, 0) 保护。比内容表上加计数多一张表但保证了幂等性也为后面做“我赞过什么”的查询留了数据。4. 从验证码到审核登录鉴权、内容发布与管理后台的实现细节4.1 验证码BufferedImage 画图与 Session 绑定登录页验证码是第一个接口。原文用 BufferedImage、Graphics 和 String 在内存里画图再把随机字符串存进 session 的 random 字段登录时比对。原代码把 response 放在 BaseController 里直接用我这里显式传入参数逻辑更清晰。Controller public class CaptchaController extends BaseController { private static final int WIDTH 61; private static final int HEIGHT 21; GetMapping(/captcha) public void captcha(HttpServletResponse response, HttpSession session) throws IOException { response.setContentType(image/jpeg); response.setHeader(Pragma, No-cache); response.setHeader(Cache-Control, no-cache); response.setDateHeader(Expires, 0); BufferedImage image new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics g image.getGraphics(); Random random new Random(); // 画背景色 g.setColor(getRandColor(199, 249)); g.fillRect(0, 0, WIDTH, HEIGHT); g.setFont(new Font(Times New Roman, Font.PLAIN, 17)); // 155条干扰线提高OCR识别成本 g.setColor(getRandColor(159, 199)); for (int i 0; i 155; i) { int x random.nextInt(WIDTH); int y random.nextInt(HEIGHT); g.drawLine(x, y, x random.nextInt(12), y random.nextInt(12)); } // 生成4位字母数字混合验证码 String code random.ints(48, 123) .filter(c - (c 48 c 57) || (c 65 c 90) || (c 97 c 122)) .limit(4) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString(); session.setAttribute(random, code); g.drawString(code, 10, 16); g.dispose(); ImageIO.write(image, JPEG, response.getOutputStream()); } private Color getRandColor(int fc, int bc) { Random random new Random(); if (fc 255) fc 255; if (bc 255) bc 255; int r fc random.nextInt(bc - fc); int g fc random.nextInt(bc - fc); int b fc random.nextInt(bc - fc); return new Color(r, g, b); } }几个参数值得说。WIDTH 和 HEIGHT 是 61 x 21图片小渲染快干扰线 155 条配随机颜色简单 OCR 识别率会明显下降字体用 Times New Roman 是因为它是 JDK 自带字体换特殊字体可能 drawString 画不出来验证码只保留数字和字母过滤掉容易混淆的区间。session.setAttribute 是校验凭证后面登录接口从同一个 session 取 random 比对。4.2 登录鉴权与 MD5 加密的取舍登录逻辑三步走先校验验证码再查用户表最后比对密码。按原文 MD5 加密实现如下。public class MD5Util { public static String md5(String source) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(source.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }PostMapping(/login) public String login(String name, String password, String captcha, HttpSession session) { String sessionCode (String) session.getAttribute(random); if (sessionCode null || !sessionCode.equalsIgnoreCase(captcha)) { return 验证码错误; } Users user userMapper.getByName(name); if (user null || !user.getPassword().equals(MD5Util.md5(password))) { return 用户名或密码错误; } session.setAttribute(loginUser, user); return redirect:/index; }比对顺序是有讲究的验证码错误时不用查库能挡住一部分脚本请求用户不存在和密码错误返回同一个提示避免攻击者根据提示差异枚举有效用户名。session 里存的是完整 user 对象后面发布内容、加好友、点赞都要从这里拿当前用户只存 uid 的话每个接口都要回表查一次。MD5 本身已经不是安全的密码存储方案查表攻击很容易还原常见密码。原文用 MD5 是满足毕设对加密的要求生产环境建议加盐后做多次摘要或者直接换 BCrypt。改造代价不高密码字段已经留了 VARCHAR(64)BCrypt 输出也在 60 字符以内只需替换注册和登录两处的加密函数。4.3 内容发布文字、图片、视频的上传与入库内容模块最大的特点是三种内容分开发表。文字内容直接提交表单字段图片和视频先传文件拿到访问路径再和标题一起入库。以文字发布为例Controller 核心逻辑如下。PostMapping(/writing/publish) public String publish(Writings writings, HttpSession session) { Users user (Users) session.getAttribute(loginUser); if (user null) { return redirect:/login; } writings.setUid(user.getUid()); writings.setLikes(0); writings.setIsverify(0); // 发布即待审核 writingMapper.insert(writings); return redirect:/my/writings?state0; }流程是“发布即待审”用户提交内容、管理员审核、其他用户浏览。isverify 是这里最重要的状态位插入时固定写 0信息流查询只捞 1后台审核列表捞 0 和 2。图片和视频的发布接口多了文件处理Spring Boot 里用 MultipartFile 接收transferTo 写到上传目录再把拼好的访问路径存入 contents 字段。上传目录我习惯放工程外部比如 /data/social/uploads不要放 target 下因为重新打包会清空 target用户传的文件就全丢了。如果部署时前面挂了 Nginx后端配了 10MB 还要同步改 client_max_body_size否则会先被 Nginx 拦下来报 413。4.4 管理员审核链路与状态流转后台审核是这套社交平台区别于普通博客的核心。管理员做审核本质是对 isverify 的更新但比普通 UPDATE 多一层权限校验。PostMapping(/admin/verify) public String verify(RequestParam Integer wid, RequestParam String result, RequestParam(required false) String reason, HttpSession session) { Admin admin (Admin) session.getAttribute(admin); if (admin null) { return redirect:/admin/login; } if (!Arrays.asList(1, 2).contains(result)) { return 非法状态; } writingMapper.updateVerify(wid, result); return redirect:/admin/writings?state0; }入参有三个wid 是内容编号result 是审核结果reason 是驳回原因。updateVerify 只更新 isverify 字段result 必须白名单校验防止参数注入任意状态。驳回时前端要展示原因的话可以加一个 verify_reason 字段或者触发一条系统通知。对比用户发布接口可以看出用户侧没有权限判断管理侧必须先确认 session 里存在 admin 对象这个差异就是前后端权限边界。表 4-1 审核状态流转isverify 值含义可见范围0待审核仅发布者本人和管理员1审核通过所有用户可见2审核驳回仅发布者本人提示拦截器方案比在每个管理接口里写 if (admin null) 更省事但逐个判断有一个好处每条管理路径的权限逻辑都暴露得很清楚新接手的开发者不容易漏。4.5 用户推荐与内容推荐的落地方式推荐模块在原文中的描述是“根据用户的喜好和设置来推断好友和内容的推荐方式”。遇到这种需求不要一上来就上推荐引擎。常见做法是两层好友推荐查 Friends 表中关注了同一批用户的其他人内容推荐按当前用户点赞过的内容类型做权重排序。先用普通 SQL 把这两个查询跑通观察一段时间行为数据之后再考虑协同过滤。这个项目的体量离推荐引擎还远SQL 加 Redis 缓存已经足够。5. 上线前要盯的指标Redis 缓存、Tomcat 参数与压测验证5.1 Redis 缓存先缓存信息流信息流是这个平台读多写少的典型场景。浏览首页每次都查数据库再分页压力全在 MySQL。用 StringRedisTemplate 缓存审核通过的内容列表写法很直接。String key content:page: page; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; } ListWritings list writingMapper.listWritings(1, offset, limit); redisTemplate.opsForValue().set(key, JSON.toJSONString(list), 10, TimeUnit.MINUTES); return JSON.toJSONString(list);这段逻辑是缓存命中直接返回未命中查库后序列化写入 Redis10 分钟过期。注意管理员审核通过新内容后对应分页缓存要删除否则新内容不展示。省事的做法是审核通过后删第一页缓存因为新内容大概率出现在第一页。5.2 内嵌 Tomcat 的连接器参数内嵌 Tomcat 的线程池参数直接决定压测数据。max-threads 控制并发处理线程数accept-count 是排队长度。线程数不是越大越好要和对数据库连接池 maxActive 匹配否则线程全阻塞在数据库连接上CPU 都耗在线程切换。server: tomcat: max-threads: 200 min-spare-threads: 20 accept-count: 1005.3 功能测试与压测样例原文非功能需求给了两条硬指标TPS 不小于 100平均响应时间在 1000-2000ms。功能用例先跑通再压测。表 5-1 功能测试与性能验证用例测试项预期结果判断方式错误验证码登录登录失败提示验证码错误不进入主页面用户发布文字内容写入 Writtingsisverify0自己可见他人不可见管理员审核通过isverify1出现在公开列表删除缓存后首页可见100并发访问列表接口失败数低于1%平均响应小于2000msab 报告指标压测工具我用 ab简单不依赖界面。# 100并发打首页列表接口请求1000次 ab -n 1000 -c 100 http://localhost:8080/writing/list?page1压测前先用 curl 确认无登录态能直接访问列表接口或者给 ab 加上登录后的 Cookie否则压到的是登录重定向页面TPS 会虚高。报告重点看 Requests per second 和 Failed requests。如果 TPS 长时间停在两位数先看数据库连接池 maxActive 是否太小再看 MySQL 慢查询日志多数情况是这两处不是 Tomcat 线程数不够。本文还有配套的精品资源点击获取