const axios = require('axios') const config = require('../config.json') function getPaymentConfig() { return config.pay || {} } function normalizePayBaseUrl(url) { return String(url || '').trim().replace(/\/+$/, '') } function buildPaymentApiUrl(act, params = {}) { const paymentConfig = getPaymentConfig() const baseUrl = normalizePayBaseUrl(paymentConfig.url) if (!baseUrl) { throw new Error('支付配置错误') } const url = new URL(`${baseUrl}/api.php`) url.searchParams.set('act', act) for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null || value === '') continue url.searchParams.set(key, String(value)) } return url.toString() } function parsePaymentResponseBody(data) { if (data && typeof data === 'object') return data if (typeof data !== 'string') return null const text = data.trim() if (!text) return null try { return JSON.parse(text) } catch (_) { return null } } function getAxiosPaymentOptions(extra = {}) { return { timeout: 15000, maxRedirects: 5, proxy: false, validateStatus: (status) => status >= 200 && status < 500, ...extra } } function formatPaymentHttpError(error, fallbackMessage) { if (error?.code === 'ECONNABORTED') { return '支付平台响应超时,请稍后重试' } if (error?.code === 'ERR_FR_TOO_MANY_REDIRECTS') { return '支付平台重定向异常,请检查 pay.url 配置或关闭系统代理后重试' } const status = Number(error?.response?.status || 0) const parsed = parsePaymentResponseBody(error?.response?.data) if (parsed?.msg) return String(parsed.msg) if (status === 503) return '支付平台暂时不可用,请稍后重试' if (status >= 500) return `支付平台异常(${status}),请稍后重试` return fallbackMessage || error?.message || '支付平台请求失败' } async function queryPaymentOrder(orderId, logger) { const paymentConfig = getPaymentConfig() if (!paymentConfig.pid || !paymentConfig.url || !paymentConfig.key) { throw new Error('支付配置错误') } const queryUrl = buildPaymentApiUrl('order', { pid: paymentConfig.pid, key: paymentConfig.key, out_trade_no: orderId }) let response try { response = await axios.get(queryUrl, getAxiosPaymentOptions()) } catch (error) { const message = formatPaymentHttpError(error, '查询支付状态失败') logger?.warn?.(`查询支付状态失败,订单号:${orderId},URL:${queryUrl},原因:${message}`) throw new Error(message) } const result = parsePaymentResponseBody(response.data) if (!result) { logger?.warn?.(`支付平台返回非 JSON,订单号:${orderId},HTTP ${response.status}`) throw new Error('支付平台响应异常') } return result } function formatPaymentRefundError(response, fallbackMessage) { return formatPaymentHttpError({ response }, fallbackMessage || '支付平台退款失败') } async function requestPaymentRefund({ orderId, tradeNo, money, logger }) { const paymentConfig = getPaymentConfig() if (!paymentConfig.url || !paymentConfig.pid || !paymentConfig.key) { throw new Error('支付配置错误') } const refundUrl = buildPaymentApiUrl('refund') const params = new URLSearchParams() params.append('pid', String(paymentConfig.pid)) params.append('key', paymentConfig.key) if (tradeNo) { params.append('trade_no', tradeNo) } else { params.append('out_trade_no', orderId) } params.append('money', String(money)) let response try { response = await axios.post( refundUrl, params.toString(), getAxiosPaymentOptions({ timeout: 30000, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, validateStatus: () => true }) ) } catch (error) { logger?.error?.(`易支付退款请求失败 订单号:${orderId}:${error.stack || error}`) throw new Error(formatPaymentHttpError(error, '无法连接支付平台,请稍后重试')) } const result = parsePaymentResponseBody(response.data) logger?.info?.(`易支付退款响应 订单号:${orderId},HTTP ${response.status},结果:${JSON.stringify(result ?? response.data)}`) if (response.status >= 400) { throw new Error(formatPaymentRefundError(response, '支付平台退款失败')) } if (!result || Number(result.code) !== 1) { throw new Error(result?.msg || '支付平台退款失败') } return result } module.exports = { normalizePayBaseUrl, buildPaymentApiUrl, getAxiosPaymentOptions, formatPaymentHttpError, formatPaymentRefundError, parsePaymentResponseBody, queryPaymentOrder, requestPaymentRefund }