feat: migrate payment provider to easy-pay, add order history and refund support
- Replace zpay with easy-pay payment provider (new lib/easy-pay/ module) - Add order history page for users (pay/orders) - Add GET /api/orders/my endpoint to list user's own orders - Add GET /api/users/[id] endpoint for sub2api user lookup - Add order status tracking module (lib/order/status.ts) - Update config to support easy-pay credentials (merchant ID, key, gateway) - Update PaymentForm and PaymentQRCode components for easy-pay flow - Update pay page and admin page with new order management UI - Update order service to support easy-pay, cancellation, and refund
This commit is contained in:
74
src/lib/zpay/client.ts
Normal file
74
src/lib/zpay/client.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { getEnv } from '@/lib/config';
|
||||
import { generateSign } from './sign';
|
||||
import type { ZPayCreateResponse, ZPayQueryResponse, ZPayRefundResponse } from './types';
|
||||
|
||||
export interface CreatePaymentOptions {
|
||||
outTradeNo: string;
|
||||
amount: string; // 金额字符串,如 "10.00"
|
||||
paymentType: 'alipay' | 'wxpay';
|
||||
clientIp: string;
|
||||
productName: string;
|
||||
}
|
||||
|
||||
export async function createPayment(opts: CreatePaymentOptions): Promise<ZPayCreateResponse> {
|
||||
const env = getEnv();
|
||||
const params: Record<string, string> = {
|
||||
pid: env.ZPAY_PID,
|
||||
type: opts.paymentType,
|
||||
out_trade_no: opts.outTradeNo,
|
||||
notify_url: env.ZPAY_NOTIFY_URL,
|
||||
return_url: env.ZPAY_RETURN_URL,
|
||||
name: opts.productName,
|
||||
money: opts.amount,
|
||||
clientip: opts.clientIp,
|
||||
};
|
||||
|
||||
const sign = generateSign(params, env.ZPAY_PKEY);
|
||||
params.sign = sign;
|
||||
params.sign_type = 'MD5';
|
||||
|
||||
const formData = new URLSearchParams(params);
|
||||
const response = await fetch(`${env.ZPAY_API_BASE}/mapi.php`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
});
|
||||
|
||||
const data = await response.json() as ZPayCreateResponse;
|
||||
if (data.code !== 1) {
|
||||
throw new Error(`ZPAY create payment failed: ${data.msg || 'unknown error'}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function queryOrder(outTradeNo: string): Promise<ZPayQueryResponse> {
|
||||
const env = getEnv();
|
||||
const url = `${env.ZPAY_API_BASE}/api.php?act=order&pid=${env.ZPAY_PID}&key=${env.ZPAY_PKEY}&out_trade_no=${outTradeNo}`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json() as ZPayQueryResponse;
|
||||
if (data.code !== 1) {
|
||||
throw new Error(`ZPAY query order failed: ${data.msg || 'unknown error'}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refund(tradeNo: string, outTradeNo: string, money: string): Promise<ZPayRefundResponse> {
|
||||
const env = getEnv();
|
||||
const params = new URLSearchParams({
|
||||
pid: env.ZPAY_PID,
|
||||
key: env.ZPAY_PKEY,
|
||||
trade_no: tradeNo,
|
||||
out_trade_no: outTradeNo,
|
||||
money,
|
||||
});
|
||||
const response = await fetch(`${env.ZPAY_API_BASE}/api.php?act=refund`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
});
|
||||
const data = await response.json() as ZPayRefundResponse;
|
||||
if (data.code !== 1) {
|
||||
throw new Error(`ZPAY refund failed: ${data.msg || 'unknown error'}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
19
src/lib/zpay/sign.ts
Normal file
19
src/lib/zpay/sign.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function generateSign(params: Record<string, string>, pkey: string): string {
|
||||
const filtered = Object.entries(params)
|
||||
.filter(([key, value]) => key !== 'sign' && key !== 'sign_type' && value !== '' && value !== undefined && value !== null)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
const queryString = filtered.map(([key, value]) => `${key}=${value}`).join('&');
|
||||
const signStr = queryString + pkey;
|
||||
return crypto.createHash('md5').update(signStr).digest('hex');
|
||||
}
|
||||
|
||||
export function verifySign(params: Record<string, string>, pkey: string, sign: string): boolean {
|
||||
const expected = generateSign(params, pkey);
|
||||
if (expected.length !== sign.length) return false;
|
||||
const a = Buffer.from(expected);
|
||||
const b = Buffer.from(sign);
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
56
src/lib/zpay/types.ts
Normal file
56
src/lib/zpay/types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface ZPayCreateParams {
|
||||
pid: string;
|
||||
type: 'alipay' | 'wxpay';
|
||||
out_trade_no: string;
|
||||
notify_url: string;
|
||||
name: string;
|
||||
money: string;
|
||||
clientip: string;
|
||||
return_url: string;
|
||||
sign?: string;
|
||||
sign_type?: string;
|
||||
}
|
||||
|
||||
export interface ZPayCreateResponse {
|
||||
code: number;
|
||||
msg?: string;
|
||||
trade_no: string;
|
||||
O_id?: string;
|
||||
payurl?: string;
|
||||
qrcode?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
export interface ZPayNotifyParams {
|
||||
pid: string;
|
||||
name: string;
|
||||
money: string;
|
||||
out_trade_no: string;
|
||||
trade_no: string;
|
||||
param?: string;
|
||||
trade_status: string;
|
||||
type: string;
|
||||
sign: string;
|
||||
sign_type: string;
|
||||
}
|
||||
|
||||
export interface ZPayQueryResponse {
|
||||
code: number;
|
||||
msg?: string;
|
||||
trade_no: string;
|
||||
out_trade_no: string;
|
||||
type: string;
|
||||
pid: string;
|
||||
addtime: string;
|
||||
endtime: string;
|
||||
name: string;
|
||||
money: string;
|
||||
status: number;
|
||||
param?: string;
|
||||
buyer?: string;
|
||||
}
|
||||
|
||||
export interface ZPayRefundResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
Reference in New Issue
Block a user