京东小程序订单对接经验和订单支付架构设计

1,380 阅读7分钟

对接了这么多三方文档,腾讯的是当之无愧的写的最清楚、最详细,牛逼到什么程度呢?

  1. 如果看不懂文档,那是你自己的问题,没理解业务场景,腾讯看似伞兵的设计,对接到后期都会理解。
  2. 文档细致到可以不调接口,就看文档写代码,最后调试的结果大差不差的。

相比较起来,京东的文档差的就不是一点半点。

前端的、后端的、运营的各种文档裹在一起,技术文档还有不同的几套,有的接口每套对应的参数、含义解释还不一样。光是用户 id 就分为京东 pin、openId、xId,回调也挺混乱的,有的场景要自己主动查询,创建接口也不统一。能嗅到屎山堆积的味道。

所以我把这些天自己趟过的浑水整理一下,方便后续小伙伴少走点弯路。

我对接的业务场景是京东小程序下单,包含退款和回调,使用京东订单作为支付,不对接京东收款,开通的功能如下。

image.png

前期业务配置

首先要创建店铺,还要把店铺的品给京东那边审批,注意是 sku,审批通过后下单接口才能正常调用。

获取用户信息(头像、昵称、手机号)

获取用户手机号流程需要前后端配合。

  1. 前端经过一次普通授权,获取用户头像、昵称。信息在前端,后端没有接口调用。
  2. 前端经过二次手机号授权,获取 code。
  3. 后端使用 code 换取京东 accesstoken 和京东用户 xid。(京东用户 id 很多,xid 是最新的)
  4. 后端使用 accesstoken 换取手机号密文。
  5. 后端通过手机号密文,换取手机号明文。

京东的 code 是一次性的,换取 accesstoken 后失效,京东 accesstoken 有有效期,但是不用存储,除非要持续调用京东需要 accesstoken 的接口。京东的用户 xid 要存,后续调订单相关接口都会用到。

整个流程就是这样,获取到明文手机号后,就可以和自己库里的手机号对比,做业务逻辑。

京东 code 换取 accesstoken 的代码,只能用 http 调。

// https://mp-docs.jd.com/doc/extendAbility/ability/2073#heading-2
// 通过 code 获取 access_token
public JdAccessTokenResDto codeGetToken(String code) {
    String JD_URL = StrUtil.format("https://open-oauth.jd.com/oauth2/access_token?app_key={}&app_secret={}&", jdAppKey, jdAppSecret);
    String url = JD_URL + "grant_type=authorization_code&code=" + code;
    return restTemplate.getForObject(url, JdAccessTokenResDto.class);
}

京东 accesstoken 换取手机号明文代码,可以用 SDK 调用。

// https://mp-docs.jd.com/doc/extendAbility/ability/1808#heading-0
// 获取用户手机号 获取加密手机号并且解密
@SneakyThrows
public String getMobileByToken(String accessToken) {
    JdClient client = new DefaultJdClient(jdApiUrl, accessToken, jdAppKey, jdAppSecret);
    JdaGetMobileByTokenRequest request = new JdaGetMobileByTokenRequest();
    request.setAppId(jdAppKey);
    JdaGetMobileByTokenResponse execute = client.execute(request);
    TDEClient tdeClient = TDEClient.getInstance(jdApiUrl, accessToken, jdAppKey, jdAppSecret);
    //判断是否为加密手机号码数据
    String mobile = Optional.ofNullable(execute).map(JdaGetMobileByTokenResponse::getReturnType).map(JsonResult::getData).map(MobileScopeVO::getMobile).orElse(null);
    if (tdeClient.isEncryptData(mobile)) {
        //解密手机号
        String plaintext = tdeClient.decryptString(mobile);
        return plaintext;
    } else {
        return null;
    }
}

说到 SDK,京东的 SDK 在控制台下载,下载的时候根据具备的权限实时生成的,如果没有对应的方法和 Dto,去确认下权限是否申请到了,而且有时候下载的不全,自己确认下。

下单接口

这里我踩了坑,先把京东文档列一下,在控制台和其他地方也有文档,但后端这里是最准的。宙斯开发者中心

京东小程序下单分一单一(jingdong.miniapp.order.created(小程序内单创建订单))品和一单多品(jingdong.submitMultiSkuOrder(小程序通用下单的一单多品接口))。品指的是上面配的 sku。

我的业务场景一个订单只需要一个品就行了,比如用户买了 45 块钱苹果,或者 56 块钱梨,不会同时买苹果和梨。

