# 宠物出行

# 1. 版本历史

版本 日期 修订人 修改内容 审核人
V0.1 2024.11.07 初稿

# 2. 基础说明

# 2.1 接口协议

除非特殊指定,默认请求方式均为 HTTPS/POST 方式。

# 2.2 返回数据格式

除非特殊指定,默认返回数据格式均为 JSON 格式。

# 2.3 字符编码

除非特殊指定,默认字符编码均为 UTF-8 编码格式。

# 2.4 请求格式

接口内如果没有特殊说明的,请求格式一律统一为 application/json 。

# 2.5 重试机制

调⽤出行服务接口时,返回码 Code 不成功,要有重试机制(具体见错误码表)。

# 2.6 通用参数

字段名 字段类型 最大长度 必填 说明
accessKey String 32 腾讯出行服务分配给服务商调用的 accessKey
traceId String 36 请求流水号,调用方自动生成一个随机 ID,建议使用 uuid
timestamp Long 20 请求发送时的时间戳(unix 时间戳) 毫秒
nonce String 10 10 位随机字符串
sign String 32 验证签名参数

# 2.7 通用返回值

字段名 字段类型 最大长度 必填 说明
code Integer 11 服务响应状态,参见错误码表
message String 64 服务响应状态说明,参见错误码表
data Object - 具体的接口对应不同的对象详见具体的接口

# 2.8 鉴权说明

步骤 1 从请求串中获得 accessKey、traceId、timestamp, nonce 通用字段以及其他的业务段和鉴权结果字段 sign。

步骤 2 根据签名算法,对参与签名的内容进行签名; 按照除 sign 外参数名称排序(字典升序排列)成”key1=value1&key2=value2&....”的原始字符串 src1;参数值为 null 不参与签名。 将原始字符串+分配给调用方的 apiSecret 形成字符串 src2; 将 src2 进行 md5 后转成大写形成签名内容 dest

步骤 3 将步骤 2 中得到的签名内容 dest 与请求中的 sign 字段内容做比较,如果相同则验证成功,否则判定请求非法。

举例: 假设/order/status 接口文档中业务字段为[slon, slat],分配的 apiSecret=DZaslH9B9ycqRrE77laCPB2Om, 请求参数如下: accessKey=PSUBZLHOKUO6HV52A5CAUSSE5KSB6Y, traceId=b8b4f0b8-01fb-4c06-80b9-3ab895a8c616, timestamp=1554695343, slat=39.998299, slon=116.285561 则需要签名的内容: accessKey=PSUBZLHOKUO6HV52A5CAUSSE5KSB6Y&traceId=b8b4f0b8-01fb-4c06-80b9-3ab895a8c616&slat=39.998299&slon=116.285561&timestamp=1554695343&apiSecret=DZaslH9B9ycqRrE77laCPB2Om

步骤 4 计算的 MD5 值为 8a983278e5366eb93feb0d4143e1c522,大写值为 8A983278E5366EB93FEB0D4143E1C522。

步骤 5 将步骤 4 中得到的 MD5 值,与请求中 sign 字段的值比较。两者相同请求合法。

注:验签参考代码详见附录

# 2.10 注意事项

1、需要签名的字段与请求串中字段先后顺序没有关系,只与进行签名的内容有关,必须按照文档中的顺序拼接。
2、apiSecret 内容为双方保密内容,不在请求中传输,严禁公开。
3、通用字段 accessKey,traceId,timestamp,nonce 参与所有接口的签名。
4、字段值是 JSON 类型的,把字段值 json 串参与签名, 不用拆分里面字段。

# 2.11 数据加密

在双方进行HTTP接口交互时,为保证数据安全,须对请求参数和响应数据中的敏感数据(如手机号)加密传输,接收数据后解密使用。双方联调时提供具体加密策略。

# 2.12 接口分类

  • 主调接口(调用方:腾讯出行,被调方:各服务商),主调接口由服务商实现。
  • 被调接口(调用方:各服务商,被调方:腾讯出行),被调接口由腾讯实现。

# 3. 业务接口列表

# 3.1 业务主调接口

# 3.2 业务被调接口

  • 请求地址

正式环境 https://sp.wecar.map.qq.com/pettravel/

测试环境 https://test.tai.qq.com/pettravel/

  • 请求方式 POST

  • 被调接口通用参数

字段名 字段类型 最大长度 必填 说明
accessKey String 32 腾讯出行服务分配给服务商调用 accessKey
traceId String 36 请求流水号,调用方自动生成一个随机 ID,建议使用 uuid
timestamp Long 20 请求发送时的时间戳(unix 时间戳) 毫秒
nonce String 10 10 位随机字符串
sign String 32 验证签名参数
userCode String 32 腾讯用户Code
spId Integer 32 服务商ID
orderId String 32 腾讯侧订单ID
spOrderId String 32 服务商侧订单ID

# 3.2.1 订单状态变更通知

# 请求地址
  • /v1/callback/order/status
