feat(trade): connect WeChat refund API and callback flow

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 21:45:33 +08:00
parent e93e4b8f84
commit ab10431001
20 changed files with 455 additions and 61 deletions
@@ -529,6 +529,177 @@ export class TradeService {
return { orderId: order.id.toString(), alreadyPaid: false };
}
/** 总部审批退款 / 协同取回后退款:发起微信原路退(Mock 同步完成) */
async initiateRefund(
orderId: bigint,
ticketId: bigint,
remark: string,
actorType = 'HQ',
) {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
if (!order) throw new NotFoundException('订单不存在');
if (order.payStatus === 'REFUNDED' || order.status === 'REFUNDED') {
return { orderId: order.id.toString(), alreadyRefunded: true };
}
if (order.payStatus === 'REFUNDING' || order.status === 'REFUNDING') {
throw new BadRequestException('订单已在退款处理中');
}
if (order.payStatus !== 'PAID') {
throw new BadRequestException('订单未支付,无法退款');
}
const outRefundNo = `RF-${order.orderNo}-${ticketId}`;
const pendingLog = await this.prisma.logThirdParty.findFirst({
where: {
provider: 'WECHAT_REFUND',
externalNo: outRefundNo,
status: { in: ['PENDING', 'SUCCESS'] },
},
});
if (pendingLog?.status === 'SUCCESS') {
return { orderId: order.id.toString(), alreadyRefunded: true };
}
const fromStatus = order.status;
await this.prisma.order.update({
where: { id: orderId },
data: { status: 'REFUNDING', payStatus: 'REFUNDING' },
});
await this.prisma.commonEvent.create({
data: buildOrderStatusEvent({
orderId,
fromStatus,
toStatus: 'REFUNDING',
operator: actorType,
remark,
}),
});
let refundResult;
try {
refundResult = await this.payProvider.refundOrder(orderId, outRefundNo, remark);
} catch (err) {
this.alert.notify({
level: 'P0',
category: 'pay',
title: '微信退款发起失败',
detail: `订单 ${order.orderNo}\n${err instanceof Error ? err.message : String(err)}`,
dedupeKey: `refund_init_fail|${order.orderNo}`,
dedupeTtlSec: 120,
});
throw err;
}
if (refundResult.mode === 'mock' || refundResult.status === 'SUCCESS') {
await this.handleRefundSuccess({
outRefundNo,
refundId: refundResult.mode === 'wechat' ? refundResult.refundId : `MOCK-REF-${outRefundNo}`,
amountFen: Math.round(Number(order.payAmount) * 100),
remark,
actorType,
});
return { orderId: order.id.toString(), alreadyRefunded: false, completed: true };
}
await this.prisma.logThirdParty.create({
data: {
provider: 'WECHAT_REFUND',
scene: 'ORDER_REFUND',
refType: 'ORDER',
refId: orderId,
externalNo: refundResult.refundId ?? outRefundNo,
status: 'PENDING',
amount: order.payAmount,
},
});
return { orderId: order.id.toString(), alreadyRefunded: false, completed: false, outRefundNo };
}
/** 微信退款回调 / Mock 同步:幂等更新订单为已退款并作废权益 */
async handleRefundSuccess(params: {
outRefundNo: string;
refundId: string;
amountFen: number;
remark?: string;
actorType?: string;
}) {
const existingLog = await this.prisma.logThirdParty.findFirst({
where: {
provider: 'WECHAT_REFUND',
externalNo: params.refundId,
status: 'SUCCESS',
},
});
if (existingLog) {
return {
orderId: existingLog.refId?.toString() ?? '',
alreadyRefunded: true,
};
}
const match = params.outRefundNo.match(/^RF-(.+)-(\d+)$/);
if (!match) {
throw new BadRequestException('退款单号格式无效');
}
const [, orderNo] = match;
const order = await this.prisma.order.findUnique({ where: { orderNo } });
if (!order) throw new NotFoundException('订单不存在');
if (order.payStatus === 'REFUNDED') {
return { orderId: order.id.toString(), alreadyRefunded: true };
}
const expectedFen = Math.round(Number(order.payAmount) * 100);
if (params.amountFen > 0 && params.amountFen !== expectedFen) {
this.alert.notify({
level: 'P0',
category: 'pay',
title: '退款金额不一致',
detail: `订单 ${order.orderNo}\n期望 ${expectedFen} 分,回调 ${params.amountFen}`,
dedupeKey: `refund_amount_mismatch|${order.orderNo}`,
});
throw new BadRequestException('退款金额与订单不符');
}
const fromStatus = order.status;
const operator = params.actorType ?? 'WECHAT_REFUND';
await this.prisma.$transaction(async (tx) => {
const current = await tx.order.findUnique({ where: { id: order.id } });
if (!current || current.payStatus === 'REFUNDED') return;
await tx.order.update({
where: { id: order.id },
data: { status: 'REFUNDED', payStatus: 'REFUNDED' },
});
await tx.logThirdParty.create({
data: {
provider: 'WECHAT_REFUND',
scene: 'ORDER_REFUND',
refType: 'ORDER',
refId: order.id,
externalNo: params.refundId,
status: 'SUCCESS',
amount: order.payAmount,
},
});
await tx.commonEvent.create({
data: buildOrderStatusEvent({
orderId: order.id,
fromStatus,
toStatus: 'REFUNDED',
operator,
remark: params.remark ?? '退款成功',
}),
});
});
await this.benefitService.voidCouponsOnRefund(order.id);
return { orderId: order.id.toString(), alreadyRefunded: false };
}
async listOrders(userId: bigint, tab = 'all', page = 1, pageSize = 20) {
const statuses = orderTabToStatuses(tab);
const where = {