云计算百科
云计算领域专业知识百科平台

Flask订单系统完整实现|事务处理+状态流转+防超卖(附源码)

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


购物车出问题顶多是数量算错,订单出问题就是钱和库存对不上,真会出事。

从用户点击"提交订单"的那一刻起,一连串的动作要在极短时间内完成:校验库存、扣减库存、生成订单号、创建订单主表、创建订单明细表、清空购物车、跳支付页。

中间任何一步出问题,都可能导致"钱付了订单没了"或者"订单生成了库存没扣"的灾难。

说白了这一节你只要记住一件事:所有写库的操作必须塞一个事务里,不然早晚出事。

中间任何一步挂了,前面已经写进去的东西必须全部回滚,不然就是脏数据。


订单状态流转先搞清楚

在写代码之前,先把订单有哪些状态、状态之间怎么转的,画在脑子里。

用户下单
↓
[待支付] ──用户取消──→ [已取消]
│
└──支付成功──→ [待发货] ──商家发货──→ [待收货] ──确认收货──→ [已完成]
│ │
│ └──申请退款──→ [退款中] ──退款成功──→ [已退款]
└──申请退款──→ [退款中]

这7个状态先在脑子里过一遍,不然后面代码里 status=几 你自己都绕晕:

状态值状态名触发动作谁操作
0 待支付 用户下单 用户
1 待发货 支付成功 系统自动
2 待收货 商家发货 管理员
3 已完成 用户确认收货 用户
4 已取消 用户取消订单 用户 / 系统超时
5 退款中 用户申请退款 用户
6 已退款 退款审核通过 管理员

一、生成订单:整套流程 + 数据库事务

生成订单是最核心的代码,必须放在事务里。

import random
import time
from datetime import datetime
from flask import request, jsonify, render_template, redirect, url_for, flash
from flask_login import login_required, current_user

@app.route('/order/create', methods=['POST'])
@login_required
def order_create():
"""生成订单"""
address_id = request.form.get('address_id', type=int)
remark = request.form.get('remark', '')

try:
# 开启事务
with db.session.begin_nested():
# ========== 第1步:查购物车中选中的商品 ==========
cart_items = db.session.query(Cart, Goods).join(
Goods, Cart.goods_id == Goods.id
).filter(
Cart.user_id == current_user.id,
Cart.selected == 1
).with_for_update().all() # 行锁,防止并发

if not cart_items:
return jsonify({'code': 1, 'message': '请先选择商品'})

# ========== 第2步:校验库存 + 计算总金额 ==========
total_amount = 0
order_items_data = [] # 存订单项数据

for cart, goods in cart_items:
if goods.status != 1:
raise Exception(f'商品【{goods.name}】已下架')

if cart.quantity > goods.stock:
raise Exception(f'商品【{goods.name}】库存不足,仅剩{goods.stock}件')

subtotal = goods.price * cart.quantity
total_amount += subtotal

order_items_data.append({
'goods_id': goods.id,
'goods_name': goods.name,
'goods_image': goods.cover_image,
'price': goods.price,
'quantity': cart.quantity,
'subtotal': subtotal
})

# ========== 第3步:查收货地址 ==========
address = Address.query.filter_by(
id=address_id,
user_id=current_user.id
).first()
if not address:
raise Exception('请选择收货地址')

# 地址快照拼接(省 + 市 + 区 + 详细地址)
full_address = f"{address.province}{address.city}{address.district}{address.detail}"

# ========== 第4步:生成订单号 ==========
order_no = generate_order_no()

# ========== 第5步:创建订单主表 ==========
order = OrderInfo(
order_no=order_no,
user_id=current_user.id,
receiver_name=address.receiver,
receiver_phone=address.phone,
receiver_address=full_address,
total_amount=round(total_amount, 2),
pay_amount=round(total_amount, 2), # 没有优惠的话实付=总价
status=0, # 待支付
remark=remark
)
db.session.add(order)
db.session.flush() # 拿到 order.id

# ========== 第6步:创建订单明细 ==========
for item in order_items_data:
order_item = OrderItem(
order_id=order.id,
goods_id=item['goods_id'],
goods_name=item['goods_name'],
goods_image=item['goods_image'],
price=item['price'],
quantity=item['quantity'],
subtotal=round(item['subtotal'], 2)
)
db.session.add(order_item)

