Stripe 和 Airwallex(空中云汇)退款机制说明
Stripe 和 Airwallex(空中云汇)退款机制说明
📋 目录
退款流程概览
统一入口
所有退款请求通过 src/app/api/order/refund/route.ts 处理
退款条件
-
✅ 订单状态限制:只有
待确认(2)和待注册(3)状态的订单可以申请退款 -
✅ 生成退款单号:系统自动生成唯一的退款单号
-
✅ 状态更新:订单状态更新为
申请退款(7)
支持退款方式
-
Stripe (pay_type = 7)
-
Airwallex (pay_type = 8)
-
积分支付 (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