最近有一个版本需求,需要接入周期扣款做连续会员的功能,没想到这一做就是小半个月,趟了很多坑,所以觉得有必要记录一下

1.周期扣款总体设计

在支付宝和微信中(非苹果支付),周期扣款的流程主要有以下两种,并且各有利弊

  1. 先签约,签约成功后再由商户发起主动扣款 #推荐#
    利端:由于一般来讲连续会员会有额外的折扣优惠,先签约再扣款避免了用户薅羊毛。
    弊端:签约和发起扣款时分开的,要额外做很多工作保障一致性。并且据微信官方文档,主动扣款是要延迟一段时间才能发起的。
  2. 支付并完成签约
    利端:签约和支付是合在一起的,用户完成了签约即完成了第一次支付
    弊端:在支付过程中,用户可以取消签约,只完成普通支付(好反人类,还可以取消),导致用户一直可以享受连续会员的优惠,但是并不进行签约。

由于签约和扣款的一致性问题开发努努力还能保证一下,所以我目前选用的是先签约-再扣款流程,也推荐选用。

苹果连续订阅服务
总结一句话–对服务端来说都是满满的恶意。
首先用户付款连续订阅成功了,服务端收不到苹果的任何通知,只有IOS客户端才能知道用户订阅成功了。
其次,用户订阅期间内每次扣款,服务端也无法进行感知,必须要不断的轮询用户的订单列表才能进行判断(IOS客户端都有办法能知道用户续订了)。

下面是一图流,我目前的系统设计方案

2.支付宝周期扣款

首先贴上官方文档地址-周期扣款
和支付宝对接还是相对轻松点的,毕竟技术支持还是比较尽心尽力的。
在先签约再扣款流程中,主要有用到以下几个接口

2.1 签约接口-文档

签约接口有几个坑,在这里给大家排一下雷

  1. 签约最小的周期是七天。
  2. 签约接口无法像下单接口一样,透传业务参数,需要开发者生成签约号并保留签约相关的业务信息。
  3. 在签约回调接口中,目前只能收到用户成功签约的通知,收不到用户解约通知,用户解约的通知是回调到应用网关地址(超级坑)详见回调文档说明

