rapid-unipay
Version:
UnionPay module
382 lines (349 loc) • 13 kB
JavaScript
/**
* Copyright (c) 2017 Lucky Byte, Inc.
*/
const debug = require('debug')('unipay');
const crypto = require('crypto');
const moment = require('moment');
const winston = require('winston');
const utils = require('../lib/utils');
const post = require('../lib/post');
/**
* 标准模式支付开通
*
* 参数:
* params: 一个 JSON 对象,包含下列的 key:
* * merno: 商户代码
* * cert_sn: 签名证书 Serial Number
* * sign_pem: 签名证书,PEM
* * order_id: 订单号
* * front_url: 前台通知地址
* * back_url: 后台通知地址
* * acct_no: 账号
* * encrypt_cer: 加密证书,可选
* * encrypt_cer_sn: 加密证书编号,可选
*
* 返回
* JSON 报文,这个报文后续传递给 open_html() 生成可跳转支付网页
*/
const open_reqt = async (params) => {
if (!params) {
throw new Error('银联无跳转支付开通请求参数无效,用法 open_reqt({...})');
}
const required_keys = [
'merno', 'cert_sn', 'sign_pem', 'acct_no', 'order_id', 'front_url', 'back_url'
]
for (let i = 0; i < required_keys.length; i++) {
const key = required_keys[i];
if (!params[key]) {
throw new Error(`银联无跳转支付开通请求参数缺少[${key}]`);
}
}
let reqt_json = {
version: '5.0.0',
encoding: 'UTF-8',
certId: params.cert_sn,
signMethod: '01',
txnType: '79',
txnSubType: '00',
bizType: '000301',
accessType: '0',
channelType: '07',
txnTime: moment().format('YYYYMMDDHHmmss'),
merId: params.merno,
orderId: params.order_id,
frontUrl: params.front_url,
backUrl: params.back_url,
}
// 可以提供版本号,默认是 5.0.0
if (params.version) {
if (params.version != '5.0.0' && params.version != '5.1.0') {
throw new Error('参数 version 无效,必须是 5.0.0 或 5.1.0');
}
reqt_json.version = params.version;
}
// 如果提供了账号,则将其添加到请求中,可以进行锁卡
if (params.encrypt_cer) {
if (!params.encrypt_cer_sn) {
throw new Error('提供了加密证书,但未提供加密证书ID');
}
reqt_json.encryptCertId = params.encrypt_cer_sn;
reqt_json.accNo = utils.pubkey_encrypt(params.encrypt_cer, params.acct_no);
} else {
reqt_json.accNo = params.acct_no;
}
// 可以传递锁卡等参数
if (params.reserved) {
reqt_json.reserved = params.reserved;
}
reqt_json.signature = utils.gen_reqt_sign(reqt_json, params.sign_pem);
winston.info('银联标准支付开通请求报文:', JSON.stringify(reqt_json, null, 2));
return reqt_json;
}
/**
* 生成 HTML 页面在浏览器中打开将自动跳转到银联网关进行开通
*
* 参数
* dest_url: 银联网页支付请求地址
* reqt_json: 请求报文,JSON 格式,通过 open_reqt() 生成
* 返回
* HTML 网页,在客户端使用内置 WebView 打开此网页将自动跳转到银联网站进行开通
*/
const open_html = async (dest_url, reqt_json) => {
if (!dest_url || !reqt_json) {
throw new Error('参数无效,用法 open_html(dest_url, reqt_json)');
}
return utils.build_submit_html(dest_url, reqt_json, 'UnionPayStd');
}
/**
* 标准模式开通查询
*
* 参数:
* dest_url: 银联请求地址
* params: 一个 JSON 对象,包含下列的 key:
* * merno: 商户代码
* * cert_sn: 签名证书 Serial Number
* * sign_pem: 签名证书,PEM
* * verify_cer: 验证响应报文签名证书
* * order_id: 订单号
* * acct_no: 账号
*
* 返回
* JSON,包含 2 个字段:
* reqt: 请求报文, JSON
* resp: 响应报文, JSON, 通过报文中 respCode 确定交易是否成功
*/
const open_query = async (dest_url, params) => {
if (!dest_url || !params) {
throw new Error('银联无跳转支付开通查询参数无效,用法 open_query(dest_url, {...})');
}
const required_keys = [
'merno', 'cert_sn', 'sign_pem', 'verify_cer', 'acct_no', 'order_id'
]
for (let i = 0; i < required_keys.length; i++) {
const key = required_keys[i];
if (!params[key]) {
throw new Error(`银联无跳转支付开通查询请求参数缺少[${key}]`);
}
}
let reqt_json = {
version: '5.0.0',
encoding: 'UTF-8',
certId: params.cert_sn,
signMethod: '01',
txnType: '78',
txnSubType: '00',
bizType: '000301',
accessType: '0',
channelType: '07',
txnTime: moment().format('YYYYMMDDHHmmss'),
merId: params.merno,
orderId: params.order_id,
}
// 可以提供版本号,默认是 5.0.0
if (params.version) {
if (params.version != '5.0.0' && params.version != '5.1.0') {
throw new Error('参数 version 无效,必须是 5.0.0 或 5.1.0');
}
reqt_json.version = params.version;
}
if (params.encrypt_cer) {
if (!params.encrypt_cer_sn) {
throw new Error('提供了加密证书,但未提供加密证书ID');
}
reqt_json.encryptCertId = params.encrypt_cer_sn;
reqt_json.accNo = utils.pubkey_encrypt(params.encrypt_cer, params.acct_no);
} else {
reqt_json.accNo = params.acct_no;
}
reqt_json.signature = utils.gen_reqt_sign(reqt_json, params.sign_pem);
winston.info('银联无跳转支付开通查询请求报文:', JSON.stringify(reqt_json, null, 2));
const resp_data = await post(dest_url, reqt_json);
const resp_json = utils.parse_resp_data(resp_data);
winston.info('银联无跳转支付开通查询响应报文:', JSON.stringify(resp_json, null, 2));
if (!utils.verify_resp_sign(resp_json, params.verify_cer)) {
throw new Error('验证无跳转支付开通查询响应报文签名失败');
}
winston.info('验证无跳转支付开通查询响应报文签名通过');
return { reqt: reqt_json, resp: resp_json }
}
/**
* 标准模式发送消费短信验证码,后台同步交易
*
* 参数:
* dest_url: 银联请求地址
* params: 一个 JSON 对象,包含下列的 key:
* * merno: 商户代码
* * cert_sn: 签名证书 Serial Number
* * sign_pem: 签名证书,PEM
* * verify_cer: 验证响应报文签名证书
* * order_id: 订单号
* * txn_time: 交易时间,YYYYMMDDHHmmss
* * acct_no: 账号
* * mobile: 手机号
* * cvn: CVN
* * expiry: 有效期
* * amount: 交易金额,单位为分
*
* 返回
* JSON,包含 2 个字段:
* reqt: 请求报文, JSON
* resp: 响应报文, JSON, 通过报文中 respCode 确定交易是否成功
*/
const sendsms = async (dest_url, params) => {
if (!dest_url || !params) {
throw new Error('银联无跳转支付发送短信参数无效,用法 sendsms(dest_url, {...})');
}
const required_keys = [
'merno', 'cert_sn', 'sign_pem', 'verify_cer', 'acct_no', 'cvn', 'expiry',
'mobile', 'order_id', 'txn_time'
]
for (let i = 0; i < required_keys.length; i++) {
const key = required_keys[i];
if (!params[key]) {
throw new Error(`银联无跳转支付发送短信请求参数缺少[${key}]`);
}
}
let reqt_json = {
version: '5.0.0',
encoding: 'UTF-8',
certId: params.cert_sn,
signMethod: '01',
txnType: '77',
txnSubType: '02',
bizType: '000301',
accessType: '0',
channelType: '08',
txnTime: params.txn_time,
merId: params.merno,
orderId: params.order_id,
accType: '01',
}
// 可以提供版本号,默认是 5.0.0
if (params.version) {
if (params.version != '5.0.0' && params.version != '5.1.0') {
throw new Error('参数 version 无效,必须是 5.0.0 或 5.1.0');
}
reqt_json.version = params.version;
}
if (params.amount) {
reqt_json.currencyCode = 156;
reqt_json.txnAmt = params.amount;
}
if (params.encrypt_cer) {
if (!params.encrypt_cer_sn) {
throw new Error('提供了加密证书,但未提供加密证书ID');
}
reqt_json.encryptCertId = params.encrypt_cer_sn;
reqt_json.accNo = utils.pubkey_encrypt(params.encrypt_cer, params.acct_no);
} else {
reqt_json.accNo = params.acct_no;
}
const encrypted_info = utils.build_encrypted_info({
mobile: params.mobile, cvn: params.cvn, expiry: params.expiry,
}, params.encrypt_cer);
reqt_json.customerInfo = utils.build_cust_info({
encrypted_info: encrypted_info
});
reqt_json.signature = utils.gen_reqt_sign(reqt_json, params.sign_pem);
winston.info('银联无跳转支付发送短信请求报文:', JSON.stringify(reqt_json, null, 2));
const resp_data = await post(dest_url, reqt_json);
const resp_json = utils.parse_resp_data(resp_data);
winston.info('银联无跳转支付发送短信响应报文:', JSON.stringify(resp_json, null, 2));
if (!utils.verify_resp_sign(resp_json, params.verify_cer)) {
throw new Error('验证无跳转支付发送短信响应报文签名失败');
}
winston.info('验证无跳转支付发送短信响应报文签名通过');
return { reqt: reqt_json, resp: resp_json }
}
/**
* 标准模式消费
*
* 参数:
* dest_url: 银联请求地址
* params: 一个 JSON 对象,包含下列的 key:
* * merno: 商户代码
* * cert_sn: 签名证书 Serial Number
* * sign_pem: 签名证书,PEM
* * verify_cer: 验证响应报文签名证书
* * order_id: 订单号
* * amount: 交易金额,单位为分
* * txn_time: 交易时间,YYYYMMDDHHmmss
* * acct_no: 卡号
* * smscode: 短信验证码
* * front_url: 前台通知地址
* * back_url: 后台通知地址
*
* 返回
* JSON,包含 2 个字段:
* reqt: 请求报文, JSON
* resp: 响应报文, JSON, 通过报文中 respCode 确定交易是否成功
*/
const consume = async (dest_url, params) => {
if (!dest_url || !params) {
throw new Error('银联无跳转消费参数无效,用法 consume(dest_url, {...})');
}
const required_keys = [
'merno', 'cert_sn', 'sign_pem', 'verify_cer', 'acct_no',
'order_id', 'amount', 'txn_time', 'front_url', 'back_url'
]
for (let i = 0; i < required_keys.length; i++) {
const key = required_keys[i];
if (!params[key]) {
throw new Error(`银联无跳转支付消费请求参数缺少[${key}]`);
}
}
let reqt_json = {
version: '5.0.0',
encoding: 'UTF-8',
certId: params.cert_sn,
signMethod: '01',
txnType: '01',
txnSubType: '01',
bizType: '000301',
accessType: '0',
channelType: '07',
txnTime: params.txn_time,
merId: params.merno,
orderId: params.order_id,
currencyCode: 156,
txnAmt: params.amount,
frontUrl: params.front_url,
backUrl: params.back_url,
}
// 可以提供版本号,默认是 5.0.0
if (params.version) {
if (params.version != '5.0.0' && params.version != '5.1.0') {
throw new Error('参数 version 无效,必须是 5.0.0 或 5.1.0');
}
reqt_json.version = params.version;
}
if (params.encrypt_cer) {
if (!params.encrypt_cer_sn) {
throw new Error('提供了加密证书,但未提供加密证书ID');
}
reqt_json.encryptCertId = params.encrypt_cer_sn;
reqt_json.accNo = utils.pubkey_encrypt(params.encrypt_cer, params.acct_no);
} else {
reqt_json.accNo = params.acct_no;
}
reqt_json.customerInfo = utils.build_cust_info({
smscode: params.smscode
});
reqt_json.signature = utils.gen_reqt_sign(reqt_json, params.sign_pem);
winston.info('银联无跳转消费请求报文:', JSON.stringify(reqt_json, null, 2));
const resp_data = await post(dest_url, reqt_json);
const resp_json = utils.parse_resp_data(resp_data);
winston.info('银联无跳转消费响应报文:', JSON.stringify(resp_json, null, 2));
if (!utils.verify_resp_sign(resp_json, params.verify_cer)) {
throw new Error('验证无跳转消费响应报文签名失败');
}
winston.info('验证无跳转消费响应报文签名通过');
return { reqt: reqt_json, resp: resp_json }
}
module.exports = {
open_reqt: open_reqt,
open_html: open_html,
open_query: open_query,
sendsms: sendsms,
consume: consume,
}