☰
Django+MySQL校园二手交易网站开发实战:从选型到部署
2026/10/11 12:57:22 网站建设 项目流程

简介:面向毕业设计和Python Web学习者,基于Python+Django+MySQL的校园二手交易跳蚤市场完整源码案例,覆盖用户、商品、购物车、订单交易、安全防护等核心模块,可直接部署运行或作为课程设计参考。压缩包共494个文件,整体41.29MB,包含50个py源码文件、124个pyc编译文件、24个html页面模板、11个js和7个css前端文件、2个sql数据库脚本,以及大量jpg/png图片素材,源码、模板、静态资源与数据库脚本分层清晰。已有94人学习浏览,项目展示了从数据库建模、ORM查询到前端页面渲染的完整开发流程,帮助理解Django框架的MTV架构、MySQL数据交互以及前后端协作方式。通过阅读和改造代码,可快速上手毕业设计答辩准备,学习用户认证、商品管理、购物车结算等典型业务逻辑,也能参考其中的安全处理与目录结构设计,提升实际项目开发能力。

1. 校园二手交易网站:为什么这个题目值得做成 Django 项目

每年毕业季,某高校宿舍楼下的公告栏都会被「出售考研资料」「九成新自行车」这类纸条贴满。与其说这是校园传统,不如说是一个真实存在的信息匹配需求:买家想低价拿到学长学姐的闲置物品,卖家想腾空宿舍。把这个场景搬到线上,做一个基于 Python + Django + MySQL 的校园二手交易跳蚤市场网站,就成了计算机专业毕业设计里最常被选中的题目之一——技术栈经典、需求清晰、功能边界明确,做完能跑通,讲起来也有东西可讲。

这个项目解决的不只是「发帖卖东西」这一件事。它要覆盖用户注册登录、商品发布与分类浏览、关键词搜索、下单与订单状态管理、站内留言这几个核心闭环。对新手来说,Django 自带的 Admin 后台和 ORM 能省掉一大半重复劳动;对想冲高分的同学来说,订单状态机、图片上传、分页搜索、部署上线这些点都是可以写进论文和答辩 PPT 的亮点。这篇文章我按自己做过类似项目的习惯,把从选型到落地、从参数设置到排错思路完整拆给你。

2. Django + MySQL 技术选型:先搞清楚为什么是这三件套

2.1 为什么选 Django 而不是 Flask 或 Spring Boot

做校园二手交易网站这种典型的 CRUD 加少量业务状态流转的项目,Django 是性价比最高的选择。它自带 Admin 后台,商品分类、用户管理、订单查看这些功能不用写一行前端代码就能在后台操作;自带认证体系,用户注册、登录、会话管理直接基于内置的django.contrib.auth扩展,省去自己写密码加密和 session 存储的麻烦;ORM 让你操作 MySQL 时写 Python 对象而不是拼 SQL 字符串,大幅降低 SQL 注入风险。

对比 Flask,Flask 胜在轻量灵活,但用户认证、Admin 后台、表单校验这些都得自己集成第三方库,做一个完整交易站点的工作量会明显变大。对比 Spring Boot,Java 技术栈在校园项目里往往意味着更重的配置和更长的编译时间,而 Django 的开发调试循环短,改完代码重启服务就能看到效果。如果这个项目是你的毕设,时间通常只有几个月,Django 能让你把精力留给业务逻辑而不是框架配置。

2.2 项目结构长什么样:一个可复用的目录骨架

我通常会把 Django 项目拆成多个应用(app),每个应用只负责一块业务。下面这个结构是我做同类项目常用的布局,你拿到源码包后可以对照着看:

secondhand_market/ ├── manage.py ├── requirements.txt ├── market/ # 主配置文件目录 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ # 用户模块:注册/登录/个人信息 │ ├── goods/ # 商品模块:发布/列表/详情/搜索 │ ├── orders/ # 订单模块:下单/确认/取消 │ └── messages/ # 留言模块:站内私信 ├── static/ # 前端静态文件 ├── media/ # 用户上传图片目录 └── templates/ # HTML 模板

