微信小程序支付功能打通
This commit is contained in:
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() &&
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user