ARTICLE DETAIL

资讯详情

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

Django+Flask+Vue打造流浪狗救助捐赠平台:全栈设计与实践

Django+Flask+Vue打造流浪狗救助捐赠平台:全栈设计与实践 “这个平台的难点从来不是怎么写代码而是怎么让救助站里那台老电脑打开后台页面时不觉得反人类。”这是我做完这个基于Django、Flask和Vue的流浪狗救助捐赠平台后脑子里最真实的一句话。平台本身的功能不复杂展示待领养的流浪狗档案、受理领养申请、记录每一笔捐赠、发布救助站的志愿者活动。但它牵涉的业务状态特别多一只狗从救助进站到被领养走中间要经历体检、绝育、暂养、审核、回访这么久每一步的数据都要能查还要让普通访客一眼就能看懂这背后是一整套需要用心设计的流程。这个项目很适合刚学完Python基础、想在简历上放一个完整作品的人。我不会泛泛讲理论直接把我们按Django为主后端、Flask做旁路服务、Vue做前端、PyCharm作为开发环境拆出来的整个方案从选型思路到踩过的坑一五一十写出来。1. 项目整体设计为什么是Django和Flask同时出现1.1 双框架并存不是炫技是被业务逼出来的很多人看到“django-flask”这个组合会觉得奇怪一个人写两个后端框架不太常见。当初我也纠结过但后来想明白了一个道理这个平台的核心数据模型非常重动物档案、领养申请、捐赠记录、志愿者活动之间全是关联关系这套东西用Django做最顺手它有现成的ORM、Admin后台、认证体系我不需要自己造轮子。Flask则承担另一个完全独立的任务。当时设计平台的时候捐赠模块用的是第三方支付沙箱的异步回调以及领养状态变更时需要向前端页面推送实时通知。这两个任务其实和主站的数据模型关系很弱而且需要独立进程持续监听。如果把逻辑塞进Django里Django的请求-响应模型处理长连接很别扭我还得担心定时任务或者回调线程把主服务压垮。用Flask单独开一个小服务跑起来职责单一内存占用也低就算它挂了也影响不到主站的浏览和后台管理。有人可能问Flask能做的FastAPI不是都能做吗FastAPI确实性能更好、自带OpenAPI文档如果只写纯API网关我会选FastAPI。但这个项目的Flask子服务要配合Django的Admin体系还要对接一些老旧的运维习惯Flask的生态更简单直接业务里用起来足够稳。技术选型最大的原则是别给自己找麻烦。1.2 业务模块怎么切前端路由才能顺我先把平台拆成四大块这个拆分直接决定了后续Django的app划分和Vue的路由设计所以不要在前期偷懒动物信息模块流浪狗的档案卡片包含照片、品种、年龄、健康状况、救助时间、当前状态。领养管理模块用户在线提交申请管理员审核跟踪整个领养进度。捐赠管理模块线上捐赠入口、捐赠记录展示、感谢墙。志愿者与动态模块救助站发布的志愿活动公告以及爱心动态的图文更新。前端路由就是根据这四块来做列表页和详情页的。平台用户分三类普通游客可以浏览动物档案和动态注册用户可以提交领养申请、发起捐赠救助站管理员在Django自带的Admin后台里管理全部数据。注意红领站管理员用的是Admin后台而不是我另写的管理端页面这也是选Django红利最大的地方。Django的Admin系统相当于白送了一个带鉴权的数据管理桌面稍微配置一下list_display、search_fields就能直接给志愿者用省下大量开发时间。2. 核心数据模型设计这些表决定后续开发的体验2.1 动物档案表状态字段比删一条记录重要得多我见过很多新手项目把动物的“已领养”状态实现成把这行记录直接删掉这种设计在今天来看是灾难。因为一只狗从救助到领养中间产生的看诊记录、申请人信息、志愿者回访内容全都要挂在狗身上。我留了一个status字段用Django的choices约束取值让它只有待领养、审核中、已领养、离世这四种状态。离世也要诚实记录救助领域会有这种情况数据不该被抹去回访和复盘才靠得住。class Animal(models.Model): STATUS_CHOICES [ (available, 待领养), (pending, 领养审核中), (adopted, 已领养), (deceased, 已离世), ] name models.CharField(名字, max_length50) breed models.CharField(品种, max_length50, blankTrue) age models.IntegerField(年龄, default0) gender models.CharField(性别, max_length10, choices[(M, 公), (F, 母)]) neutered models.BooleanField(已绝育, defaultFalse) health_status models.CharField(健康状况, max_length100, default良好) rescue_date models.DateField(救助日期) photos models.ImageField(照片, upload_toanimals/, blankTrue) description models.TextField(救助故事, max_length500) status models.CharField(当前状态, max_length20, choicesSTATUS_CHOICES, defaultavailable) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) def __str__(self): return f{self.name}-{self.get_status_display()}这里有几个细节值得说。photos我在初期用的是单张ImageField但实际一只狗入院要拍好几张照片体检的、剪毛后的、在院子里晒太阳的。后来我改成了一对多的AnimalPhoto模型才真正解决了前端轮播图的数据来源问题。另外adopted状态不能光靠Animal维护必须和领养申请单关联起来所以设计了下面这张审核表。2.2 领养、捐赠、志愿者活动把责任关进笼子里领养申请不能只是简单的表单存储它要能追踪状态从待审核到初审通过再到志愿者家访、正式领养、被拒绝。这个流程用state字段做状态机是业界常规做法。class AdoptionApplication(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 初审通过), (home_visit, 家访中), (adopted, 已领养), (rejected, 已拒绝), ] animal models.ForeignKey(Animal, on_deletemodels.CASCADE, related_nameapplications) applicant models.ForeignKey(User, on_deletemodels.CASCADE, related_nameadoptions) full_name models.CharField(真实姓名, max_length30) phone models.CharField(联系电话, max_length20) address models.CharField(居住地址, max_length200) has_yard models.BooleanField(是否有独立院子, defaultFalse) family_agreed models.BooleanField(家人是否同意, defaultFalse) status models.CharField(审核状态, max_length20, choicesSTATUS_CHOICES, defaultpending) remark models.TextField(审核备注, blankTrue) created_at models.DateTimeField(auto_now_addTrue)注意animal的外键用了CASCADE意思是删除动物档案时会连着删掉所有申请。我建议把它改成PROTECT因为领养申请是产生过人工审核动作的法律性质记录不应该因为动物档案被误删而消失。就这个问题我在项目快完工时改过一次重构成本不小新同学第一次建表就想清楚这个点。捐赠表结构上有一条铁律核心金额字段必须保存业务发生那一刻的快照不能实时去关联用户表算。用户注销、改名都不应该影响历史捐赠流水。class DonationRecord(models.Model): donor models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue) donor_name models.CharField(捐赠人显示名, max_length50) amount models.DecimalField(捐赠金额, max_digits10, decimal_places2) transaction_id models.CharField(支付流水号, max_length100, uniqueTrue) status models.CharField(支付状态, max_length20, choices[(pending, 待支付), (success, 成功), (failed, 失败)], defaultpending) message models.CharField(寄语, max_length200, blankTrue) created_at models.DateTimeField(auto_now_addTrue)流水号加了unique约束这是幂等处理的关键。支付回调可能会网络重发没有唯一约束就会插入多条重复记录。这个坑我当年在别的项目里踩过一次记忆非常深刻。3. Django后端与Flask子服务从API到回调的实现细节3.1 用DRF十分钟搭出可用的动物接口Django REST Framework是这种业务场景的正解。用ModelViewSet加上router基础CRUD接口就出来了。这里我强烈建议不要自己去手写增删改查的视图函数重复劳动也没有必要。class AnimalViewSet(viewsets.ModelViewSet): queryset Animal.objects.filter(status__in[available, pending]) serializer_class AnimalSerializer permission_classes [IsAuthenticatedOrReadOnly] filterset_fields [status, breed, gender, neutered] search_fields [name, description]注意queryset里我默认过滤掉了已领养和离世的数据这样前端也不会暴露不需要展示的信息。Serializer也很简单但有一个坑直接返回外键字段时只给id前端要再发请求拿名字体验很不好。我直接用SerializerMethodField把救助站名、照片完整地址拼接进去。class AnimalSerializer(serializers.ModelSerializer): station_name serializers.CharField(sourcestation.name, read_onlyTrue) photos serializers.SerializerMethodField() age_display serializers.CharField(sourceget_age_display, read_onlyTrue) class Meta: model Animal fields [id, name, breed, age, age_display, gender, neutered, health_status, rescue_date, description, status, station_name, photos, created_at] def get_photos(self, obj): request self.context.get(request) return [request.build_absolute_uri(p.image.url) for p in obj.related_photos.all()]build_absolute_uri这个细节很影响前端体验。如果不做这一步Vue那边拿到的图片路径是/media/animals/xx.jpg这种相对路径本地测试看着能用但部署到云服务器后前端域名和后端域名不一致图片全部裂掉。路径问题一定要在生产环境之前解决。3.2 Flask轻量子服务模拟支付回调与SSE状态推送Flask这个子服务在项目里主要有两个入口。第一个是接收第三方支付沙箱的回调请求。因为沙箱可能重试回调我设计成先查流水号是否已处理再决定要不要执行更新逻辑保证幂等。代码不用复杂但要考虑到数据库连接池的释放问题。from flask import Flask, request, jsonify from datetime import datetime import requests app Flask(__name__) app.post(/api/payment/callback) def payment_callback(): data request.get_json() transaction_id data.get(transaction_id) status data.get(status) # 这里通过内部HTTP调用Django主站的API接口更新订单状态 try: resp requests.post( http://127.0.0.1:8000/api/internal/update_donation/, json{ transaction_id: transaction_id, status: status, amount: data.get(amount), paid_at: datetime.now().isoformat() }, timeout5, ) resp.raise_for_status() except requests.RequestException: # 更新失败时返回失败让沙箱过一会重试 return jsonify({code: failed}), 500 return jsonify({code: success})不要用同步requests的写法去访问Django接口这个方式是演示用的生产环境建议换成Celery异步任务或者直接把数据库更新逻辑写好。我是为了图简单Flask和Django共用一个MySQL理论上Flask直接更新数据库也是可以的但那样两个服务都操作同一批表将来迁移表结构的时候会很痛苦所以我宁可多一跳HTTP调用把数据写入口统一收敛在Django这边。第二个入口是SSE通知服务。Vue前端想实时看到审核状态的变化用轮询也能做但体验差、请求多。SSE用EventSource持久连接Flask这边做起来非常简单app.get(/api/notifications/stream) def stream(): def event_stream(): while True: # 实际项目中这里从Redis队列读取最新状态消息 msg redis_client.brpop(adoption_status_queue, timeout5) if msg: yield fdata: {msg[1].decode(utf-8)}\n\n return Response(event_stream(), mimetypetext/event-stream)这个逻辑在生产里还需要配合反向代理禁用缓冲否则响应会被nginx缓存住前端一直收不到消息。我一开始不知道在本地跑得好好的部署到服务器后就傻眼了这类问题我会在后面的踩坑环节专门讲。3.3 Django侧的内部更新接口刚才Flask回调里访问了Django的/api/internal/update_donation/这个接口不需要认证但必须限制内网访问。我只让它接受127.0.0.1来源同时加一个自定义的X-Internal-Token请求头防止被外网扫到。这是一个安全习惯就算你对安全性要求不高也建议养成这种拆分内部接口的习惯。内部接口里干了三件事第一把捐赠记录状态改成成功第二把金额累加进救助站的累计捐赠字段第三给捐赠人发一条站内信。这里不做逻辑过多展开但这类“回调后要联动好多个数据”的业务刚入手的同学最好画一个时序图把每个动作的前置条件和后置结果列清楚再动手。4. Vue前端从环境搭建到页面联调的全过程4.1 PyCharm和Vue环境配置社区版完全够用开发这个项目用的是PyCharm前端开发也是直接在里面写的。很多人卡在环境配置其实流程很清晰Python环境从官网下载Python 3.10以上版本安装时勾选Add to PATH。但我不建议直接进PyCharm新建项目先在终端用python -m venv venv建好虚拟环境再用PyCharm打开项目在Settings里把Project Interpreter选到venv路径下的python.exe。这一步能避开很多权限问题。PyCharm版本社区版完全够用。不需要折腾任何激活社区版缺的那些功能对这个项目没有任何影响。官方版本下载的时候认准社区版开源免费用它做Django开发一点毛病没有。Django安装在PyCharm的Terminal里执行pip install django djangorestframework django-cors-headers pillow别用管理员权限虚拟环境内安装最干净。Vue环境用npm create vuelatest或者直接npm create vite我记得脚手架交互式问答里选上Vue Router。Node版本至少要16以上否则新版Vite可能报错。PyCharm里可以装Vue.js插件不过不装也能跑只是语法高亮差一些。Vue项目初始化的过程中最麻烦的是跨域。开发时Vite服务跑在5173端口Django跑在8000端口。解决方式是在Django侧用django-cors-headers允许localhost:5173的跨域请求。注意在生产环境要把白名单换成真实域名。4.2 首页动物卡片流与组件插槽的使用首页是第一个交给访客看的页面要展示待领养动物的卡片。这里组件设计的核心是卡片本身可复用不同场景下卡片中间的内容不一样。比如首页的卡片显示“了解它”的按钮领养专区的卡片显示“申请领养”的按钮已领养展示的是“找到新家”的标签。没有插槽的话你得写两套几乎一样的卡片组件维护起来很消耗精力。template div classanimal-card div classcard-image img :srcanimal.photos[0] :altanimal.name / span classcard-status{{ statusText }}/span /div div classcard-body h3{{ animal.name }} · {{ animal.breed }}/h3 p{{ animal.description }}/p slot nameactions :animalanimal router-link :to/animal/${animal.id} classbtn-link查看详情/router-link /slot /div /div /template这里给插槽传了animal对象作为插槽prop父组件拿到后可以自己决定按钮绑定到哪里。这个模式在真实项目里非常常用slot-scope听上去有点绕但你只要记住一句话插槽是父组件在子组件留的坑位坑位里的内容由父组件说了算。首页的数据加载我放在了onMounted里用axios请求/api/animals/?statusavailablepage1然后渲染到列表。分页用的是Element Plus的el-pagination组件注意后端DRF返回的是带页码结构的JSON前端要拿的是data.results而不是直接data数组很多新手第一步就挂在这上面。4.3 领养申请表单和状态流转领养申请页是一个典型的表单驱动页面。我用Vue写了一个多步骤表单第一步填写基本信息第二步勾选居住环境第三步确认承诺书。每一步点下一步之前要做字段校验Element Plus表单校验规则写在rules对象里。表单数据最后会POST到后端/api/adoption/这个接口要求登录状态所以axios封装里要带上token。我是把登录后JWT存到localStorage封装axios拦截器时每次请求都自动加Authorization头。领养状态发生变化后Vue端通过EventSource订阅Flask的SSE通知。用起来很简洁const es new EventSource(http://localhost:5000/api/notifications/stream); es.onmessage (event) { const data JSON.parse(event.data); if (data.application_id currentId) { applicationState.value data.new_status; } };注意页面卸载前一定要调es.close()不然用户离开页面连接还挂着控制台报错是一方面连接泄露会影响服务器资源。5. 实测中新手最容易卡死的5个坑这个项目我自己前后改了不止五轮也帮几个朋友调试过他们照着教程搭的版本下面的问题出现频率真的非常高做了一个速查表。报错现象根本原因解决办法前端请求后端接口被浏览器拦截提示CORS错误Django没开跨域白名单在Django配置里装django-cors-headers设置CORS_ALLOWED_ORIGINS为http://localhost:5173访问/admin/没有样式全是裸的HTMLDjango的静态文件服务没跑本地开发时INSTALLED_APPS里确保django.contrib.staticfiles存在且urlpatterns里用static()配置media路由POST表单提交后Django返回403 CSRF验证失败没带CSRF Token使用DRF时开启SessionAuthentication需要手动获取CSRF Cookie更省事的方案是改用JWT认证不依赖SessionVue里请求/api/animals拿到的数据层级不对DRF分页返回了{count,next,previous,results}前端不要直接返回response.data要用response.data.results作为列表数组分页参数读response.data.count领养申请创建成功但动物状态没有从available变成pending创建申请时没同步更新Animal状态字段在创建申请的后端逻辑里开启事务create申请记录并把对应animal.status改为pending再用transaction.atomic包裹删除动物档案时报错提示被其他表引用外键约束阻止了删除确认业务是否需要级联删除。如果不希望误删把外键改成PROTECT如果确定要联删代码里显式调用animal.delete()图片上传后页面显示404MEDIA_ROOT和MEDIA_URL没配对检查Django配置里MEDIA_ROOT指向本地目录开发环境urlpatterns添加re_path配合serve函数第三个坑要说多说一句。我在项目初期用的Django自带Session认证Vue这边每次POST都要带着CSRF Token前端改起来很别扭。后来整个项目统一换成了SimpleJWT登录接口返回access token和refresh token前端每次提交在请求头里加Bearer token就行彻底摆脱了CSRF的烦恼。这也是为什么我在表单提交那节专门强调axios拦截器封装。除此之外还有一个隐性问题要提醒PyCharm里Terminal运行Vue命令时会默认使用PyCharm配置的虚拟环境有时候终端提示npm不是内部或外部命令。这不是PyCharm坏了而是npm在系统PATH里而PyCharm的Terminal只加载了虚拟环境变量。我自己最省心的解法是从Windows的设置里把Node.js安装目录加进系统环境变量的Path然后重启PyCharm。Mac和Linux用户一般没有这个问题。6. 本地运行、部署和做完这个项目的真实体会6.1 本地跑通的整体流程走一遍为了照顾第一次接触这个项目的读者我把从零到跑通的命令整理成一条线# 后端 python -m venv venv venv\Scripts\activate # Windows pip install django djangorestframework django-cors-headers pillow django-filter djangorestframework-simplejwt django-admin startproject dog_rescue python manage.py startapp animals python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 8000 # Flask子服务 pip install flask requests redis python flask_app.py # 另开一个终端窗口 # 前端 npm create vuelatest cd frontend npm install npm install axios element-plus microsoft/fetch-event-source npm run dev到这一步三个服务都跑起来后浏览器打开http://localhost:5173首页就能看到动物卡片流。别忘了在Django Admin后台先录入几只测试用的动物没有数据的前端页面看起来跟白板似的联调也无从谈起。后端部署到生产环境我补充一个小知识点不要再靠runserver扛流量用gunicorn起多个workerFlask这边也一样。前端打包用npm run build把dist目录里的静态文件交给Nginx托管再把/api和/admin的请求反代到两个后端服务。这么一套部署方案在中小企业或者个人项目的量级下完全够用也足够稳定。6.2 做完这个项目我才明白的三件事第一件事技术栈的名气不重要它和业务场景合不合身才重要。Django自带的Admin系统救了整个项目救助站的人不需要学习复杂的新后台打开就能用Flask边路服务让我在增加功能时不折腾主站的迁移和测试。如果当初为了显得“高级”硬上某一个全家桶反而会把自己卡住。第二件事从网上找免费源码一定要谨慎。我见过很多同学下载了一个“完整版”项目源码跑都跑不起来或者跑起来一改就崩。原因很简单别人项目的业务模型和你自己的场景天然不一样你把改代码的时间省下来最后会花更多时间去理解别人的脑回路。还不如从零开始一步步建表、建接口、建页面哪怕慢代码每一行都是自己的出了问题你很快能定位。第三件事现在的AI辅助开发工具确实很强大我写很多样板代码也会用AI Agent来提速。但数据模型设计、状态流转这些核心业务逻辑你必须自己先想清楚。AI可以帮你补全Serializer、帮你写Vue组件但它不会告诉你一只流浪狗的领养流程分几步更不会替你去和救助站的人聊真实的业务需求。先有思考再有工具这个顺序不能反。这个项目从构思到能上台演示前后花了一个多月大部分时间耗在数据模型调整和前端联调上。如果你正准备照着类似方向做我的建议是把动物档案、领养申请、捐赠记录这三张表的关系先想明白再写代码后面会省下很多返工的时间。领养状态和捐赠状态都做成独立的流程状态字段不要偷懒省掉这个习惯会让你在后面加功能时轻松非常多。
返回列表