HuolalaAdapter.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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. //用车时间 -- 用车时间(unix时间戳:秒)。 取值范围:(当前时间+10分钟)<=用车时间<=(当前时间+5天)
  59. $orderTime = strtotime($params['pickupTime']);
  60. if($orderTime < (time()+600) ) {
  61. $orderTime = time() + 600;
  62. }
  63. $cityId = $params['city_id'];
  64. $apiData = [
  65. 'city_id' => $cityId,
  66. 'city_info_revision' => $params['city_info_revision'],
  67. 'order_vehicle_id' => $params['order_vehicle_id'],
  68. '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. 'vehicle_std' => $params['vehicle_std'],
  93. 'spec_req' => $params['spec_req'],
  94. 'order_time' => $orderTime,
  95. 'contact_name' => $shop['merchantName'],
  96. 'contact_phone_no' => $shop['mobile'],
  97. 'out_user_id' => $params['mainId'], //下单人在服务商平台侧的唯一ID,如果没有,可以传0
  98. 'ip' => $params['ip'],
  99. 'remark' => $params['remark'], //string(300)
  100. 'total_price_fen' => $params['total_price_fen'],
  101. 'pay_type' => 4,
  102. 'price_calculate_id' => $params['price_calculate_id'], //计价id,从计价接口返回中取值
  103. 'price_item_encryption' => $params['price_item_encryption'], //估价返回的费用项,从计价接口返回中取值
  104. 'vehicle_attr' => $params['vehicle_attr'], //大小车属性,从计价接口返回中取值
  105. 'commodity_info' => $params['commodity_info'] //商品信息(直接透传u-price-multiCalculate返回的commodity_item字段)
  106. ];
  107. // 添加订单消息回调地址
  108. $apiData['notify_url'] = $this->notifyUrl;
  109. return $apiData;
  110. }
  111. /**
  112. * 创建订单(需用户授权)
  113. *
  114. * 根据货拉拉官方文档实现下单功能
  115. *
  116. * @param array $data 订单数据
  117. * @return array API响应结果
  118. */
  119. public function createOrder($order)
  120. {
  121. // 构建地址信息
  122. $addrInfo = [];
  123. if (!empty($order['addr_info']) && is_array($order['addr_info'])) {
  124. foreach ($order['addr_info'] as $addr) {
  125. $addrInfo[] = [
  126. 'name' => $addr['name'] ?? '',
  127. 'addr' => $addr['addr'] ?? '',
  128. 'city_id' => (int)($addr['city_id'] ?? 0),
  129. 'city_name' => $addr['city_name'] ?? '',
  130. 'district_name' => $addr['district_name'] ?? '',
  131. 'house_number' => $addr['house_number'] ?? '',
  132. 'contacts_name' => $addr['contacts_name'] ?? '',
  133. 'contacts_phone_no' => $addr['contacts_phone_no'] ?? '',
  134. 'lat_lon' => [
  135. 'lat' => (double)($addr['lat_lon']['lat'] ?? 0),
  136. 'lon' => (double)($addr['lat_lon']['lon'] ?? 0),
  137. ]
  138. ];
  139. }
  140. }
  141. // 构建业务数据
  142. $apiData = [
  143. 'city_id' => (int)($order['city_id'] ?? 0),
  144. 'city_info_revision' => (int)($order['city_info_revision'] ?? 0),
  145. 'order_vehicle_id' => (int)($order['order_vehicle_id'] ?? 0),
  146. 'addr_info' => $addrInfo,
  147. 'contact_name' => $order['contact_name'] ?? '',
  148. 'contact_phone_no' => $order['contact_phone_no'] ?? '',
  149. 'total_price_fen' => (int)($order['total_price_fen'] ?? 0),
  150. 'pay_type' => (int)($order['pay_type'] ?? 0),
  151. 'price_calculate_id' => $order['price_calculate_id'] ?? '',
  152. 'price_item_encryption' => $order['price_item_encryption'] ?? '',
  153. 'vehicle_attr' => (int)($order['vehicle_attr'] ?? 0),
  154. 'order_time' => (int)($order['order_time'] ?? time()),
  155. ];
  156. // 可选参数
  157. if (!empty($order['vehicle_std'])) {
  158. $apiData['vehicle_std'] = is_array($order['vehicle_std']) ? $order['vehicle_std'] : [$order['vehicle_std']];
  159. }
  160. if (!empty($order['spec_req'])) {
  161. $apiData['spec_req'] = is_array($order['spec_req']) ? $order['spec_req'] : [$order['spec_req']];
  162. }
  163. if (!empty($order['remark'])) {
  164. $apiData['remark'] = $order['remark'];
  165. }
  166. if (!empty($order['out_user_id'])) {
  167. $apiData['out_user_id'] = $order['out_user_id'];
  168. }
  169. if (!empty($order['ip'])) {
  170. $apiData['ip'] = $order['ip'];
  171. }
  172. if (!empty($order['notify_url'])) {
  173. $apiData['notify_url'] = $order['notify_url'];
  174. }
  175. if (isset($order['coupon_id'])) {
  176. $apiData['coupon_id'] = $order['coupon_id'];
  177. }
  178. if (isset($order['invoice_type'])) {
  179. $apiData['invoice_type'] = (int)$order['invoice_type'];
  180. }
  181. if (isset($order['order_service_type'])) {
  182. $apiData['order_service_type'] = (int)$order['order_service_type'];
  183. }
  184. if (!empty($order['ref'])) {
  185. $apiData['ref'] = $order['ref'];
  186. }
  187. if (!empty($order['themis_data'])) {
  188. $apiData['themis_data'] = $order['themis_data'];
  189. }
  190. if (!empty($order['commodity_info'])) {
  191. $apiData['commodity_info'] = $order['commodity_info'];
  192. }
  193. // 构建请求参数
  194. $payload = $this->buildRequestPayload(
  195. 'u-order-request',
  196. $apiData,
  197. $this->accessToken
  198. );
  199. // 发送请求
  200. $resp = HttpClient::post($this->baseUrl, $payload, ['Content-type'=>'application/json']);
  201. // 处理响应
  202. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  203. $respData = $resp['data'];
  204. return [
  205. 'code' => 0,
  206. 'platform' => 'huolala',
  207. 'data' => [
  208. 'orderId' => $respData['order_uuid'], // 订单UUID
  209. 'fee' => $respData['price_fen'] ?? 0, // 订单金额(分)
  210. 'distance' => 0,
  211. // --------
  212. 'order_display_id' => $respData['order_display_id'] ?? '', // 订单号
  213. 'pay_notify_url' => $respData['pay_notify_url'] ?? null, // 支付回调地址
  214. 'pay_mch_id' => $respData['pay_mch_id'] ?? null, // 银联商户号
  215. 'pay_serv_mch_id' => $respData['pay_serv_mch_id'] ?? null, // 银联服务商号
  216. 'pay_no' => $respData['pay_no'] ?? null, // 银联支付单号
  217. 'pay_time_expire' => $respData['pay_time_expire'] ?? null, // 支付超时时间
  218. ]
  219. ];
  220. }
  221. if(in_array($resp['ret'], [30010, 30011, 31006])){ // 错误状态码
  222. $this->cityList(); // 刷新城市列表信息
  223. }
  224. return [
  225. 'code' => $resp['ret'] ?? -1,
  226. 'msg' => $resp['msg'] ?? 'Unknown error',
  227. 'data' => null
  228. ];
  229. }
  230. public function cancelOrder($orderUuid, $post)
  231. {
  232. if(isset($post['order_cancel_reason']) && $post['order_cancel_reason'] != ''){
  233. $reason = $post['order_cancel_reason'];
  234. }else{
  235. return [
  236. 'code' => -1,
  237. 'msg' => '取消原因描述参数出错'
  238. ];
  239. }
  240. $res = $this->selectOrder($orderUuid);
  241. if ($res['ret'] != 0) {
  242. new \Exception('查询订单详情失败, code=' . $res['ret'] . ', msg=' . $res['msg']);
  243. }
  244. $status = $res['data']['order_status'];
  245. $apiData = [
  246. 'order_display_id' => $orderUuid,
  247. 'reason' => $reason,
  248. 'order_status' => $status
  249. ];
  250. // 构建请求参数
  251. $payload = $this->buildRequestPayload(
  252. 'u-order-cancel',
  253. $apiData,
  254. $this->accessToken
  255. );
  256. // 发送请求
  257. $resp = HttpClient::post($this->baseUrl, $payload);
  258. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  259. $respData = $resp['data'];
  260. return [
  261. 'code' => 0,
  262. 'platform' => 'huolala',
  263. 'data' => [
  264. 'deductionFee' => 0, //取消收费金额(单位:分)
  265. 'order_status' => $respData['order_status']
  266. ]
  267. ];
  268. }
  269. return [
  270. 'code' => $resp['ret'] ?? -1,
  271. 'platform' => 'huolala',
  272. 'msg' => $resp['msg'] ?? 'Unknown error',
  273. 'data' => null
  274. ];
  275. }
  276. /**
  277. * 查询订单详情
  278. * @param string $orderUuid 货拉拉订单号 -- 文档上写的是 order_display_id,实则要传 order_uuid
  279. * @return array
  280. */
  281. public function selectOrder($orderUuid)
  282. {
  283. $apiData = [
  284. 'order_display_id' => $orderUuid
  285. ];
  286. // 构建请求参数
  287. $payload = $this->buildRequestPayload(
  288. 'u-order-detail',
  289. $apiData,
  290. $this->accessToken
  291. );
  292. // 发送请求
  293. $resp = HttpClient::post($this->baseUrl, $payload);
  294. return $resp;
  295. }
  296. /**
  297. * 构建多车型报价请求信息列表(供并发请求使用)
  298. *
  299. * @param array $data 包含 vehicle_type_list 的数据
  300. * @param string $orderTime 配送时间
  301. * @return array 请求信息列表,以车型ID为键
  302. */
  303. public function buildPriceRequest($data, $orderTime)
  304. {
  305. //用车时间 -- 用车时间(unix时间戳:秒)。 取值范围:(当前时间+10分钟)<=用车时间<=(当前时间+5天)
  306. $orderTime = strtotime($orderTime);
  307. if($orderTime < (time()+600)) {
  308. $orderTime = time() + 600;
  309. }
  310. $requests = [];
  311. foreach($data['vehicle_type_list'] as $vehicle) {
  312. // 构建地址信息
  313. $addrInfo = [];
  314. if (!empty($vehicle['addr_info']) && is_array($vehicle['addr_info'])) {
  315. foreach ($vehicle['addr_info'] as $addr) {
  316. $addrInfo[] = [
  317. 'name' => $addr['name'],
  318. 'addr' => $addr['addr'],
  319. 'city_id' => (int)$addr['city_id'],
  320. 'city_name' => $addr['city_name'],
  321. 'district_name' => $addr['district_name'],
  322. 'house_number' => $addr['house_number'],
  323. 'contacts_name' => $addr['contacts_name'],
  324. 'contacts_phone_no' => $addr['contacts_phone_no'],
  325. 'lat_lon' => [
  326. 'lat' => (double)($addr['lat_lon']['lat'] ?? 0),
  327. 'lon' => (double)($addr['lat_lon']['lon'] ?? 0),
  328. ]
  329. ];
  330. }
  331. }
  332. $apiData = [
  333. 'city_id' => (int)$data['city_id'],
  334. 'order_vehicle_id' => (int)$vehicle['order_vehicle_id'],
  335. 'city_info_revision' => (int)$vehicle['city_info_revision'],
  336. 'order_time' => $orderTime,
  337. 'addr_info' => $addrInfo,
  338. ];
  339. if (!empty($vehicle['vehicle_std'])) {
  340. $apiData['vehicle_std'] = is_array($vehicle['vehicle_std']) ? $vehicle['vehicle_std'] : [$vehicle['vehicle_std']];
  341. }
  342. if (!empty($vehicle['spec_req'])) {
  343. $apiData['spec_req'] = is_array($vehicle['spec_req']) ? $vehicle['spec_req'] : [$vehicle['spec_req']];
  344. }
  345. if (isset($vehicle['coupon_id']) && !empty($vehicle['coupon_id'])) {
  346. $apiData['coupon_id'] = $vehicle['coupon_id'];
  347. }
  348. if (isset($vehicle['invoice_type'])) {
  349. $apiData['invoice_type'] = (int)$vehicle['invoice_type'];
  350. }
  351. if (isset($vehicle['order_service_type'])) {
  352. $apiData['order_service_type'] = (int)$vehicle['order_service_type'];
  353. }
  354. $payload = $this->buildRequestPayload(
  355. 'u-price-multiCalculate',
  356. $apiData,
  357. $this->accessToken
  358. );
  359. // 使用车型ID作为请求的唯一标识
  360. $vehicleId = (int)($vehicle['order_vehicle_id'] ?? 0);
  361. $requests["huolala_vehicle_{$vehicleId}"] = [
  362. 'url' => $this->baseUrl,
  363. 'data' => $payload,
  364. 'headers' => [],
  365. 'timeout' => 5.0,
  366. ];
  367. }
  368. return $requests;
  369. }
  370. /**
  371. * 处理货拉拉报价响应
  372. *
  373. * @param array $resp API 响应
  374. * @return array|null 解析后的报价结果
  375. */
  376. public function processPriceResponse($resp)
  377. {
  378. if (isset($resp['ret']) && $resp['ret'] == 0 && isset($resp['data'])) {
  379. return $resp['data'];
  380. }
  381. Yii::error('huolala 报价失败:'.json_encode($resp));
  382. return [
  383. 'platform' => 'huolala',
  384. 'error' => isset($resp['msg']) ? $resp['msg'] : 'fail'
  385. ];
  386. }
  387. /**
  388. * @deprecated
  389. * 获取运费报价(多品类)- 需用户授权
  390. *
  391. * 根据货拉拉官方文档 u-price-multiCalculate 实现多品类估价功能
  392. *
  393. * @param array $data 估价参数
  394. * @param string $orderTime 配送时间
  395. * @return array|null 包含多个计价方案的报价结果
  396. * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代
  397. */
  398. public function getPrice($data, $orderTime)
  399. {
  400. $requests = $this->buildPriceRequest($data, $orderTime);
  401. $priceList = [];
  402. foreach ($requests as $requestInfo) {
  403. $resp = HttpClient::post($requestInfo['url'], $requestInfo['data']);
  404. $processed = $this->processPriceResponse($resp);
  405. if ($processed) {
  406. $priceList[] = $processed;
  407. }
  408. }
  409. return !empty($priceList) ? ['price_list' => $priceList] : null;
  410. }
  411. public function addTip($data)
  412. {
  413. $orderDisplayId = $data['orderId'] ?? null;
  414. $totalTipsFen = $data['tips'] ?? null;
  415. if (empty($orderDisplayId)) {
  416. return [
  417. 'code' => -1,
  418. 'platform' => 'huolala',
  419. 'msg' => 'order_display_id不能为空',
  420. 'data' => null,
  421. ];
  422. }
  423. if (!isset($totalTipsFen) || (int)$totalTipsFen <= 0) {
  424. return [
  425. 'code' => -1,
  426. 'platform' => 'huolala',
  427. 'msg' => 'total_tips_fen必须为正整数(单位:分)',
  428. 'data' => null,
  429. ];
  430. }
  431. $apiData = [
  432. 'order_display_id' => $orderDisplayId,
  433. 'total_tips_fen' => (int)$totalTipsFen,
  434. ];
  435. $payload = $this->buildRequestPayload(
  436. 'u-order-addTips',
  437. $apiData,
  438. $this->accessToken
  439. );
  440. $resp = HttpClient::post($this->baseUrl, $payload, ['Content-type' => 'application/json']);
  441. if (isset($resp['ret']) && $resp['ret'] == 0) {
  442. return [
  443. 'code' => 0,
  444. 'platform' => 'huolala',
  445. 'data' => $resp['data'] ?? [],
  446. ];
  447. }
  448. return [
  449. 'code' => $resp['ret'] ?? -1,
  450. 'platform' => 'huolala',
  451. 'msg' => $resp['msg'] ?? 'Unknown error',
  452. 'data' => null,
  453. ];
  454. }
  455. }