ARTICLE DETAIL

资讯详情

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

Django陶瓷电商开发实战:SKU设计、库存并发与支付回调

Django陶瓷电商开发实战:SKU设计、库存并发与支付回调 做陶瓷电商这个项目前前后后折腾了大概两个月。选型的时候其实纠结过不少东西最后落到Python和Django这套组合上核心原因是快——业务侧不需要在框架层反复造轮子Django的ORM、Admin后台和自带认证体系能把从零到一的时间压缩得很短。而且陶瓷商品有一个特点规格多釉色、口径、工艺、窑口、图片量大、SKU组合复杂用Django的模型管理起来比手写SQL和模板渲染要舒服得多。这篇文章会把这套陶瓷销售商城的设计思路、表结构、核心模块实现和一些真实踩坑的点完整过一遍给正在做同类项目或者准备入坑Django电商开发的人一个可复用的参考。1. 拆解一个陶瓷商城的核心需求1.1 商品模型和普通电商有什么本质区别陶瓷不是标准品。一件青花瓷盖碗可能有“釉下彩”和“釉上彩”的区别同一款茶壶会出“柴窑”和“气窑”两个版本口径还分8.5cm和10cm。这意味着商品表不能像卖书那样只存一个标题和价格就完事必须支持规格维度的自由扩展。在设计需求阶段我把商品属性拆成了两层公共属性和SKU属性。公共属性存在商品主表里比如标题、详情描述、主图、所属分类、上下架状态SKU属性则单独建表存具体的规格项和价格库存。分类也分成两级一级是餐具、茶具、摆件、花器二级就是具体品类比如茶具下面分盖碗、紫砂壶、主人杯。这个拆法和京东淘宝那套SPU/SKU模型是对齐的。SPU就是“商品”SKU就是“具体某个规格的售卖单元”。举个例子一款“手作青花主人杯”是SPU而“口径7.5cm、釉色青花、单价168元、库存15件”就是其中一个SKU。前端用户看到的商品详情页展示的是SPU层信息用户选择规格后实际加入购物车的是SKU层数据。1.2 从用户下单到支付回调的完整闭环除了商品展示商城最核心的主线是交易。整套流程大概是用户浏览商品 → 查看详情 → 选择规格加入购物车 → 购物车结算 → 生成订单 → 在线支付 → 支付回调更新订单状态 → 后台发货。看似简单但每个环节都有一堆边界情况要处理。购物车我采用了Session和数据库双层方案。未登录用户把购物车数据存Session登录后合并到数据库的购物车表中这样既不丢失临时数据也能跨设备同步。订单表状态我设计了五个值待支付、已支付、已发货、已完成、已取消。退款设计了单独的流水记录方便财务对账。支付这块直接对接的第三方支付平台的H5支付接口交付时用的是沙箱环境测试。整个支付流程里最容易出问题的不是调起支付而是回调通知的验签和数据一致性处理——后面实操部分会把这个坑单独拎出来讲。2. 技术选型Django解决了我哪些麻烦2.1 Django的ORM让表关系管理省了一大半心做这个项目之前我其实想过用Flask。但对比下来Flask的灵活是需要用额外工作量换的你没有现成的ORM、没有Admin、没有认证模块都得自己搭。而Django把这些都内置了对电商这种强数据模型、强后台管理需求的场景来说非常合适。Django ORM最方便的地方在于模型关联查询。比如我需要查“某个用户的所有订单里包含的所有商品SKU信息”SQL写起来要join三张表但用Django的ORM只需要在Order模型的user外键和OrderItem模型的sku外键上做筛选框架自动帮你生成优化过的SQL。开发效率提升非常明显。再就是Django Admin后台。这个项目的进销存管理、订单管理、用户管理我都是先靠Admin撑起来的。你只需要把模型注册进admin.py就能得到一个可用的后台管理系统不需要写一行前端代码。后续如果要定制界面重写对应的ModelAdmin类就行。对于一个面向中小商家的商城项目这个能力起点很高。2.2 MVT架构在业务上的贴合度MVTModel-View-Template和传统的MVC比较像区别在于Django把Controller部分逻辑放进了框架本身开发者写的是View视图函数或类视图。这对商城这种“请求-渲染-响应”模式非常契合。说几个具体场景。商品列表页需要返回当前分类下的所有上架商品还要处理分页排序我用ListView类视图加paginate_by参数就解决了。商品详情页需要同时取SPU信息、SKU列表、详情轮播图、关联推荐我用get_object和get_context_data重写来组合数据。用户注册登录、密码重置直接用Django内置的auth模块加一层短信验证码绑定就够用了。模板语言也很好用。页面上的循环、条件判断、过滤器比如商品价格格式化、时间显示美化可以在模板引擎里处理不用在视图里做多余的数据加工。这种工作在代码里做会显得很啰嗦放到模板层反而简洁。3. 数据库设计与模型实现3.1 九张核心表的字段设计和关联关系商城后台的整个数据模型我设计成了九个核心模型用户表、商品分类表、商品SPU表、商品SKU表、购物车表、订单表、订单明细表、支付流水表、收货地址表。下面直接用代码展示几个关键模型。用户模型我直接继承了Django内置的AbstractUser扩展手机号字段from django.contrib.auth.models import AbstractUser class User(AbstractUser): mobile models.CharField(max_length11, uniqueTrue, verbose_name手机号) avatar models.ImageField(upload_toavatar/%Y/%m, nullTrue, blankTrue) class Meta: db_table ys_user verbose_name 用户商品分类和SPU表的长这样class Category(models.Model): name models.CharField(max_length50) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE) sort_order models.IntegerField(default0) class Meta: db_table ys_category class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.PROTECT) title models.CharField(max_length200) subtitle models.CharField(max_length300, blankTrue) main_image models.ImageField(upload_toproduct/%Y/%m) detail_images models.JSONField(defaultlist, blankTrue) is_on_sale models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table ys_productSKU模型的特点是价格和库存都挂在SKU上而不是SPU上class Sku(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameskus) spec_values models.JSONField(verbose_name规格组合) # {口径: 8.5cm, 釉色: 青花} price models.DecimalField(max_digits10, decimal_places2) stock models.IntegerField(default0) sku_code models.CharField(max_length50, uniqueTrue) class Meta: db_table ys_sku3.2 订单和购物车的并发安全设计订单表的设计要考虑并发问题。我选择的是下单时直接锁定库存即创建订单明细时校验并且扣减对应SKU的库存扣减成功才算下单成功。这个操作用Django的select_for_update()加行级锁实现避免超卖。购物车表单设计时需要注意一个点用户重复添加同一SKU时应当做数量累加而不是新增记录。所以购物车模型里我给user和sku加了unique_together约束添加商品时用get_or_create逻辑处理。订单模型的代码class Order(models.Model): STATUS_CHOICES ( (0, 待支付), (1, 已支付), (2, 已发货), (3, 已完成), (4, 已取消), ) order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE) total_amount models.DecimalField(max_digits10, decimal_places2) status models.SmallIntegerField(choicesSTATUS_CHOICES, default0) address models.ForeignKey(Address, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) paid_at models.DateTimeField(nullTrue, blankTrue) class Meta: db_table ys_order订单明细表需要和SKU做快照。这里有个经验绝对不能只存一个外键引用到SKU因为日后SKU价格或标题一旦改动历史订单的展示就会对不上。正确做法是明细表里冗余存一份商品名称、规格描述、成交单价快照。4. 核心功能模块实现与实操要点4.1 商品列表和详情页的查询优化商品列表页最怕的就是N1查询。Django ORM默认懒加载模板里每次循环访问外键都会产生一条查询语句。比如在分类页循环展示20个商品每个商品都要查一次SKU价格数据库就会执行21条SQL用户体验会非常差。解决办法是使用select_related和prefetch_related。select_related适用于一对一和多对一外键关系用SQL的JOIN一次性取出来prefetch_related适用于多对多和反向关联框架会分两次查询再在内存中组装。我在列表页的queryset里这样写的products Product.objects.filter( categorycat, is_on_saleTrue ).prefetch_related(skus).order_by(-created_at)详情页还有一个小技巧SKU的规格选择联动比如用户先选“釉色”下拉菜单的“口径”选项就要根据当前釉色过滤。这个功能可以纯前端做后端只需要一次性返回该SPU的全部SKU及其规格值前端用JSON数据渲染联动逻辑。我用的Django的JsonResponse接口配合Vue的简单语法就实现了不需要额外引入重型前端框架。4.2 购物车和订单的Session同步购物车我用了user_cart表存储登录态数据同时保留了一个session_cart的临时存储。在用户登录的时候执行一次合并def merge_cart(request): session_cart request.session.get(cart, {}) if not session_cart: return user_cart, _ Cart.objects.get_or_create(userrequest.user) for sku_id, count in session_cart.items(): obj, created CartItem.objects.get_or_create( cartuser_cart, sku_idsku_id ) if not created: obj.quantity count obj.save() request.session[cart] {}每次向购物车添加商品时还要做库存校验。库存不足时直接弹窗提示而不是让用户进结算流程再报错这样体验好很多。下单逻辑的完整流程是这样的从购物车勾选商品生成订单快照 → 事务内锁定库存扣减 → 生成支付流水 → 返回支付参数给前端 → 用户确认支付后跳转第三方收银台。事务用Django的transaction.atomic()包裹里面任何一步出错都会全部回滚避免出现扣了库存但订单没生成的情况。4.3 支付回调处理的幂等性支付回调是最容易出线上事故的环节。原因在于第三方支付平台会多次发送回调通知如果回调处理函数不处理幂等就可能把订单状态重复更新甚至重复发货。我的处理方式是先幂等判断。收到回调后先根据订单号查询订单如果订单状态已经是“已支付”直接返回成功响应不再重复执行业务逻辑。然后在事务里更新支付流水号、订单状态、支付时间这里再放一次订单状态的条件更新即UPDATE语句带WHERE status0条件保证只有一个请求能成功修改状态。数据库层面的条件更新是最可靠的幂等保障。回调验签也有讲究我用的支付SDK自带的verify方法同时对金额做了二次校验——回调返回的支付金额必须等于订单总金额防止伪造回调或者金额篡改。做这一步的意义在于即使SDK的验签逻辑被绕过金额不一致的单子也过不了这一关。5. 常见问题与排查技巧实录5.1 Django后台中ImageField图片显示不出来一开始开发环境用Django的开发服务器跑图片显示没问题但部署到生产环境后商品图片全裂了。这个问题本质上是开发环境和生产环境静态文件处理方式不同开发环境由Django直接处理media文件生产环境需要nginx配合。我的解决办法是在settings.py里配置了MEDIA_URL和MEDIA_ROOT部署时在nginx中加一条location /media/的配置指向服务器的媒体目录。Admin后台如果图片还显示不出多半是admin的静态文件没有collectstatic执行一下python manage.py collectstatic就好。5.2 ORM查询性能慢排查出是Prefetch缓存问题有段时间商品列表接口平均耗时到了700ms以上排查后发现问题出在一个很隐蔽的地方。我在列表页用prefetch_related取了SKU但在视图里对sku_list做了二次filter操作。Django的prefetch_related缓存只在第一次全部取值时有效后续对缓存结果做filter会穿透到数据库产生新查询。正确做法是如果需要在取回SKU时做过滤应该使用Prefetch对象提前指定过滤条件from django.db.models import Prefetch products Product.objects.prefetch_related( Prefetch(skus, querysetSku.objects.filter(is_activeTrue)) )不然一次页面渲染可能造成几十次或上百次的数据库往返时间全花在这些隐式查询上。5.3 用户并发下单导致的库存超卖这个问题是在压测阶段暴露的。用并发工具模拟100个用户同时抢购一件库存只有10件的商品结果生成了15个成功订单。原因就是我早期下单逻辑只判断了“库存大于购买数量”但没有做数据库层面锁导致两个请求同时通过校验同时扣库存。后来在下单逻辑里加了select_for_update()with transaction.atomic(): sku Sku.objects.select_for_update().get(idsku_id) if sku.stock buy_count: raise ValidationError(库存不足) sku.stock - buy_count sku.save()注意select_for_update必须要放在事务里否则锁不会生效。这个语法在MySQL和PostgreSQL下都能正常工作是电商场景下防止超卖的标准解法。但也要注意行锁会阻塞其他事务对该SKU的下单操作所以锁的粒度要尽可能小——我这里只锁了SKU行没有锁订单表并发性能实测可控。5.4 XSS和CSRF这两个安全问题做商城这种交互型站点安全问题需要格外留意。Django默认开启了CSRF中间件模板里的表单需要加{% csrf_token %}不要图省事关掉这个中间件。用户提交的内容比如收货人姓名、商品评论在展示时要经过模板的escape过滤器或者用Django的autoescape机制防止存储型XSS注入。另外用户上传的图片文件名要做重命名处理。我用了uuid加时间戳生成新的文件名避免用户上传的文件名包含特殊字符造成服务器安全问题同时也能防重名覆盖。6. 项目上线前的检查和优化清单6.1 环境部署时的关键配置项Django项目在本地开发和线上部署配置上差别不小。上线前一定要把这几个值检查一遍DEBUG False ALLOWED_HOSTS [www.yoursite.com, yourdomain.com]DEBUGFalse后如果页面出现500错误就是进入生产模式了。此时要配置好日志系统用Python的logging模块把异常堆栈输出到文件中。我用的是RotatingFileHandler按大小切割日志文件避免单文件无限膨胀。数据库连接最好加上数据库连接池Django原生连接不受连接池管理高并发场景下会频繁创建和释放连接。我上了django-db-connection-pool这个方案效果比较明显。缓存方面配置了Redis作为Session和缓存后端商品详情页做了Redis缓存缓存过期时间5分钟热点商品页面响应速度基本稳定在100ms以内。6.2 支付配置的沙箱切换开发时用的支付沙箱配置上线前要切换成正式环境的AppID和商户私钥。最容易漏的是回调地址配置。沙箱环境回调地址可以写本机内网穿透地址调试线上环境要改成HTTPS的正式路径并且在支付平台后台配置好授权回调域名。切换完成后一定要走一遍全流程从商品详情 → 去结算 → 下单成功 → 收银台支付 → 同步跳转回来 → 异步回调更新订单状态。拿一笔一块钱的真实支付做测试验证整条链路。我的经验是异步回调往往比同步跳转慢几秒前端页面不要因为同步跳转成功就直接显示已支付要等订单状态的异步刷新。我在前端做了一个轮询接口每2秒查一次订单状态直到返回已支付才更新页面。6.3 后台管理功能的一站式整合项目交付时后台管理我除了Django Admin还做了几个自定义的管理页面。比如订单列表页除了能看基础字段还加了筛选条件按支付状态、按时间范围、按商品关键词。发货操作在订单详情页里直接用表单提交物流单号后台保存后自动更新订单状态到已发货并且触发一个站内信通知用户。这些功能用Django Admin的list_filter、search_fields、actions很容易就能实现。所有后台操作都有操作日志记录用Django的信号量或者简单的中间件把创建人、操作时间、操作内容写入日志表。对商家来说这个记录对账和售后很有帮助。7. 这个项目还能继续扩展的方向做完核心的商城交易闭环之后我梳理了一下这个项目后续还可以往上加的功能。第一个是优惠券系统模型上可以加一张优惠券表字段包括优惠券类型满减券/折扣券、适用范围全场/指定分类/指定商品、有效期、每人限领数量。领券中心的核销逻辑也简单就是在订单结算流程里加一步校验用户选择优惠券计算优惠金额校验是否满足使用门槛一单只能用一张。第二个是商品评价体系。现在做完交易就断掉了缺少买家反馈的闭环。陶瓷这种非标品买家对釉色、手感、瑕疵情况的评价对后来的用户非常有参考价值。商品评价表可以关联订单明细确保只有真实购买过的用户才能评价这比开放评价要可靠得多。第三个是数据统计看板。包括商品浏览量排行、转化率、订单量趋势、用户活跃情况。这部分可以用Django的ORM聚合函数直接从现有数据表里统计再配合Chart.js在后台页面上画折线图和柱状图。不需要引入额外的大数据组件数据量级在中小商家的范围内这几张表完全够用。第四个是移动端适配。现在的页面是响应式的但体验只能说够用。如果后续要上小程序可以直接复用现有的后端API新增一个适合小程序的轻量接口层。Django的DRFDjango REST Framework框架就是干这个的小程序的用户登录可以对接微信的code2session接口然后用Django的token认证保持登录态前后端分离后这套架构能撑住更多终端场景。这个项目从搭建骨架到补齐功能到部署上线我最大的感受是Django真正适合这种“业务逻辑清晰、数据模型复杂、需要快速迭代”的Web项目。它帮你把那些重复的轮子全部搞定让你把时间和精力花在真正的业务难点上比如库存并发、支付安全、SKU联动。如果你们也在做一个类似的垂直品类电商项目完全可以直接拿来这套表结构和业务流程作为参考表结构设计得足够通用换一个品类也照样适用。
返回列表