TAOCARTS 知识

1.5万机票只退432?聊聊代购系统里的退款逻辑与订单状态机

2026-07-23 博客文章

看到"花1.5万买机票退票仅退432元"这条热搜,我笑了。不是幸灾乐祸,是因为做代购系统这几年,退款这事儿我太懂了。看起来简单的"退钱"两个字,背后的逻辑复杂到你想象不到。

今天就聊聊代购系统里的退款设计,以及我踩过的那些坑。

为什么退款这么复杂?

你可能会说,退款不就是把钱退回去吗?有什么难的。那是你没做过代购系统。

普通电商的退款:用户申请 → 商家审核 → 退款完成。就这么简单。

代购系统的退款呢?我给你数数有多少种情况:

用户付款后,还没下单采购,用户要退款 —— 全退

已经下单采购了,但商家还没发货,用户要退款 —— 要扣手续费,还要看商家同不同意退

商家已经发货了,还没到仓库,用户要退款 —— 更麻烦,可能要拒收,或者退回去的运费谁出?

已经到仓库了,用户要退款 —— 商品能二次销售的可以退,定制的不能退

已经发往国外了,用户要退款 —— 基本退不了,除非包裹还能截回来

部分退款 —— 一个订单里有多个商品,只退其中几个

差价退款 —— 买的时候贵了,后来降价了,用户要退差价

运费退款 —— 实际运费比预估的少,要退差价

你看,就退款这一件事,就能拆出这么多场景。如果状态机设计得不好,后期维护能把你逼疯。

订单状态机的设计

我做过的几个代购系统,包括参考taocarts的设计,订单状态机都是核心中的核心。设计得好,后面加功能都很顺;设计得不好,改一个bug出十个bug。

# 用Python实现的订单状态机

# 踩坑记录:

# 1. 状态流转必须是单向的,不能随便跳

# 2. 每个状态变更都要记录日志

# 3. 退款要单独走状态,不能和正常流程混在一起

from enum import Enum

from typing import List, Dict

class OrderStatus(Enum):

    # 正常流程状态

    PENDING_PAYMENT = "pending_payment"  # 待支付

    PAID = "paid"                        # 已支付

    PURCHASING = "purchasing"            # 采购中

    PURCHASED = "purchased"              # 已采购

    IN_WAREHOUSE = "in_warehouse"        # 已入库

    SHIPPED = "shipped"                  # 已发货

    DELIVERED = "delivered"              # 已签收



    # 退款相关状态

    REFUND_APPLYING = "refund_applying"  # 退款申请中

    REFUNDING = "refunding"              # 退款中

    REFUNDED = "refunded"                # 已退款

    REFUND_REJECTED = "refund_rejected"  # 退款被拒

class OrderStateMachine:

    # 允许的状态流转

    ALLOWED_TRANSITIONS: Dict[OrderStatus, List[OrderStatus]] = {

        OrderStatus.PENDING_PAYMENT: [OrderStatus.PAID, OrderStatus.REFUNDED],

        OrderStatus.PAID: [OrderStatus.PURCHASING, OrderStatus.REFUND_APPLYING],

        OrderStatus.PURCHASING: [OrderStatus.PURCHASED, OrderStatus.REFUND_APPLYING],

        OrderStatus.PURCHASED: [OrderStatus.IN_WAREHOUSE, OrderStatus.REFUND_APPLYING],

        OrderStatus.IN_WAREHOUSE: [OrderStatus.SHIPPED, OrderStatus.REFUND_APPLYING],

        OrderStatus.SHIPPED: [OrderStatus.DELIVERED, OrderStatus.REFUND_APPLYING],

        OrderStatus.DELIVERED: [],  # 终态



        OrderStatus.REFUND_APPLYING: [OrderStatus.REFUNDING, OrderStatus.REFUND_REJECTED, OrderStatus.PAID],

        OrderStatus.REFUNDING: [OrderStatus.REFUNDED],

        OrderStatus.REFUNDED: [],   # 终态

        OrderStatus.REFUND_REJECTED: [OrderStatus.PAID, OrderStatus.PURCHASING],

    }



    def __init__(self, current_status: OrderStatus):

        self.current_status = current_status



    def can_transition_to(self, target_status: OrderStatus) -> bool:

