☰
家庭理财数据库课设:从ER图到SQL实现,表结构设计决定分数
2026/10/14 12:18:11 网站建设 项目流程

简介:一份完整的家庭理财管理系统数据库课程设计文档,面向需要完成数据库课程设计、学习Access数据库应用及VB界面开发的高校学生与初学者。文档源自沈阳理工大学课程设计,系统涵盖基本信息管理、理财信息管理、数据统计与用户管理等模块,并以Access为数据库服务器、VB为开发工具,具有完整的项目流程。压缩包内为单个doc文件,大小1.25MB,内容包含课程设计的目的与意义、需求分析、概念结构设计(E-R图)、逻辑结构设计、数据库实施与维护,以及登录界面、系统主界面和各功能模块的界面设计与核心代码,可直接用于参考、修改或提交课设。已有180人学习,适合需要快速理解家庭理财场景下数据库设计思路、表结构划分与VB+Access联调实现的读者。文档结构清晰,章节完整,可为类似管理信息系统的课程设计提供较好的模板。

1. 家庭理财管理系统数据库课设:表结构设计比增删改查更决定分数

很多人拿到“家庭理财管理系统数据库课设”这个标题,第一反应是赶紧把增删改查跑起来,数据能录进去、能查出来就算完事。但等你真正把 ER 图和数据字典交上去,评审第一眼看的就是这两样东西:表结构合不合理、字段规不规范,其次才是业务查询有没有做全、有没有一点像样的统计报表。我见过太多人代码写得飞起,最后却在表设计上被扣了大半分数。

这一篇就按数据库课程设计最常见的落地路径来讲:从需求分析拆出实体关系,到建库建表,到业务查询和统计报表,再到交作业前最容易翻车的那几个坑。整个过程不依赖任何现成源码包,你用纯 SQL 加 JDBC 就能完整复现,适合正在做数据库课设、需要交文档和答辩的学生,也适合想拿一个小规模管理系统练手数据库设计的后端入门者。

2. 从需求到 ER 图:先把家庭理财的实体关系画明白,再谈建表

2.1 家庭理财到底要管哪几类数据

先别急着建表,回去翻一遍课程设计任务书里的功能描述。家庭理财管理系统不管界面做成什么样,数据库层面离不开这几类核心数据:家庭成员、资金账户、收入记录、支出记录、消费分类、预算计划。这六类数据基本覆盖了一个家庭记账场景的全部边界。

家庭成员表管的是“谁在记账、钱花在谁身上”;资金账户表管的是“钱放在哪里”,比如现金、工资卡、余额宝;收支记录表是流水核心,每一笔钱从哪个账户进、归到哪个分类;消费分类表管的是“这笔钱算什么”,比如餐饮、交通、医疗;预算表管的是“某个月某个分类最多花多少”。这六张表之间的关系不复杂,但如果你漏掉了账户表,后面想做“按账户查余额”就得临时改结构,课设答辩时很容易被追问到。

这里有一个常见设计分歧:收入和支出要不要拆成两张表。我的建议是拆。虽然可以合并成一张流水表加一个收支类型字段,但课设评分通常看重“功能划分清晰”,拆成 income 和 expense 两张表,后续统计月度收入、月度支出时 SQL 更直观,触发器写起来也更简单。合并表在真实场景里确实常见,但课设不是真实业务,别用高級技巧给自己制造麻烦。

2.2 实体关系怎么落:一对多关系与冗余字段的取舍

家庭成员和收支记录之间是一对多关系,一个成员可以有多笔收入和多笔支出,所以成员表的主键要作为外键出现在收支表里。消费分类和收支记录也是一对多,一个分类下可以挂多条流水。资金账户和收支记录同样是一对多,账户表里的余额字段要跟着流水变化。

预算表和成员表的关系稍微特殊一点:预算可以按成员设置,也可以按整个家庭设置。如果按成员设置,就要在预算表里放 member_id 外键;如果不按成员、只按分类设置,那预算表里就可以不出现成员字段。做课设建议按“成员 + 分类 + 月份”的组合来设计,这样能支持的查询更多,比如查“某人某月某项预算还剩多少”,答辩时讲起来也更有东西。

这里要注意一个字段冗余的取舍:收支表里除了放分类外键 category_id,我一般还会冗余一个 category_name 字段。理由是课设阶段分类表几乎不会改名,冗余字段可以省掉一半查询的 JOIN 操作。你和评审解释“这是为了减少高频查询的连表次数”即可,这是数据库优化里说得过去的理由,不算错误设计。

