
简介基于PythonDjango的购物商城管理系统是一份面向计算机专业毕业设计、课程设计与期末大作业的完整项目资源。项目采用Django框架实现商城核心功能包含用户、商品、购物车、订单等模块源码经过本地编译调试可运行并附带数据库脚本适合需要快速搭建可用商城系统进行学习或二次开发的学生。压缩包共含384个文件以94个Python源码文件、59个HTML模板、33个图片及相关JS/CSS样式为主同时提供SQL数据库脚本和两份Word架构说明文档整体体积仅15.8MB便于下载与部署。资源已有204人学习项目难度适中文档和目录结构清晰可帮助读者理解Django项目组织方式、MVT架构及电商业务逻辑是一份性价比很高的实战参考。从业务建模到页面交互均有完整实现适合在毕设答辩中展示系统完整性与代码可读性。1. Django 购物商城这套源码为什么适合拿来当毕业设计购物商城管理系统是计算机专业毕业设计里出现频率最高的题目之一。它业务闭环完整前台有商品展示、搜索、购物车和下单支付后台有商品管理和订单管理数据层还有多表关联和事务处理能在答辩时完整展示一个学生的工程能力。基于 Python 的 Django 框架把它们串起来并不吃力自带 ORM、Admin 后台、CSRF 防护和会话机制能把开发周期压到三周左右。这套基于 Python Django 的购物商城管理系统源码项目代号“蔬果优选”默认使用 SQLite 数据库前端静态页和后端逻辑打包在一起适合正在做课程设计、期末大作业或者需要项目实战练习的 Python 学习者。2. Django 项目骨架与数据模型设计拿到资源后你看到的是 index.html、main.css、reset.css、client.conf、两份 docx 文档。客户端前端是完整的商城静态页面而 Django 业务逻辑在“蔬果优选项目开发过程.docx”里有详细描述。常见做法是先把 docx 里描述的模块结构还原成 Django 工程再把静态页替换成模板文件这样前端的视觉设计不会被浪费后端也能正常跑起来。2.1 用 django-admin 重建项目骨架先确认本机 Python 环境已经装好然后创建虚拟环境并安装 Django。推荐先在项目根目录下初始化工程再按商城业务拆分成四个 appuser 管用户、goods 管商品、cart 管购物车、order 管订单。mkdir shuguoyouxuan cd shuguoyouxuan python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install django django-admin startproject shuguoyouxuan . python manage.py startapp user python manage.py startapp goods python manage.py startapp cart python manage.py startapp orderstartproject 后面的点表示在当前目录生成 manage.py这样 docx 里描述的项目根目录结构就和现实对齐了。四个 app 的职责边界非常清楚后面写路由、写 models 时不会互相缠绕。app 名尽量用单数Django 在生成数据表时默认会加上 app 前缀比如 goods 里的 Goods 模型对应 goods_goods 表可读性更好。2.2 用户、商品、购物车、订单的 ORM 模型商城系统的表结构是所有后续功能的基石。用户表直接继承 AbstractUser扩展手机号和收货地址两个字段商品归属分类分类支持一级和二级购物车用联合唯一约束保证同一个用户对同一个商品只有一条记录订单与订单项分离避免一张表存重复的商品快照。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length11, blankTrue, default) address models.CharField(max_length200, blankTrue, default) class Category(models.Model): name models.CharField(max_length50, uniqueTrue) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE ) class Goods(models.Model): name models.CharField(max_length120) category models.ForeignKey(Category, on_deletemodels.PROTECT) price models.DecimalField(max_digits10, decimal_places2) stock models.PositiveIntegerField(default0) sales models.PositiveIntegerField(default0) image models.ImageField(upload_togoods/, nullTrue, blankTrue) status models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) goods models.ForeignKey(Goods, on_deletemodels.CASCADE) quantity models.PositiveIntegerField(default1) class Meta: unique_together (user, goods) class Order(models.Model): order_sn models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE) total_amount models.DecimalField(max_digits12, decimal_places2) status models.SmallIntegerField( default0, choices((0, 待支付), (1, 已支付), (2, 已取消)) ) created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey( Order, related_nameitems, on_deletemodels.CASCADE ) goods models.ForeignKey(Goods, on_deletemodels.PROTECT) price models.DecimalField(max_digits10, decimal_places2) quantity models.PositiveIntegerField()这几个模型里有三个容易被忽略的选型细节。价格字段必须用 DecimalField 而不是 FloatFieldfloat 在做金额累加时会产生 0.1 0.2 不等于 0.3 的浮点误差订单总金额一旦对不上会让答辩非常被动。on_delete 的处理也不一样Category 被删除时用 PROTECT防止商品变成无分类的孤儿数据OrderItem 中的商品也用 PROTECT已成交订单里的商品不允许随意删除。购物车里的 CartItem 用了 unique_together 联合约束加购逻辑是 UPDATE 而不是重复 INSERT。2.3 迁移、初始化与 Admin 后台模型写好后执行数据库迁移。Django 会帮你在 SQLite 中生成全部数据表建表语句可以通过 sqlmigrate 查看这是排查字段漏写最直接的手段。python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py sqlmigrate cart 0001_initialcreatesuperuser 会交互式要求输入用户名、邮箱和密码生成的超级用户直接进 Admin 后台管理商品分类。商城管理后台是答辩演示的高频入口把模型注册进 admin 只需要几行代码注册后的后台可以直接增删改查商品不用额外写管理页面。from django.contrib import admin from goods.models import Category, Goods admin.register(Goods) class GoodsAdmin(admin.ModelAdmin): list_display (name, category, price, stock, status) search_fields (name,)注册后刷新后台商品列表支持按名称搜索价格、库存、上下架状态一眼可见。对于毕业设计演示来说这个后台已经能撑起“商品管理模块”的完整叙述。数据初始化可以用 fixtures 导入 JSON 商品数据写一个初始数据文件放商品分类和测试商品避免每次重置数据库后手动录入。表格里把这几个核心模型和实际业务模块的对应关系列出来写代码时对照着看不容易串模型对应数据表核心字段业务作用Useruser_userphone, address登录注册、收货信息Categorygoods_categoryname, parent商品分类与层级Goodsgoods_goodsprice, stock, status商品展示与库存控制CartItemcart_cartitemquantity, unique_together购物车合并与数量修改Orderorder_orderorder_sn, status订单生命周期管理OrderItemorder_orderitemprice, quantity订单商品快照3. 商城核心交易链路购物车、下单与支付状态回调购物车到订单再到支付回调是整套源码里最值得讲清楚的部分也是毕业设计答辩时老师大概率追问的环节。这里最常被问到的问题是下单过程中库存怎么扣重复提交怎么防支付回调重复通知怎么办这一章把完整链路拆出来逐段看。3.1 购物车的加购、改数量与删除购物车加购用 get_or_create 可以减少一次条件判断。第一次加购时自动创建记录并把数量设为 1如果这条用户-商品记录已经存在就只做数量累加。from django.shortcuts import get_object_or_404 from django.http import JsonResponse from cart.models import CartItem from goods.models import Goods def add_cart(request, goods_id): goods get_object_or_404(Goods, pkgoods_id, statusTrue) item, created CartItem.objects.get_or_create( userrequest.user, goodsgoods, defaults{quantity: 1} ) if not created: item.quantity 1 item.save() return JsonResponse({code: 0, quantity: item.quantity})get_or_create 内部先做一次 SELECT查不到再 INSERT返回的布尔值表示是否新建。这里有一个可以深入的点默认写法在高并发下可能出现重复记录如果要扛住压力可以把加购包在事务里并用 select_for_update 锁住商品行。毕业设计阶段不需要完整分布式锁但面试或答辩时能说出这个边界就已经説明你理解了问题本质。修改数量和删除更直接。删除购物车项时Django 的查询集支持一次性删除多个对象用 filter 再做 delete 即可不需要循环。def cart_add_quantity(request, item_id): item CartItem.objects.select_related(goods).get( iditem_id, userrequest.user ) item.quantity 1 item.save() return JsonResponse({code: 0, quantity: item.quantity}) def cart_remove(request, item_id): CartItem.objects.filter(iditem_id, userrequest.user).delete() return JsonResponse({code: 0})过滤器里的 userrequest.user 保证当前用户只能操作自己的购物车这是防止水平越权最简单有效的约束。凡是带 id 的接口都要带上当前用户作为过滤条件。3.2 下单事务库存扣减与订单生成下单是整个系统里事务边界最清楚的操作。用户在购物车里选中商品后点击结算后端要做四件事查购物车、算总价、扣库存、生成订单。任何一步失败前面所有操作都要回滚不能出现订单生成了但库存没扣的情况。from django.db import transaction from django.utils import timezone from order.models import Order, OrderItem transaction.atomic def create_order(request): cart_items list( CartItem.objects .filter(userrequest.user) .select_related(goods) ) if not cart_items: raise ValueError(购物车为空) order Order.objects.create( userrequest.user, order_sngen_order_sn(request.user), total_amount0 ) total 0 for item in cart_items: goods Goods.objects.select_for_update().get(pkitem.goods_id) if goods.stock item.quantity: raise ValueError(f{goods.name} 库存不足) goods.stock - item.quantity goods.sales item.quantity goods.save() OrderItem.objects.create( orderorder, goodsgoods, pricegoods.price, quantityitem.quantity ) total goods.price * item.quantity order.total_amount total order.save(update_fields[total_amount]) CartItem.objects.filter(userrequest.user, id__in[i.id for i in cart_items]).delete() return order这里最容易出错的是循环内多次访问数据库。代码中的 select_related 会把购物车关联商品一次性 JOIN 查出来避免逐条查商品的 N1 查询。select_for_update 专门用来锁商品行锁住之后其他请求再查同一行会等待等到第一个事务提交或回滚后才能继续这可以防止两个订单同时把最后一个库存扣成负数。事务函数要求所有数据库操作在同一个连接里完成所以写完后要通过 order.total_amount 的最终值做一次断言验证。注意gen_order_sn 返回值必须保证唯一否则 Order 表的 unique 约束直接报错。库存扣减和订单明细写入放在同一个事务里任何一个 raise 都会让 order、orderitem、goods 的修改全部回滚不会留下脏数据。3.3 模拟支付回调与幂等处理毕业设计一般不接入真实微信或支付宝项目里通常用一个模拟回调接口代替。回调接口要处理一个核心问题支付平台可能重复通知后端必须保证同一笔订单只被处理一次。def pay_callback(request): order_sn request.POST.get(order_sn) pay_no request.POST.get(pay_no, ) order Order.objects.select_for_update().get(order_snorder_sn) if order.status 0: order.status 1 order.pay_no pay_no order.paid_at timezone.now() order.save(update_fields[status, pay_no, paid_at]) return JsonResponse({code: 0, msg: success}) return JsonResponse({code: 1, msg: repeat notify})先查订单当前状态只有待支付状态才允许改为已支付。select_for_update 加锁能挡住两个回调同时到达的情况第一个请求锁住订单行并完成状态修改第二个请求等待锁释放后再读读到 status 已经是 1直接返回重复通知不做任何修改。这个幂等设计在真实支付系统里是必须的。动手改动前建议先看资源里的 docx 文档它会写明订单状态机的定义。按照状态迁移图来写回调判断比在视图里堆 if 要清晰得多。4. 前后端联调静态资源、模板渲染与商品搜索资源包里自带一套完整的商城前端页面index.html 是首页main.css 和 reset.css 是样式和初始化文件。直接双击 index.html 能看到静态效果但要把商品数据和购物车功能串起来必须把静态页改造成 Django 模板并让 Django 正确服务 CSS 静态文件。4.1 静态资源在 Django 里的正确落位Django 对静态文件和模板文件的扫描路径有严格要求。把 index.html 复制到 templates 目录把 main.css、reset.css 及图片资源复制到 static 目录然后在 settings.py 里做两处配置。BASE_DIR Path(__file__).resolve().parent.parent TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], APP_DIRS: True, ... }, ] STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]STATICFILES_DIRS 告诉 Django 在开发环境从哪里找静态文件。模板里加载 CSS 时要用 Django 标签而不是写死相对路径否则换路由后样式会丢失。{% load static %} link relstylesheet href{% static css/main.css %}4.2 用模板变量把商品列表渲染到首页index.html 原本的商品数据是写死在页面里的需要把它改成 Django 模板语法循环输出。商品列表页是最典型的场景贴上简化后的模板结构{% for g in page_obj %} div classgoods-card img src{{ g.image.url }} alt{{ g.name }} h3{{ g.name }}/h3 p classpricespan¥/span{{ g.price }}/p p classstock库存 {{ g.stock }}/p a href/goods/{{ g.id }}/ classbtn查看详情/a /div {% empty %} div classempty暂无商品/div {% endfor %}{{ g.price }} 在渲染时会自动调用 DecimalField 的字符串格式化直接输出两位小数。{{ g.image.url }} 只有在图片字段有值时才有意义开发阶段可以先用占位图代替。{% empty %} 是 Django 模板特有的分支当商品列表为空时展示空态提示比在视图里自行判断列表长度更干净。视图层准备好分页后的商品查询集模板里用变量名 page_obj 替代原生 HTML 中固定的商品卡片。页码链接也要跟着改动注意保留搜索参数。{% if page_obj.has_previous %} a href?page{{ page_obj.previous_page_number }}上一页/a {% endif %}上面这段模板里如果搜索关键词还带着 kw 参数切换分页时关键词会丢。常见做法是把当前查询串拼到分页链接后面在视图里同步返回 keywords 变量。4.3 商品搜索与分页查询组合首页和商品列表页都要支持按名称搜索商品。搜索和分页要放在同一个视图函数里处理使用 icontains 做大小写不敏感的名称模糊匹配简单直接也足够应付毕业设计的性能要求。from django.core.paginator import Paginator def goods_list(request): kw request.GET.get(kw, ).strip() qs Goods.objects.filter(statusTrue).select_related(category) if kw: qs qs.filter(name__icontainskw) paginator Paginator(qs, 12) page_obj paginator.get_page(request.GET.get(page)) return render(request, goods/list.html, { page_obj: page_obj, kw: kw, })Paginator 的第二个参数 12 表示每页 12 个商品。get_page 在页码越界时会自动返回最后一页不会抛异常。select_related(category) 在商品列表模板里如果要展示分类名就非常有用它让 category 的读取从逐条查询变成一次 JOIN 查询。调试时可以在 sqlite 的 query 日志中看到实际执行的 SQL若发现 N 次商品查询后还跟着 N 次分类查询就说明这里少了 select_related。5. 数据库配置、CSRF 和常见运行异常排查把源码在本地跑通只是第一步换一台电脑、换一个数据库或者部署到云主机时会遇到一批高频率报错。这些错误大多不是代码逻辑问题而是 Django 工程配置层面的问题。这一章把这几个坑按出现频率列出来。5.1 从 SQLite 切换 MySQL 的配置步骤开发阶段 SQLite 足够但毕业设计文档里一般会写 MySQL。切换数据库时不需要改模型只需要改 settings.py 的 DATABASES 配置并安装对应驱动。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: shuguoyouxuan, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }ENGINE 指向 mysql 后端后还要在项目目录的init.py 里启用 PyMySQL否则会报 ModuleNotFoundError。import pymysql pymysql.install_as_MySQLdb()MySQL 切换后必须先重新执行 makemigrations 和 migrate把数据表建到 MySQL 中。原来 SQLite 里的数据不会自动迁移简单做法是直接用 Django Admin 重新录入分类和商品数据。5.2 settings 里最容易出问题的三个配置项ALLOWED_HOSTS 不配置直接 runserver 后访问可能报 DisallowedHost。调试阶段可以暂时放宽部署时必须收紧为具体域名或服务器 IP。ALLOWED_HOSTS [127.0.0.1, localhost, yourdomain.com]TIME_ZONE 和 USE_TZ 的组合影响到所有时间字段。推荐统一使用下面的配置避免订单创建时间差 8 小时TIME_ZONE Asia/Shanghai USE_TZ TrueUSE_TZ True 表示数据库存 UTC 时间模板渲染时按本地时区显示。如果改成 USE_TZ FalseDjango 会把 TIME_ZONE 当作本地时间直接存储。这套项目里订单创建时间、支付时间都依赖 timezone 模块不建议在开发到一半时切换时区配置最好一开始就固定。CSRF 中间件是 Django 默认开启的。所有 POST 表单不携带 csrf_token 都会返回 403前端模板里要显式加上令牌。form methodpost {% csrf_token %} ... /form如果做前后端分离接口则使用 Django REST Framework 的 JWT 认证方式并在中间件里单独处理而不是直接关掉 CSRF。5.3 高频报错对照表以下是这个项目最常遇到的报错和修复方法排查顺序也按表格从上到下执行报错现象根因修复方式OperationalError: no such table没执行 migrate执行 python manage.py migrateInvalid HTTP_HOST headerALLOWED_HOSTS 未包含访问域名添加对应域名Forbidden (CSRF cookie not set)表单缺少 csrf_token模板加 {% csrf_token %}AttributeError: NoneType object has no attribute url商品图片字段为空模板仍访问模板判断 {{ g.image }} 是否存在Page not found at /favicon.ico浏览器请求站点图标添加 favicon 路径或忽略TemplateDoesNotExisttemplates 路径配错检查 TEMPLATES 中 DIRS 指向模板里对图片字段做空值判断的写法建议统一{% if g.image %} img src{{ g.image.url }} alt{{ g.name }} {% else %} img src{% static img/placeholder.png %} alt{{ g.name }} {% endif %}这样不依赖每张商品图都有数据后台手工录入的商品也不会在前台展示破图。6. 一个提升答辩亮点的细节订单号生成与并发扣库存毕业设计答辩走到最后老师一般会问“你这里有没有考虑高并发”。与其回答“有”不如直接展示一个能体现思考深度的具体实现。这里给出订单号生成和库存扣减的最终优化版本可以直接替换上一章的雏形。import secrets from django.utils import timezone def gen_order_sn(user_id): return {}{}{}.format( timezone.now().strftime(%Y%m%d%H%M%S), user_id, secrets.randbelow(10000) )时间戳精确到秒加上用户 ID 后唯一性已经很高再补一个 0 到 9999 的随机数保证同一用户同一秒内下多笔订单也不会冲突。secrets 模块是 Python 3.6 之后的密码学安全随机数生成器比 random 更不容易预测适合订单号这种对外暴露的编号。配合这个订单号库存扣减也应该收口在同一套事务里。最终版可以引入行级锁和二次校验transaction.atomic def buy_now(request, goods_id, quantity): goods Goods.objects.select_for_update().get(pkgoods_id) if goods.status is False: raise ValueError(商品已下架) if goods.stock quantity: raise ValueError(库存不足) goods.stock - quantity goods.sales quantity goods.save(update_fields[stock, sales]) order Order.objects.create( userrequest.user, order_sngen_order_sn(request.user.id), total_amountgoods.price * quantity, status0 ) OrderItem.objects.create( orderorder, goodsgoods, pricegoods.price, quantityquantity ) return orderselect_for_update 锁的是商品行而不只是整张表锁粒度更小两个用户同时买不同商品时互不阻塞。save 时用 update_fields 只提交 starock 和 sales 两个字段减少不必要的写操作。整个事务结束时锁才释放任何一步抛出异常都会回滚库存变化。答辩时可以向老师做一次简单验证用 shell 并发模拟两个请求同时购买同一件库存只剩 1 的商品。python manage.py shellfrom django.db import transaction from goods.models import Goods with transaction.atomic(): goods Goods.objects.select_for_update().get(pk1) assert goods.stock 1 time.sleep(2) # 模拟处理耗时 goods.stock - 1 goods.save()同时开两个终端执行这段代码第二个终端会阻塞在 select_for_update 那行直到第一个事务提交后才继续执行此时它的 assert 会因为 stock 已变成 0 而主动失败。这段演示代码能直观证明库存没有被超卖比单纯贴一条 select_for_update 更有说服力。这套资源跑通之后的顺序是先看 docx 验收项目功能清单再把购物车、订单、后台管理三条链路各走一遍最后用上面的并发验证方法确认代码的实际效果。本文还有配套的精品资源点击获取