尧图精选

流浪动物救助网站开发实战:Django+Vue前后端分离部署全记录

🕒 发布时间:2026/10/1 22:53:31 📁 来源:尧图网络
1. 项目概述与需求拆解1.1 为什么需要流浪动物救助网站十几张A4纸打印的领养信息、贴在小区门口的过期告示、微信公众号后台一条条人工记录的救助申请——这是我第一次去本地流浪动物救助站调研时看到的真实场景。志愿者很多但信息散落领养人找不到合适的渠道救助站也缺乏一套能统计申请、管理动物的系统。所以当朋友找到我想做一个流浪动物救助网站时需求其实非常明确把所有待领养动物的信息集中展示让救助站能发布、编辑动物档案让用户能搜索、提交领养申请同时留一个后台给管理员审核和统计。这个项目最终落地成了一套前后端分离的网站技术栈是Python Vue后端以Django为主Flask做辅助服务开发工具用Pycharm。写这篇文章是希望把从环境搭建到部署上线的完整过程复盘一遍特别是那些搜索引擎里不告诉你、只有自己踩过坑才能总结出来的细节给正在做类似项目的人一个参考。1.2 技术选型为什么是Python Vue Django Flask选型的时候我对比过几个方案。为什么坚持Python因为除了我这个开发熟悉之外后续接爬虫同步外部宠物数据、跑一些简单的数据分析脚本都非常方便。Python生态里Vue并没有直接关系前端选Vue是因为它对新手友好、中文文档齐全、生态成熟配合Element UI这类组件库能很快把后台管理界面做出来。后端为什么Django为主核心原因是Django自带ORM、Admin后台、用户认证和迁移机制对于这种“大量表格管理”的场景太合适了。特别是Admin后台几乎是白送的管理界面救助站工作人员只需要登录Django Admin就能录入动物信息、审核领养申请省掉大量重复开发。那Flask放哪里我单独做了一个轻量服务跑图片压缩和外部数据采集。原因很简单Django的ORM很重处理高并发小请求不如Flask轻巧。救助站每天可能从不同平台同步几十条流浪猫狗信息这些操作不涉及复杂业务逻辑用Flask写一个小接口独立部署互不干扰。两个服务通过数据库和HTTP通信。听起来复杂实际在项目结构上只是一体化下的分模块后面我会详细说明。2. 环境准备与项目骨架搭建2.1 Pycharm配置Python开发环境很多初学者第一个卡点不是代码而是开发环境。我用的版本是Pycharm 2024专业版社区版也没问题只是专业版对Web项目支持更完整。Pycharm专业版需要激活的话建议直接用JetBrains官方渠道学生和教师可以免费申请教育授权比自己找破解靠谱得多也不会有安全风险。创建项目时Python解释器我选择Python 3.10这个版本对Django、DRF、Channels的兼容性都比较稳定。用Pycharm的Virtualenv新建一个干净的虚拟环境注意别把全局环境搞乱。具体步骤打开PycharmNew Project选择Virtualenv作为环境类型Location指向项目目录Base interpreter选择一个已安装的Python 3.10然后点击Create。项目创建完成后在Terminal里输入pip install django djangorestframework channels pillow django-cors-headers把这些基础依赖装好。千万不要图省事用conda的base环境多个项目很容易版本冲突。如果你从Git拉取的代码直接在Pycharm的Settings-project-python interpreter里选择VenvPycharm会自动识别requirements.txt并提示安装依赖这个功能很实用。2.2 创建Django项目与基础APP在Pycharm底部Terminal执行django-admin startproject paw_shelter cd paw_shelter python manage.py startapp animals python manage.py startapp users创建完以后目录结构大概是paw_shelter/项目名paw_shelter/配置目录animals/动物模块users/用户模块记得把新APP和第三方库加入INSTALLED_APPS位置在paw_shelter/settings.py里INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 第三方 rest_framework, corsheaders, channels, # 自建APP animals, users, ]同时加一个REST_FRAMEWORK配置设置默认分页和认证方式REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 10, DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework.authentication.SessionAuthentication, rest_framework.authentication.TokenAuthentication, ] }这个配置决定了后面所有列表接口默认分页避免一页返回几百条数据导致前端卡死。2.3 Flask辅助服务的定位与项目结构Django负责主业务Flask服务单独放在flask_service/目录下不参与Django的启动流程。我用的版本是Flask 2.3创建一个main.py写一个简单的接口from flask import Flask, jsonify, request from flask_cors import CORS import os app Flask(__name__) CORS(app) app.route(/health, methods[GET]) def health(): return jsonify(statusok) app.route(/compact, methods[POST]) def compact_image(): # 接收前端上传的图片使用Pillow压缩后返回 return jsonify(resultdone) if __name__ __main__: app.run(host0.0.0.0, port5001)这个服务独立运行只通过HTTP和Django通信不共用数据库。你可能会问为什么不用Django自带的功能因为图片压缩和爬虫同步这类任务一旦写进Django主线程业务高峰期会占用大量资源甚至阻塞正常API请求。拆出去以后Flask挂了不影响主业务Django挂了也不影响图片处理。这个分层在项目维护期非常香改爬虫代码再也不用重启主服务了。3. 后端核心实现Django为主3.1 数据模型设计要点流浪动物救助网站最核心的模型是Animal动物信息它和救助站、领养申请、用户形成几个主链路。我设计的字段如下class Animal(models.Model): class Status(models.TextChoices): AVAILABLE available, 待领养 PENDING pending, 审核中 ADOPTED adopted, 已领养 name models.CharField(max_length50, verbose_name昵称) species models.CharField(max_length20, choices[(dog, 狗), (cat, 猫)]) gender models.CharField(max_length10, blankTrue) age models.PositiveIntegerField(blankTrue, nullTrue) image models.ImageField(upload_toanimals/, blankTrue) description models.TextField(blankTrue) status models.CharField(max_length20, choicesStatus.choices, defaultStatus.AVAILABLE) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)这里有几个关键点Status用TextChoices而不是简单的字符常量在Django Admin和前端序列化里都更容易维护image字段依赖Pillow库创建模型前记得pip install pillow年龄用PositiveIntegerField防止出现负数这种脏数据。领养申请模型class AdoptionApplication(models.Model): animal models.ForeignKey(Animal, on_deletemodels.CASCADE, related_nameapplications) applicant_name models.CharField(max_length50) phone models.CharField(max_length20) address models.CharField(max_length200) reason models.TextField() status models.CharField(max_length20, choices[ (pending, 待审核), (approved, 已通过), (rejected, 已拒绝), ], defaultpending) created_at models.DateTimeField(auto_now_addTrue)on_deletemodels.CASCADE的意思是动物被删除时关联的领养申请也会被删除符合业务逻辑没有动物了申请还存在就没有意义。related_nameapplications允许我们通过animal.applications.all()反向查询所有申请这在写序列化时会很有用。3.2 序列化与API视图有模型之后用DRF写接口效率非常高。以Animal为例from rest_framework import serializers from .models import Animal class AnimalSerializer(serializers.ModelSerializer): class Meta: model Animal fields [id, name, species, status, image, description]视图我用viewsets.ModelViewSet配合Django Rest Framework的DefaultRouter一行路由就能搞定几乎所有CRUD接口from rest_framework import viewsets from .models import Animal from .serializers import AnimalSerializer class AnimalViewSet(viewsets.ModelViewSet): queryset Animal.objects.all() serializer_class AnimalSerializer def get_queryset(self): # 支持按品种和状态过滤 qs super().get_queryset() species self.request.query_params.get(species) status self.request.query_params.get(status) if species: qs qs.filter(speciesspecies) if status: qs qs.filter(statusstatus) return qs路由注册时在paw_shelter/urls.py里from rest_framework.routers import DefaultRouter from animals.views import AnimalViewSet router DefaultRouter() router.register(ranimals, AnimalViewSet, basenameanimal) urlpatterns router.urls默认情况下/api/animals/支持GET列表、POST新增/api/animals/{id}/支持GET、PUT、PATCH、DELETE。这种开发速度是SQL写死无法比的非常适合救助站这种业务经常微调的场景。3.3 Django执行查询与删除对象实际写法与注意事项这个项目里删除场景很多管理员删除了错误发布的动物信息、用户撤销了领养申请。很多新手在Django里删除对象时容易忽视条件控制直接Animal.objects.all().delete()这是最危险的写法能把整个表清空。正确的操作逻辑是先获取对象再删除单个对象。# 删除单个对象 animal Animal.objects.get(idanimal_id) animal.delete() # 批量删除符合条件的对象 Animal.objects.filter(created_at__ltecutoff_date, statuspending).delete()删除前务必要确认两步第一步用filter条件把目标范围锁死第二步先查出一条打印确认再执行删除。我建议在这里做一个软删除设计给Animal模型加一个is_active字段默认True删除时改为False即可# 软删除 animal Animal.objects.get(idanimal_id) animal.is_active False animal.save()好处是数据可回退一旦误删还能恢复。对于救助站这种长期运营项目软删除比物理删除安全得多。如果非要物理删除执行前一定要用transaction.atomic()包裹避免删除过程中数据库崩溃导致数据不一致。另外需要注意删除关联对象时Django默认级联删除CASCADE如果Animal被删除它的AdoptionApplication也会被删。如果不想级联可以把外键改为SET_NULL同时允许字段为nullTrue, blankTrue这样删除动物后申请记录还在只是animal字段变成空。3.4 WebSocket推送后台数据实时推给前端这个项目里有个需求救助站工作人员在后台审核一个领养申请前端页面上志愿者的界面希望马上看到状态变化。传统做法是前端轮询接口每10秒请求一次浪费资源且不实时。我用Django Channels实现了WebSocket推送。安装依赖pip install channels channels-redis在settings.py里配置ASGI_APPLICATION paw_shelter.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }创建consumers.pyfrom channels.generic.websocket import AsyncWebsocketConsumer import json class NoticeConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name notice await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def send_notice(self, event): await self.send(text_datajson.dumps(event[message]))路由层面在asgi.py里from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path from animals.consumers import NoticeConsumer application ProtocolTypeRouter({ http: django_asgi_app, websocket: ( URLRouter([ path(ws/notice/, NoticeConsumer.as_asgi()), ]) ), })后台审核完一个申请只需要往channel layer发送一条消息前端就会实时收到。我在Django Admin的model方法里加上async_to_sync(channel_layer.group_send)(...)调用非常简单。生产环境用Redis做channel layer本地可以先用内存版。这里有个坑Django Channels对WSGI/ASGI部署有要求用runserver可以跑生产上必须换成daphne paw_shelter.asgi:application启动不然WebSocket不生效。4. 前端Vue实现4.1 Vue项目初始化与路由配置前端我用Vue 3 Vite Vue Router。用Pycharm的Terminal执行npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 axios element-plus路由配置是Vue单页应用的核心。我把页面分成公共页面和管理后台两块采用动态路由匹配import { createRouter, createWebHistory } from vue-router import Home from /views/Home.vue const routes [ { path: /, name: home, component: Home }, { path: /animals, name: animals, component: () import(/views/AnimalList.vue) }, { path: /animals/:id, name: animal-detail, component: () import(/views/AnimalDetail.vue) }, { path: /apply/:animalId, name: apply, component: () import(/views/ApplicationForm.vue), meta: { requiresAuth: true } }, { path: /login, name: login, component: () import(/views/Login.vue) }, { path: /admin, name: admin, component: () import(/views/AdminDashboard.vue), meta: { requiresAuth: true, role: admin } }, ] const router createRouter({ history: createWebHistory(), routes, }) export default router注意我使用了路由懒加载只有访问到对应地址时才加载对应组件首页首屏加载速度明显提升。另外在路由守卫里判断用户是否登录、是否有管理员权限router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这里不用做太复杂的权限系统前后端配合前端过滤只是改善用户体验真正的权限校验永远要在后端API里完成。4.2 页面组件与Axios交互Vue组件开发的思路是“页面拆组件”。AnimalList.vue里我拆出了AnimalCard.vue、FilterBar.vue和Pagination.vue这样每个组件职责单一后期加新页面可以复用。请求API我用Axios封装了一个实例放在src/utils/request.jsimport axios from axios const request axios.create({ baseURL: /api, timeout: 5000, withCredentials: true, }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) export default request我特意加上了withCredentials: true因为Django的Session认证需要携带Cookie。如果不带跨域请求即使配好了CORS也拿不到用户的登录状态。前端调用接口时只需要const { data } await request.get(/animals/, { params: { species: currentSpecies, status: available, page: currentPage } })交互上有个细节列表页和详情页的图片加载我用了Vue的v-if加上加载失败占位图不然用户访问一个已删除的动物图片会看到裂图观感很差。类目筛选我做成标签切换比下拉框更直观。4.3 扩展m3u8播放与直播场景虽然救助网站核心功能是领养信息管理但运营方提了一个很有价值的场景在救助站门口装几个摄像头让网友能在线看猫咪狗狗们的生活状态提高领养转化率。这类实况视频用HLS协议传输播放地址就是一个.m3u8链接。我在Vue里实现时用了hls.js代码非常简单import Hls from hls.js const video document.getElementById(live-video) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(https://your-stream-server/live/cat_shelter.m3u8) hls.attachMedia(video) }但必须提醒浏览器自动播放视频有严格限制必须静音才能自动播放而且m3u8流跨域时需要在流媒体服务器配置Access-Control-Allow-Origin。如果不配置前端会一直卡在加载中。第一次部署时我就是没配调试了很久才发现是Nginx少加了CORS响应头。5. 联调、部署与常见问题5.1 跨域与Cookie处理前后端分离最烦的就是跨域。开发时我用Django的corsheaders解决INSTALLED_APPS [corsheaders, ...] MIDDLEWARE [corsheaders.middleware.CorsMiddleware, ...] CORS_ALLOWED_ORIGINS [ http://localhost:5173, ] CORS_ALLOW_CREDENTIALS True如果前端请求地址是http://localhost:5173后端是http://localhost:8000不加CORS配置浏览器会直接拦截响应。加了CORS_ALLOW_CREDENTIALS True之后Cookie才能被浏览器保存和携带。还有一个Django特有的CSRF防护。我用SessionAuthentication所以在POST、PUT、DELETE请求中需要携带CSRF Token。最省事的方式是在前端从Cookie里读取csrftoken然后加到请求头X-CSRFToken。我在request.js的拦截器里统一处理function getCookie(name) { const value ; ${document.cookie} const parts value.split(; ${name}) if (parts.length 2) return parts.pop().split(;).shift() } request.interceptors.request.use(config { const csrfToken getCookie(csrftoken) if (csrfToken) config.headers[X-CSRFToken] csrfToken return config })只要第一次访问Django任意页面时Set-Cookie了csrftoken后面所有请求都会自动带上。注意如果前后端域名不同csrfCookie的SameSiteNone; Secure属性必须设置好否则测试环境用HTTP会无法携带Cookie。5.2 Flask部署注意事项及整个系统的部署Flask辅助服务部署比Django简单得多但有几个点容易踩。我用的方式是gunicorngevent先创建wsgi.pyfrom main import app if __name__ __main__: app.run()然后pip install gunicorn gevent gunicorn -w 2 -k gevent -b 0.0.0.0:5001 wsgi:app-w 2是指两个worker进程-k gevent是异步worker适合处理图片压缩这类带有IO等待的任务。Flask本身没有严格的并发限制但图片压缩如果同步阻塞用户并发一大就会排队用gevent能明显提升吞吐。整个系统最后用Nginx统一入口server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/frontend/dist; index index.html; location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } location /media/ { alias /var/www/paw_shelter/media/; } location /flask/ { proxy_pass http://127.0.0.1:5001; } location / { try_files $uri $uri/ /index.html; } }我在/配置了try_files $uri $uri/ /index.html这行非常重要。Vue使用history路由刷新一个子页面路径时如果Nginx不认识这个路径会返回404。加了这个配置所有请求先在静态目录里找文件找不到就返回Vue的index.html再由前端路由接管。5.3 实际调试中遇到的坑与解决技巧整个项目开发中我记了十几个坑这里挑三四个最典型的。第一个坑是Django的runserver和daphne切换问题。开发时用python manage.py runserver调试REST API一切正常一上WebSocket就发现连不上控制台报WebSocket connection failed。后来发现runserver虽然能处理HTTP但不是完整的ASGI服务器必须用daphne paw_shelter.asgi:application启动才能跑WebSocket。用Pycharm配置启动参数时我单独建了一个Daphne运行配置命令行写daphne paw_shelter.asgi:application -b 0.0.0.0 -p 8000记得选好Python解释器。第二个坑是图片上传的大小限制。Django默认DATA_UPLOAD_MAX_MEMORY_SIZE是2.5MB用户上传一张手机拍摄的猫照片经常4MB以上接口直接500。在settings.py里加上DATA_UPLOAD_MAX_MEMORY_SIZE 1024 * 1024 * 10 FILE_UPLOAD_MAX_MEMORY_SIZE 1024 * 1024 * 10顺便把前端也限制一下上传大小用Element Plus的before-upload检查文件size超过5MB直接提示不让请求发出去。第三个坑是数据库的时区问题。Django默认创建时间用的UTC前端显示时差8小时需要把TIME_ZONE Asia/Shanghai USE_TZ True设置好后所有auto_now_add的字段都会按上海时间存储。如果已经存了UTC时间可以先写脚本批量转换不然前端展示“两个小时后发布”会被用户投诉。第四个坑是Vue打包后后端Admin的静态文件丢了。collectstatic把Django自带后台样式收集到/static/但Nginx没加location /static/的alias导致后台UI白板。在Nginx加一行配置即可location /static/ { alias /var/www/paw_shelter/static/; }这个错误非常隐蔽日志里没有任何错误浏览器控制台全是静态文件404。6. 项目复盘与经验总结6.1 数据安全与审核机制做公益性质的平台数据安全反而更要重视。我从一开始就给上传的动物信息加了人工审核流程用户可以在前端提交“发现流浪动物”表单但这条记录默认是pending状态不会立即展示在首页只有管理员登录后台审核通过后才会公开。这个机制能过滤大部分垃圾信息和恶意上传降低后续救助站实际去现场核实的成本。权限上普通用户和管理员角色分开。普通用户只能浏览、提交领养申请、管理自己的资料管理员拥有所有数据的增删改查权限。实现时我用了Django自带的Group权限结合DRF的IsAuthenticatedOrReadOnly和DjangoObjectPermissions。代码层面前端用路由守卫控制路由跳转后端用权限类确保接口不会被绕过。这里特别提醒永远不要相信前端的v-if和路由守卫后端权限才是最后一道闸门。另外所有用户提交的内容包括申请理由、动物描述都要在模板里进行HTML转义。Vue默认是转义输出所以直接用{{ content }}就不会有XSS风险但要注意不要使用v-html渲染任何用户输入。我在做后台统计页面时不小心用v-html渲染了一段用户填写的救助地点结果一个用户填入了一段带样式的脚本虽然没什么危害但也给我敲了警钟。v-html能不用就不用实在要用一定要对内容做安全过滤。6.2 我的几点实操体会项目收尾阶段我最大的体会是“先构建核心闭环再谈锦上添花”。救助站最需要的是“录入动物—前端展示—用户申请—后台审核”这条主链路其他比如在线直播、数据可视化、多语言支持都是后面根据运营反馈一点点加的。第一版如果功能堆太多开发周期拉长光调试就可能拖垮热情。关于Flask和Django共存很多人质疑是不是多余。我的经验是当一个服务里同时存在重业务API和轻量定时任务时拆开部署之后的幸福感会非常强。Flask服务每次发布只需要重新启动一个很小的进程不影响主流程而Django只要改模型迁移数据库、重启主服务也不用担心把爬虫或图片处理那部分搞挂。两个服务之间的唯一通信是图片存储路径和动物ID耦合度很低。最后也是最重要的一点部署前把备份做好。我用Crontab每天凌晨对SQLite数据库或生产上的MySQL进行一次备份保留最近七天的文件。有一次我发现某个清理脚本误删了几个月前的动物记录就是因为有备份才能快速恢复不然对救助站来说可能意味着几只动物信息永久丢失。做这个项目的过程中我重新理解了“技术是为公益服务的”这句话稳定、可靠、不添乱比花哨的功能更让人放心。Pycharm里最后留一个我经常用到的调试技巧当Django接口莫名返回500时把Pycharm的运行配置里加一个环境变量DJANGO_DEBUG1然后就能在浏览器看到完整的Django错误页而不仅仅是“服务器错误”。这个技巧解决了我至少三分之一的问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →