pay.js 7.4 KB
/**
 * 民生支付模块
 * 封装民生银行支付流程,包括请求支付凭证、解密数据、调起微信支付等功能
 * @module common/pay
 */

import config from '@/config.js';
import request from './request'
import to from 'await-to-js'

/** 错误消息映射表 */
const ERROR_MESSAGES = {
	USER_CANCEL: '支付已取消',
	NETWORK_ERROR: '网络异常,请检查网络连接',
	INVALID_PARAMS: '参数错误',
	DECRYPT_FAILED: '数据解密失败',
	PARSE_FAILED: '数据解析失败',
	PAY_INFO_MISSING: '支付信息缺失'
}

/**
 * 民生支付主函数
 * @async
 * @param {string} orderId - 订单ID,用于错误处理和取消订单
 * @param {string} encryptedData - 加密数据
 * @returns {Promise<void>}
 * @throws {Error} 支付过程中发生的错误
 */
async function minShengPay(orderId, encryptedData) {
	uni.showLoading({
		title: '生成支付...',
		mask: true
	})

	// 参数校验
	if (!orderId) {
		console.error('minShengPay: orderId is required')
		handlePaymentError(new Error(ERROR_MESSAGES.INVALID_PARAMS), null)
	}

	if (!encryptedData) {
		handlePaymentError(new Error(ERROR_MESSAGES.INVALID_PARAMS), orderId)
	}

	// 1. 数据准备:将表单数据转换为 URL 编码格式
	const encodedData = buildUrlEncodedData({
		context: encryptedData
	})

	// 2. 请求支付凭证
	const [reqErr, res] = await to(requestPaymentCredential(encodedData))
	if (reqErr) {
		handlePaymentError(reqErr, orderId)
	}

	// 3. 响应验证
	if (!res?.data?.businessContext) {
		handlePaymentError({
			message: res?.data?.gateReturnMessage
		}, orderId)
	}

	// 4. 解密并处理数据
	const [decryptErr, decryptResult] = await to(decrypt({
		bussinessContext: res.data.businessContext
	}))

	if (decryptErr || !decryptResult?.data) {
		handlePaymentError(new Error(ERROR_MESSAGES.DECRYPT_FAILED), orderId)
	}

	// 5. 解析 JSON 数据
	const parsedData = safeJsonParse(decryptResult.data)
	if (!parsedData) {
		handlePaymentError(new Error(ERROR_MESSAGES.PARSE_FAILED), orderId)
	}

	// 6. 验证支付信息
	if (!parsedData.payInfo) {
		handlePaymentError(new Error(ERROR_MESSAGES.PAY_INFO_MISSING), orderId)
	}

	// 7. 解析支付参数
	const payParams = parsePayInfo(parsedData.payInfo)

	uni.hideLoading()
	// 8. 调起微信支付
	const [payErr] = await to(invokeWechatPayment(payParams, orderId))
	if (payErr) {
		handlePaymentError(payErr, orderId)
	}
}

/**
 * 将对象转换为 URL 编码格式字符串
 * @param {Object} data - 需要转换的数据对象
 * @returns {string} URL 编码格式字符串
 */
function buildUrlEncodedData(data) {
	return Object.entries(data)
		.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
		.join('&')
}

/**
 * 请求支付凭证
 * @async
 * @param {string} encodedData - URL 编码格式的请求数据
 * @returns {Promise<Object>} 支付凭证响应
 */
function requestPaymentCredential(encodedData) {
	return uni.request({
		url: config.payUrl,
		method: 'POST',
		header: {
			'Content-Type': 'application/x-www-form-urlencoded'
		},
		data: encodedData
	})
}

/**
 * 解密支付数据(待实现)
 * @param {Object} params - 解密参数
 * @param {string} params.bussinessContext - 业务上下文数据
 * @returns {Promise<Object>} 解密后的数据
 */
function decrypt(params) {
	return request({
		url: `/api/decrypt`,
		method: 'post',
		params
	})
}

/**
 * 安全解析 JSON 字符串
 * @param {string} jsonString - JSON 字符串
 * @returns {Object|null} 解析后的对象,解析失败返回 null
 */
