From 2a294b21ce9f433a386d45bed8db0182fd677316 Mon Sep 17 00:00:00 2001 From: constansino Date: Tue, 3 Feb 2026 02:03:07 +0800 Subject: [PATCH] feat: add direct alipay support and fix RSA key compatibility --- .gitignore | 1 + alipay/alipay.js | 149 ++++++++++++++++++++++ config.example.js | 14 ++- index.js | 305 +++++++++++++--------------------------------- package.json | 1 + 5 files changed, 246 insertions(+), 224 deletions(-) create mode 100644 alipay/alipay.js diff --git a/.gitignore b/.gitignore index 61d788c..e4d037a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ Thumbs.db # 临时文件 *.tmp *.temp +*.pem # 压缩包 *.zip diff --git a/alipay/alipay.js b/alipay/alipay.js new file mode 100644 index 0000000..ec3bae9 --- /dev/null +++ b/alipay/alipay.js @@ -0,0 +1,149 @@ +const { AlipaySdk, AlipayFormData } = require('alipay-sdk'); +const fs = require('fs'); +const path = require('path'); + +class AlipayClient { + constructor(config) { + this.appId = config.appId; + + // Support both direct PEM string in config or external PEM files + try { + const privateKeyPath = path.join(__dirname, '../private_key.pem'); + const alipayPublicKeyPath = path.join(__dirname, '../alipay_public_key.pem'); + + if (fs.existsSync(privateKeyPath)) { + this.privateKey = fs.readFileSync(privateKeyPath, 'ascii'); + } else { + this.privateKey = this._formatKey(config.privateKey, 'PRIVATE KEY'); + } + + if (fs.existsSync(alipayPublicKeyPath)) { + this.alipayPublicKey = fs.readFileSync(alipayPublicKeyPath, 'ascii'); + } else { + this.alipayPublicKey = this._formatKey(config.alipayPublicKey, 'PUBLIC KEY'); + } + } catch (e) { + console.error('Error loading Alipay keys:', e.message); + this.privateKey = this._formatKey(config.privateKey, 'PRIVATE KEY'); + this.alipayPublicKey = this._formatKey(config.alipayPublicKey, 'PUBLIC KEY'); + } + + this.alipaySdk = new AlipaySdk({ + appId: this.appId, + privateKey: this.privateKey, + alipayPublicKey: this.alipayPublicKey, + gateway: 'https://openapi.alipay.com/gateway.do', + }); + } + + _formatKey(key, type) { + if (!key) return ''; + if (key.includes('BEGIN')) return key; + const lines = key.match(/.{1,64}/g).join('\n'); + return `-----BEGIN ${type}-----\n${lines}\n-----END ${type}-----`; + } + + /** + * Unified Order - Generates Payment URL + */ + async unifiedOrder(orderParams) { + const formData = new AlipayFormData(); + formData.setMethod('get'); + + const totalAmount = (orderParams.amount / 100).toFixed(2); + + formData.addField('notifyUrl', orderParams.notifyUrl); + formData.addField('returnUrl', orderParams.returnUrl); + + let passbackParams = orderParams.extParam; + if (passbackParams) { + passbackParams = encodeURIComponent(passbackParams); + } + + formData.addField('bizContent', { + outTradeNo: orderParams.mchOrderNo, + productCode: 'FAST_INSTANT_TRADE_PAY', + totalAmount: totalAmount, + subject: orderParams.subject, + body: orderParams.body, + passback_params: passbackParams + }); + + const result = await this.alipaySdk.pageExec( + 'alipay.trade.page.pay', + { method: 'GET' }, + { formData: formData } + ); + + return { + payUrl: result, + payOrderId: orderParams.mchOrderNo, + }; + } + + /** + * Query Order + */ + async queryOrder(queryParams) { + const outTradeNo = queryParams.mchOrderNo; + + try { + const result = await this.alipaySdk.exec('alipay.trade.query', { + bizContent: { out_trade_no: outTradeNo } + }); + + let state = '0'; + const status = result.tradeStatus; + + if (status === 'TRADE_SUCCESS' || status === 'TRADE_FINISHED') { + state = '2'; + } else if (status === 'TRADE_CLOSED') { + state = '6'; + } else if (status === 'WAIT_BUYER_PAY') { + state = '1'; + } + + return { + payOrderId: result.tradeNo, + mchOrderNo: result.outTradeNo, + amount: Math.round(parseFloat(result.totalAmount) * 100), + state: state, + successTime: result.sendPayDate + }; + } catch (err) { + console.error('Alipay query error', err); + throw err; + } + } + + verifyNotify(params) { + return this.alipaySdk.checkNotifySign(params); + } + + async refundOrder(params) { + const result = await this.alipaySdk.exec('alipay.trade.refund', { + bizContent: { + out_trade_no: params.mchOrderNo, + refund_amount: (params.refundAmount / 100).toFixed(2), + refund_reason: params.refundReason, + out_request_no: params.mchRefundNo + } + }); + + if (result.code === '10000' && result.fundChange === 'Y') { + return { + refundOrderId: params.mchRefundNo, + payOrderId: params.payOrderId, + mchRefundNo: params.mchRefundNo, + refundAmount: params.refundAmount, + state: '2' + } + } else { + throw new Error(result.subMsg || result.msg || 'Refund failed'); + } + } + + async closeOrder() {} +} + +module.exports = AlipayClient; diff --git a/config.example.js b/config.example.js index 358bb13..3fd41dd 100644 --- a/config.example.js +++ b/config.example.js @@ -7,12 +7,16 @@ module.exports = { // ========== Jeepay 支付平台配置 ========== jeepay: { baseUrl: 'https://pay.jeepay.vip', // Jeepay API 基础地址 - mchNo: '你的商户号', // 商户号 - appId: '你的应用ID', // 应用ID - privateKey: '你的商户私钥' // 商户私钥(用于签名/验签) + mchNo: '你的商户号', + appId: '你的应用ID', + privateKey: '你的商户私钥' + }, + // 直接接入支付宝配置 (可选,若配置则优先使用支付宝模式) + alipay: { + appId: '你的支付宝应用ID', + privateKey: '你的支付宝应用私钥', + alipayPublicKey: '支付宝公钥' }, - - // ========== 易支付接口配置(适配器) ========== epay: { pid: '你的易支付商户ID', key: '你的易支付密钥' // MD5 签名密钥 diff --git a/index.js b/index.js index 8fa1de6..1bc3ec2 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,10 @@ const express = require('express'); const cors = require('cors'); const axios = require('axios'); -const querystring = require('querystring'); const path = require('path'); const session = require('express-session'); const JeepayClient = require('./jeepay/jeepay'); +const AlipayClient = require('./alipay/alipay'); const EpayAdapter = require('./epay'); const config = require('./config'); const { router: jeepayRouter, initJeepayClient } = require('./jeepay'); @@ -14,8 +14,15 @@ const testRouter = require('./test'); const app = express(); const PORT = config.server.port; +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); + // 信任反向代理(nginx等) -// 这样 Express 才能正确识别 X-Forwarded-* 头部 app.set('trust proxy', true); app.use(cors({ @@ -31,95 +38,82 @@ app.use(session({ resave: false, saveUninitialized: false, cookie: { - secure: false, // 生产环境建议设置为 true(需要 HTTPS) + secure: false, httpOnly: true, - maxAge: 24 * 60 * 60 * 1000 // 24小时 + maxAge: 24 * 60 * 60 * 1000 } })); -// 静态文件服务(用于前端界面) app.use(express.static(path.join(__dirname, 'public'))); -// 初始化 Jeepay 客户端 -const jeepay = new JeepayClient({ - baseUrl: config.jeepay.baseUrl, - mchNo: config.jeepay.mchNo, - appId: config.jeepay.appId, - privateKey: config.jeepay.privateKey -}); +// Choose Payment Client +let payClient; +let mode = 'Jeepay'; -// 初始化 Jeepay API 路由(注入客户端实例) -initJeepayClient(jeepay); +if (config.alipay && config.alipay.appId) { + mode = 'Alipay'; + payClient = new AlipayClient({ + appId: config.alipay.appId, + privateKey: config.alipay.privateKey, + alipayPublicKey: config.alipay.alipayPublicKey + }); +} else { + payClient = new JeepayClient({ + baseUrl: config.jeepay.baseUrl, + mchNo: config.jeepay.mchNo, + appId: config.jeepay.appId, + privateKey: config.jeepay.privateKey + }); + initJeepayClient(payClient); +} -// 使用配置的网站域名作为服务器地址(用于通知URL) const serverHost = config.server.siteDomain; -// 初始化 易支付 适配器 const epayAdapter = new EpayAdapter({ - jeepayClient: jeepay, - key: config.epay.key, // MD5 签名密钥 + jeepayClient: payClient, + key: config.epay.key, serverHost: serverHost, - pid: config.epay.pid // 商户ID(用于通知) + pid: config.epay.pid }); // ========== 基础路由 ========== -// 首页 - 重定向到配置管理界面 app.get('/', (req, res) => { - // 如果请求的是 API(Accept: application/json),返回 JSON if (req.headers.accept && req.headers.accept.includes('application/json')) { res.json({ - message: 'Jeepay 支付平台 API 服务运行中', + message: `KitfoxPay (${mode} Mode) API 服务运行中`, status: 'success', version: '1.0.0', configUrl: '/index.html' }); } else { - // 否则返回配置管理界面 res.sendFile(path.join(__dirname, 'public', 'index.html')); } }); -// 健康检查 app.get('/api/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), config: { - baseUrl: config.jeepay.baseUrl, - mchNo: config.jeepay.mchNo, - appId: config.jeepay.appId + mode: mode, + appId: mode === 'Alipay' ? config.alipay.appId : config.jeepay.appId } }); }); -// ========== 注册配置管理 API 路由 ========== app.use('/api/admin', adminRouter); app.use('/api/config', configRouter); - -// ========== 注册测试 API 路由 ========== app.use('/api/test', testRouter); -// ========== 注册 Jeepay API 路由 ========== -app.use("/api/jeepay", jeepayRouter); - -// ========== 易支付 支付接口适配 ========== -// 按照 易支付 新接口标准实现:https://pay.myzfw.com/doc_old.html#pay3 -// 接收 易支付 格式的请求,转发到 Jeepay 接口 +if (mode === 'Jeepay') { + app.use("/api/jeepay", jeepayRouter); +} -/** - * 获取请求参数(支持 POST body 和 query 参数) - */ function getRequestParams(req) { - return { - ...req.query, - ...req.body - }; + return { ...req.query, ...req.body }; } -/** - * 处理错误响应 - */ function handleErrorResponse(error, epayAdapter, res) { console.error('易支付 接口处理失败:', error); const errorResponse = { @@ -134,11 +128,6 @@ function handleErrorResponse(error, epayAdapter, res) { res.json(errorResponse); } -/** - * 易支付 后端API支付接口(mapi.php) - * 接口标准:https://pay.myzfw.com/doc_old.html#pay3 - * 支持 GET 和 POST 请求 - */ app.all('/mapi.php', async (req, res) => { try { const params = getRequestParams(req); @@ -149,18 +138,10 @@ app.all('/mapi.php', async (req, res) => { } }); -/** - * 易支付 前台支付提交接口(submit.php) - * 接口标准:https://pay.myzfw.com/doc_old.html#pay3 - * 支持 GET 和 POST 请求 - * 返回支付表单HTML或跳转URL - */ app.all('/submit.php', async (req, res) => { try { const params = getRequestParams(req); const result = await epayAdapter.submitOrder(params); - - // 如果返回的是表单HTML,直接返回HTML if (result.code === 1 && result.data && result.data.form) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(result.data.form); @@ -172,137 +153,77 @@ app.all('/submit.php', async (req, res) => { } }); -/** - * 易支付 统一API接口(api.php) - * 接口标准:https://pay.myzfw.com/doc_old.html#pay3 - * 通过 act 参数区分不同的操作: - * - act=order: 查询单个订单 - * - act=orders: 批量查询订单 - * - act=refund: 退款 - * - act=query: 查询商户信息 - * - act=settle: 查询结算记录 - * 支持 GET 和 POST 请求 - */ app.all('/api.php', async (req, res) => { try { const params = getRequestParams(req); const act = params.act; - - if (!act) { - return res.json({ - code: -1, - msg: '缺少参数: act', - data: null - }); - } + if (!act) return res.json({ code: -1, msg: '缺少参数: act', data: null }); switch (act) { case 'order': - // 查询单个订单 - // api.php?act=order&pid={商户ID}&key={商户密钥}&out_trade_no={商户订单号} - // 或 api.php?act=order&pid={商户ID}&key={商户密钥}&trade_no={平台订单号} - const orderResult = await epayAdapter.queryOrder(params); - return res.json(orderResult); - + return res.json(await epayAdapter.queryOrder(params)); case 'orders': - // 批量查询订单 - // api.php?act=orders&pid={商户ID}&key={商户密钥}&limit={数量}&page={页码} - const ordersResult = await epayAdapter.queryOrders(params); - return res.json(ordersResult); - + return res.json(await epayAdapter.queryOrders(params)); case 'refund': - // 退款 - // api.php?act=refund&pid={商户ID}&key={商户密钥}&out_trade_no={商户订单号}&money={退款金额} - const refundResult = await epayAdapter.refundOrder(params); - return res.json(refundResult); - + return res.json(await epayAdapter.refundOrder(params)); case 'query': - // 查询商户信息 - // api.php?act=query&pid={商户ID}&key={商户密钥} - const queryResult = await epayAdapter.queryMerchant(params); - return res.json(queryResult); - + return res.json(await epayAdapter.queryMerchant(params)); case 'settle': - // 查询结算记录 - // api.php?act=settle&pid={商户ID}&key={商户密钥} - const settleResult = await epayAdapter.querySettle(params); - return res.json(settleResult); - + return res.json(await epayAdapter.querySettle(params)); default: - return res.json({ - code: -1, - msg: `不支持的 act 参数: ${act}`, - data: null - }); + return res.json({ code: -1, msg: `不支持的 act 参数: ${act}`, data: null }); } } catch (error) { handleErrorResponse(error, epayAdapter, res); } }); -/** - * Jeepay 支付结果异步通知接口 - * POST /api/payment/notify - * 接收 Jeepay 的支付通知,转换为 易支付 格式并转发到商户的 notify_url - */ app.post('/api/payment/notify', async (req, res) => { try { - // 接收 Jeepay 格式的通知 - const jeepayNotify = req.body; - - // 验证 Jeepay 通知签名 - if (!jeepay.verifyNotify(jeepayNotify)) { - console.error('Jeepay 支付通知签名验证失败:', jeepayNotify); + const notifyData = req.body; + if (!payClient.verifyNotify(notifyData)) { + console.error(`${mode} 支付通知签名验证失败:`, notifyData); return res.send('fail'); } - console.log('收到 Jeepay 支付通知:', { - payOrderId: jeepayNotify.payOrderId, - mchOrderNo: jeepayNotify.mchOrderNo, - amount: jeepayNotify.amount, - state: jeepayNotify.state - }); + let jeepayNotify; + if (mode === 'Alipay') { + let state = '0'; + if (notifyData.trade_status === 'TRADE_SUCCESS' || notifyData.trade_status === 'TRADE_FINISHED') { + state = '2'; + } else if (notifyData.trade_status === 'TRADE_CLOSED') { + state = '3'; + } + jeepayNotify = { + payOrderId: notifyData.trade_no, + mchOrderNo: notifyData.out_trade_no, + amount: Math.round(parseFloat(notifyData.total_amount) * 100), + state: state, + extParam: notifyData.passback_params || '', + wayCode: 'alipay' + }; + } else { + jeepayNotify = notifyData; + } - // 转换为 易支付 格式的通知 const epayNotify = epayAdapter.handleNotify(jeepayNotify); - - // 从扩展参数中获取商户的 notify_url let notifyUrl = null; if (jeepayNotify.extParam) { try { - const extParamObj = JSON.parse(jeepayNotify.extParam); + let rawExtParam = jeepayNotify.extParam; + try { if (rawExtParam.includes('%')) rawExtParam = decodeURIComponent(rawExtParam); } catch(e) {} + const extParamObj = JSON.parse(rawExtParam); notifyUrl = extParamObj.epay_notify_url || null; - } catch (e) { - // extParam 不是 JSON 格式,忽略 - console.warn('extParam 解析失败:', e.message); - } + } catch (e) { console.warn('extParam 解析失败:', e.message); } } if (notifyUrl) { - // 转发通知到商户的 notify_url(使用 GET 方式) try { - const forwardResponse = await axios.get(notifyUrl, { - params: epayNotify, - timeout: 10000 // 10秒超时 - }); - - console.log('支付通知转发成功:', { - notifyUrl, - status: forwardResponse.status, - response: forwardResponse.data - }); + await axios.get(notifyUrl, { params: epayNotify, timeout: 10000 }); } catch (forwardError) { - console.error('支付通知转发失败:', { - notifyUrl, - error: forwardError.message - }); - // 转发失败不影响 Jeepay 通知的响应,但应该记录日志以便重试 + console.error('支付通知转发失败:', { notifyUrl, error: forwardError.message }); } - } else { - console.log('未找到商户 notify_url,跳过通知转发'); } - - // 必须返回 'success' 给 Jeepay res.send('success'); } catch (error) { console.error('处理支付通知失败:', error); @@ -310,90 +231,36 @@ app.post('/api/payment/notify', async (req, res) => { } }); -/** - * Jeepay 退款结果异步通知接口 - * POST /api/refund/notify - * 接收 Jeepay 的退款通知,转换为 易支付 格式并转发到商户的 notify_url - */ app.post('/api/refund/notify', async (req, res) => { try { - // 接收 Jeepay 格式的退款通知 - const jeepayRefundNotify = req.body; - - // 验证 Jeepay 通知签名 - if (!jeepay.verifyNotify(jeepayRefundNotify)) { - console.error('Jeepay 退款通知签名验证失败:', jeepayRefundNotify); + const refundNotify = req.body; + if (!payClient.verifyNotify(refundNotify)) { + console.error(`${mode} 退款通知签名验证失败:`, refundNotify); return res.send('fail'); } - - console.log('收到 Jeepay 退款通知:', { - refundOrderId: jeepayRefundNotify.refundOrderId, - payOrderId: jeepayRefundNotify.payOrderId, - mchRefundNo: jeepayRefundNotify.mchRefundNo, - refundAmount: jeepayRefundNotify.refundAmount, - state: jeepayRefundNotify.state - }); - - // 转换为 易支付 格式的退款通知 - const epayNotify = epayAdapter.handleRefundNotify(jeepayRefundNotify); - - // 从扩展参数中获取商户的 notify_url + const epayNotify = epayAdapter.handleRefundNotify(refundNotify); let notifyUrl = null; - if (jeepayRefundNotify.extParam) { + if (refundNotify.extParam) { try { - const extParamObj = JSON.parse(jeepayRefundNotify.extParam); + const extParamObj = JSON.parse(refundNotify.extParam); notifyUrl = extParamObj.epay_notify_url || null; - } catch (e) { - // extParam 不是 JSON 格式,忽略 - console.warn('extParam 解析失败:', e.message); - } + } catch (e) { } } - if (notifyUrl) { - // 转发通知到商户的 notify_url(使用 GET 方式) try { - const forwardResponse = await axios.get(notifyUrl, { - params: epayNotify, - timeout: 10000 // 10秒超时 - }); - - console.log('退款通知转发成功:', { - notifyUrl, - status: forwardResponse.status, - response: forwardResponse.data - }); - } catch (forwardError) { - console.error('退款通知转发失败:', { - notifyUrl, - error: forwardError.message - }); - // 转发失败不影响 Jeepay 通知的响应,但应该记录日志以便重试 - } - } else { - console.log('未找到商户 notify_url,跳过通知转发'); + await axios.get(notifyUrl, { params: epayNotify, timeout: 10000 }); + } catch (forwardError) { } } - - // 必须返回 'success' 给 Jeepay res.send('success'); } catch (error) { - console.error('处理退款通知失败:', error); res.send('fail'); } }); -// ========== 启动服务器 ========== app.listen(PORT, config.server.host, () => { console.log(`=================================`); - console.log(`支付平台 API 服务已启动`); + console.log(`KitfoxPay 服务已启动 [模式: ${mode}]`); console.log(`绑定地址: ${config.server.host}:${PORT}`); console.log(`服务地址: ${serverHost}`); - console.log(`配置信息:`); - console.log(`Jeepay:`); - console.log(` - Base URL: ${config.jeepay.baseUrl}`); - console.log(` - 商户号: ${config.jeepay.mchNo}`); - console.log(` - 应用ID: ${config.jeepay.appId}`); - console.log(`易支付:`); - console.log(` - 商户ID: ${config.epay.pid}`); - console.log(`网站域名: ${config.server.siteDomain}`); console.log(`=================================`); -}); +}); \ No newline at end of file diff --git a/package.json b/package.json index 3ecd388..af51197 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "", "license": "MIT", "dependencies": { + "alipay-sdk": "^4.14.0", "axios": "^1.6.0", "cors": "^2.8.5", "express": "^4.18.2",