Stripe 和 Airwallex(空中云汇)退款机制说明

Stripe 和 Airwallex(空中云汇)退款机制说明

Stripe 和 Airwallex(空中云汇)退款机制说明

📋 目录


退款流程概览

统一入口

所有退款请求通过 src/app/api/order/refund/route.ts 处理

退款条件

  • 订单状态限制:只有 待确认(2)待注册(3) 状态的订单可以申请退款

  • 生成退款单号:系统自动生成唯一的退款单号

  • 状态更新:订单状态更新为 申请退款(7)

支持退款方式

  1. Stripe (pay_type = 7)

  2. Airwallex (pay_type = 8)

  3. 积分支付 (pay_type = 4)


Stripe 退款详解

实现文件

  • API 路由src/app/api/order/refund/route.ts

  • 退款处理src/utils/stripe.ts - processStripeRefund()

退款流程


1. 验证订单信息

   ├── 检查是否有 stripe_payment_intent_id

   └── 检查订单状态是否为待确认(2)或待注册(3)



2. 调用 Stripe API 退款

   ├── 使用 payment_intent_id

   ├── 退款原因:'requested_by_customer'

   └── 全额退款



3. 处理赠送积分

   ├── 查找订单产生的赠送积分记录

   ├── 从用户余额扣除对应积分

   └── 记录积分扣除日志



4. 更新订单状态

   ├── 状态改为「已退款(8)」

   └── 记录退款完成时间 status_8_time

核心代码

API 调用


// src/app/api/order/refund/route.ts (79-90行)

case 7: // Stripe

    if (!order.stripe_payment_intent_id) {

        return error("订单缺少Stripe支付ID,无法退款")

    }

    refundResult = await processStripeRefund({

        paymentIntentId: order.stripe_payment_intent_id,

        reason: 'requested_by_customer',

        userId: userId,

        orderNum: order.orderNum,

        orderId: orderId

    });

    break;

退款处理逻辑


// src/utils/stripe.ts (95-171行)

export async function processStripeRefund({

  paymentIntentId,

  reason = 'requested_by_customer',

  userId,

  orderNum,

  orderId

}) {

  // 1. 调用 Stripe API 创建退款

  const refund = await stripe.refunds.create({

    payment_intent: paymentIntentId,

    reason: reason

  });



  // 2. 扣除赠送积分

  const bonusPointsRecord = await collection('web_points').findOne({

    userId: new ObjectId(userId),

    out_trade_no: orderNum,

    source: 4 // 商品消费

  });

  

  if (bonusPointsRecord) {

    // 扣除积分

    await collection('web_user').updateOne(

      { _id: new ObjectId(userId) },

      { $inc: { points: -bonusPointsRecord.points } }

    );

    

    // 记录日志

    await collection('web_points').insertOne({

      userId: new ObjectId(userId),

      points: bonusPointsRecord.points,

      source: 5, // 订单退款

      status: 2,

      orderNum: orderNum,

      description: `订单【${orderNum}】退款扣除赠送积分【${bonusPointsRecord.points}】积分`,

      add_time: Date.now()

    });

  }

  

  // 3. 更新订单状态为已退款

  await collection("web_order").updateOne(

    { _id: new ObjectId(orderId) },

    {

      $set: {

        status: 8,

        status_8_time: Date.now()

      }

    }

  );



  return {

    id: refund.id,

    status: refund.status,

    amount: refund.amount,

    reason: refund.reason

  };

}

Stripe 退款特点

  • 自动化处理:退款、积分扣除、状态更新一气呵成

  • 积分自动扣除:自动查找并扣除赠送的积分

  • 状态同步:自动更新订单状态为「已退款(8)」

  • 完整日志:记录所有操作日志

  • 错误处理:完善的异常捕获机制


Airwallex 退款详解

实现文件

  • API 路由src/app/api/order/refund/route.ts

  • 退款处理src/utils/airwallex.ts - processAirwallexRefund()

退款流程


1. 验证订单信息

   ├── 检查是否有 airwallex_payment_intent_id

   └── 检查订单状态



2. 获取认证 Token

   ├── 调用 getAirwallexToken()

   └── 获取访问令牌



3. 构建退款请求

   ├── endpoint: /api/v1/pa/refunds/create

   ├── payment_intent_id: 支付意图ID

   ├── reason: 退款原因

   └── request_id: 订单号或时间戳



4. 调用 Airwallex API

   ├── 发送 POST 请求

   └── 返回退款结果

核心代码

API 调用


// src/app/api/order/refund/route.ts (92-102行)

case 8: // Airwallex

    if (!order.airwallex_payment_intent_id) {

        return error("订单缺少Airwallex支付ID,无法退款")

    }

    refundResult = await processAirwallexRefund(

        order.airwallex_payment_intent_id,

        order.price_total,

        reason || '客户申请退款',

        order.orderNum

    );

    break;

退款处理逻辑


// src/utils/airwallex.ts (143-191行)

