基于Java的记账系统毕业设计:从数据库设计到部署实战

基于Java的记账系统毕业设计:从数据库设计到部署实战 简介面向Java初学者和需要完成课程设计的开发者这份基于Java的记账系统毕业设计资源可帮助解决毕业设计选题难、项目不完整、环境搭建复杂等常见问题既适合直接作为毕业设计二次开发也适合用于Java Web实战练习。整个资源包共280个文件涵盖Java源码.java、数据库脚本.sql、Spring MVC及MyBatis相关配置.xml、前端交互页面.js/.css/.html基于layui构建和部署操作录屏.mp4等压缩包约71.94MB目录结构按源码02-源码、数据库03-数据库、说明文档04-说明文档和视频01-视频分区管理便于系统化学习。已有187人学习下载。项目采用MVC分层设计涉及Java SE/EE、Spring MVC、MyBatis、MySQL等主流技术部署文档详细说明了服务器环境配置、数据库连接设置、导入IDE和打包运行等步骤视频教程则同步演示开发环境搭建与部署过程阅读源码还能学习业务逻辑设计、用户请求处理、数据库CRUD操作等实际编程技巧让学习者从设计、开发到部署全流程掌握Java Web项目是毕业设计答辩和实战能力提升的实用素材。1. 毕业设计里的记账系统和你以为的“记账App”不是一回事拿到基于Java的记账系统毕业设计(源代码数据库部署文档部署视频).zip这个压缩包的人通常分两类一类是正在做课程设计的学生另一类是帮学生搭过环境、改过 Bug 的工程师。前者关心的是“我能不能把它跑起来、答辩护不丢人”后者关心的是“这套代码的数据库脚本有没有坑、部署文档能不能直接照着执行”。这个标题的真正价值不是让你写一个类似“随手记”的手机 App而是通过一个 Web 版记账系统把 Java 后端、MySQL 数据表设计、Servlet/Spring MVC 请求处理、Tomcat 部署这几条主线串起来形成一份可以讲解、可以扩展、可以验收的毕业设计交付物。本文不从“项目介绍”讲起而是从零拆解这套系统里最容易被忽视、也最能决定答辩成败的三件事表结构怎么建、增删改查怎么写得规范、部署时哪些环节会卡住。无论你现在手上有没有这份源码读完都能自己搭一个同类的记账系统。2. 记账系统的技术选型为什么毕业设计绕不开 Java MySQL Tomcat2.1 从“纯 Java”到 Web 应用这是个需要跑在容器里的项目基于 Java 的记账系统这个描述并没有指明是 Swing 桌面程序还是 Java Web 工程。但从压缩包名称里的“部署视频”和“部署文档”可以判断这大概率是一个 Web 项目。桌面 Swing 程序最多需要打包成可执行 JAR谈不上“部署”到什么容器而 Web 项目才需要配置 Tomcat、导入数据库、打 war 包。常见的毕业设计技术栈有三类JSP Servlet JDBC Tomcat MySQL这是最经典的 Java Web 课程设计组合结构简单代码直观适合答辩时讲清每一层。Spring MVC MyBatis Maven Tomcat更接近企业开发但配置文件和注解较多对初学者反而不容易讲清楚。Spring Boot JPA/MyBatis-Plus 内嵌 Tomcat部署最简单但“框架黑盒”感更强答辩时如果被问到自动配置原理容易冷场。如果看到源码里有web.xml和HttpServlet那就是第一类如果看到Controller或RestController那就是第二或第三类。我个人在处理课程设计时倾向选择 JSP Servlet因为账务系统的核心业务无非是增删改查用最原始的 Servlet 反而能清楚地展示“请求如何走到数据库”的过程。2.2 数据访问用 JDBC 还是 MyBatis先看这个项目的可维护性数据访问层是决定这套记账系统好不好改的关键。JDBC 适合展示底层原理但每个方法都要写Connection、PreparedStatement、ResultSet、finally close代码量很大。MyBatis 或 DBUtils 能减少样板代码但项目里需要额外引入 jar 包和 XML 映射文件。一个相对均衡的方案是使用 DBUtils 这类轻量级封装。它的QueryRunner能自动处理结果集到 JavaBean 的映射同时仍然是 SQL不引入复杂动态 SQL。如果你拿到的项目是纯 JDBC也没有问题重要的是看它有没有把连接关闭放在finally或try-with-resources里。很多在线源码示例为了省事写的是Statement而不是PreparedStatement这在记账系统里是隐患因为金额和备注字段如果包含单引号会导致 SQL 拼接错误甚至注入。下面这段代码展示的是用PreparedStatement做条件查询的推荐写法public ListRecord queryByUserIdAndDate(int userId, String startDate, String endDate) { String sql SELECT id, user_id, account_id, category_id, amount, note, create_time FROM t_record WHERE user_id ? AND create_time BETWEEN ? AND ? ORDER BY create_time DESC; ListRecord list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setString(2, startDate); ps.setString(3, endDate); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Record r new Record(); r.setId(rs.getInt(id)); r.setAmount(rs.getBigDecimal(amount)); r.setNote(rs.getString(note)); list.add(r); } } } catch (SQLException e) { throw new RuntimeException(按日期查询流水失败, e); } return list; }这里用try-with-resources同时在Connection、PreparedStatement、ResultSet三层上自动关闭资源比在finally里手动判空 close 更不容易漏。参数顺序完全由 SQL 中的?决定不需要拼字符串。如果项目里所有 DAO 都按这个模式写后续改成 MyBatis 时只需要把 SQL 原样搬进select标签即可。2.3 前端页面JSP 还是 Thymeleaf别在这个环节丢分记账系统的页面数量不多登录页、注册页、主页、流水添加/编辑页、统计页。如果项目技术栈是 Spring Boot那么 Thymeleaf 是合理的默认选择如果是 JSP Servlet就用 JSTL EL 表达式。很多同学在写 JSP 时会犯一个错误把大段 Java 逻辑直接用% %写在页面上。比如在首页遍历数据库查询结果。这种做法虽然能跑但答辩时如果老师问你“为什么不用 EL”你会很难解释。正确做法是让 Servlet 把查询结果放进request.setAttribute(recordList, list)然后 JSP 用c:forEach遍历。这里有一个关键点容易被忽略在web.xml或 Servlet 3.0 的WebServlet注解中配置字符编码过滤器。记账系统里中文备注和分类名非常多如果在 JSP 页面设置了charsetUTF-8但请求没走编码过滤器必现乱码。最简单的做法是写一个CharacterEncodingFilter强制所有请求和响应都使用 UTF-8WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }这个过滤器应该在项目初期就加上否则后期增加收支类型后所有中文都会变成问号项目演示时会非常尴尬。3. 数据库设计记账系统的那几张表一张也不能少3.1 从 Excel 思维到第三范式用户表、账户表、分类表、流水表记账系统的数据库不能只建一张t_record表了事。如果只有流水表虽然可以完成记录功能但统计不同账户余额、按分类汇总支出时就很难写 SQL。毕业设计的数据模型至少要包含四张核心表表名用途关键字段t_user系统登录用户id,username,password(哈希后),create_timet_account账户/钱包id,user_id,account_name,balance,typet_category收支分类id,user_id,category_name,type(income/expense)t_record流水记录id,user_id,account_id,category_id,amount,note,create_time用第三范式来看流水表不应该直接保存“收入”“餐饮”这种字符串而是保存category_id外键。这样做的第一个好处是统一分类名称不会出现“餐饮”和“饭钱”两种一样的数据第二个好处是统计 SQL 可以直接GROUP BY t_category.category_name不用在 Java 里做映射。创建表的 SQL 示例如下CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(255) NOT NULL COMMENT 密码, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, account_name VARCHAR(50) NOT NULL COMMENT 账户名称, balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额, type VARCHAR(20) DEFAULT ASSET COMMENT 资产类型, CONSTRAINT fk_account_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账户表;注意这里password字段长度给了 255这是为存储 BCrypt 哈希值预留的而不是明文密码。很多毕业设计源码里直接用VARCHAR(20)存明文虽然演示没问题但答辩时被问到安全性就会很被动。如果原项目里已经是VARCHAR(50)至少可以在部署文档里加一句“生产环境请替换为 BCrypt 加密”。3.2 数据字典和初始化分类数据怎么预置记账系统的分类数据不能每次注册用户时都让用户从头创建。常见做法是写一个data.sql或调用一次初始化 Servlet将默认分类插入到t_category表。但这里有一个设计取舍t_category是否需要user_id。如果全局共享一个分类表所有用户看到相同的分类SQL 查询简单但用户自定义分类需要额外的关联表。如果每个用户有自己的分类注册时复制默认分类实现起来更直观答辩也容易解释。对于毕业设计建议采用第二种方案。注册用户时在UserService.register()里一次性插入默认分类INSERT INTO t_category (user_id, category_name, type) SELECT ?, 工资, income UNION ALL SELECT ?, 兼职, income UNION ALL SELECT ?, 餐饮, expense UNION ALL SELECT ?, 交通, expense;这样做的好处是后续在首页查询分类列表时只需要WHERE user_id ?不需要判断IS NULL这种全局兜底逻辑。数据库初始化脚本里要保留这一段否则注册新用户后页面上的分类下拉框就是空的。3.3 分页查询与统计 SQL毕业设计答辩时的加分项流水表会随着使用快速膨胀如果首页一次性SELECT *数据量小的时候看不出来数据量到几千条后页面就会明显变慢。分页查询是记账系统必须有的功能也是数据库课程设计里老师最喜欢问的点。MySQL 标准分页写法是LIMIT offset, size但要注意计算 offset 时不能用字符串拼接。推荐这样做-- 查询第 page 页每页 pageSize 条 SELECT r.id, r.amount, r.note, c.category_name, a.account_name, r.create_time FROM t_record r JOIN t_category c ON r.category_id c.id JOIN t_account a ON r.account_id a.id WHERE r.user_id ? ORDER BY r.create_time DESC LIMIT ?, ?;DAO 层需要接收三个参数userId、offset、pageSize。offset (page - 1) * pageSize的计算放在 Service 层完成而不是放在 JSP 里。另一个答辩加分项是月度支出统计SELECT DATE_FORMAT(create_time, %Y-%m) AS record_month, SUM(amount) AS total_expense FROM t_record r JOIN t_category c ON r.category_id c.id WHERE r.user_id ? AND c.type expense GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY record_month DESC;这里如果只用GROUP BY MONTH(create_time)跨年会混在一起。DATE_FORMAT同时保留年份和月份才是正确的月统计口径。4. 核心代码实现记账流水的增删改查怎么写才像“系统”4.1 DAO 层用 PreparedStatement 还是 Statement看一套记账逻辑源码先看 DAO 层DAO 层写得好不好直接决定数据是否安全。所谓“安全”不是说防止黑客攻击而是防止合法操作产生脏数据。比如记录一笔支出后t_account.balance必须同步减少如果新增流水和修改余额是两次独立连接操作中途断电或抛出异常就会导致余额和流水对不上。DAO 层内部可以使用Connection来提供事务上下文。常规做法是在 Service 层获取连接、设置事务手动提交再传给 DAO 方法public boolean addExpense(Connection conn, Record record, BigDecimal newBalance) throws SQLException { String insertSql INSERT INTO t_record (user_id, account_id, category_id, amount, note, create_time) VALUES (?, ?, ?, ?, ?, NOW()); String updateSql UPDATE t_account SET balance ? WHERE id ? AND user_id ?; try (PreparedStatement insertPs conn.prepareStatement(insertSql); PreparedStatement updatePs conn.prepareStatement(updateSql)) { insertPs.setLong(1, record.getUserId()); insertPs.setLong(2, record.getAccountId()); insertPs.setLong(3, record.getCategoryId()); insertPs.setBigDecimal(4, record.getAmount()); insertPs.setString(5, record.getNote()); int inserted insertPs.executeUpdate(); updatePs.setBigDecimal(1, newBalance); updatePs.setLong(2, record.getAccountId()); updatePs.setLong(3, record.getUserId()); int updated updatePs.executeUpdate(); return inserted 1 updated 1; } }这段代码里newBalance不是简单地从数据库读出来减掉这一次的金额再写回去而是在 Service 层用原子 SQL 配合行锁来保证并发正确性。如果直接用“先查余额-减金额-再更新”两个请求同时发生时最后写入的金额会把先写入的覆盖掉。4.2 Service 层金额校验和事务边界不能省Service 层是记账系统的“业务网关”。所有来自 Servlet 的参数都要先在这里做合法性校验而不是到 SQL 执行时才报错。对于记账系统至少要有这些检查amount是否为负数收入金额必须大于 0支出金额用正数记录在实际扣减时取反。支出时账户余额是否足够如果余额不足应该返回“余额不足”的业务提示而不是数据库抛出异常。分类category_id是否属于当前用户防止用户通过构造请求把记录记到别人的分类下。账户account_id是否属于当前用户这一项尤其不能漏否则跨账户操作会导致数据串号。以下是事务控制的推荐骨架public void addRecord(RecordDTO dto) { if (dto.getAmount() null || dto.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(金额必须大于0); } Account account accountDao.findById(dto.getAccountId()); if (account null || account.getUserId() ! dto.getUserId()) { throw new IllegalArgumentException(账户不存在); } BigDecimal newBalance dto.getType().equals(expense) ? account.getBalance().subtract(dto.getAmount()) : account.getBalance().add(dto.getAmount()); if (newBalance.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(余额不足); } TransactionTemplate tx new TransactionTemplate(transactionManager); tx.execute(status - { try { recordDao.insert(dto.toRecord()); accountDao.updateBalance(dto.getAccountId(), newBalance); return true; } catch (DataAccessException e) { status.setRollbackOnly(); throw e; } }); }如果项目没引入TransactionTemplate而是纯 JDBC可以在 Servlet 里写conn.setAutoCommit(false)执行成功后conn.commit()任何一步失败则conn.rollback()。这条规则写进部署文档里能避免很多“流水记录了余额没变”的问题。4.3 Web 层从表单提交到 Servlet再到 JSON 响应老式 JSP Servlet 项目的 Web 层一般这样做LoginServlet获取username和password调用UserService.login()成功就写入session失败就response.sendRedirect(login.jsp?error1)。对于“新增流水”这种操作可以做成 AJAX 请求页面不刷新就更新下方的流水列表。这里需要用到一个 JSON 响应工具方法protected void writeJson(HttpServletResponse response, Object data) throws IOException { response.setContentType(application/json;charsetUTF-8); PrintWriter writer response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(data)); }比如RecordAddServlet中WebServlet(/record/add) public class RecordAddServlet extends HttpServlet { private RecordService recordService new RecordService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Integer userId (Integer) req.getSession().getAttribute(userId); if (userId null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } RecordDTO dto new RecordDTO(); dto.setUserId(userId); dto.setAccountId(Integer.parseInt(req.getParameter(accountId))); dto.setCategoryId(Integer.parseInt(req.getParameter(categoryId))); dto.setAmount(new BigDecimal(req.getParameter(amount))); dto.setNote(req.getParameter(note)); dto.setType(req.getParameter(type)); try { recordService.addRecord(dto); writeJson(resp, Map.of(code, 0, message, success)); } catch (IllegalArgumentException e) { resp.setStatus(400); writeJson(resp, Map.of(code, 400, message, e.getMessage())); } } }这个接口设计有两个好处一是 Servlet 不直接吞异常而是将业务异常转换成 HTTP 状态码和 JSON 消息前端拿到 400 后弹提示二是判断登录状态放在每个业务 Servlet 的入口处避免未登录用户直接调用接口改数据。如果原项目用的是 BaseServlet 反射分发也建议在基类里做统一登录校验。5. 本地部署从 .zip 到 localhost:8080 的最小可行路径5.1 环境准备JDK 版本、Maven 镜像、Tomcat 版本怎么选部署文档写得再详细环境不匹配也是最常见的卡壳点。Java Web 项目经过十几年发展版本组合非常固定。拿到项目先看两处pom.xml里的java.version以及web.xml头部的 schema 版本。对于使用javax.servlet的老项目建议搭配以下环境组件推荐版本说明JDK8 或 11JDK 17 以上运行老项目可能遇到模块访问错误Maven3.6.3 或 3.8.x低于 3.5 可能无法解析当前依赖树Tomcat8.5 或 9.0Servlet 3.1/4.0 项目均可运行MySQL5.7 或 8.0utf8mb4 字符集支持要确认脚本里已声明如果你拿到的是纯 Eclipse 项目没有pom.xml可以在.classpath文件里看到引入的 jar 包手动将其拷贝到WEB-INF/lib即可。不要只装 JDK 就打开 Tomcat需要用JAVA_HOME环境变量告诉 Tomcat 去哪找 Java。这里给出一条验证是否设定成功的命令echo $JAVA_HOME java -version $CATALINA_HOME/bin/startup.sh如果在 Windows 下是startup.bat启动后出现端口冲突错误先查占用进程netstat -ano | findstr :8080 taskkill /PID pid /F5.2 数据库初始化导入 .sql 的两种方式压缩包里的.sql文件可能是account.sql、db_bookkeeping.sql这类名字。导入之前先确认两件事脚本创建的是哪个数据库名以及脚本里有没有CREATE DATABASE语句。很多脚本默认不建库需要手动执行mysql -uroot -p create database bookkeeping default character set utf8mb4; use bookkeeping; source /path/to/sql/bookkeeping.sql;第二种方式是使用 Navicat 这类 GUI 工具。打开工具后新建连接右键选择“运行 SQL 文件”注意选择“遇到错误继续”这个选项因为老脚本在 MySQL 8.0 上可能因AUTO_INCREMENT或索引语法报错但报错行之后的信息往往是完整的。导入完成后验证表是否齐全USE bookkeeping; SHOW TABLES; SELECT COUNT(*) FROM t_category;这里的t_category如果返回 0 行说明初始化数据没有导进去需要单独执行脚本里INSERT INTO t_category那段。5.3 项目打包与部署手工部署 war 包部署视频里最常见的一步是把target/xxx.war复制到 Tomcat 的webapps目录。但很多同学直接能看到项目首页却看不到登录后的效果原因通常在于数据库连接配置。网页端数据库配置一般写在WEB-INF/classes/db.properties或jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookkeeping?useUnicodetruecharacterEncodingutf8mb4 jdbc.usernameroot jdbc.password123456注意com.mysql.cj.jdbc.Driver对应 MySQL 8.0。如果原项目用的是com.mysql.jdbc.Driver在 MySQL 8 下会报警告建议改掉。执行打包前检查是否配置了 Maven 私服镜像否则首次下载依赖可能很慢。在settings.xml中加上阿里云镜像mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/central/url /mirror然后执行mvn clean package -DskipTests cp target/bookkeeping.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.shTomcat 启动后会自动解压bookkeeping.war访问路径为http://localhost:8080/bookkeeping/。如果直接访问 8080 根路径出现 Tomcat 默认页面而不是你的系统说明项目没有作为 ROOT 部署。可以把bookkeeping.war改名为ROOT.war后重新放置但需要先停掉 Tomcat 并删除旧目录。5.4 部署文档和部署视频里最容易出现的验证点部署完成后还要跑一遍核心业务路径确认不是“启动成功但什么都做不了”。我会按以下顺序验证注册一个新用户观察数据库的t_user表中是否增加记录。登录后查看分类下拉框是否有默认数据。这里最容易出现的异常是 NPE原因是注册时没有复制默认分类。添加一笔支出再查看t_account.balance是否减少、t_record是否有对应记录。查看统计页确认当月支出图表是否更新。重新登录一次检查 Session 是否过期后跳转到登录页。6. 部署后最重要的三件事日志、编码、连接池6.1 先学会看 Tomcat 的 logs 目录而不是只看页面报错部署视频里通常只演示结果但实际配置过程中Tomcat 的logs/catalina.out才是第一手排错依据。页面 500 时打开localhost.2025-06-04.log这类按日滚动的日志文件搜Exception往上翻几行就能看到具体是 SQL 语法错误还是 ClassNotFoundException。如果日志打印的是Communications link failure先检查 MySQL 服务是否启动再检查连接 URL 是否正确。如果是Access denied for user rootlocalhost把db.properties里的密码改成你的实际密码不要反复重装 Tomcat。6.2 把连接池初始化代码改成“懒加载”之外的版本很多老式记账系统在 DAO 里直接用一个static块初始化连接池。如果部署后页面第一次访问报连接超时通常是连接池初始化失败但被Class.forName吞掉了。推荐在DBUtil里加入一个显式初始化方法并在 ServletContextListener 启动时调用public class DbInitListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { try { DBUtil.init(); } catch (Exception e) { sce.getServletContext().log(数据库初始化失败, e); throw new RuntimeException(e); } } }在web.xml里注册listener listener-classcom.example.listener.DbInitListener/listener-class /listener这样如果是配置错误Tomcat 会在启动阶段直接失败而不是等用户点击页面时才抛异常。6.3 给数据库做一次备份比任何演示都更有说服力答辩现场演示记账系统时老师可能会问“如果数据库误删了怎么办”。与其口头回答不如提前准备好一个备份脚本。只需在项目根目录放一个backup.sh#!/bin/bash BACKUP_DIR./backup DB_NAMEbookkeeping DB_USERroot DB_PASSWORD123456 mysqldump -u$DB_USER -p$DB_PASSWORD $DB_NAME \ --default-character-setutf8mb4 \ --single-transaction \ --routines $BACKUP_DIR/bookkeeping_$(date %Y%m%d_%H%M%S).sql执行后生成的文件可以直接导入回 MySQL。这个脚本既能出现在部署文档中也能在答辩演示时现场跑一遍比静态截图更能体现工程意识。记账系统的核心不是花哨的图表而是确保每一笔记录在页面关闭、服务器重启、数据库异常后仍然完整这个备份脚本就是最后一道防线。本文还有配套的精品资源点击获取