
简介这份课程设计资源面向学习C#编程与数据库开发的高校学生及自学者提供一套可直接运行的银行管理系统完整方案帮助理解面向对象编程、SQL Server数据库操作与Windows桌面应用开发。压缩包共179个文件约4.62MB以66个cs源码文件、17个xaml界面文件、14个dll依赖库为主另含mdf与ldf数据库文件、sln解决方案、config配置及edmx数据模型等覆盖从界面到数据访问的完整层次。系统实现账户管理、存款取款、交易记录查询等核心功能通过Account、Transaction等类与Deposit、Withdraw方法组织业务逻辑并处理余额不足、输入验证等异常场景。已有391人学习下载适合作为课程设计参考或C#与数据库综合练习的实战案例便于快速理解项目结构与调试思路。1. 从一份课程设计说起C# 银行管理系统到底在练什么很多人第一次拿到「基于 C# 的银行管理系统」这个题目第一反应是去搜一套源码交差结果打开一看窗体堆了几十个数据库脚本却只有一张表连转账的原子性都没处理。这个题目真正练的不是拖控件而是把「账户、交易、并发、持久化」这四件事用 C# 和数据库串成一条能跑通的链路。它适合两类人一类是正在做数据库课程设计、需要一套能讲清楚 ER 图和事务边界的学生另一类是想用 C# 上位机思路练手、把 ADO.NET 增删改查和事务真正写进业务里的初学者。下面这套方案我按「能复现、能答辩、能扛住老师追问」的标准来拆源码结构、建库脚本、关键参数和踩过的坑都会给到。2. 先把数据模型立住银行管理系统的表结构与主键选型2.1 为什么账户表不能只存一个余额字段新手最容易犯的错是把「余额」当成一个孤零零的数字存在账户表里转账时直接UPDATE两次。这样做的后果是一旦第二次更新失败钱就凭空消失或凭空多出来。正确的做法是让余额成为「交易流水推导出来的结果」或者至少让每次余额变动都有一条不可篡改的流水记录与之对应。我一般会建三张核心表Account账户、TransRecord交易流水、Customer客户。账户表存开户信息、状态和当前余额流水表存每一笔发生过的金额变动带方向收入/支出和对方账号客户表存身份信息和账户是一对多。这样设计的好处是老师问「你怎么对账」时你可以直接拿流水表求和去比对账户余额而不是支支吾吾。主键选型上账户号用VARCHAR(19)而不是自增INT。原因很实际银行卡号本身就有业务含义自增 ID 在演示时看起来不像真实系统而且一旦要导入外部数据自增主键会冲突。交易流水号用BIGINT自增即可它只用于内部排序和去重。2.2 建库脚本与三个必须加的约束下面这段 SQL 是 MySQL 版本SQL Server 只需把AUTO_INCREMENT换成IDENTITY(1,1)ENGINEInnoDB去掉即可。注意三个约束账户号唯一、余额不能为负、流水金额不能为零。CREATE DATABASE BankDB DEFAULT CHARSET utf8mb4; USE BankDB; CREATE TABLE Customer ( CustId INT PRIMARY KEY AUTO_INCREMENT, CustName VARCHAR(32) NOT NULL, IdCard VARCHAR(18) NOT NULL UNIQUE, Phone VARCHAR(15) ); CREATE TABLE Account ( AccNo VARCHAR(19) PRIMARY KEY, CustId INT NOT NULL, Balance DECIMAL(18,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 1, -- 1正常 0冻结 OpenTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_acc_cust FOREIGN KEY (CustId) REFERENCES Customer(CustId), CONSTRAINT ck_balance CHECK (Balance 0) ); CREATE TABLE TransRecord ( TransId BIGINT PRIMARY KEY AUTO_INCREMENT, AccNo VARCHAR(19) NOT NULL, OppAccNo VARCHAR(19), Amount DECIMAL(18,2) NOT NULL, Direction TINYINT NOT NULL, -- 1收入 2支出 TransTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, Remark VARCHAR(64), CONSTRAINT fk_trans_acc FOREIGN KEY (AccNo) REFERENCES Account(AccNo), CONSTRAINT ck_amount CHECK (Amount 0) );逻辑说明DECIMAL(18,2)而不是FLOAT是因为金额计算不能有浮点误差这是数据库课程设计里老师必问的点。CHECK (Balance 0)是最后一道防线即使程序逻辑写漏了数据库也会拒绝把余额扣成负数。Direction用 1 和 2 而不是正负金额是为了让流水表在展示时不需要额外判断符号查询收入支出直接按Direction过滤。参数说明utf8mb4是为了支持中文姓名和备注里的特殊字符VARCHAR(19)对应 16 到 19 位银行卡号DECIMAL(18,2)表示总共 18 位、小数 2 位足够覆盖个人账户场景。如果你的课程设计要求支持对公账户把DECIMAL改成DECIMAL(20,2)即可。提示建完表后先手动插入两条测试数据确认外键和 CHECK 约束真的生效别等到写代码时才发现约束没加上。3. 用 C# 把增删改查写进业务开户、查询、转账的最小闭环3.1 连接字符串与 DbHelper 的封装边界C# 连 MySQL 常见做法是装MySql.Data包连 SQL Server 用System.Data.SqlClient。我一般会写一个DbHelper类只暴露三个方法ExecuteNonQuery、ExecuteScalar、ExecuteReader。不要一上来就上 Entity Framework课程设计的答辩场景里老师更想看到你懂SqlConnection、SqlCommand、SqlTransaction这些底层对象。public static class DbHelper { // 连接字符串放在配置文件里不要硬编码在代码中 private static readonly string ConnStr serverlocalhost;port3306;databaseBankDB;userroot;password你的密码;charsetutf8mb4;; public static int ExecuteNonQuery(string sql, params MySqlParameter[] paras) { using (var conn new MySqlConnection(ConnStr)) using (var cmd new MySqlCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, params MySqlParameter[] paras) { using (var conn new MySqlConnection(ConnStr)) using (var cmd new MySqlCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteScalar(); } } }逻辑说明using保证连接和命令对象在方法结束时释放避免连接池被占满。参数用MySqlParameter而不是字符串拼接是为了防 SQL 注入这也是答辩时能加分的细节。连接字符串里的charsetutf8mb4必须和建库时一致否则中文姓名会变成问号。参数说明server填本机就是localhostport默认 3306如果你装 MySQL 时改过端口要对应改password不要写123456然后提交到公开仓库课程设计也要养成好习惯。3.2 转账事务一个 SqlTransaction 写不对就翻车转账是整个系统里唯一必须用事务的地方。下面这段代码演示从 A 账户扣款、给 B 账户加款、写两条流水四步操作要么全成功要么全回滚。public static bool Transfer(string fromAcc, string toAcc, decimal amount) { using (var conn new MySqlConnection(ConnStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 1. 检查转出账户余额是否充足带行锁防止并发扣款 string checkSql SELECT Balance FROM Account WHERE AccNoacc FOR UPDATE; var cmdCheck new MySqlCommand(checkSql, conn, tran); cmdCheck.Parameters.AddWithValue(acc, fromAcc); decimal balance Convert.ToDecimal(cmdCheck.ExecuteScalar()); if (balance amount) { tran.Rollback(); return false; } // 2. 扣款 string outSql UPDATE Account SET BalanceBalance-amt WHERE AccNoacc; var cmdOut new MySqlCommand(outSql, conn, tran); cmdOut.Parameters.AddWithValue(amt, amount); cmdOut.Parameters.AddWithValue(acc, fromAcc); cmdOut.ExecuteNonQuery(); // 3. 入账 string inSql UPDATE Account SET BalanceBalanceamt WHERE AccNoacc; var cmdIn new MySqlCommand(inSql, conn, tran); cmdIn.Parameters.AddWithValue(amt, amount); cmdIn.Parameters.AddWithValue(acc, toAcc); cmdIn.ExecuteNonQuery(); // 4. 写两条流水 string recSql INSERT INTO TransRecord(AccNo,OppAccNo,Amount,Direction) VALUES(acc,opp,amt,dir); var cmdRec1 new MySqlCommand(recSql, conn, tran); cmdRec1.Parameters.AddWithValue(acc, fromAcc); cmdRec1.Parameters.AddWithValue(opp, toAcc); cmdRec1.Parameters.AddWithValue(amt, amount); cmdRec1.Parameters.AddWithValue(dir, 2); cmdRec1.ExecuteNonQuery(); var cmdRec2 new MySqlCommand(recSql, conn, tran); cmdRec2.Parameters.AddWithValue(acc, toAcc); cmdRec2.Parameters.AddWithValue(opp, fromAcc); cmdRec2.Parameters.AddWithValue(amt, amount); cmdRec2.Parameters.AddWithValue(dir, 1); cmdRec2.ExecuteNonQuery(); tran.Commit(); return true; } catch { tran.Rollback(); return false; } } } }逻辑说明FOR UPDATE是行级锁保证在检查余额到扣款之间没有另一个线程把同一账户的钱转走。没有这个锁两个并发转账请求可能都读到「余额充足」然后各自扣款导致余额变负。tran.Commit()之前任何一步抛异常catch里的Rollback会把四步操作全部撤销。参数说明amount用decimal类型不要用double。Direction的 2 表示支出、1 表示收入和建表时的注释保持一致。如果你用的是 SQL Server把MySqlConnection换成SqlConnectionMySqlCommand换成SqlCommandFOR UPDATE换成WITH (UPDLOCK)即可。注意FOR UPDATE必须在事务内使用且查询条件要命中索引这里是主键否则会锁表演示时如果数据量大就会卡住。4. 避坑与排查课程设计答辩前最容易翻车的五个点4.1 现象转账后余额对不上流水少了一条原因写流水的两条INSERT中有一条因为外键或字段长度失败但代码没有检查ExecuteNonQuery的返回值也没有回滚。解决把四条写操作全部放进同一个事务并且每次ExecuteNonQuery后判断返回值是否大于 0小于 0 就主动抛异常触发回滚。4.2 现象中文姓名存进数据库变成问号原因连接字符串没写charsetutf8mb4或者建库时用了latin1。解决建库语句加DEFAULT CHARSET utf8mb4连接字符串加charsetutf8mb4两处必须一致。如果已经建错用ALTER DATABASE BankDB CHARACTER SET utf8mb4;补救但已有数据可能已经损坏需要重新导入。4.3 现象并发测试时余额变成负数原因检查余额和扣款之间没有加锁两个线程同时通过检查。解决在SELECT语句后加FOR UPDATE并且确保整个操作在同一个事务里。如果不想用悲观锁也可以在UPDATE的WHERE里加AND Balance amt根据受影响行数判断是否扣款成功这是乐观锁思路。4.4 现象程序在本机跑得好好的换台电脑就连不上数据库原因连接字符串里写了localhost但新电脑的 MySQL 服务没启动或者端口不是 3306。解决先确认 MySQL 服务已启动再用telnet 127.0.0.1 3306测试端口。如果端口被占用去 MySQL 配置文件里改port同时改连接字符串。课程设计演示前一定要在答辩用的电脑上完整跑一遍。4.5 现象查询流水时程序卡死原因ExecuteReader打开后没有关闭或者DataReader还在读取时又用同一个连接执行了另一条命令。解决用using包住MySqlDataReader或者先把数据读进DataTable再关闭连接。一个连接同时只能有一个打开的DataReader这是 ADO.NET 的硬性限制。5. 进阶技巧用存储过程把转账逻辑收进数据库如果你想让课程设计在答辩时更有说服力可以把转账逻辑写成存储过程C# 只负责调用。这样做的好处是事务边界在数据库端更清晰而且老师问「如果换一个客户端语言怎么办」时你可以说「逻辑在数据库层换语言不影响」。DELIMITER // CREATE PROCEDURE sp_Transfer( IN p_from VARCHAR(19), IN p_to VARCHAR(19), IN p_amt DECIMAL(18,2), OUT p_code INT ) BEGIN DECLARE v_balance DECIMAL(18,2); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_code -1; END; START TRANSACTION; SELECT Balance INTO v_balance FROM Account WHERE AccNo p_from FOR UPDATE; IF v_balance p_amt THEN SET p_code -2; ROLLBACK; ELSE UPDATE Account SET Balance Balance - p_amt WHERE AccNo p_from; UPDATE Account SET Balance Balance p_amt WHERE AccNo p_to; INSERT INTO TransRecord(AccNo,OppAccNo,Amount,Direction) VALUES(p_from, p_to, p_amt, 2); INSERT INTO TransRecord(AccNo,OppAccNo,Amount,Direction) VALUES(p_to, p_from, p_amt, 1); COMMIT; SET p_code 0; END IF; END // DELIMITER ;C# 调用时用MySqlCommand并把CommandType设为StoredProcedure传入三个输入参数和一个MySqlDbType.Int32的输出参数。p_code返回 0 表示成功-1 表示异常回滚-2 表示余额不足。这样客户端代码会变得很薄也更容易做单元测试。验证方法开两个命令行窗口同时调用同一个存储过程转出同一笔钱观察是否只有一个成功。如果两个都成功说明FOR UPDATE没生效检查表引擎是不是 InnoDBMyISAM 不支持事务和行锁。这个测试我每次做完都会跑一遍血泪经验是别信「看起来没问题」要信并发下的实际结果。最后一个习惯把建库脚本、存储过程脚本和 C# 源码放在同一个仓库里数据库脚本用01_schema.sql、02_procedure.sql命名答辩时直接按顺序执行就能还原整个环境。我见过太多人源码交了数据库忘了导出最后只能现场重写。希望帮到你。本文还有配套的精品资源点击获取