十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot记账本源码实战:从项目骨架到SQL性能优化

SpringBoot记账本源码实战:从项目骨架到SQL性能优化 简介这是一份基于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注册不上接口总是404mybatis-plus-boot-starter单表CRUD直接继承BaseMapper与SpringBoot版本不匹配启动报Interceptor错误mysql-connector-javaMySQL驱动url里少了serverTimezone会报时区异常lombok简化getter/setterIDE默认不开注解处理编译报找不到方法knife4j/springfox生成接口调试文档与SpringBoot 2.6以上路径匹配规则冲突选MyBatis-Plus而较少选Spring Data JPA是因为账目查询往往带着多个可选条件时间范围、分类ID、类型、关键字。MyBatis-Plus的LambdaQueryWrapper能拼出稳定可读的SQL需要写统计时才落到XMLJPA的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目录找XMLtype-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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账目明细表;金额用DECIMAL(10,2)不要用float或double。记账对精度敏感浮点累加在月报里会出现0.1这类“看起来不该存在”的误差。bill表保留type字段是刻意的虽然从category也能推出收入还是支出但统计时避免每次都关联分类表才能让单表扫描保持简单。存储上bill_date用DATEcreate_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_datecreateTime对应create_time。Mapper接口保持最小化即是优势public interface BillMapper extends BaseMapperBill { }继承BaseMapper后insert、deleteById、selectById、selectPage这些单表操作全部自带不需要手写一条SQL。Service层做一层很薄的风控与业务包装Controller不直接碰Mapper对象这条分层底线能减少后面大量返工。3.3 统计报表接口中的SQL聚合与分组查询单表CRUD不复杂记账本真正有价值的是聚合统计。比如“统计某个用户在指定时间段内各分类的花费”select idsumByCategory resultTypecom.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 gt; #{startDate} AND b.bill_date lt; #{endDate} GROUP BY c.id, c.name ORDER BY totalAmount DESC /selectXML里把写成gt;是转义要求也可以用![CDATA[ ]]包起来提升可读性。GROUP BY c.id, c.name保证按分类聚合ORDER BY totalAmount DESC让花费最高的分类排最前。一个容易犯的错是只GROUP BY c.name而不带c.idMySQL在sql_mode包含ONLY_FULL_GROUP_BY时会直接报错即便不报错分类重名也会把数据合错行。如果再按月汇总就用日期格式化函数做分组键select idsumByMonth resultTypecom.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 /selectDATE_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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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里的useUnicodetrue和characterEncodingutf8直接决定中文账目备注是否乱码serverTimezoneAsia/Shanghai不写直连MySQL 8会抛时区异常。第三password用${DB_PASSWORD:123456}写法意思是环境变量DB_PASSWORD优先没有时回退到123456本地开发不用每次改代码。第四mybatis-plus下的log-impl设为StdOutImplSQL会原样打印在控制台接手后端的人能对着第一手日志调参数排完性能问题再改回日志门面。提示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.activedevmvn 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包演示用的是H2DbType要改成H2否则分页方言不匹配直接报错。H2作为本地体验库也方便数据源切换成下面的配置即可免安装启动spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:account_book;MODEMySQL;DB_CLOSE_DELAY-1;DATABASE_TO_LOWERTRUE username: sa password:MODEMySQL让H2尽量模拟MySQL行为建表SQL里的utf8mb4和反引号可以被接受DB_CLOSE_DELAY-1让内存库在连接断掉后继续存活否则同一个数据源连接关闭后整个库直接消失DATABASE_TO_LOWERTRUE避免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或rangekey显示idx_user_daterows只是所查时间段内的数据量。如果看到typeALL说明查询在走全表扫描再往下定位是索引没建还是索引没被用上。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上线前必须贴一次EXPLAINkey列非空且type不含ALL才允许进入代码评审。本文还有配套的精品资源点击获取
返回列表