)
做Python后端开发这些年经常有人问我同一个问题想做一个能跑起来、能展示、能拿去答辩或者落地的管理系统项目前端后端到底怎么搭才不会翻车今天把最近整理的一套天天便利超市购物管理系统拿出来聊一聊。这个项目用Python做后端主力Vue做前端界面开发工具选了PyCharm后端框架方面我把Django和Flask两条路线都实际走了一遍各有各的坑和甜头。系统覆盖超市购物场景里的核心业务用户登录注册、商品分类与库存管理、购物车、订单结算、后台管理前后端通过接口对接数据全程落库。适合正在学Python的人、准备做课程设计或毕业设计的人以及想从零走通PyCharm Django/Flask Vue全栈流程的开发者。1. 项目整体设计思路与选型原因1.1 为什么我选了 Python Vue 这套组合先说结论Python Vue 这套组合在管理类系统项目里是性价比非常高的搭配。Python负责数据、逻辑和接口Vue负责界面交互二者通过JSON格式的API通信。用生活化的类比来说Python是超市仓库和收银台Vue是超市门面和货架标签顾客用户看到的永远是货架但真正算钱、管库存的是仓库。选择Python作为后端直接原因就是快。Django自带ORM、Admin后台、认证体系和表单处理写一个增删改查的模块从建表到出接口比用Java写要省掉一半的样板代码。Flask就更轻适合把某个功能快速变成一个服务。Vue这边核心是组件化和响应式数据绑定页面上某个按钮点击后数据变了视图自动跟着变不像传统jQuery那样还要手动操作DOM。对于超市购物这种交互密集的场景购物车数量增减、商品列表筛选、订单状态变化用Vue写起来非常顺手。这套组合解决的最大问题是“前后端分离开发”。后端同学不用关心页面长什么样专心写接口前端同学不用关心数据存在哪个表专心调接口。两边只要把接口约定好就能并行推进。对于一个人全栈开发来说分离之后代码结构也清爽得多不至于一个文件里又是SQL又是HTML标签改哪都提心吊胆。另外Vue的生态组件丰富Element Plus、Vant这些UI库直接拿来用做后台管理界面的效率非常高。1.2 Django 还是 Flask想清楚再动手标题里同时出现了Django和Flask估计不少人在选型时纠结过。我两种框架都在这个系统里实测过直接给结论做完整的管理系统主线用Django做轻量接口、可视化小功能、爬虫数据展示用Flask。原因看对比就清楚了。维度DjangoFlask重量级全家桶ORM、Admin、表单、认证都内置微框架只带路由和模板其余自己装上手曲线略陡概念多但学完一通百通平缓一个文件就能跑出Hello WorldORM自带Model类定义完直接映射数据库需要装SQLAlchemy或自己写SQLAdmin后台自带商品表注册进去就能管理没有需要自己搭适合场景业务完整、表关系复杂的系统简单API、原型验证、单机小工具我测试过同一个超市系统Django从项目初始化到商品模块接口跑通大概半天Flask从零搭到能查商品列表大概一小时但后续要做购物车、订单、权限、事务管理Flask就得自己拼一堆扩展反而越拼越累。所以这个项目的主路径我用Django把Flask放在一个单独的sale_api.py里实现一个“按销量统计商品排名”的轻量接口和主系统并存谁都不打扰。这种混搭方案在真实项目里也很常见。1.3 项目模块拆解与数据库设计动手写代码之前先把系统拆成四个模块用户模块、商品模块、购物车模块、订单模块。每个模块承担明确的职责接口互不越界。用户模块注册、登录、Token鉴权、角色权限。角色我分了三种普通顾客、超市管理员、系统超级管理员也就是常说的RBAC模型。商品模块商品分类、商品信息、库存数量、上下架状态、商品图片。购物车模块加入购物车、修改数量、删除商品、清空购物车。订单模块生成订单、订单明细、结算金额、订单状态流转待支付、已支付、已发货、已完成、已取消。数据库设计是这套系统比较关键的一步。我建了六张核心表字段和关系如下。用户表auth_userDjango自带扩展id、username、password、email、is_staff、is_superuser额外扩展user_profile表user_id、real_name、phone、role_type分类表categoryid、name、sort_order商品表productid、category_id外键、name、price、stock、image、status上架/下架、created_at购物车表cart_itemid、user_id外键、product_id外键、quantity、selected是否勾选结算订单表orderid、order_no唯一订单号、user_id、total_amount、status、created_at订单明细表order_itemid、order_id外键、product_id、product_name冗余快照、price、quantity这里有个细节要留意订单明细里必须冗余一份商品名称和价格快照不能直接关联商品表。原因是商品价格随时可能调整如果用户下单后商家改了价格订单明细关联到的价格就会变对账时会出现偏差。这个坑在真实电商项目里很常见做系统时能提前规避就规避。2. 开发环境准备与 PyCharm 配置2.1 Python 安装与 PyCharm 版本选择开发环境直接影响写代码的幸福感这部分我踩过不少坑先把最稳的路径列出来。Python建议装3.10或3.11版本太老的版本对Django最新版支持不好太新的版本偶尔会遇到第三方库还没适配。安装时勾选“Add Python to PATH”否则后面命令行里敲python会提示找不到命令。装完在终端执行python --version和pip --version验证。PyCharm这边社区版开源免费专业版功能更强支持远程开发、数据库工具、前端开发插件但对于这个项目来说社区版完全够用。如果你有教育邮箱官方提供免费的专业版授权这对学生非常友好。我强烈建议用正版网上流传的各种“激活方法”不靠谱轻则失效重则有安全风险没必要拿自己的开发环境冒险。安装完PyCharm后在Settings里找到Project Interpreter选择新建的虚拟环境指向Python解释器即可。接下来安装后端依赖我整理了一份requirements.txt内容如下django4.2.7 djangorestframework3.14.0 django-cors-headers4.3.1 pymysql1.1.0 flask3.0.0 flask-cors4.0.0 requests2.31.0执行pip install -r requirements.txt。如果服务器在海外、下载慢建议临时切换国内镜像源例如清华源、阿里源速度能快很多。切换方式很简单pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。2.2 Vue 环境安装与初始化Vue这边需要Node.js环境。安装Node的时候顺带会装npm装完检查版本node -v和npm -v。npm是Node的包管理器类似pip对Python的作用。安装Vue项目脚手架前建议先把npm镜像源切到国内否则装个依赖要等半天。执行npm config set registry https://registry.npmmirror.com。创建项目我用的是Vite它是目前Vue官方推荐的项目构建工具比老牌的Vue CLI启动更快。在终端执行下面命令npm create vitelatest supermarket-web -- --template vue cd supermarket-web npm install npm install vue-router4 axios element-plus npm run dev项目跑起来后浏览器访问终端里显示的地址就能看到Vue默认页面。这里必须提一下Vue Devtools这个调试插件它是浏览器开发者工具里的一套面板能直观看到组件树、当前路由状态、Vuex/Pinia里的数据变化。调试购物车数据流时没有它就像盲人摸象。直接在浏览器扩展商店搜索安装即可注意版本要和浏览器匹配。2.3 Django 项目骨架搭建含数据库配置后端项目结构我习惯这样组织可维护性最好。supermart/ ├── manage.py ├── supermart/ # 主配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── user/ # 用户模块 │ ├── goods/ # 商品模块 │ └── order/ # 购物车与订单模块 ├── static/ # 静态资源 └── requirements.txt执行django-admin startproject supermart创建项目再进入目录创建app我习惯把业务app单独放一个apps目录里避免项目一长找起来费劲。创建完后去settings.py注册app配置数据库。日常开发我用SQLite零配置起步快部署时切MySQL。如果要用MySQL记得在__init__.py里加上pymysql.install_as_MySQLdb()否则会报驱动错误。在settings.py中还需要添加django-cors-headers到INSTALLED_APPS和MIDDLEWARE否则浏览器里Vue请求后端接口会被跨域拦截。这个配置在第4章会展开讲。3. 后端核心功能实现3.1 商品与库存模块Model 视图 路由商品模块是整个系统的基础我直接用Django REST FrameworkDRF来写接口它帮我们省掉大量手写JSON响应的重复劳动。先定义Model。# apps/goods/models.py from django.db import models class Category(models.Model): name models.CharField(max_length50, verbose_name分类名称) sort_order models.IntegerField(default0, verbose_name排序) class Meta: db_table category verbose_name 商品分类 class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE, verbose_name分类) name models.CharField(max_length100, verbose_name商品名称) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) stock models.IntegerField(default0, verbose_name库存) image models.ImageField(upload_toproduct/, blankTrue, nullTrue, verbose_name商品图) status models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table product verbose_name 商品这里有几个关键选择。价格字段用DecimalField而不是FloatField因为Float在计算金额时会出现0.1 0.2不等于0.3的浮点误差对收银系统来说这是致命问题。库存字段用IntegerField并在后面加一个约束库存不能小于0。尤其是高并发下单场景扣库存必须用事务配合行锁否则会出现超卖。视图部分我写了一个商品列表和详情接口# apps/goods/views.py from rest_framework import viewsets from rest_framework.response import Response from .models import Product from .serializers import ProductSerializer class ProductViewSet(viewsets.ModelViewSet): queryset Product.objects.filter(statusTrue) serializer_class ProductSerializer def list(self, request, *args, **kwargs): category_id request.query_params.get(category_id) qs self.get_queryset() if category_id: qs qs.filter(category_idcategory_id) serializer self.get_serializer(qs, manyTrue) return Response(serializer.data)路由通过DRF的DefaultRouter自动注册前端只需要访问/api/products/就能拿到商品列表。这个模块的核心经验是列表接口一定要支持按分类筛选和关键词搜索。超市系统商品品类繁多用户不可能靠翻页找到自己要的东西我后来加了keyword参数模煳匹配商品名体验提升非常明显。3.2 购物车和订单结算流程购物车是整个系统中交互最多的模块也是前后端联调最容易出问题的地方。购物车的存储方案我选了数据库存储而不是Session存储原因很简单用户换一台设备登录数据库购物车还能同步Session就丢了。对于超市场景来说让用户重新加一遍购物车是很糟糕的体验。购物车加入接口的核心逻辑是先查用户有没有已经加过这个商品如果加过就数量加一否则新建一条记录。这里需要防范重复点击导致的重复插入我用update_or_create来保证原子性。# apps/order/services.py from .models import CartItem, Order, OrderItem from django.db import transaction def add_to_cart(user, product, quantity1): cart_item, created CartItem.objects.update_or_create( useruser, productproduct, defaults{quantity: quantity} ) # 如果已经存在则累加数量 if not created: cart_item.quantity quantity cart_item.save() return cart_item订单结算这里有个非常容易踩的坑生成订单时必须先锁定库存再扣减库存最后保存订单。我用的方式是Django的transaction.atomic()配合select_for_update()保证整个操作都在一个数据库事务里。假如不这样做两个用户同时买同一件只剩一件库存的商品两个请求同时查到库存大于0结果都下单成功库存就变成-1了这就是超卖。transaction.atomic def create_order(user, cart_items): total 0 order Order.objects.create( useruser, order_nogenerate_order_no(), statusunpaid ) 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() total product.price * item.quantity OrderItem.objects.create( orderorder, productproduct, product_nameproduct.name, priceproduct.price, quantityitem.quantity ) order.total_amount total order.save() CartItem.objects.filter(useruser).delete() return order事务和行锁是这套流程的精髓我建议花时间好好理解。select_for_update翻译成白话就是把这行数据“锁起来”在事务提交之前其他任何请求都不能修改这一行。锁的粒度越小并发能力越高。3.3 用户登录与权限控制RBAC 落地登录认证部分我先用了Django自带的User模型做基础账户体系然后扩展了一张Profile表存角色字段。角色有三种CUSTOMER、STAFF、ADMIN对应顾客、店员、管理员。RBAC基于角色的访问控制不复杂核心思想是不直接给用户分配权限而是给角色分配权限再把用户挂到角色下。这样做的好处是调整权限时只需动角色不用一个个改用户。Django的Group模型天然就是干这个的我把三种角色映射到三个Group再把Group关联到Django的Permission。判断权限时用has_perm()即可。登录接口我用Token方案Django自带的Token认证在前后端分离场景下足够用不需要引入JWT。实现方式是登录成功后在Token表创建一条记录把Token返回给前端前端拿着Token放到请求头Authorization字段里后端通过认证类校验。权限校验装饰器是复用率最高的代码之一from rest_framework.permissions import BasePermission class IsAdminUser(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and request.user.groups.filter(nameADMIN).exists()这个装饰器配合DRF的permission_classes使用管理员接口和用户接口分开控制。我实际开发中的体会是初期把权限模型想清楚后面加接口就非常省事如果一开始不做权限等系统大了再补几乎等于重构。4. 前端 Vue 页面开发与前后端联调4.1 路由与页面布局前端页面结构我按业务模块划分在src/views下创建对应目录src/views/ ├── Home.vue # 首页轮播图热门商品 ├── Login.vue # 登录页 ├── Register.vue # 注册页 ├── product/ │ ├── List.vue # 商品列表 │ └── Detail.vue # 商品详情 ├── cart/ │ └── Cart.vue # 购物车页面 ├── order/ │ └── List.vue # 订单列表 └── admin/ ├── Dashboard.vue # 管理后台数据看板 └── ProductManage.vue # 商品管理路由配置用Vue Router这里要注意一个细节需要登录才能访问的页面用路由守卫统一拦截。守卫的逻辑不复杂检查本地存储里有没有Token没有就跳登录页。我把购物车、订单、管理后台全部加上了守卫。// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/Home.vue) }, { path: /login, component: () import(../views/Login.vue) }, { path: /cart, component: () import(../views/cart/Cart.vue), meta: { requiresAuth: true } }, { path: /order/list, component: () import(../views/order/List.vue), meta: { requiresAuth: true } }, { path: /admin/product, component: () import(../views/admin/ProductManage.vue), meta: { requiresAuth: true, role: ADMIN } } ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })Element Plus组件库让页面开发提速不少表格、表单、弹窗、消息提示都有现成组件不需要手写样式。我推荐新手先把Element Plus的Table和Form两个组件吃透管理后台80%的页面都是这两种组件拼出来的。4.2 Axios 接口封装与跨域处理前后端联调最关键的一步是封装请求工具。我直接在src/utils/request.js里创建一个Axios实例统一处理基础URL、Token注入和错误提示。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else { ElMessage.error(error.response?.data?.detail || 请求失败) } return Promise.reject(error) } ) export default request这里的核心逻辑是拦截器所有请求自动带上Token所有401错误统一跳登录页不用在每个页面里重复写错误处理。跨域问题在后端通过django-cors-headers解决settings.py里配置CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]之前见过有人在前端配代理绕跨域但生产环境还是要靠后端CORS配合。Vite开发环境的代理是另一套方案在vite.config.js里配server.proxy把/api前缀转发到Django的8000端口。这样前端请求的地址不用写死IP迁移环境时改动最少。4.3 静态文件与图片问题排查前后端联调过程中遇到最多的不是接口问题而是图片显示不出来。尤其常见的是img标签里写的路径在Django的static文件中总是加载失败。排查这类问题我总结出两个方向。第一确认文件到底在哪个目录。Django静态文件分static和media两类static放CSS、JS这些项目自带资源media放用户上传的图片。Product.image字段上传的图片属于media需要在settings.py配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media同时主urls.py要加上static serve的代码否则开发环境里/media/路径根本访问不到。上线后这一步交给Nginx处理即可开发阶段直接用Django来服务。第二确认前端拼接的URL是否正确。Vue项目里图片路径如果写相对路径会因为路由层级不同而解析错。我统一用绝对路径比如const imgUrl /media/product/xxx.jpg如果图片是后端返回的相对路径前端需要拼接上服务器地址或代理前缀。一个我常用的调试办法是先在浏览器地址栏直接输入图片URL看能不能打开如果能打开说明问题出在前端拼路径打不开就说明后端静态文件配置有问题。这样能快速缩小排查范围省去反复看代码的时间。5. 常见问题排查与项目扩展思路5.1 Django ORM 查询与删除对象实战ORM是Django的重头戏也是新手最容易卡住的地方。日常使用中最频繁的两个操作是查询和删除。查询方面我常用链式查询。比如查价格在10到50元之间且库存大于0的商品Product.objects.filter(price__gte10, price__lte50, stock__gt0).order_by(-created_at)注意gte、lte、gt、lt这些关键字分别代表大于等于、小于等于、大于、小于记不住可以查Django官方文档。多条件查询时可以用Q对象实现OR逻辑from django.db.models import Q Product.objects.filter(Q(category_id1) | Q(name__icontains可乐))删除对象这个操作看起来简单遇到的坑反而最多。单条删除用obj.delete()没问题但批量删除要小心。Queryset的.delete()方法是级联删除如果一个商品有多条订单明细关联连订单明细一起被删掉。这在订单场景里绝对不能接受。所以删商品前我建议先检查有没有未完成的订单引用它有的话做软删除。软删除就是加一个is_deleted字段需要展示时过滤掉。这个方案对超市这种有历史订单的系统是最安全的。5.2 Flask 快速实现接口与部署要点上面主线用了Django这里说说Flask在小功能接口上的体验。我在系统里用Flask写了一个销量统计接口因为Flask轻量一个文件就能跑起来放Django旁边一点压力都没有。# sale_api.py from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/stat/top_sales, methods[GET]) def top_sales(): top_n request.args.get(top_n, default10, typeint) # 调用数据库查询逻辑 data query_top_sales(top_n) return jsonify({code: 0, data: data}) if __name__ __main__: app.run(host0.0.0.0, port5001)Flask绑定到网页元素这个问题说的是模板渲染时把数据渲染进HTML标签但前后端分离后这种方式不推荐。再多个Flask接口要注意的是“从客户端获取的变量数据类型”。比如有用户传了top_n参数Flask默认拿到的是字符串直接和数值比较会报错。所以request.args.get(..., typeint)这个type参数非常重要它让Flask自动做类型转换。调试时可以用type()和print()快速确认数据类型。Flask部署也比Django简单生产环境常用Gunicorn或者uWSGI启动。一个常见误区是直接用Flask自带的app.run()部署上线这个服务器是单线程开发服务器并发一高就卡死。我见过有人把超市系统挂到公网一上线就被访问量打挂改用Gunicorn后压力瞬间缓解。部署命令参考gunicorn -w 4 -b 0.0.0.0:5001 sale_api:app-w 4表示4个工作进程可以并发处理请求。5.3 针对系统可做的扩展方向实时推送、可视化、视频系统核心功能跑通之后可以往几个方向扩展这些也是我在这个项目上后续规划的内容。第一个方向是WebSocket实时推送。超市运营后台希望看到实时销售额、实时库存预警而不是每次手动刷新页面。Django可以通过Channels实现WebSocket后端有数据变化时主动推给前端不需要浏览器不断轮询。比如库存低于预警线时后台页面立刻弹窗提醒。实现思路是在商品库存变更的地方通过Channels的group_send推送消息前端用WebSocket API监听并更新页面。注意Channels需要ASGI服务器运行部署方式和传统WSGI不一样。第二个方向是数据可视化。超市每天产生大量订单数据可以用Flask写一个数据分析接口配合ECharts在前端展示销售趋势、品类占比等图表。热门词里“农产品价格数据可视化-Flask”是一个典型场景其实就是把价格数据通过Flask的API暴露前端用图表组件画折线图。把这种可视化页面挂在管理后台的Dashboard上系统会显得专业很多。第三个方向是爬虫数据展示。如果超市系统对接的是外部供应链价格可以写一个Python爬虫定时抓取供货商的价格存到本地库再在管理后台展示。爬虫和可视化本质上是数据供应链的两端这套系统正好提供了一个很好的展示载体。第四个方向是广告视频播放。超市门口或货架上的电子屏用Vue写一个播放器页面播放促销视频。相关热词里反复出现的m3u8播放需求指的就是HLS流媒体格式Vue项目中可以通过video.js或hls.js实现。这个方向和购物系统本身互不干扰可以做成独立的展示模块。我不建议把视频播放功能揉进购物主流程保持模块独立更好维护。最后聊点实际经验这个系统从框架选型到前后端联调跑通给了我几个很深的体会。第一框架之争没有绝对答案Django和Flask都是工具关键看场景。完整业务系统用Django确实省心Flask适合小而美的接口。第二数据库设计一定要在写业务代码前想清楚尤其是事务和并发问题库存扣减的临界条件必须在设计阶段就明确。第三前后端分离开发时接口文档要提前写好我用的方法是先在代码里定义好API路径和返回格式前端照文档联调两边都能少走弯路。第四调试工具要配置到位Vue Devtools和浏览器的Network面板是我用得最多的工具90%的联调问题都能靠这两个工具定位。如果你正在做类似的系统不妨先把购物车和订单这两个最复杂的流程单独抽出来跑通再填充其他模块这样整体的开发节奏会顺畅很多。