news 2026/11/5 6:47:53

下单不付款库存被占?Flask定时任务自动取消超时订单+回滚库存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
下单不付款库存被占?Flask定时任务自动取消超时订单+回滚库存

⚠️免责声明:本文项目仅供技术学习参考,严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新,学术诚信从我做起 🙂


这事服务端不干,库存就锁死了

先还原一个事故现场。

你的商城里有一件爆款卫衣,库存30件。有人一口气点了3件下单,然后……不付款了。

订单挂在"待支付",库存照05篇讲的逻辑已经扣掉了。真正想买的人进来一看:库存不足。

那超时没人付的单,谁来取消?答案在后台。被取消的订单在管理后台长这样:

图:后台订单管理里,可以直接筛出「已取消」的订单

图:订单被取消后,用户在订单详情看到的最终状态

半小时后他还没付。一小时后还没付。

3件货被一个永远不会付的订单锁死了。

这篇解决的就是这件事:下单超过一定时间没付款,系统自动取消订单,把库存还回去。

核心动作就两个:定时扫描 + 取消回滚。取消回滚05篇写过,这篇的重点是"谁来定时、怎么定时、定时任务怎么不出事"。


一、为什么不能靠前端倒计时

很多人第一反应:在订单页放个30分钟倒计时,时间到了发个请求取消订单。

能跑。但只在用户还开着这个页面的时候能跑。

用户关了浏览器、断了网、切了App、直接杀进程,倒计时就没了,取消请求永远不会发。

库存照样锁着。

**前端倒计时只能用来"显示",不能用来"执行"。**真正的超时判断必须发生在服务端,而且不能依赖任何用户在线。

这是这一篇最先要想明白的事。


二、三种服务端方案,毕设选哪个

方案原理优点缺点
定时扫表每隔几分钟查一次超时订单简单、好理解、好答辩有几分钟误差
Redis过期监听下单时写一个30分钟过期的key,过期触发取消准时、性能好要装Redis,通知可能丢,配置麻烦
延迟队列(死信队列)消息进队列,延迟30分钟再消费工业级方案对毕设太重

毕设选型:定时扫表 + APScheduler。

理由很实在:不增加新组件、代码量小、逻辑一眼能看懂、答辩好讲。几分钟的误差对毕设完全无所谓,真要纠结,把扫描间隔设短一点就行。

Redis方案我会在最后提一嘴,作为"我了解更优解"的答辩储备。


三、先给订单表加一个字段

判断超时,需要知道"这单是什么时候下的"。

01篇建表时本来就有created_at,可以直接拿它算。但更规范的做法是单独留一个取消时间,方便以后查"这单是用户取消的还是系统取消的":

ALTERTABLE`order_info`ADDCOLUMN`cancel_time`datetimeDEFAULTNULLCOMMENT'取消时间',ADDCOLUMN`cancel_type`tinyintDEFAULTNULLCOMMENT'取消类型:1用户取消 2系统超时';

超时线就用created_at算:当前时间 - 下单时间 > 30分钟,且状态还是待支付(status=0),就是超时单。


四、写定时任务:扫描 + 批量回滚

先装库:

pipinstallapscheduler

新建一个scheduler.py,专门放定时任务:

