简介:《数据库课程设计报告银行管理系统》是一份面向高校学生的数据库课程设计报告,适合作为计算机、软件工程专业完成银行管理类题目的参考资料。文档基于 Visual C++ 6.0 与 SQL Server,围绕储户、活期存取款、定期存取款等核心数据表展开,涵盖需求分析、概念结构设计(E-R 图)、逻辑结构设计、物理结构设计及系统功能实现;对账户信息、操作记录等表结构的字段类型、主外键关系也做了清晰说明。报告还给出了用户管理、账户操作、数据备份与恢复等主要功能的运行结果与关键代码,并总结了开发过程中的经验与不足,能帮助读者快速理解银行管理系统的基本架构,学习课程设计报告的撰写思路。资源包内含 1 个 docx 文档,全文共 12 页,容量仅 107KB,短小精悍,已有 660 人学习浏览。通过阅读这份报告,既能巩固 SQL Server 表设计和 CRecordSet 类访问数据库的编程方法,也能为独立完成类似系统设计提供完整范本。
1. 一个数据库课程设计报告能拆出多少东西
很多学生拿到“数据库课程设计报告银行管理系统.docx”后,只是翻一翻绪论和总结,抄个目录就交差。我却建议你反过来看:这份文档里最有价值的是第三、五章,也就是数据库表结构和 VC++ 6.0 下的 CRecordSet 访问代码。它把银行管理系统按“用户管理、账户操作、数据库备份恢复”三个模块串起来,覆盖了数据库课程设计里最典型的建表、增删改查和事务一致性问题。
如果你是正在做数据库课程设计的学生,或者想快速了解 MFC 桌面程序怎么操作 SQL Server 的开发者,这份报告能让你少走不少弯路。本文会顺着报告的章节顺序,把表结构设计、登录认证、开户销户、存取款事务和备份恢复逐个拆开,把文档里一笔带过的细节补上。文档里那些 OCR 错字不用在意,重点是背后的设计选择和实现方式。
2. 银行管理系统的表结构设计与外键陷阱
2.1 从E-R图到逻辑模型:储户、活期、定期三张核心表
报告的E-R图把实体分为储户和三类操作:活期存取款、定期存款、定期取款,外加一个定期操作记录。转换出来的逻辑结构是:
储户(帐号,姓名,密码,身份证号,性别,帐户余额,开户日期,开户地点); 活期存取款(nID,帐号,金额,类型,操作日期,利息,账户余额); 定期存款(nID,帐号,存款人姓名,金额,存储年份,年利率,存储日期); 定期取款(nID,帐号,取款人姓名,取款金额,取款日期); 定期记录(nID,帐号,存取款人姓名,类型,操作金额,年份,操作日期)
这里有一个要重点提醒的点:定期取款表在报告里写的是“外键:nID;被参照表:定期存款表”,这明显是错的。定期取款的外键应该指向储户表的帐号,否则一笔取款必须对应一笔存款,同一笔定期存款想分两次支取就完全没法建模。你在做类似银行管理系统课程设计的时候,不要被这份报告的错误外键带偏。
另一个问题是,活期存取款表和定期存取款表都保留了账户余额这个冗余字段。冗余的好处是每次查询流水时不用重新计算余额,坏处是更新时必须在同一个事务里同时修改余额和流水,否则对不上账。这一点后面专门讲。
2.2 物理表字段与约束:为什么余额不能用 float
原报告把余额设计成 float 类型。在实际的银行管理系统里,float 是无法接受的选择。原因很简单:浮点数在二进制里只能近似表示。比如 0.1 在 float 中不是一个精确值,累加多次之后余额就多出 0.0000001。账务系统里这属于不可容忍的错误,课程设计里最好用 decimal(18,2) 代替 float。
下面给出我在重写时经常用的建表方案。先建储户表,再建流水表,保证外键引用顺序正确。如果在 SQL Server 上做,把 IDENTITY 和 GO 分隔符原样保留即可。
CREATE TABLE dbo.Account ( CNo varchar(20) NOT NULL PRIMARY KEY, CName varchar(20) NOT NULL, CPassword char(6) NOT NULL, CID varchar(20) NOT NULL, CSex char(2) NOT NULL, CBalance decimal(18,2) NOT NULL DEFAULT 0, CDate datetime NOT NULL, CAddress varchar(30) NOT NULL ); GO CREATE TABLE dbo.CurrentAccount ( nID int IDENTITY(1,1) NOT NULL PRIMARY KEY, CNo varchar(20) NOT NULL, CMoney decimal(18,2) NOT NULL, CStyle varchar(10) NOT NULL, CDate datetime NOT NULL, CInterest decimal(18,2) NOT NULL DEFAULT 0, CBalance decimal(18,2) NOT NULL, CONSTRAINT FK_Current_Account FOREIGN KEY (CNo) REFERENCES dbo.Account(CNo) ); GO CREATE TABLE dbo.TimeDeposit ( nID int IDENTITY(1,1) NOT NULL PRIMARY KEY, CNo varchar(20) NOT NULL, CName varchar(10) NOT NULL, CMoney decimal(18,2) NOT NULL, CDate datetime NOT NULL, CYear int NOT NULL, CRate decimal(9,4) NOT NULL, CONSTRAINT FK_TimeDeposit_Account FOREIGN KEY (CNo) REFERENCES dbo.Account(CNo) ); GO这段代码里,CPassword 用 char(6) 是刻意照应原报告,它们用定长字符串保存密码,长度固定。decimal(18,2) 表示最多 16 位整数加 2 位小数,账务金额一般够用。外键都指向 Account 表,并且刻意不加 ON DELETE CASCADE。这样删除储户时数据库会挡住外键关联的流水记录,逼迫你在业务代码里先判断有无未结清的定期和活期余额,而不是悄无声息把历史记录全删掉。
如果非要完全复现原报告的字段,可以保留以下对照表,方便检查自己建的字段是否和报告一致。
| 表名 | 主键 | 外键 | 关键业务字段 |
|---|---|---|---|
| Account 储户表 | CNo | 无 | CName, CPassword, CID, CSex, CBalance, CDate, CAddress |
| CurrentAccount 活期存取款表 | nID | CNo 指向 Account | CMoney, CStyle, CDate, CInterest, CBalance |
| TimeDeposit 定期存款表 | nID | CNo 指向 Account | CName, CMoney, CDate, CYear, CRate |
| TimeWithdraw 定期取款表 | nID | 应指向 Account | CName, CMoney, CDate |
| TimeRecord 定期记录表 | nID | 可指向 Account | CName, CStyle, CMoney, CYear, CDate |
2.3 数据库死锁与约束检查的先后顺序
课程设计里最常见的数据库死锁场景是销户。删除账户前要检查活期余额和定期记录,这看起来是两步 SELECT,实际放到 UI 线程里和别的窗口同时在操作,就可能出现两个连接互相持锁等待。比如窗口 A 正在给账号 001 做取款,更新了 Account 行,还没提交事务;窗口 B 同时销户,读取同一行后再删除,于是 B 等 A 释放行锁,A 又在等更后面插入流水时需要的锁,死锁就出现了。
避免死锁有两个常用做法。第一个是把事务做短,任何用户交互完成后立即提交,不要在事务里弹 MessageBox。第二个是访问多张表时固定先后顺序,比如坚持先 Account 后 CurrentAccount。原报告的销户代码就是先 SELECT 检查,再 SELECT 定期,最后 Delete,这中间没有任何数据库事务,在 SQL Server 默认自动提交模式下反而问题不大;如果你加上事务,反而要小心锁范围扩大。关于这一点会在第 4 章展开。
3. VC++ 6.0 下的 CRecordSet 数据访问实战
3.1 为什么选择 CRecordSet 而不是 ADO 或 ODBC API
VC++ 6.0 时代访问 SQL Server 有几种常用方式:CRecordSet、ADO 和直接 ODBC API。原报告选择的是 CDatabase 加 CRecordSet,这也是多数老教材的标准做法。CRecordSet 的核心价值在于把查询结果映射成一个 C++ 类,表的每一列对应类里的一个成员变量。用 ClassWizard 选择数据表,MFC 自动生成记录集类,后续的增删改查就变成了对成员变量的赋值和调用 AddNew、Update、Delete 这几个方法,代码量远小于裸 ODBC API。
它的边界也很明显:如果要用 join 查询多张表,CRecordSet 生成步骤比较麻烦,通常得写 SQL 视图或者用 CDatabase::ExecuteSQL。所以原报告里的界面查询都是先查 Account,再查 CurrentAccount,再查 TimeDeposit,用多次简单查询替代一次复杂连接。这在数据量小的课程设计中完全够用,也让逻辑更好懂。
3.2 登录对话框的校验流程和 SQL 注入风险
原报告把登录框做成三个编辑框:账号、密码、确认密码,逻辑是确认密码也输一遍。可以简化成两个编辑框,但既然报告这么写,就按它的结构来。控件绑定关系可以做成下面这张表。
| 控件 ID | 变量类型 | 变量名 | 说明 |
|---|---|---|---|
| IDC_EDIT_NO | CString | m_strNo | 账号 |
| IDC_EDIT_PASSWORD | CString | m_strPassword | 密码 |
| IDC_EDIT_CONFIRM | CString | m_strRePassword | 确认密码 |
| IDC_BUTTON_OK | void | 无 | 登录按钮响应 OnOK |
按钮处理函数里,先做非空和一致性校验,然后构造 SQL 打开记录集:
void CLoginDlg::OnOK() { UpdateData(TRUE); if (m_strNo.IsEmpty() || m_strPassword.IsEmpty()) { MessageBox("账号和密码不能为空"); return; } if (m_strPassword != m_strRePassword) { MessageBox("两次密码不一致"); m_strPassword.Empty(); m_strRePassword.Empty(); UpdateData(FALSE); return; } CString strSQL; strSQL.Format("SELECT * FROM Account WHERE CNo = '%s'", m_strNo); if (!m_recordset.Open(AFX_DB_USE_DEFAULT_TYPE, strSQL)) { MessageBox("数据库打开失败", "数据库错误", MB_OK); return; } if (m_recordset.IsEOF()) { MessageBox("账号不存在"); m_recordset.Close(); return; } if (m_recordset.m_CPassword != m_strPassword) { MessageBox("密码错误"); m_recordset.Close(); return; } CBankApp *pApp = (CBankApp *)AfxGetApp(); pApp->strNo = m_strNo; m_recordset.Close(); CDialog::OnOK(); }这段登录逻辑实际上比原报告更接近可用状态:先用 SELECT 定位账号,再用成员变量比对密码。Open 的第二个参数是 SQL 字符串,AFX_DB_USE_DEFAULT_TYPE 会按记录集的默认打开方式创建一个结果集。注意 Open 成功之后必须判断 IsEOF,因为 SELECT 没有命中任何行时结果集是空的,直接访问 m_CPassword 会触发断言或者读到野指针。
这里有一个典型的课程设计漏洞:SQL 语句用字符串拼接账号,输入' OR '1'='1会让 WHERE 变成恒真,整个表被打开。虽然密码比对是在 C++ 层做,不会直接登录成功,但你已经把整张表载入了内存,属于数据泄露。要彻底解决,就给 SQL Server 建存储过程,或者用 CDatabase 的参数查询接口。课程设计答辩时能主动说出这个隐患,比回避它得分更高。
3.3 开户、销户:AddNew、Delete 与检查顺序
开户窗口的初始化比较简单,在 OnInitDialog 里给性别下拉框加“男”“女”,日期控件取当前时间,然后确定按钮里执行 AddNew。下面是一段经过整理的代码:
m_recordset.AddNew(); m_recordset.m_CNo = m_strNo; m_recordset.m_CName = m_strName; m_recordset.m_CPassword = m_strPassword; m_recordset.m_CID = m_strID; m_recordset.m_CSex = m_strSex; m_recordset.m_CBalance = m_bBalance; m_recordset.m_CDate = CTime::GetCurrentTime(); m_recordset.m_CAddress = m_strAddress; m_recordset.SetFieldNull(&m_recordset.m_CBalance, FALSE); m_recordset.Update();AddNew 成功后 CRecordSet 进入编辑模式,所有对成员变量的赋值都只是在内存里。只有调用 Update,MFC 才会生成 INSERT 语句。auto-increment 的主键不要手动赋值,CurrentAccount 的 nID 是 IDENTITY,所以开户只需要插入 Account 表。注意 m_bBalance 是 double 类型,这里被赋到 decimal(18,2) 字段时,ODBC 驱动会做转换,浮点累加误差在这个阶段就可能出现。更好的做法是编辑框里拿到的是 CString,解析成 decimal 再入库。
销户代码和原报告思路一致:先找到当前用户,再判断活期余额和定期账目。
CAccountSet rs; CString strSQL; strSQL.Format("SELECT * FROM Account WHERE CNo = '%s'", pApp->strNo); if (!rs.Open(AFX_DB_USE_DEFAULT_TYPE, strSQL)) { MessageBox("打开账户表失败"); return; } if (rs.m_CBalance > 0) { MessageBox("活期余额不为零,无法销户"); rs.Close(); return; } rs.Delete(); rs.Requery(); rs.Close(); MessageBox("销户成功");这段代码没有用事务,因为 Delete 是单条语句,SQL Server 自动提交。但需要注意 Requery 的作用:Delete 之后当前记录变成无效,必须重新查询刷新结果集,否则后续再次 Open 同一张表时可能拿到缓存里的旧状态。定期账目检查我故意留到示例外,完整逻辑里要先查 TimeDeposit 是否有未结记录,有就直接 return。这个检查要放在 Delete 之前,并且和 Delete 保持在同一个短事务里,否则检查完到删除之间,另一条线程恰好存入一笔定期,账户又被删了,数据就悬空了。
4. 存取款事务与数据库备份恢复的正确姿势
4.1 取款不只是 UPDATE,需要同时写流水
原报告的活期存取款功能,表面上就是更新 Account 表的 CBalance,再往 CurrentAccount 表插一条记录。但如果这两条语句之间程序崩溃,余额已减但流水没写,月底对账就全部错乱。解决办法是把两步放进同一个数据库事务。SQL Server 从 2000 到最新版本都支持 BEGIN TRANSACTION / COMMIT / ROLLBACK,课程设计里用 CDatabase::BeginTrans 也能达到同样效果。
在 MFC 里我一般会这样写取款逻辑:
m_db.BeginTrans(); try { CString sql1; sql1.Format("UPDATE Account SET CBalance = CBalance - %f " "WHERE CNo = '%s' AND CBalance >= %f", fAmount, strNo, fAmount); m_db.ExecuteSQL(sql1); CString sql2; sql2.Format("INSERT INTO CurrentAccount " "(CNo, CMoney, CStyle, CDate, CInterest, CBalance) " "VALUES ('%s', %f, '取款', GETDATE(), 0, " "(SELECT CBalance FROM Account WHERE CNo = '%s'))", strNo, fAmount, strNo); m_db.ExecuteSQL(sql2); m_db.CommitTrans(); } catch (CDBException *e) { m_db.Rollback(); AfxMessageBox(e->m_strError); e->Delete(); }代码里 UPDATE 带了 CBalance >= fAmount 条件,如果余额不够,影响行数是 0。但 ExecuteSQL 不直接返回影响行数,所以我一般先 SELECT 出来判断一次,或者把这段逻辑放到存储过程里。注意 CInterest 字段是利息,活期这里先置 0,到结息日另行计算。
4.2 用存储过程收敛业务规则并减少数据库死锁
把上述逻辑改写成存储过程,比在 VC++ 里拼 SQL 更符合银行系统的做法。存储过程的好处是业务规则集中在数据库端,客户端只传参数。下面这段是针对定期存款到期的支取写的,关键是先锁定账户行再更新:
CREATE PROCEDURE sp_WithdrawFixed @CNo varchar(20), @Amount decimal(18,2) AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; UPDATE Account WITH (ROWLOCK, UPDLOCK) SET CBalance = CBalance + @Amount WHERE CNo = @CNo; IF @@ROWCOUNT = 0 BEGIN ROLLBACK TRANSACTION; RETURN 1; END INSERT INTO CurrentAccount (CNo, CMoney, CStyle, CDate, CInterest, CBalance) VALUES (@CNo, @Amount, '定期到期', GETDATE(), 0, (SELECT CBalance FROM Account WHERE CNo = @CNo)); COMMIT TRANSACTION; RETURN 0; ENDUPDLOCK 是更新锁指示器,让 SQL Server 在这条 UPDATE 开始时锁定目标行,直到事务结束。这样一来,两个会话同一时间操作同一个账号时,其中一个会等另一个提交。配合短事务,数据库死锁出现的概率会明显下降。还有一个经常被忽略的点:存储过程的 RETURN 值在 MFC 里用 CDatabase::ExecuteSQL 拿不到,得换成 CRecordset 执行带返回码的 SQL,或者直接把错误信息用 RAISERROR 抛出来,在客户端捕获。
4.3 备份恢复、日志传送和数据库同步软件的边界
原报告在数据库模块里做了备份与恢复。备份语句非常简单:
BACKUP DATABASE Bank TO DISK = 'E:\Backup\Bank.bak' WITH INIT, COMPRESSION;恢复时如果有其他连接占用数据库,要先切到单用户模式:
ALTER DATABASE Bank SET SINGLE_USER WITH ROLLBACK IMMEDIATE; RESTORE DATABASE Bank FROM DISK = 'E:\Backup\Bank.bak' WITH REPLACE; ALTER DATABASE Bank SET MULTI_USER;WITH INIT 表示覆盖之前的备份文件,WITH REPLACE 表示覆盖现有数据库。这两个参数在课程设计里经常被漏掉,导致第二次备份报文件已存在,或者恢复时提示数据库正在使用。要注意的是,备份恢复只是容灾的底线,不是同步。真正生产环境要用日志传送、AlwaysOn 或第三方数据库同步软件做实时复制,把变更从主库同步到备库。课程设计能做到本地备份和恢复,已经满足基本要求,但如果答辩时被问“怎么保证不丢数据”,你要能答出“本地备份不是实时同步,只能恢复到上一个备份点”。
定期存款的关键坑在于利率。原报告定期记录表里有存储年份和存储利率,取款的时候应该按存入时的利率计算本息,而不是按当前银行挂牌利率。定期存款取款的本息合计 = 本金 + 本金 × 年利率 × 存储年份。如果你的系统里 TimeDeposit 表没有单独存利率,只在取款时查当前利率,那等利率调整后,旧定期存款就会按错误利率兑付。这是银行管理系统里一个非常隐蔽的业务错误。
5. 把这个课程设计改造成能交差又能扩展的工程
5.1 从 docx 报告里的字段反推校验逻辑
这份 docx 报告的字段表列出了每个属性的类型和约束,把约束翻译成前端校验,至少能补上这些:
- 密码长度必须为 6 位,且不能有空格;
- 身份证号要按 15/18 位规则校验,并检查前 17 位数字;
- 开户地点不能为空,余额初始值必须是 0;
- 定期存款金额要大于 0,存期年份只能是 1、2、3、5 这类允许值。
原报告完全没有提这些,实际上这些都是数据库课程设计老师最爱问的问题。
5.2 用视图把多次查询改造成单表查询
原报告查询账户信息时,开两个记录集分别查 Account 和 CurrentAccount。可以创建一个视图把余额和最近一次存取款时间拼起来:
CREATE VIEW v_AccountInfo AS SELECT a.CNo, a.CName, a.CBalance, a.CDate, MAX(c.CDate) AS LastTransDate FROM Account a LEFT JOIN CurrentAccount c ON a.CNo = c.CNo GROUP BY a.CNo, a.CName, a.CBalance, a.CDate;VC++ 里再生成一个 CRecordSet 挂在 view 上,就给了 CRecordSet 对 join 支持不好的短板一个补丁。
5.3 从单账户模型升级到客户账户模型
原报告最重要的结构问题是 Account 表同时承担了客户和账户两种角色。真实银行系统是一客户多账户,同一个身份证号可以有借记卡和信用卡。建议拆成 Customer 和 Account 两张表,Account 表增加 CustomerID 外键和账户类型字段,余额从 Account 移到单独的账户表。这样一来,销户变成关闭账户,不会影响客户的其它账户。这也是把课程设计从“管理系统”提升到“略带架构意识”的关键一步。
你可以再往前推一步:把活期存取款和定期存取款统一成一张交易流水表,用交易类型字段区分活期、定期、存款、取款、利息结转,看看原来的外键和余额冗余还需要保留哪些。这比在 docx 报告里打补丁要实用得多。
本文还有配套的精品资源,点击获取