Django二手交易系统:从商品发布到交易闭环的实战项目
2026/9/15 16:34:24 网站建设 项目流程

简介:这是一套基于Python与Django框架开发的二手商品交易网站完整源码,面向计算机专业本科生及初学者,适用于毕业设计、课程设计与Web项目实践,解决从零构建用户注册登录、商品发布管理、搜索浏览到交易记录查询等核心电商功能的学习需求。资源压缩包为ZIP格式,共98个文件,包含30个Python业务逻辑与配置文件(如views.py、models.py、urls.py)、9个HTML模板页(含首页、商品列表、详情页等)、10张图片素材及6个JS/CSS前端脚本,整体体积仅673KB,结构清晰、模块分离明确(login、goodsIssue、trade等子应用划分合理)。已有228人学习下载,源码经严格测试可直接运行,附带README.md说明文档与基础部署指引,读者可快速掌握Django MTV架构实践、数据库建模、表单处理及前后端协同开发全流程。

1. 这不是又一个“Django博客模板”,而是一个能跑通完整交易闭环的二手商品系统

你可能已经见过太多基于 Django 的「个人博客」「图书管理系统」或「待办清单」——它们结构清晰、路由简单、模型干净,但离真实业务总差一口气。这个二手商品交易网站不同:它从用户注册登录、商品发布上传、列表筛选浏览、到交易记录归档,全部走通了实际业务中绕不开的环节。比如user_issue.htmlbuy_history.html并非静态页面,而是绑定真实数据库查询逻辑;upload/目录下存在可配置的图片存储路径,且views.py中明确调用request.FILES.get('image')处理二进制上传;trade/goodsIssue/两个独立 app 分别封装交易行为与商品发布流程,边界清晰,不是把所有功能堆在views.py里硬写。它适合刚学完 Django MTV 模式、正卡在「怎么把书上例子变成能上线的项目」阶段的学生,也适合需要快速验证一个轻量级 C2C 业务逻辑的技术负责人——不依赖第三方支付、不强耦合消息队列,但每一步操作都有对应数据库状态变更和权限校验。

2. Django 项目结构解析与核心模块职责划分

2.1 项目层级与 App 职责映射:为什么 trade 和 goodsIssue 必须分离?

整个项目采用标准 Django 多 App 架构,根目录下manage.py启动入口,settings.py配置全局参数,urls.py定义主路由分发。关键在于trade/login/goodsIssue/三个自定义 App 的分工设计:

  • login/:专注身份认证,包含models.py(扩展AbstractUser增加phone字段)、views.pylogin_view,register_view,logout_view)、controller.py(封装密码加密、token 生成等逻辑);
  • goodsIssue/:处理商品生命周期,models.py定义Goods模型(含title,price,description,image,owner外键),views.py实现issue_goods(POST 提交表单+文件)、goods_list(GET 分页查询)、goods_detail(按 ID 查单条);
  • trade/:管理交易动作,models.py包含TradeRecord(关联 buyer/seller/goods、status、create_time),views.py提供create_trade(检查库存、扣减、生成记录)、sale_history(查当前用户卖出记录)、buy_history(查买入记录)。

提示:这种拆分不是为了炫技,而是为后续扩展留出接口。例如未来接入微信支付,只需在trade/views.py中新增pay_with_wechat视图,无需改动商品发布逻辑;若要增加「商品审核」流程,只需在goodsIssue/admin.py中重写GoodsAdmin类,启用list_displayactions即可。

2.2 关键模型字段设计与数据库约束逻辑

goodsIssue/models.pyGoods模型定义如下(已还原.pyc反编译内容):

# goodsIssue/models.py from django.db import models from django.contrib.auth.models import User class Goods(models.Model): title = models.CharField(max_length=100, verbose_name="商品标题") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格") description = models.TextField(verbose_name="商品描述") image = models.ImageField(upload_to='upload/', blank=True, null=True, verbose_name="商品图片") owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goods_owned') status = models.CharField(max_length=20, choices=[ ('on_sale', '在售'), ('sold', '已售出'), ('deleted', '已删除') ], default='on_sale', verbose_name="商品状态") create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) class Meta: verbose_name = "商品" verbose_name_plural = "商品列表" ordering = ['-create_time']

trade/models.pyTradeRecord模型则建立三方关联:

# trade/models.py from django.db import models from django.contrib.auth.models import User from goodsIssue.models import Goods class TradeRecord(models.Model): buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bought_items') seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sold_items') goods = models.ForeignKey(Goods, on_delete=models.CASCADE, related_name='trades') status = models.CharField(max_length=20, choices=[ ('pending', '待确认'), ('completed', '已完成'), ('cancelled', '已取消') ], default='pending') create_time = models.DateTimeField(auto_now_add=True) complete_time = models.DateTimeField(blank=True, null=True) class Meta: verbose_name = "交易记录" verbose_name_plural = "交易记录列表"
字段选型依据说明:
  • price使用DecimalField而非FloatField:避免浮点数精度丢失,如99.99 + 0.01在 float 下可能得100.00000000000001,而 Decimal 精确到小数点后两位;
  • image字段upload_to='upload/'对应settings.pyMEDIA_ROOT = os.path.join(BASE_DIR, 'static/upload/'),确保文件存入项目内static/upload/目录,而非数据库 blob;
  • status字段用choices而非单独 status 表:减少 JOIN 查询,状态值有限且变动极少,直接枚举更高效;
  • related_name显式声明反向关系名(如'bought_items'),避免默认traderecord_set命名模糊,提升代码可读性。

2.3 URL 路由分发与视图函数参数传递机制

urls.py将请求分发至各 App:

# 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('', include('login.urls')), path('goods/', include('goodsIssue.urls')), path('trade/', include('trade.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

各 App 内部urls.py定义具体端点,以goodsIssue/urls.py为例:

# goodsIssue/urls.py from django.urls import path from . import views urlpatterns = [ path('list/', views.goods_list, name='goods_list'), path('detail/<int:goods_id>/', views.goods_detail, name='goods_detail'), path('issue/', views.issue_goods, name='issue_goods'), ]

对应views.pygoods_detail视图接收goods_id参数:

# goodsIssue/views.py from django.shortcuts import render, get_object_or_404 from .models import Goods def goods_detail(request, goods_id): goods = get_object_or_404(Goods, id=goods_id, status='on_sale') # 状态过滤,隐藏已售/删除商品 return render(request, 'goods_detail.html', {'goods': goods})
参数传递关键点:
  • <int:goods_id>是 Django 路径转换器,自动将 URL 中数字转为整型传入视图,避免手动int(request.GET.get('id'))可能引发的ValueError
  • get_object_or_404替代Goods.objects.get(id=goods_id):当查询无结果时返回 404 页面而非DoesNotExist异常,符合 Web 语义;
  • status='on_sale'条件写在get_object_or_404内,而非先查再过滤,减少一次 DB 查询。

3. 核心功能实现:从商品发布到交易完成的全流程代码实操

3.1 商品发布功能:表单验证、文件上传与数据库写入

goodsIssue/views.pyissue_goods视图处理 POST 请求:

# goodsIssue/views.py from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.core.files.storage import default_storage from django.core.files.base import ContentFile from .models import Goods @login_required def issue_goods(request): if request.method == 'POST': title = request.POST.get('title', '').strip() price = request.POST.get('price', '') description = request.POST.get('description', '').strip() image_file = request.FILES.get('image') # 基础字段校验 if not all([title, price, description]): return render(request, 'error_msg.html', {'msg': '请填写完整信息'}) try: price = float(price) if price <= 0: raise ValueError("价格必须大于0") except (ValueError, TypeError): return render(request, 'error_msg.html', {'msg': '价格格式错误'}) # 创建商品实例,暂不保存(因需先处理图片) goods = Goods( title=title, price=price, description=description, owner=request.user, status='on_sale' ) # 图片上传逻辑 if image_file: # 生成唯一文件名:goods_{id}_{original_name} import uuid ext = image_file.name.split('.')[-1].lower() filename = f"goods_{uuid.uuid4().hex}.{ext}" # 保存到 MEDIA_ROOT/upload/ path = default_storage.save(f'upload/{filename}', ContentFile(image_file.read())) goods.image = f'upload/{filename}' goods.save() # 最终写入数据库 return redirect('goods_detail', goods_id=goods.id) return render(request, 'user_issue.html')
关键步骤说明:
  • @login_required装饰器强制登录态,未登录用户访问/goods/issue/会重定向至settings.LOGIN_URL(默认/login/);
  • request.FILES.get('image')获取上传文件对象,ContentFile(image_file.read())将其转为可存储格式;
  • default_storage.save()是 Django 文件存储抽象层,兼容本地文件系统、S3 等后端,当前项目使用默认FileSystemStorage
  • goods.image = f'upload/{filename}'设置ImageField值为相对路径,Django 自动拼接MEDIA_URL生成前端可访问链接(如http://localhost:8000/media/upload/goods_abc123.jpg)。

3.2 交易创建逻辑:状态联动与并发安全处理

trade/views.pycreate_trade视图实现下单:

# trade/views.py from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import JsonResponse from .models import TradeRecord from goodsIssue.models import Goods @login_required def create_trade(request, goods_id): if request.method != 'POST': return redirect('goods_detail', goods_id=goods_id) goods = get_object_or_404(Goods, id=goods_id, status='on_sale') # 使用 select_for_update() 锁定商品行,防止超卖 with transaction.atomic(): goods = Goods.objects.select_for_update().get(id=goods_id) if goods.status != 'on_sale': return render(request, 'error_msg.html', {'msg': '商品已下架或售出'}) # 创建交易记录 trade = TradeRecord.objects.create( buyer=request.user, seller=goods.owner, goods=goods, status='pending' ) # 更新商品状态为已售出 goods.status = 'sold' goods.save() return redirect('trade:sale_history') # 重定向至卖家交易历史
并发控制要点:
  • transaction.atomic()确保select_for_update()goods.save()在同一事务中执行,避免中间状态被其他请求读取;
  • select_for_update()在数据库层面加行锁(MySQL InnoDB 下为SELECT ... FOR UPDATE),阻塞其他事务对同一goods_id的写操作;
  • 若不加锁,高并发场景下可能出现 A、B 两用户同时点击购买同一商品,均通过goods.status == 'on_sale'判断,最终导致商品状态被覆盖为'sold'两次,但数据库只存一条交易记录——即「超卖」。

3.3 用户交易历史页面:多表 JOIN 查询与分页优化

trade/views.pysale_history视图查询当前用户卖出的所有商品及交易状态:

# trade/views.py from django.core.paginator import Paginator from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from .models import TradeRecord from goodsIssue.models import Goods @login_required def sale_history(request): # 查询当前用户作为卖家的所有交易,关联商品信息 trades = TradeRecord.objects.filter( seller=request.user ).select_related('goods', 'buyer').order_by('-create_time') # 分页:每页 10 条 paginator = Paginator(trades, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'sale_history.html', { 'page_obj': page_obj, 'trades': page_obj.object_list })

对应sale_history.html模板中遍历:

<!-- templates/sale_history.html --> {% for trade in page_obj %} <div class="trade-item"> <p>商品:<a href="{% url 'goods_detail' trade.goods.id %}">{{ trade.goods.title }}</a></p> <p>买家:<a href="#">{{ trade.buyer.username }}</a></p> <p>状态:{{ trade.get_status_display }}</p> <p>时间:{{ trade.create_time|date:"Y-m-d H:i" }}</p> </div> {% endfor %} <!-- 分页导航 --> <div class="pagination"> {% if page_obj.has_previous %} <a href="?page=1">&laquo; 第一页</a> <a href="?page={{ page_obj.previous_page_number }}">上一页</a> {% endif %} <span class="current">第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页</span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">下一页</a> <a href="?page={{ page_obj.paginator.num_pages }}">最后一页 &raquo;</a> {% endif %} </div>
查询优化说明:
  • select_related('goods', 'buyer')预加载外键关联表,将原本 N+1 查询(查 trade → 查 goods → 查 buyer)压缩为 1 次 JOIN 查询;
  • order_by('-create_time')加索引:在TradeRecord模型 Meta 中应添加indexes = [models.Index(fields=['-create_time'])](当前源码未显式声明,但生产环境建议补上);
  • Paginator分页对象自带has_previous/has_next属性,避免手动计算页码边界。

4. 本地运行与调试:Python 环境配置、数据库迁移与常见报错排查

4.1 Python 环境与 Django 版本适配

项目未提供requirements.txt,但根据settings.pyINSTALLED_APPSviews.py语法(如@login_required装饰器、select_related方法),可推断兼容 Django 2.2 至 4.2。推荐使用 Python 3.8+(避免 Django 4.x 对 Python 3.12 的部分兼容问题):

# 创建虚拟环境(推荐) python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装 Django(指定版本避免新特性不兼容) pip install Django==3.2.23 # LTS 版本,长期维护,兼容性好 # 验证安装 python -c "import django; print(django.get_version())" # 输出:3.2.23
为什么选 Django 3.2?
  • Django 3.2 是最后一个支持 Python 3.6 的 LTS 版本,向下兼容性强;
  • select_for_update()@login_requiredImageField等核心功能在该版本完全稳定;
  • 避免 Django 4.x 的ASGI默认配置、path()路由语法变更等非必要复杂度。

4.2 数据库初始化:SQLite 迁移与初始超级用户创建

项目默认使用 SQLite(settings.pyDATABASES配置为sqlite3),无需额外安装数据库服务:

# 进入项目根目录(含 manage.py) cd /path/to/django-secondhand # 执行数据库迁移(生成表结构) python manage.py migrate # 创建超级用户(用于 admin 后台) python manage.py createsuperuser # 按提示输入用户名、邮箱、密码 # 启动开发服务器 python manage.py runserver # 访问 http://127.0.0.1:8000/admin/ 登录后台
迁移文件说明:
  • migrations/目录下应存在0001_initial.py(由python manage.py makemigrations生成),当前项目已预生成,直接migrate即可;
  • 若执行migrate报错no such table django_admin_log,说明未运行初始迁移,需先python manage.py migrate --fake-initial(仅首次部署时适用);
  • createsuperuser创建的账号可登录/admin/,在后台可直观查看GoodsTradeRecord数据,验证模型字段是否生效。

4.3 常见运行报错与定位方法

报错信息原因解决方案
ModuleNotFoundError: No module named 'PIL'缺少 Pillow 库,无法处理图片上传pip install Pillow
django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must be set.settings.pySECRET_KEY为空或注释掉手动设置SECRET_KEY = 'your-unique-secret-key-here'(生成方式:python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
OperationalError at /goods/list/ no such table: goodsIssue_goods数据库表未创建重新执行python manage.py migrate,确认migrations/目录非空
CSRF verification failed. Request aborted.表单提交缺少{% csrf_token %}检查user_issue.htmllogin.html等模板,确认<form>内有{% csrf_token %}标签
TemplateDoesNotExist at / error_msg.html模板路径错误确认templates/error_msg.html存在,且settings.pyTEMPLATES[0]['DIRS']包含'templates'

注意:error_msg.html是项目中统一错误提示页,所有校验失败均跳转至此。若该模板缺失,会导致render(request, 'error_msg.html', ...)报 500 错误。务必检查templates/目录完整性。

5. 生产环境部署准备:静态文件收集、DEBUG 关闭与安全加固

5.1 静态文件收集与 MEDIA_ROOT 配置

开发模式下DEBUG=True,Django 自动提供/media/路径服务;生产环境必须关闭DEBUG并由 Nginx/Apache 托管静态资源:

# settings.py(生产环境配置片段) DEBUG = False ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com'] # 静态文件(CSS/JS)配置 STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'collected_static') # collectstatic 输出目录 # 媒体文件(用户上传图片)配置 MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 实际文件存储路径

执行收集命令:

# 收集所有 app 的 static/ 目录到 STATIC_ROOT python manage.py collectstatic --noinput # 创建 media 目录(确保 upload/ 子目录可写) mkdir -p media/upload chmod 755 media chmod 775 media/upload
Nginx 配置示例(托管 media):
# nginx.conf server 块内 location /media/ { alias /path/to/your/project/media/; expires 1h; add_header Cache-Control "public, immutable"; }

5.2 关键安全项检查清单

安全项检查方法修复命令/配置
SECRET_KEY是否硬编码检查settings.pySECRET_KEY是否为固定字符串改为从环境变量读取:SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'fallback-key'),启动时export DJANGO_SECRET_KEY=xxx
DEBUG=True是否残留检查settings.pyDEBUG生产环境设为False,并确认ALLOWED_HOSTS包含域名
X-Frame-Options是否启用访问页面,检查响应头是否有X-Frame-Options: DENYsettings.py中添加X_FRAME_OPTIONS = 'DENY'
密码重置邮件是否启用检查login/views.pypassword_reset视图是否存在若需启用,配置EMAIL_BACKEND和 SMTP 参数,否则禁用相关 URL 路由
Admin 后台路径是否暴露访问/admin/是否可直接进入修改urls.py中 admin 路径:path('secure-admin/', admin.site.urls)
密码哈希算法验证:

Django 默认使用 PBKDF2(pbkdf2_sha256),可在settings.py中确认:

PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', ]

无需修改,此配置已满足毕业设计级别安全性要求。

5.3 一键部署脚本:简化本地测试到上线流程

编写deploy.sh(Linux/macOS)或deploy.bat(Windows)自动化部署:

#!/bin/bash # deploy.sh echo "=== 开始部署二手商品网站 ===" # 1. 激活虚拟环境 source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt 2>/dev/null || pip install Django==3.2.23 Pillow # 3. 数据库迁移 python manage.py migrate # 4. 收集静态文件 python manage.py collectstatic --noinput # 5. 创建超级用户(若不存在) echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.filter(username='admin').exists() or User.objects.create_superuser('admin', 'admin@example.com', 'admin123')" | python manage.py shell # 6. 启动服务器(仅测试用) echo "=== 部署完成!访问 http://127.0.0.1:8000 ===" python manage.py runserver 0.0.0.0:8000

运行前赋予执行权限:

chmod +x deploy.sh ./deploy.sh

该脚本将重复性操作封装,避免学生在课程设计答辩前手忙脚乱配置环境。真正上线时,只需替换runservergunicorn启动命令,并配置 systemd 服务即可。

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

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

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

立即咨询