基于Django+Vue的运动商城系统开发实战:从数据模型到订单流程
2026/9/17 1:17:41 网站建设 项目流程

简介:一份面向Python课程设计场景的运动商城系统完整项目,基于Django与Vue实现前后端分离,后端通过Django编写API接口,前端Vue代码已打包并置于static目录,可直接运行体验,也适合学习商城业务逻辑、接口设计与部署流程。压缩包共119个文件,大小12.08MB,涵盖Python源码、pyc编译文件、SQL数据库脚本、前端静态资源(JS/CSS/HTML/图片)以及Markdown格式的说明文档,目录结构清晰,便于查阅和二次开发。项目内置了运动商城典型功能模块,附带详细项目文档与API接口文档,数据库脚本可一键导入MySQL,运行环境要求与依赖安装均有明确说明,能帮助快速搭建完整环境。已有190人学习/下载,对正在做课程设计或想掌握Django+Vue整合开发的学习者来说,是一份直接可用的实战参考资料。

1. 运动商城系统的技术选型与整体架构

如果你正卡在 Python 课程设计的最后两周,又恰好需要一套能演示、能答辩、能写进文档的项目,那“基于 Django + Vue 的运动商城系统”几乎是当下最稳妥的组合。Django 负责把后台的数据模型、用户认证、订单逻辑一次理清,Vue 负责把商品列表、购物车、结算页面做得有模有样,两者通过 RESTful API 通信,职责边界清楚,既能说明白原理,又不至于在一周内把自己写崩。

这套方案面向的读者很具体:计算机相关专业要做课程设计或毕业设计的学生,以及想快速搭建一个“前后端分离商城”做练手的一线开发者。你需要掌握的基础并不高,Python 基础语法、Django 的 MTV 模式、Vue 的组件化思维,只要这三样各懂六成就能起步。数据库用的是 MySQL 或 SQLite,后者更适合本地演示,前者更能在答辩时体现“真实项目感”。

整套系统的核心链路是:Vue 页面通过 Axios 调用 Django 提供的 API 接口,Django 收到请求后经过 URL 路由分发到视图函数或类视图,视图再通过 ORM 操作数据库,最后把 JSON 数据返回给前端渲染。这条链路里最值得花时间打磨的是数据库设计,因为它决定了商品、用户、订单、购物车这四张核心表之间的关系,也决定了你后续写接口时是顺畅还是到处补丁。

2. Django 后端核心模型设计与数据库初始化

2.1 为什么先设计数据模型而不是先写页面

很多初学者习惯先把页面画出来,再回头补后端,结果往往是前端写了十几个组件,后端还不知道用户表里该放哪些字段。做商城类项目时我会先梳理业务对象:用户、商品分类、商品、购物车项、订单、订单项。这六类对象基本覆盖了一个小型电商系统的全部数据需求,也是你数据库里的六张核心表。

用户表建议直接扩展 Django 内置的AbstractUser,不要自己从零写认证逻辑。这样做的好处是 Django 自带登录、注册、会话管理,你只需增加手机号、收货地址、头像等业务字段,就能满足课程设计的“用户模块”要求。商品分类表用自关联外键实现两级分类,运动商城通常分为“跑步、健身、球类、户外”四大类,每类下再挂具体商品。

2.2 编写 models.py 的完整代码

在 Django 项目的models.py中,核心模型的写法大致如下:

