const _crypto = require("node:crypto"); const fs = require("node:fs"); const WxPay = require("wechatpay-node-v3"); const { pool, withTransaction } = require("./db"); const { creditBalance, creditUserBalance, activatePackage } = require("./billing"); let wxPayInstance = null; function getWxPay() { if (wxPayInstance) return wxPayInstance; const mchId = process.env.WECHAT_MCH_ID; const appId = process.env.WECHAT_APP_ID; const apiKey = process.env.WECHAT_API_KEY_V3; const certPath = process.env.WECHAT_CERT_PATH; const keyPath = process.env.WECHAT_KEY_PATH; if (!mchId || !appId || !apiKey) return null; let publicKey = null; let privateKey = null; try { if (certPath && fs.existsSync(certPath)) publicKey = fs.readFileSync(certPath); if (keyPath && fs.existsSync(keyPath)) privateKey = fs.readFileSync(keyPath); } catch (err) { console.error("[wechatPay] failed to read cert/key files:", err.message); return null; } if (!publicKey || !privateKey) { console.warn("[wechatPay] cert or key file missing, WeChat Pay disabled"); return null; } wxPayInstance = new WxPay({ appid: appId, mchid: mchId, publicKey, privateKey }); return wxPayInstance; } function isWechatPayEnabled() { return getWxPay() !== null; } async function createNativeOrder(orderNo, amountCents, description, notifyUrl) { const pay = getWxPay(); if (!pay) throw new Error("微信支付未配置"); const nonceStr = Math.random().toString(36).substring(2, 17); const timestamp = Math.floor(Date.now() / 1000).toString(); const url = "/v3/pay/transactions/native"; const params = { appid: process.env.WECHAT_APP_ID, mchid: process.env.WECHAT_MCH_ID, description, out_trade_no: orderNo, notify_url: notifyUrl, amount: { total: amountCents, currency: "CNY" }, }; const signature = pay.getSignature("POST", nonceStr, timestamp, url, params); const authorization = pay.getAuthorization(nonceStr, timestamp, signature); const superagent = require("superagent"); const result = await superagent .post("https://api.mch.weixin.qq.com/v3/pay/transactions/native") .send(params) .set({ Accept: "application/json", "Content-Type": "application/json", Authorization: authorization, }); if (!result.body?.code_url) { throw new Error(result.body?.message || "微信下单失败"); } return { codeUrl: result.body.code_url }; } function verifyAndDecryptNotification(headers, body) { const pay = getWxPay(); if (!pay) return null; try { const signature = headers["wechatpay-signature"]; const timestamp = headers["wechatpay-timestamp"]; const nonce = headers["wechatpay-nonce"]; if (!signature || !timestamp || !nonce) { console.error("[wechatPay] callback missing required headers"); return null; } const message = `${timestamp}\n${nonce}\n${typeof body === "string" ? body : JSON.stringify(body)}\n`; const verified = pay.verifySignature(signature, timestamp, nonce, message); if (!verified) { console.error("[wechatPay] callback signature verification failed"); return null; } const resource = body.resource || body; if (resource.ciphertext) { const apiKey = process.env.WECHAT_API_KEY_V3; const decrypted = pay.decipher_gcm( resource.ciphertext, resource.associated_data || "", resource.nonce || "", apiKey, ); return JSON.parse(decrypted); } return body; } catch (err) { console.error("[wechatPay] notification processing failed:", err.message); return null; } } async function handlePaymentSuccess(orderNo, transactionId) { const { rows } = await pool.query( "SELECT * FROM payment_orders WHERE order_no = $1 AND status = $2", [orderNo, "pending"], ); const order = rows[0]; if (!order) { console.warn("[wechatPay] no pending order found for", orderNo); return false; } await withTransaction(async (client) => { await client.query( ` UPDATE payment_orders SET status = 'paid', payment_trade_no = $1, paid_at = NOW() WHERE order_no = $2 `, [transactionId, orderNo], ); if (order.type === "personal_recharge" && order.user_id) { await creditUserBalance( order.user_id, order.amount_cents, `微信充值 ${Math.floor(order.amount_cents / 100)} 积分`, orderNo, ); } else if (order.type === "recharge") { await creditBalance( order.enterprise_id, order.amount_cents, `微信充值 ${Math.floor(order.amount_cents / 100)} 积分`, orderNo, ); } else if (order.type === "package" && order.package_id) { await activatePackage(order.enterprise_id, order.package_id); } }); return true; } module.exports = { isWechatPayEnabled, createNativeOrder, verifyAndDecryptNotification, handlePaymentSuccess, };