Skip to main content

技术实现

普通商户模式#

1.服务端生成系统订单

2.服务端通过系统订单生成微信预支付订单

3.前端使用微信预支付订单信息调起微信支付页面

4.用户输入支付密码,完成支付

5.微信推送支付成功回调信息到服务端

6.服务端解析微信回调信息

提交微信预支付订单#

即统一下单,通过系统中的订单信息在微信侧生成一个预支付订单。返回的数据用于调起微信支付页面。

Native 支付

用户打开微信扫一扫功能,扫描商户二维码后完成支付。

JSAPI 支付

用户通过微信扫码、关注公众号等方式进入商家H5页面或在小程序中使用微信支付。

关键代码示例

// 引入资源包
const omit = require('/lodash/omit');
const crypto = require('crypto');
// 商户API密钥
const mchApiSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
/**
* 将 JSON 对象转为 XML 字符串。
* @param {object} json JSON 对象
* @returns {string} XML 字符串
*/
const toXML = json => {
let xml = '';
Object.keys(json).forEach(function(key) {
xml += `<${key}><![CDATA[${json[key]}]]></${key}>`;
});
return `<xml>${xml}</xml>`;
};
/**
* 生成签名。
* @param {Object} data 输入参数
* @param {string} type 签名类型(MD5、HMAC-SHA256)
* @returns {String} 签名
*/
const sign = (data, type) => {
data = omit(data, ['app', 'sign', 'media']);
let kvps = [];
Object.keys(data || {}).sort().forEach(key => {
if (typeof data[key] === 'string') {
data[key] = data[key].replace(/&quot;/g, '"');
}
if (!(data[key] === ''
|| Array.isArray(data[key]))) {
kvps.push(`${key}=${data[key]}`);
}
});
kvps.push(`key=${mchApiSecret}`);
let sign = '';
if (type.toUpperCase() === 'MD5') {
sign = crypto
.createHash('md5')
.update(kvps.join('&'))
.digest('hex')
} else if (type.toUpperCase() === 'HMAC-SHA256') {
sign = crypto
.createHmac('sha256', mchApiSecret)
.update(kvps.join('&'))
.digest('hex');
}
return sign.toUpperCase();
};

企业付款#

付款到微信用户零钱

商户付款给用户,资金进入微信用户零钱包。

商户打款资金默认使用基本户(或余额账户)余额;如商户已开通运营账户,则使用运营账户中的资金。

不支持给非实名用户打款
同一用户单笔每日限额5000元
商户每日最多可向同一用户付款10次
商户单日限额10万元

付款到银行卡

商户付款给指定银行卡。

商户单日限额10万元
商户单次限额2万元
商户给同一银行卡付款单日限额2万元

关键代码示例