function safeJsonParse(jsonString) {
	try {
		return JSON.parse(jsonString)
	} catch (e) {
		console.error('JSON parse error:', e)
		return null
	}
}

/**
 * 解析支付信息字符串
 * 将 "key1=value1|key2=value2" 格式转换为对象
 * @param {string} payInfoStr - 支付信息字符串,格式为 "key1=value1|key2=value2|..."
 * @returns {Object} 解析后的支付参数对象
 * @example
 * // 返回 { appId: 'wx123', prepayId: 'abc' }
 * parsePayInfo('appId=wx123|prepayId=abc|')
 */
function parsePayInfo(payInfoStr) {
	if (!payInfoStr || typeof payInfoStr !== 'string') {
		return {}
	}

	const result = {}

	payInfoStr.split('|').forEach(item => {
		if (!item) return

		const index = item.indexOf('=')
		if (index === -1) return

		const key = item.substring(0, index)
		const value = item.substring(index + 1)
		result[key] = value
	})

	return result
}

/**
 * 调起微信支付
 * @param {Object} payParams - 微信支付参数
 * @param {string} payParams.appId - 应用ID
 * @param {string} payParams.nonceStr - 随机字符串
 * @param {string} payParams.prepayId - 预支付ID
 * @param {string} payParams.timeStamp - 时间戳
 * @param {string} payParams.signType - 签名类型
 * @param {string} payParams.paySign - 支付签名
 * @param {string} orderId - 订单ID
 * @returns {Promise<Object>} 支付结果
 */
function invokeWechatPayment(payParams, orderId) {
	debugger
	return new Promise((resolve, reject) => {
		// 参数校验
		const requiredFields = ['appId', 'nonceStr', 'prepayId', 'timeStamp', 'signType', 'paySign']
		const missingFields = requiredFields.filter(field => !payParams?.[field])

		if (missingFields.length > 0) {
			reject(new Error(`缺少必要支付参数: ${missingFields.join(', ')}`))
			return
		}

		uni.requestPayment({
			appId: payParams.appId,
			nonceStr: payParams.nonceStr,
			package: `prepay_id=${payParams.prepayId}`,
			timeStamp: payParams.timeStamp,
			signType: payParams.signType,
			paySign: payParams.paySign,
			success: (res) => {
				uni.showToast({
					title: '支付成功',
					duration: 2000,
					complete: () => resolve(res)
				})
			},
			fail: async (err) => {
				debugger
				// 用户取消支付
				if (err.errMsg?.includes('cancel')) {
					await handleUserCancel(orderId)
					reject(new Error('USER_CANCEL'))
				} else {
					await handlePaymentFailure(orderId, err)
					reject(err)
				}
			}
		})
	})
}

/**
 * 处理用户取消支付
 * @async
 * @param {string} orderId - 订单ID
 * @returns {Promise<void>}
 */
async function handleUserCancel(orderId) {
	const [err] = await to(request({
		url: 'cancelOrder',
		method: 'get',
		params: {
			orderId
		}
	}))

	if (err) {
		console.error('取消订单失败:', err)
		return
	}

	uni.showToast({
		title: '支付取消',
		icon: 'none',
		duration: 2000
	})
}

/**
 * 处理支付失败
 * @async
 * @param {string} orderId - 订单ID
 * @param {Error|Object} error - 错误对象
 * @returns {Promise<void>}
 */
async function handlePaymentFailure(orderId, error) {
	console.error(`支付失败 [订单: ${orderId}]:`, error)
	// 可在此处添加支付失败后的业务逻辑,如上报错误等
}

/**
 * 统一错误处理(处理后会抛出错误)
 * @param {Error} error - 错误对象
 * @param {string|null} orderId - 订单ID
 * @throws {Error} 始终抛出传入的错误对象
 */
function handlePaymentError(error, orderId) {
	uni.hideLoading()

	const message = ERROR_MESSAGES[error?.message] || error?.message || '支付失败,请重试'

	uni.showToast({
		title: message,
		icon: 'none'
	})

	// 记录错误日志,便于排查
	console.error(`支付错误 [订单: ${orderId}]:`, error)

	throw error
}

export {
	minShengPay
}