Django毕业设计:二手交易网站最小可行系统
2026/9/15 16:33:52 网站建设 项目流程

简介:这是一套基于Python与Django框架开发的二手商品交易网站完整源码,面向高校计算机专业学生及Web开发初学者,适用于毕业设计、课程设计与小型项目实践,解决从零构建用户注册登录、商品发布管理、搜索浏览到交易记录查询等核心电商功能的需求。资源包共98个文件,含30个Python业务逻辑文件(如views.py、models.py、controller.py)、9个HTML模板页(含goods_list.html、user_issue.html等)、6个JS/CSS前端资源及10张商品示例图片,结构清晰体现Django典型MVT分层设计,压缩包仅673KB,轻量易部署。已有228人学习下载,可直接运行调试,代码已通过基础功能测试,附带README.md说明与完整目录模块(login、trade、goodsIssue等),便于理解用户认证、商品流转与订单管理三大业务闭环,是掌握Django实战开发的优质入门级参考项目。

1. 为什么用 Django 做二手商品交易网站,比 Flask 或纯 Python Web 框架更省毕业设计时间?

你正在赶计算机专业本科毕业设计,选题卡在“做点实际的、能跑起来、有后台管理、能交源码、答辩不被问倒”这五条硬杠上。这时候翻 GitHub 搜“二手交易”,一堆 Vue+Node.js 的项目——但你只会 Python,老师要求用 Django;再搜“Django 二手”,要么是半成品 demo(没登录、没图片上传、没订单状态流转),要么是商业项目(代码加密、文档缺失、部署脚本藏在付费包里)。结果就是:你花三天搭好基础路由,第四天发现商品详情页无法关联用户收藏,第五天卡在 Django Admin 里怎么让“待审核商品”只对管理员可见、普通用户只能看到“已上架”状态……最后通宵改模板,答辩时被问“为什么不用信号量处理库存扣减”,当场哑火。

这个标题里的“基于 Python + Django 开发的二手商品交易网站”,不是泛泛而谈的 Web 项目,而是面向毕业设计场景的最小可行闭环系统:它必须包含用户注册/登录(带邮箱验证)、商品发布(含多图上传与分类标签)、搜索与筛选(按价格区间、城市、发布时间)、在线沟通雏形(站内消息通知)、订单生成与状态机(待付款→已发货→已完成→已评价)、以及一个可直接进后台批量审核/导出数据的 Django Admin 界面。所有功能模块都用 Django 原生能力实现——不用额外学 Redis 缓存策略,不强求 WebSocket 实时聊天,不硬套 Celery 异步任务。源码不是玩具,但也不追求高并发压测指标;它经得起答辩老师点开 admin 后台删一条测试商品、再刷新前端看是否同步消失,也禁得住你把settings.pyDEBUG=False改完后,在本地用python manage.py runserver --noreload跑通静态文件路径。

适合两类人:一是大四学生,需要一份结构清晰、注释完整、数据库 ER 图可直接截图进论文、部署步骤写进附录就能过审的参考实现;二是刚转行的初级后端,想通过一个真实业务场景(非博客/投票/TODO)理解 Django ORM 如何建模一对多/多对多关系、如何用ModelForm替代手写表单验证、如何用@login_requiredUserPassesTestMixin控制访问权限边界。它不教你怎么写单元测试覆盖率到 90%,但会告诉你python manage.py test跑哪几个用例能覆盖核心路径。


2. 用 Django 4.2 + Python 3.11 搭建二手交易网站的最小依赖与目录结构

2.1 为什么选 Django 4.2 而不是最新 5.x 或老版本 3.2?

Django 4.2 是 LTS(长期支持)版本,官方维护至 2026 年 4 月,毕业设计提交后若需补材料或二次答辩,仍有安全补丁可更新。相比 Django 5.x(2023 年底发布,LTS 尚未确认),4.2 的第三方库兼容性更稳——比如django-crispy-forms用 2.0 版本即可完美适配,而 5.x 需要升级到 2.2+;django-filter在 4.2 下默认支持SearchFilterNumberFilter组合,无需额外写FilterSet类就能实现“按价格区间+关键词”联合筛选,这对二手商品搜索足够用。更重要的是,Django 4.2 的runserver默认启用--reload-type=stat,在 Windows 上热重载稳定,避免你改完views.py却要手动 Ctrl+C 再启动的烦躁。

