ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python与Vue构建在线茶叶销售系统:前后端分离开发实战解析

Python与Vue构建在线茶叶销售系统:前后端分离开发实战解析

1. 项目定位与核心需求拆解

刚用 Python + Vue 完整跑通一套在线茶叶销售系统,趁记忆还热乎,把整个链路从头到尾捋一遍。这类系统在毕业设计、课程项目、小商户数字化转型里出现频率极高,名字五花八门,但内核差不多:一套前后端分离的商城,商品是茶叶,流程包含用户注册登录、商品浏览、购物车结算、订单管理、后台维护。

很多人拿到这种项目第一反应是“不就是个电商网站吗”,这句话对,也不对。电商确实是它的本质,但茶叶这个品类带来了一些很实在的业务约束,比如商品属性复杂、库存粒度细、订单状态流转多、后台管理需求重。如果你只是照着通用商城模板套,大概率做到一半会发现表结构设计得不对、订单状态机没想清楚、库存扣减在并发下会出问题。这些坑我在开发过程中基本都踩了一遍,所以这篇内容会按实际开发路径来拆,不吹概念,全部是可落地的做法。

这个项目适合谁来参考?我觉得三类人最合适:一是正在做课程设计或毕业设计的学生,需要一个功能完整又不至于失控的系统;二是刚学完 Django 或者 Vue 基础、想找个真实项目练手的前端或后端开发者;三是想给茶叶门店做线上化的小团队,可以拿这套架构当起点,再按需扩展。无论你属于哪一类,这篇内容的核心价值不是“看一个成品”,而是搞清楚每一个环节为什么这么设计、每一步怎么实现、踩坑之后怎么修。

1.1 这类系统的本质与适用场景

先说实话,当我看到“Python基于Vue的在线茶叶销售系统”这个标题时,第一反应是:这不只是一个网站,而是一条完整的数据流转链路。用户在前端点一下“加入购物车”,背后至少牵动商品库存表、购物车表、用户会话、价格快照、订单状态等多个模块的协作;管理员在后台改一个商品价格,前端详情页立刻要反映出来。这种前后端联动、数据状态一致性的问题,才是项目的真正难点。

从业务场景看,茶叶销售系统有几个典型诉求:

  • 商品展示需要图文混排,茶叶的产地、年份、工艺、口感描述不能像卖标准件那样只放参数表;
  • 库存管理不能简单粗暴,同一款茶可能有不同规格、不同年份,每种规格独立库存;
  • 订单流程涉及“待支付 → 已支付 → 待发货 → 已发货 → 已完成”,状态变化需要记录可追溯;
  • 后台管理需要区分普通用户和管理员,权限边界要清楚,不能让用户把自己的商品删了。

所以这套系统并不适合用“一次性模板”的思路去做,而是要从需求分析阶段就把数据模型、接口划分、页面结构一起想清楚。我在实际开发中用的是 Django 作为后端框架、Vue 作为前端框架、PyCharm 作为 IDE,这个组合覆盖了从建表到联调的完整流程,下面会展开说为什么这么选。

1.2 核心功能边界与交付标准

任何项目开始之前,先划清功能边界,不然做着做着就失控了。这个系统我拆成了“用户端”和“管理端”两大块,每一块再往下拆功能点。

用户端核心功能:

  • 注册与登录:支持用户名、密码、手机号,登录后保持会话;
  • 茶叶浏览:分类筛选、关键词搜索、商品列表页和详情页;
  • 购物车:加入商品、修改数量、删除条目、结算跳转;
  • 订单:创建订单、查看订单列表、查看订单详情、取消订单;
  • 个人信息:收货地址维护、个人资料修改。

管理端核心功能:

  • 商品管理:新增、编辑、上下架茶叶商品,维护库存量;
  • 分类管理:维护茶叶分类层级;
  • 订单管理:查看所有订单、修改订单状态(发货、完成等);
  • 用户管理:查看注册用户列表,禁用/启用账号。

