# 宠物出行
# 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×tamp=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("反序列化异常");
}
}
}