'mailno', 'transfer_status' => 'remark', 'received_at' => 'accept_time', ]; /** * 获取顺丰快递揽收数据 * @param array $logisticNums [logisticNums]快递单号数组 * @return array 快递揽收信息数组 * @throws Exception 未知的丰桥opCode */ public function get(array $logisticNums): array { // 将$logisticNums以10个位单位进行分割,返回二维数组 $logisticNums_size_10 = array_chunk($logisticNums, config('api_logistic.SF.max_size', 10)); // 遍历二维数组批量查询顺丰接口 //将查询到的结果整合到一起,更新order_packages.received_at $result = []; foreach ($logisticNums_size_10 as $numbers) { $numbersStr = implode(',', $numbers); $result_10 = $this->getResultFromSF(config('api_logistic.SF.head'), $numbersStr, config('api_logistic.SF.check_word'), config('api_logistic.SF.url')); $result = array_merge($result, $result_10); } return $result; } /** * @param string $head 客户号 * @param string $numbers 快递单号字符串,多个以','分隔 * @param string $checkWord 客户秘钥 * @param string $url 顺丰接口地址 * @return array * @throws Exception 未知的丰桥opCode */ public function getResultFromSF(string $head, string $numbers, string $checkWord, string $url): array { $responseBody = get_object_vars(simplexml_load_string($this->sendHttpToSF($head, $numbers, $checkWord, $url))->Body)['RouteResponse']; $result = []; if (is_array($responseBody)) {//SF返回多个单号的查询结果 $result = $this->transformSFMoreToArr($responseBody, $result); } else { $result[] = $this->transformSFOneToArr(get_object_vars($responseBody), []); } return $result; } /** * 构建顺丰xml请求体 * @param string $head * @param string $number * @return string */ private function buildXmlStr(string $head, string $number): string { return << $head xml; } /** * 将单个单号的顺丰数据转换为数组 * @param array $routeResponse * @param array $data * @return array * @throws Exception */ public function transformSFOneToArr(array $routeResponse, array $data): array { $data['logistic_number'] = $routeResponse['@attributes'][$this->protected_switch['logistic_number']]; try { $lastRoute = get_object_vars($routeResponse['Route'][count($routeResponse['Route']) - 1])['@attributes'];//获取最新的路由信息 $data = $this->switchOpCodeToStatus($lastRoute, $data); $data['transfer_status'] = $this->transformRoutes($routeResponse['Route']); if (!array_key_exists('exception', $data)) {//当顺丰返回异常时,不需要再根据时间判断是否异常,直接用顺丰的异常就好 $data = $this->setExceptionType($data, $lastRoute['accept_time']); } //如果没有发现额外的异常,且查询到物流轨迹,将异常置为无 if (!array_key_exists('exception', $data) && !array_key_exists('exception_type', $data) && array_key_exists('transfer_status', $data) ) { $data['exception_type'] = '无'; $data['exception'] = '否'; } } catch (Exception $e) { throw new WarningException("单号没有查询到快递路由信息','LogisticSFService->transformSFOneToArr->{$data['logistic_number']}"); } finally { return $data; } } /** * 转换快递路由信息 * @param $routs 快递路由 * @return array */ public function transformRoutes($routs): array { $result = []; if (!is_array($routs)) { $routs = [$routs]; } foreach ($routs as $route) { $route = get_object_vars($route)['@attributes']; $data['accept_time'] = $route['accept_time']; $data['accept_address'] = $route['accept_address']; $data['remark'] = $route['remark']; $result[] = $data; } return $result; } /** * 将最新路由信息转换为数组 * @param array $lastRoute * @param array $data * @return array * @throws Exception */ public function switchOpCodeToStatus(array $lastRoute, array $data): array { try { switch ($lastRoute['opcode']) { case 123: case 130: case 3036: case 31: case 30: case 36: $data['status'] = '在途'; break; case 33: $data['status'] = '派送异常'; $data['exception_type'] = '派件异常'; $data['exception'] = '是'; break; case 204: case 44: $data['status'] = '派送中'; break; case 50: $data['status'] = '已揽收'; break; case 607: case 8000: case 80: $data['status'] = '已收件'; $data['received_at'] = $lastRoute[$this->protected_switch['received_at']]; break; case 648: case 99: $data['status'] = '返回中'; break; case 70: $data['status'] = '无'; $data['exception'] = '是'; $data['exception_type'] = '其他'; break; default: throw new WarningException("未知的丰桥状态码: {$lastRoute['opcode']}->{json_encode($lastRoute)}"); } } catch (WarningException $e) { $data['status'] = '其他异常'; } finally { return $data; } } /** * @param string $head * @param string $numbers * @param string $checkWord * @param string $url * @return Response * @throws Exception */ public function sendHttpToSF(string $head, string $numbers, string $checkWord, string $url): Response { $xml = $this->buildXmlStr($head, $numbers); $checkingJson = $xml . $checkWord; $verifyCode = base64_encode(md5($checkingJson, true)); try { $response = Http::withHeaders(['Content-Type' => 'text/xml'])->get($url, ['xml' => $xml, 'verifyCode' => $verifyCode]); } catch (Exception $e) { throw new WarningException("HTTP请求顺丰接口异常->{$e->getMessage()}"); } return $response; } /** * 将多个顺丰的单号转换为数组 * @param array $responseBody * @param array $result * @return array * @throws Exception */ public function transformSFMoreToArr(array $responseBody, array $result): array { foreach ($responseBody as $routeResponse) { $result[] = $this->transformSFOneToArr(get_object_vars($routeResponse), []); } return $result; } /** * @param array $data * @param $lastRouteDate * @return array */ private function setExceptionType(array $data, $lastRouteDate = null): array { $logistic_number = $data['logistic_number']; /** @var OrderPackage $orderPackage */ $orderPackage = OrderPackage::query()->with('order')->where('logistic_number', $logistic_number)->first(); $delivered_duration = now()->diffInHours(Carbon::parse($orderPackage['sent_at'])); $last_routed_duration = now()->diffInHours(Carbon::parse($lastRouteDate)); $VALID_HOURS = 4; $SHORT_RESPONSE_HOURS = 24; $LONG_RESPONSE_HOURS = (function ($province) { switch ($province) { case '浙江省': case '江苏省': case '上海': case '安徽省': return 72; case '北京': case '天津': case '江西省': case '湖北省': case '湖南省': case '广东省': case '福建省': case '山东省': case '河北省': case '河南省': case '山西省': case '四川省': case '陕西省': case '重庆': case '广西壮族自治区': case '贵州省': case '云南省': case '海南省': case '吉林省': case '黑龙江省': case '辽宁省': return 120; case '青海省': case '宁夏回族自治区': case '甘肃省': case '内蒙古自治区': case '新疆维吾尔自治区': case '西藏自治区': return 168; default: break; } })($orderPackage->order->province); $SENDING_RESPONSE_HOURS = 48; $IS_ROUTED = 1; //0000 0001 有路由信息 $IS_IN_VALID_TIME = 2; //0000 0010 大于4小时 $IS_WEIGHED = 4; //0000 0100 称重过 $IS_RECEIVED = 8; //0000 1000 已经收货 $IS_SENDING = 16; //0001 0000 正在派送 $IS_SHORT_NO_RESPONSE = 32; //0010 0000 中转异常 $IS_LONG_NO_RESPONSE = 64; //0010 0000 疑似丢件 $IS_SENDING_NO_RESPONSE = 128; //0010 0000 派送异常 $conclusion = (function () use ( $data, $delivered_duration, $last_routed_duration, $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, $SHORT_RESPONSE_HOURS, $LONG_RESPONSE_HOURS, $SENDING_RESPONSE_HOURS, $orderPackage ) { $conclusion = 0; $conclusion |= !empty($data['transfer_status']) ? $IS_ROUTED : 0; $conclusion |= ($delivered_duration > $VALID_HOURS) ? $IS_IN_VALID_TIME : 0; $conclusion |= ($orderPackage->weighed_at) ? $IS_WEIGHED : 0; $conclusion |= ($data['status'] == '已收件') ? $IS_RECEIVED : 0; $conclusion |= ($data['status'] == '派送中') ? $IS_SENDING : 0;// $conclusion |= ($last_routed_duration > $SHORT_RESPONSE_HOURS && $last_routed_duration < $LONG_RESPONSE_HOURS) ? $IS_SHORT_NO_RESPONSE : 0; $conclusion |= ($last_routed_duration > $LONG_RESPONSE_HOURS) ? $IS_LONG_NO_RESPONSE : 0; $conclusion |= ($last_routed_duration > $SENDING_RESPONSE_HOURS && $data['status'] == '派送中') ? $IS_SENDING_NO_RESPONSE : 0; return $conclusion; })(); switch ($conclusion) { case $IS_IN_VALID_TIME: $data['exception_type'] = '疑似库内丢件'; break; case $IS_IN_VALID_TIME | $IS_WEIGHED: $data['exception_type'] = '揽件异常'; break; case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SHORT_NO_RESPONSE: case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SHORT_NO_RESPONSE | $IS_WEIGHED: $data['exception_type'] = '中转异常'; break; case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_LONG_NO_RESPONSE: case $IS_ROUTED | $IS_IN_VALID_TIME | $IS_LONG_NO_RESPONSE | $IS_WEIGHED: $data['exception_type'] = '疑似丢件'; break; default: break; } if($conclusion ==($conclusion | $IS_ROUTED | $IS_IN_VALID_TIME | $IS_SENDING | $IS_SENDING_NO_RESPONSE)){ $data['exception_type'] = '派件异常'; } switch ($conclusion) { case $IS_IN_VALID_TIME: case $IS_IN_VALID_TIME | $IS_WEIGHED: case $IS_ROUTED | $IS_SHORT_NO_RESPONSE: case $IS_LONG_NO_RESPONSE: $data['exception'] = '是'; break; default: break; } return $data; } }