ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django实战:从数据库设计到部署,构建二手交易平台

Django实战:从数据库设计到部署,构建二手交易平台

1. 项目定位与核心模块拆解

1.1 这类系统到底要解决什么问题

二手商品交易平台这套项目,说白了就是解决一个信息撮合的问题——卖家的闲置物品挂上去,买家能看到、能筛选、能联系、最终能完成交易。用Django来做这套系统,最大的优势是开发效率极高,模型、视图、模板、后台管理一条龙全部自带,一个开发者从零到一,差不多两三周就能搞定一套可演示、可部署的完整闭环。

我一开始接触这个项目的时候,第一反应是:这不就是电商系统的简化版吗?但真正把功能梳理一遍之后,发现它和标准的B2C电商平台还是有明显差异的。二手交易的核心不在支付流程多复杂,而在几个关键环节:商品信息的结构化展示、买卖双方的沟通渠道、交易状态的流转闭环、以及最容易被忽视的——信任机制。这些点如果不在一开始就设计到位,后面写代码会遇到大量返工。

那这套系统适合谁来参考?我的建议很明确:正在做毕业设计的学生、想练手Django全栈开发的初级工程师、以及想快速落地一个校内或社区二手交易演示项目的朋友。你不需要有很深的电商背景,但对Python基础语法、Django的MTV模式要有基本概念。看完这篇文章,你能掌握的不只是一套代码,而是这类信息撮合系统的完整设计思路。

1.2 模块划分与后台管理设计

我拿到这类项目的第一件事,从来不是打开代码就跑,而是先把模块边界画清楚。这套系统的模块划分,大致是这样一个结构:

  • 用户模块:注册、登录、个人信息维护、头像上传、密码修改
  • 商品模块:商品发布、商品列表、商品详情、商品搜索与筛选、商品上下架
  • 收藏模块:收藏与取消收藏、收藏列表
  • 交易模块:订单创建、订单状态流转、订单查询
  • 留言模块:商品询价留言、留言回复
  • 后台管理:商品分类管理、用户管理、商品审核、订单管理、数据统计

这个划分的底层逻辑,是遵循了Django中"业务按app拆分"的最佳实践。你可以拆成5到6个app,也可以先拆成两三个大app再在内部用类来分层,但千万别把所有逻辑都堆在根目录的一个views.py里。项目规模大了以后,这种写法会让人痛不欲生。

我习惯的拆法是这样的:apps目录下放users、products、orders、messages四个app。users处理注册登录和个人中心,products接收商品相关的所有业务,orders管订单流转,messages管站内留言。分类这种字段级数据,不单独做app,直接放进products就行。后台管理不需要自己写代码,Django自带的admin站点通过配置就能满足大部分管理需求。

后台管理这块很多人会低估。实际上,一个能自动生成的admin站点是Django项目最吸引人的地方之一。你只需要在admin.py里注册模型,就能获得一套完整的增删改查界面。更进一步,你可以自定义list_display、search_fields、list_filter,让商品审核、用户禁用这些日常操作变成点几下鼠标的事。我这里强烈建议在开发早期就把admin配置好,因为后面的数据结构调整,你要依托管理后台去验证数据状态。

2. 数据库设计——交易类项目的命门

2.1 核心表结构与字段设计

二手交易平台的数据模型,是整套系统里最不该草率的部分。我见过不少初学者喜欢把所有字段塞进一张大表里,用TextField把图片地址、标签、描述全装进去,写到后面查询写不下去,只能推倒重来。数据库设计这件事,刚开始花两个小时想清楚,后面省下的是两星期的排错时间。