把不同业务放进独立的 app,核心好处是迁移(migration)互不干扰。比如商品模块要加一个「是否加急」字段,只需要生成goods应用的迁移文件,不会牵连订单表结构。另外一个容易被忽略的点是static和media目录必须分开:static放你自己写的 CSS/JS 和 Django 自带的静态资源,media放用户上传的商品图片,两者混在一起会导致部署时静态文件收集出错。

2.3 数据模型设计:二手交易的核心表就这五张

设计数据库表时不要一上来就追求大而全,二手交易平台最核心的实体只有用户、商品、订单、留言和分类。下面是商品表的模型定义,我把字段注释写在代码里,方便你对照理解:

# apps/goods/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=32, unique=True, verbose_name="分类名") sort_order = models.IntegerField(default=0, verbose_name="排序权重") class Meta: ordering = ["sort_order", "id"] class Goods(models.Model): STATUS_CHOICES = [ ("on_sale", "在售"), ("sold", "已售出"), ("off_shelf", "已下架"), ] title = models.CharField(max_length=64, verbose_name="商品标题") desc = models.TextField(max_length=1024, verbose_name="商品描述") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格") original_price = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="原价" ) category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name="goods", verbose_name="分类" ) owner = models.ForeignKey( User, on_delete=models.CASCADE, related_name="goods", verbose_name="发布者" ) status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="on_sale") view_count = models.IntegerField(default=0, verbose_name="浏览次数") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.title

这里有两个参数值得单独说明。price字段用DecimalField而不是FloatField,是因为浮点数在 MySQL 里存储金额会出现0.1 + 0.2 != 0.3的精度问题,虽然二手交易不涉及银行级精度,但答辩时被问到「为什么用 Decimal」答不上来会很尴尬。category外键的on_delete=models.PROTECT是我刻意选的:如果某个分类下还有商品,删除分类会被数据库拒绝,避免「分类删了,商品指向空分类」这种脏数据。相比之下owner用CASCADE是合理的——用户注销时他的商品一并删除,符合校园二手平台的使用预期。

3. 核心功能怎么落地:注册、发布、搜索和订单流转

3.1 用户注册与登录:站在 Django 内置 Auth 的肩膀上

用户模块不需要自己写密码加密和 session 逻辑,Django 的django.contrib.auth已经把这些做完了。但二手交易平台需要额外的用户信息,比如学号、宿舍楼栋、联系方式,所以常见的做法是新建一个 Profile 模型做一对一扩展:

# apps/users/models.py from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") student_id = models.CharField(max_length=20, blank=True, verbose_name="学号") dorm_building = models.CharField(max_length=32, blank=True, verbose_name="宿舍楼栋") contact = models.CharField(max_length=32, blank=True, verbose_name="联系方式") def __str__(self): return self.user.username

注册的视图逻辑我会写在views.py里,用 Django 的UserCreationForm做基础校验,再手动补一个 Profile 的保存:

# apps/users/views.py from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render, redirect from django.contrib.auth import login from .models import Profile def register(request): if request.method == "POST": form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() # 自动处理密码哈希 Profile.objects.create(user=user) login(request, user) # 注册后直接登录,减少一次跳转 return redirect("goods:list") else: form = UserCreationForm() return render(request, "users/register.html", {"form": form})

这里有个值得注意的细节:login(request, user)这行代码是注册后体验的关键。如果不调用它,用户注册完还要再去登录页输一遍账号密码,校园用户本来就没有耐心,多一步跳转就可能流失。另外,Profile.objects.create(user=user)理论上应该在user.save()之后,因为Profile的外键需要主键值,但 Django 的form.save()是原子操作,可以直接拿到带主键的user对象,所以顺序没问题。

3.2 商品发布:用 ModelForm 做校验,省掉一半表单处理代码

商品发布页是用户最常用的功能,包含标题、描述、价格、分类和图片上传。直接手写表单校验很容易漏掉「价格必须大于 0」「图片大小不能超过 2MB」这类边界条件,所以我习惯用 ModelForm:

# apps/goods/forms.py from django import forms from .models import Goods class GoodsForm(forms.ModelForm): class Meta: model = Goods fields = ["title", "desc", "price", "original_price", "category", "image"] widgets = { "desc": forms.Textarea(attrs={"rows": 4, "placeholder": "描述一下物品的新旧程度、入手渠道..."}), } def clean_price(self): price = self.cleaned_data["price"] if price <= 0: raise forms.ValidationError("价格必须大于 0") return price def clean_image(self): image = self.cleaned_data.get("image") if image and image.size > 2 * 1024 * 1024: raise forms.ValidationError("图片大小不能超过 2MB") return image

ModelForm 的好处在于它自动从 Goods 模型读取字段定义,你不用重复声明每个字段的类型和校验规则。clean_price和clean_image是 Django 钩子方法,分别对应「验证价格非负」和「验证图片大小」,只要方法名以clean_加字段名命名,Django 就会在表单提交时自动调用。图片大小限制是必须做的,否则用户传一张 20MB 的照片进来,Django 处理上传时会拖垮服务器,后面静态文件目录也会被塞满。

3.3 搜索与分页:Q 对象做多字段查询,Paginator 控制每页条数

校园二手平台的搜索场景很典型:用户想找「考研数学」相关的书,但标题里写的是「数学一复习全书 2026 版」,只搜标题容易漏。所以我在搜索视图里同时匹配标题和描述字段:

# apps/goods/views.py from django.views.generic import ListView from django.db.models import Q from .models import Goods class GoodsListView(ListView): model = Goods template_name = "goods/list.html" context_object_name = "goods_list" paginate_by = 12 # 每页 12 件商品 def get_queryset(self): qs = Goods.objects.filter(status="on_sale") keyword = self.request.GET.get("keyword", "").strip() if keyword: qs = qs.filter( Q(title__icontains=keyword) | Q(desc__icontains=keyword) ) category_id = self.request.GET.get("category", "") if category_id.isdigit(): qs = qs.filter(category_id=int(category_id)) return qs.select_related("category", "owner")

paginate_by = 12这个参数是根据校园用户浏览习惯定的:手机端一屏大约显示 4 到 6 个商品卡片,12 条正好是两到三屏的量,翻页频率适中。select_related("category", "owner")是性能优化的关键,它会在 SQL 层面用 JOIN 一次性把商品对应的分类和用户信息查出来,避免每渲染一个商品就多执行两次查询。如果你的商品列表页有 N 条商品,不加这行就是 1 + N 条 SQL,加了就只有 1 条,这个差异在数据量到几百条时就能明显感知到。

3.4 订单状态流转:把交易状态写成状态机,而不是随便改字段

订单模块最容易翻车的地方是状态管理。如果只用一个status字段随改随存,会出现「已取消的订单还能确认收货」「卖家还没发货买家就能点击完成」这种逻辑漏洞。我的做法是在模型中定义状态流转的合法性,用常量加校验方法控制:

# apps/orders/models.py from django.db import models from django.core.exceptions import ValidationError class Order(models.Model): STATUS_FLOW = { "pending": ["paid", "cancelled"], # 待付款 -> 已付款 / 已取消 "paid": ["shipped", "cancelled"], # 已付款 -> 已发货 / 申请取消 "shipped": ["completed"], # 已发货 -> 已完成 "completed": [], # 终态 "cancelled": [], # 终态 } goods = models.ForeignKey("goods.Goods", on_delete=models.PROTECT) buyer = models.ForeignKey("auth.User", on_delete=models.PROTECT) status = models.CharField(max_length=16, default="pending") created_at = models.DateTimeField(auto_now_add=True) def transition_to(self, new_status): if new_status not in self.STATUS_FLOW.get(self.status, []): raise ValidationError(f"非法状态流转: {self.status} -> {new_status}") self.status = new_status self.save()

