对接了这么多三方文档,腾讯的是当之无愧的写的最清楚、最详细,牛逼到什么程度呢?
- 如果看不懂文档,那是你自己的问题,没理解业务场景,腾讯看似伞兵的设计,对接到后期都会理解。
- 文档细致到可以不调接口,就看文档写代码,最后调试的结果大差不差的。
相比较起来,京东的文档差的就不是一点半点。
前端的、后端的、运营的各种文档裹在一起,技术文档还有不同的几套,有的接口每套对应的参数、含义解释还不一样。光是用户 id 就分为京东 pin、openId、xId,回调也挺混乱的,有的场景要自己主动查询,创建接口也不统一。能嗅到屎山堆积的味道。
所以我把这些天自己趟过的浑水整理一下,方便后续小伙伴少走点弯路。
我对接的业务场景是京东小程序下单,包含退款和回调,使用京东订单作为支付,不对接京东收款,开通的功能如下。
前期业务配置
首先要创建店铺,还要把店铺的品给京东那边审批,注意是 sku,审批通过后下单接口才能正常调用。
获取用户信息(头像、昵称、手机号)
获取用户手机号流程需要前后端配合。
- 前端经过一次普通授权,获取用户头像、昵称。信息在前端,后端没有接口调用。
- 前端经过二次手机号授权,获取 code。
- 后端使用 code 换取京东 accesstoken 和京东用户 xid。(京东用户 id 很多,xid 是最新的)
- 后端使用 accesstoken 换取手机号密文。
- 后端通过手机号密文,换取手机号明文。
京东的 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 动态赋值,执行时用策略模式分别处理。