多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Django实战:从零开发社区物品捐赠网站,掌握Web开发全流程

Django实战:从零开发社区物品捐赠网站,掌握Web开发全流程 做社区物品捐赠网站这个项目算是我在Django学习路上做的第一个完整项目也是我把Django设计与实现从书本概念落到真实业务的一次集中演练。它看起来不复杂——一个发布闲置物品、让有需要的人申请领取的社区平台——但真做下来才发现用户认证、权限控制、数据建模、图片上传、状态流转、搜索分页、后台管理、部署上线Web开发里的主流程基本全涉及了。所以如果你也在学Python和Django这个项目的价值远不止做一个网站这么简单它更像是一次完整的实战拉练。这个项目解决的实际问题很具体小区、大学、公益组织里经常有人家里有闲置物品想捐但找不到合适的接收人另一边有人需要某样东西却不想花钱买新的。线下靠微信群接龙、贴公告效率低信息留存和审核也麻烦。做一个社区物品捐赠网站就是把这套流程搬到线上捐品发布、浏览检索、申请领取、捐赠方审核确认整个过程有记录、可追踪数据沉淀下来还能给管理者做统计。适合两类人来参考一是刚学完Django基础、想通过完整项目巩固知识的开发者二是有真实需求、想给社区或公益场景搭一个捐赠信息平台的人。1. 需求拆解与整体方案怎么定1.1 先想清楚这个网站到底要解决什么设计的第一步不是写代码而是把业务逻辑盘清楚。我接到这个需求时先把自己代入到社区志愿者和捐赠人的角色里列了一个最小可用流程捐赠人登录后发布物品填标题、描述、图片、品类有需要的人登录后按分类或关键词检索物品看中某件物品后提交领取申请附一段申请说明捐赠人收到申请后审核决定是否赠送审核通过后双方线下交接物品状态变为已完成管理员可以查看所有物品和申请记录做数据维护。这个流程是我跟需求方反复对齐过的。之所以一开始就要画清楚是因为后面所有数据库表设计、视图逻辑、模板页面都是由它推导出来的。如果流程没定就开始建表大概率会出现表结构返工的情况。比如曾经有个版本想把申请次数限制做进流程里后来发现实际运营中根本不需要这个限制反而增加了申请模块的复杂度最后砍掉了。流程画得清楚砍功能的时候也更有依据。1.2 功能模块怎么划分按上面的流程我把网站拆成了五个核心模块模块核心能力涉及技术点用户模块注册、登录、退出、个人资料Django 自带认证体系 自定义用户模型物品模块发布、编辑、下架、列表检索ModelForm、文件上传、分页搜索申请模块提交领取申请、审核通过/拒绝外键关联、状态机设计、事务控制后台管理数据管理、物品/申请审核查看Django Admin 二次开发前端页面首页、列表页、详情页、个人中心模板继承、静态资源配置这样划分的好处是职责清晰每个模块对应独立的应用或独立的模型后续扩展功能比如加积分、加站内信不会互相纠缠。我当时把物品和申请拆成了两个模型而不是挂在同一张表里就是因为它们的生命周期不同物品创建一次但申请会随着不同用户反复产生混在一张表里查询会越来越乱。1.3 为什么选Django而不是其他框架这个选择其实没什么悬念。当时对比过 Flask 和 Spring Boot最终定 Django理由就三条。第一Django 自带 Admin 后台像这种管理后台需求量大的项目直接省掉一大半开发时间第二自带的 ORM、表单、认证体系都是开箱即用不需要自己拼第三方库第三Python 生态里做这类网站社区资料最全的也是 Django遇到问题搜索解决方案效率高。用官方文档里那句话来说Django 适合deadline 驱动的完美主义者——内置功能多能少写很多胶水代码。如果换成 Flask这套东西虽然也能做但用户体系、Admin、ORM都要自己集成工作量至少多三成。Spring Boot 则是另一条路线对中小型社区场景来说偏重光环境配置就足够让初学者头疼。做这类业务系统Django 的约定优于配置确实是省心。2. 数据库设计模型是地基这一层必须讲透2.1 核心模型怎么建数据库模型是整个项目的骨架这段代码是基于我实际项目简化后的版本直接影响到了后面所有业务逻辑的实现方式# donation/models.py from django.db import models from django.contrib.auth.models import AbstractUser from django.utils import timezone class User(AbstractUser): 扩展自定义用户模型未来可挂手机号、头像等字段 phone models.CharField(手机号, max_length11, blankTrue) avatar models.ImageField(头像, upload_toavatars/, blankTrue, nullTrue) class Meta: verbose_name 用户 class Category(models.Model): name models.CharField(分类名称, max_length50) sort_order models.IntegerField(排序, default0) class Meta: verbose_name 物品分类 ordering [sort_order] def __str__(self): return self.name class DonationItem(models.Model): STATUS_CHOICES [ (available, 待领取), (pending, 已有人申请), (completed, 已完成), (cancelled, 已取消), ] title models.CharField(物品名称, max_length100) description models.TextField(物品描述) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, verbose_name分类) owner models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name捐赠人, related_namedonated_items) image models.ImageField(物品图片, upload_toitems/, blankTrue, nullTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultavailable) created_at models.DateTimeField(发布时间, defaulttimezone.now) class Meta: verbose_name 捐赠物品 ordering [-created_at] class DonationApplication(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 已通过), (rejected, 已拒绝), ] item models.ForeignKey(DonationItem, on_deletemodels.CASCADE, related_nameapplications, verbose_name物品) applicant models.ForeignKey(User, on_deletemodels.CASCADE, related_nameapplications, verbose_name申请人) message models.TextField(申请说明, max_length500) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(申请时间, defaulttimezone.now) class Meta: verbose_name 捐赠申请 ordering [created_at]有几个设计点值得解释。Item 的owner用了 ForeignKey 且on_deletemodels.CASCADE用户注销时关联的物品会一并删除这里在业务上需要谨慎。实际我的做法是在业务层禁止硬删除用户只做软删除标记避免历史记录丢失。Category 的外键用了SET_NULL分类删除后物品保留只是分类为空这在捐赠场景下更合理——你不想因为一个分类被清理整批捐赠记录跟着消失。2.2 用户模型为什么一开始就要自定义初学者最常见的坑项目建到一半发现需要在用户身上挂字段比如手机号、实名认证信息、信用积分这时候才发现默认的 User 模型改起来很麻烦只能建一张独立的 Profile 表做一对一关联。我当时没有走这条路而是在项目初始化时就用AUTH_USER_MODEL指定了自定义的 User。原因很简单Django 官方文档明确建议在项目一开始就自定义用户模型即便是零扩展也一样。因为AUTH_USER_MODEL必须在第一次执行migrate之前配置一旦执行过迁移项目里已经生成了引用默认 User 的迁移文件再改会牵扯到整条依赖链的迁移重做非常痛苦。具体做的第一步是在 settings.py 里加上AUTH_USER_MODEL donation.User然后先执行makemigrations donation再执行migrate。顺序错了会报一堆迁移冲突的错误后面第5章我会把这个坑单独拿出来讲。还有一个细节自定义 User 模型时如果你计划以后支持昵称、头像、手机号这类资料字段最好现在就加进去哪怕暂时不用。后期加字段要再做一次迁移但完全可控而如果一开始就走 Profile 表模式关联查询和 Admin 管理会别扭很多。2.3 状态流转用枚举状态而不是布尔值物品和申请都有状态字段这里我强烈建议用字符串 choices 而不是布尔值。以物品为例是否已送出这个字段如果设计成is_donated布尔值当需求演化出待领取、申请中、已完成、已取消四种状态时一个布尔字段根本表达不清楚还得再加字段。状态字段我定义为available待领取刚发布可被申请pending已有人申请已提交了申请正在审核中不再接受新的申请completed已完成审核通过并完成线下交接cancelled已取消捐赠人主动下架或取消。对应的申请状态有 pending、approved、rejected。这里要注意一个业务规则物品状态为 pending 时不应该再允许其他用户提交新的申请。这个校验不放在前端前端只是隐藏按钮真正要放在后端视图里强制判断否则绕过页面发请求就能重复申请这是我踩过的一个实打实的坑。3. 功能模块逐个落地从注册登录到审核闭环3.1 用户注册登录用自带认证但表单要自己写注册登录这种功能直接用 Django 自带的auth应用最省事。登录可以直接用LoginView但注册没有现成的视图需要自己写。我的做法是继承UserCreationForm做注册表单为了收集手机号再把自定义字段一起加进去。# donation/forms.py from django import forms from django.contrib.auth.forms import UserCreationForm from .models import User class RegisterForm(UserCreationForm): phone forms.CharField(max_length11, requiredFalse, label手机号) class Meta: model User fields [username, phone] def save(self, commitTrue): user super().save(commitFalse) user.phone self.cleaned_data.get(phone, ) if commit: user.save() return user注册视图里重点处理的是表单校验失败时的回显和错误信息展示模板里的错误提示我用了 Django 表单默认的错误列表没自己做一套报错逻辑省了很多事。登录、登出直接配置 URL 即可# donation/urls.py from django.contrib.auth import views as auth_views urlpatterns [ path(login/, auth_views.LoginView.as_view(template_nameregistration/login.html), namelogin), path(logout/, auth_views.LogoutView.as_view(), namelogout), path(register/, views.register, nameregister), ]权限控制方面发布物品、申请物品这两个操作都要求登录我用的是login_required装饰器。审核通过/拒绝这个操作除了要求登录还要求当前用户必须是物品的 owner这个用user_passes_test或者直接在视图里判断后者逻辑更直观。我的习惯是能用装饰器表达清楚的就用装饰器需要读取当前用户身份和资源关联关系的就老老实实在视图函数里写判断这样代码读起来不容易产生歧义。3.2 物品发布处理图片上传是第一个坎物品发布的表单用 ModelForm 一把梭这是 Django 最舒服的地方模型定好后表单代码量很小class ItemForm(forms.ModelForm): class Meta: model DonationItem fields [title, description, category, image] widgets { description: forms.Textarea(attrs{rows: 4}), }视图层保存时要注意两点。第一owner不能直接暴露给用户填要在save(commitFalse)之后手动指定为当前登录用户第二图片上传必须在 settings.py 里配置好MEDIA_ROOT和MEDIA_URL否则 Django 只是把文件路径存进数据库文件实际上没有被保存到服务器指定目录。# settings.py MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media本地开发时要在主 url 配置里加上from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)ImageField本身依赖 Pillow 库安装 Django 时不会自动带上 Pillow记得先pip install pillow否则迁移时会报错。这个问题在部署到新环境时尤其常见很多人在本地能用换到服务器就报ModuleNotFoundError: No module named PIL。另一个跟图片相关的细节是表单校验被用户上传的图片如果超大服务器内存和网络带宽都会被拖慢。我给表单加了一个简单的文件大小限制超过 5MB 直接拒绝虽然可以从前端限制但后端一定要兜底。3.3 申请与审核事务和状态校验一个都不能少提交申请这个动作涉及生成申请记录和更新物品状态两步操作必须在一个事务里完成。我用的是transaction.atomic()块包裹同时在内部用select_for_update()锁住物品行防止两个人同时申请同一件物品。from django.db import transaction login_required def apply_item(request, item_id): item get_object_or_404(DonationItem, pkitem_id) if request.method POST: with transaction.atomic(): # 锁住这条物品记录阻止并发申请 locked_item DonationItem.objects.select_for_update().get(pkitem.pk) if locked_item.status ! available: return render(request, donation/apply_result.html, {message: 该物品已被人申请或已完成}) application DonationApplication( itemlocked_item, applicantrequest.user, messagerequest.POST.get(message, ) ) application.save() locked_item.status pending locked_item.save() return redirect(donation:item_detail, item_iditem.pk) return render(request, donation/apply_form.html, {item: item})审核的逻辑相对简单但有个细节值得说捐赠人只能操作属于自己的物品的申请状态别人物品下挂的申请不能动。我一开始漏了这个校验结果任意登录用户只要知道申请ID就能把别人的申请改成通过这是典型的不安全的直接对象引用问题。后来在视图里补了一行判断if application.item.owner ! request.user: return HttpResponseForbidden(你不是该物品的捐赠人)这一步其实比功能本身更重要手动测试的时候一定要拿两个账号交叉验证。我当时的测试方法是准备一个捐赠人账号和一个申请人账号分别走完整流程再反过来试越权场景确认返回 403 才算通过。3.4 Admin后台配置少写一半管理页面Django Admin 在这个项目里不只是后台管理它几乎是给社区管理员用的完整工具。我的配置代码如下重点在于列表页的展示字段、筛选和关联编辑# donation/admin.py from django.contrib import admin from .models import Category, DonationItem, DonationApplication admin.register(DonationItem) class DonationItemAdmin(admin.ModelAdmin): list_display [title, owner, category, status, created_at] list_filter [status, category] search_fields [title, description] list_per_page 20 admin.register(DonationApplication) class DonationApplicationAdmin(admin.ModelAdmin): list_display [item, applicant, status, created_at] list_filter [status] search_fields [item__title, applicant__username]设置list_per_page 20后物品和申请记录多了也不会卡页。Admin 里的搜索、筛选这些能力如果自己从零写两周都未必做得完这就是选 Django 的底气。另外我给 Admin 添加了一个数据统计需求想快速知道每个分类下的物品数量用了list_filter默认的统计分组功能配合date_hierarchy按月筛选发布记录管理员的日常运营工作基本可以告别 SQL 手工查询了。4. 前端页面与交互细节模板组织、检索与分页4.1 模板继承别把公共代码复制十几遍网站页面不多但结构高度相似所以我用模板继承把所有公共部分收敛到 base.html。base.html 里放置了导航栏、页脚、登录状态显示以及 CSS/JS 的引用每个子页面只需要扩展 content 块!-- templates/base.html -- !DOCTYPE html html langzh-cn head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}社区物品捐赠{% endblock %}/title {% load static %} link relstylesheet href{% static css/style.css %} /head body nav a href{% url donation:item_list %}首页/a {% if user.is_authenticated %} span{{ user.username }}/span a href{% url donation:item_create %}发布捐赠/a a href{% url donation:my_items %}我的捐赠/a a href{% url donation:my_applications %}我的申请/a a href{% url logout %}退出/a {% else %} a href{% url login %}登录/a a href{% url register %}注册/a {% endif %} /nav main {% block content %}{% endblock %} /main /body /html模板里判断登录状态用的是user.is_authenticated这个变量在 Django 上下文中默认就有。初学者容易犯的错是去 request 里手动取 user其实模板直接能用。页面样式我用了最简单的手写 CSS没有引入重型前端框架。项目的价值在后端逻辑前端保持干净实用即可。导航栏的登录/退出状态切换通过if user.is_authenticated分支渲染代理后台测试时非常直观。4.2 列表页分页和搜索组合拳物品列表页是用户最高频访问的页面我实现了关键词搜索 分类筛选 分页三合一。视图代码如下from django.core.paginator import Paginator from django.db.models import Q def item_list(request): items DonationItem.objects.select_related(category, owner).all() keyword request.GET.get(q, ) category_id request.GET.get(category, ) if keyword: items items.filter(Q(title__icontainskeyword) | Q(description__icontainskeyword)) if category_id: items items.filter(category_idcategory_id) paginator Paginator(items, 12) page_number request.GET.get(page, 1) page_obj paginator.get_page(page_number) return render(request, donation/item_list.html, {page_obj: page_obj, categories: Category.objects.all()})这里用了select_related一并对 category 和 owner 做联表查询页面渲染时不需要对每条物品再发一次数据库查询。如果忘了加列表 50 条物品就会多出 100 次额外的 SQL本地看不出来上线后数据库连接会很快被打满。分页组件我直接用了 Django 的Paginator模板里渲染上一页、下一页和当前页码。需要注意的是搜索和筛选参数在翻页时必须回传否则点第二页搜索条件就丢了。模板里的分页链接我当时是手动拼的?page2q{{ keyword }}category{{ category_id }}这个细节特别容易漏。另一个实用技巧我在列表页只显示 available 状态和 pending 状态的物品completed 和 cancelled 的直接过滤掉避免用户翻到大量已结束的旧捐赠体验会好很多。4.3 详情页和申请表单的交互详情页展示物品完整信息以及当前用户的申请入口。这里有一个小设计如果当前物品状态不是 available或者当前用户就是物品 owner就不显示申请按钮改显示对应的提示文字。这个判断在前端做好后端同样做好双层校验。前端层面我直接判断item.status available and user.is_authenticated and item.owner ! user才渲染申请按钮后端则是在视图里再次校验。申请表单是一个简单的 message 文本框提交后直接跳转到一个结果页或者返回列表页。我在结果页里展示了申请成功请等待捐赠人审核这样明确的反馈用户体验会好很多。这里有个小技巧提交成功后的跳转一定要用redirect而不是render否则刷新页面会重复提交表单。这个坑我印象很深第一次做的时候没注意手动刷新页面就多了一条申请记录清理数据时才发现。5. 常见问题与排查技巧实录5.1 迁移冲突和 AUTH_USER_MODEL 的坑自定义用户模型最折腾的是迁移。如果你在创建项目后先跑过默认的 migrate再设置 AUTH_USER_MODELDjango 会报类似这样的错误ValueError: The field admin.LogEntry.user was declared with a lazy reference to auth.user, but donation.user isnt installed.我自己的处理办法是如果项目还在开发初期且还没重要数据直接删掉数据库文件和所有应用的 migrations 目录里的迁移文件保留__init__.py然后重新 makemigrations migrate。如果已经有数据了那只能在项目最开始就设好 AUTH_USER_MODEL或者用迁移工具花大力气重排迁移依赖链。这也是我在 2.2 里强调一开始就自定义的原因。做这个项目时我亲眼见过一个改了一半迁移文件的场景最后整条依赖链乱成一团花了一个下午才梳理清楚。5.2 CSRF 校验失败和403错误新手最容易遇到的是表单 403。Django 默认开启了 CSRF 校验任何 POST 请求都需要模板里有{% csrf_token %}。忘加这个标签后POST 提交一概被拒。排查方法也简单按 F12 看响应只要带着CSRF关键字的 403第一反应就是去查模板。还有一个容易忽略的点当部署到线上、跨域访问时CSRF 的 cookie 设置和 SameSite 属性可能会出问题表现形式是客户端 cookie 正常但始终校验失败。我当时在 Django 新版本里遇到过需要确认CSRF_COOKIE_SECURE、SESSION_COOKIE_SECURE这些配置是否和线上 HTTPS 场景匹配。如果你用 nginx 做了反向代理还得注意请求头里的X-Forwarded-Proto是否正确不然 Django 会认为请求不是 HTTPS导致 cookie 行为异常。5.3 并发申请同一件物品的数据一致性问题这个在前面申请视图里已经用select_for_update()解决了一半但对新手来说最容易遇到的问题是这样的两个人同时在某件 available 状态的物品上点击申请由于两个请求都通过了 status available 的校验然后都插入申请记录最后物品状态变成 pending但申请记录却生成了两条其中一条是无效的。用transaction.atomic()select_for_update()可以锁定行第二个请求在锁释放后重新读取状态会发现已经不是 available从而被拦截。这里要特别强调光靠 Django 应用层的 if 判断是不够的必须配合数据库层面的行锁。数据库引擎必须支持行级锁SQLite 和 MySQL/PostgreSQL 的差异挺大实际部署我建议用 PostgreSQL。做过一次压测就明白了SQLite 下并发一高直接报 database is locked而 PostgreSQL 处理这种场景要稳得多。5.4 静态文件和图片上传线上失效本地开发时静态文件由 Django 直接服务没问题但部署到服务器后DEBUGFalse时 Django 不再处理静态文件此时 css、js、图片全部 404。解决办法是跑python manage.py collectstatic把静态文件收集到指定目录然后交给 Nginx 托管对应的配置在 Nginx 里加一个 location /static/ 和 location /media/。Admin 后台样式丢失也是同一个原因。很多人部署完发现后台页面全是裸的还以为 Admin 崩了其实是静态文件没收集。我当时第一次部署就撞上了这个问题页面功能都正常但样式全没加载排查了半天才发现是静态文件目录没配好。还有一个容易被忽视的点collectstatic 收集的是所有应用里注册过 static 目录的文件如果你有一些自定义的静态资源放在项目根目录的 static 文件夹下记得在 settings.py 里加STATICFILES_DIRS否则收集不进去。5.5 数据库迁移到生产环境的常见坑本地开发默认用 SQLite上线时如果换成 MySQL 或 PostgreSQL有几个坑要提前知道。第一ImageField存的是字符串路径两个数据库的字段长度限制不同要注意 max_length第二DateField、DateTimeField 的时区处理在不同数据库和不同USE_TZ配置下表现不同第三SQLite 对并发写支持较弱在高并发场景下会出现数据库锁定的错误。所以我的建议是如果预期真实用户量超过几十人开发时就别用 SQLite直接上 PostgreSQL开发环境和生产环境保持一致能省很多麻烦。我实际部署时用的是云厂商托管的 PostgreSQL连接配置集中在环境变量里不用把账号密码写死在代码中。启动前先跑迁移再手动插入几个测试分类数据验证 Admin 登录和列表页功能正常后才把域名切过去。上线后注意看数据库连接池的配置Django 默认的 CONN_MAX_AGE 是 0每个请求都新建连接并发稍高就会显得吃力我把它调到了 60 秒配合 PostgreSQL 的连接复用压力小了不少。做这个项目最大的体会是一个看似简单的捐赠网站真正写下来之后能串起 Django 的几乎所有核心知识点。自定义用户模型、模型状态设计、事务与并发控制、图片上传、模板继承、搜索分页、Admin 定制、静态文件部署……这些东西单独学的时候都觉得自己会了一旦组合在一个真实项目里光状态一致性这一个点就够琢磨很久。如果你也想拿这个练手我的建议是先别急着写代码耐住性子把业务流程图和数据表结构画清楚这会省掉后续大部分返工。另外一定不要跳过部署环节把我第5章列的那些坑都踩一遍你对 Django 的理解会上一个台阶。后面还可以给这个项目继续加功能比如站内通知、积分激励机制、定期数据统计报表扩展方向很多这个地基打好了往上盖楼不会太难。
返回列表