# 跑腿上游渠道接入文档(2B)
本文档为跑腿 2B SaaS 统一接入主文档。询价下单相关接口已按下单模式拆分为 3 个子文档,请根据业务模式选择对应文档:
# 1. 接入须知
# 1.1 约定
| 类别 | 描述 |
|---|---|
| 坐标系 | 腾讯侧使用的经纬度坐标一律为国标(腾讯地图默认坐标) |
| 城市编码 | GB/T 2260,WebService API | 腾讯位置服务 (opens new window) |
| 距离 | 使用 米 作为统一单位 |
| 费用 | 使用 分 作为单位 |
# 1.2 ChangeLog
| 日期 | 主要内容 | 修订人 |
|---|---|---|
| 2025-3-6 | 初版 | @reeceliu(刘亚男) @banchlmao(毛文浩) @yvesdong(董雪峰) |
| 2025-10-27 | 询价、下单、取消、回调调整(1. 询价接口 2. 下单接口 3. 订单状态同步) | @banchlmao(毛文浩) |
| 2025-11-26 | 新增订单状态同步接口渠道方订单不存在或未派给腾讯侧错误码;新增错误码 400201 | @lancezzhu(祝浪) |
| 2025-12-16 | 新增下单带询价金额参数 | @lancezzhu(祝浪) |
| 2026-01-26 | 新增详情接口返回取送件照片 | @lancezzhu(祝浪) |
| 2026-03-11 | 下单接口新增回调地址 | @lancezzhu(祝浪) |
| 2026-05-14 | 新增骑手轨迹查询接口 | @shelbyliang(梁宸) |
| 2026-05-19 | 新增推单接口(2.2.9),加价推单接口(2.2.10),加小费接口(2.2.11) | @marcusgma(马钊) |
| 2026-06-03 | 完单回调新增签收类型 | @shelbyliang(梁宸) |
# 1.3 确认项
- 测试环境仅支持"北京"城市询价及相关测试
- 创单后金额是否固定不可变更,是否需要创单金额和完单金额保证一致
- 取消订单是否会重试,重试次数、间隔分别是多少,是否需要支持送件中取消
- 创单/取消失败后(超时、未收到响应)是否会重试创单/取消接口,如不重试在后续接收到回调初始化订单如何处理
- 是否存在直接创单的场景,不经过询价直接创建订单
- 询价阶段传入信息是否可作为下单直接使用,询价是否会脱敏用户信息
- 节假日峰值询价 QPS 为多少
- 是否存在一段时间内集中下单 & 取消的场景
- 是否有自主联调平台供联调/测试使用,or 需要人工发单
- 完单的回调是否需要骑手位置信息
- 取消费需要以回调接口里的金额为准(部分场景会存在骑手/服务商取消也收取消费的情况)(服务商取消为直接通过回调使接入方感知)
- 注意,目前腾讯侧城市编码均为地级市标准城市编码,WebService API | 腾讯位置服务 (opens new window),'北京'、'上海'、'重庆'、'天津'、'香港'、'澳门' 这几个直辖市的 city_code 为省级编码,需要适配,如北京 city_code:110000,上海:310000
- 上线后可否全量打开入口,由腾讯侧控制实际开城节奏(双方业务可提前商议节奏)
- 提供产品运营研发联系方式
# 2. 接口
# 2.1 通用约定
url_prefix:
- 测试:https://test.tai.qq.com/api/open/express/intracity/2b (opens new window)
- 正式:https://weixin.go.qq.com/api/open/express/intracity/2b (opens new window)
如非特殊约定,统一使用如下规范
| 类型 | 描述 |
|---|---|
| 协议 | HTTPS |
| 请求方式 | Post |
| 数据格式 | JSON,请设置 Header 的 Content-Type 为 application/json |
| 编码 | UTF-8 |
| 时间戳 | 毫秒 |
| 日期格式 | 日期格式采用 GB/T 7408-2005 标准中 "YYYY-MM-DD" 格式(示例:2022-08-01),时区统一采用东八区(即北京时间)。日期时间格式采用 "YYYY-MM-DD hh:mm:ss" 格式(示例:2022-08-01 19:00:00)。Java: "yyyy-MM-dd HH:mm:ss",PHP: "Y-m-d H:i:s",Go: "2006-01-02 15:04:05",Python: "%Y-%m-%d %H:%M:%S" |
# 请求Body通用字段
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| seq_id | string(36) | 请求流水号,调用方自动生成一个随机 ID,建议使用 uuid | Yes |
| timestamp | long(13) | 请求时间,Unix Timestamp 单位毫秒 | Yes |
| nonce | string(32) | 随机字符串,数字与字母组合,区分大小写 | Yes |
| api_key | string(32) | 腾讯出行服务分配给服务商的应用标识 | Yes |
| sign | string(32) | 签名验证参数 | Yes |
# 请求响应统一数据结构
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| code | int(11) | 响应码,默认 0 为成功,其他为错误 | Yes |
| message | string(64) | 错误消息,code 为 0 时返回空字符串,code 为非 0 时返回具体原因 | No |
| data | object | 业务响应主体,code 为 0 时返回,具体返回对象结构见下文具体 API;code 为非 0 时不返回 | No |
{
"code": 0,
"message": "",
"data": {
// 业务字段
}
}
# 2.2 被调(接入方调用腾讯出行)
询价与下单接口拆分说明:本主文档不再列举询价及下单接口,请根据实际下单模式查阅对应子文档:
- 普通询价下单(
/estimatePrice+/createOrder):见 04.2.1-doc-runerrand-normal-order.md- 多报价询价下单(
/estimatePriceForMultipleSp+/createOrder):见 04.2.2-doc-runerrand-multi-quote.md- 推单模式询价下单(
/pushOrder+/addFeeAfterPush):见 04.2.3-doc-runerrand-push-order.md
# 2.2.1 取消订单
Route: /cancelOrder
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
| cancel_type | int(4) | 取消类型 | No |
| cancel_reason | string(64) | 取消原因 | No |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| cancel_result | bool | 取消结果,true: 取消成功,false: 取消失败 | Yes(注意,响应 code 是代表业务响应正常,是否取消成功要根据本字段判断) |
| order_code | string(64) | 腾讯侧订单号 | No |
| cancel_fail_reason | string(64) | 取消失败原因枚举,详细见附录 3.6 | No |
| cancel_fee | int | 取消费 | No |
# 2.2.2 查询订单详情
Route: /orderDetail
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_code | string(64) | 腾讯业务订单号 | Yes |
| third_order_id | string(64) | 接入方业务订单号 | Yes |
| order_status | int(11) | 订单状态 | Yes |
| delivery_time | int(13) | 预计送达时间 | No |
| total_fee | int(11) | 应付金额 | Yes |
| pay_fee | int(11) | 实际支付金额 | Yes |
| coupon_fee | int(11) | 优惠金额 | No |
| cancel_fee | int(11) | 取消金额 | No |
| sp_info | SpInfo | 服务商信息,详情结构见附录 SpInfo | No |
| delivery_distance | int(11) | 配送距离 | 完单后:Yes |
| receive_code | string(32) | 收货码 | Yes,打开收货码才有 |
| rider_info | RiderInfo | 骑手信息 | 接单后:Yes |
| pickup_pic | List<String> | 取货照片 url | No |
| complete_pic | List<String> | 妥投照片 url | No |
# 2.2.3 查询骑手信息(位置)
Route: /riderLocation
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| data | RiderLocationInfo | 骑手信息 | Yes |
# 2.2.4 查询订单状态变更节点(可选)
Route: /orderStatusChangeNode
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| data | List<OrderStatusChangeNode> | 订单状态变更节点,按变更时间正序排序,详细信息见附录对象实体表 | Yes |
# 2.2.5 二次确认(暂时不支持)
骑手接单后需要指定窗口内调用确认选单,否则超时将自动取消订单。
# 2.2.6 骑手轨迹查询
Route: /driverTrajectory
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
| start_time | long | 查询起始时间,不传或传入时间早于司机接单时间,按照司机接单时间查询。Unix Timestamp 单位秒 | No |
| end_time | long | 不传且订单未到达终态,按照当前时间查询;不传或传入时间晚于订单终态时间,按照订单到达终态时间查询。Unix Timestamp 单位秒 | No |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| data | List<TrajectoryInfo> | 司机轨迹点信息,详细信息见附录对象实体表 | Yes |
# 2.2.7 加小费接口
Route: /addTips
注意:加小费接口,同一笔订单,要么只传
fee,要么只传total_fee,不可同时有值,不可同时为空或 0。
- 传
total_fee表示总的小费金额(覆盖式)- 传
fee表示单次添加的小费(可累加)
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| third_order_id | string(64) | 接入方运单 ID | Yes |
| fee | int | 单次小费金额,单位分(累加式) | No |
| total_fee | int | 总的小费金额,单位分(覆盖式) | No |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| third_order_id | string(64) | 接入方运单 ID(透传) | Yes |
| order_code | string(64) | 腾讯侧 order_code | Yes |
| total_fee | int | 累计的小费总额,单位分 | Yes |
# 2.2.8 预取消订单
Route: /preCancelOrder
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_id | string(64) | 订单号 | Yes |
| order_type | int(4) | 查询订单 ID 类型,详细信息见附录 3.7 | Yes |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| can_cancel | bool | 是否能取消 | Yes |
| deduction_fee | int | 取消费 | Yes |
| cancel_rule_detail | CancelRuleDetail | 取消规则 | No |
# 2.3 主调(腾讯调用接入方同步订单状态)
接入方提供 Host。
# 2.3.1 订单状态同步
Route: /status
请求Body:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_code | string(64) | 腾讯业务订单号 | Yes |
| third_order_id | string(64) | 接入方业务订单号 | Yes |
| sp_info | SpInfo | 实际运力信息,详情结构见附录 SpInfo | No |
| event | int(11) | 事件类型,见附录 3.2 事件类型 | Yes |
| status | int(11) | 订单状态 | Yes |
| fee_info | FeeInfo | 费用信息 | Yes(接单后) |
| accepted_info | AcceptedInfo | 已接单事件相关信息 | 根据具体状态填充 |
| reassign_info | ReassignInfo | 骑手转单事件相关信息 | 根据具体状态填充 |
| arrived_info | ArrivedInfo | 已到达取件点事件相关信息 | 根据具体状态填充 |
| delivering_info | DeliveringInfo | 骑士出发开启送货事件相关信息 | 根据具体状态填充 |
| send_back_info | SendBackInfo | 送回物品事件相关信息 | 根据具体状态填充 |
| cancel_info | CancelInfo | 取消事件相关 | 根据具体状态填充 |
| completed_info | CompletedInfo | 完成事件相关 | 根据具体状态填充 |
响应Body业务字段:
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| accept_time | int(13) | 通知接收处理时间 | Yes |
# 3. 附录
# 3.1 对象实体表
ProductDetail
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| goods_name | string(32) | 物品名称 | No |
| goods_num | int | 物品数量 | No |
AddressInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string(32) | 姓名/昵称 | Yes |
| phone | string(32) | 手机,传入真实手机号 | Yes |
| address_detail | string(64) | 详细地址 | No,与 poi_address 中必须传递一个 |
| poi_address | string(64) | poi 地址信息 | No,与 address_detail 中必须传递一个 |
| poi_title | string(64) | poi 标题 | Yes |
| poi_lng | double | 经度 | Yes |
| poi_lat | double | 纬度 | Yes |
SpInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| sp_name | string(64) | 服务商名称 | Yes |
| sp_code | string(64) | 服务商唯一标识 | Yes |
RiderInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string(32) | 名称 | Yes |
| phone_type | int(11) | 1-AXN/真实号,2-AXB | No,无值默认为 1 |
| phone | string(32) | 手机号(真实号或 AXN 的虚拟号),phone_type=1 时有值 | No |
| phones | List<PhoneInfo> | 下单人、发货人、收货人对应关系,phone_type=2 时有值 | No |
PhoneInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| role_type | int(11) | 角色(1-下单人,2-收货人,3-发货人);下单人手机号以注册手机号为准 | Yes |
| virtual_phone | string(32) | 虚拟手机号 | Yes |
RiderLocationInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| rider_lng | double | 骑手经度 | Yes |
| rider_lat | double | 骑手纬度 | Yes |
| name | String | 骑手姓名 | No |
| phone | String | 骑手电话 | No |
EstimateTimeInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| accept_time | int(13) | 预计骑手接单时间 | 根据订单状态决定 |
| depart_pick_time | int(13) | 预计出发时间 | 根据订单状态决定 |
| arrive_pickup_point_time | int(13) | 预计取件时间 | 根据订单状态决定 |
| finish_time | int(13) | 预计送达时间 | 根据订单状态决定 |
| send_back_finish_time | int(13) | 送回预计送达时间 | 根据订单状态决定 |
CancelInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 取消时间,Unix Timestamp 单位毫秒 | Yes |
| reason_id | int(11) | 取消原因 Id | No |
| reason_desc | string(64) | 取消原因 | No |
| cancel_from | int(11) | 取消来源,1: 用户,2: 接入方系统取消 | Yes |
| cancel_fee | int(11) | 取消费 | No |
ExceptionInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 异常产生时间,Unix Timestamp 单位毫秒 | Yes |
| exception_id | int(11) | 异常编码 | Yes |
| exception_desc | string(64) | 异常原因 | No |
CancelRule
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string(64) | 规则名称 | Yes |
| start | int(11) | 开始时间 | Yes |
| end | int(11) | 截止时间 | Yes |
| fee | int(11) | 费用 | Yes |
OrderStatusChangeNode
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| order_status | int(11) | 订单状态 | Yes |
| event | int(11) | 事件类型,见附录 3.2 事件类型 | Yes |
| order_change_time | long | 订单状态变更时间戳,单位: 毫秒 | Yes |
GoodsDetail
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string | 货品名称 | 是 |
| count | int | 货品数量 | 是 |
| price | double | 货品价值,选填,数值不小于 0,精确到小数点后两位(如果小数点后位数多于两位,则四舍五入保留两位小数) | 否 |
| unit | string | 货品单位 | 是 |
FeeInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| total_fee | int(11) | 应付金额 | Yes |
| pay_fee | int(11) | 实际支付金额(total_fee - coupon_fee) | Yes |
| coupon_fee | int(11) | 优惠金额 | No |
| cancel_fee | int(11) | 取消金额 | No |
TrajectoryInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string(32) | 名称 | Yes |
| phone | string(32) | 手机号(真实号或 AXN 的虚拟号) | No |
| rider_lng | double | 骑手经度 | Yes |
| rider_lat | double | 骑手纬度 | Yes |
| timestamp | long | 轨迹点采集时间,Unix Timestamp 单位秒 | Yes |
回调相关数据结构
AcceptedInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| estimate_time_info | EstimateTimeInfo | 预估时间信息 | No |
| rider_info | RiderInfo | 骑手信息 | Yes |
| send_code | string(32) | 发货码 | No |
| receive_code | string(32) | 收货码 | Yes |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
ReassignInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| reassign_type | int(11) | 转单原因类型,1-骑手转单,2-服务商转单 | Yes |
| estimate_time_info | EstimateTimeInfo | 新预估时间信息 | No |
| rider_info | RiderInfo | 新骑手信息 | Yes |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
ArrivedInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| estimate_time_info | EstimateTimeInfo | 新预估时间信息 | No |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
| rider_info | RiderInfo | 骑手信息 | Yes |
DeliveringInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| estimate_time_info | EstimateTimeInfo | 新预估时间信息 | No |
| pickup_pic | List<String> | 取件照片 url,回调的照片 url 为未经过风控及压缩图片且存在过期可能【慎用】 | No |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
| rider_info | RiderInfo | 骑手信息 | Yes |
CompletedInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
| complete_pic | List<String> | 妥投照片 url,回调的照片 url 为未经过风控及压缩图片且存在过期可能【慎用】 | No |
| rider_info | RiderInfo | 骑手信息 | Yes |
| receipt_type | int | 签收类型。1:正常签收;2:商家退回签收 | No |
SendBackInfo
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| timestamp | long | 事件发生时间点,Unix Timestamp 单位毫秒 | Yes |
| estimate_time_info | EstimateTimeInfo | 新预估时间信息 | No |
| rider_location_info | RiderLocationInfo | 骑手位置信息 | Yes |
| rider_info | RiderInfo | 骑手信息 | Yes |
CancelRuleDetail
| 名称 | 类型 | 描述 | 必须 |
|---|---|---|---|
| name | string | 规则名称 | Yes |
| start | int | 开始时间 | No |
| end | int | 结束时间 | Yes |
| fee | int | 费用 | Yes |
# 3.2 订单状态表:供参考
| 状态编码 | 名称 | 描述 | 支付/退款状态说明 |
|---|---|---|---|
| 10205 | DISPATCHING_SP_ORDER_FAIL | 派单失败 | 已支付 |
| 30050 | DISPATCHING | 派单中 | 已支付 |
| 30100 | PICKING | 取件中 | 已支付 |
| 30200 | ARRIVED_PICK_POINT | 到达取件点 | 已支付 |
| 30300 | DELIVERING | 送件中 | 已支付 |
| 30400 | SENDING_BACK | 送回中 | 已支付 |
| 30700 | CANCELING | 取消中 | 已支付未退款 |
| 30710 | CANCELED | 已取消 | 已退款 |
| 30900 | ARRIVED | 已送达 | 已支付 |
事件类型
| 事件编码 | 名称 | 描述 | 必须 |
|---|---|---|---|
| 101 | CREATE_FAILED | 创单失败 | Yes |
| 150 | DISPATCH | 派单/改派 | Yes |
| 200 | RIDER_ACCEPT | 骑手接单 | Yes |
| 250 | RIDER_REASSIGN | 骑手转单 | Yes: 有转单,No: 无转单 |
| 300 | ARRIVE_PICK_POINT | 到达取件点 | Yes |
| 400 | DEPART_SEND | 开始送件 | Yes |
| 450 | ARRIVED | 已送达 | Yes |
| 500 | RIDER_SEND_BACK | 骑手送回 | Yes: 有送回,No: 无送回 |
| 670 | SP_REASSIGN | 运力骑手改派(回到派单中) | Yes: 有改派,No: 无改派 |
| 950 | CANCEL | 取消,内部可能需要区分具体取消原因 | Yes: 有取消,No: 无取消 |
| 960 | REFUND | 退款通知 | Yes: 有退款,No: 无退款 |
# 3.3 物品类型表
| id | 类型 |
|---|---|
| 1 | 文件证照 |
| 2 | 服饰 |
| 3 | 食品饮料 |
| 4 | 蛋糕 |
| 5 | 鲜花 |
| 6 | 数码 |
| 7 | 水果生鲜 |
| 8 | 药品 |
| 9 | 汽配 |
| 10 | 个护美妆 |
| 11 | 家居家纺 |
| 12 | 其他 |
# 3.4 跑腿类型
| id | 类型 |
|---|---|
| 1 | 特惠拼送 |
| 2 | 极速直送 |
# 3.5 订单来源
| id | 类型 |
|---|---|
| 10 | 美团 |
| 20 | 饿了么 |
| 30 | 抖音 |
| 40 | 京东外卖 |
| 50 | 淘宝闪购 |
| 60 | 歪马微商城 |
| 70 | 有赞云 |
# 3.6 取消原因表--用户
| id | 父类别 | 子类别 | 类型 |
|---|---|---|---|
| 1 | 和自己有关 | 填写的取件/收件信息有误 | 单选 |
| 2 | 计划有变,暂时不需要寄件 | 单选 | |
| 3 | 和平台有关 | 没有骑手接单 | 单选 |
| 4 | 派的骑手距离太远 | 单选 | |
| 5 | 和骑手有关 | 联系不上骑手 | 单选 |
| 6 | 骑手要求我取消订单 | 单选 | |
| 7 | 骑手行驶过于缓慢或长时间不移动 | 单选 | |
| 20 | 其他 | 其他 | 输入框 |
# 3.6.1 取消失败原因表
| 取消失败原因 |
|---|
| 当前订单状态不支持 |
| 服务商侧取消失败 |
| 其它 |
# 3.7 查询订单ID类型表
| id | 订单类型 |
|---|---|
| 1 | 腾讯业务订单号 |
| 2 | 接入方业务订单号 |
# 3.8 腾讯错误码
| code | message | 说明 |
|---|---|---|
| 0 | success | |
| 400000 | 请求参数错误,备注超长 | |
| 400001 | 请求 URL 错误 | |
| 400002 | 请求方法错误 | |
| 400003 | appId 错误 | |
| 400004 | 时间戳过期或者 nonce 重复 | |
| 400005 | 签名验证失败 | |
| 400101 | 请求频繁,请稍后重试 | |
| 400102 | 询价价格已失效,请重新询价 | |
| 400103 | 询价地址不支持 | |
| 400100 | 通用非影响主流程错误码,无需关注的业务错误码 | |
| 400201 | 渠道方订单状态不匹配 | |
| 500000 | 其他系统错误,需要接入方以一定策略进行重试,建议指数递增重试:需要在 1s,2s,4s,8s,16s,32s,64s 进行重试,且最后一次重试应当在第一次重试 1min 以上 |
# 3.9 签名算法
# 3.9.1 描述
- 从请求串中获得
api_key、seq_id、timestamp、nonce通用字段以及其他的业务字段和鉴权结果字段sign。 - 根据签名算法,对参与签名的内容进行签名:按照除
sign外参数名称排序(字典升序排列)成key1=value1&key2=value2&....的原始字符串 src1;请求报文中不存在的参数不参与签名。将原始字符串 + 分配给调用方的api_secret形成字符串 src2;将 src2 进行 md5 后转成大写形成签名内容 dest。 - 将步骤 2 中得到的签名内容 dest 与请求中的
sign字段内容做比较,如果相同则验证成功,否则判定请求非法。
举例:假设注册接口文档中业务字段为 [name, phone, address],分配的 api_secret = DZaslH9B9ycqRrE77laCPB2Om,请求参数如下:
api_key=PSUBZLHOKUO6HV52A5CAUSSE5KSB6Y
seq_id=b8b4f0b8-01fb-4c06-80b9-3ab895a8c616
timestamp=1726214265
nonce=123456
name=test
phone=11111111111
address=北京市朝阳区亚洲金融大厦
则需要签名的内容:
address=北京市朝阳区亚洲金融大厦&api_key=PSUBZLHOKUO6HV52A5CAUSSE5KSB6Y&name=test&nonce=e2fa80izRF&phone=11111111111&seq_id=b8b4f0b8-01fb-4c06-80b9-3ab895a8c616×tamp=1726214265DZaslH9B9ycqRrE77laCPB2Om
- 计算的 MD5 值为
afc4a5b550c1f5c92bca917aba66faf5,大写值为AFC4A5B550C1F5C92BCA917ABA66FAF5。 - 将步骤 4 中得到的 MD5 值,与请求中
sign字段的值比较。两者相同请求合法。
注意:签名时对对象类型的 value 需进行 json 序列化后的字符串拼接,这里的 json 需与主调 HTTP 客户端传输的 json 保持 key 顺序一致,验签时会原样提取 json 中的 value。
# 3.9.2 示例
# Java
算法工具类
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
/**
* OpenApiSignUtil
*
* @Description: 开放平台接入鉴权工具类
*/
@Slf4j
public class OpenApiSignUtil {
private static final String SIGN_KEY = "sign";
private static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
}
/**
* @param requestJson 请求的 json,注意如果为 null 的字段 key 不要放进 json 里,不参与签名,也不要包含 sign
* @param secretKey 秘钥
* @return 签名结果
*/
public static String sign(String requestJson, String secretKey) {
JsonNode node = toJsonNode(requestJson);
return sign(node, secretKey);
}
private static String sign(JsonNode node, String secretKey) {
Map<String, String> params = trans2Map(node);
return sign(params, secretKey);
}
/**
* @param params 注意如果为 null 的字段 key 不要放进 map 里,不参与签名,也不要包含 sign
* @param secretKey 秘钥
* @return 签名结果
*/
public static String sign(Map<String, String> params, String secretKey) {
// 缺省即为字典升序
TreeMap<String, Object> signMap = new TreeMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (StringUtils.equals(entry.getKey(), SIGN_KEY)) {
continue;
}
String key = entry.getKey();
String value = entry.getValue();
signMap.put(key, value);
}
String signatureParam = join(signMap, "&", "=", true) + secretKey;
String sign = DigestUtils.md5Hex(signatureParam).toUpperCase();
log.info("sign params: {}, sign: {}", signatureParam, sign);
return sign;
}
public static boolean verify(String requestJson, String secretKey) {
JsonNode node = toJsonNode(requestJson);
return verify(node, secretKey);
}
public static boolean verify(JsonNode node, String secretKey) {
Map<String, String> params = trans2Map(node);
return verify(params, secretKey);
}
public static boolean verify(Map<String, String> params, String secretKey) {
if (MapUtils.isEmpty(params)) {
return false;
}
String signatureVerify = null;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (StringUtils.equals(entry.getKey(), SIGN_KEY)) {
signatureVerify = entry.getValue();
break;
}
}
String sign = sign(params, secretKey);
return StringUtils.equals(sign, signatureVerify);
}
private static JsonNode toJsonNode(String jsonStr) {
try {
return objectMapper.readTree(jsonStr);
} catch (JsonProcessingException e) {
log.error("json jsonStr to jsonNode failed for jsonStr:{}", jsonStr, e);
throw new RuntimeException("反序列化异常");
}
}
private static Map<String, String> trans2Map(JsonNode node) {
Iterator<String> fields = node.fieldNames();
Map<String, String> params = new HashMap<>();
while (fields.hasNext()) {
String key = fields.next();
JsonNode value = node.get(key);
if (value.isTextual()) {
params.put(key, value.asText());
} else {
params.put(key, value.toString());
}
}
return params;
}
private static <K, V> String join(Map<K, V> map, String separator, String keyValueSeparator, boolean isIgnoreNull) {
final StringBuilder strBuilder = new StringBuilder();
boolean isFirst = true;
if (MapUtils.isNotEmpty(map)) {
for (Entry<K, V> entry : map.entrySet()) {
if (!isIgnoreNull
|| null != entry.getKey()
&& null != entry.getValue()) {
if (isFirst) {
isFirst = false;
} else {
strBuilder.append(separator);
}
strBuilder.append(null == entry.getKey() ? StringUtils.EMPTY : entry.getKey().toString())
.append(keyValueSeparator)
.append(null == entry.getValue() ? StringUtils.EMPTY : entry.getValue().toString());
}
}
}
return strBuilder.toString();
}
}
用例
import org.junit.jupiter.api.Test;
class OpenApiSignUtilTest {
@Test
void testOpenApiSignUtil() {
String requestBody = "{\"name\":\"demoData\",\"spId\":1,\"spGoodsCategoryId\":1,\"goodsCategoryId\":1,\"sign\":\"676E5EFB9EED600E5F2795E27941EA00\"}";
String secretKey = "123456";
String sign = OpenApiSignUtil.sign(requestBody, secretKey);
boolean isVerified = OpenApiSignUtil.verify(requestBody, secretKey);
System.out.println("Signed Data: " + sign);
System.out.println("Is Verified: " + isVerified);
}
@Test
void testOpenApiSignUtil2() {
String requestBody = "{\"b\":\"2\",\"a\":1,\"c\":3,\"ext\": {\"b\":\"2\",\"a\":1, \"c\":3},\"sign\":\"58963F0D37D6E04910D873D77B277ADF\"}";
String secretKey = "123456";
String sign = OpenApiSignUtil.sign(requestBody, secretKey);
boolean isVerified = OpenApiSignUtil.verify(requestBody, secretKey);
System.out.println("Signed Data: " + sign);
System.out.println("Is Verified: " + isVerified);
}
}
# Golang
import "github.com/tidwall/gjson"
func Verify(params map[string]interface{}, apiSecretKey string) (bool, error) {
var keys []string
for key := range params {
if key == signKey {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
sb := strings.Builder{}
lastKeyIndex := len(keys) - 1
for index, key := range keys {
sb.WriteString(key)
sb.WriteString(splitKey1)
value := gjson.Get(body, key)
sb.WriteString(value.String())
if index < lastKeyIndex {
sb.WriteString(splitKey2)
}
}
sb.WriteString(apiSecretKey)
signStr := sb.String()
m := md5.New()
_, _ = io.WriteString(m, signStr)
signVerify := fmt.Sprintf("%x", m.Sum(nil))
signParam, _ := params[signKey]
return signVerify == signParam, nil
}
# Python
# -*- coding: utf-8 -*-
"""
OpenApiSignUtil
开放平台接入鉴权工具类(由 Java 版本翻译而来)
签名规则:
1. 参数按 key 字典升序排列
2. 跳过 key == "sign" 的字段
3. key 或 value 为 None 的条目不参与签名(对应 Java 的 isIgnoreNull=true)
4. 拼接格式:key1=value1&key2=value2... + secretKey
5. 对拼接结果做 MD5,结果转大写
"""
import hashlib
import json
import logging
from typing import Dict, Optional, Union
logger = logging.getLogger(__name__)
SIGN_KEY = "sign"
def _to_str_value(value) -> str:
"""
将 JSON 的 value 转换为字符串,等价于 Java 中的 trans2Map 逻辑:
- 文本类型直接 asText()
- 其他类型用 toString()(即原始 JSON 字符串形式)
"""
if value is None:
return None
if isinstance(value, str):
return value
if isinstance(value, bool):
# Java 中 JsonNode 的布尔 toString 为 "true"/"false"
return "true" if value else "false"
if isinstance(value, (int, float)):
# 与 Java JsonNode.toString() 表现一致
return str(value)
# dict / list 等结构化对象,输出紧凑 JSON(与 Java JsonNode.toString() 等价)
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _trans2map(node: Union[str, dict]) -> Dict[str, str]:
"""
将 json 字符串或 dict 转换为 Map<String, String>
"""
if isinstance(node, str):
try:
node = json.loads(node)
except json.JSONDecodeError as e:
logger.error("json jsonStr to jsonNode failed for jsonStr: %s", node, exc_info=e)
raise RuntimeError("反序列化异常") from e
if not isinstance(node, dict):
raise RuntimeError("反序列化异常: 输入不是 JSON 对象")
params: Dict[str, str] = {}
for key, value in node.items():
params[key] = _to_str_value(value)
return params
def _join_map(params: Dict[str, Optional[str]],
separator: str = "&",
kv_separator: str = "=",
ignore_null: bool = True) -> str:
"""
等价于 Java 中的 join 方法,按 TreeMap 字典升序拼接
"""
if not params:
return ""
parts = []
# TreeMap 字典升序
for key in sorted(params.keys()):
value = params[key]
if ignore_null and (key is None or value is None):
continue
k_str = "" if key is None else str(key)
v_str = "" if value is None else str(value)
parts.append(f"{k_str}{kv_separator}{v_str}")
return separator.join(parts)
def sign(params: Union[str, dict, Dict[str, str]], secret_key: str) -> str:
"""
生成签名
:param params: 请求参数,可以是 json 字符串、dict 或 Map<String, String>
注意:如果为 None 的字段 key 不要放进里面,不参与签名,也不要包含 sign
:param secret_key: 秘钥
:return: 大写 MD5 签名结果
"""
if isinstance(params, str):
params_map = _trans2map(params)
elif isinstance(params, dict):
# 如果 dict 的值已经全是 str,直接用;否则按 trans2Map 规则转
params_map = {k: _to_str_value(v) for k, v in params.items()}
else:
raise TypeError("params 必须是 str、dict 或 Map")
# 跳过 sign 字段
sign_map = {k: v for k, v in params_map.items() if k != SIGN_KEY}
signature_param = _join_map(sign_map, "&", "=", True) + secret_key
sign_result = hashlib.md5(signature_param.encode("utf-8")).hexdigest().upper()
logger.info("sign params: %s, sign: %s", signature_param, sign_result)
return sign_result
def verify(params: Union[str, dict, Dict[str, str]], secret_key: str) -> bool:
"""
验签
:param params: 请求参数,可以是 json 字符串、dict 或 Map<String, String>
:param secret_key: 秘钥
:return: 验签是否成功
"""
if isinstance(params, str):
params_map = _trans2map(params)
elif isinstance(params, dict):
params_map = {k: _to_str_value(v) for k, v in params.items()}
else:
raise TypeError("params 必须是 str、dict 或 Map")
if not params_map:
return False
signature_verify = params_map.get(SIGN_KEY)
expected_sign = sign(params_map, secret_key)
return expected_sign == signature_verify
# ============== 使用示例 ==============
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 示例 1:使用 dict 签名
demo_params = {
"api_key": "3b875f0fc3ce445e84d0",
"city_code": "110105",
"city_name": "北京市",
"enable_receive_code": "false",
"express_type": "1",
"goods_type": "12",
"goods_weight": "8.0",
"nonce": "EKNZhj7k0k",
"order_phone": "18843246756",
"receiver_address": '{"poi_lng":116.35722151393145,"name":"收某某","phone":"18899990000","poi_address":"","poi_lat":39.95082511963979,"address_detail":"北京","poi_title":"北京"}',
"sender_address": '{"poi_lng":116.35567034726574,"name":"chichichi","phone":"18843246756","poi_address":"","poi_lat":39.92758260361654,"address_detail":"123【chichichi】","poi_title":"123【chichichi】"}',
"seq_id": "82981d92-6023-4836-b9e5-071276e5566a",
"third_order_id": "1623460265",
"timestamp": "1781604216",
}
secret = "1acd6439e1c443dba684ac0df0a7f178"
result = sign(demo_params, secret)
print("签名结果:", result)
# 示例 2:验签
demo_params_with_sign = dict(demo_params)
demo_params_with_sign["sign"] = result
print("验签结果:", verify(demo_params_with_sign, secret))
# 3.10 运力列表 code
| sp_code | 名称 |
|---|---|
| dada | 达达 |
| shunfeng2b | 顺丰 B 端 |
| shunfeng | 顺丰 C 端 |
| uu | UU 跑腿 |
| shansong | 闪送 |
| didi | 滴滴 |
| fengniao2b | 蜂鸟 B 端 |
# 3.11 事件-状态-实体对应关系

# 3.12 状态流转
