From 450dc44d0ed08add1b0f28d21ece2c85efa73023 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Mon, 13 Jul 2026 00:07:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E5=8A=9F=E8=83=BD=E6=89=93=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/mini-user/src/lib/pay-wechat.ts | 4 +- apps/mini-user/src/pages/pay/index.tsx | 4 ++ packages/shared-types/src/config.ts | 3 +- packages/weixin-sdk/src/env.ts | 2 + packages/weixin-sdk/src/pay.ts | 48 ++++++++++++++----- .../src/integrations/pay/pay.interface.ts | 2 +- .../src/integrations/pay/pay.mock.provider.ts | 2 +- .../integrations/pay/pay.router.provider.ts | 4 +- .../integrations/pay/pay.wechat.provider.ts | 5 +- .../wechat/wechat.api.provider.ts | 18 +++++-- .../integrations/wechat/wechat.interface.ts | 1 + .../wechat/wechat.router.provider.ts | 1 + .../src/modules/trade/trade.controller.ts | 2 +- .../src/modules/trade/trade.service.ts | 5 +- 14 files changed, 73 insertions(+), 28 deletions(-) diff --git a/apps/mini-user/src/lib/pay-wechat.ts b/apps/mini-user/src/lib/pay-wechat.ts index 2d5ad16..2e1c3c6 100644 --- a/apps/mini-user/src/lib/pay-wechat.ts +++ b/apps/mini-user/src/lib/pay-wechat.ts @@ -49,7 +49,9 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> { }); 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); return paid ? 'paid' : 'pending'; } diff --git a/apps/mini-user/src/pages/pay/index.tsx b/apps/mini-user/src/pages/pay/index.tsx index e328b91..7d9f506 100644 --- a/apps/mini-user/src/pages/pay/index.tsx +++ b/apps/mini-user/src/pages/pay/index.tsx @@ -100,6 +100,10 @@ export default function PayPage() { return; } const message = e instanceof Error ? e.message : '支付失败'; + if (message.includes('取消')) { + setMsg('已取消支付'); + return; + } setMsg(message); toast(message); } finally { diff --git a/packages/shared-types/src/config.ts b/packages/shared-types/src/config.ts index 15591ef..eba2d53 100644 --- a/packages/shared-types/src/config.ts +++ b/packages/shared-types/src/config.ts @@ -60,8 +60,9 @@ export function hasWechatAuthCredentials(env?: Record): boolean { const e = readEnv(env); + const appId = e.WX_MINI_APP_ID?.trim() || e.WX_APP_ID?.trim(); return !!( - e.WX_APP_ID?.trim() && + appId && e.WX_MCH_ID?.trim() && e.WX_MCH_SERIAL_NO?.trim() && e.WX_MCH_PRIVATE_KEY?.trim() && diff --git a/packages/weixin-sdk/src/env.ts b/packages/weixin-sdk/src/env.ts index 2544a15..e91ad84 100644 --- a/packages/weixin-sdk/src/env.ts +++ b/packages/weixin-sdk/src/env.ts @@ -18,6 +18,8 @@ export function isWechatBrowser(): boolean { /** 是否微信小程序 web-view 或独立小程序环境 */ 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; const ua = navigator.userAgent.toLowerCase(); return ua.includes('miniprogram') || (window as Window & { __wxjs_environment?: string }).__wxjs_environment === 'miniprogram'; diff --git a/packages/weixin-sdk/src/pay.ts b/packages/weixin-sdk/src/pay.ts index c600dfe..bd11963 100644 --- a/packages/weixin-sdk/src/pay.ts +++ b/packages/weixin-sdk/src/pay.ts @@ -1,11 +1,14 @@ import type { WechatJsapiPrepayParams } from '@dukang/shared-types'; import { getRuntimePlatform } from './env'; import { ensureJssdkReady } from './jssdk'; +import type { MiniProgramWx } from './types'; export type WechatPayInvokeConfig = { apiBase?: string; clientApp?: string; getAccessToken?: () => string | null; + /** 显式指定运行端;Taro weapp 须传 mini */ + platform?: 'mini' | 'wechat-h5' | 'browser'; }; /** 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 { return new Promise((resolve, reject) => { const bridge = window.WeixinJSBridge; @@ -55,25 +65,37 @@ async function waitForWeixinBridge(): Promise { }); } +function invokeMiniProgramPay(prepay: WechatJsapiPrepayParams): Promise { + 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( prepay: WechatJsapiPrepayParams, config?: WechatPayInvokeConfig, ): Promise { - const platform = getRuntimePlatform(); + const platform = config?.platform ?? getRuntimePlatform(); - if (platform === 'mini' && window.wx?.requestPayment) { - return new Promise((resolve, reject) => { - 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 === 'mini') { + return invokeMiniProgramPay(prepay); } if (platform === 'wechat-h5') { diff --git a/server/dukang-api/src/integrations/pay/pay.interface.ts b/server/dukang-api/src/integrations/pay/pay.interface.ts index ae5fb15..dd0f991 100644 --- a/server/dukang-api/src/integrations/pay/pay.interface.ts +++ b/server/dukang-api/src/integrations/pay/pay.interface.ts @@ -5,5 +5,5 @@ export type PayOrderResult = | { mode: 'jsapi'; prepay: WechatJsapiPrepayParams }; export interface IPayProvider { - payOrder(orderId: bigint, openId?: string): Promise; + payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise; } diff --git a/server/dukang-api/src/integrations/pay/pay.mock.provider.ts b/server/dukang-api/src/integrations/pay/pay.mock.provider.ts index c720360..4b00cd4 100644 --- a/server/dukang-api/src/integrations/pay/pay.mock.provider.ts +++ b/server/dukang-api/src/integrations/pay/pay.mock.provider.ts @@ -4,7 +4,7 @@ import type { IPayProvider, PayOrderResult } from './pay.interface'; @Injectable() export class PayMockProvider implements IPayProvider { - async payOrder(_orderId: bigint, _openId?: string): Promise { + async payOrder(_orderId: bigint, _openId?: string, _platform?: 'h5' | 'mini'): Promise { if (!loadAppConfig().mockPay) { throw new Error('Real WeChat pay requires PayWechatProvider'); } diff --git a/server/dukang-api/src/integrations/pay/pay.router.provider.ts b/server/dukang-api/src/integrations/pay/pay.router.provider.ts index 6494ec8..9c9d035 100644 --- a/server/dukang-api/src/integrations/pay/pay.router.provider.ts +++ b/server/dukang-api/src/integrations/pay/pay.router.provider.ts @@ -16,7 +16,7 @@ export class PayRouterProvider implements IPayProvider { return loadAppConfig().mockPay ? this.mock : this.wechat; } - payOrder(orderId: bigint, openId?: string): Promise { - return this.resolve().payOrder(orderId, openId); + payOrder(orderId: bigint, openId?: string, platform?: 'h5' | 'mini'): Promise { + return this.resolve().payOrder(orderId, openId, platform); } } diff --git a/server/dukang-api/src/integrations/pay/pay.wechat.provider.ts b/server/dukang-api/src/integrations/pay/pay.wechat.provider.ts index 4106e4e..3b44fcd 100644 --- a/server/dukang-api/src/integrations/pay/pay.wechat.provider.ts +++ b/server/dukang-api/src/integrations/pay/pay.wechat.provider.ts @@ -14,7 +14,7 @@ export class PayWechatProvider implements IPayProvider { @Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider, ) {} - async payOrder(orderId: bigint, openId?: string): Promise { + async payOrder(orderId: bigint, openId?: string, platform: 'h5' | 'mini' = 'h5'): Promise { if (loadAppConfig().mockPay) { return { mode: 'mock', externalNo: `MOCK-${Date.now()}` }; } @@ -29,13 +29,14 @@ export class PayWechatProvider implements IPayProvider { if (!order) throw new Error('订单不存在'); 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({ orderNo: order.orderNo, description: `杜康好客订单 ${order.orderNo}`, amountFen, openId, notifyUrl: process.env.WX_PAY_NOTIFY_URL ?? '', + platform, }); return { mode: 'jsapi', prepay }; } diff --git a/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts b/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts index 47ead6e..e2256c1 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts @@ -50,7 +50,7 @@ export class WechatApiProvider implements IWechatProvider { const cfg = loadAppConfig(); return ( !cfg.mockPay && - !!this.appId && + !!(this.miniAppId || this.appId) && !!this.mchId && !!this.mchSerialNo && !!this.mchPrivateKey && @@ -288,6 +288,7 @@ export class WechatApiProvider implements IWechatProvider { amountFen: number; openId: string; notifyUrl: string; + platform?: 'h5' | 'mini'; }) { if (!this.isPayEnabled()) { throw new InternalServerErrorException( @@ -298,8 +299,17 @@ export class WechatApiProvider implements IWechatProvider { if (!notifyUrl) { 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 = { - appid: this.miniAppId || this.appId, + appid: payAppId, mchid: this.mchId, description: params.description, out_trade_no: params.orderNo, @@ -329,13 +339,13 @@ export class WechatApiProvider implements IWechatProvider { const timeStamp = String(Math.floor(Date.now() / 1000)); const nonceStr = randomUUID().replace(/-/g, ''); 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'); sign.update(message); sign.end(); const paySign = sign.sign(this.mchPrivateKey, 'base64'); return { - appId: this.appId, + appId: payAppId, timeStamp, nonceStr, package: packageStr, diff --git a/server/dukang-api/src/integrations/wechat/wechat.interface.ts b/server/dukang-api/src/integrations/wechat/wechat.interface.ts index fb3fbd2..61052a9 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.interface.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.interface.ts @@ -73,6 +73,7 @@ export interface IWechatProvider { amountFen: number; openId: string; notifyUrl: string; + platform?: 'h5' | 'mini'; }): Promise; /** 解析并验签支付回调通知 */ diff --git a/server/dukang-api/src/integrations/wechat/wechat.router.provider.ts b/server/dukang-api/src/integrations/wechat/wechat.router.provider.ts index 5ec7a9b..b63c0c2 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.router.provider.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.router.provider.ts @@ -75,6 +75,7 @@ export class WechatRouterProvider implements IWechatProvider { amountFen: number; openId: string; notifyUrl: string; + platform?: 'h5' | 'mini'; }) { return this.resolve().createJsapiPrepay(params); } diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts index 5be90a1..05a0e11 100644 --- a/server/dukang-api/src/modules/trade/trade.controller.ts +++ b/server/dukang-api/src/modules/trade/trade.controller.ts @@ -45,7 +45,7 @@ export class TradeController { @Post(':id/pay') @UseGuards(PhoneVerifiedGuard) 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') diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index abf5b39..d68707e 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -199,7 +199,7 @@ export class TradeService { 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({ where: { id: orderId, userId }, }); @@ -214,7 +214,8 @@ export class TradeService { if (!appConfig.mockPay && !openId) { 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') { return {