最近有好几个来咨询毕设的朋友拿的都是同一个类型的项目——基于SpringBoot的古典舞在线交流平台。有的是自己写的有的是手头有一份能跑的源码但一问到“这个表为什么这样设计”“点赞数到底存Redis还是MySQL”“上传的视频会不会因为重启项目就丢了”这类问题基本就卡壳了。这篇文章不打算只给你塞一堆源码下载地址而是想把这套项目从需求梳理、技术选型、数据库设计、核心功能实现到答辩时要养成的思考习惯完整拆开揉碎地讲一遍。选题本身并不难但它非常适合用来练一遍Spring Boot全家桶的完整链路——登录鉴权、社区发帖、关注点赞、多媒体上传该有的核心业务都有了比那种只做个增删改查的管理系统有含金量得多。就算你手头已经有一份能跑通的源码看完这篇也会知道哪些地方该补、哪些地方换了说法就能在答辩时多拿几分。1. 这个“古典舞交流平台”到底在解决什么问题先想清楚再动手很多同学拿到课题的第一反应是赶紧搜代码其实这是反的。我复盘过不下二十份类似的毕设代码最后翻车的往往不是技术难点而是从一开始就没想清楚平台的服务对象和核心痛点导致功能堆砌看起来什么都有实际上哪个模块都经不住老师追问。1.1 切入点为什么需要一个“交流平台”而不是又一个视频网站古典舞这个方向有个很特殊的场景。B站、抖音上当然有大量古典舞视频但内容混杂学习者的需求其实非常垂直她们需要一个地方可以互相纠正动作讨论某个提沉呼吸的技巧分享原创编舞的思路甚至是记录自己从零基础到能完整跳一支舞的变化过程。所以这个平台的核心价值不在“展示”而在“交流”。围绕这个判断核心功能就该收敛成这么几个作品分享图文和视频、点赞收藏、评论互动、用户关注关系以及一个管理员后台用于内容审核和用户管理。至于像在线约课、直播教学这类功能如果有精力可以做但初期完全没必要加了反而会让数据表和业务逻辑膨胀中期检查时被问到手忙脚乱。1.2 用户角色边界三类人决定了你怎么建表平台至少要有三类角色普通用户舞者/爱好者发布动态、点赞评论、关注他人、修改个人信息。授舞者/导师可以理解为进阶用户除了普通用户权限可能被平台标记为“可被咨询”的角色毕设阶段可以用一个字段区分不必做得很重。管理员审核内容、管理用户、统计平台数据。这个角色划分直接决定了用户表该怎么设计。不是简单搞一个role字段等于1/2/3就完事而是要思考授舞者有没有额外的个人介绍字段普通的舞者有没有“擅长舞种”这种属性这些字段在前期设计表结构时就确定好后面写代码会顺畅很多。2. 技术选型背后的真实考量为什么Spring Boot MyBatis-Plus就够了这套项目市面上流传最广的技术组合是Spring Boot MyBatis-Plus MySQL有的版本带Redis有的版本用Vue做前后端分离也有的是Thymeleaf直接渲染页面。到底怎么选其实取决于你的重点诉求——是要“稳妥过关”还是“冲个优秀”。2.1 单体架构为什么是这个题目的最优解我见过有人为了显得项目“高级”强行上了Spring Cloud Alibaba、Nacos、OpenFeign那一套结果服务拆了三四个每个服务里就两张表最后部署的时候自己都分不清哪个服务该开哪个端口。这个题目本质是一个中小型社区类应用单体架构完全撑得住。你不是去解决高并发也不是去验证微服务的链路追踪答辩老师心里也清楚这一点。正确的做法是在单体架构里把模块边界划分干净user、post、comment、follow、like、upload包结构按业务模块拆而不是按controller、service、mapper这种技术分层去堆。这样既满足了“代码结构清晰”的要求也为将来横向扩展留了口子——真要做微服务了按这些业务模块拆出去就行。2.2 ORM选型为什么推荐MyBatis-Plus而不是原生MyBatis或JPA这个问题几乎必被答辩老师问到。我的建议是选MyBatis-Plus理由特别实际单表CRUD不用写SQL内置的ServiceImpl和BaseMapper能覆盖绝大部分基础操作。分页插件支持得很顺手社区动态列表这种场景几千字代码就能解决。多表关联查询依然可以自己手写Select或XML不会像JPA那样在复杂查询时让你怀疑人生。JPA在实体映射上确实省事但答辩时一个“你怎么控制N1查询问题”就够喝一壶的。MyBatis-Plus至少让你在Mapper层对SQL有绝对的控制权面试时应试也更有底。2.3 鉴权方案JWT为主Session为辅别搞复杂很多毕设源码还在用传统的Session Cookie也不是不行但如果是前后端分离项目跨域Session管理要比想象中麻烦。我更推荐Spring Boot集成简单JWT比如jjwt库服务端签发Token前端每次请求放到Authorization头里用一个拦截器或Spring Security的过滤器校验。这里注意别为了“显得厉害”把Spring Security全家桶拉进来那些过滤器链的配置复杂度绝对能把你剩余的时间耗光。如果你连Redis都不熟就更别一边用Redis存Token一边解释说这是“分布式会话”很容易被问翻车。老老实实用JWT无状态方案把过期时间设为7天答辩时能解释清楚就行。3. 数据库设计别在用户表上耍花招这些细节才是答辩的加分项数据库设计是这套项目里最值得花时间打磨的部分。我帮人改过很多次表结构发现最常见的错误是字段冗余、缺乏逻辑删除、没有统一的时间字段以及最要命的——靠直接在代码里new Date()塞时间而不是用数据库自动填充。3.1 核心表结构拆解以dance_post为例动态表是整个社区功能的灵魂。一张设计合理的动态表直接决定了你后面写“首页Feed流”时的憋屈程度。这里给出一个我在实际项目中反复调整后比较稳的结构参考字段名类型说明idbigint主键雪花IDMyBatis-Plus默认user_idbigint发布人ID建立索引contentvarchar(1000)动态正文纯文本video_urlvarchar(255)视频地址没有可以不填imagesvarchar(1000)图片地址用逗号分隔最多9张categorytinyint分类0基本功1成品舞2原创编舞like_countint冗余的点赞数量后续有优化方案comment_countint冗余的评论数理由同上visibilitytinyint是否公开0公开 1仅自己可见statustinyint审核状态0待审核 1通过 2拒绝create_timedatetime创建时间自动填充update_timedatetime更新时间自动填充deletedtinyint逻辑删除标志MyBatis-Plus逻辑删除字段这里有两个最容易被忽略的点第一like_count和comment_count一定要冗余在动态表上。如果每次都靠count(*)去评论表和点赞表统计数据量稍微一大页面就卡得不能看第二images用逗号分隔存JSON字符串或普通字符串都可以但一定要做好长度校验超过9张图就报错了别傻白甜地让用户无限上传。3.2 关注关系表一张表通吃“关注”和“粉丝”用户关系在毕设阶段不建议建follow和fan两张表正宗的做法是只有一张user_follow表包含user_id主动关注者和follow_user_id被关注者配合create_time。查询某人的粉丝列表时只要把follow_user_id作为查询条件就好。这种设计能让答辩逻辑非常清晰我说“关注”和“粉丝”本质是一对反向关系只是查询视角不同所以用一张表就可以描述全部关注行为。这句话一出基本的表设计功底就有了。3.3 自动填充字段的姿势别用TableField(fill FieldFill.INSERT)就以为万事大吉MyBatis-Plus提供了自动填充功能但很多同学在配置MetaObjectHandler时只处理了create_time忘了update_time结果修改数据时时间不变被老师抓包。标准做法是同时配置两类填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这里我建议顺手把deleted和status的处理也一并思考清楚。逻辑删除字段默认值设为0在表结构里直接在DDL里写上DEFAULT 0别依赖代码层去填免得哪里漏了导致数据莫名其妙消失。4. 社区核心功能发帖、点赞、关注的实现逻辑与缓存加速这一章是全文的实操重点。我会按一个正常用户的操作顺序把发帖、点赞、关注这三个功能的实现逻辑串起来讲每一处都给出明确的代码思路和最终落地的取舍原因。4.1 发帖的消息链路不只是“把内容塞进数据库”发帖功能看起来简单但做成“答辩不出错”是有讲究的。后端接口应该接收表单提交的文本内容加多媒体文件而不是直接接收前端传来的一个拼好的JSON字符串。文本和文件分离的好处是文件上传有独立的大小限制和类型校验逻辑文本内容也有独立的敏感词过滤链路两者互不干扰。我建议的接口设计是PostMapping(/publish) public ResultLong publish(RequestParam(content) String content, RequestParam(value category, defaultValue 0) Integer category, RequestParam(value file, required false) MultipartFile[] files) { // 1. 校验内容长度和分类合法性 // 2. 调用 FileService 判断文件类型并保存上传逻辑见第5章 // 3. 组装 Post 实体写入数据库 // 4. 发布成功后将动态id放入 Redis 的“待审核队列”如果有管理员审核功能的话 // 5. 返回 postId }这里有一个细节点就是visibility。很多人忘了这个字段导致用户没有“私密动态”的选项。这个字段加进去不难但答辩时提一句“我预留了用户对动态可见性的控制”会显得你确实思考过权限这件事。4.2 点赞功能从数据库COUNT到Redis缓存加速的演进点赞是社区项目里最容易被问“高并发怎么办”的功能。如果你在答辩时说“点赞就是往dance_like表里插入一条记录然后UPDATE dance_post SET like_count like_count 1 WHERE id ?”也能跑但显得比较初级。升级版方案是引入Redis缓存。点赞操作的写入走Redis利用Redis的Set结构天然去重的特性把post:like:{postId}作为key用户id作为value就能解决“用户是否已点赞”的判断和“点赞总数”的查询// 点赞 public boolean likePost(Long userId, Long postId) { String key post:like: postId; Boolean added stringRedisTemplate.opsForSet().add(key, userId.toString()); if (Boolean.TRUE.equals(added)) { // 点赞数1 这里直接用hash或string自增也行这里演示string stringRedisTemplate.opsForValue().increment(post:like:count: postId); // 异步或者延后同步到MySQL } return Boolean.TRUE.equals(added); }然后是同步策略。我不会在每次点赞时都去更新MySQL而是用一个定时任务每隔1分钟把Redis中的数据增量同步到dance_post表的like_count字段。这里有个关键点不能直接set like_count 缓存里的值那会把异步期间的其它更新覆盖掉。稳妥的做法是在Redis里记录增量值同步到MySQL时使用UPDATE dance_post SET like_count like_count ? WHERE id ?的方式。为什么推荐这种“双写”方案因为答辩时你能清楚地说明这套系统的数据一致性策略——点赞是高频操作但数据敏感性低允许Redis和MySQL之间有一分钟的短暂不一致这是为了降低数据库压力是典型的“最终一致性”应用。这句话在计算机体系结构答辩里就是送分题。当然如果到答辩还没写Redis也别慌。底层的实现就别用count(*)来查了直接在dance_post表上加一个version乐观锁字段更新like_count时用UPDATE dance_post SET like_count like_count 1, version version 1 WHERE id ? AND version ?也能解释得通。4.3 关注与粉丝用Sentinel还是Simple SetRedis的漂亮用法关注功能在毕设里引入Redis其实很简单。user_follow表照常建但为了显示“我是用了缓存设计的”可以在用户关注时同时往Redis里维护两个Setuser:{userId}:follow我关注了谁和user:{userId}:fans谁关注了我。// 关注 public void followUser(Long userId, Long followUserId) { // 先落库再写缓存 userFollowMapper.insert(new UserFollow(userId, followUserId)); stringRedisTemplate.opsForSet().add(user: userId :follow, followUserId.toString()); stringRedisTemplate.opsForSet().add(user: followUserId :fans, userId.toString()); }这样做的好处是查询“我是否关注了对方”时不需要查数据库直接sismember user:1:follow 2就行。不过一定注意缓存和数据库的一致性最简单的办法是“先更新数据库再更新缓存”并且给所有缓存key设置合理的过期时间比如3天避免脏数据永远留在缓存里。哪怕某次同步失败过期后缓存自然消失会重新回源数据库代价可控。4.4 评论模块别忽视的“分页翻车”高发地评论功能在毕设里容易被忽略但它恰恰是很多人分页写得最烂的地方。千万别把某个动态下的全部评论一次性查出来返回给前端数据量一多页面就卡。更不要用LIMIT 0, 10000这种老掉牙的写法。你要在dance_comment表里设计一个parent_id字段来支持楼中楼子评论。基础评论按主表分页查询子评论不参与翻页点击“展开”时再查。这样逻辑清晰减少一次大查询答辩时还能顺带说明“我做了两层评论结构兼顾了信息层级与查询性能”。5. 视频与图片上传本地路径还是对象存储——一个成熟的毕设该有的样子多媒体上传是这套项目的门面也是最容易在演示时当场翻车的环节。我见过太多人把视频存在项目的resources/static/videos目录下跑起来是能看但只要在IDE里重启或者重新打包文件就丢了因为target/classes目录每次都会被清空重写。5.1 三种方案的利弊对比直接看表格方案优点缺点适合情况本地磁盘路径加速访问upload/目录简单、免费、部署容易文件不在项目里但需做虚拟路径映射换服务器要手动拷贝时间紧求稳过云厂商OSS成熟、防盗链、CDN加速需要购买/试用临时文档复杂还得应付“你怎么保证数据安全”的提问预算充足冲优秀私有化MinIO接口协议兼容AWS S3行为像OSS但能部署在自己服务器需要额外启动一个Java进程部署配置多一点想体现“工程化”和“对象存储”概念但不想花云钱如果求稳我用得最多的还是本地磁盘路径加虚拟目录映射。做法是在application.yml里配置上传路径upload: # 比如 D:/dance-platform/upload/ path: /data/dance-platform/upload/然后设置一个资源映射配置类Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 前端访问 http://localhost:8080/upload/xxx.mp4 时映射到磁盘路径 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这样就算重新部署、重启进程文件依然在服务器指定目录不会因为打包而消失。这是做毕设时最保险的方案。5.2 文件上传的安全审计不只是限制大小那么简单很多源码里只写了MultipartFile接住文件命名用UUID.randomUUID()就没了。但答辩老师只要追问一句“你怎么限制上传文件类型”很多人就开始心虚。正确的实践是至少做三层校验扩展名白名单只允许.mp4,.jpg,.jpeg,.png,.gif其他直接拒绝。文件字节流魔数校验比如JPEG文件头必须是FF D8 FFMP4文件头开箱即不是直接明文但至少可以检查文件是不是非空的二进制。扩展名可以被伪装但文件头不会。大小限制图片单张建议不超过5MB视频单个不超过300MB这个在Spring Boot配置和代码里都要双重限制。视频处理上还有个细节如果做的不是点播平台只是交流分享建议前端用video标签直接播放MP4格式后端不要做转码。真要做转码引入FFmpeg会让整个部署复杂度剧增不是毕设阶段该碰的。可以在答辩时说“针对不同的码率与分辨率兼容问题后续可以考虑引入FFmpeg进行自适应转码”一句话带过既显思考又不给自己挖坑。6. 答辩前必看这套平台的难点应对与“稳拿分”话术最后这部分我想讲讲怎么把做过的功能在答辩时转化为语言优势。技术做得再好不会说也容易吃亏。下面这几个高频问题我给出一套可以直接套用的逻辑和用语。6.1 “为什么不用微服务”很多人被问这句就慌了。标准回答思路是“当前平台的业务体量单体架构已经完全能支持。比如日均PV在几千到几万量级时单体的瓶颈主要在数据库和文件存储而不在服务本身。从成本与运维角度看单体部署更简单降低了交付风险也方便集中维护事务边界。如果未来用户量增长我会按post、user、comment这几个业务域来拆分服务RabbitMQ或Kafka做消息解耦但在现阶段引入微服务属于过度设计。”这套话术既承认了单体架构的合理性又展示了你对未来演进方向有清晰认知。6.2 “Redis里面的点赞数宕机了怎么办”这个问题比“为什么用Redis”问得更狠。回答逻辑是点赞是非关键数据即使Redis重启丢失了一小段时间内的增量数据我们还有一张dance_like明细表里面记录了所有用户的点赞行为。系统启动时可以扫描明细表用COUNT(*)重新构建一遍缓存热点数据或者干脆等首次查询回源。最好的回答是解释清楚数据一致性级别“我允许秒级的不一致但拒绝永久性的数据缺失。Redis起到的是读多写少场景下的缓存加速作用明细表才是一致性的最终兜底。”6.3 “你怎么保障图片和视频内容的安全性”这个问题今年几乎必问。回答时别只说“管理员后台能删”可以这样组织语言内容发布时先进入待审核状态看到的视频和动态都是通过审核之后的管理员后台支持查看、下架和封禁用户另外上传时做了文件类型与大小限制防止服务器被上传可执行文件或被塞满磁盘后续如果要接第三方内容审核API只需要在发布接口处增加一个HTTP同步调用预留了扩展点。6.4 “这套平台还能怎么扩展”扩展方向是一道送分题但很多人答得特别空。我建议结合古典舞的场景说三个很具体的方向第一接入ElasticSearch做全局搜索目前用MySQL的LIKE %关键词%搜动态和用户数据量大了以后效率差可以考虑分词索引第二增加一个“舞种标签”系统配合推荐算法给用户推送可能感兴趣的舞者第三把私信和评论通知抽象成消息中心用Spring事件监听解耦业务逻辑比如有人评论我的动态时异步通知到我的消息列表里。这三个方向都是基于当前业务真实存在的痛点来推的答出来会让人觉得你是真的做过产品思考而不是背答案。最后再分享一个我实际帮改代码时的体会别小看这份“古典舞在线交流平台”的题目它麻雀虽小五脏俱全从数据库建模、文件处理到缓存一致性覆盖了目前中级程序员面试高频考点的八成内容。哪怕你答辩结束也建议把点赞和关注模块的缓存改造动手做一遍这不光是对毕设负责也是在为秋招提前练兵。按照我上面的表结构和思路去整理自己的代码随便哪个老师来问你都能站得住。