This commit is contained in:
Your Name
2026-06-02 16:47:50 +08:00
parent 860b550180
commit 79719b801e
7 changed files with 469 additions and 68 deletions
@@ -333,7 +333,14 @@ class ExpressTrackingService
// 检查是否签收
if (ExpressTracking::isFinalState($tracking->current_state)) {
$tracking->is_signed = 1;
$tracking->sign_time = $now;
// 签收时间只取真实「妥投/签收/送达」轨迹时间;解析不到就保持原值,
// 绝不用同步当天 $now 冒充签收日(否则 sign_time 列会被写成「拉取当天」,导出/详情/业绩全错)。
$signFromTrace = (!empty($result['traces']) && is_array($result['traces']))
? self::effectiveSignUnixFromKuaidiPayload($result)
: 0;
if ($signFromTrace > 0) {
$tracking->sign_time = $signFromTrace;
}
$tracking->auto_update = 0; // 停止自动更新
}
@@ -1247,8 +1254,24 @@ class ExpressTrackingService
*/
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
{
$a = (int) ($detail['sign_time'] ?? 0);
$traceBased = self::traceBasedSignUnixFromTrackingDetail($detail);
if ($traceBased > 0) {
return $traceBased;
}
// 最后兜底才用 sign_time 列(历史同步可能把「拉取当天」写进该列,故放最后)。
return (int) ($detail['sign_time'] ?? 0);
}
/**
* 纯轨迹/主表推导的签收时间(不含 sign_time 列兜底):
* 签收/妥投/送达轨迹($b)→ 已签收态下最新轨迹($c)→ 主表 latest_trace_time 回填。
* 修复 sign_time 列时用它判断「列值是否被污染」(与轨迹真实时间不符)。
*
* @param array<string, mixed> $detail
*/
private static function traceBasedSignUnixFromTrackingDetail(array $detail): int
{
$traces = $detail['traces'] ?? [];
if (!is_array($traces)) {
$traces = [];
@@ -1283,7 +1306,12 @@ class ExpressTrackingService
}
}
return max($a, $b, $c, self::signedTrackingHeadFallbackUnix($detail, $stateOk));
$traceBased = $b > 0 ? $b : $c;
if ($traceBased > 0) {
return $traceBased;
}
return self::signedTrackingHeadFallbackUnix($detail, $stateOk);
}
/**
@@ -1316,8 +1344,28 @@ class ExpressTrackingService
}
$st = (string) ($tr['status'] ?? '');
$ctx = (string) ($tr['trace_context'] ?? '');
$hay = $st . ' ' . $ctx;
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
return self::contextLooksSigned($st . ' ' . $ctx);
}
/**
* 文案是否表示「已签收/已送达」。
* 先排除「派送中,请您准备签收 / 待签收」等「尚未签收」文案(含「签收」二字易误判),再匹配正向关键词。
*/
private static function contextLooksSigned(string $hay): bool
{
if ($hay === '') {
return false;
}
// 负向:出库派送阶段的「准备签收/待签收」等,并非真正签收
foreach (['准备签收', '待签收', '等待签收', '预计', '即将送达'] as $neg) {
if (mb_stripos($hay, $neg) !== false) {
return false;
}
}
// 「送达」不带「已」(顺丰末端 status=送达 / context=快件已放在…)也算签收;
// 「预计送达 / 即将送达」已在上面负向词排除,不会误判。
foreach (['签收', '妥投', '送达', '本人签收', '已放在'] as $k) {
if (mb_stripos($hay, $k) !== false) {
return true;
}
@@ -1363,13 +1411,14 @@ class ExpressTrackingService
}
$tstr = (string) ($row['time'] ?? '');
$ctx = (string) ($row['context'] ?? '');
// 顺丰等末端把签收信息放在 status(如「送达」)而非 context,故连同 status 一起判断
$st = (string) ($row['status'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts <= 0) {
continue;
}
$signedLike = self::plainContextLooksSigned($ctx);
if ($signedLike) {
if (self::contextLooksSigned($ctx . ' ' . $st)) {
$best = max($best, $ts);
}
}
@@ -1395,17 +1444,6 @@ class ExpressTrackingService
return $best;
}
private static function plainContextLooksSigned(string $context): bool
{
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
if (mb_stripos($context, $k) !== false) {
return true;
}
}
return false;
}
/**
* 提成结算 overview 等大批量场景:按订单批量从库表推导签收时间(不调用快递100)。
* 语义与 YejiStatsLogic 物流子查询 / effectiveSignUnixFromTrackingDetail 对齐。
@@ -1443,27 +1481,35 @@ class ExpressTrackingService
return [];
}
$trackingById = [];
// 仅保留「每个运单算好的签收 Unix」(int),不保留整行轨迹,避免一次性 hydrate 大量 express_trace
// 行导致内存溢出(导出全量 / 批量回填会传入上千运单,每单又有几十条轨迹)。
$signUnixByTrackingId = [];
$orderToTrackingIds = [];
$tnToTrackingIds = [];
$attachTrackingRow = static function (array $etRow) use (&$trackingById, &$orderToTrackingIds, &$tnToTrackingIds): void {
$tid = (int) ($etRow['id'] ?? 0);
$consumeTracking = static function ($tracking) use (&$signUnixByTrackingId, &$orderToTrackingIds, &$tnToTrackingIds): void {
$tid = (int) ($tracking->id ?? 0);
if ($tid <= 0) {
return;
}
$trackingById[$tid] = $etRow;
$oid = (int) ($etRow['order_id'] ?? 0);
if (!array_key_exists($tid, $signUnixByTrackingId)) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
// 算完即丢弃整行轨迹,仅留下结果 int
$signUnixByTrackingId[$tid] = (int) self::effectiveSignUnixFromTrackingDetail($row);
}
$oid = (int) ($tracking->order_id ?? 0);
if ($oid > 0) {
$orderToTrackingIds[$oid][$tid] = true;
}
$tn = trim((string) ($etRow['tracking_number'] ?? ''));
$tn = trim((string) ($tracking->tracking_number ?? ''));
if ($tn !== '') {
$tnToTrackingIds[$tn][$tid] = true;
}
};
foreach (array_chunk($orderIds, 800) as $chunk) {
// 小批量流式处理,每批结束后大对象即可回收,峰值内存只与单批运单的轨迹量相关
foreach (array_chunk($orderIds, 200) as $chunk) {
$byOrderId = ExpressTracking::with(['traces' => static function ($query): void {
$query->order('trace_time_stamp', 'desc');
}])
@@ -1471,15 +1517,14 @@ class ExpressTrackingService
->whereNull('delete_time')
->select();
foreach ($byOrderId as $tracking) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$attachTrackingRow($row);
$consumeTracking($tracking);
}
unset($byOrderId);
}
$trackingNumList = array_keys($trackingNums);
if ($trackingNumList !== []) {
foreach (array_chunk($trackingNumList, 400) as $chunk) {
foreach (array_chunk($trackingNumList, 200) as $chunk) {
$byTn = ExpressTracking::with(['traces' => static function ($query): void {
$query->order('trace_time_stamp', 'desc');
}])
@@ -1487,10 +1532,9 @@ class ExpressTrackingService
->whereNull('delete_time')
->select();
foreach ($byTn as $tracking) {
$row = $tracking->toArray();
$row['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$attachTrackingRow($row);
$consumeTracking($tracking);
}
unset($byTn);
}
}
@@ -1509,11 +1553,7 @@ class ExpressTrackingService
}
$best = 0;
foreach (array_keys($candidateIds) as $tid) {
$detail = $trackingById[(int) $tid] ?? null;
if (!is_array($detail)) {
continue;
}
$best = max($best, self::effectiveSignUnixFromTrackingDetail($detail));
$best = max($best, (int) ($signUnixByTrackingId[(int) $tid] ?? 0));
}
if ($best > 0) {
$out[$oid] = $best;
@@ -1522,4 +1562,61 @@ class ExpressTrackingService
return $out;
}
/**
* 纯库内修复:按现有 express_trace 轨迹重算每个「已签收」运单的真实签收时间,
* 回写 express_tracking.sign_time。不调用快递100(零 HTTP)。
*
* 处理历史污染:早期 queryAndUpdate 把「同步当天 $now」写进 sign_time,签收后 auto_update=0 不再纠正,
* 导致 sign_time 列=拉取当天而非真实送达日。本方法用轨迹时间覆盖被污染的列值。
*
* @return array{scanned:int, updated:int, skipped:int} 统计
*/
public static function repairSignTimeFromDbTraces(bool $dryRun = false, int $limit = 0): array
{
$scanned = 0;
$updated = 0;
$skipped = 0;
$query = ExpressTracking::with(['traces' => static function ($q): void {
$q->order('trace_time_stamp', 'desc');
}])
->whereNull('delete_time')
->where(function ($q): void {
$q->where('is_signed', 1)
->whereOr('current_state', ExpressTracking::STATE_SIGNED);
})
->order('id', 'asc');
$query->chunk(200, function ($trackings) use (&$scanned, &$updated, &$skipped, $dryRun, $limit): bool {
foreach ($trackings as $tracking) {
$scanned++;
$detail = $tracking->toArray();
$detail['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
$trueSign = self::traceBasedSignUnixFromTrackingDetail($detail);
$cur = (int) ($tracking->sign_time ?? 0);
// 仅当轨迹能推导出真实签收时间、且与现列值不同(按秒)时才修复
if ($trueSign <= 0 || $trueSign === $cur) {
$skipped++;
continue;
}
if (!$dryRun) {
$tracking->sign_time = $trueSign;
$tracking->save();
}
$updated++;
if ($limit > 0 && $updated >= $limit) {
return false; // 终止 chunk 遍历
}
}
return true;
}, 'id');
return ['scanned' => $scanned, 'updated' => $updated, 'skipped' => $skipped];
}
}