| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- <?php
- namespace App\Jobs;
- use App\Services\IpRiskService;
- use Illuminate\Bus\Queueable;
- use Illuminate\Contracts\Queue\ShouldQueue;
- use Illuminate\Foundation\Bus\Dispatchable;
- use Illuminate\Queue\InteractsWithQueue;
- use Illuminate\Queue\SerializesModels;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Redis;
- /**
- * IP风险检测异步任务
- *
- * 在用户注册或登录成功后异步执行,检测用户IP是否存在风险:
- * 1. 如果用户已在 Redis Hash 中有记录且 IP 未变 → 跳过
- * 2. IP 变更后重新检测
- * 3. 检测到风险 → 写入 Redis Hash: ip_risk_users → {UserID} → {可疑IP}
- * 4. 未检测到风险 → 从 Hash 中移除(如果之前存在)
- */
- class IpRiskDetection implements ShouldQueue
- {
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- /**
- * Redis Hash key
- */
- const REDIS_HASH_KEY = 'ip_risk_users';
- /**
- * 上次检测 IP 缓存 —— 按日分片 Hash,3 天自动过期
- * key: ip_risk_last_detected:20260507
- * field: UserID
- * value: IP
- * 每天一个 Hash,查过去 3 天的记录,无需大量独立 key
- */
- const LAST_DETECTED_PREFIX = 'ip_risk_last_detected:';
- /**
- * 每次检测覆盖的天数(含当天)
- */
- const DETECTION_WINDOW_DAYS = 3;
- /**
- * @var int 用户ID
- */
- protected $userId;
- /**
- * @var string 待检测的 IP 地址
- */
- protected $ip;
- /**
- * Create a new job instance.
- *
- * @param int $userId
- * @param string $ip
- */
- public function __construct($userId, $ip)
- {
- $this->userId = $userId;
- $this->ip = $ip;
- }
- /**
- * 获取过去 N 天的日期 key 列表(当天在最前)
- *
- * @param int $days
- * @return array
- */
- private function getRecentDateKeys($days = 3)
- {
- $keys = [];
- for ($i = 0; $i < $days; $i++) {
- $keys[] = self::LAST_DETECTED_PREFIX . date('Ymd', strtotime("-{$i} days"));
- }
- return $keys;
- }
- /**
- * Execute the job.
- */
- public function handle()
- {
- $userId = $this->userId;
- $ip = $this->ip;
- if (empty($userId) || empty($ip)) {
- return;
- }
- // 1. 检查过去 3 天的每日 Hash 中是否已检测过同一 IP
- // 只要任意一天记录相同 IP → 跳过(IP 未变)
- $recentKeys = $this->getRecentDateKeys(self::DETECTION_WINDOW_DAYS);
- $todayKey = $recentKeys[0];
- $ipUnchanged = false;
- foreach ($recentKeys as $key) {
- $storedIp = Redis::hget($key, $userId);
- if ($storedIp === $ip) {
- $ipUnchanged = true;
- break;
- }
- }
- if ($ipUnchanged) {
- // IP 在检测窗口内未变,跳过
- return;
- }
- // 2. 执行风险检测
- $service = new IpRiskService();
- $result = $service->detect($ip);
- // 3. 根据检测结果更新风险标记 Hash
- if ($result['is_risky']) {
- $value = $ip . '|' . $result['reason'];
- Redis::hset(self::REDIS_HASH_KEY, $userId, $value);
- Log::info("IpRiskDetection: user flagged", [
- 'user_id' => $userId,
- 'ip' => $ip,
- 'reason' => $result['reason'],
- ]);
- }
- // 4. 记录本次检测结果到今天的 Hash,并确保 3 天过期
- $isNew = !Redis::exists($todayKey);
- Redis::hset($todayKey, $userId, $ip);
- if ($isNew) {
- Redis::expire($todayKey, 86400 * self::DETECTION_WINDOW_DAYS);
- }
- }
- }
|