基于Django的购物商城系统设计与部署实践:从模型到订单链路
简介面向Python课程设计与期末大作业场景这份基于Django的购物商城系统源码包提供了完整可运行的电商项目方案适合有一定Python基础的在校学生作为高分课程设计或毕业设计参考。项目涵盖用户注册登录、商品分类浏览、购物车、订单结算、后台管理等核心模块功能完整、界面美观代码中附有详细注释并配有数据库文件下载后简单配置环境即可部署演示。压缩包共294个文件主要包括45个Python源码文件、36个HTML模板文件、97张JPG图片素材及PNG图标、10个JS交互脚本和4个CSS样式表同时包含SQL数据库脚本、两份docx项目说明文档与Git配置整体大小约11.94MB目录结构清晰易查阅。已有366人学习下载内容包含项目说明文档、Python电商项目文档、前端静态页面及配置文件可帮助读者理解Django商城系统的前后端交互与数据表设计思路快速掌握项目部署、功能扩展和二次开发作为期末大作业提交具有较高的实用价值。1. 课程设计选购物商城别急着写代码如果你正在准备 Python 课程设计手里拿到的是一个“基于 Django 的购物商城系统源码数据库.zip”这样的压缩包第一反应可能是解压、配环境、跑起来。但真正决定这份源码能不能成为高分课程设计的往往不是前端页面多漂亮而是数据库表怎么设计、购物车到订单的流程是否完整、部署到 Linux 服务器上会不会崩。购物商城这个选题看起来简单实际写起来最容易被“购物车存哪里”“库存怎么扣”“订单如何防重复提交”这几个问题卡住。这篇文章就按“模型设计 - 链路实现 - 数据初始化 - 部署排错 - 进阶优化”这条主线把基于 Django 的购物商城系统的关键参数和落地写法一次讲清楚适合课程设计、期末项目答辩也适合想快速用源码改造出自己项目的开发者。2. 先把购物商城的模型立住用户、商品、购物车、订单2.1 为什么课程设计用 Django 而不是 Flask 或 SpringBoot很多人在选课程设计技术栈时会犹豫但 Django 在“购物商城”这个场景下几乎是成本最低的。自带 Admin 后台可以可视化维护商品ORM 让你不用手写 SQLmigration 机制改表结构不用导出再导入。Flask 虽然轻可用户认证、表单、分页这些都要自己拼课程设计时间紧张时很容易烂尾。SpringBoot 学习曲线更陡对于 Python 课程设计明显偏离主题。Django 还有一个隐性优势当答辩老师问“数据库在哪”的时候你可以直接打开 admin 后台给他看商品数据的增删改查这是很多项目无法立刻展示的东西。购物商城系统本质上是一套对“数据关系”的管理系统用户与订单是一对多订单与商品是多对多但需要记录购买数量购物车在未登录时又可能是无状态的。Django 的 ORM 把关系用ForeignKey、ManyToManyField和OneToOneField表达出来非常直观。下面的代码就是这个项目的核心模型你可以直接把它作为models.py的基础。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length11, blankTrue, verbose_name手机号) address models.CharField(max_length255, blankTrue, verbose_name收货地址) class Category(models.Model): name models.CharField(max_length50, verbose_name分类名) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父分类) class Product(models.Model): name models.CharField(max_length200, verbose_name商品名) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) stock models.PositiveIntegerField(default0, verbose_name库存) image models.ImageField(upload_toproducts/, blankTrue, verbose_name图片) class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) product models.ForeignKey(Product, on_deletemodels.CASCADE, verbose_name商品) quantity models.PositiveIntegerField(default1, verbose_name数量) selected models.BooleanField(defaultTrue, verbose_name是否勾选) class Order(models.Model): ORDER_STATUS ( (pending, 待支付), (paid, 已支付), (shipped, 已发货), (done, 已完成), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) status models.CharField(max_length10, choicesORDER_STATUS, defaultpending) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) total_amount models.DecimalField(max_digits10, decimal_places2, default0) class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) product models.ForeignKey(Product, on_deletemodels.PROTECT) price models.DecimalField(max_digits10, decimal_places2) quantity models.PositiveIntegerField()这套模型的核心选择是自建User继承AbstractUser这样扩展手机号、地址时不用改用户认证逻辑Category用自关联外键实现无限级分类OrderItem里冗余一份price因为商品价格会调整订单里必须保留下单时的快照。on_delete参数是答辩时的高频问题PROTECT表示有订单引用时禁止删除商品避免历史订单变成一堆悬空引用而购物车和订单的级联删除用CASCADE更符合业务直觉。2.2 核心数据模型设计从字段到外键的取舍上面五张表已经覆盖了一个购物商城的最小闭环但设计时有很多可调参数。比如Product.price为什么用DecimalField而不用FloatField因为二进制浮点数计算金额会出精度误差课程设计答辩时老师可能直接让你算 0.1 0.2DecimalField配合decimal_places2才能保证金额正确。库存字段用PositiveIntegerField而不是IntegerField从数据库层面拒绝负数这是最简单也是最容易被忽视的约束。各模型字段的关键点如下模型关键字段约束用途UserphoneblankTrue扩展用户信息CategoryparentnullTrue支持二级以上分类ProductpriceDecimalField(10,2)安全存储金额ProductstockPositiveIntegerField防止负库存CartItemuserForeignKey CASCADE登录用户的购物车Orderstatuschoices状态机流转OrderItemprice下单快照避免商品改价影响历史订单这里有一个容易踩的坑ImageField需要安装Pillow如果你解压源码后运行迁移报ModuleNotFoundError: No module named PIL不要以为是 Django 问题直接pip install Pillow即可。还有CartItem没有加unique_together同一个用户添加同一商品会出现多行所以视图里要先get_or_create或对(user, product)加唯一约束。2.3 数据库选型SQLite 起步MySQL 上线课程设计最常见的要求是“使用数据库”Django 默认的 SQLite 已经满足它就是一个文件不需要额外安装服务。但很多学校的演示环境要求 MySQL或者答辩老师会问“换成 MySQL 需要改什么”。Django 的 ORM 层屏蔽了大部分数据库差异你只需要在settings.py里改DATABASES配置。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: mall_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }切换到 MySQL 时必须使用pymysql或mysqlclient。通常课程设计环境里mysqlclient安装会因为缺少编译依赖失败这时在项目入口__init__.py里写import pymysql; pymysql.install_as_MySQLdb()是个稳定的替代方案。注意NAME对应的数据库要先创建好Django 不会自动帮你建库。charset设置为utf8mb4是为了支持 emoji 和生僻字如果你不设置商品名称里有特殊符号时可能会报Incorrect string value。提示用 SQLite 时不需要配OPTIONS但如果你同时跑两个环境建议把数据库配置放到local_settings.py并按环境加载避免课程设计源码里写死你的密码。3. 创建 Django 应用并打通商品列表到购物车的完整链路3.1 创建项目和应用django-admin 与 startapp 的分工拿到源码后你常常会看到一个已经建好的 Django 项目。如果是自己从零写习惯上按业务边界拆应用goods管商品和分类cart管购物车order管订单users管用户。这样拆的好处是后续扩展支付、优惠券时不用动老代码。命令如下python -m django startproject mall . python manage.py startapp goods python manage.py startapp cart python manage.py startapp order python manage.py startapp usersstartproject后面的点表示在当前目录生成manage.py避免多套一层目录。Windows 上如果你直接运行django-admin可能遇到脚本路径问题用python -m django更稳。每个 app 生成后必须立即加入INSTALLED_APPS否则对应的models.py不会被加载makemigrations会提示“没有任何变化”这是新手最容易卡住的地方。创建 app 之后还需要把每个应用里的 URL 分发到项目根路由。mall/urls.py里使用include让每个 app 管理自己的视图函数例如from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(goods/, include(goods.urls)), path(cart/, include(cart.urls)), path(order/, include(order.urls)), ]在较新版本的 Django 中path的写法取代了老的url函数。如果你在源码里看到re_path或(?Pproduct_id\d)风格那是旧写法功能上依然能用但建议统一用path(int:product_id/, views.product_detail)参数会自动转换成整数比正则写起来更安全。3.2 配置 settingsINSTALLED_APPS、数据库、静态文件、时区settings.py是整个项目的中枢课程设计的常见报错一半以上出在这里。除了INSTALLED_APPS和DATABASES还需要确认LANGUAGE_CODE、TIME_ZONE、STATIC_URL和MEDIA_URL这几项。很多源码在 Windows 下跑得好好的放到 Linux 上图片不显示就是MEDIA_URL没配。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, goods.apps.GoodsConfig, cart.apps.CartConfig, order.apps.OrderConfig, users.apps.UsersConfig, ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media DEFAULT_AUTO_FIELD django.db.models.BigAutoFieldLANGUAGE_CODE设置为zh-hans后Django admin 会变成中文界面答辩时更友好。TIME_ZONE必须设成Asia/Shanghai如果保持默认的UTC你在后台创建的订单时间会比北京时间早 8 个小时。USE_TZ True会让DateTimeField存 UTC 时间、展示时再转换如果你觉得麻烦可以设为False但更推荐保留因为你将来部署到云服务器时服务器时区不一定是中国。DEFAULT_AUTO_FIELD在新版本中必须显式声明否则每次运行都会出现 warning虽然不影响功能但答辩演示时看到红色的提示总归不好。3.3 实现商品列表与详情视图类视图与路径参数商品列表和详情是商城最基本的页面。课程设计阶段用函数视图可以应付但更规范的是用ListView和DetailView它们把分页、单个对象查询这些通用逻辑封装好了。下面是一个商品列表的类视图写法。from django.views.generic import ListView from .models import Product class ProductListView(ListView): model Product template_name goods/product_list.html context_object_name products paginate_by 12 def get_queryset(self): qs super().get_queryset() category_id self.kwargs.get(category_id) keyword self.request.GET.get(keyword) if category_id: qs qs.filter(category_idcategory_id) if keyword: qs qs.filter(name__icontainskeyword) return qs这段视图用paginate_by 12控制每页数量get_queryset里同时支持按分类过滤和按关键词搜索。icontains是 Django ORM 里的不区分大小写模糊查询生成的是 SQL 的LIKE %keyword%这个字段在中文环境下没有问题但如果商品名有几十万条这种查询会较慢后续可以用全文检索来优化但课程设计里够用了。对应的 URL 配置需要把category_id从路径中捕获from django.urls import path from . import views urlpatterns [ path(, views.ProductListView.as_view(), nameproduct_list), path(category/int:category_id/, views.ProductListView.as_view(), nameproduct_list_by_category), path(int:pk/, views.ProductDetailView.as_view(), nameproduct_detail), ]使用name后模板中可以用{% url product_detail product.id %}动态反查 URL而不是硬编码/goods/1/。这就是 Django 所谓的reverse解析它最大的好处是 URL 地址变了模板和视图逻辑不用改你只需要改urls.py。3.4 购物车会话方案不登录也能加购购物车的实现方式有数据库表和 session 两种。课程设计里通常要求用户登录才能加购但更接近真实电商的做法是支持匿名购物车。为了降低实现复杂度我一般建议把购物车数据以 JSON 结构直接存在 session 里只有当用户下单时才写入数据库。这样避免了一张只有当前会话有意义的数据表也不需要处理用户退出时购物车合并。class Cart: def __init__(self, request): self.session request.session cart self.session.get(cart) if not cart: cart self.session[cart] {} self.cart cart def add(self, product_id, quantity1): product_id str(product_id) if product_id in self.cart: self.cart[product_id] quantity else: self.cart[product_id] quantity self.save() def save(self): self.session.modified True def remove(self, product_id): product_id str(product_id) if product_id in self.cart: del self.cart[product_id] self.save() def __iter__(self): product_ids self.cart.keys() products Product.objects.filter(id__inproduct_ids) for product in products: yield { product: product, quantity: self.cart[str(product.id)], total: product.price * self.cart[str(product.id)], }这个Cart类没有继承任何 Django 类就是普通 Python 类通过self.session.set_expiry可以控制购物车存活时间。self.session.modified True是关键否则 Django 不会认为 session 已更改内容可能不保存。Product.objects.filter(id__inproduct_ids)用了一次查询把购物车里的商品对象全取出来避免循环内逐条查询。注意 session 里的 key 是字符串而product.id是整数比较和赋值时都要做类型转换这是最多人踩的坑。3.5 订单生成与库存扣减的坑事务与行锁从购物车创建订单时必然要扣减库存。如果两个用户同时下单一个商品只剩一件两个请求都可能读到一个“足够”的库存然后都扣成功最终出现超卖。Django 的 ORM 提供了select_for_update()配合事务解决这个问题。下面是一个简化版下单逻辑。from django.db import transaction transaction.atomic def create_order(request, cart_items): order Order.objects.create( userrequest.user, statuspending, total_amount0, ) total 0 for item in cart_items: product Product.objects.select_for_update().get(iditem.product_id) if product.stock item.quantity: raise ValueError(f{product.name} 库存不足) product.stock - item.quantity product.save() OrderItem.objects.create( orderorder, productproduct, priceproduct.price, quantityitem.quantity, ) total product.price * item.quantity order.total_amount total order.save() return ordertransaction.atomic保证这段代码里任何一个步骤失败了前面的库存扣减和订单创建都会同时回滚。select_for_update()会对查询到的每一行商品在数据库层面加锁直到事务提交才释放。如果在 MySQL 的 InnoDB 引擎下这句会生成SELECT ... FOR UPDATE其他事务会被阻塞。使用 SQLite 时没有行锁但课程设计里并发量很小问题不大。答辩时老师如果问“如何防止超卖”答出这两个机制就已经是加分项。注意select_for_update()必须在事务内使用否则 Django 会抛出TransactionManagementError。另外函数里抛异常后要确保调用方捕获并显示友好错误信息不要让用户看到一长串 traceback。4. 数据库导入、超级用户与部署排错让源码跑起来4.1 初始化数据库和导入数据冷备份与 loaddata 两种套路拿到“源码数据库.zip”解压后通常会看到一个.sqlite3文件或.sql文件。如果带的是 SQLite 文件直接把文件放到项目根目录然后在settings.py中把数据库路径指过去即可不需要迁移。但很多源码的数据库是用 MySQL 导出的mall.sql无法直接在 SQLite 里打开这时你有两条路在 MySQL 里建库后source mall.sql;导入或者不用它的数据库自己生成一套新的测试数据。自己生成数据其实更可控。先把makemigrations和migrate跑通然后创建超级用户再通过 admin 后台手动添加几个商品。但如果商品有几十个一一添加太慢可以用loaddata加载固定数据。操作顺序如下python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py loaddata products.jsonproducts.json里的内容需要先通过python manage.py dumpdata goods.Product --indent 2 products.json生成。使用dumpdata导出时如果商品引用了分类分类的id也会一起导出但导入顺序不能乱要先有Category再有Product。如果只导出一张表而忽略依赖表导入时外键会报错。更稳妥的做法是整体导出dumpdata goods它会按依赖关系导出整个应用的所有表。4.2 管理后台与数据关联admin 界面美化和字段配置Django admin 是课程设计的一个大杀器但默认界面非常朴素而且列表页只显示“对象名”不利于演示。要把它改造成能直接演示效果的后台至少要在admin.py里配置list_display、search_fields和list_filter。from django.contrib import admin from .models import Product, Category admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (name, category, price, stock, image_preview) list_editable (price, stock) search_fields (name,) list_filter (category,) list_per_page 20 def image_preview(self, obj): if obj.image: return fimg src{obj.image.url} stylewidth:50px;height:50px;/ return image_preview.allow_tags True image_preview.short_description 图片list_editable可以直接在列表页修改价格和库存答辩时给老师演示“把商品价格改成 9.9然后网页立刻变化”会很直观。allow_tags在新版本中已经不推荐改用format_html更安全但课程设计里含 HTML 也无妨。search_fields会对name字段生成LIKE查询list_filter会在右侧生成分类筛选。如果你想让 admin 界面更美观可以安装django-admin-interface这个包会把后台首页改成卡片式布局但要注意它依赖python-dateutil而且必须把admin_interface.apps.AdminInterfaceConfig排在django.contrib.admin之前。如果不想引入额外依赖也可以在static/admin/css/base.css里覆盖几行样式比如给 header 换个背景色但那样会随版本更新失效不如用现成库。4.3 Django 执行查询与删除对象ORM 常用操作和级联行为课程设计答辩时老师常常会让现场写一个“从数据库里删除一条记录”的操作。多数人直接Product.objects.get(id1).delete()但这里有很多细节。delete()会返回(total_count, {app.Model: count})并且默认按照外键关系级联删除。比如删除一个Category所有属于它的Product可能都会被删掉这取决于Product.category的on_delete设置。# 删除单个对象 p Product.objects.get(id1) count, details p.delete() print(count, details) # 批量删除并返回行数 deleted, _ Product.objects.filter(stock0).delete() # 查询时排除若干 ID Product.objects.exclude(id__in[1, 2, 3]).update(price10.5)如果不想级联删除商品在定义外键时用on_deletemodels.PROTECT。Django 会拦截删除操作并抛出ProtectedError。在订单场景里历史订单引用了商品商品就不应该被物理删除。更商业化的做法是给Product加一个is_active布尔字段删除时只把状态置为False查询列表时过滤掉不可见商品这叫做软删除。课程设计里如果时间充裕加上软删除会是一个亮点。4.4 本地运行与宝塔部署常见命令和报错对照源码能在本地跑起来只是第一步很多课程的交付要求里会有一句“能在服务器上访问”。本地环境和服务器环境最大的差异在于 Python 版本、静态文件处理和数据库连接。下面是本地启动的标准流程python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt python manage.py runserver 0.0.0.0:80000.0.0.0:8000监听所有网卡这样局域网内其他设备也能通过http://你的IP:8000访问方便答辩时用手机演示。如果requirements.txt缺失至少要安装django、Pillow、mysqlclient。生产部署常见做法是用宝塔面板先在软件商店安装 Python 项目管理器添加项目时选择项目路径、Python 版本和启动方式。宝塔部署 Django 的核心在于两点一是wsgi.py的入口路径要对通常是mall.wsgi:application二是静态文件没有交给 Nginx导致 admin 后台没有样式。下面是一个常见问题和解决方案的速查表错误现象原因解决ModuleNotFoundError: No module named mysqlclient未安装数据库驱动pip install mysqlclient或改用 pymysqlDatabaseError: (2003, Cant connect to MySQL server on 127.0.0.1)MySQL 服务未启动在宝塔或服务面板启动 MySQLDisallowedHost at /访问域名没加进ALLOWED_HOSTS在 settings 中加入*或具体域名admin 后台样式丢失未配置STATIC_ROOT设置STATIC_ROOT并运行collectstaticYou have 18 unapplied migration(s)数据表未创建运行python manage.py migrateField id doesnt have a default value主键定义问题检查 models 中主键建议使用AutoFieldALLOWED_HOSTS是最常被忽略的一项。本地用127.0.0.1访问时不会报错一旦用 IP 或域名访问Django 会拒绝请求。课程设计阶段直接设成[*]省事但如果上生产必须改成具体域名。collectstatic是部署时必做的操作它会把所有 app 里的静态文件复制到STATIC_ROOT再由 Nginx 托管。宝塔里你需要在站点设置中把/static/的 root 指向项目里的staticfiles目录并设置反向代理到 Django 的8000端口。提示如果数据库中带有调试用的商品图片迁移到服务器后图片路径会变成绝对路径。干脆上传到MEDIA_ROOT下或者把MEDIA_URL设置为相对路径前端使用img src{{ product.image.url }}动态拼接这样源码在任意环境中都不会因路径问题丢失图片。5. 给课程设计加分用 select_related 与 admin 联动优化演示课程设计到了收尾阶段代码能跑、页面能看但答辩时最容易丢分的是“性能”和“细节”问题。比如商品列表页每次刷新都会产生大量 SQL 查询打开服务器日志一看一个页面执行了 20 多条 SQL老师可能就会问“你考虑过 N1 查询吗”。实际上只需要两行改动就能避免这个问题。在ProductListView.get_queryset里加一个select_related(category)因为Product的category是外键不加的话每取一个商品都会查一次分类表加了之后 Django 会通过一次JOIN把分类数据全部带出来。def get_queryset(self): return (super().get_queryset() .select_related(category) .prefetch_related(order_items))select_related适用于一对一和外键prefetch_related适用于多对多反向关联。比如在商品详情页展示“这个商品出现在哪些订单里”就用prefetch_related(order_items__order)它会先查OrderItem再查Order总共 2 条 SQL而默认写法可能会产生几十条。答辩时你可以在DEBUG True环境下打开 Django Debug Toolbar 插件展示 SQL 数量从 22 降到 2 的对比这个效果非常直观。分页也是一个容易被追问的点。默认ListView的paginate_by提供了is_paginated、page_obj等变量模板里写{% include goods/pagination.html %}即可。但如果想手动控制分页用Paginator对象也很简单from django.core.paginator import Paginator qs Product.objects.filter(category_idcategory_id) paginator Paginator(qs, 12) page_1 paginator.get_page(1)最后admin 联动的另一个加分项是给OrderItem配置TabularInline让订单详情页能直接看到商品明细而不是跑到订单明细表里去查。把这个配置加在OrderAdmin里后台操作体验会接近真实的 ERP 系统。class OrderItemInline(admin.TabularInline): model OrderItem extra 0 admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (id, user, status, total_amount, created_at) list_filter (status,) inlines [OrderItemInline]加上这个后点进一个订单就能看到下单用户、商品清单、单价数量、当前状态这些数据不需要写任何视图代码所有逻辑都由 admin 框架完成。对于课程设计来说把 Django admin 的潜力挖掘到这里再配合前台的商品展示和购物车流程整个系统的完成度已经足够拿到不错的成绩。答辩演示时建议先展示前台商品列表和加入购物车再打开 admin 后台修改库存回到前台刷新页面看到库存变化这比单纯背稿讲概念更有说服力。本文还有配套的精品资源点击获取