- 微信小程序服务端推送消息报错43101,为什么?
我用服务端推送消息,调用https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={accessToken},返回errcode:$43101,msg:$user refuse to accept the msg rid: 661ff381-7e9fd9f8-5c88ec7a。我确定我在小程序的设置-》通知管理中打开了接收通知,但是没有用,昨天我还收到了6条通知,但是后续就没有再收到,今天一条通知都没有收到,我的日志记录全是43101这个错误。
04-19 - openapi.subscribeMessage.send发送订阅消息,错误43101?
{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"Error: errCode: 43101 | errMsg: openapi.subscribeMessage.send:fail user refuse to accept the msg rid: 666fcb36-3d5d52df-1a083687\n at callWXOpenAPI (/var/user/node_modules/wx-server-sdk/index.js:2397:31)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async sendMsg (/var/user/index.js:8:18)","statusCode":430} 我自己的微信号,还有很多次的授权啊。提示错误43101,查了意思是没有授权次数了 我明明还有授权的啊。 我一开始用触发器给自己的微信号一下子发送了十几条左右的消息。然后就再也收不到这个模版的消息了。 另一个手动触发的模版消息还能收到。 这个是触发器的订阅模版消息就收不到了??
06-17 - 小游戏管理端游戏圈任务配置指引
一、能力介绍 游戏开发者可以通过游戏圈创作任务设置创作奖励,激励作者创作相应游戏内容,丰富社区内容。游戏开发者可在游戏圈管理平台(小游戏 MP ->游戏运营工具箱->活动运营工具)进行任务配置游戏圈创作任务。 游戏圈创作任务是面向全量用户可见,支持按照曝光/点赞对用户进行礼包奖励的结算。可支持的任务类型有:发表领奖/发表抽奖/梯度/排名任务。 二、前置准备工作(提前 1~2 天准备完成) 1、素材规范参考: 小游戏接入游戏圈任务素材设计规范 2、配置位置:小游戏mp->游戏运营工具箱->游戏礼包道具->游戏礼包配置 3、任务规则、说明等相关文案提前准备,提供方案模板参考如下:小游戏接入游戏圈任务素材设计规范 4、配置注意事项: 1)关于时间问题,配置任务一共涉及3个时间:投稿时间、发奖时间、领奖截止时间。 a)投稿时间:2024年9月1日~9月31日; b)发奖时间:2024年10月7日(发奖时间至少在投稿时间结束7天后); c)领奖截止时间:2024年10月14日(领奖截止时间至少在发奖时间截止7天后); 2)话题标题需要与主题关联,且不可进行利诱活动,不得使用“瓜分现金”字眼 3)如果发表奖励是抽奖类型的奖励,需要公布每种奖项的中奖概率 [图片] 三、任务配置 【任务配置总流程】 Step1:准备素材,提前上传设计审核 Step2:配置小游戏游戏圈任务礼包; Step3:创建游戏圈创作任务,并进行任务说明/奖励/奖励计算规则等相关配置; Step4:全量提审发布。 Step1:准备素材,提前上传设计审核: ① 需要准备素材,参考:小游戏接入游戏圈任务素材设计规范 ② 素材准备好后,需要提前平台上上传给设计审核,审核通过才能使用(设计审核时间 2天)。 [图片] ③ 如果素材被驳回需要及时根据驳回要求修改,改好后重新上传,查看驳回理由操作指引; [图片] [图片] [图片] Step2:配置小游戏游戏圈任务礼包; ① 注意礼包的有效期需要大于奖励领取截止时间,礼包类型选择“游戏圈创作任务”另外; ② 另外发放数量、日限制,需要与任务对应,配置位置:小游戏mp->游戏运营工具箱->游戏礼包道具->游戏礼包配置 [图片] [图片] [图片] [图片] 注意: ① 礼包名称填写具体的礼包,举例:可以填具体礼包“砖石x10+点券x10”、“皮肤体验礼包(7天)”、“金卡x1”、“新人礼包”等; 切忌填写:“发表抽奖奖励1“、”【排名奖励】top11-20”、“【点赞奖励】有效点赞≥600奖励礼包” 、“奖励1”等; ② 配置奖励图片随机拉取某一个道具的图片填充; ③ 【礼包有效期】的事件要在任务配置页配置的【奖励领取截止时间】之后; ④ 领取限制:根据奖励实际发放情况配置; 礼包单日上限/礼包单周上限/礼包总量上限 建议填一样的数字,即礼包发放总量上限; 礼包领取限制需要满足:用户单日≤用户每周≤用户总量、礼包单日≤礼包每周≤礼包总量、用户单日≤礼包单日、用户每周≤礼包每周、用户总量≤礼包总量 Step3:创建游戏圈任务,并进行任务说明/奖励/奖励计算规则等相关配置; ① 游戏圈创作任务创建入口:小游戏 MP ->游戏运营工具箱->活动运营工具->新建/编辑活动 [图片] ● 活动类型:选择“游戏圈创作任务”; [图片] ②配置流程: A)基础信息配置: ● 活动名称:活动名称不可重复,命名可参考格式:游戏名称-活动名称-20220910; [图片] B)页面配置-话题信息配置: ● 话题标题:话题的标题,15字以内 ● 话题简介:对话题的简要介绍,70字以内 ● 是否配置投票:选择是后可以配置投票信息,根据活动需求设置 ● 话题顶部是否插入直播间:选择是后可以配置直播间,点击后可以查看直播间,根据活动需求设置。 [图片] [图片] C)页面配置-任务入口详情页配置 ● 投稿时间:在这个时间段内玩家在话题中发布内容可以参与活动,超出这个时间后玩家可以发布内容,但是无法参与活动。 ● 发奖时间:是玩家看到的奖励结算日期,需要在投稿时间结束后七天以上。 ● 领奖截止时间:是活动下线的时间,需要在投稿时间结束后七天以上。 ● 任务中心宣传图:是“活动合集”处的配图。 ● 任务图:是活动详情页中的icon。 ● 任务类型:是指排名奖励和梯度奖励的计算指标。 ● 规则配置:是展示给玩家的活动参与规则,图文和视频要求设置的限制关系着玩家发布的帖子是否可以参与活动(未达到要求的会告知用户审核不通过),审核员须知则是项目组提供给审核员的审核标准,审核员会以此处规则为准进行内容审核。 [图片] [图片] D)页面配置-奖励模块配置 目前游戏圈创作任务有四种奖励模块,分别是发表奖励、梯度奖励、排名奖励、开发者抽奖。 ● 发表奖励:分为发表后领奖、发表后抽奖,领奖是直接领取该奖励,抽奖需要玩家抽取奖励,抽奖需要配置9个奖励内容 [图片] 点击新增一条,进入添加奖励页面。通过奖励库管理管理之前配置好的礼包,就可以在下拉框中选择奖励,填写好相应的奖励名称和说明,即可生成奖励。 [图片] [图片] [图片] 发表后抽奖需要配置抽奖名称、抽奖说明、抽奖次数以及宣传图,可以在查看示例看到具体的显示位置。 [图片] 点击新增一条,进入添加奖励页面。通过奖励库管理管理之前配置好的礼包,就可以在下拉框中选择奖励,填写好相应的奖励名称、抽奖图、抽奖概率和是否保底奖励,即可生成奖励。抽奖概率需要抽奖后发表中所有奖励加在一起为100.保底奖励只允许存在一个,建议为数量很多的奖励作为保底。 [图片] ● 梯度奖励:根据任务类型选择的点赞、曝光两种方式判断完成度,当玩家达到兑换门槛后即可兑换相应奖励。梯度奖励需配置3-5条确保存在梯度 [图片] 点击新增一条后进入添加奖励页面。通过奖励库管理管理之前配置好的礼包,就可以在下拉框中选择奖励,填写好相应的奖励名称和兑换门槛后即可生成奖励。玩家在达到点赞、曝光两种方式的门槛后便可领取奖励。 [图片] [图片] ● 排名奖励:取排名中设置区间内的获奖人进行发奖(如果同一人发布多条帖子,则会取最高一条),如设置的1-5与6-20,那么在排名中取得相应名次的玩家即可获得对应奖励,设置的区间需要从1开始并是连贯的。如果设置为手动发放,须在发奖时间之前通过排名奖励审核(见本文第六部分)选择区间内的获奖人,若未手动设置区间,平台会自动发放奖励。 [图片] 点击新增一条后进入添加奖励页面。通过奖励库管理管理之前配置好的礼包,就可以在下拉框中选择奖励,填写好相应的奖励名称、排名上下限。活动结束后处于排名之中的玩家便可以领取该奖励。 [图片] [图片] ● 开发者抽奖:可以选择按热度抽奖也可以选择随机抽奖,如设置自动发放,活动结束后会根据选择条件进行发放。如设置为手动发放,须在发奖时间之前通过排名奖励审核,选择区间内的获奖人,若未手动设置区间,平台会自动发放奖励。 [图片] 点击新增一条后进入添加奖励页面。通过奖励库管理管理之前配置好的礼包,就可以在下拉框中选择奖励,填写好相应的奖励名称、抽奖方式、抽奖规则便可以生成奖励。奖励数量是根据配置的礼包显示的。 [图片] [图片] Step4:全量提审发布 以上配置完成后,点击底部按钮【提交审核】即可提审成功,提审后等待1~2个工作日审核。审核通过即全量发布 [图片] C端游戏圈任务查看入口: ① 关注公众号【微信游戏内容创作】菜单栏“创作变现-任务中心” ② 活动列表预览、扫码、链接访问 [图片] 以上除了奖励库配置,所有涉及使用图片素材的地方均支持选择【审核通过】和【审核中】的素材如果选择【审核中】的素材并提审话题/任务, 话题/任务和素材将一并提审,如果素材被驳回,话题/任务也会被驳回,需要重新修改素材并重新上传使用。 每次编辑,所有图片素材都需要重新选择,否则会出错。 常见Q&A Q:活动入口在哪里? A: 活动入口有两处,一种是通过话题进入,一种是通过任务中心进入。话题可以通过(小游戏 MP ->游戏能力地图->游戏圈->运营配置->最热Tab设置)中的四种方式增加话题曝光度。 [图片] [图片] Q:上面标注了道具数量,下面也标注了数量,怕用户误解可以领取砖石233*233份; [图片] A:此处是因为奖励礼包命名的原因,命名时候注意不带“*233”即可。 [图片]
10-12 - 【官方速进】微信小游戏礼包测试调试时,返回SubErrCode=172935494,仍无法通过测试!
[图片]
2023-08-10 - 48004 api被封禁,无法发送消息?
[图片] AppID:wx7c42d3236b67487d
2023-06-28 - 信息推送出现45047?
小程序调用客服消息,进入客服会员后,下发消息失败,返回错误{"errcode":45047,"errmsg":"out of response count limit rid: 64377695-01f810cf-2709b3a8"}
2023-04-13 - 拉起小程序客服会话后,发送小程序卡片消息,服务器log中MsgType的值为什么一直都是event?
原生前端代码如下: <button class="conbutton" open-type="contact" show-message-card='true' send-message-title="**" session-from="{{sessionFrom}}" bindcontact="handleContact">立即加入</button> 服务器log接受到的信息: {"ToUserName":"*************************","FromUserName":"*********************","CreateTime":1701334405,"MsgType":"event","Event":"user_enter_tempsession","SessionFrom":"wxapp"} 其中的MsgType值不应该是miniprogrampage?为什么是event?
2023-11-30 - out of response count limit rid
您好,用户进入小程序客服界面,第一次回复客服消息的情况下,调用接口返回{\"errcode\":45047,\"errmsg\":\"out of response count limit rid: 60a1e629-2bd7c461-1fff7f6a\"} 并没有超过上限,这是什么原因呢?
2021-05-17 - 信息推送出现45047错误或者40001(invalid credential)
最近在做一个客服的信息推送,用户点击客服会话按钮后,给用户推送一条欢迎信息。很奇怪的是,第一次进去老是报45047或者40001错误,强制刷新一次access_token也还是一样,关键是只有第一次进去才会这样,后面的点击客服会话按钮进入都能够正常的给用户推送信息。官方能够解释一下原因么?还是说这个是本来存在的bug
2018-12-25 - 小程序客服会话,为什么不能主动推送消息?
小程序的客户回话为什么不能用户一点击进来就主动推送一句话呢?所有的客服都要等着客户说问题,明显不符合现在社会的客服需求呀,客服不能只作为一个被动的使用工具呀,只要客户打开客服会话,肯定是有相关的事情,主动一个问候或者介绍会减少很多冷漠感,也让小程序运营者更加主动一些,希望领导们对这种功能稍作改动,这样极大提高实用性!~~
2021-11-13 - 微信小游戏使用openCustomerServiceConversation发送客服消息的问题
微信小游戏使用wx.openCustomerServiceConversation发送客服消息,第三方开发者后台接收不到消息推送;如果手动给客服发消息,第三方开发者后台可以收到消息推送,是怎么回事?补充说明: 1.回调地址也检查过了,更改回调地址第三方后台也能收到消息; 2.调用wx.openCustomerServiceConversation后可以收到success和complete,消息内容也是ok。
2023-05-05 - 小程序接入客服消息,报错误:{"errcode":45047,"errmsg":"out of ?
小程序接入客服消息,后端监听到用户进入user_enter_tempsession事件时,给用户发送自定义的推荐内容时,报如下错误:{"errcode":45047,"errmsg":"out of response count limit rid: 64c8c1df-6b16abca-4d85d90e"},用户第一次进入就报这个错误。还没有达到条数限制。发送是通过https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=接口发送的。
2023-08-01 - appid相同的小程序,同一个手机号的用户openid不同是因为换微信号了吗?
[图片]
2023-07-18 - 一个用户在两个微信号登录相同的小程序,其openid不同吗?
一个用户在两个微信号登录相同的小程序,其openid不同吗?
2021-03-24 - 聊天工具栏可以配置聊天记录入口吗?聊天记录可以最长获取多久的?
1,聊天工具栏可以配置聊天记录入口吗? 想在聊天工具栏里配置查看聊天记录的入口,场景是一个员工离职了,转给另一个员工后,这个新员工需要能看到之前和这名客户所有的聊天记录。 2,聊天记录可以最长获取多久的? 如果可以看到之前那个员工和客户的聊天记录,最长可以看到多久以前的? 3,如果之前的员工清空过聊天记录,转给新员工后,他还能看到之前和客户的聊天记录吗? 因为怕员工离职时,恶意清空所有客户的聊天记录。
2020-10-29 - 企业微信好友上限多少人?企业微信可以加多少人?
企业微信和个人微信互通是腾讯做2B2C的一张王牌,企业微信成员可以直接添加个人微信用户,进行私域流量运营。那么企业微信和个人微信添加好友数量是一样的吗?企业微信在用户管理上有哪些优势?
2020-06-23 - 你好,原来微信的好友如何快速迁移到企业微信?
比如我到微信有100个好友(是客户好友),想批量迁移到企业微信,如何操作? 目前是需要一个一个加,并且还需要客户在微信确认后,才能加成功。
2020-02-03 - 微信用户如何迁移到企业微信
我们大概1.2w个用户需要迁移,知道一些第三方工具不敢用,因为我是技术,如果使用了工具导致微信账号被封,这个风险我承担不起,大家现在是怎么解决微信用户迁移到企业微信的那。
2020-07-29 - wx.request会超时的问题?返回fail:timeout
现在公司开发的小程序用户群体大概30几万,每天有3、4万的活跃用户,每天都会有几十个request fail的情况发生,在社区中搜索了类似的帖子,发现有很多类似的情况,但是没有找到确切的解决方案。 查看了开发文档,看到tls相关的描述,文档中提示tls版本需支持1.2及以下,否则部分安卓机型无法支持,自检了域名,发现域名未能支持tls1.0,请问这是问题的关键所在吗?或者能否提供其他解决方向,谢谢。 tls检测情况: [图片]
2022-07-26 - wx.login 为什么返回request timeout?
[图片] [图片] [图片] 请求帮助,谢谢大家,是不是腾讯把我拉黑了呀
2022-01-12 - 小程序频繁request:fail timeout
[图片] 直接请求接口是可行的
2021-08-18 - 请问{"errMsg":"request:fail timeout"}是什么原因引起的?
请求接口报这个错误{"errMsg":"request:fail timeout"} 但是服务器并没有收到请求,是不是网络丢包导致的呀
2020-10-27 - 微信小程序进入客服会话后,能自动给客服发送消息和卡片吗?
需求:用户在小程序中点击进入客服会话后,根据点击的按钮自动给客服发送消息和卡片,客服根据发送的消息定位问题。 问题:怎么实现自动发送消息给客服呢?不需要用户手动输入 [图片]
2022-10-19 - 小游戏游戏内容、素材与其他游戏存在雷同,需提交相关权利文件?
请问下具体是需要提交什么类型的资质呢,还有就是怎样才能知晓小游戏违规已经整改通过了呢,上述属于通知信息,没入口了解是否已经通过 [图片]
2022-11-02 - 使用公众号、小程序,查看任意小程序的页面路径
前言 需要准备一个 公众号 或 小程序 账号才能查看小程序页面路径,如果你没有账号但是想获取页面路径,可以尝试咨询小程序的客服、开发者。 步骤1.打开小程序搜索框 公众号看这条:登录公众号后台,依次点击 图文素材(草稿箱) -> 新的创作 -> 写新图文 -> 小程序 [图片] [图片] [图片] 小程序看这条:登录小程序后台,依次点击 工具 -> 生成小程序码 [图片] 步骤2.搜索与配置微信号 [图片] [图片] 步骤3.复制页面路径 进入要复制的小程序页面,依次点击 右上角 ··· 按钮 -> 复制页面路径(部分手机需要向左滑动才能看到) [图片] ⚠️ 注意事项 复制出来的路径 pages/watermark/index.html?key_=16311567863922 在小程序、文章使用时候,里面的 .html 需要删除: 删除前:pages/watermark/index.html?_key=16311567863922 删除后:pages/watermark/index?_key=16311567863922 如果路径用在APP、小程序推送,则不需要删除后面的.html
2022-04-16 - 微信小游戏体验版使用虚拟支付异常
微信小游戏体验版使用虚拟支付异常, 报错code=1138, msg=您一次购买的太多了,请调整数量重新购买。有哪位大佬知道是什么问题么? 感谢🙏🙏🙏🙏🙏
2022-07-29 - 为什么我的小程序无法转发?
小程序今天刚过审,本来想转发给老板看下的,结果显示“当前页面不可转发”,里面的其他页面倒是可以转发,现在就不知道什么因为什么原因[图片]
2019-09-30 - 请问专员大大,帮我查查,为什么我发布的小游戏不能转发?个人辛辛苦苦开发不容易啊
AppID: wxe78221758cee711c 小游戏名称: 魔方海洋 [图片]
2021-01-13 - 当前交易存在被骗风险如何解决?
[图片]
2020-11-17 - 【求解】本次交易存在被骗风险,为保障你的资金安全,暂无法完成交易。究竟如何解决呢?
商户号:1532459111 已多个不同客户支付提示这个,辛苦帮排查下,谢谢 [图片]
2021-02-22 - 本次交易存在被骗风险,为保障你的资金安全,暂无法完成交易。
客户通过APP微信支付,维信支付弹出“本次交易存在被骗风险,为保障你的资金安全,暂无法完成交易。请准确核实对方身份,谨防不法分子以网购、兼职、贷款等名义实施诈骗。若确认后仍需付款,可点击“查看解决方法”。” 请问这是什么问题?商户都是正常的,没有违规记录。
2020-07-09 - 米大师虚拟支付 开发小攻略
前言 这两天公司开发需要用到米大师虚拟支付,对接时,由于各种原因,导致开发时碰到很多问题,各途径求助无解,论坛求助无人回复,腾讯客服推给米大师Q群,Q群申请没有反应,现开发完成,记录一下当时碰到的一些问题,希望能帮助到大家。 1.sig签名可使用腾讯提供的API加密方法,也可以按照微信文档自行写,或者在米大师官网上能找到加密demo。 2.sig签名所需secret需要在自己的米大师后台获取。(开始以为使用官网提供秘钥就可以了,文档并没有明确说明)。 3.签名时org_loc,分为沙箱环境与正式环境,两者不可搞混,否则报错。 4.mp_sig签名后,session_key记得销毁。 5.curl传输时,参数需要转为json格式。(开始没有仔细看文档,纠结了很久才发现需要json格式)。 都是一些小问题,但是由于文档原因,浪费了很多时间,如今开贴,希望能帮助到大家。
2019-02-22 - 请问一个公司主体(营业执照)能申请多少个微信支付商户号?
请问一个公司主体(营业执照)能申请多少个微信支付商户号?
2019-04-24 - 同一个公司(主体)可以申请多少个微信支付商户号?
请问同一个公司(主体)可以申请多少个微信支付商户号,具体怎么申请多个微信支付商户号?
2019-09-02 - 一个公司主体可以申请几个微信支付商户号?
2个小程序对应的主体是一家公司,可以申请2个微信支付商户号,分别给2个小程序使用么??
2019-09-29 - 请问一个公司主体是否可申请多个小程序么?
请问一个公司主体是否可申请多个小程序? 如果可以,那么多个小程序同时运营会有什么要求呢。
2021-02-23 - 苹果6splus进入游戏WAGame.js报错
ConsoleSystem All Error Warning Info Log Debug Wechat Lib:2.9.3, 2019.11.8 14:51:39 gameThirdScriptError SyntaxError at line:42,column:86525,SyntaxError: Unexpected token '='. Expected a ')' or a ',' after a parameter declaration. Stack: (native code) at evaluateScriptFile (WAGame.js:1:86525) at value (WAGame.js:1:391577) at value (WAGame.js:1:390725) at (WAGame.js:1:396729) at (WAGame.js:1:396732) at c (WAGame.js:1:388921) at (WAGame.js:1:396625) at y (WAGame.js:1:3624) at b (WAGame.js:1:3874) at v (WAGame.js:1:396476) at (WAGame.js:1:1267950) at global code (WAGame.js:1:1267955) e@[图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:36655 value@[图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:32222 value@[图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:31467 [图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:16387 [图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:19197 forEach@[native code] _log@[图片]https://res.servicewechat.com/weapp/debug/wx7d4b6849a3956e82/2/:27576:19087 想请教一下这个错误一般怎么处理呢
2019-11-14 - 小游戏创建插屏出现报错 Uncaught (in promise) FrameworkError
不影响游戏运行, 插屏可以关闭, 也可以再次打开, 打开报同样的错 完整报错如下: Uncaught (in promise) FrameworkError Object { errMsg: "removeTextView:fail" } Object { <span style="color: #a71d5d;">errMsg</span>: <span style="color: #183691;">"removeTextView:fail"</span> }<div class="eruda-json eruda-hidden"></div> at e.value (wagame://servicewechat.com/WAGameVConsole.html:27576:37895) at new e (wagame://servicewechat.com/WAGameVConsole.html:27576:36659) at t.value (wagame://servicewechat.com/WAGameVConsole.html:27576:32217) at t.value (wagame://servicewechat.com/WAGameVConsole.html:27576:31461) at f.e.<computed> [as error] (wagame://servicewechat.com/WAGameVConsole.html:27576:16382) at wagame://servicewechat.com/WAGameVConsole.html:27576:19192 at Array.forEach (<anonymous>) at console._log (wagame://servicewechat.com/WAGameVConsole.html:27576:19080) at <anonymous>:1:9
2020-09-21 - 微信分账时,xml格式错误解决方法
前两天做微信分账功能,遇到了xml错误,看着代码,怎么看怎么正确,可是,就是报错。 最后,退而求其次,换个方法,问题解决了。 1、首先,生成xml格式字符串 关键代码: public function toXml($arr) { $xml = '<xml>'; $xml .= $this->array_to_xml($arr); $xml .= '</xml>'; return $xml; } //array转xml public function array_to_xml($arr) { $xml = ''; foreach ($arr as $key => $val) { if (is_array($val)) { $xml .= '<' . $key . '>' . $this->array_to_xml($val) . '</' . $key . '>'; } else { $xml .= '<' . $key . '>' . $val . '</' . $key . '>'; } } return $xml; } 2、将这段文字写入xml文件 3、读取这个xml文件。代码如下: //1.设置分账账号 $receivers = array(); foreach ($profitSharingAccounts as $profitSharingAccount) { $tmp = array( 'type'=>$profitSharingAccount['type'], 'account'=>$profitSharingAccount['account'], 'amount'=>intval($profitSharingAccount['amount']), 'description'=>$profitSharingAccount['description'], ); $receivers[] = $tmp; } $receivers = json_encode($receivers); $totalCount = count($profitSharingOrders); $successCount = 0; $failCount = 0; $now = time(); //2.生成签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'transaction_id'=>$profitSharingOrders['transaction_id'], 'out_order_no'=>$profitSharingOrders['out_trade_no'], 'receivers'=>$receivers, ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //3.发送请求 $url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing'; $postXML = $this->toXml($postArr); //为了适应xml格式,先保存到文件,再取出,这样xml格式就能适应了。 $filename = dirname(__FILE__).'/xml/subledger.xml'; file_put_contents($filename, $postXML); $postXML = file_get_contents($filename); 最后,调用post方法 $helper = new Common_util_pub(); $ret = $helper->postXmlSSLCurl($postXML, $url); postXmlSSLCurl方法代码: /** * 作用:使用证书,以post方式提交xml到对应的接口url */ function postXmlSSLCurl($xml, $url, $second = 30) { $ch = curl_init (); //超时时间 curl_setopt ( $ch, CURLOPT_TIMEOUT, $second ); curl_setopt ( $ch, CURLOPT_URL, $url ); curl_setopt ( $ch, CURLOPT_SSL_VERIFYPEER, FALSE ); curl_setopt ( $ch, CURLOPT_SSL_VERIFYHOST, FALSE ); //设置header curl_setopt ( $ch, CURLOPT_HEADER, FALSE ); //要求结果为字符串且输出到屏幕上 curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, TRUE ); //设置证书 //使用证书:cert 与 key 分别属于两个.pem文件 curl_setopt ( $ch, CURLOPT_SSLCERT, dirname(__FILE__).'/cacert/apiclient_cert.pem' ); curl_setopt ( $ch, CURLOPT_SSLKEY, dirname(__FILE__).'/cacert/apiclient_key.pem' ); //证书密码 curl_setopt($ch, CURLOPT_SSLCERTPASSWD, WxPayConf_pub::KEY ); //post提交方式 curl_setopt ( $ch, CURLOPT_POST, true ); curl_setopt ( $ch, CURLOPT_POSTFIELDS, $xml ); //启用时会汇报所有的信息,存放在STDERR或指定的 CURLOPT_STDERR 中。 curl_setopt($ch, CURLOPT_VERBOSE, '1'); $data = curl_exec ( $ch ); //返回结果 if ($data) { curl_close ( $ch ); return $data; } else { $error = curl_errno ( $ch ); echo "curl出错,错误码:$error" . "<br>"; echo "<a href='http://curl.haxx.se/libcurl/c/libcurl-errors.html'>错误原因查询</a></br>"; curl_close ( $ch ); return false; } } 其它代码,网上很多,就不贴出来了。
2021-01-06 - 统一下单后 notify_url回调的值为空
[图片] 2个值都为空 订单号 4200000723202007251157796779 [图片]
2020-07-25 - 第三方平台消息与事件接收 URL 无法收到消息事件?
第三方平台( APPID: wx109b9567c7be5e5a) 设置 消息与事件接收 的URL(http://terminalopen.dingyou168.com/api/wechat/msgnotify/$APPID$/callback) 无法收到消息回调 请帮忙确认url是否有消息推送过来或者是不是url是否正确
2021-02-24 - 小程序客服消息bug
在小程序里进入客服入口,发送消息之后,没有自动回复,但是实际客户已经设置了自动回复,且没有勾选客服消息权限给到第三方平台,但是发送消息的时候一直收不到自动回复,是什么问题呢?[图片]
2021-01-13 - api秘钥和apiv3秘钥的区别
api秘钥和apiv3秘钥的区别,是否接口url中带v3计算签名值是用apiv3秘钥 不带的用api秘钥进行签名
2020-07-22 - 订单支付成功,但是一直未收到微信的支付成功回调通知
微信支付完成后notify_url,没有收到回调通知情况下,可按以下几个步骤排查问题: 确认上送微信回调地址与自己系统实际回调地址是否一致;核实上送回调地址是否可被外网访问,是否有DNS解析;核实是否有安全策略拦截微信支付回调通知;确认回调地址代码的可用性;如果使用V3接口,确认下是否设置加密的秘钥,登录商户平台操作~请参考APIV3秘钥设置。附:回调通知注意事项、支付回调和查单实现指引
2021-04-27 - 公众号支付 jsapi支付授权目录到底应该怎么填?
假如支付页面地址为: pay.wxpay.com/course/courseOrder/courseId/137 那么应该填 pay.wxpay.com/course/courseOrder/courseId/ 吗?
2020-04-20 - “小游戏”是否可申请微信支付?
目前,小游戏不支持使用普通微信支付接口,将单独提供小游戏虚拟支付接口。 申请开通虚拟支付能力的条件:已认证并开通微信支付、且主体为企业/个体工商户的小游戏帐号; 开通流程:微信认证->开通微信支付(开通流程与普通小程序一致,包括商户号绑定)->开通虚拟支付能力。 [图片]
2020-04-10 - 虚拟支付有订单信息,但绑定的商户号里面没交易订单信息
小游戏绑定了企业商户号,游戏的虚拟支付上有显示IAP虚拟支付订单,但是在所绑定的企业商户号中一个交易订单的信息都没,结算信息也是什么都没。 绑定的商户号没结算信息,那这个游戏的虚拟支付怎么才能结算? 求大神解答下,谢谢了 微信小游戏虚拟支付所看见的订单的图 [图片] 这是所绑定微信商户号的交易订单界面图 [图片] 这是所绑定微信商户号的交易订单未结算界面图 [图片] 这是所绑定微信商户号的交易订单已结算界面图 [图片] 从上面可以看见微信小游戏虚拟支付是有订单的,而所绑定的商户号的交易订单、未结算、已结算的界面都是没有一个订单显示的,请问下这是什么情况,商户号没结算信息,我们该如何结算虚拟支付。。 求大神,解答下,谢谢了!
2019-05-25 - 小游戏wx.requestMidasPayment 一直返回错误码-15004 跪求解决
[图片][图片] 之前是有过营业执照未更新 所以支付权限停了 但更新后 更新过后还是不行 这个到底是什么错误啊 太笼统了 现在无从下手 资料什么的都看过无数遍了 有没大佬帮帮忙 再不解决 感觉要被祭天了
2020-12-03 - JSAPI支付返回: JSAPI缺少参数total_fee
请根据以下几点排查问题: 1)package参数格式错误,package参数格式应该是这样: package:"prepay_id=wx201410272009395522657a690389285100" 2)package参数没有获取,返回null。 3)请检查预支付会话标识prepay_id是否已失效 4)请求的appid与下单接口的appid是否一致 5)微信支付金额为“分”,不能有小数点 6)预支付ID为空(1、金额为空 2、订单编号重复 3、订单编号为空) 该条内容由「社区Memory」提供,官方审核发布
2021-03-12 - V3特约商户进件信息加密sodium问题?
环境是php5.5,安装了libsodium插件时,显示安装不成功函数sodium_crypto_aead_aes256gcm_decrypt也用不了,请问下怎么解决呢?
2021-02-15 - 微信合单支付demo - PHP
微信合单支付PHP版demo 1、获取API v3证书 /** * 获取证书 * @return mixed */ public function getCert() { $url = 'https://api.mch.weixin.qq.com/v3/certificates'; $timestamp = time(); $nonce = $this->nonce_str(); $body = ''; $sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); $header = [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, ]; $result = $this->curl($url, '', $header, 'GET'); $result = json_decode($result, true); return $result['data']['0']['serial_no']; } 2、支付请求 /** * @return bool|mixed */ public function pay() { $url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/app'; $requestData = [ 'combine_appid' => $this->app_id, 'combine_mchid' => $this->mch_id, 'combine_out_trade_no' => 'app_pay_' . time(), 'scene_info' => [ 'device_id' => 'pay_device_id', 'payer_client_ip' => '127.0.0.1', ], 'time_start' => date('c', time()), 'time_expire' => date('c', time() + 7200), 'notify_url' => 'http://www.ttglad.com/notify.php', 'sub_orders' => [ [ 'mchid' => $this->mch_id, 'attach' => 'notify with attach', 'amount' => [ 'total_amount' => 100, 'currency' => 'CNY', ], 'out_trade_no' => 'sub_order_' . time(), 'sub_mchid' => $this->sub_mch_id, // 二级商户号 需要进件系统生成 'profit_sharing' => true, // 分账 'description' => '描述', ], ], ]; $header = $this->getCurlHeader($url, json_encode($requestData), 'POST'); $result = $this->curl($url, json_encode($requestData), $header, 'POST'); return json_decode($result, true); } 3、支付查询 /** * @return mixed */ public function payQuery() { $url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/out-trade-no/'; $url = $url . ''; // 支付单号 $method = 'GET'; $data = ''; $header = $this->getCurlHeader($url, $data, $method); $result = $this->curl($url, $data, $header, $method); return json_decode($result, true); } 4、退款 /** * @return mixed */ public function refund() { $requestData = [ 'sp_appid' => $this->app_id, 'sub_mchid' => $this->sub_mch_id, 'transaction_id' => '',// 支付三方流水 'out_refund_no' => '',// 退款单号 'notify_url' => 'http://www.ttglad.com/notify_refund.php', 'amount' => [ 'refund' => 100, 'total' => 100, 'currency' => 'CNY', ] ]; $header = $this->getCurlHeader($this->refundUrl, json_encode($requestData), 'POST'); $result = $this->curl($this->refundUrl, json_encode($requestData), $header, 'POST'); return json_decode($result, true); } 5、退款查询 /** * @return mixed */ public function refundQuery() { $url = 'https://api.mch.weixin.qq.com/v3/ecommerce/refunds/id/' . '' . '?sub_mchid=' . $this->sub_mch_id; $method = 'GET'; $data = ''; $header = $this->getCurlHeader($url, $data, $method); $result = $this->curl($url, $data, $header, $method); return json_decode($result, true); } 6、支付通知 public function notify() { $header = $this->getHeaders(); $body = $GLOBALS['HTTP_RAW_POST_DATA']; if (empty($header) || empty($body)) { throw new Exception('通知参数为空', 2001); } $timestamp = $header['WECHATPAY-TIMESTAMP']; $nonce = $header['WECHATPAY-NONCE']; $signature = $header['WECHATPAY-SIGNATURE']; $serialNo = $header['WECHATPAY-SERIAL']; if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) { throw new Exception('通知头参数为空', 2002); } $cert = $this->getCertBySerialNo($serialNo); $message = "$timestamp\n$nonce\n$body\n"; //校验签名 if (!$this->verify($message, $signature, $cert['plainCertificate'])) { throw new Exception('验签失败', 2005); } $decodeBody = json_decode($body, true); if (empty($decodeBody) || !isset($decodeBody['resource'])) { throw new Exception('通知参数内容为空', 2003); } $decodeBodyResource = $decodeBody['resource']; $decodeData = $this->decryptToString($decodeBodyResource['associated_data'], $decodeBodyResource['nonce'], $decodeBodyResource['ciphertext'], ''); $decodeData = json_decode($decodeData, true); if (empty($decodeData)) { throw new Exception('通知参数解密发生错误', 2004); } // todo 业务逻辑 } 7、其他方法 /** * 初始化参数 */ public function __construct() { parent::__construct(); // 微信支付 商户号 $this->mch_id = ''; // 二级商户号,需要走进件系统生成 $this->sub_mch_id = ''; // 微信支付 商户号绑定的appid $this->app_id = ''; // 商户私钥 $this->private_key = ''; // 商户证书序列号 // 如何查看证书序列号:https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao $this->serial_no = ''; // apiv3秘钥:https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/api-v3-mi-yao $this->mch_key = ''; } /** * @param $serialNo * @return mixed */ private function getCertBySerialNo($serialNo) { $url = 'https://api.mch.weixin.qq.com/v3/certificates'; $timestamp = time(); $nonce = $this->nonce_str(); $body = ''; $sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); $header = [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, ]; $result = $this->curl($url, '', $header, 'GET'); $cert = json_decode($result, true); $return = []; if (!empty($cert['data'])) { foreach ($cert['data'] as $item) { if ($serialNo == $item['serial_no']) { $return = $item; break; } } } return $return; } /** * @param $url * @param $body * @param $method * @return array */ protected function getCurlHeader($url, $body, $method) { $timestamp = time(); $nonce = $this->nonce_str(); $sign = $this->sign($url, $method, $timestamp, $nonce, $body, $this->getPrivateKey($this->private_key), $this->mch_id, $this->serial_no); return [ 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $sign, 'Accept:application/json', 'User-Agent:' . $this->mch_id, 'Content-Type:application/json', 'Wechatpay-Serial:' . $this->getCert(), ]; } /** * @return string */ protected function nonce_str() { static $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < 32; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } return $randomString; } /** * @param $key * @return bool|resource */ protected function getPrivateKey($key) { return openssl_get_privatekey($key); } /** * @param $key * @return resource */ protected function getPublicKey($key) { return openssl_get_publickey($key); } /** * @param $url * @param $http_method * @param $timestamp * @param $nonce * @param $body * @param $mch_private_key * @param $merchant_id * @param $serial_no * @return string */ protected function sign($url, $http_method, $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no) { $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $message = $http_method . "\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $schema = 'WECHATPAY2-SHA256-RSA2048 '; $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $merchant_id, $nonce, $timestamp, $serial_no, $sign); return $token; } /** * @param $message * @param $signature * @param $merchantPublicKey * @return bool|int */ private function verify($message, $signature, $merchantPublicKey) { if (empty($merchantPublicKey)) { return false; } if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); } $signature = base64_decode($signature); return openssl_verify($message, $signature, $this->getPublicKey($merchantPublicKey), 'sha256WithRSAEncryption'); } /** * @param $url * @param array $data * @param $header * @param string $method * @param int $time_out * @return mixed */ private function curl($url, $data = [], $header, $method = 'POST', $time_out = 3) { $curl = curl_init(); // 设置curl允许执行的最长秒数 curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HTTPHEADER, $header); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_TIMEOUT, $time_out); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); if ($method == 'POST') { curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } // 执行操作 $result = curl_exec($curl); curl_close($curl); return $result; } /** * @param $associatedData * @param $nonceStr * @param $ciphertext * @param $aesKey * @return bool|string */ private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey = '') { if (empty($aesKey)) { $aesKey = $this->mch_key; } $ciphertext = \base64_decode($ciphertext); if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) { return false; } // ext-sodium (default installed on >= PHP 7.2) if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // ext-libsodium (need install libsodium-php 1.x via pecl) if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // openssl (PHP >= 7.1 support AEAD) if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE); $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE); return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData); } throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php'); } /** * @return array */ private function getHeaders() { $headers = array(); foreach ($_SERVER as $key => $value) { if ('HTTP_' == substr($key, 0, 5)) { $headers[str_replace('_', '-', substr($key, 5))] = $value; } if (isset($_SERVER['PHP_AUTH_DIGEST'])) { $header['AUTHORIZATION'] = $_SERVER['PHP_AUTH_DIGEST']; } elseif (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { $header['AUTHORIZATION'] = base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']); } if (isset($_SERVER['CONTENT_LENGTH'])) { $header['CONTENT-LENGTH'] = $_SERVER['CONTENT_LENGTH']; } if (isset($_SERVER['CONTENT_TYPE'])) { $header['CONTENT-TYPE'] = $_SERVER['CONTENT_TYPE']; } } return $headers; } 8、注意事项 支付通知需要根据通知header里面的证书序号获取证书 合单支付同一个子单不允许在不同的父单支付 9、最近写了个支付的package,仅供参考:https://github.com/ttglad/payment 参考文档: 合单支付文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/combine.shtml接口规则:https://wechatpay-api.gitbook.io/wechatpay-api-v3/
2021-04-16 - 微信小程序客服消息回复开发
概述 微信小程序为了提高小程序的服务质量,提供了客服消息能力,目的是为了让用户快捷地与小程序服务提供方进行沟通。小程序的客服消息回复有两种方式:一种是接入用户消息到微信公众平台网页版客服工具和客服小助手小程序进行客服消息回复,接入后客服可以看到用户留言,根据用户问题进行专门解答。一种是开启消息推送,当客服无法及时回复的时候能够指导用户联系客服人员或者解决问题。 如果需要接入微信公众平台网页版客服工具和客服小助手,只需要在小程序后台->客服里头添加客服人员就可以,客服人员就可以实时接收到用户消息并且与用户沟通。 如果需要开启消息推送可以参考下文的接入过程。 消息推送开发准备条件 在小程序中设置button组件并且把open-type属性设置为contact 前往小程序后台开发->开发设置->消息推送配置相关信息,可在此指定消息加密方式和数据格式。注意当开启了消息推送,普通微信用户向小程序客服发消息时,微信服务器会先将消息 POST 到开发者填写的 URL 上。在此处的填写我选择的是明文模式和JSON数据格式。 开发过程 处理初次验证 填写完消息推送的配置并且提交后,微信服务器将发送GET请求到填写的URL地址进行校验。因此首先要进行的就是针对微信服务器的初次校验做处理。微信官方在消息推送章节已经提出了校验代码此处便不再重复。 [代码]//微信服务器验证处理 if (isset($_GET['echostr'])) { //调用微信提供的校验代码 if ($this->checkSignature() == false) { exit(); } $echoStr = $_GET['echostr']; echo $echoStr; exit; } [代码] 处理消息 校验成功后,微信服务器会将用户在客服会话中的消息转发到开发者的服务器上,针对微信服务器传入的消息的类型,开发者们可以编写不同的业务逻辑处理。以用户在客服会话中写入文本为例:根据选择的数据格式JSON或者XML,微信服务器会传入相应格式的数据包。根据"MsgType"可以分辨微信服务器转发的是何种类型消息,并编写不同的业务逻辑。 [代码]//1接受微信推送消息 $message = $GLOBALS["HTTP_RAW_POST_DATA"]; $message = json_decode($message, true); //2判定用户发送消息的类型 if (!empty($message['MsgType']) && $message['MsgType'] == 'text') { //do something } [代码] 在处理完微信转发的消息之后,开发者可以根需要调用服务端的客服消息发送接口发送消息给用户。 [代码]$fromUsername = $message['FromUserName']; //发送者openid $url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=$access_token";//根据需要获取access_token $data = array( "touser" => $fromUsername, "msgtype" => "text", "text" => array( "content" => "客服消息推送测试" ) ); $data = json_encode($data, JSON_UNESCAPED_UNICODE); $result = httpRequest($url, "post", $data); $result = json_decode($result, true); if ($result['errcode'] == 0) { //当处理成功之后返回空字符串或者success都可以防止微信服务器重新发起请求 echo ""; exit; } [代码] 转发客服消息 小程序设置了推送消息之后,还可以接入到网页版客服工具中,只需要设置返回数据的MsgType为transfer_customer_service返回给微信服务器。 [代码]//设置转发数据 $transferData = array( "ToUserName" => $message['FromUserName'],//用户的OpenID "FromUserName" => $message['ToUserName'],//小程序原始id "CreateTime" => $message['CreateTime'],//创建时间 "MsgType" => "transfer_customer_service",//指定为transfer_customer_service 消息将会转发到客服工具中 ); $transferData = json_encode($transferData, JSON_UNESCAPED_UNICODE); [代码] 完整代码演示 [代码]//接受微信服务器转发的请求。 public function getMessage() { // 判断是否为微信验证消息 if (isset($_GET['echostr'])) { if ($this->checkSignature() == false) { exit(); } $echoStr = $_GET['echostr']; echo $echoStr; exit; } //接受微信推送消息 $message = $GLOBALS["HTTP_RAW_POST_DATA"]; if (!empty($message)) { $access_token = $this->getAccess();//根据需要获取小程序对应的 access_token //设置转发客服消息 $fromUsername = $message['FromUserName'];//消息发起用户的open_id $transferData = array( "ToUserName" => $fromUsername,//接收方帐号(用户的OpenID) "FromUserName" => $message['ToUserName'],//小程序原始id "CreateTime" => $message['CreateTime'],//创建时间 "MsgType" => "transfer_customer_service",//指定为transfer_customer_service 消息将会转发到客服工具中 ); $transferData = json_encode($transferData, JSON_UNESCAPED_UNICODE); $message = json_decode($message, true); //判定消息类型并处理 if (!empty($message['MsgType']) && $message['MsgType'] == 'text') { //调用send接口发送相对应的消息 $url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=$access_token"; $data = array( "touser" => $fromUsername, "msgtype" => "text", "text" => array( "content" => "客服消息推送测试" ) ); $data = json_encode($data, JSON_UNESCAPED_UNICODE); $result = httpRequest($url, "post", $data); $result = json_decode($result, true); //回复消息之后 不转发消息到客服系统 返回success 或者空字符串 避免微信提示严重错误 if ($result['errcode'] == 0) { echo ""; exit; } //回复消息之后 转发客服消息到客服系统 将$transferData['MsgType']设置为transfer_customer_service //if ($result['errcode'] == 0) { //echo $transferData; //exit; //} } } } /** * 处理微信验证函数 */ public function checkSignature() { $signature = $_GET['signature']; $timestamp = $_GET['timestamp']; $nonce = $_GET['nonce']; $token = "customer12"; //填写在后台配置的Token(令牌) $tmpArr = array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr = implode($tmpArr); $tmpStr = sha1($tmpStr); //加密 if ($tmpStr == $signature) { return true; } else { return false; } } /** * CURL请求 * @param $url 请求url地址 * @param $method 请求方法 get post * @param null $postfields post数据数组 * @param array $headers 请求header信息 * @param bool|false $debug 调试开启 默认false * @return mixed */ function httpRequest($url, $method, $postfields = null, $headers = array(), $debug = false) { $method = strtoupper($method); $ci = curl_init(); curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0"); curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */ curl_setopt($ci, CURLOPT_RETURNTRANSFER, true); switch ($method) { case "POST": curl_setopt($ci, CURLOPT_POST, true); if (!empty($postfields)) { $tmpdatastr = is_array($postfields) ? http_build_query($postfields) : $postfields; curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr); } break; default: curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */ break; } $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE; curl_setopt($ci, CURLOPT_URL, $url); if ($ssl) { curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在 } curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/ curl_setopt($ci, CURLOPT_HTTPHEADER, $headers); curl_setopt($ci, CURLINFO_HEADER_OUT, true); $response = curl_exec($ci); $requestinfo = curl_getinfo($ci); $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); if ($debug) { echo "=====post data======\r\n"; var_dump($postfields); echo "=====info===== \r\n"; print_r($requestinfo); echo "=====response=====\r\n"; print_r($response); } curl_close($ci); return $response; } [代码]
2019-05-29 - 搞懂微信支付 v3 接口规则-【附Java源码】
简介 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 其实还要一个主要因素是「为了符合监管的要求」。 主要是为了符合监管的要求,保证更高的安全级别。《中华人民共和国电子签名法》、《金融电子认证规范》及《非银行支付机构网络支付业务管理办法》中规定 “电子签名需要第三方认证的,由依法设立的电子认证服务提供者提供认证服务。”,所以需使用第三方 CA 来确保数字证书的唯一性、完整性及交易的不可抵赖性。 支付宝支付也是如此,从之前的「普通公钥方式」新增了 「公钥证书方式」。今天的主角是微信支付 Api v3 这里就不展开讲支付宝支付了。 微信支付 Api v3 接口规则 官方文档 v2 与 v3 的区别 V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付 Api-v2 版本详细介绍请参数之前博客 微信支付,你想知道的一切都在这里 干货多,屁话少,下面直接进入主题,读完全文你将 Get 到以下知识点 如何获取证书序列号 非对称密钥 SHA256-RSA 加密与验证签名 AES-256-GCM 如何解密 API 密钥设置 请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。 具体操作步骤请参见:什么是APIv3密钥?如何设置? 获取 API 证书 请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。 具体操作步骤请参见:什么是API证书?如何获取API证书? 按照以上步骤操作后你将获取如下内容: apiKey API 密钥 apiKey3 APIv3 密钥 mchId 商户号 apiclient_key.pem X.509 标准证书的密钥 apiclient_cert.p12 X.509 标准的证书+密钥 apiclient_cert.pem X.509 标准的证书 请求签名 如何生成签名参数?官方文档 描述得非常清楚这里就不啰嗦了。 示例代码 构造签名串 [代码] /** * 构造签名串 * * @param method {@link RequestMethod} GET,POST,PUT等 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 * @return 待签名字符串 */ public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { return new StringBuilder() .append(method.toString()) .append("\n") .append(url) .append("\n") .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 构造 HTTP 头中的 Authorization [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType) throws Exception { // 构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); // 获取商户私钥 String key = PayKit.getPrivateKey(keyPath); // 生成签名 String signature = RsaKit.encryptByPrivateKey(buildSignMessage, key); // 根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 * @return 请求头 Authorization */ public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { Map<String, String> params = new HashMap<>(5); params.put("mchid", mchId); params.put("serial_no", serialNo); params.put("nonce_str", nonceStr); params.put("timestamp", timestamp); params.put("signature", signature); return authType.concat(" ").concat(createLinkString(params, ",", false, true)); } [代码] 拼接参数 [代码] public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); StringBuilder content = new StringBuilder(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); // 拼接时,不包括最后一个&字符 if (i == keys.size() - 1) { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value); } } else { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); } } } return content.toString(); } [代码] 从上面示例来看我们还差两个参数 serial_no 证书序列号 signature 使用商户私钥对待签名串进行 SHA256 with RSA 签名 如何获取呢?不要着急,容我喝杯 「89年的咖啡」提提神。 获取证书序列号 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 通过代码获取 [代码]// 获取证书序列号 X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("apiclient_cert.pem 证书路径")); System.out.println("输出证书信息:\n" + certificate.toString()); System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16)); System.out.println("版本号:" + certificate.getVersion()); System.out.println("签发者:" + certificate.getIssuerDN()); System.out.println("有效起始日期:" + certificate.getNotBefore()); System.out.println("有效终止日期:" + certificate.getNotAfter()); System.out.println("主体名:" + certificate.getSubjectDN()); System.out.println("签名算法:" + certificate.getSigAlgName()); System.out.println("签名:" + certificate.getSignature().toString()); /** * 获取证书 * * @param inputStream 证书文件 * @return {@link X509Certificate} 获取证书 */ public static X509Certificate getCertificate(InputStream inputStream) { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); cert.checkValidity(); return cert; } catch (CertificateExpiredException e) { throw new RuntimeException("证书已过期", e); } catch (CertificateNotYetValidException e) { throw new RuntimeException("证书尚未生效", e); } catch (CertificateException e) { throw new RuntimeException("无效的证书", e); } } [代码] SHA256 with RSA 签名 获取商户私钥 [代码] /** * 获取商户私钥 * * @param keyPath 商户私钥证书路径 * @return 商户私钥 * @throws Exception 解析 key 异常 */ public static String getPrivateKey(String keyPath) throws Exception { String originalKey = FileUtil.readUtf8String(keyPath); String privateKey = originalKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); return RsaKit.getPrivateKeyStr(RsaKit.loadPrivateKey(privateKey)); } public static String getPrivateKeyStr(PrivateKey privateKey) { return Base64.encode(privateKey.getEncoded()); } /** * 从字符串中加载私钥 * * @param privateKeyStr 私钥 * @return {@link PrivateKey} * @throws Exception 异常信息 */ public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { try { byte[] buffer = Base64.decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) { throw new Exception("无此算法"); } catch (InvalidKeySpecException e) { throw new Exception("私钥非法"); } catch (NullPointerException e) { throw new Exception("私钥数据为空"); } } [代码] 私钥签名 [代码]/** * 私钥签名 * * @param data 需要加密的数据 * @param privateKey 私钥 * @return 加密后的数据 * @throws Exception 异常信息 */ public static String encryptByPrivateKey(String data, String privateKey) throws Exception { PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); PrivateKey priKey = keyFactory.generatePrivate(priPkcs8); java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return StrUtil.str(Base64.encode(signed)); } [代码] 至此微信支付 Api-v3 接口请求参数已封装完成。 执行请求 [代码]/** * V3 接口统一执行入口 * * @param method {@link RequestMethod} 请求方法 * @param urlPrefix 可通过 {@link WxDomain}来获取 * @param urlSuffix 可通过 {@link WxApiType} 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath apiclient_key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @param file 文件 * @return {@link String} 请求返回的结果 * @throws Exception 接口执行异常 */ public static Map<String, Object> v3Execution(RequestMethod method, String urlPrefix, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType, File file) throws Exception { // 构建 Authorization String authorization = WxPayKit.buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); if (method == RequestMethod.GET) { return doGet(urlPrefix.concat(urlSuffix), authorization, serialNo, null); } else if (method == RequestMethod.POST) { return doPost(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.DELETE) { return doDelete(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.UPLOAD) { return doUpload(urlPrefix.concat(urlSuffix), authorization, serialNo, body, file); } return null; } [代码] 网络请求库默认是使用的 Hutool 封装的一套 Java 工具集合来实现 GET 请求 [代码]/** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doGet(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] POST 请求 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doPost(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] DELETE 请求 [代码]/** * delete 请求 * * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doDelete(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.delete(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] 上传文件 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @param file 上传的文件 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doUpload(String url, String authorization, String serialNumber, String jsonData, File file) { return HttpRequest.post(url) .addHeaders(getUploadHeaders(authorization, serialNumber)) .form("file", file) .form("meta", jsonData) .execute(); } [代码] 构建 Http 请求头 [代码]private Map<String, String> getBaseHeaders(String authorization) { String userAgent = String.format( "WeChatPay-IJPay-HttpClient/%s (%s) Java/%s", getClass().getPackage().getImplementationVersion(), OS, VERSION == null ? "Unknown" : VERSION); Map<String, String> headers = new HashMap<>(3); headers.put("Accept", ContentType.JSON.toString()); headers.put("Authorization", authorization); headers.put("User-Agent", userAgent); return headers; } private Map<String, String> getHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", ContentType.JSON.toString()); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } private Map<String, String> getUploadHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", "multipart/form-data;boundary=\"boundary\""); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } [代码] 构建 Http 请求返回值 从响应的 HttpResponse 中获取微信响应头信息、状态码以及 body [代码]/** * 构建返回参数 * * @param httpResponse {@link HttpResponse} * @return {@link Map} */ private Map<String, Object> buildResMap(HttpResponse httpResponse) { Map<String, Object> map = new HashMap<>(); String timestamp = httpResponse.header("Wechatpay-Timestamp"); String nonceStr = httpResponse.header("Wechatpay-Nonce"); String serialNo = httpResponse.header("Wechatpay-Serial"); String signature = httpResponse.header("Wechatpay-Signature"); String body = httpResponse.body(); int status = httpResponse.getStatus(); map.put("timestamp", timestamp); map.put("nonceStr", nonceStr); map.put("serialNumber", serialNo); map.put("signature", signature); map.put("body", body); map.put("status", status); return map; } [代码] 至此已完成构建请求参数,执行请求。接下来我们就要实现响应数据的解密以及响应结果的验证签名 对应的官方文档 证书和回调报文解密 签名验证 验证签名 构建签名参数 [代码]/** * 构造签名串 * * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 * @return 应答待签名字符串 */ public static String buildSignMessage(String timestamp, String nonceStr, String body) { return new StringBuilder() .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 证书和回调报文解密 官方文档文末有完整的源码这里就不贴了。贴一个示例大家参数一下 [代码]try { String associatedData = "certificate"; String nonce = "80d28946a64a"; String cipherText = "DwAqW4+4TeUaOEylfKEXhw+XqGh/YTRhUmLw/tBfQ5nM9DZ9d+9aGEghycwV1jwo52vXb/t6ueBvBRHRIW5JgDRcXmTHw9IMTrIK6HxTt2qiaGTWJU9whsF+GGeQdA7gBCHZm3AJUwrzerAGW1mclXBTvXqaCl6haE7AOHJ2g4RtQThi3nxOI63/yc3WaiAlSR22GuCpy6wJBfljBq5Bx2xXDZXlF2TNbDIeodiEnJEG2m9eBWKuvKPyUPyClRXG1fdOkKnCZZ6u+ipb4IJx28n3MmhEtuc2heqqlFUbeONaRpXv6KOZmH/IdEL6nqNDP2D7cXutNVCi0TtSfC7ojnO/+PKRu3MGO2Z9q3zyZXmkWHCSms/C3ACatPUKHIK+92MxjSQDc1E/8faghTc9bDgn8cqWpVKcL3GHK+RfuYKiMcdSkUDJyMJOwEXMYNUdseQMJ3gL4pfxuQu6QrVvJ17q3ZjzkexkPNU4PNSlIBJg+KX61cyBTBumaHy/EbHiP9V2GeM729a0h5UYYJVedSo1guIGjMZ4tA3WgwQrlpp3VAMKEBLRJMcnHd4pH5YQ/4hiUlHGEHttWtnxKFwnJ6jHr3OmFLV1FiUUOZEDAqR0U1KhtGjOffnmB9tymWF8FwRNiH2Tee/cCDBaHhNtfPI5129SrlSR7bZc+h7uzz9z+1OOkNrWHzAoWEe3XVGKAywpn5HGbcL+9nsEVZRJLvV7aOxAZBkxhg8H5Fjt1ioTJL+qXgRzse1BX1iiwfCR0fzEWT9ldDTDW0Y1b3tb419MhdmTQB5FsMXYOzqp5h+Tz1FwEGsa6TJsmdjJQSNz+7qPSg5D6C2gc9/6PkysSu/6XfsWXD7cQkuZ+TJ/Xb6Q1Uu7ZB90SauA8uPQUIchW5zQ6UfK5dwMkOuEcE/141/Aw2rlDqjtsE17u1dQ6TCax/ZQTDQ2MDUaBPEaDIMPcgL7fCeijoRgovkBY92m86leZvQ+HVbxlFx5CoPhz4a81kt9XJuEYOztSIKlm7QNfW0BvSUhLmxDNCjcxqwyydtKbLzA+EBb2gG4ORiH8IOTbV0+G4S6BqetU7RrO+/nKt21nXVqXUmdkhkBakLN8FUcHygyWnVxbA7OI2RGnJJUnxqHd3kTbzD5Wxco4JIQsTOV6KtO5c960oVYUARZIP1SdQhqwELm27AktEN7kzg/ew/blnTys/eauGyw78XCROb9F1wbZBToUZ7L+8/m/2tyyyqNid+sC9fYqJoIOGfFOe6COWzTI/XPytCHwgHeUxmgk7NYfU0ukR223RPUOym6kLzSMMBKCivnNg68tbLRJHEOpQTXFBaFFHt2qpceJpJgw5sKFqx3eQnIFuyvA1i8s2zKLhULZio9hpsDJQREOcNeHVjEZazdCGnbe3Vjg7uqOoVHdE/YbNzJNQEsB3/erYJB+eGzyFwFmdAHenG5RE6FhCutjszwRiSvW9F7wvRK36gm7NnVJZkvlbGwh0UHr0pbcrOmxT81xtNSvMzT0VZNLTUX2ur3AGLwi2ej8BIC0H41nw4ToxTnwtFR1Xy55+pUiwpB7JzraA08dCXdFdtZ72Tw/dNBy5h1P7EtQYiKzXp6rndfOEWgNOsan7e1XRpCnX7xoAkdPvy40OuQ5gNbDKry5gVDEZhmEk/WRuGGaX06CG9m7NfErUsnQYrDJVjXWKYuARd9R7W0aa5nUXqz/Pjul/LAatJgWhZgFBGXhNr9iAoade/0FPpBj0QWa8SWqKYKiOqXqhfhppUq35FIa0a1Vvxcn3E38XYpVZVTDEXcEcD0RLCu/ezdOa6vRcB7hjgXFIRZQAka0aXnQxwOZwE2Rt3yWXqc+Q1ah2oOrg8Lg3ETc644X9QP4FxOtDwz/A=="; AesUtil aesUtil = new AesUtil(wxPayV3Bean.getApiKey3().getBytes(StandardCharsets.UTF_8)); // 平台证书密文解密 // encrypt_certificate 中的 associated_data nonce ciphertext String publicKey = aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText ); // 保存证书 FileWriter writer = new FileWriter(wxPayV3Bean.getPlatformCertPath()); writer.write(publicKey); // 获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes())); return certificate.getSerialNumber().toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } [代码] 验证签名 [代码]/** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certInputStream 微信支付平台证书输入流 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); // 获取证书 X509Certificate certificate = PayKit.getCertificate(certInputStream); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 公钥验证签名 * * @param data 需要加密的数据 * @param sign 签名 * @param publicKey 公钥 * @return 验证结果 * @throws Exception 异常信息 */ public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initVerify(publicKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); } [代码] 至此微信支付 Api-v3 接口已介绍完,如有疑问欢迎留言一起探讨。 完整示例 SpringBoot 参考资料 你真的了解 HTTPS 吗? WechatPay-API-v3
2021-03-02 - 一文搞懂微信支付 Api-v3 规则实现(附源码)
v2 与 v3 的区别 先看看 v2 与 v3 的区别,做到心中有数不怯场:) V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付Api-v3 规则 官方文档 ,此规则需要你耐心细品,重复多此细品效果更佳。 以下是我细品后,总结的实现方案,在这里就分享给大家,干货多屁话少直接贴实现。 Talk is cheap. Show me the code. 获取证书序列号 通过代码获取 这里我们使用第三方的库 x509,如你知道其它获取方法欢迎留言 [代码]const cert = x509.parseCert('cert.pem 证书绝对路径') console.log(`证书序列号:${cert.serial}`) [代码] 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 构建请求头 1、构建请求签名参数 [代码] /** * 构建请求签名参数 * @param method {RequestMethod} Http 请求方式 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 */ public static buildReqSignMessage(method: RequestMethod, url: string, timestamp: string, nonceStr: string, body: string): string { return method .concat('\n') .concat(url) .concat('\n') .concat(timestamp) .concat('\n') .concat(nonceStr) .concat('\n') .concat(body) .concat('\n') } [代码] 2、使用 SHA256 with RSA 算法生成签名 [代码] /** * SHA256withRSA * @param data 待加密字符 * @param privatekey 私钥key key.pem fs.readFileSync(keyPath) */ public static sha256WithRsa(data: string, privatekey: Buffer): string { return crypto .createSign('RSA-SHA256') .update(data) .sign(privatekey, 'base64') } [代码] 3、根据平台规则生成请求头 authorization [代码] /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 */ public static getAuthorization(mchId: string, serialNo: string, nonceStr: string, timestamp: string, signature: string, authType: string): string { let map: Map<string, string> = new Map() map.set('mchid', mchId) map.set('serial_no', serialNo) map.set('nonce_str', nonceStr) map.set('timestamp', timestamp) map.set('signature', signature) return authType.concat(' ').concat(this.createLinkString(map, ',', false, true)) } [代码] 4、Show Time [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param key key.pem 证书 * @param body 接口请求参数 */ public static async buildAuthorization(method: RequestMethod, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, body: string): Promise<string> { let timestamp: string = parseInt((Date.now() / 1000).toString()).toString() let authType: string = 'WECHATPAY2-SHA256-RSA2048' let nonceStr: string = Kits.generateStr() // 构建签名参数 let buildSignMessage: string = this.buildReqSignMessage(method, urlSuffix, timestamp, nonceStr, body) // 生成签名 let signature: string = this.sha256WithRsa(key, buildSignMessage) // 根据平台规则生成请求头 authorization return this.getAuthorization(mchId, serialNo, nonceStr, timestamp, signature, authType) } [代码] 封装网络请求 每个人都有个性,可使用的网络库也比较多(Axios、Fetch、Request 等),为了适配能适配这里做一代理封装。具体实现如下,网络请求库默认是使用的 Axios 1、抽离抽象接口 [代码]/** * @author Javen * @copyright javendev@126.com * @description 封装网络请求工具 */ export class HttpKit { private static delegate: HttpDelegate = new AxiosHttpKit() public static get getHttpDelegate(): HttpDelegate { return this.delegate } public static set setHttpDelegate(delegate: HttpDelegate) { this.delegate = delegate } } export interface HttpDelegate { httpGet(url: string, options?: any): Promise<any> httpGetToResponse(url: string, options?: any): Promise<any> httpPost(url: string, data: string, options?: any): Promise<any> httpPostToResponse(url: string, data: string, options?: any): Promise<any> httpDeleteToResponse(url: string, options?: any): Promise<any> httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise<any> upload(url: string, filePath: string, params?: string): Promise<any> } [代码] 2、Axios 具体实现 [代码]/** * @author Javen * @copyright javendev@126.com * @description 使用 Axios 实现网络请求 */ import axios from 'axios' import * as fs from 'fs' import { HttpDelegate } from './HttpKit' import * as FormData from 'form-data' import * as https from 'https' import concat = require('concat-stream') export class AxiosHttpKit implements HttpDelegate { httpGet(url: string, options?: any): Promise<any> { return new Promise((resolve, reject) => { axios .get(url, options) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } httpGetToResponse(url: string, options?: any): Promise<any> { return new Promise(resolve => { axios .get(url, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpPost(url: string, data: string, options?: any): Promise<any> { return new Promise((resolve, reject) => { axios .post(url, data, options) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } httpPostToResponse(url: string, data: string, options?: any): Promise<any> { return new Promise(resolve => { axios .post(url, data, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpDeleteToResponse(url: string, options?: any): Promise<any> { return new Promise(resolve => { axios .delete(url, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise<any> { return new Promise((resolve, reject) => { let httpsAgent = new https.Agent({ pfx: certFileContent, passphrase }) axios .post(url, data, { httpsAgent }) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) } upload(url: string, filePath: string, params?: string): Promise<any> { return new Promise((resolve, reject) => { let formData = new FormData() formData.append('media', fs.createReadStream(filePath)) if (params) { formData.append('description', params) } formData.pipe( concat({ encoding: 'buffer' }, async data => { axios .post(url, data, { headers: { 'Content-Type': 'multipart/form-data' } }) .then(response => { if (response.status === 200) { resolve(response.data) } else { reject(`error code ${response.status}`) } }) .catch(error => { reject(error) }) }) ) }) } } [代码] 3、使其支持 Api-v3 接口规则 [代码]/** * 微信支付 Api-v3 get 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key * @param params */ public static async exeGet(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, params?: Map<string, string>): Promise<any> { if (params && params.size > 0) { urlSuffix = urlSuffix.concat('?').concat(this.createLinkString(params, '&', true, false)) } let authorization = await this.buildAuthorization(RequestMethod.GET, urlSuffix, mchId, serialNo, key, '') return await this.get(urlPrefix.concat(urlSuffix), authorization, serialNo) } /** * 微信支付 Api-v3 post 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key * @param data */ public static async exePost(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, data: string): Promise<any> { let authorization = await this.buildAuthorization(RequestMethod.POST, urlSuffix, mchId, serialNo, key, data) return await this.post(urlPrefix.concat(urlSuffix), data, authorization, serialNo) } /** * 微信支付 Api-v3 delete 请求 * @param urlPrefix * @param urlSuffix * @param mchId * @param serialNo * @param key */ public static async exeDelete(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer): Promise<any> { let authorization = await this.buildAuthorization(RequestMethod.DELETE, urlSuffix, mchId, serialNo, key, '') return await this.delete(urlPrefix.concat(urlSuffix), authorization, serialNo) } /** * get 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async get(url: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpGetToResponse(url, { headers: this.getHeaders(authorization, serialNumber) }) } /** * post 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async post(url: string, data: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpPostToResponse(url, data, { headers: this.getHeaders(authorization, serialNumber) }) } /** * delete 方法 * @param url 请求 url * @param authorization 授权信息 * @param serialNumber 证书序列号 */ public static async delete(url: string, authorization: string, serialNumber?: string) { return await HttpKit.getHttpDelegate.httpDeleteToResponse(url, { headers: this.getHeaders(authorization, serialNumber) }) } /** * 获取请求头 * @param authorization 授权信息 * @param serialNumber 证书序列号 */ private static getHeaders(authorization: string, serialNumber: string): Object { let userAgent: string = 'WeChatPay-TNWX-HttpClient/%s (%s) nodejs/%s' userAgent = util.format( userAgent, '2.4.0', os .platform() .concat('/') .concat(os.release()), process.version ) return { Authorization: authorization, Accept: 'application/json', 'Content-type': 'application/json', 'Wechatpay-Serial': serialNumber, 'User-Agent': userAgent } } [代码] 如何使用? 这里以「获取平台证书」为例,来演示上面封装的系列方法如何使用 [代码]try { let result = await PayKit.exeGet( WX_DOMAIN.CHINA, // WX_API_TYPE.GET_CERTIFICATES, config.mchId, x509.parseCert(config.certPath).serial, fs.readFileSync(config.keyPath) ) console.log(`result.data:${result.data}`) // 应答报文主体 let data = JSON.stringify(result.data) // 应答状态码 console.log(`status:${result.status}`) console.log(`data:${data}`) // http 请求头 let headers = result.headers // 证书序列号 let serial = headers['wechatpay-serial'] // 应答时间戳 let timestamp = headers['wechatpay-timestamp'] // 应答随机串 let nonce = headers['wechatpay-nonce'] // 应答签名 let signature = headers['wechatpay-signature'] console.log(`serial:\n${serial}`) console.log(`timestamp:\n${timestamp}`) console.log(`nonce:\n${nonce}`) console.log(`signature:\n${signature}`) ctx.body = data } catch (error) { console.log(error) } [代码] 至此微信支付 Api-v3 规则的接口已经测试通过。 但还有其他细节如要我们继续完善,比如 验证签名、证书和回调报文解密 证书和回调报文解密 AEAD_AES_256_GCM 解密算法实现 [代码] /** * AEAD_AES_256_GCM 解密 * @param key apiKey3 * @param nonce 加密使用的随机串初始化向量 * @param associatedData 附加数据包 * @param ciphertext 密文 */ public static aes256gcmDecrypt(key: string, nonce: string, associatedData: string, ciphertext: string): string { let ciphertextBuffer = Buffer.from(ciphertext, 'base64') let authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16) let data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16) let decipherIv = crypto.createDecipheriv('aes-256-gcm', key, nonce) decipherIv.setAuthTag(Buffer.from(authTag)) decipherIv.setAAD(Buffer.from(associatedData)) let decryptStr = decipherIv.update(data, null, 'utf8') decipherIv.final() return decryptStr } [代码] 保存微信平台证书示例 [代码] // 证书和回调报文解密 let certPath = '/Users/Javen/cert/platform_cert.pem' try { let decrypt = PayKit.aes256gcmDecrypt( config.apiKey3, ctx.app.config.AEAD_AES_256_GCM.nonce, ctx.app.config.AEAD_AES_256_GCM.associated_data, ctx.app.config.AEAD_AES_256_GCM.ciphertext ) // 保存证书 fs.writeFileSync(certPath, decrypt) ctx.body = decrypt } catch (error) { console.log(error) } [代码] 验证签名 示例 [代码]// 根据序列号查证书 验证签名 let verifySignature: boolean = PayKit.verifySignature(signature, data, nonce, timestamp, fs.readFileSync(ctx.app.config.WxPayConfig.wxCertPath)) console.log(`verifySignature:${verifySignature}`) [代码] 构建应答签名参数 [代码]/** * 构建应答签名参数 * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 */ public static buildRepSignMessage(timestamp: string, nonceStr: string, body: string): string { return timestamp .concat('\n') .concat(nonceStr) .concat('\n') .concat(body) .concat('\n') } [代码] 使用平台证书验证 [代码] /** * 验证签名 * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param publicKey 平台公钥 */ public static verifySignature(signature: string, body: string, nonce: string, timestamp: string, publicKey: Buffer): boolean { // 构建响应体中待签名数据 let buildSignMessage: string = this.buildRepSignMessage(timestamp, nonce, body) return Kits.sha256WithRsaVerify(publicKey, signature, buildSignMessage) } [代码] [代码] /** * SHA256withRSA 验证签名 * @param publicKey 公钥key * @param signature 待验证的签名串 * @param data 需要验证的字符串 */ public static sha256WithRsaVerify(publicKey: Buffer, signature: string, data: string) { return crypto .createVerify('RSA-SHA256') .update(data) .verify(publicKey, signature, 'base64') } [代码] 完整示例代码 Egg-TNWX TNWX: TypeScript + Node.js + WeiXin 微信系开发脚手架,支持微信公众号、微信支付、微信小游戏、微信小程序、企业微信/企业号、企业微信开放平台。最最最重要的是能快速的集成至任何 Node.js 框架(Express、Nest、Egg、Koa 等) 微信支付已支持 Api-v3 以及 Api-v2 版本接口,同时支持多商户多应用,国内与境外的普通商户模式和服务商模式,v2 接口同时支持 MD5 以及 HMAC-SHA256 签名算法。 如有疑问欢迎留言或者站内私信。
2020-04-21 - 配置了支付授权路径在支付时还是提示当前页面的URL未注册
配置了支付授权路径在支付时还是提示当前页面的URL未注册是什么情况呢? 授权目录已经在商户里面配置过了, [图片] 我的支付请求操作页面都是在这个目录下面. 最终支付请求页地址如下: http://weixin.qq.com/weixinzhifu/merchantpay/fukuan.html?orderNo=Y20200604142918949 然后我在商品后台配置的授权目录为: http://weixin.qq.com/weixinzhifu/merchantpay/ 那么在微信里面,进行付款的时候,就提示: [图片] 为什么还会这样呢?是我哪里做错了吗?什么公众号的安全域名类的,这些基础的都配置过了.
2020-06-04 - 用户在小程序“客服会话按钮”进入客服会话时,收不到事件?
[图片] 请问这个事件是怎么触发的呢? [图片] 没办法在用户一点击客服消息的时候,就接受到这个事件么?还是说接收到了,但是无法在刚进客服消息的时候,给用户发送消息?
2020-02-10 - 商户JSAPI支付权限冻结!麻烦处理一下谢谢!
商户号:1501698971,注册以来一直没使用过,现在要开始使用了。 消息中心没有收到相关邮件,手机端登录商户助手也没有“小黄条”显示,麻烦帮解冻 谢谢! [图片]
2021-01-15 - 客服会话如果自动回复一个带标题的的链接!
[图片] 如图所示,大佬们有没有做过的啊~~~~~
2017-12-20 - 小游戏客服功能
在小游戏中调用客服功能, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ var dt={ sessionFrom:"", showMessageCard:false, sendMessageTitle:"11", sendMessagePath:"", sendMessageImg:"", success:(res)=>{ console.log(res); }, fail: (res) => { console.log(res); }, complete: (res) => { console.log(res); }, } wx.openCustomerServiceConversation(dt); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~` 报错: Cannot read property 'nickname' of undefined;wx.openCustomerServiceConversation TypeError: Cannot read property 'nickname' of undefined 请问nickname在哪啊,是个什么鬼?
2018-05-22 - 有玩家最近申请小游戏退款,但我们小游戏后台上虚拟支付累计金额 0,不能在直接后台退款给玩家,如何解决
这边一个问题,有玩家最近申请退款,但是我们游戏上个月已经暂停服务了,现在后台上虚拟支付累计金额 0,拿不出对应的退款金额退给玩家。 请问下上诉情况怎么解决,要怎么才能进行退款给玩家? 因为现在玩家天天打电话给我们客服说要求退款,不退就要12315投诉我们。 所以这边想知道有没有什么办法能够通过微信渠道退给玩家。 跪求告知了。
2020-05-15 - 沙箱模式充值的订单是会自动结算的吗
不小心在沙箱模式下充值了不少钱,不知道这个钱是不是也会结算单的,还是要自己去申请
2020-12-30 - wx.requestMidasPayment调用失败 返回错误码8?
参数如下,麻烦帮忙看下,紧急!!!谢谢!! [图片] var obj = {} obj.mode = "game" obj.env = 1; obj.offerId = "1450025569" obj.currencyType ="CNY" obj.platform = "android" obj.buyQuantity =30 obj.zoneId ="1"
2020-07-13 - error occurs:ENOENT: no such file or directory?
error occurs:ENOENT: no such file or directory, access '/storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/wxanewfiles/30f161c39a57feda338c6bb898d74792/miniprogramLog/log1 [图片] 我新建了一个飞机小游戏项目,真机调试的时候出现了这个错误。同时,我自己写的项目也出现这个错误,但是在微信开发工具中可以正常运行。请问这是怎么回事啊?
2020-12-21