简介:面向毕业设计的一套个人理财系统完整资源,基于 SpringBoot+Vue+MySQL 构建,包含源码、数据库、毕业论文和视频演示。系统分为管理员与普通用户两类角色,管理员可维护用户信息、管理账单类型;用户可录入收入与支出账单、制定理财计划、查看统计分析报表,并能使用备忘录记录还款日等重要事项,基本覆盖个人财务管理核心场景。压缩包共 407 个文件,约 18.88MB,主要包含 98 个 Java 后端源码文件、40 个 Vue 前端组件文件、161 个 SVG 图标,以及 SQL 数据库脚本、Maven 与 bat 一键构建运行脚本,并配有 HTML、CSS、JS 等静态资源,目录按前后端分层组织,便于定位和改造。已有 149 人学习/下载,适合需要快速搭建毕设项目、理解前后端分离实践或进行二次开发的计算机专业学生。配套的毕业论文和视频演示可帮助梳理系统架构、接口设计、数据库表结构及答辩演示要点,为课程设计与毕业设计提供直接参考。
1. 用 SpringBoot + Vue 做毕业设计:个人理财系统到底该怎么做
毕业设计选“个人理财系统”,技术栈定为 SpringBoot + Vue + MySQL,几乎是目前最稳妥且演示效果最好的组合。这套方案的价值在于:后端用 SpringBoot 把接口、权限、业务逻辑统一收口,前端用 Vue 把数据渲染和交互独立出来,MySQL 负责把所有流水账、预算、分类数据落库。整个体系不依赖分布式中间件,一台电脑就能跑通,但覆盖了后端开发、前端开发、数据库设计、接口联调四条完整链路,论文材料也容易整理。
很多同学拿到的源码包里有“源码+数据库+毕业论文+视频演示”,但如果你只会按着视频点下一步,答辩时面对“你讲讲表结构怎么设计的”“接口异常怎么排查”这类问题容易答不上来。更好的做法是:自己把这个项目拆开,弄清楚每张表、每个接口、每条请求为什么这样写,再把别人的源码当作对照参考。这样毕业论文有内容可写,系统演示也不会一问三不知。本文围绕“怎么做一套能过答辩、可扩展、代码干净的个人理财系统”展开,从建表、后端接口、前端页面到联调验证,全部落到可执行的细节上。
2. SpringBoot + Vue + MySQL 的技术分工与表结构设计
2.1 为什么是这套组合:各层职责怎么划分
个人理财系统本质上是一个“单用户或多用户的记账应用”,核心操作是增删改查流水记录,辅助操作是统计、预算、图表。这种业务用 SpringBoot + Vue + MySQL 做,工作量适中,且每一层都有明确的技术考点。
- SpringBoot 负责暴露 RESTful API,处理登录鉴权、参数校验、事务控制和业务逻辑。比如“新增一笔支出”要先判断金额是否合法、分类是否存在、账户余额是否被扣。
- Vue(配合 Vue Router、Axios、ECharts)负责页面渲染、表单校验、路由跳转和图表展示。用户在页面上填金额、选分类,前端把数据组装成 JSON 发给后端。
- MySQL 负责持久化存储。用户表、流水表、分类表、预算表都落在这层,后端通过 MyBatis 或 MyBatis-Plus 操作数据。
这样的前后端分离结构,最大的好处是接口清晰:前端拿到的永远是 JSON,后端不关心页面长什么样。答辩时既能讲“松耦合”,又能讲“接口设计规范”,比纯 JSP 项目有更多可以展开的内容。
2.2 个人理财系统需求拆解与模块划分
先把功能模块列清楚,再决定表结构。一个基础但完整可演示的个人理财系统,至少要包含以下模块:
| 模块 | 功能点 | 数据库表 |
|---|---|---|
| 用户模块 | 注册、登录、个人信息修改 | t_user |
| 账单模块 | 新增收支、编辑、删除、分页查询 | t_transaction |
| 分类模块 | 收入/支出分类管理 | t_category |
| 预算模块 | 每月预算设置、预算使用进度提醒 | t_budget |
| 统计模块 | 按日期/分类汇总收支、图表展示 | 基于 t_transaction 聚合 |
不建议在毕业设计里堆砌“股票模拟”“账单导入”等功能,功能越多越容易暴露边界 bug。把上面的五个模块做扎实,已经足够支撑论文里的“需求分析”和“系统设计”两章。
2.3 建表脚本:字段类型、索引、外键怎么设
表设计是整个数据库课程设计环节最容易丢分的点。每张表都要回答三个问题:主键是什么?哪些字段经常被查询?数据一致性靠什么保证?
DROP DATABASE IF EXISTS finance_db; CREATE DATABASE finance_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE finance_db; CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL COMMENT '用户名', password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)', email VARCHAR(100) DEFAULT NULL COMMENT '邮箱', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB COMMENT '用户表'; CREATE TABLE t_category ( id INT NOT NULL AUTO_INCREMENT COMMENT '分类ID', user_id INT NOT NULL COMMENT '所属用户', name VARCHAR(30) NOT NULL COMMENT '分类名称', type TINYINT NOT NULL COMMENT '类型:1收入 2支出', sort_order INT DEFAULT 0 COMMENT '排序值', PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINE=InnoDB COMMENT '收支分类表'; CREATE TABLE t_transaction ( id INT NOT NULL AUTO_INCREMENT COMMENT '流水ID', user_id INT NOT NULL COMMENT '用户ID', category_id INT NOT NULL COMMENT '分类ID', amount DECIMAL(10,2) NOT NULL COMMENT '金额', type TINYINT NOT NULL COMMENT '1收入 2支出', note VARCHAR(255) DEFAULT NULL COMMENT '备注', trans_date DATE NOT NULL COMMENT '交易日期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (id), KEY idx_user_date (user_id, trans_date), KEY idx_category (category_id) ) ENGINE=InnoDB COMMENT '收支流水表'; CREATE TABLE t_budget ( id INT NOT NULL AUTO_INCREMENT COMMENT '预算ID', user_id INT NOT NULL COMMENT '用户ID', category_id INT NOT NULL COMMENT '分类ID', month VARCHAR(7) NOT NULL COMMENT '月份,如2025-03', amount DECIMAL(10,2) NOT NULL COMMENT '预算金额', PRIMARY KEY (id), UNIQUE KEY uk_user_cat_month (user_id, category_id, month) ) ENGINE=InnoDB COMMENT '月度预算表';这里选择了 InnoDB 引擎、utf8mb4 字符集、联合索引。三个细节值得注意:
- 金额用 DECIMAL(10,2) 而不是 FLOAT/DOUBLE,避免浮点数精度误差;答辩时提到这个点,能明显体现出你懂数据精度问题。
- trans_date 使用 DATE 类型而不是 DATETIME,因为统计查询按“天/月”做分组,日期类型更干净。
- 外键没有在表上显式声明,而是在应用层维护一致性。毕业设计论文里可以这样写:“考虑到后续扩展和删除分类时的灵活性,选择在服务层通过事务控制关联数据的完整性”,这句话就是答辩时的加分项。
3. SpringBoot 后端:从项目初始化到核心接口落地
3.1 用 IDEA 创建 SpringBoot 项目:依赖版本与配置
后端开发推荐使用 IntelliJ IDEA。创建项目时选择 Spring Initializr,注意 SpringBoot 版本不要选太高的预览版,使用 2.7.x 或 3.x 的稳定版即可。搭建时按以下依赖选择:
| 依赖 | 用途 | 备注 |
|---|---|---|
| Spring Web | 提供 RESTController 和 HTTP 能力 | 必选 |
| MySQL Driver | Java 连接 MySQL | 版本跟随 SpringBoot 依赖管理 |
| MyBatis-Plus | 简化单表 CRUD,支持条件构造器 | 毕业设计里比 JPA 更直观 |
| Lombok | 减少 getter/setter 样板代码 | 需要安装 IDEA Lombok 插件 |
| JWT 相关库 | 生成和校验登录令牌 | java-jwt 或 hutool 均可 |
在 application.yml 里配置数据源和 MyBatis-Plus:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/finance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplURL 参数必须带上 serverTimezone=Asia/Shanghai,否则连接 MySQL 8.x 会报时区错误。map-underscore-to-camel-case 开启后,数据库字段 trans_date 能自动映射到实体类属性 transDate,省掉大量 ResultMap 配置。
3.2 实体类、Mapper、Service、Controller 四层编写顺序
后端代码按“实体 → Mapper → Service → Controller”的顺序写。以流水表为例,先用 Lombok 定义实体类:
@Data @TableName("t_transaction") public class Transaction { @TableId(type = IdType.AUTO) private Integer id; private Integer userId; private Integer categoryId; private BigDecimal amount; private Integer type; private String note; private LocalDate transDate; private LocalDateTime createTime; }Mapper 层直接用 MyBatis-Plus 的 BaseMapper:
@Mapper public interface TransactionMapper extends BaseMapper<Transaction> { }Service 层写业务逻辑,比如新增支出时要校验分类是否存在,同时回写预算使用记录。下面这段代码演示了核心逻辑:
@Service public class TransactionServiceImpl implements TransactionService { @Resource private TransactionMapper transactionMapper; @Resource private CategoryService categoryService; @Override @Transactional(rollbackFor = Exception.class) public Transaction addTransaction(TransactionDTO dto, Integer userId) { Category category = categoryService.getById(dto.getCategoryId()); if (category == null || !category.getUserId().equals(userId)) { throw new BizException("分类不存在"); } Transaction tx = new Transaction(); tx.setUserId(userId); tx.setCategoryId(dto.getCategoryId()); tx.setAmount(dto.getAmount()); tx.setType(dto.getType()); tx.setNote(dto.getNote()); tx.setTransDate(dto.getTransDate()); transactionMapper.insert(tx); return tx; } }Controller 层负责参数接收和结果封装:
@RestController @RequestMapping("/api/transaction") public class TransactionController { @Resource private TransactionService transactionService; @PostMapping("/add") public Result<Transaction> add(@RequestBody TransactionDTO dto, HttpServletRequest request) { Integer userId = (Integer) request.getAttribute("userId"); return Result.success(transactionService.addTransaction(dto, userId)); } @GetMapping("/list") public Result<Page<TransactionVO>> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Integer type, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate, HttpServletRequest request) { Integer userId = (Integer) request.getAttribute("userId"); return Result.success(transactionService.pageQuery(userId, page, size, type, startDate, endDate)); } }Controller 层不要写业务逻辑,只做参数接收、调用 service、返回统一结果。统一返回类 Result 包含 code、message、data 三个字段,前端 Axios 拦截器统一判断 code 是否为 200,这样前后端协作时心智负担最小。
3.3 登录鉴权:JWT 令牌与路由拦截器
个人理财系统的数据是用户私有的,所以登录鉴权是答辩必考题。常见方案是 JWT:用户登录成功后,后端签发一个令牌,前端把令牌存在 localStorage,后续请求在请求头携带 Authorization 字段。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BizException(401, "未登录"); } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId", Integer.class)); return true; } }注册拦截器时,放行登录和注册接口,其他接口全部拦截:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register"); } }这个逻辑展开讲,能覆盖“SpringBoot 拦截器”“JWT 原理”“前后端分离下怎么控制访问权限”三个论文考点。
4. Vue 前端:工程搭建、请求封装与核心页面实现
4.1 用 Vue 创建前端项目:依赖安装与代理配置
前端工程建议使用 Vue 3 + Vite,开发效率高。创建完项目后要安装 vue-router、axios、echarts 三个核心依赖:
npm create vue@latest finance-web cd finance-web npm install npm install axios vue-router@4 echarts npm run devVue 开发时访问后端接口会遇到跨域问题。最常见的解决办法是在 Vite 配置里加代理,让前端请求/api时转发到http://localhost:8080:
// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } });注意 target 指向的是 SpringBoot 启动的端口,不是前端端口。changeOrigin 设置为 true 时,后端拿到请求后会认为来自同源地址,避免 CORS 报错。这是联调时最常见的坑,配置不对时页面请求全是红色报错,但 SpringBoot 后台日志却显示有请求进来。
4.2 注册登录页面与 Axios 请求封装
前端把 axios 封装成一个统一模块,方便过滤 token 和统一错误提示:
// src/api/request.js import axios from 'axios'; import router from '@/router'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(error.message || '网络异常'); return Promise.reject(error); } ); export default request;登录页面调用后端接口时,把用户名和密码通过 POST 请求发送:
const login = async () => { const data = await request.post('/user/login', { username: form.value.username, password: form.value.password }); localStorage.setItem('token', data.token); localStorage.setItem('username', data.username); router.push('/dashboard'); };这里登录成功后调用了 Vue Router 的 push 方法跳转到主页面。router.push 是 Vue Router 4 的编程式导航写法,如果面试问“路由跳转有几种方式”,至少要答出 router-link、router.push、router.replace 三种。
4.3 记账页面、ECharts 统计与分类下拉联动
主页面的记账表单是核心交互区。用户选择收入/支出类型后,分类下拉框要联动变化。实现方式是把分类数据按 type 字段过滤:
const categories = ref([]); const selectedType = ref(2); // 默认支出 const filteredCategories = computed(() => { return categories.value.filter(c => c.type === selectedType.value); }); // 修改类型时清空已选分类,避免收入类型选了支出分类 const handleTypeChange = () => { form.value.categoryId = null; };统计页面用 ECharts 展示近 6 个月的支出趋势。后端提供一个聚合查询接口,返回结构是[{ month: '2025-03', total: 1200.5 }],前端拿到数据后拼成坐标轴需要的格式:
const chartOption = computed(() => ({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: statData.value.map(item => item.month) }, yAxis: { type: 'value' }, series: [{ name: '支出', type: 'line', data: statData.value.map(item => item.total), smooth: true }] }));ECharts 图表的表头、图例、数据点提示都要能随窗口自适应,使用 resize 方法监听容器尺寸变化:
const myChart = echarts.init(container.value); window.addEventListener('resize', () => myChart.resize());4.4 路由守卫:未登录用户强制跳转登录页
前端路由不能只依赖后端的拦截器,因为如果用户没登录直接访问未授权页面,后端会返回 401,但页面已经白屏了。更好的体验是前端在路由跳转前就做判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); } else if (!token) { ElMessage.warning('请先登录'); next('/login'); } else { next(); } });路由守卫是 Vue Router 的全局前置守卫。这个代码解决了“刷新页面后 token 丢失导致页面跳回登录页”的问题,因为每次跳转都会检查 localStorage 里的 token。
5. 联调、打包部署与答辩验证
5.1 前端打包后部署到 SpringBoot 静态目录
开发完成后,前端需要构建成静态文件。Vue 项目执行npm run build后会在 dist 目录生成打包产物,把这个目录的全部文件复制到 SpringBoot 的src/main/resources/static下,重新打包运行,前端和后端就合并成了一个应用。访问地址变为http://localhost:8080/。
SpringBoot 的静态资源配置默认会映射/路径,但 Vue 路由是 history 模式时,刷新页面会 404。解决办法是配置一个路径转发,把非接口请求转发到 index.html。常见做法是加一个 Controller 处理error路径,或者在 Vite 配置里把路由模式改为 hash。毕业设计演示时建议使用 hash 模式,避免部署时还要额外处理路由回退。
5.2 用 F12 和 SQL 日志验证数据正确性
联调完成后,用两个手段做回归验证。第一是在浏览器按 F12 打开开发者工具,切到 Network 面板,点击“新增支出”按钮,找到那条 POST 请求,查看 Payload 里的 JSON 是否包含 userId 和 categoryId 字段。如果接口报 401,说明 Authorization 头没有带上去;如果请求 URL 显示 8080 端口,说明 Vite 代理没生效。
第二是看后端控制台的 MyBatis 日志。启动配置里启用了 StdOutImpl,每个 SQL 语句都会打印出来。查询时检查 WHERE 条件是否包含user_id = ?参数,如果漏了这个条件,就是严重的安全漏洞,用户 A 可以查到用户 B 的账单。这个点可以主动写进论文的“系统测试”章节,展示你做了越权测试。
5.3 答辩时数据库方面会被问到的 3 个问题
答辩老师喜欢围绕数据库表设计提问,最容易踩坑的是以下三个问题:
第一个问题:“金额字段为什么不选 float?” 答 Float 和 Double 是二进制浮点数,存储金额会有精度误差,比如 0.1 + 0.2 不等于 0.3。DECIMAL 是定点数,按位数精确存储,所以用 DECIMAL(10,2)。
第二个问题:“t_transaction 表为什么没有外键?” 答 在外键约束下删除分类时,需要先处理关联流水,限制较多。采用逻辑关联,在应用层通过事务校验分类是否存在,既保证数据完整性,又保留在业务里处理错误提示的能力。
第三个问题:“如果用户量变大,这张表怎么优化?” 答 靠 idx_user_date 联合索引覆盖“每个用户按时间查账单”的场景;数据量上亿后可以按 user_id 做哈希分表或按月做分区。讲到这一步,会明显超过同等层级毕设的水平。
5.4 演示环境自检清单
演示现场最容易出意外的不是代码逻辑,而是环境问题。视频演示里录得好好的,到了现场 MySQL 没启动连不上。按照下面的清单提前自检:
| 检查项 | 操作 | 预期结果 |
|---|---|---|
| MySQL 服务 | mysql -u root -p可登录 | 能进入命令行 |
| 数据库初始化 | 连接 finance_db 后SHOW TABLES | 能看到 4 张表 |
| 后端启动 | SpringBoot 无红色报错 | 控制台出现 Tomcat started on port(s) 8080 |
| 前端页面 | 注册新账号并登录 | 能进入主页面,无跨域报错 |
| 新增账单 | 添加一笔支出 | 列表页立即刷新,ECharts 数据同步变化 |
| 刷新页面 | F5 刷新当前页 | 停留在当前路由,不跳回登录页 |
这些检查项每一项都对应一个论文测试用例,可以直接整理成“功能测试表”放进毕业论文。对于需要视频演示的场景也不要只录正常流程,把“登录失败提示”“预算超额警告”这些分支情况也各录一段 10 秒短视频,演示时更有说服力。整个个人理财系统的核心价值就在这套可验证的数据闭环上:用户从注册拿到合法身份,到录入关系型数据,再到统计聚合回显图表,每一步都有迹可循。把这一条链路打穿,项目交付时心里就有底。
本文还有配套的精品资源,点击获取