提示:不要用 Python 3.12——虽然 Django 4.2 官方支持,但pillow(处理商品图片缩略图必需)在 3.12 下编译失败率高;Python 3.11 是当前最平衡的选择,pip install django==4.2.11可直接安装,无编译报错。

2.2 初始化项目与核心 app 划分逻辑

执行以下命令创建项目骨架(注意路径命名规范,避免空格和中文):

# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装 Django 4.2.11 pip install django==4.2.11 pillow django-filter # 创建项目(项目名用英文小写,如 secondhand) django-admin startproject secondhand . # 创建核心 app(按业务域拆分,非技术层) python manage.py startapp users python manage.py startapp products python manage.py startapp orders python manage.py startapp messages

此时目录结构应为:

secondhand/ ├── manage.py ├── secondhand/ # 项目配置包 │ ├── __init__.py │ ├── settings.py # 关键:DATABASES、INSTALLED_APPS、STATIC_ROOT 必须设对 │ ├── urls.py # 主路由,聚合各 app 的 urls.py │ └── wsgi.py ├── users/ # 用户认证与资料 ├── products/ # 商品发布、分类、搜索 ├── orders/ # 订单状态流转、支付模拟 ├── messages/ # 站内信(轻量级,不接 WebSocket) └── templates/ # 公共模板基类 base.html

INSTALLED_APPS中必须加入:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 第三方 'django_filters', # 商品筛选必备 'crispy_forms', # 表单渲染美化(毕业设计界面整洁度加分项) # 自定义 app 'users', 'products', 'orders', 'messages', ]

settings.py中关键配置项(复制即用,参数说明见下表):

配置项说明
DEBUGTrue(开发期) /False(答辩前)关系到错误页面是否暴露代码路径,答辩前务必关掉
ALLOWED_HOSTS['localhost', '127.0.0.1', '[::1]'](开发)
['your-domain.com'](部署)
DEBUG=False时必须显式声明,否则 500 错误
STATIC_URL'/static/'前端引用 CSS/JS 的路径前缀
STATIC_ROOTBASE_DIR / 'staticfiles'python manage.py collectstatic输出目录,宝塔部署时指向此路径
MEDIA_URL'/media/'用户上传图片的 URL 前缀
MEDIA_ROOTBASE_DIR / 'media'上传文件物理存储路径,需在urls.py中显式添加static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

2.3 数据库模型设计:紧扣二手交易核心实体关系

二手交易的本质是“人-物-钱-信”四元组。products/models.py中定义商品主模型:

from django.db import models from django.contrib.auth.models import User from django.core.validators import MinValueValidator, MaxValueValidator class Category(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(max_length=50, unique=True) # 用于 URL,如 /category/shouji/ def __str__(self): return self.name class Product(models.Model): STATUS_CHOICES = [ ('draft', '草稿'), ('pending', '待审核'), ('published', '已上架'), ('sold', '已售出'), ('closed', '已下架'), ] title = models.CharField(max_length=200) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)]) category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products') owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products') status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.title} ({self.get_status_display()})"

关键点说明:

  • on_delete=models.PROTECT用于Category:防止删除分类时误删所有商品,答辩时老师问“如果删了手机分类,商品去哪了?”,你能答“Django 会抛 IntegrityError,强制开发者先迁移商品”;
  • related_name='products'user.products.all()可直接查该用户所有商品,比user.product_set.all()更语义化;
  • status字段用choices而非整数枚举,Admin 界面自动渲染下拉菜单,且get_status_display()返回中文(如'已上架'),前端模板中{% if product.status == 'published' %}可直接用。

orders/models.py定义订单状态机(简化版,满足毕业设计要求):

class Order(models.Model): STATUS_CHOICES = [ ('pending', '待付款'), ('paid', '已付款'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ] buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders_as_buyer') seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders_as_seller') product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='orders') amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at']

注意:这里没引入OrderItem(订单明细表),因为二手商品单笔订单只含一个商品,加明细表反而增加答辩解释成本。若老师问“以后支持购物车怎么办?”,可答:“当前架构预留扩展,只需新增OrderItem模型并关联Orderamount字段移至明细表”。


3. 实现商品发布、搜索与权限控制的三步落地法

3.1 用 ModelForm 快速构建商品发布表单,绕过手写 HTML 验证

products/forms.py

