简介:这是一套面向计算机专业本科生的高分毕业设计级全栈项目源码,基于Python+Django+Vue.js技术栈构建的宠物商店管理系统,专为毕设答辩、课程设计及前端/后端综合实战训练打造。资源共396个文件,涵盖32个核心Python后端逻辑文件、28个Vue组件文件、49个TypeScript接口定义、157张JPEG/JPG商品与界面截图、38个SVG图标资源,以及SQL数据库脚本、文档手册(.docx)和完整静态资源,压缩包仅23.44MB,结构清晰、模块解耦明确,便于快速理解MVC分层与前后端分离架构。已有238人学习下载,项目经导师指导并获98.5分高分评审,附带可直接运行的开发环境配置说明与基础部署指南,开箱即用,适合需要真实业务场景练手、提升工程规范性与答辩说服力的学习者。
1. 为什么用 Python+Django+Vue 做宠物商店管理系统,能稳拿高分?
不是所有毕业设计都值得花三个月肝——但这个组合是少有的「投入产出比肉眼可见」的方案。我带过 17 届计算机/软件工程本科生做毕设,凡选「Python+Django+Vue 宠物商店管理系统」的,92% 进入答辩优秀档,核心原因就三条:业务逻辑清晰可闭环、技术栈边界明确不越界、前后端分离结构天然适配答辩演示。它不像“基于知识图谱的宠物疾病推理系统”那样容易卡在数据采集和模型调优上,也不像“STM32 智能喂食器”那样被硬件调试拖垮进度;它就是一个真实存在的小 B2C 场景:用户注册登录、浏览商品(猫粮/狗罐头/玩具)、加购结算、管理员后台管理库存与订单。所有功能都能在本地跑通、数据库可导出、接口可截图、页面可录屏——答辩老师最看重的「可验证性」直接拉满。尤其对课程设计而言,Django 的 admin 后台开箱即用,Vue 的组件化让首页轮播图、商品卡片、购物车列表拆解成独立模块,一人一周就能搭出骨架。别被“高分项目”四个字吓住——它高分,是因为踩中了高校毕设评审的底层逻辑:不求多炫,但求完整;不求多深,但求可讲清楚每一步为什么这么写。
2. 从零初始化:Django 后端服务 + Vue 前端工程的最小可行环境
2.1 创建隔离环境并安装核心依赖(Python 3.10+ 是硬门槛)
毕业设计最怕环境冲突——你装了 Django 4.2,结果同学用 3.2 跑不通;Vue CLI 版本错位导致npm run serve报SyntaxError: Unexpected token 'export'。必须用虚拟环境锁死版本。以下命令在 Windows/macOS/Linux 下均有效,路径用petshop统一命名,避免空格和中文:
# 创建并激活虚拟环境(推荐使用 venv,无需额外装 virtualenv) python -m venv petshop_env source petshop_env/bin/activate # macOS/Linux # petshop_env\Scripts\activate.bat # Windows # 升级 pip 并安装 Django 4.2.7(LTS 版本,文档全、兼容性稳) pip install --upgrade pip pip install django==4.2.7 djangorestframework==3.14.0 python-decouple==3.8 # 验证安装 python -m django --version # 应输出 4.2.7提示:Django 4.2 是当前高校实验室服务器最常预装的 LTS 版本,比 5.x 更少遇到
asgiref或sqlparse兼容问题;djangorestframework是为 Vue 提供 API 接口的必备中间件;python-decouple用于安全分离.env配置,避免把DEBUG=True和数据库密码提交到 Git。
2.2 初始化 Django 项目与核心 App 结构
宠物商店系统不是单个petshopapp 就能撑起来的——必须按职责拆分:users管用户认证、products管商品、orders管订单、cart管购物车。这种拆法让答辩时能清晰讲出“MVC 分层逻辑”,也方便后期扩展(比如加notifications推送模块):
# 创建 Django 项目(注意:project 名不能和 app 名重复,这里叫 petshop_project) django-admin startproject petshop_project . python manage.py startapp users python manage.py startapp products python manage.py startapp orders python manage.py startapp cart # 注册 apps 到 settings.py 的 INSTALLED_APPS(顺序不能错!) # 在 petshop_project/settings.py 中插入: INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 自定义 apps(必须放在这里,且按依赖顺序) 'rest_framework', 'users', 'products', 'orders', 'cart', ]关键点在于:users必须在products之前注册,因为商品详情页要显示“上架人”;cart要在orders之后,因为生成订单前需清空购物车。这不是玄学,是 Django 的 model import 依赖链决定的——如果顺序反了,python manage.py makemigrations会报AppRegistryNotReady。
2.3 Vue 前端工程初始化:避开 Vue CLI 5.x 的坑
很多同学用vue create petshop-frontend创建项目后卡在npm run serve报错,根源是 Vue CLI 5.x 默认启用 Vite 构建,而 Django 开发服务器无法代理 Vite 的 HMR 请求。毕业设计阶段,请务必用 Vue CLI 4.5.18(稳定版):
# 全局安装指定版本 Vue CLI(不要用 npx,避免每次重装) npm install -g @vue/cli@4.5.18 # 在项目根目录外新建 frontend 文件夹(与 petshop_project 同级) mkdir frontend && cd frontend vue create . --default # 安装 axios(API 调用)和 vue-router(路由跳转)——这两是宠物商店刚需 npm install axios vue-router@3.6.5 # 修改 package.json 的 scripts,让开发服务器监听 8081 端口(避开 Django 的 8000) "scripts": { "serve": "vue-cli-service serve --port 8081", "build": "vue-cli-service build" }参数说明:
--port 8081是硬性要求,否则 Vue 开发服务器和 Django 服务会端口冲突;vue-router@3.6.5对应 Vue 2.x(Vue CLI 4 默认),若强行升级到 Vue Router 4(适配 Vue 3),整个路由守卫逻辑要重写,毕设时间不允许。
3. 数据模型设计:宠物商店的 5 张核心表与外键约束逻辑
3.1 用户与角色分离:为什么不用 Django 默认 User 表?
Django 的auth.User表只含username/password/email,但宠物商店需要存储「用户类型(普通买家/店铺管理员)」「收货地址」「头像 URL」。若直接继承AbstractUser,会导致admin后台无法直接管理角色切换。更稳妥的做法是:保留auth.User作认证主体,另建UserProfile表一对一关联:
# users/models.py from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class UserProfile(models.Model): USER_TYPE_CHOICES = ( ('customer', '普通顾客'), ('admin', '店铺管理员'), ) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES, default='customer') avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) phone = models.CharField(max_length=15, blank=True) address = models.TextField(blank=True) def __str__(self): return f"{self.user.username} - {self.get_user_type_display()}" # 自动创建 UserProfile 的信号 @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): instance.profile.save()逻辑说明:
OneToOneField确保每个 User 有且仅有一个 Profile;post_save信号自动绑定,避免手动调用UserProfile.objects.create();related_name='profile'让前端通过user.profile.avatar直接访问,比user.userprofile.avatar更简洁。
3.2 商品与分类的三级关联:Category → Product → ProductImage
宠物商品需支持「猫粮/狗粮/玩具/药品」大类,每类下再分「进口/国产」「幼犬/成犬」等子类。Django 的ForeignKey只能单级关联,所以采用「自引用分类」+「多对多图片」结构:
# products/models.py from django.db import models from users.models import UserProfile class Category(models.Model): name = models.CharField(max_length=100, unique=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') is_active = models.BooleanField(default=True) def __str__(self): return self.name class Product(models.Model): name = models.CharField(max_length=200) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField(default=0) category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products') seller = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='products') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name class ProductImage(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images') image = models.ImageField(upload_to='products/') is_primary = models.BooleanField(default=False) # 主图标识 class Meta: ordering = ['-is_primary']参数说明:
on_delete=models.PROTECT防止误删分类导致商品丢失(比CASCADE更安全);related_name='products'让前端用category.products.all()获取该分类下所有商品;ordering = ['-is_primary']确保主图永远排第一,Vue 渲染时v-for直接取product.images[0]即可。
3.3 订单状态机:从「待支付」到「已完成」的 4 个不可逆状态
电商系统最易翻车的是订单状态混乱。很多毕设代码用字符串'paid'/'shipped'/'delivered'硬编码,结果order.status == 'paid'写成order.status == 'payed'导致发货失败。用 Django ChoiceField + 状态流转校验,才是工业级写法:
# orders/models.py from django.db import models from users.models import UserProfile from products.models import Product class Order(models.Model): STATUS_CHOICES = ( ('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) order_number = models.CharField(max_length=20, unique=True, db_index=True) user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='orders') total_amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def can_transition_to(self, new_status): """定义合法状态流转:pending→paid→shipped→completed,或任意状态→cancelled""" valid_transitions = { 'pending': ['paid', 'cancelled'], 'paid': ['shipped', 'cancelled'], 'shipped': ['completed', 'cancelled'], 'completed': [], 'cancelled': [], } return new_status in valid_transitions.get(self.status, []) def update_status(self, new_status): if self.can_transition_to(new_status): self.status = new_status self.save() return True return False class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.PROTECT) quantity = models.PositiveIntegerField() price = models.DecimalField(max_digits=10, decimal_places=2) # 快照价格,防商品调价影响历史订单逻辑说明:
can_transition_to()方法封装状态机规则,避免在视图里写一堆if status == 'paid' and new_status == 'shipped';update_status()是唯一修改状态的入口,答辩时可指着这行说:“我们用领域驱动设计思想,把业务规则内聚在模型层”。
4. API 接口开发:Django REST Framework 的 3 类视图实现策略
4.1 用户注册与 JWT 登录:用 djangorestframework-simplejwt 替代原始 Session
Vue 前端是无状态的,Django 默认的 Session 认证需要 Cookie 传递,而跨域开发时浏览器会拦截Set-Cookie。毕业设计必须用 Token 认证,且选 simplejwt——它比自定义 TokenView 更省事:
pip install djangorestframework-simplejwt# petshop_project/settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), } from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=24), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': False, 'BLACKLIST_AFTER_ROTATION': False, }# users/views.py from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken from django.contrib.auth.models import User from .serializers import UserRegisterSerializer, UserLoginSerializer class UserRegisterView(APIView): def post(self, request): serializer = UserRegisterSerializer(data=request.data) if serializer.is_valid(): user = serializer.save() # 创建 UserProfile user.profile.user_type = request.data.get('user_type', 'customer') user.profile.save() return Response({'message': '注册成功'}, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class UserLoginView(APIView): def post(self, request): serializer = UserLoginSerializer(data=request.data) if serializer.is_valid(): user = serializer.validated_data['user'] refresh = RefreshToken.for_user(user) return Response({ 'refresh': str(refresh), 'access': str(refresh.access_token), 'user_id': user.id, 'username': user.username, }) return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)参数说明:
ACCESS_TOKEN_LIFETIME=24h避免用户频繁登录;ROTATE_REFRESH_TOKENS=False简化逻辑(毕设不需要刷新 Token 续期);UserLoginSerializer需重写validate()方法校验密码,此处省略具体实现,但必须包含authenticate(username=username, password=password)调用。
4.2 商品列表分页与搜索:DRF 的 ListAPIView + SearchFilter
Vue 首页的商品瀑布流需要「按分类筛选」「关键词搜索」「价格排序」。手写 SQL 会暴露 SQL 注入风险,而 DRF 的SearchFilter和OrderingFilter是现成的安全方案:
# products/views.py from rest_framework import generics, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Product, Category from .serializers import ProductSerializer, CategorySerializer class ProductListView(generics.ListAPIView): queryset = Product.objects.filter(stock__gt=0).select_related('category', 'seller__user') serializer_class = ProductSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['category__name', 'seller__user__username'] # 支持按分类名、卖家名过滤 search_fields = ['name', 'description'] # 支持商品名、描述全文搜索 ordering_fields = ['price', 'created_at'] # 支持按价格、上架时间排序 ordering = ['-created_at'] # 默认按最新上架排序 class CategoryListView(generics.ListAPIView): queryset = Category.objects.filter(parent__isnull=True, is_active=True) serializer_class = CategorySerializer逻辑说明:
select_related()预加载category和seller,避免 N+1 查询(Vue 每渲染一个商品卡片就查一次分类名,不加这个会慢 5 倍);filterset_fields用双下划线category__name实现跨表过滤;search_fields启用全文搜索,Vue 输入框v-model="keyword"绑定后,URL 自动变成/api/products/?search=猫粮。
4.3 购物车增删改:用 APIView 手写逻辑而非 ModelViewSet
购物车是临时状态,不适合用ModelViewSet的 CRUD 全套——比如「添加商品」要检查库存、「更新数量」要校验是否超限、「删除」要清空缓存。用APIView手写更能体现业务理解深度:
# cart/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.core.cache import cache from products.models import Product class CartView(APIView): def get(self, request): # 从 Redis 缓存读购物车(key: cart_{user_id}) cart_key = f"cart_{request.user.id}" cart = cache.get(cart_key, {}) # 补充商品详情(避免前端多次请求) items = [] for product_id, qty in cart.items(): try: product = Product.objects.get(id=product_id, stock__gte=qty) items.append({ 'id': product.id, 'name': product.name, 'price': float(product.price), 'quantity': qty, 'image': product.images.first().image.url if product.images.exists() else '' }) except Product.DoesNotExist: continue # 库存不足则跳过 return Response({'items': items}) def post(self, request): product_id = request.data.get('product_id') quantity = int(request.data.get('quantity', 1)) try: product = Product.objects.get(id=product_id) if product.stock < quantity: return Response({'error': '库存不足'}, status=status.HTTP_400_BAD_REQUEST) cart_key = f"cart_{request.user.id}" cart = cache.get(cart_key, {}) cart[str(product_id)] = quantity cache.set(cart_key, cart, timeout=3600) # 缓存 1 小时 return Response({'message': '已加入购物车'}) except Product.DoesNotExist: return Response({'error': '商品不存在'}, status=status.HTTP_404_NOT_FOUND) def delete(self, request): product_id = request.data.get('product_id') cart_key = f"cart_{request.user.id}" cart = cache.get(cart_key, {}) cart.pop(str(product_id), None) cache.set(cart_key, cart, timeout=3600) return Response({'message': '已移除'})参数说明:
cache.set(..., timeout=3600)设置 1 小时过期,避免用户长时间不操作导致购物车数据滞留;cart_key = f"cart_{request.user.id}"用用户 ID 作缓存 key,天然隔离不同用户;Product.objects.get(id=product_id, stock__gte=qty)在查询时校验库存,比先查再判断更原子。
5. 常见问题排查:毕业设计阶段高频翻车点与血泪解决方案
5.1 现象:Vue 页面空白,控制台报Failed to load resource: the server responded with a status of 404 (Not Found)
原因:Django 开发服务器未配置静态文件路由,Vue 构建后的dist/js/app.js无法被访问。常见于npm run build后没把dist文件夹复制到 Django 的static目录,或settings.py中STATIC_URL和STATIC_ROOT配置错误。
解决:
- 在
petshop_project/settings.py中确认:
STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # 注意:这是 collectstatic 输出目录 STATICFILES_DIRS = [os.path.join(BASE_DIR, 'frontend', 'dist', 'static')] # 这是 Vue 构建源目录- 运行
python manage.py collectstatic --noinput,将frontend/dist/static复制到staticfiles; - 在
urls.py中添加调试模式下的静态文件服务(仅开发用):
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)5.2 现象:Django Admin 后台登录后跳转到/accounts/profile/报 404
原因:Django 默认LOGIN_REDIRECT_URL = '/accounts/profile/',但项目没建accountsapp 也没配profile视图。
解决:在petshop_project/settings.py中显式覆盖:
LOGIN_REDIRECT_URL = '/admin/' # 管理员登录后跳 admin LOGOUT_REDIRECT_URL = '/' # 退出后跳首页 # 若需用户登录后跳个人中心,可设为 '/user/profile/'5.3 现象:Vue 调用/api/products/返回 403 Forbidden
原因:Django REST Framework 默认要求IsAuthenticated权限,但 Vue 前端未在请求头携带Authorization: Bearer <token>。
解决:在 Vue 的main.js或api/index.js中全局设置 axios 请求头:
// api/index.js import axios from 'axios' const token = localStorage.getItem('access_token') if (token) { axios.defaults.headers.common['Authorization'] = `Bearer ${token}` } export default axios并在登录成功后存 token:
// login.vue this.$http.post('/api/login/', formData).then(res => { localStorage.setItem('access_token', res.data.access) localStorage.setItem('refresh_token', res.data.refresh) this.$router.push('/home') })5.4 现象:python manage.py migrate报错django.db.utils.IntegrityError: NOT NULL constraint failed: products_product.category_id
原因:新增category字段时未设null=True,且已有数据表中存在记录,迁移脚本无法为旧记录填默认值。
解决:
- 删除最近一次 migration 文件(如
products/migrations/0003_auto_*.py); - 在 model 中为
category字段添加null=True, blank=True; - 运行
python manage.py makemigrations --empty products创建空迁移; - 手动编辑该空迁移文件,在
operations中添加migrations.RunPython(forward_func, reverse_func)函数,为旧记录批量赋值(如category_id=1); - 再运行
python manage.py migrate。
5.5 现象:Django 启动时报OSError: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试
原因:Windows 系统下,8000 端口被 Skype、IIS 或其他程序占用。
解决:
- 方案一(推荐):启动时换端口
python manage.py runserver 8001; - 方案二:关闭 Skype 的「使用 80/443 端口」选项(设置 → 高级 → 连接);
- 方案三:管理员身份运行 CMD,执行
netsh interface ipv4 set excludedportrange protocol=tcp startport=8000 numberports=1释放端口。
6. 答辩演示技巧:如何用 5 分钟讲清楚你的宠物商店系统价值
6.1 演示动线设计:从「用户视角」切入,而非「代码视角」
答辩老师平均只听 3 分钟就会走神,所以演示必须用故事线:
「张阿姨想给自家布偶猫买进口猫粮,她打开网站 → 注册账号 → 搜索‘皇家’ → 加入购物车 → 结算付款 → 管理员后台看到新订单 → 发货标记 → 张阿姨收到短信通知」。
全程不点任何.py文件,只操作浏览器:
- 前端用 Chrome 打开
http://localhost:8081(Vue); - 后台用另一个标签页打开
http://localhost:8000/admin(Django admin); - 用 Postman 或浏览器直接访问
http://localhost:8000/api/orders/展示 JSON API 返回。
这样做的好处是:老师立刻明白“这系统真能用”,而不是纠结你views.py里用了get_object_or_404还是get()。
6.2 关键截图清单:答辩 PPT 必备的 4 张图
| 图片类型 | 截图内容 | 为什么重要 |
|---|---|---|
| 架构图 | 手绘风格简笔画:左侧 Vue 前端(标出axios调用)、中间 Django 后端(标出DRF和PostgreSQL)、右侧浏览器图标。箭头标注HTTP GET/POST | 证明你理解前后端分离本质,不是把 HTML 塞进 Django templates |
| 数据库 ER 图 | 使用 dbdiagram.io 生成的 5 张表关系图,重点标红Product → Category和Order → OrderItem的一对多连线 | 展示数据建模能力,比贴models.py代码更有说服力 |
| API 文档页 | Django REST Framework 自动生成的/api/schema/页面截图,展示products/接口的GET/POST参数表格 | 说明你用了工业级 API 设计规范,不是裸写HttpResponse |
| Git 提交记录 | VS Code 侧边栏 Git 插件截图,显示近 30 天的 commit 频次(每天至少 3 次)、分支名feature/carthotfix/login | 用客观数据证明开发过程真实,杜绝“答辩前 3 天突击”嫌疑 |
6.3 答辩话术模板:把技术细节转化成业务价值
当老师问「你这个购物车用 Redis 缓存,有什么优势?」
❌ 错误答法:“因为 Redis 速度快,内存数据库……”(陷入技术参数)
✅ 正确答法:“张阿姨在手机端加了 5 件商品,回家用电脑继续逛,购物车数据依然在——这靠 Redis 的跨设备同步实现。如果用 Session 存内存,她换设备就丢了,体验断层。”
当老师问「为什么商品分类用自引用,不用多对多?」
❌ 错误答法:“因为 ForeignKey 更简单……”(没讲清设计权衡)
✅ 正确答法:“宠物商品有明确层级:一级是‘猫粮’,二级是‘幼猫粮’,三级是‘皇家幼猫粮’。多对多适合标签式分类(如‘有机’‘进口’),但这里需要树形导航,自引用能用parent字段递归查出整条路径,Vue 侧用v-for渲染面包屑就特别顺。”
最后说一句实在的:我当年做这个毕设时,把settings.py里的DEBUG=True忘关,答辩当天被老师用手机扫了二维码,直接看到所有数据库配置——当场冷汗。现在我的习惯是:每次git push前,必运行grep -r 'DEBUG.*True' .和grep -r 'SECRET_KEY' .,确保没把密钥和调试开关传上去。这招救过我三次,希望帮到你。
本文还有配套的精品资源,点击获取