- AES-GCM在Node10解密消息时有可能崩溃,使用云开发环境的同学需要进来看一下
背景知识 [代码]AES-GCM[代码]是微信支付APIv3的加解密方案之一,定义可见rfc5116,v3使用的是[代码]aead_aes_256_gcm[代码]。稍微补充一个[代码]aead[代码]的的描述,[代码]aead[代码]加密方式与其他对称加密方式主要不同的地方就是: 它每一段密文必定有对应的校验码,通过核对校验码来判断密文是否完整。 APIv3回调通知和平台证书下载文档上有介绍[代码]AES-GCM[代码]的使用场景。nodejs原生[代码]crypto[代码]模块,在处理[代码]GCM[代码]模式解密时,从变更历史上看,[代码]Node11[代码]加入了强制校验[代码]auth_tag[代码](authentication tag)长度规则,[代码]Node10[代码]目前全系列还没有合并这个向前兼容规则,详情可见 https://github.com/nodejs/node/pull/20039 。 测试代码 先上一段测试用js代码,来复现 nodejs#20039 上连带反馈的问题: [代码]const crypto = require('crypto') const decrypt = (ciphertext, key, iv, aad = '') => { const buf = Buffer.from(ciphertext, 'base64') const tag = buf.slice(-16) const payload = buf.slice(0, -16) const decipher = crypto.createDecipheriv( 'aes-256-gcm', key, iv ).setAuthTag(tag).setAAD(Buffer.from(aad)) return Buffer.concat([ decipher.update(payload, 'hex'), decipher.final() ]).toString('utf8') } const mockupIv = 'abcdef0123456789' const mockupKey = 'abcdef0123456789abcdef0123456789' try { decrypt('', mockupKey, mockupIv) } catch {} [代码] 上述代码,在node10.15-10.24,均抛出如下不可捕获的错误(fatal error),程序会直接挂掉,在12-15之间,可以正常运行。 错误日志 类似如下: [代码]node[97219]: ../src/node_crypto.cc:3047:CipherBase::UpdateResult node::crypto::CipherBase::Update(const char *, int, unsigned char **, int *): Assertion `MaybePassAuthTagToOpenSSL()' failed. 1: 0x100d69661 node::Abort() (.cold.1) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 2: 0x10003aeb4 node_module_register [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 3: 0x100039fb9 node::AddEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 4: 0x100112fae node::StringBytes::InlineDecoder::Decode(node::Environment*, v8::Local<v8::String>, v8::Local<v8::Value>, node::encoding) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 5: 0x1001119dc node::crypto::CipherBase::Update(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 6: 0x1002386c3 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 7: 0x100237bae v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 8: 0x10023728a v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 9: 0x37d3d8d5bf3d 10: 0x37d3d8d118d5 11: 0x37d3d8d0a5c3 12: 0x37d3d8d118d5 13: 0x37d3d8d0a5c3 [1] 97218 abort npm test [代码] 上述错误日志,发生在我本地的[代码]Node10[代码]环境中。我花了几个小时,翻了好几遍github issues,最后找到了 nodejs#20039 pull requests,通读下来并反复测试了10.19-10.24版本,均无法正常捕获,这应该是上述pr没合并至[代码]Node10[代码]系列所致。 产生条件 稍微分析一下,可能产生致命错误的条件: 密文为空字符串时,程序会崩 密文为 [代码]Cg==[代码](base64空字符串) CLI会有 Warning DEP0090 弹出 (node:987) [DEP0090] DeprecationWarning: Permitting authentication tag lengths of 1 bytes is deprecated. Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16. 微信支付官方文档在解密示例代码 常量定义了这个[代码]auth_tag[代码]长度为128位16字节,匹配rfc5116规范并且取的是最大值。 这下问题来了,万一无法正常获取到待解密字符串或者获取到的是空字符串,[代码]GCM[代码]模式校验码位又必须是16字节,业务逻辑又强依赖解密后字符串(验签证书是v3通讯强依赖)这崩掉了,着急上火的可真就是摊上事儿了! 向前兼容方案 找到问题关键点,那就打个业务逻辑补丁:应用端,对输入待解密字符串,做长度校验,长度为0的,不进入解密函数;或者可以采用如下向前兼容js patch补丁: [代码]- ).setAuthTag(tag).setAAD(Buffer.from(aad)) + ) + + // Restrict valid GCM tag length, patches for Node < 11.0.0 + // more @see https://github.com/nodejs/node/pull/20039 + const tagLen = tag.length + if (tagLen > 16 || (tagLen < 12 && tagLen != 8 && tagLen != 4)) { + let backport = new TypeError(`Invalid authentication tag length: ${tagLen}`) + backport.code = 'ERR_CRYPTO_INVALID_AUTH_TAG' + throw backport + } + decipher.setAuthTag(tag).setAAD(Buffer.from(aad)) [代码] 上述代码取自 wechatpay-axios-plugin@aa36a56,也已随源码用例覆盖[代码]Node[代码]10-15版本,均达预期,可安全使用。 可能的影响面 小程序云开发标配目前是[代码]Node10[代码],不清楚云开发团队在处理[代码]消息通知及关键信息解密[代码]时,是否采用的是轻量化如nodejs原生[代码]crypto[代码]这样的解决方案,这个就需要云产品团队相关的同学进来看看,评估一下有无风险点了。 对自主对接云开发的开发者来说,建议尽快给打下业务逻辑补丁或者程序解密补丁,避免不可预期的错误发生(虽然极小概率,但支付的事,可真不是小事儿)。 题外话 建议云开发平台,能够升级一下[代码]Node10[代码]至最新[代码]lts[代码]运行时,一并建议能同时支持[代码]Node12[代码]、[代码]Node14[代码]运行时。
2021-03-10 - 云函数V3支付全家桶
// 云函数入口文件 var cloud = require('wx-server-sdk') crypto = require('crypto') request = require('request') NodeRSA = require('node-rsa')//npm安装 Form = require('./Form.class') //Form 源自https://https://developers.weixin.qq.com/community/develop/article/doc/000c24f0390ff8b5d91b2489059413 //再次感谢社区北望大佬 cloud.init({ env: 'xxxxxx' }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const rq = options => new Promise((resolve, reject) => {...}) // 基本参数 var facilitator = event.facilitator=='undefined'?false:event.facilitator//facilitator=true时为服务商模式,false为直连模式 console.log(`${facilitator?'服务商':'直连'}模式`) var mchid = facilitator?'160xxxxx92':'16xxxxx84'//服务商or直连商户号 var appId = 'wx41xxxxxxfd6' //服务商和直连商户的appId var APIv3Key = facilitator?'xxxxxxxxx':'xxxxxxxxxx'//服务商or直连商户APIv3密钥 var cert_Doc = facilitator?'certificates':'direct_certificates' //存在数据库的证书记录的_id var timeStamp = parseInt(Date.now()/1000) var nonce_str = Math.random().toString(36).substr(2, 13) var url = 'https://api.mch.weixin.qq.com'+event.url //证书与私钥 var certificates = (await db.collection('a-data').doc(cert_Doc).get()).data //存在数据库的证书 var {serial_no} = certificates var {wx_serial_no} = certificates // 敏感信息加密 var RSAoaep = (e)=>{...} // getHeaders const getHeaders = (method,meta)=>{...} // JSAPI支付 if(event.url.indexOf('/transactions/jsapi')>-1){ var {data} = event if(facilitator){//服务商模式 data.sp_mchid = mchid data.sp_appid = appId }else{//直连模式 data.mchid = mchid data.appid = appId } ..... return { payment,//前端由此唤起支付 errMsg:'下单成功', errCode:0 } } // 申请退款 if(event.url=='/v3/refund/domestic/refunds'){ .... } // 特约商户进件:提交申请单 if(event.url=='/v3/applyment4sub/applyment/'){ .... } // 特约商户进件:图片上传接口 if(event.url=='/v3/merchant/media/upload'){ ..... } // 特约商户进件:查询申请单状态 if(event.url.indexOf('/v3/applyment4sub/applyment/business_code')>-1||event.url.indexOf('/v3/applyment4sub/applyment/applyment_id')>-1){ ...... } // 检测微信支付平台证书是否更新,该方法应定期且间隔不超过12小时执行一次,我是在更新公众号access_token的方法中调用,1h一次 if(event.url=='/v3/certificates'){ ...... } /*更新平台证书方法(由于云函数nodejs版本过低,该方法请本地运行): if(event.url=='/v3/certificates'){ .... } */ } 感兴趣请私信交流
2021-08-28 - 从APIv3到APIv2再到企业微信,这款微信支付开发包的README你应该来读一读
The WeChatPay OpenAPI v2&v3’ Smart Development Kit [图片][图片][图片][图片][图片][图片] 主要功能 使用Node原生[代码]crypto[代码]实现微信支付APIv3的AES加/解密功能([代码]aes-256-gcm[代码] with [代码]aad[代码]) 使用Node原生[代码]crypto[代码]实现微信支付APIv3的RSA加/解密、签名、验签功能([代码]sha256WithRSAEncryption[代码] with [代码]RSA_PKCS1_OAEP_PADDING[代码]) 大部分微信支付APIv3的HTTP GET/POST/PUT/PATCH/DELETE应该能够正常工作,依赖 Axios, 示例代码如下 支持微信支付APIv3的媒体文件上传(图片/视频)功能,需手动安装 form-data, 示例代码如下 支持微信支付APIv3的应答证书下载功能,需手动安装 yargs, 使用手册如下 支持微信支付APIv3的帐单下载及解析功能,示例代码如下 支持微信支付APIv2 & APIv3面向对象编程模式,示例代码如下 支持 [代码]Typescript[代码] 支持微信支付XML风格的接口(通常所说v2)调用,依赖 node-xml2js, 示例代码如下 支持微信支付APIv2版的 [代码]AES-256-ECB/PKCS7PADDING[代码] 通知消息加/解密 APIv2 & APIv3 与微信交互的各种数据签名用法示例 支持 企业微信-企业支付-企业红包/向员工付款 功能,示例用法及代码如下 系统要求 NodeJS原生[代码]crypto[代码]模块,自v12.9.0在 [代码]publicEncrypt[代码] 及 [代码]privateDecrypt[代码] 增加了 [代码]oaepHash[代码] 入参选项,本类库封装的 [代码]Rsa.encrypt[代码] 及 [代码]Rsa.decrypt[代码] 显式声明了此入参,测试下来在NodeJS10.15.0上可正常工作;虽然在v10.15上可用,不过仍旧推荐使用 NodeJS >= v12.9.0。 安装 [代码]$ npm install wechatpay-axios-plugin[代码] 起步 v3平台证书 微信支付APIv3使用 (RESTful API with JSON over HTTP)接口设计,数据交换采用非对称([代码]RSA-OAEP[代码])加/解密方案。 API上行所需的[代码]商户API私钥[代码],可以由商户官方专用证书生成工具生成, API下行所需的[代码]平台证书[代码]须从[代码]v3/certificates[代码]接口获取(应答证书还经过了对称[代码]AES-GCM[代码]加密,须采用[代码]APIv3密钥[代码]才能解密)。 本项目也提供了命令行下载工具,使用手册如下: [代码]$ ./node_modules/.bin/wxpay crt --help[代码] [代码]wxpay crt The WeChatPay APIv3's Certificate Downloader cert -m, --mchid The merchant's ID, aka mchid. [string] [required] -s, --serialno The serial number of the merchant's certificate aka serialno. [string] [required] -f, --privatekey The path of the merchant's private key certificate aka privatekey. [string] [required] -k, --key The secret key string of the merchant's APIv3 aka key. [string] [required] -o, --output Path to output the downloaded WeChatPay's platform certificate(s) [string] [default: "/tmp"] Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] 注: 像其他通用命令行工具一样,[代码]--help[代码] 均会打印出帮助手册,说明档里的[代码][required][代码]指 必选参数; [代码][string][代码]指 字符串类型,[代码][default][代码]指默认值 [代码]$ ./node_modules/.bin/wxpay crt</b> -m N -s S -f F.pem -k K -o .[代码] [代码]The WeChatPay Platform Certificate#0 serial=HEXADECIAL notBefore=Wed, 22 Apr 2020 01:43:19 GMT notAfter=Mon, 21 Apr 2025 01:43:19 GMT Saved to: wechatpay_HEXADECIAL.pem You may confirm the above infos again even if this library already did(by Rsa.verify): openssl x509 -in wechatpay_HEXADECIAL.pem -noout -serial -dates [代码] 注: 提供必选参数且运行后,屏幕即打印出如上信息,提示[代码]证书序列号[代码]及[代码]起、止格林威治(GMT)时间[代码]及证书下载保存位置。 命令行请求 v0.5版,命令行工具做了加强,增加了基础请求方法,可以用来做快速接入体验,用法如下: 帮助信息 [代码]$ ./node_modules/.bin/wxpay req --help[代码] [代码]wxpay req <uri> Play the WeChatPay OpenAPI requests over command line <uri> -c, --config The configuration [required] -b, --binary Point out the response as `arraybuffer` [boolean] -m, --method The request HTTP verb [choices: "DELETE", "GET", "POST", "PUT", "PATCH", "delete", "get", "post", "put", "patch"] [default: "POST"] -h, --headers Special request HTTP header(s) -d, --data The request HTTP body -p, --params The request HTTP query parameter(s) Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] v3版Native付 [代码]./node_modules/.bin/wxpay v3.pay.transactions.native \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -d.appid wxd678efh567hg6787 \ -d.mchid 1230000109 \ -d.description 'Image形象店-深圳腾大-QQ公仔' \ -d.out_trade_no '1217752501201407033233368018' \ -d.notify_url 'https://www.weixin.qq.com/wxpay/pay.php' \ -d.amount.total 100 \ -d.amount.currency CNY [代码] v2版付款码付 [代码]./node_modules/.bin/wxpay v2.pay.micropay \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.device_info 013467007045764 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.detail 'Image形象店-深圳腾大-QQ公仔' \ -d.spbill_create_ip 8.8.8.8 \ -d.out_trade_no '1217752501201407033233368018' \ -d.total_fee 100 \ -d.fee_type CNY \ -d.auth_code 120061098828009406 [代码] v2版付款码查询openid [代码]./node_modules/.bin/wxpay v2/tools/authcodetoopenid \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.auth_code 120061098828009406 [代码] 面向对象模式 本类库自[代码]0.2[代码]开始,按照 [代码]URL.pathname[代码] 以[代码]/[代码]做切分,映射成对象属性,[代码]0.4[代码]版开始,支持APIv2的[代码]pathname[代码]映射,编码书写方式有如下约定: 请求 [代码]pathname[代码] 作为级联对象,可以轻松构建请求对象,例如 [代码]v3/pay/transactions/native[代码] 即自然翻译成 [代码]v3.pay.transactions.native[代码]; 每个 [代码]pathname[代码] 所支持的 [代码]HTTP METHOD[代码],即作为 请求对象的末尾执行方法,例如: [代码]v3.pay.transactions.native.post({})[代码]; 每个 [代码]pathname[代码] 级联对象默认为HTTP[代码]POST[代码]函数,其同时隐式内置[代码]GET/POST/PUT/PATCH/DELETE[代码] 操作方法链,支持全大写及全小写两种编码方式,说明见[代码]变更历史[代码]; 每个 [代码]pathname[代码] 有中线(dash)分隔符的,可以使用驼峰[代码]camelCase[代码]风格书写,例如: [代码]merchant-service[代码]可写成 [代码]merchantService[代码],或者属性风格,例如 [代码]v3['merchant-service'][代码]; 每个 [代码]pathname[代码] 中,若有动态参数,例如 [代码]business_code/{business_code}[代码] 可写成 [代码]business_code.$business_code$[代码] 或者属性风格书写,例如 [代码]business_code['{business_code}'][代码],抑或按属性风格,直接写值也可以,例如 [代码]business_code['2000001234567890'][代码]; SDK内置的 [代码]v2/[代码] 对象,其特殊标识为APIv2级联对象,之后串接切分后的[代码]pathname[代码],如源 [代码]pay/micropay[代码] 翻译成 [代码]v2.pay.micropay[代码] 即以XML形式请求远端接口; 建议 [代码]pathname[代码] 按照 [代码]PascalCase[代码] 风格书写, [代码]TS Definition[代码] 已在路上(还有若干问题没解决),将是这种风格,代码提示将会很自然; 以下示例用法,均以[代码]Promise[代码]或[代码]Async/Await[代码]结合此种编码模式展开,级联对象操作符的调试信息见文档末。 初始化 [代码]const {Wechatpay, Formatter} = require('wechatpay-axios-plugin') const wxpay = new Wechatpay({ // 商户号 mchid: 'your_merchant_id', // 商户证书序列号 serial: 'serial_number_of_your_merchant_public_cert', // 商户API私钥 PEM格式的文本字符串或者文件buffer privateKey: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', certs: { // CLI `wxpay crt -m {商户号} -s {商户证书序列号} -f {商户API私钥文件路径} -k {APIv3密钥(32字节)} -o {保存地址}` 生成 'serial_number': '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', }, // APIv2密钥(32字节) v0.4 开始支持 secret: 'your_merchant_secret_key_string', // 接口不要求证书情形,例如仅收款merchant对象参数可选 merchant: { // 商户证书 PEM格式的文本字符串或者文件buffer cert: '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', // 商户API私钥 PEM格式的文本字符串或者文件buffer key: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', // or // passphrase: 'your_merchant_id', // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'), }, // APIv2沙箱环境地址 // baseURL: 'https://api.mch.weixin.qq.com/sandboxnew/', // 建议初始化设置此参数,详细说明见Axios官方README // maxRedirects: 0, }) [代码] 初始化字典说明如下: [代码]mchid[代码] 为你的商户号,一般是10字节纯数字 [代码]serial[代码] 为你的商户证书序列号,一般是40字节字符串 [代码]privateKey[代码] 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是[代码]apiclient_key.pem[代码]文件,支持纯字符串或者文件流[代码]buffer[代码]格式 [代码]certs{[serial_number]:string}[代码] 为通过下载工具下载的平台证书[代码]key/value[代码]键值对,键为平台证书序列号,值为平台证书pem格式的纯字符串或者文件流[代码]buffer[代码]格式 [代码]secret[代码] 为APIv2版的[代码]密钥[代码],商户平台上设置的32字节字符串 [代码]merchant.cert[代码] 为你的商户证书,一般是文件名为[代码]apiclient_cert.pem[代码]文件,支持纯字符串或者文件流[代码]buffer[代码]格式 [代码]merchant.key[代码] 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是[代码]apiclient_key.pem[代码]文件,支持纯字符串或者文件流[代码]buffer[代码]格式 [代码]merchant.passphrase[代码] 一般为你的商户号 [代码]merchant.pfx[代码] 为你的商户[代码]PKCS12[代码]格式的证书,文件名一般为[代码]apiclient_cert.p12[代码],支持二进制文件流[代码]buffer[代码]格式 注: 0.4版本做了重构及优化,APIv2&v3以及Axios初始参数,均融合在一个型参上。 APIv3 Native下单 [代码]wxpay.v3.pay.transactions.native .post({/*文档参数放这里就好*/}) .then(({data: {code_url}}) => console.info(code_url)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询订单 [代码]wxpay.v3.pay.transactions.id['{transaction_id}'] .get({params: {mchid: '1230000109'}, transaction_id: '1217752501201407033233368018'}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 关闭订单 [代码]wxpay.v3.pay.transactions.outTradeNo['1217752501201407033233368018'] .post({mchid: '1230000109'}) .then(({status, statusText}) => console.info(status, statusText)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 合单支付下单 [代码]wxpay.v3.combineTransactions.jsapi .post({/*文档参数放这里就好*/}) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] H5下单 [代码]wxpay.v3.pay.transactions.h5 .post({/*文档参数放这里就好*/}) .then(({data: {h5_url}}) => console.info(h5_url)) .catch(console.error) [代码] 对账单下载及解析 [代码]const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') wxpay.v3.bill.tradebill.get({ params: { bill_date: '2021-02-12', bill_type: 'ALL', } }).then(({data: {download_url, hash_value}}) => wxpay.v3.billdownload.file.get({ params: (new URL(download_url)).searchParams, signed: hash_value, responseType: 'arraybuffer', })).then(res => { assert(sha1(res.data.toString()) === res.config.signed, 'verify the SHA1 digest failed.') console.info(Formatter.castCsvBill(res.data)) }).catch(error => { console.error(error) }) [代码] 创建商家券 [代码]wxpay.v3.marketing.busifavor.stocks .post({/*商家券创建条件*/}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询用户单张券详情 [代码];(async () => { try { const {data: detail} = await wxpay.v3.marketing.busifavor .users.$openid$.coupons['{coupon_code}'].appids['wx233544546545989'] .get({openid: '2323dfsdf342342', coupon_code: '123446565767'}) console.info(detail) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 服务商模式Native下单 [代码];(async () => { try { const res = await wxpay.v3.pay.partner.transactions.native({ sp_appid, sp_mchid, sub_mchid, description, out_trade_no, time_expire: new Date( (+new Date) + 33*60*1000 ), //after 33 minutes attach, notify_url, amount: { total: 1, } }) console.info(res.data.code_url) } catch (error) { console.error(error) } })() [代码] 支付即服务 [代码];(async () => { try { const {status, statusText} = await wxpay.v3.smartguide.guides.$guide_id$.assign .post({sub_mchid, out_trade_no}, {guide_id}) console.info(status, statusText) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 商业投诉查询 [代码];(async () => { try { const res = await wxpay.v3.merchantService.complaints.get({ params: { limit : 5, offset : 0, begin_date : '2020-03-07', end_date : '2020-03-14', } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 图片上传 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const imageMeta = { filename: 'hellowechatpay.png', // easy calculated by the command `sha256sum hellowechatpay.png` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const imageData = new FormData() imageData.append('meta', JSON.stringify(imageMeta), {contentType: 'application/json'}) imageData.append('file', createReadStream('./hellowechatpay.png')) ;(async () => { try { const res = await wxpay.v3.marketing.favor.media.imageUpload.post(imageData, { meta: imageMeta, headers: imageData.getHeaders() }) console.info(res.data.media_url) } catch (error) { console.error(error) } })() [代码] 查询优惠券详情 [代码];(async () => { try { const res = await wxpay.v3.marketing.favor.stocks.$stock_id$.post({ params: { stock_creator_mchid, }, stock_id, }) console.info(res.data) } catch(error) { console.error(error) } })() [代码] 优惠券委托营销 [代码](async () => { try { const res = await wxpay.v3.marketing.partnerships.build.post({ partner: { type, appid }, authorized_data: { business_type, stock_id } }, { headers: { [`Idempotency-Key`]: 12345 } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 优惠券核销记录下载 [代码](async () => { try { let res = await wxpay.v3.marketing.favor.stocks.$stock_id$.useFlow.get({stock_id}) res = await wxpay.v3.billdownload.file.get({ params: (new URL(res.data.url)).searchParams, responseType: 'arraybuffer' }) // 备注:此接口下载的文件格式与商户平台下载的不完全一致,Formatter.castCsvBill解析有差异 console.info(res.data.toString()) } catch (error) { console.error(error) } })() [代码] 视频文件上传 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const videoMeta = { filename: 'hellowechatpay.mp4', // easy calculated by the command `sha256sum hellowechatpay.mp4` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const videoData = new FormData() videoData.append('meta', JSON.stringify(videoMeta), {contentType: 'application/json'}) videoData.append('file', createReadStream('./hellowechatpay.mp4')) ;(async () => { try { const res = await wxpay.v3.merchant.media.video_upload.post(videoData, { meta: videoMeta, headers: videoData.getHeaders() }) console.info(res.data.media_id) } catch (error) { console.error(error) } })() [代码] GZIP下载资金账单 [代码]const {unzipSync} = require('zlib') const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') ;(async () => { try { const {data: {download_url, hash_value}} = await wxpay.v3.bill.fundflowbill.GET({ params: { bill_date: '2020-02-12', bill_type: 'BASIC', tar_type: 'GZIP', } }) const {data} = await wxpay.v3.billdownload.file.GET({ params: (new URL(download_url)).searchParams, responseType: 'arraybuffer' }) // note here: previous `hash_value` was about the source `csv`, not the `gzip` data // so it needs unziped first, then to compare the `SHA1` degest const bill = unzipSync(data) assert.ok(hash_value === sha1(bill.toString()), 'SHA1 verification failed') console.info(Formatter.castCsvBill(bill)) } catch (error) { console.error(error) } })() [代码] APIv2 付款码(刷卡)支付 [代码]wxpay.v2.pay.micropay({ appid: 'wx8888888888888888', mch_id: '1900000109', nonce_str: Formatter.nonce(), sign_type: 'HMAC-SHA256', body: 'image形象店-深圳腾大-QQ公仔', out_trade_no: '1217752501201407033233368018', total_fee: 888, fee_type: 'CNY', spbill_create_ip: '8.8.8.8', auth_code: '120061098828009406', }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] H5支付 [代码]wxpay.v2.pay.unifiedorder({ appid: 'wx2421b1c4370ec43b', attach: '支付测试', body: 'H5支付测试', mch_id: '10000100', nonce_str: Formatter.nonce(), notify_url: 'http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php', openid: 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o', out_trade_no: '1415659990', spbill_create_ip: '14.23.150.211', total_fee: 1, trade_type: 'MWEB', scene_info: JSON.stringify({ h5_info: { type:"IOS", app_name: "王者荣耀", package_name: "com.tencent.tmgp.sgame" } }), }).then(({data: {mweb_url}}) => console.info(mweb_url)).catch(console.error); [代码] 申请退款 [代码]wxpay.v2.secapi.pay.refund.post({ appid: 'wx8888888888888888', mch_id: '1900000109', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', total_fee: 100, refund_fee: 100, refund_fee_type: 'CNY', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 现金红包 [代码]wxpay.v2.mmpaymkttransfers.sendredpack.POST({ nonce_str: Formatter.nonce(), mch_billno: '10000098201411111234567890', mch_id: '10000098', wxappid: 'wx8888888888888888', send_name: '鹅企支付', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, total_num: 1, wishing: 'HAPPY BIRTHDAY', client_ip: '192.168.0.1', act_name: '回馈活动', remark: '会员回馈活动', scene_id: 'PRODUCT_4', }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 企业付款到零钱 [代码]wxpay.v2.mmpaymkttransfers.promotion.transfers({ appid: 'wx8888888888888888', mch_id: '1900000109', partner_trade_no: '10000098201411111234567890', openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', check_name: 'FORCE_CHECK', re_user_name: '王小王', amount: 10099, desc: '理赔', spbill_create_ip: '192.168.0.1', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 企业付款到银行卡-获取RSA公钥 [代码]wxpay.v2.risk.getpublickey({ mch_id: '1900000109', sign_type: 'MD5', nonce_str: Formatter.nonce(), }, { baseURL: 'https://fraud.mch.weixin.qq.com' }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 企业微信 企业微信的企业支付,数据请求包需要额外的签名,仅需做如下简单扩展适配,即可支持;以下签名注入函数所需的两个参数[代码]agentId[代码] [代码]agentSecret[代码]来自企业微信工作台,以下为示例值。 [代码]const agentId = 1001001 const agentSecret = 'from_wework_agent_special_string' const {Hash} = require('wechatpay-axios-plugin') [代码] 企业红包-注入签名规则 [代码]Wechatpay.client.v2.defaults.transformRequest.unshift(function workwxredpack(data, headers) { const {act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid} = data if (!(act_name && mch_billno && mch_id && nonce_str && re_openid && total_amount && wxappid)) { return data } data.workwx_sign = Hash.md5( Formatter.queryStringLike(Formatter.ksort({ act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid })), agentSecret, agentId ).toUpperCase() return data }) [代码] 发放企业红包 [代码]wxpay.v2.mmpaymkttransfers.sendworkwxredpack({ mch_billno: '123456', wxappid: 'wx8888888888888888', sender_name: 'XX活动', sender_header_media_id: '1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, wishing: '感谢您参加猜灯谜活动,祝您元宵节快乐!', act_name: '猜灯谜抢红包活动', remark: '猜越多得越多,快来抢!', mch_id: '1900000109', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(console.error) [代码] 向员工付款-注入签名规则 [代码]Wechatpay.client.v2.defaults.transformRequest.unshift(function wwsptrans2pocket(data, headers) { const {amount, appid, desc, mch_id, nonce_str, openid, partner_trade_no, ww_msg_type} = data if (!(amount && appid && desc && mch_id && nonce_str && openid && partner_trade_no && ww_msg_type)) { return data } data.workwx_sign = Hash.md5( Formatter.queryStringLike(Formatter.ksort({ amount, appid, desc, mch_id, nonce_str, openid, partner_trade_no, ww_msg_type })), agentSecret, agentId ).toUpperCase() return data }) [代码] 向员工付款 [代码]wxpay.v2.mmpaymkttransfers.promotion.paywwsptrans2pocket({ appid: 'wxe062425f740c8888', device_info: '013467007045764', partner_trade_no: '100000982017072019616', openid: 'ohO4Gt7wVPxIT1A9GjFaMYMiZY1s', check_name: 'NO_CHECK', re_user_name: '张三', amount: '100', desc: '六月出差报销费用', spbill_create_ip: '10.2.3.10', ww_msg_type: 'NORMAL_MSG', act_name: '示例项目', mch_id: '1900000109', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(console.error) [代码] 自定义打印日志 [代码]// APIv2 日志 Wechatpay.client.v2.defaults.transformRequest.push(data => (console.log(data), data)) Wechatpay.client.v2.defaults.transformResponse.unshift(data => (console.log(data), data)) // APIv3 日志 Wechatpay.client.v3.defaults.transformRequest.push((data, headers) => (console.log(data, headers), data)) Wechatpay.client.v3.defaults.transformResponse.unshift((data, headers) => (console.log(data, headers), data)) [代码] 获取RSA公钥 非标准接口地址,也可以这样调用 [代码]Wechatpay.client.v2.post('https://fraud.mch.weixin.qq.com/risk/getpublickey', { mch_id: '1900000109', nonce_str: Formatter.nonce(), sign_type: 'HMAC-SHA256', }) .then(({data}) => console.info(data)) .catch(({response}) => console.error(response)) [代码] XML形式通知应答 [代码]const {Transformer} = require('wechatpay-axios-plugin') const xml = Transformer.toXml({ return_code: 'SUCCESS', return_msg: 'OK', }) console.info(xml) [代码] aes-256-ecb/pcks7padding 解密 [代码]const {Aes: {AesEcb}, Transformer, Hash} = require('wechatpay-axios-plugin') const secret = 'exposed_your_key_here_have_risks' const xml = '<xml>' + ... '</xml>' const obj = Transformer.toObject(xml) const res = AesEcb.decrypt(obj.req_info, Hash.md5(secret)) obj.req_info = Transformer.toObject(res) console.info(obj) [代码] 加密 [代码]const obj = Transformer.toObject(xml) const ciphertext = AesEcb.encrypt(obj.req_info, Hash.md5(secret)) console.assert( obj.req_info === ciphertext, `The notify hash digest should be matched the local one` ) [代码] APIv2数据签名 JSAPI [代码]const {Hash, Formatter} = require('wechatpay-axios-plugin') const v2Secret = 'exposed_your_key_here_have_risks' const params = { appId: 'wx8888888888888888', timeStamp: `${Formatter.timestamp()}`, nonceStr: Formatter.nonce(), package: 'prepay_id=wx201410272009395522657a690389285100', signType: 'HMAC-SHA256', } params.paySign = Hash.sign(params.signType, params, v2Secret) console.info(params) [代码] APP [代码]const {Hash, Formatter} = require('wechatpay-axios-plugin') const v2Secret = 'exposed_your_key_here_have_risks' const params = { appid: 'wx8888888888888888', partnerid: '1900000109', prepayid: 'WX1217752501201407033233368018', package: 'Sign=WXPay', timestamp: `${Formatter.timestamp()}`, noncestr: Formatter.nonce(), } params.sign = Hash.sign('MD5', params, v2Secret) console.info(params) [代码] APIv3数据签名 JSAPI [代码]const {Rsa, Formatter} = require('wechatpay-axios-plugin') const privateKey = require('fs').readFileSync('/your/merchant/priviate_key.pem') const params = { appId: 'wx8888888888888888', timeStamp: `${Formatter.timestamp()}`, nonceStr: Formatter.nonce(), package: 'prepay_id=wx201410272009395522657a690389285100', signType: 'RSA', } params.paySign = Rsa.sign(Formatter.joinedByLineFeed( params.appId, params.timeStamp, params.nonceStr, params.package ), privateKey) console.info(params) [代码] 商家券-小程序发券v2版签名规则 [代码]const {Hash, Formatter} = require('wechatpay-axios-plugin') const v2Secret = 'exposed_your_key_here_have_risks' // flat the miniprogram data transferring structure for sign const busiFavorFlat = ({send_coupon_merchant, send_coupon_params = []} = {}) => { return { send_coupon_merchant, ...send_coupon_params.reduce((des, row, idx) => ( Object.keys(row).map(one => des[`${one}${idx}`] = row[one]), des ), {}), } } // the miniprogram data transferring structure const busiFavor = { send_coupon_params: [ {out_request_no:'1234567',stock_id:'abc123'}, {out_request_no:'7654321',stock_id:'321cba'}, ], send_coupon_merchant: '10016226' } busiFavor.sign = Hash.sign('HMAC-SHA256', busiFavorFlat(busiFavor), v2Secret) console.info(busiFavor) [代码] 商家券-H5发券v2版签名规则 [代码]const {Hash, Formatter} = require('wechatpay-axios-plugin') const v2Secret = 'exposed_your_key_here_have_risks' const params = { stock_id: '12111100000001', out_request_no: '20191204550002', send_coupon_merchant: '10016226', open_id: 'oVvBvwEurkeUJpBzX90-6MfCHbec', coupon_code: '75345199', } params.sign = Hash.sign('HMAC-SHA256', params, v2Secret) console.info(params) [代码] 常见问题 Q: APIv3消息通知,[代码]AES-256-GCM[代码]加密字段,应该如何解密? 官方文档有介绍,APIv3平台证书及消息通知关键信息均使用[代码]AesGcm[代码]加解密,依赖[代码]APIv3密钥[代码],商户侧解密可参考[代码]bin/cli/cert.js[代码]证书下载工具,例如: [代码]AesGcm.decrypt(nonce, secret, ciphertext, aad); [代码] Q: 敏感信息或者幂等操作要求额外头信息上送时,应该如何构建请求参数? [代码]DELETE[代码]/[代码]GET[代码]请求的第一个参数,[代码]POST[代码]/[代码]PUT[代码]/[代码]PATCH[代码]请求的第二个参数,是 AxiosRequestConfig 对象,可以按需上送额外头参数,例如: [代码]wxpay.v3.applyment4sub.applyment.$noop$( {}, { noop: '', headers: { 'Wechatpay-Serial': '123456' } }, ).then(console.info).catch(console.error); [代码] 可参考 #17 Q: 接口地址为slash([代码]/[代码])结尾的,应该如何构建请求参数? 动态参数[代码]uri_template[代码]或者属性[代码]property[代码]方式构建,可参考 #16 单元测试 [代码]npm install && npm test[代码] 技术交流 如果遇到困难或建议可以 提ISSUE 或 加群,交流技术,分享经验。 QQ群: 684379275 文末打印一波示例方法链 [代码][Function (anonymous)] { v2: [Function: v2] { risk: [Function: v2/risk] { getpublickey: [Function: v2/risk/getpublickey] }, pay: [Function: v2/pay] { micropay: [Function: v2/pay/micropay] }, secapi: [Function: v2/secapi] { pay: [Function: v2/secapi/pay] { refund: [Function: v2/secapi/pay/refund] } }, mmpaymkttransfers: [Function: v2/mmpaymkttransfers] { sendredpack: [Function: v2/mmpaymkttransfers/sendredpack], promotion: [Function: v2/mmpaymkttransfers/promotion] { transfers: [Function: v2/mmpaymkttransfers/promotion/transfers], paywwsptrans2pocket: [Function: v2/mmpaymkttransfers/promotion/paywwsptrans2pocket] }, sendworkwxredpack: [Function: v2/mmpaymkttransfers/sendworkwxredpack] } }, v3: [Function: v3] { pay: [Function: v3/pay] { transactions: [Function: v3/pay/transactions] { native: [Function: v3/pay/transactions/native], id: [Function: v3/pay/transactions/id] { '{transaction_id}': [Function: v3/pay/transactions/id/{transaction_id}] }, outTradeNo: [Function: v3/pay/transactions/out-trade-no] { '1217752501201407033233368018': [Function: v3/pay/transactions/out-trade-no/1217752501201407033233368018] } }, partner: [Function: v3/pay/partner] { transactions: [Function: v3/pay/partner/transactions] { native: [Function: v3/pay/partner/transactions/native] } } }, marketing: [Function: v3/marketing] { busifavor: [Function: v3/marketing/busifavor] { stocks: [Function: v3/marketing/busifavor/stocks], users: [Function: v3/marketing/busifavor/users] { '$openid$': [Function: v3/marketing/busifavor/users/{openid}] { coupons: [Function: v3/marketing/busifavor/users/{openid}/coupons] { '{coupon_code}': [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}] { appids: [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids] { wx233544546545989: [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/wx233544546545989] } } } } } }, favor: [Function: v3/marketing/favor] { media: [Function: v3/marketing/favor/media] { imageUpload: [Function: v3/marketing/favor/media/image-upload] }, stocks: [Function: v3/marketing/favor/stocks] { '$stock_id$': [Function: v3/marketing/favor/stocks/{stock_id}] { useFlow: [Function: v3/marketing/favor/stocks/{stock_id}/use-flow] } } }, partnerships: [Function: v3/marketing/partnerships] { build: [Function: v3/marketing/partnerships/build] } }, combineTransactions: [Function: v3/combine-transactions] { jsapi: [Function: v3/combine-transactions/jsapi] }, bill: [Function: v3/bill] { tradebill: [Function: v3/bill/tradebill], fundflowbill: [Function: v3/bill/fundflowbill] }, billdownload: [Function: v3/billdownload] { file: [Function: v3/billdownload/file] }, smartguide: [Function: v3/smartguide] { guides: [Function: v3/smartguide/guides] { '$guide_id$': [Function: v3/smartguide/guides/{guide_id}] { assign: [Function: v3/smartguide/guides/{guide_id}/assign] } } }, merchantService: [Function: v3/merchant-service] { complaints: [Function: v3/merchant-service/complaints] }, merchant: [Function: v3/merchant] { media: [Function: v3/merchant/media] { video_upload: [Function: v3/merchant/media/video_upload] } } } } [代码] 变更历史 v0.5.5 (2021-04-13) 优化文档,[代码]证书[代码]相关名词与官方文档保持一致 优化代码,使用ES6 [代码]Reflect.set[代码]代替[代码]param-reassign[代码],性能更高 新增函数[代码]Hash.hmac[代码]方法,广度支持[代码]Hash-based Message Authentication Code[代码] 调整函数[代码]Hash.hmacSha256[代码]为不推荐方法,内部改写为固定[代码]Hash.hmac[代码]调用 调整CLI [代码]req <uri>[代码]成功调用仅返回[代码]{config, headers, data}[代码]数据结构 v0.5.4 (2021-04-08) 优化CLI,[代码]wxpay crt[代码] 下载平台证书仅在成功验签完成后写入文件 优化文档,[代码]AesGcm[代码] 解密示例 优化内部[代码]chain[代码]逻辑,遵循 [代码]RFC3986[代码] 规范,[代码]baseURL[代码]支持带部分路径的海外接入点 优化代码[代码]SonarQube[代码]检测结果[代码]3A+0.5%[代码] v0.5.3 优化CLI,[代码]wxpay <uri>[代码] 向前兼容以支持slash(/)结尾的请求,形如 [代码]v3/applyment4sub/applyment/[代码] v0.5.2 优化CLI,[代码]wxpay <uri>[代码] 现在支持型如 [代码]v2.pay.micropay[代码], [代码]v3.pay.transactions.native[代码] 调用 优化[代码]README[代码]文档,适配最新CLI用法;增加APIv3消息通知QA章节;增加技术交流QQ群说明 v0.5.1 优化CLI,可以直接 [代码]wxpay <uri>[代码] 发起请求 优化[代码]README[代码]文档,适配最新CLI用法 v0.5.0 新增命令行方式与微信支付接口交互工具 调整可选依赖包为[代码]peerDependencies[代码],使用完整功能需手动安装 [代码]form-data[代码] 或/及 [代码]yargs[代码] v0.4.6 使用最新版[代码]eslint[代码]及[代码]eslint-config-airbnb-base[代码] 增加[代码]utils.merge[代码]依赖函数测试校验 v0.4.5 支持APIv2版的俩账单下载,调用方法与APIv3类同; 增加测试用例覆盖,初始化参数[代码]secret[代码](for APIv2)如未设置,[代码]HMAC-SHA256[代码]数据签名时,可能引发 #14 v0.4.4 优化[代码]Wechatpay[代码]在多次实例化时赋值[代码]Symbol(CLIENT)[代码]异常问题,增加[代码]wechatpay.test.js[代码]测试用例覆盖 v0.4.3 支持 企业微信-企业支付 链式调用,需要额外注入签名规则,见上述文档用法示例 v0.4.2 文件名大小写问题 #11 感谢 @LiuXiaoZhuang 报告此问题 v0.4.1 解决了一个[代码]AES-GCM[代码]在[代码]Node10[代码]上的解密兼容性问题,程序在[代码]Node10[代码]上有可能崩溃,建议[代码]Node10[代码]用户升级至此版本 v0.4.0 重构 [代码]Wechatpay[代码] 类,同时支持 APIv2&v3’s 链式调用 改变 [代码]Wechatpay.client[代码] 返回值为[代码]Wechatpay.client.v3[代码],[代码]Wechatpay.client.v2[代码] 为 [代码]xmlBased[代码] 接口客户端 废弃 [代码]withEntities[代码] 方法,其在链式多次调用时,有可能达不到预期,详情见 #10,感谢 @ali-pay 报告此问题 README 文档中文化 完善补缺 [代码]tsd[代码] 声明 v0.3.4 Typed and tips on [代码]Wechatpay[代码] class(#9), thanks @ipoa v0.3.3 Upgrade Axios for the CVE-2020-28168 v0.3.2 Optim: Let [代码]Aes.pkcs7.padding[代码] strictly following the [代码]rfc2315[代码] spec Optim: Better of the [代码]Hash.md5[代码] and [代码]Hash.hmacSha256[代码] Coding comments and README v0.3.1 Optim: new param on [代码]xmlBased({mchid})[代码], while passed in, then [代码]Transformer.signer[代码] doing the [代码]assert[代码] with the post data. Feature: Customize the HTTP [代码]User-Agent[代码]. Refactor: Split [代码]aes.js[代码] as of [代码]Aes[代码], [代码]AesGcm[代码] and [代码]AesEcb[代码] classes for [代码]aes-256-ecb/pkcs7padding[代码] algo. v0.3.0 Feature: The XML based API requests. v0.2.3 Optim: Coding quality. v0.2.2 Fix: #8 [代码]verfier[代码] on the [代码]204[代码] status case. v0.2.1 Optim: Back compatible for [代码]12.4.0[代码] < [代码]Node[代码] ≧ [代码]10.15.0[代码]. v0.2.0 Feature: [代码]OOP[代码] developing style of the wechatpay APIv3. v0.1.0 Optim: Toggle the [代码]Nodejs[代码] version ≧ [代码]10.15.0[代码]. Optim: Documentation and coding comments. v0.0.9 Feature: definition of the [代码]Typescript[代码] v0.0.8 Optim: on [代码]castCsvBill[代码], drop the [代码]trim[代码] on each rows Optim: on [代码]response[代码] validation, checking ± 5 mins first then to [代码]Rsa.verify[代码] Optim: moved the [代码]commander[代码] optional dependency, because it’s only for the [代码]CLI[代码] tool Feature: shipped 99 tests([代码]npm test[代码]) v0.0.7 Feature: billdownload and castCsvBill eslint enabled ([代码]npm run lint[代码]) v0.0.6 Chinese document v0.0.5 Renew document and codes comments v0.0.4 Feature: certificate downloader, deps on [代码]commander[代码] v0.0.3 Feature: media file upload, optional deps on [代码]form-data[代码] v0.0.2 Feature: Assert the response’s timestamp ± 5 mins Refactor as CommonJS style(#6) Limits the communicating parameters(#7) Coding styles(#5) Coding comments and Document(#4, #3, #2, #1) v0.0.1 Init ES2015+ style License The MIT License (MIT) Copyright © 2020-2021 James ZHANG(TheNorthMemory) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2021-05-10 - 免充值产品测试验收用例的wechatpay-axios-plugin教学帖
社区内大佬们都有贡献 「免充值代金券」 的测试用例实现,这篇文章也来凑一下热闹,顺带帮你把一下为啥要做这个 「测试验收」 以及 「验收注意」 细节。 引言 官方用例文档链接 一共25页,非常详细,照着描一步步做,很快就能验收完。本篇作为「教学帖」,试着让友商们理解,这个验收的重要性,如果希望能够获取帮助直接验收,请阅读代金券接口升级验收脚本 用例组合1001+1002+1003+1004+1005。 安装nodejs sdk 本篇以 [代码]wechatpay-axios-plugin[代码] 这款npm开发包展开,详细介绍可阅读 从APIv3到APIv2再到企业微信,这款微信支付开发包的README你应该来读一读。 [代码]npm install wechatpay-axios-plugin@"v0.5.5" [代码] v0.6系列做了[代码]返回数据签名强校验[代码],以下示例代码需要做特殊处理,本篇以v0.5.5展开。 获取沙箱密钥 先要理解,沙箱环境是个仿真环境,不是生产环境,友商朋友们应该做环境隔离。起步需要商户使用生产环境的 [代码]API密钥[代码],去获取[代码]沙箱密钥[代码],后续所有[代码]沙箱环境[代码]操作都要使用由[代码]沙箱密钥[代码]生成的[代码]数据签名sign[代码]。 [代码]const { Wechatpay, Formatter } = require('wechatpay-axios-plugin'); const mchid = '你的商户号'; const secret = '你的32字节的`API密钥`字符串'; const appid = '你的APPID字符串'; const mch_id = mchid; const noop = {serial: 'any', privateKey: 'any', certs: {any: undefined}}; // 实例化一个对象 let wxpay = new Wechatpay({ secret, mchid, ...noop }); const { data: { sandbox_signkey } } = await wxpay.v2.sandboxnew.pay.getsignkey({ mch_id, nonce_str: Formatter.nonce() }); [代码] 1001 付款码(刷卡)支付 订单金额 [代码]5.01[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,用户实际支付 [代码]5.00[代码] 元。验证商户具备正确解析及识别免充值代金券字段的能力。 1001.1 请求支付 [代码]// 重新实例化一个沙箱环境的对象 wxpay = new Wechatpay({ secret: sandbox_signkey, mchid, ...noop }); // 模拟一个商户订单号 let out_trade_no = `SD${+new Date()}_501`; const { data: { coupon_fee, settlement_total_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.micropay({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, body: 'dummybot', total_fee: 501, spbill_create_ip: '127.0.0.1', auth_code: '120061098828009406' }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee }); [代码] 打印日志应如下: [代码]┌──────┬─────┐ │ (index) │ Values │ ├──────┼─────┤ │ 代金券金额 │ '1' │ │应结订单金额 │ '500' │ │ 订单金额 │ '501' │ └──────┴─────┘ [代码] 1001.2 获取支付结果 [代码]const { data: { settlement_total_fee, total_fee, coupon_fee, coupon_fee_0, coupon_type_0, coupon_count, } } = await wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee, 单个代金券支付金额: coupon_fee_0, 代金券类型: coupon_type_0, 代金券使用数量: coupon_count }); [代码] 打印日志应如下: [代码]┌──────────┬──────┐ │ (index) │ Values │ ├──────────┼──────┤ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '500' │ │ 订单金额 │ '501' │ │单个代金券支付金额 │ '1' │ │ 代金券类型 │ 'NO_CASH'│ │ 代金券使用数量 │ '1' │ └──────────┴──────┘ [代码] 1002 付款码(刷卡)支付退款 订单金额 [代码]5.02[代码] 元,其中 [代码]0.01[代码] 元使用免充值代金劵,实际支付 [代码]5.01[代码] 元,退款查询升级。 1002.1 请求支付 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_502`; const { data: { coupon_fee, settlement_total_fee, total_fee, } } = await wxpay .v2.sandboxnew.pay.micropay({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, body: 'dummybot', total_fee: 502, spbill_create_ip: '127.0.0.1', auth_code: '120061098828009406' }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee }); [代码] 打印日志应如下: [代码]┌────────┬─────┐ │ (index) │ Values │ ├────────┼─────┤ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '501' │ │ 订单金额 │ '502' │ └────────┴─────┘ [代码] 1002.2 获取支付结果 [代码]const { data: { settlement_total_fee, total_fee, coupon_fee, coupon_fee_0, coupon_type_0, coupon_count } } = await wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, }); console.table({ 商户订单号: out_trade_no, 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee, 单个代金券支付金额: coupon_fee_0, 代金券类型: coupon_type_0, 代金券使用数量: coupon_count }); [代码] 打印日志应如下: [代码]┌──────────┬────────────┐ │ (index) │ Values │ ├──────────┼────────────┤ │ 商户订单号 │'SD1618966329677_502'│ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '501' │ │ 订单金额 │ '502' │ │单个代金券支付金额│ '1' │ │ 代金券类型 │ 'NO_CASH' │ │ 代金券使用数量 │ '1' │ └──────────┴─────────────┘ [代码] 1002.3 请求退款 [代码]const { data: { cash_refund_fee, cash_fee, refund_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.refund({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, out_refund_no: `RD${out_trade_no}`, total_fee: 502, refund_fee: 501, }); console.table({ 退款金额: refund_fee, 标价金额: total_fee, 现金支付金额: cash_fee, 现金退款金额: cash_refund_fee, }); [代码] 打印日志应如下: [代码]┌────────┬─────┐ │ (index) │ Values │ ├────────┼────┤ │ 退款金额 │ '502' │ │ 标价金额 │ '502' │ │ 现金支付金额 │ '501' │ │ 现金退款金额 │ '501' │ └───────┴─────┘ [代码] 1002.4 获取退款结果 [代码]const { data: { settlement_total_fee, total_fee, cash_fee, settlement_refund_fee, coupon_refund_fee_0, coupon_type_0_0, coupon_refund_fee_0_0, refund_fee_0, coupon_refund_count_0, } } = await wxpay .v2.sandboxnew.pay.refundquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ 应结订单金额: settlement_total_fee, 订单金额: total_fee, 现金支付金额: cash_fee, 退款金额: settlement_refund_fee, 总代金券退款金额: coupon_refund_fee_0, 代金券类型: coupon_type_0_0, 总代金券退款金额: coupon_refund_fee_0_0, 申请退款金额: refund_fee_0, 退款代金券使用数量: coupon_refund_count_0, }); [代码] 1003 JSAPI/APP/Native支付 订单金额 [代码]5.51[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,实际支付 [代码]5.50[代码] 元。 验证正常支付流程,商户使用免充值代金券支付。 1003.1 统一下单 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_551`; const {data: { prepay_id } } = await wxpay .v2.sandboxnew.pay.unifiedorder({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), body: 'dummybot', total_fee: 551, notify_url: 'https://www.weixin.qq.com/wxpay/pay.php', spbill_create_ip: '127.0.0.1', trade_type: 'JSAPI' }); console.table({ 预支付交易会话标识: prepay_id }); [代码] 1003.2 获取支付结果 [代码]const { data: { out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 } } = wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 }); [代码] 1004 JSAPI/APP/Native支付退款 订单金额 [代码]5.52[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,实际支付 [代码]5.51[代码] 元。 1004.1 统一下单 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_551`; const {data: { prepay_id } } = await wxpay .v2.sandboxnew.pay.unifiedorder({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), body: 'dummybot', total_fee: 552, notify_url: 'https://www.weixin.qq.com/wxpay/pay.php', spbill_create_ip: '127.0.0.1', trade_type: 'JSAPI' }); console.table({ 预支付交易会话标识: prepay_id }); [代码] 1004.2 获取支付结果 [代码]const { data: { out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 } } = wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 }); [代码] 1004.3 请求退款 [代码]const { data: { cash_refund_fee, cash_fee, refund_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.refund({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, out_refund_no: `RD${out_trade_no}`, total_fee: 552, refund_fee: 551, }); console.table({ 退款金额: refund_fee, 标价金额: total_fee, 现金支付金额: cash_fee, 现金退款金额: cash_refund_fee, }); [代码] 打印日志应如下: [代码]┌───────┬────┐ │ (index) │ Values│ ├───────┼────┤ │ 退款金额 │ '552' │ │ 标价金额 │ '552'│ │现金支付金额│ '551' │ │现金退款金额│ '551' │ └──────┴────┘ [代码] 1004.4 获取退款结果 [代码]const { data: { settlement_total_fee, total_fee, cash_fee, settlement_refund_fee, coupon_refund_fee_0, coupon_type_0_0, coupon_refund_fee_0_0, refund_fee_0, coupon_refund_count_0, } } = await wxpay .v2.sandboxnew.pay.refundquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ 应结订单金额: settlement_total_fee 订单金额: total_fee 现金支付金额: cash_fee, 退款金额: settlement_refund_fee 总代金券退款金额: coupon_refund_fee_0 代金券类型: coupon_type_0_0, 总代金券退款金额: coupon_refund_fee_0_0 申请退款金额: refund_fee_0 退款代金券使用数量: coupon_refund_count_0, }); [代码] 1005 交易对账单下载 使用了免充值券的订单,免充值券部分的金额不计入结算金额。验证商户对账能正确理解到这一点,对账无误。这里预期会返回 [代码]1269[代码] 条明细数据。 汇总结果:总交易单数,应结订单总金额,退款总金额,充值券退款总金额,手续费总金额,订单总金额,申请退款金额。 这里数据应为: [代码]1269, `10.79, `5.93, `0.24, `0.0,`11.27, `6.37 [代码] 以生产标准为例,查询当前时间偏移两天以前的账单: [代码]const { data } = await wxpay .v2.sandboxnew.pay.downloadbill({ appid, mch_id, bill_type: 'ALL', bill_date: ( new Date(+new Date() + (8 - 48)*3600*1000) ).toISOString().slice(0, 10), nonce_str: Formatter.nonce(), }, { transformResponse: [] }); console.table(data); [代码] 打印日志形如: [代码]交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,应结订单金额,代金券金额,微信退款单号,商户退款单号,退款金额,充值券退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率,订单金额,申请退款金额,费率备注 `2016-05-04 02:18:18,`wxf7c30a8258df4208,`10014843,`0,`harryma007,`4.00123E+27,`autotest_20160501030456_45023,`oT2kauIMXH398DZBeJ4m22CuSDQ0,`NATIVE,`REFUND,`PAB_DEBIT,`CNY,`0,`0,`2.00123E+27,`REF4001232001201605015390231647,`0.01,`0,`ORIGINAL,`PROCESSING,`body中文测试,`attach中文测试,`0,`0.60%,`0,`0.01,` `2016-05-04 02:18:18,`wxf7c30a8258df4208,`10014843,`0,`harryma007,`4.00123E+27,`autotest_20160501060418_79156,`oT2kauIMXH398DZBeJ4m22CuSDQ0,`NATIVE,`REFUND,`PAB_DEBIT,`CNY,`0,`0,`2.00123E+27,`REF4001232001201605015391766944,`0.01,`0,`ORIGINAL,`PROCESSING,`body中文测试,`attach中文测试,`0,`0.60%,`0,`0.01,` ... 14:51:51,`wxf7c30a8258df4208,`10014843,`0,`harryma8888,`4.00968E+27,`wxautotest1462344441,`oT2kauGtJag902bjdvevrJbpGuxo,`NATIVE,`SUCCESS,`CMBC_CREDIT,`CNY,`0.05,`0.01,`0,`0,`0,`0,`中文[body],`测试中文[attach],`0,`0.60%,`0.05,`0,` 总交易单数,应结订单总金额,退款总金额,充值券退款总金额,手续费总金额,订单总金额,申请退款总金额 1269,`10.79,`5.93,`0.24,`0,`11.27,`6.37 [代码] 一铲到底 以下是一键验收全流程的一个[代码]魔性 chain[代码]到底的实现,感谢您阅读至此。 [图片] 本文如果对你开通 「免充代金券」 功能有帮助,那就来个赞呗。
2021-05-10 - 如何用十个月时间,做出一款迄今为止无法超越的微信支付SDK基础开发包
20年中在对接商家券的时候,翻了官方PHP SDK的牌子,0.1系缺个媒体文件上传比较优雅的解决方案,于是顺手就为其提了PR。之后,我就在思考,动态编程语言讲究的快,是不是应该可以在做点儿好玩的,有意义的,可以加速开发对接的,不被人抱怨的开发包呢。索性,仗着多年的从业经验,尝试从底层开始写,不断迭代,于是就有了这么一款小巧,低依赖,符合规范,面向对象模式,命令行模式,适用涵盖Windows、macOS、Linux的微信支付nodejs版基础开发包。 下面我以版本迭代时间线为过程,来讲讲这款SDK开发包的开发故事。 v0.0.1 初始版本 这个版本,发布于2020年6月26号,核心是3个类,4个文件共计11个方法,已经是完整实现了APIv3的技术规范:[代码]AES-GCM[代码]对称加解密,[代码]RSA-OAEP[代码]非对称加解密及验签规范。[代码]RSA-OAEP[代码]的原生公钥加密、私钥解密方法封装,是翻过无数遍nodejs的文档,最后精炼到各5行代码共计10行,这应该是nodejs中,[代码]RSA-OAEP[代码]加解密原生实现的最早封装。 v0.1.0 正式版本 这个系列,发布于2020年7月2号。在申城最热的一段时间,晚上睡不着就捋代码呗,哔哩啪啦猛敲代码(其实也没多少行代码,全被精简了),迭代了共计十个版本,于是就有了这么一个正式版。 v0.2 面向对象模式开发 这个系列,发布于2020年7月7号,止于2020年9月9号,四个版本。重要功能就是可以顺溜地使用动态对象来对接APIv3接口了,同时降低NodeJS版本兼容至10.15(这个是云开发的环境版本)。详细可阅读微信支付APIv3的Nodejs版SDK,让开发变得简单不再繁琐。 v0.3 支持APIv2调用 这个系列,发布于2020年9月11号,止于2021年1月22号。是个被“逼”出来的系列。这个系列加入了APIv2版的接口驱动,重要功能就是对「付款码支付」及「退款」能力的支持。详细可阅读适合云开发的微信支付v2及v3版Nodejs SDK,这就不展开了。在这一段时间里,有太多事情要跟进,所以一直没有太多时间写代码。展期了好久好久才进入了重要的下一重构时刻。 v0.4 同质化v2&v3 OOP开发 这个系列,发布于2021年2月28号,止于2021年3月25日,五个版本。这个系列的由来是因一位同学提的issue展开的,于是做了超量重构,不仅改写了入口程序,还加强了APIv2版的同质化调用,详细可阅读微信支付开发,可以简化到复制+粘贴这种地步 及纯属意外,还可以这么起底微信支付接口对接。 [图片] 有那么一瞬间,我以为这个版本就会是终点了,可以进入LTS版仅需长期维护了。殊不知,CLI工具集还有一漏勺没有完善好,那好吧,那就再来一波加强重组呗。 v0.5 命令行工具集 这个系列,发布于2021年3月27号,加强了命令行工具集,请求微信支付官方接口,现在可以在命令行上跑起来了。英文slogan就是: Play the OpenAPI requests over command line。 这得解释解释,为啥用“Play”呢,而不是其他词儿呢?其实这个功能早在20年10月,chain支付宝OpenAPI接口时,脑阔开了个洞,想出来的。微信支付的CLI工具已经迟到好久了,现在好了,两款驰名的支付平台,都可以如丝质般顺溜,搞起CI了。这就是play的由来——为开发而生: commit to play for running。 说说功能点,这个系列就加强了命令行方式与接口交互能力,同时降级了自0.0.4版就一直存在的 「平台证书」下载器 为工具集内一条特殊命令。详细玩法可阅读 开发系列之「起步」 及 真香:一行命令即可体验「微信支付」全系接口能力。 另外,这个加强重构版的CLI交互工具,从设计一开始就支持扩展能力,有兴趣的同学完全可以基于[代码]bin/cli/request.js[代码]构建出更多子命令,不干扰纯应用,跑着欢实就好。 NEXT 如果还有NEXT,那可能就是得把tsd文件翻新一下,[代码]ProxyConstructor[代码]那块是[代码]TypeScript[代码] 自4.0引入,动态属性类型签名的高级甚高级用法,还不太会,学习中,如果有娴熟的同学,可以提PR贡献一下。 哦,对了,文章如果看着还不错,SDK用着还顺手,那就star一下repo吧 https://github.com/TheNorthMemory/wechatpay-axios-plugin MIT开源,纯免费,随便用。做开源不易,况且还是为国民应用做底层基础开发包就更是难啊,平时又太忙,只能是按需来做,有空再续。
2021-05-10 - 适合云开发的微信支付v2及v3版Nodejs SDK
接续微信支付APIv3的Nodejs版SDK,在2020这个时间节点,之所以再造一遍微信支付v2(相对于APIv3来说)的轮子,实属是无奈之举,线下交易场景常见的付款码支付及退款功能,官方当下还没有开放出来v3版的,只能借助v2接口来处理;[代码]wechatpay-axios-plugin[代码] 从一开始目标就是为云原生而设计,遂再造一遍轮子,也抽出一些共生方法,为v3而用。 设计思路 此类库核心部件是利用了Axios的transform功能,数据在内部流转过程中,会经过 [代码]transformRequest[代码] 及 [代码]transformResponse[代码] 处理,通过构造两个自定义transformer,完整实现v2版的技术规格要求,从而完成 HTTP 请求/响应 处理。 Transformer.request 方法返回值是个数组,含两个方法 [代码][signer, toXml][代码],字面意思即,对输入数据签名,然后转换成xml; Transformer.response 方法返回值是个数组,含两个方法 [代码][toObject, verifier][代码],字面意思即,返回值做数据转换为对象,然后校验签名; 证书设置 凡是涉及资金变动的接口,均需要商户证书,此实现同时支持 [代码]pem[代码] 及 [代码]p12[代码] 格式的证书,使用方法见随包README: [代码]const {Wechatpay, Formatter: fmt} = require('wechatpay-axios-plugin') const client = Wechatpay.xmlBased({ secret: 'your_merchant_secret_key_string', merchant: { cert: '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', key: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', // or // passphrase: 'your_merchant_id', // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'), }, }) [代码] 实力化一个 [代码]client[代码] 的最小参数为 [代码]secret[代码],即所谓的 密钥,字符串形式,32字节长度。 自定义打印日志 按需,如果需要检测类库的数据情况,在实例化完成后,可以加入如下类似两段代码,即可以打印出日志;当然也可以按需把日志输出至文件等,抛砖引玉而已。 [代码]//在格式转换完后,打印日志 client.defaults.transformRequest.push(data => (console.log(data), data)) //在请求返回,先行打印日志 client.defaults.transformResponse.unshift(data => (console.log(data), data)) [代码] 使用示例 实例化对象 [代码]secret[代码] 所对应的商户类型,可以是服务商、普通商户、特约商户,入参按照官方文档,手捋填入即可,以下几个方法,均测试过,正常运转。 申请退款 [代码]client.post('/secapi/pay/refund', { appid: 'wx8888888888888888', mch_id: '1900000109', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', total_fee: 100, refund_fee: 100, refund_fee_type: 'CNY', nonce_str: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] [代码]//log输入 { return_code: 'SUCCESS', return_msg: 'OK', appid: 'wx8888888888888888', mch_id: '1365319302', nonce_str: 'X8bpYtUJbPHK0Fyd', sign: '12BDC0390958455875108947AD51D897', result_code: 'SUCCESS', transaction_id: '4200000684202009114087736848', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', refund_id: '50300005642020091102621479983', refund_channel: '', refund_fee: '100', coupon_refund_fee: '0', total_fee: '100', cash_fee: '100', coupon_refund_count: '0', cash_refund_fee: '100' } [代码] 付款码支付 [代码]client.post('/pay/micropay', { appid: 'wx8888888888888888', mch_id: '1900000109', nonce_str: fmt.nonce(), sign_type: 'HMAC-SHA256', body: 'image形象店-深圳腾大-QQ公仔', out_trade_no: '1217752501201407033233368018', total_fee: 888, fee_type: 'CNY', spbill_create_ip: '8.8.8.8', auth_code: '120061098828009406', }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 现金红包 [代码]client.post('/mmpaymkttransfers/sendredpack', { nonce_str: fmt.nonce(), mch_billno: '10000098201411111234567890', mch_id: '10000098', wxappid: 'wx8888888888888888', send_name: '鹅企支付', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, total_num: 1, wishing: 'HAPPY BIRTHDAY', client_ip: '192.168.0.1', act_name: '回馈活动', remark: '会员回馈活动', scene_id: 'PRODUCT_4', }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 企业付款 [代码]client.post('/mmpaymkttransfers/promotion/transfers', { appid: 'wx8888888888888888', mch_id: '1900000109', partner_trade_no: '10000098201411111234567890', openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', check_name: 'FORCE_CHECK', re_user_name: '王小王', amount: 10099, desc: '理赔', spbill_create_ip: '192.168.0.1', nonce_str: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 和v3一起用 [代码]const wxpay = new Wechatpay({ mchid: 'your_merchant_id', serial: 'serial_number_of_your_merchant_public_cert', privateKey: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', certs: { 'serial_number': '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', } }) [代码] Native下单 [代码]wxpay.v3.pay.transactions.native .post({/*文档参数放这里就好*/}) .then(({data: {code_url}}) => console.info(code_url)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询订单 [代码]wxpay.v3.pay.transactions.id['{transaction_id}'] .withEntities({transaction_id: '1217752501201407033233368018'}) .get({params: {mchid: '1230000109'}}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 关单 [代码]wxpay.v3.pay.transactions.outTradeNo['1217752501201407033233368018'] .post({mchid: '1230000109'}) .then(({status, statusText}) => console.info(status, statusText)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 创建商家券 [代码]wxpay.v3.marketing.busifavor.stocks .post({/*商家券创建条件*/}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询用户单张券详情 [代码];(async () => { try { const {data: detail} = await wxpay.v3.marketing.busifavor.users.$openid$.coupons['{coupon_code}'].appids['wx233544546545989'] .withEntities({openid: '2323dfsdf342342', coupon_code: '123446565767'}) .get() console.info(detail) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 上传图片 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const imageMeta = { filename: 'hellowechatpay.png', // easy calculated by the command `sha256sum hellowechatpay.png` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const imageData = new FormData() imageData.append('meta', JSON.stringify(imageMeta), {contentType: 'application/json'}) imageData.append('file', createReadStream('./hellowechatpay.png')) Wechatpay.client.post('/v3/marketing/favor/media/image-upload', imageData, { meta: imageMeta, headers: imageData.getHeaders() }).then(res => { console.info(res.data.media_url) }).catch(error => { console.error(error) }) [代码] 下载账单并格式化 [代码]const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') Wechatpay.client.get('/v3/bill/tradebill', { params: { bill_date: '2020-06-01', bill_type: 'ALL', } }).then(({data: {download_url, hash_value}}) => Wechatpay.client.get(download_url, { signed: hash_value, responseType: 'arraybuffer', })).then(res => { assert(sha1(res.data) === res.config.signed, 'verify the SHA1 digest failed.') console.info(fmt.castCsvBill(res.data)) }).catch(error => { console.error(error) }) [代码] 委托营销 [代码](async () => { try { const res = await Wechatpay.client.post(`/v3/marketing/partnerships/build`, { partner: { type, appid }, authorized_data: { business_type, stock_id } }, { headers: { [`Idempotency-Key`]: 12345 } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 查询投诉信息并解密 [代码];(async () => { try { const res = await Wechatpay.client.get('/v3/merchant-service/complaints', {params: { limit: 50, offset: 0, begin_date: (new Date(+new Date - 29*86400*1000)).toJSON().slice(0, 10), end_date: (new Date).toJSON().slice(0, 10), }}) // decrypt the `Sensitive Information` res.data.data.map(row => (row.payer_phone = rsa.decrypt(row.payer_phone, merchantPrivateKey), row)) console.info(res.data) } catch({response: {status, statusText, data, headers}, request, config}) { console.error(status, statusText, data) } })() [代码] TODO v2版的[代码]AES-256-ECB/PKCS7Padding[代码]未做封装,这个不难,npm上也有许多优秀的类库可用,暂且先这样。 写到最后 MIT开放源码@npm, github ,可用于企业商业用途。
2020-10-16 - 微信支付开发,可以简化到复制+粘贴这种地步
距离上一版(0.3)过去了有几个月了,这几个月没少折腾微信团队,春节前后“突然”利好不断,许多几乎不可能的事项都有显著改善,这也许就是认(一)真(本)做(正)事(经)的回报吧。2月下旬的时候,有一小伙伴,开了个issue#10 反馈,lib在变量重入的时候,存在bug,无法正确获取订单信息。仔细推演之后,决定重构一下,把树形结构[代码]Wechatpay SDK[代码]起个底儿,许多(BT)用法,真的是事后测试用例覆盖时才发现,啪一下吧。 0.4版做了这些改进 [代码]README[代码]文档中文化 重构 [代码]Wechatpay[代码] 类,同时支持 [代码]APIv2&v3[代码] 链式调用 改变 [代码]Wechatpay.client[代码] 返回值为[代码]Wechatpay.client.v3[代码],[代码]Wechatpay.client.v2[代码] 为 [代码]xmlBased[代码] 接口客户端 废弃 [代码]withEntities[代码] 方法,其在链式多次调用时,有可能达不到预期 解决 [代码]AES-GCM[代码]在[代码]Node10[代码]上的解密兼容性问题,程序在[代码]Node10[代码]上有可能崩溃,建议[代码]Node10[代码]用户升级至此版本 支持 企业微信-企业支付 链式调用,需要额外注入签名规则,见用法示例 优化[代码]Wechatpay[代码]在多次实例化时赋值[代码]Symbol(CLIENT)[代码]异常问题,增加[代码]wechatpay.test.js[代码]测试用例覆盖 链式chain调用 这个想法是自0.2版开始,是把[代码]URL.pathname[代码]以[代码]/[代码]做切分,映射成对象属性,0.4版做了极致优化,支持APIv2的[代码]pathname[代码]映射,效果较上一贴 微信支付APIv3的Nodejs版SDK,打印的树形更优美了,效果如下: [代码][Function (anonymous)] { v2: [Function: /v2] { risk: [Function: /v2/risk] { getpublickey: [Function: /v2/risk/getpublickey] }, pay: [Function: /v2/pay] { micropay: [Function: /v2/pay/micropay] }, secapi: [Function: /v2/secapi] { pay: [Function: /v2/secapi/pay] { refund: [Function: /v2/secapi/pay/refund] } }, mmpaymkttransfers: [Function: /v2/mmpaymkttransfers] { sendredpack: [Function: /v2/mmpaymkttransfers/sendredpack], promotion: [Function: /v2/mmpaymkttransfers/promotion] { transfers: [Function: /v2/mmpaymkttransfers/promotion/transfers], paywwsptrans2pocket: [Function: /v2/mmpaymkttransfers/promotion/paywwsptrans2pocket] }, sendworkwxredpack: [Function: /v2/mmpaymkttransfers/sendworkwxredpack] } }, v3: [Function: /v3] { pay: [Function: /v3/pay] { transactions: [Function: /v3/pay/transactions] { native: [Function: /v3/pay/transactions/native], id: [Function: /v3/pay/transactions/id] { '{transaction_id}': [Function: /v3/pay/transactions/id/{transaction_id}] }, outTradeNo: [Function: /v3/pay/transactions/out-trade-no] { '{out_trade_no}': [Function: /v3/pay/transactions/out-trade-no/{out_trade_no}] } } favor: [Function: /v3/marketing/favor] { media: [Function: /v3/marketing/favor/media] { imageUpload: [Function: /v3/marketing/favor/media/image-upload] }, stocks: [Function: /v3/marketing/favor/stocks] { '$stock_id$': [Function: /v3/marketing/favor/stocks/{stock_id}] { useFlow: [Function: /v3/marketing/favor/stocks/{stock_id}/use-flow] } } }, partnerships: [Function: /v3/marketing/partnerships] { build: [Function: /v3/marketing/partnerships/build] } }, smartguide: [Function: /v3/smartguide] { guides: [Function: /v3/smartguide/guides] { '$guide_id$': [Function: /v3/smartguide/guides/{guide_id}] { assign: [Function: /v3/smartguide/guides/{guide_id}/assign] } } } } } [代码] 上述 [代码]/v2[代码] 树节点,是本SDK“创造”出来的,用以区分APIv3调用,默认不是 [代码]/v2[代码]开头的节点,均以APIv3方式调用; 这么做是因为,微信支付APIv3版,[代码]URL.pathname[代码] 目前已知至少有一个不是以 [代码]/v3[代码] 开头的; [代码]/v2[代码] 官方又没用到,咱就先占着用吧,所以如是APIv2版的调用,可以这么 [代码]chain[代码], [代码]v2.pay.micropay = /v2/pay/micropay[代码] 即以XML负载形式请求远端接口; 较0.2版,每个级联对象为APIv2做了改变,每个节点本身是个[代码]Function[代码],链接的是HTTP POST方法,入参接受两个参数 [代码](data, config)[代码],返回值是[代码]Promise[代码]对象; 另外每个节点同时隐式内置了[代码]GET/POST/PUT/PATCH/DELETE[代码] 操作方法链,支持全大写及全小写(未来有可能会删除)两种编码方式,完美支持APIv3的多HTTP verbs协议(目前统计出来,有至少5处蓝瘦的地方,有空了再说)。 怎么用 一句话可能就说完了,实例化后,按[代码]pathname[代码]拆分,然后再执行文档上说的HTTP verbs方法,附带所需参数,然后就没有然后了~ 实例化 [代码]const {Wechatpay, Formatter} = require('wechatpay-axios-plugin') const wxpay = new Wechatpay({ mchid: 'your_merchant_id', serial: 'serial_number_of_your_merchant_public_cert', privateKey: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', certs: { 'serial_number': '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', }, // APIv2参数 >= 0.4.0 开始支持 secret: 'your_merchant_secret_key_string', // 注: 如果不涉及资金变动,如仅收款,merchant参数可选,仅需 `secret` 一个参数,注意其为v2版的。 merchant: { cert: '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----', key: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----', // or // passphrase: 'your_merchant_id', // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'), }, }) [代码] APIv3 Native下单 [代码]wxpay.v3.pay.transactions.native .post({/*文档参数放这里就好*/}) .then(({data: {code_url}}) => console.info(code_url)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] APIv3 查询订单 [代码]wxpay.v3.pay.transactions.id['{transaction_id}'] .get({params: {mchid: '1230000109'}, transaction_id: '1217752501201407033233368018'}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] APIv3 关单 [代码]wxpay.v3.pay.transactions.outTradeNo['1217752501201407033233368018'] .post({mchid: '1230000109'}) .then(({status, statusText}) => console.info(status, statusText)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] APIv3 对账单下载及解析 [代码]const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') wxpay.v3.bill.tradebill.get({ params: { bill_date: '2021-02-12', bill_type: 'ALL', } }).then(({data: {download_url, hash_value}}) => wxpay.v3.billdownload.file.get({ params: (new URL(download_url)).searchParams, signed: hash_value, responseType: 'arraybuffer', })).then(res => { assert(sha1(res.data.toString()) === res.config.signed, 'verify the SHA1 digest failed.') console.info(Formatter.castCsvBill(res.data)) }).catch(error => { console.error(error) }) [代码] APIv2 Native下单 [代码]wxpay.v2.pay.unifiedorder .post({/*文档参数放这里就好*/}) .then(({data: {code_url}}) => console.info(code_url)) .catch(console.error) [代码] APIv2 付款码(刷卡)支付 [代码]wxpay.v2.pay.micropay({ appid: 'wx8888888888888888', mch_id: '1900000109', nonce_str: Formatter.nonce(), sign_type: 'HMAC-SHA256', body: 'image形象店-深圳腾大-QQ公仔', out_trade_no: '1217752501201407033233368018', total_fee: 888, fee_type: 'CNY', spbill_create_ip: '8.8.8.8', auth_code: '120061098828009406', }) .then(res => console.info(res.data)) .catch(console.error) [代码] APIv2 现金红包 [代码]wxpay.v2.mmpaymkttransfers.sendredpack({ nonce_str: Formatter.nonce(), mch_billno: '10000098201411111234567890', mch_id: '10000098', wxappid: 'wx8888888888888888', send_name: '鹅企支付', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, total_num: 1, wishing: 'HAPPY BIRTHDAY', client_ip: '192.168.0.1', act_name: '回馈活动', remark: '会员回馈活动', scene_id: 'PRODUCT_4', }) .then(res => console.info(res.data)) .catch(console.error) [代码] APIv2 企业付款到零钱 [代码]wxpay.v2.mmpaymkttransfers.promotion.transfers({ appid: 'wx8888888888888888', mch_id: '1900000109', partner_trade_no: '10000098201411111234567890', openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', check_name: 'FORCE_CHECK', re_user_name: '王小王', amount: 10099, desc: '理赔', spbill_create_ip: '192.168.0.1', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(console.error) [代码] APIv2 企业付款到银行卡-获取RSA公钥 这个用法,是测试用例发掘出来的,注意请求带了第二个参数 [代码]{baseURL:'https://fraud.mch.weixin.qq.com'}[代码] [代码]wxpay.v2.risk.getpublickey({ mch_id: '1900000109', sign_type: 'MD5', nonce_str: Formatter.nonce(), }, { baseURL: 'https://fraud.mch.weixin.qq.com' }) .then(res => console.info(res.data)) .catch(console.error) [代码] 企业微信 企业微信的企业支付,数据请求包需要额外的签名,仅需做如下简单扩展适配,即可支持;以下签名注入函数所需的两个参数[代码]agentId[代码] [代码]agentSecret[代码]来自企业微信工作台,以下为示例值。 [代码]const agentId = 1001001 const agentSecret = 'from_wework_agent_special_string' const {Hash} = require('wechatpay-axios-plugin') [代码] APIv2 企业红包-注入签名规则 [代码]Wechatpay.client.v2.defaults.transformRequest.unshift(function workwxredpack(data, headers) { const {act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid} = data if (!(act_name && mch_billno && mch_id && nonce_str && re_openid && total_amount && wxappid)) { return data } data.workwx_sign = Hash.md5( Formatter.queryStringLike(Formatter.ksort({ act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid })), agentSecret, agentId ).toUpperCase() return data }) [代码] APIv2 发放企业红包 [代码]wxpay.v2.mmpaymkttransfers.sendworkwxredpack({ mch_billno: '123456', wxappid: 'wx8888888888888888', sender_name: 'XX活动', sender_header_media_id: '1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0', re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E', total_amount: 1000, wishing: '感谢您参加猜灯谜活动,祝您元宵节快乐!', act_name: '猜灯谜抢红包活动', remark: '猜越多得越多,快来抢!', mch_id: '1900000109', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(console.error) [代码] APIv2及APIv3之间的编程体验,几乎是完全一致的了~ 更多用法示例,已经完全中文化发在github上,地址是:https://github.com/TheNorthMemory/wechatpay-axios-plugin 花式开发玩法,下一耙再叙~
2021-03-08 - 微信支付优惠扣除逻辑
一、优惠券叠加使用逻辑1)不同商户创建的全场优惠(包括券和立减)默认叠加使用 假设:某笔订单可以享受全场优惠a(A商户创建,互斥使用)、全场优惠b(B商户创建,互斥使用)、全场优惠c(C商户创建,互斥使用) 则:该笔订单会同时享受 a + b + c,共3个优惠 2)同一商户创建的全场券可配置叠加使用或互斥使用,单笔订单可以核销同一商户创建的所有叠加券 + 互斥券中的一个 假设:某笔订单可以享受同一商户创建的全场券a(叠加使用)、全场券b(叠加使用)、全场券c(互斥使用)、全场券d(互斥使用) 则:该笔订单会享受 a + b + c或d中的一个,共3个优惠 3)同一商户创建的全场立减可配置叠加使用或互斥使用,单笔订单可以核销同一商户创建的所有叠加立减 + 互斥立减中的一个 假设:某笔订单可以享受同一商户创建的全场立减a(叠加使用)、全场立减b(叠加使用)、全场立减c(互斥使用)、全场立减d(互斥使用) 则:该笔订单会享受 a + b + c或d中的一个,共3个优惠 4)同一商户创建的全场立减和全场券默认叠加使用 假设:某笔订单可以享受同一商户创建的全场券a(互斥使用)、全场立减b(互斥使用) 则:该笔订单会享受 a + b,共2个优惠 5)一笔订单中,同一商品(sku维度)只能享受一个单品优惠(包括券和立减),不同的商品可以享受不同的单品优惠 假设:某笔订单可以享受单品优惠a(sku:01),单品优惠b(sku:02),单品优惠c(sku:02) 则:则该笔订单会享受 a + b或c中的一个,共2个优惠 6)若某笔订单享受了全场优惠(包括券和立减),且其中至少一个全场优惠是叠加使用的,则该笔订单才能叠加使用单品优惠 假设:某笔订单可以享受全场优惠a(叠加使用),全场优惠b(互斥使用),单品优惠c 若该笔订单享受a,则可同时享受c;若该笔订单享受b,则不可同时享受c;若该笔订单享受a+b,则可同时享受c * 指定支付方式(例如指定银行卡)的优惠需关注: 优惠是否可以叠加使用,是以制券商户号维度进行判断的,与批次指定的支付方式无关。若多个指定了支付方式的全场优惠配置了不可叠加使用,即使这几个优惠指定的支付方式不同,用户也只能享受其中一个。 举例:某笔订单可以享受全场优惠a(指定了A银行信用卡,互斥使用),全场优惠b(指定了A银行储蓄卡,互斥使用),则无论主扫或被扫,用户都只能享受a或b中的一个,且由系统指定,用户无法切换优惠。 所以,若同时存在多银行或多卡种的活动,建议将其均配置为 [可叠加使用] 。 二、优惠时的优先级当用户有多个优惠,且多个优惠不能同时使用时,优惠的使用顺序如下: 面额越高优先级越高门槛越高优先级越高过期时间越近优先级越高领券时间越近优先级越高批次ID越小优先级越高 当用户有多个可叠加使用的优惠,但订单无法满足叠加使用的条件时,优惠的使用顺序如下: 面额越高优先级越高门槛越高优先级越高当面额与门槛均相同时,则随机使用顺序,此时不判断过期时间与领取时间。 三、一笔订单最多可使用的优惠数单笔订单最多可以使用20个单品优惠,以及8个全场优惠。 当订单可用的全场券超过8张时,微信支付将筛选可用券中面额最高的8张进行优惠计算,故无法保证得出最优解。
2021-01-16 - 云开发实践 | 如何使用 tcb-js-sdk 实现图片上传功能(含源码)
前言 tcb-js-sdk 让开发者可以在网页端使用 JavaScript 代码服务访问云开发的服务,以轻松构建自己的公众号页面或者独立的网站等 Web 服务。本文将以实现图片上传功能为例,介绍 tcb-js-sdk 的基本配置流程。想了解云开发更多知识也可以查看技术文档:https://cloudbase.net?from=10004 在使用云开发 Cloudbase 开发应用的过程中,除了借助云开发提供的数据库存储数据以外,常常还会遇到需要实现图片上传功能的需求,在这种情况下,可以通过 tcb-js-sdk 来完成图片上传的功能。接下来,我们来开发一个简易的图片上传功能,作为演示。 主要流程: [图片] 1. 引入 tcb-js-sdk 想要使用 tcb-js-sdk 来完成开发,我们需要在项目中引入 tcb-js-sdk ,从而可以借助其提供的 API 实现简单的调用云开发各项能力。 在具体的接入时,可以根据你的项目类型,选择使用 CDN 接入还是通过包管理器接入,这里我使用 CDN 来完成接入。 在项目中添加如下代码,并根据 NPM 上的说明,选择最新版本的 SDK ,比如当前最新版本是 1.7.1 版本,就可以将下方代码中的 [代码]${version}[代码] 替换为 1.7.1 。 [代码]<script src="//imgcache.qq.com/qcloud/tcbjs/${version}/tcb.js"></script> <script> const app = tcb.init({ env: 'your-env-id' //填写云环境ID }); const auth = app.auth(); </script> [代码] 2. 配置用户登录 云开发的 API 调用需要用户具备身份,为了简化,这里我选择使用匿名登录,这样就可以更简单的完成项目的开发。关于匿名登录的用户配置,你可以在云开发的官方文档中找到相关的说明。当你将你的登录配置成这样的时候,就说明配置好了用户登录。 [图片] 此外,你还需要将需要调用云开发 SDK 的网页配置到安全域名中,从而确保可以正常调用 API 接口。 [图片] 在完成了用户登录的配置以后,可以在项目代码中加入用户匿名登录的配置 [代码]<script> const app = tcb.init({ env: 'your-env-id' //填写云环境ID }); const auth = app.auth(); auth.anonymousAuthProvider().signIn() // 匿名登录,实际业务时根据需要进行调整 .then(() => { console.log("登陆成功") //登录成功 }).catch(err => { console.log("登录失败",err) //登录失败 }) [代码] 3. 编写上传代码 在完成了基础的用户登录后,就可以编写具体的上传代码了。 [图片] 在具体的实现过程中,我在页面中放置了一个按钮,用作图片选择,并放置一个 Button 用来确认选择,并提交选择。具体的页面结构代码如下: [代码]<div id="upload" > <input id="pic" type="file" accept="image/png,image/jpg,image/jpeg" name="pic" /> + </div> <div id="summit" onclick="upload()">上传</div> <div id="preview"></div> [代码] 有了页面的代码,我们就可以通过编写代码来获取我们所选择的文件,并将其上传到云存储中,获得访问链接。 具体的获取文件的代码如下: [代码]var fileinput = document.getElementById('pic'), preview = document.getElementById('preview'); // 获取对象 fileinput.addEventListener('change',function(){//监听 input 的 change事件 if (!fileinput.value) { // 判断当前是否有文件 console.log('no pic'); return; } else{ console.log('success') } var file = fileinput.files[0];//如果有,则获取到文件 if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/jpg') {//判断文件是否符合img alert('仅支持jpeg和png'); fileinput.value = '';//如不符合则清空value以及backgroundImage preview.style.backgroundImage = 'url(#)'; return; }else{ var reader = new FileReader(); reader.onload = function(e){ var data = e.target.result; preview.style.backgroundImage = 'url(' + data + ')'; } reader.readAsDataURL(file); console.log(file.name) } }) [代码] 当我们拿到数据以后,我们就可以借助 [代码]app.uploadFile[代码]将其上传到云存储中,实现本地文件的上传。 [代码]function upload(){ app .uploadFile({ // 云端路径 cloudPath: document.getElementById('pic').files[0].name, // 需要上传的文件,File 类型 filePath: document.getElementById('pic').files[0] }) .then(res => { // 返回文件 ID console.log(res.fileID) console.log('上传成功') }) } [代码] 以及,如果你在上传完成后,希望获取到图片的下载地址,则可以调用 [代码]app.getTempFileURL[代码] 来获取图片的访问地址。 [代码] app .getTempFileURL({ fileList: [res.fileID]//要下载的文件 ID 组成的数组 本示例填写为上面代码的文件id }) .then(res => { res.fileList.forEach((el) => { if(el.code === 'SUCCESS') { console.log(el.tempFileURL)//打印下载地址 } else { console.log('获取地址失败')//获取地址失败 } }) }); } [代码] 4. 部署应用上线 在完成了基础功能的编写后,我们可以借助云开发提供的静态托管能力,快速将项目部署上线。 在云开发控制台中开通静态托管服务: [图片] 并安装 Cloudbase Framework ,来实现快速部署: [代码]npm install -g @cloudbase/cli@beta # 安装 cloudbase framework cloudbase login # 登录 cd 项目目录 cloudbase init --without-template # 初始化部署模板 cloudbase framework:deploy # 部署项目 [代码] 根据提示,完成部署,就可以看到我们的项目的访问地址。 [图片] 总结 通过引入 tcb-js-sdk ,我们可以十分方便的在 Web 应用中引入云开发的文件存储系统。对于前端来说,可以更加简单的完成数据存储相关逻辑的编写,对于前端开发者来说,十分的友好,特别是一些快节奏的业务开发,可以借助 tcb-js-sdk 实现业务需求。 文章的项目源码: https://github.com/Handsomedoggy/cloudbase-image-upload-example 【产品介绍】云开发(CloudBase)是云端一体化的后端云服务,采用 serverless 架构,免去了移动应用构建中繁琐的服务器搭建和运维。同时云开发提供的静态托管、命令行工具(CLI)、Flutter SDK 等能力极大的降低了应用开发的门槛。使用云开发可以快速构建完整的小程序/小游戏、H5、Web、移动 App 等应用。 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-09-14