我建议的核心表设这样:

  • 用户表:直接继承Django的AbstractUser扩展,加上phone(手机号)、avatar(头像)、credit_score(信用分)等字段。继承而不是新建独立表,原因很简单:Django的认证框架默认就绑定User模型,你重写它只是加字段,不用动认证逻辑。

  • 分类表:name、parent(自关联、支持二级分类)、sort_order(排序权重)。分类表别看简单,它是商品检索和首页导航的骨架。

  • 商品表:title(标题)、description(描述)、price(价格)、original_price(原价)、condition(新旧程度)、category(外键)、seller(外键)、cover_image(封面图)、status(上架/下架/已售)、view_count(浏览次数)、created_at、updated_at。这里有一个细节:为什么要有original_price?因为二手商品的"性价比"要有参照物,买家和展示界面都能形成对比。

  • 商品图片表:product(外键)、image(图片字段)、sort_order。一张商品可以有多张图片,封面图字段并不和这个表冲突,封面是给列表页用的,详情页用图片表。

  • 收藏表:user和product的双外键组合,加上时间戳。这个表要加UniqueConstraint,防止同一个用户反复收藏同一件商品。

  • 订单表:order_no(订单编号)、product(外键)、buyer(买家外键)、seller(卖家外键)、amount(成交金额)、status(待支付/已支付/已发货/已完成/已取消)、created_at、paid_at。

  • 留言表:product、sender、reply_to(回复哪条留言)、content、created_at。

这套表结构不是什么创新设计,但它覆盖了二手交易全流程的持久化需求。核心原则就是:状态变化要有时间戳记录,金额要单独存字段而不是从商品表实时读取。因为商品价格可能会改,但订单成交金额必须锁定下单那一刻的值。

2.2 关键约束与外键策略

外键这块,需要讲究。Django的ORM默认外键删除行为是CASCADE——父表数据被删,子表数据一并删除。这在社交媒体评论场景没问题,但在交易系统里就很危险。你想,一个用户注销了,你总不能把他的所有成交订单都删掉吧?订单记录是交易凭证,必须留存。

所以我处理订单表的seller和buyer外键时,用on_delete=models.SET_NULL, null=True, related_name='seller_orders'。这样即使用户注销,订单记录还在,只是关联的用户变成空。related_name必须显式指定,否则反向查询时会和默认的order_set搅在一起,出现字段冲突。

商品表的外键策略又不一样。商品被删的时候,它下面的图片、收藏记录应该跟着清理,所以product外键用CASCADE合理。但留言表呢?留言可能有交易沟通记录的价值,我不建议级联删除,还是用SET_NULL更稳妥。

还有一个很多人爱踩的坑:用DecimalField存金额,不要用FloatField。二进制浮点数在加法乘法后会出现精度误差,比如0.1加0.2不等于0.3。金额这种字段一旦需要做对账,浮点误差足以让你头疼。DecimalField(max_digits=10, decimal_places=2)在Django里映射的数据库类型是DECIMAL,从ORM到数据库层面都是定点数运算,安全得体验好到飞起。

3. 核心业务功能的实现细节

3.1 用户认证与注册登录

这套系统的用户认证,我建议直接使用Django自带认证框架,然后做两件事:一是集成一个简单的手机验证码或邮箱验证逻辑,二是自定义用户模型。第一件事的原因很简单:二手交易平台买家和卖家本身就是私人用户,不能用后台管理员的账号体系,你需要的是面向大众的注册入口。

注册流程说起来不复杂:前端表单收集用户名、邮箱、密码、确认密码,后端用UserCreationForm子类校验,如果自定义了User模型,就要继承UserCreationForm并指定model。这里有个关键点——创建超级用户一定要在数据库迁移之后再做,我一直强调顺序:迁移→建管理员→正常注册用户。

密码存储这块,Django默认的PBKDF2算法已经够用。很多人看别的项目用MD5存密码,就觉得网上教程是那么教的,也跟着做。这里我必须严正说明:明文密码和弱哈希加密方案在真实项目里就是安全事故。Django的create_user()方法会自动处理加密和加盐,你不需要自己写任何密码哈希逻辑,这就是框架给你的安全兜底——不使用它,等于主动把安全大门敞开。

登录视图也有讲究。直接用login(request, user)方法登录,注意区分authenticate()和login()两个方法:前者是校验用户凭证,后者是把登录状态写入session。校验不通过,要跳回登录页并提示"用户名或密码错误",但别具体告诉用户是用户名错了还是密码错了,防止别人探测已注册的用户名。这个细节虽然小,但体现了一个开发者是否有安全意识。

3.2 商品发布与管理

商品发布是整个平台最核心的写操作。前端表单需要处理的字段有:标题、描述、分类、价格、原价、新旧程度、图片列表。Django的处理思路是用ModelForm绑定商品模型,图片部分单独处理。

