
Spring Boot 3 Vue 3 ECharts 做家庭理财记账系统是近几年毕业设计和课程设计里出现频率很高的全栈方向。它的价值在于后端覆盖了 CRUD、权限、事务前端覆盖了组件通信、路由拦截、状态管理图表部分又可以把家庭收支的分类占比、月度趋势、账户余额变化直观展示出来。一个项目能把这三件事串起来基本就能体现“全栈开发”的完整链路。这篇文章适合准备做毕设或课设的同学也适合想快速搭一套家庭记账系统练手的前端或后端开发者。我会按实际开发顺序来拆先定业务和数据模型再搭 Spring Boot 3 后端然后建 Vue 3 前端最后接入 ECharts 做统计。每个环节都会说明判断标准比如接口返回什么样算正常、图表为什么空白、批量导入要注意什么。1. 先想清楚这个系统要完成什么再动手写代码1.1 家庭记账的核心业务闭环很多同学拿到题目就开始建表、写接口做到统计图表时发现数据对不上然后回头改表结构。这个问题本质是业务边界没有提前画清楚。家庭理财记账系统最核心的业务闭环就四步用户登录后创建家庭或加入家庭。家庭成员记录收入、支出流水每笔流水必须选择分类和账户。系统对流水做聚合统计按时间、分类、账户维度展示。用户可以设置预算超过预算时在仪表盘上给出提醒。注意这里不要把“家庭共享”做得太复杂。如果只是毕设做到一个家庭内多个成员可以维护账目、所有成员能看到公共流水就已经够了。不要一上来就做好友邀请、权限细分、家庭切换那会把工期拉长而且答辩时也不容易讲清楚。1.2 为什么选 Spring Boot 3、Vue 3、ECharts 这个组合选型要能讲出理由评委经常会问“你为什么不用 XXX”。Spring Boot 3 相比 2.x 有几个明显变化最低要求 Java 17包名从 javax 改成 jakartaSpring Security 6 的配置方式也变了。对毕设来说用 Spring Boot 3 能体现出你关注了技术演进而不是停留在老师 PPT 里的旧版本。Vue 3 的 Composition API 让组件逻辑更集中配合 Vite 启动速度很快。相比 Vue 2 的 Options APIComposition API 在写记账表单、统计图表这类具有多个状态和副作用的页面时更清晰。更重要的是Vue 3 是当前企业招聘里的主流要求做毕设时同步接触它对面试也有帮助。ECharts 做统计图表的理由是它成熟、文档全、社区案例多。家庭记账最需要的就是折线图看趋势、饼图看分类占比、柱状图看月度对比ECharts 默认配置就能覆盖不需要自己手写 Canvas。一句话总结这个组合后端稳定、前端现代、图表成熟整个项目既能体现工程化思路又不至于难到完不成。2. 后端搭建数据模型和接口是统计报表的地基2.1 核心数据表设计我建议核心表控制在一张用户表、一张账户表、一张分类表、一张流水表、一张预算表的规模。太多表会给自己挖坑太少又体现不出完整设计。用户表只需要 id、用户名、密码、昵称、家庭 ID。密码不要明文存用 BCrypt 加密Spring Security 里自带。账户表用来区分钱放在哪里比如现金、银行卡、支付宝、微信。字段包括账户名、余额、类型、所属用户。分类表是统计的关键。收入分类可以有工资、奖金、理财收益支出分类可以有餐饮、交通、居住、购物、娱乐、医疗、教育。分类要设计成树形还是平面结构毕设场景建议平面结构加一个 parent_id支持两级就够不需要做复杂树。流水表是整个系统的核心字段至少要包含类型收入还是支出金额用 decimal(10,2)分类 ID账户 ID发生时间备注创建人预算表可以简化成“某分类在某月的预算额度”不用做复杂的周期规则。这里有一个很容易踩的坑金额字段不要用 float 或 double必须用 decimal。记账系统对金额精度有要求用浮点类型做累加会出现 0.1 0.2 0.30000000000000004 这类问题。2.2 Spring Boot 3 项目初始化和依赖创建 Spring Boot 3 工程时Java 版本选 17 或 21。常用的依赖是spring-boot-starter-web spring-boot-starter-validation spring-boot-starter-security mybatis-plus-spring-boot3-starter mysql-connector-j lombok如果不想用 MyBatis-Plus也可以用 Spring Data JPA。我的建议是 MyBatis-Plus因为它的条件构造器在处理“按时间范围查询”“按分类分组统计”时比 JPA 更直观而且国内资料多。application.yml 里重点关注几个配置spring: datasource: url: jdbc:mysql://localhost:3306/family_finance?serverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted这里最容易出问题的就是时区。如果你的服务器和数据库时区不一致统计“按月分组”时会出现数据落到前一天或后一天的情况。所以 datasource URL 里显式写 serverTimezoneAsia/Shanghai后端 JSON 序列化时也统一 GMT8前后端就都能对上。2.3 统一返回结构和异常处理后端接口返回格式建议统一{ code: 200, message: success, data: {} }每个接口都返回这个结构前端 axios 拦截器只需要判断 code 是否为 200不用每个页面单独处理错误。统一返回的类可以这样设计Data public class ResultT { private Integer code; private String message; private T data; }异常处理用 RestControllerAdvice把业务异常、校验异常、系统异常分别处理。这一块在答辩时很加分因为很多学生项目不处理异常接口一报错就把整个堆栈抛给前端。2.4 统计接口怎么设计统计接口是整套系统的亮点设计思路是“后端做聚合前端只负责画图”。不要在 Vue 里用 computed 对列表数据做大量二次计算由后端按 SQL 聚合返回这样数据量大时也不会卡。常用统计接口有三种按日期的收支趋势按月份或按天分组分别汇总收入和支出。按分类的支出占比对分类做 group bysum 金额返回分类名称和金额。账户余额视图查询所有账户的当前余额用于仪表盘展示。以 MyBatis-Plus 为例按分类统计支出的 Mapper 写法类似Select(SELECT c.name AS categoryName, SUM(t.amount) AS total FROM transaction t LEFT JOIN category c ON t.category_id c.id WHERE t.type expense AND t.user_id #{userId} AND t.occur_time BETWEEN #{start} AND #{end} GROUP BY c.id, c.name) ListCategoryStatVO sumExpenseByCategory(Param(userId) Long userId, Param(start) LocalDateTime start, Param(end) LocalDateTime end);注意这里要 left join 分类表不然分类被删后统计结果会丢失。同时在 SQL 里过滤时间范围而不是查出全量再在 Java 里过滤后者在数据量上来之后会很慢。3. 前端工程Vue 3 Vite 的页面骨架和请求封装3.1 创建 Vue 3 项目和基础依赖用 Vite 创建项目的命令很简单npm create vitelatest family-finance-web # 选择 Vue 框架再选 JavaScript 或 TypeScript建议直接用 TypeScript虽然初期会有一点学习成本但后面写接口类型时能少很多调试时间。其实对毕设来说JavaScript 也够用关键看你自己的熟练度不需要为了“高级”强行上 TS。创建完成后安装基础依赖npm install vue-router4 pinia axios echarts外加一个 UI 组件库Element Plus 或者 Naive UI 都可以。记账系统的表单、表格、弹窗如果完全手写工作量会非常大用组件库能快速搭建出可用的界面而且 Element Plus 的 Table、Form、DatePicker 都很适合这类系统。3.2 页面和路由设计前端页面可以拆成 6 个左右登录和注册页简单表单提交到后端换取 token。仪表盘首页账户余额卡片、最近流水、预算进度、基础图表。记账页一个表单选择收入/支出、分类、账户、金额、时间、备注。账单列表页支持按时间、分类、类型筛选分页展示。统计页放 ECharts 图表包括折线图、饼图、柱状图。预算页设置分类月度预算展示使用进度。路由设计要加上守卫未登录不能访问除登录注册外的页面router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })3.3 axios 封装和环境变量请求封装的重点有四个baseURL、token 注入、统一错误提示、超时处理。import axios from axios const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000, }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } )这里要注意Vue 3 项目在本地开发时容易遇到跨域问题。最简单的方案不是在后端加 CrossOrigin而是利用 Vite 的代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } }前端请求 /api 开头的地址Vite 开发服务器会把它转发到后端 8080这样浏览器端就没有跨域限制了。生产部署时一般由 Nginx 做同样的代理转发。4. ECharts 图表落地收支趋势、分类占比、月度对比4.1 折线图展示收支趋势家庭记账最核心的图表是收支趋势折线图。后端返回一个包含月份、收入、支出三个字段的数组前端直接用折线图渲染。ECharts 的配置核心是 option 对象const option { tooltip: { trigger: axis }, legend: { data: [收入, 支出] }, xAxis: { type: category, data: monthList }, yAxis: { type: value }, series: [ { name: 收入, type: line, smooth: true, data: incomeList }, { name: 支出, type: line, smooth: true, data: expenseList }, ], }如果图表空白最常见的原因是容器 div 没有设置高度。ECharts 在初始化时会读取容器尺寸如果容器高度为 0图表会渲染成空白。所以记得给图表容器设置明确的 height比如 350px 或 40% 加 min-height。4.2 饼图展示支出分类占比饼图适合看钱花到哪了。后端返回分类名和金额前端配置填充const option { tooltip: { trigger: item, formatter: {b}: {c} ({d}%) }, legend: { orient: vertical, left: left }, series: [ { name: 支出分类, type: pie, radius: 60%, data: categoryStatList, } ], }这幅图的关键是数据量。如果分类很多比如十几个饼图会非常拥挤。建议后端在统计时把金额极小的分类合并成“其他”或者前端只展示前 8 个分类剩余归入其他。4.3 柱状图展示月度对比或账户余额柱状图适合“本月各分类支出对比”或者“近六个月支出对比”。配置和折线图类似把 type 改成 bar 即可series: [ { name: 支出, type: bar, barWidth: 40%, data: expenseList, } ]如果需要同时展示收入和支出可以用两个 series让它们并排。如果要做累计效果还可以用 stack 属性设置堆叠。4.4 Vue 3 中使用 ECharts 的正确方式很多同学会把 ECharts 封装成全局组件这里我会建议“适度封装”。不要为每个图表写一套复杂的 props 和事件机制不然代码很难维护。一个通用思路是使用一个 ChartCard 组件接收 option 作为 prop内部维护 echarts 实例的生命周期template div refchartRef classchart-container/div /template script setup import { ref, onMounted, onBeforeUnmount, watch } from vue import * as echarts from echarts const props defineProps({ option: { type: Object, required: true }, }) const chartRef ref(null) let chartInstance null onMounted(() { chartInstance echarts.init(chartRef.value) chartInstance.setOption(props.option) window.addEventListener(resize, handleResize) }) watch(() props.option, (newVal) { chartInstance?.setOption(newVal, true) }, { deep: true }) function handleResize() { chartInstance?.resize() } onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chartInstance?.dispose() }) /script要注意的点组件销毁时一定要调用 dispose否则页面切换后会出现内存泄漏。window resize 时要调用实例的 resize 方法不然窗口拉大后图表会被拉伸变形。如果 option 是异步获取后更新的直接 setOption(newVal, true) 会比较稳第二个参数 true 会清空之前的配置再重绘但注意这会丢失动画过渡效果需要时可以先显示 loading。ECharts 的数据更新还需要注意 Vue 3 的响应式机制。如果你把 option 定义成 reactive 对象深层次修改时 watch 可能触发多次或触发不及时。更推荐的做法是接口返回后用 computed 生成图表数据再传入 ChartCard 组件由 watch 监听并更新图表。4.5 图表的交互事件ECharts 还可以绑定点击事件比如点击饼图的某个分类跳转到该分类的账单列表。用 echarts.on 绑定chartInstance.on(click, (params) { router.push({ path: /bills, query: { category: params.name } }) })这里要注意事件名是 clickparams.name 对应你传给 series 的 data 项中的 name 字段。如果 data 里只有 value点击事件拿不到分类名。5. 联调和测试从单条记账到统计图表逐步验证5.1 联调顺序整个项目第一次联调时我建议不要跳着测按这个顺序来先注册一个测试账号确认登录、注册接口正常。登录后创建账户再录入第一笔收入和第一笔支出。在账单列表确认这两条记录能查询出来。调用统计接口确认返回的分类聚合数据正确。前端把图表渲染出来和数据库里的金额对得上。最后再去调预算提醒、导出报表这些附加模块。每跑通一步就相当于一个里程碑。如果直接跳到统计图表一旦数据不对你很难判断是后端 SQL 的问题还是前端图表配置的问题。5.2 常见报错排查下面这几个问题是我见过频率最高的先说排查顺序再改代码前端请求报 401先检查 token 是否真的存到了 localStorage再看后端 Security 配置是否放行了 /login 和 /register。请求 500优先看后端控制台完整堆栈不要只看前端提示。通常不是 SQL 写错就是空指针。跨域错误如果用的是 Vite 代理确认请求的 URL 是 /api 开头且后端 context-path 没有设置成其他前缀。时间字段少了 8 小时检查 datasource URL 的 serverTimezone、后端 Jackson 时区、前端显示时区三处要一致。图表显示空白先打开浏览器控制台看 ECharts 是否报错再看容器高度是否为 0。可以临时给容器加一个背景颜色如果背景没显示说明高度问题。图表能显示但数据不对用浏览器开发者工具查看接口返回确认后端聚合结果。如果接口返回正确再检查前端 series 的 data 字段是否映射错了。MyBatis-Plus 自动填充不生效字段名是不是用了 createTime但没有配置 MetaObjectHandler要么手动设置要么加一个自动填充处理器。5.3 批量导入账目的思路家庭记账系统如果只有手工录入功能上也算完整。但如果有导入 Excel 的需求这里给一个稳妥思路前端用 Element Plus 的 Upload 组件把 Excel 文件上传到后端。后端用 EasyExcel 或 Apache POI 解析逐行校验。校验通过的数据批量写入流水表失败的行返回错误原因。批量导入的核心不是解析而是校验。常见校验包括分类是否存在、金额格式、日期格式、账户是否存在。如果某一行失败最好不要把整个文件回滚而是记录失败原因最后返回一个失败清单。这样用户体验好答辩时也能讲出你考虑了异常处理。6. 毕业设计交付时最容易丢分的几个地方6.1 项目亮点怎么讲答辩时评委不听你念 PPT他们要听到的是“你是怎么思考的”。建议重点准备这几个问题为什么选这个技术栈答Spring Boot 3 是当前主流后端框架Vue 3 Vite 是前端主流开发方式ECharts 是成熟的图表库组合起来可以快速构建一个可用的全栈项目。表结构为什么这么设计答按业务实体拆分成用户、账户、分类、流水、预算五张表流水表通过外键关联分类和账户保证数据一致性。统计功能怎么做答由后端 SQL 聚合返回前端只负责图表渲染避免数据量增大后前端拖慢。还有一个加分项把项目部署到服务器上给评委演示线上版本而不是只在本地 IDEA 里跑。部署时要注意数据库初始化脚本、后端打包命令、前端 build 产物和 Nginx 配置。6.2 文档和初始化脚本交付时要包含一个完整的数据库初始化 SQL表结构、初始分类数据、测试账号一起放在里面。分类数据尤其重要因为统计图表依赖分类如果用户注册后分类为空整个统计功能就没办法演示。再准备一份 README至少包含技术栈和版本本地启动步骤数据库初始化和账号说明默认端口和访问地址演示数据说明这些内容虽然看起来不复杂但很多项目在交付时没有 README评委连怎么启动都不知道印象分就会差很多。6.3 代码组织的一些建议Controller 只做参数接收和结果返回业务逻辑写 Service数据访问放在 Mapper。不要在 Controller 里直接写 SQL 或业务判断。DTO、VO 不能和实体类混用。比如登录接口入参用 LoginDTO返回结果用 LoginVO实体 User 只在 Mapper 层使用。这样代码结构清晰答辩时也能解释分层目的。最后说一句这种项目真正做完之后你会发现最有价值的不是“会写增删改查”而是理解了“前后端如何协作、数据如何流转、统计如何落地”。家庭理财记账系统刚好能把这些全串起来是一个性价比很高的练习项目。上手时不要急着扩充功能先把一条记账流水从录入到统计图表完整跑通后面的一切都会顺很多。