from django import forms from .models import Product, Category class ProductForm(forms.ModelForm): # 覆盖默认字段,添加图片多文件上传支持 images = forms.ImageField( widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False ) class Meta: model = Product fields = ['title', 'description', 'price', 'category', 'status'] widgets = { 'description': forms.Textarea(attrs={'rows': 4}), 'price': forms.NumberInput(attrs={'step': '0.01'}), } def clean_price(self): price = self.cleaned_data.get('price') if price < 0.01: raise forms.ValidationError("价格不能低于 0.01 元") return price def clean(self): cleaned_data = super().clean() status = cleaned_data.get('status') if status == 'published' and not self.instance.pk: # 新建商品设为已上架,需触发审核流程(此处简化为直接设 pending) cleaned_data['status'] = 'pending' return cleaned_data

products/views.py中处理发布逻辑(含图片保存):

from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.core.files.storage import default_storage from .forms import ProductForm from .models import Product @login_required def product_create(request): if request.method == 'POST': form = ProductForm(request.POST, request.FILES) if form.is_valid(): product = form.save(commit=False) product.owner = request.user product.save() # 处理多图上传(Django 4.2+ 支持 request.FILES.getlist) for file in request.FILES.getlist('images'): # 使用 default_storage 保存到 MEDIA_ROOT path = default_storage.save(f'products/{request.user.id}/{file.name}', file) # 此处应关联到 ProductImage 模型(为简化,暂存路径到 product.image_urls 字段) # 实际毕业设计中建议新建 ProductImage 模型,外键关联 Product return redirect('products:detail', pk=product.pk) else: form = ProductForm() return render(request, 'products/product_form.html', {'form': form})

products/templates/products/product_form.html(使用 crispy_forms 渲染):

{% load crispy_forms_tags %} <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form|crispy }} <button type="submit" class="btn btn-primary">发布商品</button> </form>

提示:enctype="multipart/form-data"必不可少,否则request.FILES为空;crispy_forms|crispy过滤器自动添加 Bootstrap 样式,避免手写 CSS 影响答辩演示效果。

3.2 用 django-filter 实现“价格区间+关键词+分类”三合一搜索

products/filters.py

import django_filters from .models import Product class ProductFilter(django_filters.FilterSet): min_price = django_filters.NumberFilter(field_name='price', lookup_expr='gte', label='最低价格') max_price = django_filters.NumberFilter(field_name='price', lookup_expr='lte', label='最高价格') search = django_filters.CharFilter( field_name='title', lookup_expr='icontains', label='商品名称' ) category = django_filters.ModelChoiceFilter( queryset=Category.objects.all(), label='分类' ) class Meta: model = Product fields = ['search', 'min_price', 'max_price', 'category']

products/views.py中集成过滤器:

from django.shortcuts import render from django_filters.views import FilterView from .filters import ProductFilter from .models import Product class ProductListView(FilterView): model = Product template_name = 'products/product_list.html' context_object_name = 'products' filterset_class = ProductFilter paginate_by = 12 # 每页 12 条,符合毕业设计展示需求 def get_filterset_kwargs(self, filterset_class): kwargs = super().get_filterset_kwargs(filterset_class) # 默认只显示已上架商品 kwargs['queryset'] = Product.objects.filter(status='published') return kwargs

products/urls.py

from django.urls import path from . import views urlpatterns = [ path('', views.ProductListView.as_view(), name='list'), path('<int:pk>/', views.ProductDetailView.as_view(), name='detail'), ]

products/templates/products/product_list.html中渲染过滤表单:

<!-- 过滤表单 --> <form method="get"> {{ filter.form }} <button type="submit" class="btn btn-outline-primary">搜索</button> <a href="{% url 'products:list' %}" class="btn btn-outline-secondary">重置</a> </form> <!-- 商品列表 --> <div class="row"> {% for product in products %} <div class="col-md-4 mb-4"> <div class="card"> <img src="{{ product.image_url|default:'https://via.placeholder.com/300x200?text=No+Image' }}" class="card-img-top" alt="{{ product.title }}"> <div class="card-body"> <h5 class="card-title">{{ product.title }}</h5> <p class="card-text">{{ product.description|truncatewords:20 }}</p> <p class="text-danger">¥{{ product.price }}</p> <a href="{% url 'products:detail' product.pk %}" class="btn btn-sm btn-primary">查看详情</a> </div> </div> </div> {% endfor %} </div>

注意:filter.form自动渲染所有字段,min_price/max_price生成<input type="number">search生成<input type="text">category生成<select>。答辩时老师点“手机”分类+“1000”最低价,页面实时刷新,证明你真懂查询逻辑。

3.3 权限控制:谁可以编辑商品?谁能看到待审核商品?

