ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue+Django实战:家具商城与活动抽奖系统开发全解析

Vue+Django实战:家具商城与活动抽奖系统开发全解析 这段时间刚把一个家居商城的全栈项目收尾主技术栈定的是 Vue 3 Django中间为了对比轻量方案又用 Flask 单独写了抽奖接口的参考实现。整套开发都在 PyCharm 里完成从 Python 环境搭建到 Vue 工程初始化再到抽奖并发控制踩坑记录攒了不少。这篇文章就把这个“家具商城 家居店活动抽奖系统”完整拆一遍把技术选型、商城模块、抽奖算法、常见坑位都讲清楚。如果你准备做电商类毕设或者想给实体家居店做一个集商品浏览、下单、会员抽奖于一体的前后端分离项目这篇可以直接当实战笔记来用。1. 项目定位与整体拆解1.1 标题里说的“家具商城 家居店活动抽奖系统”本质上是什么很多刚接触 Python 的初学者看到“基于 Vue 的家具商城”这种标题会以为要同时精通前端 Vue、后端 Python、数据库、部署好像是个大工程。其实剥开来看这个项目就是两个相互独立又能连通的业务场景嵌套商城侧提供家居商品的分类浏览、详情展示、搜索、加购、下单。这类功能是所有电商系统的地基不管你是卖家具、卖农产品还是卖虚拟商品骨架都一样。活动侧以“家居店周年庆抽奖”为入口用户在商城消费后或每日签到获得抽奖次数点击抽奖后后端根据奖品库存和权重概率返回中奖结果。抽奖活动要和商城用户体系打通同时必须满足并发下不超卖、不重复抽奖。换句话说项目的核心不是“怎么写一个好看的家具页面”而是“如何用 Python 后端把商品订单和营销活动这两类业务安全、稳定地串起来”。商城负责商品数据流和交易流程抽奖负责概率玩法和库存控制两边通过同一个用户身份做关联。1.2 为什么技术栈是 Vue Django/Flask PyCharm 的组合先讲选型。前端选 Vue最现实的理由有两个一是 Vue 的生态和上手曲线对 Python 开发者比较友好组件化和 SPA 开发模式能在一个页面里承载商品列表、购物车弹窗、抽奖转盘这些复杂交互二是现在前后端分离已经是常态前端只需要通过接口拿数据不需要再纠结 Django 模板语法。后端在 Django 和 Flask 之间我是这样分配的整套商城系统用 Django抽奖接口单独写一份 Flask 参考实现。为什么这么拆Django 自带 ORM、Admin 后台、Session、权限体系做商城这种实体关系复杂、后台管理需求多的项目能省掉大量重复开发。商品分类、购物车、订单、奖品表之间的关联Django ORM 的 ForeignKey 能直接支撑。Flask 更轻适合做单一职责的抽奖微服务。如果团队已经有商城系统不想动核心代码完全可以独立部署一个 Flask 抽奖服务前端单独调用它。Flask 没有“全家桶”约束启动速度快路由灵活。开发工具统一用 PyCharm。PyCharm 对 Django 有成熟的工程支持对 Vue 也有对应插件和 Node 集成一个 IDE 同时管理前后端项目联调时不用来回切换软件。当然实际生产环境里你不想维护两套后端框架的话抽奖接口也完全可以放进 Django 的 lottery app 里。这篇文章会把两种写法都写出来你看完就能判断哪种更适合自己的场景。1.3 三端功能模块怎么划分整个系统我拆成三个部分模块前端Vue后端Django后端Flask 参考实现商城商品模块商品列表、详情、分类筛选商品/分类模型、商品接口不参与购物车与订单购物车页面、订单确认页购物车模型、订单创建、库存扣减不参与抽奖活动抽奖转盘、中奖记录页活动表、奖品表、抽奖记录表独立抽奖服务权重概率、库存扣减模块划分的原则只有一个每个后端接口职责单一前端页面只关心数据展示不关心业务规则。这样后续做促销、改装抽奖规则时只需要改后端逻辑前端页面基本不动。比如临时要把一等奖概率从 1% 调到 3%前端一行代码都不用改后台改一下权重字段就行。2. 开发环境配置PyCharm 里管理 Python 与 Vue 双端工程2.1 Python 环境安装与虚拟环境先说环境。很多人卡在第一步Python 官网下载安装包时忘记勾选 “Add Python to PATH”导致后续在命令行敲python没反应。我建议 Windows 上安装时直接勾选 PATH可以省掉后续手动配置环境变量的麻烦。更稳妥的做法是在 PyCharm 里新建项目时直接选择 “New environment using Virtualenv”让 PyCharm 自动创建虚拟环境。虚拟环境解决什么问题不同项目依赖不同版本的 Django如果不隔离A 项目升级了 DjangoB 项目可能直接跑不起来。虚拟环境给每个项目分配独立的依赖目录互不污染。我这里的后端工程名叫furnishop终端按顺序操作# 进入虚拟环境后 pip install django django-admin startproject furnishop cd furnishop python manage.py startapp mall python manage.py startapp lotterymall应用负责商城商品、购物车、订单lottery应用负责活动抽奖。按应用拆业务是 Django 官方推荐的做法也是后期维护的关键一个应用只做一类事。2.2 Vue 项目初始化与依赖安装Vue 这边需要 Node.js 环境。装完 Node 后npm命令就跟着有了。我用 Vue CLI 初始化了一个叫furnishop-web的前端工程npm install -g vue/cli vue create furnishop-web cd furnishop-web npm install axios vue-router4 npm run dev创建过程中会让你选 preset建议选 Vue 3。新手先用 JavaScript 版本不用 TypeScript减少心智负担。vue-router用来管理路由项目里主要用到这几个页面首页/、商品列表/products/:id?、购物车/cart、订单确认/order、抽奖页/lottery。2.3 PyCharm 运行配置与联调小技巧PyCharm 里要同时跑两个进程。右上角打开运行配置添加两个配置项Django 配置Module name 填furnishopParameters 填runserver 127.0.0.1:8000Python interpreter 选刚才创建的虚拟环境。Vue 配置新建一个 npm 配置working directory 指到furnishop-webscripts 选dev。实际开发时有个非常实用的联调方案在furnishop-web/vite.config.js里做代理转发让前端请求/api自动打到后端 8000 端口而不是直接让前端访问跨域地址import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样做的好处是前端页面访问http://localhost:5173但/api下的请求都被代理到 Django浏览器不会出现跨域预检问题。部署阶段通过 Nginx 对/api再做一次反向代理路径保持统一切换环境时只需要改配置文件。3. 家具商城核心模块商品、购物车、订单一步到位3.1 商品分类、商品和购物车模型的字段设计商城模块的表结构我用了最经典的三张表分类表Category、商品表Product、购物车表Cart。放进mall/models.py里大概是这样from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length50, uniqueTrue) sort models.IntegerField(排序, default0) class Meta: db_table mall_category class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE, related_nameproducts) name models.CharField(商品名称, max_length120) cover models.CharField(封面图, max_length255) price models.DecimalField(价格, max_digits10, decimal_places2) stock models.IntegerField(库存, default0) detail_pdf models.CharField(说明书PDF, max_length255, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table mall_product class Cart(models.Model): user_id models.IntegerField(用户ID, db_indexTrue) product_id models.IntegerField(商品ID) quantity models.IntegerField(数量, default1) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table mall_cart字段设计核心原则只存真正需要的信息。拿价格举例为什么用DecimalField而不是FloatField因为浮点数做金额计算会出现 0.1 0.2 这种精度问题电商金额必须精确一旦涉及优惠券抵扣、订单分账浮点数误差会被放大。DecimalField把金额当定点数处理正确性优先级最高。删除数据时也值得注意如果不想要某个分类及其下面的商品直接写Product.objects.filter(category_id5).delete()可以批量删除商品然后再处理分类。外键用on_deletemodels.CASCADE以后删除分类时关联商品也会被自动清理但业务上要谨慎不要把重要的商品数据误删。3.2 商品接口与 Vue 页面绑定后端接口我用了纯 Django 的JsonResponse没有额外引入 Django REST Framework。这样自由度更高也方便新手看清“接口返回值”到底是怎么拼出来的。商品列表接口大概长这样from django.http import JsonResponse def product_list(request, cid0): qs Product.objects.select_related(category).order_by(-id) if cid: qs qs.filter(category_idcid) data [{ id: p.id, name: p.name, cover: p.cover, price: str(p.price), category: p.category.name, } for p in qs] return JsonResponse({code: 0, data: data})在config/urls.py里注册路由把mall应用地址映射进来。返回格式统一用{code: 0, data: ...}前端axios就能统一做拦截处理import axios from axios const request axios.create({ baseURL: /api, timeout: 5000 }) request.interceptors.response.use( res { if (res.data.code ! 0) { return Promise.reject(new Error(res.data.msg || 请求失败)) } return res.data.data }, err Promise.reject(err) ) export default requestVue 页面里调用就非常简单了。商品列表页结构大致是顶部是分类 tab下面是一个v-for渲染卡片列表。路由用了:cid?可选参数点击分类后更新 URL同时触发重新请求。商品详情页如果需要展示 PDF 说明书直接用iframe :srcpdfUrl就能在页面里显示浏览器原生支持 PDF 预览前提是后端用FileResponse返回 PDF 并保证响应头是application/pdf。这个能力和 Vue 本身关系不大本质是浏览器对 PDF 类型文件的处理逻辑。3.3 购物车、订单与库存扣减的“原子性”商城模块里最关键的操作是下单扣库存。这里最容易犯的错是“先检查库存再修改库存”——两个操作之间别的用户可能已经抢走了库存。用一个生活类比电影院只剩一张票你和朋友同时点购买如果系统先各自查一遍余票都发现剩 1 张然后各自扣减结果就是超卖 1 张。正确做法是让库存扣减变成一个原子操作。Django 里最简单的方式是使用事务配合F表达式from django.db import transaction from django.db.models import F from django.http import JsonResponse transaction.atomic def create_order(request): product_id request.POST.get(product_id) quantity int(request.POST.get(quantity, 1)) try: product Product.objects.get(idproduct_id) except Product.DoesNotExist: return JsonResponse({code: 1, msg: 商品不存在}) if product.stock quantity: return JsonResponse({code: 1, msg: 库存不足}) # 库存扣减放到一条 SQL 里完成避免并发覆盖 Product.objects.filter(idproduct_id, stock__gtequantity).update(stockF(stock) - quantity) order_no generate_order_no(request.user.id) # 创建订单记录... return JsonResponse({code: 0, data: {order_no: order_no}})第二个容易被忽略的点是订单号生成。不要用数据库自增 ID 直接当订单号很容易被猜出业务量而且分布式部署时自增 ID 会冲突。我习惯用时间戳加用户 ID 加随机数拼一个 32 位以内字符串规则可控也不会暴露真实订单量。4. 家居店抽奖活动系统加权概率、库存锁与防刷设计4.1 抽奖活动、奖品、抽奖记录三张表抽奖模块的数据结构比商城模块稍复杂核心是三张表Activity定义活动起止时间、每人每日抽奖次数、总抽奖次数上限。Prize属于某个活动包含奖品名称、等级、权重、总库存、剩余库存、是否兜底奖品。DrawRecord记录谁在哪个活动里抽到了什么奖品也是防刷校验的数据基础。# lottery/models.py from django.db import models from django.utils import timezone class Activity(models.Model): name models.CharField(活动名称, max_length64) start_at models.DateTimeField(开始时间) end_at models.DateTimeField(结束时间) daily_limit models.IntegerField(每日抽奖次数, default1) total_limit models.IntegerField(活动总抽奖次数, default5) is_active models.BooleanField(启用, defaultTrue) class Prize(models.Model): activity models.ForeignKey(Activity, on_deletemodels.CASCADE, related_nameprizes) name models.CharField(奖品名, max_length64) level models.CharField(等级, max_length20, defaultnormal) weight models.IntegerField(权重, default1) total_count models.IntegerField(总库存, default0) remaining_count models.IntegerField(剩余库存, default0) is_default models.BooleanField(兜底奖品, defaultFalse) class DrawRecord(models.Model): user_id models.IntegerField(用户ID, db_indexTrue) activity models.ForeignKey(Activity, on_deletemodels.CASCADE) prize models.ForeignKey(Prize, nullTrue, on_deletemodels.SET_NULL) created_at models.DateTimeField(抽奖时间, defaulttimezone.now)这里要解释“权重”的作用。很多抽奖系统直接把概率写死在代码里一旦运营想调整奖品概率就得重新发版非常麻烦。把权重放到数据库字段里运营在后台改数值接口概率立即变化。比如一等奖权重 1、二等奖权重 3、谢谢参与权重 6概率池就是 10某个奖品的中奖概率等于它的权重除以总权重。4.2 加权随机算法与限量奖品处理核心算法其实是一段很短的代码import random def pick_prize(prizes): total_weight sum(p.weight for p in prizes) if total_weight 0: return None seed random.randint(1, total_weight) for prize in prizes: seed - prize.weight if seed 0: return prize return prizes[-1]逻辑很简单假设一等奖权重 1、二等奖权重 2总权重 3。系统随机生成 1 到 3 的整数落在 1一等奖落在 2 或 3二等奖。权重越大占据的随机区间越长中奖概率自然就高。但真实项目不能只写这一段还有一个关键动作限量奖品剩余库存为 0 时必须从候选奖池中剔除。如果奖品发完还占着权重用户会不断抽到已空的奖品然后永远落在空挡上。正确处理是先查剩余数大于 0 的奖品再进入权重计算。另外如果所有非兜底奖品都被抽完接口要能正常返回“未中奖”或者直接给兜底奖品而不是抛异常。4.3 Django 抽奖接口事务、行锁与防刷下面是抽奖接口最核心的一段。完整逻辑是先做活动状态和用户次数校验再用事务包裹“锁奖品、扣库存、写记录”三步。重点看select_for_updatefrom datetime import date from django.db import transaction from django.http import JsonResponse from django.utils import timezone def draw(request): user_id request.session.get(user_id) if not user_id: return JsonResponse({code: 401, msg: 请先登录}) activity_id request.POST.get(activity_id) activity Activity.objects.filter(idactivity_id, is_activeTrue).first() now timezone.now() if not activity or not (activity.start_at now activity.end_at): return JsonResponse({code: 1, msg: 活动未开始或已结束}) # 防刷校验每天一次 used_today DrawRecord.objects.filter( user_iduser_id, activity_idactivity_id, created_at__datedate.today() ).count() if used_today activity.daily_limit: return JsonResponse({code: 1, msg: 今天的抽奖次数用完了}) with transaction.atomic(): # 锁住所有非空奖品行避免并发超发 prize_rows list( Prize.objects.select_for_update() .filter(activity_idactivity_id, remaining_count__gt0) .order_by(id) ) # 过滤掉兜底奖品正常奖品优先参与概率计算 normal_prizes [p for p in prize_rows if not p.is_default] target pick_prize(normal_prizes) if target is None: # 所有非兜底奖品都被抽完直接未中奖 DrawRecord.objects.create(user_iduser_id, activity_idactivity_id, prizeNone) return JsonResponse({code: 0, data: {prize: None, msg: 未中奖}}) # 扣减奖品库存并写入抽奖记录 target.remaining_count - 1 target.save(update_fields[remaining_count]) DrawRecord.objects.create(user_iduser_id, activity_idactivity_id, prizetarget) return JsonResponse({code: 0, data: {prize: target.name, level: target.level}})为什么必须用select_for_update如果不锁行两个并发请求可能同时读到remaining_count1然后都执行remaining_count - 1库存变成 -1也就是奖品超发。select_for_update会在数据库层面对读到的奖品行加排他锁第二个请求必须等第一个事务提交后才能读同一行扣减操作就串行化了。还要注意两点select_for_update必须在事务里使用锁的粒度越细越好。千万别给整个 Activity 表加锁那会让一个活动所有用户的请求都排队等待严重影响体验。生产环境流量特别大时更常用的是把奖品剩余数放到 Redis 里用 INCR/DECR 做原子扣减但数据库行锁在小流量到中等流量下已经足够可靠实现也更简单。4.4 Flask 抽奖参考实现更轻的另一种写法如果抽奖模块不想和 Django 商城绑在一起用 Flask 独立写一个服务是更轻的选择。Flask 的数据库操作可以用 SQLAlchemy下面是一个精简版演示from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy import random from datetime import date app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///draw.db db SQLAlchemy(app) class Prize(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64)) weight db.Column(db.Integer, default1) remaining_count db.Column(db.Integer, default0) class DrawRecord(db.Model): id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, indexTrue) prize_id db.Column(db.Integer) created_at db.Column(db.DateTime, defaultdate.today) app.post(/api/draw) def draw(): user_id request.headers.get(X-User-Id) if not user_id: return jsonify(code401, msg未登录) used DrawRecord.query.filter( DrawRecord.user_id int(user_id), DrawRecord.created_at date.today() ).count() if used 1: return jsonify(code1, msg今天已经抽过) prizes Prize.query.filter(Prize.remaining_count 0).all() total_weight sum(p.weight for p in prizes) if total_weight 0: return jsonify(code0, data{prize: None}) seed random.randint(1, total_weight) target None for p in prizes: seed - p.weight if seed 0: target p break if target is None: return jsonify(code0, data{prize: None}) target.remaining_count - 1 db.session.add(DrawRecord(user_idint(user_id), prize_idtarget.id)) db.session.commit() return jsonify(code0, data{prize: target.name})这个简化版为了演示省略了并发控制生产环境同样需要把“查奖品、扣库存、写记录”放进事务并使用SELECT ... FOR UPDATE。Flask 和 Django 的核心差别不在业务代码写法而在于配套Django 自带 Admin、Migration、SessionFlask 要自己组装换来的是极简和灵活。如果公司已有现成用户体系只想快速上线一个营销接口Flask 更合适如果从零做一个商城加活动老老实实用 Django能少操心很多基建问题。5. 高发问题排查与上线前检查清单5.1 跨域、Token 校验、Vue 路由刷新 404前后端分离项目里这三个问题是新手高频踩坑点。跨域方面开发环境最推荐用 Vite proxy前文已经写过配置好处是不用在后端开跨域浏览器看到的请求路径还是同源。但如果你是 Vue 和后端分属两个域名比如后端只做 API、前端独立部署就必须在后端开 CORS。Django 装django-cors-headers在settings.py里配置CORS_ALLOWED_ORIGINSFlask 用 Flask-CORS 的CORS(app)即可。登录状态校验上这里的商城接口为了演示用了request.session抽奖接口要求请求头带用户 ID。真实项目建议用 JWT前端登录拿到 token之后每个请求放进Authorization头后端统一校验。Vue 的axios拦截器非常适合处理这个场景请求前自动追加 token响应 401 时统一跳转登录页。Vue 路由刷新 404 是另一个经典问题。前端用 history 模式时URL 没有#刷新页面浏览器会向后端发起真实请求如果后端没有对应路由就会 404。解决方法本地开发靠 Vite 开发服务器自动回退到 index.html生产环境在 Nginx 配置location / { try_files $uri $uri/ /index.html; }让所有前端路由都回到 SPA 入口。5.2 PyCharm 调试断点打在前后端哪一侧我在 PyCharm 里调试这个项目的习惯是“后端打主战场”。商城和抽奖的大部分核心逻辑都在 Python 侧比如抽奖接口的权重计算、库存扣减、记录写入。遇到问题直接在后端接口函数里打断点然后从前端页面触发一次请求浏览器 Network 面板找到失败请求PyCharm 的 Debugger 停在断点处逐步查看变量值很快就能定位问题。Vue 侧调试更多是检查页面数据和接口返回是否对应。展示不对先看浏览器 Network 面板里的 JSON 是否符合预期再回 Vue 组件排查渲染逻辑。PyCharm 新版自带 Vue 文件高亮和语法提示插件方面建议装 Vue.js 插件。至于 AI 辅助插件可以有但别指望它直接纠正复杂业务逻辑最终还是要靠读代码和调试理解问题。5.3 上线前必须做的一串检查项目跑起来不等于能上线以下几个点我每次都会过一遍后端settings.py里DEBUG必须改为FalseALLOWED_HOSTS写清楚域名不要用*。数据库迁移是否完整依次执行python manage.py makemigrations和migrate确认mall_category、mall_product、lottery_prize等表都创建成功。抽奖活动时间边界结束时间过了以后接口要能正确拒绝不能只靠前端隐藏按钮因为抽奖接口可以直接被外部调用。奖品库存和权重字段要提前做好备份运营改配置时要有据可查。抽奖记录和订单表要考虑重复提交问题必要时加唯一索引或做幂等键避免同一用户并发点击生成多条异常数据。最后分享一个实际开发中的体会这个项目的抽奖模块完成后运营最常操作的不是抽奖本身而是改奖品权重和补库存。所以像权重、库存这种字段一定不要写死在代码里全部做成数据库可配置。你在后台改一条权重运营立刻能看到概率变化这种细节比炫酷的转盘动画更影响用户体验。希望这篇实战笔记能帮你少踩几个坑顺利把家具商城和活动抽奖系统跑起来。
返回列表