2026-07-27 11:28:33 +08:00

219 lines
7.3 KiB
JavaScript

const dayjs = require("dayjs");
const request = require("../../request");
const crypto = require('crypto');
const baseUrl = process.env.CONFIG_UNION_PAY_BASE_URL;
const appId = process.env.CONFIG_UNION_PAY_APPID;
const appKey = process.env.CONFIG_UNION_PAY_APPKEY;
const mid = process.env.CONFIG_UNION_PAY_MID;
const tid = process.env.CONFIG_UNION_PAY_TID;
const notifyUrl = process.env.CONFIG_UNION_PAY_NOTIFY;
const BillStatus = {
PAID: 'PAID', // 已支付
UNPAID: 'UNPAID', // 未支付
CLOSED: 'CLOSED', // 已关闭
REFUND: 'REFUND', // 已退款
UNKNOWN: 'UNKNOWN' // 未知状态
}
const accessToken = {
promise: null,
token: '',
expireTime: 0,
}
function getNonce() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 128; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
function getSignature(timestamp, nonce) {
const str = appId + timestamp + nonce + appKey;
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
}
exports.main = async (event) => {
switch (event.type) {
case 'closeUnpayTrade':
return await closeUnpayTrade(event);
case 'getUnionPayQrcode':
return await getUnionPayQrcode(event);
case 'getUnionPayBill':
return await getUnionPayBill(event);
case 'refundUnionPayBill':
return await refundUnionPayBill(event);
}
}
async function closePayQrcode(ctx) {
const qrCodeId = typeof ctx.qrCodeId === 'string' ? ctx.qrCodeId.trim() : '';
if (!qrCodeId) {
return { success: false, message: '参数错误' }
}
const data = {
qrCodeId,
mid,
tid,
instMid: 'QRPAYDEFAULT',
requestTimestamp: dayjs().format("YYYY-MM-DD HH:mm:ss")
}
const res = await unionPayRequest(`${baseUrl}/netpay/bills/close-qrcode`, data);
if (res.success) {
return { success: true, data: res.data, message: '银联支付二维码关闭成功' }
}
return res
}
async function closeUnpayTrade(ctx) {
const billNo = typeof ctx.billNo === 'string' ? ctx.billNo.trim() : '';
const billDate = ctx.billDate && dayjs(ctx.billDate).isValid() ? dayjs(ctx.billDate).format("YYYY-MM-DD") : '';
const qrCodeId = typeof ctx.qrCodeId === 'string' ? ctx.qrCodeId.trim() : '';
if (!billNo || !billDate || !qrCodeId) {
return { success: false, message: '参数错误' }
}
const { success, data, message } = await getUnionPayBill({ billNo, billDate });
if (!success) {
return { success: false, message };
}
if (data.billStatus == BillStatus.UNPAID) {
const res = await closePayQrcode({ qrCodeId });
if (!res.success) {
return res;
}
return { success: true, tradeStatus: 'closed', message: '银联支付订单关闭成功' }
}
if (data.billStatus == BillStatus.CLOSED) {
return { success: true, tradeStatus: 'closed', message: '银联支付订单已关闭' }
}
if (data.billStatus == BillStatus.PAID) {
return { success: false, tradeStatus: 'paid', message: '银联支付订单已支付' }
}
if (data.billStatus == BillStatus.REFUND) {
return { success: false, tradeStatus: 'refund', message: '银联支付订单已退款' }
}
return { success: false, message: '银联支付订单状态未知' }
}
async function getAccessToken() {
if (accessToken.token && Date.now() < accessToken.expireTime) {
return { success: true, data: accessToken.token, message: '获取access token成功' }
}
accessToken.promise = accessToken.promise || getUnionPayAccessToken();
const res = await accessToken.promise;
if (res.success) {
accessToken.token = res.data;
accessToken.expireTime = res.expireTime;
accessToken.promise = null;
}
return res;
}
async function getUnionPayAccessToken() {
const now = dayjs();
const params = {
appId,
timestamp: dayjs().format("YYYYMMDDHHmmss"),
nonce: getNonce(),
signMethod: 'SHA256',
}
params.signature = getSignature(params.timestamp, params.nonce);
const res = await request.main(`${baseUrl}/token/access`, params, "POST");
if (res && res.errCode === '0000') {
const expireTime = now.add(res.expiresIn, 'second').valueOf();
const token = `OPEN-ACCESS-TOKEN AccessToken=${res.accessToken}`;
return { success: true, data: token, message: '获取access token成功', expireTime }
}
return { success: false, message: `获取银联access token失败:${res?.errCode || res?.errInfo || '未知错误'}` }
}
async function getUnionPayBill(ctx) {
const billNo = typeof ctx.billNo === 'string' ? ctx.billNo.trim() : '';
const billDate = ctx.billDate && dayjs(ctx.billDate).isValid() ? dayjs(ctx.billDate).format("YYYY-MM-DD") : '';
if (!billNo || !billDate) {
return { success: false, message: '参数错误' }
}
const data = {
billNo,
billDate,
mid,
tid,
instMid: 'QRPAYDEFAULT',
requestTimestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"),
}
const res = await unionPayRequest(`${baseUrl}/netpay/bills/query`, data);
if (res.success) {
return { success: true, data: res.data, message: '查询银联支付订单成功' }
}
return res
}
async function getUnionPayQrcode(ctx) {
// 生产环境 银联分配的账单号必须以3JHR开头
if (typeof ctx.billNo !== 'string' || !ctx.billNo.startsWith('3JHR')) {
return { success: false, message: '账单号格式错误(以3JHR开头)' }
}
const data = {
mid,
tid,
instMid: 'QRPAYDEFAULT',
requestTimestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"),
billNo: ctx.billNo,
billDate: dayjs().format("YYYY-MM-DD"),
billDesc: ctx.desc || '',
totalAmount: ctx.totalAmount,
notifyUrl,
expireTime: ctx.expireTime
}
const res = await unionPayRequest(`${baseUrl}/netpay/bills/get-qrcode`, data);
if (res.success) {
return { success: true, notifyUrl, appId, qrCodeId: res.data.qrCodeId, qrcode: res.data.billQRCode, message: '获取银联支付二维码成功' }
}
return res
}
async function refundUnionPayBill(ctx) {
const billNo = typeof ctx.billNo === 'string' ? ctx.billNo.trim() : '';
const billDate = ctx.billDate && dayjs(ctx.billDate).isValid() ? dayjs(ctx.billDate).format("YYYY-MM-DD") : '';
const refundAmount = ctx.refundAmount > 0 && Number.isInteger(Number(ctx.refundAmount)) ? Number(ctx.refundAmount) : 0;
const refundOrderId = typeof ctx.refundOrderId === 'string' ? ctx.refundOrderId.trim() : '';
if (!billNo || !billDate || !refundAmount) {
return { success: false, message: '参数错误' }
}
const data = {
billDate: billDate,
billNo: billNo,
mid,
refundAmount,
tid,
instMid: 'QRPAYDEFAULT',
requestTimestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"),
}
if (refundOrderId) {
data.refundOrderId = refundOrderId;
}
const res = await unionPayRequest(`${baseUrl}/netpay/bills/refund`, data);
if (res.success) {
return { success: true, data: res.data, message: '银联支付订单退款成功' }
}
return res
}
async function unionPayRequest(url, data) {
const res = await getAccessToken();
if (!res || !res.success) {
return res
}
const headers = { Authorization: res.data };
const result = await request.main(url, data, "POST", { headers });
if (result && result.errCode === 'SUCCESS') {
return { success: true, data: result, message: '银联业务请求成功' }
}
return { success: false, message: `银联业务请求失败:${result?.errMsg || result?.errCode || '未知错误'}` }
}