这些功能看起来常规,但每个点都有隐藏的细节。比如“上下架”意味着商品列表要过滤下架商品,不是简单加个字段就行;“修改库存”意味着订单创建时要扣减库存,订单取消时要回补库存。这些逻辑没有考虑周全,做到联调阶段就会痛苦。

我给自己定的交付标准也很简单:用户能走通“注册登录 → 浏览商品 → 加购物车 → 下单 → 查看订单”的完整闭环,管理员能走通“登录后台 → 管理商品 → 处理订单”的完整闭环,并且这两端的数据是互通的。只要这条主链路通畅,剩下的都是锦上添花。

2. 技术选型解析:为什么是Django、Vue和PyCharm

技术选型这块,网上争论很多,Django 和 Flask 哪个好、Vue 2 和 Vue 3 用哪个、PyCharm 是不是不够轻量,类似的帖子能吵几百楼。我的态度很明确:选型不是选一个“最好的”,而是选一个“在项目约束下最不容易出错的”。下面把每个环节的实际考量说清楚。

2.1 Django还是Flask?两种框架的取舍逻辑

标题里同时出现了 django 和 flask,说明很多同学在两难中选择。我两种都用过,给你一个实际的对比维度。

Flask 的优势是轻、自由、灵活。它不会替你做太多决定,路由、ORM、表单校验、用户认证都需要你自己组装或者选第三方库。对于一个小而美的接口服务,或者你想彻底掌控每一层实现,Flask 很舒服。但代价是,你需要自己把“用户认证、后台管理、ORM 迁移、Admin 站点”这些能力一块块拼起来。对于茶叶销售系统这种包含完整后台管理、多种角色权限的项目,Flask 的工作量会明显变大。

Django 的优势是“全家桶”式集成。自带 ORM、自带 Admin 后台、自带用户认证体系、自带迁移机制,这些都是大量项目验证过的轮子,不用重复造。尤其后台管理这一块,Django Admin 几乎是白送的管理界面,虽然样式朴素,但该有的增删改查、筛选、排序全部都有。我的经验是,像这类业务逻辑复杂、后台要求高的项目,选 Django 能省下至少三分之一的时间。

当然,如果你特别想用 Flask 也没问题,后面我会在实操环节专门展示 Flask 版本的接口实现方式,把两者的差异点讲清楚,方便你按自己的偏好选型。

2.2 前端为什么用Vue,版本怎么选

Vue 在前端框架里算是对新手最友好的一类。它的模板语法接近原生 HTML,响应式数据绑定可以让你把精力集中在业务逻辑上,不用频繁操作 DOM。相对于 React 的 JSX 和生态系统,Vue 的上手曲线更平缓;相对于 Angular 的重型概念,Vue 又轻得多。

当前如果新起项目,我会直接用 Vue 3 + Vite + Element Plus。很多老教程还在讲 Vue 2 和 Vue CLI,不是说不能用,而是 Vue 3 已经是主流,生态组件基本都兼容了,没必要从旧版本起步。Vite 相比 Webpack 最大的体感提升是启动速度,开发环境秒开,热更新也快,这在前后端联调时能省很多等待时间。

Element Plus 是 Vue 3 的组件库,表格、表单、弹窗、分页、日期选择器这些管理后台常用的东西全都封装好了。尤其是后台的商品管理页面,用 Element Plus 的表格 + 表单组件,配合弹窗做新增编辑,开发效率能提升好几倍。如果你不太熟悉前端,别自己去写复杂的 CSS 布局,直接用现成组件库,把精力放在交互逻辑上。

2.3 PyCharm在开发流程中的实际作用

很多教程把 PyCharm 当作一个编辑器提一句就过去了,但实际开发中它对我的帮助远不止写代码。首先,PyCharm 对 Django 的支持极其完善,创建项目时可以直接选择 Django 模板,自动生成 manage.py、settings.py、urls.py 等文件,还能通过 Run Configuration 直接启动 Django 服务。这意味着你不用在终端里手动记忆那些命令,点一下三角形按钮就能跑起来。

