☰
Python+Vue+Django/Flask 前后端分离在线超市系统开发实战
2026/10/7 11:41:15 网站建设 项目流程

最近我把一套线上超市购物系统从零到能跑起来、能演示的完整状态做了一遍。技术栈就是标题里那一套:Python 做后端,Vue 做前端,Django 和 Flask 两个框架我都实际用上了,开发环境全程在 PyCharm 里完成。这套系统不搞花里胡哨的微服务,就是一个标准的前后端分离单体项目,业务上覆盖了用户登录、商品展示、购物车、下单、支付模拟、订单管理,后台管理直接用现成方案。适合正在做课程设计的学生、想练手全栈项目的新手,以及产品原型阶段需要快速交付的团队拿来借鉴。

做这个项目的过程比我预想的要折腾一些,尤其在购物车合并、库存扣减、跨域、静态文件部署这几个点上,每个都踩过不止一个坑。所以这篇文章不打算只贴代码,而是把我从搭建环境到选型、从数据库设计到接口联调、从本地跑通到服务器部署的完整链路都写出来,包括每个关键决策背后的原因,以及常规文档里不会写的那些教训。

1. 项目定位与技术选型:这套系统到底要做什么

1.1 线上超市的核心业务模块拆解

线上超市听起来是个大工程,但剥开来看,核心业务就六个字:逛、选、下单、付、查、管。

逛是用户进来看商品列表、看分类、搜索;选是加购物车;下单是提交收货信息、生成订单;付是模拟支付然后订单状态流转;查是查订单列表和详情;管是后台要能上架商品、调库存、处理订单。再往后还能加推荐、加优惠券、加社区评价,但MVP阶段把这些核心链路打通,系统就已经具备完整的骨架了。

我拆解出来的模块清单大致如下:

  • 用户模块:注册、登录、个人信息。MVP阶段用手机号加密码就够了,收货地址先用一个简单字段存,不需要单独建地址库。
  • 商品模块:分类、商品列表、商品详情、搜索。这里比较容易忽略的是排序逻辑,销量排序、价格排序在超市场景里很常用。
  • 购物车模块:这个模块最容易被低估,后面我会单独讲。选品加购看起来简单,但涉及到本地状态、持久化、登录后合并,比想象中复杂。
  • 订单模块:创建订单、订单列表、订单详情、状态流转(待支付 -> 已支付 -> 已发货 -> 已完成,以及已取消)。
  • 支付模块:MVP阶段用模拟支付接口,把订单状态从待支付变成已支付即可。
  • 后台管理:商品管理、库存调整、分类管理、订单处理。这部分直接靠 Django admin 就能解决,不需要重新造轮子。

每定义一个模块的时候,我习惯先画一条用户路径:用户打开商城 -> 浏览商品 -> 加购物车 -> 去结算 -> 填收货信息 -> 提交订单 -> 支付 -> 查看订单。这串流程就是数据库表和接口设计的出发点,后面的模型、接口全是给这条路径服务的。

1.2 Django与Flask的选择:我为什么两个都用了

标题里同时出现 Django 和 Flask,有人可能觉得奇怪。这里我把真实情况说清楚:这个项目我前后做了两个版本。

第一版用 Flask 快速搭了一个原型,因为 Flask 实在太轻了,两三百行代码就能把一个可用的接口服务拼起来,非常适合验证流程。但做着做着问题就来了:后台管理没有现成方案,需要自己写权限、写列表页、写表单;ORM 虽然可以用 SQLAlchemy,但序列化、分页、认证这些还是得自己组装。等我把这些轮子一个个拼完,发现时间全浪费在了与业务无关的基础设施上。

于是第二个版本切到了 Django。Django 的定位是把基础设施全给你配好:自带 admin 后台,商品、订单、分类的管理界面直接能用;内置用户认证体系,密码哈希、登录会话都是现成的;Django REST Framework 做 API 也相当成熟。对于线上超市这种业务逻辑集中、需要大量增删改查的管理后台的项目,Django 的业务匹配度明显更高。

