last_sync_visit_count), * 且自上次同步以来进线未增长时累计 streak,触线才标记降权。 */ class SplitNumberRatioDeferredService { /** * 是否具备计入 streak 的落地页访问基数 * * @param array|Number $number */ public function hasLandingVisitBaseline($number): bool { $visitCount = $this->readInt($number, 'visit_count'); if ($visitCount <= 0) { return false; } $lastVisit = $this->readInt($number, 'last_sync_visit_count'); return $visitCount > $lastVisit; } /** * 根据访问与进线快照计算新的 streak * * @param array|Number $number */ public function computeStreakForVisit($number, int $currentStreak): int { if (!$this->hasLandingVisitBaseline($number)) { return $currentStreak; } $inboundCount = $this->readInt($number, 'inbound_count'); $lastInbound = $this->readInt($number, 'last_sync_inbound_count'); if ($inboundCount <= $lastInbound) { return $currentStreak + 1; } return 0; } /** * 同步侧:根据 visit / inbound 与上次同步检查点重算 streak * * 访问侧(recordVisitAfterRedirect)已按次 +1 累计 streak,此处不得再叠加 delta,否则首次同步会双倍计次导致提前关号。 * 取 max(访问侧 streak, 自上次同步以来的点击增量) 作为裁决值,并兜底访问钩子未触发的场景。 * * @param array|Number $number */ public function computeStreakForSync($number, int $currentStreak): int { $visitCount = $this->readInt($number, 'visit_count'); if ($visitCount <= 0) { return 0; } $lastVisit = $this->readInt($number, 'last_sync_visit_count'); $inboundCount = $this->readInt($number, 'inbound_count'); $lastInbound = $this->readInt($number, 'last_sync_inbound_count'); if ($inboundCount > $lastInbound) { return 0; } if ($visitCount > $lastVisit) { $deltaSinceSync = $visitCount - $lastVisit; return max($currentStreak, $deltaSinceSync); } return $currentStreak; } /** * 是否应标记 ratio_deferred(访问侧触线降权,不关 status) */ public function shouldMarkDeferred(int $streak, int $assignRatio, bool $hasLandingBaseline): bool { return $hasLandingBaseline && $assignRatio > 0 && $streak >= $assignRatio; } /** * 是否为应清理的无效降权记录 * * @param array|Number $number */ public function isInvalidDeferredRecord($number): bool { if ($this->readInt($number, 'ratio_deferred') !== 1) { return false; } return !$this->hasLandingVisitBaseline($number); } /** * 清理无效降权标记(ratio_deferred=1 但无有效落地页访问基线) * * @param int[]|null $adminIds 数据权限管理员 ID 列表,null 表示不限制 * @return int 清理条数 */ public function cleanupInvalidDeferred(?array $adminIds = null): int { $query = Number::where('ratio_deferred', 1); if ($adminIds !== null && $adminIds !== []) { $query->where('admin_id', 'in', $adminIds); } $rows = $query->field('id,visit_count,last_sync_visit_count,ratio_deferred,no_inbound_click_streak')->select(); if ($rows === null || $rows === []) { return 0; } $now = time(); $count = 0; Db::startTrans(); try { foreach ($rows as $row) { if (!$this->isInvalidDeferredRecord($row)) { continue; } $update = [ 'ratio_deferred' => 0, 'ratio_deferred_at' => null, 'updatetime' => $now, ]; if ($this->readInt($row, 'visit_count') <= 0) { $update['no_inbound_click_streak'] = 0; } Number::where('id', (int) $row['id'])->update($update); $count++; } Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } return $count; } /** * 统计当前权限范围内无效降权条数(清理前预览) * * @param int[]|null $adminIds */ public function countInvalidDeferred(?array $adminIds = null): int { $query = Number::where('ratio_deferred', 1); if ($adminIds !== null && $adminIds !== []) { $query->where('admin_id', 'in', $adminIds); } $total = 0; foreach ($query->field('id,visit_count,last_sync_visit_count,ratio_deferred')->select() as $row) { if ($this->isInvalidDeferredRecord($row)) { $total++; } } return $total; } /** * @param array|Number $number */ private function readInt($number, string $field): int { if (is_array($number)) { return (int) ($number[$field] ?? 0); } return (int) $number->getAttr($field); } }