This commit is contained in:
Your Name
2026-05-13 16:54:54 +08:00
parent 1e8b5c4646
commit 0eaacbb4ce
9 changed files with 1373 additions and 98 deletions
@@ -862,4 +862,237 @@ class ExpressTrackingService
return $data;
}
/**
* 与 PrescriptionOrderLogic::logisticsTrace 一致:优先库表 express_tracking + express_trace
* 库内算不出签收时(或库无记录),可选走快递100(与详情「刷新轨迹」同源)。
*
* 提成结算 orderLines 等对 SQL JOIN 无法覆盖「仅 API 有轨迹」的运单做补算。
*
* @param bool $queryKuaidiWhenDbUnusable true 时与 logisticsTrace 完全同路径;false 时仅数据库推导(overview 大批量避免 HTTP
*/
public static function resolveSignUnixTimeForCommission(
string $trackingNumber,
string $expressCompany,
string $recipientPhone,
bool $queryKuaidiWhenDbUnusable = true
): int {
$num = trim($trackingNumber);
if ($num === '') {
return 0;
}
$cacheKey = $num . '|' . ($queryKuaidiWhenDbUnusable ? '1' : '0');
static $memo = [];
if (isset($memo[$cacheKey])) {
return $memo[$cacheKey];
}
$detail = self::loadTrackingDetailNormalized($num);
if (is_array($detail)) {
$fromDb = self::effectiveSignUnixFromTrackingDetail($detail);
if ($fromDb > 0) {
$memo[$cacheKey] = $fromDb;
return $fromDb;
}
}
if (!$queryKuaidiWhenDbUnusable) {
$memo[$cacheKey] = 0;
return 0;
}
$ec = strtolower(trim($expressCompany));
if ($ec === '') {
$ec = 'auto';
}
$api = ExpressTrackService::query($ec, $num, $recipientPhone, '');
$fromApi = self::effectiveSignUnixFromKuaidiPayload($api);
$memo[$cacheKey] = $fromApi;
return $fromApi;
}
/**
* 单号精确匹配失败时尝试 TRIM(TrackingNumber)(历史数据首尾空格)。
*
* @return array<string, mixed>|null
*/
private static function loadTrackingDetailNormalized(string $trackingNumber): ?array
{
$n = trim($trackingNumber);
if ($n === '') {
return null;
}
$first = self::getDetailByTrackingNumber($n);
if ($first !== null) {
return $first;
}
$tracking = ExpressTracking::with(['traces' => function ($query) {
$query->order('trace_time_stamp', 'desc');
}])
->whereRaw('TRIM(`tracking_number`) = ?', [$n])
->whereNull('delete_time')
->find();
if (!$tracking) {
return null;
}
$data = $tracking->toArray();
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
return $data;
}
/**
* 与 YejiStatsLogic 物流子查询中每条 express_tracking 的 GREATEST 语义对齐。
*
* @param array<string, mixed> $detail getDetail* / loadTrackingDetailNormalized 结果
*/
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
{
$a = (int) ($detail['sign_time'] ?? 0);
$traces = $detail['traces'] ?? [];
if (!is_array($traces)) {
$traces = [];
}
$b = 0;
foreach ($traces as $tr) {
if (!is_array($tr)) {
continue;
}
if (!self::dbTraceRowLooksSigned($tr)) {
continue;
}
$ts = self::traceRowToUnix($tr);
if ($ts > 0) {
$b = max($b, $ts);
}
}
$c = 0;
$stateOk = ((string) ($detail['current_state'] ?? '') === ExpressTracking::STATE_SIGNED
|| (int) ($detail['is_signed'] ?? 0) === 1);
if ($stateOk) {
foreach ($traces as $tr) {
if (!is_array($tr)) {
continue;
}
$ts = self::traceRowToUnix($tr);
if ($ts > 0) {
$c = max($c, $ts);
}
}
}
return max($a, $b, $c);
}
/**
* @param array<string, mixed> $tr express_trace 行
*/
private static function dbTraceRowLooksSigned(array $tr): bool
{
$code = (string) ($tr['status_code'] ?? '');
if (in_array($code, ['3', '301', '302', '304'], true)) {
return true;
}
$st = (string) ($tr['status'] ?? '');
$ctx = (string) ($tr['trace_context'] ?? '');
$hay = $st . ' ' . $ctx;
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
if (mb_stripos($hay, $k) !== false) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $tr
*/
private static function traceRowToUnix(array $tr): int
{
$ts = (int) ($tr['trace_time_stamp'] ?? 0);
if ($ts > 0) {
return $ts;
}
$raw = trim((string) ($tr['trace_time'] ?? ''));
if ($raw === '') {
return 0;
}
$p = strtotime($raw);
return $p !== false ? (int) $p : 0;
}
/**
* @param array<string, mixed> $api ExpressTrackService::query 返回值
*/
private static function effectiveSignUnixFromKuaidiPayload(array $api): int
{
$traces = $api['traces'] ?? [];
if (!is_array($traces) || $traces === []) {
return 0;
}
$state = (string) ($api['state'] ?? '');
$best = 0;
foreach ($traces as $row) {
if (!is_array($row)) {
continue;
}
$tstr = (string) ($row['time'] ?? '');
$ctx = (string) ($row['context'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts <= 0) {
continue;
}
$signedLike = self::plainContextLooksSigned($ctx);
if ($signedLike) {
$best = max($best, $ts);
}
}
if ($best > 0) {
return $best;
}
if ($state === '3') {
foreach ($traces as $row) {
if (!is_array($row)) {
continue;
}
$tstr = (string) ($row['time'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts > 0) {
$best = max($best, $ts);
}
}
}
return $best;
}
private static function plainContextLooksSigned(string $context): bool
{
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
if (mb_stripos($context, $k) !== false) {
return true;
}
}
return false;
}
}