2.3 数据字典:给每张表定字段、类型和约束

建表之前先把数据字典写清楚,这张表本身就是课程设计文档的必需内容。字段命名建议统一用 snake_case,主键叫 id,外键叫 xxx_id,时间字段按精度取名 create_time 或 record_date。下面这份是经过验证的字段清单,你直接抄进文档没问题。

表名字段名类型约束说明
member 家庭成员idINT主键,自增成员ID
nameVARCHAR(20)NOT NULL姓名
roleVARCHAR(20)NOT NULL角色:爸爸/妈妈/孩子
phoneVARCHAR(20)可空联系方式
account 资金账户idINT主键,自增账户ID
member_idINT外键→member.id账户所属成员
account_nameVARCHAR(30)NOT NULL账户名称
balanceDECIMAL(10,2)NOT NULL DEFAULT 0当前余额
income 收入记录idINT主键,自增收入ID
member_idINT外键→member.id收入人
account_idINT外键→account.id入账账户
income_typeVARCHAR(20)NOT NULL工资/理财/奖金等
amountDECIMAL(10,2)NOT NULL, CHECK(amount>0)金额
income_dateDATENOT NULL收入日期
expense 支出记录idINT主键,自增支出ID
member_idINT外键→member.id消费人
account_idINT外键→account.id扣款账户
category_idINT外键→category.id消费分类
category_nameVARCHAR(20)冗余字段分类冗余名
amountDECIMAL(10,2)NOT NULL, CHECK(amount>0)支出金额
expense_dateDATENOT NULL支出日期
category 消费分类idINT主键,自增分类ID
category_nameVARCHAR(20)UNIQUE NOT NULL分类名称
budget 预算idINT主键,自增预算ID
member_idINT外键→member.id预算归属人
category_idINT外键→category.id预算分类
budget_monthCHAR(7)NOT NULL预算月份,如2025-06
budget_amountDECIMAL(10,2)NOT NULL预算金额

金额字段统一用 DECIMAL(10,2) 而不是 FLOAT,这是一个老生常谈但课设里依然频繁踩坑的问题。FLOAT 是近似存储,0.1 加 0.2 都可能算出 0.30000000000000004,对账时差出几分钱说不清楚。DECIMAL 是定点数,按位存储,适合金额这种不允许误差的字段。

日期字段用 DATE 类型,预算月份用 CHAR(7) 存“2025-06”这种格式,不要用 INT 存 202506。虽然 INT 也能比较大小,但你要是写 DATE_FORMAT 之类的日期函数,INT 就得先做类型转换,索引也帮不上忙,属于给自己挖坑。

3. 建库建表与数据初始化:把 ER 图落成可运行的 MySQL DDL

3.1 完整建库建表 SQL:引擎、字符集与约束一起到位

表结构设计好后,下一步就是用 SQL 把结构落下来。下面这一段是完整的建库建表脚本,MySQL 8.0 环境下可以直接执行。为了方便你对照数据字典,我在关键字段上加了注释。

-- 建库:utf8mb4 比 utf8 多覆盖一部分生僻字和特殊符号 CREATE DATABASE IF NOT EXISTS family_finance DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE family_finance; -- 家庭成员表 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, role VARCHAR(20) NOT NULL, phone VARCHAR(20) ) ENGINE=InnoDB; -- 资金账户表:外键关联成员,一个人可以开多个账户 CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, account_name VARCHAR(30) NOT NULL, balance DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT fk_account_member FOREIGN KEY (member_id) REFERENCES member(id) ) ENGINE=InnoDB; -- 消费分类表:分类名称唯一,防止同一分类重复录入 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(20) UNIQUE NOT NULL ) ENGINE=InnoDB; -- 支出记录表:外键关联成员、账户、分类 CREATE TABLE expense ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, account_id INT NOT NULL, category_id INT NOT NULL, category_name VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL CHECK (amount > 0), expense_date DATE NOT NULL, CONSTRAINT fk_expense_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_expense_account FOREIGN KEY (account_id) REFERENCES account(id), CONSTRAINT fk_expense_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINE=InnoDB; -- 收入记录表 CREATE TABLE income ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, account_id INT NOT NULL, income_type VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL CHECK (amount > 0), income_date DATE NOT NULL, CONSTRAINT fk_income_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_income_account FOREIGN KEY (account_id) REFERENCES account(id) ) ENGINE=InnoDB; -- 预算表:按成员 + 分类 + 月份唯一 CREATE TABLE budget ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, category_id INT NOT NULL, budget_month CHAR(7) NOT NULL, budget_amount DECIMAL(10,2) NOT NULL, CONSTRAINT fk_budget_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_budget_category FOREIGN KEY (category_id) REFERENCES category(id), CONSTRAINT uk_budget_member_category_month UNIQUE (member_id, category_id, budget_month) ) ENGINE=InnoDB;