fromdatetimeimportdatetime,timedeltafromapscheduler.schedulers.backgroundimportBackgroundScheduler# 后台调度器:不阻塞 Flask 主线程scheduler=BackgroundScheduler(timezone='Asia/Shanghai')# 超时时间(分钟)ORDER_TIMEOUT_MINUTES=30# 每次最多处理多少单,防止一次捞出几万条把库打挂BATCH_SIZE=100defcancel_timeout_orders(app):"""取消超时未支付订单,并回滚库存"""# 定时任务不在请求上下文里,要手动推入 app 上下文才能用 dbwithapp.app_context():fromapp.modelsimportOrderInfo,OrderItem,Goods,db deadline=datetime.now()-timedelta(minutes=ORDER_TIMEOUT_MINUTES)# 关键条件:状态=待支付 且 下单时间早于截止线timeout_orders=OrderInfo.query.filter(OrderInfo.status==0,OrderInfo.created_at<deadline).order_by(OrderInfo.created_at.asc()).limit(BATCH_SIZE).all()ifnottimeout_orders:returntry:fororderintimeout_orders:# 再判一次状态,防止扫描到处理的间隙用户刚好付了款(幂等)iforder.status!=0:continue# 1. 回滚库存:加回库存,减回销量items=OrderItem.query.filter_by(order_id=order.id).all()foriteminitems:Goods.query.filter_by(id=item.goods_id).update({'stock':Goods.stock+item.quantity,'sales':Goods.sales-item.quantity})# 2. 订单置为已取消,标记是系统取消的order.status=4order.cancel_time=datetime.now()order.cancel_type=2db.session.commit()app.logger.info(f"定时任务取消超时订单{len(timeout_orders)}单")exceptExceptionase:db.session.rollback()app.logger.error(f"超时订单处理失败:{e}")definit_scheduler(app):"""注册任务并启动"""# interval:固定间隔。每5分钟跑一次scheduler.add_job(func=lambda:cancel_timeout_orders(app),trigger='interval',minutes=5,id='cancel_timeout_orders',replace_existing=True)scheduler.start()

库存回滚那段,和05篇"用户手动取消订单"是同一套逻辑:stock加回去,sales减回来。区别只是触发方从用户点击变成了定时任务,以及多了一个cancel_type=2。


五、在应用启动时把任务挂上去

在 Flask 应用工厂(一般是app/__init__.py)里启动调度器:

from.schedulerimportinit_schedulerdefcreate_app():app=Flask(__name__)# ... 各种初始化、注册蓝图 ...# 生产/正常启动时才开调度器init_scheduler(app)returnapp

然后正常flask run或python run.py,任务就活了:每5分钟醒一次,找超时单,处理,睡回去。


六、三个一定会踩的坑

坑1:debug模式下任务被执行两次

这是APScheduler配Flask最经典的坑。

Flask的debug模式默认会启动一个reloader 监视进程 + 一个真正的服务进程,等于应用被加载了两次,调度器也跟着启动了两个,同一批订单可能被处理两遍。

两种解决办法,任选:

# 办法一:启动时关掉 reloader(毕设调试期够用)app.run(debug=True,use_reloader=False)
# 办法二:只在 reloader 的子进程里启动调度器(推荐)importosdefcreate_app():app=Flask(__name__)# ...# 环境变量 WERKZEUG_RUN_MAIN 为 true 说明是真正干活的子进程ifos.environ.get('WERKZEUG_RUN_MAIN')=='true':init_scheduler(app)returnapp

记住一句话:调度器在整个系统里只能有一个实例。多实例不光重复跑,还可能重复回滚库存。

坑2:任务里报"Working outside of application context"

定时任务不是HTTP请求,没有请求上下文,直接用db.session会报错。

所以任务函数里必须套一层with app.app_context():,上面代码里已经写了。把app传进去就是为了这个。

坑3:时间别用数据库的NOW()拍脑袋

判断超时时,"当前时间"以应用服务器为准(datetime.now()),下单时间以created_at为准。如果以后上云,应用和数据库分属不同机器,要确认两边时区一致,统一用Asia/Shanghai,否则可能出现"订单刚下就被判超时"或者"永远不超时"的灵异现象。

调度器初始化时传timezone='Asia/Shanghai'也是这个原因。


七、答辩被追问"高并发怎么办"

定时扫表毕设够用,但如果老师追问"订单量大了这个方案有什么问题",你可以这样答:

扫表有两个局限:一是有轮询间隔,取消不够实时;二是订单量大了,每5分钟全表扫一次会有压力,需要加索引、分批处理。

更大的系统会用Redis:下单时往Redis写一个30分钟过期的key(或用ZSet,score存过期时间戳),过期/轮询到就发取消消息,由消费者去取消订单并回滚库存。这样准时,且不给主库添负担。

这段一说,老师就知道你不是只会写个定时器,而是清楚这个方案的边界在哪。

另外补一个工程细节:取消订单和回滚库存必须在同一个事务里(05篇的begin_nested),否则可能出现订单取消了、库存没还回去的半吊子状态。上面代码整体commit/rollback就是在保证这一点。


定时任务,答辩常问这几问

1. 为什么不在前端用倒计时自动取消?

