Add WeChat in-app JSAPI recharge for g2.tkmind.cn.

Use JSAPI with bound openid inside WeChat instead of QR long-press, and include related auth redirect and admin UX fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-16 16:20:04 +08:00
parent bb70634a96
commit f818dd0307
15 changed files with 260 additions and 32 deletions
+74
View File
@@ -215,6 +215,33 @@ async function wechatV3Request(config, method, path, bodyObj) {
return data;
}
export function buildV2JsapiParams(config, prepayId) {
const timeStamp = String(Math.floor(Date.now() / 1000));
const nonceStr = randomNonce(16);
const pkg = `prepay_id=${prepayId}`;
const signType = 'MD5';
const paySign = signV2Params(
{
appId: config.appId,
timeStamp,
nonceStr,
package: pkg,
signType,
},
config.apiKey,
);
return { appId: config.appId, timeStamp, nonceStr, package: pkg, signType, paySign };
}
export function buildV3JsapiParams(config, prepayId) {
const timeStamp = String(Math.floor(Date.now() / 1000));
const nonceStr = randomNonce(16);
const pkg = `prepay_id=${prepayId}`;
const message = `${config.appId}\n${timeStamp}\n${nonceStr}\n${pkg}\n`;
const paySign = signMessage(message, config.privateKey);
return { appId: config.appId, timeStamp, nonceStr, package: pkg, signType: 'RSA', paySign };
}
function createV2Client(config) {
const createNativeOrder = async ({ outTradeNo, description, amountCents, clientIp }) => {
const data = await wechatV2Request(config, '/pay/unifiedorder', {
@@ -254,6 +281,25 @@ function createV2Client(config) {
return { h5Url: data.mweb_url };
};
const createJsapiOrder = async ({ outTradeNo, description, amountCents, clientIp, openid }) => {
if (!openid) {
throw new Error('JSAPI 支付缺少 openid');
}
const data = await wechatV2Request(config, '/pay/unifiedorder', {
body: description,
out_trade_no: outTradeNo,
total_fee: String(amountCents),
spbill_create_ip: clientIp || '127.0.0.1',
notify_url: config.notifyUrl,
trade_type: 'JSAPI',
openid,
});
if (!data.prepay_id) {
throw new Error('微信支付未返回 prepay_id');
}
return { jsapiParams: buildV2JsapiParams(config, data.prepay_id) };
};
const verifyNotify = ({ headers: _headers, body }) => {
const bodyText = typeof body === 'string' ? body : String(body ?? '');
const fields = parseXmlFields(bodyText);
@@ -279,8 +325,10 @@ function createV2Client(config) {
return {
enabled: true,
apiVersion: 'v2',
appId: config.appId,
createNativeOrder,
createH5Order,
createJsapiOrder,
verifyNotify,
};
}
@@ -319,6 +367,26 @@ function createV3Client(config) {
return { h5Url: data.h5_url };
};
const createJsapiOrder = async ({ outTradeNo, description, amountCents, clientIp, openid }) => {
if (!openid) {
throw new Error('JSAPI 支付缺少 openid');
}
const data = await wechatV3Request(config, 'POST', '/v3/pay/transactions/jsapi', {
appid: config.appId,
mchid: config.mchId,
description,
out_trade_no: outTradeNo,
notify_url: config.notifyUrl,
amount: { total: amountCents, currency: 'CNY' },
payer: { openid },
scene_info: clientIp ? { payer_client_ip: clientIp } : undefined,
});
if (!data.prepay_id) {
throw new Error('微信支付未返回 prepay_id');
}
return { jsapiParams: buildV3JsapiParams(config, data.prepay_id) };
};
const verifyNotify = ({ headers, body }) => {
const timestamp = headers['wechatpay-timestamp'] ?? headers['Wechatpay-Timestamp'];
const nonce = headers['wechatpay-nonce'] ?? headers['Wechatpay-Nonce'];
@@ -371,8 +439,10 @@ function createV3Client(config) {
return {
enabled: true,
apiVersion: 'v3',
appId: config.appId,
createNativeOrder,
createH5Order,
createJsapiOrder,
verifyNotify,
};
}
@@ -382,12 +452,16 @@ export function createWechatPayClient(config) {
return {
enabled: false,
apiVersion: config?.apiVersion ?? null,
appId: config?.appId ?? null,
async createNativeOrder() {
throw new Error('微信支付未配置');
},
async createH5Order() {
throw new Error('微信支付未配置');
},
async createJsapiOrder() {
throw new Error('微信支付未配置');
},
verifyNotify() {
throw new Error('微信支付未配置');
},