
简介这套基于 .NET/C# 开发的试卷生成系统源码面向需要构建在线题库与自动组卷功能的 ASP.NET 开发者从题目录入、类型维护、人工勾选组卷、随机抽题到最终导出 Word 试卷提供了完整可运行的实现方案。压缩包共 278 个文件大小仅 1.06MB核心代码以 48 个 C# 文件为主负责业务逻辑与数据处理30 个 ASPX 页面承载前台交互界面同时配有 176 个 GIF 图片和少量 CSS、JS、配置文件另含数据库 mdf/ldf 文件基本涵盖一个 Web 项目的主要工程结构适合直接导入 Visual Studio 学习和修改。系统在页面组织上覆盖试题添加、试卷设置、用户管理、角色权限等多个模块能够支撑教师手工选题或按题型题量随机生成的典型考务场景进而满足日常教学与培训考核的出卷需求同时生成试卷后可导出为 Word 文档便于打印和分发。目前已有 159 人学习使用对于想了解 .NET WebForms 传统开发模式或快速搭建组卷原型的读者是一份很直观的参考资料。 开学季一到后台总有人问我有没有能直接用的考试系统。学校里老师要出卷培训机构要做模拟考企业要搞员工测评——需求一大把但市面上的商业系统要么贵要么不够灵活。干脆把以前做过的一个.NET版试卷生成系统源码拆开讲讲从题库管理到自动组卷再到Word导出整个链路怎么设计、代码怎么组织、坑在哪一次说清楚。这个系统适合谁正在做毕业设计的.NET方向学生、需要给单位搭内部考试平台的后端开发、还有想理解“随机组卷”这类业务到底怎么落地的朋友都可以参考。它的核心价值不在于功能多花哨而在于框架干净、逻辑清晰把“手动出题”这件麻烦事变成了“选条件一键生成”而且题目重复率可控、难度分布可按需调整拿过去改改就能用在真实场景里。1. 整体设计与技术选型思路1.1 为什么选.NET而不是其他技术栈先说选型。可能有人会觉得“不就是个出题系统吗Python写脚本也能搞定”。但真放到生产环境里差距就出来了。这套系统的定位是有管理后台、有角色权限、要操作数据库、要导出标准格式文档的完整Web应用这种场景下.NET的生态优势非常明显。我选用的是.NET 8 ASP.NET Core Web API EF Core SQL Server的组合。选.NET 8而不是停留在Framework 4.8原因有三跨平台部署方便Docker里跑得很稳性能比Framework时代提升了一大截同时处理几百个并发请求毫无压力内置的依赖注入、配置系统、中间件管道都是标准化的写起来整洁、好维护。EF Core做ORM确实有学习成本但一旦用熟CRUD这种重复劳动能省掉大半时间。前端我选了最省事的方案——服务端渲染的Razor Pages加少量jQuery。很多人一上来就搞前后端分离Vue或React配Web API不是说不行而是这种内部工具类项目多一层前端工程就多一层维护负担。服务端渲染的好处是页面和接口在同一个项目里部署简单调试直接适合一个人全栈搞定。1.2 系统分层与项目结构项目按经典的DDD思路做了适度分层没有过度设计。整个解决方案分为四个项目ExamPaper.Core领域层放实体类、枚举、业务接口。比如题库实体、试卷模板实体、抽题策略接口都在这层。ExamPaper.Application应用层承载具体业务逻辑。组装试卷、计算难度、处理导出数据这些活儿都在这里干。ExamPaper.Web表现层包含所有Razor页面、API控制器、静态资源。ExamPaper.EntityFrameworkCore基础设施层负责数据库上下文、仓储实现、迁移脚本。为什么要这样拆核心好处是职责清晰。比如未来要换数据库从SQL Server换到MySQL只需要改基础设施层上面的业务代码一行不用动。再比如要给系统加一个微信小程序端只需要在Web层新增API控制器复用Application层的服务就行。这种“扩展不改动旧代码”的体验等你真正维护过一堆乱七八糟的单体项目后就懂了。2. 核心模块与数据库设计2.1 题库管理与题目导入题库是整个系统的基础没有题组卷就是空谈。题目实体我设计了这些字段Id、QuestionType单选/多选/判断/填空/简答、Content题干、Options选项JSON存储、Answer正确答案、Analysis解析、Difficulty难度系数1-5、SubjectId所属科目、ChapterId章节、CreatedAt。很多初学的人会把选项设计成单独的关联表但实际用下来选项用JSON字符串存反而更香。原因很简单选项永远跟着题目走不存在脱离题目的独立选项操作查询时少一次联表性能也更好。JSON字段在EF Core里可以映射成字符串类型用System.Text.Json序列化取出时反序列化就行。题目导入这块我做了Excel批量导入用的是NPOI库。模板固定列题型、题干、选项A-D、答案、解析、难度、科目、章节。后台解析Excel时要注意单元格为空、格式不对等异常情况我的方案是逐行校验把错误行号和原因汇总写进导入日志而不是遇到一个错误就中断整个导入流程。2.2 试卷模板与组卷策略试卷模板我用“模板 规则”的方式来设计。模板实体记录试卷名称、总时长、总分、适用年级/部门以及一组抽题规则集合。每个抽题规则包含题型单选/多选/判断等该题型题量该题型单题分值覆盖章节范围关联多个章节难度分布比如容易30%、中等50%、困难20%知识点权重这套设计的核心思路是把“组卷”拆成“按规则去题库里挑选符合条件的题目”而不是一次性拼出整张卷子。举个例子数学老师想生成一套高一的期中测试卷规则就是选择题15道每题3分、填空题5道每题4分、解答题3道每题10分难度分布按4:5:1章节覆盖集合为“集合与函数”“基本初等函数”。系统解析每条规则聚合出最终试卷。题型枚举设计如下public enum QuestionType { SingleChoice 1, // 单选题 MultipleChoice 2, // 多选题 Judgment 3, // 判断题 FillBlank 4, // 填空题 ShortAnswer 5 // 简答题 }2.3 数据库表关系与索引设计整个系统核心是五张表Subjects科目、Questions题库、ExamTemplates试卷模板、TemplateRules抽题规则、Exams生成的正式试卷。多对多的关系主要体现在“题目出现在多套试卷”以及“一份试卷包含多道题目”上因此我额外设计了ExamQuestions中间表记录每套试卷的题目快照——包括题目ID、分值、排序号。索引设计这里要提醒一句题库表如果数据量上了万级查询必须走索引。我建了几个关键索引Questions表上的SubjectId QuestionType联合索引、Difficulty单列索引、ChapterId单列索引ExamQuestions表上的ExamId索引。实际压测下来5万道题的题库按规则过滤加随机排序的查询可以控制在300毫秒以内完全够用。3. 核心实现随机组卷算法与试卷导出3.1 组卷算法的两种实现方式组卷算法是这个系统的灵魂也是我最想详细讲的部分。当初设计时我对比了两种方案纯SQL随机抽取和内存中洗牌。方案一纯SQL随机抽取。思路是根据规则条件用ORDER BY NEWID()从符合条件的题目中随机取N条。优点是实现简单代码量少。缺点是题库大到一定程度时NEWID()排序不走索引全表扫描的老问题会回来性能明显下降而且多次抽题之间无法保证不重复。方案二内存洗牌。思路是先用索引条件把候选题目ID全部查出来这个查询走索引速度快再丢到内存里用Fisher-Yates算法打乱顺序从打乱后的列表中取前N个。在此基础上还可以做“排除已用题目”的过滤。这种方案多了一个内存占用但换来的是稳定、可控、不重复。我最终用的是方案二。Fisher-Yates洗牌算法的代码很简单public static ListT ShuffleT(ListT list, int seed) { var rng new Random(seed); for (int i list.Count - 1; i 0; i--) { int j rng.Next(i 1); (list[i], list[j]) (list[j], list[i]); } return list; }这里的seed是基于“当前时间戳 模板ID”生成的一个整数这样做的好处是同一套模板同一分钟生成的两份卷子题目顺序和组合会不同但保留了一定的确定性——方便复现问题。你还想让某次生成的卷子能复现把seed记到试卷表里就行重新执行一次组卷逻辑就能还原。3.2 难度分布和重复率的控制逻辑很多人以为抽题就是“随机取N条”这其实是最大的误区。一道试卷不能全是难题也不能全是送分题每个知识点的覆盖率也有要求。我的处理方式分为两步第一步按难度分层。假设某条规则要出15道单选题难度比例是容易4道、中等7道、困难4道。我就把候选题目按难度值1-2为容易3为中等4-5为困难先分组然后对每个分组内部各自洗牌按需要的数量抽取。这样出来的试卷难度分布严格可控。第二步控制重复率。做法是组卷前把“最近三次考试已用题目ID”查出来在候选题目列表里将其过滤掉。为了提高性能这个已用ID集合我存进了内存缓存MemoryCache设置10分钟的过期时间。这样组卷频繁发起时不会每次都去打数据库。当然如果是小题库低于500道题全部过滤可能导致候选题目数量不够所以代码里要加个兜底过滤后数量不够时按“先过滤再补充”的逻辑最后空位从全部候选题目里补。候选题目不足时抛出明确异常if (candidateIds.Count rule.QuestionCount) { throw new ExamGenerationException( $组卷失败章节【{rule.ChapterName}】的【{rule.QuestionType}】题目数量不足当前可用 {candidateIds.Count} 道需要 {rule.QuestionCount} 道); }这个异常信息一定要写得具体老师看到后能直接去题库补题而不是一头雾水。3.3 试卷导出Word的实现细节试卷生成之后在线预览是一回事但实际使用场景往往是老师要Word文件。这里我用的是Aspose.Words for .NET。选它而不是NPOI的XWPF是因为Aspose对样式的处理省心得多公式排版也更可控缺点是商业授权不便宜不过个人项目和学习场景自己斟酌即可。导出流程是这样先定义一份Word模板文件里面按位置放好书签{studentInfo},{questionsList},{answerSheet}用DocumentBuilder定位到书签批量插入题目。单选题和多选题的选项我用项目符号列表渲染填空题用下划线________代替留白简答题下面留3行空行方便手写。最后通过SaveToFile输出到Server.MapPath(~/GeneratedExams/)目录文件名用“试卷名称_生成时间.doc”。有一个细节要注意Aspose.Words在处理中文内容时文档默认字体经常被替换成宋体或乱码。解决办法是在构造Document时调用FontSettings.DefaultFontName 宋体或者直接将模板中正文字体设为微软雅黑。如果你在导出后发现中文全部变成“口口口”基本就是这个原因。3.4 API层设计与Swagger调试虽然页面是服务端渲染的我还是额外暴露了一组Web API方便以后接小程序或别的客户端。API只有三个核心端点[HttpPost(api/exam/generate)] public async TaskIActionResult Generate(GenerateExamRequest request) [HttpGet(api/exam/{id})] public async TaskIActionResult GetExam(int id) [HttpPost(api/exam/{id}/export)] public async TaskIActionResult Export(int id)ASP.NET Core内置Swagger开发时直接通过/swagger页面调试每个接口比Postman更顺滑省去了复杂的登录态设置。记住要在Program.cs里确认if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }4. 常见问题与排查实录4.1 组卷接口报500错误这个问题出现得相当高频。我校验时发现大多数时候是候选题目不足或输入参数非法比如题量为负数、难度比例之和不是100但这些异常没有统一处理直接以500状态码吐给前端。后来我加了一个全局异常过滤器app.UseExceptionHandler(errorApp { errorApp.Run(async context { var exception context.Features.GetIExceptionHandlerFeature()?.Error; context.Response.StatusCode 500; await context.Response.WriteAsJsonAsync(new { message exception?.Message }); }); });然后前端就能看到具体的异常信息而不是干巴巴的“Internal Server Error”。排查速度直接翻倍。4.2 Aspose导出Word中文乱码刚才提过一次这里再展开说。乱码根源就是字体设置。我用两行代码解决doc.FontSettings.DefaultFontName 微软雅黑; doc.Styles[StyleIdentifier.Normal].Font.Name 微软雅黑;还要提醒一下部署到Linux容器时服务器上必须安装中文字体否则还是乱码。Docker镜像里执行apt-get install -y fonts-wqy-microhei就能装上中文字体这个坑我踩过一次部署完白屏乱码排查半天。4.3 生成试卷速度慢早期做性能测试时发现组卷一次要2秒多实在太慢了。用EF Core的日志功能一看发现组卷过程产生了大量查询——每个规则查询一次题库每组难度又各自查询一次。优化后的做法是先按大条件一次性查出该章节全部候选题目ID只查ID字段避免查询长文本字段拖慢速度在内存里按难度分组。千万别把题干、解析这些大字段一次查出网络传输和内存占用都扛不住。优化之后5万道题的题库生成一套含50道题的试卷耗时从2.3秒降到400毫秒左右这个速度老师就基本无感了。4.4 题目选项乱序问题组卷时发现一份卷子里第一题选项顺序和第二题完全一致。这虽然不是“错”但连续几题出现相同选项排列学生很容易摸出规律这套卷子效度就下降了。解决方法是对每个题目的选项也做一次洗牌。单选和多选在抽完题后对Options集合调用Shuffle方法同时把正确答案的文本同步更新。记住多选题的正确答案在洗牌后要同步调整排序否则判卷时按字符串比较就对不上了。5. 从能用到好用可以扩展的方向整套系统做完后它已经能覆盖“出卷”这个核心场景在真实环境里跑了大半个学期老师反馈整体可用性不错。但如果要继续扩展我觉得有三个方向优先级最高第一试卷在线作答与自动判分。把答题页做成H5单选、判断混在一起自动判分多选题要支持部分得分简答题需要人工复核。这是从“出题工具”走向“完整考试平台”的关键一步。第二题库导入支持更多格式。现在只支持Excel如果加上Word导入格式上有一定的规范性才能解析老师的门槛会更低。不过Word解析的兼容性是个大坑要谨慎。第三基于学生历次成绩的智能组卷。如果把班级学生的知识点得分率数据沉淀下来组卷时就能针对薄弱点重点出题做到“哪里不会考哪里”。这需要数据分析和推荐算法的积累不是一两天能做完的但方向是对的。最后分享一个自己的体会做这种工具型系统千万别贪大求全。一开始就把题库管理、组卷、PDF导出、在线考试、成绩分析全都塞进去多半会烂尾。先把“题库 → 组卷 → 导出Word”这个最小闭环跑通扔给真实用户用起来再根据反馈迭代新功能这样一步步走下来的系统才是真正能落地的系统。本文还有配套的精品资源点击获取