其次是调试功能。前后端分离项目出问题,十有八九是接口返回的数据结构不对。在 PyCharm 里给视图函数打断点,可以看到请求参数、数据库查询结果、序列化器输出每一步的状态,排查效率非常高。之前我调试一个订单金额计算错误的问题,就是靠断点一步步看 DecimalField 的精度转换,两分钟就定位了问题。

PyCharm 的数据库面板也很实用,可以直接连接本地数据库,可视化查看表结构和数据。我在设计茶叶商品表时,就用这个面板反复调整字段,看看数据展示效果,比来回切数据库客户端方便多了。专业版还能直接连 Vue 的前端调试,不过社区版已经够用,这个后面再细说。

3. 数据库设计与业务模型梳理

数据库设计是整个系统最该花时间的地方。很多项目做到一半改需求,改的往往不是界面,而是表结构和接口。表设计得好,后面写代码跟填空一样顺畅;表设计得乱,后面每个功能都要打补丁。

3.1 核心数据表:从用户到订单的完整链路

我们这个系统我最终设计了六张核心表,加上 Django 自带的用户权限相关表,一共覆盖了完整的业务闭环。

表名核心字段作用
用户表用户名、密码、手机号、收货地址、角色登录认证、权限区分
茶叶分类表名称、父分类、排序商品分类层级管理
茶叶商品表名称、分类、产地、描述、价格、库存、图片、上下架状态商品展示和库存管理
购物车表用户、商品、数量、加入时间临时购物车存储
订单表订单号、用户、总金额、状态、收货信息、创建时间订单主表
订单明细表订单、商品、购买单价、数量、小计订单快照明细

用户表我直接继承了 Django 的 AbstractUser 扩展,而不是自己从零写。好处是 Django 自带的认证、密码哈希、权限体系全部可用,你只需要加手机号、收货地址等业务字段。

茶叶商品表的字段设计是典型的电商冗余思路。比如价格用 DecimalField 而不是 FloatField,因为浮点数在做金额计算时会引入精度误差,一分钱对不上在订单结算时很头疼。库存用 IntegerField 单独存放,不通过订单明细去反算,这样商品列表页可以直接读库存字段,不需要每次关联查询。上下架状态用 BooleanField,搜索和列表接口直接过滤这个字段。

订单明细表存在的意义容易被新手忽略。如果你只在订单表里存一个“总金额”,而不记录每一款商品当时的单价和数量,将来用户投诉“我买的时候不是这个价”,你就没有任何凭据。明细表里的购买单价是从商品表快照过来的,商品后来涨价降价都不影响历史订单的计算,这是电商系统的标准做法。

3.2 订单状态流转与商品库存扣减逻辑

订单状态这块,我用的是一个简单的状态机:待支付 → 已支付 → 待发货 → 已发货 → 已完成。用户端取消订单限定在“待支付”状态,管理员发货之后就不能取消了。这个限制一定要在接口层面做校验,而不是前端隐藏按钮就完了,因为接口可以被绕过。

库存扣减是另一个容易翻车的地方。最早我的写法是:

tea.stock -= 1 tea.save()

单用户测试没问题,一旦两个用户同时下单,就可能出现“两个人同时读到库存是5,各自减1,最后库存变成4而不是3”的并发问题。正确做法是使用数据库行锁,具体代码后面实操部分会写。这里先记住一个原则:读取库存和扣减库存必须在同一事务里搞定,中间不能有间隙。

订单号生成也是一个细节。我用了“时间戳 + 随机数 + 用户ID”的组合,生成唯一订单号。不要直接用自增 ID 当订单号,一来容易被猜到订单量,二来业务上订单号一般要求可读性更好。订单号生成后建立唯一索引,防止重复。

3.3 权限体系设计

这套系统有两种角色:普通用户和管理员。Django 自带 Group 权限模型,但我没有过度设计,直接给管理员用户加一个 is_staff 标志,再通过自定义权限类控制接口访问。前端菜单也根据用户角色做动态渲染,普通用户看不到后台管理入口,管理员看不到购物车功能。

