看到"基于flask-django"这种题目的时候,不少同学第一反应是:这俩框架是不是得二选一?或者怕写成"用Django做了个主页、用Flask做了个登录页"的缝合怪。其实这类设计题目真正想考察的,是你能不能理解两个Web框架各自的能力边界,并且用一套合理的架构让它们协作完成一个完整业务。
这个校园个人闲置物品换购平台,我当初从需求分析到部署上线完整走了一遍,坑踩了不少,但做完之后对Python Web开发的理解确实上了一个台阶。这篇文章我会把整套设计思路、核心数据表结构、双框架协作方式、关键代码实现,以及部署阶段遇到的真实问题全部梳理出来。不管你是要做课程设计、毕业设计,还是单纯想练手两个主流Python框架,这篇都值得看完。
1. 双框架并存的核心思路:主业务收敛、辅助功能外放
先直接说结论:一个项目里同时出现Flask和Django,绝不是让它们做重复的事,而是让每个框架去做自己最擅长的事。
1.1 Django和Flask的能力边界
Django自带ORM、Admin后台、用户认证体系、表单处理、CSRF防护,这种"全家桶"特性非常适合承载业务主链路。对于校园闲置物品换购平台来说,用户注册登录、物品发布、订单交易、后台审核这些核心模块,用Django来做能省掉大量重复造轮子的工作,而且数据模型和数据库迁移都通过Django ORM统一管理,代码维护成本低。
Flask则是一个轻量级框架,它不替你做任何决定,但胜在灵活、启动快、写小服务非常顺手。在这个项目里,我把推荐接口、数据统计聚合、图片压缩处理这类相对独立的辅助功能放到了Flask侧。这类功能的特点是:逻辑相对独立、调用频率可能很高、不影响主交易流程,即使某个接口挂了,也不能拖垮用户下单。
1.2 双框架协作的三种常见模式
我见过不少团队做这类双框架项目,拆法大致有三种:
| 协作模式 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 方式A:共享数据库,独立服务 | Django跑主站,Flask跑API子服务,两者连同一个MySQL,通过HTTP接口互相调用 | 课程设计、毕业设计,也是本文采用的方案 | 架构清晰、容易演示,但要注意两个框架访问同一批表时的模型一致性问题 |
| 方式B:主进程嵌入 | 用werkzeug的DispatcherMiddleware把Flask应用作为Django的子应用挂载 | 只想演示"同时使用了两个框架"时用来交差 | 部署简单,但两个框架的session、中间件会互相干扰,后期难维护 |
| 方式C:完全微服务化 | 各自独立数据库,通过REST API松耦合通信 | 分布式结课项目 | 演示效果好,但数据一致性难保证,工作量也大 |
我最终选了方式A。理由很现实:课程设计和毕设的评审老师最看重的是"业务闭环",也就是用户能发布物品、能下单、能完成换购。共享一个MySQL能保证交易的强一致性,Flask只负责读多写少的辅助接口,就算Flask挂了,Django站点的核心功能也不受影响。这其实是真实企业里"主业务收敛、辅助功能外放"思想的一个简化版,答辩的时候这样讲,非常加分。
1.3 为什么推荐先设计架构而不是先写代码
很多同学拿到题目第一件事就是django-admin startproject,然后就开始写models。我的建议是反过来,先花半天时间把"这个平台到底有哪些角色、哪些核心流程、哪些状态"想清楚。
你可以问自己三个问题:
- 谁是系统用户?学生、管理员,还可能有什么?
- 最核心的流程是什么?发布物品、发起换购、确认交换、互相评价。
- 哪些功能写进Django,哪些拆给Flask?根据依赖关系划分,而不是按页面划分。
想清楚这三个问题,后面写代码就是照图施工,而不是边写边改。这个项目里我最深刻的体会就是:前期架构多花的时间,后期至少能省三倍。
2. 校园换购平台的需求本质:不是简单二手交易
闲置物品换购听起来和闲鱼、转转差不多,但落在校园场景里,需求逻辑其实有明显区别。搞清楚这些,数据库表设计才有的放矢。
2.1 校园场景的三个核心差异
第一,用户身份天然可信。注册必须绑定学校域名邮箱或学号,这意味着平台内交易双方的"违约成本"更高,不需要像闲鱼那样做复杂的芝麻信用体系。
第二,交易以线下自提为主。大学生活动范围高度集中,宿舍区、教学楼、食堂就是天然的交货点。所以系统不需要物流模块,但要记录"交易地点偏好",比如"只限本校图书馆自提"。
第三,"换购"包含了物物交换和补差价。这是这个项目和普通二手交易平台最大的区别。用户发布物品时,除了标价,还可以写明"想交换什么",比如"用一台Kindle换一副降噪耳机,可以补差价50元"。这意味着订单表的设计不能只有买家、卖家、金额三个字段。
2.2 核心角色与功能边界
我把系统拆成前台、后台、辅助服务三块:
| 角色 | 核心功能 | 所属框架 |
|---|---|---|
| 普通学生 | 注册登录、浏览物品、发布闲置、发起换购、留言询问、确认收货、评价 | Django |
| 系统管理员 | 审核物品、处理举报、用户封禁、数据统计 | Django Admin + Flask统计接口 |
| 辅助服务 | 物品推荐、浏览量统计、图片压缩、Excel导出 | Flask |
功能边界定了之后,页面结构就清楚了。前台总共没几个页面:首页(物品流)、物品详情、发布页、个人中心、订单列表、站内信。管理后台直接用Django Admin改一改就够用,不需要额外写。
2.3 交易状态机:整个业务最核心的部分
换购流程不是简单的"下单-付款-发货-收货",因为存在"双方交换物品"的情况,状态机必须多几条分支。我自己项目里的状态流转是这样设计的:
- 用户A发布物品X,状态为"在售"。
- 用户B发起购买或换购请求,物品X变为"交易中",同时生成一条订单记录。
- 双方约定时间地点,线下验货。
- 双方都在系统中点击"确认交换"或"确认收货",订单变为"已完成"。
- 如果任何一方在"交易中"超时未确认,可以申请取消,订单变为"已取消",物品X自动回到"在售"。
单独把"换购"拎出来说:当B提出"我想用物品Y换你的X"时,系统要同时锁定X和Y两件物品,直到双方确认完成或者取消。这个"双向锁定"逻辑容易漏写,业务代码里一定要同时处理两个物品的状态。
3. 数据库设计:换购业务的核心表长什么样
选型上我用了MySQL 8.0,原因无他,校园网环境里MySQL最通用,后面查资料、找问题都容易。Django侧用ORM管理表结构,Flask侧通过SQLAlchemy读取同一批表。
3.1 用户表与扩展资料表
Django自带的auth_user不要直接改,而是建一张profile表一对一关联。需要存的就是学号、学校邮箱、宿舍区域、信用分、头像路径。
class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") student_id = models.CharField(max_length=20, unique=True, verbose_name="学号") campus_email = models.EmailField(unique=True, verbose_name="校园邮箱") dorm_area = models.CharField(max_length=50, blank=True, verbose_name="宿舍区域") credit_score = models.IntegerField(default=100, verbose_name="信用分") avatar = models.ImageField(upload_to="avatars/", blank=True)为什么不直接在User上加字段?因为Django的User模型被Admin、认证、session到处引用,直接改字典字段容易在后续升级或第三方库适配时报错。一对一的Profile几乎是所有Django项目的标准做法。
3.2 物品表:状态和换购意向是灵魂
物品表是整个平台的流量核心,字段设计直接影响后续的检索和交易。
class Item(models.Model): STATUS_CHOICES = [ ("on_sale", "在售"), ("trading", "交易中"), ("sold", "已售出"), ("off_shelf", "已下架"), ] title = models.CharField(max_length=100) category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True) description = models.TextField() price = models.DecimalField(max_digits=8, decimal_places=2) want_exchange = models.CharField(max_length=200, blank=True, verbose_name="想换的物品") condition_level = models.IntegerField(choices=[(i, f"{i}成新") for i in range(1, 11)]) image = models.ImageField(upload_to="items/") owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="items") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="on_sale") view_count = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True)这里有一个值得说的细节:condition_level我用了0到10的成新度而不是"几乎全新/轻微使用痕迹"这样的模糊描述。原因很简单:成新度便于后期用SQL做排序和筛选,比如"只看九成新以上的物品",文本字段就没法高效查。
3.3 订单表:换购和购买共用一张表
我见过很多项目把"购买订单"和"换购订单"分成两张表,这是个坑。因为换购本质上就是"双方互为买家和卖家"的交易,拆成两张表后,查询"我参与过的所有交易"就变得非常痛苦。
正确的做法是用一张order表,通过判断exchange_item字段是否为空来区分交易类型:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | 主键 | 订单号 |
| item | FK | 主物品 |
| buyer | FK | 发起交易的人 |
| seller | FK | 物品发布者 |
| exchange_item | FK可空 | 若为换购,则指向买家提供的物品 |
| price | Decimal | 成交价或补差价 |
| status | CharField | pending / confirmed / completed / cancelled |
| created_at / updated_at | DateTime | 记录时间 |
关键逻辑是:如果exchange_item不为空,那么这笔订单成交时,buyer的exchange_item状态必须变为"sold",同时item状态变为"sold"。这两个更新必须放在同一个数据库事务里,否则会出现"A以为换成功了、B的物品还挂着卖"的数据错乱。
3.4 留言、举报、评价表
这三张表结构都比较简单。留言表关联物品和用户;举报表关联被举报的物品或留言;评价表关联订单,买家和卖家各自只能评价一次,用order_id + reviewer_id做联合唯一约束。这些都属于快速迭代的表,先建起来,后续有需求再加字段就行。
数据库设计这块,我的实际经验是:不要想着一步到位建出完美表结构,先让核心交易闭环跑通,再根据实际页面反馈补索引和字段。这个项目里我最开始没给item.status加索引,结果数据量到几千条时首页查询明显变慢,后来才补上。对课程设计级别的数据量来说,这一步不做也没事,但知道了对答辩有好处。
4. Django主站:从注册登录到订单流转的实现
Django侧承载了平台的全部写操作,我按"用户-物品-订单-后台"四条线来讲。
4.1 校园身份验证:注册时拦截非校园邮箱
校园平台最大的特色是身份可信,所以注册环节要拦住校外人。实现方式就是在UserCreationForm的子类里重写clean_email,强制邮箱后缀为学校域名。
class CampusUserCreationForm(UserCreationForm): email = forms.EmailField(required=True) def clean_email(self): email = self.cleaned_data["email"].lower() if not email.endswith("@your-university.edu.cn"): raise forms.ValidationError("请使用校园邮箱注册") return email这里的@your-university.edu.cn替换成你自己的学校域名即可。另一个可选的验证是让用户填学号,再和管理员维护的学号表比对,不过课程设计阶段用邮箱后缀就够用了。
4.2 物品发布的图片处理
物品发布页用ModelForm绑定Item模型,图片上传这块有几个容易踩的坑。第一个是默认的ImageField一次只支持一张图,想支持多图就得建一个ItemImage子表;第二个是用户上传的原图动辄几兆,直接存服务器既浪费空间又拖慢页面加载。
我的做法是在保存时用Pillow把图片压缩两份:一份是列表页用的缩略图(例如400x300裁剪),一份是详情页用的原图(限制最大边1600px)。上传逻辑写在form.save()里重写:
def save(self, commit=True): item = super().save(commit=False) if self.cleaned_data.get("image"): img = Image.open(self.cleaned_data["image"]) img.thumbnail((1600, 1600)) # 压缩后保存到 MEDIA_ROOT,并重新赋值路径 if commit: item.save() return item这段代码的核心意图是:不要在模板里做任何图片缩放,全部在服务端生成好物理文件。Django模板层虽然也能做缩略图,但那是在每次请求时动态处理的,高并发下CPU直接被打满。
4.3 订单状态机的事务控制
当用户B对物品X发起换购时,要同时做三件事:创建订单、把X的状态改为"交易中"、把B的物品Y的状态也改为"交易中"。这三个写操作必须在一个原子事务里,用Django的transaction.atomic包起来。
from django.db import transaction @transaction.atomic def create_exchange_order(item_x, item_y, buyer, price_diff): order = Order.objects.create( item=item_x, exchange_item=item_y, buyer=buyer, seller=item_x.owner, price=price_diff, status="pending", ) item_x.status = "trading" item_x.save() item_y.status = "trading" item_y.save() return order是否要用select_for_update()加行锁?如果只是课程设计,数据量小、并发低,不加也能跑。但如果你在答辩时想展示自己对并发问题的理解,这是绝佳素材。select_for_update()会在数据库层面锁定这两件物品的行,防止两个用户同时发起换购导致状态错乱。
item_x = Item.objects.select_for_update().get(pk=item_x_pk) item_y = Item.objects.select_for_update().get(pk=item_y_pk)这里要提醒一句:select_for_update()必须在事务内使用,也就是说调用这个查询的函数要被@transaction.atomic包裹,否则锁会在查询后立刻释放,起不到作用。
4.4 用Django Admin做管理后台
管理后台我几乎没有额外写页面,全部靠Django Admin定制。核心配置就是把Item、Order、Report注册进去,并定制列表展示字段:
@admin.register(Item) class ItemAdmin(admin.ModelAdmin): list_display = ("title", "owner", "price", "status", "created_at") list_filter = ("status", "category") search_fields = ("title", "owner__username") actions = ["force_off_shelf"] @admin.action(description="强制下架选中物品") def force_off_shelf(self, request, queryset): queryset.update(status="off_shelf")actions自定义操作在处理举报时非常管用:管理员在举报列表里看到被举报物品,勾选、下拉选择"强制下架"、执行,三步搞定。不需要写任何前端代码。
5. Flask辅助服务:推荐接口和统计看板怎么落地
Django把主要业务包圆了,那Flask在项目里到底干什么?我这边定了两个明确的活:推荐接口和统计看板。这两块逻辑简单、读多写少、和主流程解耦,非常适合用Flask起一个轻量服务。
5.1 Flask推荐接口:基于浏览记录的简单协同过滤
推荐这块不用整复杂的机器学习模型,课程设计阶段做"看过这个物品的人还在看同类物品"就够了。算法原理很简单:
- 根据用户在
ItemViewLog表里的浏览记录,找出浏览过当前物品的所有用户; - 再查这些用户还浏览过哪些同分类的物品;
- 按出现次数倒序取前6个,排除用户已拥有的物品。
@app.route("/api/recommend/<int:item_id>") def recommend(item_id): item = db.session.get(Item, item_id) viewers = [v.user_id for v in db.session.query(ItemViewLog).filter_by(item_id=item_id)] candidates = {} for uid in viewers: for vlog in db.session.query(ItemViewLog).filter_by(user_id=uid).all(): if vlog.item_id != item_id: candidates[vlog.item_id] = candidates.get(vlog.item_id, 0) + 1 ranked = sorted(candidates.items(), key=lambda x: x[1], reverse=True)[:6] return jsonify([{"item_id": k} for k, _ in ranked])这段代码本身没什么技术含量,但它在架构上定义了"Flask负责计算、Django负责展示"的协作模式。前端页面通过fetch("/api/recommend/3")拿到推荐列表,再渲染物品卡片,主站代码保持干净。
5.2 统计看板:给管理员看的轻量报表
第二个Flask接口是统计看板。我需要几组数据:每日发布物品数、当前在售物品分类占比、热门物品Top10。直接在Flask里用SQLAlchemy聚合查询然后返回JSON,前端用Chart.js渲染图表。
@app.route("/api/stats/category") def stats_category(): rows = db.session.query(Item.category_id, func.count(Item.id)).group_by(Item.category_id).all() return jsonify([{"category_id": cid, "count": cnt} for cid, cnt in rows])这个统计接口如果也塞进Django里,代码量和路由复杂度都会增加。放在Flask这边,整个Django项目只需要在Admin后台里嵌一个iframe或写一个fetch调用就能展示图表。
5.3 双框架共享同一个MySQL的模型一致性问题
这是整个项目里最隐蔽的坑。Django的ORM会自动给模型加字段和约束,比如多对多关系会生成中间表,在某些情况下还有局部索引。Flask的SQLAlchemy模型是从零手工写的,两边字段定义稍有出入,查询结果就可能出问题,轻则查不到数据,重则SQLalchemy报"字段不存在"。
我的经验是:以Django的模型为准,Flask侧只做只读查询的模型定义,字段精简到只定义要用到的部分,并且不允许Flask侧做任何写操作。比如Item表在Flask侧只需要定义id、title、category_id、price、status、view_count这几个字段,因为推荐和统计只用到这些。这样两边模型的字段交集很小,出错的概率就低。
还有一个细节:created_at字段在Flasks侧定义时必须加上timezone=True,否则查出来的时间会比实际时间慢8小时。这个问题我排查了很久,最后发现是Django启用了USE_TZ=True,MySQL里存的是UTC时间,SQLAlchemy默认按本地时间解析,两边时区没有对齐。
5.4 Flask服务的独立部署
Flask服务最终是作为一个独立进程跑的。开发环境直接python app.py就行,部署时我用Gunicorn来启动,和Django那边的uWSGI区分开。这样两个服务互不干扰,Flask挂了重启Flask,Django挂了重启Django。
gunicorn -w 2 -b 127.0.0.1:5001 app:app日志单独写到flask_app.log,方便排查问题。
6. 开发到部署阶段的真实踩坑清单
这部分是全文含金量最高的地方。以下每一条我都实际遇到过,不给结论直接给排查思路。
6.1 Python版本和Django版本之间的兼容性
先检查服务器上的Python版本再选Django版本。Django 4.2要求Python 3.10以上,如果服务器是3.8,装新版Django直接报语法错误。我一开始在本地Windows上用Python 3.12开发一切正常,部署到Linux服务器发现系统自带的Python是3.8,最后锁定方案是Django 4.1.7 + Flask 2.3,兼容性最稳。
提示:做项目前先确定团队或实验室服务器的Python版本,
python3 --version一看便知,不要想当然装最新版。
6.2 Pillow扩展库装不上的问题
图片处理依赖Pillow,在纯净系统上经常装失败。报错信息通常是RequiredDependencyException: zlib或者jpeg。原因很直白:系统缺底层图片库。解决方式:
# Ubuntu/Debian sudo apt-get install libjpeg-dev zlib1g-dev pip install Pillow6.3 图片上传后页面404
本地开发时MEDIA_URL和MEDIA_ROOT配置正确,图片能显示。部署到Nginx后所有图片路径全部404。原因是Django本身不提供静态文件服务,Nginx需要把包括/media/和/static/在内的路径直接代理到服务器的物理目录。
Nginx配置里加一段:
location /media/ { alias /var/www/project/media/; }这个坑几乎人人都会踩,因为这属于"Nginx配置"不属于"Django代码",教程里很少写清楚。
6.4 表结构改来改去的迁移灾难
开发过程中我多次修改Item模型,导致迁移文件堆积,数据库状态和模型不同步的怪问题时有出现。后来养成一个习惯:每次改模型前先python manage.py makemigrations --dry-run看看即将生成的迁移操作是否符合预期,再真正执行。
如果已经乱套了怎么办?我的经验是别硬修迁移文件,直接重置:
python manage.py migrate app_name zero python manage.py makemigrations python manage.py migrate但前提是能接受清空该app的数据。开发阶段无所谓,如果是接近答辩的数据,就不要这么干。
6.5 换购并发导致的状态错乱
有次测试时发现,两个同学同时点击"换购"按钮,同一件物品生成了两笔订单。原因就是查询物品状态和创建订单不在一个事务里,也没有加行锁。
修复方案前面已经说了:select_for_update()加transaction.atomic。这是一个非常好的答辩回答题目,面试官问到"如何防止超卖"时,你拿这个项目里真实修过的bug举例,说服力远超背书。
6.6 Django时区与MySQL时区不一致
前面提到Flask侧查时间差8小时,其实Django侧也遇到过。解决方案是:
# settings.py USE_TZ = True TIME_ZONE = "Asia/Shanghai"这样Django在读写MySQL时会自动把UTC时间转换为东八区。Flask侧则是手动datetime.now(timezone.utc)或者直接查询时加上convert_tz。
6.7 双服务部署时Nginx的upstream配置
两个服务同时跑,前端要同时访问Django的/路径和Flask的/api/路径,靠Nginx路由区分:
upstream django_backend { server 127.0.0.1:8000; } upstream flask_backend { server 127.0.0.1:5001; } server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://flask_backend; } location / { proxy_pass http://django_backend; } }这样一个域名同时跑两个Python Web应用,演示的时候不用来回切换端口,交互体验也自然。
6.8 页面模板和静态资源的重复加载
最后是一个容易被忽略的性能问题。Django的模板是每次请求都渲染一遍,如果有高频页面(比如首页物品流),建议加一层缓存。最简单的做法是Django的cache_page装饰器缓存首页60秒:
from django.views.decorators.cache import cache_page urlpatterns = [ path("", cache_page(60)(views.index), name="index"), ]加完缓存后,首页压力大幅下降,Flask推荐接口那边也轻松不少。
7. 答辩演示和后续扩展的建议
做完项目到真正演示还有一段距离。我见过实力不错但演示翻车的,也见过功能一般但演示节奏极好的。基于实际经验分享几点。
7.1 演示前准备好"种子数据"和录屏
不要现场发布物品、现场拍图。提前把一个品类下放满十几件物品,图片用真实拍的照片而非占位图,这样评委打开首页时视觉效果好很多。同理,推荐接口的演示也别用空数据库去测,先在ItemViewLog里造好一批浏览记录,否则推荐结果永远为空。
录屏软件开好,万一现场网络不稳定或者服务没起来,放录屏照样能讲完。
7.2 讲清楚"框架边界"比讲功能更重要
答辩时评委最常问的就是:"你为什么要用Flask,Django不能做推荐接口吗?" 这时候你把第1节的架构图往白板上一画,说明"写操作集中在Django、读密集的辅助服务放Flask、共享数据库保证一致性",评委立刻知道你理解架构设计,而不是只会堆功能。
7.3 后续可以扩展的三个方向
这个项目做完后,想进一步延伸可以考虑三条线:
- 给物品加全文搜索,用Django的
SearchVector或者接入Elasticsearch,解决物品多了之后分类筛选不好用的问题; - 把推荐服务升级成基于用户行为排序的算法,比如根据分类偏好加权,不改变架构,只改Flask侧的计算逻辑;
- 加Redis缓存热点数据,首页物品流和推荐结果都缓存到Redis,进一步减轻数据库压力。
我自己做这个项目的最大体会是:技术难点不在某个框架的API上,而在"如何让两个框架各司其职、协同不打架"。把这个问题想透,你就能从"跟着教程敲代码"进阶到"真正理解Web应用怎么搭"。希望这篇能帮你少走几个弯路。