基于Django+Vue的党史学习考试系统开发实战

发布时间:2026/10/7 3:21:35
基于Django+Vue的党史学习考试系统开发实战 最近在做一个基于Python加Vue的党员党史研究学习考试管理系统后端主用Django前端Vue开发工具选的PyCharm。整个项目从需求理解到部署上线横跨了数据建模、接口开发、前端页面、联调测试和服务器部署几大块做下来最大的感受是这种系统远不是简单搭个增删改查页面那么轻省很多细节在动手前没想清楚后面就是连环返工。这个系统到底要解决什么问题其实就是一个标准的学习培训闭环把党史学习资料统一管理起来让用户在系统里完成资料阅读、视频观看、在线答题管理员负责维护题库、组织考试、查看成绩和统计学习情况。它自带内容管理、考试流程、权限控制和数据统计几个核心能力很适合拿来练手一套完整的前后端分离项目。不管你是要做同类内部学习平台还是想找一个能写进简历的全栈项目下面的内容都可以直接参考。1. 系统需求拆解与业务模块规划1.1 先看清楚业务闭环再写代码做这种项目最容易犯的错误就是一上来就建表、写登录、加菜单最后发现业务走不通。我建议先把业务闭环画出来这个系统本质上是一条链资料入库 → 学习过程 → 发起考试 → 在线答题 → 自动判分 → 成绩统计 → 结果回溯。每一步要有什么数据支撑提前列清楚后面写代码就轻松。打个比方这就跟驾考APP一样用户报名、看视频、约考、考完出分、教练看通过率。把里面“驾考内容”换成党史学习资料把“学员”换成党员用户业务形态是一致的。理解了这一点设计系统时就有了主线一切功能都要回扣“学”和“考”。学要留痕考要公平成绩要可追溯。我见过不少项目把学习模块做成纯列表页面点击一下就算学过时间、进度、完成率全是假的这种系统上线后基本没有说服力。所以设计第一版时我给自己定的原则是学习过程要有记录考试过程要有防作弊和断点续答成绩出来后要有维度和时间线的回溯能力。1.2 功能模块与角色权限怎么切分规划后台系统最怕功能堆在一起。我按业务语义把系统拆成六个核心模块每个模块的职责相对独立模块主要功能涉及的核心数据用户与组织管理用户账号、组织架构、角色分配用户表、组织表、角色表内容管理党史文献、课程视频、PDF课件、专题分类资料表、分类表学习管理学习计划、进度记录、完成状态学习记录表考试管理题库维护、组卷规则、在线答题、自动判分试题表、试卷表、答卷表成绩中心成绩查询、通过率、组织维度统计成绩表、考试记录表系统运维操作日志、数据字典、后台配置日志表、字典表角色权限这里要重点设计它决定了后台菜单和接口的复杂度。这个系统需要三类角色系统管理员管用户、管组织、管系统配置权限最高。内容与考务管理员维护学习资料、题库、试卷查看考试成绩不碰用户账号。普通学员登录后进入学习中心、考试中心、个人成绩只能看自己范围内的内容。权限控制不能只靠前端隐藏菜单后端每个接口都必须校验角色。我的做法是登录接口返回当前用户角色码前端根据角色码渲染不同菜单后端接口用装饰器或中间件做二次鉴权这样即使有人手动调接口也拿不到越权数据。2. 技术选型背后的取舍2.1 后端选型Django还是Flask标题里同时出现了Django和Flask说明很多人在这个项目上是有犹豫的。我先把两者差异讲清楚再告诉你我的结论。经常有人拿Flask和FastAPI比性能这类对比对业务系统意义不大。我们这个项目是典型的IO密集型加事务密集型场景瓶颈在数据库查询、鉴权、判分逻辑的准确性框架本身的性能差异根本体现不出来反而是框架的成熟生态能省下大量时间。Django自带ORM、迁移、Admin后台、用户认证体系做一个需要后台管理的业务系统再合适不过。对比一下维度DjangoFlaskORM内置且成熟迁移工具齐全需要自己组合SQLAlchemyAdmin后台自带开发期效率极高需要额外接入flask-admin权限认证自带User、Group、Permission体系需要引入flask-login或自己写项目结构约束较强适合规范化团队灵活度高但也容易自由到失控学习曲线概念多前期略陡上手快但组合成本在后面我的建议是如果你有开发团队、后续要持续迭代选Django如果你只是要提供轻量API、快速出活选Flask也不差。但做党史学习考试这种自带后台管理需求的系统Django的完整度明显更高所以我主项目用DjangoFlask版本只作为轻量接口方案做了对比研究。2.2 前端为什么选Vue后台管理类系统的前端Vue一直比React更顺手因为配套的Element Plus组件库几乎把表格、表单、弹窗、菜单全部封装好了开发效率能明显提升。这个项目用Vue3加Vite。Vite的冷启动和热更新比Webpack快太多了开发时改代码基本秒级刷新。配套组件我选了Element Plus、Pinia做状态管理、vue-router做路由、axios做请求。没有用任何重型中后台脚手架因为那类脚手架配置项太多反而不利于理解路由和权限的底层逻辑。2.3 PyCharm在项目里的实际作用PyCharm不是可有可无的编辑器。在这个项目里它承担了解释器管理、虚拟环境创建、接口调试、数据库面板、版本管理几个关键职责。使用PyCharm的Project Interpreter创建venv所有依赖都装在项目内部换机器部署不会出现包冲突。Django项目的run配置还可以直接支持runserver、makemigrations等常用命令配合断点调试排查后端问题时能直接看到每一层调用关系和数据值。如果你没有PyCharm专业版授权社区版也够用只是少了数据库面板和一些前端框架支持。我不建议做破解激活这类操作用社区版完全能撑起这个项目。3. 开发环境搭建与项目初始化3.1 Python环境准备的几个坑这个项目我建议用Python 3.10或3.11别一上来追最新版。3.12在中后期可能遇上某些依赖包还没适配的情况没必要给项目添堵。安装Python时Windows用户一定要在安装向导里勾选Add Python to PATH否则后面在终端敲python命令会提示找不到。装完验证一下python --version pip --version创建独立虚拟环境是必须做的事。命令很简单mkdir party_study cd party_study python -m venv venv # Windows激活 venv\Scripts\activate # Mac/Linux激活 source venv/bin/activate如果你的项目后续要处理成绩报表、数据分析可能会用到numpy和pandas。这类包安装不顺利通常有两个原因一是当前Python版本太新二是默认源下载慢。直接指定国内镜像源装一遍基本都能解决pip install numpy pandas -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 创建Django项目和应用后端项目结构我建议按业务边界拆应用不要把所有模型写在同一个models.py里。我用Django默认的manage.py方式创建pip install django djangorestframework djangorestframework-simplejwt django-admin startproject config . python manage.py startapp users python manage.py startapp materials python manage.py startapp exams python manage.py startapp learning这里有个容易踩的坑应用名不要和Python内置模块或第三方包重名比如test、site、dist。曾经有人把应用命名为test结果引入了无数让人摸不着头脑的冲突。创建完应用后记得去config/settings.py的INSTALLED_APPS里注册这一步漏了会导致数据库迁移时找不到表。我还建议直接建一个templates目录和一个static目录虽然纯DRF后端用不到模板渲染但Django项目的目录结构保持完整后面对接Nginx时逻辑更清爽。3.3 初始化Vue3项目与代理配置Vue部分用Vite创建项目这里有一个需要提前规划的点前后端分离联调时接口跨域问题一定会遇到与其后端写一堆CORS配置不如让开发环境的Vite代理帮你转发这样最快。初始化命令我用的npm create vuelatest选上Router、Pinia其他按需勾选。安装完依赖后在项目根目录的vite.config.js里配置代理import { fileURLToPath, URL } from node:url import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })配置完成后前端所有以/api开头的请求都会被转发到Django的8000端口开发时就不存在跨域问题。但注意生产环境不能依赖这个代理后面部署时要交给Nginx处理。4. 后端核心功能实现Django版4.1 数据模型设计数据模型是整个系统的地基。我建模型时坚持一个原则能记录历史的都不删除能用外键关联的都关联起来。以用户、学习资料、试题答案、考试记录四条主线为例# users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class UserProfile(AbstractUser): ROLE_CHOICES ( (admin, 系统管理员), (teacher, 内容与考务管理员), (student, 学员), ) role models.CharField(max_length20, choicesROLE_CHOICES, defaultstudent) org_name models.CharField(max_length100, blankTrue, verbose_name所属组织) phone models.CharField(max_length20, blankTrue) class Meta: db_table sys_user# materials/models.py from django.db import models class Category(models.Model): name models.CharField(max_length50, uniqueTrue) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE) class Meta: db_table material_category class Material(models.Model): title models.CharField(max_length200) category models.ForeignKey(Category, on_deletemodels.PROTECT) content_type models.CharField(max_length20, choices( (pdf, PDF文档), (video, 视频), (article, 图文), )) file_url models.CharField(max_length500, blankTrue) video_url models.CharField(max_length500, blankTrue) content models.TextField(blankTrue) is_published models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table material_info这里有两个细节。一是外键删除策略Material关联的Category我用了PROTECT防止分类下还有资料时被误删ExamRecord关联Paper用CASCADE因为试卷删了答题记录留着没意义。二是考试相关模型必须考虑多选判分的存储方式我的题目表用JSON字段保存选项答案保存为字符串数组的JSON序列化形式扩展性比固定四个选项字段好得多。4.2 登录认证与权限控制这个项目没有用Django自带的session认证而是用了JWT。原因是前后端分离后Vue页面通常部署在静态服务器API部署在后端用session需要处理跨域Cookie问题JWT在请求头里带token就完了简单直接。我用的djangorestframework-simplejwt配置方法不复杂# config/settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), DEFAULT_PERMISSION_CLASSES: ( rest_framework.permissions.IsAuthenticated, ), } from datetime import timedelta SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), }登录返回的token里可以带上用户ID和角色前端解析出来就能决定渲染什么菜单# users/views.py from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response from django.contrib.auth import authenticate api_view([POST]) permission_classes([AllowAny]) def login_view(request): username request.data.get(username) password request.data.get(password) user authenticate(usernameusername, passwordpassword) if not user: return Response({detail: 账号或密码错误}, status401) refresh RefreshToken.for_user(user) return Response({ access: str(refresh.access_token), refresh: str(refresh), role: user.role, username: user.username, })后端接口的权限校验我建议先写一个全局的权限类校验当前用户角色和接口要求的角色是否匹配。不要把权限逻辑散落到每个视图函数里那样后期维护是灾难。4.3 自动组卷与自动判分逻辑考试模块的核心难点不在页面而在组卷规则和判分逻辑。设计时我给管理员提供了手动选题和自动组卷两种方式。自动组卷的规则我控制为四个参数题型数量、总题数、知识点分布比例、难度比例。后端收到组卷请求后在数据库里按规则做随机抽样# exams/services.py def build_auto_paper(category_id, single_count, multi_count, judge_count): from .models import Question, Paper questions [] single_pool Question.objects.filter( category_idcategory_id, q_typesingle, is_activeTrue ) multi_pool Question.objects.filter( category_idcategory_id, q_typemulti, is_activeTrue ) judge_pool Question.objects.filter( category_idcategory_id, q_typejudge, is_activeTrue ) # 校验数量充足 if single_pool.count() single_count: raise ValueError(单选题库存不足请减少数量或补充题库) # 随机抽样 singles list(single_pool.order_by(?)[:single_count]) multis list(multi_pool.order_by(?)[:multi_count]) judges list(judge_pool.order_by(?)[:judge_count]) questions singles multis judges # 存入试卷表并关联题目判分逻辑这里要特别谨慎。单选题和判断题直接比对答案就行多选题如果要求全对才得分那就必须把用户提交的选项列表排序后与标准答案做集合比较避免用户少选或错选也得分# exams/services.py def score_answer(question, user_answer): if question.q_type single: return 1 if user_answer question.correct_answer else 0 if question.q_type judge: return 1 if user_answer question.correct_answer else 0 if question.q_type multi: # 用户答案和标准答案都转成集合比较 user_set set(user_answer) correct_set set(question.correct_answer) if user_set correct_set: return question.score return 0多选题按全对才得分有一个好处规则简单透明学员查分时容易理解。如果你要做部分给分评分规则会更复杂需要定义选对一个得几分、错选扣不扣分这类需求不是必须的不建议第一版就实现。主观题怎么处理我的方案是考试区分客观题和主观题客观题全自动判分主观题先让学生提交再让内容管理员在后台人工打分。实际做下来这个设计很重要因为纯客观题考试永远无法评价学习深度而人工判分又不能让普通用户等待太久两者结合最稳妥。4.4 查询、分页与删除对象时的坑Django的查询和删除是日常操作但在这个项目里我踩过几个值得记录的坑。先看一个复杂查询场景考试记录要做多条件筛选按用户组织、按时间段、按成绩范围、按试卷名称。直接用filter链式调用没问题但如果条件不是全都有值就要小心空条件。推荐用Q对象组合from django.db.models import Q def query_exam_records(org_nameNone, start_dateNone, end_dateNone, passedNone): filters Q() if org_name: filters Q(user__org_name__icontainsorg_name) if start_date: filters Q(exam_time__gtestart_date) if end_date: filters Q(exam_time__lteend_date) if passed is not None: filters Q(is_passpassed) records ExamRecord.objects.filter(filters).select_related(user, paper) return records注意这里用了select_related因为查询成绩列表时要展示用户姓名和试卷名称不预查询的话每行记录都会额外产生一次数据库查询列表页一旦数据量大接口响应时间会呈线性恶化。删除对象这里要重点说。学习考试系统的数据是有审计价值的用户答过什么题、得多少分、学过什么资料这些记录删除后无法重新生成。Django的delete()会直接物理删除记录我实际项目中基本不用物理删除而是给需要保护的数据表统一加一个is_deleted字段做逻辑删除。查询时默认过滤掉class ExamRecord(models.Model): is_deleted models.BooleanField(defaultFalse) # 查询时 ExamRecord.objects.filter(is_deletedFalse)这种方式唯一要注意的是凡是列表查询都要记得加过滤条件否则逻辑删除后数据仍然显示在列表里反而比物理删除更麻烦。我后来写了一个公共的QuerySet管理器统一处理这个逻辑保证所有查询都默认过滤。还有一个分页问题。DRF的PageNumberPagination没问题但当数据到达几十万级简单的offset分页会有性能问题。第一版不要过度设计先把PageNumberPagination配好每页默认20条前端传page参数来控制# config/settings.py REST_FRAMEWORK.update({ DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20, })5. Vue前端搭建与联调5.1 页面结构与路由守卫前端项目我按功能区块组织了页面目录结构大概是这样的src/ ├── api/ │ ├── auth.js │ ├── exam.js │ ├── material.js ├── router/ │ ├── index.js ├── stores/ │ ├── user.js ├── views/ │ ├── LoginView.vue │ ├── LayoutView.vue │ ├── study/ │ ├── exam/ │ └── admin/路由设计了登录页、主布局和若干子页面。这里重点说一下动态权限路由。后端登录接口返回角色后前端要根据角色筛选出可访问的路由避免管理员账户看到学员页面菜单。我的实现方式是在路由meta里配置roles字段然后在全局前置守卫里做判断// router/index.js { path: /admin, component: LayoutView, meta: { roles: [admin] }, children: [ { path: questions, component: () import(/views/admin/QuestionManage.vue) }, { path: papers, component: () import(/views/admin/PaperManage.vue) } ] }// router/guard.js router.beforeEach((to, from, next) { const userStore useUserStore() if (!userStore.token) { if (to.path /login) return next() return next(/login) } if (to.meta.roles to.meta.roles.length 0) { if (!to.meta.roles.includes(userStore.role)) { return next(/403) } } next() })这种做法能挡住页面跳转层面的大部分越权访问同时再配合后端接口鉴权双保险。5.2 Axios封装与token管理axios封装是整个前端项目的工程化基础。我做了一层统一封装把所有请求都经过拦截器处理。拦截器里做了三件事自动携带token、处理401跳登录、统一弹错误提示。// src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router import { useUserStore } from /stores/user const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) ElMessage.error(登录已过期请重新登录) } else { const msg error.response?.data?.detail || error.message || 请求失败 ElMessage.error(msg) } return Promise.reject(error) } ) export default request这里提醒一下代理配置的baseURL用的是/api是因为开发环境有Vite代理生产环境前面还会加一段Nginx反向代理所以这个baseURL在两种场景下都能直接工作不需要频繁改配置。5.3 考试页面的关键交互考试页面是这个项目前端最花心思的地方基础需求是倒计时、答题卡、交卷确认进阶需求是防作弊和断点续答。倒计时用setInterval实现这个大家都知道但有个坑页面切换或组件卸载时必须清理定时器否则计时器会重复启动导致倒计时紊乱。我在beforeUnmount钩子里清掉了定时器let timer null onMounted(() { timer setInterval(() { remainSeconds.value - 1 if (remainSeconds.value 0) { clearInterval(timer) handleSubmit() } }, 1000) }) onBeforeUnmount(() { if (timer) clearInterval(timer) })防作弊我做了三个层面考试页面禁止右键、禁止复制、监听浏览器beforeunload事件提示用户离开会交卷。但这些都只是软提醒真正可靠的是后端记录开始时间前端倒计时结束后自动交卷后端也按截止时间校验防止用户通过修改本地时间延长考试时长。断点续答也很重要。如果考生中途网络断了已经把答完的题丢了是灾难。我的方案是每答完一题就把当前答案存入localStorage同时在交卷前向后端发送暂存接口。实现起来不复杂但在真实考试场景里能救回不少数据。6. 打包部署与资源展示6.1 Django加Vue怎么部署到一台服务器上前后端分离项目的部署我推荐一个通用方案Nginx托管Vue构建产物同时反向代理/api接口到Django服务。先在本地构建前端npm run build构建完成后dist目录里的静态文件丢到服务器的/var/www/party_study目录。Nginx配置如下server { listen 80; server_name your_domain_or_ip; root /var/www/party_study; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /var/www/party_study/static/; } location / { try_files $uri $uri/ /index.html; } }这里有个关键点location /下面配置try_files这是为了让前端路由在history模式下刷新页面时不报404。如果没有这一行点击页面跳转没问题但按F5刷新就会404。Django后端启动用gunicorn。先安装并收集静态文件pip install gunicorn python manage.py collectstatic gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3如果用的是Flask部署方式略有不同比gunicorn更推荐waitresspip install waitress waitress-serve --host127.0.0.1 --port8000 app:app部署完之后记得去settings.py把DEBUG改成False把ALLOWED_HOSTS填上服务器IP或域名然后把SECRET_KEY换成随机长字符串。这些不设置好项目上线后随时可能出安全问题。6.2 学习资料里的PDF和视频怎么不踩坑党史学习资料的类型很杂PDF课件、历史影像、图文资料都有。这一块最容易踩坑的就是浏览器资源加载问题。PDF展示不要用自己封装的东西直接用iframe或embed标签就能解决大部分需求iframe :srcpdfUrl stylewidth: 100%; height: 80vh/iframe但坑往往不在前端而在资源能不能被正常访问。如果PDF文件存在Django的Media目录接口返回的URL必须能直接访问到文件否则iframe里只会一片空白。我建议在Django里加一个文件访问路由或者把文件放到Nginx可以直接访问的路径下。排查时第一件事用浏览器的Network面板看PDF请求状态码如果是404多半是文件存储路径和Nginx静态目录没对上。视频播放也是类似问题。现在很多学习资料视频是m3u8格式的它不是单个文件而是一个索引文件浏览器原生不支持得引入hls.jsimport Hls from hls.js const videoEl document.getElementById(player) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(videoUrl) hls.attachMedia(videoEl) }m3u8在本地开发时通常能正常播放部署到线上就可能出现跨域问题因为m3u8内部引用的ts分片地址可能指向另一个域名。这个问题建议统一处理视频文件和m3u8文件放同一域名下避免分片跨域。6.3 安全性与数据备份要点数据库备份是这类系统最容易忽略的一环。我建议使用Django的dumpdata做每日逻辑备份同时如果用的是MySQL配合mysqldump做物理备份。安全方面有几件事必须做改掉Django默认的admin路径或限制访问IP、密码不能明文存储、给所有接口加访问频率限制。JWT的密钥要单独配置不要写死在代码里环境变量管理是最低要求。系统上线后用扫描工具检查一下常见的Web漏洞XSS和SQL注入这个项目基本不会被Django的机制坑到但越权访问这类业务逻辑漏洞才是最需要关注的。7. 常见问题与排查技巧实录7.1 问题清单速查表做完整套项目把调试过程中遇到的高频问题整理成了一张速查表问题现象原因分析解决方案前端页面刷新404路由history模式没有配置try_filesNginx配置location /里加try_files项接口能通但带不了登录状态token未正确带在请求头检查axios拦截器里的Authorization写法登录后接口返回401JWT过期或token解析失败确认token有效期配置和localStorage取值PDF在iframe里空白文件路径不对或接口未返回文件流用Network排查请求状态核对静态文件路径考试倒计时每小时快一点setInterval定时器被页面卡顿延迟后端校验绝对截止时间前端只做提醒多选判分不准没有对选项顺序做处理用户答案和标准答案转成集合比较列表接口越查越慢N1查询问题使用select_related和prefetch_relatedVue构建后打包体积过大全量引入了Element Plus改为按需引入数据库删除后关联数据异常物理删除导致级联问题改用逻辑删除配置外键保护7.2 三个典型的排查过程第一个是前端刷新404的问题。我用的是history模式一开始部署后页面能正常访问点菜单也没问题我一度以为部署成功了直到按刷新键才发现白屏。Nginx的error.log里显示找不到对应路由文件。后来在配置里加了try_files $uri $uri/ /index.html彻底解决。这个问题几乎每个做前后端分离部署的人都会遇到排查时要先去Nginx日志确认是路由问题还是静态文件问题。第二个是考试倒计时不准的问题。有反馈说倒计时到了系统还没交卷我第一反应是setInterval不准确后来加日志发现前端定时器确实有延迟。但更根本的问题是即使前端精准用户本地修改系统时间也能骗过倒计时。最后的修法是把交卷判定的最终权限交给后端后端按试卷配置的考试时长检查超过截止时间就不再接收答题数据。前端倒计时的作用只是用户体验不是真正的计时依据。第三个是Django的admin后台样式丢失。用package模式部署后发现admin页面没有CSS样式。原因是collectstatic没有执行或者STATIC_ROOT配置不对。Django默认把admin样式放在应用包内部署时必须执行collectstatic把所有静态文件拷贝到指定目录再让Nginx指向这个目录。这个坑简单但很常见早遇到比晚上线遇到好。7.3 一些零散但关键的开发习惯项目收尾阶段有几个小习惯帮了大忙。一是统一异常处理。写一个全局异常处理器把数据库错误、视图函数错误、参数校验错误全部转成规范格式的JSON响应前端拦截器就能统一弹提示而不是偶尔冒出英文报错。二是所有接口的参数校验放在最前面。不要相信前端传来的任何字段后端必须做非空和类型校验。三是接口返回值格式统一。我的项目里统一返回{ code, message, data }前端拿着非常容易处理不会出现这个地方返回data那个地方返回dataList的情况。再有就是代码版本管理。这个项目虽然不大但git提交留痕很重要。每完成一个功能模块就提交一次提交信息写清楚做了什么。后面如果改出问题可以快速回溯到可用版本节省大量时间。我个人在实际操作中的体会是做这种学习考试系统技术难点真的不算高难的是把规则设计清楚、把细节想周全。判分逻辑、权限边界、数据追溯这些地方如果匆忙动手后面基本都要返工。每次动代码之前先问自己一句“这个功能失败的时候要怎么办”很多坑都能提前躲过去。项目做完之后如果再扩展可以考虑加智能组卷、按知识点薄弱点推荐学习内容但那是二期的事第一版把学、考、查的闭环跑扎实已经足够解决问题了。