做救助站的朋友找我,说他们现在的领养信息还靠微信群和Excel表格,宠物档案、领养人申请、回访记录全是散着的,容易漏也容易乱。我听完就决定用Django给他搭一个宠物领养系统,从宠物信息发布到领养申请审核,再到后台批量管理,一条线走通。这个项目本身不算特别大,但覆盖面很完整,Django的ORM、认证、Admin后台、文件上传、分页筛选这些技能点全都会用到,对于正在学Django、想拿一个完整项目练手的人,或者想给自己救助站、宠物店做一套管理工具的人,都非常有参考价值。这篇文章我就把整个项目的设计思路、核心模块实现、开发环境搭建、部署和排坑过程完整记录下来,希望能帮你少走一些弯路。
1. 项目背景与整体设计思路
1.1 宠物领养场景里真正需要解决的问题
宠物领养这件事,看起来只是“把宠物送出去”,但在实际运营中要处理的信息量远比想象中大。线下救助站通常需要记录每只宠物的来源、健康状况、疫苗和绝育状态、当前安置位置,还要管理有意向领养人的联系方式、家庭环境、领养意向,甚至后续的回访记录。如果只靠表格和聊天记录,信息很快就会对不上:同一只宠物被问了好几次、不同人提交了重复申请、领养人联系不上导致回访中断,这些全是实际会发生的问题。
这个项目要做的,就是把“宠物档案”和“领养申请”两条核心数据线搬到线上。救助站工作人员可以在后台录入宠物信息并配图,访客在前台浏览宠物列表、查看详情、提交领养申请,管理员审核申请后更新宠物状态,整个流程在系统里有迹可循。
1.2 技术选型:为什么用Django而不是Flask或Vue全家桶
选Django的原因很直接:这个系统有大量的数据管理和后台操作需求,而Django自带Admin后台、ORM、认证系统、表单处理和迁移工具,这些都是现成的能力。如果用Flask,虽然轻量灵活,但用户认证、后台管理、ORM都需要自己集成,开发周期会被拉长不少。用Node.js或者纯前后端分离方案也能做,但对一个需要快速落地、维护成本不高的内部管理项目来说,Django“一套标准下来全都有”的开发体验是最省心的。
Django的MVT模式也很适合这类业务系统。Model负责数据建模,View处理请求逻辑,Template负责页面渲染,对于没有专职前端配合的团队来说,直接使用Django模板渲染页面比额外搭一套Vue或React前端要方便得多。后期如果确实需要小程序或App入口,也可以在此基础上用Django REST Framework提供API,不会把路堵死。
1.3 系统功能模块划分
我把整个系统拆成了几个相对独立的模块,这样开发的时候可以分步实现,不会互相牵扯:
| 模块 | 核心功能 | 主要服务对象 |
|---|---|---|
| 宠物信息管理 | 宠物档案增删改查、图片上传、状态管理 | 管理员、前台访客 |
| 领养申请流程 | 提交申请、审核、状态流转 | 领养人、管理员 |
| 用户系统 | 注册、登录、个人信息维护 | 全部用户 |
| 前台浏览搜索 | 宠物列表、关键字搜索、多条件筛选、分页 | 访客 |
| 后台管理 | 数据管理、申请审核、统计查看 | 管理员 |
| 系统配置 | 基础数据、站点信息、媒体文件管理 | 管理员 |
这种划分方式没有过度设计,每块功能都是实际使用中确实需要的。比如初始版本没打算做消息通知,但后来发现领养人提交申请后不知道审核进度,所以加了状态查询入口,这个在后续版本里可以根据需求再扩展。
2. 数据模型设计:把业务变成表结构
2.1 宠物信息表:核心字段与选择逻辑
宠物信息是整个系统的数据核心,设计这张表的时候要尽量把字段考虑全面,因为中途加字段虽然不麻烦,但如果数据已经录入很多了,再补历史数据就很痛苦。所以我一开始就把救助场景里常见的属性都加上了。
from django.db import models from django.contrib.auth.models import User class Pet(models.Model): GENRE_CHOICES = [ ('dog', '犬'), ('cat', '猫'), ('rabbit', '兔'), ('bird', '鸟'), ('other', '其他'), ] SEX_CHOICES = [ ('male', '公'), ('female', '母'), ('unknown', '未知'), ] STATUS_CHOICES = [ ('available', '待领养'), ('pending', '审核中'), ('adopted', '已领养'), ('offline', '已下架'), ] name = models.CharField('宠物名字', max_length=50) genre = models.CharField('宠物种类', max_length=20, choices=GENRE_CHOICES) breed = models.CharField('品种', max_length=100, blank=True) sex = models.CharField('性别', max_length=20, choices=SEX_CHOICES, default='unknown') age = models.IntegerField('年龄(月)', default=1, help_text='以月为单位') weight = models.FloatField('体重(kg)', null=True, blank=True) is_neutered = models.BooleanField('是否绝育', default=False) is_vaccinated = models.BooleanField('是否接种疫苗', default=False) status = models.CharField('领养状态', max_length=20, choices=STATUS_CHOICES, default='available') description = models.TextField('宠物描述', blank=True, help_text='性格、生活习惯、救助经历等') city = models.CharField('所在城市', max_length=50, blank=True) cover_image = models.ImageField('封面图片', upload_to='pets/covers/', blank=True, null=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_pets', verbose_name='发布人') created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: db_table = 'pet' verbose_name = '宠物信息' verbose_name_plural = '宠物信息' ordering = ['-created_at'] def __str__(self): return f'{self.name}-{self.get_genre_display()}'几个字段设计上比较关键的地方:
- status字段是状态机的核心。待领养、审核中、已领养、已下架四个状态之间流转是有规则的,不是随意改的。比如“已领养”的宠物不能再被别人提交申请,这个逻辑在视图层和表单层都要控制。
- age用“月”作单位,是因为幼猫幼犬的年龄在头一年变化很快,按月记录比按年更精确,展示的时候再转换成“X个月/ X岁”。
- cover_image用 ImageField 而不是普通的 URL 字段,这样图片文件可以直接上传到服务器指定目录,不依赖外部图床,后期备份也方便。使用 ImageField 之前要先安装 Pillow 库。
2.2 领养申请表:申请人与宠物的关系建模
领养申请表要解决的核心问题是“谁在什么时候申请了哪只宠物”,并且要记录审核结果。这里用 ForeignKey 关联 Pet 和 User,一条申请记录对应一个用户和一只宠物。
class AdoptionApplication(models.Model): STATUS_CHOICES = [ ('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝'), ('cancelled', '已取消'), ] pet = models.ForeignKey(Pet, on_delete=models.CASCADE, related_name='applications', verbose_name='申请宠物') applicant = models.ForeignKey(User, on_delete=models.CASCADE, related_name='adoption_applications', verbose_name='申请人') applicant_name = models.CharField('申请人姓名', max_length=50) applicant_phone = models.CharField('联系电话', max_length=20) applicant_address = models.CharField('居住地址', max_length=200) has_experience = models.BooleanField('是否有养宠经验', default=False) reason = models.TextField('申请理由', max_length=500) status = models.CharField('审核状态', max_length=20, choices=STATUS_CHOICES, default='pending') admin_note = models.TextField('审核备注', blank=True, help_text='管理员填写审核意见') created_at = models.DateTimeField('申请时间', auto_now_add=True) reviewed_at = models.DateTimeField('审核时间', null=True, blank=True) class Meta: db_table = 'adoption_application' verbose_name = '领养申请' verbose_name_plural = '领养申请' ordering = ['-created_at'] def __str__(self): return f'{self.applicant_name}申请领养{self.pet.name}'有一点需要注意:申请人姓名和电话是单独存在申请表里的,而不是直接去关联的 User 表里取。这样设计是因为用户注册信息可能是昵称和邮箱,但领养审核需要真实姓名和联系方式,把这两份数据拆开更合理,也方便统计。
外键删除策略上,宠物和领养申请之间用了 CASCADE,因为申请对应的宠物如果被删掉了,保留孤儿申请记录意义不大。但用户被删除时,申请记录也会跟着删掉,这在真实运营中可能不是最理想的,最好是把申请人信息也独立保存一份。我在实际项目里建议改成 SET_NULL,把 applicant 外键允许为空,同时保留 applicant_name 等字段,这样用户注销后申请记录还在。
2.3 用户扩展信息:扩展Django自带的User模型
Django自带的User模型已经包含了用户名、密码、邮箱、姓名等基础字段,但领养系统需要知道用户是普通访客还是管理员。虽然可以用 is_staff 和 is_superuser 来区分管理员,但为了让权限模型更清晰,我定义了一个UserProfile模型来补充信息:
class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name='用户') phone = models.CharField('手机号', max_length=20, blank=True) avatar = models.ImageField('头像', upload_to='users/avatars/', blank=True, null=True) city = models.CharField('所在城市', max_length=50, blank=True) bio = models.TextField('个人简介', blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user_profile' verbose_name = '用户资料' verbose_name_plural = '用户资料'这里用的是 OneToOneField 扩展而不是重写 User 模型。重写 User 模型在项目初期做起来方便,但如果项目已经创建过数据库或者后续要接第三方应用,迁移会很痛苦。用 UserProfile 做一对一扩展是最稳妥的方式,不用改动认证流程,Django Admin 也可以用 Inline 的方式把资料和用户放在同一页面管理。
3. 核心功能模块实现:从表单到视图再到模板
3.1 用户注册登录:用Django自带的认证系统改造
用户认证不需要自己造轮子,Django 的django.contrib.auth已经提供了完整的登录、登出、会话管理功能。注册部分要自定义一个视图,因为默认的 UserCreationForm 只有用户名、密码,没有邮箱和姓名。
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django import forms class RegisterForm(UserCreationForm): email = forms.EmailField(required=True) first_name = forms.CharField(max_length=30, required=False, label='姓名') class Meta: model = User fields = ['username', 'first_name', 'email', 'password1', 'password2']注册视图里要注意,表单校验通过后要把用户数据保存,同时顺手创建对应的 UserProfile:
from django.shortcuts import render, redirect from django.contrib.auth import login from django.contrib import messages def register_view(request): if request.method == 'POST': form = RegisterForm(request.POST) if form.is_valid(): user = form.save() UserProfile.objects.create(user=user) login(request, user) messages.success(request, '注册成功,欢迎加入!') return redirect('pet_list') else: form = RegisterForm() return render(request, 'accounts/register.html', {'form': form})登录直接用 Django 内置的 LoginView,在 urls.py 里配置一下就行,不需要自己写视图。模板里用{{ form }}渲染表单,加上 CSRF token 就可以了。这里有个我踩过的坑:如果用UserProfile.objects.create(user=user)创建资料,而后面代码里又用了user.profile.phone去获取手机号,如果在某些视图里用户资料不存在会直接报错,所以注册时创建资料这步不能省。
3.2 图片上传:不只是设置一个ImageField那么简单
很多人以为模型里加了 ImageField 就能上传图片,结果总是图片不显示、form 表单报错、或者上传的图片存不到指定目录。这里把整个链路完整的代码写出来,照着配置就不会出问题。
第一步,在 settings.py 里配置媒体文件路径:
import os MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')第二步,在项目根 urls.py 里开发环境下的媒体文件访问路由:
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)如果不在 DEBUG 模式下加这个静态路由,开发时上传的图片就会 404。因为 Django 开发服务器默认只处理 URL 配置里的路径,不会自动服务媒体目录。生产环境则由 Nginx 直接映射 /media/ 目录。
第三步,表单要用 POST + enctype="multipart/form-data":
在模板里写表单的时候必须加上enctype,否则浏览器不会把文件内容发送到服务器:
<form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">提交</button> </form>第四步,视图里要从 request.FILES 里取文件并保存:
from django.shortcuts import get_object_or_404 def pet_create_view(request): if request.method == 'POST': form = PetForm(request.POST, request.FILES) if form.is_valid(): pet = form.save(commit=False) pet.created_by = request.user pet.save() return redirect('pet_detail', pk=pet.pk) else: form = PetForm() return render(request, 'pets/pet_form.html', {'form': form})注意PetForm(request.POST, request.FILES)这里,FILES 必须作为第二个参数传进去,表单类才能正确处理文件字段。一旦漏了,form.is_valid()会返回 True,但图片字段会是空值,很多新手在这里卡住。
3.3 领养申请流程:有限状态机的转换控制
领养申请不是一个简单的提交和保存,而是要控制状态流转。我实际开发时的规则是这样的:
- 用户只能对“待领养”状态的宠物提交申请。
- 同一只宠物,同一用户不能重复提交未处理的申请。
- 管理员审核通过后,宠物状态变为“已领养”,其他待审核申请自动变为“已拒绝”。
- 管理员审核拒绝后,宠物回到“待领养”状态。
这套规则在视图层实现:
from django.core.exceptions import PermissionDenied from django.contrib import messages def apply_adoption_view(request, pet_id): pet = get_object_or_404(Pet, pk=pet_id) if pet.status != 'available': messages.error(request, '这只宠物当前不可申请领养') return redirect('pet_detail', pk=pet.id) existing = AdoptionApplication.objects.filter( pet=pet, applicant=request.user, status='pending' ).exists() if existing: messages.warning(request, '你已提交过申请,请耐心等待审核') return redirect('pet_detail', pk=pet.id) if request.method == 'POST': form = AdoptionApplicationForm(request.POST) if form.is_valid(): application = form.save(commit=False) application.pet = pet application.applicant = request.user application.save() pet.status = 'pending' pet.save() messages.success(request, '申请提交成功,等待管理员审核') return redirect('my_applications') else: form = AdoptionApplicationForm() return render(request, 'applications/apply_form.html', {'form': form, 'pet': pet})这里有一个很容易被忽略的细节:提交申请后要把宠物状态改成“pending”,否则会出现“同一只宠物多人同时申请,然后管理员只能通过一个人,其他人又被驳回”的并发问题。虽然数据库并发极端情况下还是会出问题,但在应用层先挡住大部分情况,已经能解决绝大多数运营问题。如果数据量大了,可以考虑加事务锁,但当前阶段不用过度设计。
3.4 搜索筛选与分页:Django ORM的链式查询写法
前台首页不能只展示所有宠物,需要支持按品种、城市、性别、状态等条件筛选。Django ORM 的链式过滤非常适合这种场景:
from django.core.paginator import Paginator def pet_list_view(request): pets = Pet.objects.filter(status='available') genre = request.GET.get('genre') if genre: pets = pets.filter(genre=genre) city = request.GET.get('city') if city: pets = pets.filter(city__icontains=city) keyword = request.GET.get('q') if keyword: pets = pets.filter( models.Q(name__icontains=keyword) | models.Q(breed__icontains=keyword) | models.Q(description__icontains=keyword) ) pets = pets.order_by('-created_at') paginator = Paginator(pets, 12) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'genre': genre, 'city': city, 'keyword': keyword, } return render(request, 'pets/pet_list.html', context)关键词搜索里用了Q对象实现多字段模糊匹配,这样用户输入“金毛”就能同时在名字、品种、描述里匹配。分页用Paginator把每页 12 条限制住,模板里渲染页码时保留当前筛选参数很关键,不然翻页之后筛选条件就丢了:
<a href="?page={{ page_obj.next_page_number }}&genre={{ genre }}&city={{ city }}&q={{ keyword }}">下一页</a>3.5 后台管理:Django Admin让管理变得极其省力
这个系统里最惊喜的部分是 Django Admin,几乎不需要额外写任何管理页面,把模型注册到 admin 就能直接用。
from django.contrib import admin from .models import Pet, AdoptionApplication, UserProfile @admin.register(Pet) class PetAdmin(admin.ModelAdmin): list_display = ['name', 'genre', 'breed', 'status', 'city', 'created_at'] list_filter = ['status', 'genre', 'is_neutered', 'is_vaccinated'] search_fields = ['name', 'breed', 'city'] list_editable = ['status'] readonly_fields = ['created_at', 'updated_at'] actions = ['mark_as_adopted'] @admin.action(description='将选中的宠物标记为已领养') def mark_as_adopted(self, request, queryset): queryset.update(status='adopted')这里用list_editable = ['status']就可以在列表页直接下拉修改宠物状态,不需要进入详情页,运营人员用起来效率极高。list_filter按状态、种类筛选,search_fields支持跨字段搜索,这两个配置加完,管理体验完全不输定制开发的后台。
4. 开发环境搭建与项目初始化
4.1 创建虚拟环境并安装Django
整个项目从零开始搭,尽量模拟最典型的开发流程。第一步是创建虚拟环境,让项目的依赖和系统全局Python环境隔离。
mkdir django_pet_adoption cd django_pet_adoption python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate pip install django pillow pip freeze > requirements.txtDjango 和 Pillow 是必需的两个包,Pillow 是处理图片时必须的库。如果还会用到 MySQL 或 PostgreSQL,提前把对应的驱动库也装好,比如mysqlclient或psycopg2-binary。
4.2 创建项目和应用
django-admin startproject config . python manage.py startapp pets python manage.py startapp users python manage.py startapp applications我习惯把项目名叫config,然后放在当前目录(注意后面有个点),这样项目配置目录和业务应用目录平级,看起来更整洁。应用划分按业务边界来:pets 管宠物模型,users 管用户资料,applications 管领养申请。也可以只建一个 app 把所有模型放里面,项目小的时候没问题,但一旦模型多了回头再拆就很费劲,所以一开始就按模块拆开。
4.3 settings.py 的关键配置
创建完项目后,需要修改 settings.py 里的几个地方:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'pets', 'users', 'applications', ] # 数据库默认使用SQLite,如果需要切换MySQL: # DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.mysql', # 'NAME': 'pet_adoption', # 'USER': 'root', # 'PASSWORD': 'your_password', # 'HOST': '127.0.0.1', # 'PORT': '3306', # } # } # 语言与时区 LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')LANGUAGE_CODE = 'zh-hans'和TIME_ZONE = 'Asia/Shanghai'这两个一定要改,Django 默认是英文和 UTC 时区,不改成中文的话 Admin 后台和模板里的时间显示都会有问题。数据库方面,本地开发用 SQLite 完全够用,但生产环境建议切换成 PostgreSQL 或 MySQL,SQLite 在并发写入场景下会锁表。
4.4 数据库迁移与后台注册
模型定义好之后,执行迁移命令:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser遇到模型字段改动比较频繁的时候,makemigrations 可能会提示检测到非可空字段但没有默认值,解决方法是要么给字段加default参数,要么在命令行交互过程中选择提供默认值。我通常在模型里直接写好default,避免在终端里手动填空。
把所有模型都注册到 admin 之后,登录http://127.0.0.1:8000/admin/就能在后台看到数据表,直接增删改查了。这一步做完基本的数据管理能力就有了,后续再慢慢做前台页面对接,开发节奏很舒服。
5. 开发中的关键细节与模板渲染技巧
5.1 Django模板中的静态资源引用
Django 模板里引用 CSS、JS 和图片不能直接写相对路径,要先用{% load static %}加载静态文件模块:
{% load static %} <!DOCTYPE html> <html lang="zh-hans"> <head> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> </head> <body> <img src="{% static 'images/logo.png' %}" alt="logo"> <img src="{{ pet.cover_image.url }}" alt="{{ pet.name }}"> </body> </html>宠物图片用{{ pet.cover_image.url }}来获取,这个属性是 ImageField 根据 MEDIA_URL 自动生成的完整访问路径。如果图片没上传,cover_image为 None,直接调用.url会报错,所以模板里最好加个判断:
{% if pet.cover_image %} <img src="{{ pet.cover_image.url }}" alt="{{ pet.name }}"> {% else %} <img src="{% static 'images/default_pet.jpg' %}" alt="暂无图片"> {% endif %}5.2 CSRF 防护与表单提交
Django 默认开启了 CSRF 防护,所有以 POST 方式提交的模板表单必须带上{% csrf_token %},否则会返回 403 错误。这个机制是安全设计,不要为了省事关掉。如果做前后端分离,用 DRF 写接口的时候,则需要在 Django 设置里配置好 CSRF 的 API 策略,或者使用 JWT 认证方案,那是另一套玩法了。
5.3 用消息框架给用户操作反馈
django.contrib.messages框架可以在操作成功或失败后给用户显示一次性提示。在试图里调用messages.success或messages.error,然后在模板底部循环渲染:
{% if messages %} <div class="messages"> {% for message in messages %} <div class="alert alert-{{ message.tags }}"> {{ message }} </div> {% endfor %} </div> {% endif %}这个功能在用户提交申请、修改资料、管理员审核操作等场景下非常实用,比写死一个成功页面要友好得多。
6. 项目部署上线与常见问题排查
6.1 部署到服务器:参考方案与基本思路
项目在本地开发完成后,要部署到服务器让它真正可用。常见做法是把 Django 项目放到云服务器上,用 Gunicorn 或 uWSGI 运行,再用 Nginx 反代并托管静态文件和媒体文件。也可以使用宝塔面板的 Python 项目管理器来简化流程。
核心步骤大致是:
- 把项目代码传到服务器(可以用 git clone 或者 FTP)。
- 在服务器上创建虚拟环境并安装依赖。
- 收集静态文件:
python manage.py collectstatic。 - 使用 Gunicorn 启动服务:
gunicorn config.wsgi:application -b 0.0.0.0:8000。 - 在 Nginx 中配置反向代理,把域名或端口的请求转发到 8000 端口。
- 配置 /static/ 和 /media/ 目录的访问路径。
- 关闭 DEBUG,配置 ALLOWED_HOSTS 和数据库连接。
如果对服务器配置不熟,用django-admin自带的runserver仅适合本地调试,生产环境千万不要直接跑 runserver,性能和安全性都不行。
6.2 常见问题速查表
我把自己在开发过程中遇到的高频问题整理成了表格,方便你直接对号入座:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 上传图片后前端页面图片不显示 | 没配置 MEDIA_URL / MEDIA_ROOT,或开发环境没加 static 路由 | 按上文配置 media 路径,urls.py 加 static() |
| 表单提交返回 403 | 缺少 {% csrf_token %} | 模板中表单里加上 CSRF token |
| 403 且请求包含 CSRF token 但失败 | Cookie 和 session 配置异常 | 检查 settings.py 中 SESSION_COOKIE_SECURE 设置 |
| 图片字段提示 Pillow 未安装 | 缺少图片处理库 | pip install pillow |
| runserver 启动报错端口被占用 | 8000 端口已被其他进程占用 | 换端口:python manage.py runserver 8001 |
| 数据库迁移提示 non-nullable field | 新增非空字段但没有默认值 | 给字段加 default 或 null=True |
| 中文页面乱码 | 编码配置问题 | 设置 LANGUAGE_CODE='zh-hans',页面加 meta charset="utf-8" |
| Admin 后台没有样式 | 静态文件没配置好 | 检查 STATIC_URL 和 django.contrib.staticfiles |
| ORM 查询返回的是 QuerySet 而不是对象 | 误用了 filter 而没有 get/first | filter 后用 first() 获取单条记录 |
| 分页翻页后筛选条件丢失 | 分页链接没有带上 GET 参数 | 生成分页链接时保留原 query 参数 |
6.3 几个值得注意的踩坑经验
第一个坑是删库重来的代价。开发早期总是改模型字段,直接删掉数据库重新 migrate 是很爽,但一旦有了真实数据就不能这么干了。我在项目第二次部署后就不敢随便删库了,遇到字段改动用makemigrations生成迁移文件,必要时写数据迁移脚本,保证历史数据不丢。
第二个坑是 Admin 后台上传图片后不显示。明明上传成功了,图片文件也能在 /media/ 目录里看到,但页面上一刷新就 404。排查了一圈发现是开发模式下没有在 urlpatterns 里加static(MEDIA_URL, document_root=MEDIA_ROOT)。这个问题太典型了,值得单独提醒。
第三个坑是权限问题。Django 自带的@login_required装饰器只能保证用户已登录,不能区分用户的角色。如果要做管理员和普通用户的权限隔离,需要自定义装饰器或者用user.is_staff判断。我在写申请审核接口时一开始只加了 login_required,结果所有登录用户都能访问审核页面,后来加了 is_staff 判断并做了 403 处理才堵住。
6.4 关于后续扩展方向
这个系统做到这里已经可以跑起来了,但从长期使用的角度看,还有几个扩展点:一个是加消息通知,管理员审核通过或拒绝时给用户发送系统通知;另一个是增加收藏功能,用户可以收藏心仪的宠物,方便后续跟踪;再一个是接入地图,在前台展示救助站的地理位置。如果想把宠物领养做成一个互联网产品,可以考虑把宠物信息通过开放接口共享给其他平台。但这些都是锦上添花的事情,核心的领养闭环已经完整了。
整个项目做下来,我最大的体会是 Django 特别适合这种“管理后台+前台展示+流程审核”的项目,所有功能模块都能在框架里找到对应的解决方案,不需要从零造轮子。对我来说,最值回票价的部分是 Django Admin 在内部管理上的巨大效率提升,运营人员几乎没有学习成本就能管理好所有宠物和申请。如果你也正在做一个类似的信息管理类项目,直接照着这个思路去搭,能少走很多弯路。