from django.db import models from django.contrib.auth.models import AbstractUser # 扩展 Django 自带的用户模型,增加商城业务字段 class User(AbstractUser): phone = models.CharField(max_length=11, unique=True, verbose_name='手机号') address = models.CharField(max_length=255, blank=True, verbose_name='收货地址') avatar = models.ImageField(upload_to='avatars/', blank=True, verbose_name='头像') class Meta: verbose_name = '用户' verbose_name_plural = verbose_name # 运动商品分类,自关联实现两级分类 class Category(models.Model): name = models.CharField(max_length=50, verbose_name='分类名称') parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name='父分类') class Meta: verbose_name = '商品分类' verbose_name_plural = verbose_name def __str__(self): return self.name # 运动商品表,关键字段为价格和库存 class Product(models.Model): name = models.CharField(max_length=100, verbose_name='商品名称') category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products', verbose_name='所属分类') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格') stock = models.PositiveIntegerField(default=0, verbose_name='库存') sales = models.PositiveIntegerField(default=0, verbose_name='销量') description = models.TextField(blank=True, verbose_name='商品详情') image = models.ImageField(upload_to='products/', blank=True, verbose_name='商品图片') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') is_on_sale = models.BooleanField(default=True, verbose_name='是否上架') class Meta: verbose_name = '运动商品' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return self.name # 购物车项,一个用户对应多个商品 class CartItem(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='cart_items') product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name='商品') quantity = models.PositiveIntegerField(default=1, verbose_name='数量') selected = models.BooleanField(default=True, verbose_name='是否选中') created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '购物车项' verbose_name_plural = verbose_name # 订单主体,记录订单状态和用户信息 class Order(models.Model): STATUS_CHOICES = ( ('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) order_no = models.CharField(max_length=32, unique=True, verbose_name='订单号') user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders') total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单总金额') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='订单状态') receiver_name = models.CharField(max_length=50, verbose_name='收货人') receiver_phone = models.CharField(max_length=11, verbose_name='收货电话') receiver_address = models.CharField(max_length=255, verbose_name='收货地址') created_at = models.DateTimeField(auto_now_add=True, verbose_name='下单时间') paid_at = models.DateTimeField(null=True, blank=True, verbose_name='支付时间') class Meta: verbose_name = '订单' verbose_name_plural = verbose_name ordering = ['-created_at'] # 订单项,记录订单中每件商品的快照 class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) product_name = models.CharField(max_length=100, verbose_name='商品名称快照') product_price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='商品价格快照') quantity = models.PositiveIntegerField(default=1, verbose_name='购买数量') class Meta: verbose_name = '订单项' verbose_name_plural = verbose_name

这套模型设计的核心逻辑有两点。第一,订单项中保存了product_nameproduct_price这两个快照字段,好处是即使商品在后续被修改或删除,历史订单中的数据仍然完整,这在答辩演示时很有说服力。第二,商品价格使用DecimalField而不是FloatField,避免浮点数精度问题带来的价格计算误差——这属于实战项目中才会注意到的细节,写进文档是加分项。

2.3 数据库迁移与初始化命令

模型定义完成后,需要依次执行数据库迁移命令。这里推荐使用 MySQL,原因是在课程设计文档中写“使用 MySQL 5.7 构建关系型数据存储”会显得项目更完整。先安装驱动:

pip install mysqlclient

然后在settings.py中配置数据库连接:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'sport_mall', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', } } }

关键参数说明:utf8mb4字符集必须设置,否则中文商品名在存储时可能出现编码问题。接着执行迁移命令:

python manage.py makemigrations python manage.py migrate

迁移完成后,需要创建超级管理员账号,用于登录 Django 后台:

python manage.py createsuperuser

最后启动开发服务器,访问http://127.0.0.1:8000/admin就能看到 Django 自带的数据库后台管理界面。建议在后台手动录入几条运动商品数据,比如“李宁跑步鞋”、“威尔胜篮球”、“迪卡侬瑜伽垫”,这样前端页面有数据可渲染,演示效果更直观。

3. Vue 前端页面结构与 API 对接实现

3.1 创建 Vue 项目并配置开发环境

Vue 前端建议使用 Vue CLI 创建项目,版本选择 Vue 2,因为它与 Element UI 组件库配合最稳定,也是当前课程设计中使用最广泛的组合。环境准备包括 Node.js 18+,安装好之后执行:

npm install -g @vue/cli vue create sport-mall-frontend

创建项目时选择 Manually select features,勾选 Router 和 Vuex。进入项目目录后安装 UI 组件库和 Axios:

npm install element-ui axios

安装 Element UI 后,在main.js中全局引入:

import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import App from './App.vue' Vue.use(ElementUI) new Vue({ router, store, render: h => h(App) }).$mount('#app')

