到家侧接口开发规范

一、调用介绍

O2O作为京东的数据出口,用户只需要按照O2O的规范拼装一个正确的URL,通过https请求到O2O,即能获取到所需的数据。

二、API调用注意事项

• 所有的API请求和响应数据编码皆为UTF-8格式(切勿转成unicode编码),如采用Get方式请求,URL里的所有参数值在发送请求时请做UrlEncoder处理。

API请求的应用级参数需要转换成json格式,例如:jd_param_json={"return_id":"20032","trade_no":"20032"}

• json格式应为标准的json

• 目前只支持返回结果是json格式

• 所有API请求和响应内的日期格式都为yyyy-MM-dd HH:mm:ss,注意小时格式是24小时制,例如:2011-07-21 20:23:30

• API接口的错误信息在http response body内

• 签名方式为 md5(appsecret + key+ value .... key + value+appsecret)然后转大写字母,其中key、value对是除签名所有请求参数按key做的升序排列,value无需编码。

•关于订单查询,推荐 每次返回少量结果,然后多次按页查询。 在获取订单查询结果后,建议优先进行订单排重处理。

•查询开放平台接口触发限频场景,建议开发者自行实现重试补偿,以免因触发限流影响业务流程。

三、调用环境入口

环境入口释义:调用API时,需要传入如下地址,获取相应环境下的数据。

调用沙箱环境API入口地址:https://openapi.jddj.com/mockapi/(查看详情

调用正式环境API入口地址:https://openapi.jddj.com/djapi/(查看详情

四、调用参数

调用API时,必须传入系统级参数和对应的应用级参数。各个API的输入参数和返回结果详见API文档。

①系统级参数

字段

类型

是否必须

描述

token

String

采用OAuth授权方式为必填参数。

app_key

String

应用的app_key

sign

String

签名

timestamp

String

时间戳,格式为yyyy-MM-dd HH:mm:ss,例如:2011-06-16 13:23:30。每次请求时间戳要求秒级不同

format

String

暂时只支持json

v

String

API协议版本,可选值:1.0.


②应用级参数(以通过京东SKUID修改门店价格 API为例)

字段 类型 是否必须 示例值 描述
skuId Long
京东SKU编号。
stationNo String
京东门店编号。
price Long
价格(单位:分)。
marketPrice Long
市场价(单位:分)。


③如下为O2O应用级参数拼装(拼装示例 ,仅供参考)

字段

描述

示例值

jd_param_json

String

标准json 类型

{
"skuId": "123456789",
"stationNo": "135792468",
"price": "20",
"marketPrice": "20"
}


关于uuid字段的说明

在常规接口的调用中,返回的msg字段中会带有uuid的值,这个值唯一标识一次请求,可以帮助到家侧快速定位请求和解决问题

五、API签名

签名测试工具 

调用API时需要对请求参数进行签名,O2O服务器端会验证请求参数是否合法。

加密规则

① 所有请求参数按照字母先后顺序排列

例如:将token,app_key,timestamp,format,v,jd_param_json{} 排序为app_key,format,jd_param_json{},timestamp,token,v

② 把所有参数名和参数值进行拼装

例如:app_keyxxxformatxxxxjd_param_json{xxxx}timestampxxxxxxtokenxxxvx

③ 把appSecret夹在字符串的两端

例如:appSecret+XXXX+appSecret

使用MD5进行加密,再转化成大写

参考示例代码

示例(以下示例只体现逻辑)

调用https://openapi.jddj.com/djapi/ ,假设app_key=7fd1c34598924181b3ba295b41c63507、appSecret=a7182e7f06274e4ebcbb0c64213fcfa7、token=2f3da4db-a0d4-40a8-bf4e-22007b5603d5

1) 输入参数

token=2f3da4db-a0d4-40a8-bf4e-22007b5603d5
app_key=7fd1c34598924181b3ba295b41c63507
timestamp=2016-08-08  12:00:00
format=json
v=1.0
jd_param_json=
{
"skuId":"123456789",
"stationNo":"135792468",
"price": "20",
"marketPrice":"20"
}

2) 把参数按照字母顺序排列