这段 DDL 里有几个参数值得说明。ENGINE=InnoDB 保证了外键约束和事务支持,MyISAM 不支持外键,课设里不要用。utf8mb4 字符集是 MySQL 5.7 之后的推荐选择,utf8mb4_general_ci 作为排序规则性能够用且兼容性好。CHECK 约束从 MySQL 8.0.16 开始才真正生效,8.0.16 之前的版本只解析不执行,如果你用的是 5.7,金额大于零的校验就得靠应用层去判断。

预算表末尾那个 UNIQUE 联合约束是这张表的灵魂,它保证了同一成员在同一个月对同一个分类只能有一条预算记录。没有这个约束,应用层得先 SELECT 再 INSERT 才能避免重复,有了唯一约束之后,重复插入会直接报错,数据库在源头就把数据质量卡住了。

3.2 初始化数据:先父表后子表,让账号余额能对上

表建好后要插入实验数据,不然后面的查询语句没有数据可跑。插入顺序有讲究:必须先插入 member 和 category 这两张父表,再插入 account,最后才能插入 income 和 expense。因为子表的外键依赖父表主键,顺序反了会直接报外键约束错误。

-- 1. 家庭成员 INSERT INTO member (name, role) VALUES ('张伟', '爸爸'), ('李娜', '妈妈'), ('张小雅', '孩子'); -- 2. 消费分类 INSERT INTO category (category_name) VALUES ('餐饮'), ('交通'), ('居住'), ('医疗'), ('教育'), ('娱乐'), ('其他'); -- 3. 资金账户:账户余额先按初始值录,后面靠流水对账 INSERT INTO account (member_id, account_name, balance) VALUES (1, '工资卡', 8000.00), (1, '现金', 500.00), (2, '余额宝', 15000.00), (3, '校园卡', 200.00); -- 4. 收入记录:发工资入工资卡,理财收益入余额宝 INSERT INTO income (member_id, account_id, income_type, amount, income_date) VALUES (1, 1, '工资', 12000.00, '2025-06-05'), (2, 3, '理财收益', 300.00, '2025-06-10'); -- 5. 支出记录:category_name 是冗余,插入时手工保持与分类表一致 INSERT INTO expense (member_id, account_id, category_id, category_name, amount, expense_date) VALUES (1, 1, 1, '餐饮', 45.50, '2025-06-06'), (2, 3, 1, '餐饮', 88.00, '2025-06-08'), (3, 4, 5, '教育', 120.00, '2025-06-09'), (1, 2, 4, '医疗', 56.00, '2025-06-12'); -- 6. 预算:家庭成员 + 分类 + 月份 唯一 INSERT INTO budget (member_id, category_id, budget_month, budget_amount) VALUES (1, 1, '2025-06', 1000.00), (2, 1, '2025-06', 800.00), (1, 4, '2025-06', 500.00);

上面这笔手工数据里,张伟的工资卡初始余额 8000,发工资 +12000,餐饮支出 -45.50,李娜的余额宝 -88 用于餐饮。你可以在写完增删改查之后写一条 SQL 验证账户余额是否等于“初始余额 + 收入 - 支出”,这个闭环在答辩时拿出来讲很加分,因为大多数人的系统根本没有对账意识。

3.3 索引怎么建:先满足查询需求,再做数据库优化

课设文档里常有一个“索引设计”小节,很多人直接交白卷。建索引不用贪多,优先给三类字段加:外键字段、高频查询字段、范围查询字段。上面的建表 SQL 里已经通过 FOREIGN KEY 自动为 member_id、category_id 这类字段建了索引,你还需要手动补两个高频查询索引。

-- 按日期查询流水的场景最多,给日期字段加索引 CREATE INDEX idx_expense_date ON expense(expense_date); CREATE INDEX idx_income_date ON income(income_date); -- 预算表按 member_id + budget_month 查询,这个联合索引直接覆盖查询条件 CREATE INDEX idx_budget_member_month ON budget(member_id, budget_month);