Django 默认User模型权限粒度粗,毕业设计不必接入django-guardian,用UserPassesTestMixin足够:

products/views.py中限制编辑权限:

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.views.generic import UpdateView class ProductUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): model = Product form_class = ProductForm template_name = 'products/product_form.html' success_url = '/products/' def test_func(self): product = self.get_object() # 仅商品拥有者或超级用户可编辑 return product.owner == self.request.user or self.request.user.is_superuser def handle_no_permission(self): from django.http import HttpResponseForbidden return HttpResponseForbidden("您没有权限编辑此商品")

secondhand/urls.py中注册:

from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('users/', include('users.urls')), path('products/', include('products.urls')), path('orders/', include('orders.urls')), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

products/admin.py中定制 Admin 界面(答辩亮点):

from django.contrib import admin from .models import Product, Category @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['title', 'price', 'category', 'owner', 'status', 'created_at'] list_filter = ['status', 'category', 'created_at'] search_fields = ['title', 'description'] list_editable = ['status'] # 直接在列表页修改状态 date_hierarchy = 'created_at' raw_id_fields = ['owner'] # 避免用户多时下拉卡顿 def save_model(self, request, obj, form, change): # 保存时记录操作人 if not change: obj.created_by = request.user super().save_model(request, obj, form, change) @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} # 输入 name 自动填充 slug

提示:prepopulated_fields让分类名输入后自动生成 URL 友好 slug(如“笔记本电脑”→bi-jing-ben-dian-nao),答辩时老师输入中文,看到 URL 变成拼音,会觉得你考虑周全。


4. 宝塔面板部署 Django 项目的实操步骤与避坑清单

4.1 宝塔安装与 Python 项目环境配置

登录宝塔面板后,按顺序操作:

  1. 软件商店 → Python 项目管理 → 安装 Python 3.11(勾选pipsetuptools);
  2. 网站 → 添加站点:域名填your-domain.com,根目录选/www/wwwroot/secondhand,PHP 版本选“纯静态”(Django 不用 PHP);
  3. Python 项目 → 添加项目
    • 项目名称:secondhand
    • 项目路径:/www/wwwroot/secondhand
    • Python 版本:3.11
    • 启动文件:/www/wwwroot/secondhand/manage.py
    • 启动命令:gunicorn secondhand.wsgi:application -c /www/wwwroot/secondhand/gunicorn.conf.py
    • 运行目录:/www/wwwroot/secondhand

注意:宝塔默认用gunicorn,不是runservergunicorn.conf.py需手动创建(内容见下节),否则启动失败。

4.2 gunicorn 配置文件与静态资源处理

/www/wwwroot/secondhand/gunicorn.conf.py

import multiprocessing bind = '127.0.0.1:8000' # gunicorn 监听端口,Nginx 反向代理至此 bind_address = '127.0.0.1:8000' workers = multiprocessing.cpu_count() * 2 + 1 worker_connections = 1000 max_requests = 1000 timeout = 30 keepalive = 2 preload = True daemon = False pidfile = '/www/wwwroot/secondhand/gunicorn.pid' accesslog = '/www/wwwroot/secondhand/logs/access.log' errorlog = '/www/wwwroot/secondhand/logs/error.log' loglevel = 'info'

执行收集静态文件(关键!否则 CSS/JS 404):

cd /www/wwwroot/secondhand source /www/server/python/v3.11/bin/activate python manage.py collectstatic --noinput

此时STATIC_ROOT指向的/www/wwwroot/secondhand/staticfiles目录已生成全部静态资源。

4.3 Nginx 反向代理配置与 media 文件直出

宝塔网站设置 → 配置文件,替换原有内容为:

# 代理 Django 应用 location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态文件由 Nginx 直出(提升性能) location /static/ { alias /www/wwwroot/secondhand/staticfiles/; } # 用户上传图片直出 location /media/ { alias /www/wwwroot/secondhand/media/; }

保存后重启 Nginx。此时访问http://your-domain.com应显示首页,/admin/可登录后台。

提示:/media/路径必须与settings.pyMEDIA_URL一致,且alias后路径末尾带/,否则图片 404。答辩前务必上传一张测试图,打开浏览器开发者工具 Network 标签,确认media/xxx.jpg返回 200。

4.4 常见报错与速查解决方案

