C#+SQL Server学生信息管理系统:从表设计到功能实现的完整指南
1. 拿到题目先别急着写代码需求与表结构设计1.1 这个项目到底在考什么C# SQL Server做学生信息管理系统是数据库课程里最经典的大作业类型。你会拿到一个看起来很简单的题目描述无非就是“实现学生信息的增删改查”、“支持按学号/姓名查询”、“统计各专业人数”但实际上它考察的东西远不止界面好不好看。这个项目本质上是三块内容串在一起数据库设计能力、C#与SQL Server的交互能力、工程化交付能力。如果你只用了Form拖几个控件、双击按钮写几句SqlCommand那做完确实不难但答辩时老师随便问一个“你的表结构为什么这样设计”“如果某个学生选了十门课成绩表怎么处理”就很容易卡住。我建议拿到题目的第一步不是打开Visual Studio而是先把需求吃透。给一个最典型的“学生信息管理”需求清单这是我在多个项目中总结出来的核心范围学生信息维护学号、姓名、性别、年龄、专业、班级、入学年份、联系方式课程信息维护课程编号、课程名称、学分、授课教师成绩登记学生选课、录入成绩、成绩修改查询统计按学号精确查询、按姓名模糊查询、按专业统计人数、按课程统计平均分用户登录区分管理员权限和普通用户权限到这里就发现一个关键问题学生信息管理不是一张学生表就能搞定的。只要牵扯到成绩就必须有“学生—课程—成绩”三张关联表。很多同学一开始只建了学生表做到后来发现成绩存哪都不合适返工成本高。所以建表之前先把业务关系画清楚哪怕草稿纸上画几个框和连线都行。1.2 学生信息管理系统的核心表设计我们按标准的三范式来拆但也不要死磕范式导致过度拆分。学生信息管理系统用一个合理的“三张业务表 一张用户表”结构就够了表名用途关键字段Student学生基本信息学号主键、姓名、性别、出生日期、专业、班级、入学年份、联系电话Course课程信息课程编号主键、课程名称、学分、授课教师Score成绩表自增主键、学号外键、课程编号外键、成绩、考试学期UserInfo登录用户用户ID、用户名、密码、角色这里有几个设计细节特别容易踩坑学号为什么不设成自增列因为学号是业务意义上的唯一标识它由学校统一分配比如“20220001”这种格式跟数据库自增的数字完全无关。你如果设成IDENTITY(1,1)那录入真实学号时会非常别扭。所以学号用nvarchar(20)类型手动录入并且设置主键或唯一约束。成绩表为什么还要单独一个自增主键理论上学号 课程编号联合起来可以作为主键但实际使用中联合主键有些麻烦比如你录入成绩时同一学生同一课程只允许一条记录这个联合唯一约束确实能保证数据不重复但如果你以后要扩展考试次数、补考记录联合主键反而限制操作。我一般习惯加一个自增的ScoreId作为主键再对StudentId, CourseId加唯一约束兼顾灵活性和数据完整性。删除策略要提前想好。如果学生被删除他的成绩记录怎么办两种情况要么禁止删除有成绩记录的学生外键默认行为要么删除学生时同步删除他的成绩级联删除。作业阶段我建议在开头就明确外键关系怎么写避免后面做删除功能时弹出一堆“外键冲突”的报错。1.3 字段与约束的设计细节字段类型的选择是对基本功最直接的检验。给出我常用的建表字段清单学号 StudentIdnvarchar(20)主键。别用int因为学号可能含字母比如某些学校的学号有字母后缀虽然大多数是纯数字但nvarchar兼容性最好姓名 StudentNamenvarchar(50)不能为空性别 Genderchar(2)写死为“男”或“女”程序端用下拉框控制数据库端加CHECK约束兜底出生日期 BirthDatedate类型别用datetime多余的时间部分根本没意义专业 Majornvarchar(50)班级 ClassNamenvarchar(50)入学年份 EnrollYearint后面统计同一届人数时会用到联系电话 Phonenvarchar(20)注意电话号码如果用int存前导0会丢所以必须字符串类型这些约束不一定都要在数据库端实现也可以在C#端校验。但我的习惯是能加约束就加约束数据库层面校验是最后一道防线C#端只是第一道拦截。比如在SQL Server Management Studio里右键设置主键、默认值、CHECK约束都是鼠标操作成本极低但答辩时能说出“我通过数据库约束保证数据一致性”比空口说“我在代码里判断了”要有说服力。2. 数据库建模与SQL脚本一次性把基础打牢2.1 建库建表脚本SQL脚本一定要自己手写一遍不要完全依赖SSMS的图形界面建表。图形界面建表你自己确实能建出来但你没法把脚本交上去大作业交付通常要求提供.sql文件。而且手写一遍才能理解每句SQL在做什么。打开SQL Server Management Studio新建查询先建库CREATE DATABASE StudentDB; GO USE StudentDB; GO然后依次建四张表。注意外键的创建顺序先创建被引用的主表Student、Course、UserInfo再创建引用它们的从表Score否则SQL会报“引用的对象不存在”的错误。CREATE TABLE Student ( StudentId NVARCHAR(20) PRIMARY KEY, StudentName NVARCHAR(50) NOT NULL, Gender CHAR(2) NOT NULL CHECK (Gender IN (男, 女)), BirthDate DATE NULL, Major NVARCHAR(50) NULL, ClassName NVARCHAR(50) NULL, EnrollYear INT NULL, Phone NVARCHAR(20) NULL ); GO CREATE TABLE Course ( CourseId NVARCHAR(20) PRIMARY KEY, CourseName NVARCHAR(100) NOT NULL, Credit DECIMAL(3, 1) NULL, Teacher NVARCHAR(50) NULL ); GO CREATE TABLE UserInfo ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, Password NVARCHAR(50) NOT NULL, Role NVARCHAR(20) NOT NULL DEFAULT (管理员) ); GO CREATE TABLE Score ( ScoreId INT IDENTITY(1,1) PRIMARY KEY, StudentId NVARCHAR(20) NOT NULL, CourseId NVARCHAR(20) NOT NULL, ScoreValue DECIMAL(5, 2) NULL, Semester NVARCHAR(20) NULL, CONSTRAINT FK_Score_Student FOREIGN KEY (StudentId) REFERENCES Student(StudentId), CONSTRAINT FK_Score_Course FOREIGN KEY (CourseId) REFERENCES Course(CourseId), CONSTRAINT UQ_Student_Course UNIQUE (StudentId, CourseId) ); GO这里特别注意几个细节CHECK约束限制性别字段。虽然很多教材上说“CHECK约束可用可不用”但加上之后哪怕C#端忘记校验数据库也不会接受“中”这种非法值。这个细节答辩时很加分。Password字段不要明文存储。作业阶段我承认很多同学直接存明文但如果想让项目看起来更专业可以存哈希值。C#端用MD5或SHA256计算后再入库。不过这一点是加分项不是硬性要求。DECIMAL(5,2)存成绩。成绩可能是95.5这种带小数位的用int太粗糙用float又可能出现精度问题。DECIMAL(5,2)最大支持999.99完全够用。2.2 初始数据与关联关系建好表之后录入几条初始数据。初始数据不是随便填的建议模拟一个真实的班级场景这样后面测试查询功能时能覆盖到各种情况INSERT INTO Student (StudentId, StudentName, Gender, BirthDate, Major, ClassName, EnrollYear, Phone) VALUES (20220001, 张伟, 男, 2003-05-12, 计算机科学与技术, 计科2201, 2022, 13800000001), (20220002, 李娜, 女, 2003-08-23, 计算机科学与技术, 计科2201, 2022, 13800000002), (20220003, 王强, 男, 2002-11-30, 软件工程, 软工2201, 2022, 13800000003), (20210004, 赵芳, 女, 2002-02-14, 计算机科学与技术, 计科2101, 2021, 13800000004); INSERT INTO Course (CourseId, CourseName, Credit, Teacher) VALUES (C001, 数据库原理, 4.0, 刘老师), (C002, C#程序设计, 3.5, 陈老师), (C003, 数据结构, 4.0, 周老师); INSERT INTO UserInfo (UserName, Password, Role) VALUES (admin, 123456, 管理员), (student01, 123456, 普通用户); INSERT INTO Score (StudentId, CourseId, ScoreValue, Semester) VALUES (20220001, C001, 88.5, 2023-2024-1), (20220001, C002, 92.0, 2023-2024-1), (20220002, C001, 76.0, 2023-2024-1), (20220003, C003, 85.0, 2023-2024-1);录入完成后手动执行几条联合查询验证数据关联是否正确。比如SELECT s.StudentId, s.StudentName, c.CourseName, sc.ScoreValue FROM Student s JOIN Score sc ON s.StudentId sc.StudentId JOIN Course c ON c.CourseId sc.CourseId WHERE s.StudentId 20220001;如果结果正常显示该学生选了哪些课、成绩是多少就说明外键和应用逻辑都通了。这一步能提前暴露很多问题比如学号打错导致查不到记录或者成绩表的外键方向建反了。2.3 数据访问层封装C#连接数据库的方式数据库脚本写完后接下来是C#项目。很多同学直接把SqlConnection和SqlCommand写在按钮点击事件里一个项目十几处重复代码后面改一个连接字符串要改半天。我强烈建议从开始就做一个简单的数据访问层封装。新建项目时选择Windows窗体应用.NET Framework或.NET都行看你学校的环境项目结构可以这样规划StudentManage/ ├── Models/ // 实体类 │ ├── Student.cs │ ├── Course.cs │ └── Score.cs ├── DAL/ // 数据访问层 │ └── DBHelper.cs ├── BLL/ // 业务逻辑层作业阶段可省略 ├── UI/ // 窗体文件 │ ├── LoginForm.cs │ ├── MainForm.cs │ ├── StudentForm.cs │ ├── CourseForm.cs │ └── ScoreForm.cs └── Program.cs这个结构不复杂但比把所有代码堆在Form1里规范得多。DBHelper.cs的核心内容其实就几个方法public static class DBHelper { private static string connStr Server.;DatabaseStudentDB;User Idsa;Password你的密码;; public static SqlConnection GetConnection() { return new SqlConnection(connStr); } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn GetConnection()) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } } public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn GetConnection()) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } } }这段代码用到了using语句确保SqlConnection和SqlCommand用完即释放避免连接泄漏。连接字符串里的Server.;这个“.”表示本机SQL Server默认实例。如果你的SQL Server是命名实例就得写“Server你的计算机名\SQLExpress”这种形式。很多连接不上数据库的问题就出在这一步。3. C#端功能实现从UI到数据的完整链路3.1 项目结构与界面设计界面设计这块大作业不要求花哨要求的是功能完整、布局清晰、操作顺手。主窗体用菜单栏或TabControl分模块左侧放导航右侧放操作区域这种布局比较常见且好用。我的建议是做一个登录窗体验证通过后打开主窗体。主窗体上放一个TabControl分三个Tab页“学生信息管理”“课程管理”“成绩管理”。每个Tab页内部上面是查询条件区中间是DataGridView数据表格下面是操作按钮区新增、修改、删除、保存、刷新。窗体设计有个容易被忽视的点DataGridView的列要提前设计好不要等运行时自动生成。默认情况下DataGridView会自动根据返回的DataTable生成列这样虽然省事但列头显示的是英文字段名而且列宽、显示顺序都不可控。正确的做法是在设计器里手动配置列并将列的DataPropertyName绑定到数据表的字段名。给一个绑定列的例子// 设计器里设置dgvStudents的Columns集合 // 添加“学号”列DataPropertyName设为StudentId // 添加“姓名”列DataPropertyName设为StudentName // 以此类推... dgvStudents.DataSource dt;这样显示出来的表头就是中文而且字段顺序可控。3.2 登录模块与登录名验证登录功能是这个项目最容易拿到的分数也是安全性问题最容易暴露的地方。先讲一个反面案例——很多同学的登录查询是这么写的string sql SELECT * FROM UserInfo WHERE UserName txtUser.Text AND Password txtPwd.Text ;这种字符串拼接SQL的方式存在SQL注入风险。用户在用户名框里输入 OR 11 --就能直接绕过密码验证。老师一眼就能看出这是安全隐患。正确做法是使用参数化查询string sql SELECT Role FROM UserInfo WHERE UserNamename AND Passwordpwd; SqlParameter[] paras { new SqlParameter(name, txtUser.Text.Trim()), new SqlParameter(pwd, txtPwd.Text.Trim()) }; DataTable dt DBHelper.ExecuteQuery(sql, paras); if (dt.Rows.Count 0) { string role dt.Rows[0][Role].ToString(); MessageBox.Show(登录成功当前角色 role); // 打开主窗体 } else { MessageBox.Show(用户名或密码错误); }参数化查询不仅安全代码也更好读。注意密码字段这里直接存了明文实际项目中会用哈希但作业阶段能写出参数化查询已经比拼接SQL高一个段位了。3.3 学生信息的增删改查学生信息维护是核心功能。整个操作逻辑围绕DataGridView展开用户在下方输入框填写信息点击“新增”数据插入到数据库选中一行后点击“修改”对应数据填充到文本框点击“删除”弹出确认后从数据库移除。新增功能的核心代码string sql INSERT INTO Student (StudentId, StudentName, Gender, BirthDate, Major, ClassName, EnrollYear, Phone) VALUES (id, name, gender, birth, major, class, year, phone); SqlParameter[] paras { new SqlParameter(id, txtStudentId.Text.Trim()), new SqlParameter(name, txtStudentName.Text.Trim()), new SqlParameter(gender, cboGender.SelectedItem.ToString()), new SqlParameter(birth, dtpBirth.Value.Date), new SqlParameter(major, txtMajor.Text.Trim()), new SqlParameter(class, txtClass.Text.Trim()), new SqlParameter(year, txtEnrollYear.Text.Trim()), new SqlParameter(phone, txtPhone.Text.Trim()) }; int rows DBHelper.ExecuteNonQuery(sql, paras); if (rows 0) { MessageBox.Show(添加成功); LoadStudentData(); // 重新加载表格数据 }修改功能注意一点修改时使用学号作为条件而且不能修改学号本身因为它是主键。如果你的业务允许修改学号就得写UPDATE语句先改所有关联表的外键值复杂度会上升不少所以界面设计上我建议把学号设为只读。也就是说绑定数据后学号文本框不可编辑修改时只更新姓名、性别、专业等字段学号只作为定位记录的WHERE条件。删除功能同理定义删除的SQL语句时先检查该学生是否在Score表中有成绩记录。有两种处理方式方式一直接删除数据库外键约束会拒绝执行然后代码捕获异常弹出友好提示。try { DBHelper.ExecuteNonQuery(DELETE FROM Student WHERE StudentIdid, param); } catch (SqlException ex) { MessageBox.Show(该学生有成绩记录无法删除); }方式二先查询成绩表再决定是否允许删除。我建议用方式二逻辑更清晰也能在答辩时说明你考虑了数据完整性string checkSql SELECT COUNT(*) FROM Score WHERE StudentIdid; // 如果返回数量大于0提示“请先删除该学生的成绩记录” // 否则执行DELETE3.4 查询统计与DataGridView展示查询功能大致分三种精确查询、模糊查询、统计查询。精确查询一般指按学号查因为学号唯一模糊查询是按姓名或专业做LIKE匹配统计查询是分组汇总比如按专业统计人数、按课程统计平均分。这三类需求背后对应不同的SQL写法。模糊查询示例string keyword txtKeyword.Text.Trim(); string sql SELECT * FROM Student WHERE StudentName LIKE kw OR Major LIKE kw OR ClassName LIKE kw; SqlParameter p new SqlParameter(kw, % keyword %); DataTable dt DBHelper.ExecuteQuery(sql, p); dgvStudents.DataSource dt;这里有个重要的细节LIKE语句的参数值是%关键字%但要注意% 符号作为参数值传入而不是拼进SQL字符串。如果写成LIKE %kw%SQL会把它当成字面量什么都查不出来。统计查询则需要使用GROUP BY聚合函数这类查询的结果不在任何单一表里而是由多表联查产生的动态结果集。例如统计每个专业的学生人数SELECT Major, COUNT(*) AS StudentCount FROM Student GROUP BY Major;统计每个课程的平均分SELECT c.CourseName, AVG(sc.ScoreValue) AS AvgScore, MAX(sc.ScoreValue) AS MaxScore, MIN(sc.ScoreValue) AS MinScore FROM Course c JOIN Score sc ON c.CourseId sc.CourseId GROUP BY c.CourseName;如果还希望支持按照学期过滤就加上WHERE条件再GROUP BY。这些查询语句建议先在SSMS里跑通确认结果没问题后再把它们复制到C#代码中。DataGridView展示动态统计结果时需要注意列名。由于聚合函数生成的列名是StudentCount、AvgScore这种英文列名你可以通过SELECT语句直接指定别名或者在绑定前用代码修改列头或者抛弃手动绑定让DataGridView自动生成列之后再通过代码修改列头。我个人习惯用SQL别名优先比如SELECT c.CourseName AS 课程名称, AVG(sc.ScoreValue) AS 平均分这样在C#端显示时就自动是中文列名了。不过这个技巧只适合查询展示不方便做数据绑定编辑所以读取展示按这个来编辑场景还是用前面的手配置列方式。4. 实操中的高频报错与排查记录4.1 连接不上数据库这是我见过的第一大问题。典型报错System.Data.SqlClient.SqlException: 在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器。排查顺序我从经验总结为三步第一步确认连接字符串里的Server部分写对了。本机SQL Server默认实例用Server.;命名实例要写Server计算机名\\实例名格式不对肯定连不上。第二步确认SQL Server服务已启动。打开SQL Server配置管理器找到“SQL Server服务”检查你的实例是否处于“正在运行”状态如果没启动右键启动它。很多同学都是服务没启动一直以为是代码问题。第三步确认身份验证模式。如果你的连接字符串写的是User Idsa;Passwordxxx而SQL Server实例只开了Windows身份验证那就连不上。在SSMS里右键服务器属性选择“安全性”勾选“SQL Server和Windows身份验证模式”然后重启服务。还有一个细节远程连接时可能需要开启TCP/IP协议、设置防火墙规则不过本地作业一般不用考虑这些。4.2 SQL语句执行报错这部分高频报错列几个最典型的“列名无效”或“对象名无效”。这是SQL语法问题通常是表名或列名写错了比如把Student写成Students或者把CourseId写成CourseID。SQL Server的排序规则默认不区分大小写所以大小写错误一般不报真正的错误是少写或多写了字母或者把单词前后顺序写反。遇到这类报错先回到SSMS里试运行一遍SQL而不是在C#代码里反复改。“将nvarchar转换为int时失败”。这是参数类型不匹配。比如你把学号以int类型的数据传入nvarchar字段或者反过来在SQL里把varchar字段和数字比较。排查方法是确认参数的类型和长度如果拿不准就在参数化查询里显式指定new SqlParameter(id, SqlDbType.NVarChar, 20) { Value txtStudentId.Text.Trim() };“数据库已存在”。执行CREATE DATABASE脚本时如果数据库已经创建过再执行就会报这个错。解决办法一般是在脚本开头加IF判断或者先手动把已有数据库删掉。考虑到这是作业脚本个人建议加一个幂等判断更显专业IF DB_ID(StudentDB) IS NOT NULL BEGIN DROP DATABASE StudentDB; END CREATE DATABASE StudentDB;但注意DROP DATABASE会删除所有数据正式项目千万别这么干但作业脚本要反复运行测试这样反而省事。4.3 DataGridView数据不刷新问题表现增删改后表格还是显示旧数据。原因很简单你的DataSource没有重新赋值。解决方案就是每个增删改操作后调用一次加载数据的方法private void LoadStudentData() { DataTable dt DBHelper.ExecuteQuery(SELECT * FROM Student); dgvStudents.AutoGenerateColumns false; // 使用手动配置的列 dgvStudents.DataSource dt; }然后所有增删改成功的分支末尾调用LoadStudentData()。这里还隐藏着一个坑如果你的DataGridView开启了数据排序或用户编辑功能DataSource在UI层的缓存可能导致表格内容不更新。所以最稳妥的做法是加载时用BindingSource中转一下或者直接重新设置DataSource并调用Refresh方法。dgvStudents.DataSource null; dgvStudents.DataSource dt; dgvStudents.Refresh();4.4 外键约束冲突删除一个已有成绩记录的学生时最常见的报错是DELETE 语句与 REFERENCE 约束FK_Score_Student冲突。这个报错不是你的代码逻辑错了而是数据库在保护数据完整性。处理方式前面已经说过就是删除前先检查该学生是否有成绩记录。这里再补充一点如果你确实希望实现“删学生连带删成绩”的效果可以在外键创建时加ON DELETE CASCADECONSTRAINT FK_Score_Student FOREIGN KEY (StudentId) REFERENCES Student(StudentId) ON DELETE CASCADE但我不太建议为了省事直接上级联删除因为作业场景中“先查再删”的逻辑能体现你的业务流程思考而且万一你扩展了其他关联表级联删除风险很大。所以推荐保留外键代码里处理。5. 提交前检查从“能跑”到“好用”5.1 参数化查询与SQL注入在答辩现场老师最爱问的问题之一就是“你如何防止SQL注入”。如果你全部使用了参数化查询那这个问题就很稳解释一下拼字符串的SQL可以被用户输入改变逻辑注入语句可能绕过登录验证或泄露数据而参数化查询把用户输入当数据处理SQL引擎不会把它解析成执行指令。除了登录模块增删改查每条SQL都建议参数化。如果有同学图省事写了拼接SQL提交前务必全部改掉。这不仅是为了安全性也是评审老师判断你代码水平的重要依据。5.2 界面友好度与输入校验很多同学的作业功能都能跑但一输入非法数据就崩溃。挂在“输入校验”上非常可惜。做个简单总结输入框校验要求学号必填、长度不超过20姓名必填、不能包含数字出生日期不能晚于当前日期成绩0到100之间允许小数电话必填、长度合适C#端校验手写判断判断为空string.IsNullOrWhiteSpace(txtName.Text)判断是否为数字int.TryParse或decimal.TryParse判断日期DateTime.TryParse校验失败的提示不要用默认的异常弹窗可以用MessageBox提示或者用ErrorProvider控件在控件旁边显示红色图标。这里纯粹是体验问题但老师演示时会留下直观印象。另外要注意的是所有文本框Trim()后再赋值避免用户输入空格导致的“看着一样但匹配不上”的问题。我记得有个场景用户注册时不小心在用户名后面敲了个空格登录时一直失败就是没Trim的典型翻车案例。5.3 答辩准备与常见提问代码搞定之后提交前的论文和答辩材料同样重要。数据库大作业通常不只要求交代码还要交课程设计报告或项目说明书。报告中至少要包含以下几块内容需求分析数据库概念结构设计ER图数据库逻辑结构设计表结构数据库物理结构设计文件组、索引系统功能实现截图核心代码说明测试用例与测试结果总结与体会答辩时老师的高频提问提前准备“学号为什么用nvarchar而不是int”答学号虽然看起来是数字但不在数据库里参与数学计算而且部分学校学号可能包含字母或前导零用字符串更通用。“如果同一学生同一课程录入两次成绩会怎样”答Score表对StudentId, CourseId加了唯一约束第二条记录插入会违反约束报错可以通过异常处理提示用户。“COUNT(*)和COUNT(列名)有什么区别”答COUNT(*)统计全部行数COUNT(列名)只统计该列非NULL的行数。“两个表之间怎么关联的”答成绩表通过学号外键关联学生表通过课程编号外键关联课程表形成多对多关系的中间表。“这个系统有没有什么不足”答可以从密码加密存储、日志记录、数据分页、权限细化等方面讲展现思考深度。答辩时最忌讳的就是只会点击界面操作说不清背后的原理。所以搞清楚每个功能按钮背后执行的SQL是什么比什么都重要。我个人实际操作中的体会是这类大作业真正花时间的不是写代码而是前面设计表结构和后面处理各种边界情况。设计阶段想清楚后面代码是一次性通畅的设计阶段草率后面边写边改越改越乱。如果你打算扩展这个项目可以往这两个方向走一是加一个“课程选课”的前端页面让普通用户角色能自助选课而不是登录后什么都管二是给密码字段换成哈希存储再把连接字符串抽到配置文件里这会让项目看起来更有工程味。但不管怎样先把四张表的数据流程跑通你的大作业就已经稳稳拿分了。