export async function processAirwallexRefund(

  paymentIntentId: string, 

  amount: number, 

  reason?: string,

  orderNum?: string

) {

  try {

    // 1. 获取认证token

    const token = await getAirwallexToken();

    

    // 2. 使用订单号作为request_id,便于追踪

    const requestId = orderNum || `refund_${Date.now()}`;

    

    // 3. 构建请求体

    const requestBody: any = {

      payment_intent_id: paymentIntentId,

      reason: reason || '客户申请退款',

      request_id: requestId

    };

    

    // 4. 调用 Airwallex API

    const response = await fetch(`${AIRWALLEX_CONFIG.baseUrl}/api/v1/pa/refunds/create`, {

      method: 'POST',

      headers: {

        'Content-Type': 'application/json',

        'Authorization': `Bearer ${token}`,

        'x-client-id': AIRWALLEX_CONFIG.clientId!

      },

      body: JSON.stringify(requestBody)

    });



    if (!response.ok) {

      const errorData = await response.json();

      throw new Error(errorData.message || `Airwallex refund failed: ${response.status}`);

    }



    const data = await response.json();

    console.log('Airwallex refund response:', data);

    return data;



  } catch (error) {

    console.error('Airwallex refund failed:', error);

    throw error;

  }

}

Airwallex 退款特点

  • API 调用:直接通过 HTTP API 调用 Airwallex

  • 唯一标识:使用订单号作为 request_id 便于追踪

  • 错误处理:完善的异常捕获和日志

  • ⚠️ 积分处理:目前只调用退款 API,不处理积分扣除

  • ⚠️ 状态更新:目前只调用退款 API,不自动更新订单状态


功能对比

| 特性 | Stripe | Airwallex |

|------|--------|-----------|

| API 调用方式 | Stripe SDK | HTTP API |

| 退款处理 | 自动化 | 半自动化 |

| 积分扣除 | ✅ 自动扣除 | ❌ 未处理 |

| 状态更新 | ✅ 自动更新为「已退款」 | ❌ 未自动更新 |

| 日志记录 | ✅ 完整的操作日志 | ⚠️ 仅 API 响应 |

| 错误处理 | ✅ 完整 | ✅ 完整 |

| 退款原因 | 'requested_by_customer' | 可自定义 |


代码实现

订单状态定义


订单状态:

1 - 待付款

2 - 待确认 (✅ 可退款)

3 - 待注册 (✅ 可退款)

4 - 已完成

5 - 已取消

6 - 已下载

7 - 申请退款

8 - 已退款

支付方式定义


支付方式:

0 - 未知

1 - 微信

2 - 支付宝

3 - 银行卡

4 - 积分兑换

5 - PayPal余额

6 - PayPal信用卡

7 - Stripe ✅ 支持退款

8 - 空中云汇 ✅ 支持退款


注意事项

⚠️ Airwallex 退款的问题

问题 1:订单状态未自动更新

当前行为

  • Airwallex 退款成功后,订单状态仍停留在 申请退款(7)

  • 需要手动更新或通过 Webhook 处理

建议解决方案


// 在 processAirwallexRefund 成功后添加

if (refundResult && userId && orderId) {

  // 扣除赠送积分

  const bonusPointsRecord = await collection('web_points').findOne({

    userId: new ObjectId(userId),

    out_trade_no: orderNum,

    source: 4

  });

  

  if (bonusPointsRecord) {

    await collection('web_user').updateOne(

      { _id: new ObjectId(userId) },

      { $inc: { points: -bonusPointsRecord.points } }

    );

  }

  

  // 更新订单状态为已退款

  await collection("web_order").updateOne(

    { _id: new ObjectId(orderId) },

    {

      $set: {

        status: 8,

        status_8_time: Date.now()

      }

    }

  );

}

问题 2:未处理赠送积分

当前行为

  • Airwallex 退款成功,但未扣除赠送的积分

建议解决方案

  • 在退款成功后查找并扣除对应的赠送积分

  • 记录积分扣除日志

✅ Stripe 退款的优点

  • 自动化处理所有步骤

  • 完整的数据一致性

  • 详细的日志记录

🔄 积分退款(pay_type = 4)


case 4: // 积分

    // 积分退款 - 直接退还积分给用户

    await collection("web_user").updateOne(

        { _id: new ObjectId(userId) },

        { $inc: { points: order.price_total } }

    );

    refundResult = { status: 'succeeded', amount: order.price_total };

    break;


总结

Stripe 退款

  • 成熟完善:全自动处理,无需额外操作

  • 功能完整:积分扣除、状态更新、日志记录一应俱全

  • 推荐使用:适合需要自动化处理的场景

Airwallex 退款

  • ⚠️ 需要改进:目前只完成 API 调用,缺少数据库操作

  • ⚠️ 待完善:需要手动处理订单状态和积分扣除

  • 🔧 建议:参考 Stripe 的实现,添加完整的数据库操作逻辑


相关文件

核心文件

  • src/app/api/order/refund/route.ts - 退款 API 入口

  • src/utils/stripe.ts - Stripe 退款处理

  • src/utils/airwallex.ts - Airwallex 退款处理

  • src/DB/web_order.ts - 订单数据模型

相关页面

  • src/app/user/order/modal/refund.tsx - 退款申请界面

  • src/app/user/order/page.tsx - 订单管理页面


最后更新:2025年1月


本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。

萧兮的博客https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/ba602082-0f99-4b10-b8e4-b082979c5308