diff --git a/Server/application/command/CalculateWechatAccountScoreCommand.php b/Server/application/command/CalculateWechatAccountScoreCommand.php index dbc7ec5b..aeb73784 100644 --- a/Server/application/command/CalculateWechatAccountScoreCommand.php +++ b/Server/application/command/CalculateWechatAccountScoreCommand.php @@ -6,6 +6,7 @@ use think\console\Command; use think\console\Input; use think\console\Output; use think\Db; +use think\facade\Log; use app\common\service\WechatAccountHealthScoreService; /** @@ -17,36 +18,122 @@ use app\common\service\WechatAccountHealthScoreService; */ class CalculateWechatAccountScoreCommand extends Command { + /** + * 数据库表名 + */ + const TABLE_WECHAT_ACCOUNT = 's2_wechat_account'; + const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score'; protected function configure() { $this->setName('wechat:calculate-score') - ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)'); + ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)') + ->addOption('only-init', null, \think\console\input\Option::VALUE_NONE, '仅执行初始化步骤') + ->addOption('only-update', null, \think\console\input\Option::VALUE_NONE, '仅执行更新评分记录步骤') + ->addOption('only-batch', null, \think\console\input\Option::VALUE_NONE, '仅执行批量更新健康分步骤') + ->addOption('account-id', 'a', \think\console\input\Option::VALUE_OPTIONAL, '指定账号ID,仅处理该账号') + ->addOption('batch-size', 'b', \think\console\input\Option::VALUE_OPTIONAL, '批处理大小', 50) + ->addOption('force-recalculate', 'f', \think\console\input\Option::VALUE_NONE, '强制重新计算基础分'); } + /** + * 执行命令 + * + * @param Input $input 输入对象 + * @param Output $output 输出对象 + * @return int 命令执行状态码(0表示成功) + */ protected function execute(Input $input, Output $output) { + // 解析命令行参数 + $onlyInit = $input->getOption('only-init'); + $onlyUpdate = $input->getOption('only-update'); + $onlyBatch = $input->getOption('only-batch'); + $accountId = $input->getOption('account-id'); + $batchSize = (int)$input->getOption('batch-size'); + $forceRecalculate = $input->getOption('force-recalculate'); + + // 参数验证 + if ($batchSize <= 0) { + $batchSize = 50; // 默认批处理大小 + } + + // 显示执行参数 $output->writeln("=========================================="); $output->writeln("开始统一计算微信账号健康分..."); $output->writeln("=========================================="); + if ($accountId) { + $output->writeln("指定账号ID: {$accountId}"); + } + + if ($onlyInit) { + $output->writeln("仅执行初始化步骤"); + } elseif ($onlyUpdate) { + $output->writeln("仅执行更新评分记录步骤"); + } elseif ($onlyBatch) { + $output->writeln("仅执行批量更新健康分步骤"); + } + + if ($forceRecalculate) { + $output->writeln("强制重新计算基础分"); + } + + $output->writeln("批处理大小: {$batchSize}"); + + // 记录命令开始执行的日志 + Log::info('开始执行微信账号健康分计算命令', [ + 'accountId' => $accountId, + 'onlyInit' => $onlyInit ? 'true' : 'false', + 'onlyUpdate' => $onlyUpdate ? 'true' : 'false', + 'onlyBatch' => $onlyBatch ? 'true' : 'false', + 'batchSize' => $batchSize, + 'forceRecalculate' => $forceRecalculate ? 'true' : 'false' + ]); + $startTime = time(); - $service = new WechatAccountHealthScoreService(); + + try { + // 实例化服务 + $service = new WechatAccountHealthScoreService(); + } catch (\Exception $e) { + $errorMsg = "实例化WechatAccountHealthScoreService失败: " . $e->getMessage(); + $output->writeln("{$errorMsg}"); + Log::error($errorMsg); + return 1; // 返回非零状态码表示失败 + } + + // 初始化统计数据 + $initStats = ['success' => 0, 'failed' => 0, 'errors' => []]; + $updateStats = ['total' => 0]; + $batchStats = ['success' => 0, 'failed' => 0, 'errors' => []]; try { // 步骤1: 初始化未计算基础分的账号 - $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); - $initStats = $this->initUncalculatedAccounts($service, $output); - $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + if (!$onlyUpdate && !$onlyBatch) { + $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); + Log::info('[步骤1] 开始初始化未计算基础分的账号'); + $initStats = $this->initUncalculatedAccounts($service, $output, $accountId, $batchSize); + $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + Log::info("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + } // 步骤2: 更新评分记录(根据wechatId和alias不一致情况) - $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); - $updateStats = $this->updateScoreRecords($service, $output); - $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + if (!$onlyInit && !$onlyBatch) { + $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); + Log::info('[步骤2] 开始更新评分记录(根据wechatId和alias不一致情况)'); + $updateStats = $this->updateScoreRecords($service, $output, $accountId, $batchSize); + $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + Log::info("更新评分记录完成:处理了 {$updateStats['total']} 条记录"); + } // 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分) - $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); - $batchStats = $this->batchUpdateHealthScore($service, $output); - $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + if (!$onlyInit && !$onlyUpdate) { + $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); + Log::info('[步骤3] 开始批量更新健康分(只更新动态分)'); + $batchStats = $this->batchUpdateHealthScore($service, $output, $accountId, $batchSize, $forceRecalculate); + $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + Log::info("批量更新健康分完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + } // 统计信息 $endTime = time(); @@ -60,40 +147,89 @@ class CalculateWechatAccountScoreCommand extends Command $output->writeln("更新评分记录: {$updateStats['total']} 条"); $output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + // 记录命令执行完成的日志 + Log::info("微信账号健康分计算命令执行完成,总耗时: {$duration} 秒," . + "初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条," . + "更新评分记录: {$updateStats['total']} 条," . + "批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + if (!empty($initStats['errors'])) { $output->writeln("\n初始化错误详情:"); + Log::warning("初始化阶段出现 " . count($initStats['errors']) . " 个错误"); + foreach (array_slice($initStats['errors'], 0, 10) as $error) { $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + Log::error("初始化错误 - 账号ID {$error['accountId']}: {$error['error']}"); } + if (count($initStats['errors']) > 10) { $output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误"); + Log::warning("初始化错误过多,只记录前10个,还有 " . (count($initStats['errors']) - 10) . " 个错误未显示"); } } if (!empty($batchStats['errors'])) { $output->writeln("\n批量更新错误详情:"); + Log::warning("批量更新阶段出现 " . count($batchStats['errors']) . " 个错误"); + foreach (array_slice($batchStats['errors'], 0, 10) as $error) { $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + Log::error("批量更新错误 - 账号ID {$error['accountId']}: {$error['error']}"); } + if (count($batchStats['errors']) > 10) { $output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误"); + Log::warning("批量更新错误过多,只记录前10个,还有 " . (count($batchStats['errors']) - 10) . " 个错误未显示"); } } - } catch (\Exception $e) { - $output->writeln("\n错误: " . $e->getMessage()); + } catch (\PDOException $e) { + // 数据库异常 + $errorMsg = "数据库操作失败: " . $e->getMessage(); + $output->writeln("\n数据库错误: " . $errorMsg . ""); $output->writeln($e->getTraceAsString()); + + // 记录数据库错误 + Log::error("数据库错误: " . $errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 2; // 数据库错误状态码 + } catch (\Exception $e) { + // 一般异常 + $errorMsg = "命令执行失败: " . $e->getMessage(); + $output->writeln("\n错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录严重错误 + Log::error($errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 1; // 一般错误状态码 + } catch (\Throwable $e) { + // 其他所有错误 + $errorMsg = "严重错误: " . $e->getMessage(); + $output->writeln("\n严重错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录严重错误 + Log::critical($errorMsg); + Log::critical("错误堆栈: " . $e->getTraceAsString()); + + return 3; // 严重错误状态码 } + + return 0; // 成功执行 } /** * 初始化未计算基础分的账号 * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function initUncalculatedAccounts($service, $output) + private function initUncalculatedAccounts($service, $output, $accountId = null, $batchSize = 50) { $stats = [ 'total' => 0, @@ -102,41 +238,67 @@ class CalculateWechatAccountScoreCommand extends Command '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(); + try { + // 获取所有未计算基础分的账号 + // 优化查询:使用索引字段,只查询必要的字段 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->alias('a') + ->leftJoin([self::TABLE_WECHAT_ACCOUNT_SCORE => 's'], 's.accountId = a.id') + ->where('a.isDeleted', 0) + ->where(function($query) { + $query->whereNull('s.id') + ->whereOr('s.baseScoreCalculated', 0); + }); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('a.id', $accountId); + } + + $accounts = $query->field('a.id, a.wechatId') // 只查询必要的字段 + ->select(); + } catch (\Exception $e) { + Log::error("查询未计算基础分的账号失败: " . $e->getMessage()); + throw new \Exception("查询未计算基础分的账号失败: " . $e->getMessage(), 0, $e); + } $stats['total'] = count($accounts); if ($stats['total'] == 0) { $output->writeln("没有需要初始化的账号"); + Log::info("没有需要初始化的账号"); return $stats; } $output->writeln("找到 {$stats['total']} 个需要初始化的账号"); + Log::info("找到 {$stats['total']} 个需要初始化的账号"); - $batchSize = 100; + // 优化批处理:使用传入的批处理大小 $batches = array_chunk($accounts, $batchSize); + $batchCount = count($batches); + + Log::info("将分 {$batchCount} 批处理,每批 {$batchSize} 个账号"); foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + $batchSuccessCount = 0; + $batchFailedCount = 0; + foreach ($batch as $account) { try { $service->calculateAndUpdate($account['id']); $stats['success']++; + $batchSuccessCount++; - if ($stats['success'] % 100 == 0) { + if ($stats['success'] % 20 == 0) { // 更频繁地显示进度 $output->write("."); + Log::debug("已成功初始化 {$stats['success']} 个账号"); } } catch (\Exception $e) { $stats['failed']++; + $batchFailedCount++; + $errorMsg = "初始化账号 {$account['id']} 失败: " . $e->getMessage(); + Log::error($errorMsg); $stats['errors'][] = [ 'accountId' => $account['id'], 'error' => $e->getMessage() @@ -144,9 +306,12 @@ class CalculateWechatAccountScoreCommand extends Command } } - if (($batchIndex + 1) % 10 == 0) { - $output->writeln(" 已处理 " . ($batchIndex + 1) * $batchSize . " 条"); - } + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + + // 每批次完成后输出进度信息 + $output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}"); + Log::info("初始化批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}"); } return $stats; @@ -155,52 +320,90 @@ class CalculateWechatAccountScoreCommand extends Command /** * 更新评分记录(根据wechatId和alias不一致情况) * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function updateScoreRecords($service, $output) + private function updateScoreRecords($service, $output, $accountId = null, $batchSize = 50) { $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(); + try { + // 优化查询:合并两次查询为一次,减少数据库访问次数 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', ''); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('id', $accountId); + } + + $accounts = $query->field('id, wechatId, alias, IF(wechatId = alias, 0, 1) as isModifiedAlias') + ->select(); + + // 分类处理查询结果 + $inconsistentAccounts = []; + $consistentAccounts = []; + + foreach ($accounts as $account) { + if ($account['isModifiedAlias'] == 1) { + $inconsistentAccounts[] = $account; + } else { + $consistentAccounts[] = $account; + } + } + } catch (\Exception $e) { + Log::error("查询需要更新评分记录的账号失败: " . $e->getMessage()); + throw new \Exception("查询需要更新评分记录的账号失败: " . $e->getMessage(), 0, $e); + } $allAccounts = array_merge($inconsistentAccounts, $consistentAccounts); $stats['total'] = count($allAccounts); if ($stats['total'] == 0) { $output->writeln("没有需要更新的账号"); + Log::info("没有需要更新的评分记录"); return $stats; } $output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")"); + Log::info("找到 {$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++; + // 优化批处理:使用传入的批处理大小 + $batches = array_chunk($allAccounts, $batchSize); + $batchCount = count($batches); + + Log::info("将分 {$batchCount} 批更新评分记录,每批 {$batchSize} 个账号"); + + foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + $batchUpdatedCount = 0; - if ($updatedCount % 100 == 0) { - $output->write("."); + foreach ($batch as $account) { + $isModifiedAlias = isset($account['isModifiedAlias']) ? + ($account['isModifiedAlias'] == 1) : + in_array($account['id'], array_column($inconsistentAccounts, 'id')); + + $this->updateScoreRecord($account['id'], $isModifiedAlias, $service); + $updatedCount++; + $batchUpdatedCount++; + + if ($batchUpdatedCount % 20 == 0) { + $output->write("."); + } } + + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + + // 每批次完成后输出进度信息 + $output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录"); + Log::info("更新评分记录批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录"); } if ($updatedCount > 0 && $updatedCount % 100 == 0) { @@ -213,27 +416,44 @@ class CalculateWechatAccountScoreCommand extends Command /** * 批量更新健康分(只更新动态分) * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function batchUpdateHealthScore($service, $output) + private function batchUpdateHealthScore($service, $output, $accountId = null, $batchSize = 50, $forceRecalculate = false) { - // 获取所有已计算基础分的账号 - $accountIds = Db::table('s2_wechat_account_score') - ->where('baseScoreCalculated', 1) - ->column('accountId'); + try { + // 获取所有已计算基础分的账号 + // 优化查询:只查询必要的字段,使用索引字段 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('baseScoreCalculated', 1); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('accountId', $accountId); + } + + $accountIds = $query->column('accountId'); + } catch (\Exception $e) { + Log::error("查询需要批量更新健康分的账号失败: " . $e->getMessage()); + throw new \Exception("查询需要批量更新健康分的账号失败: " . $e->getMessage(), 0, $e); + } $total = count($accountIds); if ($total == 0) { $output->writeln("没有需要更新的账号"); + Log::info("没有需要批量更新健康分的账号"); return ['success' => 0, 'failed' => 0, 'errors' => []]; } $output->writeln("找到 {$total} 个需要更新动态分的账号"); + Log::info("找到 {$total} 个需要更新动态分的账号"); - $stats = $service->batchCalculateAndUpdate($accountIds, 100, false); + // 使用传入的批处理大小和强制重新计算标志 + Log::info("使用批量大小 {$batchSize} 进行批量更新健康分,强制重新计算基础分: " . ($forceRecalculate ? 'true' : 'false')); + $stats = $service->batchCalculateAndUpdate($accountIds, $batchSize, $forceRecalculate); return $stats; } @@ -245,31 +465,47 @@ class CalculateWechatAccountScoreCommand extends Command * @param bool $isModifiedAlias 是否已修改微信号 * @param WechatAccountHealthScoreService $service 评分服务 */ + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + * @return bool 是否成功更新 + */ private function updateScoreRecord($accountId, $isModifiedAlias, $service) { - // 获取账号数据 - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); + Log::debug("开始更新账号 {$accountId} 的评分记录,isModifiedAlias: " . ($isModifiedAlias ? 'true' : 'false')); - if (empty($accountData)) { - return; - } - - // 确保评分记录存在 - $scoreRecord = Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->find(); + try { + // 获取账号数据 - 只查询必要的字段 + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('id', $accountId) + ->field('id, wechatId, alias') // 只查询必要的字段 + ->find(); + + if (empty($accountData)) { + Log::warning("账号 {$accountId} 不存在,跳过更新评分记录"); + return false; + } + + // 确保评分记录存在 - 只查询必要的字段 + $scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->field('accountId, baseScore, baseScoreCalculated, baseInfoScore, dynamicScore') // 只查询必要的字段 + ->find(); if (empty($scoreRecord)) { // 如果记录不存在,创建并计算基础分 + Log::info("账号 {$accountId} 的评分记录不存在,创建并计算基础分"); $service->calculateAndUpdate($accountId); - $scoreRecord = Db::table('s2_wechat_account_score') + $scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->find(); } if (empty($scoreRecord)) { + Log::warning("账号 {$accountId} 的评分记录创建失败,跳过更新"); return; } @@ -297,15 +533,26 @@ class CalculateWechatAccountScoreCommand extends Command $healthScore = max(0, min(100, $healthScore)); $updateData['healthScore'] = $healthScore; $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + + Log::info("账号 {$accountId} 的基础信息分从 {$oldBaseInfoScore} 更新为 {$newBaseInfoScore}," . + "基础分从 {$oldBaseScore} 更新为 {$newBaseScore},健康分更新为 {$healthScore}"); } } else { // 基础分未计算,只更新标记和基础信息分 $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; } - Db::table('s2_wechat_account_score') + $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); + + Log::debug("账号 {$accountId} 的评分记录更新" . ($result !== false ? "成功" : "失败")); + + return $result !== false; + } catch (\Exception $e) { + Log::error("更新账号 {$accountId} 的评分记录失败: " . $e->getMessage()); + return false; + } } } diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index 02a89a9b..d6246a26 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -4,6 +4,8 @@ namespace app\common\service; use think\Db; use think\Exception; +use think\facade\Log; +use think\facade\Cache; /** * 微信账号健康分评分服务(优化版) @@ -14,33 +16,63 @@ use think\Exception; * 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; - const FRIEND_COUNT_SCORE_51_500 = 6; - const FRIEND_COUNT_SCORE_501_3000 = 8; - const FRIEND_COUNT_SCORE_3001_PLUS = 12; + /** + * 好友数量分数区间 + */ + 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_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'; + /** * 计算并更新账号健康分 * @@ -48,38 +80,65 @@ class WechatAccountHealthScoreService * @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('s2_wechat_account') + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', $accountId) ->find(); + + Log::debug("查询账号数据: " . ($accountData ? "成功" : "失败")); } if (empty($accountData)) { - throw new Exception("账号不存在:{$accountId}"); + $errorMsg = "账号不存在:{$accountId}"; + Log::error($errorMsg); + throw new Exception($errorMsg); } $wechatId = $accountData['wechatId'] ?? ''; if (empty($wechatId)) { - throw new Exception("账号wechatId为空:{$accountId}"); + $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); // 计算总分 @@ -93,6 +152,9 @@ class WechatAccountHealthScoreService // 计算每日最大加人次数 $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore); + Log::info("健康分计算结果,accountId: {$accountId}, baseScore: {$baseScore}, dynamicScore: {$dynamicScore}, " . + "healthScore: {$healthScore}, maxAddFriendPerDay: {$maxAddFriendPerDay}"); + // 更新评分记录 $updateData = [ 'dynamicScore' => $dynamicScore, @@ -109,11 +171,16 @@ class WechatAccountHealthScoreService 'updateTime' => time() ]; - Db::table('s2_wechat_account_score') + $updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); + + // 更新成功后,清除缓存 + if ($updateResult !== false) { + $this->clearScoreCache($accountId); + } - return [ + $result = [ 'accountId' => $accountId, 'wechatId' => $wechatId, 'healthScore' => $healthScore, @@ -127,8 +194,19 @@ class WechatAccountHealthScoreService 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; - } catch (Exception $e) { - throw new Exception("计算健康分失败:" . $e->getMessage()); + 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); } } @@ -141,11 +219,13 @@ class WechatAccountHealthScoreService */ private function getOrCreateScoreRecord($accountId, $wechatId) { - $record = Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->find(); + // 尝试获取现有记录 + $record = $this->getScoreRecord($accountId); + // 如果记录不存在,创建新记录 if (empty($record)) { + Log::info("为账号 {$accountId} 创建新的评分记录"); + // 创建新记录 $data = [ 'accountId' => $accountId, @@ -163,7 +243,7 @@ class WechatAccountHealthScoreService 'updateTime' => time() ]; - Db::table('s2_wechat_account_score')->insert($data); + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); return $data; } @@ -175,13 +255,33 @@ class WechatAccountHealthScoreService * 获取评分记录 * * @param int $accountId 账号ID - * @return array + * @param bool $useCache 是否使用缓存(默认true) + * @return array 评分记录,如果不存在则返回空数组 */ - private function getScoreRecord($accountId) + private function getScoreRecord($accountId, $useCache = true) { - return Db::table('s2_wechat_account_score') + // 生成缓存键 + $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() ?: []; + ->find(); + + // 如果记录存在且使用缓存,则缓存记录 + if ($record && $useCache) { + Cache::set($cacheKey, $record, self::CACHE_TTL); + Log::debug("缓存评分记录,accountId: {$accountId}"); + } + + return $record ?: []; } /** @@ -240,12 +340,41 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID * @param array $baseScoreData 基础分数据 + * @return bool 更新是否成功 */ private function updateBaseScore($accountId, $baseScoreData) { - Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->update($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; } /** @@ -310,16 +439,44 @@ class WechatAccountHealthScoreService * @param int $accountId 账号ID * @param int $friendCount 好友数量 * @param string $source 来源(manual=手动,sync=同步) - * @return bool + * @return bool 更新是否成功 + * @throws Exception 如果参数无效或更新过程中出现错误 */ public function updateFriendCountScore($accountId, $friendCount, $source = 'manual') { - $scoreRecord = $this->getScoreRecord($accountId); + // 参数验证 + if (empty($accountId) || !is_numeric($accountId)) { + $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId)); + Log::error($errorMsg); + throw new Exception($errorMsg); + } - // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) - if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { - // 同步数据不允许修改已计算的基础分 - return false; + 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); @@ -348,16 +505,36 @@ class WechatAccountHealthScoreService $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore); } - Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->update($updateData); - - return true; + 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 现有评分记录 @@ -365,6 +542,11 @@ class WechatAccountHealthScoreService */ private function calculateDynamicScore($accountData, $scoreRecord) { + $accountId = $accountData['id'] ?? 0; + $wechatId = $accountData['wechatId'] ?? ''; + + Log::debug("开始计算动态分,accountId: {$accountId}, wechatId: {$wechatId}"); + $result = [ 'total' => 0, 'frequentPenalty' => 0, @@ -377,13 +559,33 @@ class WechatAccountHealthScoreService 'isBanned' => 0 ]; - $accountId = $accountData['id'] ?? 0; - $wechatId = $accountData['wechatId'] ?? ''; - 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("账号没有添加好友记录,动态分为0,accountId: {$accountId}"); + return $result; + } + + Log::debug("账号有添加好友记录,继续计算动态分,accountId: {$accountId}"); + // 继承现有数据 if (!empty($scoreRecord)) { $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null; @@ -396,20 +598,20 @@ class WechatAccountHealthScoreService } // 1. 检查频繁记录(从s2_friend_task表查询,只统计近30天) - $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); + $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); + $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); + $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo); $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; @@ -417,6 +619,10 @@ class WechatAccountHealthScoreService // 计算总分 $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; } @@ -428,16 +634,20 @@ class WechatAccountHealthScoreService * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $scoreRecord 现有评分记录 + * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) * @return array|null */ - private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord) + private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null) { - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } // 查询包含"操作过于频繁"的记录(只统计近30天) // extra字段可能是文本或JSON格式,使用LIKE查询 - $frequentTasks = Db::table('s2_friend_task') + // 优化查询:只查询必要的字段,减少数据传输量 + $frequentTasks = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) ->where('createTime', '>=', $thirtyDaysAgo) ->where(function($query) use ($wechatId) { @@ -448,7 +658,7 @@ class WechatAccountHealthScoreService ->where(function($query) { // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON) $query->where('extra', 'like', '%操作过于频繁%') - ->whereOr('extra', 'like', '%"操作过于频繁"%'); + ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); }) ->order('createTime', 'desc') ->field('id, createTime, extra') @@ -491,20 +701,25 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID * @param string $wechatId 微信ID + * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) * @return array|null */ - private function checkBannedFromMessage($accountId, $wechatId) + private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null) { - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } // 查询封号消息(只统计近30天) - $banMessage = Db::table('s2_wechat_message') + // 优化查询:只查询必要的字段,减少数据传输量 + $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(); @@ -530,9 +745,10 @@ class WechatAccountHealthScoreService * @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) + private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null) { $result = [ 'bonus' => 0, @@ -544,8 +760,10 @@ class WechatAccountHealthScoreService return $result; } - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } $currentTime = time(); // 获取最后一次频繁时间(30天内最后一次频繁的时间) @@ -601,29 +819,54 @@ class WechatAccountHealthScoreService * @param int $batchSize 每批处理数量 * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @return array 处理结果统计 + * @throws Exception 如果参数无效或批量处理过程中出现严重错误 */ public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) { - $stats = [ - 'total' => 0, - 'success' => 0, - 'failed' => 0, - 'errors' => [] - ]; - - // 如果没有指定账号ID,则处理所有账号 - if (empty($accountIds)) { - $accountIds = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->column('id'); + // 参数验证 + 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 $batch) { + foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + foreach ($batch as $accountId) { try { $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); @@ -634,11 +877,30 @@ class WechatAccountHealthScoreService '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); + } } /** @@ -672,7 +934,7 @@ class WechatAccountHealthScoreService if (empty($scoreRecord)) { // 如果记录不存在,先创建 - $accountData = Db::table('s2_wechat_account') + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', $accountId) ->find(); @@ -725,17 +987,36 @@ class WechatAccountHealthScoreService * 获取账号健康分信息 * * @param int $accountId 账号ID + * @param bool $useCache 是否使用缓存(默认true) + * @param bool $forceRecalculate 是否强制重新计算(默认false) * @return array|null */ - public function getHealthScore($accountId) + public function getHealthScore($accountId, $useCache = true, $forceRecalculate = false) { - $scoreRecord = $this->getScoreRecord($accountId); + // 如果强制重新计算,则不使用缓存 + 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; } - return [ + $healthScoreInfo = [ 'accountId' => $scoreRecord['accountId'], 'wechatId' => $scoreRecord['wechatId'], 'healthScore' => $scoreRecord['healthScore'] ?? 0, @@ -753,5 +1034,31 @@ class WechatAccountHealthScoreService '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; } }