Flask 在第二版里也没闲着,我用它单独写了一个小服务,跑在另一个端口上,负责给前端提供商品推荐接口。这个小服务只有两个接口,逻辑简单,用 Flask 反而比 Django 轻快。所以我的建议是:如果项目主体是管理系统、商城这类要频繁操作数据的,优先选 Django;如果只是写几个小接口、做微服务或快速原型,Flask 完全不亏。

对比项DjangoFlask
自带后台管理有,可直接用无,需自己集成
用户认证内置完整方案需要扩展包
ORMDjango ORM,自带迁移SQLAlchemy,需自己配
学习曲线相对陡峭平缓,易上手
适合场景业务系统、商城、后台管理轻量API、微服务、原型
社区生态大而全小而灵活

1.3 前后端分离:为什么要用 Vue 接 Python

商城这类系统有一个特点:页面交互复杂,状态多。加购物车要即时反馈,购物车角标要变,下单流程要跳转,搜索要防抖。如果用传统的服务端模板渲染这类页面,前后端代码揉在一起,每次改交互都要动后端模板,人力成本很高。

所以我把前后端拆开:后端 Python 只提供 JSON 接口,前端 Vue 负责页面和交互。Vue3 搭配 Vite 开发体验很好,热更新快,组件化以后页面结构清晰。前端完成npm run build之后是一堆纯静态文件,可以交给 Nginx 托管,也可以扔进任何静态服务器,和后端服务完全解耦。

这套架构还有一个好处是接口可以被多个端复用。我同一套后端接口,一个 Web 前端在用,一个用 Postman 测试在用,后续如果要做小程序端,后端接口几乎不用改。Vue 本身的上手门槛也不高,只要理解了组件、路由、状态管理三个概念,写个商城前端完全够用。

2. 后端设计:数据库模型、接口与核心业务流程

2.1 数据库模型设计:把商品、购物车、订单串起来

后端是整个项目的核心,数据库模型又是核心中的核心。我在设计模型时有一条原则:一张表只负责一个业务实体的数据,多对多关系尽量通过中间表表达,凡是涉及金额、数量、状态的数据,字段类型一定要选对。

商品和分类用 Django 写起来大概是这样的:

# products/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=50, unique=True) sort_order = models.IntegerField(default=0, help_text="排序权重,小的靠前") class Meta: verbose_name = "商品分类" verbose_name_plural = "商品分类" def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") name = models.CharField(max_length=200, db_index=True) subtitle = models.CharField(max_length=300, blank=True, help_text="一句话卖点") # 价格一定用 DecimalField,不要用 FloatField price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.IntegerField(default=0) sales = models.IntegerField(default=0, help_text="销量用于排序展示") cover = models.ImageField(upload_to="covers/", null=True, blank=True) detail = models.TextField(blank=True, help_text="商品详情描述") is_on_sale = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"] def __str__(self): return self.name

订单相关的模型是这套系统里最需要注意的部分。我强烈建议在订单明细里保存商品名称和价格快照,而不是只用外键关联商品表。原因很简单:商品后来可能改名、改价、甚至下架,但用户的历史订单必须保持下单那一刻的信息不变。快照字段就是干这个的。

# orders/models.py from django.conf import settings from django.db import models class Order(models.Model): STATUS_CHOICES = ( ("pending", "待支付"), ("paid", "已支付"), ("shipped", "已发货"), ("finished", "已完成"), ("canceled", "已取消"), ) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="orders") order_no = models.CharField(max_length=32, unique=True, help_text="业务订单号") total_amount = models.DecimalField(max_digits=12, decimal_places=2) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending") receiver = models.CharField(max_length=50) phone = models.CharField(max_length=20) address = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey("products.Product", on_delete=models.PROTECT) # 快照字段:下单时的名称和价格,防止商品变更影响历史订单 product_name = models.CharField(max_length=200) price_snapshot = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.IntegerField()

这里有两个细节你可能会忽略。第一,OrderItem.product外键的on_delete我用了PROTECT,意思是如果商品已经被订单引用,就不允许直接删除,只能下架。这么做是为了避免订单明细里出现找不到商品的脏数据,代价是后台删除商品时会报保护错误,需要你先处理关联订单。第二,订单号order_no不要用自增主键直接暴露给用户,可以用时间戳加热随机数生成,方便后续对账和排查。

