HuolalaAdapter.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <?php
  2. namespace common\components\delivery\services\adapter;
  3. use Yii;
  4. use common\components\delivery\helpers\HttpClient;
  5. use common\components\delivery\platform\huolala\Huolala;
  6. /**
  7. * 货拉拉适配器
  8. *
  9. * 基于货拉拉开放平台API进行封装
  10. * 文档参考:货拉拉API调用协议与签名算法
  11. */
  12. class HuolalaAdapter extends Huolala implements Adapter
  13. {
  14. /**
  15. * 获取已开通城市列表
  16. *
  17. * 通过此接口可获取到货拉拉货运业务所有已开通的城市列表信息
  18. *
  19. * @return array 包含城市列表的响应数据
  20. */
  21. public function cityList()
  22. {
  23. // 构建请求参数(无业务数据)
  24. $payload = $this->buildRequestPayload('u-city-list', []);
  25. $resp = HttpClient::post($this->baseUrl, $payload);
  26. $citiesArr = $resp['data']['city_list'];
  27. $newCitiesArr = [
  28. 'expire' => $resp['data']['expire'],
  29. ];
  30. foreach($citiesArr as $city) {
  31. $newCitiesArr[$city['name']] = [
  32. 'city_id' => $city['city_id'],
  33. 'city_name' => $city['name'],
  34. ];
  35. }
  36. // 把newCitiesArr保存到 platform/huolala/cities.php 文件中
  37. $citiesFile = Yii::getAlias('@common/components/delivery/platform/huolala/cities.php');
  38. // 如果文件不存在,则创建文件
  39. if (!file_exists($citiesFile)) {
  40. file_put_contents($citiesFile, '<?php namespace common\components\delivery\platform\huolala; return []; ?>');
  41. }
  42. // 文件内容要包含命名空间
  43. $content = '<?php namespace common\components\delivery\platform\huolala; return ' . var_export($newCitiesArr, true) . '; ?>';
  44. file_put_contents($citiesFile, $content);
  45. return $resp;
  46. }
  47. /**
  48. * 转化订单数据
  49. * @param $order
  50. * @param $orderType
  51. * @param $shop
  52. * @param $params
  53. * @return array
  54. * @throws \Exception
  55. */
  56. public function formatOrderData($order, $orderType, $shop, $params)
  57. {
  58. // 下单时必须优先使用估价时透传的时间,避免货拉拉核价失败。
  59. $orderTime = $params['order_time'] ?? null;
  60. if (empty($orderTime)) {
  61. $orderTime = strtotime($params['pickupTime']);
  62. if($orderTime < (time()+600) ) {
  63. $orderTime = time() + 600;
  64. }
  65. }
  66. $cityId = $params['city_id'];
  67. // 下单时必须优先使用估价时透传的地址
  68. $addrInfo = $params['addr_info'] ?? [
  69. [
  70. 'name' => $shop['merchantName'], //地址名称
  71. 'addr' => $shop['fullAddress'],
  72. 'lat_lon' => ['lat' => (float)$shop['lat'], 'lon' => (float)$shop['long']],
  73. 'city_id' => $cityId,
  74. 'city_name' => $shop['city'],
  75. 'district_name' => $shop['dist'],
  76. 'house_number' => $shop['floor'],
  77. 'contacts_name' => $shop['mobile'],
  78. 'contacts_phone_no' => $shop['mobile'],
  79. ],
  80. [
  81. 'name' => empty($order['showAddress']) ? $order['customName'] : $order['showAddress'],
  82. 'addr' => $order['fullAddress'],
  83. 'lat_lon' => ['lat' => (float)$order['lat'], 'lon' => (float)$order['long']],
  84. 'city_id' => $cityId,
  85. 'city_name' => $order['city'],
  86. 'district_name' => $order['dist'],
  87. 'house_number' => $order['floor'],
  88. 'contacts_name' => $order['customName'],
  89. 'contacts_phone_no' => Functions::getMobile($order),
  90. ]
  91. ];
  92. $apiData = [
  93. 'city_id' => $cityId,
  94. 'city_info_revision' => $params['city_info_revision'],
  95. 'order_vehicle_id' => $params['order_vehicle_id'],
  96. 'addr_info' => $addrInfo,
  97. 'vehicle_std' => $params['vehicle_std'] ?? [],
  98. 'spec_req' => $params['spec_req'],
  99. 'order_time' => (int)$orderTime,
  100. 'contact_name' => $shop['merchantName'],
  101. 'contact_phone_no' => $shop['mobile'],
  102. 'out_user_id' => $params['mainId'], //下单人在服务商平台侧的唯一ID,如果没有,可以传0
  103. 'ip' => $params['ip'],
  104. 'remark' => $params['remark'], //string(300)
  105. 'total_price_fen' => $params['total_price_fen'],
  106. 'pay_type' => 4,
  107. 'price_calculate_id' => $params['price_calculate_id'], //计价id,从计价接口返回中取值
  108. 'price_item_encryption' => $params['price_item_encryption'], //估价返回的费用项,从计价接口返回中取值
  109. 'vehicle_attr' => $params['vehicle_attr'], //大小车属性,从计价接口返回中取值
  110. 'commodity_info' => $params['commodity_info'], //商品信息(直接透传u-price-multiCalculate返回的commodity_item字段)
  111. 'invoice_type' => $params['invoice_type']
  112. ];
  113. if (isset($params['coupon_id'])) {
  114. $apiData['coupon_id'] = $params['coupon_id'];
  115. }
  116. // 添加订单消息回调地址
  117. $apiData['notify_url'] = $this->notifyUrl;
  118. // 跑腿车型,传这个值会出现"核价失败",所以要去除
  119. if ($params['type'] == '跑腿') {
  120. unset($apiData['spec_req']);
  121. }
  122. return $apiData;
  123. }
  124. /**
  125. * 创建订单(需用户授权)
  126. *
  127. * 根据货拉拉官方文档实现下单功能
  128. *
  129. * @param array $data 订单数据
  130. * @return array API响应结果
  131. */
  132. public function createOrder($order)
  133. {
  134. // 构建地址信息
  135. $addrInfo = [];
  136. if (!empty($order['addr_info']) && is_array($order['addr_info'])) {
  137. foreach ($order['addr_info'] as $addr) {
  138. $addrInfo[] = [
  139. 'name' => $addr['name'] ?? '',
  140. 'addr' => $addr['addr'] ?? '',
  141. 'city_id' => (int)($addr['city_id'] ?? 0),
  142. 'city_name' => $addr['city_name'] ?? '',
  143. 'district_name' => $addr['district_name'] ?? '',
  144. 'house_number' => $addr['house_number'] ?? '',
  145. 'contacts_name' => $addr['contacts_name'] ?? '',
  146. 'contacts_phone_no' => $addr['contacts_phone_no'] ?? '',
  147. 'lat_lon' => [
  148. 'lat' => (double)($addr['lat_lon']['lat'] ?? 0),
  149. 'lon' => (double)($addr['lat_lon']['lon'] ?? 0),
  150. ]
  151. ];
  152. }
  153. }
  154. // 构建业务数据
  155. $apiData = [
  156. 'city_id' => (int)($order['city_id'] ?? 0),
  157. 'city_info_revision' => (int)($order['city_info_revision'] ?? 0),
  158. 'order_vehicle_id' => (int)$order['order_vehicle_id'],
  159. 'addr_info' => $addrInfo,
  160. 'contact_name' => $order['contact_name'] ?? '',
  161. 'contact_phone_no' => $order['contact_phone_no'] ?? '',
  162. 'total_price_fen' => (int)$order['total_price_fen'],
  163. 'pay_type' => $order['pay_type'],
  164. 'price_calculate_id' => $order['price_calculate_id'] ?? '',
  165. 'price_item_encryption' => $order['price_item_encryption'] ?? '',
  166. 'vehicle_attr' => (int)($order['vehicle_attr'] ?? 0),
  167. 'order_time' => (int)$order['order_time'],
  168. ];
  169. // 可选参数
  170. if (!empty($order['vehicle_std'])) {
  171. $apiData['vehicle_std'] = is_array($order['vehicle_std']) ? $order['vehicle_std'] : [$order['vehicle_std']];
  172. }
  173. if (!empty($order['spec_req']) && is_array($order['spec_req'])) {
  174. $apiData['spec_req'] = array_map('intval', $order['spec_req']);
  175. }
  176. if (!empty($order['remark'])) {
  177. $apiData['remark'] = $order['remark'];
  178. }
  179. if (!empty($order['out_user_id'])) {
  180. $apiData['out_user_id'] = $order['out_user_id'];
  181. }
  182. if (!empty($order['ip'])) {
  183. $apiData['ip'] = $order['ip'];
  184. }
  185. if (!empty($order['notify_url'])) {
  186. $apiData['notify_url'] = $order['notify_url'];
  187. }
  188. if (isset($order['coupon_id'])) {
  189. $apiData['coupon_id'] = intval($order['coupon_id']);
  190. }
  191. if (isset($order['invoice_type'])) {
  192. $apiData['invoice_type'] = (int)$order['invoice_type'];
  193. }
  194. if (isset($order['order_service_type'])) {
  195. $apiData['order_service_type'] = (int)$order['order_service_type'];
  196. }
  197. if (!empty($order['ref'])) {
  198. $apiData['ref'] = $order['ref'];
  199. }
  200. if (!empty($order['themis_data'])) {
  201. $apiData['themis_data'] = $order['themis_data'];
  202. }
  203. if (!empty($order['commodity_info'])) {
  204. $apiData['commodity_info'] = $order['commodity_info'];
  205. }
  206. // 构建请求参数
  207. $payload = $this->buildRequestPayload(
  208. 'u-order-request',
  209. $apiData,
  210. $this->accessToken
  211. );
  212. // 发送请求
  213. $resp = HttpClient::post($this->baseUrl, $payload, ['Content-type'=>'application/json']);
  214. // 处理响应
  215. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  216. $respData = $resp['data'];
  217. return [
  218. 'code' => 0,
  219. 'platform' => 'huolala',
  220. 'data' => [
  221. 'orderId' => $respData['order_uuid'], // 订单UUID
  222. 'fee' => $respData['price_fen'] ?? 0, // 订单金额(分)
  223. 'distance' => 0,
  224. // --------
  225. 'order_display_id' => $respData['order_display_id'] ?? '', // 订单号
  226. 'pay_notify_url' => $respData['pay_notify_url'] ?? null, // 支付回调地址
  227. 'pay_mch_id' => $respData['pay_mch_id'] ?? null, // 银联商户号
  228. 'pay_serv_mch_id' => $respData['pay_serv_mch_id'] ?? null, // 银联服务商号
  229. 'pay_no' => $respData['pay_no'] ?? null, // 银联支付单号
  230. 'pay_time_expire' => $respData['pay_time_expire'] ?? null, // 支付超时时间
  231. ]
  232. ];
  233. }
  234. if(in_array($resp['ret'], [30010, 30011, 31006])){ // 错误状态码
  235. $this->cityList(); // 刷新城市列表信息
  236. }
  237. return [
  238. 'code' => $resp['ret'] ?? -1,
  239. 'msg' => $resp['msg'] ?? 'Unknown error',
  240. 'data' => null
  241. ];
  242. }
  243. public function cancelOrder($orderUuid, $post)
  244. {
  245. if(isset($post['order_cancel_reason']) && $post['order_cancel_reason'] != ''){
  246. $reason = $post['order_cancel_reason'];
  247. }else{
  248. return [
  249. 'code' => -1,
  250. 'msg' => '取消原因描述参数出错'
  251. ];
  252. }
  253. $res = $this->selectOrder($orderUuid);
  254. if ($res['ret'] != 0) {
  255. new \Exception('查询订单详情失败, code=' . $res['ret'] . ', msg=' . $res['msg']);
  256. }
  257. $status = $res['data']['order_status'];
  258. $apiData = [
  259. 'order_display_id' => $orderUuid,
  260. 'reason' => $reason,
  261. 'order_status' => $status
  262. ];
  263. // 构建请求参数
  264. $payload = $this->buildRequestPayload(
  265. 'u-order-cancel',
  266. $apiData,
  267. $this->accessToken
  268. );
  269. // 发送请求
  270. $resp = HttpClient::post($this->baseUrl, $payload);
  271. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  272. $respData = $resp['data'];
  273. return [
  274. 'code' => 0,
  275. 'platform' => 'huolala',
  276. 'data' => [
  277. 'deductionFee' => 0, //取消收费金额(单位:分)
  278. 'order_status' => $respData['order_status']
  279. ]
  280. ];
  281. }
  282. return [
  283. 'code' => $resp['ret'] ?? -1,
  284. 'platform' => 'huolala',
  285. 'msg' => $resp['msg'] ?? 'Unknown error',
  286. 'data' => null
  287. ];
  288. }
  289. /**
  290. * 查询订单详情
  291. * @param string $orderUuid 货拉拉订单号 -- 文档上写的是 order_display_id,实则要传 order_uuid
  292. * @return array
  293. */
  294. public function selectOrder($orderUuid)
  295. {
  296. $apiData = [
  297. 'order_display_id' => $orderUuid
  298. ];
  299. // 构建请求参数
  300. $payload = $this->buildRequestPayload(
  301. 'u-order-detail',
  302. $apiData,
  303. $this->accessToken
  304. );
  305. // 发送请求
  306. $resp = HttpClient::post($this->baseUrl, $payload);
  307. return $resp;
  308. }
  309. /**
  310. * 构建多车型报价请求信息列表(供并发请求使用)
  311. *
  312. * @param array $data 包含 vehicle_type_list 的数据
  313. * @param string $orderTime 配送时间
  314. * @return array 请求信息列表,以车型ID为键
  315. */
  316. public function buildPriceRequest($data, $orderTime)
  317. {
  318. //用车时间 -- 用车时间(unix时间戳:秒)。 取值范围:(当前时间+10分钟)<=用车时间<=(当前时间+5天)
  319. $orderTime = strtotime($orderTime);
  320. if($orderTime < (time()+600)) {
  321. $orderTime = time() + 600;
  322. }
  323. $requests = [];
  324. foreach($data['vehicle_type_list'] as $vehicle) {
  325. // 构建地址信息
  326. $addrInfo = [];
  327. if (!empty($vehicle['addr_info']) && is_array($vehicle['addr_info'])) {
  328. foreach ($vehicle['addr_info'] as $addr) {
  329. $addrInfo[] = [
  330. 'name' => $addr['name'],
  331. 'addr' => $addr['addr'],
  332. 'city_id' => (int)$addr['city_id'],
  333. 'city_name' => $addr['city_name'],
  334. 'district_name' => $addr['district_name'],
  335. 'house_number' => $addr['house_number'],
  336. 'contacts_name' => $addr['contacts_name'],
  337. 'contacts_phone_no' => $addr['contacts_phone_no'],
  338. 'lat_lon' => [
  339. 'lat' => (double)($addr['lat_lon']['lat'] ?? 0),
  340. 'lon' => (double)($addr['lat_lon']['lon'] ?? 0),
  341. ]
  342. ];
  343. }
  344. }
  345. $apiData = [
  346. 'city_id' => (int)$data['city_id'],
  347. 'order_vehicle_id' => (int)$vehicle['order_vehicle_id'],
  348. 'city_info_revision' => (int)$vehicle['city_info_revision'],
  349. 'order_time' => $orderTime,
  350. 'addr_info' => $addrInfo,
  351. ];
  352. if (!empty($vehicle['vehicle_std'])) {
  353. $apiData['vehicle_std'] = is_array($vehicle['vehicle_std']) ? $vehicle['vehicle_std'] : [$vehicle['vehicle_std']];
  354. }
  355. if (!empty($vehicle['spec_req'])) {
  356. $apiData['spec_req'] = is_array($vehicle['spec_req']) ? $vehicle['spec_req'] : [$vehicle['spec_req']];
  357. }
  358. if (isset($vehicle['coupon_id']) && !empty($vehicle['coupon_id'])) {
  359. $apiData['coupon_id'] = $vehicle['coupon_id'];
  360. }
  361. if (isset($vehicle['invoice_type'])) {
  362. $apiData['invoice_type'] = (int)$vehicle['invoice_type'];
  363. }
  364. if (isset($vehicle['order_service_type'])) {
  365. $apiData['order_service_type'] = (int)$vehicle['order_service_type'];
  366. }
  367. $payload = $this->buildRequestPayload(
  368. 'u-price-multiCalculate',
  369. $apiData,
  370. $this->accessToken
  371. );
  372. // 使用车型ID作为请求的唯一标识
  373. $vehicleId = $vehicle['order_vehicle_id'];
  374. $requests["huolala_vehicle_{$vehicleId}"] = [
  375. 'url' => $this->baseUrl,
  376. 'data' => $payload,
  377. 'headers' => [],
  378. 'timeout' => 5.0,
  379. ];
  380. }
  381. return $requests;
  382. }
  383. /**
  384. * 处理货拉拉报价响应
  385. *
  386. * @param array $resp API 响应
  387. * @return array|null 解析后的报价结果
  388. */
  389. public function processPriceResponse($resp)
  390. {
  391. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  392. return $resp['data'];
  393. }
  394. Yii::error('huolala 报价失败:'.json_encode($resp));
  395. return [
  396. 'platform' => 'huolala',
  397. 'error' => isset($resp['msg']) ? $resp['msg'] : 'fail'
  398. ];
  399. }
  400. /**
  401. * @deprecated
  402. * 获取运费报价(多品类)- 需用户授权
  403. *
  404. * 根据货拉拉官方文档 u-price-multiCalculate 实现多品类估价功能
  405. *
  406. * @param array $data 估价参数
  407. * @param string $orderTime 配送时间
  408. * @return array|null 包含多个计价方案的报价结果
  409. * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代
  410. */
  411. public function getPrice($data, $orderTime)
  412. {
  413. $requests = $this->buildPriceRequest($data, $orderTime);
  414. $priceList = [];
  415. foreach ($requests as $requestInfo) {
  416. $resp = HttpClient::post($requestInfo['url'], $requestInfo['data']);
  417. $processed = $this->processPriceResponse($resp);
  418. if ($processed) {
  419. $priceList[] = $processed;
  420. }
  421. }
  422. return !empty($priceList) ? ['price_list' => $priceList] : null;
  423. }
  424. public function addTip($data)
  425. {
  426. $orderDisplayId = $data['orderId'] ?? null;
  427. $totalTipsFen = $data['tips'] ?? null;
  428. if (empty($orderDisplayId)) {
  429. return [
  430. 'code' => -1,
  431. 'platform' => 'huolala',
  432. 'msg' => 'order_display_id不能为空',
  433. 'data' => null,
  434. ];
  435. }
  436. if (!isset($totalTipsFen) || (int)$totalTipsFen <= 0) {
  437. return [
  438. 'code' => -1,
  439. 'platform' => 'huolala',
  440. 'msg' => 'total_tips_fen必须为正整数(单位:分)',
  441. 'data' => null,
  442. ];
  443. }
  444. $apiData = [
  445. 'order_display_id' => $orderDisplayId,
  446. 'total_tips_fen' => (int)$totalTipsFen,
  447. ];
  448. $payload = $this->buildRequestPayload(
  449. 'u-order-addTips',
  450. $apiData,
  451. $this->accessToken
  452. );
  453. $resp = HttpClient::post($this->baseUrl, $payload, ['Content-type' => 'application/json']);
  454. if (isset($resp['ret']) && $resp['ret'] == 0) {
  455. return [
  456. 'code' => 0,
  457. 'platform' => 'huolala',
  458. 'data' => $resp['data'] ?? [],
  459. ];
  460. }
  461. return [
  462. 'code' => $resp['ret'] ?? -1,
  463. 'platform' => 'huolala',
  464. 'msg' => $resp['msg'] ?? 'Unknown error',
  465. 'data' => null,
  466. ];
  467. }
  468. }