
简介这是一份基于Django框架开发的购物商城系统课程设计源码包面向计算机相关专业学生也适合需要完成期末大作业、课程设计项目的开发者。项目用Django与数据库实现了商品展示、购物车、订单管理等商城核心流程代码注释较为详细基础一般的读者也能逐步读懂项目结构划分清楚方便在此基础上继续做功能扩展。压缩包为ZIP格式整体大小14.35MB包含完整的项目源码和数据库文件因文件总数及类型明细暂无有效数据此处不作展开。目前已有109人浏览学习。整套代码来自97分高分课程设计项目下载后即可运行既可作为课程设计或期末大作业的参考模板也能帮助学习者理解Django中模型、视图、模板的常见组织方式是一份实用性强、上手门槛较低的实战素材。1. 基于Django的购物商城课程设计从评分标准倒推系统边界课程设计交付的是一份能演示、能答辩的Django Web项目评分标准和企业开发完全不同。老师不会压QPS也不会问缓存集群他看的是业务链路是否完整、表结构是否讲得通以及「下单会不会超卖」「支付回调会不会重复改状态」这类基础问题。标题里「源码数据库高分代码」背后其实是大多数课设的真实形态——拿一份能跑的Python商城源码导入数据库换个前端壳就提交。这种方案最大风险在答辩环节商品、SKU、订单、购物车四张表的关联关系下单时库存怎么扣模拟支付回调为什么不能直接改订单状态一问就露馅。这篇按答辩最常盘问的顺序把Django购物商城的数据建模、下单事务、支付回调、admin后台四条线拆开讲最后附一份能直接对着核对的回归自测清单。适合正在用Python做课程设计以及拿到别人Django商城源码做二次开发和讲清业务逻辑的人。2. 购物商城数据模型商品SKU、购物车与订单表的结构设计2.1 商品表和SKU表分离一货多价怎么存最简单的课设做法是把价格、库存直接写进商品表遇到手机、服饰这类有规格的商品马上出问题同一款手机颜色不同价格不同同一件衣服尺码不同库存不同。价格放商品表就只能为每个规格重复建商品行搜索列表重复、订单定位不到具体规格、库存统计混乱。正确做法是拆成两张表商品表描述「这个商品是什么」SKU表描述「这个商品下具体哪一组规格可以下单」。from django.db import models class Category(models.Model): name models.CharField(分类名, max_length32) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.SET_NULL) class Product(models.Model): title models.CharField(商品标题, max_length128) category models.ForeignKey(Category, on_deletemodels.PROTECT) main_image models.ImageField(主图, upload_toproduct/%Y/%m/) detail models.TextField(图文详情, blankTrue) is_on models.BooleanField(上架状态, defaultTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class SKU(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameskus) spec_key models.CharField(规格组合key, max_length64) spec_desc models.CharField(规格文案, max_length128) price models.DecimalField(售价, max_digits10, decimal_places2) stock models.PositiveIntegerField(库存, default0) sales models.PositiveIntegerField(销量, default0) class Meta: unique_together (product, spec_key)spec_key 是把「颜色黑色,容量256G」按固定顺序拼接成的字符串用于保证同一商品下不会出现两组相同的规格spec_desc 是给人看的完整描述下单时直接冗余进订单明细。字段注释里的 verbose_name 会直接显示在 django admin 的表头上课程设计的评审老师很吃这一套。选择 related_nameskus 后视图和模板里用 product.skus.all() 就能取到某个商品的全部可售规格Category 的 parent 自关联实现二级分类课设的分类展示用这种简单的上下级结构就够不必引 django-mptt 这类树形插件。on_deletemodels.PROTECT 是关键参数分类只要还挂着商品后台删除分类时就会抛 ProtectedError从根上防止删出孤儿数据。2.2 购物车选型Session、Redis和数据库表的取舍购物车实现方案直接影响答辩时「系统设计」这一环节的观感。Session 方案最简单但服务重启或换设备购物车就丢Redis 方案要额外拉依赖在只有 SQLite 或 MySQL 的课设环境里容易给自己挖坑数据库表方案没有额外依赖还能顺带展示 ORM 的多表查询能力。三者的取舍在表格里看得更清楚存储位置额外依赖丢失场景答辩可讲点Session无服务重启、换浏览器Django Session 机制Redisredis-py内存溢出策略缓存失效与持久化数据库表无不丢外键关系、联表查询课设场景我一般建议直接上数据库表游客加购后登录再合并购物车这个常见需求也能顺带处理。from django.conf import settings class CartItem(models.Model): user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE) sku models.ForeignKey(SKU, on_deletemodels.CASCADE) quantity models.PositiveIntegerField(数量, default1) created_at models.DateTimeField(加入时间, auto_now_addTrue) class Meta: unique_together (user, sku)unique_together 保证同一用户同一 SKU 只有一条记录重复加购走 update 而不是再 create。合并 Session 购物车时先按 user 和 sku 遍历目标表里已存在的记录存在就累加 quantity不存在再新建不要盲目 create否则 unique 约束直接报错。提示CartItem 里不存价格。加入购物车时的价格和结算时的价格可能不一致价格始终以 SKU 表为准购物车只存数量和关联。2.3 订单主表和订单明细表金额精度与状态机的两个坑订单必须拆主表和明细表。主表描述「这次购买」的整体状态和总金额明细表描述「这次购买里每个商品各买了多少、成交单价多少」。只建一张表的话一个订单多个商品就会变成多行同 order_no统计总金额和修改状态全靠重复行演示时很容易被问倒。class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 待发货), (shipped, 已发货), (completed, 已完成), (cancelled, 已取消), ] order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT) status models.CharField(订单状态, max_length12, choicesSTATUS_CHOICES, defaultpending) total_amount models.DecimalField(实付金额, max_digits10, decimal_places2) created_at models.DateTimeField(下单时间, auto_now_addTrue) paid_at models.DateTimeField(支付时间, nullTrue, blankTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) sku models.ForeignKey(SKU, on_deletemodels.PROTECT) sku_desc models.CharField(下单时规格文案, max_length128) price models.DecimalField(成交单价, max_digits10, decimal_places2) quantity models.PositiveIntegerField(数量, default1)金额字段必须用 DecimalField这是第一个坑。FloatField 用二进制存浮点数0.1 加 0.2 会算成 0.30000000000000004订单金额一旦出现这种精度误差对不上账在答辩里非常难看。Decimal 在 Python 侧对应 decimal.Decimal在 MySQL 侧对应 DECIMAL(10,2)浮点数表示误差是计算机组成原理课上的经典话题这里正好是现成案例。第二个坑是明细表里冗余了 sku_desc 和 price。这是快照设计下单后管理员可能改价、下架甚至删除 SKU如果没有冗余历史订单显示的金额和规格文案会跟着变。OrderItem 的 price 是下单那一刻的成交价和 SKU 表当前 price 无关。外键保护上Order 对 User 用 PROTECT防止删用户连带删订单OrderItem 对 SKU 用 PROTECT防止删 SKU 时把订单历史撕掉。这两个参数是答辩时能主动讲解的细节。3. Django下单流程事务、行锁与模拟支付回调3.1 先查再减为什么错并发下的库存超卖课程设计里最常见的扣库存写法是「先查 stock够就减 1不够就提示」对应代码长这样sku SKU.objects.get(idsku_id) if sku.stock 1: sku.stock - 1 sku.save()这段代码在单用户演示时没问题但两个请求同时进来后两个进程都可能读到 stock1都认为自己拿到了最后的库存结果都执行 save库存变成 0 甚至负数。Django ORM 的 save 是整行更新最后一次写覆盖前一次这条竞态在并发下必然翻车。正确做法是用事务加行锁把「查库存、扣库存、生成订单」包进同一个原子操作from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_http_methods import uuid from .models import Order, OrderItem, SKU login_required require_http_methods([POST]) def create_order(request): sku_id request.POST.get(sku_id) quantity int(request.POST.get(quantity, 1)) with transaction.atomic(): sku SKU.objects.select_for_update().get(pksku_id) if sku.stock quantity: return JsonResponse({code: 1, msg: 库存不足}) sku.stock - quantity sku.sales quantity sku.save(update_fields[stock, sales]) order Order.objects.create( order_nouuid.uuid4().hex[:20], userrequest.user, statuspending, total_amountsku.price * quantity, ) OrderItem.objects.create( orderorder, skusku, sku_descsku.spec_desc, pricesku.price, quantityquantity, ) return JsonResponse({code: 0, order_no: order.order_no})select_for_update 是行级锁在 MySQL InnoDB 下对命中行加写锁另一个事务想读同一行会阻塞到当前事务提交。Django 要求它必须在 transaction.atomic 块内使用脱离事务直接调用会抛 TransactionManagementError。update_fields[stock, sales] 限定只更新这两个字段避免把整行其他字段也刷一遍也降低并发时互相覆盖无关列的概率。这个点值得主动讲给老师听select_for_update 锁的是 SKU 那一行不是整张表也只有真正发生竞争的 SKU 才会等待其他商品的订单完全不受影响。3.2 登录校验与CSRFfetch请求不再403下单、加购这类接口必须要求登录视图层用 login_required 装饰器就能把未登录用户重定向到登录页。另一个高频坑是 CSRFDjango 默认开启 CSRF 中间件模板渲染页面里的表单加 {% csrf_token %} 就行但前端用 fetch 发 JSON 或 FormData 时请求头没带 token 会直接 403。function csrfToken() { return document.cookie.split(; ) .find(row row.startsWith(csrftoken)) .split()[1]; } fetch(/cart/add/, { method: POST, headers: { X-CSRFToken: csrfToken(), Content-Type: application/json }, body: JSON.stringify({ sku_id: 42, quantity: 1 }) });Django 的 CSRF 校验逻辑是「请求头里的 X-CSRFToken 和 cookie 里的 csrftoken 必须匹配」。模板渲染的页面里 {% csrf_token %} 会同时输出隐藏 input 和 cookie所以从 cookie 取 token 是通用做法。如果项目把前端拆成了纯静态页面记得在登录视图里用 ensure_csrf_cookie 装饰器给浏览器种上 token否则 cookie 不存在时上面的 find 会返回 undefined 然后报错。视图层的装饰器顺序也值得注意组合顺序是从下往上代码最上面的 login_required 包在最外层请求先过它再进方法校验。反过来写未登录用户会先收到 405再收到登录跳转行为不可控。封装视图类时则用 LoginRequiredMixin放在继承列表第一位。3.3 模拟支付回调的幂等处理课设不接真实支付网关是常态常见做法是做一个「模拟支付接口」前台点支付后调用它把订单改成已支付。模拟接口也要按真实回调的幂等标准写否则演示时手滑点了两次支付按钮订单状态被改了两次支付时间也对不上。from django.db import transaction from django.http import JsonResponse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from .models import Order csrf_exempt def mock_pay_callback(request): if request.method ! POST: return JsonResponse({code: 1, msg: method not allowed}) order_no request.POST.get(order_no) with transaction.atomic(): order Order.objects.select_for_update().get(order_noorder_no) if order.status ! pending: return JsonResponse({code: 0, msg: 订单状态已更新忽略重复回调}) order.status paid order.paid_at timezone.now() order.save(update_fields[status, paid_at]) return JsonResponse({code: 0, msg: 支付成功})幂等靠的是「锁住订单行后再判断状态」。两个重复回调同时进来时第二个阻塞在 select_for_update 上等第一个事务提交后读到的是最新状态此时 status 已经是 paid直接返回不会二次改单。这里不能用普通的 get 再 if status那样两个并发回调都能读到 pending双双改成功。提示csrf_exempt 在课设里可以理解成「模拟支付网关回调不校验页面 CSRF」但不要把这个装饰器加到下单、加购这类前端接口上。代码习惯上把 csrf_exempt 写在最上层。4. 商品列表与管理后台ListView分页和django admin提效4.1 首页商品列表与搜索分页首页商品列表用 Django 的通用类视图 ListView 能同时搞定查询、排序、分页三件事省掉手写分页逻辑的代码量。课程设计里常见的写法是这样from django.views.generic import ListView from .models import SKU class ProductListView(ListView): model SKU template_name shop/index.html context_object_name sku_list paginate_by 12 ordering [-sales] def get_queryset(self): qs super().get_queryset() kw self.request.GET.get(kw) if kw: qs qs.filter(product__title__icontainskw) return qs.filter(product__is_onTrue)ordering 按销量倒序排商城首页默认排序一般这么写get_queryset 里先按关键词过滤再统一过滤上架商品。这里关联查询用的是 product__title__icontains双下划线让 ORM 自动 join 到 Product 表不用手动写 JOIN数据量大时有性能问题但课设演示完全够用。分页参数 paginate_by12 表示每页 12 条模板里 ListView 会自动注入 page_obj{% for sku in sku_list %} div classcard img src{{ sku.product.main_image.url }} alt h3{{ sku.product.title }}/h3 p{{ sku.spec_desc }}/p p¥{{ sku.price }}/p /div {% empty %} p没有商品/p {% endfor %} div classpagination {% if page_obj.has_previous %} a href?page{{ page_obj.previous_page_number }}上一页/a {% endif %} span{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}/span {% if page_obj.has_next %} a href?page{{ page_obj.next_page_number }}下一页/a {% endif %} /div模板里取图用 {{ sku.product.main_image.url }}这个 url 由 ImageField 根据 MEDIA_ROOT 拼接。配置完 MEDIA_URL 后本地开发环境要在项目 urls.py 里加静态资源兜底路由常见错误是 admin 里传了图但首页 img 一直 404。排查顺序是MEDIA_URL 是否设置、urls.py 有没有用 django.conf.urls.static 的 static 函数挂载上传目录、文件是否真的写到了 MEDIA_ROOT 下。页面里的链接不要硬编码路径用 {% url shop:index %} 反向解析urlpatterns 变动时模板不用跟着改。4.2 django admin界面美化从默认后台到simpleui默认 admin 自带的功能已经覆盖增删改查但默认样式放到演示投影上显得过于朴素。先别急着写一套自定义后台把 ModelAdmin 的配置项用足观感提升非常明显from django.contrib import admin from .models import Product, SKU, Order, OrderItem admin.register(SKU) class SKUAdmin(admin.ModelAdmin): list_display [product, spec_desc, price, stock, sales] list_filter [product__category, product__is_on] search_fields [product__title, spec_desc] list_editable [price, stock]三个配置项的作用分别是list_display 决定列表页显示哪些列关联字段用双下划线穿透list_filter 在右侧生成筛选面板多分类管理时效率高list_editable 允许在列表页直接改价格和库存给补货演示省掉进入详情页的步骤。注意 list_editable 里的字段不能出现在第一列第一列是详情页跳转链接写进去会报错。想进一步美化最省事的方案是装 django-simpleui两步搞定pip install django-simpleui然后把 simpleui 加进 INSTALLED_APPS并放在 django.contrib.admin 之前。它会自动替换默认 admin 模板提供侧边栏菜单、面包屑和更现代的表格样式源码里不需要改任何视图代码。这个库只做模板覆盖不改变 admin 的数据操作逻辑答辩被问到用了什么方案一句「基于 admin 的模板覆盖」就能解释清楚。4.3 用admin actions批量发货订单状态从 paid 变成 shipped 是重复性很高的操作一件件进详情页改效率低。admin 的 actions 机制可以在列表页勾选多笔订单后批量执行admin.action(description批量标记为已发货) def mark_shipped(modeladmin, request, queryset): queryset.update(statusshipped) class OrderAdmin(admin.ModelAdmin): list_display [order_no, user, status, total_amount, created_at] list_filter [status] actions [mark_shipped]admin.action 的 description 参数是下拉菜单里显示的文案queryset 是勾选记录的查询集。queryset.update 直接执行 SQL 级 UPDATE不走模型的 save 方法也不会触发 auto_now 时间字段更新。如果订单模块有操作日志需求就得改成 for order in queryset 逐条处理save 后写日志表。表演示时可以说批量无业务逻辑用 queryset.update单条带日志用 save。5. 提交前自测清单与高分兜底从能跑到能答辩5.1 回归自测清单代码改完不叫做完能过一遍下面的场景才叫能交付。表里的每一行都对应前面章节里的一个实现点演示时按这个顺序跑节奏不会乱场景操作预期结果注册登录注册新账号退出再登录session 状态正确访问受限页跳登录购物车合并未登录加购后登录登录后商品还在数量累加不重复正常下单选商品提交订单库存减 quantity订单状态 pending库存不足把某 SKU 库存改成 1再下单提示库存不足订单不生成重复支付同一订单号连续调两次回调第二次返回已更新paid_at 不覆盖后台改价列表页直接改 price前台商品详情价格立即变化批量发货勾选多笔 paid 订单执行 action列表状态全部变成 shipped5.2 数据库迁移与交付形式sqlite3文件还是dumpdata交付时数据库有两种常见形式。教学环境允许直接交 db.sqlite3 文件改好 settings 里的 NAME 路径就能跑数据库文件跟着项目目录走不用额外部署数据库服务。需要交 MySQL 版本时建库要指定字符集避免中文乱码CREATE DATABASE shop_db DEFAULT CHARACTER SET utf8mb4;settings 里把 ENGINE 换成 django.db.backends.mysql并在项目环境按需安装 mysqlclient。Windows 上 mysqlclient 编译经常失败课设环境如果装不上退回 sqlite3 完全不影响演示逻辑。跨机器恢复数据推荐用 Django 自带的 fixture 命令python manage.py dumpdata --excludecontenttypes --excludeauth.Permission -o db.json python manage.py loaddata db.jsondumpdata 会把模型数据序列化成 JSONloaddata 原样恢复。排掉 contenttypes 和 auth.Permission 是为了避免目标库已有这些表时出现冲突。注意 fixture 不搬 Media 目录下的图片文件交源码包时要单独把 MEDIA_ROOT 里的文件一起打进去否则首页图片全挂。5.3 答辩时主动讲的三个点与其开场讲「我用的是 Django 的 MVT 架构」不如直接从实现细节切入。第一个讲 select_for_update 行锁说明并发下单时如何避免超卖第二个讲 OrderItem 里的 sku_desc 和 price 冗余说明下单之后商品改价不影响历史订单第三个讲支付回调的状态判断说明重复通知时如何保证幂等。三个点各配一段小代码总共不超过两分钟但覆盖了数据库锁、事务、快照、幂等四个高频考点。演示时先在 admin 里把某 SKU 库存改成 1开两个浏览器窗口同时下单同一个商品一个窗口提示库存不足另一个生成订单——这个现场演示比任何口述都有说服力。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。