我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

SpringBoot+Vue3实战:从数据库设计到部署的IT交流平台

SpringBoot+Vue3实战:从数据库设计到部署的IT交流平台 1. 项目缘起为什么我要自己搭一个IT交流分享平台这个念头其实酝酿了很久。平时逛技术社区总感觉要么是算法推荐太吵要么是评论区的氛围不太对味很多一手踩坑记录发出去没多久就沉底了。再加上当时正好在系统学习SpringBoot和Vue3后端写过一些小接口前端能画基本的页面就想拿一个完整的全栈项目把自己所学串起来——做一个带用户体系、帖子管理、评论互动、标签检索的IT交流和分享平台。技术栈就定为Java SpringBoot Vue3 MyBatis MySQL前后端分离数据落到MySQL里。做完第一版之后我的感受是这个项目作为练手非常合适既能覆盖后端从接口设计到数据库建模的完整链路又能把前端路由守卫、请求封装、状态管理这些东西落到真实场景里。如果你也是学完框架基础、想做点真正能跑起来的东西这个项目可以作为参考模板。整篇文章会按照我实际开发的顺序来讲重点放在数据库设计、后端分层、前端基建、联调问题、部署收尾这几块每一块都会给出我最后采用的方案和放弃其他方案的理由。有一点我先说明白项目定位是“交流和分享”不是做又一个高并发社区。所以整个系统的架构设计都围绕“够用、清晰、好改”这几个字来。比如点赞用一张独立的关联表来做幂等而不是在帖子上加一个count字段直接加减你会发现这种看似绕路的做法在联调和排错的时候反而更省心。2. 数据库先行交流平台的表结构设计与索引取舍很多初学者拿到一个项目就急着写Controller我个人的建议刚好相反先把表结构定下来再动代码。因为表设计决定了接口形态接口形态决定了前端页面怎么拿数据。在动手写SpringBoot之前我花了整整两天梳理第一版的功能清单和表结构。2.1 核心表拆分用户、帖子、评论、标签到底该怎么放整个平台最核心的几类数据就是用户、帖子、评论、标签、点赞、收藏。我最终建了7张表用户表、分类表、帖子表、评论表、标签表、帖子标签关联表、点赞记录表。这里最有讨论价值的是“标签”到底怎么存。第一种方案是帖子表里直接放一个varchar字段存逗号分隔的标签字符串比如Java,SpringBoot,MySQL。这种方案查询某个标签下的所有帖子时只能用LIKE %标签%去匹配标签一旦多起来索引失效全表扫描数据量上来之后体验直线下降。第二种方案是单独建标签表再用一张中间表把帖子和标签关联起来这套设计是经典的“多对多”建模虽然查询的时候要多join一次但标签的精确匹配、计数统计、后续扩展都干净得多。我选了第二种。再具体看每张表的职责。用户表存账号、密码密文、昵称、头像地址、个人简介、状态分类表是顶层的帖子大类比如Java、前端、数据库、职场帖子表关联用户和分类存标题、正文、浏览数、置顶标记评论表关联帖子和用户用父评论ID支持楼层回复中间表就简简单单两个外键字段不做额外操作。这里要特别提醒一点正文不要用varchar要直接用LONGTEXT我在开发中遇到过帖子正文超过2万字符被截断的情况排查了半天才想到是字段类型的问题。2.2 关键字段设计细节从时间字段到点赞幂等这一小节值得仔细看因为踩坑往往都藏在字段层面的小选择里。时间字段我统一用datetime而不是timestamp。两者看起来都能存日期时间但有区别timestamp的取值上限到2038年而且受MySQL时区参数影响在不同环境下部署查出来的时间可能差几个小时。datetime不依赖时区存什么就是什么配合Java端的LocalDateTime类型前后端传输时的格式更可控。博主之间交流的时候经常提到“8小时时差”多半就是timestamp在时区设置不同的服务器之间迁移导致的。点赞表的设计我特意做成了一张独立的“事件表”字段包括主键、帖子ID、用户ID、创建时间。表里加一个唯一索引组合字段是(post_id, user_id)。这样做的好处是点赞和取消点赞不需要在帖子表里同步加减一个count字段而是走“先查询再插入或删除”的逻辑唯一索引从数据库层面保证了同一个用户对同一篇帖子只能有一条记录天然具备幂等性。帖子页面上显示的点赞数直接在主表上冗余一个like_count字段每次点赞操作成功后UPDATE一次这样列表查询不需要每次都COUNT(*)整张点赞表。索引设计这块我在MySQL里给外键字段都补了索引。MySQL的InnoDB引擎虽然会自动给外键建索引但如果我只建逻辑外键、不建物理外键约束就必须手动给post_id、user_id、category_id这些高频查询字段加上普通索引。列表页的查询条件主要是分类、置顶状态、创建时间所以加了一个联合索引(category_id, is_top, created_at)实际测试里列表接口的响应从300多毫秒降到了50毫秒以内效果非常直观。3. 后端SpringBoot落地分层规范与认证授权的核心逻辑后端这一层我尽量按照“一个完整项目”的标准来要求自己不是随手写一堆能跑的接口就完事。项目分包、统一返回结构、全局异常、JWT认证、MyBatis动态SQL这些看起来是工程上的细活但恰恰决定了一个项目能不能持续迭代下去。3.1 项目分包与统一返回结构避免写到后面自己都看不懂SpringBoot项目的基础分层基本是entity、mapper、service、controller加上dto、vo和config。我的分包方式是在com.example下面直接按controller、service、mapper、entity、dto、vo、config、util、interceptor来分。很多教程喜欢按业务模块分包也就是user、post、comment各自带一套完整的三层结构两种方式各有优点我选择按技术层分包是因为项目规模不算大按层分包结构更简单新人接手也更容易理解数据的流转链路。统一返回结构是前后端分离项目里非常值得做的一件事。这个项目里我定义了一个通用响应类包含三个字段code、message、data。code为0表示成功非0表示业务错误比如401表示未登录、403表示无权限。前端每次请求只用判断code是否为0不用关心HTTP状态码到底返回的是200还是500逻辑会清爽很多。统一结果类大概长这样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(0); 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; } }配合全局异常处理器可以在Service层直接通过抛出业务异常来返回错误信息。比如发帖时如果标题没有填就throw new BizException(标题不能为空)全局异常处理器捕获后封装成Result.error(...)返回。这样Controller层就非常薄只需要接收参数、调用Service、返回结果不需要每一段都写try-catch。3.2 JWT登录认证的实现要点token怎么发、怎么验、怎么续用户登录模块是几乎所有平台类项目都要做的也是前后端分离场景下最容易出问题的一块。在Session和JWT之间我最终选择了JWT。原因不是它比Session更安全而是JWT天然适合前后端分离、服务端无状态的存储需求后端不需要维护会话记录前端只要在请求头里带上token即可。生成token的逻辑集中在工具类里。我在token的载体中放的数据只包含用户ID和用户名不放入敏感信息。token设置一个合理的过期时间这个项目里我设的是2小时。有朋友建议设成7天说用户免登录方便但安全性和体验是矛盾的2小时过期配合前端刷新机制既不会频繁让用户重新登录也不会让一个token在泄露后长时间有效。后端校验token的方式是实现一个Spring MVC的拦截器把所有需要登录的接口路径拦截下来从请求头里取出Authorization字段解析并验证token。如果token缺失或者过期直接返回401错误码。有些接口是“推荐登录但不强制”比如查看文章时如果登录了就显示当前用户是否点过赞没登录就不显示这种我单独用一个可选拦截逻辑处理不直接拒绝请求。有个细节值得说JWT的过期时间校验是精确到秒的但前端请求有网络耗时如果token恰好在一个请求发出后过期就会出现明明刚登录成功一会儿就提示登录失效的情况。我的处理方式是前端在axios响应拦截器里拿到401时立即尝试调用一个刷新接口用未过期的旧token换新token再重放原请求。这个机制在开发中很常见但初学者很容易忽略。3.3 MyBatis动态SQL实战多条件搜索与帖子详情统计MyBatis在这个项目里的主要场景是写动态SQL。我用的是MyBatis的XML映射文件方式而不是注解方式。注解方式适合简单的单表操作但这个平台的帖子列表需要根据分类、标签、置顶状态、关键字做动态拼接查询XML文件里的where和if标签非常合适。典型的多条件查询SQL是列表页的核心。比如帖子列表接口接收的参数可能包括分类ID、标签ID、关键字。分类和关键字直接用条件拼接标签ID因为是多对多关系需要先join到帖子标签表再过滤。SQL骨架大概是select idlistPosts resultTypecom.example.vo.PostVO SELECT p.id, p.title, p.summary, p.view_count, p.like_count, p.created_at, u.nickname AS authorName, u.avatar AS authorAvatar FROM post p LEFT JOIN user u ON p.user_id u.id if testtagId ! null INNER JOIN post_tag pt ON pt.post_id p.id AND pt.tag_id #{tagId} /if where if testcategoryId ! null AND p.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (p.title LIKE CONCAT(%, #{keyword}, %) OR p.summary LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY p.is_top DESC, p.created_at DESC /select这里有一个很容易踩的坑如果tagId和keyword同时存在INNER JOIN和where中的条件必须写对位置否则SQL拼接结果可能变成“先join再过滤”。看起来没什么大不了但如果关联字段的索引没建好网络上去之后这个接口会非常慢。帖子详情页的统计信息包括浏览数、点赞数、评论数。浏览数我选择直接在查询详情时执行一行UPDATE post SET view_count view_count 1 WHERE id ?。这个方式在并发量不高的小平台没有问题但如果你预期未来有较大并发建议把浏览数增加操作放到独立的计数表或消息队列里异步处理避免每一次详情请求都锁住帖子主表的一行。4. Vue3前端核心基建从请求封装到登录态管理Vue3这一侧我用了标准的技术组合Vite做构建工具、Vue Router做路由、Pinia做状态管理、Axios做HTTP请求。这个组合现在是Vue3生态里的主流配置资料多、坑也有相对成熟的解决方案。4.1 前端项目结构与路由设计页面拆分和权限控制前端项目的目录结构我会按页面和通用模块来分views目录放页面级组件比如首页、帖子详情、发布页面、登录页面、个人中心components目录放可复用的通用组件比如帖子卡片、评论列表、分页条api目录按后端接口模块拆成不同的请求文件stores目录放Pinia的storerouter目录放路由配置和守卫。路由设计上这个平台分为两种页面公开页面和需要登录的页面。公开页面包括首页、帖子详情、搜索结构需要登录的页面包括发帖、编辑个人资料、我的收藏。实现方式是在路由配置里给需要登录的路由添加meta.requiresAuth: true然后在全局前置守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })登录后跳转回原页面的逻辑就是上面query里redirect参数的作用。这个小细节直接决定了用户体验如果用户被登录页拦住登录成功后就该回到他原本想看的页面而不是丢回首页。4.2 axios封装与token刷新别让每个页面重复写请求逻辑Axios的封装是整个前端项目里最重要的基建之一。一个平台类的项目几乎每个页面都要发请求如果每个页面都单独写axios.get带headers代码会非常冗余且难以维护。我封装了一个统一的请求模块核心做了三件事。第一件是请求拦截器每次请求自动把本地存储的token加进请求头Authorization。第二件事是响应拦截器先判断HTTP状态码如果是401就去调刷新token的接口刷新成功之后用新的token重放原请求刷新失败就清空登录态并跳转登录页。第三件事是统一处理业务code如果后端返回的code非0弹出对应的错误提示。刷新token的重放逻辑用Axios的重试机制实现比较巧妙的思路是在请求拦截器里用一个数组存住“刷新token期间同时到达的请求”等刷新完成后一起重放。这样不会因为刷新接口还没返回时前端又发了几个并行请求导致多个请求同时去刷新token造成死循环。4.3 富文本编辑与内容安全发帖体验和XSS怎么平衡IT交流平台的帖子内容不能只是纯文本代码高亮、标题段落、链接引用这些都需要富文本编辑器支撑。常用的中文互联网组件里我对比过几款编辑器最终选择了一款社区维护比较活跃的富文本编辑器配置了代码块高亮插件。Vue3里接入这类编辑器要注意组件封装的方式。编辑器本身可能依赖DOM操作要在onMounted生命周期里去初始化并且注意组件卸载时销毁实例否则切换路由的时候会发现页面飘着上一个编辑器的残留。富文本带来的另一个问题就是XSS攻击。用户提交的HTML如果原样存进数据库再原样渲染别人完全可以在帖子里插入一段恶意脚本。我做了两层处理。生成端在编辑器初始化时就禁止掉粘贴外部HTML的默认行为统一转成纯文本再走编辑器的格式化逻辑。服务端在保存帖子正文时用一个开源的HTML白名单过滤库把script、iframe、onerror等危险标签和属性全部剔除。过滤发生在服务端很重要因为请求不一定是前端页面发出来的任何人都可以直接调接口传一段恶意HTML。5. 前后端联调中的钉子户问题跨域、时区与日期格式前后端分离项目在联调阶段最让人头疼的永远是跨域和日期格式。这两个问题不解决前端页面打开是一片红接口看起来调通了数据显示出来又是错的。把我的处理过程分享出来你大概率也能直接用上。5.1 跨域到底用CORS还是反向代理跨域问题的来源很清晰前端页面运行在Vite开发服务器上端口是5173后端接口跑在8080浏览器会认为这是两个不同的源所以直接请求会被拦截。我一开始考虑在后端加一个CORS全局配置来解决也的确能解决一部分问题。但CORS方案有个细节上的麻烦如果前端开发时和后端不在同一台机器上或者后端起在别的端口CORS的allowedOrigins就得配成动态的或写一个数组。调试时还得处理预检请求OPTIONS配置稍有漏洞就报错。后来我换成了前端Vite代理方案开发环境下前端通过/api前缀发起请求Vite把请求转发到http://localhost:8080这样浏览器看到的所有请求都来自同一个源跨域问题从根上消失了。生产部署时这个问题就反过来了我会用Nginx做反向代理将/api前缀的请求转发到后端服务端口。所以整个项目里跨域问题的最终答案是“开发环境用Vite代理、生产环境用Nginx代理”后端代码里完全不用写CORS配置。5.2 日期字段从数据库到前端展示的格式链路日期格式的问题比较隐蔽一开始我的后端代码没有统一处理序列化格式结果前端拿到的时间是类似2025-06-15T09:30:00这种带T的字符串展示在页面里非常难看。有些地方的方案是把实体里的日期字段改成String在SQL里用DATE_FORMAT先格式化好再返回这个方案在小项目里确实快但破坏了ORM的完整性后续一旦要对时间做计算就非常麻烦。我的处理方式是在后端启动类或配置类里注册一个Jackson的JavaTimeModule设定全局的日期序列化格式让所有LocalDateTime类型在序列化成JSON时都变成yyyy-MM-dd HH:mm:ss。前端的帖子列表、评论列表拿到的时间字符串直接展示即可。如果需要在页面上做“几分钟前”这种相对时间显示就在前端写一个格式化函数解析这个字符串成时间戳再计算。前端部分有一个经验可以分享展示相对时间时不要用后端返回的字符串去new Date(...)因为不同浏览器对yyyy-MM-dd HH:mm:ss这种带横杠格式的解析结果可能不同。更稳的做法是后端同时返回一个createdAt格式化好的展示用和timestamp毫秒时间戳逻辑计算用或者前端在收到数据后把字符串里的横杠替换成斜杠再构造日期对象否则很容易出现“同一行代码在Chrome正常、在Safari显示NaN”的尴尬。6. 部署上线的完整清单与安全收尾项目本地跑通之后部署上线还会遇到一连串问题。这个平台最终用了一套非常经典的前后端分离部署方案前端打包成静态文件交给Nginx托管后端打成jar包用Java直接运行MySQL单独部署在服务器上所有配置通过环境变量注入。6.1 前后端分离部署nginx托管静态资源与反向代理先把构建后的前端文件放到某个目录比如/var/www/xxx-platform然后在Nginx的配置里写一个server块监听80端口。location /指向静态文件目录location /api/反向代理到后端接口地址。这里要特别留意try_files的配置因为Vue3是单页应用路由是前端路由如果没有正确配置try_files用户直接访问/post/123这样的深层链接时Nginx会返回404因为服务器上根本不存在这个物理文件。正确的location配置大致是location / { root /var/www/xxx-platform; index index.html; try_files $uri $uri/ /index.html; }这样所有的未知路径都会回退到入口的index.html再由前端路由接管。后端jar包则通过java -jar启动端口和数据库连接串从环境变量读取。为了安全我还在Nginx层给/api/配置了请求体大小限制和超时时间避免上传超大图片或海报时请求迟迟不返回。6.2 上线前必查的配置项密码加密、文件上传、SQL日志关闭上线前的收尾工作我列过一个检查清单每一条都是实际踩过或帮别人排查过的坑。第一件是密码加密。用户密码一定不能明文存储我在项目里用BCrypt加密。BCrypt的特点是每次加密同一个明文生成的密文都不同而且通过内置的加盐机制不需要额外维护盐值字段。第二件是文件上传的大小限制。这个平台允许用户上传头像和帖子里的图片SpringBoot默认的单文件上传大小限制是1MB我在配置类里调大到5MB同时在前端上传前进行同样的文件大小校验。如果不做双重校验用户选了一个20MB的图片结果等上传完才收到后端报错体验非常差。第三件是关闭MyBatis的SQL日志打印。开发时打开SQL日志能看到每一条执行的SQL方便排查问题。但生产环境如果继续开控制台和日志文件会被刷爆而且SQL里的参数值可能会包含敏感信息。我在生产环境的配置里将日志级别设为warn。第四件是接口层面的越权检查。这个点值得展开说一句。发帖接口、编辑接口属于登录用户操作如果只在前端隐藏入口防盗能力等于零。任何一个熟悉接口调试工具的人都可以直接构造请求去调用。我专门写了一个校验逻辑在编辑帖子时从token里取出当前用户ID再判断这条帖子是否属于这个用户不匹配就直接抛异常拒绝。平台上线之后我把这个逻辑纳入代码审查的必检项每次涉及写操作的功能都重新确认一遍。部署完成之后我又把页面里的明文报错信息统一改成了通用提示后端详细异常只输出到日志文件不直接返回给前端。这一步不算功能开发但确实让系统在对外暴露时更稳。做这个项目的整个过程里我个人最大的体会是前后端分离不是简单地把页面和接口拆开它需要你同时具备数据库建模的克制力、接口设计的约束力、前端状态管理的组织力。表结构建模时多想一点后期联调就少改一点统一返回结构和通用请求封装看起来多花时间最后你会发现它能帮你省下成倍的排查时间。这个平台的代码我已经整理成一套完整的项目结构数据库脚本、初始化SQL、部署文档都补齐了。如果你正在找SpringBoot和Vue3的完整实践项目希望这份拆解能帮你把思路捋顺顺着这套工程习惯去搭自己的东西能少走不少弯路。
返回列表