组件库的引入是前端开发效率的分水岭。不引入任何 UI 框架,你要花大量时间写 CSS 来让页面“看得过去”;引入 Element UI 后,商品卡片、导航栏、分页组件都能直接用现成的,而且呈现出的视觉效果远好于手写样式。务必在项目文档中写明这一选型理由。

3.2 封装 Axios 请求并处理跨域问题

前后端分离项目绕不开跨域问题。Django 后端默认只允许同源请求,需要安装django-cors-headers来解决:

pip install django-cors-headers

settings.py中添加应用并配置允许跨域的来源:

INSTALLED_APPS = [ # 其他应用 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 其他中间件 ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:8080', ]

前端这边把 Axios 封装成统一的请求模块,方便所有组件调用。在src/utils/request.js中写入:

import axios from 'axios' import { Message } from 'element-ui' // 创建 axios 实例,配置基础 URL 和超时时间 const service = axios.create({ baseURL: 'http://127.0.0.1:8000/api', timeout: 10000 }) // 请求拦截器:从 localStorage 读取 token 并附加到请求头 service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'JWT ' + token } return config }, error => { return Promise.reject(error) } ) // 响应拦截器:统一处理错误状态码 service.interceptors.response.use( response => { return response.data }, error => { Message.error(error.response?.data?.detail || '请求失败') return Promise.reject(error) } ) export default service

参数说明:baseURL指向 Django 后端的 API 地址,开发阶段用127.0.0.1:8000,部署时可以改为服务器域名。请求拦截器的作用是每次请求自动携带 token,这样后端能够识别当前登录用户;响应拦截器把错误信息统一弹出提示,避免每个组件重复处理异常逻辑。

3.3 商品列表页与购物车状态管理

商品列表页是运动商城的门户,也是 Vue 组件化开发的典型场景。在src/views/ProductList.vue中,页面结构分为三个部分:顶部导航栏、商品分类侧边栏、商品卡片网格。

<template> <div class="product-list"> <el-row :gutter="20"> <el-col :span="6"> <el-menu default-active="0" @select="handleCategorySelect" > <el-menu-item index="0">全部商品</el-menu-item> <el-menu-item v-for="category in categories" :key="category.id" :index="String(category.id)" > {{ category.name }} </el-menu-item> </el-menu> </el-col> <el-col :span="18"> <el-row :gutter="16"> <el-col v-for="product in products" :key="product.id" :span="8" > <el-card class="product-card"> <img :src="product.image" class="product-image" /> <div class="product-name">{{ product.name }}</div> <div class="product-price">¥{{ product.price }}</div> <el-button type="primary" size="small" @click="addToCart(product)" > 加入购物车 </el-button> </el-card> </el-col> </el-row> <el-pagination layout="prev, pager, next" :total="total" :page-size="6" @current-change="handlePageChange" > </el-pagination> </el-col> </el-row> </div> </template> <script> import request from '../utils/request' export default { name: 'ProductList', data() { return { categories: [], products: [], currentCategory: 0, currentPage: 1, total: 0 } }, created() { this.fetchCategories() this.fetchProducts() }, methods: { // 获取商品分类列表 async fetchCategories() { const res = await request.get('/categories/') this.categories = res }, // 获取商品列表,支持分类筛选与分页 async fetchProducts() { const params = { page: this.currentPage, category: this.currentCategory || undefined } const res = await request.get('/products/', { params }) this.products = res.results this.total = res.count }, handleCategorySelect(index) { this.currentCategory = Number(index) this.currentPage = 1 this.fetchProducts() }, handlePageChange(page) { this.currentPage = page this.fetchProducts() }, // 加入购物车操作,需要登录状态 async addToCart(product) { try { await request.post('/cart/', { product_id: product.id, quantity: 1 }) this.$message.success('已加入购物车') } catch (error) { this.$message.error('请先登录') } } } } </script>

这段代码中,request是从前面封装的 Axios 模块导入的,所以每个方法都自动携带了 token。fetchProducts中把currentCategorycurrentPage作为查询参数传给后端,Django 后端的视图通过分页器处理这两个参数,返回resultscount字段。这种接口结构是 Django REST Framework 分页的默认返回格式,前端只需按约定解析。

3.4 购物车状态管理与 Vuex 模块化

购物车是商城系统的核心交互区域。在 Vuex 中单独建立一个cart模块,状态包含购物车商品列表、总数量、总金额三个字段:

// src/store/modules/cart.js import request from '../../utils/request' export default { namespaced: true, state: { items: [], totalCount: 0, totalAmount: 0 }, mutations: { SET_ITEMS(state, items) { state.items = items state.totalCount = items.reduce((sum, item) => sum + item.quantity, 0) state.totalAmount = items.reduce( (sum, item) => sum + item.quantity * item.product.price, 0 ) } }, actions: { // 获取当前用户的购物车数据 async fetchCart({ commit }) { const res = await request.get('/cart/') commit('SET_ITEMS', res) }, // 更新商品数量 async updateQuantity({ dispatch }, { itemId, quantity }) { await request.patch(`/cart/${itemId}/`, { quantity }) dispatch('fetchCart') } } }

购物车模块的核心技巧是把“总数量”和“总金额”通过 Vuex 的getters计算出来,而不是在每个组件里重复写 reduce 逻辑。这样不管是购物车页面还是结算页面,只要调用同一个 getter 就能拿到一致的数据,避免了多个页面数据不同步的问题。

4. 运动商城的订单流程与支付模块实现

4.1 订单状态机设计与状态流转控制

订单模块是整个商城系统中业务逻辑最复杂、也最值得在答辩时展开讲解的部分。一个完整的运动商城订单至少要经历五个状态:待支付、已支付、已发货、已完成、已取消。状态之间的合法流转关系需要在后端用代码约束,否则会出现“已取消的订单突然变成已发货”的脏数据。

在 Django 的Order模型中定义一个状态流转校验方法:

class Order(models.Model): # 字段定义见前文,此处省略 def can_transition_to(self, new_status): """检查订单状态是否允许从当前状态流转到目标状态""" allowed_transitions = { 'pending': ['paid', 'cancelled'], 'paid': ['shipped', 'cancelled'], 'shipped': ['completed'], 'completed': [], 'cancelled': [], } return new_status in allowed_transitions.get(self.status, [])

参数说明:allowed_transitions字典定义了每个状态可以跳转到的合法状态集合。例如待支付订单可以直接取消,也可以支付后变成已支付;已支付的订单只能发货或取消,不能跳回待支付。这个方法的调用时机是关键——在订单视图更新状态之前必须先调用can_transition_to,校验不通过则直接返回错误响应。

在订单创建接口中,完整的事务逻辑代码如下:

from django.db import transaction from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status class CreateOrderView(APIView): def post(self, request): user = request.user cart_items = user.cart_items.filter(selected=True) if not cart_items.exists(): return Response({'detail': '购物车中没有选中的商品'}, status=status.HTTP_400_BAD_REQUEST) # 生成订单号:时间戳+用户ID+随机数 order_no = f'{timezone.now().strftime("%Y%m%d%H%M%S")}{user.id}' with transaction.atomic(): # 创建订单主体 order = Order.objects.create( order_no=order_no, user=user, total_amount=0, status='pending', receiver_name=request.data.get('receiver_name'), receiver_phone=request.data.get('receiver_phone'), receiver_address=request.data.get('receiver_address') ) total_amount = 0 order_items = [] for item in cart_items: product = item.product if item.quantity > product.stock: raise ValueError(f'{product.name} 库存不足') # 扣减库存、增加销量 product.stock -= item.quantity product.sales += item.quantity product.save() # 创建订单项快照 order_items.append(OrderItem( order=order, product=product, product_name=product.name, product_price=product.price, quantity=item.quantity )) total_amount += product.price * item.quantity OrderItem.objects.bulk_create(order_items) order.total_amount = total_amount order.save() # 清空购物车中已下单的商品(持久化到数据库的 item 对象在这里消费) selected_ids = list(cart_items.values_list('id', flat=True)) user.cart_items.filter(id__in=selected_ids).delete() return Response({'order_no': order.order_no, 'total': float(total_amount)}, status=status.HTTP_201_CREATED)

这段代码中有几个需要重点理解的细节。

transaction.atomic()是关键中的关键。订单创建涉及多张表的写入操作——创建订单主体、创建订单项、扣减商品库存、增加商品销量、清空购物车——任何一步失败都会导致数据不一致。使用事务包裹后,要么全部成功提交,要么全部回滚,这是课程设计答辩中最容易被打分老师追问的地方。

bulk_create方法用于批量创建订单项,避免在循环中逐条执行 SQL,性能上比逐个 save 快得多。购物车的删除操作使用filter(id__in=selected_ids)批量删除,而不是循环删除。这些写法体现了对 ORM 性能的考量。

4.2 支付回调接口与订单状态更新

真实商城系统中,支付功能会对接支付宝或微信支付的开放 API。课程设计阶段不强制要求对接真实支付渠道,但需要实现一个模拟支付接口,演示完整的支付流程。

class MockPayView(APIView): def post(self, request, order_no): try: order = Order.objects.get(order_no=order_no) except Order.DoesNotExist: return Response({'detail': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) # 校验当前订单状态必须是待支付 if order.status != 'pending': return Response({'detail': '订单状态不允许支付'}, status=status.HTTP_400_BAD_REQUEST) order.status = 'paid' order.paid_at = timezone.now() order.save() return Response({'detail': '支付成功'}, status=status.HTTP_200_OK)

这个模拟支付接口的核心逻辑是状态校验:只有待支付订单才能发起支付,已经取消或已经支付的订单会被拒绝。真实接入支付宝时,订单号会和支付宝交易号做关联,并通过异步回调来确认支付结果。建议在项目文档中写明对接真实支付时需要在回调接口中验签、查询订单号、更新订单状态这三个步骤。

5. 项目文档组织与后续扩展技巧

5.1 课程设计文档的目录结构与写作要点

运动商城系统的技术实现只是完成了一半工作,课程设计的另一半是写出一份像样的项目文档。文档的作用不仅是给老师看,更是帮助你自己理清思路——写文档的过程中往往能发现代码中的逻辑漏洞。

一份标准的课程设计文档建议按以下结构组织:

章节内容要点篇幅参考
需求分析功能需求、用户角色、用例图3-5页
系统设计架构图、数据库ER图、接口设计5-8页
功能实现每个模块的截图+代码+说明8-12页
系统测试测试用例表、测试结果、边界情况3-5页
总结与展望遇到的问题与解决方案、可扩展方向1-2页

数据库 ER 图可以用 draw.io 或 ProcessOn 画,把六张表的字段名和关联关系画清楚,这对后续写作和答辩都是重要的可视化材料。

5.2 数据库备份与恢复的便捷命令

课程设计演示前,最好备份一份数据库,防止演示现场出现数据丢失的尴尬。MySQL 的备份和恢复命令:

# 备份整个运动商城数据库 mysqldump -u root -p sport_mall > sport_mall_backup.sql # 恢复数据库 mysql -u root -p sport_mall < sport_mall_backup.sql

如果是使用 SQLite 的项目,直接复制db.sqlite3文件即可完成备份。在项目文档中写明备份恢复命令,属于答辩时的加分项——它说明你考虑了数据安全性这一实际问题。

5.3 前后端联调时的接口调试技巧

开发过程中经常遇到前端页面报错但不知道是接口问题还是渲染问题。我的做法是在浏览器开发者工具中打开 Network 面板,观察具体请求的响应内容。如果是跨域报错,检查 Django 的 CORS 配置;如果是 JSON 结构对不上,先在后端视图函数里print输出数据,再比对前端解析逻辑。

在 Django 中开启 SQL 日志的输出,能看到每个接口背后的实际数据库查询语句:

# settings.py 底部添加 LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, }, }

开启后,控制台会输出每次数据库操作对应的 SQL 语句。比如商品列表页如果出现了 N+1 查询问题——假设有 10 个商品,却执行了 11 条 SQL(1条查询商品 + 10条查询分类)——能在日志中直接看到。解决方法是使用 Django ORM 的select_relatedprefetch_related优化查询,在数据库字段量大的项目中,这一步常成为答辩中的亮点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询