⚠️ 免责声明:本文项目仅供技术学习参考,严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新,学术诚信从我做起 🙂
购物车出问题顶多是数量算错,订单出问题就是钱和库存对不上,真会出事。
从用户点击"提交订单"的那一刻起,一连串的动作要在极短时间内完成:校验库存、扣减库存、生成订单号、创建订单主表、创建订单明细表、清空购物车、跳支付页。
中间任何一步出问题,都可能导致"钱付了订单没了"或者"订单生成了库存没扣"的灾难。
说白了这一节你只要记住一件事:所有写库的操作必须塞一个事务里,不然早晚出事。
中间任何一步挂了,前面已经写进去的东西必须全部回滚,不然就是脏数据。
订单状态流转先搞清楚
在写代码之前,先把订单有哪些状态、状态之间怎么转的,画在脑子里。
用户下单
↓
[待支付] ──用户取消──→ [已取消]
│
└──支付成功──→ [待发货] ──商家发货──→ [待收货] ──确认收货──→ [已完成]
│ │
│ └──申请退款──→ [退款中] ──退款成功──→ [已退款]
└──申请退款──→ [退款中]
这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,下面是订单卡片(订单号、商品明细、下单时间、金额、详情按钮):

@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 这个库就行,流程是:
这个功能做了,毕设直接上一个档次。
订单模块,答辩常问这几问
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 · 转载请注明出处
网硕互联帮助中心






评论前必须登录!
注册