索引不是越多越好。课设这种数据量,索引建多了反而拖慢插入速度,还会把 InnoDB 的存储空间撑大。经验原则是:单个表的索引数量控制在 5 个以内,联合索引的字段顺序要把等值查询字段放在前面、范围查询字段放在后面。idx_budget_member_month 里 member_id 在前,budget_month 在后,因为 member_id 是等值匹配,budget_month 是范围匹配,这个顺序能让联合索引完全生效。

4. 业务查询与增删改查:覆盖课设评分点的核心 SQL 与 JDBC 落地方案

4.1 增删改查:链接管理、预编译参数与事务边界

数据库课程设计的评分点里,增删改查至少占去一半分数。下面这张表把最常用的 9 条操作列全,你在写代码时对照着逐条测过一遍,这部分基本不会丢分。

编号操作类型SQL 说明
1新增收入INSERT INTO income
2新增支出INSERT INTO expense
3修改支出金额/分类UPDATE expense SET
4按ID删除支出DELETE FROM expense WHERE id=?
5查询全部家庭成员SELECT * FROM member
6按日期区间查支出SELECT ... WHERE expense_date BETWEEN ? AND ?
7按分类统计月支出SELECT category_name, SUM(amount) GROUP BY category_name
8查某账户流水SELECT ... WHERE account_id=? ORDER BY expense_date DESC
9多表连接查支出明细JOIN member / category 组合查询

第 3 条修改操作有一个隐藏坑。如果修改了 category_id,就必须同步修改冗余的 category_name,否则会出现分类ID指向新分类、名称还是旧名称的数据不一致。正确做法有两种:要么在 UPDATE 语句里同时写两个字段,要么改成关联子查询直接拿分类名。推荐后者,代码更稳。

-- 修改支出记录:金额、分类、分类名、日期一起更新 UPDATE expense e JOIN category c ON e.category_id = c.id SET e.amount = ?, e.category_id = ?, e.category_name = c.category_name, e.expense_date = ? WHERE e.id = ?;

这条语句用 JOIN 关联了分类表,category_name 直接从分类表现取,就不会出现上面说的不一致问题。注意 UPDATE 关联子查询的语法在 MySQL 和部分数据库里语义是先查后改,不会触发“You can't specify target table for update in FROM clause”这类自查询限制。

4.2 JDBC 落地:从 URL 参数到 PreparedStatement 防注入

课设要求用 Java 操作数据库的话,JDBC 是绕不开的一环。先搭一个最小可用的连接工具类,把 URL 参数调对。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // MySQL 8.0 驱动类;5.7 用 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/family_finance" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

URL 里有三个参数必须解释清楚。useSSL=false 是因为本地开发没有配置 SSL 证书,MySQL 8.0 默认尝试 SSL 握手,不关掉会报错或者握手超时;serverTimezone=Asia/Shanghai 是因为 JDBC 驱动与 MySQL 服务端时区不一致时,读写 DATETIME 会差 8 小时;characterEncoding=utf8 是为了让中文不乱码,但这个参数要配合 MySQL 端 character_set_server 一起看,后面避坑章节细说。

增删改操作统一用 PreparedStatement,不要用 Statement 拼字符串。PreparedStatement 有预编译优化,第二次执行同一模板 SQL 时不需要重新解析;更重要的是它把参数值作为数据传输,而不是拼进 SQL,从机制上挡住了 SQL 注入。下面是一段新增支出的示例:

public void insertExpense(int memberId, int accountId, int categoryId, String categoryName, double amount, String expenseDate) { String sql = "INSERT INTO expense (member_id, account_id, category_id, category_name, amount, expense_date) " + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, memberId); ps.setInt(2, accountId); ps.setInt(3, categoryId); ps.setString(4, categoryName); ps.setBigDecimal(5, BigDecimal.valueOf(amount)); ps.setDate(6, java.sql.Date.valueOf(expenseDate)); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }

注意金额字段用了 setBigDecimal 而不是 setDouble,这与数据库端 DECIMAL 类型对应,防止 double 转 decimal 时出现精度误差。日期字段用 java.sql.Date.valueOf 把字符串转成 SQL 日期,而不是手工拼进 SQL 字符串,这样既安全又能保证格式被数据库正确解析。

4.3 统计查询:GROUP BY 与 DATE_FORMAT 写出月度收支报表

