Spring Boot 3 + Vue 3 + ECharts 家庭理财记账系统全栈开发实战
2026/9/7 9:53:21 网站建设 项目流程

Spring Boot 3 + Vue 3 + ECharts 做家庭理财记账系统,是近几年毕业设计和课程设计里出现频率很高的全栈方向。它的价值在于:后端覆盖了 CRUD、权限、事务,前端覆盖了组件通信、路由拦截、状态管理,图表部分又可以把家庭收支的分类占比、月度趋势、账户余额变化直观展示出来。一个项目能把这三件事串起来,基本就能体现“全栈开发”的完整链路。

这篇文章适合准备做毕设或课设的同学,也适合想快速搭一套家庭记账系统练手的前端或后端开发者。我会按实际开发顺序来拆:先定业务和数据模型,再搭 Spring Boot 3 后端,然后建 Vue 3 前端,最后接入 ECharts 做统计。每个环节都会说明判断标准,比如接口返回什么样算正常、图表为什么空白、批量导入要注意什么。

1. 先想清楚这个系统要完成什么,再动手写代码

1.1 家庭记账的核心业务闭环

很多同学拿到题目就开始建表、写接口,做到统计图表时发现数据对不上,然后回头改表结构。这个问题本质是业务边界没有提前画清楚。

家庭理财记账系统最核心的业务闭环就四步:

  1. 用户登录后创建家庭或加入家庭。
  2. 家庭成员记录收入、支出流水,每笔流水必须选择分类和账户。
  3. 系统对流水做聚合统计,按时间、分类、账户维度展示。
  4. 用户可以设置预算,超过预算时在仪表盘上给出提醒。

注意,这里不要把“家庭共享”做得太复杂。如果只是毕设,做到一个家庭内多个成员可以维护账目、所有成员能看到公共流水,就已经够了。不要一上来就做好友邀请、权限细分、家庭切换,那会把工期拉长,而且答辩时也不容易讲清楚。

1.2 为什么选 Spring Boot 3、Vue 3、ECharts 这个组合

选型要能讲出理由,评委经常会问“你为什么不用 XXX”。

Spring Boot 3 相比 2.x 有几个明显变化:最低要求 Java 17,包名从 javax 改成 jakarta,Spring Security 6 的配置方式也变了。对毕设来说,用 Spring Boot 3 能体现出你关注了技术演进,而不是停留在老师 PPT 里的旧版本。

Vue 3 的 Composition API 让组件逻辑更集中,配合 Vite 启动速度很快。相比 Vue 2 的 Options API,Composition 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?serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted

这里最容易出问题的就是时区。如果你的服务器和数据库时区不一致,统计“按月分组”时会出现数据落到前一天或后一天的情况。所以 datasource URL 里显式写 serverTimezone=Asia/Shanghai,后端 JSON 序列化时也统一 GMT+8,前后端就都能对上。

2.3 统一返回结构和异常处理

后端接口返回格式建议统一:

{ "code": 200, "message": "success", "data": {} }

每个接口都返回这个结构,前端 axios 拦截器只需要判断 code 是否为 200,不用每个页面单独处理错误。统一返回的类可以这样设计:

@Data public class Result<T> { private Integer code; private String message; private T data; }

异常处理用 @RestControllerAdvice,把业务异常、校验异常、系统异常分别处理。这一块在答辩时很加分,因为很多学生项目不处理异常,接口一报错就把整个堆栈抛给前端。

2.4 统计接口怎么设计

统计接口是整套系统的亮点,设计思路是“后端做聚合,前端只负责画图”。不要在 Vue 里用 computed 对列表数据做大量二次计算,由后端按 SQL 聚合返回,这样数据量大时也不会卡。

常用统计接口有三种:

  1. 按日期的收支趋势:按月份或按天分组,分别汇总收入和支出。
  2. 按分类的支出占比:对分类做 group by,sum 金额,返回分类名称和金额。
  3. 账户余额视图:查询所有账户的当前余额,用于仪表盘展示。

以 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") List<CategoryStatVO> 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 vite@latest family-finance-web # 选择 Vue 框架,再选 JavaScript 或 TypeScript

建议直接用 TypeScript,虽然初期会有一点学习成本,但后面写接口类型时能少很多调试时间。其实对毕设来说,JavaScript 也够用,关键看你自己的熟练度,不需要为了“高级”强行上 TS。

创建完成后安装基础依赖:

npm install vue-router@4 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 ref="chartRef" class="chart-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 } }) })

这里要注意事件名是 'click',params.name 对应你传给 series 的 data 项中的 name 字段。如果 data 里只有 value,点击事件拿不到分类名。

5. 联调和测试:从单条记账到统计图表,逐步验证

5.1 联调顺序

整个项目第一次联调时,我建议不要跳着测,按这个顺序来:

  1. 先注册一个测试账号,确认登录、注册接口正常。
  2. 登录后创建账户,再录入第一笔收入和第一笔支出。
  3. 在账单列表确认这两条记录能查询出来。
  4. 调用统计接口,确认返回的分类聚合数据正确。
  5. 前端把图表渲染出来,和数据库里的金额对得上。
  6. 最后再去调预算提醒、导出报表这些附加模块。

每跑通一步,就相当于一个里程碑。如果直接跳到统计图表,一旦数据不对,你很难判断是后端 SQL 的问题,还是前端图表配置的问题。

5.2 常见报错排查

下面这几个问题是我见过频率最高的,先说排查顺序,再改代码:

  1. 前端请求报 401:先检查 token 是否真的存到了 localStorage,再看后端 Security 配置是否放行了 /login 和 /register。

  2. 请求 500:优先看后端控制台完整堆栈,不要只看前端提示。通常不是 SQL 写错就是空指针。

  3. 跨域错误:如果用的是 Vite 代理,确认请求的 URL 是 /api 开头,且后端 context-path 没有设置成其他前缀。

  4. 时间字段少了 8 小时:检查 datasource URL 的 serverTimezone、后端 Jackson 时区、前端显示时区,三处要一致。

  5. 图表显示空白:先打开浏览器控制台看 ECharts 是否报错,再看容器高度是否为 0。可以临时给容器加一个背景颜色,如果背景没显示,说明高度问题。

  6. 图表能显示但数据不对:用浏览器开发者工具查看接口返回,确认后端聚合结果。如果接口返回正确,再检查前端 series 的 data 字段是否映射错了。

  7. 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 层使用。这样代码结构清晰,答辩时也能解释分层目的。

最后说一句:这种项目真正做完之后,你会发现最有价值的不是“会写增删改查”,而是理解了“前后端如何协作、数据如何流转、统计如何落地”。家庭理财记账系统刚好能把这些全串起来,是一个性价比很高的练习项目。上手时不要急着扩充功能,先把一条记账流水从录入到统计图表完整跑通,后面的一切都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询