
1. 项目到底在做什么一个家具商城的促销抽奖系统做电商相关的开发久了你会发现商城系统本身不是难点难的是把“促销”和“互动”这两个环节真正做起来。最近我把一个基于 Python Vue 的家具商城升级成了带活动抽奖功能的版本后端用 Django 和 Flask 都完整跑过一遍前端用 Vue 写页面IDE 全程使用 PyCharm。整体做下来这个系统覆盖了商品展示、购物车、订单、抽奖资格发放、抽奖开奖、奖品核销这一整套流程。今天把架构设计、核心代码、排坑过程都整理出来给正在做 Python 全栈项目或者在 PyCharm 里折腾 Django 和 Vue 联调的朋友一个可以照着抄的参考。这个项目最适合两类人一类是刚学完 Python 基础想找一个完整全栈实战项目练手的人另一类是已经能写出单页面但没搞过后台权限、抽奖概率控制和并发扣减的人。看完这篇文章你不仅会知道每个文件该写什么还会知道为什么这么写以及哪些地方改一行代码就能让系统崩掉。1.1 核心模块拆解一个家具商城的抽奖系统听起来业务不少但真正拆开来看核心还是四个模块用户、商品、订单、奖品。用户模块负责登录注册和身份识别商品模块负责家具分类与展示订单模块负责交易流转奖品模块负责抽奖活动的配置与中奖记录。前后端分离之后后端只做两件事把数据以 API 的形式吐给前端把业务规则在服务端守住。举个例子“一单满 5000 元送一次抽奖机会”这个规则必须写在服务端不能让前端控制否则用户修改请求参数就能伪造抽奖资格。前端用 Vue 负责渲染商品列表、购物车、转盘动画和结果弹窗PyCharm 则承担后端开发、接口调试和虚拟环境管理的工作。这套设计最大的好处在于后端不用关心页面长什么样前端也不用管数据是从哪来的。以后想换成小程序或者 App后端代码一行都不用动把 Django 换成 Flask只要接口返回的字段不变前端也不需要改动。这种解耦带来的灵活性是我坚持用前后端分离架构来做这个项目的主要原因。1.2 用户场景与业务流程我在动手写代码之前先完整列了一遍用户从进店到抽奖的路径。用户打开商城首页按分类浏览家具把沙发、餐桌、衣柜这些东西加进购物车然后下单付款。订单状态变为已完成之后系统自动给用户发放抽奖机会。用户在“我的奖品”页面看到可用次数点进抽奖页面转盘开始转动后端一次性返回奖品 ID 和名称前端根据返回结果做动画最后弹出“恭喜中奖”的窗口。后端在这条链路上要守三道关卡。第一抽奖资格只能由已完成的订单产生不能通过手工修改数据库来伪造。第二每次抽奖必须实时扣减奖品库存不能出现超发的情况。第三中奖结果要落库保存方便线下核销和后期统计。如果只是搭个 Demo这三道关卡全都可以省略。但项目一旦真上线哪怕日活只有几十个人库存超发的问题也会立刻暴露。所以我写代码时把抽奖资格生成和奖品库存扣减都放进了后端事务里处理。订单完成的同时开启一个事务把抽奖机会记录写进去如果后续操作失败就整体回滚。这个思路在搭建商城类项目时几乎是必须养成的习惯。2. 技术选型Django、Flask 和 PyCharm 是怎么分工的2.1 为什么后端选择了 Django又给 Flask 留了一条路很多人在 Django 和 Flask 之间纠结我的建议是看项目复杂度。这个家具商城涉及用户、商品、订单、中奖记录等多张表还有后台管理功能Django 自带的 ORM、Admin 后台和数据库迁移工具能节省大量开发时间。比如把奖品表做成一个后台可维护的页面在 Django 里只需要三步定义模型、注册到 admin.py、登录后台添加数据。这种省事程度对个人开发者或者小团队来说非常明显。但 Flask 也要在考虑范围内因为并不是所有模块都需要 Django 这种重量级结构。商品展示接口只有三五条没有复杂权限控制时用 Flask 加 SQLAlchemy 完全够用。我在实际开发中先用 Django 实现了商品模块和抽奖模块后来单独把抽奖接口抽出来用 Flask 重写了一遍做对比。做完之后的直观感受是Flask 胜在轻量和自由路由和视图之间的逻辑非常直白想加什么装饰器随手就加Django 胜在全家桶项目结构固定团队协作时不会因为代码组织风格不同而互相看不懂。对比点DjangoFlask项目结构固定 MTV 模式app 划分清晰完全自由入口是一个 app 实例数据库操作自带 ORM 和迁移工具需要自行集成 SQLAlchemy 或 Peewee后台管理自带 admin 后台零成本配置需要 flask-admin 扩展路由写法使用 path 或 re_path 声明使用 app.route 装饰器适用场景多数据模型、复杂业务流小工具、单模块、快速原型表格里的差异很直观。这个项目最终以 Django 为主是因为抽奖相关的数据表有奖品配置表、用户抽奖记录表、机会流水表关联关系比较多Django 的 ORM 在处理这类关联查询时更顺手。但如果你只想做一个转盘抽奖单独应用Flask 会更快。2.2 Vue 在系统里承担的职责前后端分离后Vue 主要解决两个重点商品列表和购物车的状态管理以及抽奖转盘的动画交互。我这次用的是 Vue 3 配合 Vite比 Vue CLI 的启动速度快了一个量级。Vue 最核心的设计理念就是数据驱动视图商品数量一旦变化页面会自动重新渲染完全不需要手动操作 DOM。这种体验在做购物车加减数量、总价实时计算这类交互时比传统的模板引渲染舒服太多了。Vue 项目内部结构我保持得非常简洁只有 views、components、router、api 四个目录。views 放页面级组件components 放可复用组件router 管理路由api 目录统一封装 axios 请求。所有接口路径都从环境变量文件里读取后端地址一旦更换只需要修改 .env 文件不需要在代码里逐个找硬编码的 URL。抽奖转盘用了一个比较取巧的方式不依赖第三方动画库而是把转盘图片分成几个扇形区域每次抽奖前由前端随机生成一段旋转动画最终停止的位置由后端返回的奖品索引决定。后端返回奖品 ID同时返回一个角度偏移量前端把这个角度拼接进 CSS 的 transform 属性里转盘就会精准停在指定区域。这样做的好处是前端无法通过篡改请求来指定中奖结果所有概率判断都在服务端完成。2.3 PyCharm 环境搭建和项目初始化用 PyCharm 打开项目之前先把两个基础环境准备好Python 解释器和虚拟环境。这里有个经验一定要在项目目录里创建 venv 虚拟环境而不是直接复用全局解释器。虚拟环境隔离依赖版本这个坑我踩过好几次。之前在全局环境装过 Django 3.2后来新项目需要 Django 4.2版本冲突导致服务起来就报错。换成虚拟环境之后每个项目独立一套依赖不会再互相干扰。具体操作步骤是在 PyCharm 的 Settings 菜单里找到 Project Python Interpreter选择新建虚拟环境Python 版本建议用 3.9 或者 3.10。不建议直接追最新版 Python有些第三方依赖还没有完成适配。创建好虚拟环境后在 PyCharm 下方的 Terminal 面板里执行 pip 安装命令。这里有一个小技巧不要用 Settings 里的图形界面搜索安装包直接在 Terminal 里通过 pip 命令安装配合国内镜像源速度快很多。PyCharm 本身不用做过多的配置就能直接写 Django 项目。唯一的补充项是安装几个必要的插件Django 插件在 PyCharm Professional 里是自带的社区版需要手动安装Vue 插件如果缺失打开 .vue 文件时代码高亮不正常一样在插件市场搜索下载就能解决。3. 家具商城后端 API 的实现细节3.1 数据模型设计家具商城不是只有一张商品表这么简单。一个能跑通完整流程的最小版本至少要包含以下数据表用户表、商品分类表、商品表、订单表、订单明细表、抽奖机会表、奖品表、中奖记录表。商品表里需要定义分类外键、名称、描述、价格、库存、图片地址、上下架状态、创建时间。价格字段不建议用 FloatField浮点数在运算时会产生精度问题应该使用 DecimalField。库存字段要加默认值并设置校验保证不能为负数。上下架状态是布尔字段控制商品是否在前端展示。订单表和抽奖机会表是一对多的关系。一个订单在完成状态变更后需要在抽奖机会表里生成一条记录这条记录要保存来源订单号方便排查问题。奖品表里要定义奖品名称、奖品等级、库存总量、剩余库存、中奖权重。中奖权重这个字段是抽奖概率控制的核心后面会专门讲。以 Django 模型为例我摘几个关键部分。商品模型大概长这样from django.db import models class Category(models.Model): name models.CharField(max_length50) sort models.IntegerField(default0) class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE) name models.CharField(max_length100) price models.DecimalField(max_digits10, decimal_places2) stock models.IntegerField(default0) image models.URLField(blankTrue) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue)写完模型后在 PyCharm 的 Terminal 里执行两条命令makemigrations 和 migrate。Django 会把模型自动转换成数据库表整个过程不需要手写任何 SQL。如果使用 Flask模型定义方式差别不大只是需要用 SQLAlchemy 的 Column 类型来声明字段。3.2 用 Django REST Framework 提供商品接口模型定义好之后接下来就是把数据通过 API 暴露出去。这里我强烈建议直接使用 Django REST Framework而不是自己手动写 JSONResponse。DRF 提供了序列化器、分页、认证、权限控制等一系列组件能让代码结构清晰很多。商品列表接口的实现方式是这样的from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from .models import Product from .serializers import ProductSerializer class ProductListView(APIView): def get(self, request): queryset Product.objects.filter(is_activeTrue) category request.query_params.get(category) if category: queryset queryset.filter(category_idcategory) paginator PageNumberPagination() page paginator.paginate_queryset(queryset, request) serializer ProductSerializer(page, manyTrue) return paginator.get_paginated_response(serializer.data)说明一下这里面几个容易被忽略的点。第一分页是必须的家具商城商品数量过百之后一次性返回所有数据会让前端渲染卡顿。第二筛选参数要从 query_params 里读取而不是直接写在 url 路由里这样更灵活。第三过滤条件是 is_activeTrue下架商品不应该出现在前台。serializer 的定义同样简单from rest_framework import serializers from .models import Product class ProductSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) class Meta: model Product fields [id, name, price, stock, image, category_name]这里用 source 参数把分类名称带了出来前端拿到数据后就不需要再根据 category_id 去查字典减少一次请求。3.3 购物车与订单接口设计购物车的数据我选择存放在前端状态里没有单独建表。原因是购物车本质上是一个临时性的数据结构用户清空购物车或者退出登录后不需要保留。真正需要永久保存的是订单。这个取舍在小型商城里完全够用但如果要做多端同步购物车就需要在后端建表。下单接口是核心逻辑必须严谨。用户从前端提交订单时后端要做的第一件事不是扣库存而是校验商品是否还有效。校验通过后在同一个事务里完成两件事生成订单记录、扣减库存。这里的重点在于并发问题。两个用户同时下单同一件商品如果不做控制可能出现库存只剩一件但两个订单都扣减成功的现象。解决这个问题最直接的方式是使用数据库的行级锁。在 Django 中可以通过 select_for_update 实现from django.db import transaction transaction.atomic def create_order(user, items): for item in items: product Product.objects.select_for_update().get(iditem[product_id]) if product.stock item[quantity]: raise ValueError(库存不足) # 创建订单和订单明细 # 扣减库存 # 生成抽奖机会select_for_update 会给查询到的商品行加上锁直到事务结束才释放。这样两个并发请求同时进来时第二个请求会等待第一个请求完成然后才能继续执行。这个机制简单有效代价是性能有一定损耗但在这个量级的商城里完全足够。完成订单后抽奖机会流水也要在同一个事务里生成。规则是订单金额每满 5000 元生成一次抽奖机会以此类推。这块计算逻辑放在生成订单的地方统一处理避免用户重复下单时漏发。4. 活动抽奖系统的核心逻辑4.1 抽奖资格与奖品池的初始化抽奖系统最关键的不是转盘动画而是资格的产生和消耗。用户在完成订单后获得抽奖机会机会记录保存在抽奖机会表里字段至少要包含用户 ID、来源订单 ID、状态未使用、已使用、已过期、有效期截止时间。奖品池的配置建议直接在 Django Admin 后台维护。每一条记录包含奖品名称、奖品等级、权重。权重决定着这个奖品的实际抽出概率。比如一共设置四个奖品奖品等级库存权重一万元优惠券特等奖51高端按摩椅一等奖205品牌抱枕二等奖20030谢谢参与参与奖999991000这里的权重并不是中奖率而是相对权重。算出所有奖品总权重每个奖品被抽中的概率等于自身权重除以总权重。这种设计的好处是运营人员调整某个奖品的权重所有奖品的概率都会自动重新计算不需要改代码。库存字段要单独设置剩余库存。每次开奖时先判断剩余库存是否大于零再进行权重随机最后扣减库存。如果随机到了库存为空的奖品要抛给前端一个“奖品已抽完”的提示并让用户重新抽一次或者自动掉落到下一档奖品。这里我选择了重新抽一次逻辑更简单。4.2 抽奖概率控制的算法实现抽奖算法用 Python 实现起来非常直接。核心思路是把每个奖品看成一段区间区间的长度等于权重值。然后把所有区间连起来生成一个随机数看这个随机数落在哪个区间里就选中哪个奖品。代码实现大概是这样的import random def draw_prize(prizes): total_weight sum(p.weight for p in prizes) random_value random.randint(1, total_weight) current 0 for prize in prizes: current prize.weight if random_value current: return prize return None这个算法的优点在于简单高效即使奖品池有上百个奖品循环一遍也就完成。实际项目中需要注意的一点是random.randint 的区间和判断逻辑要保持一致不然会出现权重最小的奖品永远抽不到的情况。为了提高抽奖体验我还在代码里加了一层“阻尼”逻辑如果特等奖和一等奖被抽走剩余库存减少它们的实际中奖率就会自然下降。运营人员不需要手动去调整权重系统会随着库存消耗自动收敛。这是保证活动不亏本的一个重要手段。4.3 库存扣减与防止超发上面提到过并发的库存扣减问题。抽奖接口同样要处理并发场景而且比下单接口更敏感。用户疯狂点击抽奖按钮后端如果同时收到多个请求每个请求都读到了相同的库存就会出现超发。解决办法和订单扣库存一样使用事务加行锁。奖品表是一个独立的数据表每次抽奖时先 select_for_update 锁定奖品记录然后判断库存执行扣减。要注意的是中奖记录必须在同一个事务里写入锁的释放依赖于事务提交。有些项目会引入 Redis 做库存预扣减在高并发场景下性能更好但复杂度也更高。对这个家具商城抽奖活动来说数据库行锁完全够用不需要额外引入缓存中间件。中奖记录表里要保存用户 ID、奖品 ID、抽奖时间、领取状态。用户抽完奖后弹窗提示中奖结果记录进入“我的奖品”列表。工作人员线下核销时根据记录中的唯一流水号进行确认核销后标记为已领取。5. 从零到跑通完整实操流程记录5.1 PyCharm 中创建后端项目和虚拟环境打开 PyCharm选择新建项目项目名称取 furniture_mall解释器选择新建虚拟环境。虚拟环境创建完成后打开 Terminal依次执行安装命令pip install django djangorestframework django-cors-headers安装三个库就够了。MySQL 客户端库这里不装我直接用 Django 自带的 SQLite 数据库跑开发环境方便迁移和调试。如果想上 MySQL只需在 settings.py 里改数据库配置并安装 mysqlclient。接下来创建 Django 项目和 appdjango-admin startproject config . python manage.py startapp mall python manage.py startapp lotterymall 这个 app 负责商品和订单lottery 负责奖品和抽奖机会逻辑划分清楚避免所有代码堆在一堆。创建完成后在 config/settings.py 里注册 app并把 DRF 加到 INSTALLED_APPS。同时要配置跨域。前后端分离开发时Vue 跑在 5173 端口Django 跑在 8000 端口两者端口不同浏览器会拦截跨域请求。安装 django-cors-headers 后在 MIDDLEWARE 里添加 CorsMiddleware并配置允许的域名CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]这样前端页面才能正常调用后端接口。这个配置不写接口联调阶段必然报跨域错误提前做好能少踩一个坑。5.2 前端 Vue 项目生成与联调配置在 PyCharm 里同时管理前后端项目方法很简单把 Vue 项目直接放在同一个根目录下的前端文件夹里用 PyCharm 的终端执行 Vite 相关命令。创建 Vue 项目npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios vue-router启动开发服务器后Vue 默认监听 5173 端口。在 frontend 根目录创建 .env 文件写入后端地址VITE_API_BASE_URLhttp://127.0.0.1:8000/api之后在代码里统一使用 axios 发起请求baseURL 从环境变量中读取。联调时先在浏览器手动访问 Django 接口地址确认数据返回正常再到前端页面调试。这样可以快速定位问题出在前端还是后端。5.3 抽奖接口和前端转盘的对接后端写好转盘接口后前端调用流程如下用户点击“开始抽奖”前端先调用后端接口获取可用抽奖次数如果次数大于零向后端 POST 抽奖请求。请求成功后后端返回奖品信息和停驻角度。前端拿到停驻角度后给转盘元素加上 CSS transition 属性然后修改 transform rotate 值转盘就会平滑转动到指定区域。动画结束后弹窗显示中奖结果。调用接口和动画结束之间要注意时序不要让用户多次点击按钮导致重复抽奖。前端加一个 loading 状态动画未结束前禁用按钮。6. 常见问题与排错经验实录6.1 跨域请求被拦截前后端分离项目里这个错误出现频率最高。现象是前端 axios 请求报错浏览器控制台显示 CORS 错误。解决办法就是上面提到的 django-cors-headers 配置。需要注意 CORS_ALLOWED_ORIGINS 里的地址必须和前端页面的完整地址一致包括端口号。localhost 和 127.0.0.1 是两种不同的来源都要配进去。6.2 数据库时区和时间显示问题Django 默认使用 UTC 时间如果前端展示时间发现比本地时间差了 8 小时需要在 settings.py 里配置TIME_ZONE Asia/Shanghai USE_TZ True配置后Django 写入的时间会转化为本地时区。要注意的是数据库中存储的仍然可能是 UTC 时间由 Django 在读取时自动转换。如果自己写原生 SQL 查询可能需要手动处理时区转换。6.3 前端组件中图片不显示Vue 项目中img 标签的 src 如果直接写成相对路径打包后容易失效。正确做法是把图片放在 src/assets 目录下然后通过 import 引入或者使用绝对路径的环境变量拼接。这里最容易踩的坑是使用反向代理时图片地址被代理规则改写导致浏览器返回 404。实际情况中我建议商品图片直接存放对象存储服务数据库里保存完整 URL。这样前端展示时不用考虑打包路径问题后端也不会因为图片文件管理而头疼。6.4 PyCharm 安装依赖包失败在 PyCharm 里安装 numpy、pandas 或者 mysqlclient 时经常会遇到安装失败。大多数时候是 pip 版本太旧或者是网络连接国外源不稳定。先升级 pip 再安装python -m pip install --upgrade pip pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple配置国内镜像源之后安装速度和成功率都会有明显提升。如果某个包仍然安装失败建议先在命令行单独安装这个包确认成功后再回来跑项目避免项目启动时因为依赖缺失报一堆错。6.5 抽奖概率和实际结果对不上如果发现实测抽奖结果和预设概率差异很大先检查权重算法是否正确。最常出的问题是总权重计算时把已下架奖品的权重也算进去了。正确做法是只统计剩余库存大于零的奖品。再一个原因是前面讲的 random_value 的边界问题做一次简单的循环测试就能发现。我习惯在开发阶段写一个单元测试模拟抽奖十万次统计每个奖品出现的次数占比对比预设权重是否在合理误差范围内。这个测试能快速定位概率算法的错误比上线后靠用户反馈靠谱得多。7. 个人实操体会和后续扩展方向在做这个项目的过程中我最大的体会是一个看起来简单的抽奖功能其实牵涉到订单系统、库存系统、账户系统等多个模块的联动。如果一开始不把事务边界划清楚后面改任何一个环节都可能引发连锁问题。分享一个自己在实际开发中总结的经验在写业务代码之前花半小时把核心流程的数据流向画出来标注哪些操作在同一个事务里哪些操作可以异步执行。画完再动手代码质量完全不一样。这个项目后续可以扩展的方向很多。比如抽奖机会可以通过每日签到获得增加用户活跃度优惠券中奖后可以直接关联商城订单抵扣中奖记录增加短信通知功能。如果你正在做类似的全栈项目建议先从最简版本跑通再逐步增加这些附加功能这样每一步都有明确的验收标准不会被突如其来的 bug 打乱节奏。