查询统计是课设的压轴戏。大多数人的系统只做增删改查,能写出一个按月份分组的收支对比就已经超过九成同学。下面这段 SQL 把收入和支出分别按月汇总,再 UNION 成一张对比表。

-- 月度收支统计:用 UNION ALL 把收入和支出打到一张结果集里 SELECT DATE_FORMAT(income_date, '%Y-%m') AS month, '收入' AS type, SUM(amount) AS total FROM income GROUP BY DATE_FORMAT(income_date, '%Y-%m') UNION ALL SELECT DATE_FORMAT(expense_date, '%Y-%m') AS month, '支出' AS type, SUM(amount) AS total FROM expense GROUP BY DATE_FORMAT(expense_date, '%Y-%m') ORDER BY month;

DATE_FORMAT 把 DATE 类型格式化成“YYYY-MM”字符串,这样 GROUP BY 就能把同一个月的多条记录归并成一条。UNION ALL 不去重,因为收入和支出分组后不存在完全相同的两行,用 UNION ALL 比 UNION 少了去重开销,查询更快。中间章节的数据库优化里这就是一个常见技巧点,讲课时提一句“这里为什么不加 DISTINCT”,评审会高看一眼。

如果你还想做“各分类本月支出排行”,把 GROUP BY 换成 category_name 就行。这个查询在答辩里几乎必问,先写熟了再去讲视图和存储过程。

5. 避坑:家庭理财管理系统课设最常见的 6 个翻车现场

5.1 MySQL 8.0 驱动连接报错:Public Key Retrieval is not allowed

现象:Java 连数据库时报 “Public Key Retrieval is not allowed”,程序直接崩溃。

原因:MySQL 8.0 默认使用 caching_sha2_password 认证插件,客户端首次连接需要从服务器拿公钥做 RSA 加密,JDBC 驱动出于安全策略默认禁止自动拿公钥。

解决:在 JDBC URL 后面追加 allowPublicKeyRetrieval=true。注意这个参数和 useSSL=false 是配套出现的,如果开了 SSL,这个参数会被忽略。

jdbc:mysql://localhost:3306/family_finance?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

5.2 金额字段用 FLOAT 导致对账差几分

现象:账户余额明明只有 100 块,算支出总和却是 99.99999999,页面上显示 99.99999999。

原因:FLOAT 是单精度浮点,二进制无法精确表示 0.1 之类的十进制小数,误差在多次累加后会被放大。

解决:所有金额字段改成 DECIMAL(10,2),Java 侧用 BigDecimal,不要在 SQL 里写乘法乘出一个无限小数。已经用 FLOAT 建好的表,用 ALTER TABLE 修改字段类型:

ALTER TABLE expense MODIFY COLUMN amount DECIMAL(10,2) NOT NULL DEFAULT 0.00;

5.3 有外键约束时插入数据顺序错乱

现象:想往 expense 表插一条记录,报错 “Cannot add or update a child row: a foreign key constraint fails”。

原因:插入时传入的 member_id 或 category_id 在父表里不存在。常见于前端用下拉框选择分类,但下拉框 value 和数据库主键没对上。

解决:先确认父表存在对应主键,再插子表。同时给应用层加一个兜底校验,在 DAO 层先查一次父表,查不到直接返回业务错误,而不是等数据库抛异常。

5.4 中文乱码:characterEncoding 与 MySQL 服务端不一致

现象:浏览器里正常显示中文,Java 读出来全是问号。

原因:JDBC URL 里 characterEncoding=utf8,但 MySQL 服务端 character_set_server 是 latin1,两边转换链断裂。

解决:统一链路字符集。数据库建库用 utf8mb4,连接 URL 用 characterEncoding=utf8(utf8 是 utf8mb4 的别名,JDBC 驱动会做兼容),再确认 MySQL 配置文件 my.cnf 里 character_set_server=utf8mb4。课设环境排查时一条 SQL 直接看三处状态:

SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'character_set_database'; SHOW VARIABLES LIKE 'character_set_client';

5.5 交课设时不知道要交数据库什么文件

现象:老师要求提交“数据库脚本”,有人直接把自己电脑里的 MySQL data 目录压缩上传。

原因:MySQL 的数据文件是二进制私有格式,版本不对或环境不同根本打不开。

解决:课设最终交付统一用 SQL 脚本。Navicat 里可以右键数据库,“转储 SQL 文件”,生成一个包含全部建表和插入语句的脚本。也可以用 mysqldump 命令行导出:

mysqldump -u root -p family_finance > family_finance.sql