"""检查是否可以流转到目标状态"""

allowed = self.ALLOWED_TRANSITIONS.get(self.current_status, [])

return target_status in allowed

def transition(self, target_status: OrderStatus, operator: str, remark: str = "") -> bool:

"""执行状态流转"""

        if not self.can_transition_to(target_status):

            raise ValueError(

                f"不允许从 {self.current_status.value} 流转到 {target_status.value}"

            )



        # 记录状态变更日志(这个很重要!出问题了能回溯)

        self._log_status_change(

            from_status=self.current_status,

            to_status=target_status,

            operator=operator,

            remark=remark,

        )



        self.current_status = target_status

return True

def _log_status_change(self, from_status, to_status, operator, remark):

"""记录状态变更日志"""

        # 实际项目中这里要写数据库

        print(f"状态变更: {from_status.value} -> {to_status.value}, "

              f"操作人: {operator}, 备注: {remark}")

退款金额的计算

退款金额怎么算?这又是个大坑。

我见过最蠢的设计,就是退款金额直接等于订单金额。然后出问题了——用户用了优惠券怎么办?用了积分怎么办?运费怎么算?手续费怎么算?

正确的做法是,退款金额要单独计算,不能直接用订单金额。

def calculate_refund_amount(order, refund_items: list, refund_type: str) -> dict:

"""

计算退款金额

踩坑记录:

1. 优惠券要按比例分摊到每个商品

2. 运费退不退要看情况:还没发货的全退,发了的不退

3. 手续费:不同阶段退款手续费比例不一样

4. 积分抵扣的部分要退积分,不是退钱

"""

total_refund = 0

coupon_refund = 0

points_refund = 0

shipping_refund = 0

    service_fee = 0



    # 计算商品退款金额

for item in refund_items:

order_item = order.items.get(item["item_id"])

if not order_item:

            continue



        # 商品本身的金额

        item_amount = order_item.price * item["quantity"]

        total_refund += item_amount



        # 分摊的优惠券

        item_coupon = order_item.coupon_share * item["quantity"]

        coupon_refund += item_coupon



        # 分摊的积分

        item_points = order_item.points_share * item["quantity"]

        points_refund += item_points



    # 运费退款

    if refund_type == "full" and order.status == "paid":

        # 整单退款且还没发货,退运费

        shipping_refund = order.shipping_fee



    # 手续费(根据订单状态不同,比例不同)

    fee_rates = {

"paid": 0.0, # 刚付款还没采购,不收手续费

"purchasing": 0.05, # 采购中,收5%手续费

"purchased": 0.1, # 已采购,收10%手续费

"in_warehouse": 0.15, # 已入库,收15%手续费

    }

    fee_rate = fee_rates.get(order.status, 0.2)

    service_fee = total_refund * fee_rate



    # 实际退款金额 = 商品金额 - 优惠券 - 手续费

    actual_refund = total_refund - coupon_refund - service_fee



    return {

"total_refund": total_refund, # 商品总金额

"coupon_refund": coupon_refund, # 退回优惠券

"points_refund": points_refund, # 退回积分

"shipping_refund": shipping_refund, # 退运费

"service_fee": service_fee, # 手续费

"actual_refund": actual_refund, # 实际退给用户的钱

}

异步退款的坑

还有个坑很多人踩过:同步退款。

就是用户点了退款,你直接调用支付接口退款,然后等结果。这在量小的时候没问题,量大了就完蛋。支付接口有时候会超时,或者网络波动,你这边超时了,那边实际退款成功了,然后用户说没收到钱,你一查发现成功了,尴尬不?

正确的做法是异步退款。用户申请后,系统生成退款单,状态是"处理中",然后后台异步去调支付接口。支付结果通过回调通知更新状态。这样就算网络出问题,也可以靠定时任务重试。

最后说两句

退款这事儿,看起来简单,做起来全是细节。我刚入行的时候,以为退款就是update一下订单状态,后来才发现水太深了。

taocarts的代购系统在这块做得就很完善,人家光退款相关的状态就有十几种,各种边界情况都考虑到了。这就是成熟系统和小作坊的区别。

今天就聊到这儿。你们做系统的时候有没有遇到过什么奇葩的退款需求?评论区聊聊。