# 宠物出行

# 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("反序列化异常");
        }
    }

}