feat(trade): WeChat trade-manage push + delay shipping upload
Receive trade_manage_order_settlement to sync confirm-receive/settle; wait ~65s and retry on 10060001 before upload_shipping_info. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { buildOrderStatusEvent } from '../../common/event/event.helpers';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { WechatOrderShippingService } from './wechat-order-shipping.service';
|
||||
import type { WechatTradeManageEvent } from './wechat-msg-crypto.util';
|
||||
|
||||
/**
|
||||
* 小程序发货信息管理相关消息推送处理。
|
||||
* @see https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
|
||||
*/
|
||||
@Injectable()
|
||||
export class WechatTradeManageService {
|
||||
private readonly logger = new Logger(WechatTradeManageService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly wechatOrderShipping: WechatOrderShippingService,
|
||||
) {}
|
||||
|
||||
async handleEvent(evt: WechatTradeManageEvent): Promise<{ handled: string }> {
|
||||
switch (evt.event) {
|
||||
case 'trade_manage_order_settlement':
|
||||
return this.handleOrderSettlement(evt);
|
||||
case 'trade_manage_remind_shipping':
|
||||
return this.handleRemindShipping(evt);
|
||||
case 'trade_manage_remind_access_api':
|
||||
case 'wxa_trade_controlled':
|
||||
await this.logEvent(evt, null, 'SUCCESS', evt.msg || evt.event);
|
||||
return { handled: evt.event };
|
||||
default:
|
||||
await this.logEvent(evt, null, 'SUCCESS', `ignored:${evt.event || 'empty'}`);
|
||||
return { handled: 'ignored' };
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrderSettlement(evt: WechatTradeManageEvent) {
|
||||
const order = await this.findOrder(evt);
|
||||
const isConfirmOrSettle =
|
||||
evt.confirmReceiveTime != null || evt.settlementTime != null || evt.confirmReceiveMethod != null;
|
||||
|
||||
if (!isConfirmOrSettle) {
|
||||
// 发货时推送:仅有 shipped_time / estimated_settlement_time
|
||||
await this.logEvent(evt, order?.id ?? null, 'SUCCESS', 'shipped_notify');
|
||||
return { handled: 'settlement_shipped_notify' };
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
await this.logEvent(evt, null, 'FAILED', 'ORDER_NOT_FOUND');
|
||||
this.logger.warn(
|
||||
`trade_manage_order_settlement order not found tradeNo=${evt.merchantTradeNo} tx=${evt.transactionId}`,
|
||||
);
|
||||
return { handled: 'settlement_order_missing' };
|
||||
}
|
||||
|
||||
if (order.status === 'COMPLETED') {
|
||||
await this.logEvent(evt, order.id, 'SUCCESS', 'already_completed');
|
||||
return { handled: 'settlement_already_completed' };
|
||||
}
|
||||
|
||||
if (order.payStatus !== 'PAID') {
|
||||
await this.logEvent(evt, order.id, 'FAILED', 'NOT_PAID');
|
||||
return { handled: 'settlement_not_paid' };
|
||||
}
|
||||
|
||||
const methodLabel =
|
||||
evt.confirmReceiveMethod === 2
|
||||
? '微信自动确认收货'
|
||||
: evt.confirmReceiveMethod === 1
|
||||
? '微信手动确认收货'
|
||||
: '微信订单结算';
|
||||
|
||||
const now = new Date();
|
||||
const remark = [
|
||||
methodLabel,
|
||||
evt.confirmReceiveTime ? `confirmAt=${evt.confirmReceiveTime}` : null,
|
||||
evt.settlementTime ? `settleAt=${evt.settlementTime}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ')
|
||||
.slice(0, 512);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
completedAt: order.completedAt ?? now,
|
||||
},
|
||||
});
|
||||
await tx.orderDelivery.updateMany({
|
||||
where: { orderId: order.id, deliveredAt: null },
|
||||
data: { deliveredAt: now },
|
||||
});
|
||||
await tx.commonEvent.create({
|
||||
data: buildOrderStatusEvent({
|
||||
orderId: order.id,
|
||||
fromStatus: order.status,
|
||||
toStatus: 'COMPLETED',
|
||||
operator: 'WECHAT_TRADE_MANAGE',
|
||||
remark,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await this.logEvent(evt, order.id, 'SUCCESS', methodLabel);
|
||||
this.logger.log(
|
||||
`WeChat confirm/settle → COMPLETED order=${order.orderNo} method=${evt.confirmReceiveMethod ?? '-'}`,
|
||||
);
|
||||
return { handled: 'settlement_completed' };
|
||||
}
|
||||
|
||||
private async handleRemindShipping(evt: WechatTradeManageEvent) {
|
||||
const order = await this.findOrder(evt);
|
||||
await this.logEvent(evt, order?.id ?? null, order ? 'SUCCESS' : 'FAILED', evt.msg || 'remind_shipping');
|
||||
if (order) {
|
||||
this.wechatOrderShipping.uploadForOrderSafe(order.id);
|
||||
}
|
||||
return { handled: 'remind_shipping' };
|
||||
}
|
||||
|
||||
private async findOrder(evt: WechatTradeManageEvent) {
|
||||
if (evt.merchantTradeNo) {
|
||||
const byNo = await this.prisma.order.findUnique({ where: { orderNo: evt.merchantTradeNo } });
|
||||
if (byNo) return byNo;
|
||||
}
|
||||
if (evt.transactionId) {
|
||||
return this.prisma.order.findFirst({
|
||||
where: { payExternalNo: evt.transactionId },
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async logEvent(
|
||||
evt: WechatTradeManageEvent,
|
||||
orderId: bigint | null,
|
||||
status: 'SUCCESS' | 'FAILED',
|
||||
note: string,
|
||||
) {
|
||||
await this.prisma.logThirdParty.create({
|
||||
data: {
|
||||
provider: 'WECHAT_PAY',
|
||||
scene: 'TRADE_MANAGE_PUSH',
|
||||
refType: orderId ? 'ORDER' : 'SYSTEM',
|
||||
refId: orderId ?? undefined,
|
||||
requestUrl: 'callbacks/wechat/message',
|
||||
requestBody: {
|
||||
event: evt.event,
|
||||
merchantTradeNo: evt.merchantTradeNo,
|
||||
transactionId: evt.transactionId,
|
||||
confirmReceiveMethod: evt.confirmReceiveMethod,
|
||||
confirmReceiveTime: evt.confirmReceiveTime,
|
||||
settlementTime: evt.settlementTime,
|
||||
shippedTime: evt.shippedTime,
|
||||
estimatedSettlementTime: evt.estimatedSettlementTime,
|
||||
msg: evt.msg,
|
||||
},
|
||||
responseBody: { note },
|
||||
externalNo: evt.transactionId || evt.merchantTradeNo,
|
||||
status,
|
||||
errorMessage: status === 'FAILED' ? note.slice(0, 512) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user