客服评分体系优化
This commit is contained in:
@@ -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("<error>{$errorMsg}</error>");
|
||||
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<error>数据库错误: " . $errorMsg . "</error>");
|
||||
$output->writeln($e->getTraceAsString());
|
||||
|
||||
// 记录数据库错误
|
||||
Log::error("数据库错误: " . $errorMsg);
|
||||
Log::error("错误堆栈: " . $e->getTraceAsString());
|
||||
|
||||
return 2; // 数据库错误状态码
|
||||
} catch (\Exception $e) {
|
||||
// 一般异常
|
||||
$errorMsg = "命令执行失败: " . $e->getMessage();
|
||||
$output->writeln("\n<error>错误: " . $errorMsg . "</error>");
|
||||
$output->writeln($e->getTraceAsString());
|
||||
|
||||
// 记录严重错误
|
||||
Log::error($errorMsg);
|
||||
Log::error("错误堆栈: " . $e->getTraceAsString());
|
||||
|
||||
return 1; // 一般错误状态码
|
||||
} catch (\Throwable $e) {
|
||||
// 其他所有错误
|
||||
$errorMsg = "严重错误: " . $e->getMessage();
|
||||
$output->writeln("\n<error>严重错误: " . $errorMsg . "</error>");
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user