Files
cunkebao_v3/Server/application/common/service/WechatAccountHealthScoreService.php
2025-11-21 17:22:33 +08:00

1065 lines
42 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\common\service;
use think\Db;
use think\Exception;
use think\facade\Log;
use think\facade\Cache;
/**
* 微信账号健康分评分服务(优化版)
* 基于《微信健康分规则v2.md》实现
*
* 优化点:
* 1. 基础分只计算一次
* 2. 各个评分维度独立存储
* 3. 使用独立的评分记录表
* 4. 好友数量评分特殊处理(避免同步问题)
* 5. 动态分仅统计近30天数据
* 6. 优化数据库查询,减少重复计算
* 7. 添加完善的日志记录,便于问题排查
*
* 健康分 = 基础分 + 动态分
* 基础分60-100分默认60分 + 基础信息10分 + 好友数量30分
* 动态分:扣分和加分规则
*
* @author Your Name
* @version 2.0.0
*/
class WechatAccountHealthScoreService
{
/**
* 缓存相关配置
*/
const CACHE_PREFIX = 'wechat_health_score:'; // 缓存前缀
const CACHE_TTL = 3600; // 缓存有效期(秒)
/**
* 默认基础分
*/
const DEFAULT_BASE_SCORE = 60;
/**
* 基础信息分数
*/
const BASE_INFO_SCORE = 10;
/**
* 好友数量分数区间
*/
const FRIEND_COUNT_SCORE_0_50 = 3; // 0-50个好友
const FRIEND_COUNT_SCORE_51_500 = 6; // 51-500个好友
const FRIEND_COUNT_SCORE_501_3000 = 8; // 501-3000个好友
const FRIEND_COUNT_SCORE_3001_PLUS = 12; // 3001+个好友
/**
* 动态分扣分规则
*/
const PENALTY_FIRST_FREQUENT = -15; // 首次频繁扣15分
const PENALTY_SECOND_FREQUENT = -25; // 再次频繁扣25分
const PENALTY_BANNED = -60; // 封号扣60分
/**
* 动态分加分规则
*/
const BONUS_NO_FREQUENT_PER_DAY = 5; // 连续3天不触发频繁每天+5分
/**
* 数据库表名
*/
const TABLE_WECHAT_ACCOUNT = 's2_wechat_account';
const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score';
const TABLE_FRIEND_TASK = 's2_friend_task';
const TABLE_WECHAT_MESSAGE = 's2_wechat_message';
/**
* 计算并更新账号健康分
*
* @param int $accountId 账号IDs2_wechat_account表的id
* @param array $accountData 账号数据(可选,如果不传则从数据库查询)
* @param bool $forceRecalculateBase 是否强制重新计算基础分默认false
* @return array 返回评分结果
* @throws Exception 如果计算过程中出现错误
*/
public function calculateAndUpdate($accountId, $accountData = null, $forceRecalculateBase = false)
{
// 参数验证
if (empty($accountId) || !is_numeric($accountId)) {
$errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId));
Log::error($errorMsg);
throw new Exception($errorMsg);
}
try {
Log::info("开始计算账号健康分accountId: {$accountId}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false'));
// 获取账号数据
if (empty($accountData)) {
$accountData = Db::table(self::TABLE_WECHAT_ACCOUNT)
->where('id', $accountId)
->find();
Log::debug("查询账号数据: " . ($accountData ? "成功" : "失败"));
}
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);
}
Log::debug("账号数据: accountId={$accountId}, wechatId={$wechatId}");
// 获取或创建评分记录
$scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId);
Log::debug("获取评分记录: " . ($scoreRecord ? "成功" : "失败"));
// 计算基础分(只计算一次,除非强制重新计算)
if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) {
Log::info("计算基础分accountId: {$accountId}, baseScoreCalculated: " .
($scoreRecord['baseScoreCalculated'] ? 'true' : 'false') .
", forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false'));
$baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord);
$this->updateBaseScore($accountId, $baseScoreData);
Log::debug("基础分计算结果: " . json_encode($baseScoreData));
// 重新获取记录以获取最新数据
$scoreRecord = $this->getScoreRecord($accountId);
}
// 计算动态分(每次都要重新计算)
Log::info("计算动态分accountId: {$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);
Log::info("健康分计算结果accountId: {$accountId}, baseScore: {$baseScore}, dynamicScore: {$dynamicScore}, " .
"healthScore: {$healthScore}, maxAddFriendPerDay: {$maxAddFriendPerDay}");
// 更新评分记录
$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'],
'healthScore' => $healthScore,
'maxAddFriendPerDay' => $maxAddFriendPerDay,
'updateTime' => time()
];
$updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->update($updateData);
// 更新成功后,清除缓存
if ($updateResult !== false) {
$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
];
Log::debug("健康分计算完成,返回结果: " . json_encode($result));
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)) {
Log::info("为账号 {$accountId} 创建新的评分记录");
// 创建新记录
$data = [
'accountId' => $accountId,
'wechatId' => $wechatId,
'baseScore' => 0,
'baseScoreCalculated' => 0,
'baseInfoScore' => 0,
'friendCountScore' => 0,
'dynamicScore' => 0,
'frequentCount' => 0,
'consecutiveNoFrequentDays' => 0,
'healthScore' => 0,
'maxAddFriendPerDay' => 0,
'createTime' => time(),
'updateTime' => time()
];
Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data);
return $data;
}
return $record;
}
/**
* 获取评分记录
*
* @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);
Log::debug("从缓存获取评分记录accountId: {$accountId}");
return $cachedData ?: [];
}
// 从数据库获取记录
$record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->find();
// 如果记录存在且使用缓存,则缓存记录
if ($record && $useCache) {
Cache::set($cacheKey, $record, self::CACHE_TTL);
Log::debug("缓存评分记录accountId: {$accountId}");
}
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);
Log::debug("更新基础分accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败"));
// 更新成功后,清除缓存
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);
Log::debug("清除评分记录缓存accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败"));
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
];
if (empty($accountId) || empty($wechatId)) {
Log::warning("计算动态分失败: accountId或wechatId为空");
return $result;
}
// 计算30天前的时间戳在多个方法中使用
$thirtyDaysAgo = time() - (30 * 24 * 3600);
// 检查添加好友记录表是否有记录如果没有记录则动态分为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("账号没有添加好友记录动态分为0accountId: {$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;
}
// 1. 检查频繁记录从s2_friend_task表查询只统计近30天
$frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo);
$result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null;
$result['frequentCount'] = $frequentData['frequentCount'] ?? 0;
$result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0;
// 2. 检查封号记录从s2_wechat_message表查询
$banData = $this->checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo);
if (!empty($banData)) {
$result['isBanned'] = $banData['isBanned'];
$result['banPenalty'] = $banData['banPenalty'];
}
// 3. 计算不频繁加分基于近30天的频繁记录反向参考频繁规则
$noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo);
$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天的数据
*
* @param int $accountId 账号ID
* @param string $wechatId 微信ID
* @param array $scoreRecord 现有评分记录
* @param int $thirtyDaysAgo 30天前的时间戳可选如果已计算则传入以避免重复计算
* @return array|null
*/
private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null)
{
// 如果没有传入30天前的时间戳则计算
if ($thirtyDaysAgo === null) {
$thirtyDaysAgo = time() - (30 * 24 * 3600);
}
// 查询包含"操作过于频繁"的记录只统计近30天
// extra字段可能是文本或JSON格式使用LIKE查询
// 优化查询:只查询必要的字段,减少数据传输量
$frequentTasks = Db::table(self::TABLE_FRIEND_TASK)
->where('wechatAccountId', $accountId)
->where('createTime', '>=', $thirtyDaysAgo)
->where(function($query) use ($wechatId) {
if (!empty($wechatId)) {
$query->where('wechatId', $wechatId);
}
})
->where(function($query) {
// 检查extra字段是否包含"操作过于频繁"可能是文本或JSON
$query->where('extra', 'like', '%操作过于频繁%')
->whereOr('extra', 'like', '%"当前账号存在安全风险"%');
})
->order('createTime', 'desc')
->field('id, createTime, extra')
->select();
// 获取最新的频繁时间
$latestFrequentTime = !empty($frequentTasks) ? $frequentTasks[0]['createTime'] : null;
// 计算频繁次数统计近30天内包含"操作过于频繁"的记录)
$frequentCount = count($frequentTasks);
// 如果30天内没有频繁记录清除扣分
if (empty($frequentTasks)) {
return [
'lastFrequentTime' => null,
'frequentCount' => 0,
'frequentPenalty' => 0
];
}
// 根据30天内的频繁次数计算扣分
$penalty = 0;
if ($frequentCount == 1) {
$penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分
} elseif ($frequentCount >= 2) {
$penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分
}
return [
'lastFrequentTime' => $latestFrequentTime,
'frequentCount' => $frequentCount,
'frequentPenalty' => $penalty
];
}
/**
* 从s2_wechat_message表检查封号记录
* content包含"你的账号被限制"且msgType为10000
* 只统计近30天的数据
*
* @param int $accountId 账号ID
* @param string $wechatId 微信ID
* @param int $thirtyDaysAgo 30天前的时间戳可选如果已计算则传入以避免重复计算
* @return array|null
*/
private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null)
{
// 如果没有传入30天前的时间戳则计算
if ($thirtyDaysAgo === null) {
$thirtyDaysAgo = time() - (30 * 24 * 3600);
}
// 查询封号消息只统计近30天
// 优化查询:只查询必要的字段,减少数据传输量
$banMessage = Db::table(self::TABLE_WECHAT_MESSAGE)
->where('wechatAccountId', $accountId)
->where('msgType', 10000)
->where('content', 'like', '%你的账号被限制%')
->where('isDeleted', 0)
->where('createTime', '>=', $thirtyDaysAgo)
->field('id, createTime') // 只查询必要的字段
->order('createTime', 'desc')
->find();
if (!empty($banMessage)) {
return [
'isBanned' => 1,
'banPenalty' => self::PENALTY_BANNED // 封号-60分
];
}
return [
'isBanned' => 0,
'banPenalty' => 0
];
}
/**
* 计算不频繁加分
* 反向参考频繁规则查询近30天的频繁记录计算连续不频繁天数
* 规则30天内连续不频繁的只要有一次频繁就得重新计算重置连续不频繁天数
* 如果连续3天没有频繁则每天+5分
*
* @param int $accountId 账号ID
* @param string $wechatId 微信ID
* @param array $frequentData 频繁数据包含lastFrequentTime和frequentCount
* @param int $thirtyDaysAgo 30天前的时间戳可选如果已计算则传入以避免重复计算
* @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;
}
// 如果没有传入30天前的时间戳则计算
if ($thirtyDaysAgo === null) {
$thirtyDaysAgo = time() - (30 * 24 * 3600);
}
$currentTime = time();
// 获取最后一次频繁时间30天内最后一次频繁的时间
$lastFrequentTime = $frequentData['lastFrequentTime'] ?? null;
// 规则30天内连续不频繁的只要有一次频繁就得重新计算重置连续不频繁天数
if (empty($lastFrequentTime)) {
// 情况130天内没有频繁记录说明30天内连续不频繁
// 计算从30天前到现在的连续不频繁天数最多30天
$consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400));
} else {
// 情况230天内有频繁记录从最后一次频繁时间开始重新计算连续不频繁天数
// 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算
// 计算从最后一次频繁时间到现在,连续多少天没有频繁
$timeDiff = $currentTime - $lastFrequentTime;
$consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数
// 边界情况如果最后一次频繁时间在30天前理论上不应该发生因为查询已经限制了30天则按30天处理
if ($lastFrequentTime < $thirtyDaysAgo) {
$consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 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;
}
/**
* 根据健康分计算每日最大加人次数
* 公式:每日最大加人次数 = 健康分 * 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 是否强制重新计算基础分
* @return array 处理结果统计
* @throws Exception 如果参数无效或批量处理过程中出现严重错误
*/
public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = 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);
}
try {
$startTime = microtime(true);
Log::info("开始批量计算健康分batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false'));
$stats = [
'total' => 0,
'success' => 0,
'failed' => 0,
'errors' => []
];
// 如果没有指定账号ID则处理所有账号
if (empty($accountIds)) {
Log::info("未指定账号ID处理所有未删除账号");
$accountIds = Db::table(self::TABLE_WECHAT_ACCOUNT)
->where('isDeleted', 0)
->column('id');
}
$stats['total'] = count($accountIds);
Log::info("需要处理的账号总数: {$stats['total']}");
// 分批处理
$batches = array_chunk($accountIds, $batchSize);
$batchCount = count($batches);
Log::info("分批处理,共 {$batchCount}");
foreach ($batches as $batchIndex => $batch) {
$batchStartTime = microtime(true);
Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号");
foreach ($batch as $accountId) {
try {
$this->calculateAndUpdate($accountId, null, $forceRecalculateBase);
$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) . " 批处理完成,耗时: {$batchDuration}秒," .
"成功: {$stats['success']},失败: {$stats['failed']}");
}
$endTime = microtime(true);
$totalDuration = round($endTime - $startTime, 2);
Log::info("批量计算健康分完成,总耗时: {$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);
}
}
/**
* 记录频繁事件已废弃改为从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);
Log::debug("从缓存获取健康分信息accountId: {$accountId}");
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
];
// 如果使用缓存,则缓存健康分信息
if ($useCache) {
Cache::set($cacheKey, $healthScoreInfo, self::CACHE_TTL);
Log::debug("缓存健康分信息accountId: {$accountId}");
}
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);
Log::debug("清除健康分信息缓存accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败"));
return $result;
}
}