- H1: 支付宝响应验签 (verifyResponseSign + bracket-matching 提取签名内容) - H2/H3: EasyPay queryOrder 从 GET 改 POST,PKEY 不再暴露于 URL - H5: users/[id] IDOR 修复,校验当前用户只能查询自身信息 - H6: 限额校验移入 prisma.$transaction() 防止 TOCTOU 竞态 - C1: access_token 增加 24h 过期、userId 绑定、派生密钥分离 - M1: EasyPay 回调增加 pid 校验防跨商户注入 - M4: 充值码增加 crypto.randomBytes 随机后缀 - M5: 过期订单批量处理增加 BATCH_SIZE 限制 - M6: 退款失败增加 [CRITICAL] 日志和余额补偿标记 - M7: admin channels PUT 增加 Zod schema 校验 - M8: admin subscriptions 分页参数增加上限 - M9: orders src_url 限制 HTTP/HTTPS 协议 - L1: 微信支付回调时间戳 NaN 检查 - L9: WXPAY_API_V3_KEY 长度校验
133 lines
4.0 KiB
TypeScript
133 lines
4.0 KiB
TypeScript
import { getEnv } from '@/lib/config';
|
||
import { generateSign } from './sign';
|
||
import type { EasyPayCreateResponse, EasyPayQueryResponse, EasyPayRefundResponse } from './types';
|
||
|
||
export interface CreatePaymentOptions {
|
||
outTradeNo: string;
|
||
amount: string;
|
||
paymentType: string;
|
||
clientIp: string;
|
||
productName: string;
|
||
returnUrl?: string;
|
||
}
|
||
|
||
function normalizeCidList(cid?: string): string | undefined {
|
||
if (!cid) return undefined;
|
||
const normalized = cid
|
||
.split(',')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
.join(',');
|
||
return normalized || undefined;
|
||
}
|
||
|
||
function resolveCid(paymentType: string): string | undefined {
|
||
const env = getEnv();
|
||
if (paymentType === 'alipay') {
|
||
return normalizeCidList(env.EASY_PAY_CID_ALIPAY) || normalizeCidList(env.EASY_PAY_CID);
|
||
}
|
||
return normalizeCidList(env.EASY_PAY_CID_WXPAY) || normalizeCidList(env.EASY_PAY_CID);
|
||
}
|
||
|
||
function assertEasyPayEnv(env: ReturnType<typeof getEnv>) {
|
||
if (
|
||
!env.EASY_PAY_PID ||
|
||
!env.EASY_PAY_PKEY ||
|
||
!env.EASY_PAY_API_BASE ||
|
||
!env.EASY_PAY_NOTIFY_URL ||
|
||
!env.EASY_PAY_RETURN_URL
|
||
) {
|
||
throw new Error(
|
||
'EasyPay environment variables (EASY_PAY_PID, EASY_PAY_PKEY, EASY_PAY_API_BASE, EASY_PAY_NOTIFY_URL, EASY_PAY_RETURN_URL) are required',
|
||
);
|
||
}
|
||
return env as typeof env & {
|
||
EASY_PAY_PID: string;
|
||
EASY_PAY_PKEY: string;
|
||
EASY_PAY_API_BASE: string;
|
||
EASY_PAY_NOTIFY_URL: string;
|
||
EASY_PAY_RETURN_URL: string;
|
||
};
|
||
}
|
||
|
||
export async function createPayment(opts: CreatePaymentOptions): Promise<EasyPayCreateResponse> {
|
||
const env = assertEasyPayEnv(getEnv());
|
||
const params: Record<string, string> = {
|
||
pid: env.EASY_PAY_PID,
|
||
type: opts.paymentType,
|
||
out_trade_no: opts.outTradeNo,
|
||
notify_url: env.EASY_PAY_NOTIFY_URL,
|
||
return_url: opts.returnUrl || env.EASY_PAY_RETURN_URL,
|
||
name: opts.productName,
|
||
money: opts.amount,
|
||
clientip: opts.clientIp,
|
||
};
|
||
|
||
const cid = resolveCid(opts.paymentType);
|
||
if (cid) {
|
||
params.cid = cid;
|
||
}
|
||
|
||
const sign = generateSign(params, env.EASY_PAY_PKEY);
|
||
params.sign = sign;
|
||
params.sign_type = 'MD5';
|
||
|
||
const formData = new URLSearchParams(params);
|
||
const response = await fetch(`${env.EASY_PAY_API_BASE}/mapi.php`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
signal: AbortSignal.timeout(10_000),
|
||
});
|
||
|
||
const data = (await response.json()) as EasyPayCreateResponse;
|
||
if (data.code !== 1) {
|
||
throw new Error(`EasyPay create payment failed: ${data.msg || 'unknown error'}`);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
export async function queryOrder(outTradeNo: string): Promise<EasyPayQueryResponse> {
|
||
const env = assertEasyPayEnv(getEnv());
|
||
// 使用 POST 避免密钥暴露在 URL 中(URL 会被记录到服务器/CDN 日志)
|
||
const params = new URLSearchParams({
|
||
act: 'order',
|
||
pid: env.EASY_PAY_PID,
|
||
key: env.EASY_PAY_PKEY,
|
||
out_trade_no: outTradeNo,
|
||
});
|
||
const response = await fetch(`${env.EASY_PAY_API_BASE}/api.php`, {
|
||
method: 'POST',
|
||
body: params,
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
signal: AbortSignal.timeout(10_000),
|
||
});
|
||
const data = (await response.json()) as EasyPayQueryResponse;
|
||
if (data.code !== 1) {
|
||
throw new Error(`EasyPay query order failed: ${data.msg || 'unknown error'}`);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
export async function refund(tradeNo: string, outTradeNo: string, money: string): Promise<EasyPayRefundResponse> {
|
||
const env = assertEasyPayEnv(getEnv());
|
||
const params = new URLSearchParams({
|
||
pid: env.EASY_PAY_PID,
|
||
key: env.EASY_PAY_PKEY,
|
||
trade_no: tradeNo,
|
||
out_trade_no: outTradeNo,
|
||
money,
|
||
});
|
||
const response = await fetch(`${env.EASY_PAY_API_BASE}/api.php?act=refund`, {
|
||
method: 'POST',
|
||
body: params,
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
signal: AbortSignal.timeout(10_000),
|
||
});
|
||
const data = (await response.json()) as EasyPayRefundResponse;
|
||
if (data.code !== 1) {
|
||
throw new Error(`EasyPay refund failed: ${data.msg || 'unknown error'}`);
|
||
}
|
||
return data;
|
||
}
|