DadaAdapter.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <?php
  2. namespace common\components\delivery\services\adapter;
  3. use common\components\delivery\helpers\HttpClient;
  4. use common\components\delivery\helpers\SignHelper;
  5. use Yii;
  6. /**
  7. * 达达(DaDa)配送平台适配器
  8. *
  9. * 基于达达开放平台 API 接口规范进行封装
  10. * API 文档参考:达达开放平台 - 接口调用协议与签名算法
  11. */
  12. class DadaAdapter implements Adapter
  13. {
  14. // API 配置常量
  15. const API_VERSION = '1.0';
  16. const FORMAT = 'json';
  17. const RESPONSE_TYPE = 'code';
  18. // 测试环境和生产环境基础 URL
  19. const TEST_BASE_URL = 'https://newopen.qa.imdada.cn';
  20. const PROD_BASE_URL = 'https://newopen.imdada.cn';
  21. protected $baseUrl;
  22. protected $appKey;
  23. protected $appSecret;
  24. protected $sourceId;
  25. protected $isSandbox;
  26. /**
  27. * 初始化达达适配器
  28. * 根据运行环境加载对应的配置信息
  29. */
  30. public function __construct()
  31. {
  32. $isProduction = getenv('YII_ENV') == 'production';
  33. if ($isProduction) {
  34. // 生产环境配置(需要替换为实际的生产环境凭证)
  35. $this->baseUrl = self::PROD_BASE_URL;
  36. $this->appKey = 'dadaf2279d901a5ba40';
  37. $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
  38. $this->sourceId = '499021274';
  39. } else {
  40. // 测试环境配置(需要替换为实际的测试环境凭证)
  41. $this->baseUrl = self::TEST_BASE_URL;
  42. $this->appKey = 'dadaf2279d901a5ba40';
  43. $this->appSecret = '828a03677a1c00a3a5b3c59209c4433d';
  44. $this->sourceId = '499021274';
  45. }
  46. $this->isSandbox = !$isProduction;
  47. }
  48. /**
  49. * 获取已开通城市列表
  50. *
  51. * 通过此接口可获取到达达配送业务所有已开通的城市列表信息
  52. *
  53. * @return array 包含城市列表的响应数据
  54. * 示例结构:
  55. * [
  56. * 'status' => 'success',
  57. * 'code' => 0,
  58. * 'msg' => '成功',
  59. * 'result' => [
  60. * 'cityList' => [
  61. * [
  62. * 'cityCode' => '110101',
  63. * 'cityName' => '东城区',
  64. * 'provinceName' => '北京市',
  65. * ],
  66. * // ... 更多城市
  67. * ]
  68. * ]
  69. * ]
  70. */
  71. public function cityList()
  72. {
  73. // 业务参数为空时使用空字符串
  74. $body = '';
  75. // 构建完整的 API 请求参数
  76. $payload = $this->buildRequestPayload('cityList', $body);
  77. // 发送 POST 请求
  78. $resp = HttpClient::post("{$this->baseUrl}/cityList", $payload);
  79. return $resp;
  80. }
  81. /**
  82. * 创建订单(指派订单给配送员)
  83. *
  84. * 根据达达官方文档实现下单功能
  85. *
  86. * @param array $data 订单数据,结构参考:
  87. * [
  88. * 'order_id' => '订单编号' (必需), // 商户系统内部订单编号
  89. * 'shop_no' => '门店编号' (必需), // 门店编号
  90. * 'transporter_id' => 配送员ID (必需), // 指定配送员ID(可选,为空则由系统自动分配)
  91. * 'receiver_name' => '收货人名称' (可选),
  92. * 'receiver_address' => '收货地址' (可选),
  93. * 'receiver_phone' => '收货人电话' (可选),
  94. * 'receiver_lng' => 经度 (可选),
  95. * 'receiver_lat' => 纬度 (可选),
  96. * 'cargo_price' => 货物价格(分) (可选),
  97. * 'tips' => 小费(分) (可选),
  98. * 'distance' => 距离(米) (可选),
  99. * 'prepare_time' => 商品准备时间 (可选), // 单位:秒
  100. * 'remark' => '备注' (可选),
  101. * 'callback' => '回调地址' (可选),
  102. * ]
  103. * @return array API 响应结果
  104. */
  105. public function createOrder($data)
  106. {
  107. // 验证必需字段
  108. if (empty($data['order_id']) || empty($data['shop_no'])) {
  109. return [
  110. 'code' => -1,
  111. 'msg' => '缺少必需参数: order_id 和 shop_no'
  112. ];
  113. }
  114. // 构建业务参数(按达达 API 规范)
  115. $apiData = [
  116. 'order_id' => $data['order_id'],
  117. 'shop_no' => $data['shop_no'],
  118. ];
  119. // 如果指定了配送员ID,添加到请求
  120. if (isset($data['transporter_id'])) {
  121. $apiData['transporter_id'] = (int)$data['transporter_id'];
  122. }
  123. // 可选参数
  124. if (!empty($data['receiver_name'])) {
  125. $apiData['receiver_name'] = $data['receiver_name'];
  126. }
  127. if (!empty($data['receiver_address'])) {
  128. $apiData['receiver_address'] = $data['receiver_address'];
  129. }
  130. if (!empty($data['receiver_phone'])) {
  131. $apiData['receiver_phone'] = $data['receiver_phone'];
  132. }
  133. if (isset($data['receiver_lng']) && $data['receiver_lng'] !== '') {
  134. $apiData['receiver_lng'] = (double)$data['receiver_lng'];
  135. }
  136. if (isset($data['receiver_lat']) && $data['receiver_lat'] !== '') {
  137. $apiData['receiver_lat'] = (double)$data['receiver_lat'];
  138. }
  139. if (isset($data['cargo_price'])) {
  140. $apiData['cargo_price'] = (int)$data['cargo_price'];
  141. }
  142. if (isset($data['tips'])) {
  143. $apiData['tips'] = (int)$data['tips'];
  144. }
  145. if (isset($data['distance'])) {
  146. $apiData['distance'] = (int)$data['distance'];
  147. }
  148. if (isset($data['prepare_time'])) {
  149. $apiData['prepare_time'] = (int)$data['prepare_time'];
  150. }
  151. if (!empty($data['remark'])) {
  152. $apiData['remark'] = $data['remark'];
  153. }
  154. if (!empty($data['callback'])) {
  155. $apiData['callback'] = $data['callback'];
  156. }
  157. // 将业务参数转换为 JSON 字符串
  158. $body = json_encode($apiData, JSON_UNESCAPED_UNICODE);
  159. // 构建完整的 API 请求参数
  160. $payload = $this->buildRequestPayload('orderAddRequest', $body);
  161. // 发送 POST 请求
  162. $resp = HttpClient::post("{$this->baseUrl}/orderAddRequest", $payload);
  163. return $resp;
  164. }
  165. /**
  166. * 获取订单运费报价
  167. *
  168. * 根据达达官方文档实现订单运费估价功能
  169. *
  170. * @param array $data 运费估价参数,结构参考:
  171. * [
  172. * 'shop_no' => '门店编号' (必需),
  173. * 'origin_id' => '原始订单ID' (必需), // 商户侧订单编号
  174. * 'receiver_address' => '收货地址' (必需),
  175. * 'receiver_phone' => '收货人电话' (必需),
  176. * 'receiver_lng' => 经度 (必需),
  177. * 'receiver_lat' => 纬度 (必需),
  178. * 'cargo_price' => 货物价格(分) (可选), // 商品价格,单位为分
  179. * 'distance' => 距离(米) (可选),
  180. * 'prepare_time' => 商品准备时间 (可选), // 单位:秒
  181. * ]
  182. * @return array|null 运费报价结果
  183. */
  184. public function getPrice($data)
  185. {
  186. // 验证必需字段
  187. if (empty($data['shop_no']) || empty($data['origin_id']) ||
  188. empty($data['receiver_address']) || empty($data['receiver_phone']) ||
  189. !isset($data['receiver_lng']) || !isset($data['receiver_lat'])) {
  190. return [
  191. 'code' => -1,
  192. 'msg' => '缺少必需参数'
  193. ];
  194. }
  195. // 构建业务参数(按达达 API 规范)
  196. $apiData = [
  197. 'shop_no' => $data['shop_no'],
  198. 'origin_id' => $data['origin_id'],
  199. 'receiver_address' => $data['receiver_address'],
  200. 'receiver_phone' => $data['receiver_phone'],
  201. 'receiver_lng' => (double)$data['receiver_lng'],
  202. 'receiver_lat' => (double)$data['receiver_lat'],
  203. ];
  204. // 可选参数
  205. if (isset($data['cargo_price'])) {
  206. $apiData['cargo_price'] = (int)$data['cargo_price'];
  207. }
  208. if (isset($data['distance'])) {
  209. $apiData['distance'] = (int)$data['distance'];
  210. }
  211. if (isset($data['prepare_time'])) {
  212. $apiData['prepare_time'] = (int)$data['prepare_time'];
  213. }
  214. // 将业务参数转换为 JSON 字符串
  215. $body = json_encode($apiData, JSON_UNESCAPED_UNICODE);
  216. // 构建完整的 API 请求参数
  217. $payload = $this->buildRequestPayload('queryDeliverFee', $body);
  218. // 发送 POST 请求
  219. $resp = HttpClient::post("{$this->baseUrl}/queryDeliverFee", $payload);
  220. // 处理响应
  221. if (isset($resp['status']) && $resp['status'] === 'success' && $resp['code'] == 0) {
  222. $result = $resp['result'] ?? [];
  223. return [
  224. 'code' => 0,
  225. 'platform' => 'dada',
  226. 'deliver_fee' => $result['deliver_fee'] ?? 0, // 配送费(分)
  227. 'distance' => $result['distance'] ?? 0, // 距离(米)
  228. 'duration' => $result['duration'] ?? 0, // 配送时间(分钟)
  229. 'tips' => $result['tips'] ?? 0, // 小费(分)
  230. ];
  231. }
  232. return [
  233. 'code' => $resp['code'] ?? -1,
  234. 'msg' => $resp['msg'] ?? 'Unknown error',
  235. 'data' => null
  236. ];
  237. }
  238. /**
  239. * 构建达达 API 请求参数
  240. *
  241. * 根据达达 API 规范构建完整的请求参数,包括签名
  242. * 参数说明详见接口协议文档
  243. *
  244. * @param string $apiMethod 接口方法名(不包括基础 URL)
  245. * @param string $body 业务参数 JSON 字符串
  246. * @return array 完整的 POST 请求参数
  247. */
  248. protected function buildRequestPayload($apiMethod, $body = '')
  249. {
  250. // 获取当前时间戳(单位:秒)
  251. $timestamp = (string)time();
  252. // 构建待签名的参数(不包括 signature)
  253. $params = [
  254. 'app_key' => $this->appKey,
  255. 'v' => self::API_VERSION,
  256. 'format' => self::FORMAT,
  257. 'source_id' => $this->sourceId,
  258. 'timestamp' => $timestamp,
  259. 'body' => $body,
  260. ];
  261. // 计算签名(按达达 API 规范)
  262. $signature = $this->generateSignature($params);
  263. // 加入签名到请求参数
  264. $params['signature'] = $signature;
  265. return $params;
  266. }
  267. /**
  268. * 生成达达 API 签名
  269. *
  270. * 达达签名算法:
  271. * 1. 将所有参数(除 signature 外)按 key 升序排列
  272. * 2. 拼接为 key1value1key2value2...keyNvalueN 的形式
  273. * 3. 在字符串后面加上 app_secret
  274. * 4. 对结果进行 MD5 加密,得到 32 位大写字符串
  275. *
  276. * @param array $params 待签名参数(不包括 signature)
  277. * @return string 签名值(32 位大写)
  278. */
  279. protected function generateSignature($params)
  280. {
  281. // 按 key 升序排列参数
  282. ksort($params);
  283. // 拼接参数字符串
  284. $str = '';
  285. foreach ($params as $key => $value) {
  286. // 跳过空值
  287. if ($value === '' || $value === null) {
  288. continue;
  289. }
  290. $str .= $key . $value;
  291. }
  292. // 加上 app_secret
  293. $str .= $this->appSecret;
  294. // MD5 加密,返回 32 位大写字符串
  295. return strtoupper(md5($str));
  296. }
  297. /**
  298. * 生成随机数字符串(备用方法,如需要使用)
  299. *
  300. * @param int $length 长度
  301. * @return string 随机字符串
  302. */
  303. protected function generateNonceStr($length = 16)
  304. {
  305. $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  306. $str = '';
  307. for ($i = 0; $i < $length; $i++) {
  308. $str .= $chars[random_int(0, strlen($chars) - 1)];
  309. }
  310. return $str;
  311. }
  312. }