app_key=7fd1c34598924181b3ba295b41c63507
format=json
jd_param_json=
{
"marketPrice":"20"
"price": "20",
"skuId":"123456789",
"stationNo":"135792468"
}
timestamp=2016-08-08 12:00:00
token=2f3da4db-a0d4-40a8-bf4e-22007b5603d5
v=1.0

3) 连接参数名与参数值,并在首尾加上appSecret

授权接口:(示例)

a7182e7f06274e4ebcbb0c64213fcfa7app_key7fd1c34598924181b3ba295b41c63507formatjsonjd_param_json{"marketPrice":"20","price":"20","skuId":"123456789","stationNo":"135792468"

}timestamp2016-08-08 12:00:00token2f3da4db-a0d4-40a8-bf4e-22007b5603d5v1.0a7182e7f06274e4ebcbb0c64213fcfa7

注:

①以上字符串不允许有跨行,粘贴时请特别注意,除日期和时间中间的空格之外,不允许在其它位置出现空格。

②时间戳 timestamp最好就填写当前时间的前几分钟(必须在6分钟之内)


4) MD5加密后转成大写:08D99B718B35A0A98B07B2271ABB87F1


六、拼装https请求

调用API时,将所有的参数转换成UTF-8编码,然后进行拼装,通过浏览器访问该地址即可成功调取一次接口。

[使用Get方式请求]

    授权API 沙箱调用URL示例(仅为示例):

    https://openapi.jddj.com/mockapi/order/finish?jd_param_json={"marketPrice":"20","price":"20","skuId":"123456789","stationNo":"135792468"}&token=12345678-b0e1-4d0c-9d10-a998d9597d75 &app_key=58830cc993ce426d9896b23670810653&timestamp=2016-08-08 12:00:00&v=1.0&format=json&sign=08D99B718B35A0A98B07B2271ABB87F1

    授权API 正式调用URL示例(仅为示例):

    https://openapi.jddj.com/djapi/order/finish?jd_param_json={"marketPrice":"20","price":"20","skuId":"123456789","stationNo":"135792468"}&token=12345678-b0e1-4d0c-9d10-a998d9597d75 &app_key=58830cc993ce426d9896b23670810653&timestamp=2016-08-08 12:00:00&v=1.0&format=json&sign=08D99B718B35A0A98B07B2271ABB87F1

    若拼装后的请求URL长度小于1024个字符,可以使用Get方式请求。否则,必须用Post方式请求,平台所有API均支持Post方式请求。

[使用Post方式请求]

    根据不同的开发语言实现Post表单方式请求(Content-Type应该是application/x-www-form-urlencoded),请求时参数须以名值对(name/value)形式传递,具体代码实现可参考“SDK下载”链接中的SDK源码。

七、数据加密规则

解密测试工具 

1、加密算法采取AES(AES/CBC/NoPadding,即AES-128-CBC加密的NoPadding模式)算法。返回结果增加新字段encryptData,此字段是对原字段data的整体加密,加密的接口在API文档列表上会打上密标
2、数据解析规则:

1)、对于无需加密的接口,数据内容不变

2)、对于需要加密的接口,明文密文并行期,两个字段data(明文),encryptData(密文)均有值

3)、并行期过后,仅encryptData(密文)有值

4)、具体处理逻辑(参考):

所有接口,优先判断密文字段encryptData内容是否为空,

如果为空,仍然解析明文字段data中的业务数据

如果不为空的,获取密文字段encryptData内容并解密,解密后获取业务数据(解密后的数据等效于明文字段data中的数据)

一定不要将需要解密的接口限制到一个固定范围,只针对现有加密接口进行解密。请参考以上逻辑,一旦有非加密接口变成加密接口时,仍然能够正常解析

5)、AES算法需要的key和偏移量采用appSecret的前16位和后16位

以java版SDK为例,使用方法

String key = appSecret.substring(0, 16);
String iv = appSecret.substring(16, 32);
String json = AESUtils.decryptAES(encryptData, key, iv);

3、参考示例(可用于验证算法):