交作业前在新环境里用 source 命令导入一遍,验证脚本能完整复原数据库,这一步相当于给课设做了“后悔药”。

5.6 账户余额字段与流水不一致

现象:账户表里的 balance 是手工维护的,用户直接改数据库里的余额字段,造成余额和流水对不上。

原因:没有用数据库机制保证余额变更与流水写入的一致性。课设里很多人图省事,添支出时只插入流水,忘记同时改余额。

解决:最小方案是在业务层把“插入流水 + 更新余额”放进同一个事务,两个操作要么都成功、要么都回滚。进阶方案是建触发器让数据库自动维护余额。课设推荐事务方案,逻辑直观,答辩也好讲:

conn.setAutoCommit(false); try { // 插入支出 // 更新账户余额 balance = balance - ? conn.commit(); } catch (SQLException e) { conn.rollback(); }

6. 报表统计与进阶验证:用 SQL 画出月度收支趋势,再给课设加分

最后一章给一个能直接升级课设档次的实操:用单条 SQL 生成月度收支趋势数据,再配合展示层画成折线图。界面不是重点,重点是这条 SQL 能不能一次查出收入和支出的逐月累计值。

-- 月度收支趋势:按月汇总收入、支出、结余三列,用 LEFT JOIN 避免月份缺失 SELECT t.month, IFNULL(i.total_income, 0) AS total_income, IFNULL(e.total_expense, 0) AS total_expense, IFNULL(i.total_income, 0) - IFNULL(e.total_expense, 0) AS month_balance FROM ( SELECT DATE_FORMAT(income_date, '%Y-%m') AS month FROM income UNION SELECT DATE_FORMAT(expense_date, '%Y-%m') AS month FROM expense ) t LEFT JOIN ( SELECT DATE_FORMAT(income_date, '%Y-%m') AS month, SUM(amount) AS total_income FROM income GROUP BY DATE_FORMAT(income_date, '%Y-%m') ) i ON t.month = i.month LEFT JOIN ( SELECT DATE_FORMAT(expense_date, '%Y-%m') AS month, SUM(amount) AS total_expense FROM expense GROUP BY DATE_FORMAT(expense_date, '%Y-%m') ) e ON t.month = e.month ORDER BY t.month;

这条 SQL 比前面 UNION ALL 的版本多出了一个维度:它用子查询先取“有收入的月份和有支出的月份的并集”,再分别 LEFT JOIN 收入和支出的月度汇总,这样某个月只有支出没有收入时,结余也能正确算出负数。IFNULL 把缺失的一侧补成 0,避免结余变成 NULL。这个写法在报表需求里是标准做法,课设里能用出来就说明你真懂 JOIN。

拿到这份数据后,展示层可以做两件事。第一,用 ECharts 或者 JFreeChart 画一张双柱图,左边收入右边支出,中间画结余折线;第二,把数据导到 Excel 里生成透视表,评审时演示一下也能撑场面。

再进阶一步,可以给预算表加一个超支提醒的视图。视图把预算金额、实际支出金额、差额三个字段算好,Java 端只查视图就能拿结果:

CREATE VIEW budget_status AS SELECT b.id, b.budget_month, b.category_id, b.budget_amount, IFNULL(SUM(e.amount), 0) AS actual_amount, b.budget_amount - IFNULL(SUM(e.amount), 0) AS remaining_amount FROM budget b LEFT JOIN expense e ON e.category_id = b.category_id AND DATE_FORMAT(e.expense_date, '%Y-%m') = b.budget_month GROUP BY b.id, b.budget_month, b.category_id, b.budget_amount;

视图建好之后,一条 SELECT * FROM budget_status WHERE remaining_amount < 0 就能查出哪个分类超支了。这比在 Java 里先查一次预算再查一次实际支出分成两次数据库访问要优雅得多,也正好回扣前面说的数据库优化主题。

我在课设里踩得最狠的一次,就是 FLOAT 存金额导致对账差了两分钱,答辩时被老师顺着余额一路追问到表结构设计,最后只拿了良。后来再做这类系统,我养成了一个习惯:建表之前先把所有要做的查询在纸上列出来,每个查询反推需要的字段和索引,再回头定表结构。表结构一旦定好,后面写的每一行 SQL 都只是在验证它。

这个方向值得投入的地方在于:家庭理财管理系统结构小、边界清,却完整覆盖了数据库设计的全部环节,做完一次,建表、约束、索引、事务、统计查询的基本功就都练到了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询