STATUS_FLOW字典定义了一张状态转移表,这是从有限状态机理论落到代码的最简实现。transition_to方法在每次状态变更时检查合法性,非法流转直接抛异常。它的好处不只是逻辑严谨,答辩时你还能顺势讲出「状态机避免了逻辑散落各处」的设计思路,这比「我在视图里判断了一下」要有说服力得多。

4. 把 MySQL 接进 Django:配置、驱动和初始化数据

4.1 settings 配置:五个必填参数和两个容易忽视的选项

Django 连接 MySQL 的配置集中在settings.py的DATABASES字典里,如果你拿到的是sqlite3的演示版本,迁移到 MySQL 的第一步就是改这里:

# market/settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "secondhand_market", # 数据库名,需提前在 MySQL 中创建 "USER": "root", # 数据库账号 "PASSWORD": "your_password", # 数据库密码 "HOST": "127.0.0.1", # MySQL 主机地址 "PORT": "3306", # 默认端口 "OPTIONS": { "charset": "utf8mb4", # 中文与 emoji 全支持 "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", }, } }

这里有两处是我反复强调的。第一,charset必须写成utf8mb4而不是utf8,因为 MySQL 的 utf8 实际上是 utf8mb3,不支持 emoji 和部分生僻字,用户发布「📚 九成新教材」这种标题时写入数据库会报错。第二,sql_mode='STRICT_TRANS_TABLES'让 MySQL 在写入超长字符串或非法日期时直接报错而不是静默截断,这能帮你早点发现数据问题,而不是等数据烂了再排查。

4.2 PyMySQL 驱动与__init__.py补丁

Django 本身不自带 MySQL 驱动,常见做法是安装 PyMySQL,然后在项目主配置目录的__init__.py里注册它:

pip install pymysql
# market/__init__.py import pymysql pymysql.install_as_MySQLdb()

这行补丁的意义在于,Django 默认import MySQLdb,而 PyMySQL 提供了完全兼容的模块,install_as_MySQLdb()会让 Django 把 PyMySQL 当作 MySQLdb 使用。另一个可选方案是安装mysqlclient驱动,它的性能略好但需要系统编译环境,Windows 上经常因为缺 VC 编译器安装失败。我的建议是:本地开发和毕设演示用 PyMySQL 足够,别在环境上浪费太多时间。

4.3 迁移、初始数据与常见导入错误

数据库连接配置完成后,依次执行下面三条命令:

python manage.py makemigrations # 生成迁移文件 python manage.py migrate # 把迁移文件应用到数据库 python manage.py loaddata initial_data.json # 导入初始分类数据

makemigrations会扫描所有应用下的 models 变更生成迁移文件,只生成不执行;migrate才是真正建表。新手最容易犯的错是改完模型直接跑migrate而跳过makemigrations,导致 Django 报No changes detected。初始分类数据我一般用dumpdata生成 JSON 文件放进fixtures目录,这样每次重建库都能一键恢复分类、管理员账号等基础数据,不用手动重建。

5. 避坑指南:Django 二手交易项目常见的五个翻车现场

5.1 图片上传后前端无法显示,页面一片空白

现象:商品发布成功,后台能看到图片文件,但前端img标签的地址是 404。

原因:media目录的 URL 没有映射到 Django 路由。Django 默认只处理static目录,media是用户上传文件,需要手动加一条路由,且DEBUG = False时 Django 根本不处理媒体文件服务。

解决:在urls.py中添加:

from django.conf import settings from django.conf.urls.static import static urlpatterns = [...] # 你的其他 URL if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

同时检查settings.py里有没有MEDIA_URL = "/media/"和MEDIA_ROOT = BASE_DIR / "media"这两行。部署环境的图片服务应交给 Nginx 之类的 Web 服务器处理,不要依赖 Django。

5.2 数据库迁移时报IndexError: string index out of range

现象:执行python manage.py migrate时,某一步突然抛出这个异常,表建了一半。

原因:常见于项目从 SQLite 切换 MySQL 后产生的迁移依赖混乱,特别是历史迁移文件引用了旧数据库的字段类型或不存在的 app 标签。

解决:如果数据不重要,最省事的办法是删掉所有 app 下的migrations目录中除__init__.py外的文件,连同数据库一起重建:

python manage.py makemigrations --empty users python manage.py makemigrations python manage.py migrate --run-syncdb

如果已有用户数据不想丢,则要逐个检查迁移文件中的依赖项,找到报错的哪一行做修正。说句实在话,这种问题卡太久就直接重建库,毕设项目的数据量没到值得花两天去修迁移历史的程度。

5.3 注册时用户名冲突导致 500 错误

现象:注册一个已存在的用户名,页面报 500,而不是提示「用户名已注册」。

原因:UserCreationForm的is_valid()里会检查用户名唯一性并生成错误信息,但你没有把错误信息渲染到模板,或者视图写成了if request.method == "POST": user = form.save()而不检查is_valid()。

解决:注册视图必须先调用form.is_valid()再取数据;模板里用{{ form.errors }}输出错误。另外一个容易踩的是大小写问题——Django 默认用户名不区分大小写,Admin和admin视为同一个,前端提示信息要写清楚,否则用户会困惑为什么换了大小写还是提示已占用。

5.4 搜索关键字是空字符串时返回空列表

现象:搜索页默认进入时没有传keyword,结果商品列表为空,正常应该显示全部商品。

原因:get_queryset里写了if keyword:的判断,但视图先执行了filter(status="on_sale"),空字符串关键字不会进入过滤分支,问题一般出在模板的表单里给搜索框设置了name="keyword"以外的名字,或者前端把空值也提交成了一个空格字符。

解决:在视图里统一做keyword = self.request.GET.get("keyword", "").strip(),先.strip()掉首尾空格再判断。同时在模板表单用method="get"保证搜索参数出现在 URL 里,这样用户刷新页面不会丢失搜索结果,也能直接复制 URL 分享给别人。

5.5 下架商品后商品详情页仍可通过直达链接访问

现象:卖家把商品下架了,但持有旧链接的人仍能打开详情页并下单。

原因:详情页视图只按主键查了商品,没有校验状态。

解决:商品详情视图的查询条件加上status="on_sale",或者至少对非在售状态做区分处理:

# apps/goods/views.py from django.shortcuts import get_object_or_404 from .models import Goods def detail(request, pk): goods = get_object_or_404(Goods, pk=pk, status="on_sale") # 如果商品已下架,直接返回 404 而不是渲染详情页 return render(request, "goods/detail.html", {"goods": goods})

同理,add to order的操作也要重复检查商品状态,防止「详情页看不到,但通过直接构造 POST 请求下单」的漏洞。政务类系统的血泪经验告诉我们,前端隐藏入口不等于后端安全。

6. 最后一公里:让项目从「能跑」变成「拿得出手」

如果只追求功能跑通,项目到这里已经完成了。但想在答辩或实际使用时不翻车,我还会做三件事。第一,给搜索接口加缓存——校园二手平台的访问曲线很集中,午休和晚间是高峰,用 Django 自带的cache_page装饰器缓存搜索结果页 60 秒,能显著降低 MySQL 的压力:

from django.views.decorators.cache import cache_page urlpatterns = [ path("goods/", cache_page(60)(GoodsListView.as_view()), name="goods-list"), ]

第二,给关键接口补充单元测试,至少覆盖注册、发布、下单三条主流程。Django 的TestCase跑起来很快,十几秒就能验证改造没有破坏核心逻辑,这比在浏览器里手动点半天要高效得多。第三,部署前把DEBUG关掉、把SECRET_KEY改成环境变量、用whitenoise处理静态文件——这三件事不做完,项目一放到公网就会在安全性和资源加载上出问题。

我记得有一次给模拟项目X 做演示环境部署,就是因为SECRET_KEY写死在代码仓库里,被扫描工具拉出了风险告警,虽然只是校园内网项目,但也够让人冒冷汗的。所以你现在花半小时把这些配置从代码里拆出去,省的是将来的补救时间。做这类项目,最重要的不是炫技,而是把状态流转、查询性能、数据安全这些基础点做扎实。

希望帮到你。

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

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

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

立即咨询