另外,商品列表页要展示销量、价格、分类这些信息,我建议给name、category加上索引。超市商品多起来之后,列表查询加上 where 条件如果没有索引,数据库会全表扫描,体验很差。Django 的 ORM 在CharField上加db_index=True就够用了。

2.2 RESTful 接口规划与 JWT 认证

模型设计好之后,接口规划就顺理成章了。我用 Django REST Framework 做接口层,接口路径和功能对应如下:

方法路径功能说明
POST/api/users/register/用户注册
POST/api/users/login/用户登录,返回 JWT Token
GET/api/products/商品列表,支持分类筛选、关键字搜索、排序参数
GET/api/products/{id}/商品详情
GET/api/cart/获取购物车列表
POST/api/cart/添加商品到购物车
PUT/api/cart/{id}/修改购物车商品数量
DELETE/api/cart/{id}/删除购物车商品
POST/api/orders/创建订单
GET/api/orders/当前用户订单列表
GET/api/orders/{id}/订单详情
POST/api/orders/{id}/pay/模拟支付

认证方案我选了 JWT,而不是传统的 Session。原因很简单:前后端分离之后,后端接口是无状态的,前端把 Token 存在本地,每次请求带上Authorization: Bearer <token>,后端校验即可。Django 这边推荐用djangorestframework-simplejwt,注册、登录、刷新 Token 的接口都是现成的,只需要配置好认证方式和权限类。

密码存储这件事必须说清楚:绝不能明文存密码。Django 自带的用户模型默认用 PBKDF2 算法做哈希,注册时传入密码,Django 自动完成哈希存储,登录时用authenticate校验,这些基础能力不用自己写。如果强行自己实现一套密码存储,很容易写出有安全隐患的代码。

接口层的序列化器在 DRF 里很直观,比如商品序列化器:

# products/serializers.py from rest_framework import serializers from .models import Product class ProductSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source="category.name", read_only=True) class Meta: model = Product fields = ["id", "name", "subtitle", "price", "cover", "sales", "category_name"]

使用ModelSerializer的好处是字段定义与模型绑定,后期模型加了字段,序列化器如果不需要暴露就不受影响,接口不会因为模型变更随意崩掉。

2.3 下单扣库存:事务与并发处理

下单是整个系统里最容易出 bug 的环节。想象一个场景:用户把商品加入购物车,结算提交订单,后端需要做四件事:创建订单主表、创建订单明细、扣减库存、清空购物车。这四件事必须要么全成功,要么全失败,否则就会出现"订单创建了但库存没扣"或者"库存扣了但订单不存在"的数据不一致问题。

解决办法就是数据库事务。Django 的写法是用transaction.atomic()包裹整个下单逻辑:

# orders/services.py from django.db import transaction from django.db.models import F def create_order(user, cart_items, receiver_info): with transaction.atomic(): order = Order.objects.create( user=user, order_no=generate_order_no(), total_amount=calc_total(cart_items), status="pending", **receiver_info, ) for item in cart_items: # 用 select_for_update 锁定商品行,防止并发超卖 product = Product.objects.select_for_update().get(id=item["product_id"]) if product.stock < item["quantity"]: raise StockNotEnough(product.name) OrderItem.objects.create( order=order, product=product, product_name=product.name, price_snapshot=product.price, quantity=item["quantity"], ) # 用 F 表达式做原子扣减,避免自增/自减的竞态问题 product.stock = F("stock") - item["quantity"] product.sales = F("sales") + item["quantity"] product.save(update_fields=["stock", "sales"]) clear_cart(user)

这里有一个非常关键的细节:扣减库存用的是select_for_update()行锁加上F()表达式原子操作。如果只是先查库存、判断足够、再更新库存,高并发下会出现超卖。两个用户同时查到库存只剩 1 件,都判断可以购买,结果都扣减成功,库存变成负数。行锁会让第二个请求等待第一个请求提交后再读库存,F()表达式则让加减操作在数据库层面完成,避免先读后写带来的窗口期。

