个人案例
- 小云名片
超简单的多重身份名片生成工具
小云名片扫码体验
- Teem
Teem,真的可以见到陌生人
社交破冰扫码体验
- 虚拟业务指南请收好。
在小程序生态中,基于苹果运营规范,小程序内暂不支持iOS端虚拟支付业务。为此小编为大家整理了一份虚拟支付业务指南,希望大家在做虚拟业务时有所帮助: [视频] 那么,到底什么是虚拟支付业务呢? 虚拟支付业务是指购买非实物商品。比如:VIP会员、充值、录制课程、录制音频视频等虚拟产品。目前iOS端暂不支持虚拟支付业务。 我们常见iOS虚拟支付的不合规示例有哪些呢? 示例一 :小程序内存在付费购买虚拟内容或道具。商品多体现为提前编辑好的、录制好的虚拟商品。如录制视频课程、游戏道具。 整改建议 :建议去除小程序内所有付费购买虚拟服务,并根据提示修改相关内容及文案,文案可参照“由于相关规范,iOS功能暂不可用”。 [图片] 示例二 :付费解锁优质服务。多体现为提供虚拟商品的小程序可通过支付购买、开通虚拟会员等形式,体验小程序付费服务。比如:支付阅读章节小说、同城生活服务平台付费发帖/付费置顶等。 整改建议 :建议可以关闭iOS端虚拟支付通道,并将【马上充值】更改为【由于相关规范,iOS功能暂不可用】,并不再提供iOS端会员服务。 [图片] 示例三 :关闭iOS端虚拟支付功能后,虚拟商品页面仍然保留货架价格标签展示、购买/付费/订阅等功能或按钮。 整改建议 :建议去除小程序中的虚拟商品的价格展示,并更改为【免费】;并将【订阅 ¥128】更改为【由于相关规范,iOS功能暂不可用】,并不再提供iOS端虚拟商品购买服务。 [图片] 示例四 :关闭iOS端虚拟支付功能后,提供引导用户前往其他支付的路径/文案,完成虚拟支付闭环。 整 改建议 :建议去除iOS端小程序内引导用户前往其他支付路径/文案,并不再提供iOS端虚拟商品购买服务。 [图片] 示例五 :小程序含需要付费的虚拟商品,并设置限时免费的服务,限时免费结束后需付费才能继续提供服务。 整改建议 :建议将iOS端小程序中所有虚拟付费内容更改为免费,并不再提供iOS端虚拟商品购买服务。 [图片] 示例六 :关闭iOS端虚拟支付功能后,小程序中虚拟产品页面不可以含有付费性质的关键字(如:购买、已购、付费、支付等),包括但不限于功能按钮、功能页面、支付提示及任何商品介绍等。 整改建议 :建议将小程序iOS端虚拟产品页面中的文案/按钮/功能tab含有限制的关键字更改为【免费】或删除。并不再提供iOS端虚拟商品购买服务。 [图片] 如小程序内存在以上不合规的虚拟支付内容,请开发者重视并及时整改。对于首次违规的小程序,平台将下发站内信整改通知,并给予三天整改时间,请开发者按照提示在限期内完成整改。平台将会对到期未完成整改的小程序进行搜索策略调整,并在小程序功能使用上进行一定的限制,直到小程序完成内容整改。
2020-04-23 - 支付后打开半屏小程序能力的相关调整通知
结合开发者的反馈,支付后直接打开半屏小程序能力的回收时间将延长到 2024 年 6 月 25 日,请各位开发者尽快适配,避免影响业务。 各位开发者: 打开半屏小程序 能力是微信团队提供的一项方便用户从小程序便捷打开另一个小程序的轻量化体验能力。为了优化用户体验,避免用户在没有预期的情况下以半屏方式打开另一个小程序,微信团队将回收支付后直接打开半屏小程序的能力。具体说明如下: 自 2024 年 4 月 25 日起(以下简称 “生效期”),当用户微信客户端的基础库为 3.3.5 版本及以上时,开发者将无法在 wx.requestPayment、wx.requestOrderPayment 的接口回调(包括 success 与 fail )中成功调用 wx.openEmbeddedMiniProgram 接口,其他场景不受影响。自生效期起,若调用接口,开发者将会收到 “openEmbeddedMiniProgram forbidden after payment” 的报错;用户的小程序将不会在支付后以半屏形式直接打开另一个小程序,也不会出现相关报错信息。 注意: 1、本次调整后,若开发者需要在其他场景下应用半屏小程序能力,仅支持由以下 3 个事件触发 tap 事件wx.showModal 接口 success 回调wx.showActionSheet 接口 success 回调2、若用户微信客户端的基础库低于 3.3.5 版本,开发者仍然能够在 wx.requestPayment、wx.requestOrderPayment 的接口回调中成功调用 wx.openEmbeddedMiniProgram 接口;用户的小程序仍会在支付后直接打开半屏小程序 [图片] 微信团队 2024年3月25日
04-18 - 【必坑指南】snapshot组件使用问题,超长截图,离屏渲染,出现空白占位?截图不完整?答案在回复里
[图片] 离屏渲染按要求设置后,截图只截了一半,哪位大神遇到过?
2023-09-13 - 番茄鉴书小程序接入 Foolstack 海报图生成服务
我自己做了个用来搜书和拍照生成读书笔记的微信小程序「番茄鉴书」。之前为了生成书和书摘的分享图片,调研了一下。发现小程序端的 Canvas 倒是可以,但是实现起来比较繁琐,而且不够灵活,要只是出于「学习」的目的,倒是可以一试。puppeteer 方案呢,相对就比较简单灵活一些了,写好展示页面,用 puppeteer 操作 headless chrome 访问 url 生成截图就好了。这种方案也比较成熟,实现成本也还好,最大的问题其实是服务器资源。 我自己用的服务器是阿里云 1 核 2 G 的 ECS。部署了 2个 koa api server,用 PM2 做进程守护;一个 Mysql 实例用来做 state/pre 环境。因为 Nodejs 和 Mysql 资源消耗的低消门槛,这些加起来差不多已经占去了很多内存,puppetter 再一跑起来我就经常收到阿里云的监控警报。并发再多几个得话我这些服务估计全得受影响,好在用得人不多。但其实问题就在这儿,为了这么一个使用量不多的功能,需要常驻一个服务,但只要有少量并发,这台机器的配置根本没法支撑。 于是我把这个海报生成的服务单独抽出来,做成了一个生成分享图片的服务 - Foolstack,通过函数计算/独立服务器部署,按调用次数付费。 从我目前这种方案切过去也很方便。实际替换过程也确实很方便,只需要更改一下之前请求我自己那个 puppetter 服务的部分: const { data } = await axios({ url: poster.domain, method: 'POST', data: { url: `${app.domain}/${type}/${id}?uid=${uid}`, option: { width: 500, height: 780, } }, headers: { 'content-type': 'application/json; charset=utf-8', Authorization: `Bearer ${poster.token}`, }, responseType: 'arraybuffer', }) 然后把生成的图片存储一份,之后请求的时候就不用调用这个服务进行生成了,即加快了响应速度,也降低了生成成本。 生成效果如下: 书 [图片] 书摘 [图片] 这种提供 url 的方式,整个图片的生成过程会稍慢一点儿,毕竟会有额外的网络请求时间。如果使用另外一种方式,直接把页面 html 代码传过去,生成速度会更快一些。 后面我会增加一些内置的海报图模板,不用自己去写截图用的展示页面,只需要把需要的数据传过去就可以。不仅海报图的生成速度快了,整个的接入过程也会更快。
2023-05-31 - 求问,怎么才能先执行app.js,执行完了,才执行page里的js呢
如题,我想在app.js里用接口获取一个参数,用到page里面,显示在页面上,但是每次页面加载完了,app.js才获取到参数,这怎么办呢
2018-01-11 - 微信小程序通过npm 安装 Vant Weapp 安装成功后,找不到组件?
按照官方的文档npm后,再引用组件时,始终找不到路径。 [图片] 下面是再app.json中全局引用,或者index.json引用,都试过了,不管写全路径,还是写@vant/weapp/button/index 都不行。 [图片]
2022-04-05 - 小云名片
超简单的多重身份名片生成工具
2022-05-14 - 复制任意微信小程序页面路径
以下以微信小程序“虎牙直播”为例,演示如何复制微信小程序页面的路径。 1.进入小程序的“关于虎牙直播”页面 [图片] 2.点击右上角的“…”进入“更多资料”页面 [图片] [图片] [图片] 3.复制AppID:wx74767bf0b684f7d3 4.进入小程序后台输入appid并搜索,然后点下一步 [图片] 5.鼠标移动到“获取更多页面路径”,在弹出窗口输入当前登陆的小程序的任意开发者微信号,然后点击开启,出现顶部的“开启入口成功”就可以使用手机访问“虎牙直播”任意页面进行复制了 [图片] 6.某个直播间的页面路径:pages/main/liveRoom/index.html?anchorUid=1678113423&source=search[图片] PS:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问。
2020-01-16 - 如何阻止open-type="share" 按钮的默认行为
因为分享地址需要带某些参数,但是参数必须登录过后才能获得, 点击button直接呼起分享面板,我无法先进行判断是否登录, 有没有内置事件,可以进行判断后再选择是否呼起分享面板
2018-04-19 - 社交破冰
社交产品有那么多了,为什么还要多这一个?在开发这个社交小程序之前我思考了很久。 先说说初衷。对于我来讲,有一个可能相对小众的需求:和不同的陌生人面对面交流。如今社会给予我的压力太大,卷在职场,回家躺平,几乎没有其他办法能让我从这种状态下解脱。这种跨行业或者不同人生阅历的面对面交流会让我很放松,什么都可以说,情感愉悦度高。曾经每次开车跨城市出差,我都会发个顺风车拉个人同行,不是为了赚点钱,而是享受这不算长也不算短的陌生人交流时光。 出于个人需求,我下载了很多社交软件,慢慢的去使用去体验,发现约见面社交太难了。归纳了一下,大概原因有: 托太多,约见面就要约你去酒吧消费。出于安全原因,女性不会轻易和陌生人见面。这么多年的发展,用户已经不信任社交软件,优质用户流失。迫于现实生活压力,在社交软件体验现实生活没有的存在感。在寻找了一圈没有合适的产品,我计划自己开发。如果有合适的产品能满足我的需求,可以告诉我。 这个产品一定不会是第二个微信、第二个陌陌、第二个探探、以及第二个 Soul。我理想中的产品是一个工具类型的产品,没有 UGC,没有聊天框,目标简单:通过工具让用户真实、安全、便捷的见面社交。 可能有人会问,没有聊天框,不让聊怎么知道合不合适。我认为陌生人在社交软件里聊是不会聊出什么花样的,80% 的人会把天聊的很尴尬,最后不了了之。相反不让聊天,只根据产品给出的选项做选择就会很简单,比如:对方照片你是否满意,选择 YES 或者 NO;比如工具推送的地点远近你是否可以接收,选择 YES 或 NO,当双方所有都选择 YES 后,那么见面社交意向就很强烈。 “真实”和“安全”是基本盘。“真实”做到完美很难,有网络就有虚假,靠用户自觉做到处处真实是不切实际的,所以我认为提高作假的门槛即可,比如虚假资料受到举报封号后,不能再注册账号,或者再注册账号成本很高(同一微信号及关联的手机号不能再次登录)。其次“安全”由公立场合来保证,即双方都不能指定地点,由工具指定。最后“便捷”也很重要,因为孤独感亦或是失落感来之某一时刻的情景触动,若此时能见个和自己毫不相干的陌生人互相倾述和排解是非常好的,过了这个时间点我可能就不需要排解了。 那如何让两个陌生人见面呢?两个陌生人为什么要见面?我认为是处于相同时间点相同状态下的两个人愿意见面的概率是相对高的。比如:今天下班后想看场日本恐怖电影;午后约个下午茶聊聊 2022 国足世界杯预选赛之旅。这些是某个时刻某个状态下所产生的社交诉求,还有一些是长久的社交诉求,比如:完成 2022 的 Flag,春节带个对象回家过年。 产品体验很重要,这款工具产品无需付费开会员,也无需 VIP 解锁高级功能,所有功能对使用者都是一样的。因为是工具产品,能简单解决问题就简单,所以也不用下载 App,微信小程序即可使用。也不用填写问卷、不用性格测试、不用填写一堆 Tag 生成 Profile,只在必要的环节添加必要的信息即可。 有兴趣的朋友可以微信搜一搜:Teem。 Teem 是 Meet 单词倒序写法,表达了真实见面社交的观点。
2022-04-16 - 如何分账给服务商?
需要使用服务商分账接口实现服务商收手续费费功能,分账的时候分账方可以传服务商自己的商户号吗?如果不行的话是要服务商在申请一个普通商户才行吗?这个普通商户可以是和服务商同主体吗?
2020-09-01 - 「笔记」订阅消息-订阅次数维护
前言 距离1月10日模板消息下架只有2天了,在社区里经常能看到有帖子在问关于怎么记录订阅次数的问题,这里在这里介绍一下自己用的简单方案,仅供参考。 误区一 [图片] 上面这个图大家应该都比较熟悉了,很多人总是误以为勾选“总是保持以上选择,不再询问”,就可以无限发送订阅消息,这个是错误的想法,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口而已。 误区二 订阅消息不能通过bindsubmit的方式触发,必须通过bindtap的方式触发。 误区三 触发订阅窗口后,不管用户点击了允许还是取消,都会进入订阅消息的success回调中,所以通过这个来判断用户是否订阅是错误的。 订阅次数的维护 先看下官方的文档: [图片] 那么我们该如何使用呢? 我们通过 wx.requestSubscribeMessage 接口发送的时候是知道需要让用户订阅哪几个模板的,就是 tmplIds 这个参数填的数组。那么根据官方文档的回调内容,我们就可以直接在success内去获取对应的key所返回的状态。把获取到的状态分别存入自己的数据库里。发送的时候去数据库里查询需要发送的模板并且状态为accept的去发送,如果发送成功则删除一条记录(因为没有过期一说,所以随便删除哪一条记录都不影响)。 参考代码 [图片] 查询模板订阅状态 需要基础库大等于2.10.0才支持。 wx.getSetting({ withSubscriptions: true, success (res) { console.log(res) } }) 官方文档 补充 如果用户选择了不再接收消息会清空之前的订阅次数,但是这个不会主动告诉开发者,所以发送订阅消息失败后,需要根据返回内容自行清空记录,重新计算。 相关文章 「笔记」订阅消息-订阅次数维护(2020年3月更新改动) 「笔记」订阅消息体验踩坑
2020-03-06 - 订阅消息群发?
云函数for循环列表中的openid然后传入订阅消息,不起作用 [图片]
2021-02-25 - 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题
目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗
2019-10-18 - 纪录一个增加订阅次数的交互方案
普通小程序没有长期订阅消息,只能按照用户订阅次数发送订阅消息。如果订阅次数用完了怎么办呢? 可以让用户多点几次订阅,就可以做到了,但是这个操作对用户来说还是不太方便。怎样设计交互,让用户做起来不那么麻烦,是需要好好考虑的事情。 最近正好看到ReadHub的订阅方式,截图纪录一下。 [图片][图片][图片] 首先提醒用户,通知次数用完了,需要手动点击订阅,增加订阅次数。第一次会申请两个订阅,“每日早报提醒”和“订阅次数耗尽提醒”。用户点击允许以后,会发现可以接收的次数变成1了。 [图片][图片] 再次点击增加通知次数,从第二次开始,只申请一个“每日早报提醒”,每操作一次,订阅次数+1。点多了以后,嫌麻烦索性选中“总是保持以上选择,不再询问”,然后狂按增加订阅次数,会发现增加的次数并没有那么快,应该是每次订阅服务器端都要纪录一下才能展示出来,也就是说每次点击都落到数据库了。这样大概每秒1次,我点这21下也花了接近一分钟时间,不是一个可以忽略的操作。 更多参考: 云开发·多次订阅一次性订阅消息后定时发送
2021-03-08 - 云开发云函数定时触发器讲解
任何可以产生事件,触发云函数执行的均可以被称为触发器,而定时触发器则是可以处理周期性的事情,比如时报、日报、周报等通知提醒,也可以处理倒计时任务,比如节假日、纪念日以及你可以指定一个具体时间的倒计时任务,除此之外,定时触发器还可以用来周期性处理一些定时任务。比如定期清理一些不必要的数据,定期更新集合内的数据。 13.5.1 定时触发器使用说明1、定时触发器的配置与部署配置了定时触发器的云函数,会在相应时间点被自动触发,云函数的返回结果不会返回给调用方。在对某个云函数使用定时触发器前,首先要保证该云函数在小程序端可以调用成功,更准确的说是能够在不传入参数的情况下在云开发控制台的云端测试能调试成功(小程序端调用有登录态)。 云函数目录里的 config.json 文件可以用来配置权限和定时触发器,如果你的云函数目录下面没有这个配置文件,可以自己创建一个,创建的结构目录如下: test //云函数目录 ├── config.json //权限和定时触发器等的配置文件 ├── index.js //云函数 ├── package.json //云函数的依赖管理 然后再来在配置文件 config.json 里进行类似如何格式的配置,config.json 严格遵循配置文件所要求的格式,比如数组最后一项不能有逗号[代码],[代码];配置文件里不能有注释等 triggers 字段是触发器数组,但是目前云函数只支持一个触发器,即数组只能填写一个,不可添加多个;name 是触发器的名字,最大支持 60 个字符,支持 a-z, A-Z, 0-9, - 和 _,必须以字母开头;type 为触发器类型,timer 是定时触发器config 是触发器的定时配置,里面为 cron 表达式(后面有介绍),cron 有七个必需字段,不能多也不能少(以下为每天早上 9 点到 12 点每隔 5 秒触发一次);{ "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * 9-12 * * * *" } ] } 当我们在修改触发器配置文件 config.json 后,首先鼠标右键 config.json 选择“云函数增量上传:更新文件”,然后再右键 config.json 选择“上传触发器”。这里的“云函数增量上传:更新文件”是让云函数端的触发器文件更新;而“上传触发器”则是让触发器开始生效执行。如果在云函数端的触发器没有更新的情况下就“上传触发器”来执行定时触发,文件可能没有更新,执行的还是旧的触发器内容。当我们想暂停或删除触发器时,可以右键选择“删除触发器”。 2、Cron 表达式语法Cron 表达式有七个必填字段,按空格分隔,既不能多写也不能少写,每一个字段都有它的含义对应着不同的时间点,表达式的取值都为整数且为时间制的范围(注意月在星期的前面): 第一位第二位第三位第四位第五位第六位第七位秒(0-59 )分钟(0-59)小时(0-23)日(1-31)月(1-12或三个字母的英文缩写)星期(0-6或三个字母的英文缩写)年(1970~2099 ) 下面是 cron 表达式的案例,以及我们需要了解一下 cron 表达式里的通配符以及直接写数字的含义: [代码],[代码],表示并集,在时间的表述里是“和”的意思,比如在“小时”字段中, [代码]1,2,3[代码]表示 1 点、2 点和 3 点;[代码]-[代码],指定范围的所有值,在时间的表述里是“到”的意思,比如在“日”字段中,[代码]1-15[代码]包含指定月份的 1 号到 15 号;[代码]*[代码],表示所有值,在时间的表述里是“每”的意思,比如在“小时”字段中,[代码]*[代码]表示每小时;[代码]/[代码],指定步长,在时间的表述里是“隔”的意思,比如在“秒”字段中,[代码]*/5[代码]表示每隔 5 秒;直接写数字,在时间的表述里是“第”(时间点)的意思,比如在“月”字段中,[代码]5[代码]表示每月的第 5 日;//表示每隔5秒触发一次, */5 * * * * * * //表示在每月的1日的凌晨2点触发 0 0 2 1 * * * //表示在周一到周五每天上午10:15触发 0 15 10 * * MON-FRI * //表示在每天上午10点,下午2点,4点触发 0 0 10,14,16 * * * * //表示在每天上午9点到下午5点内每半小时触发 0 */30 9-17 * * * * //表示在每个星期三中午12点触发 0 0 12 * * WED * 定时触发器的 Cron 语法没法实现每隔 90 秒钟或 90 分钟发送一次这样的效果,因为 90 秒超过了秒的时间制上限 60,而 cron 在跨位组合(比如 90 秒需要结合秒和分)上无法覆盖所有的时间;除此之外,云开发的触发器暂时不支持多个定时触发器的叠加;在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”的关系,即两者的条件均生效;值得一提的是,尽管云函数的时区为 UTC+0 时区,但是定时触发器的时间还是北京时间。 13.5.2 用定时触发器调用云函数定时触发器的使用非常简单,使用开发者工具新建一个云函数比如 trigger,然后在 index.js 里输入以下代码: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { console.log(event); return event; }; 再在 trigger 云函数目录下的 config.json(如果没有这个文件,就创建一个),然后输入以下触发器,为了调试方便,我们可以每隔 5 秒触发一次: { "permissions": { "openapi": [ ] }, "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * * * * * *" } ] } 然后分别右键 index.js 和 config.json,选择“云函数增量上传:更新文件”,然后再来右键 config.json 选择“上传触发器”。云函数就会每隔 5 秒自动触发,相关的日志我们可以在开发者工具的云开发控制台以及腾讯云云开发网页控制台的云函数的日志里查看。 注意小程序端调用 trigger 云函数返回的 event 对象,和使用定时触发器返回的 event 对象的不同,用定时触发器触发云函数是获取不到 openId 的,同时这里有一个 Time 时间是时区为 UTC+0 的时间,比北京时间晚 8 个小时: //在小程序端调用trigger云函数之后返回的event对象 { "userInfo":{ "appId":"wxda99******7046", "openId":"oUL-m5F******buEDsn8" } } //使用定时触发器触发云函数之后返回的event对象 { "Message":"", "Time":"2020-06-11T11:43:35Z", "TriggerName":"tomylove", "Type":"timer", "userInfo":{ "appId":"wxda99********46" } } 13.5.3、定时触发器的应用定时触发器的应用非常广泛,以下仅举一些常用案例,并加以说明: 1、结合消息推送这里的消息推送不仅仅只是指订阅消息,还可以是统一服务消息、公众号的消息(可以用云函数开发微信公众号)、小程序内自己开发的通知(只是用户只有在打开小程序时才能看到)、Email 邮件等等。 比如用户订阅了日报、周报、月报等周期性的通知提醒或者我们需要给用户发送一些汇总信息,就可以固定写一个定时触发器,比如我们需要给指定用户发送工作周报,每周五晚上 17 点 30 分就定时从数据库获取数据发送消息,cron 表达式写法如下: * 30 17 * * FRI * 还可以用来处理一些倒计时(指定时间点)的任务,比如节假日、纪念日以及一些活动时间节点(定时触发器目前只能一个云函数配一个触发器,但是可以提前管理),比如我们希望在六一儿童节的早上 9 点调用云函数给指定用户群体发送消息: 0 0 9 1 6 * * 当然这样的具体时间点显得过于的不灵活,但是如果把时间与云开发数据库结合起来,灵活性就会大很多,比如在运营上每天早上 11 点是你们用户访问最多的时间点,你只需要写一个云函数,把所有的活动都在这个时间点来推送,让定时触发器每天这个时间点都触发,有活动(数据库里有数据)就会发消息,如果没有就不发(云函数调用一次的成本极低)。 如果是实时数据,我们还可以把定时触发器的频率调高,每 5 秒就触发一次,比如我们的数据库只要有最新的数据,就会发消息给指定用户。尽管不是完全的实时,但是 5 秒的频率和实时的差别也就不大了。你也可以根据情况,来调整触发器的频率,毕竟 5 秒和 1 分钟的频率给用户的体验差异并没有太大,但是成本却是 12 倍的关系。 可能你还希望在指定的时间段才触发云函数,比如你只希望在工作日、或者在早上 9 点到晚上 18 点才触发,在指定的时间段才触发既可以让触发更精准不扰民,也可以节约成本,比如下面的触发器就是工作日早上 9 点到 12 点和下午 14 点到 18 点这个时间段,每 5 秒触发一次。 */5 * 9-12,14-18 * MON,TUE,WED,THU,FRI * 从以上案例我们可以了解到,云函数的定时触发可以来自于 cron 表达式的配置,我们可以指定时间点时间段和频率来达到我们想要的效果,同时这个时间“也可以来自于数据库的配置”(伪装),意思是我们可以设置触发器的时间段或频率,如果数据库里有数据就发送,没有数据就不发送,这样就可以达到触发器在时间上的灵活性了。 2、实时获取数据有的时候我们的数据并不是来自于数据库,而是来自于第三方服务,比如前面介绍过的历史上的今天的 API,天气的 API,知乎日报的 API 等等,以及一些 webhook,这些 API 和第三方服务提供的是 json 格式的文件,API 的数据也会随时更新,但是它们更新了却并不会主动通知我们,这时我们可以使用定时触发器向这些 API 发起请求,如果数据出现更新,我们就可以将更新的数据存储到我们的数据库或者进行其他处理,比如企业微信的机器人等机器人通知服务就是如此。 当然定期获取的数据还可以是爬虫,比如我们可以定期抓取指定关键词的新闻或者指定网站的动态,当爬虫获取到了不同的数据的时候,就将最新的动态以机器人消息或者其他方式进行及时的处理。 也就是说,我们无法实时监听到第三方 API 或者网站数据的变动,但是可以用定时触发器来发起请求或者爬虫抓取数据,通过数据的变化来达到“实时”获取数据的目的。 3、自动化处理在数据库的设计里,我们就提到有时候需要对数据库里的数据进行定期的备份与删除等清理维护工作,比如超过一定时间的日志,具有很强时效性的活动数据,以及为了性能考虑而做的虚假删除(数据库性能与优化有介绍)等,毕竟数据库有一定的存储成本而且过多无用数据也会影响数据库的性能,我们可以写一个云函数用定时触发器来执行此类任务。 我们还可以在用户并发比较少的时间段(比如凌晨几点)来处理一些比较耗云函数、数据库性能的任务,比如图片的审核与裁剪、缩略等处理,用户评论是否包含敏感词汇(尽管经过安全处理,但是有时候我们还会设置特别的敏感词),数据的汇总,云存储里废弃文件的删除,用户信息是否完整等等。 也就是说,结合定时触发器,我们可以实现一些任务的自动化处理。 4、密集型任务分流我们知道云函数在处理一些复杂性的任务时是有一些限制的,一是执行时间的限制,建议在设置时执行时间一般不要超过 20s,最长不要超过 60s;二是并发的限制,云函数最大的并发为 1000;三是云函数在查询数据库时一次可以获取最多 1000 条的数据,面对这三个限制,我们应该如何处理密集型的任务呢,比如发送 100 万封邮件,导出几百万条数据到 Excel,发送十万级的订阅消息或消息等等,这个时候就可以使用到定时触发器来处理了。 借助于定时触发器,我们可以将需要耗时较长、对并发要求较高以及数据库请求等的任务进行分批处理,比如我们要给 100 万人发邮件:云函数发起数据库请求,一次只请求 1000 条未发送过邮件的用户(用 where 条件查询某个字段,比如[代码]status:false[代码]),然后将邮件发给 1000 个人(可以参考前面的邮件发送),发完邮件并对这 1000 条数据进行标记(比如使用更新指令将 status 改为 true),这样下次查询未发送过邮件的用户时,就不会重复发送了。通过定时触发器,每 2 秒执行一次发送任务,几十分钟就可以处理完任务。
2021-09-10 - 云开发云存储入门讲解
在云开发能力章节我们了解到小程序端和服务端都可以上传文件到云存储,不过在实际开发中云存储里的文件链接需要被记录在数据库里才方便调用。接下来我们就来介绍云存储文件的增删改查是如何与数据库的增删改查结合在一起的。在云数据库入门章节我们所涉及到的数据库里数据类型还非常简单,在这一章里我们会来介绍如何操作数据库的数组和对象等复杂数据类型的增删改查。 云存储与数据库的关系不经过数据库直接把文件上传到云存储里,这样文件的上传、删除、修改、查询是无法和具体的业务对应的,比如文章商品的配图、表单图片附件的添加与删除,都需要图片等资源能够与文章、商品、表单的 ID 能够一一对应才能进行管理(在数据库里才能对应),而这些文章、商品、表单又可以通过数据库与用户的 ID、其他业务联系起来,可见数据库在云存储的管理上扮演着极其重要的角色。 数据库的设计与结构和 Excel 表、关系型数据库(如 MySQL)以行和列、多表关系来设计表结构不同的是,云开发的数据库是基于文档的。我们可以在一个记录里嵌套多层数组和对象,把每个文档所需要的数据都嵌入到一个文档里,而不是分散到多个不同的集合。 比如我们想做一个网盘小程序,用来记录用户信息,以及创建的相册、文件夹,这里相册和文件夹因为可以创建很多个,所以它是一个数组;而每一个相册对象和文件夹对象里都可以存储一个照片列表和文件列表,我们发现在云开发数据库里一个元素的值是数组,数组里又嵌套对象,对象里又有元素是数组是非常常见的事情。 以下是网盘小程序的数据库设计,包含了一个用户的信息,上传的所有文件和照片等信息: { "_id": "自动生成的ID", "_openid": "用户在当前小程序的openid", "nickName": "用户的昵称", "avatarUrl": "用户的头像链接", "albums": [ { "albumName": "相册名称", "coverURL": "相册封面地址", "photos": [ { "comments": "照片备注", "fileID": "照片的地址" } ] } ], "folders": [ { "folderName": "文件夹名称", "files": [ { "name": "文件名称", "fileID": "文件的地址", "comments": "文件备注" } ] } ] } 如果是用关系型数据库,就会建 user 表来存储用户信息,albums 表存储相册信息,folders 表存储文件夹信息,photos 表存储照片信息,files 表存储文件信息,相信大家可以通过这个案例对云数据库是面向文档的有一个大致的了解。 当然云开发的数据库也是可以把数据分散到不同集合的,需要视不同的情况而定,在后面章节我们会介绍。这种将每个文档所需的数据都嵌入到一个文档内部的做法,我们称之为反范式化(denormalization),将数据分散到多个不同的集合,不同集合之间相互引用称之为范式化(normalization),也就是说反范式化文档里包含子文档,而范式化呢,文档的子文档则是存储在另一个集合之中。 fileID 是存储与数据库的纽带从上面可以看出,云存储与数据库就是通过 fileID 来取得联系的,数据库只记录文件在云存储的 fileID,我们可以访问数据库相应的 fileID 属性进行记录的增删改查操作,与此同时调用云存储的上传文件、下载文件、删除文件等 API,这样云存储就被数据库给管理起来了。 打开云开发技术文档里云存储的所有 API,如上传文件 uploadFile、下载文件 downloadFile、删除文件 deleteFile、用云文件 ID 换取真实链接 getTempFileURL,我们发现这些 API 始终是围绕 fileID 来展开的,要么 fileID 是 success 回调返回的对象,要么 fileID 是 API 必备的属性。 建立用户与数据的关系 openid 与云开发在前面我们已经了解到,用户在小程序里有着独一无二的 openid,用 openid 完全可以区分用户;使用云开发时用户在小程序端上传文件到云存储,这个 openid 会被记录在文件信息里;添加数据到数据库这个 openid 会被保存在_openid 的字段里(也就是说我们除了可以用云函数如前面的 login 来获取用户的 openid,还可以通过数据库的_openid 字段来获取 openid);而且我们在小程序端查询数据时(查询时改、删、更新等的前提),都会默认有一个 where({_openid:当前用户的 openid})的条件,限制了用户 write 写(改、删、更新)的权限。 _id 与云开发当用户在小程序端往数据库用 Collection.add 添加记录 document 时,会自动给该记录生成_id,同时也会创建一个_openid,_id 和_openid 由于都是独一无二的,只要我们获取每个用户创建的记录_id,也就能同时确定这个用户的 openid。 判断用户是否存在并创建记录打开云开发控制台的数据库标签,新建一个 clouddisk 的集合,并修改它的权限为为“所有人可读,仅创建者可读写”(或使用安全规则)。使用开发者工具新建一个 folder 的页面,然后在 folder.js 的页面生命周期函数 onLoad 里输入以下代码: this.checkUser() this 调用自定义函数,开发者可以添加任意的函数或数据到 Object 参数中,在页面的函数中用 this 可以访问 然后再在 Page()对象里输入以下代码,代码的意思是如果 clouddisk 里没有用户创建的数据,那就在 clouddisk 里新增一条记录;如果有数据,就返回数据: async checkUser() { //获取clouddisk是否有当前用户的数据,注意这里默认带了一个where({_openid:"当前用户的openid"})的条件 const userData = await db.collection('clouddisk').get() console.log("当前用户的数据对象",userData) //如果当前用户的数据data数组的长度为0,说明数据库里没有当前用户的数据 if(userData.data.length === 0){ //没有当前用户的数据,那就新建一个数据框架,其中_id和_openid会自动生成 return await db.collection('clouddisk').add({ data:{ //nickName和avatarUrl可以通过getUserInfo来获取,这里不多介绍 "nickName": "", "avatarUrl": "", "albums": [ ], "folders": [ ] } }) }else{ this.setData({ userData }) console.log('用户数据',userData) } }, 一个用户只能创建一条记录,如果是开一个用户可以创建多条记录… 预先搭好文档的数据框架方便我们在后面以 update 的方式来更新数据。 async/await 的使用说明async 是“异步”的简写,async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成,await 只能出现在 async 函数中。await 在 async 函数中才会有效。假设一个业务需要分步完成,每个步骤都是异步的,而且依赖上一步的执行结果,甚至依赖之前每一步的结果,就可以使用 Async Await 来完成 小程序端现在完全支持 async/await 的写法,不过需要在开发者工具-详情-本地设置,勾选增强编译才行,否则会报以下错误。 Uncaught ReferenceError: regeneratorRuntime is not defined async 函数返回值是 Promise 对象, async 函数内部 return 返回的值。会成为 then 方法回调函数的参数。如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。async 函数返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变。也就是说,只有当 async 函数内部的异步操作都执行完,才会执行 then 方法的回调。 在 async 函数中使用 await,那么 await 这里的代码就会变成同步的了,意思就是说只有等 await 后面的 Promise 执行完成得到结果才会继续下去,await 就是等待,这样虽然避免了异步,但是它也会阻塞代码,所以使用的时候要考虑周全。await 会阻塞代码,每个 await 都必须等后面的 fn()执行完成才会执行下一行代码 云存储文件夹管理在小程序端创建一个文件夹,需要考虑三个方面,一是文件夹在云存储里是怎么创建的;二是文件夹在数据库里的表现形式;三是小程序端页面应该怎么交互才算是创建了一个文件夹; 文件夹在云存储里是怎么创建的在云开发能力章节我们了解到,要上传 demo.jpg 到云存储的 cloudbase 文件夹里,只需要指明 cloudPath 云存储的路径为 cloudbase/demo.jpg 即可,这里的 cloudbase 文件夹,在我们上传文件时代码会自动创建,也就是说我们在小程序端创建文件夹不需要对云存储做任何事情,因为在云存储这里,文件夹是只有在文件上传时才会创建。 文件夹在数据库里的表现形式尽管文件夹在小程序端的页面交互看来非常复杂,但是它在数据库的形式看起来却非常简单,我们创建文件夹只是在操作(增删改查)数组和对象而已,以下的 folders 数组是文件夹列表,而一个文件夹只是数组里的一个对象而已。 "folders": [ { "folderName": "文件夹名称", "files": [ ] } ] 文件夹的创建与页面交互通过前面的分析可知,在小程序端创建文件夹,只会操作数据库的数据,而不会操作云存储,我们来看具体的代码实现。使用开发者工具新建一个 folder 的页面,然后在 folder.wxml 里输入以下代码: <form bindsubmit="formSubmit"> <input name="name" placeholder='请输入文件夹名' auto-focus value='{{inputValue}}' bindinput='keyInput'></input> <button type="primary" formType="submit">新建文件夹</button> </form> 方法一:使用 push 和 在 folder.js 里输入以下代码: async createFolder(e) { let foldersName = e.detail.value.foldersName const folders = this.data.userData.data[0].folders folders.push({ foldersName: foldersName, files: [] }) const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.set(folders) } }) }, 技术文档:字段更新操作符 set 方法二: 在 folder.js 里输入以下代码: async createFolder(e) { let foldersName = e.detail.value.foldersName const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.push([{ foldersName: foldersName, files: [] }]) } }) }, 技术文档:数组更新操作符 push 先读后写与先写后读 上传单个文件到文件夹相信大家都应该在其他小程序体验过文件上传的功能,在交互上这个功能虽然看起来简单,但是在代码的逻辑上却包含着四个关键步骤: 首先把文件上传到小程序的临时文件,并获取临时文件地址以及文件的名称;将临时文件上传到云存储指定云文件里,并 q 取到文件的 FileID;将文件在云存储的 FileID 和文件的名称上传到数据库;获取文件夹内所有文件的信息。 上传文件到小程序的临时文件使用开发者工具在 folder.wxml 里输入以下代码: <form bindsubmit="uploadFiles"> <button type="primary" bindtap="chooseMessageFile">选择文件</button> <button type="primary" formType="submit">上传文件</button> </form> 然后在 folder.js 里输入以下代码: chooseMessageFile(){ const files = this.data.files wx.chooseMessageFile({ count: 5, success: res => { console.log('选择文件之后的res',res) let tempFilePaths = res.tempFiles for (const tempFilePath of tempFilePaths) { files.push({ src: tempFilePath.path, name: tempFilePath.name }) } this.setData({ files: files }) console.log('选择文件之后的files', this.data.files) } }) }, 将临时文件上传到云存储技术文档:wx.cloud.uploadFile uploadFiles(e) { const filePath = this.data.files[0].src const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/) wx.cloud.uploadFile({ cloudPath,filePath }).then(res => { this.setData({ fileID:res.fileID }) }).catch(error => { console.log("文件上传失败",error) }) }, 上传成功后会获得文件唯一标识符,即文件 ID,后续操作都基于文件 ID 而不是 URL。 将文件信息存储到数据库 addFiles(fileID) { const name = this.data.files[0].name const _id= this.data.userData.data[0]._id db.collection('clouddisk').doc(_id).update({ data: { 'folders.0.files': _.push({ "name":name, "fileID":fileID }) } }).then(result => { console.log("写入成功", result) wx.navigateBack() } ) } 匹配数组第 n 项元素 如果想找出数组字段中数组的第 n 个元素等于某个值的记录,那在 匹配中可以以 字段.下标 为 key,目标值为 value 来做匹配。如对上面的例子,如果想找出 number 字段第二项的值为 20 的记录,可以如下查询(注意:数组下标从 0 开始) 获取文件夹内文件列表在 onload 生命周期函数里输入 this.getFiles() 然后再在 Page 对象里添加 getFiles()方法,获取该用户的数据 getFiles(){ const _id= this.data.userData.data[0]._id db.collection("clouddisk").doc(_id).get() .then(res => { console.log('用户数据',res.data) }) .catch(err => { console.error(err) }) } 要实际开发一个具体的功能,一定要先思考这个功能的页面交互是怎样的,而页面交互的背后都只不过是简单的数据,但正是这些简单的数据经过页面交互处理之后却“蒙蔽”了用户的双眼,让用户觉得复杂,觉得这个功能真实存在。 嵌套数组和对象的查询我们可以对对象、对象中的元素、数组、数组中的元素进行匹配查询,甚至还可以对数组和对象相互嵌套的字段进行匹配查询/更新 匹配记录中的嵌套字段// 方式一 db.collection('todos').where({ style: { color: 'red' } }).get() // 方式二 db.collection('todos').where({ 'style.color': 'red' }).get() 匹配并更新数组中的元素 上传多个文件到文件夹 查询所有数据const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const MAX_LIMIT = 100 exports.main = async (event, context) => { // 先取出集合记录总数 const countResult = await db.collection('china').count() const total = countResult.total // 计算需分几次取 const batchTimes = Math.ceil(total / 100) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('china').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => { return { data: acc.data.concat(cur.data), errMsg: acc.errMsg, } }) } 小程序端下载并预览文件技术文档:wx.openDocument()、wx.cloud.downloadFile 使用云开发来下载云存储里面的文件,就不会有域名校验备案的问题 previewFile(){ wx.cloud.downloadFile({ fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技术工坊预备手册.pdf' }).then(res => { const filePath = res.tempFilePath wx.openDocument({ filePath: filePath }) }).catch(error => { console.log(error) }) } 删除记录与删除字段技术文档:deleteFile 可以根据文件 ID 下载文件,用户仅可下载其有访问权限的文件: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const fileIDs = ['xxx', 'xxx'] const result = await cloud.deleteFile({ fileList: fileIDs, }) return result.fileList } 嵌套删除字段 return await db.collection("clouddisk").doc("_id").update({ data:{ "folders.0.files.1": _.remove() } }) 获取临时链接并分享文件技术文档:getTempFileURL 将服务端的文件传到小程序端技术文档:downloadFile
2021-09-10 - 页面最后一个元素的margin-bottom 无效
窗体最底部导航 是用fixed 定位的, 而不希望 这个位置遮挡住页面最下面的部分,所有在页面盒子中使用了 margin-bottom ,但是这个属性在 微信小程序不起作用,求解决方案!!!! [图片]
2017-10-10 - 核销事件回调通知API v3 仅有核销通知吗?无过期通知吗
我看通知里的 代金券状态: SENDED USED EXPIRED 3种,但仅仅接收到核销状态的,过期的没有收到
2020-08-14 - 创建代金券的时候,我传了订单优惠标记(goods_tag),但是在查询批次详情的时候,订单标识为空?
创建代金券的时候,我传了订单优惠标记(goods_tag),但是在查询批次详情的时候,订单标识为空?[图片][图片]
2020-11-26 - 微信小程序发券插件至验签流程
经过两天的穷举尝试,终于把微信小程序发券插件验签流程通过了,在此给大家分享一下。 提过一个问题(https://developers.weixin.qq.com/community/pay/doc/000a063bb50380943c9cc755e51c00),大家可以去看一看,再回来看正确的过程。 正确的验签参数,请仔细看红色说明: [图片] 发一个小程序端领券成功的截图(虽然遇到新的问题,这个我还要去研究一下,哈哈): [图片]
2021-08-11 - 领券插件发券签名错误
{errcode: 272758293, graphid: 33580361, hint: "pdbb0i", msg: "插件发券签名错误", time: "2021-07-08 15:58:33"} 插件领券可以领券代金券吗?
2021-07-08 - 代金券如何插入卡包?
我看文档上有些可以插入卡包,比如小程序怎么领取插入卡包、查看卡包中的代金券?我没有找到具体的文档,公众号和小程序提供的文档都是mp平台的那套卡卷的文档。 [图片]
2020-07-08 - 你配置的信息需要开通特殊权限
问题:创建代金券接口报错:“你配置的信息需要开通特殊权限” 解决方案: 首先这里的这个错误信息不是因为权限问题导致 接下来我们确认一下这几个参数: 1.stock_name:最多可填写9个字 2.max_coupons_per_user:单天发放个数上限不能为0 3. coupon_amount:10<=coupon_amount<=100000 4.available_time_after_receive:可用时间:相对时间,按分钟设置,是否1min<=分钟范围<=1440min 5.transaction_minimum校验规则: a、使用门槛-券面额>=0.01(门槛要大于面额) b、0.1元<=门槛<=100000 6.stock_type:目前只支持NORMAL 7.out_request_no:校验规则:不可以重复 8、开始时间结束时间控制在90天内 9、不可使用的时间参数不可以传递 另外参考一下我的创建示列,改为您自己的参数就可以创建: 长示列: { "stock_name": "耗子尾汁", "comment": "零售批次", "belong_merchant": "2480255141", "available_begin_time": "2020-12-1T15:40:35.120+08:00", "available_end_time": "2020-12-30T15:40:36.120+08:00", "stock_use_rule": { "max_coupons": 10, "max_amount": 5000, "max_amount_by_day": 500, "max_coupons_per_user": 1, "natural_person_limit": true, "prevent_api_abuse": true }, "pattern_info": { "description": "微信支付营销代金券", "merchant_logo": "https://wx.gtimg.com/mmpay_pub/KQwOuXiby6RR2Da2oGPMe4ZGjiaC8wdwZUNf9SageS77G8ibicGCdD6S9WIpEBbaibIbq/0", "merchant_name": "耗子尾汁", "background_color": "COLOR060", "coupon_image": "https://wx.gtimg.com/mmpay_pub/KQwOuXiby6RR2Da2oGPMe4ZGjiaC8wdwZUNf9SageS77G8ibicGCdD6S9WIpEBbaibIbq/0" }, "coupon_use_rule": { "fixed_normal_coupon": { "coupon_amount": 500, "transaction_minimum": 1000 }, "goods_tag": [ "my-test" ], "combine_use": true, "available_merchants": [ "2480255141" ], "limit_pay" :["CBHB_DEBIT"] }, "no_cash": false, "stock_type": "NORMAL", "out_request_no": "123333333-111100" } 短示列: { "stock_name": "GD测批次4", "comment": "验证活动", "belong_merchant": "xxxxxx", "available_begin_time": "2020-02-13T18:00:00.120+08:00", "available_end_time": "2020-02-20T23:59:59.120+08:00", "stock_use_rule": { "max_coupons": 10, "max_amount": 100, "max_coupons_per_user": 10, "natural_person_limit": false, "prevent_api_abuse": false }, "coupon_use_rule": { "fixed_normal_coupon": { "coupon_amount": 10, "transaction_minimum": 10 }, "available_merchants": [ "209784532", "221003827" ] }, "no_cash": false, "stock_type": "NORMAL", "out_request_no": "207662xxxxxx" }
2021-03-23 - 服务商如何对接微信代金券制券与发券API,解决“你配置的信息需要开通特殊权限”问题。
第一:调用的接口文档地址 https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/marketing/convention/chapter3_1.shtml 第二:如果是服务商需要在产品中心,特约商户产品里面开通预充值与免充值代金券功能,并对要制券的商户进行授权。(微信服务商后台操作) 第三:向v_yranzhou@tencent.com发送开通跨商户制券发券 权限申请: 邮件内容: 申请原因: 制券商户号: 发券商户号: 第三:发送JSON串进行请求如下 {"stock_type":"NORMAL","coupon_use_rule":{"available_merchants":["1502263041"],"fixed_normal_coupon":{"coupon_amount":500,"transaction_minimum":1000}},"no_cash":true,"stock_use_rule":{"prevent_api_abuse":false,"natural_person_limit":true,"max_amount":1000000,"max_coupons_per_user":60,"max_coupons":2000},"stock_name":"满10减5","available_end_time":"2021-02-20T13:29:35.120+08:00","belong_merchant":"1500611721","out_request_no":"15006117211500611721","available_begin_time":"2021-01-28T15:30:35.120+08:00"} 第四:如果返回的JSON为 {"code":"NO_AUTH","message":"你配置的信息需要开通特殊权限"} 这个错误并不是你缺少什么权限,而是传参不符合规则主要几点注意: 1.stock_name:最多可填写9个字 2.max_coupons_per_user:单天发放个数上限不能为0 3. coupon_amount:1<=coupon_amount<=100000 4.available_time_after_receive:可用时间:相对时间,按分钟设置,是否1min<=分钟范围<=1440min 5.transaction_minimum校验规则: a、使用门槛-券面额>=0.01(门槛要大于面额) b、0.1元<=门槛<=100000 6.stock_type:目前只支持NORMAL 7.out_request_no:校验规则:不可以重复 8.一定要注意开始时间与结束时间间隔不能大于90天 如果以上都不能解决你的问题的话,你需要在文档的右侧: [图片] 发送人工服务,排队请求官方支援。
2021-01-29 - 使用微信服务商下关联微信商户添加代金券时提示“可用商户不符合规则”
如题 "{\"code\":\"INVALID_REQUEST\",\"message\":\"可用商户不符合规则,请检查\"}"
2021-05-20 - 营销事件推送开通问题
现在就是卡券核销以后不能收到事件通知消息,看了官方文档说明是需要开通营销事件推送能力,但是这个功能按钮在什么问题,现在怎么找不到,请麻烦给发给地址或者连接 [图片]
2020-05-21 - 一个身份证号码只能绑定5个小程序,是针对一个主体?还是无论在哪个主体都是这样的?
一个身份证号码只能绑定5个小程序,是针对一个主体?还是无论在哪个主体都是这样的? 比如张三身份证号码, 在A公司注册满5个小程序, 那在B公司还能注册吗?
2020-12-21 - 微信支付商户免充值代金券接口升级验收脚本 用例组合1001+1002+1003+1004+1005(强迫症专用)
为什么要进行接口升级验收 商户开通免充值类产品功能后,微信支付接口和账单格式会有调整,商户内部系统需要适配升级后的接口参数。 注:本文所提供脚本代码需要电脑安装python环境才可以运行。 注意:一键验收脚本虽好,自己应用层代码也需要改哦 如何进行接口升级 准备事项 1) 微信支付商户号 -2)微信支付商户号对应32位秘钥 以下是升级脚本代码,替换自己的商户号和密钥运行即可 运行后可以在下面链接查询验收结果,脚本执行遇到报错或遇到网络问题可以多次运行直到成功 https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=15_6&index=4 [代码]import time from xml.dom import minidom import hashlib from heapq import heappush, heappop from collections import OrderedDict import requests SandBox_Url = 'https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey' MicroPay_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/micropay" UnifiedOrder_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder" OrderQuery_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/orderquery" ReFund_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/refund" RefundQuery_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/refundquery" DownloadBill_Url = "https://api.mch.weixin.qq.com/sandboxnew/pay/downloadbill" nonce_str = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS" if __name__ == '__main__': mch_id = "" wxpay_key = "" def get_sign_key(mch_id, key): template = "<xml><mch_id><![CDATA[{0}]]></mch_id>" \ "<nonce_str><![CDATA[{1}]]></nonce_str>" \ "<sign><![CDATA[{2}]]></sign></xml>" nonce_str = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS" encrypted_str = "mch_id=" + mch_id + "&nonce_str=" + nonce_str + "&key=" + key m = hashlib.md5() m.update(encrypted_str.encode('utf-8')) sign_key_request_data = template.format(mch_id, nonce_str, m.hexdigest().upper()) result = requests.post(SandBox_Url, sign_key_request_data) dom = minidom.parseString(result.content) root = dom.documentElement sandbox_signkey = '' if root.getElementsByTagName("return_code")[0].childNodes[0].nodeValue == "FAIL": retmsg = root.getElementsByTagName("return_msg")[0].childNodes[0].nodeValue raise RuntimeError("请求出了点小错误:" + retmsg) else: sandbox_signkey = root.getElementsByTagName("sandbox_signkey")[0].childNodes[0].nodeValue print("亲,这就是你的沙箱密钥了哦: \n" + sandbox_signkey) return sandbox_signkey def to_tree_map(param_map): keys = param_map.keys() heap = [] for item in keys: heappush(heap, item) sort = [] while heap: sort.append(heappop(heap)) res_map = OrderedDict() for key in sort: res_map[key] = param_map.get(key) return res_map def build_xml(param, wxpay_key): tree_map = to_tree_map(param) encrypted_str = "" for k in tree_map: encrypted_str += "{}={}&".format(k, tree_map[k]) encrypted_str = encrypted_str + "key=" + wxpay_key m = hashlib.md5() m.update(encrypted_str.encode('utf-8')) sign = m.hexdigest().upper() param.update(sign=sign) complete_tree_map = to_tree_map(param) xml = "<xml>" for k in complete_tree_map: xml += "<{}><![CDATA[{}]]></{}>".format(k, complete_tree_map[k], k) xml += "</xml>" return xml def request_handler(url, xml, desc): result = requests.post(url, xml) print(desc + "我才不是请求结果呢:\n" + result.content.decode("utf-8")) def upgrade(mch_id, wxpay_key): if mch_id == "": raise RuntimeError("出差错了哦,亲,你的商户号在哪呢?不填写商户号亲亲是要给空气去验收吗?") if wxpay_key == "": raise RuntimeError("出差错了哦,亲,你不填写商户密钥怎么继续呢,是用爱吗?") key = get_sign_key(mch_id, wxpay_key) nonce_str = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS" out_trade_no = round(time.time()) MicroPay_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'body': "check", 'out_trade_no': out_trade_no, 'total_fee': "501", 'spbill_create_ip': "8.8.8.8", 'auth_code': "120061098828009406", } MicroPay_xml = build_xml(MicroPay_param, key) request_handler(MicroPay_Url, MicroPay_xml, "亲,用例编号1001刷卡正常支付有结果了,快来看呀 \n") time.sleep(1) OrderQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'out_trade_no': out_trade_no, } OrderQuery_xml = build_xml(OrderQuery_param, key) request_handler(OrderQuery_Url, OrderQuery_xml, "亲,用例编号1001刷卡正常支付查询出结果了,快来看呀 \n") time.sleep(1) out_trade_no_2nd = round(time.time()) print("我是1002下单的订单号:",+ out_trade_no_2nd) MicroPay_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'body': "check", 'out_trade_no': out_trade_no_2nd, 'total_fee': "502", 'spbill_create_ip': "8.8.8.8", 'auth_code': "120061098828009406", } MicroPay_xml = build_xml(MicroPay_param, key) request_handler(MicroPay_Url, MicroPay_xml, "亲,用例编号1002刷卡正常支付结果来了,你还抓紧不来看 \n") time.sleep(1) OrderQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'out_trade_no': out_trade_no_2nd, } OrderQuery_xml = build_xml(OrderQuery_param, key) request_handler(OrderQuery_Url, OrderQuery_xml, "亲,用例编号1002刷卡正常支付查询结果,结果好像有点不太对呢 \n") time.sleep(1) ReFund_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str':nonce_str, 'out_refund_no': out_trade_no, 'total_fee': "502", 'refund_fee': "501", 'out_trade_no': out_trade_no_2nd, } ReFund_xml = build_xml(ReFund_param, key) request_handler(ReFund_Url, ReFund_xml, "亲,下面展示的是用例编号1002刷卡支付退款的结果,你猜对不对 \n") time.sleep(1) RefundQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str':nonce_str, 'out_trade_no': out_trade_no_2nd, } RefundQuery_xml = build_xml(RefundQuery_param, key) request_handler(RefundQuery_Url, RefundQuery_xml, "亲,用例编号1002刷卡支付退款查询结果返回中,加载不出来长按电源键或Ait+F4重试哦 \n") time.sleep(1) nonce_str = "5K8264ILTKCH16CQ2502SI8ZNMTM67VS" out_trade_no = round(time.time()) UnifiedOrder_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'body': "check", 'out_trade_no': out_trade_no, 'total_fee': "551", 'notify_url':"https://www.weixin.qq.com/wxpay/pay.php", 'spbill_create_ip': "8.8.8.8", 'trade_type': "JSAPI", } UnifiedOrder_xml = build_xml(UnifiedOrder_param, key) request_handler(UnifiedOrder_Url, UnifiedOrder_xml, "亲,用例编号1003-公众号/APP/扫码正常支付有结果了,快来看呀 \n") time.sleep(1) OrderQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'out_trade_no': out_trade_no, } OrderQuery_xml = build_xml(OrderQuery_param, key) request_handler(OrderQuery_Url, OrderQuery_xml, "亲,用例编号1003-公众号/APP/扫码正常支付查询出结果了,快来看呀 \n") time.sleep(1) out_trade_no_2nd = round(time.time() * 1000) print("我是1002下单的订单号:",+ out_trade_no_2nd) UnifiedOrder_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'body': "check", 'out_trade_no': out_trade_no_2nd, 'total_fee': "552", 'notify_url':"https://www.weixin.qq.com/wxpay/pay.php", 'spbill_create_ip': "8.8.8.8", 'trade_type': "JSAPI", } UnifiedOrder_xml = build_xml(UnifiedOrder_param, key) request_handler(UnifiedOrder_Url, UnifiedOrder_xml, "亲,用例编号1004-公众号/APP/扫码支付退款结果来了,你还抓紧不来看 \n") time.sleep(1) OrderQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'out_trade_no': out_trade_no_2nd, } OrderQuery_xml = build_xml(OrderQuery_param, key) request_handler(OrderQuery_Url, OrderQuery_xml, "亲,用例编号1004-公众号/APP/扫码支付退款查询结果,结果好像有点不太对呢 \n") time.sleep(1) ReFund_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str':nonce_str, 'out_refund_no': out_trade_no, 'total_fee': "552", 'refund_fee': "551", 'out_trade_no': out_trade_no_2nd, } ReFund_xml = build_xml(ReFund_param, key) request_handler(ReFund_Url, ReFund_xml, "亲,下面展示的是用例编号1004-公众号/APP/扫码支付退款的结果,你猜对不对 \n") time.sleep(1) RefundQuery_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str':nonce_str, 'out_trade_no': out_trade_no_2nd, } RefundQuery_xml = build_xml(RefundQuery_param, key) request_handler(RefundQuery_Url, RefundQuery_xml, "亲,用例编号1004-公众号/APP/扫码支付退款查询结果返回中,加载不出来长按电源键或Ait+F4重试哦 \n") time.sleep(1) DownloadBill_param = { 'appid': "wxd678efh567hg6787", 'mch_id': mch_id, 'nonce_str': nonce_str, 'bill_date': "2021-04-01", 'bill_type': "ALL" } DownloadBill_xml = build_xml(DownloadBill_param, key) request_handler(DownloadBill_Url, DownloadBill_xml, "亲,你要下载交易的对账单来了,加载中······,加载不出来长按电源键或Ait+F4重试哦 \n") upgrade(mch_id, wxpay_key) [代码] 没有写注释的习惯,遇到错误可以私信我,拒绝手摸手教学
2021-05-10 - 微信沙箱验证接口升级的,有APIv3版本吗?
我是微信服务商,已经完成APIv3版本的微信支付(小程序)的功能开发,并且已经上线运营了。现在开发需要接入「商家券」功能,在微信服务商后台添加特约商户产品「免充值商家券」,提示需要进行接口升级,接口升级需要经过沙箱环境(url里加入sandbox)验证,发现针对的都是老的api,难不成我v3已经做好了,现在还得把老的api(v2?)再开发一遍,然后在接入沙箱验证??就算验证了也是v2版本的api,v3的也没有验证?怎么搞?
2021-08-01 - 服务商模式下返回"appid和openid不匹配"
我们的问题是这样的,已有一个小程序与一个普通商户号绑定,可以正常支付。现在我们为了快速接入更多商户,申请了服务商模式。可是经过与微信客服沟通,小程序appid无法与服务商号相绑定。于是我们用公众号的服务号绑定了此服务商号。现在在调用统一下单接口时,微信返回错误: appid和openid不匹配 这个openid是我们的用户在登陆小程序时从微信获得的,也就是说它是对应小程序这个体系的。当我们拿着这个openid进行下单时,因为我们的服务商号绑定的是服务号appid而不是小程序appid,微信返回了这个错误。 那么请问,在这种情况下(小程序appid无法与服务商号绑定,绑定了服务号appid又返回不匹配的错误),我们究竟该如何做呢?
2019-04-17 - v3图片上传接口java代码(httpclient和httpmime实现)
之前用HttpURLConnection实现的,现在改成用httpclient和httpmime实现(我用的是4.5.9版本),感觉会简单一点。水平有限,如有错误请指正。 [代码]public[代码] [代码]class[代码] [代码]uploadFileTest2 {[代码] [代码] [代码][代码]public[代码] [代码]static[代码] [代码]void[代码] [代码]main(String[] args) {[代码][代码] [代码] [代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码] [代码] [代码][代码]//商户号[代码][代码] [代码][代码]String mchid = [代码][代码]""[代码][代码];[代码][代码] [代码][代码]//证书序列号[代码][代码] [代码][代码]String serial_no = [代码][代码]""[代码][代码];[代码][代码] [代码][代码]//商户私钥(拷贝apiclient_key.pem文件里-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----之间的内容[代码][代码])[代码][代码] [代码][代码]String rsaPrivateKey = [代码][代码]""[代码][代码];[代码][代码] [代码][代码]//微信支付平台公钥[代码][代码] [代码][代码]String rsaPublicKeyFile = [代码][代码]""[代码][代码]; [代码][代码] [代码][代码]//时间戳[代码][代码] [代码][代码]String timestamp = Long.toString(System.currentTimeMillis()/[代码][代码]1000[代码][代码]);[代码][代码] [代码][代码]//随机数[代码][代码] [代码][代码]String nonce_str = [代码][代码]""[代码][代码];[代码][代码] [代码] [代码] [代码][代码]//图片文件[代码][代码] [代码][代码]String filePath = [代码][代码]""[代码][代码];[代码][代码]//文件路径[代码][代码] [代码][代码]File file = [代码][代码]new[代码] [代码]File(filePath);[代码][代码] [代码][代码]String filename = file.getName();[代码][代码]//文件名[代码][代码] [代码][代码]String fileSha256 = DigestUtils.sha256Hex([代码][代码]new[代码] [代码]FileInputStream(file));[代码][代码]//文件sha256[代码][代码] [代码] [代码] [代码][代码]//拼签名串[代码][代码] [代码][代码]StringBuffer sb = [代码][代码]new[代码] [代码]StringBuffer();[代码][代码] [代码][代码]sb.append([代码][代码]"POST"[代码][代码]).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]sb.append([代码][代码]"/v3/merchant/media/upload"[代码][代码]).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]sb.append(timestamp).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]sb.append(nonce_str).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]sb.append([代码][代码]"{\"filename\":\""[代码][代码]+filename+[代码][代码]"\",\"sha256\":\""[代码][代码]+fileSha256+[代码][代码]"\"}"[代码][代码]).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]System.out.println([代码][代码]"签名原串:"[代码][代码]+sb.toString());[代码][代码] [代码] [代码] [代码][代码]//计算签名[代码][代码] [代码][代码]String sign = [代码][代码]new[代码] [代码]String(Base64.encodeBase64(signRSA(sb.toString(),rsaPrivateKey)));[代码][代码] [代码][代码]System.out.println([代码][代码]"签名sign值:"[代码][代码]+sign);[代码][代码] [代码] [代码] [代码][代码]//拼装http头的Authorization内容[代码][代码] [代码][代码]String authorization =[代码][代码]"WECHATPAY2-SHA256-RSA2048 mchid=\""[代码][代码]+mchid+[代码][代码]"\",nonce_str=\""[代码][代码]+nonce_str+[代码][代码]"\",signature=\""[代码][代码]+sign+[代码][代码]"\",timestamp=\""[代码][代码]+timestamp+[代码][代码]"\",serial_no=\""[代码][代码]+serial_no+[代码][代码]"\""[代码][代码];[代码][代码] [代码][代码]System.out.println([代码][代码]"authorization值:"[代码][代码]+authorization);[代码][代码] [代码] [代码] [代码][代码]//接口URL[代码][代码] [代码][代码]String url = [代码][代码]"https://api.mch.weixin.qq.com/v3/merchant/media/upload"[代码][代码]; [代码][代码] [代码][代码]CloseableHttpClient httpclient = HttpClients.createDefault(); [代码][代码] [代码][代码]HttpPost httpPost = [代码][代码]new[代码] [代码]HttpPost(url);[代码][代码] [代码] [代码] [代码][代码]//设置头部[代码][代码] [代码][代码]httpPost.addHeader([代码][代码]"Accept"[代码][代码], [代码][代码]"application/json"[代码][代码]);[代码][代码] [代码][代码]httpPost.addHeader([代码][代码]"Content-Type"[代码][代码], [代码][代码]"multipart/form-data"[代码][代码]);[代码][代码] [代码][代码]httpPost.addHeader([代码][代码]"Authorization"[代码][代码], authorization);[代码][代码] [代码] [代码] [代码][代码]//创建MultipartEntityBuilder [代码][代码] [代码][代码]MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()[代码].setMode(HttpMultipartMode.RFC6532);[代码] [代码][代码]//设置boundary[代码][代码] [代码][代码]multipartEntityBuilder.setBoundary([代码][代码]""[代码][代码]);[代码][代码] [代码][代码]multipartEntityBuilder.setCharset(Charset.forName([代码][代码]"UTF-8"[代码][代码]));[代码][代码] [代码][代码]//设置meta内容[代码][代码] [代码][代码]multipartEntityBuilder.addTextBody([代码][代码]"meta"[代码][代码], [代码][代码]"{\"filename\":\""[代码][代码]+filename+[代码][代码]"\",\"sha256\":\""[代码][代码]+fileSha256+[代码][代码]"\"}"[代码][代码], ContentType.APPLICATION_JSON);[代码][代码] [代码][代码]//设置图片内容[代码][代码] [代码][代码]multipartEntityBuilder.addBinaryBody([代码][代码]"file"[代码][代码], file, ContentType.create([代码][代码]"image/jpg"[代码][代码]), filename);[代码][代码] [代码][代码]//放入内容[代码][代码] [代码][代码]httpPost.setEntity(multipartEntityBuilder.build()); [代码][代码] [代码] [代码] [代码][代码]//获取返回内容[代码][代码] [代码][代码]CloseableHttpResponse response = httpclient.execute(httpPost);[代码][代码] [代码][代码]HttpEntity httpEntity = response.getEntity(); [代码][代码] [代码][代码]String rescontent = [代码][代码]new[代码] [代码]String(InputStreamTOByte(httpEntity.getContent()));[代码][代码] [代码][代码]System.out.println([代码][代码]"返回内容:"[代码] [代码]+ rescontent);[代码][代码] [代码][代码]//获取返回的http header[代码][代码] [代码][代码]Header headers[] = response.getAllHeaders(); [代码][代码] [代码][代码]int[代码] [代码]i = [代码][代码]0[代码][代码]; [代码][代码] [代码][代码]while[代码] [代码](i < headers.length) { [代码][代码] [代码][代码]System.out.println(headers[i].getName() + [代码][代码]": "[代码] [代码]+ headers[i].getValue()); [代码][代码] [代码][代码]i++; [代码][代码] [代码][代码]} [代码][代码] [代码] [代码] [代码][代码]//验证微信支付返回签名[代码][代码] [代码][代码]String Wtimestamp = response.getHeaders([代码][代码]"Wechatpay-Timestamp"[代码][代码])[[代码][代码]0[代码][代码]].getValue();[代码][代码] [代码][代码]String Wnonce = response.getHeaders([代码][代码]"Wechatpay-Nonce"[代码][代码])[[代码][代码]0[代码][代码]].getValue();[代码][代码] [代码][代码]String Wsign = response.getHeaders([代码][代码]"Wechatpay-Signature"[代码][代码])[[代码][代码]0[代码][代码]].getValue();[代码][代码] [代码][代码]//拼装待签名串[代码][代码] [代码][代码]StringBuffer ss = [代码][代码]new[代码] [代码]StringBuffer();[代码][代码] [代码][代码]ss.append(Wtimestamp).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]ss.append(Wnonce).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]ss.append(rescontent).append([代码][代码]"\n"[代码][代码]);[代码][代码] [代码][代码]//验证签名[代码][代码] [代码][代码]if[代码][代码](verifyRSA(ss.toString(), Base64.decodeBase64(Wsign.getBytes()), rsaPublicKeyFile)) {[代码][代码] [代码][代码]System.out.println([代码][代码]"签名验证成功"[代码][代码]);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]System.out.println([代码][代码]"签名验证失败"[代码][代码]);[代码][代码] [代码][代码]}[代码][代码] [代码] [代码] [代码][代码]EntityUtils.consume(httpEntity);[代码][代码] [代码][代码]response.close();[代码][代码] [代码] [代码] [代码][代码]} [代码][代码]catch[代码] [代码](Exception e) {[代码][代码] [代码][代码]System.out.println([代码][代码]"发送POST请求异常!"[代码] [代码]+ e);[代码][代码] [代码][代码]e.printStackTrace();[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码][代码] [代码] [代码] [代码][代码]public[代码] [代码]static[代码] [代码]byte[代码][代码][] InputStreamTOByte(InputStream in) [代码][代码]throws[代码] [代码]IOException{ [代码][代码] [代码] [代码] [代码][代码]int[代码] [代码]BUFFER_SIZE = [代码][代码]4096[代码][代码]; [代码][代码] [代码][代码]ByteArrayOutputStream outStream = [代码][代码]new[代码] [代码]ByteArrayOutputStream(); [代码][代码] [代码][代码]byte[代码][代码][] data = [代码][代码]new[代码] [代码]byte[代码][代码][BUFFER_SIZE]; [代码][代码] [代码][代码]int[代码] [代码]count = -[代码][代码]1[代码][代码]; [代码][代码] [代码] [代码] [代码][代码]while[代码][代码]((count = in.read(data,[代码][代码]0[代码][代码],BUFFER_SIZE)) != -[代码][代码]1[代码][代码]) [代码][代码] [代码][代码]outStream.write(data, [代码][代码]0[代码][代码], count); [代码][代码] [代码] [代码] [代码][代码]data = [代码][代码]null[代码][代码]; [代码][代码] [代码][代码]byte[代码][代码][] outByte = outStream.toByteArray();[代码][代码] [代码][代码]outStream.close();[代码][代码] [代码] [代码] [代码][代码]return[代码] [代码]outByte; [代码][代码] [代码][代码]} [代码][代码] [代码] [代码] [代码] public static byte[] signRSA(String data, String priKey) throws Exception { //签名的类型 Signature sign = Signature.getInstance("SHA256withRSA"); //读取商户私钥,该方法传入商户私钥证书的内容即可 byte[] keyBytes = Base64.decodeBase64(priKey); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); sign.initSign(privateKey); sign.update(data.getBytes("UTF-8")); return sign.sign(); } public static boolean verifyRSA(String data, byte[] sign, String pubKey) throws Exception{ if(data == null || sign == null || pubKey == null){ return false; } CertificateFactory cf = CertificateFactory.getInstance("X.509"); FileInputStream in = new FileInputStream(pubKey); Certificate c = cf.generateCertificate(in); in.close(); PublicKey publicKey = c.getPublicKey(); Signature signature = Signature.getInstance("SHA256WithRSA"); signature.initVerify(publicKey); signature.update(data.getBytes("UTF-8")); return signature.verify(sign); } [代码][代码] [代码] [代码] [代码]}[代码]
2019-09-17