简介:面向Java毕业设计的记账管理系统完整资料包,涵盖毕业论文、答辩PPT、源代码与数据库文件,适合计算机专业学生用于课程设计或毕业设计参考,也可作为记账类系统开发与答辩演示的范例。压缩包内共13个文件,包含7张系统页面截图、1个SQL数据库脚本、2个zip归档(源代码和论文等),以及3条辅导视频链接,整体大小约24.71MB。系统功能覆盖用户注册登录、收支记录、月统计报表、管理员信息管理等模块,数据库中包含建表与示例数据,可直接导入使用;视频教程依次演示环境部署、数据库创建与项目启动,以及用户端和管理员端的完整操作流程。目前已有1165人学习下载,可作为项目开发、文档撰写和答辩准备的完整方案,尤其适合需要快速理解Java项目结构和业务流程的读者。
1. java记账管理系统,并非简单增删改查的毕业设计
“java毕业设计——基于java记账管理系统的设计与实现”这个题目在高校选题表中年年出现,数据量小、页面直观,第一眼看上去就是用户表和流水表的增删改查。可一旦真正落库,金额精度、分类统计、多用户数据隔离和月度报表SQL就会冒出一堆隐藏细节,而这些又恰好是答辩老师最习惯追问的区域。用Spring Boot + MyBatis + MySQL实现时,该项目覆盖了数据库表设计、MyBatis映射、HTTP接口联调、打包运行全套Java Web链路,难度匹配软件工程、计算机科学与技术、信息管理等专业培养方案。5年以上的工程师带这个题,也能顺手把事务失效、BigDecimal比较、联合索引这些java面试高频概念重新理一遍。下面不按zip包目录逐文件讲,只挑最影响结果的几部分:技术选型、表结构、核心模块和答辩前验证。
2. 选对Spring Boot + MyBatis组合,java记账管理系统才能稳定复现
记账系统每年的实现方案总量不少,但真正能顺利跑完答辩演示的,多数把技术栈复杂度控制在“可复现”这条线上。用Spring Boot 2.x + MyBatis + MySQL 8.0是这类题目最常见的答案;换成原生SSM,环境配置链路太长,答辩机器上很难在5分钟内把一个war包跑起来;再往上走微服务和分布式事务,负担过重,单价也不匹配。把复杂度压在数据库SQL这一层,是这类题目最稳的路线。
2.1 为什么Spring Boot + MyBatis成为记账管理系统的默认答案
Spring Boot内嵌Tomcat,部署形式发生根本变化:一台装了JDK 8的电脑,java -jar即可启动,不需要单独配置外部容器。spring-boot-starter-web把Spring MVC、默认JSON序列化和内嵌服务器一并装好;数据库层由mybatis-spring-boot-starter自动注册SqlSessionFactory、DataSource和事务管理器。源码换机器后,只要JDK版本和MySQL实例在,启动成功率比传统SSM高很多。
| 对比项 | 原生SSM | Spring Boot + MyBatis |
|---|---|---|
| 启动方式 | 外部Tomcat部署war包 | 内嵌Tomcat执行jar |
| 配置文件量 | springmvc.xml、mybatis-config等多文件 | application.yml集中管理 |
| 换机成本 | 需要预装匹配版本Tomcat | JDK8即可运行 |
| 答辩常见故障 | jar包冲突、web.xml加载失败 | 数据源参数、驱动版本问题 |
MyBatis保留了手工SQL能力,不会被全自动ORM黑盒掩盖问题。MyBatis和MyBatis-Plus选哪个,我的建议是:分类维度少、统计逻辑明确的记账系统用原生MyBatis,XML里手写SQL,答辩时指着统计部分说做了复杂聚合;MyBatis-Plus虽然省掉单表CRUD的XML,但实体上的@TableName、@TableId一旦讲不清,答辩反而扣分。
2.2 包结构与模块边界:controller、service、mapper、entity
包结构按经典分层展开,代码量不大但职责清晰:
com.example.account ├── controller # 接收HTTP请求,参数校验,返回统一结果 ├── service # 业务规则,金额校验、分类归属、统计口径 ├── mapper # MyBatis的Mapper接口,XML中写SQL ├── entity # user、category、record表对应的JavaBean ├── config # 拦截器、WebMvc配置 └── common # Result包装、自定义异常controller里不写SQL,最多做参数装配。service是业务规则唯一入口,比如记新账之前判断分类归属哪个用户。mapper只定义接口,配合XML完成数据库操作。分层后的直接收益是:被问到“改一处会影响哪些地方”时,能直接给出Controller -> Service -> Mapper的调用链。没有这一层,回答就散落在各个类里。
2.3 pom.xml与application.yml中直接影响启动的参数
依赖坐标不能随意拷贝,版本匹配决定启动成败。下面是一份最小可运行依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.18</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>spring-boot-starter-web提供Spring MVC与内嵌Tomcat,启动后页面404先查它是否被排除。mybatis-spring-boot-starter 2.3.2对应Spring Boot 2.7.x,放入Spring Boot 3.x会触发自动装配失败。mysql-connector-j是MySQL 8推荐的驱动坐标,旧项目常见的mysql:mysql-connector-java:5.1.49连MySQL 8时会因为认证插件差异报Public Key Retrieval is not allowed,这是本地开发期最隐蔽的坑。
资源文件的这几项,换机后经常决定成败:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/account?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: "your_password" driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.account.entityserverTimezone=Asia/Shanghai解决MySQL服务器时区与JVM默认时区不一致导致的日期偏移,能在连接层直接规避“记账时间少8小时”的问题。allowPublicKeyRetrieval=true仅开发环境建议开启,没有SSL证书时MySQL 8下不配置该参数会拒绝连接。driver-class-name用com.mysql.cj.jdbc.Driver,是mysql-connector-j对应驱动类;写成旧版com.mysql.jdbc.Driver会在启动时告警,部分版本直接抛ClassNotFoundException。mybatis的mapper-locations必须指向XML实际目录,漏掉后项目能启动,第一个查询接口执行时才报BindingException: Invalid bound statement。
3. 数据库设计:记账管理系统的ER关系与统计SQL才是翻盘点
记账系统如果只有一张流水表,在数据库课程设计层面就不合格。正常设计至少三张核心表:用户表、分类表、收支记录表。ER关系是用户对分类一对多、用户对记录一对多、分类对记录一对多。记录表通过category_id引用分类,分类表通过user_id归属用户,整条链路形成闭环,不跨实体越权。
3.1 用户表与分类表:索引、密码字段和状态字段怎么定
用户表是入口表,不需要像订单系统那样堆字段,但两处不能省:用户名唯一索引和密码字段长度。唯一索引让登录查询的WHERE username=?走索引,同时从数据库层拦截重复名。密码字段不能用VARCHAR(32),加盐后MD5会超过32位,SHA-256必然64位,预留VARCHAR(64)更合理。
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(64) NOT NULL, `nickname` VARCHAR(50) DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 1, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';status中1表示启用、0表示停用,比物理删除安全。用户不做注销功能时,状态字段仍能支撑后续“只统计启用用户”的WHERE条件。
分类表是统计的维度表,分类按用户私有处理,不搞全局共享:
CREATE TABLE `category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `name` VARCHAR(30) NOT NULL, `type` TINYINT NOT NULL, `sort` INT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支分类表';type这里不用ENUM,扩展新收支类型时,TINYINT加注释只改数据字典,不需要ALTER TABLE。idx_user索引保证“按用户拉分类列表”走索引范围扫描,缺了它,几万条分类数据后接口延迟会明显起来。
3.2 record流水表:DECIMAL精度、联合索引与完整性约束
流水表是最核心的表。金额不用FLOAT或DOUBLE,原因在于二进制浮点数无法精确表示0.1这类十进制小数,统计月度合计时会出现尾部脏数据。对比效果如下:
| 字段类型 | 存储值示例 | 累加结果风险 |
|---|---|---|
| FLOAT | 101.1 | 101.09999847... |
| DOUBLE | 101.1 | 多次累加出现长尾 |
| DECIMAL(12,2) | 101.10 | 严格按两位小数累加 |
DECIMAL(12,2)的12位总精度包含2位小数,覆盖个人记账到中小商户业务量。record_date用DATE而不是DATETIME,统计查询以“某天到某天”为范围条件,DATE更贴合业务语义,也便于MySQL做范围优化。
CREATE TABLE `record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `category_id` BIGINT NOT NULL, `amount` DECIMAL(12,2) NOT NULL, `type` TINYINT NOT NULL, `remark` VARCHAR(200) DEFAULT NULL, `record_date` DATE NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`, `record_date`), CONSTRAINT `fk_record_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), CONSTRAINT `fk_record_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支流水表';联合索引设计为(user_id, record_date),同时服务“按用户查全部流水”和“按用户加日期范围取数”两类高频查询。索引列顺序不能反过来,写成(record_date, user_id)在多用户同一天的场景下,过滤能力会明显下降。外键这里保留,记录表引用分类表时RESTRICT策略防止误删分类产生孤儿数据;去掉外键就要在应用层补等价校验,否则统计JOIN会出现NULL。
3.3 月度统计与分类统计的GROUP BY正确写法
月度统计把一段日期内的明细压缩成每个月的收入、支出和结余。口径先写对,其他统计逻辑自然通用:
SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS month_income, SUM(CASE WHEN type = 0 THEN amount ELSE 0 END) AS month_expense, SUM(CASE WHEN type = 1 THEN amount ELSE -amount END) AS month_balance FROM record WHERE user_id = #{userId} AND record_date BETWEEN #{beginDate} AND #{endDate} GROUP BY DATE_FORMAT(record_date, '%Y-%m') ORDER BY month;WHERE先执行,把当前用户范围内的数据缩窄,聚合处理尽量小的结果集。CASE WHEN在SUM里完成条件聚合,比先查收入再查支出两次访问表少一次IO。month_balance中type=1记为正、type=0记为负,得到用户可见的“本月结余”,答辩时可直接对照页面图表数值。GROUP BY后面不要再加记录表主键,否则退化成行级统计。
按分类汇总时,关键是理解MySQL 8默认的ONLY_FULL_GROUP_BY约束:
SELECT c.name AS category_name, c.type, SUM(r.amount) AS total FROM record r JOIN category c ON r.category_id = c.id WHERE r.user_id = #{userId} AND r.record_date BETWEEN #{beginDate} AND #{endDate} GROUP BY r.category_id, c.name, c.type ORDER BY total DESC;select列表里的非聚合列必须出现在GROUP BY中。c.name和c.type与r.category_id存在函数依赖,但MySQL在两个表JOIN场景下不能可靠推断这种依赖,所以GROUP BY中一并写上。ORDER BY total DESC让支出最多的分类排最前,结果可以直接输给前端饼图。
4. 核心模块实现:从登录拦截到记账增删改查的完整链路
技术选型与表结构确定后,工程化工作集中在登录会话拦截、记账增删改查边界、统计接口VO组装三层。每一层都有学生容易踩的坑,下面按模块展开。
4.1 登录态与权限拦截:HandlerInterceptor自定义注解更省事
登录拦截使用Spring MVC的HandlerInterceptor,它能拿到Controller方法对象并判断接口是否需要登录。相比Filter,HandlerInterceptor在preHandle里return false就结束请求,还能通过注解注入Service,避免在Filter里手写ApplicationContext。
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.hasMethodAnnotation(PassToken.class)) { return true; } } Object userId = request.getSession().getAttribute("userId"); if (userId == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } return true; } }核心逻辑检查方法上是否存在@PassToken注解,存在则放行;否则从Session取userId,取不到返回401并写出JSON。这里不用Filter的另一个原因是HandlerMethod能准确区分静态资源和真正接口,避免未登录请求被静态资源拦截干扰。自定义注解声明为@Target(ElementType.METHOD)和@Retention(RetentionPolicy.RUNTIME),就能被反射读取。登录、注册接口加上@PassToken,其余接口默认拦截。
注册到MVC需要配置类:
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/user/login", "/user/register", "/error"); } }addPathPatterns("/**")拦截所有路径,excludePathPatterns放行登录注册。接口未加@PassToken时,也可能被路径规则排除。这里把“哪些接口不用登录”的开关统一收口到一个配置类,不散落到Controller里。
4.2 记账Service:BigDecimal比较与事务边界
新增记账如果只有一条insert,实现没有悬念,真正决定质量的是事务边界。记账动作要判断:金额大于0、分类属于当前用户、日期合法。三件事任一失败就不能插入,放在同一方法并加事务。
@Service public class RecordServiceImpl implements RecordService { @Resource private RecordMapper recordMapper; @Resource private CategoryMapper categoryMapper; @Override @Transactional(rollbackFor = Exception.class) public Long addRecord(RecordAddDTO dto, Long userId) { if (dto.getAmount() == null || dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new BizException("金额必须大于0"); } Category category = categoryMapper.selectById(dto.getCategoryId()); if (category == null || !category.getUserId().equals(userId)) { throw new BizException("分类不存在或不属于当前用户"); } Record record = new Record(); record.setUserId(userId); record.setCategoryId(dto.getCategoryId()); record.setAmount(dto.getAmount()); record.setType(category.getType()); record.setRemark(dto.getRemark()); record.setRecordDate(dto.getRecordDate()); recordMapper.insert(record); return record.getId(); } }金额用BigDecimal.compareTo而不是equals,因为equals比较精度,2.0和2.00的equals结果是false,compareTo结果是0。@Transactional必须写明rollbackFor = Exception.class,不写时Spring默认只对RuntimeException回滚,BizException继承自Exception时插入会提交,统计口径直接错掉。分类归属校验的equals使用Long的equals,double与包装类型不会混淆。依据category.getType()设置记录type,前端不用传type,口径由后端单点决定。
这套校验逻辑在java面试中也常见,事务什么时候失效、BigDecimal为什么不能随便equals,都是java面试题常考点。答辩时能讲清这两个细节,比多说一个列表接口更有分量。
下面是一次数据操作的接口约定:
| 接口路径 | 方法 | 请求参数 | 响应说明 |
|---|---|---|---|
| /user/login | POST | username, password | 登录成功后写入Session |
| /record | POST | categoryId, amount, remark, recordDate | 新增一笔流水 |
| /record/list | GET | userId, begin, end, pageNum, pageSize | 分页流水列表 |
| /stat/monthly | GET | userId, begin, end | 月度统计列表 |
| /stat/category | GET | userId, begin, end | 分类汇总列表 |
4.3 统计报表接口与服务端VO组装
统计接口不直接暴露Mapper,而是让Mapper返回VO。下面是MyBatis XML完成月度聚合的写法:
<select id="selectMonthStat" resultType="com.example.account.entity.MonthStatVO"> SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS month_income, SUM(CASE WHEN type = 0 THEN amount ELSE 0 END) AS month_expense FROM record WHERE user_id = #{userId} AND record_date BETWEEN #{begin} AND #{end} GROUP BY DATE_FORMAT(record_date, '%Y-%m') ORDER BY month </select>resultType直接指定MonthStatVO,MyBatis会把month_income映射到monthIncome属性,前提是开启map-underscore-to-camel-case,或SQL中直接AS monthIncome。WHERE里即便begin和end是字符串,MyBatis也按预编译参数位传递,不存在SQL注入,前提是不在XML中写${begin}这类字符串替换。
Controller可以收得很薄:
@GetMapping("/stat/monthly") public Result<List<MonthStatVO>> monthlyStat(@RequestParam Long userId, @RequestParam String begin, @RequestParam String end) { return Result.success(recordMapper.selectMonthStat(userId, begin, end)); }userId应当从Session获取而不是信任前端参数,直接接受前端传参会造成纵向越权。统计时序是前端选月份范围、后端按用户会话id与日期边界聚合、返回月度趋势数据给图表。统一返回体Result中,code为0时成功且data有值;code非0时msg携带业务异常,联调时不需要猜错误码。
5. 答辩前的最终验证:从数据库交付到统计口径一致
源码、数据库脚本和PPT打包成zip交出去之前,不需要再写新功能,而是把已有链路完整过一遍。三个验证点在答辩前夜最值得做。
5.1 交付zip内数据库脚本的自检清单
直接按表检查比逐段说明更高效:
| 自检项 | 通过标准 |
|---|---|
| account.sql可执行 | 在空MySQL实例上source执行无报错 |
| 初始数据覆盖最近2个月 | 流水表不少于30条,含收入和支出 |
| 表结构完整性 | user、category、record三张表都在 |
| 字符集一致 | 全部utf8mb4,页面不乱码 |
mysqldump导出的脚本必须在新库上执行一次,原库能跑不代表新库能过。长期使用的库可能出现自增主键与数据不一致,新库导入还会暴露出字符集问题。
5.2 答辩时关于事务与统计的表述口径
老师问“用了哪些技术”,建议按接口实现、认证拦截、数据库层三段回答,把数据一致性控制放在前边;问“为什么加事务”,举addRecord的场景:分类校验通过后insert,insert失败时校验结果要全部回滚。这样“事务”不是空词。
讲统计要落到具体指标:本月收入、本月支出、本月结余、分类汇总,分别对应哪条SQL的哪一段。能说出GROUP BY在XML第几行的人,在答辩判断里归属真实参与开发的那档。
5.3 现场可复现的验证技巧:两行SQL核对接口结果
答辩当日开浏览器前,先用两行SQL确认预期值:
SELECT SUM(amount) FROM record WHERE user_id = 1 AND type = 0 AND record_date BETWEEN '2024-10-01' AND '2024-10-31'; SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, SUM(amount) AS total FROM record WHERE user_id = 1 GROUP BY month;第一行验证单月支出总额,第二行验证月维度分组。打开页面,把月度图表里的“支出”和第一行结果对齐,把“结余”与接口JSON中month_balance对比,全部一致后再展开讲解。对不上时,按user_id、type、日期三个维度逐个排查,问题通常出在时区转换或金额类型上。
本文还有配套的精品资源,点击获取