下单之后订单处于"待支付"状态,预留一定时间。模拟支付接口的逻辑更简单:验证订单属于当前用户、状态是待支付,然后把状态改成已支付。真实项目接入微信或支付宝时,这一步要做的是生成支付参数、跳转支付、接收回调通知,并在回调里做签名验证和幂等处理。幂等处理的要点是:同一个支付通知可能被第三方重复发送,后端要根据订单号和支付流水号判断是否已经处理过,避免重复修改订单状态。

2.4 购物车方案:localStorage 与后端接口的组合

购物车是商城产品经理特别看重的模块,技术上也有几种做法。

方案一:完全存在前端 localStorage。优点是后端零压力,实现简单,用户未登录也能加购。缺点是换设备数据就没了,无法统计用户先加购后不买的弃单行为。

方案二:完全存在后端数据库。优点是数据持久可靠、利于统计分析。缺点是没有登录就不能用购物车,用户第一次进站就得注册,转化率受损。

方案三:我采用的折中方案。未登录用户的购物车数据存在 localStorage,登录之后,前端把本地购物车数据当作待合并列表提交到后端,后端把这些条目合并进数据库购物车,合并完清空本地数据。

这个方案的好处很明显:游客可以愉快地逛,加了购物车也不用强制登录;等到结算或登录时,本地数据自动合入账号体系。实现上的关键点是合并逻辑要处理同一商品加多次的情况,后端看到同一个商品 ID 有多个条目时,应该累加数量而不是新建多条记录。

3. 前端 Vue 实现:从页面到联调

3.1 工程初始化与路由规划

前端我用 Vue3 加 Vite 初始化,这一步很简单:

npm create vite@latest supermarket-web -- --template vue cd supermarket-web npm install npm install vue-router@4 pinia axios element-plus

页面结构分成四个主要视图:商品列表页、商品详情页、购物车(我用抽屉组件实现,不单独占一个页面路由)、订单页。商品列表页默认展示全部商品,顶部有分类导航和搜索框;商品详情页展示大图、价格、卖点、详情描述,加购按钮放在显眼位置;订单页展示用户的历史订单和订单状态。

路由配置如下:

// src/router/index.js import { createRouter, createWebHistory } from "vue-router" const routes = [ { path: "/", name: "home", component: () => import("@/views/HomeView.vue") }, { path: "/product/:id", name: "product-detail", component: () => import("@/views/ProductDetail.vue") }, { path: "/orders", name: "orders", component: () => import("@/views/OrderListView.vue") }, ] const router = createRouter({ history: createWebHistory(), routes, }) export default router

路由懒加载是我特别推荐的做法,每个页面组件按需加载,首屏体积会小很多。多人协作时,每个成员可以只关注自己负责的视图文件,基本不会互相冲突。

3.2 组件与状态管理:购物车抽屉是怎么实现的

商城类项目前端最容易乱的地方是状态管理。购物车角标、抽屉里的小计、结算页总价,多处地方都要用到购物车数据,如果每个页面各自存一份,同步起来全是 bug。我用 Pinia 统一管理购物车状态,这才是它的用武之地。

// src/stores/cart.js import { defineStore } from "pinia" import { http } from "@/utils/http" export const useCartStore = defineStore("cart", { state: () => ({ items: JSON.parse(localStorage.getItem("cart_items") || "[]"), }), getters: { count: (state) => state.items.reduce((sum, i) => sum + i.quantity, 0), totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price * i.quantity, 0), }, actions: { addItem(product, quantity = 1) { const existing = this.items.find((i) => i.product_id === product.id) if (existing) { existing.quantity += quantity } else { this.items.push({ product_id: product.id, name: product.name, price: product.price, cover: product.cover, quantity, }) } this.persist() }, persist() { localStorage.setItem("cart_items", JSON.stringify(this.items)) }, clear() { this.items = [] this.persist() }, }, })