答:前端倒计时依赖页面开着,用户关浏览器、断网、杀进程就失效了。超时判断必须放在服务端,不依赖用户是否在线。前端倒计时只负责给用户看,服务端定时任务才负责真正执行。

2. 定时任务和用户刚好付款,冲突了怎么办?

答:任务在真正改状态前会再查一次订单状态,只有仍然是"待支付"才取消。支付回调那边同样会先判状态。两边都做状态校验,保证一笔订单不会被重复处理,这就是幂等。

3. 取消订单时为什么要回滚库存,还要减销量?

答:下单瞬间已经按05篇的逻辑扣了库存、加了销量。订单取消意味着这笔交易没发生,库存要加回来,销量也要减回去,两个字段一起还原,数据才一致。而且这两步和订单状态更新在同一个事务里。

4. APScheduler 定时任务在debug下为什么会跑两次?

答:Flask debug的reloader会加载两次应用:一次监视进程、一次服务进程,调度器被启动了两个实例。用use_reloader=False,或者判断WERKZEUG_RUN_MAIN只在子进程启动,都能解决。一句话记牢:调度器全局只能有一个。

5. 一次查出几十万条超时订单怎么办?

答:用limit分批处理,每批比如100条,配合created_at索引;任务间隔内消化不完,下一轮接着处理。同时给status和created_at建联合索引,避免全表扫描。


超时取消这个功能,代码量不大,但它考的是一种思维:系统得能在"什么都没发生"的时候自己把残局收拾干净。

用户不付款、关掉页面、人间蒸发——这些才是真实世界的常态。好的系统不能假设每一步都有人盯着点。

把这一篇和05篇连起来看,下单扣库存、付款流转、超时自动释放,订单从生到死这一整条,就接上了。


梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 6:45:57

CRA Draft Guidance(指导文件草案)解读:谁才是真正的制造商?

为什么需要关注 CRA Draft Guidance 2026 年 3 月&#xff0c;欧盟委员会发布了《Cyber Resilience Act&#xff08;CRA&#xff09;Draft Guidance》征求意见稿&#xff0c;旨在帮助企业更好地理解和落实 CRA 的具体要求。CRA 法规本身规定了企业需要履行的义务&#xff0c;而…

作者头像 李华
网站建设 2026/11/5 6:45:56

中项-案例分析2

规划质量管理过程中可用来规避该问题的工具 / 技术&#xff1a; 1. 过程决策程序图 (PDPC) 这是一种系统性的管理规划工具&#xff0c;旨在识别正在制定的计划中可能出现的问题。通过该工具&#xff0c;团队可以提前制定对策来预防或抵消这些问题&#xff0c;从而修订计划以避开…

作者头像 李华
网站建设 2026/11/5 6:45:52

15个字符串处理绝招,让你代码效率翻倍

一、同样的这个写作的情况, 为什么人家那边只用了那个三行就搞定了&#xff0c;但是你自己这边却写了足足三十行之多呢?从事开发工作的人都有一个比较一致的共识, 就是字符串处理这个事情, 在日常的编码任务里面所占的比重是高达百分之六十以上的。无论你是需要爬取数据, 还是…

作者头像 李华
网站建设 2026/11/5 6:44:43

微调、蒸馏、RAG 之后,要不要重新备案?三种改造方式的触发差异

1. 先说结论&#xff1a;三种改造方式&#xff0c;备案触发点完全不同 很多团队在模型上线后做了一轮优化——微调、蒸馏、接入 RAG——然后卡在同一个问题上&#xff1a;这些改造是否需要重新备案&#xff1f; 先给一个可以直接拿去与合规同学对齐的结论表&#xff1a;改造方式…

作者头像 李华
网站建设 2026/11/5 6:43:53

QuickJS 顶层 await 为何只执行两次:事件循环挂起原因与修复

QuickJS 顶层 await 为何只执行两次&#xff1a;事件循环挂起原因与修复 【免费下载链接】quickjs Public repository of the QuickJS Javascript Engine. 项目地址: https://gitcode.com/gh_mirrors/qu/quickjs 在 QuickJS 里&#xff0c;同一段等待逻辑&#xff0c;写…

作者头像 李华