news 2026/9/13 0:46:27

ASP.NET MVC在线考试系统源码解析:路由、控制器与计分全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ASP.NET MVC在线考试系统源码解析:路由、控制器与计分全流程

简介:基于ASP.NET MVC的简易在线考试系统是一份面向计算机相关专业学生、教师及企业开发者的学习与实战资源,适合用作课程设计、毕业设计或项目初期立项演示。项目代码经过运行验证,功能完整可靠。压缩包共155个文件、约532KB,主要包含68个cshtml视图模板、45个cs控制器与业务逻辑、13个js交互脚本,以及配置文件、SQL数据库脚本和字体图标等,覆盖了MVC分层架构中视图、控制器、模型与配置的典型组成。通过Visual Studio可直接打开解决方案运行,便于对照学习用户登录、考试管理、题目管理等核心模块的编码方式。当前已有186人浏览学习,具备一定的参考价值。下载后既可系统梳理ASP.NET MVC开发流程,也能在此基础上扩展功能,满足毕设、课设或实际项目二次开发需求。

1. 一个基于 ASP.NET MVC 的简易在线考试系统,先拆给你看

打开压缩包的第一眼,没有一堆 .aspx 页面文件,而是 Global.asax、Web.config、UserController.cs、ExamController.cs、ExamViewModel.cs 这些文件把整套考试系统撑了起来。这就是 ASP.NET MVC 与 Web Forms 最直观的差别——请求先进路由,再进控制器,最后才轮到视图。这个简易系统麻雀虽小,但用户登录、试题加载、答案提交、自动计分的完整闭环都在里面,适合刚学完 C# 基础、想搞懂 MVC 设计模式真实跑法的开发者,也适合做课程设计时快速判断“这套代码能不能改成自己的毕设”。下载之后如果没有 README,建议按 Global.asax → Web.config → UserController → ExamController → ExamViewModel 的顺序拆代码,正好对应一次请求从入口到绑定的完整链路。

2. 路由与配置链路:Global.asax、Web.config、packages.config 协同工作的底层逻辑

2.1 为什么启动顺序先看 Global.asax

ASP.NET MVC 不是靠物理 URL 去找页面,而是靠 RouteTable 里注册的路由规则来匹配 controller/action。应用启动时,Global.asax 里的 Application_Start 只执行一次,负责注册路由、初始化数据库、绑定依赖注入容器等。这个项目里的 Global.asax 虽然只有短短几行,但整个系统的入口就在这里。下面这段代码就是最常见的默认路由注册:

// Global.asax 关键代码 protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteTable.Routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "User", action = "Login", id = UrlParameter.Optional } ); }

这里的{controller}/{action}/{id}是占位符。默认请求/时,由于 defaults 指定了 controller=User、action=Login,所以会直接进入 UserController 的 Login 方法。如果访问/Exam/Index,则 id 为空。MapRoute 的 name 参数只是路由名,不参与匹配,但后续用 Url.RouteUrl 生成链接时会用到。改动 defaults 里的 controller 或 action,就能调整整个应用的首屏入口,这也是排查 404 时最先该看的地方。

顺带说一句,如果你是先接触 Spring MVC 再回来看这个项目,会发现两者的路由心智模型基本一致:Spring 的 @RequestMapping 在 ASP.NET MVC 里对应着 RouteTable 加 Action 方法上的路由特性。传统 ASP.NET MVC 4/5 采用集中式路由,而 ASP.NET Core 更推荐用 Attribute Routing。简单项目用集中式路由足够了,但若你打算以后迁移到 Core,可以把 controller 和 action 上的路由特性提前用起来。

2.2 Web.config:连接字符串、认证与模块注册

Web.config 承担运行时的上下文配置。简易在线考试系统的 Web.config 通常至少包含三块:connectionStrings 存放数据库连接,system.web 下的 compilation 控制调试状态和框架版本,authentication 决定未登录用户跳转到哪个页面。下面是一个典型片段:

<connectionStrings> <add name="OnlineExamDb" connectionString="Data Source=.;Initial Catalog=OnlineExam;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <compilation debug="true" targetFramework="4.7.2" /> <authentication mode="Forms"> <forms loginUrl="~/User/Login" timeout="60" /> </authentication> </system.web>

