拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Django二手交易系统:从商品发布到交易闭环的实战项目

简介这是一套基于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.html和buy_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_goodsPOST 提交表单文件、goods_listGET 分页查询、goods_detail按 ID 查单条trade/管理交易动作models.py包含TradeRecord关联 buyer/seller/goods、status、create_timeviews.py提供create_trade检查库存、扣减、生成记录、sale_history查当前用户卖出记录、buy_history查买入记录。提示这种拆分不是为了炫技而是为后续扩展留出接口。例如未来接入微信支付只需在trade/views.py中新增pay_with_wechat视图无需改动商品发布逻辑若要增加「商品审核」流程只需在goodsIssue/admin.py中重写GoodsAdmin类启用list_display和actions即可。2.2 关键模型字段设计与数据库约束逻辑goodsIssue/models.py中Goods模型定义如下已还原.pyc反编译内容# goodsIssue/models.py from django.db import models from django.contrib.auth.models import User class Goods(models.Model): title models.CharField(max_length100, verbose_name商品标题) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) description models.TextField(verbose_name商品描述) image models.ImageField(upload_toupload/, blankTrue, nullTrue, verbose_name商品图片) owner models.ForeignKey(User, on_deletemodels.CASCADE, related_namegoods_owned) status models.CharField(max_length20, choices[ (on_sale, 在售), (sold, 已售出), (deleted, 已删除) ], defaulton_sale, verbose_name商品状态) create_time models.DateTimeField(auto_now_addTrue) update_time models.DateTimeField(auto_nowTrue) class Meta: verbose_name 商品 verbose_name_plural 商品列表 ordering [-create_time]trade/models.py中TradeRecord模型则建立三方关联# 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_deletemodels.CASCADE, related_namebought_items) seller models.ForeignKey(User, on_deletemodels.CASCADE, related_namesold_items) goods models.ForeignKey(Goods, on_deletemodels.CASCADE, related_nametrades) status models.CharField(max_length20, choices[ (pending, 待确认), (completed, 已完成), (cancelled, 已取消) ], defaultpending) create_time models.DateTimeField(auto_now_addTrue) complete_time models.DateTimeField(blankTrue, nullTrue) class Meta: verbose_name 交易记录 verbose_name_plural 交易记录列表字段选型依据说明price使用DecimalField而非FloatField避免浮点数精度丢失如99.99 0.01在 float 下可能得100.00000000000001而 Decimal 精确到小数点后两位image字段upload_toupload/对应settings.py中MEDIA_ROOT os.path.join(BASE_DIR, static/upload/)确保文件存入项目内static/upload/目录而非数据库 blobstatus字段用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_rootsettings.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, namegoods_list), path(detail/int:goods_id/, views.goods_detail, namegoods_detail), path(issue/, views.issue_goods, nameissue_goods), ]对应views.py中goods_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, idgoods_id, statuson_sale) # 状态过滤隐藏已售/删除商品 return render(request, goods_detail.html, {goods: goods})参数传递关键点int:goods_id是 Django 路径转换器自动将 URL 中数字转为整型传入视图避免手动int(request.GET.get(id))可能引发的ValueErrorget_object_or_404替代Goods.objects.get(idgoods_id)当查询无结果时返回 404 页面而非DoesNotExist异常符合 Web 语义statuson_sale条件写在get_object_or_404内而非先查再过滤减少一次 DB 查询。3. 核心功能实现从商品发布到交易完成的全流程代码实操3.1 商品发布功能表单验证、文件上传与数据库写入goodsIssue/views.py中issue_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( titletitle, priceprice, descriptiondescription, ownerrequest.user, statuson_sale ) # 图片上传逻辑 if image_file: # 生成唯一文件名goods_{id}_{original_name} import uuid ext image_file.name.split(.)[-1].lower() filename fgoods_{uuid.uuid4().hex}.{ext} # 保存到 MEDIA_ROOT/upload/ path default_storage.save(fupload/{filename}, ContentFile(image_file.read())) goods.image fupload/{filename} goods.save() # 最终写入数据库 return redirect(goods_detail, goods_idgoods.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 等后端当前项目使用默认FileSystemStoragegoods.image fupload/{filename}设置ImageField值为相对路径Django 自动拼接MEDIA_URL生成前端可访问链接如http://localhost:8000/media/upload/goods_abc123.jpg。3.2 交易创建逻辑状态联动与并发安全处理trade/views.py中create_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_idgoods_id) goods get_object_or_404(Goods, idgoods_id, statuson_sale) # 使用 select_for_update() 锁定商品行防止超卖 with transaction.atomic(): goods Goods.objects.select_for_update().get(idgoods_id) if goods.status ! on_sale: return render(request, error_msg.html, {msg: 商品已下架或售出}) # 创建交易记录 trade TradeRecord.objects.create( buyerrequest.user, sellergoods.owner, goodsgoods, statuspending ) # 更新商品状态为已售出 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.py中sale_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( sellerrequest.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 classtrade-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 classpagination {% if page_obj.has_previous %} a href?page1laquo; 第一页/a a href?page{{ page_obj.previous_page_number }}上一页/a {% endif %} span classcurrent第 {{ 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)预加载外键关联表将原本 N1 查询查 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.py中INSTALLED_APPS和views.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 Django3.2.23 # LTS 版本长期维护兼容性好 # 验证安装 python -c import django; print(django.get_version()) # 输出3.2.23为什么选 Django 3.2Django 3.2 是最后一个支持 Python 3.6 的 LTS 版本向下兼容性强select_for_update()、login_required、ImageField等核心功能在该版本完全稳定避免 Django 4.x 的ASGI默认配置、path()路由语法变更等非必要复杂度。4.2 数据库初始化SQLite 迁移与初始超级用户创建项目默认使用 SQLitesettings.py中DATABASES配置为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/在后台可直观查看Goods、TradeRecord数据验证模型字段是否生效。4.3 常见运行报错与定位方法报错信息原因解决方案ModuleNotFoundError: No module named PIL缺少 Pillow 库无法处理图片上传pip install Pillowdjango.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must be set.settings.py中SECRET_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.html、login.html等模板确认form内有{% csrf_token %}标签TemplateDoesNotExist at / error_msg.html模板路径错误确认templates/error_msg.html存在且settings.py中TEMPLATES[0][DIRS]包含templates注意error_msg.html是项目中统一错误提示页所有校验失败均跳转至此。若该模板缺失会导致render(request, error_msg.html, ...)报 500 错误。务必检查templates/目录完整性。5. 生产环境部署准备静态文件收集、DEBUG 关闭与安全加固5.1 静态文件收集与 MEDIA_ROOT 配置开发模式下DEBUGTrueDjango 自动提供/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/uploadNginx 配置示例托管 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.py中SECRET_KEY是否为固定字符串改为从环境变量读取SECRET_KEY os.environ.get(DJANGO_SECRET_KEY, fallback-key)启动时export DJANGO_SECRET_KEYxxxDEBUGTrue是否残留检查settings.py中DEBUG值生产环境设为False并确认ALLOWED_HOSTS包含域名X-Frame-Options是否启用访问页面检查响应头是否有X-Frame-Options: DENY在settings.py中添加X_FRAME_OPTIONS DENY密码重置邮件是否启用检查login/views.py中password_reset视图是否存在若需启用配置EMAIL_BACKEND和 SMTP 参数否则禁用相关 URL 路由Admin 后台路径是否暴露访问/admin/是否可直接进入修改urls.py中 admin 路径path(secure-admin/, admin.site.urls)密码哈希算法验证Django 默认使用 PBKDF2pbkdf2_sha256可在settings.py中确认PASSWORD_HASHERS [ django.contrib.auth.hashers.PBKDF2PasswordHasher, django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher, ]无需修改此配置已满足毕业设计级别安全性要求。5.3 一键部署脚本简化本地测试到上线流程编写deploy.shLinux/macOS或deploy.batWindows自动化部署#!/bin/bash # deploy.sh echo 开始部署二手商品网站 # 1. 激活虚拟环境 source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt 2/dev/null || pip install Django3.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(usernameadmin).exists() or User.objects.create_superuser(admin, adminexample.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该脚本将重复性操作封装避免学生在课程设计答辩前手忙脚乱配置环境。真正上线时只需替换runserver为gunicorn启动命令并配置 systemd 服务即可。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门