UNPKG

rapid-unipay

Version:

UnionPay module

382 lines (349 loc) 13 kB
/** * 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, }