图片上传这块,我建议在settings.py中配置:

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 += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

注意,这个配置只用于开发环境。生产环境里,媒体文件应该交给静态服务(如对象存储或独立的静态文件服务器),Django只负责接收上传,不负责托管文件的长期访问。你如果直接把DEBUG=True挂到线上,媒体文件请求由Django处理,性能会很难看。

再一个细节就是图片格式和大小校验。前端可以做accept="image/*",后端也要校验,因为接口是可以绕过的。用Pillow库读取图片,检查扩展名、用MAX_IMAGE_SIZE限制大小,再顺手压缩一下。这点体验上很有用,一个5兆的照片传到服务器,列表页加载起来像蜗牛。我在这个项目里做了个简单的压缩:把图片转成RGB模式并限制最长边不超过1280像素,压缩后大多数图片在200K以内,加载速度立竿见影。

商品管理的另一个重点,就是上下架状态切换。卖家可以把自己的商品下架,下架后的商品在前台列表页不能再出现。这里要注意查询逻辑:列表页所有商品查询,都要加上status='on_sale'这个条件。如果你用了Django的ModelManager,可以直接在模型里定义:

class ProductQuerySet(models.QuerySet): def on_sale(self): return self.filter(status='on_sale')

然后在商品模型上加objects = ProductQuerySet.as_manager()。这样前台调用Product.objects.on_sale()就能拿全部在售商品,后台管理可以用默认的objects查看全部数据。语法简洁,还不容易在视图里忘记过滤条件。

3.3 订单与交易流程

订单流程是我认为这套系统里最见功力的部分。二手交易平台的支付,在演示项目里通常都是模拟支付——用户点击购买,弹出订单确认,然后点击确认支付,订单状态变为已支付。但即便是模拟,状态机的设计也必须逻辑严谨。

我设计的状态流是这样的:待支付 → 已支付 → 已发货 → 已完成;待支付可以主动取消;待支付超时(比如48小时)可以做自动取消。每次状态变更,都要检查当前状态是否允许跳转。也就是说,你不能让一个"已发货"的订单直接变成"待支付",这会破坏业务闭环。

代码层面,一个值得学习的技巧是使用事务。创建订单时,涉及商品状态检查和订单记录写入,这两步要放在同一个事务里:

from django.db import transaction with transaction.atomic(): product = Product.objects.select_for_update().get(pk=product_id) if product.status != 'on_sale': raise ValidationError('商品已不在售') product.status = 'sold' product.save() Order.objects.create(...)

看到select_for_update()这个词了吗?它把商品记录锁住,防止两个买家同时下单同一件商品。不加这个锁,在高并发场景下会出现超卖或重复订单的问题。演示项目里体现不出并发压力,但这个写法是生产环境里防止数据错乱的标配,值得了解并写进代码里。

订单编号我建议用时间戳加随机数拼接,比如:

import time import random order_no = f"{time.strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"

别用数据库自增ID做订单号,理由是订单号在很多场景下会对用户展示,会直观反映出平台的订单量,这是商家避讳的信息。随机订单号等于抹平这个泄露。

交易完成之后,还可以做一个信用的联动——买家确认收货后给卖家加一个信用分。信用分是二手交易平台里非常核心的信任数据,哪怕初始都是100分,交易完成加1分,被投诉且核实扣分,简单朴素,但对平台氛围有实际影响。这一步的做法是:订单状态变为已完成时,同时更新卖家的credit_score。

4. 后台管理与权限控制

4.1 管理后台的自定义

Django的admin站点是这类管理系统的加分项,完全可以做到"即插即用"。商品分类管理、用户管理、订单查询都不需要额外写前端页面,只需要在admin.py里做好配置。

这里分享一个我的习惯配置:

@admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ('title', 'price', 'seller', 'status', 'view_count', 'created_at') list_filter = ('status', 'category', 'condition') search_fields = ('title', 'seller__username') readonly_fields = ('view_count', 'created_at', 'updated_at')

readonly_fields很重要。所有由系统维护的计数和时间戳,不应该让管理员在后台直接改,不然数据就失真了。价格和商品状态可以改,这是客服和高管的合理需求,但查看次数、创建时间这类信息应该只读。