# 请求参数
参数名 类型 必传 说明
orderStatus Integer 订单状态(详见附录订单状态)
driverInfo DriverInfo 司机信息(司机接单时回传)
goodsVerifyCode String 收件码(司机接单时回传)
cancelType Integer 取消类型(1-乘客取消,2-司机取消,3-客服取消,4-未接单自动取消,5-超时未支付自动取消)(订单状态910时传递)
cancelReasonDesc String 取消原因具体描述(取消时回传)
# DriverInfo
参数名 类型 必传 说明
driverId String 司机ID
driverOrderId String 司机订单ID
driverName String 司机姓名
driverAXNVirtualPhone String 司机AXN虚拟号
vehiclePlateNum String 车牌号
vehicleColor String 车身颜色
vehicleModel String 车型名称
# 请求示例
{
  "accessKey": "QWERTYASDFGZXC",
  "traceId": "12345678910111213141516",
  "timestamp": 1234567891011,
  "nonce": "qwertyuiop",
  "userCode": "a1b2c7d5e9f8g1h",
  "orderStatus": 401,
  "driverInfo" : {},
  "goodsVerifyCode": "123456",
  "sign": "A1B2C3D4E5F6G7H8I9J0"
}
# 响应数据
参数名称 类型 说明
- - -
# 响应示例
{
  "code": 0,
  "message": "成功"
}

# 3.2.2 照片上传接口

# 请求地址
  • /v1/callback/photo/upload
# 请求参数
参数名称 类型 必传 说明
photoType Integer 照片类型(1-占座照片上传,2-中途照片上传,3-上门取件照片,4-上门送件照片)
photos List 照片信息(url)
# 请求示例
{
  "accessKey": "QWERTYASDFGZXC",
  "traceId": "12345678910111213141516",
  "timestamp": 1234567891011,
  "nonce": "qwertyuiop",
  "userCode": "a1b2c7d5e9f8g1h",
  "photoType": 1,
  "photos": ["url1", "url2"],
  "sign": "A1B2C3D4E5F6G7H8I9J0"
}
# 响应数据
参数名称 类型 说明
- - -
# 响应示例
{
  "code": 0,
  "message": "成功"
}

# 4. 附录

# 4.1 订单状态说明

订单状态 对应事件名称
401 车主接单
501 车主前往起点
551 车主到达起点
601 车主已接到宠物
701 车主开始运送
801 车主到达终点
901 行程结束
910 取消行程(cancelType:1-乘客取消,2-司机取消,3-客服取消,4-未接单自动取消,5-超时未支付自动取消)

# 4.2 接口错误码

错误码 描述
0 成功
-1 服务内部错误
1 回调请求签名校验不通过
2 回调请求参数错误
20 回调处理失败

# 4.3 签名示例代码

以下代码仅为示例,以实际联调过程中双方沟通结论为准。

服务商接收到腾讯侧请求,无需转换对象,直接将这个请求JSON串传入requestSign(String requestStr, String secret)方法计算得到签名sign。 注:为保证结果一致,建议使用相同的JSON工具。

import org.apache.commons.lang3.StringUtils;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * 验签
 */
public class SignUtil {

    private static final String MD5 = "MD5";

    /**
     * 请求签名算法入口
     * @param requestStr
     * @param secret
     * @return
     */
    public static String requestSign(String requestStr, String secret) {
        Map<String, Object> paramsMap = JsonTool.toObj(requestStr, TreeMap.class);
        Map<String, String> params = new HashMap<>();
        String[] keys = paramsMap.keySet().toArray(new String[0]);
        for (String key : keys) {
            if (org.apache.commons.lang3.StringUtils.isNotBlank(key)) {
                String value = paramsMap.get(key).toString();
                if (null != value) {
                    params.put(key.trim(), value);
                }
            }
        }
        params.remove("sign");
        return sign(params, secret);
    }

    /**
     * 签名算法核心
     * @param params
     * @param secret
     * @return
     */
    public static String sign(Map<String, String> params, String secret) {
        // 第一步:检查参数是否已经排序
        String[] keys = params.keySet().toArray(new String[0]);
        Arrays.sort(keys);
        // 第二步:把所有参数名和参数值串在一起
        StringBuilder sb = new StringBuilder();
        for (String key : keys) {
            if (StringUtils.isNotBlank(key)) {
                String value = params.get(key);
                if (null != value) {
                    sb.append(key.concat("="));
                    sb.append(value.concat("&"));
                }
            }
        }
        sb.append("apiSecret=" + secret);
        return toHex(getMd5(sb.toString()));
    }

    private static String toHex(byte[] byteArray) {
        if (null == byteArray) {
            return null;
        }
        StringBuilder md5Str = new StringBuilder();
        for (byte b : byteArray) {
            md5Str.append(String.format("%02x", b));
        }
        return md5Str.toString().toUpperCase();
    }

    private static byte[] getMd5(String str) {
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance(MD5);
            messageDigest.reset();
            messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            return new byte[0];
        }
        return messageDigest.digest();
    }
}

JSON工具类:

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * json tools
 */
public class JsonTool {

    private static final ObjectMapper objectMapper;

    static {
        objectMapper = new ObjectMapper();
        objectMapper.setDefaultPropertyInclusion(Include.NON_NULL);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
    }

    public static String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (Exception ex) {
            throw new RuntimeException("序列化异常");
        }
    }

    public static <T> T toObj(String json, Class<T> valueType) {
        try {
            return objectMapper.readValue(json, valueType);
        } catch (Exception ex) {
            throw new RuntimeException("反序列化异常");
        }
    }

}