Initial commit: OmniAI backend server
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user