商品分类在admin里做树形展示,需要一点小配置。你可以用第三方库,或者干脆用ordering = ('parent_id', 'sort_order')来让父子分类排在一起,后台界面手动选择parent就行。对内部管理来说,这样够用。

4.2 权限与安全

权限控制要分两层讲:一层是Django自带的三级权限(超级管理员、staff标记、普通用户),另一层是业务上的"某人只能操作自己的商品和订单"。

第一层很好办:在admin.py中注册模型时,Django会自动为staff用户生成增删改查权限。如果你只想让管理员管理某些模型,就在该模型的ModelAdmin里覆写has_add_permission、has_delete_permission等方法,返回False即可。比如订单模块,一般只给查看和修改订单状态权限,不给删除权限。交易数据不能删,这是铁律。

第二层是视图级别的权限。在商品编辑、删除、上下架的视图里,必须先取到商品对象,判断request.user == product.seller,不是就直接返回403或者重定向到详情页。这一步很多人会漏掉。他们觉得路由URL是/product/edit/3/,改一下数字就能改别人的商品?是的,只要你不检查归属,任何人都能改。这个漏洞叫IDOR(不安全的直接对象引用),在漏洞报告里非常常见。

一个适合放在视图里的装饰器写法:

def seller_required(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): product = get_object_or_404(Product, pk=kwargs['pk']) if product.seller != request.user: raise PermissionDenied return view_func(request, *args, **kwargs) return wrapper

这个装饰器挂在商品编辑、删除、上下架的视图函数上,一次性搞定权限校验。注意Django视图有CBV和FBV两种风格,我用这个装饰器的前提是视图是FBV,如果你用CBV,更优雅的方式是重写dispatch方法或者在get_object里判断。

5. 部署实施与关键配置

5.1 环境准备

实战部署时,你会需要准备的东西包括:一台能跑Python的服务器(虚拟主机或云主机都可以)、Python 3.8以上版本、一个数据库(PostgreSQL或MySQL,不建议在生产环境用SQLite)、以及反向代理工具。

虚拟环境是一定要建的,不管你在本地还是服务器上,都建议用python -m venv venv创建隔离环境。我吃过很多次亏,项目代码在不同机器上跑不起来,查了半天发现是全局环境里的包版本冲突。有了虚拟环境,requirements.txt一锁版本,代码搬到任何机器都能复现,这才是靠谱的流程。

安装Django和相关依赖:

pip install django pillow pip freeze > requirements.txt

生成依赖清单这个习惯,不要等到上线前一天才做。每装一个新包,就顺手更新一下requirements.txt,这样不会遗漏依赖。

5.2 数据库迁移

Django的迁移体系是这样的:你用模型定义了数据结构,makemigrations生成迁移文件,migrate把迁移文件同步到数据库。

python manage.py makemigrations users products orders messages python manage.py migrate

这里有一个常见的坎:如果你在迁移过程中改了已经应用过的模型,然后迁移文件报错,先去检查数据库表结构和你当前模型是否一致。Django会对比迁移记录和django_migrations表,不一致就会报冲突。解决的办法通常是回滚迁移,或者在确认改动方向正确的前提下,直接删除有问题的迁移文件并重新生成。

不过有个例外:users这个app里有User表,它关联Django的认证框架。任何对它的模型调整,都要格外小心。如果你要改变AbstractUser的字段,必须在makemigrations之前备份数据库,因为你可能动到了核心认证表,一旦出错,登录系统就崩了。

迁移完成后,创建超级管理员:

python manage.py createsuperuser

输入用户名、邮箱、密码,这个账号就是后台管理的入口。

5.3 静态文件与上线

生产环境下,Django默认不部署静态文件。你需要先在settings.py里设置STATIC_ROOT,然后运行:

python manage.py collectstatic

这个命令会把所有静态文件收集到STATIC_ROOT目录下,交给Nginx或Apache去服务。注意DEBUG=False时Django不会自动处理静态文件请求,所以collectstatic这一步不能省。

ALLOWED_HOSTS这个配置也要正确填写。只写['*']在开发调试时方便,但线上一定要限制域名列表,否则你的站点会被垃圾流量扫描盯上,无谓增加安全风险。

