Java JDBC 控制台记账系统:从登录到账务管理、文件上传下载
这是我学习 Java 后完成的第一个 JDBC 小项目:一个基于控制台的记账系统。
项目没有使用 Spring、MyBatis 等框架,主要通过JDBC 连接池 + DbUtils + 三层架构操作 MySQL;另外还使用 Socket 实现了账务文件和普通文件的上传、下载。
本文不贴完整源码,重点记录项目的整体流程、关键代码和实际运行效果,方便后续复习。
一、项目功能
系统包含两部分功能:
1. 本地账务管理
用户注册、登录
查询全部账务
按日期查询账务
添加账务
修改账务
删除单条或多条账务
按类别、账户、说明、金额搜索账务
导出账务到本地 txt 文件
2. Socket 文件传输
上传指定日期的账务文件到服务器
从服务器下载账务文件
上传普通文件,例如图片、txt 文件等
下载普通文件
二、项目结构
项目按照常见的三层架构划分:
src
├── app # 程序启动入口
├── view # 控制台菜单、输入和输出
├── service # 业务逻辑层
├── dao # 数据库操作层
├── domain # 实体类
├── utils # JDBC连接池工具类
├── net # Socket客户端工具
├── server # Socket服务端
└── client # 独立Socket客户端
核心调用关系如下:
用户输入
↓
View(展示菜单、读取输入)
↓
Service(处理业务逻辑、校验参数)
↓
DAO(执行 SQL)
↓
MySQL 数据库
例如用户添加一条账务时,执行流程是:
MainView.add()
↓
ZhangWuService.add()
↓
ZhangWuDao.add()
↓
INSERT INTO cuit_zhangwu ...
这样分层的好处是:控制台交互、业务逻辑和 SQL 操作彼此分开,代码更容易阅读和维护。
三、数据库设计
项目使用两张表。
1. 用户表 user
CREATE TABLE `user` ( id INT PRIMARY KEY AUTO_INCREMENT, usernmae VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(20) NOT NULL );这里字段名使用的是
usernmae,是项目最初建表时的命名,后续 Java 代码也保持一致。
2. 账务表 cuit_zhangwu
CREATE TABLE cuit_zhangwu ( zwid INT PRIMARY KEY AUTO_INCREMENT, flname VARCHAR(50) NOT NULL, money DOUBLE NOT NULL, zhanghu VARCHAR(50) NOT NULL, createtime DATE NOT NULL, description VARCHAR(255), username VARCHAR(20) NOT NULL );账务表中最关键的是username字段。
因为每次查询、修改、删除账务时,都会带上当前登录用户:
WHERE zwid=? AND username=?这样可以保证用户只能管理自己的账务数据。
四、数据库连接:DBCP 连接池
项目没有每次操作都手动创建数据库连接,而是使用 DBCP 连接池统一管理连接。
jdbc.properties:
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/zhangwu_dbuseSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username=root
password=你的数据库密码
initialSize=5
maxActive=10
maxIdle=5
minIdle=2
maxWait=3000
核心工具类代码:
static { try { Properties properties = new Properties(); Reader reader = new FileReader("jdbc.properties"); properties.load(reader); reader.close(); dataSource = BasicDataSourceFactory.createDataSource(properties); } catch (Exception e) { e.printStackTrace(); } } public static DataSource getDataSource() { return dataSource; }这里的逻辑很简单:
读取 jdbc.properties
↓
根据配置创建 DataSource 连接池
↓
DAO 层通过 getDataSource() 获取连接池
DAO 中创建QueryRunner:
private final QueryRunner runner = new QueryRunner(JDBCUtils4.getDataSource());之后就可以通过 DbUtils 简化 JDBC 操作,不需要手动写Connection、PreparedStatement、ResultSet的关闭代码。
五、用户注册和登录流程
程序从App.java启动:
String username = new LoginView(scanner).show(); if (username != null) { new MainView(scanner, username).show(); }整体流程:
启动程序
↓
进入登录注册菜单
↓
注册成功或登录成功
↓
返回当前用户名
↓
进入账务主菜单
登录使用的 SQL:
public boolean login(String username, String password) throws SQLException { Number count = runner.query( "SELECT COUNT(*) FROM user WHERE usernmae=? AND password=?", new ScalarHandler<Number>(), username, password); return count.longValue() == 1; }这里使用?占位符传入用户名和密码,而不是直接拼接 SQL 字符串。
注册时会进行基本校验:
if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { return "用户名和密码不能为空"; } if (!username.matches("[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+")) { return "用户名只能包含中文、字母、数字、下划线或短横线"; }预期运行效果:
==== 记账系统 ====
1 登录 2 注册 0 退出
请选择:2新用户名:qiweikai
新密码:123456
注册成功==== 记账系统 ====
1 登录 2 注册 0 退出
请选择:1用户名:qiweikai
密码:123456
登录成功
六、账务管理的核心流程
登录成功后进入主菜单:
==== 当前用户:qiweikai ====
3 查询所有账务
4 按日期查询(支持单天或范围)
5 添加账务(ID自动分配)
6 编辑账务(可改一个字段或全部)
7 删除账务(支持删一条或多条)
8 搜索账务(输入关键词或金额)
9 上传账务到服务器
10 从服务器下载账务
11 上传任意文件到服务器
12 从服务器下载文件
13 导出账务到本地文件(不需要服务器)
0 退出
请选择:
1. 查询全部账务
查询时只查询当前登录用户的数据:
public List<ZhangWu> findAll(String username) throws SQLException { return runner.query( "SELECT * FROM cuit_zhangwu WHERE username=? ORDER BY zwid", new BeanListHandler<ZhangWu>(ZhangWu.class), username); }BeanListHandler<ZhangWu>会自动将数据库查询结果封装为:
List<ZhangWu>预期效果:
请选择:3
【功能3:查询所有账务】
共 3 条,按ID从小到大排列:ID 类别 金额 账户 日期 说明
--------------------------------------------------------------------------------------
1 吃饭支出 25.5 支付宝 2026-07-11 午饭
2 工资收入 5000.0 工商银行 2026-07-11 七月工资
3 交通支出 3.0 微信 2026-07-12 乘坐地铁
--------------------------------------------------------------------------------------
2. 添加账务
添加账务时,用户不需要输入 ID,zwid由数据库自动递增生成。
插入数据的关键代码:
public boolean add(ZhangWu z) throws SQLException { return runner.update( "INSERT INTO cuit_zhangwu" + "(flname,money,zhanghu,createtime,description,username)" + " VALUES(?,?,?,?,?,?)", z.getFlname(), z.getMoney(), z.getZhanghu(), z.getCreatetime(), z.getDescription(), z.getUsername()) == 1; }添加时会校验金额和日期:
double value = Double.parseDouble(text); if (value <= 0) { throw new IllegalArgumentException("金额必须是大于0的数字"); }日期格式必须是:
yyyy-MM-dd
预期效果:
请选择:5
【功能5:添加账务】
类别:吃饭支出
金额:35
账户:支付宝
日期:2026-07-11
说明:和朋友吃饭✔ 添加成功!
3. 按日期查询
系统支持两种日期输入方式:
2026-07-11
表示查询某一天。
2026-07-11,2026-07-20
表示查询一个日期范围。
DAO 层根据开始日期和结束日期是否相同,执行不同 SQL:
if (begin.equals(end)) { return runner.query( "SELECT * FROM cuit_zhangwu " + "WHERE username=? AND createtime=? ORDER BY zwid", new BeanListHandler<ZhangWu>(ZhangWu.class), username, begin); } return runner.query( "SELECT * FROM cuit_zhangwu " + "WHERE username=? AND createtime BETWEEN ? AND ? ORDER BY zwid", new BeanListHandler<ZhangWu>(ZhangWu.class), username, begin, end);预期效果:
请选择:4
请输入日期:2026-07-11,2026-07-12
共 2 条记录:
ID 类别 金额 账户 日期 说明
--------------------------------------------------------------------------------------
1 吃饭支出 35.0 支付宝 2026-07-11 和朋友吃饭
3 交通支出 3.0 微信 2026-07-12 乘坐地铁--------------------------------------------------------------------------------------
4. 编辑账务
编辑前,系统会先查询该账务是否属于当前用户:
public ZhangWu findOwned(int id, String username) throws SQLException { return runner.query( "SELECT * FROM cuit_zhangwu WHERE zwid=? AND username=?", new BeanHandler<ZhangWu>(ZhangWu.class), id, username); }编辑支持两种方式:
1. 修改某一个字段
2. 重新填写全部字段
修改单个字段时,字段名使用白名单校验:
if (!("flname".equals(field) || "money".equals(field) || "zhanghu".equals(field) || "createtime".equals(field) || "description".equals(field))) { throw new IllegalArgumentException("不允许修改该字段"); }这样避免将任意字段名直接拼接到 SQL 中。
预期效果:
请选择:6
输入要编辑的账务ID:1
当前记录:
1 吃饭支出 35.0 支付宝 2026-07-11 和朋友吃饭选择编辑方式:
1 只改某一个字段
2 重新填写全部字段
请选择:1选择要修改的字段:
1 类别 2 金额 3 账户 4 日期 5 说明
请选择字段编号:2当前金额:35.0
新金额:40✔ 编辑成功!
5. 批量删除账务
删除功能支持输入一个或多个 ID:
3
或者:
3,4,5
关键 SQL 是动态拼接IN条件:
StringBuilder sql = new StringBuilder( "DELETE FROM cuit_zhangwu WHERE username=? AND zwid IN ("); for (int i = 0; i < ids.size(); i++) { if (i > 0) sql.append(','); sql.append('?'); } sql.append(')');最终效果类似:
DELETE FROM cuit_zhangwu WHERE username=? AND zwid IN (?, ?, ?)预期效果:
请选择:7
输入要删除的ID:3,4,5
✔ 删除成功,共删除 3 条。
6. 搜索账务
搜索功能可以匹配:
类别
账户
说明
金额
核心 SQL:
String like = "%" + text + "%"; return runner.query( "SELECT * FROM cuit_zhangwu WHERE username=? AND " + "(flname LIKE ? OR zhanghu LIKE ? OR description LIKE ? " + "OR CAST(money AS CHAR) LIKE ?) ORDER BY zwid", new BeanListHandler<ZhangWu>(ZhangWu.class), username, like, like, like, like);预期效果:
请选择:8
输入搜索内容:吃饭
共 1 条匹配记录:
ID 类别 金额 账户 日期 说明
--------------------------------------------------------------------------------------
1 吃饭支出 40.0 支付宝 2026-07-11 和朋友吃饭--------------------------------------------------------------------------------------
七、账务导出流程
功能13可以把账务导出为本地 txt 文件,不需要连接服务器。
导出逻辑:
选择导出全部账务或指定日期范围
↓
从数据库查询账务
↓
将账务格式化为文本
↓
写入 exports 文件夹
导出文件的核心代码:
byte[] content = format(rows).getBytes(StandardCharsets.UTF_8); FileOutputStream fos = new FileOutputStream(outFile); try { fos.write(content); } finally { fos.close(); }预期效果:
请选择:13
选择导出范围:
1 导出全部账务
2 按日期范围导出
请选择:1✔ 导出成功!
文件路径:D:\workspace\ZhangWuSystem\exports\qiweikai_全部账务_2026-07-11.txt
共导出 3 条记录
导出的 txt 内容:
ID 类别 金额 账户 日期 说明
1 吃饭支出 40.0 支付宝 2026-07-11 和朋友吃饭
2 工资收入 5000.0 工商银行 2026-07-11 七月工资
八、Socket 文件上传下载流程
除了 JDBC 功能外,项目还实现了一个简单的 Socket 文件服务器。
服务端启动类:
server.ServerApp
启动后监听12000端口:
服务器监听:0.0.0.0:12000
数据目录:D:\workspace\ZhangWuSystem\server-data
客户端和服务端的通信格式大致如下:
客户端连接服务器
↓
发送 command 命令
↓
发送 username 用户名
↓
发送 filename 文件名
↓
发送 length 文件长度
↓
发送文件字节内容
↓
服务端保存文件并返回结果
项目中定义的命令:
| 命令 | 功能 |
|---|---|
| 9 | 上传账务文件 |
| 10 | 下载账务文件 |
| 11 | 上传普通文件 |
| 12 | 下载普通文件 |
| 13 | 账务自动同步 |
1. 上传账务文件
上传账务时,系统会根据日期将账务生成 txt 文件。
例如上传2026-07-11的账务:
2026-07-11.txt
服务端保存位置:
server-data/qiweikai/zhangwu/2026-07-11.txt
客户端发送数据的关键代码:
out.writeInt(command); out.writeUTF(username); out.writeUTF(new File(filename).getName()); out.writeLong(bytes.length); out.write(bytes); out.flush();预期效果:
请选择:9
请输入日期:2026-07-11
服务器IP:127.0.0.1开始上传...
✔ 2026-07-11.txt 上传成功:上传成功:2026-07-11.txt上传完成,共上传 1 个文件
2. 上传普通文件
上传普通文件后,服务端会保存到:
server-data/用户名/files/
例如上传图片:
D:\workspace\ZhangWuSystem\ggz.jpg
预期效果:
请选择:11
服务器IP:127.0.0.1
文件路径:D:\workspace\ZhangWuSystem\ggz.jpg文件名:ggz.jpg
文件大小:123456 字节
正在上传...✔ 上传成功:上传成功:ggz.jpg
3. 下载文件
下载时,客户端会先获取服务端文件列表:
1. 2026-07-11.txt
2. 2026-09-01.txt
输入序号或文件名(直接回车取消):
下载后的账务文件保存到:
downloads/用户名/zhangwu/
普通文件保存到:
downloads/用户名/files/
预期效果:
请选择:10
服务器IP:127.0.0.1
正在获取服务器文件列表...
1. 2026-07-11.txt
2. 2026-09-01.txt输入序号或文件名:1
✔ 下载成功,文件保存到:
D:\workspace\ZhangWuSystem\downloads\qiweikai\zhangwu\2026-07-11.txt
九、项目完整运行流程
最后用一张流程图总结整个项目:
启动 App
↓
注册 / 登录
↓
获取当前登录用户名
↓
进入账务主菜单
↓
选择账务操作
├── 查询:DAO 执行 SELECT
├── 添加:DAO 执行 INSERT
├── 修改:DAO 执行 UPDATE
├── 删除:DAO 执行 DELETE
├── 搜索:DAO 执行 LIKE 查询
└── 导出:查询账务后写入本地 txt
↓
如果选择上传或下载文件
↓
SocketClient 连接 ServerApp
↓
通过 12000 端口传输文件
↓
文件保存到 server-data 或 downloads 目录
十、总结
这个项目是一个以 JDBC 为核心的控制台记账系统,完成了从用户登录到账务 CRUD 的完整流程。
项目中重点练习了:
JDBC 连接 MySQL
DBCP 数据库连接池
Apache DbUtils
三层架构
用户登录注册
账务增删改查
日期范围查询
模糊搜索
批量删除
文件导出
Socket 文件上传下载
虽然项目是控制台形式,但它把 JavaSE 阶段常见的数据库操作、文件操作、网络编程和分层设计串联到了一起。对刚学完 JDBC 的同学来说,适合用来理解一个完整 Java 小项目是如何组织和运行的。
