Skip to content

自动续订

概述

为助力开发者高效运营微应用付费业务,简化周期性付费场景的开发与管理成本,我们特开放自动订阅续费核心功能,供微应用开发者快速集成使用。

流程概述

自动续订流程图

前端实现

支付示例代码

javascript
// 唤起支付页面,在用户输入支付密码后支付完成
const payRes = await mos.subscribe({
   subscribeAmount: "16.99",
   currency: "USD",
   billingCycle: "CONTINUOUS_MONTHLY_SUBSCRIPTION",
   billingCycleDesc: "连续包月会员",
   notifyUrl: "https://yourdomain.com/mos/pay/callback",
   extData: "your bisiness data",
});

const { result } = payRes;
if (result === "SUCCESS") {
  // 支付完成后的业务逻辑,如跳转到支付完成页面
}

后端实现

java

import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.Map;
import java.util.TreeMap;

@Slf4j
@Component
public class MiniAppPayService {

    /**
     * Mos微应用的调用地址
     */
    @Value("${mos.mini-app.base-url}")
    private String baseUrl;
    /**
     * Mos微应用的appKey
     */
    @Value("${mos.mini-app.app-key}")
    private String appKey;
    /**
     * Mos微应用的appSecret
     */
    @Value("${mos.mini-app.app-secret}")
    private String appSecret;

    private final RestTemplate restTemplate;

    public MiniAppPayService() {
        SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
        httpRequestFactory.setConnectTimeout(30000);
        httpRequestFactory.setReadTimeout(60000);
        this.restTemplate = new RestTemplate(httpRequestFactory);
    }

    /**
     * 请求Mos服务端微应用开放接口
     *
     * @param uri          接口URI
     * @param ao           请求参数
     * @param clazz 响应类型
     * @return 响应对象
     */
    private <T> T call(String uri, BaseSignedAo ao, Class<T> clazz) {
        // 签名
        ao.setAppKey(this.appKey);

        Map<String, String> data = JSON.parseObject(JSON.toJSONString(ao), new TypeReference<Map<String, String>>() {
        });
        String sign = MiniAppUtil.generateSignature(data, this.appSecret);
        ao.setSign(sign);

        // 发起请求
        String url = this.baseUrl + uri;
        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, ao, String.class);
        if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) {
            log.error("微应用请求异常 uri={} ao={}", uri, JSON.toJSONString(ao));
            return null;
        }

        // 解析响应 code(int) message(String) data(T)
        JSONObject result = JSON.parseObject(responseEntity.getBody());
        if ((int)result.get("code") != 200) {
            log.error("微应用请求异常 uri={} ao={} result={}", uri, JSON.toJSONString(ao), JSON.toJSONString(result));
            return null;
        }
        return JSON.to(clazz, result.get("data"));
    }

   public CancelAutoSubscriptionVo cancelAutoSubscription(CancelAutoSubscriptionAo ao) {
      return this.call("/open-apis/mp/v1/pay/cancelAutoSubscription", ao, CancelAutoSubscriptionVo.class);
   }

   public AutoSubscriptionQueryVo autoSubscriptionQuery(AutoSubscriptionQueryAo ao) {
      return this.call("/open-apis/mp/v1/pay/autoSubscriptionQuery", ao, AutoSubscriptionQueryVo.class);
   }
   

    @Data
    public static class BaseSignedAo {

        /**
         * 微应用 appKey
         */
        @NotNull
        private String appKey;

        /**
         * 签名
         */
        @NotNull
        private String sign;
    }

   @Data
   @EqualsAndHashCode(callSuper = true)
   public class CancelAutoSubscriptionAo extends BaseSignedBo {

      /**
       * 32位随机字符串,需保证系统内唯一
       */
      @NotNull
      private String nonceStr;

      /**
       * 商户微应用系统订单号
       */
      @NotNull
      private String outTradeNo;

   }

   @Data
   public class CancelAutoSubscriptionVo {

      /**
       * 商户微应用系统订单号
       */
      private String outTradeNo;

   }

   @Data
   @EqualsAndHashCode(callSuper = true)
   public class AutoSubscriptionQueryAo extends BaseSignedAo {

      /**
       * 32位随机字符串,需保证系统内唯一
       */
      @NotNull
      private String nonceStr;

      /**
       * 商户微应用系统订单号
       */
      @NotNull
      private String outTradeNo;
   }

   @Data
   public class AutoSubscriptionQueryVo {


      /**
       * 商户微应用系统订单号
       */
      private String outTradeNo;

      /**
       * 计费周期
       */
      private String billingCycle;

      /**
       * 状态
       */
      private String status;

      /**
       * 开通时间
       */
      private Date startTime;

      /**
       * 货币类型
       */
      private String currency;

      /**
       * 订阅价格
       */
      private BigDecimal subscribeAmount;

      /**
       * 支付时间
       */
      private Date payTime;

      /**
       * 生效结束时间
       */
      private Date effectiveEndTime;
   }

    public static class MiniAppUtil {

        /**
         * 生成签名
         */
        public static String generateSignature(Map<String, String> data, String appSecret) {
            // 1. Sort the parameters alphabetically
            Map<String, String> sortedData = new TreeMap<>(data);

            // 2. Concatenate the parameters in the format key1=value1&key2=value2...
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : sortedData.entrySet()) {
                if ("sign".equals(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
                    // sign 不参与签名 && 空值不参与签名
                    continue;
                }
                sb.append(entry.getKey()).append("=").append(entry.getValue().trim()).append("&");
            }
            sb.append("secret=").append(appSecret);

            // 3. Hash the concatenated string using MD5
            return new MD5().digestHex(sb.toString());
        }

    }
}

