ARTICLE DETAIL

资讯详情

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

Flask+Django组合架构:学术交流报告管理系统开发全记录

Flask+Django组合架构:学术交流报告管理系统开发全记录 前阵子学院里要搞一个学术交流报告管理系统目的很朴素把各类讲座、学术会议、外出访学、校企合作这些学术活动产生的报告统一放到线上平台来提交、审批、归档和统计。当时第一反应就是拿 Python 做但到底用 Flask 还是 Django我们内部争了好几轮。最后定下来的方案是“Django 做主体业务、Flask 做辅助服务”的组合架构也就是标题里那个“flask-django”的由来。这篇文章不打算讲太多虚的就把我自己从需求梳理、框架选型、数据库设计、核心功能编码、部署上线的完整过程掰开揉碎写一遍。里面有一些代码片段、环境配置细节、生产环境的坑也有我踩过之后觉得最值得注意的几个问题。如果你正在做类似的课程设计、毕业设计或者实验室想快速搭一套流程审批类系统这篇应该能帮你节省不少时间。1. 学术交流报告管理系统到底在管什么很多朋友拿到这种题目的第一反应是“这不就是一个增删改查吗”。表面看确实如此但高校学术交流报告管理这个场景里真正的难点不在 CRUD而在“状态流转”和“角色权限”的边界划分。我建议你在写第一行代码之前先把业务流动线画清楚。1.1 高校学术交流场景的痛点学术交流活动通常分几种本校老师外出参加学术会议、学院邀请外校专家来做讲座、组织内部学术沙龙、校企合作项目交流。活动结束后主办方或参与人需要提交一份“学术交流报告”内容一般包括活动基本信息、报告摘要、照片、参会人员情况等。这些报告以前往往靠邮件发来发去或者写在学院共享盘的 Excel 里最后归档的时候各种乱。我调研过后总结了几类核心痛点报告散落在个人邮箱、本地 Word、微信聊天记录里没法统一检索和沉淀。审批进度不透明老师提交完不知道卡在哪一层只能打电话问科研秘书。年终统计非常痛苦职称评审、学科评估需要按院系、按类别、按时间段拉数据人工筛 Excel 容易漏还不一定有权限看别人的报告。附件格式五花八门有的 PDF 是扫描件、有的 Word 排版混乱想做成知识库没有基础数据。这些问题听起来跟“管理系统”这种典型选题很像但只有真正把角色流程理清系统才不至于做成一个简单的数据库录入界面。1.2 角色、权限与状态流转我一开始定的角色很简单只有三种普通教师/学生、院系科研秘书、科研处管理员。等做到中间发现还需要一个系统管理员用来管用户激活和字典数据。角色对应的权限大致如下角色核心权限边界教师/学生提交报告、撤回草稿、修改被驳回的报告只能看到自己的报告院系科研秘书审核本院系报告、退回/通过只能操作本院系的数据科研处管理员终审、归档、统计导出、全院数据查看可以查看所有已提交报告系统管理员用户管理、角色管理、系统配置不参与业务审批报告的状态流转我们最终收敛成了这么一条链草稿 → 已提交 → 院系审核中 → 科研处审核中 → 已通过 / 已驳回。驳回后可以重新编辑再提交状态重新进入“院系审核中”。这个看似简单的状态机后面处理并发和操作权限的时候坑了我不少时间。1.3 功能清单怎么收敛高校做项目最怕的就是需求无限膨胀。老师一开始提了一堆想法什么自动查重、短信提醒、在线直播、二维码签到都要。但我们的原则是先做 MVP把主干流程跑通再考虑锦上添花。最终第一期功能收敛成这七个模块登录注册与身份激活。学术报告的新建、编辑、附件上传。报告提交后的层层审批。审批过程的批注记录留痕。按院系、类别、时间维度查询统计。后台管理页面用来配置用户和基础数据。数据导出方便年底考核。至于直播、签到、查重这些全部挪到二期。事实证明把这句话写在需求文档里能劝退至少一半后来追加的需求。2. 为什么这个项目选了“Flask Django”的组合路线标题里的“flask-django”并不是说两个框架混在一个进程里跑而是我在项目不同阶段、不同模块里分别用了它们。这是有原因的下面说说我的真实决策过程。2.1 先用 Flask 跑通核心验证项目刚启动的时候业务需求极度不稳定今天说流程里要加“学会会员人数”明天说要加“活动是否使用预算”。如果用 Django 那种相对重一点的工程结构改模型、改迁移、改 Admin 页面会拖慢节奏。所以我先用 Flask 写了一个最小可运行的原型。一个简单的 Flask 应用只需要一个app.py加一个模板就能跑起来。我当时在本地建了一个测试表单把“提交报告—保存到 SQLite—列表展示”这条路先走通。核心代码大致长这样from flask import Flask, render_template, request, redirect, url_for import sqlite3 from datetime import datetime app Flask(__name__) app.route(/report/new, methods[GET, POST]) def report_new(): if request.method POST: title request.form.get(title) summary request.form.get(summary) conn sqlite3.connect(demo.db) conn.execute( INSERT INTO report (title, summary, created_at) VALUES (?, ?, ?), (title, summary, datetime.now().isoformat()) ) conn.commit() conn.close() return redirect(url_for(report_list)) return render_template(report_new.html) app.route(/report/list) def report_list(): conn sqlite3.connect(demo.db) rows conn.execute(SELECT title, summary, created_at FROM report).fetchall() conn.close() return render_template(report_list.html, rowsrows) if __name__ __main__: app.run(debugTrue, port5000)这个原型不到一小时就写完了。给学院老师看的时候他们指着页面上某个字段说“这个应该改成下拉框”我现场改代码刷新页面就能看到效果。这种快速响应在需求调研阶段价值极高。2.2 切到 Django 的主要理由原型确认之后问题来了如果继续用 Flask后面用户注册登录要自己写、表单校验要自己写、Admin 后台要自己搭、ORM 迁移要自己管理虽然都能做但工作量大。尤其是这个系统有比较复杂的角色权限Flask 生态里缺少像 Django Admin 这样“开箱即用”的后台管理组件全靠手写太累。Django 让我最终定下决心的几个点自带 Authentication 和 Permission 体系二次开发成本低。ORM 的 migrations 机制非常成熟模型改动后有清晰的迁移记录。Admin 后台可以直接作为内部运营界面节省大量前端代码。表单 Form/ModelForm 能解决大部分校验和渲染需求。Django REST Framework 可以顺手把接口层准备好未来做小程序端不用重写。所以我做了一个决定主体业务全部用 Django 重写Flask 原型废弃。但 Flask 并没有完全退出它被我用到了后面的辅助服务里。2.3 哪一部分留给了 FlaskDjango 负责业务主干没问题但有些功能放进去反而别扭。比如用户上传报告附件后需要自动生成 PDF 缩略图、抽取正文里的关键词、做敏感信息检测。这类任务有几个特点CPU 密集、依赖第三方库、失败不应该影响主流程。如果直接在 Django 的视图里做一个十几兆的 PDF 可能让请求阻塞好几秒用户还以为页面卡死了。所以我单独用 Flask 起了一个辅助服务监听 8081 端口提供两个接口/worker/thumb接收文件路径生成 PDF 缩略图。/worker/check检测文档中是否包含手机号、身份证号等敏感信息。Django 那边通过 HTTP 调用这个 Flask 服务。它的好处是两个进程的依赖环境可以独立Flask 服务里装PyMuPDF等重库不会污染 Django。如果后续处理任务变多可以把这个 Flask 服务换成真正的工作队列比如 Celery Redis。单点故障不会拖垮核心业务Flask 挂了报告还是能提交只是缩略图没了。2.4 两个框架共存时怎么组织工程这是大家问得最多的问题两个框架代码怎么放我最终采用了这样的工程结构project-root/ ├── django_backend/ │ ├── config/ # Django 项目配置 │ ├── accounts/ # 用户模块 │ ├── reports/ # 报告模块 │ ├── approvals/ # 审批模块 │ └── dashboard/ # 统计模块 ├── flask_worker/ │ ├── app.py # Flask 辅助服务入口 │ ├── handlers/ │ └── requirements.txt ├── docker-compose.yml # 本地开发编排可选 ├── nginx/ │ └── app.conf └── deploy/ ├── gunicorn.conf.py └── supervisor.ini两个服务的数据库怎么共用我的方案是Flask 服务不直接读 Django 的业务表只处理文件和文件内容通过 HTTP 拿到的是 Django 上传后落盘的文件路径。这样耦合度非常低。如果硬要让 Flask 直接访问同一个 MySQL一旦 Django 改动表结构Flask 那边的SELECT很可能也会跟着崩完全没必要。3. 系统核心模块的设计与实现框架路线定了接下来就是把几个核心模块做扎实。我挑最有代表性的四个部分展开用户认证、报告模型、审批流、文件上传。3.1 用户认证与角色权限怎么落地用户在系统里需要额外存学号/工号、所属学院、职称等信息所以不能用 Django 默认的User表一了百了。官方推荐的做法是创建自定义用户模型在项目第一次migrate之前就设置好AUTH_USER_MODEL。我在accounts/models.py里这样写的from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES [ (teacher, 教师), (student, 学生), (secretary, 院系秘书), (admin, 科研处管理员), (sysadmin, 系统管理员), ] role models.CharField(角色, max_length20, choicesROLE_CHOICES, defaultteacher) college models.CharField(所属学院, max_length50, blankTrue) employee_id models.CharField(工号/学号, max_length30, blankTrue, uniqueTrue) class Meta: db_table auth_user_custom注册的流程要卡一下新用户注册后默认is_activeFalse要等系统管理员在 Admin 后台激活后才能登录。这样可以防止校外垃圾账号混进来。学院老师的账号通常还可以接学校的统一身份认证 LDAP但课程设计阶段做成“注册 后台激活”已经足够了。权限控制方面我没有去重写复杂的三方权限组件直接用login_required 自定义装饰器。比如院系秘书只能编辑本院系的报告在视图里根据report.submitter.college request.user.college判断即可。后期如果权限变得很复杂可以引入django-guardian但一期别过度设计。3.2 学术报告提交与审批流报告的模型是整张业务表的中心很多字段都是围绕它展开的。我的reports/models.py核心字段如下class AcademicReport(models.Model): STATUS_CHOICES [ (draft, 草稿), (submitted, 已提交), (college_review, 院系审核中), (dept_review, 科研处审核中), (approved, 已通过), (rejected, 已驳回), ] title models.CharField(报告标题, max_length200) category models.CharField(活动类别, max_length20) activity_date models.DateField(活动日期) location models.CharField(活动地点, max_length100) participants_num models.IntegerField(参与人数, default0) summary models.TextField(内容摘要) file models.FileField(附件, upload_toreports/%Y/%m/, validators[validate_report_file]) submitter models.ForeignKey(accounts.User, verbose_name提交人, on_deletemodels.PROTECT) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultdraft) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue)审批记录我单独建了一张表这样可以完整追溯一份报告从提交到最终通过的每次操作class ApprovalRecord(models.Model): report models.ForeignKey(AcademicReport, verbose_name报告, on_deletemodels.CASCADE) reviewer models.ForeignKey(accounts.User, verbose_name审批人, on_deletemodels.PROTECT) action models.CharField(动作, max_length10) comment models.TextField(审批意见, blankTrue) created_at models.DateTimeField(操作时间, auto_now_addTrue)审批动作的核心逻辑不是简单地改状态而是要保证并发情况下不会出现两份审批人同时通过的情况。我处理的方法是在状态变更方法里做一次select_for_update()行锁from django.db import transaction def approve_report(report_id, user, comment): with transaction.atomic(): report AcademicReport.objects.select_for_update().get(pkreport_id) if report.status not in (college_review, dept_review): raise ValueError(当前状态不可审批) # 根据角色决定下一个状态 if user.role secretary and report.status college_review: report.status dept_review elif user.role admin and report.status dept_review: report.status approved else: raise ValueError(当前角色无权审批) report.save(update_fields[status, updated_at]) ApprovalRecord.objects.create( reportreport, revieweruser, actionapprove, commentcomment )这里要注意select_for_update()只有数据库支持事务行锁才有效SQLite 下效果有限项目上线换成 MySQL/PostgreSQL 后就很关键了。3.3 文件上传的坑与安全处理报告附件是高频使用的功能也是最容易被攻击的入口。我在FileField的validators里做了两层限制import os def validate_report_file(value): ext os.path.splitext(value.name)[1].lower() if ext not in [.pdf, .docx, .doc, .zip, .jpg, .png]: raise ValidationError(不支持的文件格式%s % ext) if value.size 50 * 1024 * 1024: raise ValidationError(文件大小不能超过50MB)除了限制扩展名和大小更重要的一点是保存到磁盘的文件名要改成随机名不能直接使用用户上传的文件名。我写了一个通用的上传路径函数import uuid from django.utils.deconstruct import deconstructible deconstructible class UploadToPath: def __init__(self, prefix): self.prefix prefix def __call__(self, instance, filename): ext os.path.splitext(filename)[1].lower() return f{self.prefix}/{instance.activity_date:%Y/%m}/{uuid.uuid4().hex}{ext}这样做的原因是防止目录遍历和文件名注入。比如一个文件名叫../../static/x.php如果直接用原始名拼接路径后果会很糟糕。用 UUID 后原始文件名只能通过FileField.name记录但落盘名是随机串攻击面就小了很多。生产环境里媒体文件不能由 Django 直接服务也要放到 Nginx 的alias目录下避免把上传目录暴露到 Python 进程里。MEDIA_URL和MEDIA_ROOT分开配置这点后面部署部分会再提。3.4 统计报表和搜索的优化统计模块是老师们最看重的年终功能。我没有用专门的数据可视化大屏而是做了一个相对简单的管理端 Dashboard按月份、按院系、按类别统计报告数量。关键代码是用 Django ORM 的TruncMonth做时间维度聚合from django.db.models.functions import TruncMonth from django.db.models import Count def monthly_report_count(): return ( AcademicReport.objects .filter(statusapproved) .annotate(monthTruncMonth(activity_date)) .values(month) .annotate(totalCount(id)) .order_by(month) )搜索结果需要支持标题模糊搜索、提交人搜索、时间范围筛选。这里推荐用Q对象避免复杂判断导致代码又长又乱from django.db.models import Q def query_reports(keyword, start_date, end_date): qs AcademicReport.objects.all() if keyword: qs qs.filter( Q(title__icontainskeyword) | Q(submitter__username__icontainskeyword) | Q(summary__icontainskeyword) ) if start_date: qs qs.filter(activity_date__gtestart_date) if end_date: qs qs.filter(activity_date__lteend_date) return qs数据量超过几万条之后icontains查询会很慢那时候再考虑full-text search或者接 Elasticsearch。对学生项目来说Django ORM 优化好索引就够了。4. 从开发到部署的关键几步后台开发完之后最容易被忽视的往往是环境和部署。很多同学代码在本地跑得飞起一到服务器上就各种奇奇怪怪的问题。这一部分我把从零配置到上线的流程捋一遍。4.1 环境与项目初始化开发阶段我用的是 VSCode Python 虚拟环境。安装 Python 的版本建议选择 3.10 或 3.12我个人不推荐用最新版本刚发布的第一个小版本第三方库可能还没适配。虚拟环境创建和依赖安装以下命令可以直接复制python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install django5.0.* djangorestframework flask requests gunicorn pillow安装完依赖后把版本号固化下来pip freeze requirements.txt有些同学在 Windows 上明明安装了 Python但pip命令却找不到大概率是没有勾选“Add Python to PATH”。这种情况下直接使用python -m pip install xxx永远不要直接试pip因为可能会命中别的垃圾环境。4.2 Django 项目结构怎么拆Django 项目里最忌讳把一堆代码塞进一个app。我最终拆成了accounts、reports、approvals、dashboard四个模块职责划分很清晰accounts用户模型、注册激活、登录退出。reports报告模型、报告表单、文件上传逻辑。approvals审批视图、审批记录、审批状态机。dashboard统计查询、导出、首页展示。创建 app 的时候用命令python manage.py startapp reports python manage.py startapp approvals还要注意的是如果你打算自定义User模型一定要在第一次migrate之前就写到accounts/models.py里同时到settings.py里设置AUTH_USER_MODEL。一旦已经用默认 User 表迁移过再切换自定义模型会很麻烦虽然可以手动改但真的没必要给自己挖这个坑。4.3 生产部署Nginx Gunicorn Flask 独立服务生产环境我选择的是 Linux 服务器。Django 侧使用 GunicornFlask 辅助服务也使用 Gunicorn两个进程分别监听不同的端口。Django 启动命令gunicorn config.wsgi:application -b 127.0.0.1:8000 -w 4 --timeout 60Flask 侧启动命令gunicorn app:app -b 127.0.0.1:8081 -w 2 --timeout 120Nginx 配置我简化了一下核心部分大概是这样的server { listen 80; server_name your-domain.com; client_max_body_size 50m; location /static/ { alias /var/www/django_backend/static/; } location /media/ { alias /var/www/django_backend/media/; } location /worker/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; } }client_max_body_size一定要调大不然上传报告附件超过默认的 1MB 就会直接 413。这个坑我第一次部署时踩了线上用户的 PDF 动辄十几兆Nginx 默认配置根本传不上去。记得在 Django 的settings.py里做两件事关闭DEBUG False设置ALLOWED_HOSTS [your-domain.com]。否则浏览器访问会直接报DisallowedHost这个问题新手特别容易遇到。4.4 通知与提醒机制审批状态变化之后要通知提交人不然老师不知道自己的报告进展。最简单的方案是给被驳回和被通过的处理方法里加一封邮件。Django 自带邮件框架配置 SMTP 就可以。我在settings.py里这样写EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend EMAIL_HOST smtp.example.com EMAIL_PORT 465 EMAIL_USE_SSL True EMAIL_HOST_USER no-replyexample.com EMAIL_HOST_PASSWORD xxx DEFAULT_FROM_EMAIL EMAIL_HOST_USER发送邮件的代码放在审批成功之后from django.core.mail import send_mail send_mail( subject你的学术交流报告已通过审核, messagef《{report.title}》已通过最终审核可登录系统查看。, from_emailNone, # 使用 DEFAULT_FROM_EMAIL recipient_list[report.submitter.email], )当然如果学校没有邮件服务器也可以在一个辅助进程里接钉钉或企业微信机器人但那种方式需要单独维护配置和密钥一期没有必要。邮件是最通用、最没有侵入性的选择。5. 开发过程中踩过的几个坑这个项目前前后后踩了不少坑。有些坑是百度能搜到的有些只能靠日志和“直觉”排查。我把值得记录的几个写在这里希望能帮后来人省点时间。5.1 文件下载时中文文件名乱码系统里用户上传的原始文件名是中文比如“2024年学术交流活动总结.pdf”。虽然落盘时用了 UUID但用户下载的时候希望看到的还是原名。直接通过浏览器访问media下的 UUID 文件名完全没有任何业务语义。于是我加了一个下载视图用FileResponse返回文件并显式设置响应头from django.http import FileResponse from urllib.parse import quote def download_report_file(request, report_id): report get_object_or_404(AcademicReport, pkreport_id) response FileResponse(report.file.open(), as_attachmentTrue) # 解决中文文件名乱码这里要双编码 response[Content-Disposition] ( attachment; filename*UTF-8 quote(report.original_filename) ) return responsefilename*后面带UTF-8是标准做法如果直接把中文拼到filename后面在 Chrome / Firefox 上大概率乱码或者直接截断。5.2 审批并发导致状态被二次审核有一次两个管理员同时打开同一个报告列表恰好对同一份报告都点了“通过”。其中一个人应该看到“该报告已经处理”但代码逻辑没有加锁导致两份ApprovalRecord都写进去了状态也变了两次。后来我加了transaction.atomicselect_for_update()同时把状态机里的前置条件也校验得更加严格。现在重复操作会直接弹“当前状态不可审批”不会再出现脏数据。5.3 CSV 导出中文乱码统计功能里做了一个“按院系导出 CSV”的按钮。本地 Windows 上 Excel 打开乱码Linux 上cat又没事典型症状是 CSV 用 UTF-8 编码但没有 BOM。Excel 对 UTF-8 无 BOM 非常不友好。解决办法很简单在输出前加一个\ufeffimport csv from django.http import HttpResponse def export_csv(request): response HttpResponse(content_typetext/csv) response[Content-Disposition] attachment; filenamereports.csv response.write(\ufeff) # 加 BOMExcel 才能正确识别 UTF-8 writer csv.writer(response) ... return response如果用的是 Excel 的.xlsx导出直接让openpyxl写单元格内容即可不会出现这个编码问题。5.4 用runserver直接部署导致大量 503项目刚上线那几天访问量稍微大一点就开始出现Connection reset和 503。查了半天才发现运维同事直接用python manage.py runserver挂在后台跑。runserver是开发调试用的默认线程模型非常弱顶不住几个并发。这个问题不是写法错而是把开发服务器当生产用了。切到 Gunicorn 后内存占用和并发稳定性立刻不一样。5.5 版本兼容问题如果你用的还是别人给的旧项目代码一定要先确认 Python 和 Django 的大版本匹配。Django 2.x 用的还是 Python 3.6 时代的写法直接跑在 Python 3.11 上各种报错Flask 2.x 和 3.x 的部分接口也有差异。我的建议很直接新项目尽量选 LTS 版本。Django 4.2 LTS 和 Django 5.0 都可以用Flask 3.0 配合 Python 3.10 以上没问题。不要追求最新版本号稳定优先。6. 如果你也想快速复刻这个系统几点实用建议最后这部分不是空泛的总结而是我做完整套项目后最想对准备动手的人说的话。6.1 按这个顺序开发能少返工如果让我重做一遍开发顺序一定会是先把 Django 项目和自定义User模型搭好这个步骤越早越好。再做AcademicReport模型和附件上传因为所有流程都围绕这颗表展开。然后做审批流和权限控制这是业务的核心也是最容易出现逻辑漏洞的地方。最后才是统计导出和管理后台样式优化。千万不要一上来就折腾前端界面也不要一开始就写复杂的后台审批逻辑。先想办法让一份报告“提交上去、审下来、看到状态”走通一个最小闭环再去迭代扩展。6.2 准备好测试数据与演示账号答辩或者演示的时候最尴尬的事情就是系统里空荡荡点开统计页面没有任何数据。建议提前写一个管理命令批量生成一份测试数据# reports/management/commands/seed_demo.py from django.core.management.base import BaseCommand from reports.models import AcademicReport from accounts.models import User class Command(BaseCommand): def handle(self, *args, **options): for i in range(50): AcademicReport.objects.create( titlef机器学习前沿进展学术讲座 {i}, category讲座, activity_date2024-06-01, location东校区报告厅, participants_num80, summary本次讲座主要介绍了近期大模型的进展..., statusapproved, submittersome_user, ) self.stdout.write(demo data created)再准备至少三个演示账号普通教师、院系秘书、科研处管理员。每次演示都稳固地用这几个账号展示不要临时注册新用户因为注册后还要验证容易卡壳。6.3 答辩和 DEMO 技巧演示的时候先讲清楚痛点再演示核心流程提交报告 → 院系审核 → 科研处审核 → 被驳回 → 重新提交 → 通过。这个闭环讲透比展示花哨页面有说服力得多。如果被问到“为什么不用 Flask 直接全做完”就说三个理由Django 的 Admin 后台能直接管理用户和数据Auth 体系比 Flask 自带方案成熟ORM 迁移在项目迭代中更省心Flask 保留在辅助服务里是为了隔离 CPU 密集型任务并便于扩展成消息队列。这样回答基本很稳。整套项目做完我的总体感受是像“高校学术交流报告管理系统”这类业务本质就是“表单 审批 统计 权限”的组合没有特别玄学的算法。只要你把数据模型设计清楚、状态流转定清楚、权限边界划清楚Python 的 Django 和 Flask 完全能高效地完成这件事。尤其是 Django在开发这类后台管理系统时确实节省了大量的重复劳动。如果你正卡在框架选型或者某个功能实现细节上希望这篇记录能帮你少走一点弯路。
返回列表