购物车抽屉组件的交互逻辑也不复杂:点加购按钮时调用 store 的addItem,角标数字自动刷新;打开抽屉能看到商品列表、修改数量、删除条目、合计金额。这里我踩过一个交互坑:多个商品修改数量时,每次点击都直接改 localStorage,结果高频点击时偶发数据错乱。后来调整策略,只在修改动作触发时写一次 localStorage,并用watch监听 store 变化自动持久化,问题就稳定了。

Vue 的插槽在商品卡片这类场景中很好用。父组件只负责提供布局,卡片内部的按钮、标签、价格展示位置可以由插槽决定,不同页面使用同一张卡片时能灵活替换局部内容,减少重复代码。

3.3 接口联调:Axios 封装与跨域处理

前后端分离之后,接口联调是必经之路。我在前端封装了一个 Axios 实例,统一处理基站地址、Token 注入和错误响应:

// src/utils/http.js import axios from "axios" export const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, }) http.interceptors.request.use((config) => { const token = localStorage.getItem("token") if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) http.interceptors.response.use( (response) => response.data, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem("token") // 跳转到登录页,这里按实际路由处理 } return Promise.reject(error.response?.data || error) } )

开发阶段最常见的跨域问题,我的解决办法不是在 Django 里开启 CORS,而是用 Vite 的代理功能,让前端请求走同一个域:

// vite.config.js export default defineConfig({ server: { proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true, }, }, }, })

这样在浏览器里,前端请求/api/products/,Vite 开发服务器会把它转发到http://127.0.0.1:8000/api/products/,浏览器层面没有跨域问题,后端也不需要考虑 CORS 白名单。

我自己实际开发时习惯准备两个环境变量文件:.env.development里VITE_API_BASE_URL=/api,走代理;.env.production里VITE_API_BASE_URL=https://你的域名/api,走真实接口。这样代码里不需要写死任何环境相关的地址。

4. PyCharm 开发环境:让全栈开发更顺手

4.1 从零配置 Python 虚拟环境

很多人拿到一个项目,第一步就卡在环境上。我建议所有 Python 项目都用虚拟环境,把项目依赖隔离起来,避免和系统 Python 环境互相污染。

在 PyCharm 里,最简单的方式是用它自带的虚拟环境创建功能。打开项目后,在设置里选择 Python 解释器,点击新增,PyCharm 会帮你新建一个.venv目录并配置好解释器。之后打开终端,PyCharm 会自动激活这个虚拟环境,命令行前面会出现(.venv)标识,这时候执行pip install -r requirements.txt安装依赖,所有包都会装进这个虚拟环境。

Python 版本建议用 3.10 或 3.11,Django 4.x、新版 Flask 对这些版本的兼容性都很稳。如果机器上同时装了多个 Python 版本,记得在创建虚拟环境时手动指定版本路径,不然容易踩到版本不对的坑。

关于 PyCharm 版本:社区版免费,写 Python 后端完全够用;专业版额外带了前端支持、数据库工具、远程调试这些功能。做全栈项目时专业版确实方便,但没必要为了省一个会员而去折腾一些不正规的激活手段,正常订阅或者先用社区版都行。开发工具是提高效率的,不是项目的负担。

4.2 调试与接口测试的实用技巧

我调试后端接口时有一套固定打法,比单纯运行python manage.py runserver然后拿浏览器戳要快得多。

第一,善用断点调试。在 Django 视图函数里打上行断点,然后跑 Debug 模式,请求进来后就可以逐步查看变量、确认 SQL 查询是否走了预期索引。比一堆print再删掉优雅太多。

第二,用 PyCharm 的 Database 工具直接看数据。专业版里可以连接项目用的数据库,运行时直接查看商品表、订单表里的数据变化,比用命令行敲 SQL 直观得多。不依赖数据库类型,SQLite、MySQL 都支持。

第三,用内置的 HTTP Client 写接口测试文件。在 PyCharm 里新建一个.http文件,可以像 Postman 一样直接发送请求、查看响应,还支持环境变量。我一般把这个文件提交到仓库里,团队成员拉下来后点一下就能测接口,减少沟通成本。

