diff --git a/server/app/command/ExpressAutoUpdate.php b/server/app/command/ExpressAutoUpdate.php index 24499dc8..9ab1ba00 100755 --- a/server/app/command/ExpressAutoUpdate.php +++ b/server/app/command/ExpressAutoUpdate.php @@ -38,7 +38,8 @@ class ExpressAutoUpdate extends Command try { $result = ExpressTrackingService::autoUpdateBatch(1000); $recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000); - + + $duration = round(microtime(true) - $startTime, 2); $output->writeln("更新完成!"); diff --git a/server/app/common/service/ExpressTrackService.php b/server/app/common/service/ExpressTrackService.php index c16d50b7..1bf84831 100755 --- a/server/app/common/service/ExpressTrackService.php +++ b/server/app/common/service/ExpressTrackService.php @@ -63,29 +63,94 @@ class ExpressTrackService $cfg = Config::get('logistics.kuaidi100', []); $enable = !empty($cfg['enable']); + + $result = $out; if (! $enable) { - $out['hint'] = '未配置快递100查询密钥或已关闭(LOGISTICS_KUAIDI100_DISABLE),仅可打开官网查件。请在 .env 中配置 LOGISTICS_KUAIDI100_CUSTOMER、LOGISTICS_KUAIDI100_KEY'; - - return $out; - } - - // 顺丰(及快递100 要求电话的承运商)无 phone 时不请求接口,避免无效调用 - if ($phoneForKuaidi === '' && self::kuaidiPhoneRequired($kuaidiCom)) { - $out['hint'] = '顺丰查询需在快递100 中同时提交单号与收/寄件人电话(可与面单一致的完整手机号或后四位)。请填写收件电话后点「刷新轨迹」。'; - - return $out; - } - - $lastFail = null; - foreach ($comCandidates as $tryCom) { - $tryOut = self::queryKuaidiOnce($cfg, $tryCom, $num, $phoneForKuaidi, $carrier, $label); - if (!empty($tryOut['traces']) || ($tryOut['state'] ?? '') !== '') { - return $tryOut; + $result['hint'] = '未配置快递100查询密钥或已关闭(LOGISTICS_KUAIDI100_DISABLE),仅可打开官网查件。请在 .env 中配置 LOGISTICS_KUAIDI100_CUSTOMER、LOGISTICS_KUAIDI100_KEY'; + } elseif ($phoneForKuaidi === '' && self::kuaidiPhoneRequired($kuaidiCom)) { + // 顺丰(及快递100 要求电话的承运商)无 phone 时不请求接口,避免无效调用 + $result['hint'] = '顺丰查询需在快递100 中同时提交单号与收/寄件人电话(可与面单一致的完整手机号或后四位)。请填写收件电话后点「刷新轨迹」。'; + } else { + $matched = null; + $lastFail = null; + foreach ($comCandidates as $tryCom) { + $tryOut = self::queryKuaidiOnce($cfg, $tryCom, $num, $phoneForKuaidi, $carrier, $label); + if (!empty($tryOut['traces']) || ($tryOut['state'] ?? '') !== '') { + $matched = $tryOut; + break; + } + $lastFail = $tryOut; } - $lastFail = $tryOut; + $result = $matched ?? $lastFail ?? $out; } - return $lastFail ?? $out; + // 京东自营单(JDVE…)兜底:快递100 无轨迹/陈旧时,用京东官方接口(更新或更全才采用)。 + // 未配置京东官方接口时 isConfigured()=false,本段跳过,行为与原先一致。 + if ($carrier === 'jd' && JdLogisticsService::isConfigured()) { + try { + $jd = JdLogisticsService::queryTrace($num); + if ($jd !== null && !empty($jd['traces']) && self::jdResultPreferred($jd, $result)) { + $result['traces'] = $jd['traces']; + $result['state'] = (string) $jd['state']; + $result['state_text'] = (string) $jd['state_text']; + $result['source'] = 'jd_official'; + $result['hint'] = ''; + // carrier / carrier_label / official_url / kuaidi_com 保留原值 + } + } catch (\Throwable $e) { + Log::warning('ExpressTrackService jd official fallback failed', [ + 'num' => $num, + 'error' => $e->getMessage(), + ]); + } + } + + return $result; + } + + /** + * 京东官方轨迹是否应优先于快递100 结果采用: + * 快递100 无轨迹 → 直接用;否则京东更「新」(最新轨迹时间更晚)或同样新但条目更多 → 用。 + * + * @param array{traces?:array,newest_unix?:int} $jd + * @param array{traces?:array} $kuaidi + */ + private static function jdResultPreferred(array $jd, array $kuaidi): bool + { + $kuaidiTraces = is_array($kuaidi['traces'] ?? null) ? $kuaidi['traces'] : []; + if ($kuaidiTraces === []) { + return true; + } + + $jdNewest = (int) ($jd['newest_unix'] ?? 0); + $kuaidiNewest = self::newestUnixFromTraces($kuaidiTraces); + if ($jdNewest > $kuaidiNewest) { + return true; + } + if ($jdNewest === $kuaidiNewest && $jdNewest > 0) { + return count($jd['traces'] ?? []) > count($kuaidiTraces); + } + + return false; + } + + /** + * @param array $traces + */ + private static function newestUnixFromTraces(array $traces): int + { + $best = 0; + foreach ($traces as $t) { + if (!is_array($t)) { + continue; + } + $p = strtotime((string) ($t['time'] ?? '')); + if ($p !== false && (int) $p > $best) { + $best = (int) $p; + } + } + + return $best; } /** diff --git a/server/app/common/service/ExpressTrackingService.php b/server/app/common/service/ExpressTrackingService.php index 2bee0ab0..638b69d6 100755 --- a/server/app/common/service/ExpressTrackingService.php +++ b/server/app/common/service/ExpressTrackingService.php @@ -1047,7 +1047,7 @@ class ExpressTrackingService ->field('et.*') ->limit($limit) ->select(); - + $success = 0; $failed = 0; $assistantCleared = 0; diff --git a/server/config/logistics.php b/server/config/logistics.php index 2d1413f3..ce537280 100755 --- a/server/config/logistics.php +++ b/server/config/logistics.php @@ -41,6 +41,32 @@ $forceDisable = filter_var($forceDisableRaw, FILTER_VALIDATE_BOOLEAN); // 已填 CUSTOMER+KEY 即启用;需临时关闭时设 LOGISTICS_KUAIDI100_DISABLE=true(LOGISTICS_KUAIDI100_ENABLE 已废弃,可删) $kuaidiEnabled = ! $forceDisable && $hasCreds; +// 京东官方物流接口(JOS 开放平台 https://api.jd.com/routerjson) +// 仅当填好 APP_KEY / APP_SECRET / ACCESS_TOKEN 时启用;作为快递100 对京东自营单(JDVE…)轨迹陈旧时的兜底数据源。 +// 未配置时本段 enable=false,ExpressTrackService 完全沿用现有快递100 逻辑,互不影响。 +$envJd = static function (string $suffix, $default = '') use ($stripQuotes) { + $top = 'JD_LOGISTICS_' . $suffix; + $v = env($top, null); + if ($v !== null && $v !== '') { + return $stripQuotes((string) $v); + } + // 兼容误写进 [trtc] 等分区的情况 + $nested = env('trtc.' . $top, $default); + + return $stripQuotes((string) $nested); +}; + +$jdAppKey = $envJd('APP_KEY', ''); +$jdAppSecret = $envJd('APP_SECRET', ''); +$jdAccessToken = $envJd('ACCESS_TOKEN', ''); +$jdHasCreds = $jdAppKey !== '' && $jdAppSecret !== '' && $jdAccessToken !== ''; +$jdDisableRaw = env('JD_LOGISTICS_DISABLE', null); +if ($jdDisableRaw === null || $jdDisableRaw === '') { + $jdDisableRaw = env('trtc.JD_LOGISTICS_DISABLE', false); +} +$jdDisabled = filter_var($jdDisableRaw, FILTER_VALIDATE_BOOLEAN); +$jdEnabled = ! $jdDisabled && $jdHasCreds; + return [ 'kuaidi100' => [ 'enable' => $kuaidiEnabled, @@ -48,4 +74,18 @@ return [ 'key' => $key, 'query_url' => $envKuaidi('QUERY_URL', 'https://poll.kuaidi100.com/poll/query.do'), ], + 'jd' => [ + 'enable' => $jdEnabled, + 'app_key' => $jdAppKey, + 'app_secret' => $jdAppSecret, + 'access_token' => $jdAccessToken, + 'gateway' => $envJd('GATEWAY', 'https://api.jd.com/routerjson'), + // 青龙运单轨迹查询;如改用 ECLP 等可在 .env 覆盖 method / param_key / waybill_field + 'method' => $envJd('METHOD', 'jingdong.ldop.receive.trace.get'), + 'param_key' => $envJd('PARAM_KEY', '360buy_param_json'), + 'waybill_field' => $envJd('WAYBILL_FIELD', 'waybillCode'), + // 部分接口需商家编码 / 事业部编码,可选 + 'customer_code' => $envJd('CUSTOMER_CODE', ''), + 'api_version' => $envJd('API_VERSION', '2.0'), + ], ];