摘要:商城的订单(Day10)和支付(Day11)都搞定了,今天上促销三板斧——优惠券、秒杀、拼团。优惠券搞定生成/领取/核销/过期的完整生命周期;秒杀用 Redis 预减库存加异步下单扛住高并发;拼团处理成团、失败、超时的状态流转。三套系统,三种不同的并发挑战,一篇讲透。代码全部可运行,配套文件day12_promotion.py。为什么电商一定要做促销你打开任何一个电商 App,首页最显眼的位置永远不是商品列表——而是各种促销入口。“限时秒杀”、“新人优惠券”、“3 人拼团享低价”……为什么?因为促销是电商增长的核心武器。没有促销的电商就像没有打折季的商场,用户进来逛逛就走了,不会掏钱。优惠券:降低决策门槛。用户本来犹豫要不要买,一看有张 20 块的券,“用掉算了”——成交。秒杀:制造紧迫感。“还剩 3 件”、“倒计时 02:15”——人类对"错过"的恐惧远大于对"得到"的渴望。拼团:社交裂变。一个人买 99 块,三个人拼团 69 块——你不去拉人,别人就替你拉了。每个用户都变成了你的免费推广员。但问题来了——这三个功能,每一个都是技术深坑。优惠券要处理时间窗口、叠加规则、防止超领;秒杀要扛住瞬时高并发、防止超卖、保证数据一致性;拼团要处理成团逻辑、超时退款、并发参团。任何一个没做好,都是线上事故。我在做商城的时候,促销模块返工了四次。不是功能做不出来,是边界情况太多——你以为处理完了,测试一跑又冒出个新 bug。今天,把这三把刀的核心逻辑和踩过的坑一次性讲清楚。原理速览优惠券:一张券的一生优惠券的生命周期看起来很简单:生成 → 领取 → 核销(使用)→ 归档 ↓ 过期(没用就自动过期了)听起来没什么难度对吧?但细节是魔鬼。领取:要防超领。券只发了 1000 张,结果领了 1001 张——财务月底对不上账,整个团队加班排查。核销:要校验的东西太多了——时间窗口对不对?使用门槛够不够?这张券是不是已经被用过了?订单金额扣掉优惠后是不是变成负数了?过期:用户领了不用怎么办?系统要自动标记过期,不然用户拿着过期券来下单,前端显示"已优惠",后端计算发现券过期了,金额对不上——又是一场事故。核心思路:每张券都是独立实例,有自己独立的状态流转。不是数据库里改个字段就完事的。秒杀:在洪峰中活下来秒杀的技术挑战和其他模块完全不在一个量级。平时你的系统可能每秒处理 100 个请求,数据库轻松应对。但秒杀开始的瞬间——比如早上 10:00 整——可能有 10000 个请求同时涌入,抢 10 件商品。如果用常规方式直接操作数据库扣库存:10000 个 UPDATE 同时打到同一行数据 → 数据库行锁争抢 → 请求排队等待 → 连接池耗尽 → 超时 → 整个系统被拖垮 → 不仅秒杀挂了,连正常购物也挂了这就是所谓的"秒杀效应"——一个功能拖垮整个系统。所以秒杀的设计原则只有一个:把压力挡在数据库之外。三层防线:层级手段作用第一层限流过滤掉重复请求和恶意脚本,减轻后端压力第二层Redis 预减库存用内存扛并发,单线程原子操作,QPS 轻松 10 万+第三层异步下单削峰填谷,保护数据库不被瞬时流量击穿拼团:一场限时社交实验拼团本质上是一个多人协作加时间约束的状态机:开团 → 成团中(等待更多人加入) ├── 人数达标 → 成团成功 → 发货 ├── 超时了 → 成团失败 → 退款 └── 人数满了但还不够 → 继续等,直到超时关键问题有三个:并发参团:两个人同时点"参团",但团只能再进 1 个人,谁进谁被拒?必须用锁。超时处理:怎么知道一个团什么时候超时?不能靠用户触发,必须有后台定时任务主动扫描。失败退款:团散了,已经付了钱的人怎么退?要关联 Day11 的支付退款接口。动手:优惠券先定义三种券类型:满减券、折扣券、无门槛券。每种券计算逻辑不一样,但状态流转一样。模型核心字段:classCoupon(models.Model):"""优惠券模型:定义券的规则和有效期"""COUPON_TYPE_CHOICES=[('fixed','满减券'),# 满 100 减 20('discount','折扣券'),# 满 200 打 85 折('no_threshold','无门槛券'),# 直接减 5 块]name=models.CharField(max_length=100)coupon_type=models.CharField(max_length=20,choices=COUPON_TYPE_CHOICES)threshold=models.DecimalField(max_digits=10,decimal_places=2,default=0)discount_value=models.DecimalField(max_digits=10,decimal_places=2)total_count=models.IntegerField()# 发行总量remaining_count=models.IntegerField()# 剩余可领数量valid_start=models.DateTimeField()# 有效期开始valid_end=models.DateTimeField()# 有效期结束用户领券,用乐观锁防超领——原理和 Day10 的库存扣减一模一样(核心逻辑):defclaim_coupon(user_id,coupon_id):"""用户领取优惠券——乐观锁防超领"""withtransaction.atomic():# 原子操作:remaining_count > 0 才减 1,领完则 affected = 0affected=Coupon.objects.filter(id=coupon_id,remaining_count__gt=0,).update(remaining_count=F('remaining_count')-1)ifaffected==0:raiseValidationError('优惠券已被领完')# 创建用户券实例——注意,这是独立的一张券returnUserCoupon.objects.create(user_id=user_id,coupon_id=coupon_id,status='unused')核销是优惠券系统最复杂的部分。一张券从"领了"到"用了",要过五关斩六将(核心框架):第 1 关:行锁锁定(select_for_update)—— 防止同一张券被并发用两次 第 2 关:状态校验 —— 只有 unused 才能用 第 3 关:时间窗口 —— 在有效期内吗?过期了顺手标记 expired 防脏数据 第 4 关:计算实际优惠 —— 满足使用门槛吗?discount <= 0 则拒绝 第 5 关:标记 used + 绑定订单 —— 返回 discount优惠金额的计算逻辑,三种券各有算法:defcalculate_discount(self,order_amount):ifself.coupon_type=='fixed':# 满减券:够门槛才减returnself.discount_valueiforder_amount>=self.thresholdelse0elifself.coupon_type=='discount':# 折扣券:够门槛打折iforder_amount>=self.threshold:returnround(order_amount*(1-self.discount_value),2)return0else:# 无门槛券:直接减,但不超过订单金额returnmin(self.discount_value,order_amount)别忘了过期处理。用户领了 100 张券,99 张没用,不能一直挂着"未使用"。写个定时任务每天扫一遍:defexpire_coupons_batch():"""批量过期(Celery Beat 定时任务,建议每天凌晨跑一次)"""now=timezone.now()returnUserCoupon.objects.filter(status='unused',coupon__valid_end__lt=now).update(status='expired')优惠券系统的核心教训:每张券都是独立实例,有自己的生命周期。不要想着用一张"模板券"加个数量字段就完事——核销、退款、对账都会出问题。领出去 1000 张,就得有 1000 条独立记录。动手:秒杀秒杀是整个电商系统里并发压力最大的场景。设计思路四步。第一步:库存预热活动开始前 5-10 分钟,把库存从数据库加载到 Redis。为什么?因为秒杀开始时不能有数据库查询——每一毫秒都珍贵。核心就一句:SET flash_sale:{id}:stock <剩余库存>,带过期时间。第二步:限流——挡住恶意请求总有人写脚本刷接口。同一用户 60 秒内最多 3 次尝试:rate_limit_key=f'flash_sale:{flash_sale_id}:user:{user_id}:count'current_count=self.redis.get(rate_limit_key)ifcurrent_countandint(current_count)>=3:return{'success':False,'message':'操作过于频繁,请稍后再试'}# 用 pipeline 保证原子性:计数 + 设置过期时间pipe=self.redis.pipeline()pipe.incr(rate_limit_key)pipe.expire(rate_limit_key,60)pipe.execute()第三步:Redis 原子预减库存这是秒杀最核心的操作。用DECR命令——它是原子的,单线程执行,天然不会并发冲突:stock_key=f'flash_sale:{flash_sale_id}:stock'remaining=self.redis.decr(stock_key)# 原子扣减,返回扣减后的值ifremaining<0:self.redis.incr(stock_key)# 库存不够,把多扣的 1 个加回去return{'success':False,'message':'已售罄'}为什么用DECR而不是"先读再减"?因为 Redis 虽然是单线程,但如果你分成两步——先GET读到库存是 1,然后准备SET为 0——在这两步之间,另一个请求也GET到了 1,也准备扣减。结果两个请求都认为自己买到了最后一件。这就是经典的竞态条件。而DECR是一条命令,Redis 保证原子性。10000 个DECR排着队执行,每个都安全。第四步:异步下单预减库存成功不代表订单创建成功。数据库操作太慢,不能同步执行。把任务丢进异步队列,让后台 Worker 慢慢处理:预减成功 → 创建排队记录(status='queued') → 放入异步队列(实际项目用 Celery.send_task) → 立即返回前端"抢购成功,正在排队处理"后台 Worker 处理队列时,数据库层面要再次校验库存(秒杀场景必须用锁),Redis 和 DB 之间可能不一致——比如 Redis 重启了、网络抖动了、Worker 崩了。Redis 预减成功了,数据库还得再验一次。秒杀的核心教训:不要信任任何单一环节。Redis 预减 + 数据库二次校验,双层才能万无一失。用户那边看到的是"排队处理中",最终结果是"成功"或"失败",体验流畅。动手:拼团拼团的难点不在技术复杂度,在于边界情况特别多。你每加一个功能,就多三种异常场景。先定义拼团实例的状态机:forming(成团中) → success(成团成功)→ 发货 forming(成团中) → failed(超时/失败)→ 退款开团:团长发起一个新团,自动成为第一个成员。校验活动在有效期内,事务里创建GroupBuyInstance(status=‘forming’,current_members=1)+ 创建团长成员记录。参团——最容易出 bug 的地方。核心框架:defjoin_group(self,user_id,group_id):withtransaction.atomic():# 行锁锁定,防止两个人同时参团导致人数计算错误group=GroupBuyInstance.objects.select_for_update().get(id=group_id)# 校验 1:团的状态(只有 forming 才能加人)# 校验 2:是否超时(超时则标记 failed,自动关闭)# 校验 3:人数是否已满# 校验 4:是否重复参团# 加入 + 人数加 1(用 F 表达式)GroupBuyMember.objects.create(group=group,user_id=user_id)group.current_members=F('current_members')+1group.save()# 关键!refresh_from_db 获取数据库真实值group.refresh_from_db()ifgroup.current_members>=group.activity.min_members:group.status='success'group.save()这里有个大坑——refresh_from_db()那行。F('current_members') + 1在 Python 层面并没有改变current_members的值,它只是生成一条 SQL 表达式current_members = current_members + 1。所以save()之后,Python 对象里的值还是旧的。如果你不做refresh_from_db(),直接拿group.current_members判断是否成团——3 个人同时参团,3 个人读到的都是旧值,3 个人都觉得"差一个人就成团了",结果团的实际人数已经够了,但没人触发成团逻辑。超时处理——定时任务扫描:defcheck_and_close_expired_groups(self):"""定时任务:扫描超时未成团的拼团(建议每 5 分钟执行一次)"""now=timezone.now()expired_groups=GroupBuyInstance.objects.filter(status='forming',expire_at__lt=now)forgroupinexpired_groups:withtransaction.atomic():# 双重校验:防止查询和执行之间被其他进程处理了fresh=GroupBuyInstance.objects.select_for_update().get(id=group.id)iffresh.status=='forming':fresh.status='failed'fresh.save()# TODO: 关联 Day11 的退款接口,给所有参团成员退款拼团的核心教训:永远不要信任内存中的状态。在分布式环境下,你内存里读到的值,可能 0.1 秒前就已经被别的请求改了。select_for_update()加锁、refresh_from_db()刷新——这两个操作在并发场景下就是生命线。用 AI 设计促销规则的状态转换三个系统里,拼团的状态转换最复杂。成团中 → 成功,触发发货;成团中 → 失败,触发退款。这些是基础流程。但现实的边界情况远不止这些:团长在成团中退出,怎么处理?整个团解散还是换团长?团员在成团中退出,人数不够了,团还能继续吗?成团成功后有人申请退款,会不会影响其他团员?活动结束了但团还没满,是等它自然超时还是立刻关闭?这些规则从零设计,光画状态转换图就得一下午,还不一定能想全。我实际用 AI 试了一下,prompt 是这样的:你是一个电商系统架构师。请为拼团系统设计完整的状态转换规则。 基本状态:forming(成团中)、success(成团成功)、failed(成团失败) 需要考虑的边界情况: 1. 团长在成团中退出,怎么处理? 2. 团员在成团中退出,怎么处理? 3. 成团成功后有人申请退款,怎么处理? 4. 活动结束但团还没满,怎么处理? 5. 系统并发导致的人数异常,怎么处理? 输出:完整的状态转换图 + 每个转换的触发条件和副作用 + 异常降级策略AI 给出了一个很完整的方案,其中有一个我完全没想到的设计:成团成功后的部分退款——增加success_partial_refund子状态,团还是成功的,其他团员正常发货,只有退款那个人走退款流程。这个思路确实很好。但我最终还是没用——因为商城的业务规则是"成团后不支持单个退款,只能整团退"。这就是 AI 辅助的边界:AI 能帮你穷举可能性,但最终的业务决策必须你来做。AI 加速心得:状态转换设计特别适合让 AI 穷举边界情况。人类思维天然容易遗漏"如果 A 和 B 同时发生怎么办"这种组合场景,但 AI 不会。把 AI 当成"边界情况检查器"——用它查漏补缺,而不是照搬方案。它给 10 个方案,你选 3 个最合适的,效率就很高了。学完来练练手优惠券叠加:支持多张券叠加使用,但有优先级(先算满减再算折扣)。注意:叠加后总金额不能为负数。秒杀预热倒计时:给秒杀接口加"预热期"——活动开始前 5 分钟,用户访问返回倒计时信息,同时自动预热 Redis 库存。拼团模拟退团:成团中允许团员退出,退出后current_members - 1。如果团长退出,把团长身份转移给最早参团的团员。秒杀实时监控:用 Redis 的GET读取剩余库存,写个接口返回"已售 X 件,剩余 Y 件",前端做成进度条。配套文件day12_promotion.py(优惠券 + 秒杀 + 拼团完整实现)可参考学习。本系列为梅雅达编程笔记原创,首发于 CSDN。有问题?评论区见
摘要:商城的订单(Day10)和支付(Day11)都搞定了,今天上促销三板斧——优惠券、秒杀、拼团。优惠券搞定生成/领取/核销/过期的完整生命周期;秒杀用 Redis 预减库存加异步下单扛住高并发;拼团处理成团、失败、超时的状态流转。三套系统,三种不同的并发挑战,一篇讲透。代码全部可运行,配套文件day12_promotion.py。为什么电商一定要做促销你打开任何一个电商 App,首页最显眼的位置永远不是商品列表——而是各种促销入口。“限时秒杀”、“新人优惠券”、“3 人拼团享低价”……为什么?因为促销是电商增长的核心武器。没有促销的电商就像没有打折季的商场,用户进来逛逛就走了,不会掏钱。优惠券:降低决策门槛。用户本来犹豫要不要买,一看有张 20 块的券,“用掉算了”——成交。秒杀:制造紧迫感。“还剩 3 件”、“倒计时 02:15”——人类对"错过"的恐惧远大于对"得到"的渴望。拼团:社交裂变。一个人买 99 块,三个人拼团 69 块——你不去拉人,别人就替你拉了。每个用户都变成了你的免费推广员。但问题来了——这三个功能,每一个都是技术深坑。优惠券要处理时间窗口、叠加规则、防止超领;秒杀要扛住瞬时高并发、防止超卖、保证数据一致性;拼团要处理成团逻辑、超时退款、并发参团。任何一个没做好,都是线上事故。我在做商城的时候,促销模块返工了四次。不是功能做不出来,是边界情况太多——你以为处理完了,测试一跑又冒出个新 bug。今天,把这三把刀的核心逻辑和踩过的坑一次性讲清楚。原理速览优惠券:一张券的一生优惠券的生命周期看起来很简单:生成 → 领取 → 核销(使用)→ 归档 ↓ 过期(没用就自动过期了)听起来没什么难度对吧?但细节是魔鬼。领取:要防超领。券只发了 1000 张,结果领了 1001 张——财务月底对不上账,整个团队加班排查。核销:要校验的东西太多了——时间窗口对不对?使用门槛够不够?这张券是不是已经被用过了?订单金额扣掉优惠后是不是变成负数了?过期:用户领了不用怎么办?系统要自动标记过期,不然用户拿着过期券来下单,前端显示"已优惠",后端计算发现券过期了,金额对不上——又是一场事故。核心思路:每张券都是独立实例,有自己独立的状态流转。不是数据库里改个字段就完事的。秒杀:在洪峰中活下来秒杀的技术挑战和其他模块完全不在一个量级。平时你的系统可能每秒处理 100 个请求,数据库轻松应对。但秒杀开始的瞬间——比如早上 10:00 整——可能有 10000 个请求同时涌入,抢 10 件商品。如果用常规方式直接操作数据库扣库存:10000 个 UPDATE 同时打到同一行数据 → 数据库行锁争抢 → 请求排队等待 → 连接池耗尽 → 超时 → 整个系统被拖垮 → 不仅秒杀挂了,连正常购物也挂了这就是所谓的"秒杀效应"——一个功能拖垮整个系统。所以秒杀的设计原则只有一个:把压力挡在数据库之外。三层防线:层级手段作用第一层限流过滤掉重复请求和恶意脚本,减轻后端压力第二层Redis 预减库存用内存扛并发,单线程原子操作,QPS 轻松 10 万+第三层异步下单削峰填谷,保护数据库不被瞬时流量击穿拼团:一场限时社交实验拼团本质上是一个多人协作加时间约束的状态机:开团 → 成团中(等待更多人加入) ├── 人数达标 → 成团成功 → 发货 ├── 超时了 → 成团失败 → 退款 └── 人数满了但还不够 → 继续等,直到超时关键问题有三个:并发参团:两个人同时点"参团",但团只能再进 1 个人,谁进谁被拒?必须用锁。超时处理:怎么知道一个团什么时候超时?不能靠用户触发,必须有后台定时任务主动扫描。失败退款:团散了,已经付了钱的人怎么退?要关联 Day11 的支付退款接口。动手:优惠券先定义三种券类型:满减券、折扣券、无门槛券。每种券计算逻辑不一样,但状态流转一样。模型核心字段:classCoupon(models.Model):"""优惠券模型:定义券的规则和有效期"""COUPON_TYPE_CHOICES=[('fixed','满减券'),# 满 100 减 20('discount','折扣券'),# 满 200 打 85 折('no_threshold','无门槛券'),# 直接减 5 块]name=models.CharField(max_length=100)coupon_type=models.CharField(max_length=20,choices=COUPON_TYPE_CHOICES)threshold=models.DecimalField(max_digits=10,decimal_places=2,default=0)discount_value=models.DecimalField(max_digits=10,decimal_places=2)total_count=models.IntegerField()# 发行总量remaining_count=models.IntegerField()# 剩余可领数量valid_start=models.DateTimeField()# 有效期开始valid_end=models.DateTimeField()# 有效期结束用户领券,用乐观锁防超领——原理和 Day10 的库存扣减一模一样(核心逻辑):defclaim_coupon(user_id,coupon_id):"""用户领取优惠券——乐观锁防超领"""withtransaction.atomic():# 原子操作:remaining_count > 0 才减 1,领完则 affected = 0affected=Coupon.objects.filter(id=coupon_id,remaining_count__gt=0,).update(remaining_count=F('remaining_count')-1)ifaffected==0:raiseValidationError('优惠券已被领完')# 创建用户券实例——注意,这是独立的一张券returnUserCoupon.objects.create(user_id=user_id,coupon_id=coupon_id,status='unused')核销是优惠券系统最复杂的部分。一张券从"领了"到"用了",要过五关斩六将(核心框架):第 1 关:行锁锁定(select_for_update)—— 防止同一张券被并发用两次 第 2 关:状态校验 —— 只有 unused 才能用 第 3 关:时间窗口 —— 在有效期内吗?过期了顺手标记 expired 防脏数据 第 4 关:计算实际优惠 —— 满足使用门槛吗?discount <= 0 则拒绝 第 5 关:标记 used + 绑定订单 —— 返回 discount优惠金额的计算逻辑,三种券各有算法:defcalculate_discount(self,order_amount):ifself.coupon_type=='fixed':# 满减券:够门槛才减returnself.discount_valueiforder_amount>=self.thresholdelse0elifself.coupon_type=='discount':# 折扣券:够门槛打折iforder_amount>=self.threshold:returnround(order_amount*(1-self.discount_value),2)return0else:# 无门槛券:直接减,但不超过订单金额returnmin(self.discount_value,order_amount)别忘了过期处理。用户领了 100 张券,99 张没用,不能一直挂着"未使用"。写个定时任务每天扫一遍:defexpire_coupons_batch():"""批量过期(Celery Beat 定时任务,建议每天凌晨跑一次)"""now=timezone.now()returnUserCoupon.objects.filter(status='unused',coupon__valid_end__lt=now).update(status='expired')优惠券系统的核心教训:每张券都是独立实例,有自己的生命周期。不要想着用一张"模板券"加个数量字段就完事——核销、退款、对账都会出问题。领出去 1000 张,就得有 1000 条独立记录。动手:秒杀秒杀是整个电商系统里并发压力最大的场景。设计思路四步。第一步:库存预热活动开始前 5-10 分钟,把库存从数据库加载到 Redis。为什么?因为秒杀开始时不能有数据库查询——每一毫秒都珍贵。核心就一句:SET flash_sale:{id}:stock <剩余库存>,带过期时间。第二步:限流——挡住恶意请求总有人写脚本刷接口。同一用户 60 秒内最多 3 次尝试:rate_limit_key=f'flash_sale:{flash_sale_id}:user:{user_id}:count'current_count=self.redis.get(rate_limit_key)ifcurrent_countandint(current_count)>=3:return{'success':False,'message':'操作过于频繁,请稍后再试'}# 用 pipeline 保证原子性:计数 + 设置过期时间pipe=self.redis.pipeline()pipe.incr(rate_limit_key)pipe.expire(rate_limit_key,60)pipe.execute()第三步:Redis 原子预减库存这是秒杀最核心的操作。用DECR命令——它是原子的,单线程执行,天然不会并发冲突:stock_key=f'flash_sale:{flash_sale_id}:stock'remaining=self.redis.decr(stock_key)# 原子扣减,返回扣减后的值ifremaining<0:self.redis.incr(stock_key)# 库存不够,把多扣的 1 个加回去return{'success':False,'message':'已售罄'}为什么用DECR而不是"先读再减"?因为 Redis 虽然是单线程,但如果你分成两步——先GET读到库存是 1,然后准备SET为 0——在这两步之间,另一个请求也GET到了 1,也准备扣减。结果两个请求都认为自己买到了最后一件。这就是经典的竞态条件。而DECR是一条命令,Redis 保证原子性。10000 个DECR排着队执行,每个都安全。第四步:异步下单预减库存成功不代表订单创建成功。数据库操作太慢,不能同步执行。把任务丢进异步队列,让后台 Worker 慢慢处理:预减成功 → 创建排队记录(status='queued') → 放入异步队列(实际项目用 Celery.send_task) → 立即返回前端"抢购成功,正在排队处理"后台 Worker 处理队列时,数据库层面要再次校验库存(秒杀场景必须用锁),Redis 和 DB 之间可能不一致——比如 Redis 重启了、网络抖动了、Worker 崩了。Redis 预减成功了,数据库还得再验一次。秒杀的核心教训:不要信任任何单一环节。Redis 预减 + 数据库二次校验,双层才能万无一失。用户那边看到的是"排队处理中",最终结果是"成功"或"失败",体验流畅。动手:拼团拼团的难点不在技术复杂度,在于边界情况特别多。你每加一个功能,就多三种异常场景。先定义拼团实例的状态机:forming(成团中) → success(成团成功)→ 发货 forming(成团中) → failed(超时/失败)→ 退款开团:团长发起一个新团,自动成为第一个成员。校验活动在有效期内,事务里创建GroupBuyInstance(status=‘forming’,current_members=1)+ 创建团长成员记录。参团——最容易出 bug 的地方。核心框架:defjoin_group(self,user_id,group_id):withtransaction.atomic():# 行锁锁定,防止两个人同时参团导致人数计算错误group=GroupBuyInstance.objects.select_for_update().get(id=group_id)# 校验 1:团的状态(只有 forming 才能加人)# 校验 2:是否超时(超时则标记 failed,自动关闭)# 校验 3:人数是否已满# 校验 4:是否重复参团# 加入 + 人数加 1(用 F 表达式)GroupBuyMember.objects.create(group=group,user_id=user_id)group.current_members=F('current_members')+1group.save()# 关键!refresh_from_db 获取数据库真实值group.refresh_from_db()ifgroup.current_members>=group.activity.min_members:group.status='success'group.save()这里有个大坑——refresh_from_db()那行。F('current_members') + 1在 Python 层面并没有改变current_members的值,它只是生成一条 SQL 表达式current_members = current_members + 1。所以save()之后,Python 对象里的值还是旧的。如果你不做refresh_from_db(),直接拿group.current_members判断是否成团——3 个人同时参团,3 个人读到的都是旧值,3 个人都觉得"差一个人就成团了",结果团的实际人数已经够了,但没人触发成团逻辑。超时处理——定时任务扫描:defcheck_and_close_expired_groups(self):"""定时任务:扫描超时未成团的拼团(建议每 5 分钟执行一次)"""now=timezone.now()expired_groups=GroupBuyInstance.objects.filter(status='forming',expire_at__lt=now)forgroupinexpired_groups:withtransaction.atomic():# 双重校验:防止查询和执行之间被其他进程处理了fresh=GroupBuyInstance.objects.select_for_update().get(id=group.id)iffresh.status=='forming':fresh.status='failed'fresh.save()# TODO: 关联 Day11 的退款接口,给所有参团成员退款拼团的核心教训:永远不要信任内存中的状态。在分布式环境下,你内存里读到的值,可能 0.1 秒前就已经被别的请求改了。select_for_update()加锁、refresh_from_db()刷新——这两个操作在并发场景下就是生命线。用 AI 设计促销规则的状态转换三个系统里,拼团的状态转换最复杂。成团中 → 成功,触发发货;成团中 → 失败,触发退款。这些是基础流程。但现实的边界情况远不止这些:团长在成团中退出,怎么处理?整个团解散还是换团长?团员在成团中退出,人数不够了,团还能继续吗?成团成功后有人申请退款,会不会影响其他团员?活动结束了但团还没满,是等它自然超时还是立刻关闭?这些规则从零设计,光画状态转换图就得一下午,还不一定能想全。我实际用 AI 试了一下,prompt 是这样的:你是一个电商系统架构师。请为拼团系统设计完整的状态转换规则。 基本状态:forming(成团中)、success(成团成功)、failed(成团失败) 需要考虑的边界情况: 1. 团长在成团中退出,怎么处理? 2. 团员在成团中退出,怎么处理? 3. 成团成功后有人申请退款,怎么处理? 4. 活动结束但团还没满,怎么处理? 5. 系统并发导致的人数异常,怎么处理? 输出:完整的状态转换图 + 每个转换的触发条件和副作用 + 异常降级策略AI 给出了一个很完整的方案,其中有一个我完全没想到的设计:成团成功后的部分退款——增加success_partial_refund子状态,团还是成功的,其他团员正常发货,只有退款那个人走退款流程。这个思路确实很好。但我最终还是没用——因为商城的业务规则是"成团后不支持单个退款,只能整团退"。这就是 AI 辅助的边界:AI 能帮你穷举可能性,但最终的业务决策必须你来做。AI 加速心得:状态转换设计特别适合让 AI 穷举边界情况。人类思维天然容易遗漏"如果 A 和 B 同时发生怎么办"这种组合场景,但 AI 不会。把 AI 当成"边界情况检查器"——用它查漏补缺,而不是照搬方案。它给 10 个方案,你选 3 个最合适的,效率就很高了。学完来练练手优惠券叠加:支持多张券叠加使用,但有优先级(先算满减再算折扣)。注意:叠加后总金额不能为负数。秒杀预热倒计时:给秒杀接口加"预热期"——活动开始前 5 分钟,用户访问返回倒计时信息,同时自动预热 Redis 库存。拼团模拟退团:成团中允许团员退出,退出后current_members - 1。如果团长退出,把团长身份转移给最早参团的团员。秒杀实时监控:用 Redis 的GET读取剩余库存,写个接口返回"已售 X 件,剩余 Y 件",前端做成进度条。配套文件day12_promotion.py(优惠券 + 秒杀 + 拼团完整实现)可参考学习。本系列为梅雅达编程笔记原创,首发于 CSDN。有问题?评论区见