diff --git a/Server/application/command.php b/Server/application/command.php index f1d4d72b..b5c4d0e5 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -39,4 +39,7 @@ return [ 'workbench:groupCreate' => 'app\command\WorkbenchGroupCreateCommand', // 工作台群创建任务 'workbench:import-contact' => 'app\command\WorkbenchImportContactCommand', // 工作台通讯录导入任务 'kf:notice' => 'app\command\KfNoticeCommand', // 客服端消息通知 + + 'wechat:calculate-score' => 'app\command\CalculateWechatAccountScoreCommand', // 统一计算微信账号健康分 + 'wechat:update-score' => 'app\command\UpdateWechatAccountScoreCommand', // 更新微信账号评分记录 ]; diff --git a/Server/application/command/CalculateWechatAccountScoreCommand.php b/Server/application/command/CalculateWechatAccountScoreCommand.php new file mode 100644 index 00000000..dbc7ec5b --- /dev/null +++ b/Server/application/command/CalculateWechatAccountScoreCommand.php @@ -0,0 +1,311 @@ +setName('wechat:calculate-score') + ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("=========================================="); + $output->writeln("开始统一计算微信账号健康分..."); + $output->writeln("=========================================="); + + $startTime = time(); + $service = new WechatAccountHealthScoreService(); + + try { + // 步骤1: 初始化未计算基础分的账号 + $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); + $initStats = $this->initUncalculatedAccounts($service, $output); + $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + + // 步骤2: 更新评分记录(根据wechatId和alias不一致情况) + $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); + $updateStats = $this->updateScoreRecords($service, $output); + $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + + // 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分) + $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); + $batchStats = $this->batchUpdateHealthScore($service, $output); + $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + + // 统计信息 + $endTime = time(); + $duration = $endTime - $startTime; + + $output->writeln("\n=========================================="); + $output->writeln("任务完成!"); + $output->writeln("=========================================="); + $output->writeln("总耗时: {$duration} 秒"); + $output->writeln("初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + $output->writeln("更新评分记录: {$updateStats['total']} 条"); + $output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + + if (!empty($initStats['errors'])) { + $output->writeln("\n初始化错误详情:"); + foreach (array_slice($initStats['errors'], 0, 10) as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + if (count($initStats['errors']) > 10) { + $output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误"); + } + } + + if (!empty($batchStats['errors'])) { + $output->writeln("\n批量更新错误详情:"); + foreach (array_slice($batchStats['errors'], 0, 10) as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + if (count($batchStats['errors']) > 10) { + $output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误"); + } + } + + } catch (\Exception $e) { + $output->writeln("\n错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 初始化未计算基础分的账号 + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function initUncalculatedAccounts($service, $output) + { + $stats = [ + 'total' => 0, + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 获取所有未计算基础分的账号 + $accounts = Db::table('s2_wechat_account') + ->alias('a') + ->leftJoin(['s2_wechat_account_score' => 's'], 's.accountId = a.id') + ->where('a.isDeleted', 0) + ->where(function($query) { + $query->whereNull('s.id') + ->whereOr('s.baseScoreCalculated', 0); + }) + ->field('a.id, a.wechatId') + ->select(); + + $stats['total'] = count($accounts); + + if ($stats['total'] == 0) { + $output->writeln("没有需要初始化的账号"); + return $stats; + } + + $output->writeln("找到 {$stats['total']} 个需要初始化的账号"); + + $batchSize = 100; + $batches = array_chunk($accounts, $batchSize); + + foreach ($batches as $batchIndex => $batch) { + foreach ($batch as $account) { + try { + $service->calculateAndUpdate($account['id']); + $stats['success']++; + + if ($stats['success'] % 100 == 0) { + $output->write("."); + } + } catch (\Exception $e) { + $stats['failed']++; + $stats['errors'][] = [ + 'accountId' => $account['id'], + 'error' => $e->getMessage() + ]; + } + } + + if (($batchIndex + 1) % 10 == 0) { + $output->writeln(" 已处理 " . ($batchIndex + 1) * $batchSize . " 条"); + } + } + + return $stats; + } + + /** + * 更新评分记录(根据wechatId和alias不一致情况) + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function updateScoreRecords($service, $output) + { + $stats = ['total' => 0]; + + // 查找wechatId和alias不一致的账号 + $inconsistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias') + ->select(); + + // 查找wechatId和alias一致的账号 + $consistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->field('id, wechatId, alias') + ->select(); + + $allAccounts = array_merge($inconsistentAccounts, $consistentAccounts); + $stats['total'] = count($allAccounts); + + if ($stats['total'] == 0) { + $output->writeln("没有需要更新的账号"); + return $stats; + } + + $output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")"); + + $updatedCount = 0; + + foreach ($allAccounts as $account) { + $isModifiedAlias = in_array($account['id'], array_column($inconsistentAccounts, 'id')); + $this->updateScoreRecord($account['id'], $isModifiedAlias, $service); + $updatedCount++; + + if ($updatedCount % 100 == 0) { + $output->write("."); + } + } + + if ($updatedCount > 0 && $updatedCount % 100 == 0) { + $output->writeln(""); + } + + return $stats; + } + + /** + * 批量更新健康分(只更新动态分) + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function batchUpdateHealthScore($service, $output) + { + // 获取所有已计算基础分的账号 + $accountIds = Db::table('s2_wechat_account_score') + ->where('baseScoreCalculated', 1) + ->column('accountId'); + + $total = count($accountIds); + + if ($total == 0) { + $output->writeln("没有需要更新的账号"); + return ['success' => 0, 'failed' => 0, 'errors' => []]; + } + + $output->writeln("找到 {$total} 个需要更新动态分的账号"); + + $stats = $service->batchCalculateAndUpdate($accountIds, 100, false); + + return $stats; + } + + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + */ + private function updateScoreRecord($accountId, $isModifiedAlias, $service) + { + // 获取账号数据 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return; + } + + // 确保评分记录存在 + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($scoreRecord)) { + // 如果记录不存在,创建并计算基础分 + $service->calculateAndUpdate($accountId); + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + } + + if (empty($scoreRecord)) { + return; + } + + // 更新isModifiedAlias字段 + $updateData = [ + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新基础信息分和基础分 + if ($scoreRecord['baseScoreCalculated']) { + $oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + $newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分 + + if ($oldBaseInfoScore != $newBaseInfoScore) { + $oldBaseScore = $scoreRecord['baseScore'] ?? 60; + $newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore; + + $updateData['baseInfoScore'] = $newBaseInfoScore; + $updateData['baseScore'] = $newBaseScore; + + // 重新计算健康分 + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + } + } else { + // 基础分未计算,只更新标记和基础信息分 + $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + } +} + diff --git a/Server/application/command/CheckAndFixWechatAccountCommand.php b/Server/application/command/CheckAndFixWechatAccountCommand.php deleted file mode 100644 index e498f77b..00000000 --- a/Server/application/command/CheckAndFixWechatAccountCommand.php +++ /dev/null @@ -1,159 +0,0 @@ -setName('wechat:check-and-fix') - ->setDescription('检测和修复s2_wechat_account表中wechatId和alias不一致的问题,并更新健康分'); - } - - protected function execute(Input $input, Output $output) - { - $output->writeln("开始检测和修复s2_wechat_account表..."); - - try { - // 1. 检测wechatId和alias不一致的记录 - $output->writeln("步骤1: 检测wechatId和alias不一致的记录..."); - $inconsistentAccounts = $this->findInconsistentAccounts(); - $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录"); - - if (empty($inconsistentAccounts)) { - $output->writeln("没有发现不一致的记录,任务完成!"); - return; - } - - // 2. 修复不一致的记录 - $output->writeln("步骤2: 修复不一致的记录..."); - $fixedCount = $this->fixInconsistentAccounts($inconsistentAccounts); - $output->writeln("已修复 " . $fixedCount . " 条记录"); - - // 3. 更新isModifiedAlias字段 - $output->writeln("步骤3: 更新isModifiedAlias字段..."); - $updatedCount = $this->updateModifiedAliasFlag(); - $output->writeln("已更新 " . $updatedCount . " 条记录的isModifiedAlias字段"); - - // 4. 重新计算健康分 - $output->writeln("步骤4: 重新计算健康分..."); - $healthScoreService = new WechatAccountHealthScoreService(); - $accountIds = array_column($inconsistentAccounts, 'id'); - $stats = $healthScoreService->batchCalculateAndUpdate($accountIds, 100); - $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); - - if (!empty($stats['errors'])) { - $output->writeln("错误详情:"); - foreach ($stats['errors'] as $error) { - $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); - } - } - - $output->writeln("任务完成!"); - - } catch (\Exception $e) { - $output->writeln("错误: " . $e->getMessage()); - $output->writeln($e->getTraceAsString()); - } - } - - /** - * 查找wechatId和alias不一致的记录 - * - * @return array - */ - private function findInconsistentAccounts() - { - // 查找wechatId和alias不一致的记录 - // 条件:wechatId不为空,alias不为空,且wechatId != alias - $accounts = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId != alias') - ->field('id, wechatId, alias, nickname, isModifiedAlias') - ->select(); - - return $accounts ?: []; - } - - /** - * 修复不一致的记录 - * 策略:从s2_wechat_friend表中查找最新的alias值来更新 - * - * @param array $accounts 不一致的账号列表 - * @return int 修复数量 - */ - private function fixInconsistentAccounts($accounts) - { - $fixedCount = 0; - - foreach ($accounts as $account) { - $wechatId = $account['wechatId']; - - // 从s2_wechat_friend表中查找最新的alias值 - $latestAlias = Db::table('s2_wechat_friend') - ->where('wechatId', $wechatId) - ->where('alias', '<>', '') - ->order('updateTime', 'desc') - ->value('alias'); - - // 如果找到了最新的alias,则更新 - if (!empty($latestAlias) && $latestAlias !== $account['alias']) { - Db::table('s2_wechat_account') - ->where('id', $account['id']) - ->update([ - 'alias' => $latestAlias, - 'updateTime' => time() - ]); - $fixedCount++; - } - } - - return $fixedCount; - } - - /** - * 更新isModifiedAlias字段 - * 如果wechatId和alias不一致,则标记为已修改 - * - * @return int 更新数量 - */ - private function updateModifiedAliasFlag() - { - // 更新isModifiedAlias字段:wechatId != alias 的记录标记为1 - $updatedCount = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId != alias') - ->update([ - 'isModifiedAlias' => 1, - 'updateTime' => time() - ]); - - // 更新isModifiedAlias字段:wechatId == alias 的记录标记为0 - Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId = alias') - ->update([ - 'isModifiedAlias' => 0, - 'updateTime' => time() - ]); - - return $updatedCount; - } -} - diff --git a/Server/application/command/UpdateWechatAccountScoreCommand.php b/Server/application/command/UpdateWechatAccountScoreCommand.php new file mode 100644 index 00000000..13ded8d4 --- /dev/null +++ b/Server/application/command/UpdateWechatAccountScoreCommand.php @@ -0,0 +1,168 @@ +setName('wechat:update-score') + ->setDescription('更新微信账号评分记录,根据wechatId和alias不一致情况更新isModifiedAlias字段(仅用于评分)'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("开始更新微信账号评分记录..."); + + try { + // 1. 查找所有需要更新的账号 + $output->writeln("步骤1: 查找需要更新的账号..."); + + // 查找wechatId和alias不一致的账号 + $inconsistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias') + ->select(); + + // 查找wechatId和alias一致的账号 + $consistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->field('id, wechatId, alias') + ->select(); + + $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录(已修改微信号)"); + $output->writeln("发现 " . count($consistentAccounts) . " 条一致记录(未修改微信号)"); + + // 2. 更新评分记录表中的isModifiedAlias字段 + $output->writeln("步骤2: 更新评分记录表..."); + $updatedCount = 0; + $healthScoreService = new WechatAccountHealthScoreService(); + + // 更新不一致的记录 + foreach ($inconsistentAccounts as $account) { + $this->updateScoreRecord($account['id'], true, $healthScoreService); + $updatedCount++; + } + + // 更新一致的记录 + foreach ($consistentAccounts as $account) { + $this->updateScoreRecord($account['id'], false, $healthScoreService); + $updatedCount++; + } + + $output->writeln("已更新 " . $updatedCount . " 条评分记录"); + + // 3. 重新计算健康分(只更新基础信息分,不重新计算基础分) + $output->writeln("步骤3: 重新计算健康分..."); + $allAccountIds = array_merge( + array_column($inconsistentAccounts, 'id'), + array_column($consistentAccounts, 'id') + ); + + if (!empty($allAccountIds)) { + $stats = $healthScoreService->batchCalculateAndUpdate($allAccountIds, 100, false); + $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); + + if (!empty($stats['errors'])) { + $output->writeln("错误详情:"); + foreach ($stats['errors'] as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + } + } + + $output->writeln("任务完成!"); + + } catch (\Exception $e) { + $output->writeln("错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + */ + private function updateScoreRecord($accountId, $isModifiedAlias, $service) + { + // 获取或创建评分记录 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return; + } + + // 确保评分记录存在 + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($scoreRecord)) { + // 如果记录不存在,创建并计算基础分 + $service->calculateAndUpdate($accountId); + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + } + + if (empty($scoreRecord)) { + return; + } + + // 更新isModifiedAlias字段 + $updateData = [ + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新基础信息分和基础分 + if ($scoreRecord['baseScoreCalculated']) { + $oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + $newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分 + + if ($oldBaseInfoScore != $newBaseInfoScore) { + $oldBaseScore = $scoreRecord['baseScore'] ?? 60; + $newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore; + + $updateData['baseInfoScore'] = $newBaseInfoScore; + $updateData['baseScore'] = $newBaseScore; + + // 重新计算健康分 + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + } + } else { + // 基础分未计算,只更新标记和基础信息分 + $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + } +} + diff --git a/Server/application/common/model/WechatAccountScore.php b/Server/application/common/model/WechatAccountScore.php new file mode 100644 index 00000000..43170f78 --- /dev/null +++ b/Server/application/common/model/WechatAccountScore.php @@ -0,0 +1,44 @@ + 'integer', + 'baseScore' => 'integer', + 'baseScoreCalculated' => 'integer', + 'baseInfoScore' => 'integer', + 'friendCountScore' => 'integer', + 'friendCount' => 'integer', + 'dynamicScore' => 'integer', + 'frequentCount' => 'integer', + 'frequentPenalty' => 'integer', + 'consecutiveNoFrequentDays' => 'integer', + 'noFrequentBonus' => 'integer', + 'banPenalty' => 'integer', + 'healthScore' => 'integer', + 'maxAddFriendPerDay' => 'integer', + 'isModifiedAlias' => 'integer', + 'isBanned' => 'integer', + 'lastFrequentTime' => 'integer', + 'lastNoFrequentTime' => 'integer', + 'baseScoreCalcTime' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer', + ]; +} + diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index cda09cfd..02a89a9b 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -6,9 +6,15 @@ use think\Db; use think\Exception; /** - * 微信账号健康分评分服务 + * 微信账号健康分评分服务(优化版) * 基于《微信健康分规则v2.md》实现 * + * 优化点: + * 1. 基础分只计算一次 + * 2. 各个评分维度独立存储 + * 3. 使用独立的评分记录表 + * 4. 好友数量评分特殊处理(避免同步问题) + * * 健康分 = 基础分 + 动态分 * 基础分:60-100分(默认60分 + 基础信息10分 + 好友数量30分) * 动态分:扣分和加分规则 @@ -18,13 +24,14 @@ class WechatAccountHealthScoreService // 默认基础分 const DEFAULT_BASE_SCORE = 60; - // 基础信息权重和分数 - const BASE_INFO_WEIGHT = 0.2; + // 基础信息分数 const BASE_INFO_SCORE = 10; - // 好友数量权重和分数 - const FRIEND_COUNT_WEIGHT = 0.3; - const FRIEND_COUNT_MAX_SCORE = 30; + // 好友数量分数区间 + const FRIEND_COUNT_SCORE_0_50 = 3; + const FRIEND_COUNT_SCORE_51_500 = 6; + const FRIEND_COUNT_SCORE_501_3000 = 8; + const FRIEND_COUNT_SCORE_3001_PLUS = 12; // 动态分扣分规则 const PENALTY_FIRST_FREQUENT = -15; // 首次频繁扣15分 @@ -39,9 +46,10 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID(s2_wechat_account表的id) * @param array $accountData 账号数据(可选,如果不传则从数据库查询) + * @param bool $forceRecalculateBase 是否强制重新计算基础分(默认false) * @return array 返回评分结果 */ - public function calculateAndUpdate($accountId, $accountData = null) + public function calculateAndUpdate($accountId, $accountData = null, $forceRecalculateBase = false) { try { // 获取账号数据 @@ -55,39 +63,68 @@ class WechatAccountHealthScoreService throw new Exception("账号不存在:{$accountId}"); } - // 计算基础分 - $baseScore = $this->calculateBaseScore($accountData); + $wechatId = $accountData['wechatId'] ?? ''; + if (empty($wechatId)) { + throw new Exception("账号wechatId为空:{$accountId}"); + } - // 计算动态分 - $dynamicScore = $this->calculateDynamicScore($accountData); + // 获取或创建评分记录 + $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); + + // 计算基础分(只计算一次,除非强制重新计算) + 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 = [ - 'healthScore' => $healthScore, - 'baseScore' => $baseScore, 'dynamicScore' => $dynamicScore, - 'scoreUpdateTime' => time() + '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() ]; - Db::table('s2_wechat_account') - ->where('id', $accountId) + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) ->update($updateData); return [ 'accountId' => $accountId, - 'wechatId' => $accountData['wechatId'] ?? '', + 'wechatId' => $wechatId, 'healthScore' => $healthScore, 'baseScore' => $baseScore, + 'baseInfoScore' => $scoreRecord['baseInfoScore'], + 'friendCountScore' => $scoreRecord['friendCountScore'], 'dynamicScore' => $dynamicScore, - 'baseInfoScore' => $this->getBaseInfoScore($accountData), - 'friendCountScore' => $this->getFriendCountScore($accountData['totalFriend'] ?? 0), - 'maxAddFriendPerDay' => $this->getMaxAddFriendPerDay($healthScore) + 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], + 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], + 'banPenalty' => $dynamicScoreData['banPenalty'], + 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; } catch (Exception $e) { @@ -96,24 +133,119 @@ class WechatAccountHealthScoreService } /** - * 计算基础分 - * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(30分) + * 获取或创建评分记录 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array 评分记录 + */ + private function getOrCreateScoreRecord($accountId, $wechatId) + { + $record = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($record)) { + // 创建新记录 + $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('s2_wechat_account_score')->insert($data); + + return $data; + } + + return $record; + } + + /** + * 获取评分记录 + * + * @param int $accountId 账号ID + * @return array + */ + private function getScoreRecord($accountId) + { + return Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find() ?: []; + } + + /** + * 计算基础分(只计算一次) + * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(3-12分) * * @param array $accountData 账号数据 - * @return int 基础分 + * @param array $scoreRecord 现有评分记录 + * @return array 基础分数据 */ - private function calculateBaseScore($accountData) + private function calculateBaseScore($accountData, $scoreRecord = []) { $baseScore = self::DEFAULT_BASE_SCORE; // 基础信息分(已修改微信号得10分) - $baseScore += $this->getBaseInfoScore($accountData); + $baseInfoScore = $this->getBaseInfoScore($accountData); + $baseScore += $baseInfoScore; - // 好友数量分(最高30分) - $totalFriend = $accountData['totalFriend'] ?? 0; - $baseScore += $this->getFriendCountScore($totalFriend); + // 好友数量分(特殊处理:使用快照值,避免同步问题) + $friendCountScore = 0; + $friendCount = 0; + $friendCountSource = 'manual'; - return $baseScore; + // 如果已有评分记录且好友数量分已计算,使用历史值 + 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 基础分数据 + */ + private function updateBaseScore($accountId, $baseScoreData) + { + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($baseScoreData); } /** @@ -125,23 +257,36 @@ class WechatAccountHealthScoreService */ private function getBaseInfoScore($accountData) { - // 检查是否已修改微信号 - // 如果isModifiedAlias字段为1,或者wechatId和alias不一致,则认为已修改 - $isModifiedAlias = isset($accountData['isModifiedAlias']) ? (int)$accountData['isModifiedAlias'] : 0; - $wechatId = trim($accountData['wechatId'] ?? ''); - $alias = trim($accountData['alias'] ?? ''); - - // 如果字段标记为已修改,或者wechatId和alias不一致,则得分 - if ($isModifiedAlias == 1 || (!empty($wechatId) && !empty($alias) && $wechatId !== $alias)) { + 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; + } + /** * 获取好友数量分 - * 根据好友数量区间得分(最高30分) + * 根据好友数量区间得分(最高12分) * * @param int $totalFriend 总好友数 * @return int 好友数量分 @@ -149,92 +294,292 @@ class WechatAccountHealthScoreService private function getFriendCountScore($totalFriend) { if ($totalFriend <= 50) { - return 3; // 0-50: 3分 + return self::FRIEND_COUNT_SCORE_0_50; } elseif ($totalFriend <= 500) { - return 6; // 51-500: 6分 + return self::FRIEND_COUNT_SCORE_51_500; } elseif ($totalFriend <= 3000) { - return 8; // 501-3000: 8分 + return self::FRIEND_COUNT_SCORE_501_3000; } else { - return 12; // 3001以上: 12分 + return self::FRIEND_COUNT_SCORE_3001_PLUS; } } + /** + * 手动更新好友数量分(用于处理同步问题) + * + * @param int $accountId 账号ID + * @param int $friendCount 好友数量 + * @param string $source 来源(manual=手动,sync=同步) + * @return bool + */ + public function updateFriendCountScore($accountId, $friendCount, $source = 'manual') + { + $scoreRecord = $this->getScoreRecord($accountId); + + // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) + if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { + // 同步数据不允许修改已计算的基础分 + return false; + } + + $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); + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + + return true; + } + /** * 计算动态分 * 动态分 = 扣分 + 加分 * * @param array $accountData 账号数据 - * @return int 动态分 + * @param array $scoreRecord 现有评分记录 + * @return array 动态分数据 */ - private function calculateDynamicScore($accountData) + private function calculateDynamicScore($accountData, $scoreRecord) { - $dynamicScore = 0; + $result = [ + 'total' => 0, + 'frequentPenalty' => 0, + 'noFrequentBonus' => 0, + 'banPenalty' => 0, + 'lastFrequentTime' => null, + 'frequentCount' => 0, + 'lastNoFrequentTime' => null, + 'consecutiveNoFrequentDays' => 0, + 'isBanned' => 0 + ]; - // 处理扣分 - $dynamicScore += $this->calculatePenalty($accountData); + $accountId = $accountData['id'] ?? 0; + $wechatId = $accountData['wechatId'] ?? ''; - // 处理加分 - $dynamicScore += $this->calculateBonus($accountData); + if (empty($accountId) || empty($wechatId)) { + return $result; + } - return $dynamicScore; + // 继承现有数据 + 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); + $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; + $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; + $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; + + // 2. 检查封号记录(从s2_wechat_message表查询) + $banData = $this->checkBannedFromMessage($accountId, $wechatId); + if (!empty($banData)) { + $result['isBanned'] = $banData['isBanned']; + $result['banPenalty'] = $banData['banPenalty']; + } + + // 3. 计算不频繁加分(基于近30天的频繁记录,反向参考频繁规则) + $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']; + + return $result; } /** - * 计算扣分 - * 首次频繁:-15分 - * 再次频繁:-25分 - * 封号:-60分 + * 从s2_friend_task表检查频繁记录 + * extra字段包含"操作过于频繁"即需要扣分 + * 只统计近30天的数据 * - * @param array $accountData 账号数据 - * @return int 扣分数 + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @param array $scoreRecord 现有评分记录 + * @return array|null */ - private function calculatePenalty($accountData) + private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord) { + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); + + // 查询包含"操作过于频繁"的记录(只统计近30天) + // extra字段可能是文本或JSON格式,使用LIKE查询 + $frequentTasks = Db::table('s2_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分 + } - // 检查是否有频繁记录 - $lastFrequentTime = $accountData['lastFrequentTime'] ?? null; - $frequentCount = $accountData['frequentCount'] ?? 0; + return [ + 'lastFrequentTime' => $latestFrequentTime, + 'frequentCount' => $frequentCount, + 'frequentPenalty' => $penalty + ]; + } + + /** + * 从s2_wechat_message表检查封号记录 + * content包含"你的账号被限制"且msgType为10000 + * 只统计近30天的数据 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array|null + */ + private function checkBannedFromMessage($accountId, $wechatId) + { + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); - if (!empty($lastFrequentTime)) { - // 判断是首次频繁还是再次频繁 - if ($frequentCount == 1) { - $penalty += self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 - } elseif ($frequentCount >= 2) { - $penalty += self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + // 查询封号消息(只统计近30天) + $banMessage = Db::table('s2_wechat_message') + ->where('wechatAccountId', $accountId) + ->where('msgType', 10000) + ->where('content', 'like', '%你的账号被限制%') + ->where('isDeleted', 0) + ->where('createTime', '>=', $thirtyDaysAgo) + ->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) + * @return array 包含bonus、consecutiveDays、lastNoFrequentTime + */ + private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData) + { + $result = [ + 'bonus' => 0, + 'consecutiveDays' => 0, + 'lastNoFrequentTime' => null + ]; + + if (empty($accountId) || empty($wechatId)) { + return $result; + } + + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); + $currentTime = time(); + + // 获取最后一次频繁时间(30天内最后一次频繁的时间) + $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null; + + // 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + if (empty($lastFrequentTime)) { + // 情况1:30天内没有频繁记录,说明30天内连续不频繁 + // 计算从30天前到现在的连续不频繁天数(最多30天) + $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); + } else { + // 情况2:30天内有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 + // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算 + // 计算从最后一次频繁时间到现在,连续多少天没有频繁 + $timeDiff = $currentTime - $lastFrequentTime; + $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数 + + // 边界情况:如果最后一次频繁时间在30天前(理论上不应该发生,因为查询已经限制了30天),则按30天处理 + if ($lastFrequentTime < $thirtyDaysAgo) { + $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); } } - // 检查是否封号(这里需要根据实际业务逻辑判断,比如status字段或其他标识) - // 假设status=0表示封号 - $status = $accountData['status'] ?? 1; - if ($status == 0) { - $penalty += self::PENALTY_BANNED; // 封号-60分 + // 如果连续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 $penalty; - } - - /** - * 计算加分 - * 连续3天不触发频繁:每天+5分 - * - * @param array $accountData 账号数据 - * @return int 加分数 - */ - private function calculateBonus($accountData) - { - $bonus = 0; - - $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; - $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; - - // 如果连续不频繁天数>=3,则每天+5分 - if ($consecutiveNoFrequentDays >= 3) { - $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; - } - - return $bonus; + return $result; } /** @@ -252,11 +597,12 @@ class WechatAccountHealthScoreService /** * 批量计算并更新多个账号的健康分 * - * @param array $accountIds 账号ID数组 + * @param array $accountIds 账号ID数组(为空则处理所有账号) * @param int $batchSize 每批处理数量 + * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @return array 处理结果统计 */ - public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100) + public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) { $stats = [ 'total' => 0, @@ -280,7 +626,7 @@ class WechatAccountHealthScoreService foreach ($batches as $batch) { foreach ($batch as $accountId) { try { - $this->calculateAndUpdate($accountId); + $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); $stats['success']++; } catch (Exception $e) { $stats['failed']++; @@ -296,38 +642,22 @@ class WechatAccountHealthScoreService } /** - * 记录频繁事件 + * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测) + * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成 * * @param int $accountId 账号ID * @return bool */ public function recordFrequent($accountId) { - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); - - if (empty($accountData)) { + // 频繁检测已改为从s2_friend_task表自动检测 + // 直接重新计算健康分即可 + try { + $this->calculateAndUpdate($accountId); + return true; + } catch (\Exception $e) { return false; } - - $frequentCount = ($accountData['frequentCount'] ?? 0) + 1; - - $updateData = [ - 'lastFrequentTime' => time(), - 'frequentCount' => $frequentCount, - 'consecutiveNoFrequentDays' => 0, // 重置连续不频繁天数 - 'lastNoFrequentTime' => null - ]; - - Db::table('s2_wechat_account') - ->where('id', $accountId) - ->update($updateData); - - // 重新计算健康分 - $this->calculateAndUpdate($accountId); - - return true; } /** @@ -338,21 +668,29 @@ class WechatAccountHealthScoreService */ public function recordNoFrequent($accountId) { - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); + $scoreRecord = $this->getScoreRecord($accountId); - if (empty($accountData)) { - return false; + if (empty($scoreRecord)) { + // 如果记录不存在,先创建 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return false; + } + + $this->getOrCreateScoreRecord($accountId, $accountData['wechatId']); + $scoreRecord = $this->getScoreRecord($accountId); } - $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; - $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; + $lastNoFrequentTime = $scoreRecord['lastNoFrequentTime'] ?? null; + $consecutiveNoFrequentDays = $scoreRecord['consecutiveNoFrequentDays'] ?? 0; $currentTime = time(); // 如果上次不频繁时间是昨天或更早,则增加连续天数 if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) { - // 如果间隔超过1天,重置为1天 + // 如果间隔超过2天,重置为1天 if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) { $consecutiveNoFrequentDays = 1; } else { @@ -360,13 +698,21 @@ class WechatAccountHealthScoreService } } + // 计算加分(连续3天及以上才加分) + $bonus = 0; + if ($consecutiveNoFrequentDays >= 3) { + $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; + } + $updateData = [ 'lastNoFrequentTime' => $currentTime, - 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays + 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays, + 'noFrequentBonus' => $bonus, + 'updateTime' => $currentTime ]; - Db::table('s2_wechat_account') - ->where('id', $accountId) + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) ->update($updateData); // 重新计算健康分 @@ -374,5 +720,38 @@ class WechatAccountHealthScoreService return true; } + + /** + * 获取账号健康分信息 + * + * @param int $accountId 账号ID + * @return array|null + */ + public function getHealthScore($accountId) + { + $scoreRecord = $this->getScoreRecord($accountId); + + if (empty($scoreRecord)) { + return null; + } + + return [ + '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 + ]; + } } - diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md index f5849517..f7d1dfc8 100644 --- a/Server/crontab_tasks.md +++ b/Server/crontab_tasks.md @@ -81,6 +81,8 @@ # 消息提醒 */1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 +# 客服评分 +0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 @@ -107,4 +109,10 @@ ```bash crontab -l +``` + +```bash +- 本地: php think worker:server +- 线上: php think worker:server -d (自带守护进程,无需搭配Supervisor 之类的工具) +- php think worker:server stop php think worker:server status ``` \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index adafc321..247684bc 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -18,6 +18,7 @@ use think\facade\Config; use think\facade\Log; use app\api\controller\FriendTaskController; use app\common\service\AuthService; +use app\common\service\WechatAccountHealthScoreService; use app\api\controller\WebSocketController; use Workerman\Lib\Timer; @@ -180,13 +181,11 @@ class Adapter implements WeChatServiceInterface ->select(); $taskData = array_merge($taskData, $tasks); } - if ($taskData) { foreach ($taskData as $task) { $task_id = $task['task_id']; $task_info = $this->getCustomerAcquisitionTask($task_id); - if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) { continue; } @@ -213,9 +212,86 @@ class Adapter implements WeChatServiceInterface continue; } - // 判断24h内加的好友数量,friend_task 先固定10个人 getLast24hAddedFriendsCount + // 根据健康分判断24h内加的好友数量限制 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + // 如果健康分记录不存在,先计算一次 + if (empty($healthScoreInfo)) { + try { + $healthScoreService->calculateAndUpdate($accountId); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + } catch (\Exception $e) { + Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage()); + // 如果计算失败,使用默认值5作为兜底 + $maxAddFriendPerDay = 5; + } + } + + // 获取每日最大加人次数(基于健康分) + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5; + + // 如果健康分为0或很低,不允许添加好友 + if ($maxAddFriendPerDay <= 0) { + Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 检查频繁暂停限制:首次频繁或再次频繁,暂停24小时 + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + if (!empty($lastFrequentTime) && $frequentCount > 0) { + $frequentPauseHours = 24; // 频繁暂停24小时 + $frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $frequentPauseTime) { + $remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600); + Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + + // 检查封号暂停限制:封号暂停72小时 + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + if ($isBanned == 1) { + // 查询封号时间(从s2_wechat_message表查询最近一次封号消息) + $banMessage = Db::table('s2_wechat_message') + ->where('wechatAccountId', $accountId) + ->where('msgType', 10000) + ->where('content', 'like', '%你的账号被限制%') + ->where('isDeleted', 0) + ->order('createTime', 'desc') + ->find(); + + if (!empty($banMessage)) { + $banTime = $banMessage['createTime'] ?? 0; + $banPauseHours = 72; // 封号暂停72小时 + $banPauseTime = $banTime + ($banPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $banPauseTime) { + $remainingHours = ceil(($banPauseTime - $currentTime) / 3600); + Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + } + + // 判断今天添加的好友数量,使用健康分计算的每日最大加人次数 + // 优先使用今天添加的好友数量(更符合"每日"限制) + $todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId); + if ($todayAddedFriendsCount >= $maxAddFriendPerDay) { + Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 如果今天添加数量未达上限,再检查24小时内的数量(作为额外保护) $last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId); - if ($last24hAddedFriendsCount >= 20) { + // 24小时内的限制可以稍微宽松一些,设置为每日限制的1.2倍(防止跨天累积) + $max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2); + if ($last24hAddedFriendsCount >= $max24hLimit) { + Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})"); continue; } @@ -828,6 +904,7 @@ class Adapter implements WeChatServiceInterface if (empty($deviceIds)) { return []; } + $records = Db::table('s2_wechat_account') ->where('deviceAlive', 1) ->where('wechatAlive', 1)