这里要特别提醒一点,权限校验一定要前后端同时做。前端隐藏菜单只是用户体验上的处理,真正的安全边界在后端。每一个管理接口都要校验用户是不是管理员,否则别人只要知道接口地址,就能直接操作你的商品数据。我用了一个自定义权限类,统一作用于管理相关的 ViewSet 上。

4. 实操过程:从环境搭建到核心模块落地

这一节是整个项目的核心落地过程。我会按照我实际开发的顺序来写,包括环境准备、工程结构、每个核心模块的代码思路,以及中间遇到的坑。你跟着流程走一遍,基本能复现出完整系统。

4.1 环境准备与工程结构搭建

先说工具版本,我在实际开发中用 Python 3.9 + Django 4.2 + Vue 3 + Vite。这些版本在兼容性上都没什么问题,你只要避免用 Python 2 或者 Django 1.x 就行。

后端工程创建流程:

# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(Mac/Linux) source venv/bin/activate # 安装 Django 和 REST 框架 pip install django djangorestframework django-cors-headers # 安装数据库驱动,我用的是 SQLite,零配置 # 如果需要 MySQL,安装 pymysql 并配置连接参数 # 创建 Django 项目和应用 django-admin startproject tea_shop cd tea_shop python manage.py startapp goods python manage.py startapp orders python manage.py startapp users

工程结构上我按业务模块拆分了应用,goods 负责商品和分类,orders 负责购物车和订单,users 负责用户扩展。不要把所有模型塞进一个叫 main 的应用里,项目一旦复杂起来,文件会膨胀到难以维护。

settings.py 里的关键配置:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'goods', 'orders', 'users', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... 默认配置保持不变 ] CORS_ALLOW_ALL_ORIGINS = True # 仅开发环境使用,上线要配置白名单

前端用 Vite 创建 Vue 3 项目:

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

这里的 axios 是网络请求库,vue-router 管路由,pinia 管全局状态,Element Plus 是组件库。这套组合是 Vue 3 项目的主流配置,网上资料好找,遇到问题容易排查。

4.2 用户认证与权限控制实现

用户认证我用了 djangorestframework-simplejwt,也就是基于 JWT 的认证方案。前后端分离项目里,常见的做法不是用 Django Session,而是在用户登录后发放一个 token,前端每次请求在 Header 里带上 token,后端验证通过才返回数据。

settings.py 里配置 JWT:

REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], }

用户注册和登录接口,关键代码:

from rest_framework import serializers from django.contrib.auth import get_user_model User = get_user_model() class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, min_length=6) class Meta: model = User fields = ['username', 'password', 'phone', 'address'] def create(self, validated_data): user = User.objects.create_user( username=validated_data['username'], password=validated_data['password'], phone=validated_data.get('phone', ''), address=validated_data.get('address', ''), ) return user

为什么用 create_user 而不是 create?因为 create_user 内部会对密码进行哈希加密,直接保存明文密码是大忌。这个点我在代码评审时看到过好多次,一定不要用 User.objects.create(password=raw_password)。

后端接口模板:

from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import IsAdminUser from .models import Tea from .serializers import TeaSerializer class TeaViewSet(ModelViewSet): queryset = Tea.objects.filter(is_on_sale=True) serializer_class = TeaSerializer

4.3 商品展示与搜索接口实现

商品列表页要支持分类筛选和关键词搜索,这部分我不建议在 Vue 端做纯前端过滤,因为商品数据量大了以后,一次性传输所有商品会占用大量带宽,加载也慢。正确的做法是后端接口支持 querystring 参数,搜索和筛选交给数据库。

后端代码:

from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Q from .models import Tea, Category class TeaViewSet(ModelViewSet): def get_queryset(self): queryset = Tea.objects.filter(is_on_sale=True) category_id = self.request.query_params.get('category_id') keyword = self.request.query_params.get('keyword') if category_id: queryset = queryset.filter(category_id=category_id) if keyword: queryset = queryset.filter( Q(name__icontains=keyword) | Q(description__icontains=keyword) ) return queryset

前端 Vue 的 axios 请求:

import axios from 'axios' const api = axios.create({ baseURL: 'http://127.0.0.1:8000/api', timeout: 10000, }) api.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config })