# ========== 第7步:扣减库存 ==========
for cart, goods in cart_items:
# 乐观锁方式扣库存:stock >= 数量时才更新
result = Goods.query.filter_by(id=goods.id).update({
'stock': Goods.stock – cart.quantity,
'sales': Goods.sales + cart.quantity
})
if result == 0:
raise Exception(f'商品【{goods.name}】库存不足')

# ========== 第8步:删除购物车中已结算的商品 ==========
Cart.query.filter(
Cart.user_id == current_user.id,
Cart.selected == 1
).delete(synchronize_session=False)

# 事务提交
db.session.commit()

return jsonify({
'code': 0,
'message': '订单创建成功',
'order_id': order.id,
'order_no': order.order_no
})

except Exception as e:
# 事务回滚
db.session.rollback()
return jsonify({'code': 1, 'message': str(e)})

def generate_order_no():
"""生成唯一订单号:时间戳 + 用户ID后4位 + 4位随机数"""
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
user_suffix = str(current_user.id).zfill(4)[–4:]
random_num = str(random.randint(0, 9999)).zfill(4)
return f"{timestamp}{user_suffix}{random_num}"

这段代码里有几个关键点,答辩常问:

1. 为什么用事务?

答:生成订单涉及多个数据库操作(创建订单、创建明细、扣库存、删购物车),任何一步失败都要全部回退。比如库存扣了但订单没生成——用户的钱白花了,库存也少了,两头出问题。事务保证要么全成功、要么全失败。

2. 为什么用 with_for_update()?

答:行锁。防止两个用户同时买最后一件商品时,都通过了库存检查,然后都去扣库存,导致超卖。with_for_update() 会在查询时给这些行加排他锁,其他事务要等这个事务提交了才能读,保证库存校验和扣减是原子的。

3. 订单号怎么生成的?

答:时间戳 + 用户ID后4位 + 4位随机数。这样有几个好处:时间维度一眼就能看出什么时候下的单、包含用户信息方便排查、随机数防止同一秒同一用户的多个订单重复。长度固定,32位以内。

4. 订单项为什么要存商品名称和价格的快照?

答:因为商品信息会变——改名、调价、下架、删除。如果只存商品ID,以后查历史订单时,商品名可能变了,价格也不一样了,用户看到的订单信息就不对了。快照存下来,订单数据永远是下单时的样子。

5. 地址为什么存快照?

答:同理,用户可能修改或删除收货地址。订单里的收货地址必须是下单那一刻的地址,不能跟着用户地址表的变化而变化。所以把收货人、电话、完整地址直接存到订单表里,做成快照。


二、订单列表(用户端)

用户的"我的订单"页面,一般支持按状态筛选:全部 / 待支付 / 待发货 / 待收货 / 退款 / 已完成。

我的订单页面长这样——顶部状态 Tab,下面是订单卡片(订单号、商品明细、下单时间、金额、详情按钮):

MYD商城订单列表

@app.route('/order/list')
@login_required
def order_list():
status = request.args.get('status', type=int) # 不传就是全部
page = request.args.get('page', 1, type=int)

query = OrderInfo.query.filter_by(user_id=current_user.id)

if status is not None:
query = query.filter_by(status=status)

query = query.order_by(OrderInfo.created_at.desc())
pagination = query.paginate(page=page, per_page=5, error_out=False)
orders = pagination.items

# 查每个订单的明细
for order in orders:
order.items = OrderItem.query.filter_by(order_id=order.id).all()

# 各状态数量(Tab 上显示红点)
status_counts = {
'pending_pay': OrderInfo.query.filter_by(user_id=current_user.id, status=0).count(),
'pending_ship': OrderInfo.query.filter_by(user_id=current_user.id, status=1).count(),
'pending_receive': OrderInfo.query.filter_by(user_id=current_user.id, status=2).count(),
'refund': OrderInfo.query.filter_by(user_id=current_user.id, status=5).count(),
}

return render_template('order/list.html',
orders=orders,
pagination=pagination,
current_status=status,
status_counts=status_counts)

模板渲染成 Tab 切换的形式:

<div class="order-tabs">
<a href="/order/list" class="{% if current_status is none %}active{% endif %}">全部</a>
<a href="/order/list?status=0" class="{% if current_status == 0 %}active{% endif %}">
待支付 {% if status_counts.pending_pay > 0 %}<span class="badge">{{ status_counts.pending_pay }}</span>{% endif %}
</a>
<a href="/order/list?status=1" class="{% if current_status == 1 %}active{% endif %}">
待发货 {% if status_counts.pending_ship > 0 %}<span class="badge">{{ status_counts.pending_ship }}</span>{% endif %}
</a>
<a href="/order/list?status=2" class="{% if current_status == 2 %}active{% endif %}">
待收货 {% if status_counts.pending_receive > 0 %}<span class="badge">{{ status_counts.pending_receive }}</span>{% endif %}
</a>
<a href="/order/list?status=5" class="{% if current_status == 5 %}active{% endif %}">
退款中 {% if status_counts.refund > 0 %}<span class="badge">{{ status_counts.refund }}</span>{% endif %}
</a>
<a href="/order/list?status=3" class="{% if current_status == 3 %}active{% endif %}">已完成</a>
</div>


三、订单操作:取消、确认收货、申请退款

取消订单(待支付状态才可以取消)

@app.route('/order/cancel/<int:order_id>', methods=['POST'])
@login_required
def order_cancel(order_id):
order = OrderInfo.query.filter_by(
id=order_id,
user_id=current_user.id
).first()

if not order:
return jsonify({'code': 1, 'message': '订单不存在'})

if order.status != 0: # 只有待支付才能取消
return jsonify({'code': 1, 'message': '当前状态不能取消'})

try:
with db.session.begin_nested():
# 1. 更新订单状态
order.status = 4 # 已取消
order.cancel_time = datetime.now()

# 2. 恢复库存(取消订单要把库存加回去)
items = OrderItem.query.filter_by(order_id=order.id).all()
for item in items:
Goods.query.filter_by(id=item.goods_id).update({
'stock': Goods.stock + item.quantity,
'sales': Goods.sales – item.quantity
})

db.session.commit()
return jsonify({'code': 0, 'message': '订单已取消'})
except:
db.session.rollback()
return jsonify({'code': 1, 'message': '取消失败'})

确认收货(待收货状态)

@app.route('/order/confirm/<int:order_id>', methods=['POST'])
@login_required
def order_confirm(order_id):
order = OrderInfo.query.filter_by(
id=order_id,
user_id=current_user.id
).first()

if not order or order.status != 2: # 待收货才能确认
return jsonify({'code': 1, 'message': '当前状态不能确认收货'})

order.status = 3 # 已完成
order.finish_time = datetime.now()
db.session.commit()

return jsonify({'code': 0, 'message': '已确认收货'})

申请退款(待发货 / 待收货都能申请)

@app.route('/order/refund/<int:order_id>', methods=['POST'])
@login_required
def order_refund(order_id):
order = OrderInfo.query.filter_by(
id=order_id,
user_id=current_user.id
).first()

if not order or order.status not in [1, 2]: # 待发货或待收货才能申请退款
return jsonify({'code': 1, 'message': '当前状态不能申请退款'})

order.status = 5 # 退款中
db.session.commit()

return jsonify({'code': 0, 'message': '退款申请已提交,等待审核'})


四、后台订单管理

管理员那边的订单管理,主要是发货和退款审核。

发货(待发货 → 待收货)

@app.route('/admin/order/ship/<int:order_id>', methods=['POST'])
@admin_required
def admin_order_ship(order_id):
order = OrderInfo.query.get_or_404(order_id)

if order.status != 1: # 只有待发货才能发
return jsonify({'code': 1, 'message': '当前状态不能发货'})

order.status = 2 # 待收货
order.ship_time = datetime.now()

# 如果有快递单号,可以传过来
express_no = request.form.get('express_no')
if express_no:
order.express_no = express_no # 表结构里加上这个字段

db.session.commit()
return jsonify({'code': 0, 'message': '发货成功'})

退款审核(退款中 → 已退款)

@app.route('/admin/order/refund-approve/<int:order_id>', methods=['POST'])
@admin_required
def admin_order_refund_approve(order_id):
order = OrderInfo.query.get_or_404(order_id)

