- 连线消除游戏的原理和实现
嗨!大家好,我是小蚂蚁。 继三消和点消之后,我们来继续了解下一种连线消除游戏的原理和实现。连线消除和之前的两种有着很大的不同,因为它不需要自动的查找算法,连线的整个过程都是由玩家自主手动完成的。 [图片] 如图,连线消除游戏的规则是这样的:从玩家按住第一个图标开始,可以沿着水平,竖直,斜方向划线,如果划到的另一个图标跟之前的图标是一样的,就连起来,可以一直连直到再没有相同的图标为止。此时玩家松手,如果连成线的图标数量大于等于三个,则这些图标就能够被消除。 在连线消除游戏中,重点有两个,一个是判断下一个划到的图标跟之前的图标是否相同,是否应纳入匹配队列,另一个是如何将这些相同的图标用线连起来。 先理论老规矩,我们还是先看理论部分,先来解决第一个问题,如何判断下一个划到的图标跟之前的图标是否相同。 [图片] 如图,中间的 6 个绿色的三角形图标是相同的,可以用一条线把它连起来。但是我们怎么才能让计算机知道它们 6 个是一样的呢?如果之前有好好学习的话,那你应该立即反应过来,数据抽象呀! [图片] 如图,每个图标都对应一个数字,我们就可以把当前的整个游戏抽象成一个包含数字的表格,判断两个图标是否一样,其实就变成了判断两个数字是否相等。 至于如何能知道玩家当前按住了哪个图标呢?网格布局呀!至于如何知道玩家手指当前划到了哪个图标呢?还是网格布局呀!不清楚的话赶紧去【这里】补补课。 再来解决第二个问题,如何将相同的图标用一条线连接起来。 [图片] 把所有相同的图标连成一条长线,其实就是在每两个相邻的图标之间连一条短线,如图,一共是 6 个相同的图标,那么共需要 5 条短线来把它们全部连接起来。 在玩家操作的过程中,我们可以将相同图标的位置存储到一个匹配列表中,连线的时候,只需要从匹配列表中依次的取出两个相邻图标的位置,然后在这两个位置之间创建一条直线即可。 至于如何消除,消除后上方的图标如何下落,系列课里之前都有讲过,不会的同学可以去前面补课,这里就不再重复讲了。 以上就是理论部分了,相比三消和点消,连线消除真的简单很多。 再实践接下来,我们就来看一下连线消除游戏在微信小游戏制作工具中的实现吧! 首先,来看一下对于玩家操作处理的积木逻辑。 玩家手指按下: [图片] 玩家手指滑动: [图片] 玩家手指抬起: [图片] 其中有一个地方可能有的同学不太理解,就是为什么要把行列号换算成索引,有的时候又要把索引换算成行列号呢?因为换算成索引可以让我们只需要使用一个列表就可以存储一个图标位置。我们都知道行列号是一个图标的唯一标识,如果我们存储行列号的话,一个行号一个列号就得使用表格的形式,或者使用两个列表。现在我们把它换算成索引,只需要使用一个列表就够了。之前的课程中我们讲过,行列号和索引是等价的,是可以互推的,所以为了存储方便,我们就转换成索引,为了计算方便,我们就转换成行列号。 接着再来看一下图标上的匹配判断。 [图片] 匹配判断很简单,整个过程是这样的:每当玩家的手指滑动到一个相邻的图标上时,就给所有的图标发送一次匹配通知,每个图标都会判断当前是否划到了自己身上,并且判断自己是否跟之前连接的图标类型相同,当同时满足这两个条件时,就把这个图标加入到匹配列表中。 以上的这些是如何找到相同的图标,并且把它们的索引记录到一个匹配元素列表中。有了这个列表,就相当于我们有了每个图标的行列位置,接下来就是依次的取出两个图标,然后用一条线将它们连接起来了。 [图片] 如图,是连接线的积木逻辑,注释已经写的很清楚了。这里我们重点来看一下其中的获取两个点位置的函数,这个函数的作用是利用两个图标的索引,计算出这两个图标在屏幕上的坐标位置(x,y)。 [图片] 这个函数的积木逻辑主要进行计算处理,先利用图标的索引计算出图标的行号和列号,然后再根据行列号计算出图标在屏幕上的位置坐标 (x,y),并存储在返回列表中,这里我们利用了一个局部的列表变量来存储函数计算的值,用于在后续创建连线的时候使用。 最后,我们再来看一下如何根据两个点的坐标位置,设置一条连接线。 [图片] 都是一些简单的数学计算,这里就不再赘述了。 连线消除的教程到此就结束了,至此,我已经写完了三消,点消,连线消的所有教程,这个坑总算是填完了,终于可以长长的舒一口气了。 消除游戏至今仍然是一个很大的品类,受众很广泛,对于个人游戏开发者来讲依然是个不错的选择。希望这些系列教程能够帮助你了解各种各样的消除游戏,如果觉得教程不错,不要忘了点赞转发,也算是对我辛勤创作的一点儿鼓励了,哈哈! --- 自己学习没氛围学不下去?遇到问题无人解答?缺少经验不知该如何前行?......欢迎加入小蚂蚁的游戏开发课,不只是一门课,而是围绕着新手学习做游戏有关的一整套服务。从入门到进阶一套服务全部搞定,欢迎来跟一百多位同学一起学习做游戏。【点击这里】可了解课程服务详情。 欢迎关注我的微信公众号【小蚂蚁教你做游戏】,学习更多游戏开发原创教程。 [图片] 也欢迎加小蚂蚁微信(xiaomayi6669),交个朋友,不闲聊,只接受付费咨询,望见谅。 [图片] 这里是小蚂蚁的小游戏系列,闲暇之余希望能给你带来片刻的放松和愉悦。无需下载安装,微信扫码可以直接玩啦! [图片] [图片] [图片] [图片] [图片] [图片] [图片]
2023-04-18 - 从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 - 如果我收了用户100元,手续费是0.6%,我通过付款到零钱给用户退了100元那么手续费是如何计算的
如题目,如果我收了用户100元,手续费是0.6%,我通过付款到零钱的接口 给用户退了100元那么手续费是如何计算的
2021-11-11 - 我在前端使用wx.chooseWXPay支付完成后点击完成后为什么直接返回到微信聊天界面了?
代码是这样的 wx.chooseWXPay({ timestamp:res.data.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: res.data.nonceStr, // 支付签名随机串,不长于 32 位 package:res.data.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*) signType:res.data.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' paySign: res.data.paySign, // 支付签名 success: function (result) { $(".shadow").hide(); $(".payType").hide(); $.toast('支付成功'); location.href="{php echo $this->createMobileUrl('index',array('op'=>'order_service'))}"; }, fail: function (result) { alert(2) $.alert('fail : ' + result); }, complete: function (result) { alert(result.errMsg) debugger isLoading=false; } });
2021-11-30 - 从安全角度审视,我是奉劝您,尽量不要轻易升级至APIv3
运行环境 微信支付运行时,依托强大的微信环境,国民级应用,安全性想说都没法说,杠杆滴;而接口服务,是一个后端服务,几乎看不到摸不着,这里我就简单的说一两句。 数据签名 之所以有数据签名,也是基于安全考虑,在异步运行时环境里,变量因素太多,保证请求到数据没有被篡改、伪造,简单粗暴的方式就是对数据签名,引申出的就有如下的签名方案: MD5摘要(哈希)算法 这其实是个单向数据摘要方案,官方加密方案是把secret拼接到待摘要字符串末尾,进行摘要。MD5摘要的签名,目前公认不安全了(暴力强算可逆),不建议再使用了。 HMAC-SHA256摘要(哈希)算法 稍微比MD5数据签名安全一点的是HMAC-SHA256,这也是单向数据摘要方案。HMAC缩写的全称是Hash-based Message Authentication Code,摘要算法支持 sha256, sha512 等算法。 在这不得不提一下APIv2上几个不洁癖的地方,就是在返回值验签上,有些接口验签默认值MD5,有些是HMAC-SHA256,有些接口xml只返回了sign字段,有一些还没有,迭代风格迥异。 研发的同学可以按如下规律来判断,在有返回[代码]sign[代码]字段的时候,官方是采用的哪种签名方案,即: 返回sign长度为32字节,平台方采用的是[代码]MD5[代码]数据摘要方案; 返回的sign长度为64字节,官方采用的是[代码]HMAC-SHA256[代码]数据摘要方案; 无sign字段的几个接口,属于特定领域使用的,估计官方也不会再迭代了,先将就着用吧。 AES-ECB加解密 Advanced Encryption Standard with ECB (Electronic Codebook 电码本) mode 模式是分组密码的一种最基本的工作模式。算法及模式可以百科一下,这里就不展开了,属于对称加密,安全强度相较于 MD5/HMAC-SHA256 要高许多,不过存在共享[代码]密钥[代码]问题。 AES-GCM加解密 Advanced Encryption Standard with GCM (Galois/Counter Mode) 指的是该对称加密采用Counter模式,并带有GMAC消息认证码。属于对称加密,密文相较于AES-ECB解密难度要高,对应的安全等级也高一些,不过也存在共享[代码]密钥[代码]问题。 RSA-OAEP加解密 RSA加密属于非对称加密,公钥加密/私钥解密,公钥是系统间交互的,私钥是双方秘密保存的,解决了共享[代码]密钥[代码]问题,相对来说,安全级别高许多。 APIv2 适用业务形态 收单服务 资金应用 营销工具 通关报关 APIv3 适用的业务形态 收单服务 资金应用 支付分 智慧零售 智慧商圈 支付即服务 支付即会员 消费者服务 营销工具 两个版本安全对比 业务形态 APIv2密钥 API证书 APIv3密钥 收单服务 需要 无需 需要 资金应用 需要 需要 需要 营销工具 需要 需要 需要 通关报关 需要 需要 - 支付分 - 需要 需要 智慧零售 - 需要 需要 从上图表格以及技术方案来看,APIv3的数据安全等级高了许多,然而,所有的操作,均使用一套 [代码]APIv3证书[代码] 及 [代码]APIv3密钥[代码],这对商户的安全等级考量是非常高的了,至少要达到 [代码]安全合格[代码] 检查的3类级别,通俗的讲就是要去商户,对关键信息具有较强的安全管控,以规避因误造成的损失。白话说一下风险点就是,只要掌握了商户的 [代码]API证书[代码],那么商户的“底裤条纹及颜色”就曝露在光天化日之下了,这是对商户的一个考验。 而APIv2,收单服务仅依赖密钥,在不暴露 [代码]API证书[代码](或者根本没有申请)的情况下,仅存在理论上的“暴露短裤”风险,短裤和底裤比较起来,相对还好的吧。。。 所以,在当下这个时间节点,在升级APIv3这件事情上,商户需要谨慎考虑并且需要做 等保合规 评估。 安全防护方面较弱的商户,我的建议是,能不提供 [代码]API证书[代码] 的绝不提供;能用APIv2的,绝不升APIv3;实在没法儿的业务,坚决要做到[代码]API证书[代码]管控,宁可不做,也不能存资金安全隐患。 不得不用的场景 对于官方新晋的赋能产品,有很多产品功能包,均是以[代码]APIv3[代码]形式提供,比如 消费者投诉接口,这项服务是微信生态内,健康交易的一个双向保障,对以服务为导向的行业来说,有此接口能力,是一个很好的体现服务质量的通道。强烈建议商户朋友们能够尽快接入。 若要接入,这下问题就来了,APIv3接口,全部需要API证书,而API证书又什么都能干。这么大的权限,对不得不接入的功能,应该如何接入呢? 从实践上,有如下几个建议: 建议[代码]API证书[代码]做安全等级管控,商户/服务商平台上,对于出资金可以设置白名单的地方,均设置上安全请求IP白名单(白名单目前只有5个名额,而且还不支持IP网段形式,且用且珍惜); 建议使用独立的[代码]签名服务[代码],把[代码]API证书[代码]圈定在有限的环境内,避免将[代码]API证书[代码]本身分发出去,特别地,对于APP对接微信支付,千万千万 不要把API证书内置了,APP本身就是分发机制,这证书要是分发出去,后果简直这太可怕了; 建议独立的[代码]签名服务[代码]要对请求签名的[代码]应用请求[代码]进行身份认证鉴权;尤其是对于关键的 出资金 业务,不仅要验证[代码]应用请求[代码]和参与签名的路径,甚至请求报文的核心字段要做验证; 远期上来看,APIv3肯定是趋势,何以“破解”API证书权限过大问题,待就等等官方的大智慧吧。 附注链接 APIv2 安全规范-签名算法 APIv2 SHA1数据签名 APIv2 退款结果通知-数据加解密 APIv3 数据签名 APIv3 敏感信息加解密 APIv3 账单下载SHA1数据签名
2021-05-10 - 微信支付商户平台(服务商平台)扫码登录后提示“登录超时,请重新登录”时该怎么处理
问题说明 微信支付的服务商 or 商户在登录微信支付平台时,通过扫码方式登录,在手机微信端选择「允许登录」后,商户平台一直提示“登录超时,请重新登录”,很多人这时候会以为是自己有问题,就会重新扫码登录,然后发现还是无法登录,无限死循环。换个浏览器操作,发现又可以正常登录。心里默默的「文明用语」问候一下开发者。 问题复现 访问腾讯地图 map.qq.com后,再访问微信支付商户后台,扫码登录就会出现「登录超时,请重新登录」这个死循环。这时候就有人想说了,你干嘛要访问腾讯地图呢?可能有一些新商户或者新服务商不太了解以前微信支付推出的「智慧经营」活动,也就是早期的「微信支付交易达标免费投放朋友圈广告」功能,这个功能在操作时需要为广告指定一个「门店」,以门店周围3公里为准进行朋友圈广告曝光,这个「门店」添加时需要在腾讯地图上标注过才能在服务商平台里面进行选择,于是访问服务商平台的同时,还会访问腾讯地图查询商户是否可以搜到,如果搜不到就要给商户进行地图标注。 问题来了,这时候微信支付服务商平台还是可以正常使用的,如果你一旦主动退出或会话超时自动退出、切换商户号后,再去登录微信支付商户平台就会出现「登录超时,请重新登录」,此时心里又默默的「文明用语」。当然,如果你打开浏览器后,先访问腾讯地图网站后,再登录微信支付服务商平台,那么同样会出现这个问题。 bug原因 该问题与微信支付商户平台网页的「cookies」有关。如果只登录微信支付商户平台,这个时候平台页面对应的cookies中,只有一个「Name」为 「session_id」 的「cookies」,该「cookies」的「domain」为 「pay.weixin.qq.com」 。 [图片] 如果访问过腾讯地图网站后,那微信支付商户平台页面对应的「cookies」中,就会出现2个「Name」为 「session_id」 的「cookies」,多了一个「domain」为 「.qq.com 」。 [图片] 正是因为这个「cookies」的原因,才导致商户平台出现「登录超时,请重新登录」这个死循环。 如何解决 1、不要在同一个浏览器同时登录微信支付商户平台和腾讯地图网站 2、出现扫码登录确认后,商户平台出现「登录超时,请重新登录」的情况时,清空浏览器浏览记录中的cookies,然后重新扫码登录即可。 3、在浏览器的收藏夹中新增一个书签,名称自己随便取,哪怕你取个「文明用语」,你只要能知道是做什么用的就好了,把下面内容复制添加到网址里面,扫码登录后出现「登录超时,请重新登录」的情况时点击这个添加好的书签,你会神奇的发现,你可以正常访问微信支付商户后台了: [代码]javascript: void((function(){function delecookie(a){var b=new Date;b.setTime(b.getTime()-1e5),document.cookie=a+"=v;expires="+b.toGMTString()+";path=/;domain=.qq.com"}delecookie("session_id");window.location.href = $(".page-error p a").attr("href")})())[代码] 结束语 此问题发现接近「四年」,期间反馈给各种支付、地图各种渠道N次,无奈一直没能解决,希望可以早日修复,大家都不会用到这篇教程。祝大家新的一年里,代码没bug,升职加薪。
2021-03-17