简介:这是一份基于SpringBoot实现的记账本系统完整源码包,面向Java方向毕业设计、课程实训以及SSM技术栈学习者。项目围绕日常收支管理场景,内置账单增删改查、分类管理、用户登录与数据表格展示等模块,Controller、实体类、业务辅助类等分层清晰,适合直接运行调试或作为二次开发基础。压缩包共390个文件,整体仅3.56MB,代表性文件类型包括gif操作演示、js动态脚本、java后端代码、class编译字节码、css样式与xml配置文件,另含properties/yml环境配置、SQL初始化脚本及Maven工程描述文件,便于本地快速搭建运行环境。目前已有28人学习下载,源码经本地编译可正常启动,功能已获指导老师认可;参考预览中出现的账单实体、控制器及工具类,可快速定位核心代码,配合HTML页面与资源文件即可完成一套毕设级记账管理系统。
1. 拿到SpringBoot记账本源码.zip,先避开运行时的三个大坑
解压SpringBoot记账本源码.zip,最先要看的不是Controller怎么写,而是pom.xml和application.yml有没有对齐。记账本业务不大,用户、分类、收支、报表四件事就能装下全部核心功能,但也正因简单,它把SpringBoot项目里最常见的几个坑集中摆在明面上:数据源连不上、Mapper扫描不到、报表统计SQL越跑越慢。对这个题目感兴趣的通常有两类人,一类是拿它当Java毕设或入行练手,想看一个完整后端闭环;另一类是前端或全栈工程师,拿到后端后短时间内要能上手改代码。不管哪一类,下面这套处理顺序都适用:先识别工程骨架与依赖,再理清数据模型和核心接口,跑通本地配置,最后回归统计SQL做一次性能验证。
2. SpringBoot记账本项目结构:从zip包识别工程骨架与核心依赖
2.1 解压后先确认Maven还是Gradle
拿到zip包,我一般不会先双击main类,而是在命令行里做一次“轻量体检”。压缩包解压放到独立目录,避免目录层级混乱导致的target遗留文件干扰判断:
unzip -q 基于SpringBoot项目记账本源码.zip -d account-book cd account-book ls -la-d account-book表示解压到指定目录;ls -la看顶层内容。顶层出现pom.xml就是Maven工程,出现build.gradle或settings.gradle就是Gradle工程。大多数记账本模板都走Maven,原因后面讲。继续往下看一层,确认src/main/java、src/main/resources、src/test目录都在,至少说明源码包没有缺胳膊少腿。
完整到能单独运行起来的Maven项目,常见目录轮廓是这样:
account-book/ ├── pom.xml ├── README.md └── src/main/ ├── java/com/example/accountbook/ │ ├── AccountBookApplication.java │ ├── config/MybatisPlusConfig.java │ ├── controller/BillController.java │ ├── service/BillService.java │ └── mapper/BillMapper.java └── resources/ ├── application.yml └── mapper/BillMapper.xml注意resources/mapper目录不是SpringBoot强制的,因为MyBatis的XML可以写在java包下,只是放resources里更符合约定。前面这个结构如果完整,说明项目已经按“分层”规划清楚:controller收参数和返回结果,service处理业务规则,mapper与数据库交互。
2.2 记账本源码里的核心依赖与版本陷阱
工程结构确认后,我的习惯是先打开pom.xml看parent和依赖部分,别的先不动。常见的记账本依赖组合见下表:
| 依赖 | 典型用途 | 常见坑 |
|---|---|---|
| spring-boot-starter-web | 提供REST接口与内嵌Tomcat | 漏加则Controller注册不上,接口总是404 |
| mybatis-plus-boot-starter | 单表CRUD直接继承BaseMapper | 与SpringBoot版本不匹配,启动报Interceptor错误 |
| mysql-connector-java | MySQL驱动 | url里少了serverTimezone会报时区异常 |
| lombok | 简化getter/setter | IDE默认不开注解处理,编译报找不到方法 |
| knife4j/springfox | 生成接口调试文档 | 与SpringBoot 2.6以上路径匹配规则冲突 |
选MyBatis-Plus而较少选Spring Data JPA,是因为账目查询往往带着多个可选条件:时间范围、分类ID、类型、关键字。MyBatis-Plus的LambdaQueryWrapper能拼出稳定可读的SQL,需要写统计时才落到XML;JPA的Specification写同样逻辑需要额外语义层,项目变大后维护起来绕。这里没有对错,而是记账本这种以查询统计为主的场景,MyBatis-Plus的阻抗更小。招聘里也常问这类选型,本质上是看接口风格与业务描述是否匹配。
版本上需要警惕的是SpringBoot本身。pom里若是3.x,本地JDK至少得是17及以上,否则直接编译失败;3.x同时把javax迁移到jakarta命名空间,源码里如果还有import javax.servlet.*,启动阶段会立刻报类找不到。把这类问题留在解压后第一轮检查,比跑完代码再回头找效率高得多。
2.3 启动类、配置文件与XML路径的对应关系
记一个最常见的“看起来能跑但跑不起来”的案例:数据库密码改了、表也建了,启动却报找不到某个Bean。九成是Mapper没被扫描到。标准主类写法是:
@SpringBootApplication @MapperScan("com.example.accountbook.mapper") public class AccountBookApplication { public static void main(String[] args) { SpringApplication.run(AccountBookApplication.class, args); } }@MapperScan里写的包路径必须和BillMapper等接口实际所在包完全一致,少一个字母都不行。漏了它,MyBatis不会生成Mapper代理对象,Service注入时就会报UnsatisfiedDependency。另一种等价写法是在每个Mapper接口上标@Mapper,但Mapper一多就容易漏,团队项目里风格不一致反而增加排查成本。
XML位置需要在配置里显式声明,否则手写的统计查询不会生效:
mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.accountbook.entity configuration: map-underscore-to-camel-case: truemapper-locations告诉MyBatis去classpath的mapper目录找XML;type-aliases-package让XML里的resultType只写类名即可;map-underscore-to-camel-case把数据库的bill_date自动映射成实体里的LocalDate billDate字段。这三项配套,实体类就可以不写一堆@TableField注解。排查时,如果启动日志里出现“Invalid bound statement”,基本就是XML没被加载,问题就锁定在这段配置里。
3. 记账本数据模型与SpringBoot核心业务接口设计
3.1 用户、分类、账单三张核心表的字段规划
多用户记账本至少要三张表:user存登录信息,category存收入和支出的分类,bill存每一笔账目明细。三张表的关系是“用户拥有多个分类,分类下挂多条账单记录”。字段设计时,最值得花功夫的是账单表:
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '归属用户,支持多用户记账', `name` VARCHAR(50) NOT NULL, `type` TINYINT NOT NULL COMMENT '1=支出,2=收入', `icon` VARCHAR(100) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表'; CREATE TABLE `bill` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `category_id` BIGINT NOT NULL, `amount` DECIMAL(10,2) NOT NULL, `type` TINYINT NOT NULL COMMENT '1=支出,2=收入', `remark` VARCHAR(255) DEFAULT NULL, `bill_date` DATE NOT NULL COMMENT '业务日期', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `bill_date`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账目明细表';金额用DECIMAL(10,2),不要用float或double。记账对精度敏感,浮点累加在月报里会出现0.1这类“看起来不该存在”的误差。bill表保留type字段是刻意的,虽然从category也能推出收入还是支出,但统计时避免每次都关联分类表,才能让单表扫描保持简单。存储上,bill_date用DATE,create_time用DATETIME:前者表示“这笔账发生在哪天”,后者表示“什么时候写库”,跨天补录时不会互相污染。索引优先建(user_id, bill_date)复合索引,因为列表查询和报表查询都以“哪个用户”加“哪个时间范围”为前置条件。
3.2 用MyBatis-Plus映射实体并完成基础CRUD
实体类对应表结构可以这样写:
@Data @TableName("bill") public class Bill { @TableId(type = IdType.AUTO) private Long id; private Long userId; private Long categoryId; private BigDecimal amount; private Integer type; private String remark; private LocalDate billDate; private LocalDateTime createTime; }@TableName指定表名,@TableId标记主键并让数据库自增。其余字段依赖第2章配置里的map-underscore-to-camel-case自动完成snake_case到驼峰的映射,所以billDate对应bill_date,createTime对应create_time。Mapper接口保持最小化即是优势:
public interface BillMapper extends BaseMapper<Bill> { }继承BaseMapper后,insert、deleteById、selectById、selectPage这些单表操作全部自带,不需要手写一条SQL。Service层做一层很薄的风控与业务包装,Controller不直接碰Mapper对象,这条分层底线能减少后面大量返工。
3.3 统计报表接口中的SQL聚合与分组查询
单表CRUD不复杂,记账本真正有价值的是聚合统计。比如“统计某个用户在指定时间段内各分类的花费”:
<select id="sumByCategory" resultType="com.example.accountbook.vo.CategoryStatVO"> SELECT c.name AS categoryName, SUM(b.amount) AS totalAmount, COUNT(*) AS billCount FROM bill b JOIN category c ON b.category_id = c.id WHERE b.user_id = #{userId} AND b.bill_date >= #{startDate} AND b.bill_date <= #{endDate} GROUP BY c.id, c.name ORDER BY totalAmount DESC </select>XML里把>=写成>=是转义要求,也可以用<![CDATA[ >= ]]>包起来提升可读性。GROUP BY c.id, c.name保证按分类聚合,ORDER BY totalAmount DESC让花费最高的分类排最前。一个容易犯的错是只GROUP BY c.name而不带c.id:MySQL在sql_mode包含ONLY_FULL_GROUP_BY时会直接报错,即便不报错,分类重名也会把数据合错行。
如果再按月汇总,就用日期格式化函数做分组键:
<select id="sumByMonth" resultType="com.example.accountbook.vo.MonthStatVO"> SELECT DATE_FORMAT(bill_date, '%Y-%m') AS month, SUM(amount) AS totalAmount FROM bill WHERE user_id = #{userId} GROUP BY DATE_FORMAT(bill_date, '%Y-%m') ORDER BY month DESC </select>DATE_FORMAT把DATE截成“2024-04”这样的月度前缀,配合GROUP BY一句SQL就产出月度趋势。代价是如果表里数据量很大,函数套在索引列上会让该索引失效,这层关系留到最后一章单独处理。
3.4 Controller层的参数校验与统一返回结构
接口层建议使用spring-boot-starter-validation,在Bill实体或单独请求DTO上用@NotNull、@DecimalMin做约束。返回值构造一个BillVO而不是直接暴露实体,既控制字段可见性,也能在返回时把分类名填好。合理顺序是Controller校验参数,Service决定“是否允许这笔记账落库”,Mapper只执行数据操作。分层边界清晰之后,前端工程师接手SpringBoot后端时,只需要看返回结构里的code、message、data三段式约定,就能直接进入接口联调,不必翻遍每个方法的内部实现。
4. 跑通SpringBoot记账本的本地配置:数据源、分页插件与启动命令
4.1 application.yml里的四个必调参数
SpringBoot记账本跑不通,一半原因在数据源配置。先用最小可运行配置把端口拉起来:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/account_book?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: ${DB_PASSWORD:123456} jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.accountbook.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl四个必调点逐一核对。第一,driver-class-name必须和数据源配套,MySQL 8驱动写com.mysql.cj.jdbc.Driver,不是旧的com.mysql.jdbc.Driver。第二,url里的useUnicode=true和characterEncoding=utf8直接决定中文账目备注是否乱码;serverTimezone=Asia/Shanghai不写,直连MySQL 8会抛时区异常。第三,password用${DB_PASSWORD:123456}写法,意思是环境变量DB_PASSWORD优先,没有时回退到123456,本地开发不用每次改代码。第四,mybatis-plus下的log-impl设为StdOutImpl,SQL会原样打印在控制台,接手后端的人能对着第一手日志调参数,排完性能问题再改回日志门面。
提示:log-impl只适合本地调试。联调完换成org.apache.ibatis.logging.slf4j.Slf4jImpl,避免生产控制台被SQL刷爆。
4.2 Maven启动与IDE启动的差异
命令行启动是校验环境最彻底的方式:
mvn clean package -DskipTests java -jar target/account-book-0.0.1-SNAPSHOT.jar --spring.profiles.active=devmvn clean package先做全量编译和打包,-DskipTests跳过测试以减少无关失败。第二行jar包名以pom.xml里的artifactId和version为准,我写的是常见命名的示例。跑起来后看控制台输出:SpringBoot版本号、Tomcat端口、启动耗时、SQL打印顺序,几处都对上,说明机器层面的环境没有问题了。
IDE里直接运行main类和这条链路有区别。IDE会用模块自己的输出目录,即使pom里资源过滤写得不严谨,application.yml也能正常加载;而用java -jar时,工作目录、classpath和外部配置文件优先级会更严格地生效。以前在IDE里好端端的项目,打成jar后找不到配置,多半就是路径写死成了源码相对路径。所以凡是要交付的源码,建议至少各自跑一遍IDE方式和jar方式,两条路径差一个都不算真正“配置完成”。
4.3 分页插件与H2内存数据库的最短路径
分页插件不配,后面列表接口会踩一个隐蔽的坑:调用selectPage时SQL里没有LIMIT,全表数据都查出来,页面却只显示一页。原因是MyBatis-Plus的分页能力依赖拦截器,没有它不会自动补count和limit。分页配置如下:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }发现“分页没生效”时,先检查项目里有没有这个配置类,再确认DbType与实际数据库一致。如果zip包演示用的是H2,DbType要改成H2,否则分页方言不匹配直接报错。H2作为本地体验库也方便,数据源切换成下面的配置即可免安装启动:
spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:account_book;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE username: sa password:MODE=MySQL让H2尽量模拟MySQL行为,建表SQL里的utf8mb4和反引号可以被接受;DB_CLOSE_DELAY=-1让内存库在连接断掉后继续存活,否则同一个数据源连接关闭后整个库直接消失;DATABASE_TO_LOWER=TRUE避免MySQL风格的大小写差异把复杂SQL误伤。若源码里的SQL完全按MySQL方言写,这种H2方案只适合“先把代码跑起来”,要验证聚合统计和备份恢复,还得回到MySQL环境。
5. 用EXPLAIN验证记账本统计SQL是否命中复合索引
月报接口变慢,通常不是表太大,而是SQL写法让索引根本没被用上。验证方法很简单:拿一条正在用的统计SQL看执行计划。
EXPLAIN SELECT c.name, SUM(b.amount) FROM bill b JOIN category c ON b.category_id = c.id WHERE b.user_id = 7 AND b.bill_date BETWEEN '2024-01-01' AND '2024-03-31' GROUP BY c.id, c.name;重点看三列:type、key、rows。命中第3章预留的idx_user_date时,type会落到ref或range,key显示idx_user_date,rows只是所查时间段内的数据量。如果看到type=ALL,说明查询在走全表扫描,再往下定位是索引没建还是索引没被用上。Extra里出现Using temporary或Using filesort,代表GROUP BY没能在索引顺序内完成,需要额外排序,数据量一大就得回头调索引或改写SQL。
另一种常见反例,是把时间条件写成了格式化后的字符串:
SELECT DATE_FORMAT(bill_date, '%Y-%m') AS month, SUM(amount) FROM bill WHERE user_id = 7 AND DATE_FORMAT(bill_date, '%Y-%m') BETWEEN '2024-01' AND '2024-03' GROUP BY DATE_FORMAT(bill_date, '%Y-%m');DATE_FORMAT包住索引列,MySQL优化器一般会放弃索引,type直接降成ALL。正确做法是让WHERE保持原生日期范围,按月分组的工作只落在输出层:
SELECT DATE_FORMAT(bill_date, '%Y-%m') AS month, SUM(amount) FROM bill WHERE user_id = 7 AND bill_date >= '2024-01-01' AND bill_date < '2024-04-01' GROUP BY DATE_FORMAT(bill_date, '%Y-%m');GROUP BY仍是函数表达式,但范围扫描已利用idx_user_date把结果集压到很小,剩余排序成本可控。对报表接口再补两条约定:接口永远强制带userId,不允许出现不指定用户却统计全库账单的查询;日期范围上限设为一年,超出要求调用方按月拆批。前者让索引条件恒定成立,后者避免报表SQL在后端把跨月数据折叠成一张大临时表。数据量继续涨,比如单用户月账单超过几十万行,再把“年月”拆成独立统计字段,配合(user_id, stat_month)复合索引做物化汇总。最终检查点可以记成一句约定:统计SQL上线前必须贴一次EXPLAIN,key列非空且type不含ALL,才允许进入代码评审。
本文还有配套的精品资源,点击获取