ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django校园二手交易系统开发:从CRUD到订单状态机的完整实践

Django校园二手交易系统开发:从CRUD到订单状态机的完整实践 简介这是一份面向计算机专业毕业设计的校园二手交易网站完整案例技术栈为Python、Django与MySQL。项目瞄准在校生买卖闲置物品的场景覆盖用户注册登录、商品发布与分类、购物车、订单处理、交易状态跟踪、评价等模块并考虑常见的网络安全防护有助于理解Django的架构和关系型数据库设计。压缩包共494个文件整体大小41.29MB其中50个源码文件为后端逻辑主体124个编译缓存文件、222张页面截图与37张图标素材辅助理解其余为前端模板、样式脚本、数据库脚本和说明文档便于对照界面与代码学习。附带数据库脚本可快速还原表结构目录按模块划分便于二次开发。该案例已有94人学习适合用于毕业设计的开题、系统设计、论文撰写与答辩演示也可作为快速搭建校园商城类项目的参考底座。1. 校园二手交易网站的完整闭环不是功能堆砌而是 CRUD 与订单状态机的缩影拿到这份基于 Python Django MySQL 的校园二手交易跳蚤市场毕业设计源码时我的第一反应是这类项目网上太多了但真正能跑通、能答辩、能往简历上写的其实没几个。这份源码的价值不在于功能多而在于它把「发布商品 → 浏览搜索 → 下单购买 → 后台管理」这条主链路完整地实现了同时兼顾了用户认证、会话保持、图片上传、分页搜索这些 Web 开发绕不开的基础操作。对正在做毕业设计、或者想用 Django 练手完整项目的读者来说它是一份可以直接照着复现的骨架——重点是坑都已经被踩平了。2. 把需求拆成 Django 数据模型User 扩展、商品、订单、收藏的四表设计与迁移2.1 项目根目录与 Django 项目配置settings.py 里需要改的最少配置拿到压缩包后先别急着python manage.py runserver。第一步是看清目录结构。常见的 Django 项目布局是一个外层目录里有一个manage.py一个与项目同名的配置包以及若干个 app。这份源码里通常包含goods、user、order之类的 app具体名称以实际目录为准。我自己习惯先把settings.py打开检查四项内容数据库配置、INSTALLED_APPS、MEDIA_URL/MEDIA_ROOT、TEMPLATES里的模板路径。数据库部分我的做法是直接改成本地 MySQL 连接DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: second_hand, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }这段配置的要点在于NAME必须是你已经在 MySQL 里创建好的数据库名Django 不会自动建库下一节详细说。OPTIONS里指定utf8mb4是为了让商品描述里的 emoji 符号不至于写入报错这在二手交易场景里挺常见——学生会把“xx新可小刀”写成带表情的文案。USER和PASSWORD按你本机的 MySQL 账户填别直接抄源码里的默认值。HOST用127.0.0.1而不是localhost在某些 Windows 的 MySQL 8 环境下能避开 socket 连接方式不一致导致的Access denied问题。2.2 四张核心表的设计逻辑为什么订单要单独建表而不挂在商品上数据模型的决定权在手项目的高度就在手。我用文字描述这份资源里的表结构设计。最核心的是用户扩展表通常叫UserProfile它通过OneToOneField关联到 Django 自带的auth.User表再挂上手机号、学号、头像字段。这样做的理由很简单Django 自带的User表已经处理了密码哈希、权限系统、会话关联你不需要重写认证逻辑只需要扩展业务字段。商品表是主力常见字段包括标题、描述、原价、现价、图片、发布者外键、发布时间、状态字段在售/已售/下架。订单表单独建是因为一个商品的生命周期有「被加入收藏 → 被下单 → 被标记已售」多个步骤它的状态变化需要被记录而且同一件商品可能被多个用户咨询或下单实际成交只有一个如果订单字段直接挂在商品表上就没法记录“谁在什么时候下过单、最终是否成交”这类过程信息。收藏表则是经典的「多对多关系的中间表」记录用户和商品之间的关系以及收藏时间。2.3 迁移命令与 MySQL 建库字符集和时区是第一个坑数据库不是自动生成的需要先在 MySQL 里手动建库。我一般这样操作mysql -u root -p CREATE DATABASE second_hand DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;建库时的字符集决定了后面所有表的默认编码用utf8mb4而不是utf8是为了完整支持中文和特殊符号。unicode_ci排序规则对中文搜索比较友好。库建好后执行迁移python manage.py makemigrations python manage.py migrate如果makemigrations提示No changes detected先检查INSTALLED_APPS里有没有正确注册goods、order这些 app。另外一个高频问题是 MySQL 8 默认使用caching_sha2_password加密方式而 Django 的 MySQL 客户端版本太旧时连接会报错。我的处理是在 Django 项目的__init__.py里加一行pymysql.install_as_MySQLdb()前提是你已经pip install pymysql然后安装最新版mysqlclient。这一步不做第二个坑就是密码插件不兼容。执行完迁移后可以通过python manage.py shell快速验证表是否建好用from django.contrib.auth.models import User; print(User.objects.count())确认数据库通路没有断。3. 用户注册、登录与商品发布打通认证、会话、图片上传三条链路3.1 注册视图的 ModelForm 写法与密码哈希处理校园二手交易站的第一关就是注册。源码里注册功能通常用UserCreationForm做基础再扩展业务字段。这里面有一个关键的工程决策密码不能明文存。Django 自带的create_user方法会调用set_password自动做哈希所以如果你看到源码里注册视图只是先form.save()再把密码 set 进去这个逻辑就对了。一个典型的注册视图框架如下def register(request): if request.method POST: form RegisterForm(request.POST) if form.is_valid(): user form.save(commitFalse) user.set_password(form.cleaned_data[password1]) user.save() profile UserProfile.objects.create(useruser, student_idform.cleaned_data[student_id]) return redirect(login) else: form RegisterForm() return render(request, user/register.html, {form: form})这段代码里commitFalse是精髓先拿到 Django 生成但不是最终入库的用户对象然后用set_password做哈希再真正写入数据库。如果直接调form.save()UserCreationForm内部确实也会处理密码但你要是自定义了RegisterForm比如加了学号字段就必须显式处理。UserProfile.objects.create的参数要和你的扩展表字段严格对应这里如果写成student_id但模型里字段名是student_number立刻会报TypeError。3.2 登录与会话控制request.user和login_required的正确姿势登录用的是 Django 内置的authenticatelogin组合源码里会验证用户名和密码成功后把用户 ID 写进 session。这里有一个新手容易搞混的点login(request, user)并不会自动检查用户是否活跃也不会生成令牌它的职责就是建立会话关联。判断用户是否登录靠request.user.is_authenticated限制页面访问靠装饰器login_required。这段逻辑的责任边界也该说清装饰器只是在视图函数入口拦截不解决数据权限问题。也就是说如果你在商品详情页里通过 URL 参数拿到商品 ID 然后执行“删除”操作光加login_required是不够的——任何登录用户都可以删掉别人的商品。所以我建议源码里的删除逻辑必须是“判断goods.user request.user”才放行。如果你在源码里只看到了login_required而没有对象归属校验这是第一个要补的地方。3.3 商品发布表单图片上传的 MEDIA 配置与模板回显商品发布是二手交易高频操作图片上传的坑我今天也照着源码走了一遍表单模型用ImageField视图把request.FILES传进表单模板里enctypemultipart/form-data三件套缺一不可。# models.py class Goods(models.Model): title models.CharField(max_length100) price models.DecimalField(max_digits8, decimal_places2) image models.ImageField(upload_togoods/%Y/%m/%d/) owner models.ForeignKey(User, on_deletemodels.CASCADE) # settings.py MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)upload_to这边的分工是自动按年月日分目录存文件避免一个目录里塞几万张图片这是为后期扩展做的结构约定。MEDIA_ROOT是文件落盘的物理路径MEDIA_URL是访问前缀。很多同学在开发环境直接访问图片链接 404就是因为项目根urls.py里没有加static转发。正确的补法是在urlpatterns末尾追加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这一点不做商品图片永远在开发环境显示不出来上传文件也会失效。模板里商品卡片图片的回显标准写法是src{{ goods.image.url }}ImageField自带.url属性不用手动拼接MEDIA_URL前缀。4. 商品列表、搜索与详情ORM 查询优化、分页边界与模板取值4.1 列表页的 ORM 查询与分页objects.filter与Paginator的配合商城类页面的核心场景是列表页在校园二手场景下数据量虽然不大几千条但如果图省事直接Goods.objects.all()全量渲染后端每次请求都要把整张表的数据打包成 HTML 渲染出去页面上显示 500 条商品记录页面的体积就是几个 MB。源码里我用过一个列表视图不该只有一个.all()我建议你默认加order_by(-create_time)按发布时间倒序让新发布的商品优先展示。配合Paginator做分页from django.core.paginator import Paginator goods_list Goods.objects.filter(statuson_sale).order_by(-create_time) paginator Paginator(goods_list, 12) page_number request.GET.get(page) page_obj paginator.get_page(page_number)这里paginator.get_page(page_number)比paginator.page(page_number)更稳健——当 URL 里传的page参数不是一个数字或者超界时get_page会落回第一页或最后一页而page()会直接抛 404 或InvalidPage异常。模板里渲染分页按钮时你要拿page_obj.has_previous()、page_obj.previous_page_number()、page_obj.has_next()这些属性来画上一页和下一页按钮而不是写死 1 到 N。分页器的容量我从实际使用效果来看设为 12 比较稳妥桌面端一行四个商品三行正好一屏翻页时视觉节奏舒服移动端也不会因为一页太多而滑动太久。4.2 搜索功能的写法与三个边界坑搜索是二手交易的重灾区很多源码直接filter(title__containskeyword)但中文场景下你可能希望按标题和描述同时搜并且处理“搜索结果包含已售商品、空关键词、以及关键词包含空格”这三种边界情况。常见写法是用Q对象from django.db.models import Q keyword request.GET.get(keyword, ).strip() goods_qs Goods.objects.filter(statuson_sale) if keyword: goods_qs goods_qs.filter( Q(title__icontainskeyword) | Q(description__icontainskeyword) ).distinct()icontains对应的 SQL 是LIKE %keyword%不区分大小写对英文书名、课程代号这类内容友好。distinct()的作用是去重因为Q对象的OR连接在跨表关联查询时可能会产生重复行这算是我今天重新审源码时的一个经验。三个边界坑逐一说明。第一空关键词如果 URL 是/search/没有带参数get会返回NoneNone.strip()会崩所以必须.strip()之前先判断。第二空格关键词 全空白字符串在strip()后长度为零此时不应该执行模糊查询否则会命中所有商品。第三已售商品混入如果查询没有限定statuson_sale搜索出来的全是已售出的宝贝学生点进去发现买不了体验很差也会让答辩老师质疑逻辑完备性。4.3 详情页与收藏功能模板里的一对多关系取值需要注意什么详情页的数据取用就简单了。一个商品对应一个发布者商品详情页上方显示发布者的昵称、学号、头像。模板里通过{{ goods.owner.profile.student_id }}这样的链式取值就能拿到。这里有个理解上的要点goods.owner是 User 对象.profile是通过OneToOneField反向取到的UserProfile对象student_id是它上面的字段这个链路上的任何一个环节为空页面就会直接报错。收藏功能的本质是建立「用户 — 商品」的关系记录favorite Favorite.objects.filter(userrequest.user, goodsgoods) if favorite.exists(): favorite.delete() # 再点一次取消收藏 else: Favorite.objects.create(userrequest.user, goodsgoods)这里有一个防御性的问题filter返回的是一个 QuerySet判断是否收藏用.exists()比.count() 0更高效。而且你必须先查再删否则用户快速连点两次收藏按钮数据库里就会插入两条相同的收藏记录详情页里显示“收藏人数”就会变成 2这也是一个常见的数据一致性隐患。优化的做法是在Favorite模型的Meta里加unique_together (user, goods)让数据库层面做兜底真出现脏数据时能抛异常提示而不是默默重复入库。5. 避坑指南Django MySQL 项目最常踩的六个坑与排查顺序5.1 高发坑清单现象 → 原因 → 解决今天照着源码完整跑了一遍把最可能让你卡壳的六个问题统一列在这里每一条都是实际翻车现场。坑一mysqlclient安装失败。现象pip install mysqlclient在 Windows 上报Microsoft Visual C 14.0 is required。原因是mysqlclient默认需要编译。解决Windows 上我一般直接改用pip install pymysql然后在项目__init__.py里写import pymysql; pymysql.install_as_MySQLdb()和mysqlclient的效果一致不需要折腾编译环境。坑二migrate时报表不存在或字符集错误。现象执行migrate后报Unknown collation: utf8mb4_unicode_ci或者表建好了但中文写入乱码。原因MySQL 版本过低不支持utf8mb4或者建库时没指定字符集。解决先确认 MySQL 版本是 5.7 及以上然后按我第 2.3 节的方式重新建库指定DEFAULT CHARACTER SET utf8mb4。坑三图片上传后页面无法显示。现象控制台没有报错文件也传到了media目录但img标签 404。原因开发环境没有配static()转发。解决在项目urls.py末尾加上urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)同时确认 Django 的DEBUG True生产环境直接DEBUG False时开发服务器不提供媒体服务这属于另一个话题。坑四登录后request.user是匿名用户。现象登录成功了但页面不显示用户名。原因模板里用的是{{ request.user.username }}而不是{{ user.username }}或者渲染视图时没把request传入模板上下文。解决检查TEMPLATES设置里context_processors是否包含django.template.context_processors.request。缺少这一项request变量在模板里不可用。另外视图函数记得传request给模板渲染函数render(request, template, context)的第一个参数不能省。坑五删除或下架商品后订单记录仍然存在。现象订单列表里能看到已被删除的商品的标题点击后 404。原因订单表的ForeignKey设置了on_deletemodels.SET_NULL或CASCADE但没处理商品被删后的展示兜底。解决在订单详情视图里判断goods is None时显示“商品已下架”而不是直接读goods.title。这个对二手交易尤为重要因为商品会频繁上架下架订单必须保留记录方便后台对账。坑六表单提交后多对多或外键报NOT NULL constraint failed。现象提交新商品时goods.owner没有赋值报NOT NULL constraint failed。原因视图里保存Goods时没有把当前登录用户关联进去。解决goods form.save(commitFalse); goods.owner request.user; goods.save()。这是一个非常典型的新手错误也是一份“合格但不够精细”的源码容易漏掉的地方。5.2 排查顺序与定位手段从 URL 到异常栈的查找路径遇到问题我建议按这个顺序排查先看浏览器地址栏的 URL 和请求方法GET/POST确认视图是否被正确路由然后看终端里的完整异常栈重点找File xxx/views.py, line xx这一行——这行之外的任何输出都只作为参考接着看QuerySet的查询行为——如果怀疑数据有问题用python manage.py shell直接执行模型查询看返回结果这能区分“代码逻辑错误”还是“数据状态错误”。一个快速定位手段python manage.py check --deploy这个命令的意义在于帮你发现配置层面的安全隐患比如DEBUGTrue、SECRET_KEY硬编码虽然它不是 debug 工具但在答辩前跑一次能避免被评委问“你的项目安全吗”这类抽象问题。更实用的排查手段是临时在视图里打印request.POST和request.FILES确认表单数据是否完整传到后端这一段是很多前端表单问题定位的金钥匙。6. 把订单流程做成一个小型状态机事务、防并发与验证方法这个项目的订单流程如果你只做到“点按钮插一条记录”就停手答辩时大概率被追问“并发下单怎么处理”。我当时在做这套源码的订单模块时把它重构成一个简单的状态机商品状态只有在售、已下单、已售出三态只有处于在售的商品才能被下单下单动作在一个事务里完成两步——校验状态、变更状态。这样就从架构层面规避了两个人同时下单同一件商品的可能性。事务的写法我习惯用transaction.atomic()from django.db import transaction with transaction.atomic(): # 步骤1查询并锁行 goods Goods.objects.select_for_update().get(pkgoods_id) # 步骤2校验状态 if goods.status ! on_sale: return JsonResponse({code: 1, msg: 商品已被购买}) # 步骤3变更状态 创建订单 goods.status sold_out goods.save() Order.objects.create(goodsgoods, buyerrequest.user, pricegoods.price)select_for_update()的作用是在数据库层面锁住这一行直到事务结束。两个人的请求同时进来第二个请求的get()会阻塞等待直到第一个事务结束后再去读此时状态已经变成sold_out校验失败返回提示。这是一个小而完整的状态机——状态不是散落在各处 if 分支里而是有一个明确的“当前状态 允许的状态迁移”模型。回到这份源码goods.status字段是必需的如果它只在模板里展示而没在订单逻辑里参与校验你可以按上面的方式补上。日期处理还有一个习惯值得养成所有订单在创建时要带上时间戳。MySQL 里DateField会自动维护auto_now_add这个属性但手动跑批量导入脚本或后台直接改数据库时它不生效。我都是把订单创建时间显式赋为timezone.now()这样任何入口创建的数据都有一致的时区基准。Django 的USE_TZ True和TIME_ZONE Asia/Shanghai要同时设置只改TIME_ZONE不设USE_TZMySQL 的DATETIME会没有时区信息排序和统计偏差很大。从那以后我每次接手这类 Django 毕设项目都强制自己在跑通前先过一遍订单状态链路和权限归属校验再谈功能演示。这两个点立住了项目的精神内核就立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表