简介:面向数据库课程设计的家庭理财管理系统完整课设文档,适合计算机相关专业学生参考,用于学习Access数据库设计与VB前端开发相结合的系统实现思路。整份资料以一个doc文件呈现,大小约1.25MB,涵盖需求分析、概念结构设计(E-R图)、逻辑结构设计、数据库实施与维护等完整流程,并给出了用户管理、基本信息管理、理财信息管理、数据统计管理等模块的VB界面设计与核心代码。文档还整理了系统总体设计图、用户管理关系图、系统流程分析图、功能模块示意图以及数据库关系图,并逐一说明了用户表、银行信息表、家庭成员信息表、日常支出信息表、银行账号信息表、银行交易信息表等数据表结构。登录界面、系统主界面及多个管理模块的主要代码片段也一并附上,可直接对照课程设计任务书修改和复用。目前已有180人学习下载,对于需要快速搭建家庭理财类数据库课程设计框架、撰写设计文档与答辩材料的读者有较高参考价值。
1. 为什么说这个课设选题看着简单、拿高分难
“家庭理财管理系统数据库课设.doc”这类题目,交付物是一份 Word 文档,核心不是把代码跑起来,而是把数据库设计讲清楚。它和图书管理系统、学生选课系统处于同一个难度段位,但正因为人人都能做,老师提问反而更细:余额为什么不直接存?删除为什么不用 DELETE?月度统计的 SQL 为什么慢?这套系统的表结构、增删改查、约束和索引,几乎覆盖了数据库课程设计要考的全部基本功,也和你以后面试被问的数据库面试题高度重叠。本文按我做过这类课设的习惯,从建表、增删改查、视图与触发器,一直讲到答辩前怎么把报告整理得能扛住追问,新手可以照着做,熟手也能拿去对一下自己的设计有没有留坑。
2. 先把表结构定对:五张表、两条关系和范式取舍
2.1 从需求到实体:别一上来就只做一张流水表
很多同学拿到“家庭理财”第一反应是建一张大表,字段拉成“日期、金额、用途、备注、账户、余额”,美其名曰简单直观。这种做法在课程设计里通常只能拿及格分,因为它至少违反了两个基本设计原则:一是把“账户”和“流水”塞在同一张表里,账户改名要连带改所有历史流水;二是没有分类维度,后面做月度统计、预算提醒全都无从下手。
我一般会先画一个最简 ER 图,把实体拆成四类:用户、账户、收支分类、收支流水,再加一个预算表做月度预算控制。用户和账户是 1 对 N,账户和流水是 1 对 N,流水和分类是 N 对 1。这个关系直接对应后面的外键和索引设计,也是报告里 ER 图的核心内容。预算表单独拆出来,是因为它记录的“计划值”和流水的“实际值”在业务上是两回事,混在一张表里会让更新逻辑变得很拧巴。
2.2 建表脚本:一套能直接跑的最小库结构
下面的建表脚本基于 MySQL 8.0,五张表,全部使用 InnoDB 引擎和 utf8mb4 字符集。字段注释写在 COMMENT 里,这样后面生成数据字典时可以直接引用 information_schema,不用手敲两遍。
CREATE DATABASE family_finance DEFAULT CHARACTER SET utf8mb4; USE family_finance; CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password_hash CHAR(64) NOT NULL COMMENT '口令散列值,不存明文', real_name VARCHAR(30) DEFAULT NULL COMMENT '真实姓名', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开户时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_account ( account_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账户ID', user_id INT NOT NULL COMMENT '所属用户', account_name VARCHAR(50) NOT NULL COMMENT '账户名,如工资卡、现金', account_type TINYINT NOT NULL DEFAULT 0 COMMENT '类型:0现金,1银行卡,2电子钱包', init_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '开户初始余额', state TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常,0已注销', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', UNIQUE KEY uk_user_account (user_id, account_name), CONSTRAINT fk_acct_user FOREIGN KEY (user_id) REFERENCES t_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表'; CREATE TABLE t_category ( category_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', parent_id INT DEFAULT NULL COMMENT '上级分类ID,NULL表示顶级', category_name VARCHAR(30) NOT NULL COMMENT '分类名称,如餐饮、工资', trans_type TINYINT NOT NULL COMMENT '1收入类,2支出类' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支分类表'; CREATE TABLE t_transaction ( trans_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '流水ID', user_id INT NOT NULL COMMENT '所属用户', account_id INT NOT NULL COMMENT '发生账户', category_id INT NOT NULL COMMENT '收支分类', trans_type TINYINT NOT NULL COMMENT '类型:1收入,2支出', amount DECIMAL(12,2) NOT NULL COMMENT '金额,大于0', occur_time DATETIME NOT NULL COMMENT '业务发生时间', remark VARCHAR(200) DEFAULT NULL COMMENT '备注', state TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1有效,0已作废', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', KEY idx_user_time (user_id, occur_time), KEY idx_category (category_id), CONSTRAINT fk_txn_account FOREIGN KEY (account_id) REFERENCES t_account (account_id), CONSTRAINT fk_txn_category FOREIGN KEY (category_id) REFERENCES t_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支流水表'; CREATE TABLE t_budget ( budget_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '预算ID', user_id INT NOT NULL COMMENT '用户', category_id INT NOT NULL COMMENT '分类', month VARCHAR(7) NOT NULL COMMENT '预算月份,格式如2025-06', budget_amount DECIMAL(12,2) NOT NULL COMMENT '月度预算金额', UNIQUE KEY uk_budget (user_id, category_id, month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月度预算表';几个关键参数说明。流水表主键用 BIGINT 而不是 INT,因为家庭流水日积月累可能过万,INT 上限 21 亿虽然够用,但课设答辩时老师会问“为什么用 BIGINT”,这是个很容易拿分的设计细节。金额字段统一用 DECIMAL(12,2),不用 FLOAT 或 DOUBLE,原因后面避坑章节会专门讲。state 字段是所有业务表的“逻辑删除标记”,这也是一个刻意设计:流水一旦记错,业务上应该“作废”而不是“抹掉”,否则月底对账会非常被动。
2.3 范式不是拿来背的:用两个反例说明取舍
报告里很多同学会抄一句“本设计满足第三范式”,但老师追问“哪里满足?哪里故意不满足?”就答不上来。这里给一个能写进报告的说法。第一范式要求字段不可再分:上表里没有“收入金额+支出金额”这种复合字段,全部拆成单值字段,满足。第二范式要求非主键字段完全依赖主键:流水表的 remark、amount 都依赖 trans_id,没有部分依赖,满足。第三范式要求非主键字段不传递依赖:t_transaction 里没有“账户名”,因为账户名属于 t_account,如果冗余进来,就构成了 trans_id → account_id → account_name 的传递依赖。
真正需要讲清楚的是“故意违反范式”的地方。账户表里的 init_balance 一定余额吗?其实它不是余额,是开户时的初始值。如果你想要展示“当前余额”,有两个方案:方案一,每次用 SUM 汇总流水算余额,完全符合范式,但流水多了以后每次查询都要扫全表;方案二,在账户表冗余一个 current_balance 字段,读写快,但每次新增流水都要同步更新它,属于典型的反范式设计。我建议课设里写明“采用方案一,余额为计算字段”,一是逻辑简单、不容易被追问出漏洞,二是可以顺势引出后面的视图和索引优化。
3. 把数据库增删改查做成四个能演示的模块
3.1 数据层用 PreparedStatement 而不是拼 SQL
课设的界面层用什么框架不重要,重要的是数据访问层要写成能被答辩老师一眼看懂的样子。这里给一个最朴素的 JDBC 写法,没有引入 MyBatis 或 Hibernate,因为课设评审更看重你对 SQL 和事务本身的理解。
public int addTransaction(Connection conn, Transaction txn) throws SQLException { String sql = "INSERT INTO t_transaction " + "(user_id, account_id, category_id, trans_type, amount, occur_time, remark, state) " + "VALUES (?, ?, ?, ?, ?, ?, ?, 1)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, txn.getUserId()); ps.setInt(2, txn.getAccountId()); ps.setInt(3, txn.getCategoryId()); ps.setInt(4, txn.getTransType()); ps.setBigDecimal(5, txn.getAmount()); ps.setTimestamp(6, new Timestamp(txn.getOccurTime().getTime())); ps.setString(7, txn.getRemark()); return ps.executeUpdate(); } }要点有两个。一是所有参数都用 ? 占位符,由 PreparedStatement 预编译,避免字符串拼接引入 SQL 注入,这个点在报告里写一句“数据访问层全部使用参数化查询”就能体现工程意识。二是 try-with-resources 自动释放 PreparedStatement,避免连接泄漏;注意这里没有关闭 Connection,因为连接由调用方统一管理,放在 DAO 里关反而会导致上层事务失效。参数下标从 1 开始,setBigDecimal 对应 DECIMAL 字段,setTimestamp 对应 DATETIME 字段,这几个类型对应关系是课设里最常见的低级错误来源。
3.2 最容易被问的“月度收支统计”:三行 SQL 里有三个考点
收入、支出、结余的月度汇总几乎是每个家庭理财系统都有的功能,也是答辩必问。常见做法是写一条 GROUP BY 的聚合 SQL:
SELECT DATE_FORMAT(occur_time, '%Y-%m') AS month, SUM(CASE WHEN trans_type = 1 THEN amount ELSE 0 END) AS income, SUM(CASE WHEN trans_type = 2 THEN amount ELSE 0 END) AS expense, SUM(CASE WHEN trans_type = 1 THEN amount ELSE -amount END) AS balance FROM t_transaction WHERE user_id = ? AND state = 1 GROUP BY DATE_FORMAT(occur_time, '%Y-%m') ORDER BY month DESC;这条 SQL 有三个容易被追问的点。第一,DATE_FORMAT 作用于 occur_time 之后,索引 idx_user_time 就无法用于月份过滤,因为索引里存的是完整时间值,不是格式化后的字符串。如果按月份查询,更稳妥的写法是把过滤条件改成 occur_time >= ? AND occur_time < ?,分别传入月初和下月初,这样能走索引。第二,SUM(CASE WHEN ...) 是标准的行转列写法,比在 Java 里循环累加要高效得多,而且结果集直接就是报表数据。第三,balance 不能简单地用 income 减 expense 两条子查询得到,因为那样会扫两遍表;一条 SQL 里同时做条件聚合才是正确姿势。这三点写进报告,比干巴巴写“使用了 group by”更有说服力。
3.3 分页查询与软删除:两个边角功能决定体验
列表页通常要做分页,MySQL 里用 LIMIT ? OFFSET ?。但流水按时间倒序排时,OFFSET 越大扫描越慢,这在课设数据量下看不出问题,但老师可能会问“为什么不用游标分页”。更实用的改进是记住上一页最后一条 occur_time,用条件过滤代替 OFFSET,这个可以写在“改进与展望”一节当加分项。
软删除的实现是 UPDATE 而不是 DELETE:
UPDATE t_transaction SET state = 0 WHERE trans_id = ? AND user_id = ?;所有查询都带 state = 1 条件,配合前面统计 SQL 里的 state = 1,保证作废流水不会污染报表。对应的“删除账户”也不是真删,而是把 t_account 的 state 置为 0。这里要特别说明:账户软删除后,t_transaction 里的历史流水还引用着这个账户,所以账户表的外键不能用 ON DELETE CASCADE,否则软删账户会把历史流水一起物理删掉,这个误区在避坑章节还会展开。
4. 视图、存储过程和触发器的三个加分项:边界与实现
4.1 先用视图把“家庭月报”固定下来
视图的价值不是“看起来高级”,而是把常用的统计 SQL 固化成一个虚拟表,让上层查询变短。课设里做一个 v_monthly_report 视图,可以把上面的统计 SQL 直接包进去:
CREATE VIEW v_monthly_report AS SELECT t.user_id, DATE_FORMAT(t.occur_time, '%Y-%m') AS month, SUM(CASE WHEN t.trans_type = 1 THEN t.amount ELSE 0 END) AS income, SUM(CASE WHEN t.trans_type = 2 THEN t.amount ELSE 0 END) AS expense, SUM(CASE WHEN t.trans_type = 1 THEN t.amount ELSE -t.amount END) AS balance FROM t_transaction t WHERE t.state = 1 GROUP BY t.user_id, DATE_FORMAT(t.occur_time, '%Y-%m');之后查某用户的月报只需要SELECT * FROM v_monthly_report WHERE user_id = ?,JSP 或者 Swing 界面里不用再拼复杂 SQL。视图在 MySQL 里本质是“物化还是非物化”由优化器决定,课设里不需要深究,但报告里可以写一句“视图屏蔽了聚合细节,方便权限控制和界面复用”。注意:视图里不写 ORDER BY,排序交给调用方,这是视图设计的基本规范,写进视图的 ORDER BY 在某些版本里可能被忽略。
4.2 存储过程:把一笔收支写进库的正确姿势
存储过程适合做“多步操作必须一起成功或一起失败”的场景。添加一笔流水时,如果需要同时更新账户余额,就必须放在同一个事务里。下面是带事务和分类校验的存储过程:
DELIMITER $$ CREATE PROCEDURE p_add_transaction( IN p_user_id INT, IN p_account_id INT, IN p_category_id INT, IN p_trans_type TINYINT, IN p_amount DECIMAL(12,2), IN p_occur_time DATETIME, IN p_remark VARCHAR(200) ) BEGIN DECLARE v_txn_type TINYINT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; SELECT trans_type INTO v_txn_type FROM t_category WHERE category_id = p_category_id; IF v_txn_type IS NULL OR v_txn_type <> p_trans_type THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '分类与收支类型不匹配'; END IF; INSERT INTO t_transaction (user_id, account_id, category_id, trans_type, amount, occur_time, remark, state) VALUES (p_user_id, p_account_id, p_category_id, p_trans_type, p_amount, p_occur_time, p_remark, 1); COMMIT; END$$ DELIMITER ;这里有两个刻意设计。一是先校验分类的 trans_type 是否与传入的收支类型一致,防止往“餐饮”分类里插入一笔“工资收入”这种脏数据;校验失败时用 SIGNAL 抛出异常,上层能捕获到错误信息。二是 EXIT HANDLER 捕获所有 SQL 异常后回滚并 RESIGNAL,把原始错误继续抛给调用方,避免异常被吞掉。调用存储过程时,Java 端仍然是CallableStatement,但课设里能写出这个存储过程并解释清楚事务边界,就已经是明显的加分项。
4.3 触发器:如果你坚持冗余余额,就必须上触发器
很多参考代码会在账户表放一个 current_balance 字段,每次新增流水直接改余额。这是一种反范式设计,我前面建议用视图实时汇总来代替。但如果你的课设已经选了冗余方案,就必须用触发器保证一致性,否则新增一条流水后余额没变,答辩时会被当场拆穿。
DELIMITER $$ CREATE TRIGGER trg_txn_after_insert AFTER INSERT ON t_transaction FOR EACH ROW BEGIN IF NEW.state = 1 THEN IF NEW.trans_type = 1 THEN UPDATE t_account SET current_balance = current_balance + NEW.amount WHERE account_id = NEW.account_id; ELSE UPDATE t_account SET current_balance = current_balance - NEW.amount WHERE account_id = NEW.account_id; END IF; END IF; END$$ DELIMITER ;注意触发器里判断了 NEW.state,因为软删除的作废操作走的是 UPDATE,不会触发 AFTER INSERT;如果插入时就带 state = 0,自然也不该影响余额。这个细节直接体现了对业务状态机的理解。写报告时,我建议把这套“触发器维护余额”方案放在“备选方案对比”一节,和视图汇总方案并列,说明你选的是哪种以及为什么。选视图就少一个触发器,选触发器就多一份一致性维护成本,两者都讲清楚,比只写一种显得更完整。
5. 避坑:从建表到答辩最容易翻车的六个现场
5.1 金额用 FLOAT 导致对不上账
现象:某笔支出录入 34.58 元,列表显示 34.58,但月度汇总总是差 0.01 元;用 FLOAT 累加几百条记录后误差放大到几毛钱。
原因:FLOAT 和 DOUBLE 是浮点数,二进制无法精确表示 0.58,累加时误差累积。数据库里涉及金额的字段必须用定点数 DECIMAL。
解决:所有金额字段统一改为 DECIMAL(12,2),Java 端用 BigDecimal 或 String 接收,不要用 double 接收后转字符串。这个坑在报告里写一笔,属于“踩过才知道”的经验,老师看了会觉得你是真做过而不是纯抄代码。
5.2 时间字段的默认值老是不生效
现象:insert 时没给 created_at 赋值,报错说字段没有默认值;或者建表时报错 “there can be only one TIMESTAMP column with CURRENT_TIMESTAMP”。
原因:这是 MySQL 版本差异问题。5.7 及更早版本里,一张表只允许一个 TIMESTAMP 字段使用 CURRENT_TIMESTAMP 作为默认值,且 TIMESTAMP 范围只到 2038 年;DATETIME 在旧版本不支持函数默认值。
解决:课设直接用 MySQL 8.0,时间字段统一用 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,避免踩老版本的坑。如果你的运行环境是 5.7,就把默认值交给应用层在插入时显式写入,不要在表定义里纠结。
5.3 物理删除了流水,月底数据对不上
现象:发现一条录错的水电费,直接 DELETE 掉,月底结余比实际少了一笔。
原因:家庭理财里“删错账”和“账做错了”是两件事。物理删除后原始记录彻底消失,无法追溯是谁在什么时候删的,也无法保留正确的更正记录。
解决:全部走逻辑删除,也就是业务表都带 state 字段,删除改为 UPDATE state = 0;查询和统计统一过滤 state = 1。如果再讲究一点,加一个 t_audit_log 审计表记录“谁在何时修改了哪笔流水”,这就是审计4J那类变更审计框架做的事,课设里做到逻辑删除已经足够。
5.4 字符集用了 utf8,中文乱码、emoji 存不进
现象:插入“😀”表情符号报错 “Incorrect string value”,或者中文偶尔变成问号。
原因:MySQL 的 utf8 其实是 utf8mb3,只支持三字节字符,emoji 是四字节;另外建库时没指定字符集,继承服务端默认的 latin1 也会乱码。
解决:建库建表统一加DEFAULT CHARACTER SET utf8mb4,连接串里加useUnicode=true&characterEncoding=utf8mb4。注意 utf8mb4 下定义 VARCHAR 索引时长度受限,经典坑是VARCHAR(255)建索引报 “Specified key was too long; max key length is 767 bytes”,因为 255 * 4 = 1020 字节超了。解决方法是把长度降到 191 以内,或者只对前缀建索引,这个细节在报告的数据字典里直接体现。
5.5 外键到底建不建:课设和生产是两种答案
现象:写课设时很多人怕麻烦不建外键,用 Java 代码保证一致性;结果老师说“表关系在哪里体现?你的 ER 图是画的,库里没有约束”。
原因:生产环境为了高并发和分库分表,常常故意不用外键,一致性交给应用层。但课设考察的就是关系型数据库的基本约束,不建外键属于自废武功。
解决:课设里建外键,但删除策略选 RESTRICT 而不是 CASCADE。账户被流水引用时,默认禁止删除账户,业务上需要“注销账户”就走软删除。这样既展示了外键,又避免了级联删除误伤历史数据。答辩被问“为什么不用 CASCADE”,就回答“历史流水需要保留,级联删除会破坏审计数据”,这个回答本身就是得分点。
5.6 生成的报告和数据字典不一致
现象:ER 图里画了 4 张表,正文数据字典却有 5 张;字段名一会儿 txn_id 一会儿 trans_id,老师按图索骥对不上。
原因:图是先画的,表是后改的,改表时没有同步更新文档。
解决:用 information_schema 直接导出现有库的字段清单,再生成数据字典,保证图、表、文档三者一致。下面这查询可以拿到所有字段和注释:
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'family_finance' ORDER BY TABLE_NAME, ORDINAL_POSITION;这个查询结果可以直接贴进 Word 整理成数据字典表格,省去手敲,还能避免图表不一致。
6. 把数据字典和 SQL 整理成一份能扛住答辩的报告
课设最后交付的是 .doc 文档,篇幅重点应该放在数据字典、ER 图说明和核心 SQL 的设计意图上,而不是贴大段 Java 代码。数据字典我习惯按下表格式整理,一表一节,字段顺序和建表语句完全一致,老师顺着看不会乱。
| 字段名 | 类型 | 允许空 | 键 | 默认值 | 备注 |
|---|---|---|---|---|---|
| trans_id | BIGINT | 否 | 主键 | 自增 | 流水ID |
| user_id | INT | 否 | 普通索引 | 无 | 所属用户,外键 |
| amount | DECIMAL(12,2) | 否 | 无 | 无 | 金额,大于0 |
| state | TINYINT | 否 | 无 | 1 | 1有效,0作废 |
每张表配一段“设计说明”,讲清三个问题:这张表解决什么需求;为什么拆出来而不合并;删除和修改分别走什么策略。比如预算表设计说明可以写“用 user_id + category_id + month 唯一键保证一个用户一个月内对同一分类只有一条预算,重复提交预算时用 INSERT ... ON DUPLICATE KEY UPDATE 实现幂等更新”。
答辩前的自检我有个固定习惯:把自己当成老师,对着库逐条过一遍“如果这个字段用 varchar 存金额会怎样”“如果删掉这个索引统计还跑得动吗”“如果用户重复点提交按钮会不会插两条重复流水”。最后一个问题最常被问到,所以我在存储过程里用事务包裹,在预算表里用唯一键兜底,这两处代码在文档里要给醒目位置。这套设计我做过不止一次,早期翻过车,后来养成的习惯是每张表必须带 state 和 created_at,所有删除走软删,所有金额走 DECIMAL。这次把完整方案写出来,希望能帮你少走一段弯路,希望帮到你。
本文还有配套的精品资源,点击获取