简介:这是一套基于Java的记账系统毕业设计资源,围绕用户、账目记录等典型业务场景展开,面向需要完成课程设计、毕业设计,以及想系统练习Java Web开发的初学者。资源共280个文件,压缩包约71.94MB,核心内容包括java源码、xml及yml配置、sql数据库脚本、docx部署文档和mp4部署视频,同时包含大量js、css前端资源与字体图标文件,目录按源码、数据库、说明文档、视频清晰划分,便于按序学习。项目覆盖MVC设计模式、Spring MVC、MyBatis/Hibernate、JUnit等知识点,也涉及jQuery、Bootstrap前端库的整合应用;通过阅读源码可理解分层设计,通过部署文档和视频可完成环境配置、数据库连接、项目导入与打包运行,数据库脚本可直接初始化示例账目数据。源码结构完整,适合在本地动手改造和学习。目前已有187人学习下载,对需要快速掌握Java Web开发全流程和毕业设计选题的同学有较高参考价值。
1. 记账系统毕业设计的资源构成与运行逻辑
拿到这套基于Java的记账系统毕业设计资源(源代码+数据库+部署文档+部署视频)时,压缩包分成四块:01-视频、02-源码、03-数据库、04-说明文档。
视频录的是从IDEA导入到启动Tomcat的完整走查,数据库目录放着建表SQL,源码web目录下能看到layui.css、layer.css、index.css这些Layui静态资源。技术栈在Java Web里很典型:Spring MVC控制请求流转,MySQL存账目流水,Layui做列表弹窗,Tomcat做运行容器。
对做课程设计或毕业设计的Java学习者来说,这套资源的价值在于一条账目数据从MySQL经Java到页面的完整链路能一次看清。下面按数据库表结构、后端分层、前端对接、部署排错四段展开。
2. 记账系统的MySQL表结构与账目数据模型
2.1 三张核心表的职责划分与外键关系
先把数据库打开看明白,后面所有操作才接得上。记账系统的数据逻辑不复杂,核心就是“谁、在哪个分类下、花了或收入了多少钱、什么时候发生的”。落到MySQL里,常见设计是三张表:sys_user保存登录用户,category保存收支分类,bill保存每一条账目流水。分类表单独拆出来而不是把分类写死在bill表里,是为了后面做统计时不用每条SQL都带着一串重复的字符串。
CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE category ( category_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_name VARCHAR(50) NOT NULL, category_type TINYINT NOT NULL COMMENT '0-支出 1-收入', FOREIGN KEY (user_id) REFERENCES sys_user(user_id) ); CREATE TABLE bill ( bill_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, bill_date DATE NOT NULL, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES sys_user(user_id), FOREIGN KEY (category_id) REFERENCES category(category_id) );几个关键字段的设计取舍先说清楚。金额字段用DECIMAL(10,2)而不是FLOAT或DOUBLE,是因为浮点型在累加时会产生精度漂移,做月度汇总时很可能出现0.999999这类结果;DECIMAL按十进制字符串存储,适合账目这种对精度敏感的场景。category_type用TINYINT标记收支方向,0表示支出、1表示收入,比用VARCHAR存汉字省空间,也方便前端用三元表达式直接翻译。外键的作用是保证bill里指向的category_id真实存在,防止删除分类后留下悬挂数据。
bill表同时存了user_id和category_id,单看冗余,实际是刻意保留的。统计“当前用户某个月的支出”时,直接对bill.user_id过滤即可,不需要每次JOIN到category再关联回sys_user,查询路径短一截。这个取舍在数据量不大的毕设里不明显,但体现的是“查询优先”的建模思路,答辩时可以拿出来讲。下面用表格把bill表的核心字段列清楚:
| 字段 | 类型 | 说明 |
|---|---|---|
| bill_id | INT | 账目主键,自增 |
| user_id | INT | 所属用户,外键关联sys_user |
| category_id | INT | 所属分类,外键关联category |
| amount | DECIMAL(10,2) | 金额,收支方向由分类决定 |
| bill_date | DATE | 账目发生日期 |
| remark | VARCHAR(200) | 备注,允许为空 |
2.2 数据库脚本执行与连接配置
表结构设计好之后,需要把SQL脚本导入MySQL。03-数据库目录里放的通常是bookkeeping.sql这类建库脚本。我习惯用命令行导入,不受图形工具版本影响:
mysql -uroot -p source /path/to/bookkeeping.sql; SHOW TABLES;source命令会逐行执行脚本里的SQL,脚本开头一般有CREATE DATABASE IF NOT EXISTS bookkeeping DEFAULT CHARACTER SET utf8mb4;,所以本地即使已经建过同名库也不会直接报错。登录MySQL的用户名密码要和后面Java配置文件里的保持一致;如果本机MySQL密码和项目里写的不一样,应该去改jdbc配置,而不是反过来把数据库密码改成源码里的弱口令。
Java后端连接数据库的配置集中在jdbc.properties(或者application.properties)里,核心四行如下:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/bookkeeping?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456url后面的参数经常被忽略。useUnicode=true&characterEncoding=utf8保证中文备注不乱码;useSSL=false避免MySQL 8.0以上默认开启SSL导致控制台刷出大量警告;serverTimezone=Asia/Shanghai解决高版本驱动连接时的时区异常。如果用的是MySQL 8.x,驱动类名要把com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver,这个差异在部署文档里未必会专门标出来,但日志里看到ClassNotFoundException时先查这一项,大部分情况下问题就出在这里。
2.3 账目增删改查与统计SQL的写法
理解了表结构,就可以把账目相关的增删改查和统计逻辑写出来,这些SQL在项目源码的Mapper文件里基本都能找到对应物,只是被MyBatis的参数占位符包了一层。单笔账目新增是最基础的操作,五个字段对应页面表单里用户填写的内容。真正的难点是统计类的聚合查询。比如首页要展示“本月支出按分类的分布”,SQL长这样:
SELECT c.category_name, SUM(b.amount) AS total FROM bill b JOIN category c ON b.category_id = c.category_id WHERE b.user_id = #{userId} AND b.bill_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND b.bill_date < LAST_DAY(CURDATE()) + INTERVAL 1 DAY GROUP BY c.category_id, c.category_name;DATE_FORMAT(CURDATE(), '%Y-%m-01')算出当月第一天的日期,LAST_DAY(CURDATE()) + INTERVAL 1 DAY算出下个月第一天的零点,用“大于等于月初且小于下月月初”的区间把整个月框住,能正确走bill_date索引,避免写成WHERE MONTH(bill_date) = MONTH(CURDATE())这种无法使用索引的写法。GROUP BY后统计出的total再交给Java端组装成饼图或柱状图数据。这条统计SQL是记账项目里最高频的查询形态,面试被问到SQL优化基础手段时拿它举例很合适。
3. Spring MVC分层中的记账业务与请求流转
3.1 Controller层的URL设计与参数绑定
后端部分,先从前端最容易接触到的Controller层说起。Controller的职责非常单一:接收页面请求、调用Service、把结果转成JSON或跳转视图。常见的URL设计是REST风格与功能名混合:
@Controller @RequestMapping("/bill") public class BillController { @Autowired private BillService billService; @ResponseBody @RequestMapping("/list") public LayuiTableResult list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, HttpSession session) { Integer userId = (Integer) session.getAttribute("userId"); List<BillVO> list = billService.pageQuery(userId, page, limit); return LayuiTableResult.ok(list, billService.count(userId)); } @ResponseBody @PostMapping("/add") public Result add(Bill bill) { billService.addBill(bill); return Result.success(); } }@RequestParam(defaultValue = "1")处理分页参数,前端Layui表格第一次加载时可能不传page和limit,默认值保证接口不会因为空参数报错。userId不直接从前端传,而是从HttpSession里取,避免用户越权查询别人的账目,这是很小的安全习惯,但答辩时提到会加分。@PostMapping("/add")限制请求方法,防止地址栏直接GET提交把脏数据写进库。add方法的Bill参数由Spring MVC按同名属性自动绑定,页面表单里name="amount"、name="categoryId",这里就自动组装出一个Bill对象;如果表单里有日期字段billDate,后端对应属性是Date类型,需要配合@DateTimeFormat(pattern = "yyyy-MM-dd")才能完成字符串到日期的转换,否则会抛400绑定异常。
3.2 Service层的事务边界与统计逻辑
Service层是记账系统里业务逻辑最厚的地方。一个典型场景是用户执行“添加账单”时,如果选中的分类已经被删掉,系统需要自动创建一个“未分类”兜底;或者注册用户时先插入sys_user,再初始化一套默认分类(餐饮、交通、购物等)。这类跨表操作只要有一半失败,数据就会不一致,所以必须加事务控制:
@Service public class BillServiceImpl implements BillService { @Autowired private BillMapper billMapper; @Autowired private CategoryMapper categoryMapper; @Override @Transactional(rollbackFor = Exception.class) public void addBillWithDefaultCategory(Bill bill) { Category category = categoryMapper.selectById(bill.getCategoryId()); if (category == null) { Category defaultCat = new Category(); defaultCat.setUserId(bill.getUserId()); defaultCat.setCategoryName("未分类"); defaultCat.setCategoryType(0); categoryMapper.insert(defaultCat); bill.setCategoryId(defaultCat.getCategoryId()); } billMapper.insert(bill); } }@Transactional(rollbackFor = Exception.class)是关键。默认情况下Spring只在遇到RuntimeException时回滚,如果业务方法抛出的是受检异常,事务不会自动回滚;显式指定rollbackFor = Exception.class能保证所有异常都触发回滚。很多人写毕设时不加这个参数,日常流程跑起来没事,但一旦数据库写了一半抛异常,账目前半截存进去、后半截丢了,对账就对不上。答辩时主动说出这个区别,比背十道java面试题更能体现对事务机制的理解。另一个细节是:事务应该加在Service层的公开方法上,而不是Controller层;如果一个Service方法内部调用了另一个Service方法,只有经过Spring代理的入口才会生效,同类内部调用不触发事务。
3.3 持久层用MyBatis还是JdbcTemplate
账目查询和写入最终都要落到SQL执行上,这个项目的持久层选择要在MyBatis、JdbcTemplate和Spring Data JPA之间取舍。毕业设计里绝大多数用MyBatis,因为SQL完全自己控制,复杂的分类统计可以直接写XML映射文件,调试时把SQL单独拿出来就能跑。下面这个对比整理了三种方案的适用边界,方便按自己的需求调整或改造:
| 维度 | MyBatis | JdbcTemplate | Spring Data JPA |
|---|---|---|---|
| SQL控制力 | 完全可控,XML里写原生SQL | 完全可控,Java代码里拼SQL | 弱,复杂查询要写@Query |
| 学习曲线 | 中,需要理解Mapper映射 | 低,会JDBC就能上手 | 中高,要理解ORM生命周期 |
| 适合场景 | SSM结构的Web项目 | 轻量工具类、快速原型 | 以实体管理为主的业务系统 |
| 踩坑概率 | 参数映射、动态SQL标签 | 手动拼SQL容易漏空格 | N+1查询、懒加载异常 |
建议是:如果这个项目源码里已经用了MyBatis,就顺着它的Mapper接口走,新增功能时在XML里加SQL即可;如果是为了快速改造成自己的课设,JdbcTemplate的代码量更少,三个类能写完账目全部CRUD。JPA在记账这种以聚合查询为主的场景里反而要写很多@Query,不如前两个直接。持久层选型要讲得出理由,答辩老师问“为什么不用Hibernate”时,从SQL控制力和查询性能两个角度答,比说“大家都用这个”更有说服力。
4. Layui前端与后端接口的数据对接
4.1 静态资源在web目录中的加载顺序
源码web目录下能看到layui.css、layer.css、public.css、index.css、laydate.css、dtree.css、dtreefont.css、code.css这些文件,它们共同组成Layui的前端资源体系。很多人导入项目后页面样式全乱,第一反应是改CSS,其实是引入顺序不对。正确顺序是:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/layui.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/public.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/index.css">layui.css是框架核心样式,必须先加载;public.css放公共的按钮、表格间距调整;index.css是当前页面私有样式,最后加载可以覆盖前两层。layer.css、laydate.css分别是弹层组件和日期组件的独立样式,用到对应组件时再加;dtree.css和dtreefont.css是树形组件专用的,做分类树时才需要,不用分类树的页面不要引入,减少样式冲突。排查样式问题的通用做法是:先在浏览器开发者工具里看某个元素实际命中了哪条CSS规则,被覆盖了就查引入顺序,而不是盲目加!important。
4.2 table模块的请求格式与后端JSON约定
Layui表格组件是列表展示的核心。table.render会在初始化时自动向配置的url发送请求,并携带page和limit两个参数。核心配置如下:
table.render({ elem: '#billTable', url: '/bill/list', page: true, limit: 10, cols: [[ { field: 'billDate', title: '日期', width: 120 }, { field: 'categoryName', title: '分类', width: 120 }, { field: 'amount', title: '金额', width: 100, templet: function(d){ return d.categoryType === 0 ? '-' + d.amount : '+' + d.amount; }}, { field: 'remark', title: '备注' } ]] });这里要讲清楚的是后端返回的JSON结构。Layui table默认要求的格式是固定的四层结构,这个约定直接决定了上一章Controller里LayuiTableResult长什么样:
| 返回字段 | 类型 | 说明 |
|---|---|---|
| code | int | 0表示成功,非0表示失败 |
| msg | String | 失败时的错误提示,成功可为空 |
| count | int | 数据总数,用于计算分页总页数 |
| data | Array | 当前页的数据行数组 |
字段名一个都不能错:count少了,表格底部分页栏会显示“共0条”;data不是数组,表格整页空白;code非0时表格会把msg里的内容当作错误提示展示。Java后端可以直接用Map或专门的LayuiTableResult类构造:
Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("msg", ""); result.put("count", total); result.put("data", list);templet回调里通过d.categoryType判断收支方向,拼成“-128.00”或“+200.00”的展示形式,比在Java端预先拼好字符串再传过来更灵活,修改展示规则时不用动后端接口。
4.3 表单弹窗、laydate日期组件与layer消息反馈
添加和编辑账目通常通过弹窗完成。Layui生态里弹层和表单是拆开的组件:layer.open负责弹窗容器,弹窗内放一个form表单,日期字段用laydate渲染,核心逻辑如下:
layui.use(['layer', 'form', 'laydate', 'table'], function(){ var layer = layui.layer, form = layui.form, laydate = layui.laydate, table = layui.table; laydate.render({ elem: '#billDate', trigger: 'click' }); form.on('submit(addBill)', function(data){ $.post('/bill/add', data.field, function(res){ if(res.code === 0){ layer.closeAll('page'); table.reload('billTable'); layer.msg('保存成功', {icon: 1}); } else { layer.msg(res.msg, {icon: 2}); } }); return false; }); $('#addBtn').on('click', function(){ layer.open({ type: 1, title: '记一笔', area: ['420px', '380px'], content: $('#billForm').html() }); }); });form.on('submit(addBill)')在layui.use回调里绑定一次即可,Layui内部用事件委托处理动态插入的表单按钮。真正容易忽略的是两点:按钮必须有lay-submit和lay-filter="addBill"两个属性,并且必须位于form标签内部,否则点击时不会触发这个回调;如果弹窗内容是用JS字符串动态拼接的,拼接完成后需要调用form.render()重新渲染,否则下拉框和开关组件不生效。提交成功后先layer.closeAll('page')关掉弹窗,再table.reload('billTable')刷新列表,最后layer.msg给出反馈,这是Layui页面里最常见的交互节奏。
5. 部署Tomcat时的环境配置与常见异常
5.1 war包部署与访问路径
部署前先确认JAVA_HOME环境变量指向JDK安装目录,PATH里能找到java,命令行执行java -version能正常回显,再进入打包步骤。项目根目录下有mvnw.cmd,Windows下可以直接执行:
.\mvnw.cmd clean package打包完成后target目录下会生成war文件。把war复制到Tomcat的webapps目录,启动Tomcat后war会自动解压,访问路径默认是http://localhost:8080/war包名/。想直接通过根路径访问,把war包改名为ROOT.war再放进去。如果是在IDEA里调试,可以在Run Configuration里选本地Tomcat,在Deployment页签把war加进去;这种方式改完Java代码后IDEA会自动编译并热部署,比手动复制war包高效。部署后在IDEA里打断点,如果提示“当前不会命中断点”,优先检查运行中的class和源码是不是同一份,重新package一次再部署,比反复Clean项目有用。
5.2 数据库连接失败与驱动缺失的日志定位
启动阶段最常见的两类异常是数据库连不上和驱动类找不到。前者日志里会看到Communications link failure或Access denied for user,后者是ClassNotFoundException: com.mysql.jdbc.Driver。这两类问题按下面的顺序排查最省时间:
| 日志关键字 | 直接原因 | 检查项 |
|---|---|---|
| ClassNotFoundException: com.mysql.jdbc.Driver | JDBC驱动jar缺失 | WEB-INF/lib下有没有mysql-connector-java.jar |
| Access denied for user | 用户名或密码错误 | jdbc.properties的password和MySQL实际密码是否一致 |
| Communications link failure | 端口、服务或防火墙 | MySQL服务是否启动、3306端口是否被占用 |
| Unknown database | 库名不匹配 | 建表脚本执行后库名是否就是jdbc.url里的名字 |
| Table 'xxx' doesn't exist | 数据表缺失 | 是否只建了库没导入表,或表名大小写不一致 |
5.3 用JUnit测试Service层缩小问题范围
以上都排查完项目还是起不来,可以绕过Tomcat直接对Service层写一个JUnit测试,单独验证数据访问是否正常。比如测试账单按月统计的逻辑:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring/spring-mvc.xml", "classpath:spring/spring-mybatis.xml"}) public class BillServiceTest { @Autowired private BillService billService; @Test public void testMonthlySummary() { List<CategorySummaryVO> list = billService.summaryByMonth(1, "2025-06"); Assert.assertNotNull(list); for (CategorySummaryVO vo : list) { System.out.println(vo.getCategoryName() + ": " + vo.getTotal()); } } }这个测试不启动Web容器就能加载Spring容器,直接验证Mapper映射和SQL语句是否正确。如果测试通过但页面还是报错,问题就缩小到了Controller参数绑定或前端请求路径;如果测试失败,日志里会有具体的SQL异常信息,比在Tomcat一堆启动日志里翻高效得多。拿到这套资源后先把数据库脚本执行一遍,确认三张表都建出来再启动项目;日志里看到INFO: Deployment of web application archive ... has finished就说明Tomcat已经成功加载了这个记账系统的war包,后续所有调试都以这一行为基线。
本文还有配套的精品资源,点击获取