微信JSSDK对接,代下单支付

This commit is contained in:
2026-08-02 11:49:35 +08:00
parent 50ba6e9c56
commit 09e732bfa3
32 changed files with 1223 additions and 218 deletions
@@ -1,9 +1,17 @@
import type { WechatJsapiPrepayParams } from '@dukang/shared-types';
export type PayMethod = 'JSAPI' | 'NATIVE';
export type PayOrderResult =
| { mode: 'mock'; externalNo: string }
| { mode: 'jsapi'; prepay: WechatJsapiPrepayParams };
| { mode: 'jsapi'; prepay: WechatJsapiPrepayParams }
| { mode: 'native'; codeUrl: string; externalNo: string };
export interface IPayProvider {
payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise<PayOrderResult>;
payOrder(
orderId: bigint,
openId?: string,
platform?: 'h5' | 'mini',
payMethod?: PayMethod,
): Promise<PayOrderResult>;
}
@@ -1,13 +1,30 @@
import { Injectable } from '@nestjs/common';
import { loadAppConfig } from '@dukang/shared-types';
import type { IPayProvider, PayOrderResult } from './pay.interface';
import { PrismaService } from '../../common/prisma/prisma.module';
import type { IPayProvider, PayMethod, PayOrderResult } from './pay.interface';
@Injectable()
export class PayMockProvider implements IPayProvider {
async payOrder(_orderId: bigint, _openId?: string, _platform?: 'h5' | 'mini'): Promise<PayOrderResult> {
constructor(private readonly prisma: PrismaService) {}
async payOrder(
orderId: bigint,
_openId?: string,
_platform?: 'h5' | 'mini',
payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (!loadAppConfig().mockPay) {
throw new Error('Real WeChat pay requires PayWechatProvider');
}
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
}
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { loadAppConfig } from '@dukang/shared-types';
import type { IPayProvider, PayOrderResult } from './pay.interface';
import type { IPayProvider, PayMethod, PayOrderResult } from './pay.interface';
import { PayMockProvider } from './pay.mock.provider';
import { PayWechatProvider } from './pay.wechat.provider';
/** 按当前 process.env 动态选择 Mock / 微信 JSAPI 支付 */
/** 按当前 process.env 动态选择 Mock / 微信支付 */
@Injectable()
export class PayRouterProvider implements IPayProvider {
constructor(
@@ -16,7 +16,12 @@ export class PayRouterProvider implements IPayProvider {
return loadAppConfig().mockPay ? this.mock : this.wechat;
}
payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise<PayOrderResult> {
return this.resolve().payOrder(orderId, openId, platform);
payOrder(
orderId: bigint,
openId?: string,
platform?: 'h5' | 'mini',
payMethod?: PayMethod,
): Promise<PayOrderResult> {
return this.resolve().payOrder(orderId, openId, platform, payMethod);
}
}
@@ -3,7 +3,7 @@ import { loadAppConfig } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { WECHAT_PROVIDER } from '../integrations.constants';
import type { IWechatProvider } from '../wechat/wechat.interface';
import type { IPayProvider, PayOrderResult } from './pay.interface';
import type { IPayProvider, PayMethod, PayOrderResult } from './pay.interface';
@Injectable()
export class PayWechatProvider implements IPayProvider {
@@ -14,28 +14,60 @@ export class PayWechatProvider implements IPayProvider {
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
) {}
async payOrder(orderId: bigint, openId?: string, platform: 'h5' | 'mini' = 'h5'): Promise<PayOrderResult> {
async payOrder(
orderId: bigint,
openId?: string,
platform: 'h5' | 'mini' = 'h5',
payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (loadAppConfig().mockPay) {
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
if (!this.wechat.isPayEnabled()) {
throw new Error('微信支付未配置:请关闭 MOCK_PAY 并配置 WX_MCH_ID 等商户参数');
}
if (!openId) {
throw new Error('微信支付需要用户 openId,请先完成微信授权登录');
}
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
if (!order) throw new Error('订单不存在');
const amountFen = Math.round(Number(order.payAmount) * 100);
const notifyUrl = process.env.WX_PAY_NOTIFY_URL ?? '';
if (payMethod === 'NATIVE') {
this.logger.log(`create NATIVE prepay order=${order.orderNo} mchid=${this.wechat.getMchId()}`);
const { codeUrl } = await this.wechat.createNativePrepay({
orderNo: order.orderNo,
description: `杜康好客订单 ${order.orderNo}`,
amountFen,
notifyUrl,
});
return {
mode: 'native',
codeUrl,
externalNo: `NATIVE-${order.orderNo}`,
};
}
if (!openId) {
throw new Error('微信支付需要用户 openId,请先完成微信授权登录');
}
this.logger.log(`create JSAPI prepay order=${order.orderNo} mchid=${this.wechat.getMchId()} platform=${platform}`);
const prepay = await this.wechat.createJsapiPrepay({
orderNo: order.orderNo,
description: `杜康好客订单 ${order.orderNo}`,
amountFen,
openId,
notifyUrl: process.env.WX_PAY_NOTIFY_URL ?? '',
notifyUrl,
platform,
});
return { mode: 'jsapi', prepay };
@@ -553,6 +553,55 @@ export class WechatApiProvider implements IWechatProvider {
};
}
async createNativePrepay(params: {
orderNo: string;
description: string;
amountFen: number;
notifyUrl: string;
}) {
if (!this.isPayEnabled()) {
throw new InternalServerErrorException(
'微信支付未配置:请关闭 MOCK_PAY 并配置 WX_MCH_ID、WX_MCH_SERIAL_NO、WX_MCH_PRIVATE_KEY、WX_API_V3_KEY',
);
}
const notifyUrl = params.notifyUrl || this.notifyUrl;
if (!notifyUrl) {
throw new InternalServerErrorException('请配置 WX_PAY_NOTIFY_URL');
}
const payAppId = this.appId || this.miniAppId;
if (!payAppId) {
throw new InternalServerErrorException('微信支付未配置:请设置 WX_APP_ID');
}
const body = {
appid: payAppId,
mchid: this.mchId,
description: params.description,
out_trade_no: params.orderNo,
notify_url: notifyUrl,
amount: { total: params.amountFen, currency: 'CNY' },
};
const path = '/v3/pay/transactions/native';
const payload = JSON.stringify(body);
const auth = this.signPayRequest('POST', path, payload);
const res = await this.fetchPayJson<{ code_url?: string }>(
`https://api.mch.weixin.qq.com${path}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: auth,
},
body: payload,
},
);
if (!res.code_url) {
throw new InternalServerErrorException('微信 Native 下单失败');
}
this.logger.log(`NATIVE prepay ok mchid=${this.mchId} orderNo=${params.orderNo}`);
return { codeUrl: res.code_url };
}
async parsePayNotification(
headers: Record<string, string | string[] | undefined>,
rawBody: string,
@@ -51,6 +51,10 @@ export class WechatDisabledProvider implements IWechatProvider {
return this.disabled();
}
createNativePrepay() {
return this.disabled();
}
parsePayNotification() {
return this.disabled();
}
@@ -132,6 +132,14 @@ export interface IWechatProvider {
platform?: 'h5' | 'mini';
}): Promise<WechatJsapiPrepayParams>;
/** 创建 Native 扫码支付 code_url */
createNativePrepay(params: {
orderNo: string;
description: string;
amountFen: number;
notifyUrl: string;
}): Promise<{ codeUrl: string }>;
/** 解析并验签支付回调通知 */
parsePayNotification(
headers: Record<string, string | string[] | undefined>,
@@ -80,6 +80,10 @@ export class WechatMockProvider implements IWechatProvider {
throw new NotImplementedException('FEATURE_DISABLED');
}
createNativePrepay(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
parsePayNotification(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
@@ -80,6 +80,15 @@ export class WechatRouterProvider implements IWechatProvider {
return this.resolve().createJsapiPrepay(params);
}
createNativePrepay(params: {
orderNo: string;
description: string;
amountFen: number;
notifyUrl: string;
}) {
return this.resolve().createNativePrepay(params);
}
parsePayNotification(
headers: Record<string, string | string[] | undefined>,
rawBody: string,