更新
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user