
前后端分离这四个字在现在的招聘需求里几乎快成了标配但真正能把一套项目从数据库设计、接口联调、打包部署全程走通的人其实不算多。我上个月帮本地技术社群做了一个IT交流和分享平台用的正好是SpringBootVueMyBatisMySQL这套栈从零开始写跑通后部署到服务器上给几十个群友在线体验整体效果还算可以。这篇我就把完整源码的使用方式、选型思路、数据库设计、核心代码和部署细节全部拆开讲保证是可以直接照着复现的那种。先说结论如果你正在找一套能拿来练手、能改改就上线的前后端分离项目这套系统值得参考如果你已经在用若依之类的开源脚手架这篇文章也能帮你理解脚手架背后那些通用模块是怎么设计的。后续按“为什么这么选型—数据库怎么设计—后端怎么实现—前端怎么开发—怎么部署上线—遇到问题怎么排查”的顺序写每一步我都会带上实际用到的配置和参数而不是只讲空泛的概念。1. 项目定位与技术选型为什么是这套组合1.1 交流平台核心功能拆解从标题看这是一个“IT交流和分享平台系统”本质上就是一个垂直技术社区类似一个小型论坛。我做的版本功能边界是这样的用户端支持账号注册登录、个人信息维护、帖子发布和编辑、按板块分类浏览文章、关键词搜索、评论互动、点赞收藏以及一个资源分享模块可以上传PDF、压缩包等文件供其他用户下载管理端则负责用户管理、帖子审核与置顶、板块分类维护、资源审核下架等。为什么要把功能边界先定死这是做项目管理很关键的一步。社区类系统的功能可以无限堆叠比如私信、关注、积分、动态流但一套用于学习和复现的项目功能多了反而容易把核心逻辑淹没。我刻意只保留一个内容型社区最主干的闭环注册登录、内容发布、内容消费、互动反馈、管理审批。这个闭环能把绝大部分技术栈的典型问题都覆盖到又不会让人看代码时晕头转向。1.2 为什么选SpringBootVueMyBatisMySQL这套组合能成为国内中小型前后端分离项目的“事实标准”不是没有道理的。SpringBoot的价值在于它把Spring生态里大量的样板配置自动化了内嵌Tomcat本地写代码时直接mvn spring-boot:run就能起来不像老SSH项目要先装外部容器再打war包。Vue的优势是组件化和响应式数据绑定写页面状态管理比直接用jQuery操作DOM清爽太多而且Vue的学习曲线对后端转前端的开发者相对友好。MyBatis的定位是半自动ORMSQL到底怎么写完全由自己控制特别适合社区类项目里大量“按条件动态查询”的场景写起来比JPA那种自动生成的查询直观也更容易调优。MySQL做中小型应用的数据存储完全够用部署也方便一台普通云服务器就能跑得很稳。有人可能会问为什么不直接用MyBatis-Plus我用它就是图省事如果自己从零搭要手写BaseMapper、分页插件、逻辑删除工作量会大不少一个简洁的框架在这里作用有限。实际选型时我保留了原生MyBatis的xml方式写复杂SQL同时用MyBatis-Plus内置的CRUD方法处理简单的单表操作两者混用并不会冲突关键是分页插件要配置正确。还有一点这套组合网上能搜到的排错资料非常多遇到问题不会卡死对初学者友好。1.3 相对传统单体模板页方案的优势传统的JSPServlet模板页方案前后端代码混在一起每次改个页面样式都要重启应用前端工程师想并行开发几乎不可能。前后端分离之后Vue项目独立开发、独立构建后端只提供JSON接口两边通过HTTP协议对接开发效率提升非常明显。放到部署层面前端构建出来的静态文件可以用Nginx直接托管后端服务就是独立的Java进程哪个挂了单独重启哪个互不影响线上排查问题的思路也清晰得多。2. 数据库设计与MyBatis使用要点2.1 核心表结构设计与字段说明这套系统的数据库我拆成了六个核心表用户表sys_user、板块表biz_category、帖子表biz_article、评论表biz_comment、点赞收藏表biz_like、资源分享表biz_resource。用户表主要字段是id、username、passwordBCrypt加密后存储、nickname、avatar、role区分普通用户和管理员、status账号是否冻结。帖子表是内容区的核心包含id、category_id、user_id、title、content、view_count、like_count、comment_count、top_flag是否置顶、status待审核/已发布/已下架create_time等。这里有几个设计细节值得说明。第一帖子的点赞数和评论数一定要冗余在帖子表里而不是每次都在统计子表里count否则列表页的排序和展示性能会很差。第二评论表用parent_id表示父评论直接回复楼层和“回复某人的回复”都可以通过parent_id串联省去额外的主题回复表。第三表名加biz_和sys_前缀是区分业务表和系统表这个习惯在团队协作时特别有用一眼就能看出表归属哪个模块。每个表都建议建好索引帖子表的category_id、create_time、status是高频查询条件必须加索引评论表的article_id是典型外键查询字段同样需要索引。数据库编码建议统一用utf8mb4而不是utf8因为utf8在MySQL里最多存3字节的字符遇到表情符号和生僻字会直接报错IT社区里用户头像昵称出现特殊字符的概率非常高utf8mb4才能完整覆盖。排序规则用utf8mb4_general_ci就够除非你明确需要中文拼音排序才考虑utf8mb4_unicode_ci。2.2 MyBatis分页插件和缓存配置的坑MyBatis分页如果自己用LIMIT写每个查询都要拼参数还要算total太累。这里直接用官方推荐的MyBatis-Plus分页插件。需要注意的是新版的SpringBoot用MybatisPlusInterceptor来注册分页插件写法如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setOverflow(false); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(100L)的意思是单页最大查100条防止有人手滑传一个极大的pageSize把数据库拖垮这个防御性设计建议所有人都加上。分页插件拦截的是Mapper层的Executor所以它要求分页查询的Mapper方法返回值必须是IPageT参数里带上Page对象否则不会生效。我第一次写的时候忘了改方法签名调试半天分页就是不生效SQL里死活不出现LIMIT后来检查才发现拦截器没匹配到方法。再说缓存。MyBatis自带一级缓存和二级缓存。一级缓存是SqlSession级别的同一次会话里重复执行相同SQL会直接命中但Spring整合后每次Mapper操作默认走的是不同的SqlSession所以一级缓存基本等于没用。二级缓存是namespace级别的可以跨SqlSession共享但社区类系统的数据实时性要求比较高帖子一发出来别人就要立刻看到开二级缓存容易读到旧数据还要处理缓存刷新策略。我的建议是默认不开启Mapper的二级缓存后续数据量真上去了再引入Redis而不要依赖MyBatis自己的二级缓存去顶高并发那个方案在分布式多实例部署时还会出现实例间缓存不一致的问题。3. SpringBoot后端实现与关键配置3.1 项目分层结构与统一响应体后端我采用标准的controller-service-mapper三层结构实体类直接用MyBatis-Plus的注解映射比如TableName(biz_article)、TableId(type IdType.AUTO)。所有接口返回统一封装成一个Result对象包含code、message和data三个字段。为什么要统一因为前端Axios拦截器只需要判断一次code就可以统一处理业务错误、登录失效、参数校验失败不用每个页面单独写错误判断逻辑。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }全局异常处理用RestControllerAdvice配合ExceptionHandler。Controller层不要到处写try-catch业务校验抛出自定义异常全局异常处理器统一捕获然后转换成Result返回。这样Controller的方法签名能保持很短只负责接收参数和调用Service。3.2 JWT登录认证与权限控制登录功能用JWT无状态token方案。用户登录成功后服务端根据用户ID和角色生成一个token返回给前端前端把它存在localStorage里每次请求时通过Axios请求头Authorization: Bearer token带给后端。后端写一个拦截器在进入Controller之前解析token并把用户信息放进ThreadLocal后续业务代码随时可以取到当前登录用户。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { String jwt token.substring(7); try { Claims claims JwtUtil.parseToken(jwt); UserContext.set(claims); return true; } catch (Exception e) { // token无效或过期 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录已过期请重新登录\}); return false; } }管理端的权限校验会再严一层我是在拦截器解析出用户角色之后再根据接口路径前缀做判断比如/admin/**开头的请求要求角色必须是ADMIN否则返回403。JWT密钥和过期时间放在application.yml里生产环境密钥一定要足够长别用网上demo里的默认密钥不然别人很容易伪造token。3.3 文件上传和静态资源映射资源分享模块需要上传文件SpringBoot默认单文件最大1MB这个限制对技术文档和工具包来说太小了。我在配置里调整成了单文件最大20MB总请求大小50MBspring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB文件上传后不能直接放项目目录里因为打成的jar包重启后写入的文件可能会丢失而且项目目录扩容也麻烦。我统一把文件存到服务器磁盘的独立路径比如/data/upload/通过自定义的WebMvcConfigurer做一个静态资源映射让/files/**这个URL能访问到磁盘目录。这样上传和访问都很干净后续想迁移到OSS也只需要改一个上传方法和一个访问路径生成逻辑。3.4 两种部署形态的适配源码里的后端默认是jar包形态本地测试环境直接java -jar就非常顺。如果有人手头只有Tomcat需要打war包部署源码这边也做了兼容启动类继承SpringBootServletInitializer并重写configure方法同时在pom里把打包方式改成war、排除内嵌Tomcat依赖。两套脚本我都准备好了jar包适合配Nginx的场景war包适合沿用旧服务器里现成Tomcat的场景哪个环境顺手用哪个。启动类改war包的写法是这样的SpringBootApplication public class CommunityApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(CommunityApplication.class, args); } Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(CommunityApplication.class); } }4. Vue前端实现与接口联调要点4.1 环境准备与项目初始化前端用的是Vue 2.7加Element-UINode版本建议14以上。不少人卡在第一步其实是Node版本不对或者npm安装依赖时网络太慢安装依赖时如果官方源拉包慢可以把npm源换成国内镜像源命令是npm config set registry https://registry.npmmirror.com实测下载速度能快很多。项目结构上Vue项目里我按api、router、store、views、components来组织代码。每个页面模块的接口请求单独放在api目录下比如article.js、user.js、resource.js这样做的好处是接口路径集中管理后续后端接口变了只需要改一个文件。4.2 路由设计与权限控制路由设计上前端分为公开路由和需要登录的路由。公开的可访问首页、文章详情、板块列表需要登录的包括发布文章、个人中心管理端的路由单独放在一个子路由里由路由守卫判断用户角色。Vue路由传参有两种常用方式一种是在path里带参数比如/article/detail/12页面用this.$route.params.id拿另一种是query方式适合列表页跳详情页带上来源信息比如/article/detail?id12fromhot。社区类系统建议用params方式URL更干净也方便分享。路由守卫里要处理一个经典问题刷新页面时vuex里存的用户信息会丢因为store是内存态。解决方法是把用户信息同步存一份到localStorage刷新后路由守卫里先取localStorage的数据恢复登录态再向后端拉一次最新的用户信息做校验。如果后端返回401就清空本地登录态并跳转登录页。4.3 Axios请求封装与跨域处理Axios封装的核心是request.js这个模块。baseURL我用相对路径/api因为开发环境的代理和生产环境的Nginx反向代理都对这个前缀做了特殊处理前端代码可以无缝切换环境。请求拦截器里加上token响应拦截器里统一判断HTTP状态码和业务code遇到401直接跳登录遇到500弹出错误message。开发环境的跨域问题通过vue.config.js的devServer代理解决不需要后端代码做任何CORS跨域处理。代理配置如下// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };这里有个细节值得注意后端接口本身没有/api前缀前端请求要写/api/user/login代理转发时把/api去掉再转发到后端8081端口。生产环境同样在Nginx里做一次反向代理前端代码一个字都不用改。4.4 核心页面与组件的实现思路首页我做了两个模块置顶公告区和帖子列表区。帖子列表用分页组件配合后端返回的总数渲染每次切换页码调用一次接口。文章详情页是重点用watch监听路由参数变化因为同一组件复用时会触发“从一篇帖子切换到另一篇帖子”的场景不监听路由变化的话页面内容就不会刷新这是Vue开发者常踩的坑。评论区按时间倒序加载登录用户才能发表回复回复成功后更新帖子详情里的评论总数。发布页面是一个带背景图的富文本编辑器我用的是简洁的markdown编辑器组件因为IT社区的内容经常有代码块markdown的体验比普通富文本好。个人中心展示自己发布的帖子、收藏列表、评论记录后台管理页做成Tab页形式统一封装了表格组件和状态标签组件代码复用率很高。5. 完整部署实战从源码到线上环境5.1 环境准备清单上线前把环境列一个清单Linux服务器一台2核4G起步、JDK 1.8或11、Maven 3.6、MySQL 5.7或8.0、Node 14打包前端用打完包后服务器不需要Node、Nginx 1.20。MySQL安装这里多说一句Linux上用yum install mysql-server或apt-get install mysql-server安装完后默认root用户可能只能本地登录且密码策略很严建议安装后先用mysql_secure_installation初始化一次设置好root密码再创建业务账号给项目用不要用root账号跑应用。5.2 数据库初始化把项目里的community.sql用source命令导入即可也可以先mysql -u账号 -p进入命令行然后用source /usr/local/community.sql;导入。导入之后验证一下表结构和初始数据是否完整重点看sys_user里有没有初始管理员账号。数据库初始化完成后修改后端application.yml里的数据源配置把url、username、password都改成生产环境的特别注意url要带上useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai参数否则容易出现中文乱码和时区差8小时的问题。5.3 后端打包与启动后端打包命令是mvn clean package -DskipTests跳过测试是为了减少打包时的不确定因素。打包完成后target目录下会生成jar文件用nohup java -jar community-server.jar --server.port8081 app.log 21 启动。8081是我给后端选的生产端口跟Nginx的80端口区分开。启动后看一眼app.log确认没有异常后在服务器本地用curl http://localhost:8081/api/user/info测一下接口通不通。环境变量也是一个好习惯数据库密码这种敏感信息我在生产环境用--spring.datasource.passwordxxx覆盖配置文件里的默认值避免把真实密码写进代码仓库。5.4 前端构建与Nginx配置前端构建命令是npm run build构建完会生成dist目录。注意构建前要检查vue.config.js里publicPath如果你的应用不是部署在域名根路径而是部署在子路径比如/community/publicPath就要改成/community/否则静态资源的CSS、JS路径全是404。把dist目录上传到服务器我用的是/usr/share/nginx/html/。Nginx配置关键块如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /files/ { proxy_pass http://localhost:8081/files/; } }try_files $uri $uri/ /index.html这一行必须有因为Vue Router用的history模式刷新页面时如果请求的是/article/detail/12后端静态目录里没有这个文件Nginx就会把它重写到index.html交给前端路由处理。这是部署前后端分离项目最容易踩的坑不写这行所有深层链接一刷新就是404。5.5 一键部署与版本回滚部署次数多了之后我写了一个简单的部署脚本里面依次执行拉代码、构建后端、构建前端、备份旧版本、重启服务这几个动作。备份很重要每次发布前把当前jar包和dist目录复制一份带时间戳的备份一旦新版本有问题回滚很轻松。脚本的核心逻辑就几十行但能避免反复输入命令造成的低级失误。6. 实际运行中遇到的高频问题与排查实录6.1 登录失效和token过期的问题测试环境一切正常上线后用户挂机一晚上第二天再操作就提示登录过期这个属于预期行为。真正麻烦的是用户在发布长文时写完已经过了一个多小时点击提交时token已过期文章内容全部丢失。我在前台加了一个登录状态预检用户在打开富文本编辑器的时候就先查一次token过期时间剩余时间不足10分钟时自动静默刷新token前端拿到新token后更新本地存储把一次糟糕的用户体验消弭于无形。6.2 跨域和404问题开发环境跨域报错99%是vue.config.js的代理没生效检查一下是不是改了/api路径但没有重写代理或者后端接口确实没有/api前缀。生产环境出现跨域则基本都是Nginx配置漏了proxy_pass或add_header不过只要所有跨域请求都走反向代理后端完全不需要开启CORS。404分两种一种是页面404就是没配try_files另一种是接口404可能是Nginx把/api转发到了错误的端口或者后端接口路径拼错。6.3 MyBatis的相关问题好多人在MyBatis里遇到参数不生效、SQL查不出结果的问题。最基础也最容易被忽略的是xml文件里的#{param}和前端传参名的对应关系多参数方法最好用Param注解显式声明参数名避免依赖编译时的参数名保留开关。另外建议在开发环境把SQL打印打开方案是加配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开之后每个SQL的预编译语句和参数值都会打印到控制台定位拼接问题非常快。如果你遇到某个Update语句执行很慢先用EXPLAIN看查询计划大概率是更新条件字段没走索引或者表里数据量上来了没重建统计信息。还有一个小细节批量更新或插入时尽量用批量执行不要一条条循环调用Mapper否则数据库连接开销会非常明显。6.4 Vue打包后页面空白与依赖安装失败打包后页面空白通常就是publicPath配错了。如果在本地devServer下正常打包上线空白先F12看控制台的资源加载路径路径不对就把publicPath改成./或具体的子路径。依赖安装失败比较多样常见node_modules缓存出了问题删除整个node_modules和package-lock.json后重新npm install能解决大部分。网上还有不少把源码download下来之后直接build报错的情况多数是node版本跟项目依赖要求的版本不一致建议尽量固定Node大版本再操作。6.5 部署阶段的服务端口和内存问题SpringBoot默认8080端口跟Vue devServer也用的是8080本地联调时容易把两个服务端口搞混。我建议一开始就约定前端devServer用8080后端接口用8081。部署到服务器时因为Nginx占了80后端继续占8081各干各的谁都不用改。还有服务器的内存2G内存跑MySQL加一个Java服务是够的但启动时如果报OutOfMemoryError可以在启动命令里加-Xms256m -Xmx512m限制JVM堆内存防止内存不够把进程直接杀掉。6.6 数据库连接和文件存储的坑MySQL长连接空闲久了会被服务端断开应用会报“Connection is not available”之类的错误。解决方法是给数据源连接池配置空闲超时时间检查一下Druid或HikariCP的配置参数。HikariCP自身能通过maximum-pool-size和idle-timeout来控制建议maximumPoolSize不要超过20连接数开得过大反而会拖垮MySQL。文件存储的坑在于路径问题。本地Windows上路径可能是D:/upload/服务器Linux上是/data/upload/如果配置文件没分开维护上传文件就会全部失败。我在application.yml里用file.upload-path这个自定义配置统一管理本地和服务器各写各的值打包后通过启动参数覆盖从来没有出过错。7. 最后分享一点实际运营中的体会整套系统从开发到上线最深的感触是技术选型没有那么多花活选熟不选生选稳不选炫。SpringBootVueMyBatisMySQL这套组合贵在资料多、社区大、招人容易遇到任何问题都能快速找到答案这就是它作为“项目标配”的价值。真正决定项目能不能长期跑下去的往往是那些容易被忽略的细节比如索引有没有建、文件路径有没有分离配置、Nginx的try_files有没有写、token过期有没有做静默刷新。每一处小事都会在线上变成事故提前处理能省几宿觉。如果你准备拿这套源码去学习或者二次开发我的建议是先在本机完整跑通一遍再上服务器。本机跑通能让你快速建立对全链路的感觉哪个模块调哪个接口、数据怎么流转心里有数之后再去改代码效率会高很多。后续扩展方向也很多数据量大了可以引入Redis缓存热门文章和验证码文件可以迁移到OSS搜索可以接入Elasticsearch文章编辑器还可以扩展视频教程播放能力这些都是在这个基础架构上顺理成章的事。项目是死的思路是活的希望这篇记录能帮你少走点弯路。