以下是签约接口调用的sdk示例

 /*** @Description: 生成客户端唤起签约页面的参数* @param notifyUrl 签约成功回调通知地址* @param isH5 是否为h5* @return: xxx.Result* @Author: lvqiushi* @Date: 2021-04-21*/public static Result<String> userAgreement(AlipayUserAgreementPageSignModel model, String notifyUrl, boolean isH5) {AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", AlipayAppId, AlipayPriveteKey, "json",H5AlipayConfig.CHARSET, AlipayPublicKey, "RSA2");AlipayUserAgreementPageSignRequest request = new AlipayUserAgreementPageSignRequest();request.setBizModel(model);request.setNotifyUrl(notifyUrl);// 周期扣款场景使用小程序/h5 接口跳转至签约页面时请使用 alipayClient.sdkExecute 方法; 若想获取跳转链接转换二维码可使用 alipayClient.pageExecute(request,"get")AlipayUserAgreementPageSignResponse response = null;try {if (isH5) {response = alipayClient.pageExecute(request,"get");} else {response = alipayClient.sdkExecute(request);}} catch (AlipayApiException e) {log.error("生成支付宝签约参数时,发生错误", e);return Result.fail("失败");}return Result.ok(response.getBody());}public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();AlipayUserAgreementPageSignModel model = new AlipayUserAgreementPageSignModel();model.setSignValidityPeriod("7d");model.setProductCode("CYCLE_PAY_AUTH");model.setPersonalProductCode("CYCLE_PAY_AUTH_P");model.setSignScene("INDUSTRY|SOCIALIZATION");// 自定义订单号model.setExternalAgreementNo("XIUCAI2013548132543612315");model.setAgreementEffectType("DIRECT");AccessParams accessParams = new AccessParams();accessParams.setChannel("ALIPAYAPP");model.setAccessParams(accessParams);PeriodRuleParams periodRuleParams = new PeriodRuleParams();// 周期天数periodRuleParams.setPeriodType("DAY");periodRuleParams.setPeriod(7L);periodRuleParams.setExecuteTime(now.format(CommonConstant.SIMPLE_DAY_FORMATTER_OTHER));// 单个周期价格int singlePrice = 100;periodRuleParams.setSingleAmount(Money.ofCent(singlePrice).getYuanString());periodRuleParams.setTotalAmount(Money.ofCent(singlePrice * 36L).getYuanString());periodRuleParams.setTotalPayments(36L);model.setPeriodRuleParams(periodRuleParams);Result<String> signResult = AlipayUtils.userAgreement(model, "http://192.168.0.1:222222/callback/sign/aliNotify", true);}

2.2 主动扣款接口-文档

主动扣款接口其实使用的普通的收单交易接口
注意点有以下几个

  1. SDK方法默认是同步返回扣款结果的,如果需异步通知,需要设置is_async_pay参数。
  2. 支持提前5天发起扣款。
  3. 在一个扣款周期内,只能发起一次扣款,后续扣款会返回40004和无法再次扣款的响应。
  4. 同步返回结果中,只有total_amount字段有值,在周期扣款中,表示实际扣款金额。

2.3 解约接口-文档

解约接口的话,没什么好讲的,就调用就好了。。。。

3.微信周期扣款

截止目前仍没申请下来。。。等申请下来了再补上

4.苹果连续订阅

苹果本身对于内购和连续订阅的购买逻辑是统一的
在用户完成付款后,会立即向IOS客户端返回苹果交易号transaction_id和用户订单凭证receipt。
这时需要客户端将这两个参数加上订单号通知服务端,完成后续付款操作。
但是对于连续订阅的receipt验证与普通订单有些区别,需要额外的苹果专用共享密钥。
以下这个验证方法也是我从别的地方看过来的,再分享一下吧。
这个是苹果官方的订单信息文档

 private static class TrustAnyTrustManager implements X509TrustManager {public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[]{};}}/** 苹果沙盒环境  */private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";/** 正式环境  */private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";/** 专用共享密钥 */private static final String IOS_SHARED_SECRET_PASSWORD = "xxxxxx";/*** @Description: 连续订阅验证回执* @param receipt* @param online* @return: java.lang.String* @Author: lvqiushi* @Date: 2021-04-25*/public static String buyAppVerifyContinuesSub(String receipt, boolean online) {String url = online ? url_verify : url_sandbox;InputStream is = null;try {SSLContext sc = SSLContext.getInstance("SSL");sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());URL console = new URL(url);HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();conn.setSSLSocketFactory(sc.getSocketFactory());conn.setHostnameVerifier(new TrustAnyHostnameVerifier());conn.setRequestMethod("POST");conn.setRequestProperty("content-type", "text/json");conn.setRequestProperty("Proxy-Connection", "Keep-Alive");conn.setDoInput(true);conn.setDoOutput(true);BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());String str = String.format(Locale.CHINA,"{\"receipt-data\":\"" + receipt + "\",\"password\":\"" + IOS_SHARED_SECRET_PASSWORD + "\"}");hurlBufOus.write(str.getBytes());hurlBufOus.flush();is = conn.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(is));String line = null;StringBuffer sb = new StringBuffer();while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();} catch (Exception ex) {log.error("调用苹果服务器,进行验证订单回执异常", ex);} finally {try {is.close();} catch (IOException e) {e.printStackTrace();}}return null;}

