ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Django购物商城课程设计:从数据库建模到并发安全

Django购物商城课程设计:从数据库建模到并发安全 简介基于Django的购物商城系统源码与数据库是一份面向Python课程设计和期末大作业的高分参考项目尤其适合计算机相关专业学生快速完成在线商城应用开发。资源以zip压缩包形式提供整体体积14.35MB内含项目完整源码与数据库文件代码注释做得非常细致核心业务逻辑一目了然即使是入门不久的小白也能轻松读懂并有充分空间在此项目基础上进行二次功能拓展。目前已有109人学习浏览在同类课程设计资源中具有较好的口碑和参考价值。该项目曾获得97分的期末高分评价覆盖用户注册登录、商品分类展示、购物车管理、订单生成等典型电商业务模块下载后即可直接运行既能帮助学习者深入理解Django框架的MVT设计模式与数据库关联查询也能为课程设计报告撰写、答辩演示提供一套完整且可落地的工程样例。1. 课程设计不是跑通就行Django 购物商城到底在考察什么答辩教室的投影仪上一个基于 Django 的购物商城系统正在加载首页。老师通常不会先问“能跑吗”而是直接点开后台看商品表有没有 category 外键再打开订单表问一句“这个 total_amount 是你计算后存进去的还是每次查询时实时算的”。很多同学在这里被问住了。能运行只是及格线高分代码考的是三件事数据库表设计是否合理、购物车和订单流程有没有考虑到并发与数据一致性、后台管理能否让老师一眼看出全部业务数据。这篇博文从一个空目录开始不依赖任何现成大包把基于 Django 的购物商城系统从数据库建模、商品展示、购物车、下单到 Admin 后台完整搭一遍适合刚学完 Python 基础、正在做课程设计或准备把项目写进简历的读者。2. 从零搭建 Django 商城骨架环境、App 划分与第一个页面2.1 为什么课程设计里的商城普遍用 Django 而不是 Flask购物商城看起来功能不多但业务链条很长用户注册登录、商品浏览、购物车、下单、库存扣减、后台管理。Flask 也可以做但需要自己组合 URL 路由、ORM、表单校验、登录会话和后台界面工作量会翻一倍以上。Django 把这些能力全部内置了尤其是自带 Admin 后台和一套完善的 ORM能让课程设计在短时间内呈现出“完整产品”的观感。Django 采用 MVT 模式View 负责业务逻辑Model 负责数据库表结构Template 负责页面渲染。这个结构与电商系统的天然分层非常匹配商品、用户、订单各是一个 AppApp 之间通过外键和 service 方法协作。更关键的是Django 的 ORM 在答辩时非常好讲objects.filter()、select_related()、transaction.atomic()都是可以直接说出来的技术点而 Flask 的场景下你需要自己解释 SQLAlchemy 的 Session 管理老师追问的深度会完全不同。2.2 用 venv 隔离环境锁住 Django 依赖版本课程设计最常见的问题是“在我电脑上能跑”。为了减少环境差异我建议从第一步就建立虚拟环境并把依赖写进 requirements.txt。这里不用 conda因为 conda 创建的 Python 解释器路径在换机器后容易漂移venv 相对更直观。python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate pip install Django pip freeze requirements.txt这段命令先创建一个名为 venv 的虚拟环境然后激活它。后续安装的 Django 只存在于这个环境里不会污染系统 Python。pip freeze requirements.txt会把当前环境所有包及精确版本号写入文件换机器时执行pip install -r requirements.txt即可复现。这里不指定 Django 版本默认会安装当前最新的稳定版课程设计用 Django 4.x 或 5.x 都足够。需要注意如果你的 Python 是 3.7 及以下新版 Django 会拒绝安装建议提前升级到 3.10 以上。2.3 startproject 与 startapp商城的 App 边界划分Django 项目提倡“一个业务模块一个 App”。商城最少拆成三个业务 Appaccounts 管用户、goods 管商品和分类、orders 管购物车与订单。不要把商品和订单写在同一个 App 里否则 models.py 会越来越臃肿答辩时也很难讲清楚模块边界。django-admin startproject shopping_mall . python manage.py startapp accounts python manage.py startapp goods python manage.py startapp orders项目根目录下会生成 manage.py 和 shopping_mall 配置目录三个 App 各自拥有独立的 models.py、views.py、admin.py。表结构定义在哪个 App数据库迁移就归哪个 App 管。下面这张表可以帮助理解每个目录的职责。目录/文件作用shopping_mall/settings.py项目全局配置注册 App、数据库、中间件shopping_mall/urls.py根路由把不同前缀转发给各 Appaccounts/models.py自定义用户表扩展手机号等字段goods/models.py商品分类、商品表orders/models.py购物车、订单、订单明细表创建完 App 后需要修改 settings.py。INSTALLED_APPS 里追加三个 App 的配置类同时把语言和时区改成中文这是课程设计里最容易拿到的印象分。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, accounts.apps.AccountsConfig, goods.apps.GoodsConfig, orders.apps.OrdersConfig, ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai MEDIA_ROOT BASE_DIR / media MEDIA_URL media/注册 App 的目的是让 Django 能发现每个 App 下的迁移文件和模板目录。LANGUAGE_CODE 和 TIME_ZONE 这一对配置记得成对修改否则后台显示时间会比北京时间差 8 小时。MEDIA_ROOT 和 MEDIA_URL 是给商品图片用的课程设计里商品图上传是高频功能这里先留好配置后面模型里一写 ImageField 就能直接用。接着在根路由里把各 App 的 URL 接进来。商品首页可以先放一个最简视图验证整个链路通不通。from django.contrib import admin from django.urls import path, include from goods.views import index urlpatterns [ path(admin/, admin.site.urls), path(, index, nameindex), path(goods/, include(goods.urls)), path(accounts/, include(accounts.urls)), path(orders/, include(orders.urls)), ]根路由只做统一分发具体视图逻辑放各 App 自己的 urls.py 里。这样做的好处是老师问“路由是怎么管理的”时你可以直接展示这个分层结构而不是一个堆了两百行路由的 urls.py。此时执行python manage.py runserver浏览器访问 http://127.0.0.1:8000/ 能看到一个简单页面说明项目骨架已经通了。接下来才是重点把数据库表建出来。3. 商城数据库建模Django Models 里的外键、Decimal 与库存扣减3.1 购物商城最少需要几张业务表很多课程设计会把表设计得非常多比如单独拆出秒杀表、优惠券表、支付流水表。我的建议是控制在六张表以内。用户表、商品分类表、商品表、购物车表、订单表、订单明细表这个规模足够支撑一次漂亮的答辩。表多了容易在数据关联上出 bug少了又体现不出设计能力。六张表之间的核心关系是用户和订单一对多订单和订单明细一对多商品分类和商品一对多商品和订单明细多对一。购物车表比较特殊可以设计成用户和商品的多对多关系在中间表上额外记录加购数量。这里用一个外键加 unique_together 就能表达清楚不需要引入复杂的中间模型。3.2 用自定义用户模型扩展字段Django 默认的 User 表只有用户名、邮箱、密码这些基础字段商城里一般要加手机号、收货地址。最好第一步就自定义用户模型因为中途切换 AUTH_USER_MODEL 的成本很高——一旦执行过迁移改用户模型需要重置整个数据库课程设计阶段不建议冒险。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(手机号, max_length11, blankTrue) address models.CharField(收货地址, max_length255, blankTrue) class Meta: verbose_name 用户 verbose_name_plural verbose_name def __str__(self): return self.username继承 AbstractUser 而不是重写一个全新模型可以保留 Django 自带的登录、权限系统同时增加两个业务字段。需要在 settings.py 中声明AUTH_USER_MODEL accounts.User并且在所有外键里都指向这个自定义用户模型而不是直接写ForeignKey(User)。后面订单表的 user 字段也要用settings.AUTH_USER_MODEL字符串来引用这样 Django 在加载模型时不会因为 App 加载顺序报错。3.3 商品、订单与订单明细的模型实现商品表是商城最核心的模型。这里要特别注意价格字段的数据类型课程设计里最常见的错误是用 FloatField但浮点数在金额计算时会出现 0.1 加 0.2 不等于 0.3 的问题。Django 官方推荐使用 DecimalField配合 max_digits 和 decimal_places 两个参数保证精度。from django.conf import settings from django.db import models class Category(models.Model): name models.CharField(分类名, max_length32, uniqueTrue) parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, related_namechildren ) class Meta: verbose_name 商品分类 verbose_name_plural verbose_name def __str__(self): return self.name class Product(models.Model): category models.ForeignKey( Category, on_deletemodels.PROTECT, related_nameproducts ) title models.CharField(商品标题, max_length120) price models.DecimalField(单价, max_digits10, decimal_places2) stock models.PositiveIntegerField(库存, default0) image models.ImageField(商品图, upload_toproducts/, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [-created_at] verbose_name 商品 verbose_name_plural verbose_name def __str__(self): return self.titleCategory 里的parent是自关联外键实现了多级分类比如“数码”下挂“手机”。on_deletemodels.PROTECT表示如果分类下还有商品禁止直接删除分类这比 CASCADE 更符合真实电商场景。Product 里用ForeignKey(Category, on_deletemodels.PROTECT)也是同样的道理。related_name 指定反向查询名后续可以通过category.products.all()拿到某个分类下的所有商品。订单和订单明细需要明确区分订单表存一笔订单的整体信息明细表存每件商品的快照。快照的意思是把商品名称和单价冗余到订单明细里因为商品价格将来可能调整历史订单必须保留下单时的价格。class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), (cancelled, 已取消), ] user models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameorders ) total_amount models.DecimalField(订单金额, max_digits10, decimal_places2) status models.CharField(订单状态, max_length16, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(下单时间, auto_now_addTrue) class Meta: verbose_name 订单 verbose_name_plural verbose_name class OrderItem(models.Model): order models.ForeignKey( Order, on_deletemodels.CASCADE, related_nameitems ) product models.ForeignKey(Product, on_deletemodels.PROTECT) product_name models.CharField(商品快照, max_length120) price models.DecimalField(成交单价, max_digits10, decimal_places2) quantity models.PositiveIntegerField(购买数量) class Meta: verbose_name 订单明细 verbose_name_plural verbose_nameOrderItem 里冗余了 product_name 和 price 两个字段即上面提到的快照机制。product 外键只用于关联查询商品详情即使将来商品被删除或改名历史订单的展示也不受影响。on_deletemodels.PROTECT在这里意味着商品被下单后不能直接物理删除这也是一个可以主动讲给老师听的设计细节。3.4 执行迁移与写入初始数据模型定义完后执行迁移让数据库把表建出来。建议迁移和初始数据分开操作方便后续调整模型结构。python manage.py makemigrations accounts goods orders python manage.py migrate python manage.py createsuperusermakemigrations 会扫描三个 App 下的 models.py生成迁移文件migrate 真正执行建表。createsuperuser 创建后台管理员账号用于后面登录 Django Admin。如果此时没有设置 AUTH_USER_MODEL这一步可能会遇到模型冲突所以自定义用户模型一定要在最早期完成。初始数据不建议手工一条条添加可以写一个 Django 管理命令来批量生成分类和商品。下面这段代码放到 goods/management/commands/init_goods.py 里之后就能用python manage.py init_goods一键导入。from django.core.management.base import BaseCommand from goods.models import Category, Product class Command(BaseCommand): help 初始化商品分类和测试商品 def handle(self, *args, **options): electronics, _ Category.objects.get_or_create(name数码) phone, _ Category.objects.get_or_create(name手机, parentelectronics) products [ {title: 旗舰手机 12, price: 3999.00, stock: 100}, {title: 蓝牙耳机, price: 199.00, stock: 300}, ] for item in products: Product.objects.get_or_create( categoryphone, titleitem[title], defaults{ price: item[price], stock: item[stock], }, ) self.stdout.write(self.style.SUCCESS(商品初始化完成))管理命令用 get_or_create 避免重复执行造成数据堆积根据 title 和 category 判断记录是否已存在defaults 里的字段只在创建时生效。这种方式在课程设计里非常实用删库后重新跑一条命令就能恢复全部演示数据。4. 商品列表、购物车与下单把核心业务用 Django 视图串起来4.1 商品列表和搜索用类视图代码更少商品列表页建议用 Django 内置的 ListView。它天然支持分页还省去写if request.method GET这种模板代码。购物商城系统的商品列表通常还要带搜索功能在 get_queryset 里追加过滤条件即可。from django.views.generic import ListView, DetailView 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().select_related(category) kw self.request.GET.get(keyword, ).strip() if kw: qs qs.filter(title__icontainskw) return qs class ProductDetailView(DetailView): model Product template_name goods/product_detail.html context_object_name productpaginate_by 12让每页显示 12 个商品模板里可以用is_paginated判断是否需要显示分页条。select_related(category)一次查询把关联的分类数据带出来避免在模板里循环访问product.category.name时产生 N1 次数据库查询。title__icontainskw是 Django ORM 里的模糊查询icontains 不区分大小写。4.2 购物车为什么要放 Session 而不是直接写数据库购物车是课程设计答辩的经典提问点。把购物车存数据库实现很简单但真实项目里更常见的做法是先放 Session用户登录后或结算时再同步到服务端。Session 购物车的优势是无状态、不占用数据库连接用户把商品加入购物车不需要强制登录。我一般用类封装 Session 购物车让视图代码更干净。class SessionCart: SESSION_KEY cart def __init__(self, request): self.session request.session self.cart self.session.get(self.SESSION_KEY, {}) def add(self, product, quantity1): pid str(product.id) if pid in self.cart: self.cart[pid][quantity] quantity else: self.cart[pid] { quantity: quantity, price: str(product.price), } self.save() def remove(self, product_id): if str(product_id) in self.cart: del self.cart[str(product_id)] self.save() def total_amount(self): total 0 for item in self.cart.values(): total item[quantity] * float(item[price]) return round(total, 2) def save(self): self.session[self.SESSION_KEY] self.cart self.session.modified TrueSessionCart 的 add 方法接收一个 Product 对象用商品主键作 key如果商品已经在购物车里就增加数量否则新增一条记录。price 转成字符串存储是为了规避 JSON 序列化时 Decimal 无法直接转换的问题。total_amount 在每次页面刷新时重新计算而不是把总价存进 Session这样即使用户改了前端提交的价格后端也能重新算一遍这是购物车里非常重要的安全细节。视图里使用它的方式也很直接。def add_to_cart(request, pk): product get_object_or_404(Product, pkpk) cart SessionCart(request) cart.add(product, quantity1) return redirect(goods:cart_detail)提示Session 有有效期和大小限制默认只有 30 天且适合存小体量数据。如果商品数量很多可以改用数据库购物车模型把第 3 章中的 Cart 表用起来Session 方案强调无状态数据库方案强调持久化两者答辩时都能站得住。4.3 下单时的库存扣减与并发安全库存扣减是整个商城代码里最容易踩坑的地方。最保守的方案是先查出商品判断库存够不够再减库存。但在并发请求下两个请求同时读到库存为 1都通过了判断就会出现超卖。解决思路是用select_for_update()锁定行把扣减包进事务。from django.db import transaction from django.shortcuts import get_object_or_404 from decimal import Decimal transaction.atomic def create_order(request, product_id, quantity): product get_object_or_404(Product.objects.select_for_update(), pkproduct_id) if product.stock quantity: return False, 库存不足 product.stock - quantity product.save() order Order.objects.create( userrequest.user, total_amountproduct.price * quantity, statuspending, ) order.items.create( productproduct, product_nameproduct.title, priceproduct.price, quantityquantity, ) return True, ordertransaction.atomic保证代码块里的数据库操作要么全部成功要么全部回滚。select_for_update()在事务内对商品行加写锁其他事务必须等这个事务提交才能操作同一行。这样即使两个请求同时进来第二个请求在锁释放后才能读取库存此时库存已经被第一个请求扣减。product.price * quantity返回 Decimal 类型不需要担心 Float 精度。下面的表格可以帮你理解不同并发场景下的表现场景不加锁加 select_for_update两个请求同时买最后一件商品可能都通过库存判断导致库存变负第二个请求看到库存为 0提示库存不足订单创建失败库存可能已扣但订单没生成事务回滚库存自动还原用户刷新页面重复提交产生重复订单仍需额外做幂等建议前端加防重复提交课程设计阶段能做到锁行加事务已经是高分水平。如果老师追问更极端的情况可以补充说生产环境一般还会加 Redis 预扣库存和消息队列削峰但这些不是必须实现的内容能说清楚原理即可。5. 用 Admin 后台和代码分层提升课程设计的完成度5.1 让 Django Admin 直接管理商品、订单与库存Django 自带的后台是课程设计的加分项不需要额外写页面就能完成商品上下架、订单状态修改。Admin 的默认展示比较朴素但通过几行配置就能变成像样的管理系统。from django.contrib import admin from .models import Product, Category admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display [id, name, parent] search_fields [name] admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display [id, title, category, price, stock, created_at] list_editable [stock, price] list_filter [category, created_at] search_fields [title] list_per_page 20list_editable可以直接在列表页修改库存和价格演示时不用进入编辑页就能调整商品数据视觉冲击力很强。list_filter在右侧生成筛选栏老师想查看某个分类下所有商品时点一下就行。search_fields给后台搜索框配置检索字段这里只用了 title也可以加上category__name实现按分类名搜索。订单是另一个高频管理对象需要配合 Inline 把订单明细直接嵌入编辑页。class OrderItemInline(admin.TabularInline): model OrderItem extra 0 readonly_fields [product_name, price, quantity] admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display [id, user, total_amount, status, created_at] list_filter [status, created_at] search_fields [user__username] inlines [OrderItemInline]extra 0表示不显示空的明细行。readonly_fields防止从后台手改明细里的快照价格保证订单金额和明细一致。这里把 status 设置为可修改字段演示时就能从后台把订单从未支付改成已发货完整展示订单状态流转。5.2 把业务逻辑从视图里拆出来代码结构向真实项目靠拢很多课程设计代码的问题不在于功能缺失而在于视图函数越来越长最终变成一个几百行的大函数。为了让代码看起来更专业可以引入 service 层。视图只负责接收请求、解析参数、调用 serviceservice 负责业务规则模型只负责数据字段定义。from decimal import Decimal from django.db import transaction from ..models import Order, OrderItem, Product class OrderService: staticmethod transaction.atomic def create_order(user, cart): if not cart.items: raise ValueError(购物车为空) total Decimal(0.00) order Order.objects.create( useruser, total_amounttotal, statuspending, ) for cart_item in cart.items: product Product.objects.select_for_update().get(pkcart_item[product_id]) if product.stock cart_item[quantity]: raise ValueError(f{product.title} 库存不足) product.stock - cart_item[quantity] product.save() total product.price * cart_item[quantity] OrderItem.objects.create( orderorder, productproduct, product_nameproduct.title, priceproduct.price, quantitycart_item[quantity], ) order.total_amount total order.save() return order这个 OrderService 把第 4 章里的下单逻辑挪进了独立类购物车由 Session 里的字典转换成 items 列表传入。视图层只需要调用OrderService.create_order(request.user, cart_data)代码可读性和测试友好度都会提升。答辩时如果老师问“业务逻辑和数据显示怎么分离的”就可以直接指向这个 service 文件。5.3 一键初始化演示数据与后台美化课程设计的验收场景通常是老师打开网站看到首页有商品、后台有订单记录。为了避免现场临时录入数据的尴尬写一个初始化命令把演示数据一次生成。# orders/management/commands/init_demo.py from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from decimal import Decimal from goods.models import Product from orders.models import Order, OrderItem import random class Command(BaseCommand): help 生成演示订单数据 def handle(self, *args, **options): User get_user_model() user, _ User.objects.get_or_create( usernamedemo, defaults{password: pbkdf2_sha256$...}, ) products list(Product.objects.all()) if not products: self.stdout.write(self.style.ERROR(请先执行 init_goods)) return for _ in range(5): order Order.objects.create( useruser, total_amountDecimal(0.00), statusrandom.choice([pending, paid, shipped, completed]), ) total Decimal(0.00) for _ in range(random.randint(1, 3)): product random.choice(products) quantity random.randint(1, 2) OrderItem.objects.create( orderorder, productproduct, product_nameproduct.title, priceproduct.price, quantityquantity, ) total product.price * quantity order.total_amount total order.save() self.stdout.write(self.style.SUCCESS(演示订单生成完成))随机订单可以让后台的数据分布更接近真实场景订单状态覆盖全部状态字段演示时可以按状态筛选给老师看。列表里加状态过滤后整个后台的可验证性会明显提升。Admin 界面美化方面如果不想引入额外依赖可以给 Admin 模板覆盖一个简单的 base_site.html 加上自定义 CSS把 Django Administration 的标题改成你自己的商城名称。6. 答辩演示前必查的三个细节切 MySQL、缓存提速、讲清楚 Session6.1 课程设计要不要换成 MySQLSQLite 作为默认数据库非常适合开发阶段但课程设计通常会被要求使用 MySQL这也是“源码数据库”这个标题里最容易被检验的点。切换时只需修改 settings.py 的 DATABASES 配置。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: shopping_mall, USER: root, PASSWORD: yourpassword, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }utf8mb4 字符集是必须的否则商品标题里的 emoji 表情会保存失败。切换后需要重新执行 makemigrations 和 migrate。如果之前用 SQLite 录过数据可以用 dumpdata 导出 JSON切库后再 loaddata 导入。常见的坑是 mysqlclient 安装失败Linux 下需要先装libmysqlclient-devWindows 下建议直接使用预编译的 wheel 包。6.2 给首页加速一行配置的缓存技巧商品首页在演示时可能被反复刷新每次刷新都查一次数据库其实没必要。Django 提供了页面级缓存装饰一下就生效。from django.views.decorators.cache import cache_page from django.urls import path from .views import ProductListView, ProductDetailView urlpatterns [ path(, cache_page(60 * 15)(ProductListView.as_view()), nameproduct_list), path(product/int:pk/, ProductDetailView.as_view(), nameproduct_detail), ]cache_page(60 * 15)把整个商品列表页缓存 15 分钟。首次访问时会执行数据库查询并生成 HTML后续访问直接返回缓存内容。这个细节在答辩时可以作为“我考虑了性能优化”的依据不过要注意如果后台改了商品价格列表页最长要 15 分钟后才更新所以演示修改价格后最好刷新后台时顺手清一下缓存或者把缓存时间改成更短的 30 秒。6.3 老师最可能问的三个问题怎么答最后用一张表梳理课程设计答辩中最高频的追问和推荐回答方向。老师的问题高分回答思路购物车为什么用 Session 不用数据库表Session 无状态、降低数据库压力未登录用户也能加购登录后可通过信号同步到服务端下单时库存会不会超卖使用 select_for_update 行锁加事务保证并发场景下只有一个请求能扣减同一件商品数据库表为什么这样设计订单明细存商品快照历史订单不随商品改价而变动分类表自关联实现多级分类能把这些点讲清楚比单纯演示页面效果更有说服力。面对“如果你来做生产版本会改什么”这类问题可以回答把 Session 购物车换成 Redis、订单创建改成异步任务、库存预扣加超时回滚这些用已经掌握的 transaction 和缓存概念就能推导出来。课程设计的价值不在功能多而在每一层代码都能说出“为什么”。本文还有配套的精品资源点击获取
返回列表