100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import crypto from "crypto"
|
|
|
|
export interface WechatPayConfig {
|
|
appId: string
|
|
appSecret: string
|
|
mchId: string
|
|
apiKey: string
|
|
notifyUrl: string
|
|
}
|
|
|
|
export class WechatPayService {
|
|
constructor(private config: WechatPayConfig) {}
|
|
|
|
// 创建微信支付订单(扫码支付)
|
|
async createOrder(params: {
|
|
outTradeNo: string
|
|
body: string
|
|
totalFee: number
|
|
spbillCreateIp: string
|
|
}) {
|
|
const orderParams = {
|
|
appid: this.config.appId,
|
|
mch_id: this.config.mchId,
|
|
nonce_str: this.generateNonceStr(),
|
|
body: params.body,
|
|
out_trade_no: params.outTradeNo,
|
|
total_fee: Math.round(params.totalFee * 100).toString(), // 转换为分
|
|
spbill_create_ip: params.spbillCreateIp,
|
|
notify_url: this.config.notifyUrl,
|
|
trade_type: "NATIVE", // 扫码支付
|
|
}
|
|
|
|
const sign = this.generateSign(orderParams)
|
|
const xmlData = this.buildXML({ ...orderParams, sign })
|
|
|
|
// In production, make actual API call to WeChat
|
|
// const response = await fetch("https://api.mch.weixin.qq.com/pay/unifiedorder", {
|
|
// method: "POST",
|
|
// body: xmlData,
|
|
// headers: { "Content-Type": "application/xml" },
|
|
// })
|
|
|
|
// Mock response for development
|
|
return {
|
|
codeUrl: `weixin://wxpay/bizpayurl?pr=${this.generateNonceStr()}`,
|
|
prepayId: `prepay_${Date.now()}`,
|
|
outTradeNo: params.outTradeNo,
|
|
}
|
|
}
|
|
|
|
// 生成随机字符串
|
|
private generateNonceStr(): string {
|
|
return crypto.randomBytes(16).toString("hex")
|
|
}
|
|
|
|
// 生成签名
|
|
generateSign(params: Record<string, string>): string {
|
|
const sortedKeys = Object.keys(params).sort()
|
|
const signString = sortedKeys
|
|
.filter((key) => params[key] && key !== "sign")
|
|
.map((key) => `${key}=${params[key]}`)
|
|
.join("&")
|
|
|
|
const signWithKey = `${signString}&key=${this.config.apiKey}`
|
|
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex").toUpperCase()
|
|
}
|
|
|
|
// 验证签名
|
|
verifySign(params: Record<string, string>): boolean {
|
|
const receivedSign = params.sign
|
|
if (!receivedSign) return false
|
|
|
|
const calculatedSign = this.generateSign(params)
|
|
return receivedSign === calculatedSign
|
|
}
|
|
|
|
// 构建XML数据
|
|
private buildXML(params: Record<string, string>): string {
|
|
const xml = ["<xml>"]
|
|
for (const [key, value] of Object.entries(params)) {
|
|
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
|
|
}
|
|
xml.push("</xml>")
|
|
return xml.join("")
|
|
}
|
|
|
|
// 解析XML数据
|
|
private async parseXML(xml: string): Promise<Record<string, string>> {
|
|
const result: Record<string, string> = {}
|
|
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
|
let match
|
|
|
|
while ((match = regex.exec(xml)) !== null) {
|
|
result[match[1]] = match[2]
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|