密文:8FvHJcQmVojAIU61SNaS1ermHN2UVWknueRHFSNf2q5EbxNNmznoTYpRu7ySc/8CuU+QGZ9UIBMCyTuFafY3PuszEokEKc8M1Qfv/+o15h5bIU8LXfwRKOCm3JYzZtTOvJVU0hk/USvtDgraToszFl2hQZjZN5gGH1af0X8vopo=
key:0bcbe9d6e6124cf2
iv:aef2856a540f1326
对应明文:{"billId":"232219501234567","outBillId":"12345678901","statusId":"150","storeId":"11912345","timestamp":"2022-08-14 17:24:44"}

4、具体示例代码(JAVA):

import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @Classname AESUtils
 */
public class AESUtils {

    /**
     * @Description AES算法加密明文,加密用的Key 可以用26个字母和数字组成 使用AES-128-CBC加密模式,key需要为16位。
     * key 密钥,长度16
     * iv 偏移量,长度16
     * @param data 明文
     * @return 密文
     */
    public static String encryptAES(String data,String key,String iv) throws Exception {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            int blockSize = cipher.getBlockSize();
            byte[] dataBytes = data.getBytes();
            int plaintextLength = dataBytes.length;

            if (plaintextLength % blockSize != 0) {
                plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
            }

            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);

            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());  // CBC模式,需要一个向量iv,可增加加密算法的强度

            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
            byte[] encrypted = cipher.doFinal(plaintext);

            return AESUtils.encode(encrypted).trim(); // BASE64做转码。

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @Description AES算法解密密文
     * @param data 密文
     * @return 明文
     */
    public static String decryptAES(String data,String key,String iv) throws Exception {
        try
        {
            byte[] encrypted1 = AESUtils.decode(data);//先用base64解密

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());

            cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);

            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString.trim();
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 编码
     * @param byteArray
     * @return
     */
    public static String encode(byte[] byteArray) {
        return new String(new Base64().encode(byteArray));
    }

    /**
     * 解码
     * @param base64EncodedString
     * @return
     */
    public static byte[] decode(String base64EncodedString) {
        return new Base64().decode(base64EncodedString);
    }

}

				    

八、签名示例代码

JAVA示例
函数 作用
concatParams 将输入参数排序并拼接
getSign 通过给定的param,appSecret计算签名
getMD5String 对字符串MD5加密
import java.util.Arrays;
import java.util.Map;

public class JdHelper {

	private static String concatParams(Map<String, String> params2) {
		Object[] key_arr = params2.keySet().toArray();
		Arrays.sort(key_arr);
		StringBuilder str = new StringBuilder();
		for (Object key : key_arr) {
			String val = params2.get(key);
			str.append(key).append(val);
		}
		return str.toString();
	}

	public static String getSign(Map<String,String> param,String appSecret ) throws Exception{
		String sysStr=concatParams(param);
		StringBuilder resultStr=new StringBuilder();
		resultStr.append(appSecret).append(sysStr).append(appSecret);
		return MD5Util.getMD5String(resultStr.toString()).toUpperCase();
	}

}

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

	private static final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9','a', 'b', 'c', 'd', 'e', 'f' };

	public static String getMD5String(String s) {
		try {
			return getMD5String(s.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e.getLocalizedMessage());
		}
	}

	public static String getMD5String(byte[] bytes) {
		try {
			MessageDigest messagedigest = MessageDigest.getInstance("MD5");
			messagedigest.update(bytes);
			return bufferToHex(messagedigest.digest());
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e.getLocalizedMessage());
		}
		return null;
	}

	private static String bufferToHex(byte bytes[]) {
		return bufferToHex(bytes, 0, bytes.length);
	}

	private static String bufferToHex(byte bytes[], int m, int n) {
		StringBuffer stringbuffer = new StringBuffer(2 * n);
		int k = m + n;
		for (int l = m; l < k; l++) {
			appendHexPair(bytes[l], stringbuffer);
		}
		return stringbuffer.toString();
	}

	private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
		char c0 = hexDigits[(bt & 0xf0) >> 4];
		char c1 = hexDigits[bt & 0xf];
		stringbuffer.append(c0);
		stringbuffer.append(c1);
	}
}