ARTICLE DETAIL

资讯详情

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

Python+Vue前后端分离实战:商城、商家与优惠券系统架构解析

Python+Vue前后端分离实战:商城、商家与优惠券系统架构解析 前后端分离项目做多了你会发现真正难的不是写业务代码而是一开始就把架构想明白。我最近刚把万事屋智能服务平台从原型到落地完整走了一遍趁热打铁把这套基于Python Vue的商城、商家、优惠券三合一系统拆开揉碎讲一讲。不管你是刚入门的Python学习者还是已经能用Django写CRUD但没做过完整商业项目的开发者这篇文章都值得你花十分钟读完。先交代一下背景。这个平台叫万事屋思路其实很简单——把同城生活服务类商家保洁、维修、跑腿、宠物照看等等集中到一个平台上用户可以逛商城下单商家入驻后管理自己的店铺和订单平台通过优惠券来做拉新和促活。我当时的开发环境是Windows PyCharm Professional后端主力用的是Django 4.x Django REST Framework前端用Vue 3 Vite Element Plus另外还有一小块服务用Flask写的。整个项目从设计到上线前前后后大概两个月中间踩了不少坑也沉淀了一些值得记录的经验。这个项目最大的价值不在于功能多复杂而在于它把电商平台这条链路上的几个典型模块完整走了一遍。你把它看懂商城类项目怎么做这个领域基本就有了一个可以迁移的整体认知。1. 项目概述与整体设计思路1.1 万事屋到底是一个什么平台先把这个名字拆开。万事屋这三个字参考的是那种什么都接的服务站概念。放到这个项目里它的定位是同城生活服务撮合平台。用户端能做什么浏览商城里的服务商品比如上门保洁一次、空调清洗一台、维修服务包下单购买在个人中心查看订单状态用优惠券抵扣金额。商家端能做什么申请入驻、提交资质、平台审核通过后商家可以上架商品、管理库存、处理订单、查看营收数据。平台端能做什么审核商家入驻申请、管理全平台的商品上下架、创建优惠券活动、查看整体交易流水。这个三层角色结构决定了系统从一开始就不能只是一堆CRUD堆在一起。商家、用户、平台管理员三个角色的权限边界要清晰数据隔离要严谨优惠券的核销逻辑要经得起并发考验。你可以把它想象成一个迷你版的美团加迷你版的有赞虽然没有大厂那么庞大的体量但该有的骨架一个不少。目标用户画像很清楚我是按中小型本地生活服务商来设计的。这些商家通常没有自建小程序或App的能力需要一个现成的平台来展示和售卖服务。而用户端则需要一个一个平台搞定多种家庭服务需求的入口。平台方赚的是交易佣金和营销费用优惠券就是一种典型的平台补贴工具。1.2 为什么选前后端分离架构坦白讲用Django的模板系统DTL直接渲染页面开发速度会更快尤其适合一个人全部搞定的项目。但我最终坚持用了前后端分离原因有三。第一这个项目的角色类型多页面形态差异大。用户端是商城列表、详情、结算这种偏C端交互的页面商家端是数据表格、表单、状态流转这种偏B端的后台页面管理端更是典型的dashboard格局。如果用服务端渲染这三套东西全挤在Django模板里工程会迅速腐烂。前后端分离之后前端只是调API后端只关心数据和规则边界非常干净。第二Vue的组件化组件化开发让复用成本大幅下降。比如商家端和用户端都会用到商品卡片虽然展示逻辑不同但底层的图片懒加载、价格格式化、优惠券标签这些逻辑写成Vue组件之后可以灵活复用。第三后期如果要扩展到小程序或者AppAPI已经摆在那里了前端换一套客户端就行。后端基本不需要大改。这种扩展性是模板渲染架构给不了的。当然前后端分离也引入了额外的问题比如跨域跨域资源共享、Token鉴权、接口文档维护。这些坑我在第6章会专门展开说。你只需要知道选择分离架构不是因为它流行而是因为这个项目本身的多端属性和角色复杂度值得付出这些额外的成本。2. 技术选型Django、Flask还有PyCharm环境2.1 主力后端为什么是Django标题里Django和Flask都出现了很多读者会好奇一个项目为什么要同时用两个Python Web框架这里我想把选择逻辑讲清楚。主力后端我用了Django核心原因是这个项目里有一个非常关键的领域概念——优惠券。优惠券的创建、分发、核销、过期这几个环节之间有关联的状态流转而且涉及金额计算对数据一致性要求高。Django自带的ORM提供了完备的事务管理、字段约束和迁移机制我在写模型的时候可以少操很多心。更重要的是Django自带的Admin后台在开发阶段几乎是免费的福利我搭建平台管理端的管理界面时省了大量时间。另一个原因涉及到权限体系。商城系统里用户、商家、管理员三种角色的权限完全不同Django自带的认证系统和第三方库django-guardian对象级权限结合起来可以比较轻松地实现商家只能操作自己店铺的数据这种细粒度控制。如果用Flask这部分要从零开始造轮子开发周期至少多两周。2.2 Flask在这个项目里承担了什么角色那Flask是不是白用了也不是。我在项目里用Flask单独写了一个服务负责优惠券的定时核销任务和过期提醒通知。为什么要单独拆出来因为Django主服务的实例平时要扛商城的QPS每秒请求数我不希望一个后台定时批量任务去抢主服务的资源导致用户侧接口变慢。Flask服务麻雀虽小但配合APScheduler和Redis做了定时轮询把过期的优惠券扫出来、把即将过期的提醒消息推出去这个任务独立部署到另一台小机器上互不干扰。另外Flask非常适合做微服务或辅助型服务的起步框架——它足够轻启动快依赖少当辅助服务用体量刚好。你要记住一个原则技术选型不是哪个框架好的问题而是哪个框架在哪个位置上更合适的问题。Django是大包大揽的全家桶Flask是轻装上阵的小工具两者各有各的位置。2.3 Python版本与PyCharm开发环境配置开发环境这块我踩过最痛的坑就是Python版本混用。项目一开始我本地装的是Python 3.9后来迁移到服务器上发现是3.8有个第三方库直接编译不过去。所以如果你是新手请从第一天就固定Python版本。我在这个项目里用的是Python 3.10Django 4.2 LTS版本这两个版本到现在依然是很稳的组合。PyCharm这里我要多说几句。很多人用了多年PyCharm其实只用到了10%的功能。我在这个项目里的配置方案是这样的虚拟环境用Virtualenv不直接用PyCharm默认的Interpreter。因为默认的全局解释器容易造成不同项目的依赖污染你Django装了个旧版本另一项目立刻遭殃。Virtualenv之后每个项目有自己的依赖隔离出问题也好排查。在PyCharm里把Run Configuration设置成Django Server这样F5直接跑起来断点调试非常舒服。具体操作是Run - Edit Configurations - 左上角加号 - Django Server - 填好Host127.0.0.1、Port8000和Python解释器选你创建好的Virtualenv路径。配置好后你可以在代码行号旁边点一下设置断点跑到那一行就会停下来可以查看所有变量的值。强烈建议安装一个PyCharm插件叫Save Actions加上File Watchers可以让保存文件时自动执行ruff格式化和排序import。这个小细节能让代码整洁度提升一个档次。有读者问过PyCharm激活的问题我不赘述只是提醒一句PyCharm专业版对于开源项目和学生都有免费授权走正规渠道申请就行效率其实比你想象的快很多。3. 商城、商家、优惠券三大核心模块的实现逻辑3.1 商城模块商品模型与购物车设计商城模块是用户最直接接触的部分。第一个核心是商品模型体现服务商品的特征。与实物商品不同服务商品没有库存数量但有时段限制所以我在设计模型时增加了可预约日期和服务时长两个字段而不是传统的stock字段。商品模型的关键字段设计部分class ServiceProduct(models.Model): name models.CharField(商品名称, max_length128) shop models.ForeignKey(Shop, on_deletemodels.CASCADE, verbose_name所属店铺) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue) price models.DecimalField(原价, max_digits8, decimal_places2) vip_price models.DecimalField(优惠价, max_digits8, decimal_places2, default0) duration models.IntegerField(服务时长(分钟), default60) is_active models.BooleanField(是否上架, defaultTrue) created_at models.DateTimeField(auto_now_addTrue)这里有几个设计上要留意的地方价格一定要用DecimalField而不是FloatField。浮点数做金额计算会有精度问题比如0.1 0.2在浮点数里不等于0.3这在支付场景里是不可接受的。用DecimalField虽然操作上稍微麻烦一点但金额安全是第一位的。is_active字段是软删除/上下架逻辑的核心。你前端下架一个商品后端只是把is_active置为False而不是物理删除。这样即使用户把它加入了购物车商品下架了也不会出现订单里找不到商品的尴尬。购物车实现方面考虑到用户可能不登录就逛商城我先实现了本地购物车存入localStorage登录之后再从接口同步。本地购物车的数据格式是一个数组里面存商品ID、数量、规格ID后端每次在获取购物车详情时会一次性把商品的最新价格、上架状态拉出来返回前端。这样的好处是购物车页面永远展示实时价格不会出现结算时才发现价格已经变动的情况。3.2 商家模块入驻审核与店铺管理的权限隔离商家模块是整个平台管理侧的核心。最考验设计功底的是入驻审核流程。我把它设计成了一个简单状态机状态流转是待审核 - 审核通过同时创建店铺 / 审核驳回用户可以修改资料后重新提交。商家提交入驻申请时需要填写营业执照信息、法人身份信息、服务类别、店铺简介和logo。平台管理员在Django Admin或者管理端页面审核。审核通过时我不仅创建了店铺记录还会给商家绑定一个商家角色分组Group这个分组拥有对店铺下商品的增删改查权限——这就呼应了前面说的django-guardian对象级权限。为了防止商家看到别人的数据我在所有涉及Shop查询的地方都强制加了shop_id过滤。这里有个实际教训开发中后期某一次我写商品删除接口时忘了过滤shop_id结果出现了商家A能删除商家B商品的严重BUG。排查了半天才发现是queryset没用当前用户关联的店铺去过滤。从那以后我养成一个习惯凡是涉及商家操作商品的接口一律先从token里解析出商家ID再去查对应店铺最后在查询条件里带上shop_id。商家端的店铺管理中还有一个数据看板功能展示今日订单数、营收、商品点击量等指标。实现上我用的是Django ORM的聚合查询aggregate加Redis做缓存——因为看板的数据不需要实时刷新缓存个5分钟既保留数据近实时性又不会打垮数据库。3.3 优惠券模块创建、分发、核销的完整生命周期优惠券模块是平台营销的灵魂也是代码里踩坑最多的地方。我把劣惠券拆成两层第一个层面是模板CouponTemplate第二个层面是实例CouponInstance。模板定义规则——比如满100减20新客立减10实例是真正发放到用户手里的券。模板和实例分层的意义在于创建活动时一次定义模板然后批量生成几千张券实例这些实例绑定不同用户之后状态从未发放变成已领取。这种设计在数据统计上非常清晰——某个活动发出去多少、用了多少、过期多少都是对实例做group by效率高逻辑也干净。发券过程中有一个很经典的并发问题我在第5章会专门展开。先看核销这个环节。用户在下单结算时前端会把选择优惠券ID传给后端。后端接口需要做这样几件事校验优惠券是否属于当前用户是否处于已领取但未使用状态是否在有效期内订单金额是否满足使用门槛更新订单金额计算实付金额把优惠券状态改成已使用我一开始把这些逻辑全部写在一个视图函数里代码一团糟。后来重构时用了一个简单的方式——把校验抽成一个独立函数返回是否成功, 错误信息, 优惠券实例三元组视图函数里直接调用逻辑清晰测试也方便。def validate_coupon(user, coupon_id, order_amount): try: coupon CouponInstance.objects.select_for_update().get( idcoupon_id, useruser ) except CouponInstance.DoesNotExist: return None, 优惠券不存在 if coupon.status ! CouponStatus.UNUSED: return coupon, 优惠券已使用或已过期 if coupon.expire_time timezone.now(): return coupon, 优惠券已过期 if order_amount coupon.template.min_amount: return coupon, 订单金额未达到使用门槛 return coupon, None这里用到了select_for_update()它会对这条优惠券记录加上行级锁避免两个人同时使用同一张券。这个细节极其重要不加的话高并发下可能出现同一张券被使用两次最后订单金额少收了的BUG。现在你还觉得优惠券模块简单吗很多上线后的资损事故往往就出在这种看起来不需要锁的代码里。另外优惠券的发放方式我也做了几种新人注册自动发券、活动页面手动领取、用户分享之后赠送。不同发放方式的优惠券我通过模板的issue_type字段区分这样后续统计不同渠道的转化率也有数据支撑。4. Vue前端搭建与后端API接口对接实录4.1 Vue项目初始化和环境配置前端的第一个坑就是环境安装。Node.js版本不要用太新的我当时用的Node 16.20Vite 4对16是友好兼容的配上npm 8。安装Vue项目用官方脚手架npm create vuelatest新版脚手架会询问路由、Pinia、ESLint等特性按需选择。注意这里有个很多人忽略的细节Vite默认开发服务器端口是5173而Django跑在8000端口跨域问题从这一刻就注定了。解决办法在后端配跨域我用的插件是django-cors-headers只需要在settings.py里加上CORS_ALLOWED_ORIGINS配置把http://localhost:5173加进去前端npm run dev后端manage.py runserver两边数据才能通。但你要是以为配完跨域就万事大吉那就太天真了。开发期这样没问题部署上线时前后端不在同一个域名下Cookie方案就不能用了Token必须放在请求头里。这个我在6.1节还会讲。4.2 axios请求封装与路由拦截Vue这边我对axios做了一个统一封装。所有的请求都走一个实例自动携带Authorization请求头加上统一的错误处理。这一层封装是所有Vue项目的标配但很多新手是每写一个组件就import axios from axios代码重复不说后期改请求地址或加鉴权逻辑时改到你怀疑人生。我封装的axios实例核心逻辑const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:8000/api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response response.data, error { if (error.response?.status 401) { router.push(/login); } return Promise.reject(error); } );路由拦截也很重要。商城的一些页面要求用户登录比如收银台商家后台的页面要求必须是商家角色才能访问。我用了Vue Router的beforeEach全局守卫每次路由跳转前检查meta里声明的requiresAuth和requiresRole配合Pinia里存用户信息做了一次干净的访问控制。这个守卫逻辑是整个前端权限的核心模板项目里基本可以直接抄走。4.3 前后端联调的那点事联调阶段我反复遇到的一个问题是接口返回结构不统一。后来我在后端DRF里统一了所有接口的返回格式成功时返回{ code: 0, data: 实际数据, message: ok }失败时返回{ code: 非0, data: null, message: 具体错误 }。这样前端封装的响应拦截器只需要判断code不用每个接口单独处理错误代码量直接砍半。还有一个容易被忽略的点时间格式。Django的DateTimeField序列化后默认返回ISO格式的字符串比如2024-06-01T12:30:00Z。如果前端直接用它会当成UTC时间处理跟北京时间相差8小时。我当时在Vue里写了一个统一的formatDateTime过滤器把ISO字符串转成YYYY-MM-DD HH:mm的本地时间格式。这个细节你要是等到上线了才发现用户投诉来得比你的修复还快。对了前端播放m3u8格式视频的需求比如商家上传服务案例视频也是项目中后期才加进来的。m3u8本质是一个索引文件浏览器默认不支持直接播放我在项目里引入了hls.js库十几行代码就能搞定比什么video标签直接放链接靠谱得多。核心代码import Hls from hls.js; if (Hls.isSupported() videoEl) { const hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(videoEl); }5. 数据库设计与高并发场景下的细节打磨5.1 核心数据表结构与关联关系前后端项目做到一定规模数据库设计的合理与否直接决定了后期的开发效率。这个项目我总共设计了多少张表大概二十几张吧。但核心的业务表就几张用户表、商家表、店铺表、商品表、订单表、订单详情表、优惠券模板表、优惠券实例表、审核记录表。几个关键的关联关系你要特别注意用户和商家的关系。一个用户不能既是普通用户又是商家其实在真实场景里是可以的——用户A可以是保洁服务的消费者同时自己也经营一家维修店。所以商户信息没有直接加到用户表上而是通过一个一对一关系的商家表Merchant来挂接。这样用户表保持简洁商家信息集中管理。订单和商家、商品的关系。订单表里冗余了一个shop_id字段。虽然通过订单详情可以反查到商品再反查到店铺但如果每次都要多表查询才能知道订单属于哪个商家性能上惨不忍睹。冗余字段在这个场景里是值得的。订单和订单详情是一对多关系。一个订单可以包含多个服务商品每个服务商品可能是不同商家的。这里有一个现实问题用户一次下单买了三个不同商家的服务订单怎么拆我的方案是一个订单统一归到第一个商品的商家名下同时为每个商品单独生成一个子订单用于商家端独立接单。这个方案的好处是用户端看到一个总订单、支付一次商家端各自处理各自的子订单。虽然一开始建表时多费了点心思但后期功能迭代起来非常顺。5.2 优惠券并发超发的经典解决方案优惠券的并发问题是很多做营销系统的开发者的噩梦。想象一个场景活动上线1000张券瞬间被秒杀同时有2000个请求打过来。如果代码是先查一下还有没有库存有就发那在极端情况下会有多个请求同时看到还有最后一张券然后全部发放成功——超发就此发生。解决思路并不复杂关键在一条SQL和一把锁。第一种方案是乐观锁。在数据表里加一个version字段更新时用UPDATE ... WHERE id? AND version旧版本如果更新影响行数为0说明版本对不上就重试或失败。第二种方案是数据库行锁也是我实际使用的方案。在发券时直接对优惠券模板记录加锁with transaction.atomic(): template CouponTemplate.objects.select_for_update().get(idtemplate_id) if template.remaining_count 0: return 优惠券已被抢完 template.remaining_count - 1 template.save() CouponInstance.objects.create(useruser, templatetemplate)select_for_update会把这个模板的记录锁住其他请求的同类操作只能排队等待有效避免了超发。但是这个方案有一个隐藏代价每秒能发放的优惠券数量受到了数据库行锁的限制在超大流量下吞吐量会不足。对于我这个体量的项目完全够用如果是大厂双十一级别的抢券那就要上Redis Lua脚本做原子扣减了那是另一个话题。5.3 商城订单支付状态与退款流程虽然这个项目的支付我接的是沙箱环境但订单状态的设计我还是花了不少时间。订单状态我用了整型枚举待支付1、已支付2、商家已接单3、服务完成4、已取消5、退款中6、已退款7。这个状态机的核心规则是不能任意跳转。比如待支付只能去已支付或已取消已支付只能去商家已接单或退款中服务完成只能去已退款。我在代码里把所有允许的状态转换写成了一个字典每次更新订单状态前先校验合法性。这样做的意义是防止程序bug或者恶意请求把订单状态搞乱。退款流程我设计成先冻结优惠券补偿再退钱。用户用优惠券下单后来退款优惠券要不要归还我的策略是如果订单在优惠券有效期内退款则优惠券归还如果退款时优惠券已过期则折现为平台余额返还用户。这块规则虽然简单但涉及的钱都是真金白银建议你开发这类系统时把退款规则写成配置方便运营随时调整。6. 项目实战中的常见问题与排查技巧6.1 跨域问题开发环境与生产环境的双重解决方案跨域问题是前后端分离项目里的头号拦路虎。开发阶段我用django-cors-headers解决配置如下INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ # ... corsheaders.middleware.CorsMiddleware, ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]注意CorsMiddleware最好放在CommonMiddleware之前否则某些情况下比如使用APPEND_SLASH重定向时跨域头会丢失。生产环境我建议换一种解法——Nginx反向代理让前端和后端同域或者用Nginx直接转发接口。简单说Nginx把所有/api/前缀的请求转发到Django进程其余请求去拿Vue打包后的静态文件。这样浏览器视角只有一个域名根本不存在跨域比改响应头更干净。Nginx关键配置server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /srv/www/万事屋前台/dist; try_files $uri $uri/ /index.html; } }try_files那一行很重要它是Vue Router的history模式能正常刷新而不404的钥匙。如果你用的是hash模式可以跳过但只要用了history模式这行配置是必须的。6.2 DRF序列化性能优化别让N1查询拖垮接口写DRF接口时新手最容易犯的错误就是N1查询。什么是N1最典型的场景是查询商品列表你用ServiceProduct.objects.all()拿到50个商品然后序列化时每个商品都需要查一次它的店铺名称——结果一次列表接口产生了51条SQL查询。数据量小的时候没感觉数据一涨接口立刻变慢。解决办法是select_related和prefetch_related。在这个例子里商品查询改成ServiceProduct.objects.select_related(shop).all()select_related会把外键关系也用JOIN一次性查出来50条商品一条SQL就能带出店铺信息。对于多对多关系或者反向外键用prefetch_related则在内存里做关联。我在做优化时把所有涉及列表页的接口都检查了一遍只改了两行代码接口响应时间就从700ms降到了90ms左右。对你没有看错就两行。所以性能优化不是玄学大部分时候就是看你的查询有没有写对。我还配合用了一个工具叫django-silk它会在页面下面记录每个请求执行的SQL数量和时间。排查慢接口时先打开它看SQL列表定位问题再动手不要凭感觉瞎优化。6.3 调试阶段的实用小技巧与经验总结最后分享几个实战过程中沉淀下来的小技巧。第一Django的开发环境debug日志一定要分级别配置。我在settings.py里把SQL日志打开logging模块配一个django.db.backends的handler这样每个接口执行的SQL语句全部打印在控制台。排查ORM问题时这个日志比任何debugger都直观。但生产环境一定要关掉否则大流量会打爆你的磁盘。第二Vue前端开发时Network面板的Disable cache选项记得勾上。否则改完代码刷新浏览器还给你旧的JS文件你会误以为是后端没改。这个小问题浪费了我一个下午。第三代码版本管理一定要用Git而且commit信息按功能写清楚。fix coupon bug这种一句话还行比update强太多。我习惯格式是feat(module): description比如feat(coupon): 新增满减优惠券活动类型。回滚的时候这种日志能帮你快速定位到具体改动。第四数据库迁移文件不要乱删。python manage.py makemigrations生成的迁移文件记录了表结构的演变史它是Django判断表结构是否同步的依据。删了之后在别人的电脑上运行migrate就可能会报错说某个迁移文件找不到。正确做法是所有迁移文件进Git、跟随项目走。第五在整个项目里我养成了接口层永远不写复杂业务逻辑的习惯。复杂逻辑放到Service层你可以在Django里建一个services.py模块视图函数只做参数校验、调用服务和序列化返回。作用是测试的时候我可以直接对Service层的函数做单元测试不用启动HTTP服务、不用造HTTP请求数据测试速度快一个量级。尤其是优惠券核销这种涉及金额的逻辑没有单元测试我是不敢上线改动的。我个人在这个项目里获得的体会是一个平台型项目技术难点往往不那么惊天动地真正的门槛在于边界感——商家和用户的边界、订单和优惠券的边界、开发期和生产环境的边界。把边界理清了代码自然不会烂到哪去。我这套代码里有很多地方现在看来还可以继续优化但作为一个从零到一跑通全流程的项目它的价值已经达到了我最初的预期。如果后续继续迭代我会考虑把商家端的营收统计做成更丰富的可视化报表再把优惠券的核销逻辑抽成一个独立的微服务。不过那都是下一步的事了先把当前这套结构吃透你在类似项目里就能少走很多弯路。
返回列表