1. 在线考试系统到底在解决什么问题
先说个真实场景。我去年帮一所职业院校做过一次校内技能竞赛的在线考核,几百号人同时登录系统答题,考到一半服务器CPU飙到90%多,数据库连接池被打满,页面转圈圈,学生急得在群里刷屏。那次之后我算是彻底想明白了一件事:在线考试系统最难的不是"能不能考",而是"很多人同时考的时候还稳不稳"。
这个基于 .NET Core MVC 架构的在线考试系统项目,本质上就是在解决这类问题。它不是一个花哨的Demo,而是一个能覆盖"出题 -> 组卷 -> 发布考试 -> 考生答题 -> 自动判分 -> 成绩统计"完整闭环的管理系统。市面上这类源码项目很多,但大部分要么是单体老架构,连个依赖注入都写得稀碎,要么就是过度设计,上来就微服务、消息队列,结果一个班级考试都跑不顺。
这个项目的定位很清晰:用 .NET Core MVC 这套成熟的技术栈,把在线考试里最核心的几件事做扎实。适合谁看?两类人。一类是刚接触 .NET Core 的开发者,想找一个结构清晰、能跑通全流程的实战项目来练手;另一类是学校、培训机构、企业内部需要做考核系统但不想从零开始造轮子的朋友,拿这套源码做二次开发,比自己瞎写省太多事。
我花了两周时间把这个项目完整过了一遍,从表结构到控制器再到前端页面,每一步都实际跑过、改过、踩过坑。下面把这些经验按模块拆开讲,尽量说人话,把关键的设计思路和容易被坑的地方都点出来。
2. 为什么偏偏是 .NET Core MVC 这套组合
先别急着看代码,得先搞清楚选型逻辑。很多人一听说"在线考试系统",第一反应是 Java 的 Spring Boot,或者干脆用 PHP 一把梭。但 .NET Core MVC 在这类业务系统里有它非常舒服的地方。
2.1 .NET Core 比起老 .NET Framework 强在哪
老一代 ASP.NET WebForms 时代,页面和后端逻辑耦合得很死,一个页面对应一个 .aspx.cs,想做前后端分离或者单元测试都费劲。而 .NET Core 从架构上就是重做的,依赖注入、中间件管道、跨平台部署这些现代 Web 开发的基本盘都在。对于考试系统这种典型业务系统来说,.NET Core 的启动速度、内存占用、整体性能表现都明显比 Framework 版本好,尤其在高并发场景下差距更明显。
我用 4核8G 的云服务器做过一个简单压测,同样的业务逻辑,.NET Core 版本每秒能处理的事务数大概是 Framework 版本的两倍多,内存占用还低一截。对于百人级、千人级的同时在线考试场景,.NET Core 完全扛得住。
2.2 MVC 模式在这个项目里的具体分工
MVC 把考试系统拆成三层各管各的,这个分层在日常维护时特别舒服:
| 层次 | 职责 | 在考试系统中的具体体现 |
|---|---|---|
| Model(模型) | 数据结构和业务规则 | 试题实体、试卷实体、考生答题记录、成绩单等 |
| View(视图) | 页面展示 | 考生答题页面、后台题库管理页、成绩统计报表页 |
| Controller(控制器) | 接收请求、调度逻辑 | 接收提交答案的请求、调用判分服务、返回结果 |
举个考试中最高频的操作——考生提交一道单选题的答案。请求先打到ExamController的SubmitAnswer方法,控制器把前端传来的选项ID封装成答题记录模型,调用判分逻辑(这道题的正确答案是什么、选对了没有),然后返回 JSON 给前端。整个链路清晰,出问题的时候顺着 Controller -> Service -> Repository 一路查下去就行。
2.3 为什么不选前后端完全分离
现在很多新项目一上来就是 Vue/React + WebAPI 完全分离,但这个考试系统没有这么干。原因很实际:考试系统的页面交互虽然多,但大多是表单提交、列表展示、简单的局部刷新,用 MVC 自带的 Razor 视图引擎加一点 jQuery / Ajax 就能很好地完成。完全分离意味着要维护两套工程、处理跨域、还得设计一套接口鉴权,工程量翻倍,对这类内部业务系统来说是过度设计。
当然,这套源码也留了余地。API 控制器是单独分层的,以后想拆出独立的前端项目,后端接口可以直接复用,不用推倒重来。
3. 核心功能模块拆解:从题库到成绩单的全链路
这个考试系统的功能模块不算多,但每个模块都踩在真实需求上。我逐个过一遍,顺便说说每个模块在设计上的关键决策。
3.1 用户与权限:三种角色,各管各的
系统里有三种核心角色:管理员、教师、考生。这里没有搞 RBAC(基于角色的访问控制)那一套复杂的东西,而是用了最简单实用的方式——Role字段区分用户类型,配合Session或Claim记录登录状态,控制器层用自定义AuthorizeFilter做访问控制。
为什么要这么设计?因为考试系统的角色边界非常清晰,不会有"某个用户既是A角色又是B角色"的复杂需求。过度设计成细粒度权限反而让代码变得难懂。
具体实现上,登录接口验证用户名密码后,会把用户ID、角色、姓名写进ClaimsIdentity,然后生成加密 Cookie。后续每个请求通过[Authorize(Roles = "Teacher")]这种特性标注就能限制访问。考生进不了后台管理页,教师改不了系统设置,管理员拥有全部权限。
3.2 题库管理:题型多样才算合格的题库
题库模块支持单选题、多选题、判断题、填空题、简答题这五种常见题型。这基本覆盖了绝大多数考试场景。
题库管理的核心是试题实体设计。Question表我看了下,字段设计得挺讲究:
QuestionType:题型枚举,决定前端渲染什么控件、判分逻辑走哪条分支Difficulty:难度等级(1-5),用于后面的智能组卷OptionJson:选项内容,用 JSON 字符串存储。这个设计很聪明,因为不同题型的选项结构不一样,用关系表存的话要拆好几张表,JSON 一份搞定Answer:标准答案,选择题存选项序号,判断题存对错,简答题存要点关键词Analysis:答案解析,考完试卷分析时展示给学生看
题库的增删改查没什么稀奇的,但有两个细节值得表扬。第一是支持批量导入,Excel 模板下载后填好题目就能一次性导入几百道题,不然人工一道题一道题录,教师能疯掉。第二是题目支持按知识点、难度、题型组合查询,为组卷提供了筛选基础。
3.3 试卷管理:两种组卷方式满足不同场景
组卷是这个系统最核心的业务之一。它提供了两种模式:
手工组卷:教师从题库里逐题挑选,加入试卷,设置每题分数。适合小测验、单元测试这种对题目有精确把控的场景。实现上就是一个试卷和试题的关联表,记录了每道题在试卷里的顺序和分值。
自动组卷:教师设定试卷结构,比如"单选题10道,每题3分;多选题5道,每题4分;判断题10道,每题2分;简答题2道,每题10分",再从题库里按难度比例随机抽题。这里用了一个按难度分布抽题的算法,核心逻辑是:先统计题库里各难度题目的数量,然后按试卷要求的难度比例,用随机数从对应难度的题目里抽取,保证试卷难度分布符合预期。
自动组卷的实现代码大致长这样:
public List<Question> AutoGeneratePaper(int easyCount, int mediumCount, int hardCount, int questionType) { var easyQuestions = _questionRepository.GetQuestionsByDifficultyAndType(1, questionType) .OrderBy(q => Guid.NewGuid()).Take(easyCount).ToList(); var mediumQuestions = _questionRepository.GetQuestionsByDifficultyAndType(3, questionType) .OrderBy(q => Guid.NewGuid()).Take(mediumCount).ToList(); var hardQuestions = _questionRepository.GetQuestionsByDifficultyAndType(5, questionType) .OrderBy(q => Guid.NewGuid()).Take(hardCount).ToList(); return easyQuestions.Concat(mediumQuestions).Concat(hardQuestions).ToList(); }Guid.NewGuid()做随机排序是个小技巧,比Random类更均匀,也够简单。
3.4 考试发布与在线答题:最考验细节的模块
创建一场考试需要设置:考试名称、关联试卷、考试开始时间、结束时间、考试时长、允许参加考试的班级或考生列表。
在线答题这块有几个设计值得单独说:
第一是计时机制。不是用前端倒计时,而是后端在考生开始考试时记录StartTime,每次提交答案时校验当前时间是否超过了StartTime + Duration。为什么?因为前端倒计时可以被篡改或者绕过,而后端时间校验是硬性的。当然,前端也有倒计时提醒,只是它的角色是"提示",不是"约束"。
第二是答题状态保存。考生做完一道题就通过 Ajax 保存一道,不是等交卷时一次性提交。这样即使中途断网、浏览器崩溃,已经保存的答案都还在。保存接口接收题目ID和答案内容,写入ExamAnswerRecord表。
第三是交卷处理。交卷时要做三件事:校验时间没有超时、校验未提交的题目(如果有考生遗漏直接交卷,系统要自动把空白答案补上)、触发自动判分。
3.5 自动判分与成绩统计:简单题目机器判,复杂题目人来看
自动判分是考试系统省时省力的关键。
单选题、多选题、判断题:直接比对答案字符串,一样给分,不一样零分。多选题这里有个增强逻辑——如果试卷设置了"少选给一半分",那答案包含在正确答案里但没选全,就给一半分。这个在很多严肃考试里都有需求,源码里做到了。
填空题:简单匹配答案关键词。题库里可以配置多个可接受答案,用竖线分隔,比如"ASP.NET Core|.NET Core",匹配上任何一个都给分。
简答题:机器判分只能做关键词匹配,所以源码里的处理是——超出设定字数阈值且含有关键词的,先自动判为待人工复核,由教师在后台手动给分。这种"机器初判 + 人工复核"的模式,在大规模考试里能省掉90%的批改工作量。
成绩模块就比较常规了:考完自动生成成绩单,教师可以按班级导出 Excel,按题型统计得分率。得分率这个数据很有用,能一眼看出哪道题全班都答错,说明这道题要么有歧义,要么老师没讲透这个知识点。
4. 数据库设计:核心表结构和关联关系
代码看多了,你会发现这类业务系统的价值一半都在表结构设计里。我实际对照着建库跑了一遍,把核心表拆给你看。
4.1 用户和角色相关表
用户表User主要字段:Id(主键)、UserName、PasswordHash、RealName、Role(1管理员、2教师、3考生)、ClassId(班级外键,用于考生关联班级)。
班级表Class很简单:Id、ClassName、GradeName。别小看这张表,它是后面按班级发布考试、按班级统计成绩的基础。
4.2 题库相关表
题目表Question的核心字段我上面已经列过。这里有一个很多新手容易纠结的问题:为什么选项要用 JSON 而不是单独建一张选项表?
我个人的看法是,这个项目选择 JSON 是对的。原因有三:一是选项只在出题时设置、答题时展示,不会单独去查询"哪些题用了这个选项";二是不同题型的选项结构差异大(选择题是ABCD文本,判断题只有对错,填空题根本没有选项),一张通用表要么冗余要么稀疏;三是减少表数量,让代码层映射更简单。缺点是统计类查询(比如"哪些题目的选项包含某关键词")会稍微麻烦,但考试系统根本没这种需求。
题目和课程/知识点关联表QuestionKnowledgePoint:QuestionId、KnowledgePointId,支持一题对应多个知识点。
4.3 试卷相关表
试卷表Paper:Id、Title、TotalScore、Duration(考试时长)、CreateUserId、Status(草稿/已发布)。
试卷题目关联表PaperQuestion:Id、PaperId、QuestionId、Score(该题分值)、SortOrder(试卷内排序)。这张表是组卷功能写入的核心,手工组卷和自动组卷最终都是往这张表里插数据。
4.4 考试与答题记录表
考试表Exam:Id、Title、PaperId、StartTime、EndTime、ExamDuration、ExamStatus(未开始/进行中/已结束)、CreateUserId。
考试班级关联表ExamClass:ExamId、ClassId,一场考试可以指定多个班级参加。
考生考试记录表ExamRecord:Id、ExamId、StudentId、StartTime、SubmitTime、Score、Status(考试中/已交卷/超时自动交卷)。
答题明细表ExamAnswerRecord:Id、ExamRecordId、QuestionId、StudentAnswer、IsCorrect、GainedScore。
这几个表的关系链条是:Exam关联Paper和Class,考生点击开始考试时生成ExamRecord,每答一题写入ExamAnswerRecord,交卷时按PaperQuestion里的分值汇总到ExamRecord.Score。
这套表设计是典型的"够用但不过度":没有搞分库分表,没有搞读写分离,纯粹的单库多表就能支撑起一个中小规模的考试系统。等真到了几千人同时交卷打满单库的时候,再谈架构升级也不迟。
5. 从代码看两个关键业务流程的实现
表结构清楚了,代码逻辑就好讲了。这里挑两个最核心的业务流程——自动判分和自动交卷,把代码级别的实现思路捋一遍。
5.1 自动判分的完整调用链
交卷接口ExamController.SubmitExam被调用时,后端处理逻辑顺序是:
- 先校验
ExamRecord状态是否已经是"已交卷",避免并发重复提交 - 拉取该考生的所有
ExamAnswerRecord,加上试卷的PaperQuestion列表 - 逐题调用
ScoringService.ScoreQuestion(question, answer) - 汇总总分写入
ExamRecord.Score,状态改为"已交卷",记录SubmitTime - 更新该考试的
ExamRecord状态,回写统计信息
判分核心逻辑:
public decimal ScoreQuestion(Question question, string studentAnswer) { if (string.IsNullOrWhiteSpace(studentAnswer)) return 0; switch (question.QuestionType) { case QuestionType.SingleChoice: case QuestionType.TrueFalse: return question.Answer.Trim() == studentAnswer.Trim() ? question.Score : 0; case QuestionType.MultipleChoice: var correctSet = question.Answer.Split(',').Select(a => a.Trim()).ToHashSet(); var answerSet = studentAnswer.Split(',').Select(a => a.Trim()).ToHashSet(); if (correctSet.SetEquals(answerSet)) return question.Score; // 少选给一半分的配置在这里生效 if (question.AllowHalfScore && answerSet.IsSubsetOf(correctSet) && answerSet.Any()) return question.Score / 2; return 0; case QuestionType.FillBlank: var acceptedAnswers = question.Answer.Split('|'); return acceptedAnswers.Any(a => a.Trim().Equals(studentAnswer.Trim(), StringComparison.OrdinalIgnoreCase)) ? question.Score : 0; case QuestionType.ShortAnswer: // 超过阈值就标记待人工复核 if (studentAnswer.Length >= question.MinimumLength) return -1; // -1 表示待教师手动判分 return 0; default: return 0; } }有个小细节值得说:判断题的Answer字段存的是 "True" 或 "False" 字符串,单选存的是 "A"、"B" 这样的选项序号,多选存的是 "A,B,C" 这种逗号分隔的字符串。判分时把学生答案和标准答案都转成HashSet再比较,能天然处理顺序不一致的问题。这个技巧建议直接复用。
5.2 考试超时自动交卷的保障机制
在线考试肯定会遇到一种情况:考生答到一半,考试时间到了,但他没点交卷按钮,页面一直挂着怎么办?
这个系统用了一个"双重保险"方案:
一是答题时实时校验。每次考生通过 Ajax 保存答案时,后端都会顺手校验当前时间是否超过考试结束时间。如果超过了,返回一个特殊状态码,前端收到后弹窗告知"考试时间已到,系统将自动交卷",然后跳转到成绩页。
二是定时扫描兜底。有一个后台任务,每隔30秒扫描一次ExamRecord表,把那些考试时间已到但状态还是"考试中"的记录捞出来,自动执行交卷流程。这个后台任务在 ASP.NET Core 里用IHostedService实现,在Startup.cs里注册成单例服务,项目启动后就在后台默默工作。
public class AutoSubmitBackgroundService : BackgroundService { private readonly IServiceProvider _services; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); using (var scope = _services.CreateScope()) { var examRecordRepo = scope.ServiceProvider.GetRequiredService<IExamRecordRepository>(); await examRecordRepo.AutoSubmitExpiredExams(DateTime.Now); } } } }用CreateScope()从根容器里取 Repository,是因为BackgroundService是单例,而 DbContext 是 scoped 生命周期,直接在单例里注入 DbContext 会出问题。这个坑新手经常踩,我在这里提醒一下。
6. 部署运行全记录:从 .NET SDK 到 IIS 站点
这部分全是实际操作,我按自己踩坑的顺序一步步写。照着执行,你本地和服务器上都能跑起来。
6.1 本地开发环境准备
需要准备的东西就三样:
- .NET SDK(我用的是 8.0 LTS 版本,项目如果是 6.0 或 7.0 的也能兼容运行,实在不行改个目标框架就升级上去了)
- Visual Studio 2022 或者 VS Code + C# 插件。我用 VS Code 也行,但调试体验不如 Visual Studio
- SQL Server Express / LocalDB 或者完整的 SQL Server。开发期用 LocalDB 足够,部署了再换正式库
安装完 SDK 后,在项目根目录执行:
dotnet restore dotnet ef database update dotnet run看到Now listening on: http://localhost:5000就说明跑起来了。用浏览器打开就能看到登录页,默认管理员账号通常是admin / 123456这类初始密码,正式使用前务必要改。
6.2 数据库初始化一条龙
项目里的 EF Core 迁移文件是现成的,前提是你装了dotnet-ef工具,然后按顺序执行:
dotnet tool install --global dotnet-ef dotnet ef migrations add InitialCreate dotnet ef database update如果你连数据库都不想装,项目里一般会带一个.bak备份文件或者一个初始化 SQL 脚本(在Database目录下),直接用 SQL Server Management Studio 还原或者执行脚本也行。
6.3 发布到 Windows 服务器 IIS 的完整步骤
服务器环境:Windows Server 2019 或 2022,安装好 .NET Core 托管捆绑包(.NET Core Hosting Bundle),这是 IIS 上跑 .NET Core 的必备组件,不装的话站点会一直 502.3。
用命令发布项目:
dotnet publish -c Release -o D:\publish然后打开 IIS,右键新建站点,物理路径指向D:\publish,应用程序池改成"无托管代码"模式。这个细节很重要,.NET Core 应用不走 IIS 的托管管道,它只作为反向代理把请求转发给 Kestrel 进程,所以托管模式要选No Managed Code。
万一忘了装托管捆绑包,IIS 里这个站点启动后会直接报错,错误信息类似HTTP Error 500.35 - ANCM Out-Of-Process Startup Failure,看到这个基本就能确定是捆绑包缺失。
6.4 连接字符串和配置文件
appsettings.json里的ConnectionStrings是核心。发布到服务器后记得改成生产环境的数据库连接:
{ "ConnectionStrings": { "DefaultConnection": "Server=.;Database=ExamSystemDb;User Id=sa;Password=你的密码;TrustServerCertificate=True;" }, "ExamSettings": { "AutoSubmitIntervalSeconds": 30, "MaxConcurrentExams": 300 } }数据库账号不要用sa裸奔,我自己的做法是单独建一个exam_user账号,只授予这个数据库的读写权限。密码别硬编码在 json 里,至少用环境变量覆盖一下。ASP.NET Core 自带配置优先级是环境变量 > JSON 配置,所以你在服务器上设置ConnectionStrings__DefaultConnection环境变量就能覆盖 json 里的值,不用改文件。
7. 高并发场景下最容易崩的几个点
既然考试系统的核心是"很多人同时用",那并发问题必须单独拿出来说。我在压测和实际使用中遇到过的坑,按危害程度排个序。
7.1 连接池耗尽——最典型的在线考试事故
几百个考生同时交卷,每个交卷请求要写答题记录、查试卷明细、算分数、更新成绩,一个请求可能打开多个数据库连接。如果代码里没有及时Dispose连接或者没有用异步方法,连接池很快就满了,后续请求全部排队超时,表现就是页面转圈、接口报错。
这个项目的主体代码用的是 EF Core 仓储模式,DbContext走 scoped 生命周期,每次请求一个实例,用完后自动释放,基本不会出泄漏问题。但如果你拿到源码后自己加了原生 ADO.NET 的代码,一定要记住:
using (var connection = new SqlConnection(connectionString)) { // 你的操作 }能用using的别省,能用await的不要用.Result。同步阻塞在 .NET 里面对并发是最伤的。
7.2 交卷接口的幂等问题
我看过不少同类系统,交卷接口根本没有幂等处理。学生第一次点击交卷,网络卡了一下没响应,他以为没提交成功又点了一次。结果后端收到两个交卷请求,一条ExamRecord被处理了两遍,成绩算了两遍,最后显示的成绩还可能是第二次覆盖第一次的异常值。
这个项目的处理方式算稳妥:交卷接口第一行就检查ExamRecord.Status,如果已经是"已交卷"就直接返回成功结果,不再走判分逻辑。这种"状态机拦截"在订单、考试这类业务里是标配,拿到源码后建议不要删。
7.3 缓存题库减少数据库压力
考试开始后,所有考生都在读同一套试卷题目,如果每次都去数据库查题目,几十个考生同时读同一批题,数据库压力不小。这个项目里有一个简单的内存缓存服务,试卷发布时把题目列表缓存到内存里,考试期间答题相关的查询直接走缓存,考试结束后清理。
public class PaperCacheService { private readonly IMemoryCache _cache; public List<PaperQuestion> GetPaperQuestionsWithCache(int paperId) { var key = $"paper_questions_{paperId}"; return _cache.GetOrCreate(key, entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2); return _paperRepository.GetPaperQuestions(paperId).ToList(); }); } }注意缓存时间要大于最长考试时间(这里是2小时),否则考试还没结束缓存先过期了,又得重新查数据库。
7.4 多选题少选给分的配置陷阱
自动组卷时有一个参数是"多选题是否启用少选给一半分",这个参数只在Question表里用AllowHalfScore字段控制。但问题是,如果一场考试里混有多个题型,判断题的单选逻辑和多选题的少选逻辑在判分代码里走的是不同分支。我遇到过配置了少选给分但判断题也被减半的情况,排查后发现是switch里漏了break。源码本身没有这个问题,但如果自己改判分服务,请务必给每个分支写单元测试,尤其是多选的集合比较逻辑,最容易改出 bug。
8. 二次开发和扩展的几个方向
如果你准备在这套源码上继续做东西,我根据实际需求提几个方向,按性价比排序。
8.1 增加附件上传和视频监控
严肃一点的在线考试都需要考生上传附件(比如简答题的图片、编程题的项目文件),或者开启摄像头监控。加附件上传不难,用IFormFile接收文件,存到服务器指定目录,再把文件路径存到ExamAnswerRecord里就可以。建议加一个文件大小限制(比如单个文件不超过10MB)和文件类型白名单,防止有人上传恶意文件。
8.2 优化判分策略:引入 AI 辅助
简答题的判分一直是痛点。这套系统的关键词匹配法,对付"请简述什么是依赖注入"这种题目时相当有限。如果接一个大语言模型 API,把学生答案和参考答案发给模型让它给分,能做到"虽然是程序判的,但批改逻辑和学生工整度都像真人"的效果。注意实现时要加一个争议机制——模型给出低分且置信度不确定时,自动转入人工复核队列。
8.3 考试封卷和成绩导出
目前成绩导出是 Excel。如果学校或企业要求用特定格式(如教务系统要求 JSON 上报),可以加一个导出适配器。这类业务逻辑在源码里是单独的服务层,接口做好之后,加新的导出格式只需要新增一个实现类。
8.4 消息通知
考试发布后自动通知学生(站内信、邮件或企业微信 webhook),这个功能门槛低、见效快,尤其适合培训机构。源码的User表里已经有邮箱字段,只要在发布考试的方法里加一个异步通知调用就行。
9. 源码里值得直接抄的三个小技巧
最后说三个我实际用到别的项目里的技巧,都是从这套源码里学来的,不写就亏了。
9.1 用Guid.NewGuid()做随机排序
C# 里Random做排序,种子在短时间多次创建时可能重复,导致随机结果一样。OrderBy(q => Guid.NewGuid())虽然性能略差,但随机性足够均匀,代码还简洁。批量抽题、随机生成验证码这类场景都能用。
9.2 内存缓存加绝对过期时间
缓存题目、配置、字典这类低频变更数据时,给AbsoluteExpirationRelativeToNow设置合理的时间,比滑动过期时间更适合业务系统。滑动过期适合用户会话,绝对过期适合静态数据,理由很简单:题目改了之后缓存要尽快失效,不能让旧的题目缓存留到第二天。
9.3 通过服务定位器正确创建子作用域
第八章里BackgroundService要访问 scoped 服务时必须CreateScope(),这个模式在处理"后台任务里操作数据库"时是标准答案。我自己之前写过不少后台服务,直接在构造函数里注入 DbContext,运行一段时间后就报"无法从根容器解析服务",就是因为没按生命周期来。
这是一个非常完整、能直接拿来跑的在线考试系统。我过完一遍代码和实际部署后最大的感受是:它的设计没有炫技,每个选择都踩在业务需求上。无论是当教学项目研究,还是拿来做二次开发底座,都很值得。如果你已经在跑这个项目,或者在改的过程中遇到什么坑,欢迎评论区聊聊。