这里 connectionString 里的Data Source=.指的是本机 SQL Server,如果你用的是 SQL Express,要改成.\SQLEXPRESS;数据库名 OnlineExam 需要先在 SQL Server 里创建,否则页面在访问用户表时会直接抛异常。Forms 认证设置成登录页统一跳转到/User/Login,timeout 60 表示不操作 60 分钟后登录态失效。

需要注意,debug="true"只在开发阶段使用。它会让编译器生成更多调试符号,并且页面异常时会显示详细堆栈。线上环境必须置为 false,否则访问者能看到你的源码路径和内部变量名,也会拖慢并发响应。Web.config 里还有一个容易忽略的节点是<httpRuntime targetFramework="4.7.2" />,它和编译节点必须匹配实际安装的 .NET Framework 版本,版本不一致会导致“无法加载程序集”的错误。

2.3 packages.config:依赖的版本锁

packages.config 记录的是 NuGet 安装的每个包的精确版本。MVC 项目的 packages.config 里至少能看到 Microsoft.AspNet.Mvc 和 Microsoft.AspNet.Web.Optimization。当你在另一台机器上还原时,NuGet 会按这个文件里的版本号去下载,而不是随便拉最新版,否则容易引发编译器兼容问题。

下表对比了三个配置文件在不同阶段的作用:

| 配置文件 | 生命周期阶段 | 主要职责 | 调试时最常改什么 | | Global.asax | 启动时 | 路由注册、全局事件处理 | MapRoute 参数 | | Web.config | 运行时 | 数据库连接、认证、模块 | connectionString、debug | | packages.config | 还原依赖时 | 锁定 NuGet 包版本 | 包版本号 |

如果你在项目里找不到数据库文件,先别急着翻 Models 目录,八成是连接字符串里的 Initial Catalog 和实际库名不一致。简易系统很多直接使用 MDF 文件,此时连接写法是AttachDbFilename=|DataDirectory|\Exam.mdf,这种写法在部署到服务器时容易出现权限问题,建议改成标准 SQL Server 连接。还原包时若发现某个包版本不存在,打开 packages.config,把版本号改成本地 NuGet 缓存里已有的版本,再重新编译即可。

3. UserController 到用户体系:登录注册与 [Authorize] 过滤器的实现细节

3.1 一个典型 UserController 的 Action 划分

UserController 在在线考试系统里管的不只是考试账号,还包括注册、登录、登出和“当前用户是否已登录”的校验逻辑。从文件命名也能看出来,这个控制器把用户相关的所有 HTTP 入口集中到了一起,而不是拆成 AuthController、AccountController 两个类,简单项目这样做反而更好找。下面是一个基于 FormCollection 的登录 Action:

[HttpPost] [ValidateAntiForgeryToken] public ActionResult Login(FormCollection form) { string userName = form["UserName"]; string password = form["Password"]; if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) { ModelState.AddModelError("", "用户名和密码不能为空"); return View(); } // 简易项目常见做法:查内存集合或数据库 // 这里以固定账号示意 if (userName == "admin" && password == "123456") { FormsAuthentication.SetAuthCookie(userName, false); return RedirectToAction("Index", "Exam"); } ModelState.AddModelError("", "用户名或密码错误"); return View(); }

这里的 FormCollection 把表单里所有 name 属性收进来,灵活但容易拼错字符串。如果你手头这个资源用的是类似写法,改用户名时一定要同步检查视图里的 input name 是否完全一致。正规一点的做法是定义一个 LoginViewModel,用强类型绑定,但简易系统里为了少写一个类,用 FormCollection 也很普遍。SetAuthCookie的作用是往客户端写一个加密票据,之后 [Authorize] 过滤器通过读取这个票据来判断用户是否通过验证。它是基于 FormsAuthenticationModule 的,受 Web.config 里 authentication mode="Forms" 控制。

3.2 [Authorize] 与登录态校验

在需要登录才能访问的方法或控制器上,只要加一行[Authorize],未登录用户就会被自动重定向到登录页。比如登出方法:

[Authorize] public ActionResult LogOff() { FormsAuthentication.SignOut(); return RedirectToAction("Login", "User"); }

如果没有 [Authorize],即使登录失败,用户也能通过直接输入/User/LogOff来访问这个方法。加了过滤器后,未登录请求会被拦截并重定向到 Web.config 里配置的 loginUrl。放在控制器类名上表示类内所有 Action 都生效,放在方法上只对这单个 Action 生效。你还可以用[Authorize(Roles = "admin")]做角色隔离,但简易系统通常用不到。

