ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Django的在线考试系统设计实现:数据库建模与核心功能详解

基于Django的在线考试系统设计实现:数据库建模与核心功能详解 简介这是一套完整的基于Django框架开发的Python在线考试系统适用于本科毕业设计、课程大作业及教学实训项目聚焦多角色协同的考试全流程管理。系统支持管理员、教师、学生三级权限体系覆盖用户管理、班级课程绑定、题库建设含单选/多选/判断/填空/简答/编程题后者支持Python代码在线运行、智能组卷手动规则化随机、考试发布与阅卷统计等核心功能。资源包共607个文件含63个Python后端逻辑文件、123个Vue前端组件、159个SVG图标资源、53个JPG与49个PNG图片素材以及SQL数据库脚本、BAT自动化运行脚本等整体压缩包大小为21.63MB。已有66人学习下载提供可直接部署的完整工程结构、清晰的模块划分如exam、question、user等Django应用、配套数据库文档及初始化脚本便于快速理解架构、调试功能并二次开发。 在线考试系统这个活儿接过的人都知道看起来是个普通业务系统真做起来全是细节。题库怎么组织、试卷怎么生成、考生交卷怎么防并发问题、判分怎么兼顾客观题和主观题每一层都有讲究。最近刚好完整做了一套基于Django的在线考试系统从需求梳理到数据库设计再到核心功能实现现在已经稳定跑了好几轮模拟考试。这篇文章就把整个设计与实现过程摊开来讲重点说说数据库文档怎么沉淀、核心功能链路怎么落地以及那些不踩一遍根本发现不了的坑。这套系统适合正在做课程设计、毕业设计或者公司内部要搞培训考核、技能认证的团队参考。如果你对Django已经有一定了解想看看在线考试这类业务怎么建模、怎么实现这篇文章可以直接拿来当设计蓝本。1. 在线考试系统的需求边界一开始就把模块切清楚很多在线考试项目做崩不是代码写得差而是需求边界根本没划清楚。拿到需求第一件事不是建模型而是把角色、流程、状态全部列出来再做模块拆分。1.1 这个系统到底要做什么——从角色反推功能清单在线考试系统从用户角色上看通常分三类系统管理员、教师出题人、考生。有些场景还会加一个阅卷人角色但在中小型系统里阅卷功能常常合并到教师角色里。三类角色对应的核心诉求完全不同管理员关心的是基础数据维护用户管理、班级/组织架构管理、系统参数配置以及考试全局层面的数据统计。教师关心的是考前准备和考后处理创建题库、维护题目、组卷、发布考试、查看成绩、复核主观题。考生关心的是考试体验登录后能看到自己需要参加的考试进入考试后能正常答题、看倒计时、顺利交卷交卷后能尽快看到成绩。从这些诉求反推系统的核心模块就清晰了用户认证模块、题库管理模块、试卷管理模块、在线考试模块、自动判分模块、成绩查询模块。这里有个容易被忽略的点在线考试并不只是把纸质试卷搬到网上。它的核心优势是组卷灵活性和判分自动化。所以题库要支持题型维度、难度维度、知识点维度的筛选试卷要支持手工选题和随机抽题两种模式判分要区分客观题和主观题。如果这些在一开始没想清楚后面做数据库设计的时候会很痛苦。1.2 为什么最终是Django框架选型对比与理由市面上做这类系统的主流选择无非几种Python系的Django/Flask、Java系的Spring Boot、Node.js系的Express/NestJS。我做技术选型的时候把几个关键因素列了个对比表考量维度DjangoFlaskSpring BootExpress开发速度高内置大量组件中需要自己组装中低配置多但生态成熟中自带管理后台有Admin开箱即用无需自建无需整合无ORM能力强迁移机制完善弱需SQLAlchemy强但上手成本高弱社区与资料多中文资料丰富多很多多适合场景业务系统、CMS、考试系统轻量API、微服务企业级复杂系统实时交互服务在线考试系统是典型的业务逻辑清晰但页面和流程不少的Web应用。Django的Admin后台可以让教师不写一行前端代码就完成题库维护这是开发效率上非常大的优势。再加上Django自带用户认证、CSRF防护、Session管理这些恰好是考试系统最需要的基础能力用Django可以省掉大量重复造轮子的工作。另外从部署和维护角度考虑Django项目的结构非常规整models/views/urls/templates分层明确后期就算换人接手上手成本也低。一个小团队或者个人开发者在两三个月内交付一套可用的考试系统Django是我认为最稳的选择。2. 数据库设计是考试系统的地基五张核心表的结构与关联数据库设计是这类系统最关键的环节很多问题都是表结构设计不合理导致的。在设计阶段多花点时间后面写业务代码会顺畅很多。整个在线考试系统我最终沉淀出了十来张表但真正处于核心位置的是五张用户表可以复用Django自带、题目表、试卷表、考试记录表、答题明细表。下面把这五张表的结构和设计理由讲清楚。2.1 题库模块题干、题型、选项怎么建模题目表是我第一个设计的因为它是整个系统的数据源头。我用的是单表存储所有题型的方式而不是按题型拆表。原因是单选、多选、判断、填空题的差异主要在选项和答案的存储格式上完全可以通过字段设计兼容拆表反而会让查询逻辑变得支离破碎。class Question(models.Model): 题目表 QUESTION_TYPE ( (single, 单选题), (multiple, 多选题), (judge, 判断题), (blank, 填空题), ) DIFFICULTY ( (1, 简单), (2, 中等), (3, 困难), ) question_type models.CharField(max_length20, choicesQUESTION_TYPE, verbose_name题型) content models.TextField(verbose_name题干内容) difficulty models.PositiveSmallIntegerField(choicesDIFFICULTY, default2, verbose_name难度等级) options models.JSONField(defaultlist, verbose_name选项内容) # [{key: A, text: xxx}] answer models.JSONField(verbose_name正确答案) # [A] 或 [True] 或 [填空答案] score models.PositiveIntegerField(default5, verbose_name分值) knowledge_point models.CharField(max_length100, blankTrue, verbose_name知识点) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table exam_question indexes [ models.Index(fields[question_type, difficulty]), ]这里有个值得注意的设计要点选项和答案都用了JSONField。一开始有人会建议把选项拆成单独的表用外键关联。但我实际对比过之后发现对在线考试系统来说选项和题目是强绑定关系永远是一对多的整体读写拆表虽然更范式化但每次查题目都要多一次联表查询而且选项的顺序本身也是题目的一部分用一个JSON数组存储反而更贴合业务。正确答案字段用JSON存储是因为不同题型的答案格式不同单选题是[A]多选题是[A,C]判断题是[True]填空题可能有多个空是[Python, Django]。统一用JSON存储判分逻辑里再做类型判断处理起来最灵活。2.2 试卷与答题记录考一次试的状态机怎么流转题目表解决的是有什么可考试卷表和考试记录表解决的是怎么考和考得怎么样。试卷表我设计了两种组卷方式的兼容一种是教师手动从题库里勾选题目另一种是指定规则题型、难度、数量让系统自动抽取。无论哪种方式生成好的试卷都存一份题目快照。class Paper(models.Model): 试卷表 title models.CharField(max_length200, verbose_name试卷名称) description models.TextField(blankTrue, verbose_name考试说明) duration_minutes models.PositiveIntegerField(default60, verbose_name考试时长分钟) total_score models.PositiveIntegerField(default100, verbose_name试卷总分) questions models.JSONField(defaultlist, verbose_name题目快照) # questions: [{question_id: 1, score: 5, order: 1}, ...] is_published models.BooleanField(defaultFalse, verbose_name是否发布) publish_time models.DateTimeField(nullTrue, blankTrue, verbose_name发布时间) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间)还有一个容易被新手忽略的点试卷里的题目快照一定要存题目ID和分值而不是直接存题目的完整内容。原因是如果教师后来修改了某道题的内容已经创建的试卷不应该受影响。存快照的方式保证了考试当时考的就是这张卷子上的题这也符合线下考试的基本逻辑。考试记录表是系统的核心表记录了每次考试实例的完整生命周期。我用一个状态字段来管理考试过程的流转class ExamRecord(models.Model): 考试记录表 STATUS ( (pending, 待开始), (in_progress, 进行中), (submitted, 已交卷), (graded, 已判分), (expired, 已过期), ) student models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name考生) paper models.ForeignKey(Paper, on_deletemodels.CASCADE, verbose_name试卷) status models.CharField(max_length20, choicesSTATUS, defaultpending, verbose_name状态) score models.DecimalField(max_digits5, decimal_places1, nullTrue, blankTrue, verbose_name总得分) start_time models.DateTimeField(nullTrue, blankTrue, verbose_name开始时间) submit_time models.DateTimeField(nullTrue, blankTrue, verbose_name交卷时间) answers models.JSONField(defaultdict, verbose_name答题内容) # answers: {question_id: [A, C]} 或 {question_id: 用户输入的文本} create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table exam_record indexes [ models.Index(fields[student, status]), models.Index(fields[paper, status]), ] unique_together (student, paper)答题明细为什么不单独拆一张表这是我在设计过程中反复纠结过的地方。后来决定把答案直接存在考试记录的JSON字段里是因为在线考试的场景下答题明细永远不会脱离考试记录单独被查询——你查答案时一定是在查某一次考试、某一位考生的答题情况。合并存储后交卷时只需更新一条记录判分时只需读取一条记录事务边界变得非常简单。关于unique_together (student, paper)这个约束它保证了同一个考生对同一张试卷只能有一条考试记录。这个约束非常重要不然一个考生重复参加同一次考试会出现多条记录成绩就乱套了。3. 核心功能实现随机抽题、答题态、自动判分的完整链路数据库设计好了接下来就是核心功能的实现。在线考试系统的功能链路可以概括为三个阶段考前的自动组卷、考中的答题状态管理、考后的自动判分。每一个阶段都有很多细节需要注意。3.1 自动生成试卷按题型和难度随机抽题的实现组卷是教师使用频率最高的功能。设计成两种模式手动选题模式适合小规模、高定制化的考试自动组卷模式适合大规模、标准化考试。自动组卷的核心逻辑是按规则从题库中随机抽取指定数量的题目。这里的关键是既要保证随机性又要保证覆盖范围合理。实现方式如下import random from django.db.models import Count, Sum from .models import Question, Paper def generate_paper(paper_title, duration, rules, total_score100): 自动组卷 rules 示例格式: [ {question_type: single, difficulty: 1, count: 5, score_per: 3}, {question_type: single, difficulty: 2, count: 10, score_per: 4}, {question_type: multiple, difficulty: 2, count: 5, score_per: 6}, ] questions_pool [] total 0 for rule in rules: qs Question.objects.filter( question_typerule[question_type], difficultyrule[difficulty] ) # 使用 ordered_by(?) 随机排序取前 N 条也可以用 Python 层面随机 selected list(qs.order_by(?)[:rule[count]]) if len(selected) rule[count]: raise ValueError(f题库数量不足{rule[question_type]} 难度{rule[difficulty]} 需要 {rule[count]} 题实际只有 {len(selected)} 题) for q in selected: questions_pool.append({ question_id: q.id, score: rule[score_per], order: len(questions_pool) 1, }) total rule[score_per] paper Paper.objects.create( titlepaper_title, duration_minutesduration, total_scoretotal, questionsquestions_pool, ) return paper这里有一个我在实际开发中踩过的坑order_by(?)在数据量大的时候性能很差因为它会把全表数据都加载到内存里排序。我的题库大概有几千道题还能接受如果用这个接口应对几十万题的题库就需要考虑先按主键范围随机抽样再做二次筛选的方案。组完卷之后还有一道关键工序试卷总分校验。人工组卷时经常出现总分不是100分的情况因此我在保存试卷的时候会做一个总分校验如果总分不等于配置的目标分会给出明确的提示由教师决定是继续保存还是调整题量。这个细节在真正部署之后被教师多次表扬过因为人工操作太容易算错分值了。3.2 考试会话与提交防重复提交、倒计时、事务处理考生点击开始考试的瞬间系统要做的事情比表面上看起来多得多检查考生是否有此试卷的考试记录如果已经交卷则拒绝再次开始。创建一条状态为in_progress的考试记录记录开始时间。将试卷的题目快照与答题数据一起返回给前端初始化考试界面。这里的关键问题是考试记录的唯一性保障。如果没有数据库层面的唯一约束并发请求时可能出现一条试卷被考生同时创建两条记录的情况。我在前面设计表结构时特意加了这个约束对应的Django层逻辑也会先查后建但真正可靠的是数据库的唯一索引兜底。交卷接口是整个系统里对事务要求最高的地方。交卷动作涉及三步操作更新考试记录的状态为submitted、计算客观题得分、写入得分字段。这三步必须在一个事务里完成否则可能出现记录已经标记为已交卷但分数还没算出来的中间状态。from django.db import transaction from django.utils import timezone transaction.atomic def submit_exam(record_id, answers): 提交试卷并计算客观题得分 record ExamRecord.objects.select_for_update().get(idrecord_id) # 状态校验防止重复交卷 if record.status submitted: return {code: 1, message: 试卷已提交请勿重复操作} # 获取试卷题目快照 paper record.paper question_map {q[question_id]: q for q in paper.questions} # 判断客观题并计算得分 objective_score 0 for qid, user_answer in answers.items(): question Question.objects.get(idqid) if question.question_type in (single, multiple, judge): objective_score score_objective_question(question, user_answer, question_map.get(int(qid), {}).get(score, 0)) record.answers answers record.submit_time timezone.now() record.status submitted record.score objective_score # 主观题得分后补这里只记客观题 record.save() return {code: 0, message: 交卷成功, objective_score: objective_score}select_for_update()是这里最重要的一个细节。它会对这条考试记录加行级锁防止两个并发请求同时进入交卷逻辑。如果没有这个锁两个请求同时读到status为in_progress的记录然后同时执行更新就会造成重复交卷和判分异常。倒计时的实现也要多说一句。倒计时不能完全依赖前端因为前端时钟可以被修改。正确的做法是倒计时的基准时间是服务端返回的start_time duration_minutes前端定时向后端同步剩余时间或者干脆由后端在交卷时校验是否超时。我采用的是前端展示倒计时后端交卷时校验超时时间双保险方案。在后端校验时如果发现当前时间已经超过截止时间但考生才交卷会允许提交但不更新start_time同时记录一个超时标记用于后续的统计分析。3.3 判分逻辑单选、多选、判断、简答四种题型的处理差异判分逻辑直接体现了一个在线考试系统的专业度。不同题型的判分策略差别很大单选题答案完全匹配才得分没有部分分数。判断题同样只有对错两个结果简单。多选题这个要特别小心。我见过很多系统在多选题判分上处理得很随意有的要求完全匹配才得分有的支持少选得部分分。填空题/简答题需要人工阅卷系统只做答案采集和留痕。先看客观题的判分实现def score_objective_question(question, user_answer, question_score): 客观题判分返回该题得分 correct_answer set(question.answer) user_set set(user_answer) if not user_set: return 0 if question.question_type multiple: # 多选的判分策略完全一致才得分 if user_set correct_answer: return question_score # 如果配置了部分得分可以在这里处理 # 例如少选且没有选错时给一半分 elif user_set.issubset(correct_answer) and len(user_set) len(correct_answer): return round(question_score * 0.5, 1) else: return 0 # 单选、判断 if user_set correct_answer: return question_score return 0对于多选的判分策略我在实际项目中做成了可配置项因为不同客户/老师对少选的处理意见不统一。这个配置项放在系统参数表里默认是完全匹配才得分这样最严格也最没有争议。如果要支持部分得分需要特别注意总分计算时的精度问题。主观题的判分是另一套流程。交卷后系统会筛选出含有填空、简答题的考试记录把这些题目的答案和标准答案一起展示给教师。教师在管理后台逐题打分打完保存后系统重新汇总总分。这里有一个实现细节系统需要记录客观题得分和主观题得分两个字段而不是直接存一个总分。这样即使教师改主观题分数也能准确知道客观题部分没有被意外修改。4. 踩坑与优化从能跑到稳定跑的六个关键细节系统上线后真实使用场景会暴露出一堆开发时根本想不到的问题。这里挑几个我印象最深的坑和经验每条都是真金白银换来的。4.1 N1查询和序列化性能刚开始做这个系统时我的考试记录列表页性能很糟糕打开一次要加载两三秒。查了一下日志发现罪魁祸首是N1查询。典型场景管理员查看某个班级的考试记录列表每条记录都需要查考生姓名和试卷名称。如果不用select_related或prefetch_related每显示一条记录就要额外执行两次查询。50条记录就是100多次查询性能当然不行。# 性能差循环里查数据库 records ExamRecord.objects.filter(paper_idpid) for record in records: student_name record.student.username # 每次都触发一次数据库查询 # 性能好一次性连表查出来 records ExamRecord.objects.select_related(student, paper).filter(paper_idpid) for record in records: student_name record.student.username # 已在缓存中不会再次查库这个优化做下来列表页从两三秒降到了三百毫秒以内。类似的优化还有题目列表页用values()只取需要的字段避免把大字段JSON选项、答案文本全部加载出来。4.2 并发提交与事务边界在线考试系统并发最大的场景不是成千上万人同时读写同一份数据而是同一个考生同时打开两个标签页或者前端断线重连导致重复请求。我遇到过的问题是考生提交试卷时网络卡顿前端自动重试结果交了两遍。第一次提交成功了第二次因为有状态校验被拒绝了但考生看到的是系统异常的报错信息非常吓人。解决方案分两层服务端用select_for_update()加行级锁保证同一时刻只有一个交卷请求能修改考试记录前端在收到服务端确认后对交卷按钮做置灰处理防止用户重复点击造成焦虑。同时接口层对重复提交返回友好的提示词试卷已提交成功请勿重复操作而不是直接报500错误。4.3 时区、题目顺序打乱这些细节时区问题属于那种不在生产环境踩一次坑根本不会注意的坑。本地开发时用的是本地时区部署到服务器后如果数据库和Django的时区设置不一致会出现考试开始时间、交卷时间忽前忽后的诡异现象。后来我在settings.py里统一设置了TIME_ZONE Asia/Shanghai和USE_TZ True所有时间处理都用django.utils.timezone.now()彻底解决了这个问题。题目顺序打乱也是一个值得细想的点。如果所有考生的题目顺序完全一样那相邻座位的考生很容易互相看答案。我在返回试卷数据时对题目顺序做了随机打乱同时选项顺序也做了打乱。但要注意打乱的是展示顺序题目和选项的唯一标识ID必须保持稳定否则提交答案时会对不上。实现上我维护了一个display_order字段在前端展示时动态随机排序提交时按题目ID回传答案这样既不影响判分又能防止考生对答案。4.4 题库大数据量下的导出与备份在线考试系统跑起来之后题库数据的备份和迁移成为一个必须提前规划的问题。我用Django的dumpdata命令定期备份整个数据库但题目表里有大量JSON字段dump出来之后文件很大且不利于SQL层面的查询分析。后来我额外做了一个导出功能将题目按Excel格式导出带选择题型、难度、知识点列。这个功能不仅用于备份还被老师用在了跨系统迁移题库的场景里。Django生态里有个django-import-export库集成起来很省事支持Excel导入导出可以直接在Admin后台加一个入口。4.5 考试中途浏览器崩溃的恢复浏览器崩溃或者电脑突然断电是考试场景里一定会遇到的情况。初版系统没有做任何恢复机制考生崩溃后重新打开页面只能看到未开始考试的状态必须找管理员重置非常影响体验。后来我加了一个续考机制考生重新进入考试页面时后端检查是否存在in_progress状态的考试记录如果有就恢复之前的答题数据并把剩余时间重新计算返回给前端。这个功能用到了ExamRecord表里的answers字段——考生每次答题前端会定时把草稿同步到后端后端更新answers字段。这样就算浏览器崩溃已经答过的内容也不会丢。这个功能上线后考试系统的意外中断投诉量直接降到了零。它是那种不加看不出来、加了体验质变的功能。4.6 数据权限与安全控制在线考试系统的数据权限比普通业务系统更敏感。考生不应该看到其他考生的成绩教师只能管理自己课程下的题目和试卷管理员可以查看全部数据。Django自带的用户认证系统只解决了你是谁的问题没解决你能看什么的问题。我用Django的Group和Permission机制做了角色控制将教师、考生、管理员分成三个组分别授权。在视图层面用permission_required装饰器和自定义的queryset过滤来实现数据隔离。比如教师只能看到自己创建的题目# 教师只能操作自己创建的题目 def get_queryset(self): if self.request.user.is_superuser: return Question.objects.all() return Question.objects.filter(creatorself.request.user)还有一个细节考试过程中的页面接口必须校验当前登录用户就是考试记录的本人防止用户通过篡改URL参数查看他人考试内容。这个校验虽然简单但很多实战项目里都会遗漏。5. 数据库文档为什么值得单独写一个可以直接套用的模板标题里专门提到了数据库文档这其实是我做这类项目时比较坚持的一个习惯。很多开发者觉得数据库文档就是给数据库课程设计交差用的实际做企业项目不重要。但我恰恰相反我认为数据库文档的价值在于它把代码里的隐式逻辑显式化让后续接手的人不用一行行读models.py就能理解系统的数据流动方式。5.1 数据库文档包含什么一份能真正指导开发的数据库文档至少应该包含以下几个部分而不仅仅是列出字段名和类型表结构总览一张图或一个表格说明系统有哪些表每张表是干什么的。核心表字段说明字段名、类型、是否必填、默认值、业务含义、枚举值说明。表间关系说明外键关系、多对多关系、以及哪些关联是通过JSON字段实现的逻辑关联。关键索引说明哪些字段建了索引为什么建索引覆盖了哪些查询场景。数据生命周期说明数据从哪里产生经过哪些状态的流转最终流向哪里。5.2 这次沉淀下来的文档结构以这套考试系统为例我的数据库文档主要包含以下几张表的详细说明表名用途关键字段关联关系auth_user用户表Django内置username, password, email与exam_record一对多exam_question题目表question_type, difficulty, answer, options与paper通过JSON快照关联exam_paper试卷表questions(JSON快照), duration_minutes与exam_record一对多exam_record考试记录表status, score, answers, start_time, submit_time与user、paper多对一exam_category课程/分类表name, description与question一对多文档里我对每一个JSON字段都做了详细的格式说明比如exam_paper.questions字段的格式是[{question_id: 1, score: 5, order: 1}]exam_record.answers字段的格式是{question_id: [\A\, \C\]}。这些信息如果只写在代码注释里别人看文档根本不知道从哪下手。数据库文档的编写可以结合Django的迁移文件自动生成一些基础字段信息但业务含义和关联关系必须人工补充。我通常的做法是先用工具导出表结构然后逐表补充业务说明重点解释为什么这么设计和哪些字段是业务核心。这次文档总共写了三十多页对于后续开发、交接、答辩都是非常扎实的资料。另外数据库文档不是写完就完了。每当表结构有调整我都会同步更新对应的文档。这个习惯帮我避免了很多代码和文档对不上的尴尬场景。6. 线上部署以后数据统计与系统演进的方向系统上线跑稳定之后我又陆续加了一些数据统计的功能。考试系统天然就会沉淀大量数据——考试次数、题目正确率、成绩分布、知识点掌握情况这些数据如果不去用数据库里躺着就浪费了。我做的最简单也是最有用的一个统计是统计分析每个知识点的答题正确率。这个功能不复杂就是交卷判分时顺便把每道题的对错标记出来然后按知识点聚合。但效果出奇地好——老师能看到班级在哪些知识点上普遍薄弱从而调整后续的教学重点。这是我做这个系统之前完全没预料到的需求属于典型的数据产生之后才发现价值。如果你后续想继续扩展还有几个方向可以考虑题库的批量导入导出增强支持从Word/Excel模板中识别题目并入库。考后成绩分析报表按班级、按知识点统计正确率用图表展示。考试监控功能管理员可以实时查看考生的进入状态、交卷状态。对接企业微信、钉钉或飞书登录后自动通知考试时间和考试成绩。在线考试系统的复杂度不在某个单点技术上而在考试这个场景本身蕴含的规则和边界条件。从出题、组卷、答题、判分到成绩分析每一步都有业务的潜规则需要去挖掘和满足。数据库设计阶段多想一步功能实现阶段就能少踩好几个坑。最后再分享一个小技巧开发这类系统时把每一次考试的过程数据都保留下来不要因为感觉没用就清理。这些数据是最真实的系统测试样本。我后来排查很多bug都是靠翻考试记录里的异常数据定位的。系统稳定运行两个月后你再回看这些数据会发现它们比任何单元测试都能说明问题。本文还有配套的精品资源点击获取
返回列表