简介:一份数据库课程设计报告,主题为银行管理系统,适合数据库课程设计、期末项目及入门开发者参考。报告完整覆盖需求分析、数据库概念结构设计、表结构设计以及C#与SQL Server 2008的实现选型,并以管理员和用户两类角色为主线,梳理了开户、销户、精确与模糊查询、存款、取款、贷款、转账、还贷、透支信息查看等业务流程,包含开户最低存款十元、贷款额度与收入挂钩、转账按卡类型收取0.02或0.05手续费等规则说明。资源为单个doc文档,共1个文件,压缩包大小2.91MB,便于直接阅读和打印,已有112人学习下载。具体内容包括用户、银行卡、转账、贷款、还贷、透支等数据表的字段类型与长度设置、E-R关系图、程序流程图以及数据库设计方案,可帮助读者理解从业务需求到数据库落地的完整路径,也能为撰写同类课程设计报告提供结构参考和细节范例。
1. 数据库课程设计里的银行管理系统:这份 .doc 能省下你多少重复劳动
“数据库课程设计报告——银行管理系统”这份 .doc,我拆完之后的判断是:它不是一份能直接跑起来的成品源码,而是一张“照着敲就能跑”的完整图纸。理由很简单——它把最容易卡住人的三块都写全了:需求分析里的业务规则、数据字典里的 28 个字段定义、登录和开户两个模块的 C# 代码。业务上它模拟的是一个小型银行核心:管理员开户销户,用户存取款、贷款、转账、还贷。适合正在做数据库课程设计想快速交差的学生,也适合想用最小成本复现一个带事务约束的银行 demo、准备面试项目的初学者。下面按我复现时会走的顺序,从需求到建库、从代码到排错,一条线拆完。
2. 需求分析与数据字典:六张表、两类角色和那些隐藏业务参数
拿到课程设计报告,我习惯先看需求分析,因为后面的表结构和代码全是从这里长出来的。这份报告的需求写得不算花哨,但胜在边界清楚:管理员和用户两类角色,功能划分明确,而且每一项功能的约束条件都给了具体数字——开户最低十元、同类卡转账手续费 0.02、异类卡 0.05、透支额度按收入三倍、贷款额度按收入五倍。这些数字看着不起眼,却是后面写代码时真正的逻辑开关。
2.1 角色与功能矩阵:哪些做了、哪些标注了暂未实现
先看功能矩阵。管理员和用户的功能在报告里分得很开,管理员管的是卡的“生老病死”,用户管的是卡上的资金操作。我把它整理成一张表,方便后面建表时对照。
| 角色 | 功能 | 执行条件与约束 |
|---|---|---|
| 管理员 | 开户 | 最低存入 10 元;新用户名同时写用户表 + 银行卡表,老用户名只加银行卡表 |
| 管理员 | 销户 | 先结清存款、贷款、透支,卡上无遗留数据才允许删除 |
| 管理员 | 精确查询 | 按用户名 / 日期组合条件查询存款与贷款,结果回显列表 |
| 管理员 | 模糊查询 | 需求已定义,代码标注未实现 |
| 用户 | 存款 | 校验卡号 + 密码后入账 |
| 用户 | 取款 | 无透支功能的卡不可超余额;可透支卡取款后进入透支阶段,还清透支才能再取 |
| 用户 | 贷款 | 额度由用户收入决定,代码实现为收入五倍;透支状态下也能贷款;逾期未还冻结该卡操作 |
| 用户 | 转账 | 先判断转向卡号存在;同类卡手续费 0.02,异类卡 0.05;金额不能超过当前卡存款 |
| 用户 | 还贷 | 仅支持一次性还清,理论上的分期未实现 |
| 用户 | 还透支 | 需求已定义,代码标注未实现 |
| 用户 | 查看信息 | 点击按钮查看当前卡的贷款与透支信息 |
注意表里的两个“未实现”:模糊查询和还透支。很多课程设计会在需求里写满功能,代码却只做一部分,这份报告至少把未实现的地方标出来了,复现时别在这两个功能上浪费时间。另外用户类型的区分是靠用户表里的“用户类型”字段,登录时先验证用户名密码,再和下拉框里的类型比对,这个流程下面讲登录模块时会展开。
2.2 六张表的数据字典:字段、类型与主键一眼看全
需求分析的第二部分是数据字典,报告里给了一张完整的字段表,覆盖六张表:用户、银行卡、转账、贷款、还贷、透支。摘要里说五张主表,实际建库会用到六张——透支表是后补进来的,以正文为准。我把 28 个字段浓缩成一张紧凑表。
| 表 | 字段清单(类型 / 长度) |
|---|---|
| 用户 | 用户名 nchar(10) 主键;密码 int;用户类型 nchar(10);信誉度 int;用户收入 int |
| 银行卡 | 用户名 nchar(10);卡号 int 主键;卡类型 nchar(10);金额 float;透支功能 bit;透支额度 int;贷款额度 int |
| 转账 | 转账号 int 主键;卡号 int;转向卡号 int;转账金额 int;手续费 float;转账利率 float |
| 贷款 | 贷款号 int 主键;卡号 int;贷款金额 int;贷款日期 datetime;贷款利率 float;是否有贷款 bit;利息 float;应还金额 float |
| 还贷 | 卡号 int;贷款日期 datetime;还款时间 datetime;贷款利率 float;贷款金额 int;利息 float;应还金额 float |
| 透支 | 透支号 int 主键;卡号 int;透支金额 int;透支开始时间 datetime;透支还清时间 datetime |
这张表一摆出来,有三个点立刻值得注意。第一,用户表里没有卡号字段,因为程序的设计是先验证用户名密码、再进入卡号选择,一个用户可以开多张卡,卡号归银行卡表管。第二,还贷表没有主键,这在课程设计的容错范围内,但后面做重复还款检查时会踩坑。第三,金额类字段的类型明显混用——转账金额、贷款金额是 int,手续费、利息却是 float,后面第 5 章的精度问题就是在这里埋下的。
2.3 业务规则里的参数:十元开户、三倍透支、五倍贷款、两级手续费
需求分析里散落着一批业务参数,它们不是摆设,每一条都对应代码里的一个硬编码。复现时最怕的是参数没找全,写到一半发现自己造的规则和报告对不上。我按代码逻辑重排了一遍:
- 开户最低存入 10 元,不足十元直接提示并阻断,代码里
if (xian >= 10)就是这道闸。 - 透支额度 = 用户收入 × 3,贷款额度 = 用户收入 × 5,开户时由程序自动计算并写入银行卡表。
- 取款:无透支功能的卡余额不足即拒绝;有透支功能的卡可以取出不超过透支额度的额外金额,但一旦进入透支状态,必须还清透支后才能再次取款。
- 转账手续费按卡类型判断:转账双方卡类型相同收 0.02,不同收 0.05;转账金额不能超过当前卡余额。
- 贷款可以在有透支的情况下进行,同一个人可以同时背着贷款和透支。
- 还贷一次性还清,按贷款日期和还款时间的间隔计算利息,应还金额 = 贷款金额 + 利息。
这些参数在报告里分散在需求分析和开户代码注释中,没有单独的配置项。想在复现时调整规则,直接改开户代码里的3 * money和5 * money即可。至于贷款利息的具体公式,报告只写了“贷款利率由用户选择的贷款时间决定”,没给完整算式——常见做法是利息 = 贷款金额 × 利率 × 期数,应还金额再加回本金,你可以在存储过程或 C# 代码里按这个口径实现。
3. 建库脚本与连接配置:把 E-R 模型落成 SQL Server 2008 可执行 SQL
需求分析之后就是概念结构设计。报告里的 E-R 图是文字树状拼出来的:用户拥有银行卡,银行卡关联转账、贷款、还贷、透支。实体关系不复杂,基本是一对多:一个用户多张卡,一张卡多次贷款、多笔转账、多次透支。把它们落成 SQL Server 2008 的建表脚本,才算真正拿到可以动手的基础。
3.1 六张表建表脚本:类型、长度、主键一次对齐
报告 1.2.2 的字段表已经给了类型和长度,我按 SQL Server 2008 的语法整理成可直接执行的建表脚本。中文表名在中文字段名可以直接用,但用方括号包住更稳妥,避免和关键字冲突。
CREATE TABLE [用户] ( [用户名] nchar(10) PRIMARY KEY, [密码] int NOT NULL, [用户类型] nchar(10) NOT NULL DEFAULT '用户', [信誉度] int NOT NULL DEFAULT 1, [用户收入] int NOT NULL DEFAULT 0 ); CREATE TABLE [银行卡] ( [用户名] nchar(10) NOT NULL, [卡号] int PRIMARY KEY, [卡类型] nchar(10) NOT NULL, [金额] float NOT NULL DEFAULT 0, [透支功能] bit NOT NULL DEFAULT 0, [透支额度] int NOT NULL DEFAULT 0, [贷款额度] int NOT NULL DEFAULT 0 ); CREATE TABLE [转账] ( [转账号] int PRIMARY KEY, [卡号] int NOT NULL, [转向卡号] int NOT NULL, [转账金额] int NOT NULL, [手续费] float NOT NULL DEFAULT 0, [转账利率] float NOT NULL DEFAULT 0 ); CREATE TABLE [贷款] ( [贷款号] int PRIMARY KEY, [卡号] int NOT NULL, [贷款金额] int NOT NULL, [贷款日期] datetime NOT NULL, [贷款利率] float NOT NULL, [是否有贷款] bit NOT NULL DEFAULT 1, [利息] float NOT NULL, [应还金额] float NOT NULL ); CREATE TABLE [还贷] ( [卡号] int NOT NULL, [贷款日期] datetime NOT NULL, [还款时间] datetime NOT NULL, [贷款利率] float NOT NULL, [贷款金额] int NOT NULL, [利息] float NOT NULL, [应还金额] float NOT NULL ); CREATE TABLE [透支] ( [透支号] int PRIMARY KEY, [卡号] int NOT NULL, [透支金额] int NOT NULL, [透支开始时间] datetime NOT NULL, [透支还清时间] datetime NULL );参数说明:nchar(10)是定长 Unicode 字符,用户名和卡类型用它足够;bit对应 C# 的 bool,透支功能和是否有贷款都是开关型字段;datetime存日期时间,贷款日期、还款时间、透支起止时间都用它;金额字段先按报告原文建,跑通之后再按第 5 章说的改成decimal(18,2)。原报告里还贷表没有主键,我暂时也保留,业务逻辑上的隐患后面单独讲。
建表顺序有个讲究:先建用户表,再建银行卡表,因为银行卡的用户名要引用用户表。转账、贷款、还贷、透支都挂在银行卡的卡号上,严格说应该最后建,但这里四张表互不依赖,顺序不影响。
3.2 关系与完整性:补上外键约束,不让程序唱独角戏
报告原文建表时没有建任何外键,这是课程设计里很常见的简化:靠 C# 代码先查后插来保证数据一致性。但 E-R 图里明明画出了“用户拥有银行卡”“银行卡产生贷款”这种关系,不落成外键约束的话,数据库层面就少了一道防线。我复现时会把外键补上:
ALTER TABLE [银行卡] ADD CONSTRAINT FK_Bank_User FOREIGN KEY ([用户名]) REFERENCES [用户]([用户名]); ALTER TABLE [转账] ADD CONSTRAINT FK_Transfer_Card FOREIGN KEY ([卡号]) REFERENCES [银行卡]([卡号]); ALTER TABLE [贷款] ADD CONSTRAINT FK_Loan_Card FOREIGN KEY ([卡号]) REFERENCES [银行卡]([卡号]); ALTER TABLE [还贷] ADD CONSTRAINT FK_Repay_Card FOREIGN KEY ([卡号]) REFERENCES [银行卡]([卡号]); ALTER TABLE [透支] ADD CONSTRAINT FK_Over_Card FOREIGN KEY ([卡号]) REFERENCES [银行卡]([卡号]);补完外键的一个重要变化落在销户逻辑上:需求要求卡上无贷款、无透支、无遗留数据才能销户,原文的程序代码只删了银行卡表,如果卡号在贷款表或透支表里有残留记录,加了外键后删除会被数据库直接拒绝,等于程序漏掉的检查由数据库兜底。这也是我在课程设计里一贯的做法——程序是业务规则的第一道防线,外键和约束是第二道,两者都装上才敢叫“完整”。
3.3 连接串与老 API 兼容:ConfigurationSettings 过时后的正确写法
报告代码里读连接串用的是一行老代码:System.Configuration.ConfigurationSettings.AppSettings["DB"]。这是 .NET 2.0 时代的写法,在现在的 Visual Studio 里编译会提示已过时,但还能跑,前提是项目引用了 System.Configuration.dll,并且在 App.config 的 appSettings 里加了 DB 这个键。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="BankDB" connectionString="Server=.;Database=BankDB;Integrated Security=true" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>如果跟着原文走,App.config 里应该写<add key="DB" value="Server=.;Database=BankDB;Integrated Security=true"/>,代码里保持 ConfigurationSettings 不动。但我的习惯是顺手升级成ConfigurationManager.ConnectionStrings["BankDB"].ConnectionString,原因有二:一是新版框架里 ConfigurationSettings 长期处于过时状态,说不定哪天就不认了;二是 ConnectionStrings 节点语义更清晰,和 SqlClient 的对应关系一目了然。
注意:连接串里的
Integrated Security=true是 Windows 身份验证,适合本机调试;如果用 SQL Server 身份验证,要改成User ID=sa;Password=你的密码。课程设计在实验室机器上跑,前者通常最省事。
4. 登录与开户模块:下拉菜单填充、身份校验和额度计算的 C# 实现
报告的程序实现部分给了登录界面和管理员界面的关键代码,严格说不是完整源码,但核心逻辑都在。这两个模块恰好也是整个系统里最值得看的代码——登录的校验顺序、开户的额度计算,都是能直接抄走改到自己项目里的东西。
4.1 登录下拉菜单:select distinct 用户类型,不硬编码角色
登录界面有个细节做得很讲究:用户类型下拉框不是写死“管理员”和“用户”,而是启动时从用户表里查出来的。这样设计的好处是,如果数据库里没有管理员类型记录,界面上下拉框里就没有“管理员”这一项,安全性比硬编码高了一截。原文代码我整理成了更规范的写法:
private void loginform_Load(object sender, EventArgs e) { string constr = ConfigurationManager.ConnectionStrings["BankDB"].ConnectionString; using (SqlConnection con = new SqlConnection(constr)) { SqlCommand com = new SqlCommand("select distinct 用户类型 from 用户", con); con.Open(); SqlDataReader result = com.ExecuteReader(); string[] types = new string[3]; // 当前系统只有管理员和用户两类 int i = 0; while (result.Read()) { types.SetValue(result["用户类型"].ToString(), i); i++; } loginlist.DataSource = types; // 下拉框数据源绑定 } }逻辑说明:先读连接串,再用select distinct 用户类型 from 用户把不重复的角色列表查出来,存入数组后直接绑定为下拉框数据源。原文把连接对象 con 和命令对象 com 定义为窗体成员变量,我这里改用 using 块局部变量,避免连接对象在窗口存活期间一直占着不放。
参数说明:数组容量原代码写死为 3,理由是用户类型只有管理员和用户两种。这个容量其实有隐患,如果以后在用户表里加一个“柜员”角色,数组就会越界。复现时建议改成List<string>动态扩容,一行代码的事。另一个值得注意的点是distinct关键字,如果数据库里混入了重复的用户类型记录,它会自动去重,不会在下拉框里出现两行一模一样的“用户”。
4.2 登录校验:先密码后类型,拼接 SQL 要改成参数化
原文的登录校验流程是两步走:先按用户名查密码,比对成功后查用户类型,再和下拉框选中值比对,最后按类型跳转到管理员界面或用户界面。第二步校验里有个隐患——用户不存在时直接对空结果取值会抛异常。我把两步合并成一次查询,顺便把字符串拼接的 SQL 改成了参数化查询:
private void loginbutton_Click(object sender, EventArgs e) { string constr = ConfigurationManager.ConnectionStrings["BankDB"].ConnectionString; string name = this.usernametxt.Text; string code = this.usercodetxt.Text; using (SqlConnection con = new SqlConnection(constr)) { con.Open(); SqlCommand com = new SqlCommand( "select 密码, 用户类型 from 用户 where 用户名 = @name", con); com.Parameters.AddWithValue("@name", name); SqlDataReader result = com.ExecuteReader(); if (!result.Read()) { MessageBox.Show("用户名不存在或密码错误,请重新输入!"); return; } if (result["密码"].ToString() != code) { MessageBox.Show("用户名不存在或密码错误,请重新输入!"); return; } string dbType = result["用户类型"].ToString(); if (loginlist.Text.Trim() == dbType) { if (dbType == "管理员") { this.Hide(); 管理员.admin ad = new 管理员.admin(this); ad.Show(); } else { this.Hide(); 用户.cardselect1 se = new 用户.cardselect1(this, name); se.Show(); } } else { MessageBox.Show("用户名不存在或密码错误,请重新输入!"); } } }逻辑说明:一次查询取出密码和用户类型两个字段,先调result.Read()判断记录是否存在,再逐项比对。比对通过后按用户类型分发到不同窗体,name作为参数传给下一个界面,个户在选择卡号时能直接知道自己是谁。
参数说明:@name是 SqlParameter 参数占位符,对应原文拼接的'"+name+"'。原文写法在用户名含单引号时会直接报 SQL 语法错误,参数化不存在这个问题。这是从课程设计代码迈向工程化代码的第一步,建议保留这个改动。另外result["密码"].ToString()和用户输入做的是字符串比较,原文里密码字段是 int 类型,这里在复现时会遇到一个奇怪的问题——见第 5 章。
4.3 开户:新老用户分叉、最低十元、额度公式与原文漏字段
开户模块是管理员界面的核心功能,逻辑分三条线:最低存入十元的校验、新老用户的分叉处理、透支额度和贷款额度的自动计算。原文代码里有个 bug,老用户且不开通透支功能时,insert 语句漏写了“透支额度”列,照抄会直接报“列名或所提供值的数目与表定义不匹配”。我整理时补全了:
Random ro = new Random(); int cardNo = ro.Next(100, 999); // 随机卡号,冲突问题见第5章 int deposit = int.Parse(this.textBox4.Text); // 开户存款金额 int income = int.Parse(this.textBox5.Text); // 月收入 int overLimit = 3 * income; // 透支额度 = 收入 × 3 int loanLimit = 5 * income; // 贷款额度 = 收入 × 5 if (deposit < 10) { MessageBox.Show("开户至少存入十元!"); return; } if (isNewUser) { if (this.textBox2.Text != this.textBox3.Text) { MessageBox.Show("请重新确认密码!"); return; } // 新用户:用户表和银行卡表都要插入 string sqlUser = "insert into 用户(用户名,密码,用户类型,信誉度,用户收入) " + "values(@name,@code,'用户',1,@income)"; string sqlCard = "insert into 银行卡(用户名,卡号,卡类型,金额,透支功能,透支额度,贷款额度) " + "values(@name,@card,@bank,@deposit,@overFunc,@overLimit,@loanLimit)"; // 依次执行两条 insert } else { // 老用户:只插银行卡表 string sqlCard = "insert into 银行卡(用户名,卡号,卡类型,金额,透支功能,透支额度,贷款额度) " + "values(@name,@card,@bank,@deposit,@overFunc,@overLimit,@loanLimit)"; }逻辑说明:开户先取四个输入值——卡类型、存款金额、月收入、是否开通透支功能,然后算出透支额度和贷款额度。存款低于十元直接阻断。新用户要求确认密码一致,并同时写用户表和银行卡表;老用户只加一张卡。isNewUser是前面“点击查询”按钮查出来的结果:查得到就是老用户,查不到就是新用户,同时会把密码框和收入框设为只读,防止冒充已有用户开户。
参数说明:3 * income和5 * income就是 2.3 节说的额度和收入挂钩规则。原报告里这两行代码直接写在开户方法里,属于硬编码,想调比例只需要改这两个乘数。cardNo用的是Random.Next(100, 999),这是一个会在开户量稍大时爆雷的设计——每次随机生成的卡号可能重复,而卡号又是银行卡表主键,具体现象和解决见第 5 章。
提示:原文代码大量使用字符串拼接 SQL,开户、登录、查询都这样。我整理出来的版本全部换成了
@name这类参数占位符。如果你照着原文敲,数据库里一旦出现含单引号的用户名,或者用户输入的密码里带特殊字符,程序就会在 ExecuteNonQuery 这一步抛异常。
5. 常见问题排查:卡号冲突、float 精度和销户残留的五个坑
这一章是血泪经验的集中区。下面五条坑不是瞎猜,全部能从报告原文的代码和表结构里直接找出来。每一条我都按“现象 → 原因 → 解决”写,方便你对照自己的报错信息快速定位。
5.1 随机卡号撞主键:100-999 只有 900 个组合
现象:连续开户多次后,偶尔在 insert 银行卡时弹出“违反 PRIMARY KEY 约束”,或者两张卡的卡号相同,后存入的卡覆盖了前面的显示结果。
原因:开户代码用Random ro = new Random(); int cardNo = ro.Next(100, 999);生成卡号。三位数的随机数只有 900 种组合,而且new Random()在极短时间连续调用时可能拿到相同种子,产生相同的随机数序列。卡号又是银行卡表主键,重号必炸。
解决:加一层查重循环,插入前先查卡号是否已存在,存在就重新生成,重试三次仍冲突就报错。更根本的做法是把卡号改成数据库自增列,比如bigint IDENTITY(10000001,1),从 10000001 开始递增,既保证唯一又不依赖程序侧随机数,第 6 章会给出具体改法。
5.2 金额用 float:存取几次后出现 99.999999
现象:用户存了 100 元,又取走 50 元,余额显示不是 50.00,而是 49.999999 或 50.000001 这种带一长串小数位的值。
原因:银行卡表的金额字段用的是 float,它是二进制浮点类型,本身无法精确表示大多数十进制小数。存取款涉及加减运算,误差会累积,银行场景对账时这种“分钱不对”的问题就是从这里来的。
解决:把金额字段全部改成decimal(18,2),这是 SQL Server 里最常用的货币精度类型,18 位总长度、2 位小数。同时把转账金额、贷款金额、透支金额这几个字段一并从 int 升级到 decimal——int 存金额还有另一个问题:金额小于 1 元时直接截断成 0。
5.3 销户与需求不符:代码没检查贷款和透支就删卡
现象:卡上明明还有未还贷款,销户却能成功,贷款记录变成孤儿数据,永远挂在一个已被删除的卡号下面。
原因:需求分析里写了“销户前必须结清存款、贷款、透支”,但销户代码只对银行卡表执行了 delete,没有先查询贷款表、透支表、转账表里是否有该卡号的记录。程序漏了检查,数据库又没有外键约束,删除自然畅通无阻。
解决:两条路都走。程序侧在 delete 前加三条查询,贷款表、透支表、转账表各统计一次,有记录就不允许销户;数据库侧把第 3 章的外键约束补上,即使程序漏查,数据库也会以主外键冲突的方式拒绝删除。外键是最后的后悔药,程序检查是第一道防线。
5.4 密码字段用 int + 拼接 SQL:引号用户名直接报错
现象:用户名输入 O'Neal 这类带单引号的字符串,登录直接抛 SQL 语法异常;或者用户设置的密码是 000123,存进数据库后变成 123,下次登录怎么输都进不去。
原因:密码字段在数据字典里定义为 int 类型。int 会把前导零吃掉,000123 写入后变成 123,登录时用户输入的“000123”转成字符串比对根本对不上。单引号报错则是因为登录 SQL 用了字符串拼接,引号没有做转义处理。
解决:密码字段从 int 改成nvarchar(50),存储原样字符串。SQL 全部改成参数化查询,让 SqlParameter 处理特殊字符。更进一步的做法是给密码做哈希存储,不在数据库里保存明文——课程设计可以不做,但参数化和改类型这两步建议保留。
5.5 还贷表无主键:重复还款产生脏数据
现象:用户重复点击还贷按钮,还贷表里出现多条完全相同的记录,对账时还款总额是实际应还金额的两倍。
原因:还贷表没有主键,也没有任何唯一约束,同一笔贷款可以无限次插入还贷记录。程序侧没有在还贷前检查贷款表的状态,重复点击就重复 insert。
解决:给还贷表加一个自增主键还贷流水号 bigint IDENTITY(1,1),让每条记录有唯一标识。程序侧在还贷前先查贷款表的“是否有贷款”字段,状态已是 0 就直接提示“该笔贷款已还清”。这样即使按钮被快速点击两次,第二次也会被状态检查拦下来。
6. 验证技巧:用一组 SQL 给银行管理系统做体检
复现完成后,怎么确认系统是真能跑、而不是刚好没触发雷?我的习惯是不依赖界面点点点,直接在 SSMS 里跑一组校验 SQL。这套查询能从数据层把表结构里的隐患暴露出来,比人工点按钮快得多。
6.1 四条查询把脏数据揪出来
-- 1. 用户与卡数量:正常情况每个用户至少一张卡 SELECT u.用户名, COUNT(b.卡号) AS 卡数 FROM 用户 u LEFT JOIN 银行卡 b ON u.用户名 = b.用户名 GROUP BY u.用户名; -- 2. 透支额度校验:开户逻辑应为 用户收入 × 3 SELECT b.用户名, b.卡号, b.透支额度, u.用户收入 FROM 银行卡 b JOIN 用户 u ON b.用户名 = u.用户名 WHERE b.透支额度 <> 3 * u.用户收入; -- 3. 应还金额校验:应还金额 = 贷款金额 + 利息 SELECT 贷款号, 卡号, 贷款金额, 利息, 应还金额 FROM 贷款 WHERE ABS(应还金额 - (贷款金额 + 利息)) > 0.01; -- 4. 还贷表重复检查 SELECT 卡号, 贷款日期, COUNT(*) AS 次数 FROM 还贷 GROUP BY 卡号, 贷款日期 HAVING COUNT(*) > 1;每条查询的用途很直接。第一条查用户和银行卡的 1:N 关系是否成立,有没有用户一张卡都没有的异常;第二条验证开户时透支额度是否真的等于收入三倍,如果查到记录,说明代码里的额度公式被改过或 insert 写错;第三条检查贷款利息计算是否一致,ABS比较允许 0.01 以内的浮点误差,超过就说明金额精度出问题了;第四条专门盯还贷表的重复记录,有结果就按第 5 章第 5 条处理。
6.2 顺手把卡号生成改成 IDENTITY,一劳永逸
如果体检跑出了卡号冲突的问题,最省事的改法是动表结构而不是动代码。卡号从程序生成的随机 int 改成数据库自增列,开户时不再关心“生成什么卡号”,插入后把自增值取回来展示即可:
CREATE TABLE [银行卡] ( [卡号] bigint IDENTITY(10000001,1) PRIMARY KEY, [用户名] nchar(10) NOT NULL, [卡类型] nchar(10) NOT NULL, [金额] decimal(18,2) NOT NULL DEFAULT 0, [透支功能] bit NOT NULL DEFAULT 0, [透支额度] int NOT NULL DEFAULT 0, [贷款额度] int NOT NULL DEFAULT 0 );这里一并把金额字段改成了 decimal(18,2),顺手解决第 5 章的精度坑。C# 侧插入完成后用SELECT SCOPE_IDENTITY()拿到新卡号,再弹窗告诉用户。从那以后我拿到任何一份数据库课程设计文档,第一件事都是先把六张表建出来,跑一遍上面第二条和第三条查询,确认表结构和业务对得上,再去看 C# 代码——表结构不先钉死,代码里写再多判断都是纸面保障。希望帮到你。
本文还有配套的精品资源,点击获取