使用 FormsAuthentication 时要注意:票据默认会被写入 Cookie,如果站点部署在 HTTPS 下,建议给SetAuthCookie传入createPersistentCookie参数后,同时在 Web.config 里配置<forms requireSSL="true" />,防止票据被明文截获。课程设计不强制要求,但面试被问到安全加固时,这算是加分项。

3.3 注册逻辑:密码不能明文存

简单考试系统不一定要上 Identity,但至少不要明文存密码。常见做法是取 MD5 或 SHA256 哈希后存库。下面这个函数可以直接放在 UserController 里私有方法:

private string GetMd5Hash(string input) { using (var md5 = System.Security.Cryptography.MD5.Create()) { var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); var sb = new StringBuilder(); foreach (var b in bytes) sb.Append(b.ToString("x2")); return sb.ToString(); } }

调用时GetMd5Hash(password)得到 32 位十六进制字符串。需要注意,MD5 已被认为不适合做高强度安全存储,但作为课程设计或入门演示,性能足够;如果是正式上线的考试系统,建议换用 BCrypt 或 ASP.NET Identity 自带的 PasswordHasher。注册时还要处理重名问题:先查一下数据库里是否已存在相同的 UserName,存在就返回ModelState.AddModelError("UserName", "该用户名已被占用"),而不是依赖数据库抛唯一约束异常。

下表列出了 UserController 里最常用的 Action 设计:

| Action 名 | HTTP 动词 | 功能 | 返回 | | Login | GET/POST | 登录 | View / RedirectToAction | | Register | GET/POST | 注册新用户 | View / RedirectToAction | | LogOff | POST | 登出 | RedirectToAction |

实际项目中,LogOff 最好声明为[HttpPost],避免用户被恶意链接强制登出。整个模块保护起来的方法也很简单,在 ExamController 类名上方加一行[Authorize],就能让考试模块只对登录用户开放。这也是 MVC 过滤器最直接的应用,解释了为什么资源里会有 UserController 又会有 ExamController——一个管身份,一个管业务。

4. ExamController 与 ExamViewModel:考试答题、状态保持和计分的 MVC 玩法

4.1 ViewModel 到底解决什么问题

在线考试系统的页面要显示的不仅仅是数据库里的一张表,而是“题目集合 + 用户答案 + 得分”的组合数据结构。这种跨实体、按视图裁剪的数据,正好是 ViewModel 的用武之地。ExamViewModel.cs 就是把考试页所需的全部字段打包,让 Controller 和 View 之间传递的是一个可测试的强类型对象,而不是 ViewBag 里的散装数据。下面是一个典型的定义:

public class ExamViewModel { public string ExamName { get; set; } // 试卷名 public List<Question> Questions { get; set; } // 题目列表 public List<string> UserAnswers { get; set; } // 用户提交的答案 public int Score { get; set; } // 总分 public bool IsSubmitted { get; set; } // 是否已交卷 } public class Question { public int Id { get; set; } public string Title { get; set; } // 题干 public string OptionA { get; set; } public string OptionB { get; set; } public string OptionC { get; set; } public string OptionD { get; set; } public string CorrectAnswer { get; set; } // 正确答案 }

UserAnswers的索引和题目顺序一一对应,UserAnswers[0]表示第一题的答案。这样的设计让前端表单可以非常规整地提交一个数组。你可能注意到,Question里没有放 IsChecked 之类的视图状态字段,因为用户是否选中,应通过 UserAnswers 在 Action 里计算,而不该污染题目本身。

4.2 用模型绑定接收表单数组

当用户点击交卷按钮,表单会 POST 到 ExamController 的 Submit 方法。因为表单控件的 name 是UserAnswers[i],ASP.NET MVC 的模型绑定器会自动把它们组装成List<string>。下面的 Action 展示了完整处理:

[HttpPost] public ActionResult Submit(ExamViewModel submitted) { var vm = new ExamViewModel(); vm.ExamName = "ASP.NET MVC 基础测试"; vm.Questions = LoadQuestions(); // 重新从库里取题目 vm.UserAnswers = submitted.UserAnswers; vm.Score = ComputeScore(vm.Questions, vm.UserAnswers); vm.IsSubmitted = true; return View("Result", vm); }

为什么需要重新加载题目?因为表单不会把正确答案暴露给用户,必须在服务端根据题目 ID 重新查一次,避免用户改 HTML 作弊。如果用户直接伪造 POST 请求,只提交自己想要的答案,服务端因重新加载题目,也能保证正确答案不会被恶意篡改。

对应的 Razor 视图片段:

@model ExamViewModel @using (Html.BeginForm("Submit", "Exam", FormMethod.Post)) { for (int i = 0; i < Model.Questions.Count; i++) { <p>@(i + 1). @Model.Questions[i].Title</p> <label>@Html.RadioButton($"UserAnswers[{i}]", "A") @Model.Questions[i].OptionA</label> <label>@Html.RadioButton($"UserAnswers[{i}]", "B") @Model.Questions[i].OptionB</label> <label>@Html.RadioButton($"UserAnswers[{i}]", "C") @Model.Questions[i].OptionC</label> <label>@Html.RadioButton($"UserAnswers[{i}]", "D") @Model.Questions[i].OptionD</label> } <button type="submit">交卷</button> }

注意,Html.RadioButton的第一个参数是 name,第二个参数是 value。由于 name 相同,同一组单选按钮只能选一个;不同的i对应不同组。这里使用了$"UserAnswers[{i}]"字符串插值,编译后等价于普通字符串拼接。

计分逻辑可以写成独立的私有方法:

private int ComputeScore(List<Question> questions, List<string> userAnswers) { int score = 0; for (int i = 0; i < questions.Count; i++) { if (string.Equals(questions[i].CorrectAnswer, userAnswers?[i], StringComparison.OrdinalIgnoreCase)) { score += 10; // 每题10分 } } return score; }

userAnswers?[i]中的?是空条件运算符,防止用户漏答导致索引越界。如果题目数量超过 100,建议把每题分值改成从设计题目时传入,或根据题目难度设定权重,避免所有题平均分配导致区分度不足。

4.3 Session 与答题进度保持

HTTP 是无状态的,用户做到一半刷新页面,所有答案就丢了。简易考试系统里最常见的方案是把 ExamViewModel 塞进 Session。在加载试题时存一下:

[HttpGet] public ActionResult Index() { var vm = new ExamViewModel(); vm.ExamName = "ASP.NET MVC 基础测试"; vm.Questions = LoadQuestions(); Session["ExamProgress"] = vm; // 保存试卷快照 return View(vm); }

提交时再读:

var saved = Session["ExamProgress"] as ExamViewModel; if (saved == null) return RedirectToAction("Index");

这里要注意,Session 默认存在 IIS 进程内存中,应用池回收或服务器重启都会丢失。如果考试人数较多,把 Session 持久化到 SQL Server 或 Redis 是更稳的做法,但简易课设完全够用。另外,Session 的 Key 别用魔法字符串满天飞,建议定义成常量,防止多控制器之间拼写不一致。

下表列出了表单绑定与 ViewModel 属性的对照关系:

| HTML name | ViewModel 属性 | 说明 | | UserAnswers[0] | UserAnswers[0] | 第一题答案 | | UserAnswers[1] | UserAnswers[1] | 第二题答案 | | ExamName | ExamName | 隐藏字段或只读展示 |

如果想要支持多选题,可以把UserAnswersList<string>改成List<List<string>>,或者用逗号分隔字符串存到一个字段里。前端改成 checkbox,用"UserAnswers[" + i + "][]"作为 name,模型绑定器也能正确收集到集合,但解析时要额外做字符串拆分。

5. 把项目跑起来:applicationhost.config、IIS Express 与 Release 配置的验证技巧

5.1 定位 applicationhost.config 并启动站点

在 Visual Studio 中按 F5 时,IIS Express 会读取项目里隐藏的 .vs 目录生成的 applicationhost.config。如果目录结构变化导致站点无法启动,可以手动指定配置文件运行。下面这条命令在无 Visual Studio 的环境下也适用:

"C:\Program Files\IIS Express\iisexpress.exe" /config:"D:\OnlineExam\.vs\OnlineExam\config\applicationhost.config"

启动后控制台会列出当前站点名与物理路径,看到 Successfully registered 说明站点已经加载。项目若用到 SQL Server 本地库,注意 IIS Express 工作进程身份是当前用户,默认有权限访问 LocalDB;如果连的是 SQL Server 的 Windows 认证实例,一般也没问题。

5.2 端口冲突与 binding 修改

同一台机器上开过多个网站时,很容易碰到端口被占用。IIS Express 报错信息里会给出被占用的端口号。此时可以直接改 applicationhost.config 里的 site 节点:

<bindings> <binding protocol="http" bindingInformation="*:8080:localhost" /> </bindings>

把 8080 换成 5200 之类的空闲端口。保存后重新执行 /config 命令,访问http://localhost:5200/User/Login即可。

5.3 Release 配置如何清理调试信息

Web.Release.config 和 Web.Debug.config 是 MSBuild 发布时的转换脚本。在不使用发布向导、直接复制代码到服务器时,最需要关注的是编译节点里的 debug 属性。Debug 模式下编译体积大、执行路径中有额外日志,长期运行最好关掉。可以在 Web.Release.config 里写:

<system.web> <compilation xdt:Transform="RemoveAttributes(debug)" /> </system.web>

这样发布后 Web.config 中的debug="true"会被自动移除,而本地 Debug 运行不受影响。

5.4 用 curl 和浏览器做快速验证

启动站点后,先访问默认路由看是否跳转到登录页:

curl -I http://localhost:5200/User/Login

正常返回 200 后,再继续访问考试首页。如果某个 Action 返回 404,先看 Global.asax 里的路由注册,再看控制器类名是否以 Controller 结尾;如果 500,马上打开 Web.config 设置:

<system.web> <customErrors mode="Off" /> </system.web>

然后重启 IIS Express,让异常信息直接渲染在页面上。根据堆栈里的文件名和行号,能快速定位是数据库连接失败还是空引用。修复后记得把 customErrors 改成 RemoteOnly,避免线上泄露内部信息。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 0:35:22

基于 Python 的宠物食品销售数据分析可视化系统的设计与实现

1. 引言随着宠物经济的快速发展&#xff0c;宠物食品市场规模持续扩大&#xff0c;消费者对宠物食品的品质、营养和品牌关注度不断提升。面对海量的销售数据&#xff0c;如何高效地完成数据采集、清洗、分析与可视化展示&#xff0c;成为企业制定营销策略、优化产品结构的重要基…

作者头像 李华
网站建设 2026/9/12 23:56:14

PyQt5+OpenCV打造监控视频播放器:UI转换、帧率控制与异常处理

简介&#xff1a;基于Python实现本地视频播放的完整示例项目&#xff0c;面向需要快速掌握PyQt5/PySide2桌面界面开发与OpenCV视频处理的Python学习者与开发者。项目使用Qt Designer可视化设计主界面&#xff0c;生成兼容的.ui布局文件&#xff0c;再通过OpenCV的VideoCapture读…

作者头像 李华
网站建设 2026/9/12 23:54:06

Remotion 如何为视频添加 AI 自动生成的字幕?

Remotion 如何为视频添加 AI 自动生成的字幕&#xff1f; 【免费下载链接】remotion &#x1f3a5; Make videos programmatically with React 项目地址: https://gitcode.com/GitHub_Trending/re/remotion 如果你的 Remotion 项目中有一段带人声的视频&#xff0c;想通…

作者头像 李华
网站建设 2026/9/12 23:52:05

Arm-2D:面向Cortex-M的确定性2D图形加速框架

1. 为什么在Cortex-M上做2D图形加速&#xff0c;Arm-2D不是“锦上添花”&#xff0c;而是“生死线” 你有没有遇到过这样的场景&#xff1a;在一款带320240 OLED屏的工业HMI设备上&#xff0c;客户突然要求增加一个动态波形图——不是静态图标&#xff0c;是每50ms刷新一次、带…

作者头像 李华
网站建设 2026/9/12 23:50:55

极空间NAS自建Typecho博客:从云服务器迁移到Docker部署实践

1. 为什么我把博客从云服务器搬回了家里的极空间 NAS前阵子我的云服务器又到期了&#xff0c;续费价格直接翻了一倍。这台服务器上只跑着一个 Typecho 博客&#xff0c;平时流量不大&#xff0c;但我每年要为一台几乎闲置的机器付几百块续费。想想觉得挺不值的&#xff0c;干脆…

作者头像 李华