微应用后端服务的公共模块提供了通用的登录方法,及其相应的鉴权拦截器和和获取用户登录的方法,详见《微应用后端开发》章节

支付回调

回调描述

用户续订,当用户成功支付或后续自动续订成功后,mos会通过POST的请求方式,向预先设置的回调地址发送回调通知,让商户知晓用户已完成续订支付。

回调处理步骤

  1. 商户接收回调通知报文 mos支付会通过POST的方式向回调地址发送回调报文,回调通知的请求主体中会包含JSON格式的通知参数,具体的通知参数如下:
java
{
    "appKey": "58e5a50ad0b243408c77622d696ff76f",
    "mcId": "1719971391999086594",
    "openid": "531d4fc958c9cbe5e5e719013b779c0b",
    "country": "KH",
    "currency": "USD",
    "totalAmount": "14.50",
    "outTradeNo": "692751186858053",
    "extData": "business ext data",
    "timeEnd": "1751265114241",
    "resultCode": "SUCCESS",
    "resultMsg": "OK",
    "sign": "bcf0b5093e67ba0a9af4b5aaa57f2998"
}

参数说明如下:

属性类型必填说明
appKeyString微应用appKey,可到微应用控制台获取
mcIdString支付商户id,微应用提前申请
openidString下单用户openid
countryString订单所属国家编码
currencyString币种
totalAmountString订单总金额
outTradeNoString商户微应用系统订单号
extDataString外部业务字段
timeEndString支付完成的时间,UTC时间戳
resultCodeString订单状态码:SUCCESS-成功,FAIL-失败
resultMsgString成功或失败原因
signString签名,用于客户端验证接口有效性。签名生成方法见前面工具类

2. 回调验签

商户接收到回调通知报文后,需在5秒内完成对报文的验签,并应答回调通知。 对回调通知进行验签,以保证该次接口回调的有效性,具体验签方法见前面工具类。

3. 对回调通知进行应答

商户验签后,根据验签结果对回调进行应答,以json格式返回处理结果。

成功范例:

json
{
   "returnCode": "SUCCESS",
   "returnMsg": "OK"
}

失败范例:

json
{
   "returnCode": "FAIL",
   "returnMsg": "Internal server error"
}

4. mos支付回调处理机制说明

mos支付接收到商户的应答后,会根据应答结果做对应的逻辑处理:

  • 若商户应答回调接收成功,mos支付将不再重复发送该回调通知。若因网络或其他原因,商户收到了重复的回调通知,请按正常业务流程进行处理并应答。
  • 若商户应答回调接收失败,或超时(5s)未应答时,mos支付会按照(15s/30s/3m/10m/20m/30m/60m/3h/6h)的频次重复发送回调通知,直至mos支付接收到商户应答成功,或达到最大发送次数(9次)