最近有一个版本需求,需要接入周期扣款做连续会员的功能,没想到这一做就是小半个月,趟了很多坑,所以觉得有必要记录一下
在支付宝和微信中(非苹果支付),周期扣款的流程主要有以下两种,并且各有利弊
由于签约和扣款的一致性问题开发努努力还能保证一下,所以我目前选用的是先签约-再扣款流程,也推荐选用。
苹果连续订阅服务
总结一句话–对服务端来说都是满满的恶意。
首先用户付款连续订阅成功了,服务端收不到苹果的任何通知,只有IOS客户端才能知道用户订阅成功了。
其次,用户订阅期间内每次扣款,服务端也无法进行感知,必须要不断的轮询用户的订单列表才能进行判断(IOS客户端都有办法能知道用户续订了)。
首先贴上官方文档地址-周期扣款
和支付宝对接还是相对轻松点的,毕竟技术支持还是比较尽心尽力的。
在先签约再扣款流程中,主要有用到以下几个接口
签约接口有几个坑,在这里给大家排一下雷
以下是签约接口调用的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);
}
主动扣款接口其实使用的普通的收单交易接口
注意点有以下几个
解约接口的话,没什么好讲的,就调用就好了。。。。
截止目前仍没申请下来。。。等申请下来了再补上
苹果本身对于内购和连续订阅的购买逻辑是统一的
在用户完成付款后,会立即向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
*
**/
@Data
public 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;
}
@Data
public 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;
}
@Data
public 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客户端向苹果发送了确认订单后,订单会从苹果的订单列表消失