所以我用的一单一品接口,结果联调的时候发现,到京东的订单,我固定传苹果,京东随机生成苹果和梨的订单。

和京东沟通后发现,原来京东一单一品的接口,品是随机的...就挺无语的,那下单还要传品名称做什么呢。

后来改成一单多品,传品的 sku 后问题解决。

// https://jos.jd.com/apilistnewdetail?apiGroupId=145&apiId=18304&apiName=null
// jingdong.submitMultiSkuOrder(小程序通用下单的一单多品接口)
@SneakyThrows
public SubmitMultiSkuOrderResponse submitMultiSkuOrder(String xidBuyer, Long totalFee, String sku, String trackerId) {
    JdClient client = new DefaultJdClient(jdApiUrl, null, jdAppKey, jdAppSecret);
    SubmitMultiSkuOrderRequest request = new SubmitMultiSkuOrderRequest();
    request.setAmount(String.valueOf(totalFee));
    request.setQuantity("1");
    request.setSku(sku);
    request.setCallBackUrl(jdCallBackUrl);
    request.setTrackerId(trackerId);
    request.setTotalFee(totalFee);
    request.setXidBuyer(xidBuyer);
    SubmitMultiSkuOrderResponse response = client.execute(request);
    log.info("submitMultiSkuOrder request:{},response: {}", JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
    return response;
}

取消订单

京东下单后支持取消订单,不主动取消 30 分钟后自动取消,但是没有回调。

取消订单有时候会失败,但没有失败原因...额,想不通为什么,而且没回调有点奇怪,我还得自己写逻辑查询。

// https://jos.jd.com/apilistnewdetail?apiGroupId=145&apiId=18304&apiName=null
// jingdong.miniapp.order.cancel(小程序内单取消订单)
@SneakyThrows
public MiniappOrderCancelResponse miniappOrderCancel(String xidBuyer, Long orderId) {
    JdClient client = new DefaultJdClient(jdApiUrl, null, jdAppKey, jdAppSecret);
    MiniappOrderCancelRequest request = new MiniappOrderCancelRequest();
    request.setXidBuyer(xidBuyer);
    request.setOrderId(orderId);
    MiniappOrderCancelResponse response = client.execute(request);
    log.info("miniappOrderCancel request:{},response: {}", JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
    return response;
}

查询订单

这个还行,就是各种接口的返回值、属性名不统一,感觉不是一个团队写的东西,有点割裂。

// https://jos.jd.com/apilistnewdetail?apiGroupId=145&apiId=18304&apiName=null
// jingdong.miniapp.order.orderInfo(小程序订详)
@SneakyThrows
public MiniappOrderOrderInfoResponse miniappOrderOrderInfo(String xidBuyer, Long orderId) {
    JdClient client = new DefaultJdClient(jdApiUrl, null, jdAppKey, jdAppSecret);
    MiniappOrderOrderInfoRequest request = new MiniappOrderOrderInfoRequest();
    request.setXidBuyer(xidBuyer);
    request.setOrderId(orderId);
    MiniappOrderOrderInfoResponse response = client.execute(request);
    log.info("miniappOrderOrderInfo request:{},response: {}", JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
    return response;
}

申请退款

京东的退款要传业务退款单号,但不返回京东的退款单号。像创建订单就返回了京东的订单号。

京东的退款单号只在退款回调里,且退款成功时才有,如果要考虑业务以后和京东对账,这里要注意下,我的处理是如果京东有退款单号,就入库,再处理后面的逻辑。

// 如果退款回调有京东退款单号
int jdRefundId = orderRefundCallbackDto.getJdRefundId();
Integer statusDO = shopOrderRefundDO.getStatus();
if (ObjectUtil.isNotEmpty(jdRefundId)) {
    shopOrderRefundDO.setOutRefundOrderNo(String.valueOf(jdRefundId));
    shopOrderRefundDO.setUpdateAt(new Date());
    boolean update = shopOrderRefundService.updateByIdAndStatus(shopOrderRefundDO, statusDO);
    log.info("京东退款回调更新退款单号,shopOrderRefundDO:{},update:{}", shopOrderRefundDO, update);
    if (!update) {
        asyncEventBus.post(FlyEventListener.FlyEventDto.builder()
                .url(JdConstant.JD_ORDER_FLY_ALARM)
                .message(StrUtil.format("京东退款回调更新退款单号失败:" + JSONUtil.toJsonStr(statusDO)))
                .build());
        throw new BizException("京东退款回调更新退款单号失败:" + JSONUtil.toJsonStr(statusDO));
    }
}
// https://jos.jd.com/apilistnewdetail?apiGroupId=145&apiId=18304&apiName=null
// jingdong.miniapp.refund(小程序退款服务)
@SneakyThrows
public MiniappRefundResponse miniappRefund(String xidBuyer, Long orderId, String refundUuid, Long refundAmount) {
    JdClient client = new DefaultJdClient(jdApiUrl, null, jdAppKey, jdAppSecret);
    MiniappRefundRequest request = new MiniappRefundRequest();
    request.setXidBuyer(xidBuyer);
    request.setRefundUuid(refundUuid);
    request.setIsHalfRefund(true);
    request.setOrderId(orderId);
    request.setRefundAmount(refundAmount);
    MiniappRefundResponse response = client.execute(request);
    log.info("miniappRefund request:{},response: {}", JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
    return response;
}

查询退款详情

// https://jos.jd.com/apilistnewdetail?apiGroupId=145&apiId=18304&apiName=null
// jingdong.miniapp.refundQuery(京东小程序退款查询接口)
@SneakyThrows
public MiniappRefundQueryResponse miniappRefundQuery(Long orderId, String refundUuid) {
    JdClient client = new DefaultJdClient(jdApiUrl, null, jdAppKey, jdAppSecret);
    MiniappRefundQueryRequest request = new MiniappRefundQueryRequest();
    request.setRefundUuid(refundUuid);
    request.setOrderId(orderId);
    MiniappRefundQueryResponse response = client.execute(request);
    log.info("miniappRefundQuery request:{},response: {}", JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
    return response;
}

ok,以上是和京东相关的交互接口,再说说我在项目架构上的处理。

业务的支付分两种,一种是调用支付宝、微信支付这种,真正的支付;一种是使用三方订单做支付,根据订单的状态判断支付结果,我写的系统负责第二种。

我在设计的时候做了以下处理

使用 Kafka 接受三方回调

为了防止瞬间大量回调把服务打挂,在回调里只做验签,验签成功后把内容丢进 Kafka,在 consumer 做真正的业务处理。

@Operation(description = "京东回调接口")
@RequestMapping("/callback")
public Map<String, Object> callback(@RequestParam Map<String, String> getParam, HttpServletRequest request) {
    log.info("接收参数为:{}", JSON.toJSONString(getParam));
    String body = checkSignAndGetBody(getParam, request);
    jdCallKafkaProducer.send(new JdCallbackKafkaDto(body));

    // 京东要求的响应格式
    // 创建主 Map
    Map<String, Object> jsonObject = new HashMap<>();
    // 添加键值对
    jsonObject.put("sub_code", 0);
    jsonObject.put("sub_message", "success_test");
    // 添加空的子结果 Map
    Map<String, Object> subResult = new HashMap<>();
    jsonObject.put("sub_result", subResult);
    return jsonObject;
}

每次调用三方接口后,异步同步订单、退款单信息

由于京东没有取消订单回调,也为了防止回调不及时,在每次调用京东接口后,我会在 finally 中发一个异步回调,在异步回调里同步京东状态。

public Rs successWithRs() {
    try {
        // 业务处理
    } finally {
        asyncEventBus.post(JdOrderRefreshEventListener.EventData.builder().orderNo(orderNo).build());
    }
}
    // 刷新订单
    refreshJdOrder(shopOrderDO);

    // 刷新退款单
    String payOrderNo = shopOrderDO.getOrderNo();
    List<ShopOrderRefund> orderRefundList = shopOrderRefundService.listByPayOrderNo(payOrderNo);
    if (ObjectUtil.isNotEmpty(orderRefundList)) {
        for (ShopOrderRefund orderRefund : orderRefundList) {
            refreshJdOrderRefund(shopOrderDO, orderRefund);
        }
    }
    return Rs.success();

未来扩展性考虑

使用订单做支付,订单状态通常不会很复杂,但我还没接第二家,订单操作没有统一封装,未来比如淘宝的订单支付接入,我准备重新写一套对应的接口,和京东的分开。

回调也是,不同的平台回调分开。

其实这在业务场景上也是分开,不太可能一个订单又可以京东又可以淘宝,假设未来真有这种场景,那我基于这两套接口上面再做一层封装,提供统一的订单操作接口。

接收参数时使用 @JsonTypeInfo 和 @JsonSubTypes 动态赋值,执行时用策略模式分别处理。