where('id', $accountId) ->find(); // 减少不必要的日志记录 } if (empty($accountData)) { $errorMsg = "账号不存在:{$accountId}"; Log::error($errorMsg); throw new Exception($errorMsg); } $wechatId = $accountData['wechatId'] ?? ''; if (empty($wechatId)) { $errorMsg = "账号wechatId为空:{$accountId}"; Log::error($errorMsg); throw new Exception($errorMsg); } // 减少不必要的日志记录 // 获取或创建评分记录 $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); $scoreSnapshotBefore = $this->buildScoreSnapshotForLogging($scoreRecord); // 减少不必要的日志记录 // 计算基础分(只计算一次,除非强制重新计算) if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) { $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord); $this->updateBaseScore($accountId, $baseScoreData); // 减少不必要的日志记录 // 重新获取记录以获取最新数据 $scoreRecord = $this->getScoreRecord($accountId); } // 计算动态分(每次都要重新计算) $dynamicScoreData = $this->calculateDynamicScore($accountData, $scoreRecord); // 计算总分 $baseScore = $scoreRecord['baseScore']; $dynamicScore = $dynamicScoreData['total']; $healthScore = $baseScore + $dynamicScore; // 确保健康分在合理范围内(0-100) $healthScore = max(0, min(100, $healthScore)); // 计算每日最大加人次数 $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore); // 更新评分记录 $updateData = [ 'dynamicScore' => $dynamicScore, 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], 'banPenalty' => $dynamicScoreData['banPenalty'], 'lastFrequentTime' => $dynamicScoreData['lastFrequentTime'], 'frequentCount' => $dynamicScoreData['frequentCount'], 'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'], 'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'], 'isBanned' => $dynamicScoreData['isBanned'], 'lastBanTime' => $dynamicScoreData['lastBanTime'], 'healthScore' => $healthScore, 'maxAddFriendPerDay' => $maxAddFriendPerDay, 'updateTime' => time() ]; $updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); // 更新成功后,清除缓存 if ($updateResult !== false) { $this->logScoreChangesIfNeeded( $accountId, $wechatId, $scoreSnapshotBefore, [ 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], 'banPenalty' => $dynamicScoreData['banPenalty'], 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], 'dynamicScore' => $dynamicScore, 'healthScore' => $healthScore ], $dynamicScoreData ); $this->clearScoreCache($accountId); } $result = [ 'accountId' => $accountId, 'wechatId' => $wechatId, 'healthScore' => $healthScore, 'baseScore' => $baseScore, 'baseInfoScore' => $scoreRecord['baseInfoScore'], 'friendCountScore' => $scoreRecord['friendCountScore'], 'dynamicScore' => $dynamicScore, 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], 'banPenalty' => $dynamicScoreData['banPenalty'], 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; // 减少不必要的日志记录 return $result; } catch (\PDOException $e) { // 数据库异常 $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } catch (\Throwable $e) { // 其他所有异常 $errorMsg = "计算健康分失败,accountId: {$accountId}, 错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } } /** * 获取或创建评分记录 * 优化:使用事务和锁避免并发问题,减少重复查询 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @return array 评分记录 */ private function getOrCreateScoreRecord($accountId, $wechatId) { // 尝试获取现有记录 $record = $this->getScoreRecord($accountId); // 如果记录不存在,创建新记录 if (empty($record)) { // 使用事务避免并发问题 Db::startTrans(); try { // 再次检查记录是否存在(避免并发问题) $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->lock(true) // 加锁防止并发插入 ->find(); if (empty($record)) { Log::info("为账号 {$accountId} 创建新的评分记录"); // 检查表中是否存在lastBanTime字段 $this->ensureScoreTableFields(); // 创建新记录 $data = [ 'accountId' => $accountId, 'wechatId' => $wechatId, 'baseScore' => 0, 'baseScoreCalculated' => 0, 'baseInfoScore' => 0, 'friendCountScore' => 0, 'dynamicScore' => 0, 'frequentCount' => 0, 'consecutiveNoFrequentDays' => 0, 'healthScore' => 0, 'maxAddFriendPerDay' => 0, 'lastBanTime' => null, 'createTime' => time(), 'updateTime' => time() ]; Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); $record = $data; } Db::commit(); } catch (\Exception $e) { Db::rollback(); Log::error("创建评分记录失败: " . $e->getMessage()); throw $e; } } return $record; } /** * 确保评分表有所需字段 * 优化:使用静态变量缓存结果,避免重复检查 * * @return void */ private function ensureScoreTableFields() { // 使用静态变量缓存检查结果,避免重复检查 static $fieldsChecked = false; if ($fieldsChecked) { return; } try { // 检查表中是否存在lastBanTime字段 $hasLastBanTimeField = false; $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_ACCOUNT_SCORE); foreach ($tableFields as $field) { if ($field['Field'] == 'lastBanTime') { $hasLastBanTimeField = true; break; } } // 如果字段不存在,添加字段 if (!$hasLastBanTimeField) { Log::info("添加lastBanTime字段到" . self::TABLE_WECHAT_ACCOUNT_SCORE . "表"); Db::execute("ALTER TABLE " . self::TABLE_WECHAT_ACCOUNT_SCORE . " ADD COLUMN lastBanTime INT(11) DEFAULT NULL COMMENT '最后一次封号时间'"); } $fieldsChecked = true; } catch (\Exception $e) { Log::error("检查或添加字段失败: " . $e->getMessage()); // 出错时不影响后续逻辑,继续执行 } } /** * 获取评分记录 * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) * @return array 评分记录,如果不存在则返回空数组 */ private function getScoreRecord($accountId, $useCache = true) { // 生成缓存键 $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); // 减少日志记录,提高性能 return $cachedData ?: []; } // 从数据库获取记录 $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->find(); // 如果记录存在且使用缓存,则缓存记录 if ($record && $useCache) { // 根据数据更新频率设置不同的缓存时间 // 如果记录最近更新过(1小时内),使用短期缓存 $updateTime = $record['updateTime'] ?? 0; $cacheTime = (time() - $updateTime < 3600) ? self::CACHE_TTL_SHORT : self::CACHE_TTL; Cache::set($cacheKey, $record, $cacheTime); Log::debug("缓存评分记录,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $record ?: []; } /** * 计算基础分(只计算一次) * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(3-12分) * * @param array $accountData 账号数据 * @param array $scoreRecord 现有评分记录 * @return array 基础分数据 */ private function calculateBaseScore($accountData, $scoreRecord = []) { $baseScore = self::DEFAULT_BASE_SCORE; // 基础信息分(已修改微信号得10分) $baseInfoScore = $this->getBaseInfoScore($accountData); $baseScore += $baseInfoScore; // 好友数量分(特殊处理:使用快照值,避免同步问题) $friendCountScore = 0; $friendCount = 0; $friendCountSource = 'manual'; // 如果已有评分记录且好友数量分已计算,使用历史值 if (!empty($scoreRecord['friendCountScore']) && $scoreRecord['friendCountScore'] > 0) { $friendCountScore = $scoreRecord['friendCountScore']; $friendCount = $scoreRecord['friendCount'] ?? 0; $friendCountSource = $scoreRecord['friendCountSource'] ?? 'manual'; } else { // 首次计算:使用当前好友数量,但标记为手动计算 $totalFriend = $accountData['totalFriend'] ?? 0; $friendCountScore = $this->getFriendCountScore($totalFriend); $friendCount = $totalFriend; $friendCountSource = 'manual'; } $baseScore += $friendCountScore; // 检查是否已修改微信号 $isModifiedAlias = $this->checkIsModifiedAlias($accountData); return [ 'baseScore' => $baseScore, 'baseInfoScore' => $baseInfoScore, 'friendCountScore' => $friendCountScore, 'friendCount' => $friendCount, 'friendCountSource' => $friendCountSource, 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, 'baseScoreCalculated' => 1, 'baseScoreCalcTime' => time() ]; } /** * 更新基础分 * * @param int $accountId 账号ID * @param array $baseScoreData 基础分数据 * @return bool 更新是否成功 */ private function updateBaseScore($accountId, $baseScoreData) { try { $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($baseScoreData); // 减少不必要的日志记录 // 更新成功后,清除缓存 if ($result !== false) { $this->clearScoreCache($accountId); } return $result !== false; } catch (Exception $e) { Log::error("更新基础分失败,accountId: {$accountId}, 错误: " . $e->getMessage()); return false; } } /** * 清除评分记录缓存 * * @param int $accountId 账号ID * @return bool 是否成功清除缓存 */ private function clearScoreCache($accountId) { $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; $result = Cache::rm($cacheKey); // 减少不必要的日志记录 return $result; } /** * 获取基础信息分 * 已修改微信号:10分 * * @param array $accountData 账号数据 * @return int 基础信息分 */ private function getBaseInfoScore($accountData) { if ($this->checkIsModifiedAlias($accountData)) { return self::BASE_INFO_SCORE; } return 0; } /** * 检查是否已修改微信号 * 判断标准:wechatId和alias不一致且都不为空,则认为已修改微信号 * 注意:这里只用于评分,不修复数据 * * @param array $accountData 账号数据 * @return bool */ private function checkIsModifiedAlias($accountData) { $wechatId = trim($accountData['wechatId'] ?? ''); $alias = trim($accountData['alias'] ?? ''); // 如果wechatId和alias不一致且都不为空,则认为已修改微信号(用于评分) if (!empty($wechatId) && !empty($alias) && $wechatId !== $alias) { return true; } return false; } /** * 获取好友数量分 * 根据好友数量区间得分(最高12分) * * @param int $totalFriend 总好友数 * @return int 好友数量分 */ private function getFriendCountScore($totalFriend) { if ($totalFriend <= 50) { return self::FRIEND_COUNT_SCORE_0_50; } elseif ($totalFriend <= 500) { return self::FRIEND_COUNT_SCORE_51_500; } elseif ($totalFriend <= 3000) { return self::FRIEND_COUNT_SCORE_501_3000; } else { return self::FRIEND_COUNT_SCORE_3001_PLUS; } } /** * 手动更新好友数量分(用于处理同步问题) * * @param int $accountId 账号ID * @param int $friendCount 好友数量 * @param string $source 来源(manual=手动,sync=同步) * @return bool 更新是否成功 * @throws Exception 如果参数无效或更新过程中出现错误 */ public function updateFriendCountScore($accountId, $friendCount, $source = 'manual') { // 参数验证 if (empty($accountId) || !is_numeric($accountId)) { $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId)); Log::error($errorMsg); throw new Exception($errorMsg); } if (!is_numeric($friendCount) || $friendCount < 0) { $errorMsg = "无效的好友数量: {$friendCount}"; Log::error($errorMsg); throw new Exception($errorMsg); } if (!in_array($source, ['manual', 'sync'])) { $errorMsg = "无效的来源: {$source},必须是 'manual' 或 'sync'"; Log::error($errorMsg); throw new Exception($errorMsg); } try { $scoreRecord = $this->getScoreRecord($accountId); // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { // 同步数据不允许修改已计算的基础分 Log::warning("同步数据不允许修改已计算的基础分,accountId: {$accountId}"); return false; } } catch (\Exception $e) { $errorMsg = "获取评分记录失败,accountId: {$accountId}, 错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } $friendCountScore = $this->getFriendCountScore($friendCount); // 重新计算基础分 $oldBaseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE; $oldFriendCountScore = $scoreRecord['friendCountScore'] ?? 0; $baseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; $newBaseScore = self::DEFAULT_BASE_SCORE + $baseInfoScore + $friendCountScore; $updateData = [ 'friendCountScore' => $friendCountScore, 'friendCount' => $friendCount, 'friendCountSource' => $source, 'baseScore' => $newBaseScore, 'updateTime' => time() ]; // 如果基础分已计算,需要更新总分 if (!empty($scoreRecord['baseScoreCalculated'])) { $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; $healthScore = $newBaseScore + $dynamicScore; $healthScore = max(0, min(100, $healthScore)); $updateData['healthScore'] = $healthScore; $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore); } try { $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); // 更新成功后,清除缓存 if ($result !== false) { $this->clearScoreCache($accountId); $this->clearHealthScoreCache($accountId); Log::info("更新好友数量分成功,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}"); } else { Log::warning("更新好友数量分失败,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}"); } return $result !== false; } catch (\PDOException $e) { $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } catch (\Throwable $e) { $errorMsg = "更新好友数量分失败,accountId: {$accountId}, 错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } } /** * 计算动态分 * 动态分 = 扣分 + 加分 * 如果添加好友记录表没有记录,则动态分为0 * * @param array $accountData 账号数据 * @param array $scoreRecord 现有评分记录 * @return array 动态分数据 */ private function calculateDynamicScore($accountData, $scoreRecord) { $accountId = $accountData['id'] ?? 0; $wechatId = $accountData['wechatId'] ?? ''; Log::debug("开始计算动态分,accountId: {$accountId}, wechatId: {$wechatId}"); $result = [ 'total' => 0, 'frequentPenalty' => 0, 'noFrequentBonus' => 0, 'banPenalty' => 0, 'lastFrequentTime' => null, 'frequentCount' => 0, 'lastNoFrequentTime' => null, 'consecutiveNoFrequentDays' => 0, 'isBanned' => 0, 'lastBanTime' => null, 'frequentTaskIds' => [], 'banMessageId' => null ]; if (empty($accountId) || empty($wechatId)) { Log::warning("计算动态分失败: accountId或wechatId为空"); return $result; } // 不再使用30天限制 // 检查添加好友记录表是否有记录,如果没有记录则动态分为0 // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数 $hasFriendTask = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) ->where(function($query) use ($wechatId) { if (!empty($wechatId)) { $query->where('wechatId', $wechatId); } }) ->value('id'); // 只获取ID,比count()更高效 // 如果添加好友记录表没有记录,则动态分为0 if (empty($hasFriendTask)) { Log::info("账号没有添加好友记录,动态分为0,accountId: {$accountId}"); return $result; } Log::debug("账号有添加好友记录,继续计算动态分,accountId: {$accountId}"); // 继承现有数据 if (!empty($scoreRecord)) { $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null; $result['frequentCount'] = $scoreRecord['frequentCount'] ?? 0; $result['lastNoFrequentTime'] = $scoreRecord['lastNoFrequentTime'] ?? null; $result['consecutiveNoFrequentDays'] = $scoreRecord['consecutiveNoFrequentDays'] ?? 0; $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0; $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0; $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0; $result['lastBanTime'] = $scoreRecord['lastBanTime'] ?? null; } // 1. 检查频繁记录(从s2_friend_task表查询,不限制时间) $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; $result['frequentTaskIds'] = $frequentData['taskIds'] ?? []; // 2. 检查封号记录(从s2_wechat_message表查询,不限制时间) $banData = $this->checkBannedFromMessage($accountId, $wechatId); if (!empty($banData)) { $result['isBanned'] = $banData['isBanned']; $result['banPenalty'] = $banData['banPenalty']; $result['lastBanTime'] = $banData['lastBanTime']; $result['banMessageId'] = $banData['messageId'] ?? null; } // 3. 计算不频繁加分(基于频繁记录,反向参考频繁规则) $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData); $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; // 计算总分 $result['total'] = $result['frequentPenalty'] + $result['noFrequentBonus'] + $result['banPenalty']; Log::debug("动态分计算结果,accountId: {$accountId}, frequentPenalty: {$result['frequentPenalty']}, " . "noFrequentBonus: {$result['noFrequentBonus']}, banPenalty: {$result['banPenalty']}, " . "total: {$result['total']}"); return $result; } /** * 从s2_friend_task表检查频繁记录 * extra字段包含"操作过于频繁"即需要扣分 * 统计所有时间的数据(不限制30天) * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $scoreRecord 现有评分记录 * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null) { // 不再使用30天限制 // 减少不必要的日志记录 // 查询包含"操作过于频繁"的记录(统计所有时间且未被统计过的记录) // extra字段可能是文本或JSON格式,使用LIKE查询 // 优化查询:只查询必要的字段,减少数据传输量 // 添加is_counted条件,只查询未被统计过的记录 $frequentTasks = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) ->where(function($query) use ($wechatId) { if (!empty($wechatId)) { $query->where('wechatId', $wechatId); } }) ->where(function($query) { // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON) $query->where('extra', 'like', '%操作过于频繁%') ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); }) ->where(function($query) { // 只查询未被统计过的记录 // 注意:需要兼容is_counted字段不存在的情况 $query->where('is_counted', 0) ->whereOr('is_counted', null); }) ->order('createTime', 'desc') ->field('id, createTime, extra') ->select(); // 获取最新的频繁时间 $latestFrequentTime = !empty($frequentTasks) ? $frequentTasks[0]['createTime'] : null; // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录) $frequentCount = count($frequentTasks); Log::info("找到 {$frequentCount} 条未统计的频繁记录,accountId: {$accountId}, wechatId: {$wechatId}"); // 标记这些记录为已统计 if (!empty($frequentTasks)) { $taskIds = array_column($frequentTasks, 'id'); try { // 检查表中是否存在is_counted字段 $hasIsCountedField = false; $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_FRIEND_TASK); foreach ($tableFields as $field) { if ($field['Field'] == 'is_counted') { $hasIsCountedField = true; break; } } // 如果字段不存在,添加字段 if (!$hasIsCountedField) { Log::info("添加is_counted字段到" . self::TABLE_FRIEND_TASK . "表"); Db::execute("ALTER TABLE " . self::TABLE_FRIEND_TASK . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); } // 更新记录为已统计 Db::table(self::TABLE_FRIEND_TASK) ->where('id', 'in', $taskIds) ->update(['is_counted' => 1]); // 减少不必要的日志记录 } catch (\Exception $e) { Log::error("标记频繁记录失败: " . $e->getMessage()); // 出错时不影响后续逻辑,继续执行 } } // 如果30天内没有频繁记录,清除扣分 if (empty($frequentTasks)) { return [ 'lastFrequentTime' => null, 'frequentCount' => 0, 'frequentPenalty' => 0, 'taskIds' => [] ]; } // 根据30天内的频繁次数计算扣分 $penalty = 0; if ($frequentCount == 1) { $penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 Log::info("首次频繁,扣除 " . abs(self::PENALTY_FIRST_FREQUENT) . " 分,accountId: {$accountId}"); } elseif ($frequentCount >= 2) { $penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 Log::info("再次频繁,扣除 " . abs(self::PENALTY_SECOND_FREQUENT) . " 分,accountId: {$accountId}"); } return [ 'lastFrequentTime' => $latestFrequentTime, 'frequentCount' => $frequentCount, 'frequentPenalty' => $penalty, 'taskIds' => $taskIds ]; } /** * 从s2_wechat_message表检查封号记录 * content包含"你的账号被限制"且msgType为10000 * 统计所有时间的数据(不限制30天) * 每条记录只统计一次,使用is_counted字段标记 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array|null */ private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null) { // 不再使用30天限制 // 减少不必要的日志记录 // 查询封号消息(统计所有时间且未被统计过的记录) // 优化查询:只查询必要的字段,减少数据传输量 $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE) ->where('wechatAccountId', $accountId) ->where('msgType', 10000) ->where('content', 'like', '%你的账号被限制%') ->where('isDeleted', 0) ->where(function($query) { // 只查询未被统计过的记录 // 注意:需要兼容is_counted字段不存在的情况 $query->where('is_counted', 0) ->whereOr('is_counted', null); }) ->field('id, createTime') // 只查询必要的字段 ->order('createTime', 'desc') ->find(); if (!empty($banMessage)) { try { // 检查表中是否存在is_counted字段 $hasIsCountedField = false; $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_MESSAGE); foreach ($tableFields as $field) { if ($field['Field'] == 'is_counted') { $hasIsCountedField = true; break; } } // 如果字段不存在,添加字段 if (!$hasIsCountedField) { Log::info("添加is_counted字段到" . self::TABLE_WECHAT_MESSAGE . "表"); Db::execute("ALTER TABLE " . self::TABLE_WECHAT_MESSAGE . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'"); } // 更新记录为已统计 Db::table(self::TABLE_WECHAT_MESSAGE) ->where('id', $banMessage['id']) ->update(['is_counted' => 1]); // 减少不必要的日志记录 Log::info("发现封号记录,扣除 " . abs(self::PENALTY_BANNED) . " 分,accountId: {$accountId}"); } catch (\Exception $e) { Log::error("标记封号记录失败: " . $e->getMessage()); // 出错时不影响后续逻辑,继续执行 } return [ 'isBanned' => 1, 'banPenalty' => self::PENALTY_BANNED, // 封号-60分 'lastBanTime' => $banMessage['createTime'], 'messageId' => $banMessage['id'] ]; } return [ 'isBanned' => 0, 'banPenalty' => 0, 'lastBanTime' => null, 'messageId' => null ]; } /** * 计算不频繁加分 * 反向参考频繁规则:计算连续不频繁天数 * 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) * 如果连续3天没有频繁,则每天+5分 * * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount) * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性 * @return array 包含bonus、consecutiveDays、lastNoFrequentTime */ private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null) { $result = [ 'bonus' => 0, 'consecutiveDays' => 0, 'lastNoFrequentTime' => null ]; if (empty($accountId) || empty($wechatId)) { return $result; } $currentTime = time(); // 获取最后一次频繁时间 $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null; // 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) if (empty($lastFrequentTime)) { // 情况1:没有频繁记录,说明一直连续不频繁 // 默认给30天的连续不频繁天数(可以根据需要调整) $consecutiveDays = 30; } else { // 情况2:有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算 // 计算从最后一次频繁时间到现在,连续多少天没有频繁 $timeDiff = $currentTime - $lastFrequentTime; $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数 } // 如果连续3天或以上没有频繁,则每天+5分 if ($consecutiveDays >= 3) { $bonus = $consecutiveDays * self::BONUS_NO_FREQUENT_PER_DAY; $result['bonus'] = $bonus; $result['consecutiveDays'] = $consecutiveDays; $result['lastNoFrequentTime'] = $currentTime; } else { $result['consecutiveDays'] = $consecutiveDays; } return $result; } /** * 构建日志快照(用于对比前后分值) * * @param array $scoreRecord * @return array */ private function buildScoreSnapshotForLogging($scoreRecord) { $baseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE; $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; $healthScore = $scoreRecord['healthScore'] ?? ($baseScore + $dynamicScore); return [ 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0, 'banPenalty' => $scoreRecord['banPenalty'] ?? 0, 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0, 'dynamicScore' => $dynamicScore, 'healthScore' => $healthScore ]; } /** * 根据前后快照写加减分日志 * * @param int $accountId * @param string $wechatId * @param array $before * @param array $after * @param array $context * @return void */ private function logScoreChangesIfNeeded($accountId, $wechatId, array $before, array $after, array $context = []) { $healthBefore = $before['healthScore'] ?? 0; $healthAfter = $after['healthScore'] ?? 0; $this->recordScoreLog($accountId, $wechatId, 'frequentPenalty', $before['frequentPenalty'] ?? 0, $after['frequentPenalty'] ?? 0, [ 'category' => 'penalty', 'source' => 'friend_task', 'sourceId' => !empty($context['frequentTaskIds']) ? $context['frequentTaskIds'][0] : null, 'extra' => [ 'taskIds' => $context['frequentTaskIds'] ?? [], 'frequentCount' => $context['frequentCount'] ?? 0, 'lastFrequentTime' => $context['lastFrequentTime'] ?? null ], 'totalScoreBefore' => $healthBefore, 'totalScoreAfter' => $healthAfter ]); $this->recordScoreLog($accountId, $wechatId, 'banPenalty', $before['banPenalty'] ?? 0, $after['banPenalty'] ?? 0, [ 'category' => 'penalty', 'source' => 'wechat_message', 'sourceId' => $context['banMessageId'] ?? null, 'extra' => [ 'lastBanTime' => $context['lastBanTime'] ?? null ], 'totalScoreBefore' => $healthBefore, 'totalScoreAfter' => $healthAfter ]); $this->recordScoreLog($accountId, $wechatId, 'noFrequentBonus', $before['noFrequentBonus'] ?? 0, $after['noFrequentBonus'] ?? 0, [ 'category' => 'bonus', 'source' => 'system', 'extra' => [ 'consecutiveDays' => $context['consecutiveNoFrequentDays'] ?? 0, 'lastNoFrequentTime' => $context['lastNoFrequentTime'] ?? null ], 'totalScoreBefore' => $healthBefore, 'totalScoreAfter' => $healthAfter ]); $this->recordScoreLog($accountId, $wechatId, 'dynamicScore', $before['dynamicScore'] ?? 0, $after['dynamicScore'] ?? 0, [ 'category' => 'dynamic_total', 'source' => 'system', 'totalScoreBefore' => $healthBefore, 'totalScoreAfter' => $healthAfter ]); $this->recordScoreLog($accountId, $wechatId, 'healthScore', $before['healthScore'] ?? 0, $after['healthScore'] ?? 0, [ 'category' => 'health_total', 'source' => 'system', 'totalScoreBefore' => $healthBefore, 'totalScoreAfter' => $healthAfter ]); } /** * 插入健康分加减分日志 * * @param int $accountId * @param string $wechatId * @param string $field * @param int|null $beforeValue * @param int|null $afterValue * @param array $context * @return void */ private function recordScoreLog($accountId, $wechatId, $field, $beforeValue, $afterValue, array $context = []) { $beforeValue = (int)($beforeValue ?? 0); $afterValue = (int)($afterValue ?? 0); if ($beforeValue === $afterValue) { return; } $extraPayload = $context['extra'] ?? null; if (is_array($extraPayload)) { $extraPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } elseif (!is_string($extraPayload)) { $extraPayload = null; } $sourceId = null; if (array_key_exists('sourceId', $context)) { $sourceId = $context['sourceId']; } $totalScoreBefore = null; if (array_key_exists('totalScoreBefore', $context)) { $totalScoreBefore = $context['totalScoreBefore']; } $totalScoreAfter = null; if (array_key_exists('totalScoreAfter', $context)) { $totalScoreAfter = $context['totalScoreAfter']; } $data = [ 'accountId' => $accountId, 'wechatId' => $wechatId, 'field' => $field, 'changeValue' => $afterValue - $beforeValue, 'valueBefore' => $beforeValue, 'valueAfter' => $afterValue, 'category' => $context['category'] ?? null, 'source' => $context['source'] ?? null, 'sourceId' => $sourceId, 'extra' => $extraPayload, 'totalScoreBefore' => $totalScoreBefore, 'totalScoreAfter' => $totalScoreAfter, 'createTime' => time() ]; try { Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE_LOG)->insert($data); } catch (\Exception $e) { Log::error("记录健康分加减分日志失败,accountId: {$accountId}, field: {$field}, 错误: " . $e->getMessage()); } } /** * 根据健康分计算每日最大加人次数 * 公式:每日最大加人次数 = 健康分 * 0.2 * * @param int $healthScore 健康分 * @return int 每日最大加人次数 */ public function getMaxAddFriendPerDay($healthScore) { return (int)floor($healthScore * 0.2); } /** * 批量计算并更新多个账号的健康分 * 优化:使用多线程处理、优化批处理逻辑、减少日志记录 * * @param array $accountIds 账号ID数组(为空则处理所有账号) * @param int $batchSize 每批处理数量 * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @param bool $useMultiThread 是否使用多线程处理(需要pcntl扩展支持) * @return array 处理结果统计 * @throws Exception 如果参数无效或批量处理过程中出现严重错误 */ public function batchCalculateAndUpdate($accountIds = [], $batchSize = 50, $forceRecalculateBase = false, $useMultiThread = false) { // 参数验证 if (!is_array($accountIds)) { $errorMsg = "无效的账号ID数组: " . gettype($accountIds); Log::error($errorMsg); throw new Exception($errorMsg); } if (!is_numeric($batchSize) || $batchSize <= 0) { $errorMsg = "无效的批处理大小: {$batchSize}"; Log::error($errorMsg); throw new Exception($errorMsg); } // 检查是否支持多线程 if ($useMultiThread && !function_exists('pcntl_fork')) { $useMultiThread = false; Log::warning("系统不支持pcntl扩展,无法使用多线程处理,将使用单线程模式"); } try { $startTime = microtime(true); // 去除开始日志,减少日志空间消耗 $stats = [ 'total' => 0, 'success' => 0, 'failed' => 0, 'errors' => [] ]; // 如果没有指定账号ID,则处理所有账号 if (empty($accountIds)) { $accountIds = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('isDeleted', 0) ->column('id'); } $stats['total'] = count($accountIds); // 优化:减小批次大小,提高并行处理效率 $batchSize = min($batchSize, 50); // 分批处理 $batches = array_chunk($accountIds, $batchSize); $batchCount = count($batches); Log::info("分批处理,共 {$batchCount} 批"); // 多线程处理 if ($useMultiThread && $batchCount > 1) { $childPids = []; $maxProcesses = 4; // 最大并行进程数 $runningProcesses = 0; for ($i = 0; $i < $batchCount; $i++) { // 如果达到最大进程数,等待某个子进程结束 if ($runningProcesses >= $maxProcesses) { $pid = pcntl_wait($status); $runningProcesses--; } // 创建子进程 $pid = pcntl_fork(); if ($pid == -1) { // 创建进程失败 Log::error("创建子进程失败"); continue; } elseif ($pid == 0) { // 子进程 $this->processBatch($batches[$i], $i, $batchCount, $forceRecalculateBase); exit(0); } else { // 父进程 $childPids[] = $pid; $runningProcesses++; } } // 等待所有子进程结束 foreach ($childPids as $pid) { pcntl_waitpid($pid, $status); } Log::info("所有批次处理完成"); } else { // 单线程处理 foreach ($batches as $batchIndex => $batch) { $batchStats = $this->processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase); $stats['success'] += $batchStats['success']; $stats['failed'] += $batchStats['failed']; $stats['errors'] = array_merge($stats['errors'], $batchStats['errors']); } } $endTime = microtime(true); $totalDuration = round($endTime - $startTime, 2); // 只在有失败时记录日志 if ($stats['failed'] > 0) { Log::warning("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); } return $stats; } catch (\PDOException $e) { $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } catch (\Throwable $e) { $errorMsg = "批量计算健康分过程中发生严重错误: " . $e->getMessage(); Log::error($errorMsg); throw new Exception($errorMsg, $e->getCode(), $e); } } /** * 处理单个批次的账号 * * @param array $batch 批次账号ID数组 * @param int $batchIndex 批次索引 * @param int $batchCount 总批次数 * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @return array 处理结果统计 */ private function processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase) { $batchStartTime = microtime(true); // 去除批次开始日志,减少日志空间消耗 $stats = [ 'success' => 0, 'failed' => 0, 'errors' => [] ]; // 优化:预先获取账号数据,减少重复查询 $accountIds = implode(',', $batch); $accountDataMap = []; if (!empty($batch)) { $accountDataList = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', 'in', $batch) ->select(); foreach ($accountDataList as $accountData) { $accountDataMap[$accountData['id']] = $accountData; } } // 批量处理账号 foreach ($batch as $accountId) { try { $accountData = $accountDataMap[$accountId] ?? null; $this->calculateAndUpdate($accountId, $accountData, $forceRecalculateBase); $stats['success']++; // 减少日志记录,每10个账号记录一次进度 if ($stats['success'] % 10 == 0) { Log::debug("批次 " . ($batchIndex + 1) . " 已处理 {$stats['success']} 个账号"); } } catch (Exception $e) { $stats['failed']++; $stats['errors'][] = [ 'accountId' => $accountId, 'error' => $e->getMessage() ]; Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); } } $batchEndTime = microtime(true); $batchDuration = round($batchEndTime - $batchStartTime, 2); Log::info("第 " . ($batchIndex + 1) . "/" . $batchCount . " 批处理完成,耗时: {$batchDuration}秒," . "成功: {$stats['success']},失败: {$stats['failed']}"); return $stats; } /** * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测) * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成 * * @param int $accountId 账号ID * @return bool */ public function recordFrequent($accountId) { // 频繁检测已改为从s2_friend_task表自动检测 // 直接重新计算健康分即可 try { $this->calculateAndUpdate($accountId); return true; } catch (\Exception $e) { return false; } } /** * 记录不频繁事件(用于加分) * * @param int $accountId 账号ID * @return bool */ public function recordNoFrequent($accountId) { $scoreRecord = $this->getScoreRecord($accountId); if (empty($scoreRecord)) { // 如果记录不存在,先创建 $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', $accountId) ->find(); if (empty($accountData)) { return false; } $this->getOrCreateScoreRecord($accountId, $accountData['wechatId']); $scoreRecord = $this->getScoreRecord($accountId); } $lastNoFrequentTime = $scoreRecord['lastNoFrequentTime'] ?? null; $consecutiveNoFrequentDays = $scoreRecord['consecutiveNoFrequentDays'] ?? 0; $currentTime = time(); // 如果上次不频繁时间是昨天或更早,则增加连续天数 if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) { // 如果间隔超过2天,重置为1天 if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) { $consecutiveNoFrequentDays = 1; } else { $consecutiveNoFrequentDays++; } } // 计算加分(连续3天及以上才加分) $bonus = 0; if ($consecutiveNoFrequentDays >= 3) { $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; } $updateData = [ 'lastNoFrequentTime' => $currentTime, 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays, 'noFrequentBonus' => $bonus, 'updateTime' => $currentTime ]; Db::table('s2_wechat_account_score') ->where('accountId', $accountId) ->update($updateData); // 重新计算健康分 $this->calculateAndUpdate($accountId); return true; } /** * 获取账号健康分信息 * 优化:使用多级缓存策略,提高缓存命中率 * * @param int $accountId 账号ID * @param bool $useCache 是否使用缓存(默认true) * @param bool $forceRecalculate 是否强制重新计算(默认false) * @return array|null */ public function getHealthScore($accountId, $useCache = true, $forceRecalculate = false) { // 如果强制重新计算,则不使用缓存 if ($forceRecalculate) { Log::info("强制重新计算健康分,accountId: {$accountId}"); return $this->calculateAndUpdate($accountId, null, false); } // 生成缓存键 $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId; // 如果使用缓存且缓存存在,则直接返回缓存数据 if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) { $cachedData = Cache::get($cacheKey); // 减少日志记录,提高性能 return $cachedData; } // 从数据库获取记录 $scoreRecord = $this->getScoreRecord($accountId, $useCache); if (empty($scoreRecord)) { return null; } $healthScoreInfo = [ 'accountId' => $scoreRecord['accountId'], 'wechatId' => $scoreRecord['wechatId'], 'healthScore' => $scoreRecord['healthScore'] ?? 0, 'baseScore' => $scoreRecord['baseScore'] ?? 0, 'baseInfoScore' => $scoreRecord['baseInfoScore'] ?? 0, 'friendCountScore' => $scoreRecord['friendCountScore'] ?? 0, 'friendCount' => $scoreRecord['friendCount'] ?? 0, 'dynamicScore' => $scoreRecord['dynamicScore'] ?? 0, 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0, 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0, 'banPenalty' => $scoreRecord['banPenalty'] ?? 0, 'maxAddFriendPerDay' => $scoreRecord['maxAddFriendPerDay'] ?? 0, 'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0, 'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null, 'frequentCount' => $scoreRecord['frequentCount'] ?? 0, 'isBanned' => $scoreRecord['isBanned'] ?? 0, 'lastBanTime' => $scoreRecord['lastBanTime'] ?? null ]; // 如果使用缓存,则缓存健康分信息 if ($useCache) { // 根据数据更新频率设置不同的缓存时间 // 如果有频繁记录或封号记录,使用短期缓存 $cacheTime = (!empty($scoreRecord['lastFrequentTime']) || !empty($scoreRecord['isBanned'])) ? self::CACHE_TTL_SHORT : self::CACHE_TTL; Cache::set($cacheKey, $healthScoreInfo, $cacheTime); Log::debug("缓存健康分信息,accountId: {$accountId}, 缓存时间: {$cacheTime}秒"); } return $healthScoreInfo; } /** * 清除健康分信息缓存 * * @param int $accountId 账号ID * @return bool 是否成功清除缓存 */ public function clearHealthScoreCache($accountId) { $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId; $result = Cache::rm($cacheKey); // 同时清除评分记录缓存 $this->clearScoreCache($accountId); // 减少不必要的日志记录 return $result; } }