这里有个细节,token 从 localStorage 读,可以通过 axios 拦截器统一塞到请求头里,不用每个页面手动加。token 过期时后端返回 401,前端再统一跳转到登录页即可。

商品详情页除了基础字段,还要展示商品图片。Django 里用 ImageField 存储图片路径,需要安装 Pillow:

pip install Pillow

settings.py 配置媒体文件:

MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'

开发环境下路由配置:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

图片上传最常遇到的问题就是前端传的是 FormData,而后端只认 request.data 中的文件字段。后面排查章节我会详细说这个坑。目前先记住,Vue 前端上传时不要设置 Content-Type 为 application/json,要让它自动设置为 multipart/form-data。

4.4 购物车与订单模块对接

购物车简单实现可以直接在 Vue 端用 localStorage 存,但有一种更好的方式——在服务端购物车表里存数据。这样用户换设备登录,购物车还能保持,后面转化率更高。我这次做的是服务端购物车,表结构就是购物车表 + 商品外键 + 用户外键 + 数量。

购物车添加接口思路:

class CartItemViewSet(ModelViewSet): queryset = CartItem.objects.all() serializer_class = CartItemSerializer def perform_create(self, serializer): serializer.save(user=self.request.user)

下单是系统里最容易出错的一环。创建订单的逻辑必须包含:计算总金额、扣减库存、生成订单号、清空购物车、记录订单明细。这些操作必须在同一个事务里完成,任何一个步骤失败都要回滚。

事务和并发扣减库存代码:

from django.db import transaction from django.db.models import F from decimal import Decimal @transaction.atomic def create_order(user, cart_items): order = Order.objects.create( order_no=generate_order_no(user), user=user, total_amount=Decimal('0.00'), status='pending_payment', address=user.address, ) total = Decimal('0.00') for item in cart_items: tea = Tea.objects.select_for_update().get(id=item.tea_id) if tea.stock < item.quantity: raise ValueError(f"商品 {tea.name} 库存不足") tea.stock = F('stock') - item.quantity tea.save() OrderItem.objects.create( order=order, tea=tea, price=tea.price, quantity=item.quantity, subtotal=tea.price * item.quantity, ) total += tea.price * item.quantity order.total_amount = total order.save() cart_items.delete() return order

这段代码里最关键的是 select_for_update,它会锁定被查询的商品行,直到事务结束。这样两个用户同时下单时,后一个用户会等前一个用户的库存扣减完成后再执行,不会出现超卖。我最初没加这个锁,在压测时测出了库存变负数的问题,后来加上就好了。

4.5 Vue前端与后端接口联调

前后端联调阶段,最需要理顺的就是跨域和路由问题。跨域我用了 django-cors-headers 解决,开发环境直接允许所有来源。前端 Vue 项目通过 Vite 启动在 localhost:5173,后端在 localhost:8000,端口不同就是跨域,没有 CORS 配置,浏览器会直接拦截请求。

Vue 路由我用 history 模式,地址看起来更干净,但有个坑:直接访问 /detail/1 这类路径时,开发服务器会返回 404。解决办法是在 Vite 的配置中加一个重定向,把未知路径都指回 index.html:

// vite.config.js export default defineConfig({ plugins: [vue()], server: { historyApiFallback: true, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, })

这个配置顺带解决了开发环境的跨域问题,因为前端访问 /api 开头的路径会被 Vite 代理到后端,浏览器看到的是同源请求。生产环境部署时,用 Nginx 同样配置一个 /api 的转发规则,思路完全一致。

前端组件开发方面,登录页面、商品列表、购物车、订单确认页、后台商品管理,这些页面虽然多,但核心模板逻辑类似。商品列表页用 Element Plus 的 el-card 每次循环渲染一张卡片;后台管理页用 el-table + el-dialog 实现表格展示和弹窗编辑。写 Vue 时养成一个习惯,把 api 调用抽成单独的模块文件,页面组件只关注渲染和交互,这样后期维护方便。

4.6 如果你选了Flask,怎么实现同样的接口