if order.status != 5: # 只有退款中才能审核
return jsonify({'code': 1, 'message': '当前状态不能审核'})

try:
with db.session.begin_nested():
# 1. 更新订单状态
order.status = 6 # 已退款

# 2. 恢复库存
items = OrderItem.query.filter_by(order_id=order.id).all()
for item in items:
Goods.query.filter_by(id=item.goods_id).update({
'stock': Goods.stock + item.quantity
})

# 3. 真实项目里这里还要调支付接口退款
# 毕设可以略过,或者模拟一下

db.session.commit()
return jsonify({'code': 0, 'message': '退款成功'})
except:
db.session.rollback()
return jsonify({'code': 1, 'message': '退款失败'})


五、支付对接(可选加分项)

毕设里支付可以做简化版——模拟支付成功。

但如果想做得真实一点,可以接入支付宝沙箱环境,免费的,答辩效果好。

简化版支付流程:

@app.route('/order/pay/<int:order_id>')
@login_required
def order_pay(order_id):
"""模拟支付页面"""
order = OrderInfo.query.filter_by(
id=order_id,
user_id=current_user.id
).first_or_404()

if order.status != 0:
flash('订单状态异常', 'error')
return redirect(url_for('order_list'))

return render_template('order/pay.html', order=order)

@app.route('/order/pay-success/<int:order_id>', methods=['POST'])
@login_required
def order_pay_success(order_id):
"""模拟支付成功回调"""
order = OrderInfo.query.filter_by(
id=order_id,
user_id=current_user.id
).first()

if not order or order.status != 0:
return jsonify({'code': 1, 'message': '订单状态异常'})

order.status = 1 # 待发货
order.pay_time = datetime.now()
order.pay_type = 'alipay' # 模拟支付宝支付
db.session.commit()

return jsonify({'code': 0, 'message': '支付成功'})

真实接入支付宝的话,用 alipay-sdk-python 这个库就行,流程是:

  • 后端生成支付链接(调用 alipay.trade.page.pay)
  • 用户跳转到支付宝页面付款
  • 支付宝异步回调通知支付结果
  • 后端验签后更新订单状态
  • 这个功能做了,毕设直接上一个档次。


    订单模块,答辩常问这几问

    1. 怎么防止超卖?

    答:三种手段配合:①数据库事务——扣库存和创建订单在同一个事务里;②行级锁——用 SELECT … FOR UPDATE 给商品行加锁,防止并发都读到同一个库存;③乐观锁——更新库存时加 WHERE stock >= 数量 的条件,影响行数为0就回滚。三层一起用,基本不会超卖。

    2. 订单明细表为什么不直接关联商品表?

    答:关联了,存了 goods_id。但同时存了商品名称、图片、价格的快照。因为商品信息会变化,历史订单必须保留下单时的原始信息,不能跟着商品表的变化而变化。我最开始订单明细只存了 goods_id,后来商品改了个价,历史订单全跟着变了,当场就懵了,这才补的快照字段。

    3. 订单号为什么不用自增ID?

    答:自增ID有几个问题:暴露订单量(别人能从订单号推算你一天多少单)、不够长容易被猜到、不带业务信息。自定义订单号(时间戳+用户+随机数)既唯一又安全,还能从订单号看出时间信息。

    4. 取消订单为什么要恢复库存?

    答:因为下单的时候已经扣了库存,如果取消了不恢复,库存就越来越少,商品会莫名其妙地"卖完"。注意:只有待支付状态取消才恢复库存,已发货之后取消走退款流程,退款的时候再恢复。

    5. 什么是事务?ACID是什么?

    答:事务是一组数据库操作,要么全部成功,要么全部失败。ACID是事务的四个特性:A原子性(Atomicity,不可分割)、C一致性(Consistency,数据从一个一致状态变到另一个一致状态)、I隔离性(Isolation,多个事务互不干扰)、D持久性(Durability,提交后永久生效)。


    订单模块是整个电商系统的核心,也是毕设答辩老师最爱问的地方。

    这几块在答辩PPT里各占一页,老师问到哪个你都能展开两句,基本就不会被问倒。


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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Flask订单系统完整实现|事务处理+状态流转+防超卖(附源码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!