ShansongAdapter.php 15 KB

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