做这个django领养流浪动物小程序之前,我一直觉得“后端管理系统+微信小程序”这种组合是毕业设计里最稳的选择,但真正把源码29035这套东西跑完、改完、部署完,才发现里面需要填的坑比想象中多得多。项目整体就是围绕流浪动物的信息发布、线上展示、领养申请、后台审批这一整条链路来设计的,后端用Django提供接口和管理后台,前端用微信小程序做用户端展示和交互,非常适合想系统练一遍Django框架、顺便学会小程序对接的开发者参考。
我一直认为这类项目最大的价值不只是“能跑”,而是把一个完整的业务闭环走通:用户看到待领养动物、用户提交申请、管理员审核、状态回写前端,四个环节缺一不可,稍不留神业务逻辑就会乱。下面我把这整套项目的设计思路、核心后端代码、小程序端关键页面以及部署上线的实操过程全部分享出来,踩过的坑和排错思路也会一并用表格和代码记录下来。
1. 项目整体设计与需求拆解
1.1 流浪动物领养系统到底需要什么功能
很多人第一次见到“领养流浪动物”这个主题,第一反应就是做个漂亮的动物列表页。但真正动手拆解业务流程,需求远比表面看起来复杂。我把它拆成了三个角色和两类核心场景。
三个角色:
- 普通用户,也就是微信小程序的使用者,可以浏览动物列表、看动物详情、提交领养申请。
- 管理员,负责在Django后台录入动物信息、上传图片、审核领养申请、下架已领养动物。
- 系统本身,需要处理图片存储、数据统计、状态流转等一系列通用能力。
两类核心场景:
- 浏览路径:用户进入小程序,首页展示待领养动物卡片,点击进入详情页看到动物的照片、年龄、性别、性格描述,并表示“我要领养”,填写申请表。
- 管理路径:管理员在后台发布新收容的动物,收到新的领养申请后打电话回访评估,审核通过后把该动物状态改为“已领养”,小程序端同步更新。
这里有个很容易被忽略的细节:动物的状态不只是“待领养”和“已领养”两态。实际业务中动物可能被提前暂养、正在治疗、已回访确认等,所以源码里把status字段设计成可扩展的,比如pending、adopted、fostered,这样后续不管加“待体检”还是“治疗中”,都不会动表结构。
1.2 为什么选Django+微信小程序这套组合
前端技术栈可选的方案很多,小程序也能用uniapp跨端开发,但我最终确定Django + 原生微信小程序,主要从三个维度考虑。
第一,Django自带Admin后台。对于这种管理类需求,管理员界面是刚需,而Django的django.contrib.admin几乎是开箱即用的,注册一下模型就能实现增删改查。我只需要在此基础上调整列表展示字段、搜索框、过滤器,就能快速得到一个可用的运营后台,节省大量时间。
第二,Django的ORM非常适合业务状态多变的场景。领养申请的状态流转、动物列表的条件筛选,如果用原生SQL硬写,后期加字段、加状态会非常痛苦。Django的QuerySet链式查询可以随时追加筛查条件,比如:
animals = Animal.objects.filter(status='pending').order_by('-created_at')一行代码就能拿到所有待领养动物,且按发布时间倒序排列。
第三,微信小程序的生态足够成熟,用户不需要安装额外的App,扫码即用,这与流浪动物救助站“低门槛触达潜在领养人”的诉求天然匹配。小程序端只需要承载浏览和表单提交两件事,原生微信小程序语法就足够,不需要引入重型框架增加心智负担。
1.3 源码目录结构与模块划分
拿到源码29035之后,先不要急着跑起来,而是把目录结构梳理清楚。一个标准的Django项目+Django REST Framework多应用结构,目录层级大致是这样:
project_name/ ├── manage.py ├── requirements.txt ├── config/ │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ │ ├── users/ │ ├── animals/ │ ├── adoptions/ │ └── common/ ├── media/ │ └── animals/ ├── static/ └── miniprogram/ ├── pages/ │ ├── index/ │ ├── detail/ │ ├── apply/ │ └── my/ ├── utils/ └── app.jsonusers负责用户登录与身份信息,animals负责动物信息管理,adoptions负责领养申请全流程,common放一些跨模块的通用方法。小程序端的页面和Django应用基本一一对应,这种“一应用包一支线业务”的划分方式,在后续加功能、修bug时能少掉很多头发。
2. 后端核心设计与关键技术选择
2.1 前后端交互方式与接口设计思路
这个项目的交互方式不是Django传统的服务端渲染模板,而是把Django当作纯API后端,小程序端通过HTTP请求获取JSON数据。之所以这么做,是因为小程序的页面渲染完全由前端控制,Django的render函数返回的HTML在这里没有意义。
接口设计上我遵循最简原则,不做过度设计。核心接口如下:
| 功能模块 | 请求方式 | 接口路径 | 用途说明 |
|---|---|---|---|
| 动物列表 | GET | /api/animals/ | 返回待领养动物列表,支持分类筛选 |
| 动物详情 | GET | /api/animals/<id>/ | 返回单只动物的完整信息 |
| 提交申请 | POST | /api/adoptions/ | 提交领养申请表单 |
| 用户登录 | POST | /api/auth/login/ | 通过微信code换取openid并登录 |
设计接口时有几个原则值得强调。第一,接口路径要能表达业务含义,/api/animals/比/api/list/更清晰。第二,POST请求全部走application/json格式,不要混用表单格式,否则Django端解析参数异常时很难排查。第三,统一返回结构,不管成功失败都返回JSON,这样小程序端只要写一个通用的request封装函数。
2.2 数据库模型设计与ORM细节
数据库是整个项目的基石,尤其是动物和领养申请这两张表,设计好坏直接影响后续开发效率。我把核心模型代码贴在下面,并将其中的设计决策逐条拆解。
动物模型:
from django.db import models class Animal(models.Model): STATUS_CHOICES = [ ('pending', '待领养'), ('adopted', '已领养'), ('fostered', '已被暂养'), ] name = models.CharField(max_length=50, verbose_name='名字') breed = models.CharField(max_length=100, blank=True, verbose_name='品种') age = models.IntegerField(verbose_name='年龄(月)') gender = models.CharField(max_length=10, choices=[('male', '公'), ('female', '母')], verbose_name='性别') description = models.TextField(verbose_name='性格与身体状况描述') cover_image = models.ImageField(upload_to='animals/', verbose_name='封面图') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='状态') created_at = models.DateTimeField(auto_now_add=True, verbose_name='发布时间') class Meta: ordering = ['-created_at'] verbose_name = '动物信息'领养申请表:
class AdoptionApplication(models.Model): animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='applications', verbose_name='动物') applicant_name = models.CharField(max_length=50, verbose_name='申请人姓名') phone = models.CharField(max_length=20, verbose_name='联系电话') address = models.CharField(max_length=200, verbose_name='家庭住址') reason = models.TextField(verbose_name='领养理由') status = models.CharField( max_length=20, choices=[('submitted', '已提交'), ('approved', '已通过'), ('rejected', '已拒绝')], default='submitted', verbose_name='审核状态' ) created_at = models.DateTimeField(auto_now_add=True, verbose_name='申请时间')这里有几个细节新手容易忽略,我逐一说明。
on_delete=models.CASCADE:猫狗被删除时,关联的领养申请也一并删除,避免孤儿数据。这在Admin后台操作时尤其重要,不然动物删除后列表页会出现打不开详情的脏数据。
related_name='applications':通过animal.applications.all()就能拿到某只动物收到的全部申请,语义清晰,比默认的animal.adoptionapplication_set.all()可读性强得多。
ordering = ['-created_at']:直接在Model的Meta里声明默认排序,所有查询都默认按发布时间倒序,省得每个视图里都手动写order_by。
2.3 用户登录与小程序的openid换取
小程序端是没有传统账号密码登录的,标准做法是调用wx.login获取临时code,把code传到后端,由后端请求微信接口换取openid,以openid作为用户唯一标识。源码里登录接口的实现逻辑大致如下:
import requests from django.conf import settings from django.http import JsonResponse def login_view(request): code = request.GET.get('code') resp = requests.get( 'https://api.weixin.qq.com/sns/jscode2session', params={ 'appid': settings.WX_APPID, 'secret': settings.WX_SECRET, 'js_code': code, 'grant_type': 'authorization_code' } ) data = resp.json() openid = data.get('openid') if not openid: return JsonResponse({'error': '登录失败'}, status=400) user, _ = User.objects.get_or_create(openid=openid) return JsonResponse({'user_id': user.id, 'openid': openid})注意,为了简化,这里直接用openid当身份标识,但真正上线时一定要签发access_token并结合缓存做会话管理,否则每次请求都传openid,被伪造的风险很高。单机小型项目,可以在Redis里存一份token -> openid的映射关系,每次请求带Authorization头来校验。
3. 核心功能实现与代码解读
3.1 动物信息发布与后台管理
动物信息的后台录入是管理员的日常工作,所以Django Admin的体验直接决定这套系统好不好用。默认情况下Admin只展示模型名称和ID,非常不直观,因此需要对Admin类做定制。
登录Django后台,在admin.py里注册并优化:
from django.contrib import admin from .models import Animal, AdoptionApplication @admin.register(Animal) class AnimalAdmin(admin.ModelAdmin): list_display = ['name', 'breed', 'age', 'gender', 'status', 'created_at'] list_filter = ['status', 'gender'] search_fields = ['name', 'breed'] list_editable = ['status'] list_per_page = 20这些配置带来的改变是实质性的。list_display让列表页能直接看到动物的核心字段,不用逐个点进详情页确认;list_filter让管理员可以一键筛出“已领养”或“待领养”的动物;list_editable提供了在列表页直接修改状态的入口,审核完成时不用点进详情再操作,效率提升明显。
图片上传这部分有一个坑,新手容易卡住:ImageField依赖Pillow库,而且MEDIA_URL和STATIC_URL不能混为一谈。STATIC_URL管CSS、JS等静态文件,MEDIA_URL管用户上传的图片。本地开发时,在settings.py里声明好MEDIA_ROOT和MEDIA_URL,还要在根urls.py里加一个static路由才能看到图片:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # 其他路径 ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)如果不加这一步,后台能上传图片,但详情页和小程序端都会拿到一个404的图片地址。
3.2 领养申请流程与状态流转设计
领养申请是本项目业务逻辑最复杂的部分,不是简单地存一条申请记录,而是要确保同一个动物不能被重复领养、审核状态变更后用户能看到结果。
提交申请的后端视图:
from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import json from .models import Animal, AdoptionApplication @csrf_exempt def submit_application(request): if request.method != 'POST': return JsonResponse({'error': '仅支持POST请求'}, status=405) body = json.loads(request.body) try: animal = Animal.objects.get(id=body['animal_id']) except Animal.DoesNotExist: return JsonResponse({'error': '动物不存在'}, status=404) if animal.status != 'pending': return JsonResponse({'error': '该动物当前不可领养'}, status=400) application = AdoptionApplication.objects.create( animal=animal, applicant_name=body['name'], phone=body['phone'], address=body['address'], reason=body['reason'] ) return JsonResponse({'application_id': application.id, 'message': '提交成功'}, status=201)代码里最关键的一段是if animal.status != 'pending'这句判断。如果没有这道防线,用户可以同时对同一条动物提交多个申请,或者向已经领养出去的动物继续提交申请,管理员后台会看到一堆无效工单。源码里用这条校验把不可领养的申请直接挡在门外,是很典型的业务兜底逻辑。
审核状态流转也需要注意。管理员把申请标记为“通过”时,不能只改申请记录,而是要在同一个操作里把对应动物的状态同步改为“已领养”。我习惯把这类联动逻辑写到save()方法里,保证不漏执行:
class AdoptionApplication(models.Model): # 字段定义与前面相同 def save(self, *args, **kwargs): if self.status == 'approved': self.animal.status = 'adopted' self.animal.save() super().save(*args, **kwargs)3.3 文件下载与StreamingHttpResponse应用
这个项目的动物信息不一定只有图片,可能还有疫苗接种记录、体检报告之类的附件需要在小程序端下载。如果直接用FileResponse一次性读进内存,大文件会占满带宽,所以源码里用到了StreamingHttpResponse来处理附件下载:
from django.http import StreamingHttpResponse def download_file(request, file_id): file_obj = FileRecord.objects.get(id=file_id) def file_iterator(file_path, chunk_size=8192): with open(file_path, 'rb') as f: while True: chunk = f.read(chunk_size) if not chunk: break yield chunk response = StreamingHttpResponse(file_iterator(file_obj.file.path)) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = f'attachment; filename="{file_obj.filename}"' return responseContent-Disposition这行很关键,它决定了浏览器或小程序拿到文件后是直接打开还是下载保存。attachment表示强制下载,filename指定保存的文件名,如果不设置,下载下来的文件会是一串看不懂的编号。注意文件名如果是中文,最好做一次URL编码处理,否则部分平台会乱码。
4. 小程序端页面开发与交互实现
4.1 首页动物列表与下拉刷新
小程序端首先面对的页面是首页,核心任务是把后端的动物列表用卡片形式展示出来,并支持下拉刷新。请求封装我习惯放在utils/request.js里,统一配置基础地址和超时时间:
const BASE_URL = 'https://yourdomain.com/api'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, timeout: 10000, success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(res); } }, fail(err) { reject(err); } }); }); } module.exports = { request, BASE_URL };首页拉取数据的代码:
const { request } = require('../../utils/request'); Page({ data: { animals: [], loading: true }, onLoad() { this.fetchAnimals(); }, onPullDownRefresh() { this.fetchAnimals().then(() => wx.stopPullDownRefresh()); }, fetchAnimals() { return request('/animals/').then((data) => { this.setData({ animals: data, loading: false }); }); } });页面模板用wx:for循环渲染卡片,这里提醒一下,wx:key必须绑定唯一的id,否则列表更新时会出现渲染错乱或性能下降:
<view class="animal-card" wx:for="{{animals}}" wx:key="id"> <image src="{{item.cover_image}}" mode="aspectFill" /> <text class="name">{{item.name}}</text> <text class="breed">{{item.breed}}</text> <text class="status">{{item.status === 'pending' ? '待领养' : '已领养'}}</text> </view>需要注意的是,wx:wx:key的值是字段名,不加双大括号。小程序基础库对新语法和老语法兼容不同,建议直接用简洁写法。
4.2 详情页展示与动态设置标题
详情页承担的是“让用户了解这只动物并产生领养意愿”的任务。页面布局上会用轮播图展示多张动物照片,下方展示品种、年龄、性格描述,最后是一个醒目的“申请领养”按钮。
根据路由参数加载详情数据时,页面的导航栏标题应该一起变化,这就是热词里提到的“小程序动态设置标题”。源码里这样实现:
Page({ data: { animal: null }, onLoad(options) { const id = options.id; request(`/animals/${id}/`).then((animal) => { this.setData({ animal }); wx.setNavigationBarTitle({ title: animal.name + ' - 动物详情' }); }); } });wx.setNavigationBarTitle必须在onLoad之后调用,而且不要在onLoad同步执行完就调用,因为此时数据还没从后端返回。最佳实践是在接口回调里设置标题,保证用户看到的标题和数据对应的是同一只动物。
表单页的设计同样需要重视。领养申请里的“动物性别”如果让用户自己填,很容易填出各种不规范的值,所以源码里用微信小程序的radio组件做单选:
<radio-group bindchange="onGenderChange"> <label class="radio-item" wx:for="{{genderOptions}}" wx:key="value"> <radio value="{{item.value}}" checked="{{item.checked}}" /> {{item.label}} </label> </radio-group>这里有个微信小程序的细节:radio组件的checked属性是布尔值,如果从后端返回的是字符串"true"或"false",会导致所有选项都不选中。建议在JS层把值转成明确的true/false。
4.3 表单提交与图片上传功能
领养申请表单提交本身不算复杂,但如果用户想上传“居住环境照片”作为辅助材料,就会用到wx.chooseImage和wx.uploadFile。
图片上传的完整流程是:用户选择图片后,小程序将图片以multipart/form-data方式上传到后端,后端保存文件并返回图片URL,最后提交表单时再把这个URL和其它字段一起传给后端。源码里上传的关键代码:
wx.chooseImage({ count: 3, sizeType: ['compressed'], sourceType: ['album', 'camera'], success(res) { const tempFiles = res.tempFilePaths; const uploadTasks = tempFiles.map((filePath) => { return new Promise((resolve, reject) => { wx.uploadFile({ url: BASE_URL + '/upload/', filePath, name: 'file', success(fileRes) { const data = JSON.parse(fileRes.data); resolve(data.url); }, fail: reject }); }); }); Promise.all(uploadTasks).then((urls) => { this.setData({ imageUrls: urls }); }); } });这里必须用Promise.all等待所有图片上传完成后再提交表单。如果前端不等图片传完就直接把表单发出去,后端的图片URL列表会缺项,管理员看到的申请材料不完整,影响审核判断。
前端上传文件时还有一个高频问题:wx.uploadFile返回的data是字符串,不是JSON对象,必须先JSON.parse再取字段。不提前处理好,直接res.data.url会取到undefined。
5. 环境搭建与部署上线实操
5.1 本地开发环境与依赖安装
跑这个项目之前,先把本地环境准备齐。我推荐用虚拟环境管理依赖,避免多个项目之间的包版本冲突。Python 3.10环境下的实测安装流程如下:
python3 -m venv venv source venv/bin/activate pip install django djangorestframework pillow requests pip install mysqlclient这里重点说说mysqlclient的坑。这个包在Windows上安装很不友好,经常报error: Microsoft Visual C++ 14.0 is required。如果你用的是MySQL而不是SQLite,建议优先采用以下两种方案中的一种:
- Windows用户直接从
pip install mysqlclient官网的预编译轮子文件安装,避免现场编译。 - 中途切换成
pymysql,在项目同名目录的__init__.py里加两行:
import pymysql pymysql.install_as_MySQLdb()如果只是本地跑通功能演示,直接用默认的SQLite数据库最省心,用python manage.py migrate建表即可。SQLite对Django的ORM支持非常完整,实测所有模型和查询都能正常工作。只有真正上线时才需要切换MySQL,因为生产环境多进程并发写SQLite会造成锁冲突。
数据库迁移命令:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:80005.2 uWSGI + Nginx部署实际步骤
本地跑通后,要把项目部署到云服务器上,让小程序用户能够访问到接口。服务器环境同样装好Python和依赖后,我用uWSGI配合Nginx部署,原因是uWSGI能稳定处理高并发,Nginx负责静态文件托管和反向代理。
在项目根目录创建uwsgi.ini:
[uwsgi] chdir = /var/www/pet_adoption module = config.wsgi:application master = true processes = 4 threads = 2 socket = 127.0.0.1:8000 vacuum = true uid = www-data gid = www-data daemonize = /var/log/uwsgi/pet_adoption.logNginx配置:
server { listen 80; server_name yourdomain.com; location /static/ { alias /var/www/pet_adoption/static/; } location /media/ { alias /var/www/pet_adoption/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; uwsgi_read_timeout 60; } }配置完成后,分别启动uWSGI和重载Nginx:
uwsgi --ini uwsgi.ini nginx -s reload有一个部署时必须处理的问题:Django的DEBUG模式在生产环境必须设为False,同时把项目域名加入ALLOWED_HOSTS,否则Nginx转发过来的请求会直接返回400 Bad Request:
DEBUG = False ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']静态文件也要确保已收集到位:
python manage.py collectstatic --noinput5.3 小程序域名备案与上线注意事项
小程序上线前的最后一个环节,是配置域名和备案。微信小程序在正式版中,wx.request的请求域名必须满足两个硬性条件:
- 域名必须为HTTPS协议,且证书有效。
- 域名需要在小程序管理后台的“开发管理-服务器域名”里配置
request、uploadFile等合法域名。
备案这块是绕不开的合规流程,按照平台要求如实填写主办单位信息、网站名称、服务内容等即可。需要注意的实操细节是,备案信息中的网站名称要和实际业务保持一致,不要起与内容无关的名字,否则审核可能不通过。域名备案通过后,再在云服务器上申请SSL证书并配置HTTPS,Nginx里需要修改为监听443端口并配置证书路径。
部署后务必用HTTPS地址做一次接口连通性测试:
curl -I https://yourdomain.com/api/animals/如果返回200 OK,再打开小程序开发者工具,把“不校验合法域名”的勾选取消,模拟生产环境请求。这一套走完,项目才算真正具备上线条件。
6. 常见问题与排查技巧实录
6.1 Django后端高频报错梳理
我整理了一份项目开发过程中遇到的报错速查表,基本覆盖了初学者会踩的大部分坑:
| 报错信息 | 出现原因 | 解决办法 |
|---|---|---|
ModuleNotFoundError: No module named 'MySQLdb' | 未安装mysqlclient或驱动兼容问题 | 安装mysqlclient,或引入pymysql |
django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module | 同样是MySQL驱动问题 | 在__init__.py中安装pymysql |
Field 'id' expected a number but got 'abc' | 前端传了非数字ID,或查询参数类型不对 | 在前端用parseInt转换,或后端增加类型校验 |
CSRF verification failed. Request aborted. | POST请求缺少CSRF Token | 开发阶段用@csrf_exempt,生产环境推荐通过Header携带Token |
DisallowedHost at /admin/ | 请求域名不在ALLOWED_HOSTS中 | 在settings.py中添加对应域名或IP |
AttributeError: 'QuerySet' object has no attribute 'cover_image' | 忘记加.first()或遍历查询集,误把整张表当作对象 | 使用get()或first()获取单个对象 |
其中CSRF的问题特别值得展开一下。小程序端和后台Admin公用一套Django项目时,小程序POST请求如果没有CSRF Token会让Django返回403,但很多初学者复制代码时没有注意这个细节,就会一直卡着提交不了表单。源码里为了演示方便给submit_application加上了@csrf_exempt,但真正上线时不应该直接全豁免,可以在小程序端先从后端获取CSRF Token再在请求头带上,兼顾安全性和开发效率。
6.2 小程序端常见白屏与请求失败问题
小程序端比起后端,问题更多集中在“数据没渲染出来”和“接口请求不成功”两类。
第一类高频情况是:开发者工具能调通接口,真机预览却请求失败。原因通常是开发者工具开启了对合法域名校验的忽略,但真机不会忽略。处理方法是打开详情-本地设置,直接勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,先确认在开发环境能跑通;上线前一定要关闭这项并完成正常域名配置。
第二类是接口返回了数据但页面仍然空白。大概率是this.setData的this指向出了问题。在wx.request的success回调里,this不一定指向Page实例,需要用箭头函数或者在外部保存const that = this。箭头函数是更推荐的做法:
request('/animals/').then((data) => { this.setData({ animals: data }); });箭头函数继承上下文,this稳定指向Page实例,避免再出现that.setData这种绕路写法。
6.3 性能优化与数据一致性问题
项目完成后,可以再从两个角度做一轮细致的优化。第一是Django接口的性能,Animal列表接口如果动物数量上百条,管理层又会频繁按名称搜索,可以在模型里加上索引:
name = models.CharField(max_length=50, db_index=True, verbose_name='名字') status = models.CharField(max_length=20, db_index=True, choices=..., verbose_name='状态')第二是图片体积问题。手机拍摄的照片动不动就几兆,直接传到服务器不仅拖慢页面加载,还占用大量磁盘空间。我在小程序端已经做了wx.chooseImage的sizeType: ['compressed']压缩,但后端最好再配合Pillow做一次缩略图生成。Django的ImageField本身只做存储,不做压缩,需要手动处理:
from PIL import Image import os def compress_image(instance): img = Image.open(instance.cover_image.path) if img.width > 800: ratio = 800 / img.width img = img.resize((800, int(img.height * ratio))) img.save(instance.cover_image.path, quality=85, optimize=True)调用时机选在Model的save()里,上传覆盖图后自动压缩:
def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.cover_image: compress_image(self)这样处理之后,列表页的加载速度会明显提升,用户体验也更好。
数据一致性方面,比较容易出现的问题是“申请通过但动物状态未更新”。如果直接在Admin后台手工改数据库,或者用了别的方式改了动物状态,申请状态和动物状态就可能脱节。源码把状态联动逻辑写在save()里,已经解决了一大半,但仍要避免在Django Admin里通过“修改Animal对象的状态”来跳过申请流程,否则后台的申请记录会停留在“已提交”但动物却是“已领养”。我实际跑这个项目时,就靠这层联动设计和之后的排查几次,才把状态数据的可信度彻底稳住。
这套django领养流浪动物小程序从需求拆解、后端建模、小程序对接,到最后部署上线,整个过程走下来收获很实在。如果你拿到源码29035之后也想扩展功能,我建议优先考虑增加一个简单的通知机制,比如管理员审核通过后,在小程序“我的页面”里显示申请进度,这样整个闭环会更完整。我实际用下来的感受是,做好状态联动、处理好图片和部署这三个关键点,这类系统就会变得非常耐用,希望这份实操记录能帮你少走一些弯路。