先交代一个背景:前阵子我帮某高校信息中心做了一套教师职称评定管理系统,技术栈用的是 Python + Django 作为主业务框架,部分算法模块单独拆出来用 Flask 写成轻量服务。整套系统从需求梳理到部署上线大概花了一个半月,中间踩了不少坑,也积累了一些比较实战的经验。这篇文章就把整条技术路线、数据库设计、核心流程实现、部署细节和常见问题展开讲一讲,希望能给正在做类似管理系统,尤其是高校人事、教务背景项目的朋友提供一套可以直接参照的方案。
这套系统的核心目标是解决高校职称评定过程中的“材料分散、流程不可控、评审数据无法追溯”三大痛点。传统方式里,教师需要打印大量纸质材料,逐级递交到教研室、院系、校职称办,审核状态全靠人工电话催问。换成线上系统后,申报人能在网页端提交电子材料、实时查看进度;评审专家能在线打分并填写意见;管理员能一键导出汇总表,评委会也能在系统内完成最终票决记录。整条链路清晰、留痕、可追溯,这就是开发这套管理系统的价值所在。
本文适合三类人阅读:一类是正在做 Django 课程设计或毕业设计的在校生,另一类是负责校内信息系统建设的信息中心工程师,还有一类是从 Flask 或 FastAPI 转型过来,想了解 Django 项目如何做模块拆分和权限控制的开发者。
1. 项目需求与设计方案拆解
1.1 职称评定管理系统到底需要做什么
先别急着写代码,把需求搞清楚才能避免后面返工。高校教师职称评定管理系统看起来只是一个“信息管理系统”,但真正捋一遍业务之后,你会发现它至少包含六个基础模块:
- 教职工档案管理:维护教师工号、姓名、所在院系、当前职称、学历、入职时间等基础信息。
- 职称申报管理:教师提交拟申报职称(讲师、副教授、教授等),填写个人业绩综述并上传佐证材料。
- 审核流程管理:教研室初审、院系复审、校职称办资格复核,每一级都可以退回或通过。
- 专家评审管理:校外或校内专家在线查看申报材料,按教学、科研、师德等维度打分并填写意见。
- 结果公示管理:通过人员名单在系统内公示,支持导出公示文本。
- 系统权限管理:不同角色拥有不同可见范围和操作权限。
我当时跟校方访谈下来发现,很多冲突都出在“角色权限”上。比如系主任只应该看到本学院的申报人,校职称办能看全校,而评审专家只能看到分给自己的那一批人,不能看到其他专家的打分。这个权限边界如果不在设计阶段定清楚,后面改起来会非常痛苦。
1.2 为什么主框架选 Django 而不是 Flask
这个选型问题,我建议看重业务主链路的人优先考虑 Django。原因有三点:
第一,Django 自带一套完整的 ORM 和迁移机制。职称评定里最复杂的就是各种表之间的关联关系,教师、申报、成果、评审记录、审批日志互相关联。用 Django 的models.ForeignKey和ManyToManyField能非常自然地描述这些关系,配合makemigrations和migrate命令,表结构变更可以在开发环境快速迭代,不需要手写 SQL 建表脚本。
第二,Django 内置的 Admin 后台能节省大量基础维护页面的开发时间。校职称办需要时不时修改教师档案、调整评分权重,这些操作如果全部从零开发页面,工程量不小。而 Django Admin 只要把模型注册进去,自动就能增删改查,还自带搜索和分页,对内部管理系统来说足够用。
第三,Django 自带的认证和权限体系是现成的。教师登录、管理员登录、退出登录、Session 管理、密码哈希,全都有标准实现。再结合Group和Permission,可以很快把“申报人”“教研室初审”“院系审核”“职称办”“评审专家”这几类角色的权限矩阵搭起来。
当然,我不是说 Flask 不能做这类系统。如果你是写一个轻量的评分服务,Flask 反而更合适,因为它的路由和请求上下文非常轻量。但把整所学校几百号人的申报、审批、打分、公示全部做在一个 Flask 项目里,你要自己拼 ORM(比如 SQLAlchemy)、表单校验(WTForms)、用户认证(Flask-Login),这些组件版本兼容间很容易出问题。相比之下,Django 提供的是“全家桶”,开发效率会高很多。
1.3 Flask 在这套系统里的角色分工
实际落地时,我把“专家评分结果汇总”和“量化积分计算”两个模块独立出来用 Flask 实现。为什么单独拆?因为这两个模块属于典型的“高计算、低状态”场景——给它 JSON 数据,它返回计算结果,本身不依赖 Django 的 Session 和模板渲染。拆成 Flask 微服务以后,有两点好处:
- 单个服务职责单一,调用方通过 HTTP 接口拿到分数,后续如果要把评分算法改成独立定时任务,也不需要动 Django 主站。
- 两个服务可以独立部署、独立扩容。评审高峰期比如学校评职称集中在六月份,可以把评分服务起多个实例分摊压力,而 Django 主站保持单实例就够用。
有朋友问过我,“Flask 和 FastAPI 比,为什么选 Flask 不选 FastAPI?”其实现在 FastAPI 的异步性能和自动生成 API 文档确实很强,但我这边选择 Flask 主要是考虑到团队里其他人更熟悉 Flask 的生态,而且评分服务本身是同步计算,异步带来的收益不大。如果你从零开始且团队能 hold 住 FastAPI,用它也是合理的方案。
1.4 部署架构设计
整个系统部署结构如下:
- 一台阿里云 ECS 服务器,操作系统 Ubuntu 22.04。
- Nginx 监听 80/443 端口,拦截外部流量。
- Django 主服务跑在 Gunicorn 上,监听 127.0.0.1:8000,负责管理端页面和登录认证。
- Flask 评分服务跑在 Gunicorn 上,监听 127.0.0.1:8001,只对 Django 内网请求开放。
- PostgreSQL 数据库存放所有业务数据。
这样的结构好处是 Nginx 统一处理静态文件、HTTPS 证书和反向代理,业务层互相隔离,将来任一个服务挂了另一个还能继续工作。
2. 数据库设计与核心模型实现
2.1 表结构梳理
职称评定系统的数据模型我建议围绕“一次申报”为主线设计。一次申报对应一个教师、一个目标职称、一个申报年度、一个当前状态。所有评审记录、审批日志、公示信息都挂在这次申报下面。
核心表分成四类:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| 教师表 | 记录教职工基础信息 | 工号、姓名、院系、当前职称、学历、入职时间 |
| 申报表 | 每一次职称申报 | 教师外键、申报职称、年度、状态、申报书文件 |
| 成果表 | 申报相关的论文、项目、专利 | 申报外键、成果类型、名称、级别、时间、附件 |
| 评审记录表 | 专家打分记录 | 申报外键、专家外键、各维度分数、综合意见 |
在设计时需要注意:成果表不要直接挂在教师表下面,而是挂在申报表下面。因为同一篇论文,教师评讲师时可能作为主要成果提交,评副教授时可能又作为成果之一。如果成果只跟教师关联,后续做“某次申报的材料清单”时会非常难过滤。
2.2 用 Django 模型描述核心实体
下面是我实际项目里 models.py 的核心代码,可以作为一个参考:
from django.db import models from django.contrib.auth.models import AbstractUser class Teacher(AbstractUser): # 扩展 Django 自带用户模型 employee_id = models.CharField(max_length=20, unique=True, verbose_name="工号") department = models.CharField(max_length=64, verbose_name="所在院系") current_title = models.CharField(max_length=32, verbose_name="当前职称") hire_date = models.DateField(verbose_name="入职时间") education = models.CharField(max_length=32, blank=True, verbose_name="学历") class Meta: verbose_name = "教师" verbose_name_plural = "教师" class Application(models.Model): STATUS_CHOICES = [ ("draft", "草稿"), ("submitted", "已提交"), ("dept_review", "院系审核中"), ("expert_review", "专家评审中"), ("final_review", "终审中"), ("approved", "已通过"), ("rejected", "已驳回"), ] teacher = models.ForeignKey(Teacher, on_delete=models.PROTECT, verbose_name="申报人") target_title = models.CharField(max_length=32, verbose_name="拟申报职称") year = models.IntegerField(verbose_name="申报年度") status = models.CharField(max_length=32, choices=STATUS_CHOICES, default="draft", verbose_name="状态") summary = models.TextField(blank=True, verbose_name="业绩综述") application_file = models.FileField(upload_to="applications/%Y/", verbose_name="申报书") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.teacher.employee_id}-{self.teacher.username}-{self.target_title}-{self.year}" class Achievement(models.Model): ACH_TYPE_CHOICES = [("paper", "论文"), ("project", "项目"), ("patent", "专利"), ("award", "获奖")] application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="achievements") ach_type = models.CharField(max_length=16, choices=ACH_TYPE_CHOICES) title = models.CharField(max_length=255, verbose_name="成果名称") level = models.CharField(max_length=32, blank=True, verbose_name="级别/期刊/奖项") attachment = models.FileField(upload_to="achievements/%Y/", blank=True) order = models.IntegerField(default=0, verbose_name="排序权重") class ReviewRecord(models.Model): application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name="reviews") expert = models.ForeignKey(Teacher, on_delete=models.PROTECT, verbose_name="评审专家") teaching_score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="教学得分") research_score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="科研得分") moral_score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="师德得分") comment = models.TextField(blank=True, verbose_name="评审意见") created_at = models.DateTimeField(auto_now_add=True)有几个设计细节值得注意。
Teacher继承AbstractUser是 Django 扩展用户模型的推荐方式,这样原有登录、密码管理、Session 功能都有,再额外加工号、院系等字段。不要自己单独搞一张教师表再去跟 User 外键关联,那样每次要取登录用户的院系信息都得跨表查,麻烦还容易出错。
外键删除策略要仔细选。比如Application.teacher用了on_delete=models.PROTECT,图的是教师即使离职,也不允许把他的申报记录直接删掉,因为职称申报涉及行政追溯,删了就说不清了。而Achievement.application用on_delete=models.CASCADE,是因为成果隶属于某次申报,申报都没了,成果留着也没有意义。这就是 Django 删除机制选型的核心逻辑。
2.3 数据表关联和查询时的常见坑
Django 查询是个非常高频的操作,但也最容易写错。我遇到过不少同学在视图函数里这样写:
application = Application.objects.get(id=application_id) teacher = application.teacher achievement_list = Achievement.objects.filter(application=application)这样写功能没问题,但会制造 N+1 查询问题。如果页面要显示 100 条申报记录,每条记录都去查它的教师和成果列表,数据库压力就会倍增。更好的写法是用select_related和prefetch_related:
applications = Application.objects.select_related("teacher").prefetch_related("achievements").all()select_related适合优化一对一、多对一关系的外键查询;prefetch_related适合优化一对多关系的反向查询。这个优化在高并发评审期间非常关键,实测能让列表页响应时间从 3 秒降到 200 毫秒以内。
另外还有删除对象时的坑。Django 删除对象不是只删主表这一条记录,比如你删一个Teacher,相关联的Application会根据on_delete策略决定动作。如果某个模型没有正确配置on_delete,数据库迁移时会直接报错。早期的 Django 版本还会静默级联删除数据,现在版本强制要求开发者明确声明策略,这反而是件好事,逼着你思考业务上的数据依赖关系。
3. 核心流程实现与实操要点
3.1 创建 Django App 与基础工程结构
Django 项目从零搭建的步骤,很多人已经写过,我这里只说容易忽略的点。
首先,不要把所有代码写在一个 app 里。我习惯按业务边界拆分,比如认证和教师档案放在accountsapp,申报和成果放在applicationapp,专家评分放在reviewapp,首页通知和导出放在dashboardapp。这样后面维护时,每个 app 的 models、views、urls 都是独立文件,改动起来不会互相牵连。
具体创建 app 的命令是:
python manage.py startapp accounts python manage.py startapp application python manage.py startapp review python manage.py startapp dashboard建完 app 之后,一定要记得在settings.py的INSTALLED_APPS里注册,不然 Django 会当作不存在处理。新手最容易在这里卡住,反复启动服务发现 URL 路由不生效。
至于把已有的 Django 信息系统导入到 PyCharm 里运行,这个也是很多初学者问过的问题。正确做法是:在 PyCharm 里选择 File → Open,定位到项目根目录(也就是含 manage.py 的那一层),而不是打开到某个 app 的子目录。打开之后,PyCharm 会要求你选择 Python 解释器,指向你配置好的虚拟环境,随后在终端里执行:
python manage.py runserver这样就可以跑起来了。如果导入项目后提示环境依赖问题,先确认虚拟环境是否激活,再检查requirements.txt中的包是否已经安装,缺什么补装什么就行。
3.2 登录认证和角色权限控制
权限控制是这套系统里最不能偷懒的部分。Django 的装饰器可以帮助快速实现“必须登录”和“必须具有某权限”的效果。
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import permission_required @login_required def dashboard(request): return render(request, "dashboard/index.html") @permission_required("application.can_review_application") def expert_review_area(request): pass但单靠permission_required还不够,因为同一个用户可以同时具有多种角色属性,比如某位老师既是申报人又是院系评审组成员,那就要在视图里做二次过滤,确保申报人只能查看自己的记录,院系审核只能看到本院系记录。
我的做法是把“角色分离”放到视图函数里,用当前登录用户的信息来约束查询集:
from django.shortcuts import get_object_or_404 from django.core.exceptions import PermissionDenied def transaction_list(request): if request.user.is_superuser or request.user.groups.filter(name="职称办管理员").exists(): applications = Application.objects.all() elif request.user.groups.filter(name="院系审核员").exists(): applications = Application.objects.filter(teacher__department=request.user.department) elif request.user.groups.filter(name="评审专家").exists(): applications = Application.objects.filter(reviews__expert=request.user) else: applications = Application.objects.filter(teacher=request.user) return render(request, "application/list.html", {"applications": applications})这套逻辑的核心思路是:先判断角色范围,再过滤数据。务必不要直接用一个if把所有人放进同一查询集,那样越权访问很容易发生。
3.3 申报状态的机机制与流转
职称申报状态的流转,我推荐把它设计成一组常量的状态机:草稿 → 已提交 → 院系审核中 → 专家评审中 → 终审中 → 已通过 / 已驳回。
在设计时每个状态对应一段操作:
- 草稿状态下,教师可以修改申报信息和删除成果附件。
- 提交后,申报书自动锁定,教师不能再修改材料。
- 院系审核阶段,审核员可以点击“通过”进入下一环节,也可以退回,退回时需要填写理由。
- 专家评审阶段,系统按双盲规则把申报材料随机分给 2 到 3 位专家,专家各自打分,互不可见。
- 终审阶段,职称办综合专家评分、历年名额、师德审查意见给出最终结论。
状态流转的代码层面,我建议不要让状态字段被随意修改,而是通过专门的服务函数来做:
def submit_application(application: Application) -> bool: if application.status != "draft": return False application.status = "submitted" application.save(update_fields=["status", "updated_at"]) log_action(application, "提交申报") return True这样做的目的很朴素:只允许从合法状态进入目标状态,杜绝用户绕过前端直接改数据库状态的可能。同时每次状态变更都写一条操作日志,形成完整的审计链路。
3.4 专家评分接口与 Flask 服务实现
专家评分独立成 Flask 服务后,Django 端通过 HTTP 调用将申报数据和专家打分传给 Flask,Flask 根据标准权重计算总分并回传数据库。
举个例子,评分模型可以设定为:总分 = 教学得分×0.3 + 科研得分×0.5 + 师德得分×0.2。如果一所学校有一套更复杂的量化积分公式,也可以在这个服务里实现。
Flask 端代码如下:
from flask import Flask, request, jsonify app = Flask(__name__) WEIGHTS = { "teaching": 0.3, "research": 0.5, "moral": 0.2, } @app.route("/calculate_score", methods=["POST"]) def calculate_score(): data = request.get_json() if not data: return jsonify({"error": "empty request"}), 400 required = ["teaching_score", "research_score", "moral_score"] if any(key not in data for key in required): return jsonify({"error": "missing fields"}), 400 total = ( float(data["teaching_score"]) * WEIGHTS["teaching"] + float(data["research_score"]) * WEIGHTS["research"] + float(data["moral_score"]) * WEIGHTS["moral"] ) total = round(total, 2) return jsonify({"total_score": total})Django 端发起请求的方式也很简单:
import requests from django.conf import settings def calculate_total_score(review_record): resp = requests.post( f"{settings.FLASK_SERVICE_URL}/calculate_score", json={ "teaching_score": str(review_record.teaching_score), "research_score": str(review_record.research_score), "moral_score": str(review_record.moral_score), }, timeout=5, ) if resp.status_code == 200: return resp.json().get("total_score") return None这里有一个需要注意的点:Django 里DecimalField取出来是 Decimal 类型,直接传给 Flask 做 JSON 序列化有时会报错,所以我在传参前先转成了字符串。细节虽小,但排查起来挺费时间。
3.5 文件上传、材料预览与数据导出
教师申报时要上传 PDF 格式的申报书和成果证明材料。Django 的FileField可以处理文件上传,但要限制文件类型和大小。我在settings.py里做了约束:
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880同时在上传视图里检查后缀名,只允许 PDF、JPG、PNG 三种类型。这样做是为了防止用户把恶意脚本文件伪装成附件上传,避免将来浏览器直接解析造成安全问题。
文件上传到media目录后,Nginx 会把/media/路径直接映射到磁盘目录,实现静态服务。
location /media/ { alias /opt/title_review/media/; expires 7d; }数据导出是职称办最常用的功能之一,到了职称评审结束阶段,他们需要给学校党委会打印一份汇总名单。我用了openpyxl库生成 Excel 文件,导出字段包括工号、姓名、院系、拟申报职称、综合得分、评审结果、评审意见。核心代码大致这样:
from openpyxl import Workbook from openpyxl.writer.excel import save_virtual_workbook from django.http import HttpResponse def export_approved_excel(applications): wb = Workbook() ws = wb.active ws.append(["工号", "姓名", "院系", "拟申报职称", "综合得分", "评审结果"]) for app in applications: ws.append([ app.teacher.employee_id, app.teacher.username, app.teacher.department, app.target_title, app.reviews.aggregate_result(), app.get_status_display(), ]) response = HttpResponse(content=save_virtual_workbook(wb), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") response["Content-Disposition"] = "attachment; filename=approved_list.xlsx" return response4. 部署上线与常见问题排查实录
4.1 服务器环境准备与虚拟环境搭建
开发完不代表结束,部署上线才是真正的考验。我部署时先把 Python 3.10 装好(Ubuntu 22.04 自带 Python 3.10,省了很多事),然后为项目创建虚拟环境:
sudo apt update sudo apt install -y python3-venv python3-pip nginx postgresql mkdir -p /opt/title_review cd /opt/title_review python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt建议把版本锁定,比如:
Django==4.2.7 gunicorn==21.2.0 psycopg2-binary==2.9.9 requests==2.31.0 openpyxl==3.1.2 Flask==3.0.0把DEBUG改成False,并把ALLOWED_HOSTS配置成实际域名或 IP,同时做好静态文件收集:
python manage.py collectstatic python manage.py migrate python manage.py createsuperuser4.2 Gunicorn 启动与 Nginx 反向代理
我用 Gunicorn + Nginx 是最常见的组合。写一个 Gunicorn 配置文件gunicorn_conf.py:
import multiprocessing bind = "127.0.0.1:8000" workers = multiprocessing.cpu_count() * 2 + 1 timeout = 60 accesslog = "/var/log/title_review/django_access.log" errorlog = "/var/log/title_review/django_error.log"启动 Django:
cd /opt/title_review source venv/bin/activate gunicorn -c gunicorn_conf.py config.wsgi:application启动 Flask 评分服务:
nohup venv/bin/gunicorn -b 127.0.0.1:8001 -w 2 score_service:app > /var/log/title_review/flask.log 2>&1 &Nginx 配置文件这样写:
server { listen 80; server_name your_domain.com; client_max_body_size 20M; location /static/ { alias /opt/title_review/staticfiles/; } location /media/ { alias /opt/title_review/media/; } location / { 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; proxy_set_header X-Forwarded-Proto $scheme; } }client_max_body_size这个参数很关键,如果不上调,教师上传几 MB 的申报书时会直接被 Nginx 拦截,返回 413 错误,而 Django 侧根本看不到请求。我最初没加这行,结果测试当天被这份“坑”喂饱了。
4.3 高频问题排查速查表
把我在开发部署中遇到的典型问题整理成一个表格,方便后续快速定位:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 页面能打开但静态文件全 404 | collectstatic未执行或静态文件路径映射错误 | 检查STATIC_ROOT,重新执行collectstatic并确认 Nginx location 指向正确 |
| 登录后session失效极快 | 服务器时间和浏览器时间不一致 | 统一服务器时区,配置TIME_ZONE = "Asia/Shanghai",并处理 UTC 时区问题 |
| 上传大附件报 413 | Nginx 请求体大小限制太低 | 在 server 块加client_max_body_size 20M; |
| 数据库查询超时 | 未使用select_related导致 N+1 问题 | 优化查询语句,增加索引字段例如(year, status, teacher_id)联合索引 |
| 专家评分无法提交 | 请求 JSON 中 Decimal 类型无法序列化 | 将 Decimal 转为字符串或 float 再交给 Flask |
| 文件上传后通过 URL 无法访问 | settings 里MEDIA_URL未配置 | 在 settings.py 中设置MEDIA_URL = '/media/',并让 Nginx 映射到磁盘目录 |
| 页面显示“DisallowedHost” | ALLOWED_HOSTS未配置域名/IP | 在ALLOWED_HOSTS中添加服务器 IP 或域名 |
| 系统提示数据库表不存在 | 未执行迁移或者迁移被覆盖 | 执行python manage.py makemigrations后再执行migrate |
4.4 安全加固和性能优化建议
做管理系统最怕数据泄露。职称评定涉及教师个人隐私和院系评价意见,一旦泄露后果非常严重。我做安全加固时有几条硬性要求:
第一,所有涉及考勤、上传内容的后台 URL 全部要求登录,并且能给管理员看的不一定给院系审核员看。对于敏感操作,视图函数里要增加“当前用户是否具备操作权限”的判断,不能只靠前端隐藏按钮。
第二,表单提交开启 CSRF 保护。Django 默认开启了CsrfViewMiddleware,模板里用{% csrf_token %}即可,不能为了省事关掉。我们系统里所有 POST 请求都带上了 CSRF token,实测第三方构造的跨站请求根本无法提交成功。
第三,数据库访问拒绝使用弱密码,数据库账号前后端分离。用 Django 连接数据库时,密码存环境变量,避免硬编码在代码里。
性能层面,评审高峰期专家同时在线打分的概率很高。除了前面提到的查询优化外,我还把教师端首页统计查询做了缓存:
from django.core.cache import cache def teacher_summary(request, teacher_id): cache_key = f"teacher_summary_{teacher_id}" result = cache.get(cache_key) if result: return result result = compute_teacher_summary(teacher_id) cache.set(cache_key, result, timeout=300) return result缓存时间设为 300 秒,既能保证数据及时性,又能显著减少数据库压力。
5. 写在最后的一些实操体会
整套系统用下来,我个人最大的一个体会是:不要迷信“全栈框架”或“微服务”,关键是让每个工具出现在它最擅长的地方。Django 适合承载业务主体,因为它把认证、ORM、Admin 这些高频需求都做好了,开发者能集中精力处理业务逻辑;Flask 适合做独立的算法计算服务,轻量、易扩展,两边配合反而比硬塞进一个框架里更顺手。
另外一个小技巧分享给大家:配置好系统的media目录后,记得在每天定时任务里做一次磁盘备份。职称评审材料都是 PDF 和图片,单个文件不大,但量多了以后累积起来的体积并不小,一旦磁盘挂了,所有历史评审材料就全没了。我用rsync每天把整个/opt/title_review/media目录同步到另一块数据盘,既省心又可靠。
如果后续校方提了新需求,比如增加线上答辩环节、接入统一身份认证平台,或者把评审规则改成动态配置,现有这套框架也能继续扩展。Django 的插件生态比较成熟,加一个认证后端、加一个模型字段,并不会伤筋动骨。这也是我推荐用 Django 做管理系统的另一个重要原因——它给你留了足够的生长空间。