// 引入资源包
const fs = require('fs');
const xml2json = require('xml2json');
const crypto = require('crypto');
const request = require('request');
// 商户号
const mchId = 'xxxxxxxxxxx';
// 商户API密钥
const mchApiSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// 商户证书路径
const mchCertPath = 'xxxxxxxxxxxxx', mchKeyPath = 'xxxxxxxxxxxx';
/**
* 获取 RSA 公钥
* @returns {*}
*/
const getRSAPublicKey = async () => {
let mchApiCert, mchApiKey;
try {
fs.accessSync(mchCertPath);
fs.accessSync(mchKeyPath);
mchApiCert = fs.readFileSync(mchCertPath);
mchApiKey = fs.readFileSync(mchKeyPath);
} catch(e) {
throw { code: 'error.wxpay.no-such-merchant-cert', message: '缺少商户证书' };
}
// 设置参数
let params = {
mch_id: mchId, // 商户 ID
nonce_str: createNonceString(), // 随机字符串
sign_type: 'MD5' // 签名类型
};
// 设置签名
params.sign = sign(params, 'MD5', mchApiSecret);
// 设置HTTP请求选项
let options = {
method: 'POST',
url: 'https://fraud.mch.weixin.qq.com/risk/getpublickey',
body: toXML(params),
key: mchApiCert,
cert: mchApiKey
};
// 发送请求
return new Promise((resolve, reject) => {
request(options, async (e, res, body) => {
if (e) {
reject(e);
} else {
try {
body = (JSON.parse(xml2json.toJson(body)) || {}).xml || {};
} catch (e) {
void (0);
}
if (body && body['return_code'] === 'SUCCESS' && body['result_code'] === 'SUCCESS' && body['pub_key']) {
try {
resolve(body['pub_key']);
} catch (e) {
reject(e);
}
} else {
reject(body);
}
}
});
});
};
/**
* RSA 公钥加密
* @param {string} key RSA公钥
* @param {string} str 需要加密的字段
* @returns {*}
*/
const RSAEncryptPublicKey = (key, str) => {
// 加密(`key`为 RSA 公钥;`padding` 为 crypto.constants.RSA_PKCS1_OAEP_PADDING)
let encryptStr = crypto.publicEncrypt({ key: key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(str));
// 转码
return encryptStr.toString('base64');
};

服务商模式#

1.前端收集客户方信息

2.服务端提交客户方信息至微信侧,代客户申请小微商户

3.服务端获取小微商户申请状态

4.小微商户申请成功后,服务端提交商户升级申请至微信侧

5.服务端获取商户升级申请状态

6.商户升级成功后,通知客户方签约特约商户

7.代特约商户执行支付系列操作

小微商户申请入驻#

微信支付官方文档

服务商代客户方提交入驻微信支付小微商户资料。

目前仅限于餐饮、零售、居民生活服务、休闲娱乐、交通出行行业签约小微商户,暂不支持通过此渠道签约线上虚拟行业。

小微商户申请入驻状态查询#

微信支付官方文档

提交小微商户入驻申请后5分钟左右可获取申请结果。

小微商户申请升级#

微信支付官方文档

根据客户的实际情况可将小微商户升级为个体户、企业、党政、机关及事业单位、其他组织。

小微商户升级申请状态查询#

微信支付官方文档

提交升级申请后,可不定期请求此接口获取申请状态,直至状态为FINISH完成。

若申请状态为待账户验证(ACCOUNT_NEED_VERIFY),需按接口中的指引引导客户进行银行账户打款验证。
若申请状态为审核中(AUDITING),微信侧会在2个工作日内完成资料审核。
若申请状态为待签约(NEED_SIGN),接口会返回签约二维码,需引导客户扫码进行商户签约。

代特约商户提交微信预支付订单#

Native 支付

用户打开微信扫一扫功能,扫描商户二维码后完成支付。

注:接口中的商户号为服务商的商户号,接口中的子商户号为服务商下的特约商户号。

JSAPI 支付

用户通过微信扫码、关注公众号等方式进入商家H5页面或在小程序中使用微信支付。

注:接口中的商户号为服务商的商户号,接口中的子商户号为服务商下的特约商户号。

关键代码示例

// 引用资源包
const request = require('request');
const crypto = require('crypto');
const xml2json = require('xml2json');
// 服务商商户号
const serviceMchId = 'xxxxxxxxxx';
// 服务商API密钥
const serviceMchApiSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
/**
* 解密微信平台证书
* @param {String} nonce 加密证书的随机串
* @param {String} associated 加密证书的随机串,固定值:certificate
* @param {String} cipherText 加密后的证书内容
* @returns {Promise<string>}
*/
const decryptPlatCert = async function({ nonce, associated, cipherText }) {
// 创建解码器
let decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(serviceMchApiSecret, 'binary'),
Buffer.from(nonce, 'binary')
);
decipher.setAutoPadding(true);
decipher.setAAD(Buffer.from(associated, 'binary'));
let cipherBuffer = Buffer.from(cipherText, 'base64').toString('binary');
let decrypted = decipher.update(cipherBuffer, 'binary', 'utf8');
try {
// 加了报错,不加乱码
decrypted += decipher.final("utf8");
} catch (e) {
void(0);
}
// 剔除尾部乱码
decrypted = decrypted.substring(0, decrypted.indexOf('-----END CERTIFICATE-----') + '-----END CERTIFICATE-----'.length);
return decrypted;
};
/**
* 获取微信平台证书
* @returns {*}
*/
const getPlatCert = async () => {
// 设置参数
let params = {
mch_id: serviceMchId, // 服务商商户 ID
nonce_str: createNonceString(), // 随机字符串
sign_type: 'HMAC-SHA256' // 签名方式
};
// 设置签名
params.sign = sign(params, 'HMAC-SHA256');
// 设置HTTP请求选项
let options = {
method: 'POST',
url: 'https://api.mch.weixin.qq.com/risk/getcertficates',
body: toXML(params)
};
// 发送请求
return new Promise((resolve, reject) => {
request(options, async (e, res, body) => {
if (e) {
reject(e);
} else {
try {
body = (JSON.parse(xml2json.toJson(body)) || {}).xml || {};
} catch (e) {
return reject(e);
}
if (body && body['return_code'] === 'SUCCESS' && body['result_code'] === 'SUCCESS' && body['certificates']) {
try {
let certificate = ((JSON.parse(body['certificates']) || {}).data || [])[0] || {};
certificate['encrypt_certificate'] = certificate['encrypt_certificate'] || {};
// 解密证书
if (certificate['encrypt_certificate']['nonce'] && certificate['encrypt_certificate']['ciphertext']) {
let certContent = await decryptPlatCert({
nonce: certificate['encrypt_certificate']['nonce'],
associated: certificate['encrypt_certificate']['associated_data'],
cipherText: certificate['encrypt_certificate']['ciphertext'] });
if (certContent) {
body['cert_serial_no'] = certificate['serial_no'];
body['cert_content'] = certContent;
delete body['certificates'];
}
}
resolve(body);
} catch (e) {
reject(e);
}
} else {
reject(body);
}
}
});
});
};