Files
dukang/server/dukang-api/src/integrations/wechat/wechat-trade-manage.service.ts
T
jacy a0a6c14758 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>
2026-07-29 15:37:42 +08:00

167 lines
5.6 KiB
TypeScript

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,
},
});
}
}