简介《数据库课程设计报告银行管理系统》是一份面向高校学生的数据库课程设计报告适合作为计算机、软件工程专业完成银行管理类题目的参考资料。文档基于 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, CAddressCurrentAccount 活期存取款表nIDCNo 指向 AccountCMoney, CStyle, CDate, CInterest, CBalanceTimeDeposit 定期存款表nIDCNo 指向 AccountCName, CMoney, CDate, CYear, CRateTimeWithdraw 定期取款表nID应指向 AccountCName, CMoney, CDateTimeRecord 定期记录表nID可指向 AccountCName, CStyle, CMoney, CYear, CDate2.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 APIVC 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_NOCStringm_strNo账号IDC_EDIT_PASSWORDCStringm_strPassword密码IDC_EDIT_CONFIRMCStringm_strRePassword确认密码IDC_BUTTON_OKvoid无登录按钮响应 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 11会让 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 进入编辑模式所有对成员变量的赋值都只是在内存里。只有调用 UpdateMFC 才会生成 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 报告里打补丁要实用得多。本文还有配套的精品资源点击获取