- 视频号关联小商店/小程序
更新:以下内容适用于微信8.0.6版本前的设备,微信8.0.6版本后的设备详见指引 一、视频号关联商店的作用 [图片][图片] 二、关联条件 「小商店」 当前视频号的超级管理员为小商店的超级管理员当前视频号的超级管理员为小商店的成员「小程序」 当前视频号和小程序的主体一致当前视频号和小程序的超级管理员一致视频号的管理员为小程序的推广者(可在mp后台交易组件模块设置)三、如何上架直播 请完成视频号关联小商店/小程序步骤后,前往查看视频号直播关联小商店/小程序指引 四、视频号关联小商店/小程序步骤的详细说明 [图片][图片] 更多资讯,欢迎到【交流专区】微信小商店主页发帖和寻找答案。
2021-06-11 - 【分账接口】常见问题
文档地址:「请求分账(直连)」、「请求分账(服务商)」 Q1:调用请求分账接口返回”非分账订单不支持分账“是什么原因? A1:请按照以下几点检查: 微信订单号填写错误,请检查确认统一下单时未上传分账标识(profit_sharing=Y)的订单,是不支持分账的 Q2:调用请求分账接口返回”分账金额不足“是什么原因? A2:请按照以下几点检查: 该订单已全额退款,没有资金可以分账在微信支付中,实际收款之后微信支付会收取一定的结算手续费,在减去手续费后剩余的钱才能分账,详情可参考订单结算手续费说明该订单已解冻,已无分账资金(普通商户分账订单默认冻结期是30天; 电商分账订单默认冻结期是180天)超过订单剩余可分账金额或者该订单已无可分账金额,请检查确认(可调用查询订单待分账金额API确认剩余可分账金额) Q3:调用请求分账接口返回”分账接收方关系不存在,请检查参数中每个接收方的关系“是什么原因? A3:未添加分账接收方,分账接收方在分账之前需要调用“添加分账接收方接口”添加,请添加接收方后再调用请求分账接口。 Q4:调用请求分账接口返回“分账金额超出最大分账比例”是什么原因? A4:请检查分账的金额是否超出在商户平台设置的允许分账的最大比例,设置路径如下: 普通直连商户设置分账比例路径:登陆商户平台-产品中心-分账-分账管理比例普通服务商商户设置分账比例路径:需要特约商户可以登录商户平台-产品中心-授权的产品-分账授权中进行设置比例。电商收付通商户设置分账比例路径:登陆服务商商户平台-产品中心-我的工具箱-电商收付通-供应链分账设置里设置连锁品牌分账商户设置分账比例路径:登陆服务商平台-产品中心-合作工具箱-连锁品牌工具箱-品牌专区-品牌交易-品牌供应链分账-供应链分账管理设置 Q5:调用请求分账接口返回”无分账权限“是什么原因? A5:请按照以下几点排查: 1、未开通分账权限,请开通后再调用分账接口,可参考开通指引 2、请求参数错误,服务商用了普通商户的开发文档提交参数,检查确认 服务商模式请求分账文档 普通商户分账文档 Q6:分账调用“添加分账接收方接口”返回:微信用户姓名与实名不一致 A6:请求中传了字段“个人姓名name”,该字段传了之后会校验用户实名是否正确,请填写正确的用户实名(查看用户实名认证路径:微信-我-服务-右上角三点-实名认证-姓名) Q7:分账调用“请求单次分账接口”返回:分账接收方列表格式错误 A7:receivers中的参数amount类型错误,amount类型是int,请检查确认 Q8:分账接收方类型包括哪些? A8:有以下几个类型: MERCHANT_ID:商户ID PERSONAL_OPENID:个人openid(由父商户APPID转换得到)PERSONAL_SUB_OPENID: 个人sub_openid(由子商户APPID转换得到) Q9:分账调用“请求单次分账接口”,为什么不返回分账结果 A9:分账是异步的,需要调用“查询分账结果”接口查询确认 Q10:分账调用“请求分账接口”返回:订单处理中,请稍后重试 A10:请按照以下几点检查: 请在订单支付成功1分钟后再调用分账接口未结算的订单,请在结算后再调用分账接口请求分账。查看结算周期路径:超级管理员使用电脑登录商户平台(pay.weixin.qq.com),通过【账户中心】->【商户信息】->【结算信息】进行查看老资金流商户的订单,不支持分账(旧资金流流水介绍、新资金流流水介绍)商户开通了收支分离但手续费账户余额不足(手续费账户最低余额要求是100元以上,在充值手续费账户1小时后,订单会正常结算,即可正常调用分账接口) Q11:分账调用“请求分账接口”返回:分账接收方与原请求不一致 A11:商户分账单号填写错误,调用“请求分账接口”多次分账,要生成新的“商户分账单号”,不能使用已经分过账的商户分账单号 Q12:分账调用“请求单次分账接口” A12:请按照以下几点检查: 签名类型错误,分账接口签名类型目前只支持HMAC-SHA256普通商户的分账订单,请使用普通商户分账接口,不能使用服务商分账接口系统超时,请使用原参数尝试再次掉调用API Q13:调用分账接口是否有额外的手续费 A13:没有,商户的交易订单,平台会正常的收取结算手续费。商户使用分账功能没有额外的费用 Q14:分账调用“请求分账接口”返回:分账接收商户全称不匹配 A14:请按照以下几点检查: 分账接收商户全称填写错误,请填写正确的商户全称,商户全称对应进件接口中的字段“商户名称merchant_name”字段值没有加密,该字段值需要加密后上传,请正确加密后再提交。上传的中文全称乱码,请检查接口编码是否正确,接口需要使用UTF-8编码 Q15:分账调用“添加分账接收方接口”返回:账户不存在 ,请先点击充值 A15:账户未开通,请接收方商户在商户平台点击“充值”创建账户(商户平台-交易中心-充值) Q16:分账如果有退款怎么处理,是否可以回退? A16:需注意以下几点: 已分出去的资金,在商户接收方同意的情况下,可以发起分账回退。(接收方可在“商户平台-交易中心-分账-分账接收设置”中开启同意分账回退) 更多分账订单退款逻辑,请查看文档说明 [图片] Q17:分账调用“请求单次分账接口”返回:签名错误 A17:请按照以下几点检查: 使用签名检查工具校验签名算法是否有误确认秘钥是否有误(服务商模式使用服务商商户号秘钥,秘钥是在商户平台配置,如果同一商户号调用其它接口成功可排除是秘钥问题)确认接口实际的请求参数与生成签名原串的参数一致,不能增加或缺少参数(可通过打印签名原串进行排查)确认参数的大小写,参数名与接口文档一致签名原串的参数值使用原始值,不需要encode接口需要使用UTF-8编码 Q18:分账添加接收方接口,是在分账前添加一次,如果接收方无变化,后续是否还需要调用接口再添加 A18:是的,如果接收方没有变化,只需要添加一次即可 Q19:分账调用“查询分账结果接口”返回的分账单状态有几种 A19:有以下几点状态: ACCEPTED—受理成功 PROCESSING—处理中 FINISHED—处理完成 CLOSED—处理失败,已关单 Q20:在商户平台设置了分账动账通知url,为什么收不到通知 A20:请按照以下几点排查: 未设置动账通知url,该链接是通过商户平台【交易中心-分账接收设置】中配置的通知url,必须为https协议。如果链接无法访问,商户将无法接收到微信通知。必须为直接可访问的url,不能携带参数。示例:notify_url:https://pay.weixin.qq.com/wxpay/123456789商户未设置加密的密钥,请登录商户平台操作!请参考什么是APIv3密钥?如何设置?只有分账接收方才能收到分账动账通知,分账方是不会有通知的 Q21:分账调用“请求分账接口”返回:对同笔订单分账频率过高 A21:同笔订单多次分账频率是1秒1次,请降低频率后重试 Q22:分账后资金到可提现是否有中间状态 A22:没有中间状态 Q23:分账后的资金什么时候可提现 A23:分账后钱已经到商户的账户了,可以立刻提现 Q24:分账调用“完结分账接口”的作用是什么 A24: 调用该接口,可以将不需要进行分账的订单金额解冻给商户,解冻后的资金商户可自行发起提现 Q25:分账调用“分账回退接口”返回:参数不正确,请检查参数 A25:return_account与mch_id不能填写为相同的商户号,分账方与接收方商户号一致时,不需要回退 Q26:分账订单调用“申请退款接口”返回:申请退款金额大于剩余未分账金额,请等待分账完成后再试 A26:订单有过部分分账,退款金额不能大于剩余未分账金额,请调用“完结分账接口”解冻剩余资金后再发起退款 Q27:查询分账结果接口里面分账单状态(status)字段,当值为ACCEPTED时是表示分账成功了吗 A27:分账单的状态是表示分账单是否受理成功,并不代表分账是否成功。查看分账是否成功,需要调用查询分账结果接口,查看返回参数“分账接收方列表”里面的字段“分账结果result=SUCCESS”才是分账成功。 Q28:调用“添加分账接收方接口”一次可以添加多个接收方吗 A28:不可以,一次只能添加一个 Q29:请求分账接口返回:分账接收方不允许为分账出资方 A29:请按照以下几点检查: V2接口,“请求单次分账接口”分账接收方不允许为分账出资方,“请求多次分账接口”分账接收方可以为分账出资方V3接口,finish为true的情况,“请求分账接口”分账接收方不允许为分账出资方(这种场景,直接调完结分账API就好)。finish为false的情况,“请求分账接口”分账接收方可以为分账出资方 Q30:调用“请求分账接口”,分账分给多个接收方,会出现分账既有成功又有失败的情况吗 A30:同一次分账请求,会出现有的成功,有的失败的情况。具体请调用“查询分账结果接口”,查看返回参数“分账接收方列表”里面的字段“分账结果result=SUCCESS”才是分账成功。 Q31:“请求分账接口”分账接收方列表中的参数description会体现在分账账单里面吗 A31:在分账方分账账单和资金账单、分账接收方的资金账单里面都会体现 Q32:分账调用“添加分账接收方接口”返回:请求正在处理中,请稍后重试 A32:商户请求并发导致,重新再请求一次即可 Q33:分账调用“添加分账接收方接口”返回:商户已添加的分账接收方个数过多。请先删除多余的分账接收方,并在24小时之后再尝试添加 A33:添加分账接收方的个数限制是2W个,超过这个限制,请按照提示处理 Q34:电商收付通分账调用“请求分账回退接口”返回:可用余额不足,请充值后重新发起 A34:“回退商户号”的账户可用余额不足,需充值后再原单重试才能回退成功。(充值指引:登陆商户平台【交易中心】->【资金管理】->【充值/转入】,根据指引充值即可) Q35:电商收付通分账调用“请求分账回退接口”返回:可用余额不足,请充值后重新发起。这个时候,调用“查询分账回退结果API”却返回:PROCESSING(处理中),这个逻辑是正常的吗 A35:是正常的,逻辑就是这样的。这种情况,商户可以按照提示要求,提醒“回退商户号”充值后再原单重试即可回退成功 Q36:电商收付通分账调用“请求分账回退接口”返回:PROCESSING(处理中),什么情况会返回这种状态 A36:请参考以下几点: 网络抖动导致请求中断商户账户资金转账频繁,导致回退在排队时超时 Q37:电商收付通分账调用“查询分账回退结果接口”返回:TIME_OUT_CLOSED A37:TIME_OUT_CLOSED是fail状态了,也就是处于最终态,是不需要重试的。状态是SUCCESS也同理,也是最终态,不需要重试。返回TIME_OUT_CLOSED时可更换一个回退单,重新分账回退一次即可 Q38:电商收付通分账调用“请求分账接口”返回:分账补贴还未到账,不能受理分账 A38:报这个错误,是因为支付的订单在统一下单里面传了参数“补差金额:subsidy_amount”,传这个参数后,需要调用“请求补差API”完成补差,然后再调用“请求分账接口”即可正常分账 Q39:一笔交易在分账完成之后,将接收方和分账账户的绑定关系解除(删除分账接收方),然后进行分账回退,会成功吗 A39:会回退成功,不受删除分账关系的影响 这里的逻辑有两个: 这笔单曾经分给过了这个商户,且分账成功这个商户开通了分账回退 Q40:分账调用“分账回退接口”返回:PROCESSING A40:过一分钟后原单重试即可 Q41:分账回退有时间限制吗 A41:从订单创建的时间算起,现在分账回退限制180天以内的分账请求 Q42:分账方添加接口,如果相同的分账方重复提交,会返回添加失败,还是覆盖之前的分账方信息 A42:如果系统检测到已经绑定,那么会保留原来的数据,不更新数据,直接返回成功 Q43:在商户平台-管理分账接收方中手动添加分账接收方报错:系统错误,请稍后再试 A43:这个报错的原因是:账户未开通,请接收方商户在商户平台点击“充值”创建账户(商户平台-交易中心-充值) Q44:免充值和预充值的代金券,分账的时候,可分账的金额判断逻辑是一样的吗?比如10-5,使用了免充值代金券,可分账金额是5,使用了预充值代金券,可分账金额是10元还是5元呢 A44:不一样,使用了免充值代金券,可分账金额是5,使用了预充值代金券,可分账金额是10 Q45:电商收付通请求分账接口返回:appid与openid不匹配 A45:请求分账接口里面的APPID必须传电商平台服务商的APPID,所以商户在添加分账接收方时获取的openid,也必须是这个电商平台服务商APPID获取的openid Q46:请求分账回退接口返回:分账指令不存在,请检查是否有对应的分账单 A46:请按照以下几点排查: 分账回退里面的商户分账单号out_order_no,必须是请求分账接口的商户分账单号out_order_no请先调用查询分账回退结果API确认分账是否成功,分账成功的分账单才能调用回退接口正常回退。从订单创建的时间算起,分账回退限制180天以内的分账请求,超过180天不支持回退 Q47:查询订单待分账金额返回:记录不存在 A47:请按照以下几点排查: 记录不存在,可能是单号拼错了,请检查确认订单未结算,请在订单结算后再查询非分账订单,请检查订单支付时是否传了分账标识,传了分账标识的订单,才能正确查询 Q48:商户号能正常完结分账,但是查询分账结果却提示“无分账权限”。是什么原因? A48:分账权限被冻结,请登陆商户平台查看站内信,按照指引申诉处理。 能正常完结分账的原因是:完结分账,就是将这笔订单的剩余的可分账的钱,都解冻给自己,由于这笔钱本来就是自己的,所以分账完结是一个安全的操作(钱没有给其他人,也没有给服务商,给了自己),所以是不会做权限校验的。当前要分出去给到别人时,就会做相关的权限校验了。 Q49:请求分账接口,当提交请求后返回报错SYSTEM_ERROR,这个时候调用查询分账结果接口查询,每10分钟查询一次,共查询3次(共30分钟)。这样的情况下,是否可以不用原单重试?查询后是否可以换单再提交? A19:请求分账返回SYSTEM_ERROR时,调用查询分账结果接口3次(30分钟)后,查询结果仍然是不存在的情况:如果商户能保证在30分钟的窗口期内都不会重试,这样做是安全的。 但我们建议在返回SYSTEM_ERROR 情况下,商户还是原单重试,这种最安全,也不用查询和等待一个窗口期。 Q50:一个微信支付单被退完款,还可以继续分账吗? A50:不可以了,分账是针对该订单冻结的金额进行分账,如果退完款,就不能再分账了。 Q51:比如一个订单支付金额是100.1元,假如手续费是0.1元。分账前先退款了30元,默认分账比例是30%,现在可以分账的金额还是30元,这样理解没有问题吧? A51:没有问题 Q52:比如一个订单支付金额是100.1元,假如手续费是0.1元。分账前先退款了30元,默认分账比例是30%,现在可以分账的金额还是30元,那就是说,可能出现100退了80,分出去30这种情况? A52:不会, 两个相加不会超过订单金额的, 也就是说退款没有超过70元的话,可分账金额是30,超过70,可分账金额是剩下的钱。 Q53:普通服务商分账,添加分账接收方这个APPID,如果服务商商户号绑定了两个APPID“B”和"C",需要分账的订单统一下单中传的APPID是B,这个时候,添加分账接收方中的这APPID可以是“C”吗?还是说必须是“B”? A53:请注意以下两点: 添加分账接收方的时候,B下的openid,C下的openid都可以但是执行分账的时候,一次分账请求里,只能是同一个appid下的openid,不支持一次分账请求里的openid分别是俩appid下的 Q54:查询分账结果接口返回:记录不存在 A54:请按照以下几点排查: 记录不存在,可能是单号拼错了,请检查确认订单未结算,请在订单结算后再查询非分账订单,请检查订单支付时是否传了分账标识,传了分账标识的订单,才能正确查询订单未分账,所以没有记录,请在订单分账后再查询
2022-08-16 - 手续费现在的计算规则是什么
微信支付商户手续费计算说明一、名词解释: ◆结算:微信支付将结算分为正向结算和反向结算 正向结算:商户的交易款项转到商户的基本账户并扣除支付手续费的过程。 反向结算:商户发起退款后,微信支付将退款资金从商户的基本账户反向退给用户的过程。 ◆本次结算额:包含正向结算额与反向结算额。正向结算为正值,反向结算为负值。 ◆费率:交易发生时商户在微信支付平台签约的实际生效费率 ◆已结金额:正向结算的交易款金额。 ◆已结已退金额:已经正向结算后发生退款的交易款金额。 ◆已收手续费:正向结算已经收取的手续费金额。 ◆已收已退手续费:已经正向结算的交易发生退款,退还的手续费金额。 二、手续费计算逻辑 ◆对于交易收款: 应收手续费=ROUND ( 本次结算额 *费率 ) ◆对于交易退款: [图片] 说明: 1.ROUND(x),指对x做四舍五入,精确到分; 2.计算结果不满0.01不收取、退还手续费。 三、举例说明 一笔订单,金额是1,214.00元,商户手续费费率为0.6%。 1.交易收款(正向结算) 应收=ROUND ( 本次结算额 *费率 ) =ROUND ( 1,214.00 *0.6% ) =ROUND ( 7.284 ) =7.28(元) 2.交易退款(反向结算) 1)全额退款 [图片] 2)部分退款 结算时间 结算金额(元) 手续费金额(元) 第一次结算 -798.00 -4.79 第二次结算 -218.00 -1.30 第三次结算 -198.00 -1.19 具体计算方式: [图片]
2021-01-27 - 微信支付代金券退款的相关规则
1、预充值代金券退款规则 订单退款后,代金券是退还给用户还是退还给商户? 1.1、当订单全额款时,如果代金券在有效期内,会实时退还给用户,用户可继续使用2、当订单全额退款时,如果代金券不在有效期内,会实时退还给商户 1.2、当订单部分退款时,代金券资金按比耐退还给商户,如果代金券在有效期内,会在代金券 1.3、可用时间结束后,t+1退回至商户可用余额 1.4、当订单部分退款时,代金券资金按比例还给商户,如果代金券不在有效期内,实时退还至商户可用余额 部分退款时,代金券退还资金按比例计算规则: 退给用户金额=用户申请退款的订单金额*(用户实际支付金额订单总金额) 退给商户金额=用户申请退的订单金额*(优惠金额/订单总金额) 2、免充值代金券退款规则 订单退款后,代金券会不会退还给用户? 2.1、当订单全额退款时,如果代金券在有效期内,会实时退还给用户,用户可继续使用 2.2、当订单全额退款时,如果代金券不在有效期内,券不会退还给用户 2.3、当订单部分退款时,券不会退还给用户 退款时,用户退款金额计算规则: 退给用户金额=用户申请退款的订单金额*(用户实际支付金额订单总金额)
2021-01-18 - 「干货分享」一文了解微信优惠券产品(卡券、代金券、商家券)
相信很多产品运营和开发的朋友刚接触到微信营销,听到什么优惠券、卡券、代金券、支付券、商家券,是不是一脸懵逼,我只是想做个优惠而已,要不要这么复杂,这到底该接哪一个?希望这篇文章能让你有一个更加清晰的了解。 不管是卡券、代金券、商家券这些我们都可以统称为“优惠券”,而微信支付代金券有另一种叫法“支付券”,其实支付券还包含立减折扣的。 [图片] 优惠券定义 卡券:是微信公众号提供的一套电子卡券解决方案,实现卡券生成、下发、领取、核销的闭环,并使用对账、卡券管理等配套功能。 微信卡券能力不只包含普通的优惠券(代金券、折扣、兑换、团购、优惠券),还有会员卡、礼品卡、票证(电影票、汽车票、景点门票等)。商户可自行在公众平台或通过 接口 创建卡券,多种渠道投放给用户,用户用券时需核销卡券。 比如100元的订单金额,用户有一张10元代金券,商家先核销这10元代金券,再计算用户实际需要支付金额(90元),支付方式不限制微信支付、其他支付也是可以的。 代金券(即支付券):是微信支付面向商户的一种营销工具,商户创建代金券,可以发送给用户,当用户使用微信支付时,代金券会伴随交易自动核销/抵扣,帮助商户便捷地落地营销活动。 代金券类型包含预充值和免充值两种类型,预充值代金券适用于第三方出资策划的活动,例如:满100减10. 指订单金额100元,用户实付90元,商户实收100元;免充值适用于商户策划的活动,例如:满100减10。 指订单金额100元,用户实付90元(用户领券后,在支付中直接核销10元),商户实收90元。 [图片] 商家券:是微信支付为商户提供的电子优惠券解决方案,商家可在微信支付允许的范围内通过该功能实现商家优惠券信息生成、下发、领取、核销的闭环,并使用数据对账、券信息查询等配套功能完成商家券的管理操作。(目前只提供API接口功能,暂无法在商户平台创建) 其实可以说商家券就是卡券优惠券的升级版,都是商家自主核销,只是他们分属不同的平台,一个是公众号(卡券),一个是微信支付商户平台(商家券)。 [图片] 重要通知 微信卡券-优惠券功能现即将下线,有发券需要的商户尽快升级到“微信支付优惠券”:商家券或支付券(即代金券)。此次模块升级不涉及会员卡、礼品卡、票证产品不影响。https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11614329634x9Pvw&version=&lang=zh_CN&token= 优惠券产品框架 从投放场景、类型、核销看一下优惠券生态圈 [图片] 产品能力对比 内容 卡券(即将下线) 支付券(代金券) 商家券 平台体系 微信公众号 微信支付商户号 微信支付商户号 核销规则 商家核销,不限制微信支付 微信支付自动核销 商家核销,不限制微信支付 卡包 领券后进入卡包 平台发券,自动进入卡包;API发券需申请插卡权限 领券后进入卡包 自定义券码 支持 不支持 支持 营销场景 二维码;公众号消息;朋友圈广告;商家H5/APP/小程序 二维码;朋友圈;商家H5/APP/小程序;平台扫码领券、支付有礼、附近发券等 二维码;朋友圈;商家H5/APP/小程序;平台扫码领券、支付有礼、附近发券等 营销经费 无需充值(垫资) 支持预充值和免充值 无需充值(垫资) 开发能力 后台支持基本创建券与核销,同时支持商家API接口 后台支持创建券发券场景,同时支持API接口 目前只有API接口,无法在后台创建券 注:卡券优惠券产品即将下线,就不要过多关注了,了解一下就好了哈~~ 相关链接 微信卡券产品文档:https://mp.weixin.qq.com/cgi-bin/readtemplate?t=cardticket/faq_tmpl&type=info&token=1472580499&lang=zh_CN 微信卡券接口文档:https://developers.weixin.qq.com/doc/offiaccount/Cards_and_Offer/WeChat_Coupon_Interface.html 微信支付代金券产品文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter5_1_1.shtml 微信支付商家券产品文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter5_2_1.shtml
2021-05-10 - “下载二级商户资金账单”产品权限 在服务商平台上找不到这个权限?
调用申请二级商户资金账单API需要在【服务商平台 -> 产品中心】开通“下载二级商户资金账单”产品权限。但在 服务商平台 中 “产品中心” 界面找不到该权限,这个权限该如何开通?
2021-12-13 - 电商收付通分账失败,返回:订单处理中,暂时无法分账,请稍后再试
此问题可按下面几个步骤排查问题: 1,请在订单支付成功1分钟后再调用分账接口 2,未结算的订单,请在结算后再调用分账接口请求分账 3,老资金流商户的订单,不支持分账 4,商户开通了收支分离但手续费账户余额不足(手续费账户最低余额要求是100元以上,在充值手续费账户1小时后,订单会正常结算,即可正常调用分账接口)
2020-12-04 - 小程序怎么直接打开电商平台二级商户开户签约页面?
基于微信支付V3(电商收付通)接口 具体问题描述:二级商户进件,资料审核通过后,微信支付返回给平台一个链接:https://pay.weixin.qq.com/public/apply4ec_sign/s?applymentId=xxxx&sign=xxxxx,请问这个链接如何在小程序里打开让商户完成签约?我试了有个小程序:群接龙可以做到,截图如下: [图片] 2021.5.11更新:微信Android8.0.3上方案1已不能正常使用,方案2未知。 2021.5.12更新:验证方案2通过nginx转发跳转也不行,验证群接龙小程序上也更换为二维码方案。 2021.6.2更新:微信Android8.0.6上已恢复正常。 [图片]
2021-06-02 - 电商收付通分账退款,在订单完结之前,因为被扣了手续费,接口退款因余额不足返回403
全额退款因为被收了手续费,锁定余额不足,导致退款失败,提示“申请退款金额大于剩余未分账金额,请等待分账完成后再试”。 而部分退款,因为锁定余额没有这种问题,这是不是算是设计上的问题??? 2022-01-27 19:47:40.363 [ConsumeMessageThread_6-m-AC1100013B185B2133B18791829F000D] INFO c.m.portal.utils.WechatApiV3Utils - 【微信支付V3】请求消息体:{"amount":{"currency":"CNY","refund":200,"total":200},"notify_url":"xxxxxxxxxxxxxx","out_refund_no":"xxxx","out_trade_no":"xxxx","sp_appid":"xxxxx","sub_appid":"xxxxx","sub_mchid":"xxxxx"} 2022-01-27 19:47:40.917 [ConsumeMessageThread_6-m-AC1100013B185B2133B18791829F000D] INFO c.m.portal.utils.WechatApiV3Utils - 【微信支付V3】HTTP请求返回错误码:403 2022-01-27 19:47:40.917 [ConsumeMessageThread_6-m-AC1100013B185B2133B18791829F000D] INFO c.m.portal.utils.WechatApiV3Utils - 【微信支付V3】HTTP请求返回结果:{"code":"RULE_LIMIT","message":"申请退款金额大于剩余未分账金额,请等待分账完成后再试"}
2022-01-27 - 云函数(nodejs)使用上传图片api获取MediaID
接口说明 适用对象:服务商/电商平台 请求URL:https://api.mch.weixin.qq.com/v3/merchant/media/upload 逻辑流程: 从云储存或小程序端获取图片二进制数据 使用crypto生成文件摘要 使用crypto生成签名和授权信息(需要先有文件名和文件摘要) 自定义分割字符串boundary和HTTP头Content-Type 创建传输body的头部和尾部,并与文件拼接 发送请求 1. 获取图片二进制数据 可以从云储存获取,或小程序端传参 小程序获取的是ArrayBuffer,在后续使用中需要转型成Buffer 场景1: 从云储存获取图片buffer 云函数代码 [代码]// [云储存文件id] 可以从小程序端传入 const fileRes = await cloud.downloadFile({fileID: '云储存文件id'}) const imgBuffer = fileRes.fileContent [代码] 场景2:从小程序端获取图片buffer 小程序代码 [代码]const fileSystemManager = wx.getFileSystemManager() const buffer = fileSystemManager.readFileSync('图片路径') wx.cloud.callFunction({ name: '云函数名称', data: { buffer, filename: '图片名.jpg' } }) [代码] 云函数代码 [代码]let { buffer, filename } = event const imgBuffer = Buffer.from(buffer) [代码] 2. 生成摘要、创建meta [代码]const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } [代码] 3. 生成签名、创建授权信息 [代码]let getAuthorization = async function(meta) { // 获取商户私钥、证书序列号、商户号 // (可以储存在云数据中,从云数据库获取) const priKey = '商户私钥' const serialNo = '商户证书序列号' const mchid= '商户号' // 生成随机序列 let nonceStr = Math.random().toString() // 获取当前时间戳(这里需要的是秒数不是毫秒,要除以1000) let timestamp = Math.floor(Date.now() / 1000) // 创建待签名文本 let signatureText = "POST\n" + "/v3/merchant/media/upload\n" + timestamp + "\n" + nonceStr + "\n" + JSON.stringify(meta)+"\n" // 生成签名 let sign = crypto.createSign('RSA-SHA256') sign.update(signatureText) let signature = sign.sign(priKey, 'base64') // 合成授权信息 let authorization = 'WECHATPAY2-SHA256-RSA2048' + ` mchid="${mchid}"` + `,nonce_str="${nonceStr}"` + `,timestamp="${timestamp}"` + `,serial_no="${serialNo}"` + `,signature="${signature}"` return authorization } [代码] 4. 自定义分割字符串boundary和HTTP头Content-Type [代码]// 自定义分割字符串(可以自行定义,和发送的内容不重复即可) let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary [代码] 5. 创建传输body的前半部分和尾部,并与文件拼接 [代码]// 创建body的前半部分并转换为buffer // 注意反引号`与单引号'的区别 // 分割符要在boundary前加-- let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) // 创建body尾部(第一个\r\n是接在imgBuffer后面的换行) // 结束分割符要在boundary两边加-- let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) // 将imgBuffer加入头部与尾部,拼接成完整body(不能直接使用+号连接) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) [代码] 6. 发送请求 [代码]axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) }) [代码] 完整流程 [代码]const cloud = require('wx-server-sdk') cloud.init() const crypto = require('crypto') //使用crypto生成文件摘要以及签名 const axios = require('axios') //使用axios发送请求(npm install axios) exports.main =async (event, context) => { // 1. 获取图片buffer const imgBuffer = '从云储存或小程序获取' // 获取文件名(请自行获取) const filename = '图片名.jpg' // 2. 生成文件摘要 const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } // 3. 获取签名(使用上面的签名函数) let authorization = await getAuthorization(meta) // 4. 自定义分割字符串和Content-Type let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary // 5. 创建(拼接)body let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) // 6. 发送请求 return await axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) return res.data.media_id }) } [代码] 欢迎留言 本文为**电商平台(云开发)**的填坑之作,欢迎提出不足之处、分享云开发的经验和坑。
2021-06-02 - 微信支付普通分账、服务商分账申请高比例流程及材料(维护中,暂不对外,恢复时间待定)
普通分账申请高比例流程和资料 【务必按照邮件模板要求申请,附件名称规范、邮件主体带有申请表格】 流程:向微信支付运营邮件申请 注意:要如实描述场景。 别问行不行了,行不行看图不就知道了,不行的找找自己的原因,最后预祝大家申请成功。 [图片]
07-15 - 小程序单元测试
小程序单元测试小程序的测试和web应用测试区别不大,可以利用jest进行测试,但是由于jest只提供了nodejs和浏览器执行环境,因此小程序的api我们需要mock,下面讲解小程序测试的一些mock技巧。 mock小程序API我们测试小程序时,经常会调用微信api,例如wx.showLoading方法,但是因为我们的执行环境未定义该方法,会出现调用错误。 我们可以通过jest提供的global设置全局变量,可以在测试文件中单独编写,或者在package.json的jest块设置setupFiles属性,让jest自动加载。 [代码] "jest": { "setupFiles": ["./__tests__/wx.js"] },复制代码[代码]./tests/wx.js文件内容如下,表示将小程序的api方法定义为mock方法。 [代码]global.wx = { showLoading: jest.fn(), hideLoading: jest.fn(), showModal: jest.fn(), request: jest.fn(), getStorageSync: jest.fn(), showShareMenu: jest.fn(), };复制代码[代码]测试小程序页面[代码]// 空白的小程序页面代码 Page({ onLoad () { // your code } })复制代码[代码]一个空白的小程序页面,代码会被Page方法包裹,同时Page初始化后,会执行onLoad、onReady等生命周期方法,而且当前对象还能调用setData方法对页面data数据进行修改。 我们需要mock Page方法的实现,代码如下。 [代码]export const noop = () => {};export const isFn = fn => typeof fn === 'function';let wId = 0; global.Page = ({ data, ...rest }) => { const page = { data, setData: jest.fn(function (newData, cb) { this.data = { ...this.data, ...newData, }; cb && cb(); }), onLoad: noop, onReady: noop, onUnLoad: noop, __wxWebviewId__: wId++, ...rest, }; global.wxPageInstance = page; return page; };复制代码[代码]举个例子假设我们的小程序页面是一个电影列表展示,业务代码如下。 [代码]const filmServer = require('../../server/film.js'); Page({ data: { comingFilms: [], }, onLoad() { this.getComingFilm(); }, // 获取即将上映电影列表 getComingFilm() { return filmServer.getComingSoon(1, 5).then((data) => { data.films.forEach((film) => { const displayDate = `${new Date(film.premiereAt).getMonth() + 1}月${new Date(film.premiereAt).getDate()}日`; film.displayDate = displayDate; }); this.setData({ comingFilms: data.films }); }); }, });复制代码[代码]我们的编写两个测试用例保证代码的正确运行。1、保证onLoad时执行getComingFilm方法。2、保证getComingFilm后日期数据进行格式化。[代码]import '../../pages/film'; // 加载需要测试的页面 // 获取当前初始化的page对象,后续可用来调用setData等方法,类似小程序页面里的this。 const page = global.wxPageInstance; // mock网络请求 jest.mock('../../server/film.js'); describe('电影首页', () => { describe('onLoad', () => { beforeAll(() => { // spyOn后可使方法具有mock属性,同时不影响方法调用。 jest.spyOn(page, 'getComingFilm'); // 执行页面onLoad生命周期。 page.onLoad(); }); it('should getComingFilm', () => { // 断言onLoad后,是否执行了getComingFilm方法。因为我们前面已经将getComingFilm进行spyOn了,所以可以执行toBeCalled判断,否则会出错。 expect(page.getComingFilm).toBeCalled(); }); }); describe('getComingFilm', () => { it('should format premiereAt as MM月DD日 ', () => page.getComingFilm().then(() => { // 断言获取数据后,原始数据增加displayDate属性,格式化为MM月DD日 expect(page.data.comingFilms[0].displayDate).toEqual('9月12日'); })); }); });复制代码[代码]🌟🌟由于测试代码比较长,上面只截取了部分,完整代码可以访问github获取
2018-10-08 - 数据库原子操作和事务讲解
使用更新指令(如 inc、mul、addToSet)可以对云数据库的一条记录和记录内的子文档(结合反范式化设计)进行原子操作,但是如果要跨多个记录或跨多个集合的原子操作时,就需要使用云数据库的事务能力。 12.6.1 更新指令的原子操作关系型数据库是很难做到通过一个语句对数据强制一致性的需求来表示的,只能依赖事务。但是云开发数据库由于可以反范式化设计内嵌子文档,以及更新指定可以对单个记录或同一个记录内的子文档进行原子操作,所以通常情况下,云开发数据库不必使用事务。 比如调整某个订单项目的数量之后,应该同时更新该订单的总费用,我们可以设计采用如下方式设计该集合,比如订单的集合为 order: { "_id": "2020030922100983", "userID": "124785", "total":117, "orders": [{ "item":"苹果", "price":15, "number":3 },{ "item":"火龙果", "price":18, "number":4 }] } 客户在下单的时候经常会调整订单内某个商品比如苹果的购买数量,而下单的总价又必须同步更新,不能购买数量减少了,但是总价不变,这两个操作必须同时进行,如果是使用关系型数据库,则需要先通过两次查询,更新完数据之后,再存储进数据库,这个很容易出现有的成功,有的没有成功的情况。但是云开发的数据库则可以借助于更新指令做到一条更新来实现两个数据同时成功或失败: db.collection("order") .doc("2020030922100983") .update({ data: { "orders.0.number": _.inc(1), total: _.inc(15), }, }); 这个操作只是在单个记录里进行,那要实现跨记录要进行原子操作呢?更新指令其实是可以做到事务仿真的,但是比较麻烦,这时就建议用事务了。 12.6.2 事务与 ACID事务就是一段数据库语句的批处理,但是这个批处理是一个 atom(原子),多个增删改的操作是绑定在一起的,不可分割,要么都执行,要么回滚(rollback)都不执行。比如银行转账,需要做到一个账户的钱汇出去了,那另外一个账户就一定会收到钱,不能钱汇出去了,但是钱没有到另外一个的账上;也就是要执行转账这个事务,会对 A 用户的账户数据和 B 用户的账户数据做增删改的处理,这两个处理必须一起成功一起失败。 1、ACID一般来说,事务是必须满足 4 个条件(ACID): Atomicity(原子性)、Consistency(稳定性)、Isolation(隔离性)、Durability(可靠性): 原子性:整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中一部分操作,一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行前后,数据库都必须处于一致性状态。换句话说,事务的执行结果必须是使数据库从一个一致性状态转变到另一个一致性状态。比如在执行事务前,A 用户账户有 50 元,B 用户账户有 150 元;执行 B 转给 A 50 元事务后,两个用户账户总和还是 200 元。隔离性:事务的隔离性是指在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间事务之间,互不干扰。比如在线银行,同时转账的人虽然很多,但是不会出现影响 A 与 B 之间的转账;可靠性:即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态,已提交事务的更新不会丢失。 2、云函数事务注意事项01不支持批量操作,只支持单记录操作 在事务中不支持批量操作(where 语句),只支持单记录操作(collection.doc, collection.add),这可以避免大量锁冲突、保证运行效率,并且大多数情况下,单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以比如说在一个事务中同时对集合 A 的记录 x 和 y 两个记录操作、又对集合 B 的记录 z 操作。 02云数据库采用的是快照隔离 对于两个并发执行的事务来说,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据;不可重复读:在一个事务内两次读到的数据是不一样的,受到另一个事务修改后提交的影响,因此称为是不可重复读幻读:第一个事务对表进行读取,当第二个事务对表进行增加或删除操作事务提交后,第一个事务再次读取,会出现增加或减少行数的情况云开发的数据库系统的事务过程采用的是快照隔离(Snapshot isolation),可以避免并发操作带来数据不一致的问题。 事务期间,读操作返回的是对象的快照,而非实际数据事务期间,写操作会:1. 改变快照,保证接下来的读的一致性;2. 给对象加上事务锁事务锁:如果对象上存在事务锁,那么:1. 其它事务的写入会直接失败;2. 普通的更新操作会被阻塞,直到事务锁释放或者超时事务提交后,操作完毕的快照会被原子性地写入数据库中 12.6.3 事务操作的两套 API云开发数据库的事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。通过 runTransaction 回调中获得的参数 transaction 或通过 startTransaction 获得的返回值 transaction,我们将其类比为 db 对象,只是在其上进行的操作将在事务内的快照完成,保证原子性。transaction 上提供的接口树形图一览: transaction |-- collection 获取集合引用 | |-- doc 获取记录引用 | | |-- get 获取记录内容 | | |-- update 更新记录内容 | | |-- set 替换记录内容 | | |-- remove 删除记录 | |-- add 新增记录 |-- rollback 终止事务并回滚 |-- commit 提交事务(仅在使用 startTransaction 时需调用) 1、通过 runTransaction 回调获得 transaction以下提供一个使用 runTransaction 接口的,两个账户之间进行转账的简易示例。事务执行函数由开发者传入,函数接收一个参数 transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取数据库集合记录引用进行操作,rollback 方法用于在不想继续执行事务时终止并回滚事务。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const _ = db.command; exports.main = async (event) => { try { const result = await db.runTransaction(async (transaction) => { const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); console.log(`transaction succeeded`, result); return { aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(-100); } }); return { success: true, aaaAccount: result.aaaAccount, }; } catch (e) { console.error(`事务报错`, e); return { success: false, error: e, }; } }; 事务执行函数必须为 async 异步函数或返回 Promise 的函数,当事务执行函数返回时,SDK 会认为用户逻辑已完成,自动提交(commit)事务,因此务必确保用户事务逻辑完成后才在 async 异步函数中返回或 resolve Promise。 2、通过 startTransaction 获得 transactiondb.startTransaction(),开启一个新的事务,之后即可进行 CRUD 操作;db.startTransaction().transaction.commit(),提交事务保存数据,在提交之前事务中的变更的数据对外是不可见的;db.startTransaction().rollback(),事务终止并回滚事务,例如,一部分数据更新失败,对已修改过的数据也进行回滚。const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database({ throwOnNotFound: false, }); const _ = db.command; exports.main = async (event) => { try { const transaction = await db.startTransaction(); const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); await transaction.commit(); return { success: true, aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(); return { success: false, error: `rollback`, rollbackCode: -100, }; } } catch (e) { console.error(`事务报错`, e); } }; 也就是说对于多用户同时操作(主要是写)数据库的并发处理问题,我们不仅可以使用原子更新,还可以使用事务。其中原子更新主要用户操作单个记录内的字段或单个记录里内嵌的数组对象里的字段,而事务则主要是用于跨记录和跨集合的处理。
2021-09-10 - 同一个小程序,能关联多个微信支付商户吗?
我们公司目前在开发一款小程序,全国有上百家分店使用,门店都有自己的微信支付商户号,小程序内发生的交易会支付到对应门店的微信支付商户号里,想确认一下,一个小程序最多能关联多少个微信支付商户号? 一个小程序,可以关联多个商户号,但是从开发的角度来说,让发生的交易额直接支付到对应的商户号,开发流程会复杂很多,这里需要用到服务商分账的功能,先将交易资金存放在一个特定商户里,再由特约商户分配资金到对应的商户号。 服务商分账全流程 https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=24_1&index=1
2020-03-31 - 通过 GitHub Actions 自动上传小程序代码
miniporgram-ci 我们可以通过 miniporgram-ci 这个包来上传小程序代码 比如新建文件 [代码]scirpts/upload.js[代码] 示例代码如下: [代码]const path = require('path'); const {Project, upload} = require('miniprogram-ci'); const {version, description} = require('../package.json'); const {appid: appId} = require('../project.config.json'); (async () => { const project = new Project({ appid: appId, type: 'miniProgram', projectPath: path.join(__dirname, '../'), privateKeyPath: './private.key', ignores: [ '.github', 'scripts', 'README.md', 'yarn.lock', 'node_modules/**/*' ] }); const uploadResult = await upload({ project, version, desc: description, setting: { es7: true, minify: true, codeProtect: true, minifyJS: true, minifyWXML: true, minifyWXSS: true, autoPrefixWXSS: true }, onProgressUpdate: console.log }); console.log(uploadResult); })(); [代码] 我们需要在小程序中的公众平台的后台的 “开发” => “开发管理” => “开发设置” => “小程序代码上传” 下载上传秘钥,重命名为 [代码]private.key[代码] 和 禁用 “IP白名单” 功能 然后运行命令 [代码]node scripts/upload.js [代码] GitHub Actions Github 提供了 Github Actions 来出提供 CI/CD 和 自定义 workflow. 示例我们在 [代码]package.json[代码] 中的 [代码]scripts[代码] 中添加一行命令 [代码]{ "scripts": { "mp:upload": "node scripts/upload.js" } } [代码] 然后创建 [代码].github/workflows/upload_miniprogram.yml[代码] 文件,内容如下。 [代码]name: Upload MiniProgram on: push: branches: - main pull_request: types: ['labeled'] jobs: upload: if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'upload_miniprogram') runs-on: ubuntu-latest steps: - uses: actions/checkout@master - shell: bash env: MP_PRIVATE_KEY: ${{ secrets.MP_PRIVATE_KEY }} run: | if [[ -z $MP_PRIVATE_KEY ]]; then echo "##########" echo "" echo "Please set 'MP_PRIVATE_KEY' key in actions secrets" echo "" echo "##########" exit 1 else echo "$MP_PRIVATE_KEY" > private.key fi - uses: actions/setup-node@master with: node-version: '12.x' - run: | yarn yarn build yarn mp:upload [代码] if: ${{ github.ref == ‘refs/heads/main’ }} || contains(github.event.pull_request.labels.*.name, ‘upload_miniprogram’) 这里标识当分支是名称 [代码]main[代码] 的时候,或者当 PR 包含 [代码]upload_miniprogram[代码] label 的时候回出发上面的 [代码]upload[代码] 这个任务 MP_PRIVATE_KEY: ${{ secrets.MP_PRIVATE_KEY }} 有之前下载 [代码]private.key[代码] 是秘钥,所以 Git 应该 ignore 它。但是我们上传小程序的代码的时候需要使用,我们可以通过 GitHub 的 secrets 把秘钥的内容设置进去 然后我们 push [代码]main[代码] 这个分支就会出发我们的一个上传小程序代码的工作流
2021-05-29 - 服务号订阅通知灰度测试
服务号模板消息能力的设计初衷,旨在帮助开发者实现及时通知,但存在一些问题,如: 1. 部分开发者在用户无预期的情况下,发送与用户无关的信息,对用户造成了骚扰。 2. 模板消息是用户触发后的通知消息,不支持营销类消息,不能满足部分业务需求。 为提升微信用户体验,我们开始灰度测试服务号订阅通知功能。 能力说明 开发者可在服务号图文消息、网页等场景设置订阅功能,用户自主订阅后,开发者可按需求下发一条对应的订阅通知。 [图片] 用户可在图文订阅通知 [图片] 用户可在网页订阅通知 灰度测试计划 服务号订阅通知功能即日上线,已认证的境内主体服务号可前往 MP 后台开通使用,详见说明。 1. 服务号订阅通知灰度测试期自2021年1月27日0:00至4月30日24:00,期间服务号模板消息可正常使用;灰度测试期结束后服务号订阅通知的策略将另行公布,届时以官方信息为准; 2. 开发者使用订阅通知功能时,需遵循运营规范,不可用奖励或其它形式强制用户订阅,不可下发与用户预期不符或违反国家法律法规的内容。具体可参考文档:《微信公众平台运营规范》 微信团队 2021年1月27日
2021-01-29 - 解剖小程序的 setData
解剖小程序的 setData 1 转载背景 学习小程序一年时间以来,我很少转载外面的文章来社区,每个字都是深思熟虑敲的,但是这篇文章,对我意义太大了,这篇文章解决了我长久以来一个未解决的问题,对我的小程序优化起到十分重要的作业,可以让我后续的小程序在逻辑层面更加完善。 本文转自github,具体链接如下所示 https://godbasin.github.io/2018/10/05/wxapp-set-data/ 小程序的双线程,之前也有详细讲过了。而双线程的设计,使得逻辑层和渲染层无法直接进行数据传输。那双线程的渲染机制、通信机制,setData 的出现、工作原理、使用建议等,应该要怎么去理解呢? 无处不在的 setData几乎每个开发者都会用到[代码]setData[代码],要是在复杂的页面中,写了很多的[代码]setData[代码],然后我们会发现页面真的是延迟严重,甚至卡顿、假死。 官方在性能优化中有提到: 避免频繁的去 setData。避免每次 setData 都传递大量新数据。后台态页面进行 setData。但是到底是为什么呢?[代码]setData[代码]的出现、设计方案是怎样的,又为何要这么设计呢?一切都还是要从双线程说起。 小程序的虚拟 DOM双线程的难题我们知道,小程序的双线程设计,主要为了管控安全,避免操作 DOM。(可参考《小程序的底层框架》) 把开发者的 JS 逻辑代码放到单独的线程去运行,因为不在 Webview 线程里,所以这个环境没有 Webview 任何接口,自然开发者就没法直接操作 DOM,也就没法动态去更改界面。 但是,这样就产生了新的问题。没法操作 DOM,那用户交互需要界面变化的话怎么办呢? 模板数据绑定模版数据绑定的方案,已经成为前端框架中最基础的功能。 数据绑定的过程其实不复杂: 解析语法生成 AST。根据 AST 结果生成 DOM。将数据绑定更新至模板。浏览器会把 HTML 解析成一棵树,最后渲染出来。整个界面是对应着一棵 DOM 树。 其实浏览器页面的 DOM 结构树,也是 AST 的一种,把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成 HTML DOM。 而最容易引发性能问题的,主要是第三点。而关于数据更新的解决方案,React 首先提出了虚拟 DOM 的设计,而现在也基本被大部分框架吸收,小程序也不例外。 虚拟 DOM 机制说到数据更新的 Diff,更多的则是[代码]Diff + 更新模板[代码]这样一个过程。 虚拟 DOM 解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。 一般来说计算过程如下: 用JS对象模拟DOM树。 一个真正的DOM元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化 DOM 对象。我们用一个 JavaScript 对象结构表示 DOM 树的结构。 比较两棵虚拟DOM树的差异。 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录,最后得到一组差异记录。 把差异应用到真正的DOM树上。 对差异记录要应用到真正的 DOM 树上,例如节点的替换、移动、删除,文本内容的改变等。 小程序里,由于无法直接操作 DOM,主要也是通过数据传递的方式来进行相关的模版更新。模版绑定的机制、数据更新的机制,都可以参照上面的说明,想更具体理解也可以参考《前端模板引擎》。 那么既然不在一个线程,数据的通信是怎么做的呢? 小程序的数据通信与渲染机制双线程通信方式小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。 一个小程序存在多个界面,所以渲染层存在多个 WebView 线程。 逻辑层和渲染层的通信会经由微信客户端(Native)做中转,逻辑层发送网络请求也经由 Native 转发 ,小程序的通信模型如图: [图片] 当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。所以我们的[代码]setData[代码]函数将数据从逻辑层发送到视图层,是异步的。 有了线程之间的通信,我们来看看小程序的渲染机制。 双线程渲染机制双线程的渲染,其实是结合了前面的一系列机制(模版绑定、虚拟 DOM、线程通信),最后整合的一个执行步骤。 1. 通过模版数据绑定和虚拟 DOM 机制,小程序提供了带有数据绑定语法的 DSL 给到开发者,用来在渲染层描述界面的结构。 就是我们常见的这些: 1 2 3 {{ message }} 噢,这里顺便吐个槽,[代码]wx:if[代码]竟然不支持[代码][].indexOf(xx) > -1[代码]等等相关的函数运算(摔!)。 2. 小程序在逻辑层提供了设置页面数据的 api。 不用问就是[代码]setData[代码]了: 1 2 3 this.setData({ key: value }) [代码]setData[代码]函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的[代码]this.data[代码]的值(同步)。 3. 逻辑层需要更改界面时,只要把修改后的 data 通过 setData 传到渲染层。 传输的数据,会转换为字符串形式传递,故应尽量避免传递大量数据。 4. 渲染层会根据前面提到的渲染机制重新生成 VD(虚拟 DOM)树,并更新到对应的 DOM 树上,引起界面变化。 原生组件的出现原生组件的出现,其实与 setData 的机制也有那么点关系,那么就当题外话一块补充下。 频繁交互的性能我们知道,用户的一次交互,如点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过 setData 引起界面变化。这样的一个过程需要四次通信: 渲染层 -> Native(点击事件)。Native -> 逻辑层(点击事件)。逻辑层 -> Native(setData)。Native -> 渲染层(setData)。在一些强交互的场景(表单、canvas等),这样的操作流程会导致用户体验卡顿。 引入原生组件前面也说过,小程序是 Hybrid 应用,除了 Web 组件的渲染体系(上面讲到),还有由客户端原生参与组件(原生组件)的渲染。 引入原生组件主要有 3 个好处: 绕过 setData、数据通信和重渲染流程,使渲染性能更好。扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。体验更好,同时也减轻 WebView 的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。而原生组件的渲染过程: 组件被创建,包括组件属性会依次赋值。组件被插入到 DOM 树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y坐标)、宽高。组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。当位置或宽高发生变化时,组件会通知客户端做相应的调整。简单来说,就是 原生组件在 WebView 这一层只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。 有利必有弊,原生组件也是有限制的: 最主要的限制是一些 CSS 样式无法应用于原生组件由于客户端渲染,原生组件的层级会比所有在 WebView 层渲染的普通组件要高参考setData《小程序开发指南–6.3 原生组件》 结束语总而言之,这一节内容主要是围绕 setData 展开,包括双线程的渲染机制、通信机制,setData 的出现(逻辑层通知渲染层)、工作原理(evaluateJavascript 字符串传递)、使用建议(setData 交互性能)、性能优化(原生组件出现)。 小程序乍一看是简单的双线程设计,但仔细研究就会发现设计过程中也遇到了不少问题,不断探索解决才有了现在的美好样子。我们在开发过程中会踩的一些坑,其实在理解原理之后便很容易懂了。 现在再来看,官方在性能优化中说到的优化建议,你都能深刻理解了吗?
2020-11-09 - 小程序插件快速更新功能说明
功能介绍: 为帮助小程序快速迭代更新,新增插件快速更新功能。小程序开发者可在移动端便捷体验、快速更新小程序内插件的版本,无需修改代码或重新提交审核。 适用范围: 适用于不需要小程序改动原有代码逻辑或针对插件做适配,只更新插件内服务内容的插件版本更新。 使用流程: 1、插件开发者发布新版本时,选择快速更新。并选择要给正在使用哪些版本的小程序发送通知。 [图片] [图片] 2、选择快速更新后,使用低版本插件的小程序开发者管理员会收到通知 [图片] 3、在移动端选择体验最新版插件。系统将会以小程序线上版本+插件最新版本,生成体验版小程序。小程序开发者可在移动端体验该版本。 [图片] 4、体验确认服务预期后,小程序开发者可在移动端操作发布。操作后无需提交审核、直接发布现网,更新小程序版本。 [图片]
2020-03-18 - 微信小程序新能力:URL Scheme,可从短信跳转小程序
最近小程序上线了一个超级流量的新入口:URL Scheme。通过小程序页面的URL Scheme,可以在短信、邮件或微信外部的网页中打开小程序。 那么如何实现呢?官方文档已经写的很清楚啦,这里简单介绍一下。 首先,获取URL Scheme,通过服务端接口可以获取打开小程序任意页面的URL Scheme,支持生成到期失效和永久有效的URL Scheme。 [图片] 然后,通过短信群发平台将获取的URL Scheme + 营销文案发送到用户的手机上。 最后,用户收到短信后,直接点击URL Scheme唤起微信,跳转到对应小程序页面,就是这么简单。 除此之外,还可以通过邮件或外部浏览器打开跳转小程序。 由于部分操作系统仍不支持直接识别URL Scheme,因此直接将Scheme发送给用户可能存在无法打开小程序的情况。 为此,我们可以先准备一个H5页面,再从H5页面跳转到URL Scheme实现打开小程序。 [代码]location.href = 'weixin://dl/business/?ticket= *TICKET*' [代码] H5的示例代码我已经更新到Github,可以复用起来,基于官方的案例做了些改动,增加PC端打开时生成二维码方便手机扫码使用。 这次新能力的更新将使微信小程序不再局限于微信内部的流量,天花板被掀开啦。 而且短信和邮件营销的触达成本非常低,营销成本的压低也会催生出很多新的流量玩法,我们敬请期待吧。
2021-01-08 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 用 HTM 实现小程序 SVG
写在前面 今天你可以在小程序中使用 Cax 引擎高性能渲染 SVG! SVG 是可缩放矢量图形(Scalable Vector Graphics),基于可扩展标记语言,用于描述二维矢量图形的一种图形格式。它由万维网联盟制定,是一个开放标准。SVG 的优势有很多: SVG 使用 XML 格式定义图形,可通过文本编辑器来创建和修改 SVG 图像可被搜索、索引、脚本化或压缩 SVG 是可伸缩的,且放大图片质量不下降 SVG 图像可在任何的分辨率下被高质量地打印 SVG 可被非常多的工具读取和修改(比如记事本) SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性、可编程星更强 SVG 完全支持 DOM 编程,具有交互性和动态性 而支持上面这些优秀特性的前提是 - 需要支持 SVG 标签。比如在小程序中直接写: [代码]<svg width="300" height="150"> <rect bindtap="tapHandler" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"> </rect> </svg> [代码] 上面定义了 SVG 的结构、样式和点击行为。但是小程序目前不支持 SVG 标签,仅仅支持加载 SVG 之后 作为 background-image 进行展示,如 [代码]background-image: url("data:image/svg+xml.......)[代码],或者 base64 后作为 background-image 的 url。 直接看在小程序种使用案例: [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { renderSVG(html` <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>`, 'svg-a', this) }, tapHandler: function () { console.log('你点击了 rect') } }) [代码] 其中的 svg-a 对应着 wxml 里 cax-element 的 id: [代码]<view class="container"> <cax-element id="svg-c"></cax-element> </view> [代码] 声明组件依赖 [代码]{ "usingComponents": { "cax-element":"../../cax/index" } } [代码] 小程序中显示效果: [图片] 可以使用 [代码]width[代码],[代码]height[代码],[代码]bounds-x[代码] 和 [代码]bounds-y[代码] 设置绑定事件的范围,比如: [代码]<path width="100" height="100" bounds-x="50" bounds-y="50" /> [代码] 需要注意的是,元素的事件触发的包围盒受自身或者父节点的 transform 影响,所以不是绝对坐标的 rect 触发区域。 再来一个复杂的例子,用 SVG 绘制 Omi 的 logo: [代码]renderSVG(html` <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>`, 'svg-a', this) [代码] 小程序种显示效果: [图片] 在 omip 和 mps 当中使用 cax 渲染 svg,你可以不用使用 htm。比如在 omip 中实现上面两个例子: [代码] renderSVG( <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>, 'svg-a', this.$scope) [代码] [代码]renderSVG( <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>, 'svg-a', this.$scope) [代码] 需要注意的是在 omip 中传递的最后一个参数不是 [代码]this[代码],而是 [代码]this.$scope[代码]。 在 mps 中,更加彻底,你可以单独创建 svg 文件,通过 import 导入。 [代码]//注意这里不能写 test.svg,因为 mps 会把 test.svg 编译成 test.js import testSVG from '../../svg/test' import { renderSVG } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { renderSVG(testSVG, 'svg-a', this) } }) [代码] 比如 test.svg : [代码]<svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg> [代码] 会被 mps 编译成: [代码]const h = (type, props, ...children) => ({ type, props, children }); export default h( "svg", { width: "300", height: "300" }, h("rect", { bindtap: "tapHandler", x: "0", y: "0", height: "110", width: "110", style: "stroke:#ff0000; fill: #0000ff" }) ); [代码] 所以总结一下: 你可以在 mps 中直接使用 import 的 SVG 文件的方式使用 SVG 你可以直接在 omip 中使用 JSX 的使用 SVG 你可以直接在原生小程序当中使用 htm 的方式使用 SVG 这就完了?远没有,看 cax 在小程序中的这个例子: [图片] 详细代码: [代码]renderSVG(html` <svg width="300" height="200"> <path d="M 256,213 C 245,181 206,187 234,262 147,181 169,71.2 233,18 220,56 235,81 283,88 285,78.7 286,69.3 288,60 289,61.3 290,62.7 291,64 291,64 297,63 300,63 303,63 309,64 309,64 310,62.7 311,61.3 312,60 314,69.3 315,78.7 317,88 365,82 380,56 367,18 431,71 453,181 366,262 394,187 356,181 344,213 328,185 309,184 300,284 291,184 272,185 256,213 Z" style="stroke:#ff0000; fill: black"> <animate dur="32s" repeatCount="indefinite" attributeName="d" values="......太长,这里省略 paths........" /> </path> </svg>`, 'svg-c', this) [代码] 再试试著名的 SVG 老虎: [图片] path 太长,就不贴代码了,可以点击这里查看 pasiton 标签 [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="200" height="200"> <pasition duration="200" bindtap=${this.changePath} width="100" height="100" from="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88 c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242 C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879 s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z" to="M49.1 23.5H2.1C0.9 23.5 0 24.5 0 25.6s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 24.5 50.3 23.5 49.1 23.5zM49.1 7.8H2.1C0.9 7.8 0 8.8 0 9.9c0 1.1 0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 8.8 50.3 7.8 49.1 7.8zM49.1 39.2H2.1C0.9 39.2 0 40.1 0 41.3s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1S50.3 39.2 49.1 39.2z" from-stroke="red" to-stroke="green" from-fill="blue" to-fill="red" stroke-width="2" /> </svg>`, 'svg-c', this) this.pasitionElement = svg.children[0] }, changePath: function () { this.pasitionElement.toggle() } }) [代码] pasiton 提供了两个 path 和 颜色 相互切换的能力,最常见的场景比如 menu 按钮和 close 按钮点击后 path 的变形。 举个例子,看颜色和 path 同时变化: [图片] 线性运动 这里举一个在 mps 中使用 SVG 的案例: [代码]import { renderSVG, To } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` , 'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height/2 rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 this.pause = false this.interval = setInterval(()=>{ if(!this.pause){ rect.rotation++ svg.stage.update() } },15) }) [代码] 效果如下: [图片] 组合运动 [代码]import { renderSVG, To } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` ,'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 var sineInOut = To.easing.sinusoidalInOut To.get(rect) .to().scaleY(0.8, 450, sineInOut).skewX(20, 900, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(450) .to().scaleY(1, 450, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(900) .to().scaleY(0.8, 450, sineInOut).skewX(-20, 900, sineInOut) .cycle() .start() To.get(rect) .wait(1350) .to().scaleY(1, 450, sineInOut) .cycle() .start() setInterval(() => { rect.stage.update() }, 16) } }) [代码] 效果如下: [图片] 其他 vscode 安装 lit-html 插件使 htm 的 html[代码]内容[代码] 高亮 还希望小程序 SVG 提供什么功能可以开 issues告诉我们,评估后通过,我们去实现! Cax Github 参考文档
01-04 - 小程序API的异步优雅用法
先上代码,写一个全局的 [代码]wx.$promisify()[代码] 方法 [代码]wx.$promisify = (method, opts, ...params) => new Promise((resolve, reject) => wx[method]( { ...opts, success: resolve, fail: reject }, ...params ) ) [代码] 举一个几乎大家都会用到的登录为例(虽然图1我已经改进了很多次,但嵌套问题还是很刺眼) [图片] 改进后,多层嵌套变扁平了 [图片]
2020-05-20 - 微信对话开放平台客服小程序出炉,可进行移动端人工回复客服消息啦!
在上周四(2020.4.9)的推送中,我们介绍了智能客服转人工服务升级的成果,可点击这里进行回顾。客服小程序的应用,打破原来仅支持PC端的时间空间限制,只需完成绑定,无需坐在电脑面前,只需一部手机,便可随时随地可回复用户留言。今天我们将对从PC端走向移动端的客服小程序进行详细介绍。 配置方法 Step1:扫码绑定客服小程序 [图片] Step2: 进入主界面- 切换机器人 [图片] Step3:打开消息通知按钮,有未读消息,服务通知会进行消息提示。 [图片] Step4:点击【在线客服】-点击用户头像-点击下方接入人工按钮-进入人工回复 [图片] 注:当有某位客服人员接入某用户时,该用户对话内容自动全部进入此客服人员,其余客服人员无法收到该用户会话。 以上就是本次更新的全部内容啦,如果大家有什么其他想法,欢迎与我们留言互动,或者扫描下方二维码,添加我们的官方客服的微信号,邀请您进入到我们的用户答疑群,进行面对面的交流。 [图片]
2020-07-31 - 虚拟业务指南请收好。
在小程序生态中,基于苹果运营规范,小程序内暂不支持iOS端虚拟支付业务。为此小编为大家整理了一份虚拟支付业务指南,希望大家在做虚拟业务时有所帮助: [视频] 那么,到底什么是虚拟支付业务呢? 虚拟支付业务是指购买非实物商品。比如:VIP会员、充值、录制课程、录制音频视频等虚拟产品。目前iOS端暂不支持虚拟支付业务。 我们常见iOS虚拟支付的不合规示例有哪些呢? 示例一 :小程序内存在付费购买虚拟内容或道具。商品多体现为提前编辑好的、录制好的虚拟商品。如录制视频课程、游戏道具。 整改建议 :建议去除小程序内所有付费购买虚拟服务,并根据提示修改相关内容及文案,文案可参照“由于相关规范,iOS功能暂不可用”。 [图片] 示例二 :付费解锁优质服务。多体现为提供虚拟商品的小程序可通过支付购买、开通虚拟会员等形式,体验小程序付费服务。比如:支付阅读章节小说、同城生活服务平台付费发帖/付费置顶等。 整改建议 :建议可以关闭iOS端虚拟支付通道,并将【马上充值】更改为【由于相关规范,iOS功能暂不可用】,并不再提供iOS端会员服务。 [图片] 示例三 :关闭iOS端虚拟支付功能后,虚拟商品页面仍然保留货架价格标签展示、购买/付费/订阅等功能或按钮。 整改建议 :建议去除小程序中的虚拟商品的价格展示,并更改为【免费】;并将【订阅 ¥128】更改为【由于相关规范,iOS功能暂不可用】,并不再提供iOS端虚拟商品购买服务。 [图片] 示例四 :关闭iOS端虚拟支付功能后,提供引导用户前往其他支付的路径/文案,完成虚拟支付闭环。 整 改建议 :建议去除iOS端小程序内引导用户前往其他支付路径/文案,并不再提供iOS端虚拟商品购买服务。 [图片] 示例五 :小程序含需要付费的虚拟商品,并设置限时免费的服务,限时免费结束后需付费才能继续提供服务。 整改建议 :建议将iOS端小程序中所有虚拟付费内容更改为免费,并不再提供iOS端虚拟商品购买服务。 [图片] 示例六 :关闭iOS端虚拟支付功能后,小程序中虚拟产品页面不可以含有付费性质的关键字(如:购买、已购、付费、支付等),包括但不限于功能按钮、功能页面、支付提示及任何商品介绍等。 整改建议 :建议将小程序iOS端虚拟产品页面中的文案/按钮/功能tab含有限制的关键字更改为【免费】或删除。并不再提供iOS端虚拟商品购买服务。 [图片] 如小程序内存在以上不合规的虚拟支付内容,请开发者重视并及时整改。对于首次违规的小程序,平台将下发站内信整改通知,并给予三天整改时间,请开发者按照提示在限期内完成整改。平台将会对到期未完成整改的小程序进行搜索策略调整,并在小程序功能使用上进行一定的限制,直到小程序完成内容整改。
2020-04-23 - 不建议在小程序内做有偿投票业务
不知道大家是否有这样经历... 铁哥们儿的萌娃参加了英语才艺比赛,发来投票小程序让我助力一把,我当然义不容辞! 领导的闺女参加了钢琴大赛,发来链接让我点赞,我必须秒赞! 当表妹参加舞蹈比赛让我帮忙拉票时,我已经轻车熟路了.... 如今投票已成为一种社交互动的方式,但最近小编发现小程序中的有偿投票模式,让我对投票有了新的认识。有偿投票模式是指强制或吸引用户以有偿的方式进行投票或影响投票结果的行为,包括但不限于直接线上支付,可直接或间接获得奖品、积分、兑换券、折扣券等实物或虚拟物品,或者直接或间接先行购买礼品、票券等实物或虚拟物品后才能获得投票资格或不同可投票数。 平台认为投票方式旨在不受外力作用的前提下公平地选出最优秀的作品。但有偿投票不仅破坏公平性,也令投票活动失去其本身的意义,因此平台内暂不支持有偿投票。 在小程序中,有偿投票通常以如下几类形式出现: 1、明星打榜类有偿投票 小程序涉及提供打榜服务,当免费打榜次数使用完毕后,可付费购买打榜次数,打榜次数越多,对应明星的排名越高,即可通过付费改变排名结果。 [图片] 2、直播类有偿投票 小程序内提供选手直播服务,支持观众付费购买虚拟商品服务,购买的虚拟商品可获得对应“人气”用于支持参赛选手,在指定时间内“人气”数量最高的选手将获得最终胜利。 [图片] 3、线下活动有偿投票 小程序内提供线下实体活动投票平台服务,需为参赛选手购买虚拟礼物才可以获得可投票数。 [图片] 如小程序内存在以上几类或其他形式的有偿投票内容,请开发者重视并及时整改。首次发现将限期3天整改,到期未整改将封禁“小程序支付”功能。 如已整改后续仍直接或间接再有类似行为的,将对小程序永久封禁处理。 若同一主体下多个帐号均存在类似违规行为的,将根据违规程度对该主体下所有小程序采取限制功能直至拒绝再向该主体提供任何注册或认证服务。
2019-12-19 - 七牛直传插件(vktool),给你飞的翅膀
为什么要写这样一个插件 中小公司或个人都会把资源文件放到七牛上,小程序在做上传时,都要依赖服务端生成 token; 1. 服务端生成 token 代码都要写一遍 2. 一些七牛的坑或限制也要淌一遍 3. 分块、分片都实现一遍 基于这些原因写了这个小程序插件(vktool),使用云开发生成 token; 这个插件源码开放(https://github.com/myzingy/wx-plugin-oxoo), 不会储存你的ak\sk,如果你不放心,可以直接部署成你的插件; 针对七牛的处理 图片处理(20兆以内) 一般为了加快图片加载,七牛图片都会增加类似 ?imageView2/3/w/980 的缩略图方式,但如果图片很大,超过20兆时,七牛直接就报错了,图片也显示不出来,针对这个问题,生成token 上传策略时,特意另存了一份.lim.jpg 的瘦身文件。 分片上传后的 lim 文件 2.1 分片真是个复杂的事,坑已淌好,这个插件已处理好,你可以直接用; //图片瘦身另存为lim options.persistentOps = ‘imageslim|saveas/$(x:limkey)’,分片上传后一直不生成lim文件,提交了工单,七牛给出了解决方案(七牛技术服务响应很高效),需要做2次urlsafeBase64Encode [图片] 2.2 小程序 FileSystemManager.readFileSync 真机目前只能支持10兆内的文件,超过10兆直接报错,等小程序官方修复吧;目前分片上传只能支持10兆以下文件,意义不大; 即使你不用插件,希望这些坑也能帮到你,毕竟源码都给你了 案例 [图片] 自己写了个小程序 图略,新建活动或发布照片都可以体验七牛直传;
2020-04-23 - 数据库查询时怎么比较2个字段?
有个number字段,A,B,查询A>B的条件怎么写?A:_.gt(B)不行。
2019-09-25 - 获取小程序任何页面链接的方法
小程序不像网站,任何页面都可以复制出来链接。要访问某个页面,直接点击链接就可以了。其实小程序也是可以复制出链接。 不废话,马上上干货~! 1、首先进入小程序后台,把要获取链接的微信添加到项目成员。 [图片] [图片] 2、进入生成小程序码工具,添加获取链接的微信号。 不知道怎么进入生成小程序码工具,请看来一间上一篇文章:一个独特的小程序码生成方法。 [图片] 点“获取更多页面路径”打开窗口,然后输入上面添加的微信号点击“开启”按钮。如上图“开启入口成功”字样就会显示出来。这时代表这个微信号能复制出当前小程序的任意可显示页面的链接。 3、进入当前小程序,就可以获取到当前显示页面的链接。 [图片] 获取到小程序链接有什么用?请看看来一间上一篇文章。 最后再送出一个小彩蛋:其实小程序有个大原则:所见即所得! 就是你进入的页面,转发出去的页面也是当前你打开的页面。
2019-11-19 - 微信支付成功后,没有回调
微信支付成功后,没有回调,为什么会偶尔没有回调呢?
2019-10-29 - 云开发 Date 时区问题
背景:使用云函数从云数据库获取日期为大于今天00:00时,小于今天23:59时的数据,发现取出的数据异常,有间隔8小时规律 实际探究结论: 在云函数中,new Date(), 返回的是 UTC标准时间-0时区 在云函数查询数据库,返回值中的Date类型,返回的也是 UTC标准时间-0时区 而在云数据库界面上操作的是填写时候看到的是RFC-2822格式标准, 东八区时间, 而直接查询返回的是ISO-8601格式标准, UTC标准时间-0时区。举例,在云数据库界面上填写并展示的是 Wed Nov 21 2018 00:02:00 GMT+0800 (CST), 而通过查询返回的时间是 2018-11-20T16:02:00.000Z。 所以假设今天日期为 2018-11-21 当某条数据在云数据库的时间显示为, Wed Nov 21 2018 00:02:00 GMT+0800 (CST),由上述结论得出实际的UTC时间为 2018-11-20T16:02:00.000Z, 根据我的业务逻辑(查询今日的数据),我需要做如下操作 [代码] [代码][代码]const now = [代码][代码]new[代码] [代码]Date(); // 云函数当前的 UTC 0时区 标准时间, [代码] [代码] //例如当前时间为2018-11-21 9:00:00, 则now= 2018-11-21T01:00:00.000Z[代码] [代码] [代码][代码]const now_gone = [代码] [代码] + 8 * 3600000 // 东八区与0时区的时间差[代码] [代码] + now.getUTCHours() * 3600000 [代码] [代码] + now.getUTCMinutes() * 1000 * 60 [代码] [代码] + now.getUTCSeconds() * 1000 // 总和为 UTC今天已走过的时间 + 东八区时间差, 也就是东八区已经走过的时间[代码] [代码] [代码] [代码] [代码][代码]const startDate = [代码][代码]new[代码] [代码]Date(now - now_gone);[代码] [代码] [代码] [代码] [代码][代码]const endDate = [代码][代码]new[代码] [代码]Date(now - now_gone + 24 * 3600000);[代码][代码] [代码][代码]return[代码] [代码]db.collection([代码][代码]'***'[代码][代码]).where({[代码][代码] **[代码][代码]Date: _.and(_.gt(startDate), _.lt(endDate))[代码][代码] [代码][代码]}).get()[代码] 其实总结下来,就是 云函数 new Date(),是0时区的时间,与中国东八区时间由8小时间隔,计算的时候需要处理一下
2018-11-21 - 如何实现小程序的强制更新
大家都知道小程序提交审核发布以后是不会马上更新版本的,用户需要下次使用才会更新到新的版本,这就是冷更新。 那么如果要做到及时生效怎么办呢?这时候就要做处理了,将下面的代码添加到app.js,提交审核,发布就会生效了 [代码]onLaunch: [代码][代码]function[代码] [代码](options) {[代码] [代码] [代码][代码]this[代码][代码].autoUpdate()[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]autoUpdate: [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]// 获取小程序更新机制兼容[代码] [代码] [代码][代码]if[代码] [代码](wx.canIUse([代码][代码]'getUpdateManager'[代码][代码])) {[代码] [代码] [代码][代码]const updateManager = wx.getUpdateManager()[代码] [代码] [代码][代码]//1. 检查小程序是否有新版本发布[代码] [代码] [代码][代码]updateManager.onCheckForUpdate([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]// 请求完新版本信息的回调[代码] [代码] [代码][代码]if[代码] [代码](res.hasUpdate) {[代码] [代码] [代码][代码]//检测到新版本,需要更新,给出提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'更新提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'检测到新版本,是否下载新版本并重启小程序?'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//2. 用户确定下载更新小程序,小程序下载及更新静默进行[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](res.cancel) {[代码] [代码] [代码][代码]//用户点击取消按钮的处理,如果需要强制更新,则给出二次弹窗,如果不需要,则这里的代码都可以删掉了[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'温馨提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'本次版本更新涉及到新的功能添加,旧版本可能无法正常访问哦'[代码][代码],[代码] [代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码]//隐藏取消按钮[代码] [代码] [代码][代码]confirmText: [代码][代码]"确定更新"[代码][代码],[代码][代码]//只保留确定更新按钮[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//下载新版本,并重新应用[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 下载小程序新版本并重启应用[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]downLoadAndUpdate: [代码][代码]function[代码] [代码](updateManager) {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]wx.showLoading();[代码] [代码] [代码][代码]//静默下载更新小程序新版本[代码] [代码] [代码][代码]updateManager.onUpdateReady([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]wx.hideLoading()[代码] [代码] [代码][代码]//新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码] [代码] [代码][代码]updateManager.applyUpdate()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]updateManager.onUpdateFailed([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]// 新的版本下载失败[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'已经有新版本了哟'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'新版本已经上线啦,请您删除当前小程序,重新搜索打开哟'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码]
2019-06-07