简介:这份文档面向计算机相关专业准备毕业设计或课程设计的学生,主题为基于Python的主观题自动阅卷系统,可作为选题参考与论文写作范例。压缩包内仅含1个docx文件,大小约656KB,内容围绕系统设计、开发技术简介、需求分析、可行性研究、总体设计及数据库设计展开,并包含E-R图与数据库表实现等章节,便于读者了解考试、阅卷、评分三个模块的组织方式以及Python、MySQL、IDEA、JavaScript等技术选型。已有117人学习或下载,说明该选题在毕业设计场景中具有一定参考需求。读者可从中获取论文目录结构、需求分析写法、系统模块划分和数据库设计思路,用于梳理自己的设计文档框架;需注意包内为论文文档而非可运行源码,适合作为方案论证与写作模板,配合自身项目实践使用。
1. 一套 Python 主观题自动阅卷系统:把老师从两百份卷子里捞出来
期末周最熬人的活不是出卷,是改卷。一位老师带四个班,两百多份卷子,客观题机器扫一遍就完事,剩下的简答、论述、名词解释还得一份份看,看完还要对着评分点核分,核完再录系统。这套基于 Python 的主观题自动阅卷系统,冲的就是这段重复劳动:题库出题、学生在线作答、交卷后系统按评分点算出参考分并标出命中了哪些关键词、漏了哪些,老师只处理需要复核的那部分。它是一份完整的毕业设计资料,源码、MySQL 数据库脚本和论文文档都在里面,技术栈是 Python + MySQL + B/S 模式,前端用原生 HTML/CSS/JS,后台分用户管理、试题管理、试卷管理、在线考试、成绩管理几块。三类人可以直接拿走:要交毕设的学生、想找个完整小型 Web 项目练后端的人、以及需要一套能演示「自动评分」流程的老师。
2. 技术栈与目录结构:一次答卷提交在 B/S 架构里走了哪几层
先把一件事说清楚:这类毕业设计资料的论文正文,技术简介章节往往是套模板写的。翻开放文档能看到 JavaScript、MySQL、Python、IDE 几个小节,再往后又冒出 JSP、PHP 的字样,甚至把 MySQL 说成非关系型数据库。别被带偏,实际代码以 Python 为准,MySQL 就是标准的关系型库。看这套资料的正确姿势是先跑代码,再回头对论文里的需求分析和数据库设计章节,那两部分是真正有价值的。
2.1 技术栈盘点:Python 后端 + MySQL + 原生前端
后端选 Python,最直接的理由是这门语言在大学课程里出现频率最高,出了问题能在教材和公开资料里找到答案。这类体量的系统,常见做法是用 Flask 起一个轻量服务,路由数量在二三十个以内,一个app.py加几个蓝图就能装下,不需要 Django 那套 admin、middleware、settings 的全家桶。如果你手上这份源码用的是 Django 或 FastAPI,也不用慌,路由注册方式和 ORM 写法不同,业务逻辑的分层是一样的,照着第 4 章的评分流程对一遍就能接上。
数据库用 MySQL,存的东西很规整:用户、试题、试卷、试卷与试题的关联、学生答卷、阅卷结果。六张表撑起整个系统,没有复杂的事务和分库分表需求。前端是原生 HTML/CSS/JS,配 Jinja2 之类的模板引擎渲染,页面不多——登录页、后台首页、用户管理、试题管理、试卷管理、在线答题、成绩查看,加起来七八个模板文件。B/S 模式的好处在这儿体现得很实在:改一版页面刷新浏览器就生效,不用像 C/S 那样让每台机器重装客户端。
论文里提到的「系统首页展示服务器 IP、系统型号」这个功能,实现方式一般是后端读取platform和socket模块的信息,塞进模板变量渲染出来。演示价值大于实用价值,二次开发时可以直接砍掉或者换成真实的运行状态面板。
提示:判断这份源码用的是哪个框架,最快的办法是看入口文件顶部的 import。有
from flask import Flask就是 Flask,有django字样就是 Django,两者都没有而是一堆http.server的就是纯标准库手写,那反而更好读。
2.2 目录结构:照着这个骨架找文件
这类项目的目录组织大同小异,我把常见的一种骨架画出来,你打开源码压缩包可以对照着定位:
marking-system/ ├── app.py # 入口:创建应用、注册路由、启动服务 ├── config.py # 数据库连接、密钥、评分配置 ├── requirements.txt # 依赖清单 ├── static/ # 静态资源 │ ├── css/ │ ├── js/ │ └── uploads/ # 学生答卷附件、导入的题库文件 ├── templates/ # 后台与前台页面模板 │ ├── login.html # 后台登录 │ ├── index.html # 后台功能界面(上菜单下操作区) │ ├── user.html # 用户管理 │ ├── question.html # 试题管理 │ ├── paper.html # 试卷管理 │ ├── exam.html # 在线考试 │ └── score.html # 成绩管理 ├── models/ │ ├── db.py # 连接池 / 游标封装 │ ├── user.py # 用户增删改查 │ ├── question.py # 试题读写 │ └── answer.py # 答卷与成绩读写 ├── service/ │ ├── segment.py # 中文清洗、分词、关键词抽取 │ ├── similarity.py # 文本相似度计算 │ └── marker.py # 评分主流程(预处理→打分→落库) ├── sql/ │ └── exam.sql # 建库建表 + 初始数据 └── docs/ # 论文文档、E-R 图、界面截图找代码的顺序建议是从app.py的路由表入手,把所有 URL 打印出来列一遍,再顺着每个路由看它调了models里的哪个函数、service里的哪个方法。这比从目录第一层逐个文件翻要快得多,尤其是你接手的是别人写的、命名还不统一的代码。service/目录是这套系统的价值核心,用户管理和试卷管理都是常规 CRUD,真正需要读懂的是marker.py那几百行。
2.3 数据库设计:从 E-R 图到建表语句
论文的数据库设计章节给了 E-R 图,实体包括管理员、用户、考试信息,属性列得很细:编号、用户名、密码、姓名、性别、年龄、电话、邮箱、地址、身份证号。落到物理表上,管理员和普通用户通常合并成一张user表,用role字段区分权限,这样登录逻辑只需要写一套,权限判断在拦截器里做。试卷表在论文里被记作shijuan,字段有名称、类别、科目、作者、创建时间、修改时间、修改人。
我这里给出一版可以直接执行的建表脚本,字段名和论文里的表基本对得上,补了阅卷流程必需的几张表:
-- 建库:字符集必须显式指定 utf8mb4,否则中文和特殊符号会翻车 CREATE DATABASE IF NOT EXISTS exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE exam_system; -- 用户表:管理员和学生共用,靠 role 区分 CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键,自增', `name` VARCHAR(50) NOT NULL COMMENT '登录名', `pwd` VARCHAR(128) NOT NULL COMMENT '密码,存哈希不存明文', `realname` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `sex` VARCHAR(10) DEFAULT NULL COMMENT '性别', `age` INT DEFAULT NULL COMMENT '年龄', `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', `address` VARCHAR(255) DEFAULT NULL COMMENT '地址', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1教师 2超级管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 试题表:主观题和客观题都放这里,type 区分 CREATE TABLE `question` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `content` TEXT NOT NULL COMMENT '题干', `qtype` TINYINT NOT NULL DEFAULT 1 COMMENT '1主观 2单选 3判断', `ref_answer` TEXT DEFAULT NULL COMMENT '参考答案,主观题必填', `keywords` VARCHAR(500) DEFAULT NULL COMMENT '评分关键词,逗号分隔', `full_score` DECIMAL(5,1) NOT NULL DEFAULT 10.0 COMMENT '满分', `subject` VARCHAR(50) DEFAULT NULL COMMENT '科目', `creator` INT DEFAULT NULL COMMENT '出题人 user.id' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试题表'; -- 试卷表:保留论文里的字段命名习惯 CREATE TABLE `shijuan` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(255) NOT NULL COMMENT '试卷名称', `newsType` VARCHAR(50) DEFAULT NULL COMMENT '试卷类别', `author` VARCHAR(50) DEFAULT NULL COMMENT '出卷人', `makeTime` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `maker` VARCHAR(50) DEFAULT NULL, `modiTime` DATETIME DEFAULT NULL COMMENT '最后修改时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试卷表'; -- 答卷表:一个学生一道题一行,方便逐题评分和复核 CREATE TABLE `answer` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `paper_id` INT NOT NULL COMMENT '试卷 id', `question_id` INT NOT NULL COMMENT '试题 id', `user_id` INT NOT NULL COMMENT '作答学生', `content` TEXT COMMENT '学生作答原文', `submit_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_paper_q_user` (`paper_id`,`question_id`,`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答卷表'; -- 阅卷结果表:系统分和人工分分开存,复核时只改人工分 CREATE TABLE `mark_result` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `answer_id` INT NOT NULL UNIQUE, `auto_score` DECIMAL(5,1) DEFAULT 0.0 COMMENT '系统算出的分', `final_score` DECIMAL(5,1) DEFAULT 0.0 COMMENT '最终分,默认等于系统分', `hit_keywords` VARCHAR(500) DEFAULT NULL COMMENT '命中的评分点', `miss_keywords` VARCHAR(500) DEFAULT NULL COMMENT '未命中的评分点', `status` TINYINT DEFAULT 0 COMMENT '0待复核 1已确认 2已人工改分', `marker_id` INT DEFAULT NULL COMMENT '复核教师', `mark_time` DATETIME DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阅卷结果表';几个设计上的取舍值得说明。answer表加了(paper_id, question_id, user_id)唯一索引,这是防重复提交最省事的办法,学生手抖点两次提交,第二次直接触发唯一键冲突,后端捕获后返回「已提交」即可,不用在前端做花哨的防抖。mark_result把auto_score和final_score拆成两列,是为了保留系统的原始判断——老师改分之后还能回看「系统给了几分、我改成了几分」,这个差值积累下来就是调权重的依据。论文里那张user表字段类型写得比较随意,sex用了 VarChar(50) 还给了 255 的长度,实际建表按上面的来就行,长度按业务给,别照抄。
3. 环境搭建与跑通:从空目录到后台登录页的完整步骤
拿到资料后别急着读代码,先把环境跑起来。跑通一次登录,你后面看任何模块都有底气;跑不通,读再多代码都是纸上谈兵。
3.1 依赖安装与配置项
Python 版本用 3.8 以上都行,这类项目的语法不会用到太新的特性。MySQL 用 5.7 或 8.0 都可以,注意 8.0 默认的认证插件变了,老版本的连接驱动可能连不上,遇到Authentication plugin 'caching_sha2_password' cannot be loaded就去改用户密码插件或者升级驱动。依赖清单在requirements.txt里,实际装的时候主要就三样:
# 建议先建虚拟环境,别把全局 Python 搞脏 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖:Web 框架、MySQL 驱动、中文分词 pip install flask pymysql jieba cryptography最后那个cryptography容易被漏掉,MySQL 8.0 的加密连接依赖它,缺了会报cryptography package is required for sha256_password。装完之后进config.py改配置,这个文件通常长这样:
# config.py:数据库和评分相关的配置集中在这里 import os class Config: # 数据库连接:host/port 按你本机实际情况改 DB_HOST = os.getenv('DB_HOST', '127.0.0.1') DB_PORT = int(os.getenv('DB_PORT', 3306)) DB_USER = os.getenv('DB_USER', 'root') DB_PASS = os.getenv('DB_PASS', '123456') # 改成你的密码 DB_NAME = os.getenv('DB_NAME', 'exam_system') # 用环境变量兜底,本地开发不设也能跑 SECRET_KEY = os.getenv('SECRET_KEY', 'dev-only-change-me') # 评分权重:三部分加起来等于 1.0 W_KEYWORD = 0.55 # 关键词命中权重 W_SIMILAR = 0.35 # 整体文本相似度权重 W_LENGTH = 0.10 # 答案长度合理性权重 # 相似度低于该阈值直接判 0 分,避免「写满一段废话也得分」 MIN_SIMILAR = 0.12密码和密钥这种敏感项,正式部署时必须走环境变量,别把真实密码提交到代码仓库。本地调试为了方便写死也行,但要记得上线前清掉。
3.2 建库建表与初始数据
建库有两种路子:执行sql/exam.sql,或者手动跑上一节的建表语句。推荐前者,因为脚本里通常还带了初始数据——至少得有一条管理员账号,否则你连登录页都进不去。
# 方式一:直接导入脚本 mysql -u root -p < sql/exam.sql # 方式二:进交互模式,手动执行 mysql -u root -p # 进去之后:source sql/exam.sql;如果脚本里没带初始账号,自己插一条。密码别存明文,用 Werkzeug 的generate_password_hash生成:
# 生成管理员密码哈希,把结果贴进 INSERT 语句 from werkzeug.security import generate_password_hash print(generate_password_hash('admin123')) # 输出形如 pbkdf2:sha256:260000$xxxx$yyyy...-- 插入一条超级管理员,密码字段填上面生成的哈希 INSERT INTO `user` (`name`, `pwd`, `realname`, `role`) VALUES ('admin', '把上面打印的哈希贴到这里', '系统管理员', 2);导入完成后做一次验证,这条命令能查出表数量,说明库和表都建好了:
mysql -u root -p -e "USE exam_system; SHOW TABLES;" # 预期看到 user / question / shijuan / answer / mark_result 等表3.3 启动、登录与首轮自测
启动命令很简单,python app.py或者flask run都行。控制台打出Running on http://127.0.0.1:5000就说明服务起来了。浏览器打开这个地址,会跳到登录页,用刚才建的管理员账号登进去。
接下来按这条链路走一遍,每一步都是一个可验证的检查点:
- 登录后台,能看到左侧或上方的功能菜单(用户管理、试题管理、试卷管理、在线考试、成绩管理);
- 用户管理里新增一个学生账号,确认列表刷新后有数据,说明用户表读写通了;
- 试题管理里录一道主观题,题干随便写,参考答案认真填一段,关键词填三到五个,满分设 10 分;
- 试卷管理里新建一张卷子,把刚才那道题加进去;
- 用学生账号登录,找到这张卷子,答一段文字提交;
- 切回教师账号,进成绩管理,看这道题有没有出现系统分和命中关键词。
六步走完,说明前后端、数据库、评分链路全通了。哪一步卡住,问题范围就锁定在那一步对应的模块,比漫无目的地读代码高效得多。
注意:第 6 步如果系统分是 0,先别怀疑算法。绝大多数情况是参考答案为空、关键词字段没填、或者学生答案短到分词后全是停用词被过滤干净了。这三种都属于配置问题,不是代码 bug。
4. 核心模块拆解:一段主观题答案是怎么被算成 7.5 分的
前面都是铺垫,真正决定这套系统好不好用的是评分模块。主观题自动阅卷没有标准答案,只能靠几个可量化的信号拼出一个近似判断。这套源码的思路,也是同类项目最常见的做法:关键词命中率占大头,整体文本相似度做辅助,长度合理性做兜底修正。三块加权求和,再映射到满分区间。
4.1 答卷文本预处理与关键词抽取
学生提交的答案不能直接拿去比。里面混着空格、换行、全角标点、语气词,还有「我认为」「大概」这类对评分毫无贡献的表达。预处理做三件事:清噪、分词、抽词。
# service/segment.py:中文文本清洗与关键词抽取 import re import jieba import jieba.analyse # 停用词表:实际项目里从文件加载更合适,这里内嵌一份小的够用 STOPWORDS = { '的', '了', '和', '是', '在', '我', '有', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '会', '着', '没有', '看', '好', '自己', '这', '那', '因为', '所以', '但是', '如果', '可以' } def clean_text(text: str) -> str: """去掉多余空白和不可见字符,统一全角标点为半角""" if not text: return '' text = text.replace('\u3000', ' ') # 全角空格 text = re.sub(r'[\r\n\t]+', ' ', text) # 换行制表统一成空格 text = re.sub(r'\s{2,}', ' ', text) # 连续空格压成一个 return text.strip() def tokenize(text: str) -> list: """分词并过滤停用词、单字、纯数字,返回有效词列表""" words = jieba.lcut(clean_text(text)) return [ w for w in words if len(w) > 1 and w not in STOPWORDS and not w.isdigit() ] def extract_keywords(text: str, topk: int = 8) -> list: """用 TF-IDF 抽关键词;参考答案短时,topk 别设太大""" text = clean_text(text) if not text: return [] # withWeight=False 只返回词,allowPOS 限定词性能过滤掉大量虚词 return jieba.analyse.extract_tags( text, topK=topk, withWeight=False, allowPOS=('n', 'nz', 'vn', 'v') )extract_tags的三个参数值得说清楚:topK控制抽几个词,参考答案只有一两句话的时候设 8 就够了,设 20 会把无关词也拉进来稀释权重;withWeight=False表示只要词不要权重,如果你想把关键词按权重排序展示给老师看,改成True会返回(词, 权重)的元组列表;allowPOS限定词性,n是名词、nz是专有名词、vn是动名词、v是动词,这四个词性基本覆盖了评分点,能把「很」「非常」这类副词直接挡在外面。
预处理这块有个隐含前提:参考答案的质量决定上限。如果老师在试题管理里只填了「答对即可」四个字,那系统再聪明也抽不出评分点。所以在录入界面最好加个自动抽取按钮,让系统先从参考答案里抽一版关键词给老师确认和微调,比让老师手敲逗号分隔的词靠谱得多。
4.2 相似度打分与分数映射
三个信号里,关键词命中是最贴近人工阅卷逻辑的:老师看卷子,本质就是看学生有没有踩到那几个得分点。所以权重给到 0.55。整体相似度用词袋余弦,衡量表达的贴近程度,权重 0.35。长度合理性 0.10,作用是压制两种极端——写了两个字就想拿分,以及复制了一大段无关内容凑字数。
# service/marker.py:主观题评分主流程 import math from collections import Counter from service.segment import tokenize, extract_keywords, clean_text def cosine_similarity(text_a: str, text_b: str) -> float: """词袋余弦相似度,返回 0~1""" vec_a = Counter(tokenize(text_a)) vec_b = Counter(tokenize(text_b)) if not vec_a or not vec_b: return 0.0 # 只对共有的词做点积,省掉构建完整词表 common = set(vec_a) & set(vec_b) dot = sum(vec_a[w] * vec_b[w] for w in common) norm_a = math.sqrt(sum(v * v for v in vec_a.values())) norm_b = math.sqrt(sum(v * v for v in vec_b.values())) return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0 def length_factor(student_len: int, ref_len: int) -> float: """长度合理性:落在参考答案 0.6~1.5 倍之间给满分权重""" if ref_len <= 0: return 0.5 ratio = student_len / ref_len if 0.6 <= ratio <= 1.5: return 1.0 if ratio < 0.6: return max(0.0, ratio / 0.6) # 太短按比例衰减 return max(0.3, 1.5 / ratio) # 太长缓慢衰减,最低 0.3 def score_answer(student_text: str, ref_answer: str, full_score: float = 10.0, keywords: list = None, w_kw: float = 0.55, w_sim: float = 0.35, w_len: float = 0.10, min_similar: float = 0.12) -> dict: """ 返回:score 最终分、hit 命中词、miss 未命中词、detail 三段明细 """ student_text = clean_text(student_text) ref_answer = clean_text(ref_answer) if not student_text: return {'score': 0.0, 'hit': [], 'miss': keywords or [], 'detail': {}} # 没传关键词就从参考答案现抽 kws = keywords or extract_keywords(ref_answer, topk=8) # 1) 关键词命中率:学生答案里出现了几个评分点 hit = [k for k in kws if k in student_text] miss = [k for k in kws if k not in student_text] kw_rate = len(hit) / len(kws) if kws else 0.0 # 2) 整体相似度 sim = cosine_similarity(student_text, ref_answer) # 3) 长度合理性 len_rate = length_factor(len(student_text), len(ref_answer)) # 加权求和,得到 0~1 的原始分 raw = w_kw * kw_rate + w_sim * sim + w_len * len_rate # 相似度太低直接压到 0,防止「全写满但跑题」拿到基础分 if sim < min_similar: raw = 0.0 final = round(raw * full_score, 1) return { 'score': final, 'hit': hit, 'miss': miss, 'detail': {'kw_rate': round(kw_rate, 3), 'similarity': round(sim, 3), 'length_rate': round(len_rate, 3)} }参数逐个解释一遍,方便你按自己的学科特点调:
| 参数 | 含义 | 建议取值范围 | 调整影响 |
|---|---|---|---|
full_score | 题目满分 | 5~20 | 直接放大或缩小最终分 |
keywords | 评分点列表 | 3~10 个 | 越少越宽松,越多越严格 |
w_kw | 关键词权重 | 0.45~0.65 | 偏概念类题目调高 |
w_sim | 相似度权重 | 0.25~0.40 | 偏论述类题目调高 |
w_len | 长度权重 | 0.05~0.15 | 不建议超过 0.2 |
min_similar | 相似度下限 | 0.08~0.20 | 调高更严格,误判更少 |
实际跑下来你会发现,min_similar这个阈值是血泪经验攒出来的。设成 0 的话,学生写「我不知道」四个字,只要碰巧和一个评分点重了,就能拿到零点几分;设成 0.3 又太狠,正常的同义表述全被判死。0.12 附近是个比较稳的起点,具体数值要拿自己学科的几十份真实答卷试出来。
4.3 阅卷结果落库与人工复核
算完分只是中间态,必须落库并留出人工复核的口子。教师端看到的不是一串数字,而是「系统分 7.5,命中 3 个评分点,漏了 2 个」,点开能看学生原文,改完分数留痕。
# service/marker.py 续:评分结果落库 from models.db import get_conn def save_mark(answer_id: int, result: dict) -> int: """把评分结果写入 mark_result,重复评分走更新""" sql = """ INSERT INTO mark_result (answer_id, auto_score, final_score, hit_keywords, miss_keywords, status) VALUES (%s, %s, %s, %s, %s, 0) ON DUPLICATE KEY UPDATE auto_score = VALUES(auto_score), final_score = VALUES(final_score), hit_keywords = VALUES(hit_keywords), miss_keywords = VALUES(miss_keywords) """ with get_conn() as conn: with conn.cursor() as cur: cur.execute(sql, ( answer_id, result['score'], result['score'], # 初始时最终分等于系统分 ','.join(result['hit']), ','.join(result['miss']) )) conn.commit() return cur.lastrowidON DUPLICATE KEY UPDATE这段是给「重新评分」用的。老师改了参考答案或者调了权重,需要重跑一遍,直接覆盖系统分即可,但status要判断一下:如果这条记录已经被人工确认过(status=1或2),重跑时不该覆盖final_score,只更新auto_score和命中词,让人工判断保留下来。实现上把final_score = VALUES(final_score)改成条件判断,或者干脆在应用层先查一次状态再决定走 INSERT 还是 UPDATE。
复核界面拿数据就是一个三表 JOIN,直接用视图会更省事:
-- 教师复核列表:一份答卷的原文、参考答案、系统分、命中情况 SELECT a.id AS answer_id, u.realname AS student, q.content AS question, a.content AS student_answer, q.ref_answer, m.auto_score, m.final_score, m.hit_keywords, m.miss_keywords, m.status FROM answer a JOIN `user` u ON u.id = a.user_id JOIN question q ON q.id = a.question_id LEFT JOIN mark_result m ON m.answer_id = a.id WHERE a.paper_id = 1 AND m.status = 0 -- 只看待复核的 ORDER BY a.submit_time DESC;这套流程跑顺之后,一个班的卷子老师只需要看status=0的那些,系统判断明显的(全命中或全未命中)直接批量确认,工作量的下降是很实在的。
5. 常见问题排查:启动报错、评分全 0、中文乱码这些坑
这一节记录的是这类项目最容易翻车的几个位置,每条按「现象 → 原因 → 解决」写,照着对号入座就行。
5.1 环境与部署类问题
现象一:ModuleNotFoundError: No module named 'flask',但明明装过。
原因是装进了全局 Python,而运行用的是虚拟环境,或者反过来。pip -V和python -c "import sys; print(sys.executable)"两条命令一比,就能看出 pip 装到了哪里、python 从哪跑。解决办法是统一在虚拟环境里操作,激活后先which pip确认路径含有venv,再装依赖。
现象二:连数据库报Access denied for user 'root'@'localhost'。
九成是config.py里的密码没改,还是写死的占位符。也有一种情况是 MySQL 8.0 的认证插件问题,报错会变成caching_sha2_password。前者改配置,后者执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';之后再试。如果报的是Can't connect to MySQL server on '127.0.0.1',那就跟密码无关,去确认 MySQL 服务起没起、端口是不是 3306。
现象三:页面能打开,但所有中文都是问号或乱码。
这是字符集链路上某一环断了,可能是建库没指定字符集、表的默认字符集是latin1、或者 Python 连接串没带charset。三个地方一起检查:建库语句有没有DEFAULT CHARSET=utf8mb4,SHOW CREATE TABLE user;看表的字符集,连接代码里是不是pymysql.connect(..., charset='utf8mb4')。缺哪个补哪个,已经建好的表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4;转过来。
5.2 数据与评分逻辑类问题
现象四:所有主观题的系统分都是 0.0。
按这个顺序查:先看question表里那道题的ref_answer是不是空,空了就没法评分;再看keywords字段有没有值,没值会走自动抽取,而参考答案太短时抽出来的词可能一个都匹配不上;最后看mark_result表里的hit_keywords,如果命中了但分数还是 0,那问题出在min_similar这个阈值把结果压掉了,临时把它调成 0 验证一下。
现象五:分数偶尔出现 7.0 变成 7 的情况,小数位丢了。
数据库字段类型给成了INT或者DECIMAL(5,0)。评分函数返回的是round(x, 1)的一位小数,字段没有小数位就会被截断。把auto_score和final_score改成DECIMAL(5,1),同时检查 Python 侧有没有在写入前做了int()转换。
现象六:学生重复提交后报 500,日志里是Duplicate entry。
这是唯一索引在起作用,属于预期行为,但后端没捕获异常。在提交接口里 try/except 捕获pymysql.err.IntegrityError,返回「该试卷已提交,请勿重复操作」的提示页,而不是让异常冒到前端。顺带一提,提交按钮在前端也应该在点击后置灰,双保险。
现象七:修改了权重参数,分数完全没变。
大概率是参数写死在函数默认值里,而调用处又显式传了一套旧值;或者服务是常驻进程,改完config.py没重启。参数改完先重启服务,再找调用score_answer的地方看有没有覆盖默认参数。这个坑我自己踩过,查了半小时代码,最后发现是忘了重启。
6. 二次开发:把演示级阅卷改成能进课堂的版本
跑通之后你会发现这套系统「能演示」和「能上课用」之间还差一截。下面是我认为性价比最高的几个改造方向,按投入产出排了序。
| 改造点 | 主要改动文件 | 验证方法 |
|---|---|---|
| 评分权重按科目配置 | config.py、question加subject关联表 | 同一份答卷在不同科目下分数不同 |
| 相似度换成语义向量 | service/similarity.py | 同义不同词的答案能拿到相近分数 |
| 批量复核与导出 | 新增export.py、成绩页加按钮 | 导出的表格字段与页面一致 |
| 阅卷留痕与回滚 | mark_result加历史表 | 改分后能查到改前改后的值 |
语义相似度这块说细一点。词袋余弦的死穴是同义表述,「提升效率」和「提高工作效率」在词袋里几乎不重叠,但意思完全一样,学生答对了却拿不到分,这是最容易被老师质疑的地方。想改的话,可以在similarity.py里加一层:先算词袋分数,如果低于阈值再调语义模型兜底。改造时把接口签名固定成def calc(text_a, text_b) -> float,内部实现随便换,调用方一行不用动。
# service/similarity.py:保留原接口,内部可切换实现 from service.segment import cosine_similarity def calc(text_a: str, text_b: str, use_semantic: bool = False) -> float: """统一入口:默认词袋,需要时切语义模型,调用方无感""" base = cosine_similarity(text_a, text_b) if not use_semantic or base > 0.5: return base # 语义模型按需加载,别在模块顶层 import,启动会慢好几秒 from service.semantic import embed_similarity sem = embed_similarity(text_a, text_b) return max(base, sem * 0.85) # 语义分打个折,避免过度宽松验证改造有没有效果,别凭感觉。挑二十份真实答卷,先人工打一遍分作为基准,再跑系统分,算两者的平均绝对误差。误差从两分降到一分以内,这个改造就算成了;如果反而变大,多半是语义模型的相似度普遍偏高,那个 0.85 的折扣系数还得往下调。
还有一个容易被忽略的点:这套资料的论文文档本身就是资产。需求分析章节里的功能模块划分、数据库设计章节里的 E-R 图,改一改就能当你自己项目的设计文档初稿。但技术简介那部分别照抄,里面几个技术名词的说法不准确,答辩的时候容易被追问。我一般会先把文档里的技术章节删掉重写,只留需求、设计、测试三块,这样交付出去的东西自己心里有底。
从那以后我每次接手这类毕设级项目,都强制走一遍「先跑通登录、再录一道题、最后看分数」的三步验证,跑不通就不往下读代码——顺序反了,读得越多越迷糊。希望帮到你。
本文还有配套的精品资源,点击获取