SpringBoot+Vue前后端分离开发实战:古典舞交流平台全栈实现
1. 项目背景与整体思路这个项目最开始源于一个很具体的需求一群古典舞爱好者在社群里聊得热火朝天但手头的作品、教学视频、练习心得都散落在各个社交平台上没有一处能真正沉淀下来。有人发一段练习视频还要先导到网盘再甩链接有人想找某个舞种的系统教程得翻几十个帖子更别说想找一位靠谱的舞蹈老师互相交流了。于是就有了做这个古典舞在线交流平台的想法。系统定位不是做视频网站的翻版而是围绕古典舞爱好者的小圈子做一个集作品展示、教学分享、互动交流于一体的垂直社区。技术方案选定前后端分离架构后端用SpringBoot MyBatis MySQL前端用Vue全家桶整体是一套教科书级的主流组合。这篇文章会把整个项目的来龙去脉讲透从表结构设计到接口开发从前端页面到部署上线把我在实际开发中踩过的坑、验证过的方法都整理出来。适合有一定Java和前端基础的读者想完整跑通一套前后端分离项目或者正在准备毕业设计、个人作品集的开发者。1.1 为什么锁定这个选题古典舞这个方向其实是个很好的垂直切入场景。它有明确的用户画像——喜欢古典舞的人群往往对传统文化有认同感有持续的内容消费需求而且非常愿意参与互动和分享。从技术角度来说这类内容平台天然需要用户系统、内容管理、评论互动、搜索筛选、个人主页这些标准模块能覆盖大多数Web开发的核心知识点。对比做一套泛娱乐视频网站垂直领域的古典舞平台反而更容易把功能做深。比如舞蹈视频需要按舞种分类汉唐舞、敦煌舞、昆舞、水袖舞等需要按难度分级入门、进阶、表演级这些细分逻辑让数据库设计和业务接口设计都更有层次感做出来的东西不是那种一眼假的万能模板。1.2 前后端分离架构是必然选择为什么坚持前后端分离而不是传统的服务端渲染最直接的原因是开发效率。前端路由、组件化开发、状态管理这些能力让页面交互的实现成本低很多尤其像视频列表无限滚动、搜索联想、评论区实时回复这种高频交互用Vue做比用模板引擎舒畅太多了。部署层面的优势也明显。前端打包成纯静态文件扔给Nginx后端打成一个jar包独立运行两边互不干扰。后端出问题只需要重启Java进程前端发新版只需要替换静态文件这在后期迭代维护时非常重要。还有一个隐性好处前后端分离之后接口天然就是RESTful风格后续如果要做小程序端或者App端前端整套逻辑都可以复用不用另起炉灶。2. 技术栈选型与架构设计技术在选型的时候我一度纠结过要不要上微服务、要不要用Redis做缓存、要不要引入消息队列。后来把这些想法全砍掉了守住一条底线这个体量的项目用最简单的主流技术栈把它做扎实比堆砌一堆花架子有价值得多。2.1 后端三件套的取舍逻辑SpringBoot负责整体框架装配。它的自动配置机制省掉了大量XML配置内嵌Tomcat让部署变成一条java -jar命令的事。版本我用的是SpringBoot 2.7.x这个版本兼容性很好JDK用8或者11都能跑不会出现JDK版本过高导致各种配置类报错的问题。如果你用3.x版本注意JDK必须升到17有些老项目依赖的第三方库可能还没适配。MyBatis负责数据持久层。虽然JPA写起来更省事但MyBatis对SQL的掌控力是它不可替代的优势。这个项目里有不少复杂查询比如视频列表的多条件筛选按舞种、按难度、按热度、用户的关注与粉丝关系链查询用MyBatis的XML映射文件可以精确定义每一条SQL配合动态SQL标签能优雅地处理各种可选条件的拼接。MySQL负责数据存储。版本选择8.0字符集直接上utf8mb4原因很实际评论区和作品描述里经常出现生僻字和特殊符号比如古琴谱字符utf8mb4才能完整覆盖。存储引擎选InnoDB因为业务里有大量并发读写InnoDB的行级锁和事务支持是必须的。2.2 Vue全家桶选型细节前端用的是Vue 2 Element UI。没上Vue 3的原因很现实这套组合的生态最成熟踩坑的参考资料最多网上随便一搜就能找到解决方案。如果你从零开始学Vue 2的教程资源也最丰富等把这个项目吃透了再平滑过渡到Vue 3完全来得及。Vue Router采用history模式路由设计上要花点心思。页面结构分成访客可见的公共部分作品广场、舞蹈课堂、登录后可见的个人空间我的发布、我的收藏、以及管理员专属的后台管理。这三级路由的嵌套关系要理清楚不然权限控制会越写越乱。Vuex负责状态管理。用户的登录状态、Token信息、用户资料这些全局数据放进Vuex避免组件之间层层传参。还有一个容易忽视的细节页面刷新后Vuex数据会清空需要配合localStorage持久化方案否则用户明明登录过了一刷新就变成游客状态体验非常糟糕。2.3 数据库表设计思路整套系统最核心的是围绕用户和作品展开的几张表。用户表user除了基础账号信息还需要区分角色类型我用的方式是role字段0代表普通用户1代表舞蹈老师可以发布教学课程2代表管理员。这样做的好处是后期加权限拦截时逻辑清晰不用额外建角色关联表。作品表work是这个系统的灵魂。字段设计上预留了封面图URL、视频URL、标题、简介、舞种分类、难度等级、播放量、点赞量、评论量。这里有一个重要的经验统计字段播放量、点赞量、评论量直接冗余在作品表里而不是每次都去count聚合查询。虽然牺牲了一点数据一致性但对读多写少的应用场景来说性能提升是实打实的。数据不一致的问题可以通过定时任务或者异步更新来兜底。互动表comment、favorite、follow各自独立。评论表的parent_id字段支持楼中楼回复收藏表的联合唯一索引user_id, work_id防止重复收藏关注表同样用联合唯一索引防止重复关注。这三张表的设计是典型的社交系统标配做完这个项目你对关联查询和索引设计会有很直观的认知。3. 核心功能模块拆解与实现3.1 用户体系与权限控制注册登录流程用的是JWT方案。用户输入账号密码后后端验证通过就签发一个Token返回给前端前端存在localStorage里。之后的每次请求都在Authorization请求头带上Token后端通过拦截器统一校验。这里有个很关键的细节密码存储绝对不能用明文。我用的BCrypt加密它是加盐哈希算法每次加密结果都不同但校验时能正确比对。有同学自己实现MD5加盐其实没必要Spring Security库里的BCryptPasswordEncoder直接拿来用就行安全系数和易用性都比手写方案好。权限拦截在后端用拦截器实现。写一个TokenInterceptor在preHandle方法里校验Token有效性再根据接口路径前缀判断是否需要特定角色。管理相关的接口统一以/admin开头普通用户就算伪造请求也访问不了这就是前后端分离项目相比于纯前端路由守卫更安全的根本原因——前端的路由守卫只是用户体验层面的控制真正的安全防线在服务端。3.2 舞蹈视频与作品发布模块视频上传是这个模块的重头戏。文件上传接口接收前端传的MultipartFile保存到服务器指定的磁盘目录然后返回访问URL。生产环境下建议把视频文件存放在独立目录与项目代码隔离配合Nginx的静态资源映射对外提供访问。视频上传有几个实用的判断逻辑要加上。文件大小限制用Spring的max-file-size配置我这边限制单个视频500MB超过就报错提示。文件格式用后缀名校验只允许mp4、mov、avi这些常见视频格式。还有一个容易被忽视的点视频文件可能会很大前端上传时要注意配置axios的超时时间默认的几秒肯定不够用我设置的是不限制超时改用进度条组件给用户反馈上传状态。作品发布页面的表单校验也是细节活。标题必填且不超过50字简介不超过500字舞种和难度必须有默认值。我见过不少项目为了省事把非必填字段全部留空提交结果列表页到处都是未命名作品观感极差。这块用Element UI的表单校验规则就能覆盖成本很低。3.3 互动交流功能设计评论模块支持一级评论和二级回复。展示逻辑是先加载作品下的所有顶级评论parent_id 0每条顶级评论下面再加载它的回复列表。查询时我习惯用两次查询先查顶级、再批量查回复而不是一条SQL用子查询硬扛嵌套因为评论回复的层级最多就两层两次查询的性能和代码可读性都比嵌套SQL好。点赞和收藏虽然业务逻辑不同但实现思路一致先查是否存在记录联合唯一索引兜底不存在就插入并把作品表的对应计数加一存在就删除并把计数减一。这里要注意事务的问题插入/删除和计数更新必须在同一个事务里用Transactional注解包起来否则可能出现已收藏但收藏数没变的脏数据。关注功能涉及用户关系链。关注操作做的事情分两步往follow表插一条记录然后异步更新用户的粉丝数和被关注者的关注数。这里我第一次做的时候踩了坑——直接在接口里同步执行两条更新结果在关注量大的时候接口响应明显变慢后来把计数更新的逻辑挪到异步线程池简单用Spring的Async注解就解决了。3.4 后台管理模块后台管理我没有单独做一套前端工程而是把管理页面嵌在同一个Vue应用里通过路由前缀区分。用Element UI的el-menu做侧边栏导航包含用户管理、作品管理、评论管理、数据统计四个页面。用户管理做的事情是查看用户列表、禁用违法账号、重置密码。作品管理是审核新发布的作品主要靠人工看封面图是否有违规、下线违规作品。数据统计页面用ECharts展示近30天的注册量、作品发布量、活跃用户数趋势。管理员页面的数据都通过/admin前缀的接口获取后端拦截器统一校验是管理员角色这套机制很实用。4. 关键接口与代码实现细节4.1 后端接口设计规范整个项目的接口遵循一套统一的返回格式我定义了一个Result类code200成功、400参数错误、401未登录、403无权限、500服务器异常、message提示信息、data业务数据体。所有接口都返回这个统一结构前端拿到后先判断code再处理data。这样做的好处是前端的请求封装变得极其简单axios拦截器里只需要统一处理非200的code弹出对应的错误提示即可。核心接口一览模块接口路径请求方式说明用户/api/user/registerPOST用户注册用户/api/user/loginPOST登录获取Token用户/api/user/infoGET获取当前用户信息作品/api/work/listGET分页获取作品列表作品/api/work/detail/{id}GET作品详情和作者信息作品/api/work/publishPOST发布新作品互动/api/comment/list/{workId}GET获取作品评论互动/api/comment/postPOST发布评论互动/api/favorite/togglePOST切换收藏状态互动/api/follow/togglePOST切换关注状态管理/admin/user/listGET管理员查看用户列表管理/admin/work/auditPOST作品审核4.2 MyBatis动态SQL与分页分页功能用的是PageHelper插件这是MyBatis生态里最成熟的物理分页方案。使用方式很简单在service层查询前调用PageHelper.startPage(pageNum, pageSize)紧接着的查询就是分页查询返回一个PageInfo对象里面包含了总条数、总页数这些分页元数据。插件底层是拦截器机制自动在执行的SQL后面拼接limit子句对业务代码几乎无侵入。动态SQL处理多条件筛选是MyBatis的看家本领。作品列表页的筛选条件有舞种、难度、排序方式最新/最热用XML的where和if标签拼接select idselectWorkList resultTypecom.example.entity.Work SELECT w.id, w.title, w.cover_url, w.video_url, w.category, w.difficulty, w.play_count, w.like_count, w.comment_count, u.nickname as author_name FROM work w LEFT JOIN user u ON w.user_id u.id where if testcategory ! null and category ! AND w.category #{category} /if if testdifficulty ! null and difficulty ! AND w.difficulty #{difficulty} /if /where ORDER BY choose when testsort hotw.play_count DESC/when otherwisew.create_time DESC/otherwise /choose /select这个SQL里有两个值得说道的地方一是LEFT JOIN关联用户表直接查出作者昵称避免在service层做二次查询二是ORDER BY用choose标签做排序逻辑的切换把排序策略放到SQL层面解决就不用在Java代码里拼字符串了。4.3 前端路由与状态管理前端路由的设计要对齐页面的访问层级。我的路由结构长这样const routes [ { path: /, component: Layout, redirect: /home, children: [ { path: home, component: HomeView }, { path: works, component: WorkList }, { path: work/:id, component: WorkDetail }, { path: teach, component: TeachCenter }, { path: user, component: UserCenter, meta: { requireAuth: true } } ]}, { path: /admin, component: AdminLayout, meta: { requireAdmin: true }, children: [ { path: users, component: UserManage }, { path: works, component: WorkManage }, { path: stats, component: DataStats } ]} ]路由守卫的处理要着重注意。全局前置守卫里做两级判断先检查meta.requireAuth的路由用户未登录直接redirect到登录页再检查meta.requireAdmin的路由非管理员账号全部拦截。前端守卫是用户体验的第一道门但就像前面强调的真正安全的门闩在后端拦截器那里。状态管理用Vuex模块划分成user和app两个模块。user模块持有token、userInfo、isAdmin登录/登出都在这里触发action提交mutation。app模块管理一些全局UI状态比如侧边栏折叠、全屏Loading开关。关键点是user模块的持久化每次commit之后同步写入localStoragestore初始化时先从localStorage读取不要用vuex-persistedstate这种额外插件自己写几行代码就搞定了依赖更少还更可控。5. 本地开发环境搭建与常见坑5.1 环境准备清单本地开发环境建议按这个清单准备版本号是最低要求工具版本要求说明JDK1.8或以上推荐用JDK 8稳定性最好Maven3.6依赖管理和项目构建Node.js14前端开发环境MySQL8.0数据库Navicat任意数据库可视化工具也可以命令行IDEA任意后端开发IDEVSCode任意前端开发IDE有两个环境变量容易搞混Maven的MAVEN_HOME指向的是解压目录Path里要追加的是bin目录Node的PATH同理。配置完之后在命令行分别输入mvn -v和node -v验证是否生效。5.2 数据库初始化和后端启动打开Navicat新建数据库名称命名为dance_platform字符集选utf8mb4。然后用系统的SQL脚本文件直接导入建表语句和初始数据。这里有个经验项目里要放一份完整的database.sql文件包含全部建表语句和默认数据管理员账号、测试用户、演示作品这样其他人在本地部署时只需要两步操作——创建数据库、导入脚本全程不超过一分钟。后端启动过程是比较省心的。把application.yml里数据库账号密码改成自己的直接运行主类的main方法。SpringBoot会内嵌启动Tomcat默认监听8080端口。验证启动成功小技巧启动日志里看到Started Application in X seconds就说明成功了一半再访问http://localhost:8080/api/user/info测试接口是否返回JSON。5.3 前端启动和常见问题前端项目先执行npm install安装依赖这个过程是最容易出问题的。主要有两个坑第一个是网络问题导致依赖安装失败。npm install卡在某个包上半天不动最后报ECONNRESET错误多半是网络不稳定。解决方式是换成国内镜像源执行npm config set registry https//registry.npmmirror.com再重新install速度会有质的提升。第二个是依赖版本冲突。有时候网上clone的项目用的是Element UI 2.x你本地npm install装成了最新版页面直接报错。这种情况锁版本就很重要项目里应该把核心依赖的版本写死比如element-ui固定为^2.15.6。启动前端用npm run servedevServer端口默认是8080但后端已经占了8080端口所以要在vue.config.js里把前端端口改成8081同时配置代理转发/api路径到后端服务。这么配置的好处是开发阶段没有跨域问题的困扰axios请求都走相对路径由devServer转发到后端。6. 项目部署实战部署是衡量项目是否真正完工的标尺。我在部署阶段分别试了Linux服务器和Windows服务器两种方案踩了不少坑这里把完整流程和心得分享出来。6.1 Linux服务器部署完整流程Linux环境我用的是CentOS 7 Nginx Jar包的经典组合。整个部署路线是前端打包成静态文件交给Nginx托管后端打成一个可执行Jar包用systemd守护Nginx把/api路径反向代理到本机的Java服务。后端打包这步有一些细节要注意。在项目根目录执行mvn clean package -Dmaven.test.skiptrue跳过测试打包最终在target目录得到xxx.jar文件。这个jar包传到服务器后用java -jar xxx.jar测试运行如果有环境变量或配置问题日志会直接打印出来。确认能跑之后再把它注册成systemd服务保证服务器重启后Java进程能自动拉起。前端打包执行npm run build生成dist目录。整个dist目录传到服务器的/usr/share/nginx/html/下然后修改Nginx配置。我这里贴一份关键配置server { listen 80; server_name your_domain.com; root /usr/share/nginx/html; index index.html; # 前端路由history模式配置 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1: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; } # 视频文件访问映射 location /upload/ { alias /data/upload/; } }这个配置文件里有三个关键点我分别说下踩过的坑。第一history模式下刷新页面会404原因是前端路由是浏览器端的Nginx不知道/works这个路径对应的文件其实是index.html所以必须用try_files把所有路径都兜回index.html。第二/api反向代理后面不能丢掉proxy_set_header相关的头信息否则后端拿不到客户端的真实IP做访问统计或者封禁操作都会失效。第三视频文件我放在/data/upload/通过/upload/映射出去访问这样Jar包和静态文件分开存放日志和排错都很直观。最后用nginx -t检查配置语法通过后nginx -s reload生效。整套部署完成后从浏览器打开服务器IP或者域名前端页面能正常显示接口能正常访问项目就算正式上线了。6.2 Windows服务器部署方案Windows环境下的部署逻辑和Linux一样但具体操作方式有差别。后端Jar包直接用cmd窗口java -jar运行或者用WinSW这类工具把Java进程注册成Windows服务。前端dist目录放进Nginx的html目录后修改conf/nginx.conf配置逻辑和Linux版本完全一样。Windows部署最大的坑是防火墙。默认情况下Windows防火墙会拦截外部对80端口和8080端口的访问需要去高级安全Windows Defender防火墙里新建入站规则放行这两个端口。我遇到过数次程序明明在跑但外网访问不了的情况排查到最后都是在防火墙上卡住。6.3 部署过程踩过的坑部署时最容易遇到的一类问题是环境不一致。本地代码跑得欢部署到服务器就报错最典型的有三个MySQL版本不一致导致的SQL语法错误。本地用MySQL 5.7写的SQL上到8.0可能没问题但反过来就可能报错比如5.7的某些写法在8.0不兼容。解决方案是部署前让本地和服务器用同一个大版本我这边统一用8.0。JDK版本不一致导致的Class版本错误。本地用JDK 17编译的jar包放到服务器的JDK 8环境运行会报UnsupportedClassVersionError。这属于低级错误但特别常见。打包前先检查目标环境的JDK版本用maven的java.version属性控制编译版本。资源路径硬编码问题。本地开发是Windows环境文件路径用D:/xxx/upload部署到Linux就找不到了。这个问题的根治方案是文件存储路径配置化在application.yml里通过自定义属性配一个upload.path线下和线上分别配不同的值。7. 常见问题与排查技巧实录7.1 高频问题速查表把这些年在开发部署过程中遇到的高频问题整理成一张速查表每一项都是真实发生过的场景。问题现象可能原因排查命令/方法前端请求/api报404Nginx未配置反向代理检查nginx.conf中location /api配置跨域报错Nginx代理丢失或者前后端直连用Nginx统一代理避免前端直连后端上传视频报请求超时后端Tomcat未配置大文件超时检查spring.servlet.multipart配置中文乱码数据库字符集不是utf8mb4检查数据库和连接的characterEncoding刷新页面404Vue Router history模式未配置try_filesNginx配置try_files $uri $uri/ /index.html接口返回401Token过期或未携带检查请求头Authorization是否正确端口被占用多个Java进程或者Tomcat冲突netstat -tlnp | grep 8080 查PID视频能上传但播放卡顿Nginx未配置大文件传输缓存location /upload/添加sendfile on; client_max_body_size7.2 连接数据库的典型报错数据库连接这块有几个报错几乎每个做这个项目的人都会遇到。java.sql.SQLException Access denied for user rootlocalhost多半是密码配错了或者用户没有远程访问权限。MySQL 8.0默认的认证插件是caching_sha2_password有些老版本的JDBC驱动不支持会报Public Key Retrieval is not allowed解决方法是连接字符串里加上allowPublicKeyRetrievaltrue。另一个高频问题是Communications link failure。如果确定数据库地址和端口没写错优先检查服务器防火墙有没有放行3306端口。在本地开发时服务器MySQL没开的话直接终端连接报错这个判断很简单。7.3 性能优化思路与实操项目跑起来之后性能优化是让系统真正能用的关键一步。最基础也最见效的优化是给热点查询加上合理的索引。作品列表页按播放量排序的场景给create_time加普通索引效果不大但给play_count加上索引后ORDER BY play_count DESC的查询可以从全表扫描变成索引扫描。评论列表按作品ID查询的场景给comment表的work_id加上索引必须要有。用MySQL的EXPLAIN命令分析慢查询SQL是个值得长期坚持的习惯。执行EXPLAIN SELECT...之后重点看type列和rows列如果type是ALL就是全表扫描说明索引没生效需要回头检查索引设计。这套方法能让你把每条核心SQL的查询计划都摸得门儿清对索引的理解会提升一个档次。8. 代码层面的部分优化与一点经验总结整个项目做完回头复盘的时候发现有几个优化点是提高代码质量的关键。第一是全局异常处理的统一设计我写了一个GlobalExceptionHandler用RestControllerAdvice统一捕获业务异常和未知异常返回标准的Result格式。这个设计的价值在于接口层永远不用再写try-catch业务代码的异常抛出去后会自动被这个处理器接住并规范化代码整洁度大幅提升。第二是数据库层的扩展思维。现在收藏数、点赞数这些计数直接存储在作品表字段里做大之后可以考虑引入Redis缓存计数或者用异步消息队列来降低数据库压力。但这里的取舍要想清楚项目初期最需要的是逻辑直观和快速上线过度设计带来的复杂度远大于收益等真正出现瓶颈了再引入缓冲方案也不迟。第三是配置分离的思想。环境相关的配置数据库连接、上传路径、日志级别统统放在application.yml并且区分application-dev.yml和application-prod.yml两套配置用spring.profiles.active参数来切换。大家创建项目的时候用一套配置跑到底后期部署时被环境差异坑一次才会真正理解配置分离有多重要。我在做这个项目的过程中最深的体会是前后端分离项目真正的工作量不在写代码本身而在把业务的边界想清楚。前端只关注页面和交互后端只关注数据和逻辑两者通过规范的接口契约协作这个边界一旦模糊就会陷入前端等着后端改字段、后端等着前端调接口的泥潭里。所以接口文档一定要在开发前定清楚哪怕只是简单在笔记软件里列个表格也比开发到一半再口头协商效率高得多。这个平台系统的价值在于它覆盖了一个完整业务系统从设计到上线全链路的核心问题把这些经验消化掉后面不管做什么类型的Web系统都能少走不少弯路。