DidiAdapter.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. namespace common\components\delivery\services\adapter;
  3. use common\components\delivery\helpers\HttpClient;
  4. use common\components\delivery\platform\didi\Didi;
  5. use Yii;
  6. class DidiAdapter extends Didi implements Adapter
  7. {
  8. /**
  9. * 物品类型映射
  10. * Didi: FOOD, FLOWER, CAKE, FILE, FRUIT, ELECTRONIC, DRESS, MEDICINAL, TEXTILE, FRESH, CAR_PART, OTHER
  11. */
  12. const GOODS_TYPE_MAP = [
  13. '1' => 'FILE', // 文件
  14. '2' => 'FOOD', // 餐饮
  15. '3' => 'CAKE', // 蛋糕
  16. '4' => 'FLOWER', // 鲜花
  17. '5' => 'ELECTRONIC',// 数码
  18. '6' => 'MEDICINAL', // 医药
  19. '8' => 'OTHER', // 其他
  20. // 根据实际情况补充
  21. ];
  22. public function cityList()
  23. {
  24. // 滴滴快送暂无城市列表接口,返回空
  25. return [];
  26. }
  27. /**
  28. * 构建订单计费请求信息(供并发请求使用)
  29. * @param array $data 订单数据
  30. * @param string $pickupTime 取件时间
  31. * @return array 包含 url, data, headers, timeout 的请求信息
  32. */
  33. public function buildPriceRequest($data, $pickupTime)
  34. {
  35. $sender = $data['sender'];
  36. // 滴滴目前主要支持单点对单点,取第一个收件人
  37. $receiver = $data['receiverList'][0] ?? [];
  38. if (empty($receiver)) {
  39. return [];
  40. }
  41. // 服务参数列表 (询价时可以传入多个,获取不同服务的价格)
  42. // 默认请求快送(BizType=12)
  43. $serviceCategoryList = [
  44. [
  45. 'bizType' => 12, // 业务类型 -- 快送
  46. 'carType' => 1018, // 车型 -- 两轮车
  47. 'serviceLevel' => 0, // 服服务等级
  48. 'serviceType' => 1, // 服务类型(快送专用,拉货不需要传) -- 直送
  49. ],
  50. [
  51. 'bizType' => 12, // 业务类型 -- 快送
  52. 'carType' => 1018, // 车型 -- 两轮车
  53. 'serviceLevel' => 0, // 服服务等级
  54. 'serviceType' => 0, // 服务类型(快送专用,拉货不需要传) -- 拼送
  55. ],
  56. [
  57. 'bizType' => 12,
  58. 'carType' => 1017, // 小轿车
  59. 'serviceLevel' => 0,
  60. 'serviceType' => 1, // 服务类型(快送专用,拉货不需要传) -- 直送
  61. ],
  62. [
  63. 'bizType' => 12,
  64. 'carType' => 1017, // 小轿车
  65. 'serviceLevel' => 0,
  66. 'serviceType' => 0, // 服务类型(快送专用,拉货不需要传) -- 拼送
  67. ],
  68. // 普通货运业务类型
  69. [
  70. 'bizType' => 0, // 业务类型 -- 普通货运
  71. 'carType' => 1020, // 车型 -- 三轮车
  72. 'serviceLevel' => 0,
  73. //'serviceType' => 1,
  74. ],
  75. [
  76. 'bizType' => 0, // 业务类型 -- 普通货运
  77. 'carType' => 1012, // 车型 -- 微面
  78. 'serviceLevel' => 0,
  79. //'serviceType' => 1,
  80. ],
  81. [
  82. 'bizType' => 0, // 业务类型 -- 普通货运
  83. 'carType' => 1001, // 车型 -- 小面包
  84. 'serviceLevel' => 0,
  85. //'serviceType' => 1,
  86. ],
  87. ];
  88. //$ret = $this->queryCityInfo(null, (float)$receiver['toLongitude'], (float)$receiver['toLatitude']);
  89. // 构造起点信息
  90. $fromAddress = [
  91. 'name' => $sender['fromSenderName'],
  92. 'phone' => $sender['fromMobile'],
  93. 'poiName' => $sender['fromAddress'],
  94. 'poiAddress' => $sender['fromAddress'],
  95. 'addressInfo' => $sender['fromAddressDetail'],
  96. 'latitude' => (float)$sender['fromLatitude'],
  97. 'longitude' => (float)$sender['fromLongitude'],
  98. 'virtualPhoneType' => 0, // 0-真实号码
  99. ];
  100. // 构造终点信息
  101. $toAddress = [
  102. 'name' => $receiver['toReceiverName'],
  103. 'phone' => $receiver['toMobile'],
  104. 'poiName' => $receiver['toAddress'],
  105. 'poiAddress' => $receiver['toAddress'],
  106. 'addressInfo' => $receiver['toAddressDetail'],
  107. 'latitude' => (float)$receiver['toLatitude'],
  108. 'longitude' => (float)$receiver['toLongitude'],
  109. 'virtualPhoneType' => 0,
  110. ];
  111. // 物品信息
  112. $weight = (int)($receiver['weight'] ?? 1); // kg
  113. $weightGram = $weight * 1000; // 转为克
  114. $goodTypeInt = $receiver['goodType'] ?? '8';
  115. $goodsType = self::GOODS_TYPE_MAP[$goodTypeInt] ?? 'FLOWER';
  116. // 预约单处理
  117. $modeType = 1; // 1-实时单
  118. $useTime = null;
  119. if ($pickupTime) {
  120. $ts = strtotime($pickupTime);
  121. if ($ts > (time() + 300)) { // 5分钟后算预约? 滴滴快送暂不支持预约单,只能应用于普通货运
  122. $modeType = 2;// 变更为2-预约单
  123. $useTime = $ts * 1000;
  124. }
  125. }
  126. // ------------------ 入参构建 ------------------
  127. $businessParams = [
  128. 'thirdUid' => $this->thirdUid, // 使用发件人手机作为接入方用户唯一标识
  129. 'serviceCategoryList' => $serviceCategoryList,
  130. 'fromAddress' => $fromAddress,
  131. 'toAddress' => $toAddress,
  132. 'goodsType' => $goodsType,
  133. 'weight' => $weightGram,
  134. 'needPickupCode' => false,
  135. 'needReceiveCode' => false,
  136. 'modeType' => $modeType,
  137. ];
  138. if ($useTime) {
  139. $businessParams['useTime'] = $useTime;
  140. }
  141. // 构建请求Payload
  142. $payload = $this->buildRequestPayload($businessParams);
  143. $apiUrl = $this->getApiUrl('freight.open.platform.channel.standard.estimate');
  144. $request = [
  145. 'url' => $apiUrl,
  146. 'data' => $payload,
  147. 'headers' => ["Content-Type" => "application/x-www-form-urlencoded"],
  148. 'timeout' => 10.0,
  149. ];
  150. //Yii::info('滴滴构建运费报价请求信息 -- ' .json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  151. return $request;
  152. }
  153. /**
  154. * 处理询价响应
  155. */
  156. public function processPriceResponse($resp)
  157. {
  158. if (isset($resp['code']) && $resp['code'] == 200 && isset($resp['data'])) {
  159. $data = $resp['data'];
  160. $estimateList = $data['estimateList'] ?? [];
  161. if (empty($estimateList)) {
  162. return [
  163. 'code' => -1,
  164. 'platform' => 'didi',
  165. 'msg' => '没有预估结果列表',
  166. 'data' => null
  167. ];
  168. }
  169. // 选取第一个结果,或者根据策略选取(比如最便宜) TODO
  170. // 这里简单取第一个
  171. $estimate = $estimateList[0];
  172. return [
  173. 'platform' => 'didi',
  174. 'estimateList' => $estimateList,
  175. 'estimate_id' => $estimate['estimateId'], // 预估ID,发单用
  176. 'total_distance' => $estimate['distance'] ?? 0,
  177. 'total_amount' => $estimate['fee'] ?? 0, // 分
  178. 'total_fee_after_save' => $estimate['fee'] ?? 0,
  179. 'coupon_save_fee' => $estimate['discountFee'] ?? 0,
  180. 'fee_info_list' => $estimate['feeList'] ?? [],
  181. // 补充其他字段以匹配通用结构
  182. 'total_weight' => 0, // 响应里没返回重量
  183. ];
  184. }
  185. return [
  186. 'code' => $resp['code'] ?? -1,
  187. 'platform' => 'didi',
  188. 'msg' => $resp['msg'] ?? 'Unknown error',
  189. 'data' => null
  190. ];
  191. }
  192. /**
  193. * 转化订单数据
  194. * @param $order
  195. * @param $orderType
  196. * @param $shop
  197. * @param $params
  198. * @return array
  199. * @throws \Exception
  200. */
  201. public function formatOrderData($order, $orderType, $shop, $params)
  202. {
  203. // 默认实时单,拉货生效,快送暂不支持预约单都按照实时单处理
  204. $timeDiff = strtotime($params['pickupTime']) - time();
  205. $modeType = 1;
  206. if ($timeDiff > 300) {
  207. $modeType = 2;
  208. }
  209. $sender = [
  210. 'fromSenderName' => $shop['merchantName'],
  211. 'fromMobile' => $shop['mobile'],
  212. 'fromAddress' => $shop['fullAddress'],
  213. 'fromAddressDetail' => $shop['floor'],
  214. 'fromLatitude' => (float)$shop['lat'],
  215. 'fromLongitude' => (float)$shop['long'],
  216. ];
  217. $receiver = [
  218. 'toReceiverName' => empty($order['showAddress']) ? $order['customName'] : $order['showAddress'],
  219. 'toMobile' => Functions::getMobile($order),
  220. 'toAddress' => $order['fullAddress'],
  221. 'toAddressDetail' => $order['floor'],
  222. 'toLatitude' => (float)$order['lat'],
  223. 'toLongitude' => (float)$order['long'],
  224. 'weight' => round(($order['weight'] ?? 1)) * 1000,
  225. 'goodType' => self::GOODS_TYPE_MAP['4'], // const GOODS_TYPE_MAP
  226. ];
  227. $apiData = [
  228. 'outOrderNo' => $order['orderSn'],
  229. 'estimateId' => $params['estimateId'] ?? null,
  230. 'sender' => $sender,
  231. 'receiver' => $receiver,
  232. 'remark' => $params['remark'] ?? $order['remark'] ?? '',
  233. 'orderSequence' => $order['sendNum'] // 取件流水号,方便寻找订单对应的花材
  234. ];
  235. if($modeType == 2){
  236. $apiData['modeType'] = 2;
  237. $apiData['useTime'] = strtotime($params['pickupTime']) * 1000;
  238. }
  239. return $apiData;
  240. }
  241. /**
  242. * 发单
  243. */
  244. public function createOrder($order)
  245. {
  246. // 解析 $order 数据,注意这里的 $order 结构可能与 buildPriceRequest 的 $data 不同
  247. // 通常 service 层会将 price check 的结果合并进来,或者前端传回 estimateId
  248. $estimateId = $order['estimateId'] ?? null;
  249. if (!$estimateId) {
  250. return [
  251. 'code' => -1,
  252. 'platform' => 'didi',
  253. 'msg' => '缺少 estimateId',
  254. 'data' => []
  255. ];
  256. }
  257. $sender = $order['sender'];
  258. $fromAddress = [
  259. 'name' => $sender['fromSenderName'],
  260. 'phone' => $sender['fromMobile'],
  261. 'poiName' => $sender['fromAddress'],
  262. 'poiAddress' => $sender['fromAddress'],
  263. 'addressInfo' => $sender['fromAddressDetail'],
  264. 'latitude' => (float)$sender['fromLatitude'],
  265. 'longitude' => (float)$sender['fromLongitude'],
  266. 'virtualPhoneType' => 0,
  267. ];
  268. $receiver = $order['receiver'];
  269. $toAddress = [
  270. 'name' => $receiver['toReceiverName'],
  271. 'phone' => $receiver['toMobile'],
  272. 'poiName' => $receiver['toAddress'],
  273. 'poiAddress' => $receiver['toAddress'],
  274. 'addressInfo' => $receiver['toAddressDetail'],
  275. 'latitude' => (float)$receiver['toLatitude'],
  276. 'longitude' => (float)$receiver['toLongitude'],
  277. 'virtualPhoneType' => 0,
  278. ];
  279. $weight = (int)($receiver['weight'] ?? 1);
  280. $weightGram = $weight * 1000;
  281. $goodsType = $receiver['goodType'] ?? 'OTHER';
  282. // ------------------ 入参构建 ------------------
  283. $timeStamp = time();
  284. $businessParams = [
  285. 'estimateIdList' => [$estimateId], // 预估ID
  286. 'outOrderNo' => self::generateUniOroder($order['outOrderNo'], $timeStamp), // 接入方订单号
  287. 'fromAddress' => $fromAddress,
  288. 'toAddress' => $toAddress,
  289. 'goodsType' => $goodsType,
  290. 'weight' => $weightGram,
  291. 'needPickupCode' => false,
  292. 'needReceiveCode' => false,
  293. 'thirdUid' => $this->thirdUid, // 接入方用户唯一标识。保持在渠道方唯一,会用于安全风控,【聚合模式】必须传
  294. 'remark' => $order['remark'] ?? '',
  295. 'orderSequence' => $order['orderSequence'] ?? '',
  296. ];
  297. if(isset($order['modeType']) && $order['modeType'] == 2){
  298. $businessParams['modeType'] = 2;
  299. $businessParams['useTime'] = $order['useTime'];
  300. }
  301. $payload = $this->buildRequestPayload($businessParams);
  302. $url = $this->getApiUrl('freight.open.platform.channel.standard.createOrder');
  303. $headers = ["Content-Type" => "application/x-www-form-urlencoded"];
  304. $resp = HttpClient::post($url, $payload, $headers);
  305. if (isset($resp['code']) && $resp['code'] == 200 && isset($resp['data'])) {
  306. $respData = $resp['data'];
  307. return [
  308. 'code' => 0,
  309. 'platform' => 'didi',
  310. 'data' => [
  311. 'orderId' => $respData['didiOrderNo'], // 滴滴订单号
  312. 'distance' => 0,
  313. 'fee' => 0,
  314. // 可以附带其他信息
  315. 'timeStamp' => $timeStamp,
  316. ]
  317. ];
  318. }
  319. $msg = $resp['msg'] ?? 'Unknown error';
  320. if (isset($resp['code']) && isset($resp['subCode'])) {
  321. $msg .= " ({$resp['code']}-{$resp['subCode']})";
  322. }
  323. Yii::error('[DidiAdapter] createOrder failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE));
  324. return [
  325. 'code' => -1,
  326. 'platform' => 'didi',
  327. 'msg' => $msg,
  328. 'data' => []
  329. ];
  330. }
  331. /**
  332. * 添加小费
  333. */
  334. public function addTip($data)
  335. {
  336. $outOrderNo = $data['orderId'] ?? null; // 这里假设传入的是接入方订单号,或者需要映射
  337. // 注意:ShansongAdapter用的是 issOrderNo (内部订单号),Didi addTip需要 outOrderNo (接入方订单号)
  338. // 假设 $data['orderId'] 就是 outOrderNo
  339. $fee = $data['tips'] ?? 0; // 分
  340. if (empty($outOrderNo)) {
  341. return ['code' => -1, 'platform' => 'didi', 'msg' => '订单号不能为空'];
  342. }
  343. if ($fee <= 0) {
  344. return ['code' => -1, 'platform' => 'didi', 'msg' => '小费金额必须大于0'];
  345. }
  346. $businessParams = [
  347. 'outOrderNo' => $outOrderNo,
  348. 'fee' => (int)$fee,
  349. ];
  350. $payload = $this->buildRequestPayload($businessParams);
  351. $url = $this->getApiUrl('freight.open.platform.channel.standard.addTipFee');
  352. $headers = ["Content-Type" => "application/x-www-form-urlencoded"];
  353. $resp = HttpClient::post($url, $payload, $headers);
  354. if (isset($resp['code']) && $resp['code'] == 0) {
  355. return [
  356. 'code' => 0,
  357. 'platform' => 'didi',
  358. 'data' => $resp['data'] ?? []
  359. ];
  360. }
  361. return [
  362. 'code' => $resp['code'] ?? -1,
  363. 'platform' => 'didi',
  364. 'msg' => $resp['msg'] ?? 'Unknown error',
  365. 'data' => null
  366. ];
  367. }
  368. /**
  369. * 取消订单
  370. *
  371. * @param int $didiOrderNo 滴滴订单号
  372. * @param int $post
  373. */
  374. public function cancelOrder($didiOrderNo, $post)
  375. {
  376. $businessParams = [
  377. 'outOrderNo' => (string)$post['outOrderNo'],
  378. 'didiOrderNo' => (int)$didiOrderNo,
  379. 'cancelSource' => $post['cancelSource'],
  380. //'cancelReason' => '暂不需要',
  381. ];
  382. $payload = $this->buildRequestPayload($businessParams);
  383. $url = $this->getApiUrl('freight.open.platform.channel.standard.cancel');
  384. $headers = ["Content-Type" => "application/x-www-form-urlencoded"];
  385. $resp = HttpClient::post($url, $payload, $headers);
  386. if (isset($resp['code']) && $resp['code'] == 200 && isset($resp['data'])) {
  387. $data = $resp['data'];
  388. return [
  389. 'code' => 0,
  390. 'platform' => 'didi',
  391. 'data' => [
  392. 'deductionFee' => $data['cancelFee'], // 分
  393. 'cancelFee' => $data['cancelFee'],
  394. 'cancelResult' => $data['cancelResult'],
  395. ]
  396. ];
  397. }
  398. return [
  399. 'code' => $resp['code'] ?? -1,
  400. 'platform' => 'didi',
  401. 'msg' => $resp['msg'] ?? 'Unknown error',
  402. 'data' => null
  403. ];
  404. }
  405. /**
  406. * 查询订单详情
  407. */
  408. public function selectOrder($orderId, $thirdOrderNo = '', $timeStamp)
  409. {
  410. $thirdOrderNo = Didi::generateUniOroder($thirdOrderNo, $timeStamp);
  411. $businessParams = [
  412. 'didiOrderNo' => (int)$orderId,
  413. 'outOrderNo' => $thirdOrderNo,
  414. ];
  415. $payload = $this->buildRequestPayload($businessParams);
  416. $url = $this->getApiUrl('freight.open.platform.channel.standard.orderInfo');
  417. $headers = ["Content-Type" => "application/x-www-form-urlencoded"];
  418. $resp = HttpClient::post($url, $payload, $headers);
  419. if (isset($resp['code']) && $resp['code'] == 200 && isset($resp['data'])) {
  420. return [
  421. 'code' => 0,
  422. 'platform' => 'didi',
  423. 'data' => $resp['data']
  424. ];
  425. }
  426. return [
  427. 'code' => $resp['code'] ?? -1,
  428. 'platform' => 'didi',
  429. 'msg' => $resp['msg'] ?? 'Unknown error',
  430. 'data' => null
  431. ];
  432. }
  433. }