首页 / 资讯中心 / 文章详情

Flask与Django双框架协作:校园闲置物品换购平台实战详解

Flask与Django双框架协作:校园闲置物品换购平台实战详解 ★ FEATURED ARTICLE
看到基于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_deletemodels.CASCADE, related_nameprofile) student_id models.CharField(max_length20, uniqueTrue, verbose_name学号) campus_email models.EmailField(uniqueTrue, verbose_name校园邮箱) dorm_area models.CharField(max_length50, blankTrue, verbose_name宿舍区域) credit_score models.IntegerField(default100, verbose_name信用分) avatar models.ImageField(upload_toavatars/, blankTrue)为什么不直接在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_length100) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue) description models.TextField() price models.DecimalField(max_digits8, decimal_places2) want_exchange models.CharField(max_length200, blankTrue, verbose_name想换的物品) condition_level models.IntegerField(choices[(i, f{i}成新) for i in range(1, 11)]) image models.ImageField(upload_toitems/) owner models.ForeignKey(User, on_deletemodels.CASCADE, related_nameitems) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaulton_sale) view_count models.IntegerField(default0) created_at models.DateTimeField(auto_now_addTrue)这里有一个值得说的细节:condition_level我用了0到10的成新度而不是几乎全新/轻微使用痕迹这样的模糊描述。原因很简单:成新度便于后期用SQL做排序和筛选比如只看九成新以上的物品文本字段就没法高效查。3.3 订单表:换购和购买共用一张表我见过很多项目把购买订单和换购订单分成两张表,这是个坑。因为换购本质上就是双方互为买家和卖家的交易拆成两张表后查询我参与过的所有交易就变得非常痛苦。正确的做法是用一张order表通过判断exchange_item字段是否为空来区分交易类型:字段类型说明id主键订单号itemFK主物品buyerFK发起交易的人sellerFK物品发布者exchange_itemFK可空若为换购则指向买家提供的物品priceDecimal成交价或补差价statusCharFieldpending / confirmed / completed / cancelledcreated_at / updated_atDateTime记录时间关键逻辑是:如果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(requiredTrue) 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, commitTrue): item super().save(commitFalse) 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( itemitem_x, exchange_itemitem_y, buyerbuyer, selleritem_x.owner, priceprice_diff, statuspending, ) 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(pkitem_x_pk) item_y Item.objects.select_for_update().get(pkitem_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(statusoff_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_iditem_id)] candidates {} for uid in viewers: for vlog in db.session.query(ItemViewLog).filter_by(user_iduid).all(): if vlog.item_id ! item_id: candidates[vlog.item_id] candidates.get(vlog.item_id, 0) 1 ranked sorted(candidates.items(), keylambda x: x[1], reverseTrue)[: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侧定义时必须加上timezoneTrue否则查出来的时间会比实际时间慢8小时。这个问题我排查了很久最后发现是Django启用了USE_TZTrueMySQL里存的是UTC时间SQLAlchemy默认按本地时间解析两边时区没有对齐。5.4 Flask服务的独立部署Flask服务最终是作为一个独立进程跑的。开发环境直接python app.py就行部署时我用Gunicorn来启动和Django那边的uWSGI区分开。这样两个服务互不干扰Flask挂了重启FlaskDjango挂了重启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), nameindex), ]加完缓存后,首页压力大幅下降,Flask推荐接口那边也轻松不少。7. 答辩演示和后续扩展的建议做完项目到真正演示还有一段距离。我见过实力不错但演示翻车的,也见过功能一般但演示节奏极好的。基于实际经验分享几点。7.1 演示前准备好种子数据和录屏不要现场发布物品、现场拍图。提前把一个品类下放满十几件物品,图片用真实拍的照片而非占位图,这样评委打开首页时视觉效果好很多。同理,推荐接口的演示也别用空数据库去测,先在ItemViewLog里造好一批浏览记录,否则推荐结果永远为空。录屏软件开好,万一现场网络不稳定或者服务没起来,放录屏照样能讲完。7.2 讲清楚框架边界比讲功能更重要答辩时评委最常问的就是:你为什么要用Flask,Django不能做推荐接口吗? 这时候你把第1节的架构图往白板上一画,说明写操作集中在Django、读密集的辅助服务放Flask、共享数据库保证一致性,评委立刻知道你理解架构设计,而不是只会堆功能。7.3 后续可以扩展的三个方向这个项目做完后,想进一步延伸可以考虑三条线:给物品加全文搜索,用Django的SearchVector或者接入Elasticsearch,解决物品多了之后分类筛选不好用的问题;把推荐服务升级成基于用户行为排序的算法,比如根据分类偏好加权,不改变架构,只改Flask侧的计算逻辑;加Redis缓存热点数据,首页物品流和推荐结果都缓存到Redis,进一步减轻数据库压力。我自己做这个项目的最大体会是:技术难点不在某个框架的API上,而在如何让两个框架各司其职、协同不打架。把这个问题想透,你就能从跟着教程敲代码进阶到真正理解Web应用怎么搭。希望这篇能帮你少走几个弯路。
阅读完成 · 觉得有帮助?
咨询建站