拿到苹果的订单信息后,还需要进行订单验证,和续订状态的判断,相关的封装类和方法都已经贴在下面了,可以直接用来使用的

 /*** https://developer.apple.com/documentation/appstorereceipts/responsebody* @description: 苹果服务器 根据回执返回的订单信息* @author: xiucai* @create: 2021-04-29***/@Datapublic static class AppleReceiptOrder {private AppleReceipt receipt;/** latest_receipt_info 包含订阅的所有交易,其中包括初次购买和后续续期,但不包括任何恢复购买  */private List<AppleTradeInfo> latest_receipt_info;/** 最新的Base64编码的应用收据。仅针对包含自动续订的收据返回。  */private String latest_receipt;/*** 0 正常** 21000 未使用HTTP POST请求方法向App Store发送请求。** 21001 此状态代码不再由App Store发送。** 21002 receipt-data属性中的数据格式错误,或者服务遇到了临时问题。再试一次。** 21003 收据无法认证。** 21004 您提供的共享密钥与您帐户的文件共享密钥不匹配。** 21005 收据服务器暂时无法提供收据。再试一次。** 21006 该收据有效,但订阅已过期。当此状态代码返回到您的服务器时,收据数据也会被解码并作为响应的一部分返回。仅针对自动续订的iOS 6样式的交易收据返回。** 21007 该收据来自测试环境,但是已发送到生产环境以进行验证。** 21008 该收据来自生产环境,但是已发送到测试环境以进行验证。** 21009 内部数据访问错误。稍后再试。** 21010 找不到或删除了该用户帐户。*/private Integer status;}@Datapublic static class AppleReceipt {private String receipt_type;private String bundle_id;private String application_version;private String receipt_creation_date;private String receipt_creation_date_ms;private String original_purchase_date;private String original_purchase_date_ms;/** in_app 数组包含非消耗型、非续期订阅,以及用户之前购买的自动续期订阅。根据需要,检查响应中这些 App 内购买项目类型对应的值来验证交易。  */private List<AppleTradeInfo> in_app;}@Datapublic static class AppleTradeInfo {/** 购买的消费品数量  */private String quantity;/** 购买产品的唯一标识符。您可以在App Store Connect中创建产品时提供此值,该值与存储在交易的付款属性中的对象的属性相对应  */private String product_id;/** 交易的唯一标识符,例如购买,还原或续订  */private String transaction_id;/** 原始购买的交易标识符  */private String original_transaction_id;private String purchase_date;/** 对于自动续订订阅,指经过一段时间后,App Store向用户的帐户收取订阅购买或续订费用的时间  */private Long purchase_date_ms;private String original_purchase_date;/** 原始应用购买时间(以UNIX纪元时间格式),以毫秒为单位。使用此时间格式来处理日期。对于自动续订的订阅,此值指示订阅的首次购买日期  */private String original_purchase_date_ms;private String expires_date;/** 订阅到期的时间或续订的时间(以UNIX纪元时间格式),以毫秒为单位  */private Long expires_date_ms;/** 订阅所属的订阅组的标识符  */private String subscription_group_identifier;/** 订阅是否在免费试用期内的指标  */private Boolean is_trial_period;/** 自动续订订阅是否在介绍性价格期内的指标。请参阅以获取更多信息  */private Boolean is_in_intro_offer_period;}/*** @Description: 在苹果订单中,验证是否包含所给订单,如果验证成功,返回苹果订单信息* @param in_app* @param appleProductId* @param transaction_id* @return: boolean* @Author: lvqiushi* @Date: 2021-05-19*/public static AppleTradeInfo judgeTradeSuccess(List<AppleTradeInfo> in_app, String appleProductId, String transaction_id) {if (CollectionUtils.isEmpty(in_app)) {return null;}for (AppleTradeInfo tradeInfo : in_app) {if (tradeInfo.getTransaction_id().equals(transaction_id) && tradeInfo.getProduct_id().equals(appleProductId)) {return tradeInfo;}}return null;}/*** @Description: 判断苹果用户是否有进行过最新一次的订阅* @param latest_receipt_info* @param original_transaction_id* @param lastPayTime* @return: boolean* @Author: lvqiushi* @Date: 2021-04-29*/public static boolean judgeSubscriptSuccess(List<AppleTradeInfo> latest_receipt_info, String original_transaction_id, LocalDateTime lastPayTime) {if (CollectionUtils.isEmpty(latest_receipt_info)) {return false;}long expireTimeMs = lastPayTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();for (AppleTradeInfo tradeInfo : latest_receipt_info) {if (tradeInfo.getOriginal_transaction_id().equals(original_transaction_id)) {// 购买时间大于上期购买时间 则认定为有续订if (tradeInfo.getPurchase_date_ms() > expireTimeMs) {return true;}}}return false;}

唯一需要注意的是,当IOS客户端向苹果发送了确认订单后,订单会从苹果的订单列表消失

连续支付(周期扣款)功能开发及注意事项相关推荐

  1. 后端做app连续会员包月功能 -- IOS连续订阅 支付宝周期扣款

    IOS连续订阅总结 如何判断后续用户是续费 1. 服务端轮询续费表,会员到期的前一天,根据用户id.去苹果服务器检验用户是否续费成功a. 查询的状态应有:等待扣费.扣费失败b. 扣费失败 , 对于扣费 ...

  2. 支付宝周期扣款(支付后签约)业务功能总结(php+golang)

    文档 周期扣款支付后签约场景文档:https://opendocs.alipay.com/open/041bxs 业务流程 1.处理签约成功回调,添加到订阅表 2.定时任务自行请求订阅表,把达到扣款日 ...

  3. 支付宝支付(四):周期扣款-先签约后代扣场景

    目录 一.调用支付宝主动签约接口 二.参数说明 三.查询签约结果接口 四.签约成功,根据签约协议号,发起主动扣款 五.查询扣款结果接口 六.变更签约用户,下次扣款日期 七.取消签约协议接口 一.调用支 ...

  4. 支付宝支付(五):周期扣款-支付后签约场景

    目录 一.调用APP支付接口,拉起支付宝,支付并签约页面 二.业务流程图 三.心声 一.调用APP支付接口,拉起支付宝,支付并签约页面 1.业务代码如下: public AlipayTradeAppP ...

  5. 微信公众号开发,微信支付功能开发(网页JSAPI调用)

    1.微信支付的流程 如下三张手机截图,我们在微信网页端看到的支付,表面上看到的是 "点击支付按钮 - 弹出支付框 - 支付成功后出现提示页面",实际上的核心处理过程是: 点击支付按 ...

  6. 支付宝小程序唤起签约并支付(周期扣款)

    目录 获取支付参数拼接成的字符串 支付宝小程序内唤起签约并支付页面 获取支付参数拼接成的字符串 调用alipay.trade.app.pay接口,获取orderStr,具体参数如下: 公共请求参数 参 ...

  7. SpringBoot对接微信小程序支付功能开发(一,下单功能)

    1,接入前准备: 接入模式选择直连模式: 申请小程序,得到APPID,并开通微信支付: 申请微信商户号,得到mchid,并绑定APPID: 配置商户API key,下载并配置商户证书,根据微信官方文档 ...

  8. SpringBoot对接微信小程序支付功能开发(二,支付回调功能)

    接着上一篇: SpringBoot对接微信小程序支付功能开发(一,下单功能) 在上一篇下单功能中我们有传支付结果回调地址. 下面是回调接口实现 package com.office.miniapp.c ...

  9. 银行金融 智能业务对话机器人 全生命周期、功能实现及二次开发

    第92课:图解Rasa对话机器人项目实战之银行金融Financial Bot架构视角下的Training及Reference全生命周期.功能实现.及产品的二次开发 1,Rasa 3.X中Graph A ...

最新文章

  1. Python使用matplotlib可视化面积图(Area Chart)、通过给坐标轴和曲线之间的区域着色可视化面积图、在面积图的指定区域添加箭头和数值标签
  2. php中文网企业网站,闻名 PHP企业网站系统 weenCompany v5.3.0 简体中文 UTF8
  3. 嵌入式 说明书 软件著作权_软件著作权详细解读
  4. 前端项目难点及解决方法_预埋件施工重点难点的解决方法
  5. Qt 给应用程序添加图标
  6. linux基础命令下载,Linux基础命令教程豪华版
  7. C# Types Type Members
  8. 指尖初体验之主屏幕操作
  9. 2017.9.8 字符串 失败总结
  10. Python基础学习笔记(一)python发展史与优缺点,岗位与薪资
  11. 「日常温习」Hungary算法解决二分图相关问题
  12. 190116每日一句
  13. MySQL变量/参数的查看与设置
  14. 关于TI杯全国大学生电子设计竞赛
  15. 主成分与因子分析异同_主成分和因子分析原理及比较
  16. 职场中职员如何向上管理?
  17. 163888一个普通程序员写给COO李大学的一封Mail(不是转载)
  18. stm32PWM精确控制脉冲个数
  19. 雨润莲心:同幽梦、共红尘
  20. 快播转型,用户且用切珍惜

热门文章

  1. 电脑与树莓派与stm32f4串口通信
  2. Google云计算之Dremel
  3. caffe.bin caffe的框架
  4. linux系统资源查看详解
  5. C++面试知识总结-C++基础知识
  6. 时间加减计算器_FRM计算器使用流程你知道吗?
  7. win8计算机无法安装打印机驱动程序,电脑打印机无法安装驱动怎么办?如何安装驱动?...
  8. UE5中置人利用iphone驱动虚拟人面部
  9. 报告|中国智能音箱已入局全球市场,双重商业模式迅速扩张
  10. 若依如何手动修改项目包路径呢?