微信小程序支付功能打通

This commit is contained in:
2026-07-13 00:07:17 +08:00
parent f2cdc0222d
commit 450dc44d0e
14 changed files with 73 additions and 28 deletions
+3 -1
View File
@@ -49,7 +49,9 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
}); });
if (result.mode === 'jsapi' && result.prepay) { if (result.mode === 'jsapi' && result.prepay) {
await invokeWechatPay(result.prepay as WechatJsapiPrepayParams); await invokeWechatPay(result.prepay as WechatJsapiPrepayParams, {
platform: isMiniWechatEnv() ? 'mini' : undefined,
});
const paid = await waitOrderPaid(orderId); const paid = await waitOrderPaid(orderId);
return paid ? 'paid' : 'pending'; return paid ? 'paid' : 'pending';
} }
+4
View File
@@ -100,6 +100,10 @@ export default function PayPage() {
return; return;
} }
const message = e instanceof Error ? e.message : '支付失败'; const message = e instanceof Error ? e.message : '支付失败';
if (message.includes('取消')) {
setMsg('已取消支付');
return;
}
setMsg(message); setMsg(message);
toast(message); toast(message);
} finally { } finally {
+2 -1
View File
@@ -60,8 +60,9 @@ export function hasWechatAuthCredentials(env?: Record<string, string | undefined
export function hasWechatPayCredentials(env?: Record<string, string | undefined>): boolean { export function hasWechatPayCredentials(env?: Record<string, string | undefined>): boolean {
const e = readEnv(env); const e = readEnv(env);
const appId = e.WX_MINI_APP_ID?.trim() || e.WX_APP_ID?.trim();
return !!( return !!(
e.WX_APP_ID?.trim() && appId &&
e.WX_MCH_ID?.trim() && e.WX_MCH_ID?.trim() &&
e.WX_MCH_SERIAL_NO?.trim() && e.WX_MCH_SERIAL_NO?.trim() &&
e.WX_MCH_PRIVATE_KEY?.trim() && e.WX_MCH_PRIVATE_KEY?.trim() &&
+2
View File
@@ -18,6 +18,8 @@ export function isWechatBrowser(): boolean {
/** 是否微信小程序 web-view 或独立小程序环境 */ /** 是否微信小程序 web-view 或独立小程序环境 */
export function isMiniProgram(): boolean { export function isMiniProgram(): boolean {
const g = globalThis as typeof globalThis & { wx?: { requestPayment?: unknown } };
if (g.wx?.requestPayment) return true;
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
return ua.includes('miniprogram') || (window as Window & { __wxjs_environment?: string }).__wxjs_environment === 'miniprogram'; return ua.includes('miniprogram') || (window as Window & { __wxjs_environment?: string }).__wxjs_environment === 'miniprogram';
+35 -13
View File
@@ -1,11 +1,14 @@
import type { WechatJsapiPrepayParams } from '@dukang/shared-types'; import type { WechatJsapiPrepayParams } from '@dukang/shared-types';
import { getRuntimePlatform } from './env'; import { getRuntimePlatform } from './env';
import { ensureJssdkReady } from './jssdk'; import { ensureJssdkReady } from './jssdk';
import type { MiniProgramWx } from './types';
export type WechatPayInvokeConfig = { export type WechatPayInvokeConfig = {
apiBase?: string; apiBase?: string;
clientApp?: string; clientApp?: string;
getAccessToken?: () => string | null; getAccessToken?: () => string | null;
/** 显式指定运行端;Taro weapp 须传 mini */
platform?: 'mini' | 'wechat-h5' | 'browser';
}; };
/** wx.chooseWXPay 要求 timestamp 小写;服务端签名与 Bridge 使用 timeStamp */ /** wx.chooseWXPay 要求 timestamp 小写;服务端签名与 Bridge 使用 timeStamp */
@@ -19,6 +22,13 @@ function toChooseWxPayOptions(prepay: WechatJsapiPrepayParams) {
}; };
} }
function getMiniProgramWx(): MiniProgramWx | undefined {
const g = globalThis as typeof globalThis & { wx?: MiniProgramWx };
if (g.wx?.requestPayment) return g.wx;
if (typeof window !== 'undefined' && window.wx?.requestPayment) return window.wx;
return undefined;
}
function invokeBridgePay(params: WechatJsapiPrepayParams): Promise<void> { function invokeBridgePay(params: WechatJsapiPrepayParams): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const bridge = window.WeixinJSBridge; const bridge = window.WeixinJSBridge;
@@ -55,25 +65,37 @@ async function waitForWeixinBridge(): Promise<void> {
}); });
} }
function invokeMiniProgramPay(prepay: WechatJsapiPrepayParams): Promise<void> {
const wxApi = getMiniProgramWx();
if (!wxApi?.requestPayment) {
return Promise.reject(new Error('当前环境不支持微信支付'));
}
return new Promise((resolve, reject) => {
wxApi.requestPayment!({
timeStamp: prepay.timeStamp,
nonceStr: prepay.nonceStr,
package: prepay.package,
signType: prepay.signType,
paySign: prepay.paySign,
success: () => resolve(),
fail: (err) => {
const msg = err.errMsg || '支付失败';
if (msg.includes('cancel')) reject(new Error('用户取消支付'));
else reject(new Error(msg));
},
});
});
}
/** 调起微信支付 */ /** 调起微信支付 */
export async function invokeWechatPay( export async function invokeWechatPay(
prepay: WechatJsapiPrepayParams, prepay: WechatJsapiPrepayParams,
config?: WechatPayInvokeConfig, config?: WechatPayInvokeConfig,
): Promise<void> { ): Promise<void> {
const platform = getRuntimePlatform(); const platform = config?.platform ?? getRuntimePlatform();
if (platform === 'mini' && window.wx?.requestPayment) { if (platform === 'mini') {
return new Promise((resolve, reject) => { return invokeMiniProgramPay(prepay);
window.wx!.requestPayment!({
timeStamp: prepay.timeStamp,
nonceStr: prepay.nonceStr,
package: prepay.package,
signType: prepay.signType,
paySign: prepay.paySign,
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '支付失败')),
});
});
} }
if (platform === 'wechat-h5') { if (platform === 'wechat-h5') {
@@ -5,5 +5,5 @@ export type PayOrderResult =
| { mode: 'jsapi'; prepay: WechatJsapiPrepayParams }; | { mode: 'jsapi'; prepay: WechatJsapiPrepayParams };
export interface IPayProvider { export interface IPayProvider {
payOrder(orderId: bigint, openId?: string): Promise<PayOrderResult>; payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise<PayOrderResult>;
} }
@@ -4,7 +4,7 @@ import type { IPayProvider, PayOrderResult } from './pay.interface';
@Injectable() @Injectable()
export class PayMockProvider implements IPayProvider { export class PayMockProvider implements IPayProvider {
async payOrder(_orderId: bigint, _openId?: string): Promise<PayOrderResult> { async payOrder(_orderId: bigint, _openId?: string, _platform?: 'h5' | 'mini'): Promise<PayOrderResult> {
if (!loadAppConfig().mockPay) { if (!loadAppConfig().mockPay) {
throw new Error('Real WeChat pay requires PayWechatProvider'); throw new Error('Real WeChat pay requires PayWechatProvider');
} }
@@ -16,7 +16,7 @@ export class PayRouterProvider implements IPayProvider {
return loadAppConfig().mockPay ? this.mock : this.wechat; return loadAppConfig().mockPay ? this.mock : this.wechat;
} }
payOrder(orderId: bigint, openId?: string): Promise<PayOrderResult> { payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise<PayOrderResult> {
return this.resolve().payOrder(orderId, openId); return this.resolve().payOrder(orderId, openId, platform);
} }
} }
@@ -14,7 +14,7 @@ export class PayWechatProvider implements IPayProvider {
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider, @Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
) {} ) {}
async payOrder(orderId: bigint, openId?: string): Promise<PayOrderResult> { async payOrder(orderId: bigint, openId?: string, platform: 'h5' | 'mini' = 'h5'): Promise<PayOrderResult> {
if (loadAppConfig().mockPay) { if (loadAppConfig().mockPay) {
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` }; return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
} }
@@ -29,13 +29,14 @@ export class PayWechatProvider implements IPayProvider {
if (!order) throw new Error('订单不存在'); if (!order) throw new Error('订单不存在');
const amountFen = Math.round(Number(order.payAmount) * 100); const amountFen = Math.round(Number(order.payAmount) * 100);
this.logger.log(`create JSAPI prepay order=${order.orderNo} mchid=${this.wechat.getMchId()}`); this.logger.log(`create JSAPI prepay order=${order.orderNo} mchid=${this.wechat.getMchId()} platform=${platform}`);
const prepay = await this.wechat.createJsapiPrepay({ const prepay = await this.wechat.createJsapiPrepay({
orderNo: order.orderNo, orderNo: order.orderNo,
description: `杜康好客订单 ${order.orderNo}`, description: `杜康好客订单 ${order.orderNo}`,
amountFen, amountFen,
openId, openId,
notifyUrl: process.env.WX_PAY_NOTIFY_URL ?? '', notifyUrl: process.env.WX_PAY_NOTIFY_URL ?? '',
platform,
}); });
return { mode: 'jsapi', prepay }; return { mode: 'jsapi', prepay };
} }
@@ -50,7 +50,7 @@ export class WechatApiProvider implements IWechatProvider {
const cfg = loadAppConfig(); const cfg = loadAppConfig();
return ( return (
!cfg.mockPay && !cfg.mockPay &&
!!this.appId && !!(this.miniAppId || this.appId) &&
!!this.mchId && !!this.mchId &&
!!this.mchSerialNo && !!this.mchSerialNo &&
!!this.mchPrivateKey && !!this.mchPrivateKey &&
@@ -288,6 +288,7 @@ export class WechatApiProvider implements IWechatProvider {
amountFen: number; amountFen: number;
openId: string; openId: string;
notifyUrl: string; notifyUrl: string;
platform?: 'h5' | 'mini';
}) { }) {
if (!this.isPayEnabled()) { if (!this.isPayEnabled()) {
throw new InternalServerErrorException( throw new InternalServerErrorException(
@@ -298,8 +299,17 @@ export class WechatApiProvider implements IWechatProvider {
if (!notifyUrl) { if (!notifyUrl) {
throw new InternalServerErrorException('请配置 WX_PAY_NOTIFY_URL'); throw new InternalServerErrorException('请配置 WX_PAY_NOTIFY_URL');
} }
const platform = params.platform ?? 'h5';
const payAppId = platform === 'mini' ? this.miniAppId || this.appId : this.appId || this.miniAppId;
if (!payAppId) {
throw new InternalServerErrorException(
platform === 'mini'
? '小程序支付未配置:请设置 WX_MINI_APP_ID'
: '微信支付未配置:请设置 WX_APP_ID',
);
}
const body = { const body = {
appid: this.miniAppId || this.appId, appid: payAppId,
mchid: this.mchId, mchid: this.mchId,
description: params.description, description: params.description,
out_trade_no: params.orderNo, out_trade_no: params.orderNo,
@@ -329,13 +339,13 @@ export class WechatApiProvider implements IWechatProvider {
const timeStamp = String(Math.floor(Date.now() / 1000)); const timeStamp = String(Math.floor(Date.now() / 1000));
const nonceStr = randomUUID().replace(/-/g, ''); const nonceStr = randomUUID().replace(/-/g, '');
const packageStr = `prepay_id=${res.prepay_id}`; const packageStr = `prepay_id=${res.prepay_id}`;
const message = `${this.appId}\n${timeStamp}\n${nonceStr}\n${packageStr}\n`; const message = `${payAppId}\n${timeStamp}\n${nonceStr}\n${packageStr}\n`;
const sign = createSign('RSA-SHA256'); const sign = createSign('RSA-SHA256');
sign.update(message); sign.update(message);
sign.end(); sign.end();
const paySign = sign.sign(this.mchPrivateKey, 'base64'); const paySign = sign.sign(this.mchPrivateKey, 'base64');
return { return {
appId: this.appId, appId: payAppId,
timeStamp, timeStamp,
nonceStr, nonceStr,
package: packageStr, package: packageStr,
@@ -73,6 +73,7 @@ export interface IWechatProvider {
amountFen: number; amountFen: number;
openId: string; openId: string;
notifyUrl: string; notifyUrl: string;
platform?: 'h5' | 'mini';
}): Promise<WechatJsapiPrepayParams>; }): Promise<WechatJsapiPrepayParams>;
/** 解析并验签支付回调通知 */ /** 解析并验签支付回调通知 */
@@ -75,6 +75,7 @@ export class WechatRouterProvider implements IWechatProvider {
amountFen: number; amountFen: number;
openId: string; openId: string;
notifyUrl: string; notifyUrl: string;
platform?: 'h5' | 'mini';
}) { }) {
return this.resolve().createJsapiPrepay(params); return this.resolve().createJsapiPrepay(params);
} }
@@ -45,7 +45,7 @@ export class TradeController {
@Post(':id/pay') @Post(':id/pay')
@UseGuards(PhoneVerifiedGuard) @UseGuards(PhoneVerifiedGuard)
pay(@CurrentUser() user: AuthUser, @Param('id') id: string) { pay(@CurrentUser() user: AuthUser, @Param('id') id: string) {
return this.tradeService.payOrder(user.actorId, BigInt(id)); return this.tradeService.payOrder(user.actorId, BigInt(id), user.clientApp);
} }
@Put(':id/address') @Put(':id/address')
@@ -199,7 +199,7 @@ export class TradeService {
return serializeBigInt(mapOrderCompat(order)); return serializeBigInt(mapOrderCompat(order));
} }
async payOrder(userId: bigint, orderId: bigint) { async payOrder(userId: bigint, orderId: bigint, clientApp?: ClientApp) {
const order = await this.prisma.order.findFirst({ const order = await this.prisma.order.findFirst({
where: { id: orderId, userId }, where: { id: orderId, userId },
}); });
@@ -214,7 +214,8 @@ export class TradeService {
if (!appConfig.mockPay && !openId) { if (!appConfig.mockPay && !openId) {
throw new BadRequestException(WECHAT_AUTH_REQUIRED); throw new BadRequestException(WECHAT_AUTH_REQUIRED);
} }
const payResult = await this.payProvider.payOrder(orderId, openId); const payPlatform = clientApp === ClientApp.USER_MINI ? 'mini' : 'h5';
const payResult = await this.payProvider.payOrder(orderId, openId, payPlatform);
if (payResult.mode === 'jsapi') { if (payResult.mode === 'jsapi') {
return { return {