### 用户登录 POST http://127.0.0.1:8000/api/users/login/ Content-Type: application/json { "username": "test_user", "password": "test_pass123" }

这套工作流一旦习惯,接口联调效率会高出不少。看到接口报错,不急着加日志,先看调用栈和断点,往往能直接定位问题。

5. 部署上线:Django + Vue + Nginx 的组合拳

5.1 部署方案选择

项目做完要给别人看,就得部署到服务器上。我用的方案是一台轻量云服务器,Ubuntu 系统,配置比较普通,但跑这个项目已经绰绰有余。

后端部署用 Gunicorn 作为 WSGI 服务器,不能直接用 Django 自带的runserver,那是开发用的,性能和安全性都不适合线上。创建 Gunicorn 启动命令:

gunicorn supermarket.wsgi:application --workers 3 --bind 127.0.0.1:8000

三个 worker 可以支撑一般的小流量并发。如果机器内存不够,2 个 worker 也行。生产环境我建议用 systemd 管理 Gunicorn 进程,这样服务器重启后后端能自动拉起,不会出现人去手动重启的情况。

前端部署更简单:本地执行npm run build,会生成dist目录。把这个目录上传到服务器,交给 Nginx 托管即可。Vue 打包出来的都是静态文件,不依赖 Node 环境,这也是前后端分离的一个隐藏好处:前端只需一个静态服务器就能跑。

之前有人问过 Vue 打包之后能不能放进 Spring Boot 里,其实原理一模一样,就是把 Vue 的 dist 静态文件交给后端框架或 Nginx 托管。放进 Django 也是同理,用python manage.py collectstatic收集后端静态文件,再用 Nginx 做静态资源服务,后端 API 走代理转发。

5.2 Nginx 配置与常见部署坑

Nginx 是整个部署中的核心角色。它同时负责托管 Vue 静态文件、代理后端 API 请求、服务 Django 静态资源。配置文件核心部分如下:

server { listen 80; server_name your_domain_or_ip; client_max_body_size 20m; # Vue 前端打包产物 location / { root /var/www/supermarket/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Django 静态文件(admin 后台和上传图片) location /static/ { alias /var/www/supermarket/staticfiles/; } location /media/ { alias /var/www/supermarket/media/; } }

这个配置里有几个容易踩的坑。第一,try_files $uri $uri/ /index.html这一行必须有,否则 Vue 使用 history 模式路由时,用户刷新/product/3这个页面会直接 404。因为 Nginx 先去找物理文件,找不到就回退到index.html,然后 Vue Router 按路径渲染对应组件。

第二,Django 部署时DEBUG=False后,Django 自己不再处理静态文件,必须先把静态文件收集起来。执行python manage.py collectstatic,然后在 Nginx 里配置/static/指向收集目录,否则 Django admin 后台的样式会全部丢失。

第三,ALLOWED_HOSTS要配置正确。如果通过 IP 访问,就要把服务器 IP 加进去;通过域名访问,就把域名加进去。忘记配置的话,请求会被 Django 直接拒绝,浏览器里看到的是"Bad Request (400)"。

第四,上传的图片文件如果不单独配置/media/路径,用户上传商品图后,图片在页面里永远是破的。这个和DEBUG=False也有关系,开发时能访问是因为 Django 帮你处理了 media,生产环境必须交给 Nginx。

如果服务器内存和磁盘都很小,把 SQLite 直接用于生产也能顶住低流量场景。但正规发货前建议换 MySQL,到时候注意建库时统一用utf8mb4字符集,不然中文数据容易出现乱码。

6. 常见问题与排查技巧实录

6.1 开发期高频问题速查

整个开发过程我不可能一帆风顺,下面这些问题都是实际遇到并解决的,整理成速查表方便你对照处理。

问题现象根本原因解决办法
前端请求接口报 CORS 错误前端和后端端口不一致开发环境用 Vite proxy 代理;生产环境走 Nginx 同域转发
Django 提示 CSRF 验证失败前后端分离没有携带 CSRF Token使用 JWT 认证,DRF 配置为 SessionAuthentication + TokenAuthentication 混合模式,CSRF 只对 Session 认证生效
接口返回中文乱码MySQL 库表字符集不是 utf8mb4建库时指定utf8mb4,连接串加charset=utf8mb4
库存出现负数或超卖并发下单没有锁库存下单事务中加select_for_update()行锁
修改商品价格后历史订单金额变了订单明细没有存价格快照订单明细保存price_snapshot字段
pip install下载慢默认源速度慢使用国内镜像源,如pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
npm install卡住或报错Node 源连接不上配置 npm 镜像,或者使用 nvm 切换 Node 版本

这里我要特别提一下 Flask 和 FastAPI 的比较,因为我在写 Flask 推荐服务时也研究过这个方向。对于这个项目这种最简单的两个接口服务,FastAPI 的自动接口文档和类型校验确实更有优势,代码量差不多。所以如果新起一个纯接口类的项目,我大概率会优先考虑 FastAPI;但 Flask 生态更成熟,团队里 Flask 经验多一些时,Flask 依然是个稳妥选项。技术选型不是越新越好,而是匹配团队和场景。

6.2 部署期隐蔽问题集合

部署期的坑普遍比开发期更隐蔽,因为报错信息往往不出来,或者只有一个 500。

第一个隐蔽问题:Gunicorn 起来了,但系统重启后服务没了。解决办法是写一个 systemd 服务文件,把 Gunicorn 托管起来,设置Restart=always。这是生产环境运维的基本功,年轻人容易忽略。

第二个隐蔽问题:后端接口在服务器本地通了,但前端页面请求 502。多数情况是 Nginx 的proxy_pass转发地址写错。记住一个细节:proxy_pass http://127.0.0.1:8000;末尾不加路径时,会把完整 URI 追加到后端;如果加了/,路径前缀会被吞掉。这种细节出问题的时候很难排查,建议在浏览器开发者工具里看响应头确认是 Nginx 返回的还是 Django 返回的,能快速定位问题层级。

第三个隐蔽问题:部署后图片上传功能正常,但图片显示 404。这是因为 Django 的MEDIA_ROOT和MEDIA_URL没配好,加上 Nginx 缺少/media/的 location 配置。检查思路是:先看图片文件是否真的上传到了服务器对应目录,再看 Nginx 的 alias 路径是否正确。目录权限也是个容易被忽略的点,Nginx 工作进程要有读取权限。

第四个隐蔽问题:前端刷新任何非首页路由都是 404。这正是 Vue history 路由模式配合 Nginx 时最经典的问题,try_files配置必须加上,前面已经强调过,这里再次提醒一遍。

还有一个小技巧:生产环境出问题时,先看日志。Django 的日志文件、Gunicorn 的日志、Nginx 的错误日志,三个日志基本能把 90% 的问题指向揪出来。与其在服务器上调半天,不如先把日志定位看清楚再动手。

最后说一个判断前后端问题的通用方法:直接用手敲一下后端接口。如果curl http://127.0.0.1:8000/api/products/能正常返回 JSON,那问题几乎肯定在前端、Nginx 或网络层;如果这个请求都挂了,那就是 Python 后端的锅,去看日志和进程状态。用这个办法十次有八次能立刻把问题缩小到具体范围。

做完整套项目,我个人最大的体会是:技术栈本身并不是这个项目的难点,难点在于把购物车合并、订单状态流转、库存并发扣减这些业务规则理清楚,并且每一层都留有可排查的记录。Django 给我提供了可靠的基础设施,Vue 解决了复杂交互,PyCharm 让调试变得更顺手,这三个工具组合在一起,确实能在有限人力下快速交付一套可用的商城系统。

如果有人想在这个项目上继续往上走,我建议下一步优先做这几件事:一是把手机验证码登录接上,这是国内商城的标配;二是接入真实的支付渠道,把模拟支付替换成微信或支付宝,重点处理好回调验签和幂等;三是给商品搜索加上拼音模糊匹配和搜索联想,让用户能更自然地找到东西。至于我留着那个 Flask 推荐服务,正好可以改造成基于用户行为的推荐接口,把它做成整个系统的增长引擎。

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

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

立即咨询