既然标题里写了 flask,我单独说明一下。Flask 实现同样的接口不是不行,只是很多轮子要自己找。数据库我用 Flask-SQLAlchemy,序列化用 Marshmallow,认证用 Flask-JWT-Extended,跨域用 Flask-CORS。你自己算一下,光引入和配置这些库,就要多花不少时间。

用 Flask 实现商品列表接口的简化代码:

from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tea.db' db = SQLAlchemy(app) jwt = JWTManager(app) class Tea(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100)) price = db.Column(db.Numeric(10, 2)) stock = db.Column(db.Integer) @app.route('/api/teas', methods=['GET']) def list_teas(): teas = Tea.query.all() return jsonify([{ 'id': t.id, 'name': t.name, 'price': str(t.price), 'stock': t.stock, } for t in teas]) @app.route('/api/orders', methods=['POST']) @jwt_required() def create_order(): data = request.get_json() # 业务逻辑在这里手动实现 return jsonify({'message': 'order created'})

Flask 的优势是代码量更少、更容易读懂,但代价是 Django 里那些开箱即用的功能,比如 Admin 后台、ORM 迁移、权限组件,你都要要么手动实现,要么引入第三方库。我的看法是:如果你追求快速稳定完成一个包含后台管理的项目,优先 Django;如果项目本身很小、接口数有限、你对每个环节都想深度控制,可以选 Flask。两条路子都走得通,怕的是选之前没想清楚,中途换框架,那种成本最高。

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

开发过程中遇到的问题,很多是网上教程不会专门讲的,但几乎每个做前后端项目的人都会遇到。我把最典型的问题整理成一个速查表,配合排查思路,你遇到类似情况可以直接对号入座。

5.1 跨域请求失败与登录状态丢失

现象:前端调用后端接口,控制台报跨域错误,或者登录接口成功了但后续请求都返回 401。

原因通常是两个方向。第一,CORS 没配好。我开发期间前后端端口不同,跨域是必然的。我用的 django-cors-headers 全局放行,后期部署改成了白名单模式,白名单里写正式域名。第二,token 没传对。很多新手在 axios 实例里忘了加拦截器,或者后端认证类配置成了 Session 认证,两边对不上,登录状态自然就丢了。

排查顺序可以这样:先看浏览器 Network 面板,确认请求是否发出、响应状态码是什么;然后看后端日志,确认请求有没有到达视图;最后再判断是 CORS 拦截还是认证失败。分步排查比自己瞎改配置高效得多。

5.2 图片上传后前端无法显示

图片上传成功,数据库里也有路径,但前端 img 标签显示 404。这个问题我在商品管理页面遇到过。

排查后发现是 MEDIA_URL 没挂载到 Django 的路由里。开发环境需要在 urls.py 里手动加上 static() 路由,生产环境则要配置 Nginx 把 /media/ 路径映射到物理目录。另外一个容易忽视的点是前端显示的地址,如果后端返回的相对路径没有拼接基础地址,浏览器会拿前端域名去请求图片,自然就 404。前端显示图片地址时,要确保把后端的 MEDIA_URL 前缀拼上。

5.3 并发下单导致库存超卖

现象:压测时模拟多用户同时购买同一款茶,库存从 5 变成负数。

原因就是前面说的竞态条件,多个请求同时读到库存 5,各自执行减一,最后写回数据库,库存丢失了更新。解决办法是事务 + 行锁。我在代码里用了 select_for_update,并且把扣库存和创建订单放在同一个 transaction.atomic 块里。压测之后库存数据准确了。

这个问题的深层教训是:不要在 Python 层做“先读后写”的原子操作,除非你加锁。数据库层面的锁才是可靠的并发控制手段。简单说就是读库存和扣库存要一气呵成,中间不留给其他请求插队的空隙。

5.4 数据库迁移导致的历史数据问题

现象:开发过程中改了模型字段,运行 makemigrations 和 migrate 之后,老数据报错或者接口查询异常。

