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:
@@ -83,6 +83,10 @@ H5_ACCESS_PASSWORD=change-me
|
||||
# H5_WECHAT_PLATFORM_CERT="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
#
|
||||
# H5_WECHAT_NOTIFY_URL=https://goo.tkmind.cn/webhooks/wechat-pay/notify
|
||||
# 微信内 JSAPI 支付(服务号网页):
|
||||
# - 服务号「设置 → 功能设置 → JS接口安全域名」:g2.tkmind.cn
|
||||
# - 商户平台「产品中心 → 开发配置 → 支付授权目录」:https://g2.tkmind.cn/
|
||||
# - 用户须先用同一服务号微信 OAuth 登录(获取 openid)后再充值
|
||||
# 本地调试验签可临时开启(生产务必关闭):
|
||||
# H5_WECHAT_SKIP_NOTIFY_VERIFY=0
|
||||
|
||||
|
||||
+25
-2
@@ -141,12 +141,34 @@ export function createRechargeService(pool, { userAuth, wechatPay, config = load
|
||||
const outTradeNo = createOutTradeNo();
|
||||
const expireAt = now + config.orderTtlMs;
|
||||
const description = `TKMind账户充值¥${(amount / 100).toFixed(2)}`;
|
||||
const mode = payScene === 'h5' ? 'h5' : 'native';
|
||||
const mode =
|
||||
payScene === 'jsapi' ? 'jsapi' : payScene === 'h5' ? 'h5' : 'native';
|
||||
|
||||
let codeUrl = null;
|
||||
let h5Url = null;
|
||||
let jsapiParams = null;
|
||||
try {
|
||||
if (mode === 'h5') {
|
||||
if (mode === 'jsapi') {
|
||||
const appId = wechatPay.appId;
|
||||
if (!appId) {
|
||||
return { ok: false, message: '微信支付 AppID 未配置' };
|
||||
}
|
||||
const openid = await userAuth.getWechatOpenidForUser(userId, appId);
|
||||
if (!openid) {
|
||||
return {
|
||||
ok: false,
|
||||
message: '请先用微信登录并绑定账号后再充值',
|
||||
};
|
||||
}
|
||||
const result = await wechatPay.createJsapiOrder({
|
||||
outTradeNo,
|
||||
description,
|
||||
amountCents: amount,
|
||||
clientIp,
|
||||
openid,
|
||||
});
|
||||
jsapiParams = result.jsapiParams;
|
||||
} else if (mode === 'h5') {
|
||||
const result = await wechatPay.createH5Order({
|
||||
outTradeNo,
|
||||
description,
|
||||
@@ -200,6 +222,7 @@ export function createRechargeService(pool, { userAuth, wechatPay, config = load
|
||||
expireAt,
|
||||
codeUrl,
|
||||
h5Url,
|
||||
jsapiParams,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -250,6 +250,11 @@ export async function migrateSchema(pool) {
|
||||
KEY idx_wechat_pending_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
await pool.query(
|
||||
`ALTER TABLE h5_payment_orders
|
||||
MODIFY pay_mode ENUM('native', 'h5', 'jsapi') NOT NULL DEFAULT 'native'`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function initSchema(pool) {
|
||||
|
||||
+1
-1
@@ -461,7 +461,7 @@ CREATE TABLE IF NOT EXISTS h5_payment_orders (
|
||||
amount_cents BIGINT NOT NULL,
|
||||
channel ENUM('wechat') NOT NULL DEFAULT 'wechat',
|
||||
status ENUM('pending', 'paid', 'failed', 'expired', 'refunded') NOT NULL DEFAULT 'pending',
|
||||
pay_mode ENUM('native', 'h5') NOT NULL DEFAULT 'native',
|
||||
pay_mode ENUM('native', 'h5', 'jsapi') NOT NULL DEFAULT 'native',
|
||||
out_trade_no VARCHAR(64) NOT NULL,
|
||||
provider_txn VARCHAR(128) NULL,
|
||||
code_url VARCHAR(512) NULL,
|
||||
|
||||
+3
-1
@@ -1097,7 +1097,9 @@ app.post('/auth/billing/recharge-orders', jsonBody, async (req, res) => {
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.status(401).json({ message: '未登录' });
|
||||
const amountCents = Number(req.body?.amountCents);
|
||||
const payScene = req.body?.payScene === 'h5' ? 'h5' : 'native';
|
||||
const payScene = ['native', 'h5', 'jsapi'].includes(req.body?.payScene)
|
||||
? req.body.payScene
|
||||
: 'native';
|
||||
const result = await rechargeService.createOrder({
|
||||
userId: me.id,
|
||||
amountCents,
|
||||
|
||||
+10
-7
@@ -100,11 +100,7 @@ function AuthenticatedApp({
|
||||
path="/admin"
|
||||
element={
|
||||
<RequireAdmin user={user}>
|
||||
{user ? (
|
||||
<AdminLayout user={user} onLogout={handleLogout} />
|
||||
) : (
|
||||
<Navigate to="/" replace />
|
||||
)}
|
||||
{user ? <AdminLayout user={user} onLogout={handleLogout} /> : null}
|
||||
</RequireAdmin>
|
||||
}
|
||||
>
|
||||
@@ -143,7 +139,9 @@ export function App() {
|
||||
setUnauthorizedHandler(() => {
|
||||
setAuthed(false);
|
||||
setUser(null);
|
||||
navigate('/', { replace: true });
|
||||
if (window.location.pathname !== '/') {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
});
|
||||
void checkAuth().then((status) => {
|
||||
setLegacyMode(status.mode === 'legacy');
|
||||
@@ -181,7 +179,12 @@ export function App() {
|
||||
<AuthView
|
||||
legacyMode={legacyMode}
|
||||
onAuth={(nextUser, nextCapabilities, nextSkills) => {
|
||||
const returnTo = new URLSearchParams(window.location.search).get('return_to');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnTo =
|
||||
params.get('return_to') ??
|
||||
(window.location.pathname.startsWith('/admin')
|
||||
? `${window.location.pathname}${window.location.search}`
|
||||
: null);
|
||||
if (returnTo) {
|
||||
try {
|
||||
const url = new URL(returnTo, window.location.origin);
|
||||
|
||||
@@ -56,12 +56,14 @@ export function UsersPage() {
|
||||
<form className="admin-form" onSubmit={handleCreate}>
|
||||
<input
|
||||
placeholder="用户名"
|
||||
autoComplete="username"
|
||||
value={newUser.username}
|
||||
onChange={(e) => setNewUser((s) => ({ ...s, username: e.target.value }))}
|
||||
/>
|
||||
<input
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser((s) => ({ ...s, password: e.target.value }))}
|
||||
/>
|
||||
|
||||
+20
-6
@@ -111,9 +111,21 @@ async function parseErrorResponse(res: Response) {
|
||||
}
|
||||
|
||||
let unauthorizedHandler: (() => void) | null = null;
|
||||
let unauthorizedHandling = false;
|
||||
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null) {
|
||||
unauthorizedHandler = handler;
|
||||
if (handler) unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
export function resetUnauthorizedGuard() {
|
||||
unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
function notifyUnauthorized() {
|
||||
if (!unauthorizedHandler || unauthorizedHandling) return;
|
||||
unauthorizedHandling = true;
|
||||
unauthorizedHandler();
|
||||
}
|
||||
|
||||
async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
@@ -131,7 +143,7 @@ async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
unauthorizedHandler?.();
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
@@ -173,7 +185,7 @@ async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
unauthorizedHandler?.();
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
@@ -258,6 +270,7 @@ export async function checkAuth(): Promise<AuthStatus> {
|
||||
const response = await fetch('/auth/status');
|
||||
if (!response.ok) return { authenticated: false };
|
||||
const status = (await response.json()) as AuthStatus;
|
||||
if (status.authenticated) resetUnauthorizedGuard();
|
||||
if (status.authenticated && status.mode === 'user' && !status.capabilities) {
|
||||
try {
|
||||
const me = await getMe();
|
||||
@@ -303,7 +316,7 @@ export async function getBillingConfig(): Promise<BillingConfig> {
|
||||
|
||||
export async function createRechargeOrder(input: {
|
||||
amountCents: number;
|
||||
payScene: 'native' | 'h5';
|
||||
payScene: 'native' | 'h5' | 'jsapi';
|
||||
}): Promise<RechargeOrder> {
|
||||
const result = await portalFetch<{ order: RechargeOrder }>('/auth/billing/recharge-orders', {
|
||||
method: 'POST',
|
||||
@@ -565,7 +578,7 @@ export async function fetchPreviewAsset(path: string): Promise<string> {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
if (res.status === 401) {
|
||||
unauthorizedHandler?.();
|
||||
notifyUnauthorized();
|
||||
throw new ApiError(401, '未授权,请重新登录');
|
||||
}
|
||||
if (!res.ok) {
|
||||
@@ -699,7 +712,7 @@ export async function fetchMindSpacePageDraftPreview(
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
if (res.status === 401) {
|
||||
unauthorizedHandler?.();
|
||||
notifyUnauthorized();
|
||||
throw new ApiError(401, '未授权,请重新登录');
|
||||
}
|
||||
if (!res.ok) {
|
||||
@@ -1178,6 +1191,7 @@ export async function login(username: string, password: string): Promise<PortalU
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, body?.message ?? '登录失败');
|
||||
}
|
||||
resetUnauthorizedGuard();
|
||||
if (body?.user) {
|
||||
try {
|
||||
const me = await getMe();
|
||||
@@ -1734,7 +1748,7 @@ export function subscribeSessionEvents(
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
unauthorizedHandler?.();
|
||||
notifyUnauthorized();
|
||||
throw new ApiError(401, 'SSE 未授权');
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,14 @@ import { createPortal } from 'react-dom';
|
||||
import QRCode from 'qrcode';
|
||||
import { createRechargeOrder, getBillingConfig, getRechargeOrder } from '../api/client';
|
||||
import type { BillingConfig, RechargeOrder } from '../types';
|
||||
import { invokeWechatJsapiPay, isWeChatBrowser } from '../utils/wechatPay';
|
||||
|
||||
function formatYuan(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2)}`;
|
||||
}
|
||||
|
||||
function isWeChatBrowser() {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function detectPayScene(): 'native' | 'h5' {
|
||||
// 微信内置浏览器走扫码:H5 收银台需商户单独开通,长按识别二维码即可支付
|
||||
if (isWeChatBrowser()) return 'native';
|
||||
function detectPayScene(): 'native' | 'h5' | 'jsapi' {
|
||||
if (isWeChatBrowser()) return 'jsapi';
|
||||
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) ? 'h5' : 'native';
|
||||
}
|
||||
|
||||
@@ -156,6 +152,14 @@ export function RechargeModal({
|
||||
try {
|
||||
const created = await createRechargeOrderWithFallback(selectedCents);
|
||||
setOrder(created);
|
||||
if (created.payMode === 'jsapi' && created.jsapiParams) {
|
||||
startPolling(created.id);
|
||||
const result = await invokeWechatJsapiPay(created.jsapiParams);
|
||||
if (result === 'cancel') {
|
||||
setError('已取消支付,可重新发起');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (created.payMode === 'h5' && created.h5Url && !redirectedRef.current) {
|
||||
redirectedRef.current = true;
|
||||
window.location.href = created.h5Url;
|
||||
@@ -236,10 +240,15 @@ export function RechargeModal({
|
||||
{order?.payMode === 'native' && qrDataUrl && (
|
||||
<div className="recharge-qr-panel">
|
||||
<img src={qrDataUrl} alt="微信支付二维码" className="recharge-qr" />
|
||||
<p className="recharge-muted">请使用微信扫一扫完成支付</p>
|
||||
{expireLabel && <p className="recharge-muted">订单将于 {expireLabel} 过期</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{order?.payMode === 'jsapi' && (
|
||||
<div className="recharge-h5-panel">
|
||||
<p className="recharge-muted">
|
||||
{isWeChatBrowser()
|
||||
? '长按下方二维码,选择「识别图中二维码」完成支付'
|
||||
: '请使用微信扫一扫完成支付'}
|
||||
{polling ? '请在微信支付弹窗中完成付款…' : '点击「微信支付」后将直接调起微信收银台'}
|
||||
</p>
|
||||
{expireLabel && <p className="recharge-muted">订单将于 {expireLabel} 过期</p>}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { PortalUser } from '../types';
|
||||
|
||||
export function RequireAdmin({
|
||||
@@ -8,8 +9,15 @@ export function RequireAdmin({
|
||||
user: PortalUser | null;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (user?.role !== 'admin') {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
const navigate = useNavigate();
|
||||
const allowed = user?.role === 'admin';
|
||||
|
||||
useEffect(() => {
|
||||
if (!allowed) {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, [allowed, navigate]);
|
||||
|
||||
if (!allowed) return null;
|
||||
return children;
|
||||
}
|
||||
|
||||
+11
-1
@@ -143,14 +143,24 @@ export type BillingConfig = {
|
||||
balanceCents: number;
|
||||
};
|
||||
|
||||
export type JsapiPayParams = {
|
||||
appId: string;
|
||||
timeStamp: string;
|
||||
nonceStr: string;
|
||||
package: string;
|
||||
signType: 'MD5' | 'RSA';
|
||||
paySign: string;
|
||||
};
|
||||
|
||||
export type RechargeOrder = {
|
||||
id: string;
|
||||
amountCents: number;
|
||||
status: 'pending' | 'paid' | 'failed' | 'expired' | 'refunded';
|
||||
payMode: 'native' | 'h5';
|
||||
payMode: 'native' | 'h5' | 'jsapi';
|
||||
expireAt: number;
|
||||
codeUrl?: string | null;
|
||||
h5Url?: string | null;
|
||||
jsapiParams?: JsapiPayParams | null;
|
||||
};
|
||||
|
||||
export type InsufficientBalanceDetails = {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { JsapiPayParams } from '../types';
|
||||
|
||||
type WeixinJsBridge = {
|
||||
invoke(
|
||||
api: 'getBrandWCPayRequest',
|
||||
params: JsapiPayParams,
|
||||
callback: (result: { err_msg?: string }) => void,
|
||||
): void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
WeixinJSBridge?: WeixinJsBridge;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWeChatBrowser() {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
export function invokeWechatJsapiPay(params: JsapiPayParams): Promise<'ok' | 'cancel'> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const invoke = () => {
|
||||
const bridge = window.WeixinJSBridge;
|
||||
if (!bridge) {
|
||||
reject(new Error('当前环境不支持微信支付,请在微信内打开'));
|
||||
return;
|
||||
}
|
||||
bridge.invoke('getBrandWCPayRequest', params, (result) => {
|
||||
const message = result.err_msg ?? '';
|
||||
if (message === 'get_brand_wcpay_request:ok') {
|
||||
resolve('ok');
|
||||
return;
|
||||
}
|
||||
if (message === 'get_brand_wcpay_request:cancel') {
|
||||
resolve('cancel');
|
||||
return;
|
||||
}
|
||||
reject(new Error(message || '微信支付失败'));
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof window.WeixinJSBridge === 'undefined') {
|
||||
document.addEventListener('WeixinJSBridgeReady', invoke, { once: true });
|
||||
return;
|
||||
}
|
||||
invoke();
|
||||
});
|
||||
}
|
||||
@@ -1648,6 +1648,17 @@ export function createUserAuth(pool, options = {}) {
|
||||
return rows[0] ?? null;
|
||||
};
|
||||
|
||||
const getWechatOpenidForUser = async (userId, appId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT openid
|
||||
FROM h5_user_wechat_identities
|
||||
WHERE user_id = ? AND app_id = ?
|
||||
LIMIT 1`,
|
||||
[userId, appId],
|
||||
);
|
||||
return rows[0]?.openid ?? null;
|
||||
};
|
||||
|
||||
const bindWechatToUser = async ({
|
||||
userId,
|
||||
appId,
|
||||
@@ -2118,6 +2129,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
completeWechatBindAccount,
|
||||
getWechatPendingBind,
|
||||
getWechatBindingStatus,
|
||||
getWechatOpenidForUser,
|
||||
resetPassword,
|
||||
verify,
|
||||
revoke,
|
||||
|
||||
@@ -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('微信支付未配置');
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildV2JsapiParams,
|
||||
buildXml,
|
||||
createWechatPayClient,
|
||||
loadWechatPayConfig,
|
||||
@@ -81,6 +82,18 @@ test('loadWechatPayConfig selects v2 when APIv2 key is configured', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('buildV2JsapiParams signs prepay package for WeixinJSBridge', () => {
|
||||
const config = {
|
||||
appId: 'wxtestappid',
|
||||
apiKey: 'test-api-v2-key-32chars-long!!!!',
|
||||
};
|
||||
const params = buildV2JsapiParams(config, 'wx201410272009395522657a690389285100');
|
||||
assert.equal(params.appId, 'wxtestappid');
|
||||
assert.equal(params.package, 'prepay_id=wx201410272009395522657a690389285100');
|
||||
assert.equal(params.signType, 'MD5');
|
||||
assert.match(params.paySign, /^[0-9A-F]{32}$/);
|
||||
});
|
||||
|
||||
test('verifyNotify validates v2 callback signature', () => {
|
||||
const apiKey = 'test-api-v2-key-32chars-long!!!!';
|
||||
const fields = {
|
||||
|
||||
Reference in New Issue
Block a user