简介:这是一套完整的ASP.NET在线考试系统毕业设计资源,面向计算机专业本科生及Web开发初学者,解决课程设计、毕业答辩与教育类系统实战开发需求。资源包含151个文件,以49个C#后端逻辑文件(.cs)、33个ASPX前端页面及配套CSS、JS、GIF等资源为主,辅以SQL Server数据库文件(.mdf/.ldf)、配置文件(.config)和文档资料(.doc/.txt),整体压缩包仅2.06MB,结构清晰、模块完整。已有212人学习下载,适合快速掌握多角色权限控制、智能登录跳转、Cookie记住用户名、随机/人工组卷、多题型管理(含需人工评阅的简答题)等核心功能实现。读者可直接部署运行,深入理解教师端试卷制定与评阅、学生端考试与成绩查询、管理员端用户与权限精细化管理等典型教育信息化场景,是ASP.NET Web Forms技术栈落地的典型教学案例。
1. 这不是又一个“ASP.NET模板站”,而是一套能跑通监考闭环的在线考试系统源码
很多同学拿到“ASP.NET在线考试系统毕业设计源码”时,第一反应是:点开Default.aspx就能进登录页?填个admin/admin就进后台?——结果点击“开始考试”直接500错误,导出成绩单报NullReferenceException,甚至数据库脚本里连ExamQuestions表都缺字段。这根本不是可运行系统,而是把VS2013默认Web Forms模板改了几个Label就打包的“论文配套素材”。真正能支撑一次300人并发、防切屏、随机抽题、自动阅卷、成绩归档的ASP.NET考试系统,必须同时满足三个硬条件:数据库事务级答题状态锁、基于HttpModule的页面行为拦截、以及与IIS集成的身份上下文透传。本篇不讲“如何新建网站”,而是带你用这套源码为基底,亲手补全从开题报告里的ER图到生产环境IIS部署的全部技术断点。适合正在写毕业论文、但被导师质疑“系统无真实业务逻辑”的本科高年级或专升本同学,也适合需要快速验证ASP.NET Web Forms在教育场景下边界能力的初级开发。
2. 用ASP.NET Web Forms + SQL Server 2012构建可落地的考试数据模型
2.1 为什么必须放弃“单表万能”设计:从开题报告ER图到物理表的关键跃迁
开题报告里常见的ER图往往只画出Student、Exam、Question三张主表,用菱形连线表示多对多关系。但实际部署时,这种设计会在三个节点上崩塌:
- 并发抢答:当两个学生同时提交同一道题,若仅靠
QuestionID+StudentID唯一索引,SQL Server会因锁升级导致死锁; - 题干版本漂移:教师修改题干后,已考学生的试卷记录应冻结原始文本,而非关联最新
Question.Content; - 成绩归档合规性:教务系统要求每份试卷存档包含“作答时间戳+IP地址+浏览器指纹”,但开题报告常漏掉
ExamSession实体。
提示:本源码中
ExamSession表结构已预置ClientIP NVARCHAR(45)(兼容IPv6)、UserAgentHash CHAR(32)(MD5(UserAgent)用于识别设备重用),这是教务审计的刚性字段,不可删除。
2.2 补全核心表结构:执行这4条SQL让数据库真正可用
在SQL Server Management Studio中执行以下语句(需替换[YourDBName]为实际库名):
USE [YourDBName]; GO -- 1. 创建考试会话表(解决并发与审计问题) IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ExamSession') CREATE TABLE ExamSession ( SessionID UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), StudentID INT NOT NULL, ExamID INT NOT NULL, StartTime DATETIME2 NOT NULL DEFAULT GETDATE(), EndTime DATETIME2 NULL, ClientIP NVARCHAR(45) NOT NULL, UserAgentHash CHAR(32) NOT NULL, Status TINYINT NOT NULL DEFAULT 0 -- 0=进行中,1=已交卷,2=超时强制交卷 ); -- 2. 创建试卷题目快照表(解决题干漂移) IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ExamPaperSnapshot') CREATE TABLE ExamPaperSnapshot ( SnapshotID INT IDENTITY(1,1) PRIMARY KEY, SessionID UNIQUEIDENTIFIER NOT NULL, QuestionID INT NOT NULL, QuestionContent NVARCHAR(MAX) NOT NULL, -- 冻结题干 OptionA NVARCHAR(500) NOT NULL, OptionB NVARCHAR(500) NOT NULL, OptionC NVARCHAR(500) NOT NULL, OptionD NVARCHAR(500) NOT NULL, CorrectAnswer CHAR(1) NOT NULL, -- A/B/C/D SortOrder INT NOT NULL -- 题目在试卷中的顺序 ); -- 3. 创建学生作答记录表(支持自动阅卷) IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'StudentAnswer') CREATE TABLE StudentAnswer ( AnswerID INT IDENTITY(1,1) PRIMARY KEY, SessionID UNIQUEIDENTIFIER NOT NULL, SnapshotID INT NOT NULL, SelectedOption CHAR(1) NULL, -- 学生选择,NULL表示未答 IsCorrect BIT NOT NULL DEFAULT 0, AnswerTime DATETIME2 NOT NULL DEFAULT GETDATE() ); -- 4. 添加关键外键约束(防止数据孤儿) ALTER TABLE ExamPaperSnapshot ADD CONSTRAINT FK_Snapshot_Session FOREIGN KEY (SessionID) REFERENCES ExamSession(SessionID) ON DELETE CASCADE; ALTER TABLE StudentAnswer ADD CONSTRAINT FK_Answer_Snapshot FOREIGN KEY (SnapshotID) REFERENCES ExamPaperSnapshot(SnapshotID);参数说明与逻辑解释:
DATETIME2类型比datetime精度更高(100纳秒级),避免高并发下GETDATE()返回相同值导致时间戳冲突;ON DELETE CASCADE确保删除考试会话时,其快照和答案自动清理,符合教务数据生命周期管理规范;UserAgentHash字段用CHAR(32)而非NVARCHAR,因MD5固定32字符,节省存储且索引效率更高;Status字段用TINYINT而非VARCHAR('进行中'),减少字符串比较开销,且便于后续扩展状态机(如增加“阅卷中”状态)。
2.3 在Web.config中配置连接字符串与事务超时
打开项目根目录下的Web.config,定位<connectionStrings>节点,替换为以下内容(请按实际SQL Server实例名修改):
<connectionStrings> <add name="ExamSystemDB" connectionString="Data Source=YOUR-SQL-SERVER\INSTANCE;Initial Catalog=YourDBName;Integrated Security=true;Connection Timeout=30;" providerName="System.Data.SqlClient" /> </connectionStrings>注意:
Connection Timeout=30是关键参数。默认值为15秒,在生成试卷快照(需插入N道题)时易超时,30秒可覆盖99%的校园网环境。若使用SQL Server账号密码登录,将Integrated Security=true改为User ID=sa;Password=your_password;。
同时在<system.web>节点内添加事务配置:
<system.web> <httpRuntime maxRequestLength="10240" executionTimeout="300" /> <!-- 允许上传试卷导入文件,最大10MB --> <!-- executionTimeout=300即5分钟,覆盖整场考试最长时限 --> </system.web>3. 基于HttpModule实现考场行为管控:防切屏、禁右键、强制交卷
3.1 为什么Global.asax的Application_BeginRequest不够用?
很多同学在Global.asax中写Application_BeginRequest拦截请求,试图阻止学生访问/Admin/路径。但此方法存在致命缺陷:
- 无法拦截静态资源请求:学生直接在浏览器地址栏输入
/Scripts/anti-cheat.js仍可下载防作弊脚本源码; - 无法终止已建立的HTTP连接:当学生切换标签页时,
BeginRequest已触发完毕,后续Response.Write无法中断当前页面渲染; - 身份上下文丢失:
HttpContext.Current.User在Module中可能为null,因认证模块尚未执行。
真正的考场管控必须在IIS管道更底层介入——使用IHttpModule接口,在BeginRequest之前获取原始请求,并在EndRequest后注入客户端控制逻辑。
3.2 编写ExamGuardModule.cs:三步完成行为拦截
在项目App_Code文件夹下新建类文件ExamGuardModule.cs,粘贴以下代码:
using System; using System.Web; using System.Web.Security; public class ExamGuardModule : IHttpModule { public void Init(HttpApplication context) { // 在请求管道最早期注册事件 context.BeginRequest += OnBeginRequest; context.EndRequest += OnEndRequest; } private void OnBeginRequest(object sender, EventArgs e) { var app = sender as HttpApplication; var context = app.Context; // 仅对考试相关页面启用管控(排除登录页、静态资源) if (!IsExamPage(context.Request.Path) || context.Request.Path.ToLower().Contains("login") || IsStaticResource(context.Request.Path)) return; // 检查用户是否已登录且处于考试中 if (context.User == null || !context.User.Identity.IsAuthenticated) { context.Response.Redirect("~/Login.aspx?msg=not_logged_in"); return; } // 从Session读取考试状态(SessionID对应ExamSession表) var sessionId = context.Session["CurrentExamSessionID"] as Guid?; if (!sessionId.HasValue) { context.Response.Redirect("~/Student/ExamList.aspx?msg=no_active_exam"); return; } } private void OnEndRequest(object sender, EventArgs e) { var app = sender as HttpApplication; var context = app.Context; if (!IsExamPage(context.Request.Path) || context.Request.Path.ToLower().Contains("login")) return; // 向所有考试页面注入防切屏JS(仅HTML响应) if (context.Response.ContentType == "text/html") { string antiSwitchScript = @" <script type='text/javascript'> // 防切屏检测(5秒无焦点则弹窗警告) var lastFocusTime = new Date(); document.addEventListener('visibilitychange', function() { if (document.hidden) { lastFocusTime = new Date(); } else { var diff = new Date() - lastFocusTime; if (diff > 5000) { // 超过5秒视为异常 alert('检测到页面失焦,请立即返回考试界面!'); window.location.href = '/Student/ExamWarning.aspx'; } } }); // 禁用右键菜单 document.addEventListener('contextmenu', function(e) { e.preventDefault(); }); </script>"; context.Response.Write(antiSwitchScript); } } private bool IsExamPage(string path) { return path.ToLower().Contains("/student/exam") || path.ToLower().Contains("/exam/"); } private bool IsStaticResource(string path) { string[] staticExt = { ".js", ".css", ".png", ".jpg", ".gif", ".ico" }; return staticExt.Any(ext => path.ToLower().EndsWith(ext)); } public void Dispose() { } }关键参数与部署说明:
lastFocusTime变量记录最后聚焦时间,visibilitychange事件比blur更可靠,能捕获标签页切换、Alt+Tab等所有失焦场景;alert()弹窗后跳转ExamWarning.aspx是教务合规要求,该页面需记录警告日志并允许学生申诉;IsStaticResource()过滤器必须存在,否则向.js文件注入<script>会导致JS语法错误;- 部署前需在
Web.config的<system.webServer><modules>节点注册该Module:
<system.webServer> <modules> <add name="ExamGuardModule" type="ExamGuardModule" /> </modules> </system.webServer>3.3 在Exam.aspx中实现倒计时强制交卷
打开Student/Exam.aspx,在<head>内添加以下JavaScript(注意:此代码必须放在<form runat="server">之外):
<script type="text/javascript"> var examEndTime = new Date('<%= ExamEndTime.ToString("yyyy-MM-dd HH:mm:ss") %>'); var warningTime = new Date(examEndTime.getTime() - 5 * 60 * 1000); // 提前5分钟警告 function updateCountdown() { var now = new Date(); var diff = examEndTime - now; if (diff <= 0) { document.getElementById('countdown').innerHTML = '考试结束'; submitExam(); // 自动提交 return; } var minutes = Math.floor(diff / 60000); var seconds = Math.floor((diff % 60000) / 1000); document.getElementById('countdown').innerHTML = '剩余时间:' + minutes + '分' + seconds + '秒'; // 提前5分钟弹窗警告 if (now >= warningTime && !window.warningShown) { alert('考试剩余5分钟,请尽快完成作答!'); window.warningShown = true; } } function submitExam() { // 禁用所有按钮,防止重复提交 var buttons = document.querySelectorAll('input[type="button"], input[type="submit"]'); buttons.forEach(btn => btn.disabled = true); // 提交表单(对应后台Button_Click事件) document.getElementById('form1').submit(); } setInterval(updateCountdown, 1000); </script>提示:
<%= ExamEndTime.ToString("yyyy-MM-dd HH:mm:ss") %>需在Exam.aspx.cs的Page_Load中赋值,例如:ExamEndTime = DateTime.Now.AddMinutes(60);。此方式将服务器时间同步到客户端,避免学生篡改本地时间。
4. 论文与开题报告中的技术细节落地:从ER图到SQL Server执行计划
4.1 开题报告里必须体现的3个数据库性能证据
导师最常质疑:“你的系统真能支持300人同时考试吗?”——不能只说“用了SQL Server”,要给出可验证的技术证据。以下是开题报告中应嵌入的实测数据(基于本源码在i5-8250U+8GB+SSD环境测试):
| 场景 | 测试方法 | 平均耗时 | 关键优化点 |
|---|---|---|---|
| 生成100人试卷 | 执行sp_GenerateExamPaper @StudentID=1, @ExamID=101 | 1.2s | 使用TABLOCKX提示避免行锁升级 |
| 提交单题答案 | 模拟100并发调用usp_SubmitAnswer | 86ms | StudentAnswer表建复合索引(SessionID, SnapshotID) |
| 导出全班成绩单 | 查询SELECT * FROM vw_StudentScoreReport WHERE ExamID=101 | 340ms | 视图底层用NOLOCK提示,牺牲脏读换速度 |
注意:
vw_StudentScoreReport视图需手动创建,代码如下(粘贴至SQL Server执行):
CREATE VIEW vw_StudentScoreReport AS SELECT s.StudentNo, s.StudentName, e.ExamName, es.StartTime, es.EndTime, COUNT(sa.AnswerID) AS TotalQuestions, SUM(CAST(sa.IsCorrect AS INT)) AS CorrectCount, CAST(SUM(CAST(sa.IsCorrect AS INT)) AS FLOAT) * 100 / COUNT(sa.AnswerID) AS ScorePercent FROM ExamSession es WITH(NOLOCK) JOIN Student s WITH(NOLOCK) ON es.StudentID = s.StudentID JOIN Exam e WITH(NOLOCK) ON es.ExamID = e.ExamID LEFT JOIN StudentAnswer sa WITH(NOLOCK) ON es.SessionID = sa.SessionID GROUP BY s.StudentNo, s.StudentName, e.ExamName, es.StartTime, es.EndTime;4.2 论文中必须分析的执行计划瓶颈:Nested Loops vs Hash Match
在论文“系统性能分析”章节,不能只贴SQL Server Management Studio的执行计划截图,要指出具体算子问题。以usp_SubmitAnswer存储过程为例(源码中已提供),其原始写法为:
-- ❌ 低效写法:触发Nested Loops(逐行扫描) INSERT INTO StudentAnswer (SessionID, SnapshotID, SelectedOption, IsCorrect) SELECT @SessionID, qs.SnapshotID, @SelectedOption, CASE WHEN @SelectedOption = qs.CorrectAnswer THEN 1 ELSE 0 END FROM ExamPaperSnapshot qs WHERE qs.SessionID = @SessionID AND qs.QuestionID = @QuestionID;问题分析:当ExamPaperSnapshot表有10万行时,qs.SessionID = @SessionID条件无索引,SQL Server被迫对每行执行qs.QuestionID = @QuestionID判断,形成Nested Loops,CPU占用飙升。
解决方案:在ExamPaperSnapshot表上创建复合索引:
CREATE NONCLUSTERED INDEX IX_ExamPaperSnapshot_Session_Question ON ExamPaperSnapshot (SessionID, QuestionID) INCLUDE (SnapshotID, CorrectAnswer);提示:
INCLUDE子句将SnapshotID和CorrectAnswer加入索引叶节点,使查询无需回表(Key Lookup),将执行计划从Nested Loops降级为Index Seek,耗时从86ms降至12ms。
4.3 开题报告答辩必答:如何证明你的系统不是“假并发”?
评审老师会问:“你演示时只开了3个浏览器标签,怎么证明支持300人?”——回答必须包含两层证据:
- 压力测试工具链:使用Apache JMeter配置线程组(300线程,Ramp-up Period=60秒),HTTP请求指向
/Student/Exam.aspx?examid=101,监听器选“聚合报告”; - IIS监控指标:在Windows性能监视器中添加计数器:
Web Service(_Total)\Current Connections(应稳定在300±10)SQLServer:SQL Statistics\Batch Requests/sec(应≥150,表明SQL Server未成为瓶颈).NET CLR Memory(_Global_)\# Bytes in all Heaps(应无陡升,证明无内存泄漏)
将JMeter报告截图与性能监视器曲线图放入开题报告附录,比任何文字描述都有力。
5. IIS部署与生产环境避坑指南:从VS调试到教务处验收
5.1 必须修改的4个IIS设置(否则考试中途崩溃)
在IIS管理器中,右键站点→“高级设置”,重点调整以下参数:
| 设置项 | 推荐值 | 不修改的后果 |
|---|---|---|
| 闲置超时(分钟) | 0(禁用) | 学生答题超20分钟,IIS回收AppPool,Session丢失,试卷清空 |
| 队列长度 | 65535 | 默认1000,300并发请求排队溢出,返回HTTP 503 |
| 启用32位应用程序 | True | 若SQL Server驱动为32位(常见于旧版教务系统),设为False则连接失败 |
| 匿名身份验证 | Disabled | 必须启用Windows身份验证,否则HttpContext.Current.User为空 |
提示:修改后需重启AppPool,而非仅重启IIS服务。在命令行执行
iisreset /noforce比图形界面更彻底。
5.2 解决“发布后样式错乱”的终极方案:BundleConfig.cs重写
VS自带的BundleConfig在发布时会合并CSS/JS,但考试系统要求:
anti-cheat.js必须单独加载(防学生通过F12禁用);bootstrap.css需保留原始注释(教务处审计要求可追溯版本)。
在App_Start\BundleConfig.cs中,删除原有bundles.Add(...),替换为:
public static void RegisterBundles(BundleCollection bundles) { // 强制不合并防作弊脚本 bundles.Add(new ScriptBundle("~/bundles/anti-cheat").Include( "~/Scripts/anti-cheat.js")); // Bootstrap保持独立,禁用minify var bootstrapBundle = new StyleBundle("~/Content/bootstrap"); bootstrapBundle.Include("~/Content/bootstrap.css"); bootstrapBundle.Transforms.Clear(); // 关键:清除默认压缩 bundles.Add(bootstrapBundle); // 其他脚本正常合并 bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); }然后在Exam.aspx中按顺序引用:
<%: Styles.Render("~/Content/bootstrap") %> <%: Scripts.Render("~/bundles/anti-cheat") %>5.3 教务处验收时的3个现场演示技巧
演示防切屏有效性:
- 在Chrome打开考试页面 → 按
Ctrl+Shift+I打开开发者工具 → 切换到Application标签 →Clear storage→Clear site data; - 此时再切出标签页,
visibilitychange事件仍会触发(因JS已注入Response流,不依赖缓存)。
- 在Chrome打开考试页面 → 按
演示成绩实时归档:
- 在SQL Server中执行
SELECT TOP 10 * FROM vw_StudentScoreReport ORDER BY ScorePercent DESC; - 让学生现场交卷,刷新该查询,新记录立即出现(证明
StudentAnswer触发器或应用层事务已生效)。
- 在SQL Server中执行
演示数据隔离性:
- 用两个不同学号登录 → 同时进入同一场考试 → 分别作答不同题目 → 提交后各自查看成绩单;
- 在数据库中验证:
SELECT SessionID, COUNT(*) FROM StudentAnswer GROUP BY SessionID,结果应为两行且数值不同。
当教务老师看到SessionID字段在每张成绩单下方清晰显示,且与ExamSession表记录完全匹配时,会立刻理解系统的数据治理能力——这比任何架构图都直观。
本文还有配套的精品资源,点击获取