ShansongAdapter.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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 common\components\delivery\platform\shansong\Shansong;
  6. use Yii;
  7. class ShansongAdapter extends Shansong implements Adapter
  8. {
  9. public function cityList()
  10. {
  11. $payload = [
  12. "clientId" => $this->clientId,
  13. "accessToken" => $this->accessToken,
  14. "timestamp" => (int) (microtime(true) * 1000),
  15. ];
  16. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  17. $payload['sign'] = $sign;
  18. $headers = ["Content-Type" => "application/x-www-form-urlencoded;charset=utf-8"];
  19. $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/openCitiesLists", $payload, $headers);
  20. $newCitiesArr = [];
  21. $citiesArr = $resp['data'];
  22. foreach($citiesArr as $block) {
  23. foreach ($block['cities'] as $city) {
  24. $newCitiesArr[$city['name']] = $city;
  25. }
  26. }
  27. // 把newCitiesArr保存到 platform/huolala/cities.php 文件中
  28. $citiesFile = Yii::getAlias('@common/components/delivery/platform/shansong/cities.php');
  29. // 如果文件不存在,则创建文件
  30. if (!file_exists($citiesFile)) {
  31. file_put_contents($citiesFile, '<?php return []; ?>');
  32. }
  33. // 文件内容要包含命名空间
  34. $content = '<?php return ' . var_export($newCitiesArr, true) . '; ?>';
  35. file_put_contents($citiesFile, $content);
  36. return $resp;
  37. }
  38. /**
  39. * 转化订单数据
  40. * @param $order
  41. * @param $orderType
  42. * @param $shop
  43. * @param $params
  44. * @return array
  45. */
  46. public function formatOrderData($order, $orderType, $shop, $params)
  47. {
  48. $orderData = ['issOrderNo' => $params['issOrderNo']];
  49. return $orderData;
  50. }
  51. // 提交订单
  52. public function createOrder($data)
  53. {
  54. $payload = [
  55. "clientId" => $this->clientId,
  56. "accessToken" => $this->accessToken,
  57. "timestamp" => (int) (microtime(true) * 1000),
  58. "data" => json_encode($data, JSON_UNESCAPED_UNICODE),
  59. ];
  60. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  61. $payload['sign'] = $sign;
  62. $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
  63. $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderPlace", $payload, $headers);
  64. // 处理响应
  65. if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) {
  66. $respData = $resp['data'];
  67. return [
  68. 'code' => 0,
  69. 'platform' => 'shansong',
  70. 'data' => [
  71. 'orderId' => $respData['orderNumber'],
  72. 'fee' => $respData['totalAmount'],
  73. 'distance' => $respData['totalDistance'],
  74. ]
  75. ];
  76. }
  77. Yii::error('status=' . $resp['status'] . ', msg=' . $resp['msg']);
  78. return [
  79. 'code' => -1,
  80. 'platform' => 'shansong',
  81. 'msg' => $resp['msg'] ?? 'Unknown error',
  82. 'data' => []
  83. ];
  84. }
  85. /**
  86. * 构建订单计费请求信息(供并发请求使用)
  87. * @param array $data 订单数据
  88. * @param string $pickupTime 取件时间
  89. * @return array 包含 url, data, headers, timeout 的请求信息
  90. */
  91. public function buildPriceRequest($data, $pickupTime)
  92. {
  93. // 预约类型
  94. $timeDiff = strtotime($pickupTime) - time();
  95. $appointType = 0; //0立即单
  96. if ($timeDiff > 3600 ) {
  97. $appointType = 1; //1预约单
  98. }
  99. // 构建发件人信息
  100. $sd = $data['sender'];// $senderData
  101. $sender = [
  102. 'fromAddress' => $sd['fromAddress'],
  103. 'fromAddressDetail' => $sd['fromAddressDetail'],
  104. 'fromSenderName' => $sd['fromSenderName'],
  105. 'fromMobile' => $sd['fromMobile'],
  106. 'fromLatitude' => $sd['fromLatitude'],
  107. 'fromLongitude' => $sd['fromLongitude'],
  108. ];
  109. // 构建收件人信息列表
  110. $receiverList = [];
  111. foreach ($data['receiverList'] as $receiver) {
  112. $receiverList[] = [
  113. 'orderNo' => $receiver['orderNo'],
  114. 'toAddress' => $receiver['toAddress'],
  115. 'toAddressDetail' => $receiver['toAddressDetail'],
  116. 'toReceiverName' => $receiver['toReceiverName'],
  117. 'toMobile' => $receiver['toMobile'],
  118. 'toLatitude' => $receiver['toLatitude'],
  119. 'toLongitude' => $receiver['toLongitude'],
  120. 'goodType' => $receiver['goodType'], // 物品类型,默认10-其他
  121. 'weight' => (int) ($receiver['weight'] ?? 1), // 物品重量(必须是整数kg)
  122. 'remarks' => $receiver['remarks'] ?? '', // 备注
  123. //'orderingSourceType' => 5, // 订单来源枚举: 1.闪送 5.其他平台
  124. //'orderingSourceNo' => $data['sendNum'], // 对应orderingSourceType流水号,传参数时必须和orderingSourceType成对出现
  125. 'expectStartTime' => isset($receiver['expectStartTime']) ? (int) $receiver['expectStartTime'] : null,
  126. 'expectEndTime' => isset($receiver['expectEndTime']) ? (int) $receiver['expectEndTime'] : null,
  127. ];
  128. // 移除null值的期望时间字段
  129. if ($receiverList[count($receiverList) - 1]['expectStartTime'] === null) {
  130. unset($receiverList[count($receiverList) - 1]['expectStartTime']);
  131. }
  132. if ($receiverList[count($receiverList) - 1]['expectEndTime'] === null) {
  133. unset($receiverList[count($receiverList) - 1]['expectEndTime']);
  134. }
  135. }
  136. // 构建请求体
  137. $requestData = [
  138. 'cityName' => $data['cityName'],
  139. 'appointType' => $appointType,
  140. 'sender' => $sender,
  141. 'receiverList' => $receiverList,
  142. //'storeId' => isset($data['storeId']) ? (int) $data['storeId'] : $this->storeId, // shansong 报价返回错误: 店铺Id错误,请更新所在平台的sotreId或thirdStoreId~
  143. 'travelWay' => (int) ($data['travelWay'] ?? 0),
  144. 'deliveryType' => (int) ($data['deliveryType'] ?? 1),
  145. ];
  146. // 如果有预约日期,添加到请求体
  147. if ($appointType == 1) {
  148. // yyyy-MM-dd HH:mm格式(例如:2020-02-02 22:00),指的是预约取件时间,只支持一个小时以后两天以内
  149. $requestData['appointmentDate'] = $pickupTime;
  150. }
  151. $requestData = $this->filterEmptyValues($requestData);
  152. $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE);
  153. // 构建签名用的完整payload
  154. $payload = [
  155. 'clientId' => $this->clientId,
  156. 'accessToken' => $this->accessToken,
  157. 'timestamp' => (int) (microtime(true) * 1000),
  158. 'data' => $requestData,
  159. ];
  160. // 计算签名(SignHelper 会自动递归过滤空值)
  161. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  162. $payload['sign'] = $sign;
  163. return [
  164. 'url' => "{$this->baseUrl}/openapi/developer/v5/orderCalculate",
  165. 'data' => $payload,
  166. 'headers' => ["Content-Type" => "application/x-www-form-urlencoded;charset=utf-8"],
  167. 'timeout' => 10.0,
  168. ];
  169. }
  170. /**
  171. * 处理闪送报价响应
  172. *
  173. * @param array $resp API 响应
  174. * @return array|null 解析后的报价结果
  175. */
  176. public function processPriceResponse($resp)
  177. {
  178. // 处理响应
  179. if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) {
  180. $respData = $resp['data'];
  181. return [
  182. 'platform' => 'shansong',
  183. 'order_number' => $respData['orderNumber'] ?? '', // 闪送订单号(有效期30分钟)
  184. 'total_distance' => $respData['totalDistance'] ?? 0, // 总距离,单位:米
  185. 'total_weight' => $respData['totalWeight'] ?? 0, // 总重量,单位:kg
  186. 'total_amount' => $respData['totalAmount'] ?? 0, // 订单总金额(分)
  187. 'coupon_save_fee' => $respData['couponSaveFee'] ?? 0, // 优惠额度(分)
  188. 'total_fee_after_save' => $respData['totalFeeAfterSave'] ?? 0, // 实际支付费用(分)
  189. 'fee_info_list' => $respData['feeInfoList'] ?? [], // 费用明细
  190. 'interest_dto_list' => $respData['interestDtoList'] ?? [], // 增值服务明细
  191. // 弃用字段
  192. 'estimate_grab_second' => $respData['estimateGrabSecond'] ?? -1, // 预计接单时长,字段弃用
  193. 'estimate_receive_second' => $respData['estimateReceiveSecond'] ?? -1, // 预计完单时长,字段弃用
  194. ];
  195. }
  196. Yii::error('shansong 报价失败:'.json_encode($resp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  197. return [
  198. 'platform' => 'shansong',
  199. 'status' => $resp['status'],
  200. 'error' => $resp['msg'],
  201. ];
  202. }
  203. /**
  204. * @deprecated
  205. * * 下单询价 - 订单计费接口
  206. * 根据闪送官方文档 /openapi/developer/v5/orderCalculate
  207. *
  208. * @param $data
  209. * @param $orderTime
  210. * @return array|null
  211. * @deprecated 使用 buildPriceRequest 和 processPriceResponse 替代
  212. */
  213. public function getPrice($data, $orderTime)
  214. {
  215. $request = $this->buildPriceRequest($data, $orderTime);
  216. $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
  217. $resp = HttpClient::post($request['url'], $request['data'], $headers);
  218. return $this->processPriceResponse($resp);
  219. }
  220. /**
  221. * 查询订单详情
  222. * @param $orderId
  223. * @param $thirdOrderNo
  224. * @return array|null
  225. */
  226. public function selectOrder($orderId, $thirdOrderNo)
  227. {
  228. // 构建请求体
  229. $requestData = [
  230. 'issOrderNo' => $orderId,
  231. 'thirdOrderNo' => $thirdOrderNo,
  232. ];
  233. $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE);
  234. // 构建签名用的完整payload
  235. $payload = [
  236. 'clientId' => $this->clientId,
  237. 'accessToken' => $this->accessToken,
  238. 'timestamp' => (int) (microtime(true) * 1000),
  239. 'data' => $requestData,
  240. ];
  241. // 计算签名(SignHelper 会自动递归过滤空值)
  242. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  243. $payload['sign'] = $sign;
  244. $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
  245. // 发送请求到计费接口
  246. $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/orderInfo", $payload, $headers);
  247. // 处理响应
  248. if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) {
  249. $respData = $resp['data'];
  250. return [
  251. 'code' => 0,
  252. 'data' => $respData
  253. ];
  254. }
  255. return [
  256. 'code' => (int)($resp['200'] ?? -1),
  257. 'msg' => $resp['msg'] ?? 'Unknown error',
  258. 'data' => null
  259. ];
  260. }
  261. public function cancelOrder($orderId)
  262. {
  263. // 构建请求体
  264. $requestData = [
  265. 'issOrderNo' => $orderId
  266. ]; //'deductFlag' => false,
  267. $requestData = json_encode($requestData, JSON_UNESCAPED_UNICODE);
  268. // 构建签名用的完整payload
  269. $payload = [
  270. 'clientId' => $this->clientId,
  271. 'accessToken' => $this->accessToken,
  272. 'timestamp' => (int) (microtime(true) * 1000),
  273. 'data' => $requestData,
  274. ];
  275. // 计算签名(SignHelper 会自动递归过滤空值)
  276. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  277. $payload['sign'] = $sign;
  278. $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
  279. // 发送请求到计费接口
  280. $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/abortOrder", $payload, $headers);
  281. // 处理响应
  282. if (isset($resp['status']) && $resp['status'] == 200 && isset($resp['data'])) {
  283. $respData = $resp['data'];
  284. return [
  285. 'code' => 0,
  286. 'platform' => 'shansong',
  287. 'data' => [
  288. 'deductionFee' => $respData['deductAmount'], //取消收费金额(单位:分)
  289. 'abortType' => $respData['abortType'],
  290. 'abortReason' => $respData['abortReason']
  291. ]
  292. ];
  293. }
  294. return null;
  295. }
  296. public function addTip($data)
  297. {
  298. $orderNo = $data['orderId'] ?? null;
  299. $additionAmount = $data['tips'] ?? null;
  300. if (empty($orderNo)) {
  301. return [
  302. 'code' => -1,
  303. 'platform' => 'shansong',
  304. 'msg' => 'issOrderNo不能为空',
  305. 'data' => null,
  306. ];
  307. }
  308. if (!isset($additionAmount) || (int)$additionAmount <= 0) {
  309. return [
  310. 'code' => -1,
  311. 'platform' => 'shansong',
  312. 'msg' => 'additionAmount必须为正整数(单位:分)',
  313. 'data' => null,
  314. ];
  315. }
  316. if (((int)$additionAmount) % 100 !== 0) {
  317. return [
  318. 'code' => -1,
  319. 'platform' => 'shansong',
  320. 'msg' => 'additionAmount需被100整除(最小单位为1元)',
  321. 'data' => null,
  322. ];
  323. }
  324. $requestData = json_encode([
  325. 'issOrderNo' => $orderNo,
  326. 'additionAmount' => (int)$additionAmount,
  327. ], JSON_UNESCAPED_UNICODE);
  328. $payload = [
  329. 'clientId' => $this->clientId,
  330. 'accessToken' => $this->accessToken,
  331. 'timestamp' => (int)(microtime(true) * 1000),
  332. 'data' => $requestData,
  333. ];
  334. $sign = SignHelper::makeSign($payload, $this->appSecret, 'md5', true, 'shansong');
  335. $payload['sign'] = $sign;
  336. $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
  337. $resp = HttpClient::post("{$this->baseUrl}/openapi/developer/v5/addition", $payload, $headers);
  338. if (isset($resp['status']) && $resp['status'] == 200) {
  339. return [
  340. 'code' => 0,
  341. 'platform' => 'shansong',
  342. 'data' => $resp['data'] ?? null,
  343. ];
  344. }
  345. Yii::error('[ShansongAdapter] addTip failed: ' . json_encode($resp, JSON_UNESCAPED_UNICODE));
  346. return [
  347. 'code' => $resp['status'] ?? -1,
  348. 'platform' => 'shansong',
  349. 'msg' => $resp['msg'] ?? 'Unknown error',
  350. 'data' => null,
  351. ];
  352. }
  353. /**
  354. * 递归过滤数组中的空值(null、空字符串、空数组)
  355. * 确保签名时不包含空值参数
  356. *
  357. * @param array $data 需要过滤的数组
  358. * @return array 过滤后的数组
  359. */
  360. private function filterEmptyValues($data)
  361. {
  362. $filtered = [];
  363. foreach ($data as $key => $value) {
  364. // 过滤掉 null、空字符串、空数组
  365. if ($value === null || $value === '' || (is_array($value) && empty($value))) {
  366. continue;
  367. }
  368. // 如果是数组,递归处理
  369. if (is_array($value)) {
  370. $value = $this->filterEmptyValues($value);
  371. // 递归过滤后如果变成空数组,也要跳过
  372. if (empty($value)) {
  373. continue;
  374. }
  375. }
  376. $filtered[$key] = $value;
  377. }
  378. return $filtered;
  379. }
  380. }