如果用了MySQL,记得在settings.py里用环境变量读数据库配置,不要硬编码账号密码。哪怕上线后密码只有你自己知道,配置写在代码里也是一个定时炸弹。用os.environ.get()读取环境变量,敏感信息全部放服务器的环境变量文件里,这几乎是行业共识。

6. 常见问题与排查实录

6.1 问题速查表

这套项目从开发到部署,我踩过不少坑,也帮朋友排查过不少问题。我把高频问题整理成一个速查表,遇到同样现象可以直接对照:

问题现象最可能的原因排查方式与解决手段
图片上传失败或上传后无法显示MEDIA_ROOT未配置;DEBUG=False时静态媒体未被正确服务确认MEDIA_ROOT路径存在;开发环境用static()挂载,生产环境交给Nginx
迁移时报No migrations to apply迁移文件已生成但未执行,或django_migrations表记录与迁移文件不一致先运行python manage.py showmigrations检查状态,再执行migrate
登录报错"数据库里有多个匹配的User"username字段冲突,或自定义用户模型时未正确设置唯一约束清理重复用户,自定义模型上显式加unique=True
商品列表页无法加载分页数据Paginator对象未正确传递到模板上下文检查视图返回的context字典里是否包含page_obj
后台登录无法访问首页当前用户不是staff或超级管理员用createsuperuser重新创建管理员,或在后台为普通用户勾选staff标记
订单并发创建时出现了两个相同订单缺少事务和行锁在创建订单的视图里使用transaction.atomic()配合select_for_update()

这个表基本上是实用性排查手册。你遇到的问题大概率逃不出这几个方向,照着排查能省很多时间。

6.2 两个典型的难缠问题

  • 问题一:图片上传后显示404

这类问题十有八九出在媒体文件路由上。开发环境下忘了在urls.py挂载static(),或者MEDIA_ROOT指向了不存在的目录;生产环境下忘了在Nginx里配置/media/代理。一次排查解决后,我一般都会在本地写一个"部署清单",里面列上所有必须配置的条目,上线前按清单过一遍。

  • 问题二:Django时区问题

settings.py里的TIME_ZONE如果是'UTC',你存进数据库的时间就比北京时间慢8个小时。最典型的场景就是后台查看商品发布时间,总觉得对不上。正确的配置是:USE_TZ = True留作习惯,TIME_ZONE = 'Asia/Shanghai'。模板渲染时,Django会自动把UTC时间转换成配置的本地时间展示,数据库里存储的是带时区的时间。这个坑不大,但不理解的人会排查很久。

7. 扩展方向与二次开发建议

这套系统做完,如果你还想让它更落地,我建议按下面几个方向去扩展——它们每一条都贴合二手交易平台的真实场景:

  • 搜索能力升级:现在列表页的分页查询是SQL的exact匹配。你可以给title字段加__icontains实现模糊搜索,甚至可以引入全文检索引擎(比如基于PostgreSQL的全文检索,或者轻量级搜索引擎),让商品检索更精准。

  • 购物车模块:目前是单商品直接下单。如果希望买家可以一次下单多件商品,就要增加购物车表,建立CartItem与Product的关联,然后在订单结构里引入订单明细表。

  • 消息通知机制:访客下单后通知卖家,卖家发货后通知买家,这些需要一种通知队列。Django里可以用django-notifications之类的库,或者自己建一个Notification模型,在订单状态变更的代码逻辑里插入通知写入。

  • 担保交易与退款流程:真正用于生产环境的二手平台,交易资金是托管在平台方的。你可以设计一个托管资金账户,模拟买家付款后资金冻结、卖家发货后资金解冻、买家确认后资金到账。再把退款申请表单和审核流程加上,这个系统就具备接近真实商业产品的基本要素了。

每个方向我都有对应的实战笔记,但篇幅所限,这篇先落实到主体功能的完整闭环。从模型设计到部署上线,整个流程我尽量把关键的"为什么"都讲透了。选Django做这种项目,本质上是选它的工作效率和安全性兜底——你只需要关心业务逻辑,框架已经替你挡掉了一大批低级的坑。但这不代表你可以放松对数据一致性、权限校验和部署规范的要求,恰恰是这些细节,决定了这个项目在面试官或者导师眼里,是一个"能跑的demo"还是一个"有点工程素养的项目"。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进