
先说一个比较有代表性的现象绝大多数刚接触前后端分离开发的人拿到“springbootvue”这种项目需求时第一反应往往是先去翻教程、抄代码而不是先想清楚这个工具到底要解决什么问题。作为一个做过不少相关项目的人我建议换个思路——先把这个系统当成一件真实要用的东西去拆解再谈怎么用 Spring Boot 和 Vue 把它落地。这篇我就以“家庭个人财务管理工具”为例完整走一遍从需求分析、技术选型、表结构设计到后端接口实现、前端页面交互、部署时的常见坑整个过程尽量还原实际操作场景让这篇文章既是一份设计复盘也是一份可以直接参考的排雷手册。无论你是拿它做毕业设计还是单纯想练手前后端分离都应该会有收获。1. 项目整体设计与技术选型思路1.1 这个项目到底要解决什么问题家庭个人财务管理听起来很简单做起来其实比很多人想象中复杂。记账只是最表层的一环真正有价值的是花的钱能不能分类统计、预算有没有超支、每个月的收支趋势是怎样的、家庭里多个人记的账能否合并在一起看。如果只是用 Excel也能做但体验很差——手机记一笔还要开电脑多人协作更是灾难。所以这个工具的核心价值就是把这些散落的需求聚合成一个可随时访问、可视化、支持多成员的轻量应用。从这个角度看项目不能只做一个“增删改查”的记账本。至少需要包含这些能力账户管理现金、银行卡、微信、支付宝等、收支流水记录、分类统计、预算设置与超支提醒、报表图表展示、家庭多成员共享与权限区隔。这些需求直接决定了后端表结构怎么设计、接口怎么规划也决定了前端页面拆成几个模块。也就是说设计阶段想得越细后面编码就越省心。1.2 为什么是 Spring Boot Vue而不是别的组合先看后端。家庭财务管理工具属于典型的中小型业务系统特点是并发量不高、业务逻辑偏 CRUD、但需要对事务、数据校验、权限做一些规范处理。Spring Boot 在这个场景下的优势很直接——自动装配把大量繁琐的配置消掉了内嵌 Tomcat 让部署变成“一个 jar 跑起来”生态又极其成熟遇到任何问题都能搜到解决方案。相比之下如果你选 Python Flask 或 Node.js写起来可能更快但在事务管理、接口规范、后续扩展方面还是 Spring Boot 更稳。前端选 Vue 也很自然。这个项目有大量表格、表单、图表交互Vue 的响应式数据绑定让 DOM 操作成本大幅降低组件化开发也让页面结构变得清晰。特别是 Vue 配合 Element UI 这类组件库表格、弹窗、表单校验基本都是现成的开发效率非常高。而且 Vue 上手曲线相对平缓对于主要精力在后端的人也不至于被前端拖住太多时间。1.3 版本选型的实际经验Spring Boot 2.7 还是 3.xVue 2 还是 3版本问题看着小踩坑时真的很耽误事。先说后端。如果你用的是 IDEA 2023 之后的新版本新建 Spring Boot 项目时默认推荐 3.xJava 版本要求 17 以上。那是不是就直接用最新的我的建议是如果这是毕设或练手项目Spring Boot 2.7.x Java 8/11 是更稳的组合。原因有三点资料最多遇到问题最容易搜到答案javax 命名空间网上老教程基本都能直接套用MyBatis-Plus、各种生成工具对 Spring Boot 2.7 的兼容性已经被验证得相当透。Spring Boot 3.x 虽然也成熟了但换成了 jakarta 命名空间MySQL 驱动、部分第三方库都跟着变新手一旦碰到版本冲突排查成本会明显增加。前端的话Vue 3 Vite 是我现在的默认选择但如果你对着老教程学经常会看到 Vue 2 Vue CLI Element UI 的组合。这两者写法上有差异不能混着看。比较实用的做法是选 Vue 3 Vite Element Plus然后用组合式 APIsetup 语法糖来写组件。刚开始可能不太习惯但这是目前的主流方向。如果你实在赶时间也可以选 Vue 2毕竟相关的教程、面试题、踩坑记录数量确实更多但我不建议在 2024 年了还开新项目用 Vue 2。1.4 项目结构规划前后端分离目录怎么分才不乱前后端分离的第一件事就是把项目目录分开。我习惯建一个根目录里面放 frontendVue 工程和 backendSpring Boot 工程尽量不要把前端代码塞进后端的 resources/static 里否则就失去了分离的意义部署时也容易出幺蛾子。后端的包结构可以按这个方式分层com.familyfinance ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis-Plus 提供的 Mapper 接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象比如查询条件 ├── vo // 视图对象返回给前端的结构 ├── config // 配置类跨域、拦截器、MyBatis-Plus分页插件等 ├── common // 统一返回结果、异常处理、常量等 └── utils // JWT工具、日期工具等前端的目录我一般这样规划src ├── api // 按模块拆分的接口请求 ├── assets // 静态资源 ├── components // 公共组件比如分类选择器、金额输入框 ├── router // 路由配置 ├── stores // Pinia 状态管理 ├── views // 页面组件登录、记账、报表、预算、账户等 └── utils // axios 封装、日期格式化等工具这样的好处是后端按职责分层前端按页面模块划分各自清晰。真正写代码的时候你不需要在整个项目里到处找文件。2. 数据库设计与后端核心实现2.1 表结构设计不要把一切塞进一张“账单表”家庭财务管理工具的数据模型拆开来看其实就几条线用户线、家庭线、账户线、账单线、预算线、分类线。我第一次做项目时犯过很典型的错误——想着简单把账户、分类都直接塞进账单表结果统计和扩展时处处难受。所以想清楚核心之后表设计应当更规范一些。我落地的表结构大概是这样family家庭表id、family_name、create_time。不一定每个用户都必须有家庭但既然叫“家庭”财务管理肯定要支持多成员挂到同一个家庭下。user用户表id、username、password、nickname、family_id、avatar、create_time。前端登录的是用户用户再关联家庭。account账户表id、user_id、family_id、account_name、account_type现金/银行卡/微信/支付宝、balance、icon、sort、status。设计账户时要注意账户的归属可能是个人也可能是家庭公共账户。category分类表id、parent_id、category_name、type收入/支出、icon、sort。支出分类例如餐饮、交通、购物收入分类例如工资、兼职、理财。用 parent_id 做层级可以扩展二级分类。transaction账单流水表id、family_id、user_id、account_id、category_id、type收入/支出、amount、transaction_time、remark、create_time。这是最核心的一张表几乎所有统计报表都从它出数据。budget预算表id、family_id、category_id、month、amount、create_time。精确到“某分类在某月的预算额度”比一个总的月度预算更实用。这些表之间通过 id 关联最需要注意的就是金额字段。账单流水里的 amount 一定要用 decimal(10,2)不要用 float 或 double——浮点数在累加计算时会有精度问题金额算错了哪怕只差一分钱都会让你排查到怀疑人生。2.2 后端分层设计接口、业务、数据访问该如何配合Controller Service Mapper 三层结构如果只是照抄你感受不到它的价值但一旦业务复杂起来就知道它有多重要。比如记账接口表面上只是插入一条流水实际上要做的事包括校验账户是否存在、校验分类是否合法、往账户表更新余额支出则减收入则加、插入账单流水、如果存在当月预算还要回写一个“超支预警”的状态。这些操作必须放在 Service 层做并且加上事务保证要么全部成功要么全部回滚不能出现“账户余额扣了流水没记上”这种数据不一致的情况。Controller 层只负责接收参数、调用 Service、返回统一结构。统一返回结构很关键我一般这样做Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }这样前端只需要 axios 拦截器里统一处理 code 是否为 200不用每个接口都单独写错误分支代码瞬间清爽不少。2.3 核心业务逻辑记账、余额更新、预算预警的联动记账是整个系统的核心入口也是最容易出 bug 的地方。用户在前端选择“支出 50 元分类是餐饮账户是微信”后端接收到请求后业务步骤是这样的判断账户是否属于当前用户或当前家庭不能让他给别人家的账户记账。如果是支出校验账户余额是否足够不够就给出提示“余额不足”。执行 UPDATE account SET balance balance - 50 WHERE id 账户id。注意这里一定要用“余额字段 数值”的 SQL 写法而不是先在 Java 里查出余额减完再 UPDATE——后者在并发场景下会丢数据。插入 transaction 流水记录。查出当前月份、当前分类的预算额度如果预算存在计算当月已支出金额判断是否超过预算返回给前端一个提示字段比如 overBudgettrue。预算预警我用的是“查询时实时计算”的方式不需要定时任务简单直接。前端在用户记完一笔支出后如果后端返回了 overBudget 字段界面就弹个提示。这种方式对家庭记账场景完全够用没必要引入消息队列或定时任务去搞复杂的预警推送。2.4 MyBatis-Plus 带来的效率提升与自动装配原理项目里我没有手写大量 XML SQL而是用了 MyBatis-Plus。它对单表 CRUD 基本是开箱即用分页、条件构造器、逻辑删除这些也都有现成支持。比如分页只需要配置一个分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询的时候Service 层直接PageTransaction page transactionMapper.selectPage( new Page(pageNum, pageSize), new LambdaQueryWrapperTransaction() .eq(Transaction::getFamilyId, familyId) .eq(Transaction::getType, type) .between(Transaction::getTransactionTime, startTime, endTime) .orderByDesc(Transaction::getTransactionTime) );这就是为什么小项目根本不推荐手写一堆 XML。至于 Spring Boot 的自动装配原理拿 MyBatis-Plus 举例它依赖 Spring Boot 的 starter 机制通过META-INF/spring.factories里的自动配置类在项目启动时把 SqlSessionFactory、MapperScannerConfigurer 这些组件注入到容器里。你几乎不用做配置它就已经能跑了。理解这个机制后面排查“为什么我的 Bean 没生效”“为什么依赖没生效”这类问题会轻松不少。2.5 登录鉴权JWT 与拦截器的落地方式家庭财务管理工具肯定要支持登录不可能让任何人都能访问账单数据。用户登录后后端生成一个 JWT 令牌返回给前端前端存到本地后续每次请求都在请求头里带上。后端通过拦截器校验令牌是否有效并从中取出用户 ID用于后续的业务数据隔离。代码不算复杂但点挺多Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new RuntimeException(未登录); } Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); return true; } }拦截器注册进 WebMvcConfigurer注意要配置放行路径——比如登录接口、静态资源这些不需要 token 才能访问的地址。前端 axios 封装里也要统一在请求拦截器中带上 tokenservice.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })有一点要提醒JWT 的密钥在代码里写死是一种常见但不太严谨的做法实际项目中应该放到 application.yml 配置里甚至用环境变量注入。这个项目规模不大写死在常量类里也能接受但要知道这不是最佳实践。3. 前端 Vue 实现与页面交互3.1 从 Vite 搭建到工程化配置安装环境、依赖和调试前端开发可以先从搭建环境开始。新建一个 Vue 3 Vite 项目基础命令其实是比较简单的npm create vitelatest frontend -- --template vue cd frontend npm install npm run dev这里有一个新手很容易踩的坑Vite 默认监听 localhost:5173而后端接口跑在 localhost:8080直接请求会出现跨域。解决跨域有两个思路开发环境用 Vite 代理最方便在 vite.config.js 里配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/xxxVite 会自动转发到后端 8080 端口浏览器里看不到跨域问题。生产环境则有两种选择一是 nginx 把/api反向代理到后端 jar 的端口二是后端配置 CORS。我个人更推荐生产环境用 nginx 代理后端的 CORS 配置可以保留但主要当备用。必要的依赖也要装齐Element Plus、Axios、Vue Router、Pinia状态管理、ECharts图表统计。命令是npm install element-plus axios vue-router4 pinia echartsElement Plus 按需引入可以减小打包体积但如果嫌麻烦全局引入也是可以的。小团队快速开发阶段全局引入省心代价就是 bundle 大一点。3.2 路由设计与登录守卫为什么刷新页面会 404路由这里最大的坑不是怎么配置而是 history 模式刷新会 404。Vue Router 默认有 hash 模式和 history 模式两种。hash 模式 URL 里有#看起来不那么美观但不会出现刷新 404 的问题。history 模式 URL 干净部署到 nginx 时如果没配 try_files刷新 /index 路径就会 404。线上部署时需要在 nginx 里加location / { try_files $uri $uri/ /index.html; }路由结构上我一般这样区分登录页是一块主界面是一块带侧边栏和顶部栏的布局主界面下面再套子路由比如/dashboard、/transaction、/budget、/account、/report。登录守卫通过路由的 beforeEach 钩子实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这段代码很短但它是整个前端安全的第一道门。后端接口层也要有 JWT 拦截双保险。3.3 记账页面与分类选择这类交互怎么做用户才觉着好用记账页是整个系统前端交互的重点。用户的操作路径是选择收支类型收入/支出、选择分类、选择账户、输入金额、补充备注。因为移动端的使用频率很高做好这个表单的交互很关键。分类选择不要用普通下拉框太不直观。我实现的方式是弹出一个抽屉或对话框里面用表格或图标网格展示分类用户点选以后再回填。比如支出显示“餐饮、交通、购物、娱乐、居住”等图标可以用 Element Plus 的图标组件或 emoji 图标来填充。金额输入框要聚焦时弹出数字键盘移动端PC 上则做好输入校验只允许数字和小数点最多两位小数。提交按钮要做 loading 状态防止用户连续点提交导致重复记账。这个细节很多人忽略但实际使用中非常影响体验。表单校验也不要省。Element Plus 的 Form 组件本身就支持校验规则比如金额必填、账户必选、分类必选这些规则能拦截掉大量无效请求减轻后端压力。但注意前端校验只是体验优化后端必须再校验一遍接口层不能完全信任前端传参。3.4 图表报表用 ECharts 做收支趋势与分类占比报表模块是让这个工具看起来“有水平”的关键。我用了 ECharts 的两个核心图表折线图展示近 6 个月/12 个月的收支趋势饼图展示某个月份的支出分类占比。后端提供一个接口返回按月份聚合的收支数据前端直接 setOption 渲染即可。折线图的 x 轴是月份y 轴是金额两条线一条收入、一条支出。数据格式后端返回[ { month: 2024-01, income: 8500.00, expense: 6200.50 }, { month: 2024-02, income: 9200.00, expense: 7100.00 } ]饼图的数据则是[ { name: 餐饮, value: 2500 }, { name: 交通, value: 800 } ]ECharts 用起来很简单但要注意组件卸载时记得销毁实例否则页面切换多次后可能内存泄漏。Vue 3 的 onBeforeUnmount 里调用chart.dispose()避免这类问题。4. 家庭多成员与数据隔离的实现细节4.1 family_id 的妙用一套系统多家庭各自独立“家庭个人财务管理”和“个人记账软件”最大的区别就是数据要分家庭隔离。一个用户可以加入某个家庭他记的账在该家庭下可见但另一个家庭的人完全看不到。实现上最关键的是给流水、账户、预算等表都加上 family_id 字段所有查询条件严格带上 family_id不能只按 user_id 查。比如家庭首页的月度总览前端刚进入页面时先通过登录用户的 family_id 查询流水聚合数据。如果用户没加入家庭可以给个引导让他创建家庭或输入邀请码加入。后台在插入交易流水时从当前登录用户的 family_id 取而不是相信前端传来的 family_id——否则用户修改请求参数就可能把数据写到别人的家庭下。4.2 权限边界哪些数据是家庭的哪些是个人的家庭公共数据大家共享但每个家庭成员也应该保留部分个人化设置。我的方案是账单流水和家庭报表是家庭级的家庭成员都能看到账户则区分个人账户和家庭公共账户比如“家庭共用储蓄卡”属于公共账户每个成员看到的是同一个“我自己的工资卡”则属于个人账户。预算就按这个原则来——如果一个预算挂在家庭级分类下家庭成员都能看到超支情况如果是个人自定义的预算只有自己可见。在代码层面我增加了一个 scope 字段来区分数据归属家庭级数据用 family_id个人级数据用 user_id family_id 双条件。查询语句里逻辑比较直观// 查询账户列表个人账户查 user_id 当前用户公共账户查 family_id 当前家庭 LambdaQueryWrapperAccount wrapper new LambdaQueryWrapper(); wrapper.eq(Account::getFamilyId, familyId); wrapper.and(w - w.eq(Account::getUserId, userId).or().eq(Account::getScope, family)); wrapper.orderByAsc(Account::getSort);4.3 前端多角色页面适配我的记账、家庭账单和我的账单前端在菜单上我做了两个入口“我的账单”和“家庭账单”。逻辑上我的账单只展示当前用户创建的流水家庭账单按家庭维度展示所有成员的数据。这样既能满足“我想看自己这个月花了多少”的隐私需求又能实现“家里人一起看看这个月总开销”的协作场景。实现起来也很简单查询参数传 userId 就过滤个人只传 familyId 就是家庭汇总。一个接口、两个参数的问题前端传不同的查询条件即可。5. 常见问题与调试实录5.1 跨域报错前后端明明连着为什么请求就是不通跨域问题是前后端分离项目里出现频率最高的问题。“Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy” 这行报错相信每个人都见过。开发环境下我推荐用 Vite proxy 解决代码在前面已经给出。如果后端想开放 CORS 作为兜底可以加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }这里有个细节比较容易踩坑如果 allowCredentials(true)allowedOrigins 不能写*要用 allowedOriginPatterns 代替否则某些浏览器会直接拒绝请求。我第一次遇到时排查了很长时间后来才知道是这个原因。5.2 部署后刷新 404 与接口 404前端打包后部署到 nginx点击页面跳转一切正常一刷新就 404。这个问题的原因前面提到过是 Vue Router history 模式没有配置 nginx try_files。按下面的配置修好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://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端部署时也要注意Spring Boot 的 jar 如果指定了 context-path比如server.servlet.context-path/api那 nginx 的 proxy_pass 配置要对应调整不然就会出现反向代理后接口 404。我的习惯是后端统一加/api前缀前端请求全部以/api开头这样 nginx 转发规则就很清晰。5.3 版本兼容踩坑Java 17 与 javax/jakarta 的差异如果你用的是 Spring Boot 3.x Java 17有一个坑特别容易遇到网上老教程的 import 全是javax.servlet而 Spring Boot 3 已经改成jakarta.servlet直接复制代码会编译报错。这类问题虽然不难改但出现了难免心里烦躁。所以前面我说新学者不建议直接上 Spring Boot 3。搜索“springboot版本太高”相关抱怨的人大概率都是被这类兼容问题搞的。另外一个版本兼容问题是 MyBatis-Plus 与 Spring Boot 3 的适配要用专门的mybatis-plus-spring-boot3-starter而不是普通 starter。如果加上 3.x 的配置方式很多人连依赖导入都会搞错。解决方案就是照着官方文档来别盲目用搜索到的老办法。5.4 金额精度与时间时区问题金额精度问题我已经强调过数据库用 decimal(10,2)Java 实体用 BigDecimal。如果使用 double 或 float 的类型统计一个月总支出时会出现 0.1 0.2 ! 0.3 的浮点误差。记账这种场景钱算错了是大事故从一开始就避免使用浮点类型。时间问题上数据库的 transaction_time 用 datetime前端传的时间有可能是 ISO 格式字符串也可能是带时区的值。我的建议是后端统一用 LocalDateTime接口接收参数加DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)数据库连接 URL 加上serverTimezoneAsia/Shanghai。如果不加时区参数跑起来多半会遇到时间相差 8 小时的问题。5.5 常见错误速查表现象原因解决方案前端请求 404后端 context-path 或 nginx 代理路径不一致统一请求前缀 /api检查 nginx proxy_pass刷新页面 404Vue Router history 模式未配 try_filesnginx location 加 try_filesCORS 报错前后端跨域开发用 Vite proxy生产用 nginx 代理时间相差 8 小时JDBC URL 未指定时区加 serverTimezoneAsia/Shanghai金额汇总有误差字段类型用了 float/double数据库 decimalJava BigDecimal自动装配的 Bean 找不到组件扫描包名不对确认启动类放在 controller/service 包的最外层JWT 拦截器拦截了登录接口未放行 /auth/ 路径拦截器里把登录接口路径放行ECharts 图表在切换路由后异常未销毁实例组件卸载时调用 chart.dispose()5.6 一个隐藏的坑Lombok 版本与 Java 版本不兼容现在很多教程都用 Lombok 简化实体类的 getter/setter。Lombok 本身很好用但如果你用了太新的 Java 版本比如 Java 21而 Lombok 版本太老会直接启动失败或者编译不通过。解决方式就是查一下当前 Lombok 版本是否支持对应的 Java 版本不行就升级 Lombok 依赖版本。如果实在不顺手不用 Lombok手动写 getter/setter 也就多几十行代码并不丢人。5.7 前端依赖安装失败或版本冲突Vue 3 项目 npm install 时偶尔会有 peerDependencies 冲突。比如安装了某个组件库要求 Vue 3.3而项目是 Vue 3.2就会报错。解决方案一般是升级 Vue 版本或者给 npm 命令加--legacy-peer-deps跳过。Element Plus 与 Vue 3 的版本匹配也要注意不要装 Element Plus 的时候不小心装到 Vue 2 的项目里去那会直接白屏报错。6. 从项目到上线的延伸想法做完这个家庭个人财务管理工具如果你想让它从“毕设/练手项目”变成一个真正能稳定运行的小产品我个人建议后续这样扩展。一是短信或邮件提醒预算超支时自动推送而不是只在前端弹提示二是账单导入功能支付宝和微信都支持导出账单 CSV解析后批量导入能极大降低记账成本三是部署容器化把前后端打成 Docker 镜像再用 Docker Compose 编排数据库单独跑一个容器这样换服务器迁移数据时只需一条命令。整个过程做下来我最大的体会是springbootvue 这套技术栈在中小型业务系统里的开发效率确实很高但真正决定项目质量的不是框架本身而是需求拆解的细致程度和数据库设计的合理性。很多报错和返工本质上都是前面偷了懒后面用十倍精力来还。如果你准备开始这样一个项目希望这篇复盘能帮你省下一些不该踩的坑。