每年到这个阶段都会有一批学生被毕业设计折腾得焦头烂额。我接触过不少 Spring Boot 方向的实战项目其中“基于 Spring Boot 的大学生个人财务收入支出管理系统”属于一个非常经典的选题技术上覆盖了前端页面、后端接口、数据库设计、部署演示的完整链路业务上又不需要太多前置知识需求自己就能讲明白。这篇文章把我这边已经完整落地的一个项目拆开来讲从需求拆解、技术选型、数据库设计、核心接口实现到最后的部署交付和论文文档整理完整走一遍。不管你是准备拿这个题目做毕设还是单纯想了解财务类管理系统怎么开发都可以直接参考。1. 项目定位与需求拆解为什么这个题目值得做1.1 选题逻辑难度、技术与答辩的平衡选毕设题目大多数人其实就三个诉求技术栈要主流答辩时评委看得懂工作量要适中一个人两三周能做出可用版本业务逻辑要能讲得清楚用例图、流程图、架构图画起来不费劲。Spring Boot 版本的个人财务管理系统正好把这三个点都占了。从实际选题情况看这类“个人数字生活”方向的项目热度一直很高。原因是两类角色天然存在权限控制有地方放记账这种核心业务本身就是标准的增删改查任何人都能理解再加一个统计报表模块图表一展示整个项目的完成度和技术含量立刻上去了。相比电商、外卖、仓库管理这类系统它的实体关系要简单得多不会出现一上来关联表比业务表还多的情况对基本功一般的同学来说更容易把控节奏。更重要的是大学生这个使用场景非常适合收窄需求边界。个人财务系统不需要应付多账本、多币种、套账、税务这类会计专业概念核心就是收入、支出、分类、统计、预算五件事。边界清晰意味着答辩被追问时不容易翻车——你不需要解释那些自己都绕不清楚的业务规则可以放心把精力放在技术实现上。1.2 核心需求拆分谁在用、要干什么我习惯把系统先拆成“谁在用、要干什么、能管什么”三个问题再动手。谁在用普通用户学生本人和管理员两类角色。普通用户关心的是记账和看统计管理员关心的是用户管理和系统整体数据。在毕设级别系统里管理员模块更多承担“完整性”的作用不需要做太复杂但必须存在否则答辩时“角色权限设计”这块就空了。普通用户的核心功能可以这样列注册、登录、退出登录、修改个人资料。收入记账新增收入记录选择收入分类填写金额、日期、备注。支出记账新增支出记录选择支出分类填写金额、日期、备注。记录管理按时间、分类、关键字筛选分页展示支持修改和删除。分类管理维护自己的收入分类和支出分类比如餐饮、购物、兼职、奖学金。预算管理设置每月整体预算或按分类预算页面提示超支状态。统计报表月度收支汇总、分类占比、近几个月收支趋势图。管理员的功能更轻一些登录系统、查看用户列表、启用或禁用用户、重置密码再看看系统总用户数和总账单数。另外建议加一个简单的总览页用图表展示全平台每月收支总量给系统增加一点“管理视角”的内容。这里有一个容易被忽略的细节分类一定要区分收入分类和支出分类。很多人直接把分类做成一张表记账时从同一堆分类里选结果收入里出现“餐饮”这种奇怪数据。这个在需求阶段就要拎出来讲清楚否则后面改数据模型会很被动。1.3 功能优先级把毕设切成三个阶段毕设最忌讳一上来就铺开所有功能。我习惯按三个版本切第一版只做登录注册和收支两个模块的增删改查保证主流程是通的。这个版本跑通项目骨架就稳了。第二版加分类管理、分页筛选、基础统计包括每月收入、每月支出、总余额三个数字。第三版再加预算、超支提醒、ECharts 图表、管理员模块、数据导出等加分功能。这样的顺序有三个好处任何一周你都有可演示的内容就算最后几天PPT来不及美化核心流程已经在跑了每个增量都方便验收不会把问题积压到最后一晚。很多学生喜欢先搭一堆实体类和工具类结果业务没通到后期慌慌张张补接口就是这个顺序搞反了。2. 技术选型与项目架构怎么定最稳2.1 后端技术栈Spring Boot MyBatis-Plus MySQL后端我建议直接走 Spring Boot MyBatis-Plus MySQL 这个组合成熟、资料多、遇到问题一搜就有答案。Spring Boot 选 2.7.x配 JDK 8。这个组合最稳。Spring Boot 3 虽然也能做但要求 JDK 17部分老教程和依赖并不兼容毕设场景下新增的价值不大风险却不小。别为了“新”给自己埋雷。ORM 框架用 MyBatis-Plus 而不是纯 MyBatis 手写或 JPA主要考虑有三点MyBatis-Plus 提供 BaseMapper单表 CRUD 几乎不用写 SQL能省大量代码。LambdaQueryWrapper 做条件查询非常方便按日期筛选、按分类筛选、模糊搜索一个 wrapper 就解决。国内教学和企业项目里 MyBatis 系使用面很广论文里写“使用 MyBatis-Plus 减少冗余代码”评委一听就明白。数据库用 MySQL 5.7 或 8.0 都可以。如果本地装的是 8.0连接驱动和时区参数要处理好这部分我在第六部分单独说。额外建议加 Lombok实体类用 Data 省掉一堆 getter/setter代码量肉眼可见地减少论文里贴实体类代码也清爽很多。2.2 前端方案服务端渲染优先别让自己被跨域拖死前端选择往往是毕设翻车的重灾区。我见过太多学生选 Vue Element UI 前后端分离最后被跨域、Token、打包部署、Nginx 配置拖到崩溃。如果你对 Vue 没有十足把握我的建议很直接用 Thymeleaf 模板引擎 Bootstrap jQuery ECharts。理由非常实在一个 Spring Boot 的 fat jar 包就能跑前后端演示时省去启动前端工程和 Nginx 的步骤。没有跨域问题不用处理 Token 过期、刷新、拦截器放行这类额外逻辑。答辩现场演示打开一个地址就能看到完整效果。jQuery 上手成本低ECharts 官网有大量现成配置项改一改就能用。如果确实想用前后端分离前提是你已经比较熟练 Vue那也不是不行但一定要预留双倍的前端工时并且提前把接口设计文档定好。从毕设角度讲技术选型追求的不是“最潮”而是“能稳定跑完整个答辩周期”。等到了现场评委不会因为你用了分离架构加分但项目跑不起来一定扣分。2.3 分层设计与包结构论文好写答辩好讲项目包结构我建议这么规划com.example.finance ├── config // 配置类、拦截器注册、跨域配置 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 请求和响应对象 ├── common // 统一封装、异常处理、工具类分层不只是让代码好看更关键的是论文好写。论文里架构设计章节直接按这个结构画图Controller 层负责接收参数和返回响应Service 层负责业务规则Mapper 层负责数据持久化整个请求链路是 Controller → Service → Mapper。答辩时评委问“项目用了什么架构”你可以很自信地回答“严格的分层架构模块之间低耦合”。有一点需要特别注意Controller 里不要写业务逻辑所有判断放 Service。这是新手最容易犯的毛病Service 层形同虚设Controller 里堆了一堆 if else答辩被追着问几层就露馅了。3. 数据库设计表结构决定系统上限3.1 四张核心表的设计这个系统的表不需要很多核心四张就够用户表、分类表、收入表、支出表预算表再单独一张。用户表t_user字段类型说明idbigint主键自增usernamevarchar(50)用户名唯一索引passwordvarchar(100)BCrypt 加密后的密码nicknamevarchar(50)昵称roletinyint1-普通用户2-管理员statustinyint1-正常0-禁用create_timedatetime注册时间分类表t_category字段类型说明idbigint主键自增user_idbigint所属用户空表示系统默认分类typetinyint1-收入2-支出namevarchar(50)分类名称iconvarchar(50)图标标识或颜色sortint排序号deletedtinyint逻辑删除标记收入表t_income字段类型说明idbigint主键自增user_idbigint所属用户category_idbigint收入分类amountdecimal(10,2)金额income_datedate收入日期remarkvarchar(255)备注create_timedatetime创建时间支出表t_expense结构几乎一样把income_date换成expense_date即可。预算表t_budget字段类型说明idbigint主键自增user_idbigint所属用户category_idbigint分类ID空表示整体月度预算budget_monthvarchar(7)预算月份如 2025-06amountdecimal(10,2)预算金额这套设计覆盖了标题里说的“收入支出管理”全部场景用户表管身份分类表管归属收支表管流水预算表管控制缺一不可。3.2 字段设计里容易被忽略的细节第一个细节金额类型。记账系统的金额必须用 decimal(10,2)绝对不要用 float 或 double。初学者很容易踩这个坑用 double 算几笔账就出现 0.10.2 等于 0.30000000000000004 这种经典错误。10,2 表示整数部分最多 8 位、小数 2 位个人财务场景完全够用。第二个细节逻辑删除。MyBatis-Plus 原生支持逻辑删除实体上加 TableLogic配置文件里指定删除值即可。逻辑删除的好处是分类删掉后历史账单的统计不会因为关联数据被物理删除而全部丢失报表数据更完整。第三个细节索引。收支表的数据量会随着使用不断增长按用户和时间筛选是最频繁的查询路径。给 user_id 和 income_dateexpense_date建联合索引统计查询性能会好很多。几十条数据时体会不到但答辩时能说出“这里建的是联合索引基于最左前缀原则”这就是加分项。3.3 表关系与删除策略表之间的逻辑关系很清晰一个用户拥有多张分类和多条收支记录一条收支记录属于一个用户和一个分类。外键约束我建议不要用数据库物理外键只保留逻辑关系由应用层控制。物理外键开发期挺方便但后面数据清洗、扩展改造时很容易被限制。答辩时讲“我们采用逻辑外键通过应用层维护数据一致性”很专业。这里有个关键边界分类被账单引用时能不能删除我建议做一个明确的处理方案一禁止删除有账单引用的分类前端提示“该分类下已有账单不能删除”。方案二删除分类后把该分类下所有账单的 category_id 置为某个“未分类”兜底分类账单仍然可展示。方案一逻辑简单答辩好讲方案二对用户更友好但统计分组时要处理空分类名的显示。我实际项目中用方案一系统里预置一个“未分类”分类用来兜底两全其美。4. 核心功能实现从注册登录到统计报表4.1 用户认证与会话管理登录这块有四个环节需要注意。第一密码不能明文存储。用 BCryptPasswordEncoder 加密只需要引入 spring-security-crypto 依赖即可没有必要把整个 Spring Security 拉进来避免复杂配置干扰其他接口。第二注册时校验用户名是否重复。用 LambdaQueryWrapper 查一次用户表就行注意查出结果要立刻返回提示“用户名已存在”不要等落库时才报错。第三登录状态保持。简单方案是用 HttpSession。登录成功后 session.setAttribute(loginUser, user)再写一个拦截器继承 HandlerInterceptor在 preHandle 里判断 session 是否有用户没有就重定向到登录页。注册接口、登录接口、静态资源路径要放进放行名单。// 登录拦截器核心逻辑 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 注意放行路径已经在注册拦截器时配置这里只需校验登录 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } return true; } }第四管理员和普通用户共用一个登录入口。登录成功拿到用户对象后根据 role 判断跳转到用户首页还是管理员首页。权限控制放在另外一个拦截器里访问 /admin/** 的请求要求 session 中 role 等于管理员不满足就返回 403 页面。我见过不少项目在会话管理上翻车最常见的症状是“刷新页面就退出登录”或“直接访问接口不拦截”。前者大多是 session 超时时间配置得太短后者是拦截器路径配置写错了排查思路我放到第六部分。4.2 收支记账的完整实现收支模块本质是标准 CRUD但细节决定体验。新增接口接收的参数categoryId、amount、date、remark。Service 层要做这几件事金额校验用 BigDecimalcompareTo(BigDecimal.ZERO) 必须大于 0防止 0 元和负数混进来。分类校验categoryId 必须属于当前登录用户并且 type 要和记账方向匹配。收入记账只能选收入分类支出记账只能选支出分类这个校验漏了数据就会很乱。日期处理前端没有传日期就用今天传了就用传入的日期不要自己瞎改。分页查询用 MyBatis-Plus 的 Page 对象配合 LambdaQueryWrapperpublic PageResultExpenseVO pageExpense(Long userId, int page, int size, String startDate, String endDate, Long categoryId, String keyword) { LambdaQueryWrapperExpense wrapper new LambdaQueryWrapper(); // 第一条件用户隔离漏了就会查出别人的账 wrapper.eq(Expense::getUserId, userId); // 时间范围 wrapper.between(StringUtils.hasText(startDate) StringUtils.hasText(endDate), Expense::getExpenseDate, startDate, endDate); // 分类筛选 wrapper.eq(categoryId ! null, Expense::getCategoryId, categoryId); // 关键字模糊查找备注 wrapper.like(StringUtils.hasText(keyword), Expense::getRemark, keyword); // 按日期倒序最新记录在前面 wrapper.orderByDesc(Expense::getExpenseDate, Expense::getId); PageExpense pageData expenseMapper.selectPage(new Page(page, size), wrapper); return convertPage(pageData); }修改和删除要再次校验数据归属不能只看主键。毕设系统里经常出现越权删除用户 A 登录后直接把别人账单的 ID 拼到删除接口上数据就没了一行。所以所有单条数据的写操作Service 层都要先查一次数据库确认这条记录确实属于当前登录用户再动手。4.3 统计报表与预算提醒统计是这类系统的灵魂也是答辩时最能出效果的部分。月度收支汇总SQL 大致是这样SELECT DATE_FORMAT(income_date, %Y-%m) AS month, SUM(amount) AS total FROM t_income WHERE user_id ? GROUP BY month ORDER BY month;支出表同理。基于这两组数据前端用 ECharts 画一个近 6 个月的收支趋势图两条折线一张图信息量很大。分类占比报表要关联分类表SELECT c.name, SUM(e.amount) AS total FROM t_expense e LEFT JOIN t_category c ON e.category_id c.id WHERE e.user_id ? GROUP BY c.name;这里用 LEFT JOIN 而不是 INNER JOIN是为了避免分类被逻辑删除后该分类下的账单金额从统计里消失。统计口径的一致性就是在这种细节里守住的。后端接口返回的数据结构建议封装成一个 DTO一个 List 放月份一个 List 放收入一个 List 放支出。前端拿到直接喂给 ECharts不用再二次加工。预算超支判断的逻辑很直接先按月汇总支出再和预算金额比较。如果超支首页和账单页显示一个颜色醒目的提示条。实现成本不高演示效果很好答辩时能讲出“系统具备预算约束能力”这句话价值就出来了。4.4 前端页面交互要点页面不需要多花哨但几个关键页面要重点打磨。登录注册页居中卡片式布局带必要的输入校验。不要搞复杂验证码纯属给自己找麻烦。首页展示总余额收入累计减支出累计、本月收入、本月支出、预算剩余四个核心数字下面放 ECharts 的收支趋势折线图和分类占比饼图。最好把账记完立刻切到首页看图表变化这个体验非常抓人。记账页表单加实时校验。金额输入框提示保留两位小数分类用下拉框日期默认今天。提交成功后刷新列表使用 toast 提示“保存成功”。账单列表页时间范围、分类、关键字三个筛选项配合表格和分页。表格里金额用红色表示支出、绿色表示收入这是财务场景通用的视觉习惯不花哨但专业。ECharts 在 Thymeleaf 模板里的用法是用 th:inlinejavascript 把后端传来的 JSON 数据写成 JS 变量注意要防止返回值被转义成字符串金额数字一定要是数字类型否则图表显示不出来还不好排查。5. 项目打包、论文文档与演示准备5.1 本地部署运行的完整步骤毕设项目拿给评委演示时第一步能不能快速跑起来直接影响印象分。建议把所有操作步骤固化成一个 README并且自己从全新环境完整跑一遍再交付。标准流程是这样安装 JDK 8配置 JAVA_HOME。安装 MySQL 5.7 或 8.0字符集使用 utf8mb4。创建数据库create database finance_db default character set utf8mb4。导入项目提供的 db.sql 脚本初始化表结构和演示账号数据。修改 application.yml 中的数据源配置把用户名密码改成自己环境的。执行 mvn clean package 打成 jar 包。运行 java -jar finance-system.jar看到 Started 字样即成功。浏览器打开 http://localhost:8080。最容易踩坑的是 MySQL 8.0 的驱动配置spring: datasource: url: jdbc:mysql://localhost:3306/finance_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver如果用的是 MySQL 8.0 但 driver-class-name 写成旧的 com.mysql.jdbc.Driver启动就会报错。serverTimezone 不配日期时间很容易差 8 个小时这一点我在通用问题上反复强调。5.2 论文文档LW如何跟代码配合毕设答辩和论文文档是深度绑定的。这类项目的文档一般包含这些章节绪论背景、意义、国内外研究现状、需求分析用例图、可行性分析、系统设计架构图、数据库设计、系统实现功能模块截图加核心代码、系统测试测试用例与结果。写文档最有效的方式是让文档跟着代码走。每写完一个模块就截两张图界面截图加核心代码截图填进对应的实现章节。千万不要拖到最后一周集中补。页面截图注意把浏览器地址栏和用户信息带上会更可信核心代码不要贴一整段只贴关键方法、加注释解释即可。需求分析章节要特别重视用例图。两个角色管理员和用户。用户用例包括登录、记账、分类管理、预算管理、统计查询管理员用例包括用户管理、数据总览。画图不要手工画用在线绘图工具几分钟搞定关键是图例要规范不要出现断线、重叠这类低级错误。5.3 演示视频录制与答辩演示技巧演示视频不用长10 分钟以内足够了。我的录制套路前 30 秒讲清楚项目背景和功能总览。用 2 分钟演示注册、登录流程可以附带展示一下数据库中的密文体现安全意识。用 3 分钟覆盖收入记账、支出记账、修改、删除、筛选的完整链路。用 3 分钟展示统计报表重点突出图表和预算提醒。最后 1 分钟展示管理员模块。录制时分辨率不用太高1080p 足够操作之间停顿 2 秒再点下一步别人才能看清不用配背景音乐旁白简短说明“我在做什么、为什么这样做”即可。答辩现场还有一个技巧把管理员演示账号和学生演示账号都提前准备好登录密码写在便签上。真到问答环节重点是要能清楚讲出“这个表为什么这么设计”“这个接口的权限控制怎么做”比背 PPT 有效得多。6. 踩坑实录与常见问题排查6.1 金额精度BigDecimal 的正确用法用 BigDecimal 解决了计算精度问题但引入了一个新坑数据库中的 decimal 转出来是 BigDecimal 对象序列化为 JSON 后如果有超过两位的小数前端展示位数会不一致。我最终的做法是统一金额格式化。响应 DTO 里的金额字段用注解控制输出或者干脆在全局配置里把金额序列化为字符串。前端拿到字符串后直接展示不要在 JS 里再做浮点运算。算总账、算剩余的活全部放在后端 Service 里用 BigDecimal 操作。6.2 日期与时区8小时偏差的根源这个坑几乎每个项目都会遇到。症状包括数据库里存的日期比页面选择的日期少 8 小时后端接收前端日期字符串时格式解析报错统计图月份错位。原因基本都是时区没有统一。解决方法是三管齐下application.yml 的数据库连接串加 serverTimezoneAsia/Shanghai。Jackson 全局配置里固定日期序列化和反序列化格式为 yyyy-MM-dd。前端传日期时就传 yyyy-MM-dd 字符串后端用 DateTimeFormat(pattern yyyy-MM-dd) LocalDate 接收。有些同学喜欢用 LocalDateTime 接收日期再转成 LocalDate完全是多此一举。按我上面这套从头到尾只用一个格式就不会有偏差。6.3 用户数据隔离毕设系统最常见的越权漏洞用户 A 看到用户 B 的数据是这类系统最常见的安全漏洞。症状是登录后列表页还能看到别人的账单或者分页总数莫名变多。排查思路很明确检查所有列表查询是否都带 user_id 条件。用 MyBatis-Plus 尤其容易漏因为 BaseMapper.selectById 只按主键查不会自动带用户条件。我的习惯是不裸用不带条件的 selectList所有列表查询都拼上用户ID条件。删除接口更是重灾区。Restful 风格 DELETE /expense/{id}Service 里必须先 selectById 再判断 ownerId不能直接 deleteById。把这条规则写进自己的开发规范里能省掉大量安全漏洞。6.4 分页插件与统计SQL的边界MyBatis-Plus 分页必须配置 PaginationInnerInterceptor否则分页是假的数据全量查出来后在内存里切数据量一大性能就崩了。配置代码很简单放在 config 包里的一个 Configuration 类里interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)) 即可。统计 SQL 里还有几个细节日期格式函数所有 MySQL 版本都用 DATE_FORMAT别自己拆 YEAR 和 MONTH 再拼接多此一举且容易出边界问题。GROUP BY 时一定要处理 NULL。特别是用了 LEFT JOIN 的场景如果某条记录 category_id 是空的SQL 里用 IFNULL(c.name, 未分类) 兜底否则前端图表上会冒出来一条“null”分类。统计接口要自己处理默认时间范围。比如首页趋势图只展示最近 6 个月让后端 Service 生成起止日期而不是全部交给前端传参前端传多了容易漏还容易传错格式。6.5 现场演示容易翻车的三个细节演示现场翻车往往不是因为代码写得不好而是环境问题。第一个是端口被占用。8080 被其他程序占了Spring Boot 启动直接失败。提前在 application.yml 里改成 8081或者启动前用命令检查端口占用情况。第二个是数据库不可用。答辩现场的电脑如果没有 MySQL 或服务没启动整个项目就直接瘫了。交付物里我会放一个数据库初始化脚本并且把 root 密码和数据库名写在文档第一页现场导入 10 秒完成。条件允许的话准备一个已经初始化好的 Docker 容器配置现场一键拉起数据库。第三个是浏览器缓存。Thymeleaf 模板改过之后没有重启页面还是旧样子容易被误判成代码问题。开发阶段把 spring.thymeleaf.cachefalse 打开遇到缓存问题先按 CtrlF5 强制刷新能省下很多排查时间。我个人带项目过程中体会最深的是预算管理和统计报表这两个模块带来的正反馈非常明显一旦图表和数据能实时反映每一笔账测试的人就愿意持续往里录数据系统才算真正“活”起来。如果你拿这个题目做毕设我强烈建议把用户数据隔离和日期时区这两块优先测透它们是最容易扣分的隐藏点。项目骨架本身并不复杂真正的差距往往体现在这些细节的完整度上——而这些细节恰恰是答辩时评委最愿意追问的地方。