报错现象原因解决方案
ModuleNotFoundError: No module named 'secondhand.wsgi'manage.py所在目录未加入 Python path在宝塔 Python 项目设置中,“运行目录”必须填/www/wwwroot/secondhand,不是子目录
502 Bad Gatewaygunicorn 未启动或端口冲突进入终端ps aux | grep gunicorn,杀掉旧进程;检查gunicorn.conf.pybind端口是否被占用(如netstat -tuln | grep 8000
Static files not foundcollectstatic未执行或STATIC_ROOT路径错误进入宝塔终端,cd /www/wwwroot/secondhand && python manage.py collectstatic --noinput,确认staticfiles目录存在且有文件
Database connection failedsettings.pyDATABASES未切换为 MySQL安装宝塔 MySQL,创建数据库,修改settings.py
'ENGINE': 'django.db.backends.mysql'
'NAME': 'secondhand_db'
'USER': 'root'
'PASSWORD': 'your_mysql_password'

5. 毕业设计答辩高频问题应答技巧与源码交付清单

5.1 针对“Django ORM 查询优化”的三个必答话术

老师常问:“商品列表页如果数据量大,怎么优化?”
标准答案结构:承认瓶颈 + 给出 Django 原生解法 + 说明毕业设计取舍理由。

  • “当前用Product.objects.filter(status='published')查询,数据量小时无压力。若未来扩展,我会:
    ① 在status字段加数据库索引:db_index=True(已在models.py中声明);
    ② 对分类、价格范围等高频筛选字段,用django-filterIndex选项生成复合索引;
    ③ 分页用Paginator(已实现),避免LIMIT OFFSET深分页问题——但毕业设计数据量百条级,OFFSET效率足够。”


老师追问:“为什么不用原生 SQL?”
回答:“Django ORM 提供了extra()raw()方法,但会破坏模型抽象层。答辩材料中ER 图models.py保持一致,便于评审老师快速理解数据关系。若真需复杂统计,我会用Product.objects.extra(tables=['orders'], where=['products.id = orders.product_id']),而非脱离 ORM 写 SQL。”

5.2 源码交付包必须包含的 7 个文件/目录

毕业设计提交的.zip包不能只有manage.py,必须结构完整,让老师解压后pip install -r requirements.txt && python manage.py migrate即可运行:

路径作用是否必须
requirements.txtpip freeze > requirements.txt生成,含Django==4.2.11等精确版本
secondhand/settings.py注释掉DEBUG=TrueALLOWED_HOSTS设为['*'](本地测试)或['your-domain.com'](部署版)
db.sqlite3开发用 SQLite 数据库(含测试数据),避免老师装 MySQL
media/目录存放 3 张测试商品图(命名phone1.jpg,book2.jpg,laptop3.jpg),证明图片上传功能
templates/全目录base.htmlproducts/list.html等,Bootstrap 5 CSS 已内联或 CDN 引入
README.md写明:① 环境要求(Python 3.11);② 启动步骤(pip install -r req.txtpython manage.py migratepython manage.py createsuperuserpython manage.py runserver);③ 管理员账号(admin/admin123
ER_Diagram.png用 draw.io 画的实体关系图,标注User-Product一对多、Product-Category多对一等关系

注意:requirements.txt中不要出现pkg-resources==0.0.0(Ubuntu 系统 bug),用pip install --no-deps -r requirements.txt验证无报错。

5.3 答辩现场演示的 3 个黄金动作

  1. 打开 Admin 后台,现场创建一个商品
    登录http://localhost:8000/admin→ 点击ProductsADD PRODUCT→ 填标题“二手 iPhone 13”、价格“3500.00”、选分类“手机”、状态“待审核” →SAVE→ 页面跳转到列表页,看到新商品状态为“待审核”。
    目的:证明模型、表单、权限全部打通。

  2. 切换到前台,搜索该商品
    打开http://localhost:8000/products/→ 在搜索框输入“iPhone” → 点搜索 → 列表只显示刚创建的商品(因状态为 pending,不显示)→ 修改 Admin 中状态为“已上架” → 刷新前台,商品出现。
    目的:验证搜索逻辑与状态过滤联动。

  3. 用另一个账号登录,尝试编辑
    新开浏览器隐身窗口 →http://localhost:8000/users/login/→ 用测试账号testuser/test123登录 → 访问http://localhost:8000/products/1/update/(ID 为 1 的商品)→ 显示 403 Forbidden。
    目的:证明权限控制生效,不是摆设。

这三个动作耗时不到 2 分钟,却覆盖了模型、视图、模板、权限、数据库五大核心得分点。老师记笔记时,写的往往是“演示流畅,功能闭环”,而不是“代码写了多少行”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询