LogisticSFService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. namespace App\Services;
  3. use App\Exceptions\WarningException;
  4. use App\OrderPackage;
  5. use Exception;
  6. use Illuminate\Http\Client\Response;
  7. use Illuminate\Support\Carbon;
  8. use Illuminate\Support\Facades\Http;
  9. class LogisticSFService
  10. {
  11. /**
  12. * 顺丰字段与数据库字段的映射关心
  13. * @var string[]
  14. */
  15. protected $protected_switch = [
  16. 'logistic_number' => 'mailno',
  17. 'transfer_status' => 'remark',
  18. 'received_at' => 'accept_time',
  19. ];
  20. /**
  21. * 获取顺丰快递揽收数据
  22. * @param array $logisticNums [logisticNums]快递单号数组
  23. * @return array 快递揽收信息数组
  24. * @throws Exception 未知的丰桥opCode
  25. */
  26. public function get(array $logisticNums): array
  27. {
  28. // 将$logisticNums以10个位单位进行分割,返回二维数组
  29. $logisticNums_size_10 = array_chunk($logisticNums, config('api_logistic.SF.max_size', 10));
  30. // 遍历二维数组批量查询顺丰接口
  31. //将查询到的结果整合到一起,更新order_packages.received_at
  32. $result = [];
  33. foreach ($logisticNums_size_10 as $numbers) {
  34. $numbersStr = implode(',', $numbers);
  35. $result_10 = $this->getResultFromSF(config('api_logistic.SF.head'), $numbersStr, config('api_logistic.SF.check_word'), config('api_logistic.SF.url'));
  36. $result = array_merge($result, $result_10);
  37. }
  38. return $result;
  39. }
  40. /**
  41. * @param string $head 客户号
  42. * @param string $numbers 快递单号字符串,多个以','分隔
  43. * @param string $checkWord 客户秘钥
  44. * @param string $url 顺丰接口地址
  45. * @return array
  46. * @throws Exception 未知的丰桥opCode
  47. */
  48. public function getResultFromSF(string $head, string $numbers, string $checkWord, string $url): array
  49. {
  50. $responseBody = get_object_vars(simplexml_load_string($this->sendHttpToSF($head, $numbers, $checkWord, $url))->Body)['RouteResponse'];
  51. $result = [];
  52. if (is_array($responseBody)) {//SF返回多个单号的查询结果
  53. $result = $this->transformSFMoreToArr($responseBody, $result);
  54. } else {
  55. $result[] = $this->transformSFOneToArr(get_object_vars($responseBody), []);
  56. }
  57. return $result;
  58. }
  59. /**
  60. * 构建顺丰xml请求体
  61. * @param string $head
  62. * @param string $number
  63. * @return string
  64. */
  65. private function buildXmlStr(string $head, string $number): string
  66. {
  67. return <<<xml
  68. <?xml version="1.0" encoding="utf-8" ?>
  69. <Request service='RouteService' lang='zh-CN'>
  70. <Head>$head</Head>
  71. <Body>
  72. <RouteRequest
  73. tracking_number="{$number}" tracking_type='1' method_type='1'
  74. />
  75. </Body>
  76. </Request>
  77. xml;
  78. }
  79. /**
  80. * 将单个单号的顺丰数据转换为数组
  81. * @param array $routeResponse
  82. * @param array $data
  83. * @return array
  84. * @throws Exception
  85. */
  86. public function transformSFOneToArr(array $routeResponse, array $data): array
  87. {
  88. $data['logistic_number'] = $routeResponse['@attributes'][$this->protected_switch['logistic_number']];
  89. try {
  90. $lastRoute = get_object_vars($routeResponse['Route'][count($routeResponse['Route']) - 1])['@attributes'];//获取最新的路由信息
  91. $data = $this->switchOpCodeToStatus($lastRoute, $data);
  92. $data['transfer_status'] = $this->transformRoutes($routeResponse['Route']);
  93. if (!array_key_exists('exception', $data)) {//当顺丰返回异常时,不需要再根据时间判断是否异常,直接用顺丰的异常就好
  94. $orderPackageReceivedSyncService = app('OrderPackageReceivedSyncService');
  95. $exceptionData = $orderPackageReceivedSyncService->setExceptionType($data, array_key_exists('accept_time',$lastRoute) ? $lastRoute['accept_time'] : null);
  96. $data['exception_type'] = $exceptionData['exception_type'];
  97. $data['exception'] = $exceptionData['exception'];
  98. }
  99. //如果没有发现额外的异常,且查询到物流轨迹,将异常置为无
  100. if (!array_key_exists('exception', $data)
  101. && !array_key_exists('exception_type', $data)
  102. && array_key_exists('transfer_status', $data)
  103. ) {
  104. $data['exception_type'] = '无';
  105. $data['exception'] = '否';
  106. }
  107. } catch (Exception $e) {
  108. throw new WarningException("单号没有查询到快递路由信息','LogisticSFService->transformSFOneToArr->{$data['logistic_number']}");
  109. } finally {
  110. $data['routes_length'] = array_key_exists('transfer_status', $data) ? count($data['transfer_status']) : 0;
  111. return $data;
  112. }
  113. }
  114. /**
  115. * 转换快递路由信息
  116. * @param $routs 快递路由
  117. * @return array
  118. */
  119. public function transformRoutes($routs): array
  120. {
  121. $result = [];
  122. if (!is_array($routs)) {
  123. $routs = [$routs];
  124. }
  125. foreach ($routs as $route) {
  126. $route = get_object_vars($route)['@attributes'];
  127. $data['accept_time'] = $route['accept_time'];
  128. $data['accept_address'] = $route['accept_address'];
  129. $data['remark'] = $route['remark'];
  130. $result[] = $data;
  131. }
  132. return $result;
  133. }
  134. /**
  135. * 将最新路由信息转换为数组
  136. * @param array $lastRoute
  137. * @param array $data
  138. * @return array
  139. * @throws Exception
  140. */
  141. public function switchOpCodeToStatus(array $lastRoute, array $data): array
  142. {
  143. try {
  144. switch ($lastRoute['opcode']) {
  145. case 123:
  146. case 130:
  147. case 3036:
  148. case 31:
  149. case 30:
  150. case 36:
  151. $data['status'] = '在途';
  152. break;
  153. case 33:
  154. $data['status'] = '派送异常';
  155. $data['exception_type'] = '派件异常';
  156. $data['exception'] = '是';
  157. break;
  158. case 204:
  159. case 44:
  160. $data['status'] = '派送中';
  161. break;
  162. case 50:
  163. $data['status'] = '已揽收';
  164. break;
  165. case 607:
  166. case 8000:
  167. case 80:
  168. $data['status'] = '已收件';
  169. $data['received_at'] = $lastRoute[$this->protected_switch['received_at']];
  170. break;
  171. case 648:
  172. case 99:
  173. $data['status'] = '返回中';
  174. break;
  175. case 70:
  176. $data['status'] = '无';
  177. $data['exception'] = '是';
  178. $data['exception_type'] = '其他';
  179. break;
  180. default:
  181. throw new WarningException("未知的丰桥状态码: {$lastRoute['opcode']}->{json_encode($lastRoute)}");
  182. }
  183. } catch (WarningException $e) {
  184. $data['status'] = '其他异常';
  185. } finally {
  186. return $data;
  187. }
  188. }
  189. /**
  190. * @param string $head
  191. * @param string $numbers
  192. * @param string $checkWord
  193. * @param string $url
  194. * @return Response
  195. * @throws Exception
  196. */
  197. public function sendHttpToSF(string $head, string $numbers, string $checkWord, string $url): Response
  198. {
  199. $xml = $this->buildXmlStr($head, $numbers);
  200. $checkingJson = $xml . $checkWord;
  201. $verifyCode = base64_encode(md5($checkingJson, true));
  202. try {
  203. $response = Http::withHeaders(['Content-Type' => 'text/xml'])->get($url, ['xml' => $xml, 'verifyCode' => $verifyCode]);
  204. } catch (Exception $e) {
  205. throw new WarningException("HTTP请求顺丰接口异常->{$e->getMessage()}");
  206. }
  207. return $response;
  208. }
  209. /**
  210. * 将多个顺丰的单号转换为数组
  211. * @param array $responseBody
  212. * @param array $result
  213. * @return array
  214. * @throws Exception
  215. */
  216. public function transformSFMoreToArr(array $responseBody, array $result): array
  217. {
  218. foreach ($responseBody as $routeResponse) {
  219. $result[] = $this->transformSFOneToArr(get_object_vars($routeResponse), []);
  220. }
  221. return $result;
  222. }
  223. // /**
  224. // * @param array $data
  225. // * @param $lastRouteDate
  226. // * @return array
  227. // */
  228. // private function setExceptionType(array $data, $lastRouteDate): array
  229. // {
  230. // $logistic_number = $data['logistic_number'];
  231. // /** @var OrderPackage $orderPackage */
  232. // $orderPackage = OrderPackage::query()->with('order')->where('logistic_number', $logistic_number)->first();
  233. // $delivered_duration = now()->diffInHours(Carbon::parse($orderPackage['sent_at']));
  234. // $last_routed_duration = now()->diffInHours(Carbon::parse($lastRouteDate));
  235. // $VALID_HOURS = 4;
  236. // $SHORT_RESPONSE_HOURS = 24;
  237. // $LONG_RESPONSE_HOURS = (function ($province) {
  238. // switch ($province) {
  239. // case '浙江省':
  240. // case '江苏省':
  241. // case '上海':
  242. // case '安徽省':
  243. // return 72;
  244. // case '北京':
  245. // case '天津':
  246. // case '江西省':
  247. // case '湖北省':
  248. // case '湖南省':
  249. // case '广东省':
  250. // case '福建省':
  251. // case '山东省':
  252. // case '河北省':
  253. // case '河南省':
  254. // case '山西省':
  255. // case '四川省':
  256. // case '陕西省':
  257. // case '重庆':
  258. // case '广西壮族自治区':
  259. // case '贵州省':
  260. // case '云南省':
  261. // case '海南省':
  262. // case '吉林省':
  263. // case '黑龙江省':
  264. // case '辽宁省':
  265. // return 120;
  266. // case '青海省':
  267. // case '宁夏回族自治区':
  268. // case '甘肃省':
  269. // case '内蒙古自治区':
  270. // case '新疆维吾尔自治区':
  271. // case '西藏自治区':
  272. // return 168;
  273. // default:
  274. // break;
  275. // }
  276. // })($orderPackage->order->province);
  277. // $SENDING_RESPONSE_HOURS = 48;
  278. // $IS_ROUTED = 1; //0000 0001 有路由信息
  279. // $IS_IN_VALID_TIME = 2; //0000 0010 大于4小时
  280. // $IS_WEIGHED = 4; //0000 0100 称重过
  281. // $IS_RECEIVED = 8; //0000 1000 已经收货
  282. // $IS_SENDING = 16; //0001 0000 正在派送
  283. // $IS_SHORT_NO_RESPONSE = 32; //0010 0000 中转异常
  284. // $IS_LONG_NO_RESPONSE = 64; //0010 0000 疑似丢件
  285. // $IS_SENDING_NO_RESPONSE = 128; //0010 0000 派送异常
  286. // $conclusion = (function () use (
  287. // $data, $delivered_duration, $last_routed_duration,
  288. // $VALID_HOURS, $IS_ROUTED, $IS_IN_VALID_TIME, $IS_WEIGHED, $IS_RECEIVED, $IS_SENDING, $IS_SHORT_NO_RESPONSE, $IS_LONG_NO_RESPONSE, $IS_SENDING_NO_RESPONSE,
  289. // $SHORT_RESPONSE_HOURS, $LONG_RESPONSE_HOURS, $SENDING_RESPONSE_HOURS,
  290. // $orderPackage
  291. // ) {
  292. // $conclusion = 0;
  293. // $conclusion |= !empty($data['transfer_status']) ? $IS_ROUTED : 0;
  294. // $conclusion |= ($delivered_duration > $VALID_HOURS) ? $IS_IN_VALID_TIME : 0;
  295. // $conclusion |= ($orderPackage->weighed_at) ? $IS_WEIGHED : 0;
  296. // $conclusion |= ($data['status'] == '已收件') ? $IS_RECEIVED : 0;
  297. // $conclusion |= ($data['status'] == '派送中') ? $IS_SENDING : 0;//
  298. // $conclusion |= ($last_routed_duration > $SHORT_RESPONSE_HOURS && $last_routed_duration < $LONG_RESPONSE_HOURS) ? $IS_SHORT_NO_RESPONSE : 0;
  299. // $conclusion |= ($last_routed_duration > $LONG_RESPONSE_HOURS) ? $IS_LONG_NO_RESPONSE : 0;
  300. // $conclusion |= ($last_routed_duration > $SENDING_RESPONSE_HOURS && $data['status'] == '派送中') ? $IS_SENDING_NO_RESPONSE : 0;
  301. // return $conclusion;
  302. // })();
  303. // switch ($conclusion) {
  304. // case $IS_IN_VALID_TIME:
  305. // $data['exception_type'] = '疑似库内丢件';
  306. // break;
  307. // case $IS_IN_VALID_TIME | $IS_WEIGHED:
  308. // $data['exception_type'] = '揽件异常';
  309. // break;
  310. // case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SHORT_NO_RESPONSE:
  311. // case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SHORT_NO_RESPONSE | $IS_WEIGHED:
  312. // $data['exception_type'] = '中转异常';
  313. // break;
  314. // case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_LONG_NO_RESPONSE:
  315. // case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_LONG_NO_RESPONSE | $IS_WEIGHED:
  316. // $data['exception_type'] = '疑似丢件';
  317. // break;
  318. // default:
  319. // break;
  320. // }
  321. //
  322. // if($conclusion
  323. // ==($conclusion | $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SENDING | $IS_SENDING_NO_RESPONSE)){
  324. // $data['exception_type'] = '派件异常';
  325. // }
  326. //
  327. // switch ($conclusion) {
  328. // case $IS_IN_VALID_TIME:
  329. // case $IS_IN_VALID_TIME | $IS_WEIGHED:
  330. // case $IS_ROUTED | $IS_SHORT_NO_RESPONSE:
  331. // case $IS_LONG_NO_RESPONSE:
  332. // $data['exception'] = '是';
  333. // break;
  334. // default:
  335. // break;
  336. // }
  337. // return [
  338. // 'exception_type' => array_key_exists('exception_type', $data) ? $data['exception_type'] : null,
  339. // 'exception' => array_key_exists('exception', $data) ? $data['exception'] : null,
  340. // ];
  341. // }
  342. }