最常见的情况是新增了非空字段,而表里已经有历史数据,Django 会要求你提供默认值。建议在设计阶段就把字段尽量想全,不要在数据积累后再频繁改表。真遇到问题,操作顺序是先备份数据库,再生成迁移文件,如果迁移失败用 migrate 应用名 零 把应用回退到空状态,但注意这会清空该应用对应的表数据,务必谨慎。

我踩过的一个坑是修改了 CharField 的 max_length,迁移时报数据库字段长度冲突。最终方案是删掉迁移文件后重新生成,但前提是表里没有不可恢复的数据。这个经历给了一个教训:模型字段变更要认真对待,这不是前端改个变量名那么简单。

5.5 Vue路由刷新404与打包部署问题

症状:本地开发一切正常,部署上线后刷新任意非首页路径,返回 404。

原因:前端用的是 history 模式,浏览器直接请求 /product/1 时,后端服务器找不到对应文件,返回 404。解决方法是让 Nginx 把找不到的路径统一 try_files 到 index.html:

location / { try_files $uri $uri/ /index.html; }

还有一个常被忽略的部署问题,是接口环境配置。前端打包后,axios 的 baseURL 如果还指向 localhost:8000,线上页面请求就会全部失败。我习惯在项目里建一个环境变量文件,打包时区分开发环境和生产环境,这样切换环境时只改配置,不用改代码。

6. 后续迭代方向与工程习惯总结

项目做到这里,一条完整业务链路已经通了。但每次交付我项目后,我都会习惯性地想一想,如果继续往生产环境走,哪里最值得优化。这套茶叶销售系统也一样,有几个方向是从“能跑”到“好用”之间的必经之路。

6.1 可以扩展的功能方向

支付环节是最值得补的。目前系统只做到订单状态是“待支付”,真实场景里用户希望能微信或支付宝直接扫码支付。接入支付网关并不神秘,申请一个商户号、配置回调接口、在用户支付成功后同步订单状态。如果只是学习演示,也可以接入支付宝沙箱环境,体验整条支付链路。

其次是订单取消和自动超时关闭。用户下单后不支付,订单挂在“待支付”状态,应该有个定时任务每小时扫描一次,超过 15 分钟未支付就自动关闭。这个功能用 Django 的 celery 定时任务可以实现,初次接触可以先用一个管理命令跑定时循环,简单直观。

再就是数据统计。茶叶店老板会很关心哪些茶卖得好、复购率怎么样、什么季节销量上升。后台加一个简单的报表页,统计每日订单量、销售额、TOP 商品排行,其实就是一个聚合查询的事,但对用户价值很高。Django ORM 的 annotate 系列方法就是为此准备的。

6.2 最值得养成的开发习惯

回看这个项目,最值得提醒自己的是这样几个习惯:

第一,先设计数据库,再写业务代码。我见过太多人上来就写视图函数,写到一半发现表结构不对,回头改模型,连带迁移、序列化器、前端组件全都要改,损失的时间远大于当初画表花费的时间。

第二,接口返回值格式要统一。抽出统一的响应结构,比如 { code, message, data },前端可以统一处理。我早期没有做这一步,一个接口返回一个结构,前端到处判断,维护成本翻了一倍。

第三,把关键操作的日志打出来。用户下单、管理员发货、库存变动,这些操作都应该有日志记录。出了问题,日志是你的第一排查依据。否则线上出问题,你对着数据库瞪眼也看不出当时发生了什么。

最后再说一个实际建议:一定要用 Git,哪怕只是自己一个人做项目。每次完成一个功能就提交一次,哪怕只是“结了购物车”这样的非正式信息,将来你改坏了代码,随时能回退。别等最后快答辩了才想起来要一份完整的版本历史,那时候已经晚了。

这套系统做完,我最大的体会是:真正让一个项目有含金量的,不是用到的技术多新,而是你把业务闭环里每个环节都顺了一遍,知道哪些细节会咬人,为什么这么设计。茶叶销售系统没有什么黑科技,但它覆盖了用户、商品、订单、库存、权限、统计这一整条电商逻辑,认认真真走一遍,比刷十遍教程都管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进