ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot个人财务管理系统实战:从表结构到事务与统计

SpringBoot个人财务管理系统实战:从表结构到事务与统计 简介这份资源是面向Java初学者与个人开发者练手学习的SpringBoot个人财务管理系统完整项目包适合作为毕业设计、课程设计或框架入门的实战参考。系统基于SpringBoot搭建涵盖用户登录认证、账户管理、收支记录与统计报表等模块采用实体类、控制器、服务层、数据访问层分层结构便于理解企业级开发的组织方式。压缩包共6个文件约31.92MB包含项目源码压缩包、数据库SQL脚本、论文文档、数据库说明文档、演示录像及使用说明文本覆盖从代码到文档再到实操演示的完整链路。已有60人学习下载。读者可借助源码梳理SpringBoot自动配置与分层调用逻辑参考SQL快速还原数据库环境结合论文与演示视频理解需求分析与功能实现并在此基础上扩展第三方支付、财务分析或云同步等个性化功能。1. 个人财务管理系统为什么值得用 SpringBoot 重写一遍很多人第一次接触「个人财务管理系统」是在毕设选题表里觉得它简单——记账、分类、统计能有多难真动手才发现难的不是记账是让一个记账系统在你自己手里跑起来、数据不丢、统计不慢、以后想加个「信用卡还款提醒」不用推倒重来。我见过太多人用 JSP Servlet 硬写写到一半发现事务控制全靠手写commit分类统计的 SQL 嵌套了五层子查询最后连自己都不敢改。SpringBoot 在这个场景里的价值不是「流行」而是它把数据源、事务、参数校验、定时任务这些财务系统绕不开的东西变成了配置项。个人财务管理系统天然需要账户余额的原子更新、收支流水的分页查询、按月按分类的聚合统计、定期账单的自动生成。这些需求用 SpringBoot MyBatis 组合一个Transactional就能兜住转账场景的一致性一个Scheduled就能把周期性账单跑起来。这篇文章面向的是想拿它做毕设、做副业项目、或者单纯想给自己写一个能长期用的记账工具的人。下面从项目结构开始把每个能落地的环节拆开讲。2. 项目骨架与数据模型先定表结构再写代码2.1 为什么先画 ER 图而不是先建 SpringBoot 工程财务系统的表结构一旦定错后面改起来是灾难。我一般会先在纸上把核心实体列出来用户、账户、分类、交易流水、预算、周期账单。它们之间的关系决定了外键怎么设、索引怎么加。账户和流水是一对多分类和流水是一对多预算挂在「用户 分类 月份」这个组合上。这里有个容易翻车的地方很多人把「收入」和「支出」做成两张表觉得查询快。实际做统计时按月汇总要UNION两张表分页还要处理合并排序血泪经验是——用一张transaction表加一个type字段区分收支查询和统计都简单得多。-- 账户表一个用户可以有多个账户现金、银行卡、支付宝 CREATE TABLE account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL, balance DECIMAL(14,2) NOT NULL DEFAULT 0.00, type TINYINT NOT NULL COMMENT 1现金 2储蓄卡 3信用卡 4电子钱包, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 交易流水表type 区分收支避免两张表 UNION CREATE TABLE transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, account_id BIGINT NOT NULL, category_id BIGINT NOT NULL, type TINYINT NOT NULL COMMENT 1收入 2支出, amount DECIMAL(14,2) NOT NULL, remark VARCHAR(255), trade_time DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, trade_time), INDEX idx_account (account_id), INDEX idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;金额字段用DECIMAL(14,2)而不是FLOAT这是财务系统的底线。FLOAT在累加几百笔之后会出现0.01的误差对账时能把你逼疯。idx_user_time这个联合索引是给「查某个月流水」准备的WHERE user_id ? AND trade_time BETWEEN ? AND ?能直接走索引。2.2 SpringBoot 工程分层与依赖选型项目结构我习惯按职责分不按技术分。常见做法是controller/service/mapper/entity/dto/config六层。有人喜欢把vo和dto合并小项目无所谓但财务系统里「入参」和「出参」字段差异大——入参要校验出参要脱敏分开更省心。!-- pom.xml 核心依赖版本按你本地能拉到的稳定版走 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesspring-boot-starter-validation别省。财务接口的入参校验用注解比手写if-else可靠得多NotNull、DecimalMin(0.01)直接标在 DTO 字段上配合Valid就能拦住大部分脏数据。MyBatis 选starter而不是手写SqlSessionFactory是因为自动配置会帮你把DataSource和MapperScan串起来省掉一堆样板代码。2.3 配置文件里必须改的三个参数application.yml里默认的连接池配置在小并发下够用但财务系统有个特点统计查询偶尔会跑几百毫秒如果连接池太小几个统计请求就能把连接占满导致记账接口超时。spring: datasource: url: jdbc:mysql://localhost:3306/finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password hikari: maximum-pool-size: 10 # 个人项目 10 够用别设太大 minimum-idle: 2 connection-timeout: 3000 # 拿不到连接 3 秒就报错别让请求干等 max-lifetime: 1800000 # 30 分钟回收避免 MySQL 主动断连 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 数据库 snake_case 自动映射 Java camelCaseconnection-timeout设 3000 而不是默认的 30000是为了让问题暴露得快。如果统计查询把连接池占满记账请求 3 秒就失败你能立刻发现设 30 秒的话用户等半分钟才看到错误体验更差。map-underscore-to-camel-case打开后trade_time自动映射到tradeTime省掉大量resultMap配置。3. 核心业务实现转账、统计与定时账单3.1 转账接口的事务边界怎么划转账是财务系统里唯一必须保证原子性的操作扣款和入账要么都成功要么都回滚。SpringBoot 里用Transactional标在 service 方法上但有几个细节不注意就会翻车。Service public class TransferService { Autowired private AccountMapper accountMapper; Autowired private TransactionMapper transactionMapper; Transactional(rollbackFor Exception.class) public void transfer(Long fromId, Long toId, BigDecimal amount) { // 1. 扣款用乐观锁防止并发扣成负数 int deducted accountMapper.deductBalance(fromId, amount); if (deducted 0) { throw new BizException(余额不足或账户不存在); } // 2. 入账 accountMapper.addBalance(toId, amount); // 3. 记两条流水 transactionMapper.insert(buildOutflow(fromId, amount)); transactionMapper.insert(buildInflow(toId, amount)); } }rollbackFor Exception.class必须显式写。Spring 默认只对RuntimeException回滚如果抛的是受检异常事务不会回滚钱扣了但没入账。deductBalance的 SQL 里要带AND balance #{amount}这样并发时数据库行锁会保证不会扣成负数返回影响行数为 0 就说明余额不够。update iddeductBalance UPDATE account SET balance balance - #{amount} WHERE id #{id} AND balance #{amount} /update这里没有用SELECT ... FOR UPDATE再UPDATE因为两步之间有间隙并发下仍可能出问题。一条UPDATE带条件判断靠 InnoDB 的行锁就能保证原子性这是最省事的做法。3.2 按月统计的 SQL 怎么写才不慢「查某个月各分类的支出总额」是财务系统里最常用的统计。新手容易写成在 Java 里查全部流水再循环累加数据量一上来就崩。正确做法是让数据库做聚合。select idsumByCategory resultTypeCategorySumVO SELECT c.name AS categoryName, SUM(t.amount) AS total, COUNT(*) AS count FROM transaction t JOIN category c ON t.category_id c.id WHERE t.user_id #{userId} AND t.type 2 AND t.trade_time #{start} AND t.trade_time lt; #{end} GROUP BY t.category_id, c.name ORDER BY total DESC /selecttrade_time start AND trade_time end这种左闭右开的写法比BETWEEN更安全不会因为月末最后一秒的毫秒精度问题漏数据。idx_user_time索引能直接命中user_id和trade_time两个条件GROUP BY在索引扫描后做临时聚合数据量在几万条以内响应都在几十毫秒。如果流水超过十万条可以考虑按user_id分表或者把统计结果缓存到 Redis每次记账后更新对应月份的汇总值。个人项目一般到不了这个量级先别过度设计。3.3 周期账单用 Scheduled 还是 Quartz「每月 1 号自动生成房租账单」这类需求SpringBoot 自带的Scheduled就够。Quartz 适合需要持久化任务、动态增删任务的场景个人财务系统用不上。Component public class RecurringBillJob { Autowired private RecurringBillMapper recurringBillMapper; Autowired private TransactionMapper transactionMapper; // 每天凌晨 1 点检查是否有到期账单 Scheduled(cron 0 0 1 * * ?) public void generateBills() { ListRecurringBill dueList recurringBillMapper.findDueToday(); for (RecurringBill bill : dueList) { transactionMapper.insert(bill.toTransaction()); // 如果是按月周期更新下次触发时间 if (bill.getCycle() Cycle.MONTHLY) { recurringBillMapper.updateNextTrigger(bill.getId(), bill.getNextTrigger().plusMonths(1)); } } } }cron表达式0 0 1 * * ?是每天凌晨 1 点执行。注意Scheduled默认单线程如果账单生成逻辑里有耗时操作会阻塞后续任务。可以在配置类里加一个TaskScheduler指定线程池大小。另外findDueToday的查询要加status 1启用状态和next_trigger NOW()两个条件避免重复生成。4. 避坑与排查那些让我加班到凌晨的问题4.1 事务不生效方法内部调用是黑匣子现象在TransferService里写了一个public void transfer()标了Transactional然后在同一个类里另一个方法直接this.transfer()调用事务没起作用扣款成功但入账失败后数据不一致。原因Spring 的Transactional靠 AOP 代理实现类内部this调用不走代理等于没加注解。解决把transfer抽到另一个Service里注入调用或者用AopContext.currentProxy()拿到代理对象再调。我一般直接拆服务类清晰且没有额外依赖。4.2 金额精度丢失前端传 0.1 后端存成 0.099999现象用户输入 19.99存进数据库变成 19.989999对账时差一分钱。原因前端 JSON 反序列化到Double或Float二进制浮点数无法精确表示十进制小数。解决DTO 里的金额字段用BigDecimal并且加JsonFormat(shape JsonFormat.Shape.STRING)让前端传字符串。数据库字段用DECIMAL。三处统一精度问题就不会出现。4.3 统计查询超时连接池被慢 SQL 占满现象记账接口偶尔报Connection is not available日志显示连接池耗尽。原因某个统计 SQL 没走索引全表扫描几秒把 Hikari 的 10 个连接占完了。解决先用EXPLAIN看执行计划确认type不是ALL。如果是ALL检查WHERE条件字段有没有索引。财务系统里trade_time和user_id的联合索引是必须的。另外把connection-timeout调小让问题快速暴露而不是拖垮整个应用。4.4 定时任务重复执行多实例部署的坑现象本地跑没问题部署到服务器后每月 1 号生成了两份账单。原因如果部署了两个实例Scheduled在每个实例上都会触发。解决个人项目单实例部署就行。如果必须多实例用数据库行锁或 Redis 分布式锁控制只有一个实例执行。简单做法是在任务开始时UPDATE job_lock SET locked 1 WHERE job_name bill AND locked 0影响行数为 1 才继续执行。4.5 分类删除后流水变孤儿现象删除了一个分类之前关联的流水查询时categoryName为 null。原因外键没设级联或者逻辑删除没同步处理流水。解决分类用逻辑删除is_deleted字段查询流水时JOIN category带上is_deleted 0条件但已删除分类的流水仍然能查到只是显示「已删除分类」。物理删除分类是禁忌财务数据要留痕。5. 进阶技巧用 Flyway 管表结构用 Actuator 看健康状态5.1 为什么手动改表结构迟早出事项目上线后加一个字段你本地改了测试环境忘了改生产环境又改错类型。这种问题一次就够你受的。Flyway 的思路是把每次表结构变更写成一个 SQL 文件按版本号顺序执行执行过的记录在flyway_schema_history表里谁都不会漏。-- V1__init.sql CREATE TABLE account (...); CREATE TABLE transaction (...); -- V2__add_budget_table.sql CREATE TABLE budget ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, category_id BIGINT NOT NULL, month VARCHAR(7) NOT NULL COMMENT 格式 2025-01, amount DECIMAL(14,2) NOT NULL, UNIQUE KEY uk_user_cat_month (user_id, category_id, month) );文件放在src/main/resources/db/migration下SpringBoot 启动时自动执行。V2比V1后执行版本号只增不减。如果写错了已经执行过的脚本不要改原文件加一个V3去修正。这个习惯能让你在换电脑、换环境时少掉很多头发。5.2 用 Actuator 快速判断服务是否健康财务系统最怕的是「服务活着但数据库连不上」。SpringBoot Actuator 的/actuator/health端点能直接告诉你数据库、磁盘、连接池的状态。management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always访问/actuator/health返回{status:UP,components:{db:{status:UP},diskSpace:{status:UP}}}。如果db是DOWN不用看日志就知道是数据库连接问题。/actuator/metrics/hikaricp.connections.active能看到当前活跃连接数统计查询慢的时候这个值会飙升提前发现连接池瓶颈。5.3 一个我用了三年的对账习惯每月月底我会跑一条 SQL 核对「所有账户余额之和」是否等于「初始余额 总收入 - 总支出」。如果对不上说明有流水没记全或者金额算错了。SELECT (SELECT SUM(balance) FROM account WHERE user_id 1) AS current_total, (SELECT SUM(amount) FROM transaction WHERE user_id 1 AND type 1) - (SELECT SUM(amount) FROM transaction WHERE user_id 1 AND type 2) AS flow_total;两个值应该相等。不相等的时候先查有没有account_id指向已删除账户的流水再查有没有金额为负数的异常记录。这个习惯帮我抓到过两次并发扣款导致的余额错乱都是因为早期没加balance amount条件。财务系统里对账 SQL 比任何单元测试都实在。希望帮到你。本文还有配套的精品资源点击获取
返回列表