做Java后端开发的人大概率都攒过几个类似的练手项目一个Spring Boot后端连上MySQL再套个前端模板凑出一套能增删改查的系统。家庭理财系统就是其中非常经典的一类。今天这篇把Spring Boot家庭理财系统从需求到源码实现完整拆一遍包括数据库怎么设计、记账流程怎么走、预算和报表怎么做、源码拿到手之后怎么跑起来以及我在实际调试中踩过的一些坑。这套项目虽然叫“家庭理财”但结构上就是一个标准的业务系统样板用户体系、核心交易流水、分类统计、权限控制全是后端开发的高频场景拿来练手或者做课程设计都很合适源码参考价值比单纯的CRUD强得多。我最早接触这类项目的时候也以为它就是个“记账本”无非是把金额、日期、备注存进表里。真正动手做才发现里头的门道不少多账户怎么算余额、家庭成员之间数据怎么隔离、预算提醒的边界条件怎么处理、统计图表的聚合查询怎么写才不出错。把这些捋清楚了你基本就掌握了一套通用信息管理系统的开发范式换一个业务领域也能很快上手。1. 项目核心定位与设计拆解1.1 为什么这类项目总爱选Spring Boot家庭理财系统的核心是处理结构化数据而且数据量级不会太大既不需要分布式中间件也不需要复杂的消息队列。Spring Boot在这种场景下的优势非常明显自带Tomcat内嵌启动方式让本地开发和后期部署都少了很多配置配合Spring MVC接口写起来很直接再加上Spring家族对事务管理、数据源的成熟封装做账目这种对数据一致性敏感的模块心里会踏实很多。账目数据的核心特征是一笔支出可能同时影响账户余额、分类统计、预算剩余额度任何一步出问题都会导致账不平。Spring Boot提供的声明式事务恰好解决了这个问题一个Transactional注解就能保证一组数据库操作要么全成功、要么全回滚。这一点是很多新手容易忽略的也是我反复强调“不要只写增删改查”的原因所在。1.2 家庭理财系统的需求边界到底在哪里动手之前必须先做需求划分。家庭理财系统跟个人记账App最大的区别在于“家庭”两个字意味着数据不是一个人的而是多个成员共享的。常规的功能模块通常包含这几个方面用户与家庭用户注册登录、家庭成员关联、登录状态保持账户管理现金账户、银行卡、电子钱包等每个账户有独立余额收支流水记录每一笔收入或支出关联账户、分类、家庭成员、备注等信息分类管理收入分类和支出分类分开维护支持自定义增删改预算管理按月份、按分类设置预算额度实际支出超过预算后触发提醒统计报表按月看收支走势、按分类看占比、按账户看余额变化。这个范围设计是有逻辑考虑的。记账类应用如果只有流水没有账户用户对“钱去了哪里”缺乏直观感知如果只有流水没有预算那只是一个记录工具谈不上“理财”如果只有统计没有多用户隔离家庭成员之间看同一份数据会很混乱。把这几块拼起来系统才算是闭环。1.3 技术栈选型与理由不同项目源码的技术栈会有差异但这套系统比较常见的选择是后端采用Spring Boot 2.x版本持久层用MyBatis-Plus数据库用MySQL前端使用Thymeleaf模板加Bootstrap或者Layui图表展示用ECharts。有时候也会看到前后端分离版本用Vue加Axios后端只出接口。两者各有取舍模板渲染模式写起来快适合单机小项目前后端分离模式更贴近现在的团队开发方式接口和页面解耦也方便蓝牙小程序之类的其他端复用同一个后端。数据访问层我比较推荐MyBatis-Plus。它既保留了MyBatis写SQL的灵活性又提供了IService、LambdaQueryWrapper这类简洁的封装。家庭理财系统的统计SQL往往包含复杂的聚合和日期分组直接用注解在Mapper里写SQL很方便而普通的按条件查询用封装方法就能搞定不用每个接口都手写XML。2. 数据库设计与核心表结构2.1 用户与账户的模型怎么设计数据库设计是这类系统里最值得细看的部分。第一张核心表是用户表字段一般包含用户名、密码、昵称、手机号、创建时间之类的基础信息密码必须存加密后的密文。用户表之上还要有一张家庭表或者成员关系表用来把多个用户关联到同一个家庭空间下后续所有流水和预算查询都以家庭维度过滤。有些简化版本可能会直接给用户表加一个family_id字段数据量不大的情况下这样也能凑合用但正规一点的做法还是独立一张家庭表。第二张核心表是账户表字段包括账户名称、账户类型、初始余额、当前余额、所属用户或家庭、状态等。这里有一个容易踩坑的点账户余额不应该直接由程序计算得出而是要在每次记账时同步更新。如果所有余额都靠SUM聚合流水得到那么流水一多、出现删除或修改的历史数据时余额很容易对不上。反之如果你用独立的余额字段每次记一笔就加加减减实时性很好但也要保证事务一致避免出现只更新流水没更新余额的情况。2.2 收支流水表与预算表的字段要点流水表是最核心的数据载体字段大致包括用户ID、账户ID、分类ID、收支类型、金额、收支日期、备注、创建时间。这里有三个细节值得注意金额字段建议用DECIMAL(10,2)而不是FLOAT或DOUBLE因为浮点数在数据库里的精度不可控算总账时容易出现0.01之类的误差收支类型字段可以用TINYINT或者VARCHAR加枚举区分个人习惯用TINYINT0表示支出、1表示收入后台用枚举类做映射收支日期和创建时间要分开存收支出日期是业务时间创建时间是系统时间统计报表按业务日期归集才是准的。预算表的核心字段是预算月份、分类ID或账本ID、预算金额、已用金额。这类表的设计有一个进阶考量预算是在指定月份内生效的所以预算表里千万不要只放一个年月字段加一个总金额还要考虑每个分类分别限额的情况。我的做法是每条预算记录绑定一个分类预算月份用VARCHAR存“2025-01”这种格式这样统计某月是否超支时一条SQL就能把预算金额和实际支出对比出来。2.3 建表SQL与关键约束给出一份简化但结构完整的核心建表参考CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码密文, nickname varchar(50) DEFAULT NULL COMMENT 昵称, family_id bigint DEFAULT NULL COMMENT 家庭ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE account ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, account_name varchar(50) NOT NULL, account_type tinyint DEFAULT 0 COMMENT 0现金 1银行卡 2电子钱包, balance decimal(10,2) DEFAULT 0.00, status tinyint DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账户表; CREATE TABLE bill_record ( id bigint NOT NULL AUTO_INCREMENT, family_id bigint DEFAULT NULL, user_id bigint NOT NULL, account_id bigint NOT NULL, category_id bigint NOT NULL, type tinyint NOT NULL COMMENT 0支出 1收入, amount decimal(10,2) NOT NULL, record_date date NOT NULL, remark varchar(200) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_family_date (family_id, record_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收支流水表; CREATE TABLE budget ( id bigint NOT NULL AUTO_INCREMENT, family_id bigint DEFAULT NULL, category_id bigint DEFAULT NULL, budget_month varchar(7) NOT NULL COMMENT 如2025-01, amount decimal(10,2) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_month_category (budget_month, category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预算表;所有表都使用InnoDB引擎和utf8mb4字符集这是两个基本约定。InnoDB支持事务这是账目操作的前提utf8mb4保证表情符号和生僻字不会变成问号我自己就遇到过一次用户备注里带了一个emoji结果整个页面乱码的情况后来排查才发现是表字符集不是utf8mb4导致的。3. 从源码到可运行环境准备与启动流程3.1 运行环境清单与版本选择源码拿到手先别急着改代码第一步是把运行环境对齐。家庭理财系统是典型的Spring Boot单体项目版本兼容问题多半出现在这里。必备环境大致是JDK 1.8或更高版本Maven 3.6MySQL 5.7或8.0均可开发工具主流IDE即可导入Maven项目即可如果你用的是Spring Boot 2.x那么JDK 8完全够用如果源码升级到了Spring Boot 3.xJDK要切到17。这是很多人一开始就翻车的地方项目能正常编译但一运行就报UnsupportedClassVersionError基本就是JDK版本低于编译版本。MySQL方面家庭理财系统一般不会用到太新的特性5.7和8.0都行。但要注意连接驱动版本如果是MySQL 8.0驱动类名应该是com.mysql.cj.jdbc.Driver8.0之前的版本才是com.mysql.jdbc.Driver。很多基于Spring Boot 1.x的老项目会沿用旧驱动导入到新环境里启动时就报错。3.2 源码目录结构怎么读拿到源码后先用IDE打开确认是一个Maven工程检查一下整体结构。一套标准的Spring Boot业务代码目录是这样的src/main/java ├── controller ├── service │ └── impl ├── mapper ├── entity ├── config ├── common │ ├── result │ ├── exception │ └── utils src/main/resources ├── mapper ├── static └── templates读取这种结构有个顺序先看common包里的统一返回体Result类里面无非是code、message、data三个字段这是判断整个项目接口规范的基础再看config包确认是否配置了拦截器、跨域、密码加密Bean接着看mapper包和resources下的XML理解数据库操作是怎么封的最后才看controller把接口清单过一遍心里就有了全局认识。3.3 配置修改与启动验证实际启动之前需要改配置文件核心是数据源。找到src/main/resources下的application.yml修改以下几处server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/family_finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl处有三个关键参数useUnicodetrue和characterEncodingutf8保证中文不乱码serverTimezoneAsia/Shanghai是MySQL 8的时区要求不设置的话通常会报Server returns invalid timezone错误。数据库建好后把源码里自带的SQL脚本执行一遍一般文件名会是init.sql、db.sql或者schema.sql。执行顺序要注意先建库再建表最后再看是否初始化了测试账号。初始化数据里的默认账号密码往往是admin/admin123这种登录进去就能看到系统预设的分类数据和演示流水。启动方法没有特别之处等Maven下载完依赖直接运行主启动类看到Spring Boot的启动横幅和Tomcat started on port 8080就说明成功了。访问http://localhost:8080能看到登录页就是正常状态。如果端口被占用改一下server.port即可。4. 核心功能模块的实现思路与代码解读4.1 用户登录与权限控制用户模块一般包含注册和登录两块。注册时密码必须加密存储常见的是BCrypt算法也就是Spring Security提供的BCryptPasswordEncoder。BCrypt的特点是每次加密结果都不同而且自带随机盐比MD5加盐再拼接的方式安全得多。登录接口的核心逻辑是按用户名查记录把数据库里的密文和用户输入的明文放到matches方法里做校验而不是把数据库密文解密回明文再去比对。BCrypt没有解密方法它通过对同一明文重新计算并比对摘要来完成验证。校验通过后生成一个token常见做法是用JWT工具类生成令牌返回给前端前端保存在本地后续请求统一在请求头里带上。后端写一个拦截器校验token解析成功就放行失败就返回401。一个完整的登录控制器大致是这样的PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user null) { return Result.error(用户不存在); } if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); user.setPassword(null); return Result.success(token, user); }这里的细节在于查询对象或返回对象中一律不要把密码字段传给前端最稳妥的做法是在返回前把密码字段置空。很多项目就是因为忘了这一步导致接口数据里把密码哈希一起泄露出去虽然反向破解难度大但这不是好习惯。4.2 记账与分类管理的实现细节记账是整个系统使用频率最高的操作。前端提交的数据包括账户ID、分类ID、类型、金额、日期、备注后端要做两件事插入流水记录同步更新账户余额。支出时账户余额减少收入时账户余额增加。这个操作必须开启事务。因为插入流水和更新余额是两个独立的数据库写操作中间任何一个失败都会导致账目不平。Spring Boot里实现事务很简单在service方法上加Transactional注解即可。我用MyBatis-Plus时习惯把同一账户的更新包在单独的方法里再从这个方法外面调这样代理机制能正常生效避免自调用导致事务失效的经典坑。Transactional(rollbackFor Exception.class) public boolean addRecord(BillRecord record) { boolean saved this.save(record); if (!saved) { return false; } Account account accountService.getById(record.getAccountId()); if (account null) { throw new BizException(账户不存在); } BigDecimal change record.getType() 0 ? record.getAmount().negate() : record.getAmount(); account.setBalance(account.getBalance().add(change)); return accountService.updateById(account); }注意事务的回滚条件要写成rollbackFor Exception.class因为Spring默认只回滚RuntimeException而业务异常往往是自定义受检异常的子类不确定类型时统一指定更省心。分类管理相对简单一张分类表按类型区分收入和支出前端在记账页做成下拉选项。这里有一个隐藏需求分类数据最好支持用户自定义因为每个家庭的消费习惯不一样除了默认的餐饮、交通、购物等固定分类用户可能想加一个“宠物开支”之类的自定义项。所以分类表要留user_id字段允许为空表示系统默认分类不为空表示用户自定义。4.3 统计报表与图表展示报表模块是家庭理财系统里最能拉开档次的部分。常规统计包括月度收支对比、分类占比、账户余额分布。后端只需要提供几个聚合查询接口前端用ECharts渲染图表即可。月度收支统计的核心SQL是分组聚合SELECT DATE_FORMAT(record_date, %Y-%m) AS month, SUM(CASE WHEN type 0 THEN amount ELSE 0 END) AS total_expense, SUM(CASE WHEN type 1 THEN amount ELSE 0 END) AS total_income FROM bill_record WHERE family_id #{familyId} AND record_date BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(record_date, %Y-%m) ORDER BY month;这段SQL的要点在DATE_FORMAT函数。它能把date类型的日期按年月归组直接输出像“2025-01”这样的月份字符串前端直接用做X轴坐标。CASE WHEN配合SUM的写法可以避免先查支出再查收入、然后在内存里合并数据的笨办法。两次查询在数据量大时不仅慢还容易因为分组逻辑不一致导致数据对不上。分类占比统计稍微复杂一些需要关联分类表SELECT c.name AS category_name, SUM(r.amount) AS total FROM bill_record r LEFT JOIN category c ON r.category_id c.id WHERE r.family_id #{familyId} AND r.type 0 AND r.record_date BETWEEN #{startDate} AND #{endDate} GROUP BY c.name ORDER BY total DESC;这里用LEFT JOIN而不是INNER JOIN是为了防止分类被删除后历史流水在统计里凭空消失。实务中删除分类时往往会检查是否有流水引用但历史流水还是应该保留原分类名称或者其他兜底方案避免报表数据出现空缺。4.4 预算管理与超支提醒预算模块的逻辑可以归结为三件事设预算、算花费、出提醒。设预算接口通常接收月份、分类ID、金额三个参数保存到预算表。因为每月的每个分类只能有一条预算记录所以建表时给了唯一索引。前端提交重复数据时后端要捕获DuplicateKeyException或者先查后插我个人偏好先用lambdaQuery检查是否已存在存在的话提示用户修改页面数据而不是静默覆盖。算花费的时机有两种思路。一种是在用户访问预算页面时实时统计另一种是每次记账成功后触发一次预算更新。实时统计实现简单但预算页面每次刷新都要跑一次全表聚合实时更新逻辑复杂还要考虑删除流水的情况。家庭理财系统的数据量根本不需要担心性能所以我选择实时统计理由很简单代码易维护而且删除、修改、补记历史账目后预算数据永远是正确的。超支提醒不是主动推消息而是在查询预算列表接口里返回一个超支标志位。后端在组装返回数据时把预算金额和实际支出对比如果实际支出大于预算金额就在结果里标记超支前端展示时对超支项加红显示并给出文案这个交互已经足够直观。4.5 家庭成员之间的数据隔离多人共用一套系统时数据隔离是必须考虑的问题。核心方案是给所有业务表都加family_id字段每次查询都带上这个条件。用户在登录时拿到自己的family_id后续查询统一从当前登录用户上下文中获取而不是依赖前端传参。这一点容易出漏洞有些实践里把family_id放在请求参数里用户改个参数就能看到别的家庭的数据。拦截器里解析token后可以把用户和家庭信息放到ThreadLocal上下文中Service层从上下文取。这样业务代码里不需要每个方法都重复传递用户ID代码干净很多也降低了漏掉条件的概率。安全上宁可一开始就严格也不要在上线后再补因为账目数据一旦泄露整个系统的信任感就没了。5. 常见问题排查与避坑经验5.1 启动与依赖相关经典问题把源码从不熟悉的环境跑起来最容易遇到的就是环境类错误这里整理一个速查表。现象原因解决方式启动时报UnsupportedClassVersionError本地JDK版本低于项目编译版本切换JDK版本或重新编译启动时报Server returns invalid timezoneMySQL 8连接串缺少时区参数url加serverTimezoneAsia/Shanghai启动时报ClassNotFoundExceptionMaven依赖没有完全导入反复reload或clean install启动失败提示端口被占用8080端口被其他进程占用换server.port或关掉占用进程访问出现404但项目启动正常前端静态资源和模板路径不匹配检查templates和static目录位置有一个经常被忽略的知识点Spring Boot默认只能扫描启动类所在包及其子包下的Component、Service、Mapper等注解。很多人把Mapper接口放在主包之外结果启动时不报错真正访问接口时才报Invalid bound statement就是扫描不到的典型症状。解决办法是把mapper接口放在主包之下或者用MapperScan指定路径。5.2 数据库与中文乱码问题中文乱码问题的根子几乎都在字符集上。如果MySQL建库时用的是默认latin1那么即使应用层配置了UTF-8中文字符到库中还是可能变成问号。建库时建议显式指定CREATE DATABASE family_finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时要确认MySQL服务端配置文件里的character_set_server修改后需要重启MySQL服务。还有一种乱码情况只发生在页面端而数据库正常通常是Response没有设置正确的Content-Type。Spring Boot默认的StringHttpMessageConverter会把文本转成ISO-8859-1编码如果是用字符串直接写回响应就会乱码。不过一般都没这个问题因为大家都会配置统一的JSON返回体。处理这类问题时我习惯先把数据链路切成四段排查页面到Controller、Controller到Service、Service到数据库、数据库存储。每一段单独验证很快就能定位乱码出现在哪一层。5.3 上线部署前的优化清单源码在本地跑通后如果打算部署到服务器有几个地方必须调整否则体验会很糟。第一是配置文件按环境拆分。本地用application-dev.yml生产环境用application-prod.yml启动时用spring.profiles.active指定环境。不要在生产环境里使用本地数据库连接信息这是底线。第二是关闭MyBatis-Plus的SQL日志。开发阶段依赖StdOutImpl打印SQL很方便放到生产环境会让控制台疯狂刷日志而且SQL里全是用户数据既不安全也不环保。办法是把log-impl配置删掉或者在yml里用logback配置控制日志级别。第三是静态资源缓存和压缩。Spring Boot可以启用HTTP响应压缩在配置里加一段server.compression配置对JSON和静态资源都有明显效果。家庭理财系统这类页面较多的小项目静态资源本身不大但加上压缩和缓存策略会让首页加载快不少。第四是密码和密钥的硬编码问题。JWT签名密钥、数据库密码这类敏感信息不要写在代码里提交到仓库建议通过环境变量或者配置中心注入。我见过不止一次项目源码泄露导致数据库被撞的事件源头就是配置文件里写了真实的root密码。5.4 我建议你在源码上做二次改造的方向如果只是把源码下载跑起来收获有限。我建议拿到源码后做三件事第一是加入一个“账单导出”功能把流水按月份导出成Excel文件。这个功能虽然看起来简单但牵扯到POI或EasyExcel的依赖引入、日期格式处理、大数据量内存控制是很完整的实战练习。第二是给统计模块加一个“年度收支对比”视图把今年和去年的同月数据放到一张折线图上。这个功能的关键点在于SQL里要按月份做两次聚合再合并比单年度的统计更考验对数据结构的理解。第三是把登录流程从简单的JWT改成带刷新机制的双token方案。这个改造会涉及到拦截器的调整、token过期策略的设计比单纯调通项目更有深度。这些改造都不难但每一样都能把Spring Boot里的一个重要知识点串起来。测评一个项目是否真正掌握了标准不是代码能不能跑而是你敢不敢在源码上动刀。最后分享一点我自己的体会。做这种带源码的项目最忌讳的就是打开即跑、跑通即弃。家庭理财系统的业务逻辑不复杂但它是少有的能把事务、聚合查询、权限控制、状态管理全部揉在一起的完整案例。如果时间允许建议把它当成一个真实产品去打磨先跑通再逐行读一遍核心代码最后根据我上面提的方向做一两个改造。这个过程走完你对Spring Boot整体的认知会比只看文档来得扎实得多。