简介:基于Python、Django与SQLite实现的一款校园二手交易跳蚤市场网站毕业设计源码,面向计算机相关专业学生,可应用于毕业设计、课程设计或Web开发进阶练习。资源包内共571个文件,涵盖py源码、pyc编译文件、jpg/png图片素材、html页面、css/js静态资源,以及sqlite3数据库文件和依赖清单,压缩后约22.97MB,整体结构清晰。目前已有141人学习下载。项目前端采用HTML/CSS/JS配合Django模板引擎渲染,后台基于SQLite存储数据,适合初学者从零理解前后端交互。通过该项目可以体验用户注册登录、商品发布、浏览搜索与下单购买等完整电商流程,理解Django的MTV架构、URL路由、模板渲染、ORM与SQLite数据交互,还能学习settings.py配置、应用模块划分和项目部署等实用技能,是一份可以直接运行、便于拆解和二次开发的毕业设计参考资料。
1. 用 Python + Django + SQLite 搭建校园跳蚤市场,先把架构顾虑想在前头
一个反直觉的结论:校园二手交易站最难的环节不是注册登录,也不是商品卡片做得多漂亮,而是“同一件商品被重复下单”时的状态收敛。卖家在线下已经谈好买家,系统里商品却仍然挂在“在售”一列,最后重复订单由谁来负责?这个标题给出的技术组合,恰好用最低的维护成本去处理这个矛盾:Django 提供用户认证、Admin 后台和数据库 ORM,SQLite 把整库收敛到一个文件,Python 负责全部业务胶水。源码能在本地一键跑起来,不需要申请 MySQL 账号,也不依赖独立的数据库服务器。下面按一份可落地的方案把模型、视图、订单和部署依次拆开,适合正在做 Web 方向毕业设计的学生,也适合想快速验证低并发交易网站原型的开发者。
2. 模型设计先行:围绕交易状态建表,把 Order 的约束当核心资产
跳蚤市场的功能清单不长:用户、商品、订单、留言。毕业设计中常见的失控点是“先画一堆页面再回头补模型”,结果页面改版时模型不断加字段,迁移脚本越堆越乱。模型层应该从交易规则反推:先定义清楚“什么能卖、什么算成交”,再去写页面。
2.1 用户模型:用 Django 内置 User 扩展 Profile,而不是重建用户表
Django auth 模块自带 User,包含密码哈希、会话、权限和登录装饰器。重建用户表意味着这些能力全部要重写,最容易埋雷的是密码散列和会话处理。正确做法是用 OneToOneField 扩展一张 Profile 表。
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="用户") student_no = models.CharField("学号", max_length=20, unique=True) phone = models.CharField("联系电话", max_length=11) campus = models.CharField("校区", max_length=20, blank=True) def __str__(self): return f"{self.user.username} 的校园档案"这段代码里最值得关注的是unique=True,它让学号成为校园场景下的自然键,数据库层面堵住了同一个学号注册多个账号的情况。on_delete=models.CASCADE表示内置 User 被删除时 Profile 会同步删除。注意一个容易忽略的点:如果你希望用户注销后商品仍然在售,需要把 Goods.owner 的外键约束改成SET_NULL并允许为空,而不是依赖默认的级联删除。“django执行查询-删除对象”是常见搜索词,这里的on_delete参数正是在删除查询时控制级联行为的核心位置。
2.2 商品表设计:价格用 DecimalField,状态用可读字符串
商品是系统信息核心,字段不建议直接照搬电商网站那一套。校园二手讲究轻量,一张表能表达在售、预占、已售三种状态就够用。
GOODS_STATUS = ( ("0", "在售"), ("1", "预占"), ("2", "已售"), ) class Goods(models.Model): title = models.CharField("商品标题", max_length=50) price = models.DecimalField("价格", max_digits=7, decimal_places=2) description = models.TextField("商品描述", blank=True) image = models.ImageField("图片", upload_to="goods/%Y%m%d/", blank=True) owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="发布者") status = models.CharField("状态", max_length=1, choices=GOODS_STATUS, default="0") created_at = models.DateTimeField("发布时间", auto_now_add=True) class Meta: ordering = ["-created_at"]DecimalField比 FloatField 安全,它不会出现 0.1+0.2=0.30000000000000004 这类浮点误差;max_digits=7, decimal_places=2表示最高支持 99999.99 元,对二手商品足够。状态字段用单字符的“0/1/2”而不是整数,是因为数据库里直观易读,配合 choices 还能让 Django Admin 下拉框显示中文。这里用“预占”而不是“锁定”,贴合线下当面交易的校园习惯:先到先得,但不立即成交。
2.3 订单表:OneToOne 是数据库层的最后一道保险
class Order(models.Model): goods = models.OneToOneField(Goods, on_delete=models.CASCADE, verbose_name="商品") buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name="buy_orders", verbose_name="买家") price = models.DecimalField("成交价", max_digits=7, decimal_places=2) created_at = models.DateTimeField("下单时间", auto_now_add=True)OneToOneField 在数据库层生成唯一约束,同一件商品永远只能有一条订单记录。price字段保存的是下单那一刻的商品价格快照。发布者后来从 500 元改成 300 元,已生成的订单仍保留 500 元,避免产生“到底按哪个价成交”的纠纷,这是二手交易里比商品上下架更关键的规则。
2.4 SQLite 与 MySQL 的取舍:这份源码在什么场景下可以直接用
| 对比项 | SQLite | MySQL |
|---|---|---|
| 部署方式 | 文件即数据库,零配置 | 需要安装并常驻服务 |
| 并发写入 | 整库串行,写锁全局 | InnoDB 行级锁 |
| 数据备份 | 直接复制 .sqlite3 文件 | mysqldump 导出 |
| 迁移成本 | 结构简单,自由度高 | 支持更丰富的 ALTER |
在校园低并发场景,SQLite 的全局写锁反而是优点:它让“两个请求同时更改状态”退化成串行处理,配合应用层状态判断,不会出现典型的行锁死锁问题。真正需要换 MySQL 的信号是多台机器同时读写、或写入并发超过每秒几十次。Django ORM 在这一层已经做好了解耦,App 内的代码基本不用动。
3. 从源码到跑通:项目初始化、App 划分与数据库迁移
拿到一份 Django 毕业设计源码,最先要看的是项目结构而不是页面效果。结构里能看到 App 划分、配置文件的位置、以及数据库文件如何摆放。自己从头搭建时,顺序如下。
3.1 环境准备:python 安装注意 PATH,虚拟环境隔离依赖
python --version python -m venv venv # Windows 环境激活 venv\Scripts\activate # macOS/Linux 环境激活 source venv/bin/activate pip install django pillowpython 安装完成后最常见的失败原因是 PATH 没有配置好,命令行能正常输出版本号是成功启动的前提。venv 用于隔离项目依赖,避免和系统里其他 Python 项目的包互相干扰。django 是 Web 框架,pillow 是图片处理库,没有它 ImageField 无法处理上传的图片。
3.2 用 django-admin 创建项目,划分 goods、users、orders 三个 App
django-admin startproject jumpmarket cd jumpmarket python manage.py startapp goods python manage.py startapp users python manage.py startapp orders三个 App 按职责边界切分:用户、商品、订单是相对独立的业务域,后续新增留言、收藏功能时,自然落在 goods 或 orders 中,不需要大量跨模块引用。
| App | 负责内容 | 核心模型 |
|---|---|---|
| users | 注册登录、校园档案 | Profile |
| goods | 发布、列表、搜索、详情 | Goods |
| orders | 下单、交易状态 | Order |
3.3 settings.py 注册 App,并确认 SQLite 引擎配置
INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "users", "goods", "orders", ] DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } }新建 Django 项目时默认引擎就是 sqlite3,不用刻意修改。BASE_DIR / "db.sqlite3"是 pathlib 写法,在 Windows 和 Linux 下都会生成正确的分隔符,不建议改成手写字符串路径,否则换系统后容易报找不到数据库文件。
3.4 迁移与验证:makemigrations、migrate 配合 DB Browser 查表
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser三条命令分别完成:根据模型生成迁移文件、把迁移写入 db.sqlite3、创建管理员账号。迁移完成后用 DB Browser for SQLite 或 SQLiteStudio 打开 db.sqlite3,检查auth_user、goods_goods、orders_order三张核心表是否生成。源码运行期间如果频繁报 no such table,绝大多数是迁移没有执行,不要直接复制别人的 db 文件来替代。
3.5 Admin 后台注册,省去写临时管理页面的力气
# goods/admin.py from django.contrib import admin from .models import Goods, Order @admin.register(Goods) class GoodsAdmin(admin.ModelAdmin): list_display = ("title", "price", "owner", "status", "created_at") list_filter = ("status",) search_fields = ("title", "owner__username")注册之后,/admin 地址就能直接看到商品列表,标记已售、修改价格、按状态过滤全部可用。对毕业设计来说,后台就是最可靠的演示兜底入口,前端有问题时后台永远能完成操作。
4. 核心页面跑起来:发布、搜索、详情与分页的 Django 写法
从源码视角看,view 层是最常被修改的部分。关于二手市场的功能入口,最少需要注册、登录、发布、列表、搜索、详情、下单七个视图。下面按列表页最常见的写法逐步展开。
4.1 发布商品视图:login_required 与 commit=False
from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render from django.contrib import messages from .forms import GoodsForm from .models import Goods @login_required(login_url="/users/login/") def publish(request): if request.method == "POST": form = GoodsForm(request.POST, request.FILES) if form.is_valid(): goods = form.save(commit=False) goods.owner = request.user # 从 session 中取当前用户 goods.save() messages.success(request, "商品发布成功") return redirect("goods:detail", pk=goods.pk) else: form = GoodsForm() return render(request, "goods/publish.html", {"form": form})login_required拦截未登录用户,login_url指定未登录时跳转的地址。request.FILES里装的是上传的图片文件,而request.POST只有文本。commit=False让 ORM 先创建对象但不落库,手动补上表单里没有的 owner 再保存;如果直接form.save()会触发 NOT NULL 约束错误。messages.success在下一个页面顶部展示提示,避免用户发布后不知道是否成功。
4.2 列表页、模糊搜索与 Paginator 分页
from django.core.paginator import Paginator from django.db.models import Q from django.shortcuts import render from .models import Goods def index(request): q = request.GET.get("q", "").strip() goods_list = Goods.objects.filter(status="0") if q: goods_list = goods_list.filter( Q(title__icontains=q) | Q(description__icontains=q) ) paginator = Paginator(goods_list, 12) page_obj = paginator.get_page(request.GET.get("page")) return render(request, "goods/index.html", {"page_obj": page_obj, "q": q})Q对象把标题和描述两个条件组合成一条 OR 表达式,SQLite 中icontains会生成LIKE '%关键词%',中文搜索同样适用。分页参数设为 12,刚好对齐三行四列的商品卡片布局。get_page对非法页码有容错,比如?page=abc不会直接返回 500。
4.3 图片上传路径与 URL 的 reverse 匹配
# settings.py MEDIA_URL = "/media/" MEDIA_ROOT = 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)图片文件写入media/goods/日期目录,MEDIA_ROOT是磁盘实际路径,MEDIA_URL是浏览器访问 URL。模板里通过reverse("goods:detail", kwargs={"pk": goods.pk})反查/goods/1/,比硬编码地址更适合后续调整路由。核心 URL 对应如下:
| 视图 | URL 名称 | 说明 |
|---|---|---|
| index | goods:index | 在售商品列表 |
| publish | goods:publish | 发布商品 |
| detail | goods:detail | 商品详情 |
| buy | goods:buy | 发起交易 |
4.4 一种值得学习的细节:删除对象之前先查外键
“django执行查询-删除对象”是毕设答辩里经常被追问的环节。商品执行 delete 时,如果存在关联订单,Django 默认会按外键约束处理。稳妥做法是先通过Goods.objects.filter(pk=pk).annotate(order_count=Count("order"))查出是否存在订单,再决定进入删除流程还是修改状态为已售。校园交易里,直接把有订单的商品物理删除,会让买家的记录无法追溯。
5. 交易闭环:状态机、事务与并发保护
交易逻辑是整个项目的核心,也是不容易看懂的“源码价值”所在。以下从三态模型开始逐步展开。
5.1 三态状态机:在售、预占、已售
二手交易没有购物车,也很少需要完整退款链路,三态模型足够覆盖教学场景下的业务闭环:在售(0)→ 预占(1)→ 已售(2),其中预占可以回到在售。
| 当前状态 | 触发动作 | 结果状态 |
|---|---|---|
| 0 在售 | 买家下单 | 1 预占 |
| 1 预占 | 买家取消 | 0 在售 |
| 1 预占 | 卖家确认成交 | 2 已售 |
这个流转表比布尔字段 is_sold 多了一个中间态,价值在演示时相当明显:能够展示“已被预订,联系中”,而不是让商品凭空消失。
5.2 先改状态再建订单:避免重复下单的原子写法
from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render from django.contrib.auth.decorators import login_required from .models import Goods, Order @login_required @transaction.atomic def buy(request, pk): goods = get_object_or_404(Goods, pk=pk) if goods.status != "0": return render(request, "goods/status_error.html", {"goods": goods}) updated = Goods.objects.filter(pk=pk, status="0").update(status="1") if not updated: return render(request, "goods/status_error.html", {"goods": goods}) Order.objects.create(goods=goods, buyer=request.user, price=goods.price) return redirect("goods:detail", pk=pk)很多初版源码里常见的写法是“先读状态、改 status、save() 再建订单”。一旦两个请求并发进入,两个进程都可能读到 status=0,随后都执行 save(),订单就建了两条。这里的关键是filter(pk=pk, status="0").update(...):过滤出满足条件的行,再把状态原地更新。如果有并发请求先执行了 update,后一个请求匹配到 0 行返回空,事务回滚,下单被拒绝。订单表的 OneToOne 约束作为第二层保险,即使视图层有疏漏,数据库唯一索引也会拦下一次重复创建。
5.3 SQLite 场景下的锁:直接依赖整库串行写
Django 对 SQLite 的select_for_update()不会产生真正意义上的行级锁,因为 SQLite 本身用的是数据库级写锁,并且不支持 SELECT FOR UPDATE 语法。常见做法是将状态变更放到一个filter().update()的原子语句里,而不是依赖数据库锁。并发不高时,SQLite 的全局串行写会把两个事务排成先后顺序,反而让状态一致性的判断代码更简单。
5.4 留言与求购:一个外键就够用
class Inquiry(models.Model): goods = models.ForeignKey(Goods, on_delete=models.CASCADE, related_name="inquiries") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="留言者") text = models.TextField("留言内容") created_at = models.DateTimeField("留言时间", auto_now_add=True)学生之间问“还在吗”、砍价、约定见面地点,用这一张小表就能实现。外键指向 Goods,related_name="inquiries"让“取某个商品的全部留言”变成goods.inquiries.all()。删除查询时,on_delete=models.CASCADE让商品一旦删除,所有留言一起清理,不会在数据库里留下孤儿记录。
6. 答辩前特意收紧这几处验证,避免演示现场翻车
毕业设计最怕的是演示时临时改代码。以下三件事建议提前调整完,整体风险会明显下降。
6.1 用 Django TestCase 锁住“不能重复下单”
在 orders 目录下创建 tests.py:
from django.test import TestCase from django.contrib.auth.models import User from django.db import IntegrityError from goods.models import Goods, Order class OrderFlowTest(TestCase): def test_second_order_rejected(self): seller = User.objects.create_user("seller", password="123456") buyer = User.objects.create_user("buyer", password="123456") goods = Goods.objects.create(title="山地车", price=200, owner=seller) goods.status = "1" goods.save() with self.assertRaises(IntegrityError): Order.objects.create(goods=goods, buyer=buyer, price=200)订单表的 OneToOne 约束会让第二次创建抛IntegrityError,这个测试直接验证了数据库层的防重复能力。答辩现场执行python manage.py test orders,一两秒就能给出结论。
6.2 用 fixture 准备演示数据,而不是现场手敲
python manage.py dumpdata goods.Goods --indent 2 > goods/fixtures/demo.json python manage.py loaddata demo.jsondumpdata 把商品表导出成 JSON fixture,loaddata 在任意一台新电脑上都能还原演示数据。演示前先跑 migrate 再 loaddata,SQLite 单文件也可以直接从开发机拷贝,但注意拷贝前先停掉 runserver,Windows 下文件被进程占用时复制会失败。
6.3 Django Admin 的列表编辑,作为现场兜底
Admin 界面美化常用三项:list_editable允许列表页直接改价格,list_display加图片缩略图,list_filter按状态过滤。前端某个链接万一失效,直接从后台打开商品列表改状态,比现场去查路由快得多。使用 Django 自带的后台不需要额外补登录逻辑,也省去写一套管理端页面的时间。
迁移结束后记得把 db.sqlite3 和 media 目录一起准备好,评审老师的电脑上跑完 migrate 就能复现这套 Python + Django + SQLite 的校园跳蚤市场,这也是源码案例包里最标准的交付形态。
本文还有配套的精品资源,点击获取