Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop

This commit is contained in:
超级老白兔
2025-11-24 15:38:20 +08:00
15 changed files with 4716 additions and 140 deletions

View File

@@ -457,22 +457,16 @@ class WebSocketController extends BaseController
// 构建请求参数
$params = [
"cmdType" => 'CmdDownloadMomentImagesResult',
"cmdType" => 'CmdDownloadMomentImages',
"snsId" => $data['snsId'],
"urls" => $data['snsUrls'],
"wechatAccountId" => $data['wechatAccountId'],
"seq" => time(),
];
// 记录请求日志
Log::info('获取朋友圈资源链接请求:' . json_encode($params, 256));
// 发送请求
$this->client->send(json_encode($params));
// 接收响应
$response = $this->client->receive();
$message = json_decode($response, true);
$message = $this->sendMessage($params);
if (empty($message)) {
return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']);
@@ -558,15 +552,17 @@ class WebSocketController extends BaseController
$dataToSave['create_time'] = time();
$res = WechatMoments::create($dataToSave);
}
// // 获取资源链接
// if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){
// $snsData = [
// 'snsId' => $moment['snsId'],
// 'snsUrls' => $momentEntity['urls'],
// 'wechatAccountId' => $wechatAccountId,
// ];
// $this->getMomentSourceRealUrl($snsData);
// }
// 获取资源链接
if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls']) && $moment['type'] == 1) {
$snsData = [
'snsId' => $moment['snsId'],
'snsUrls' => $momentEntity['urls'],
'wechatAccountId' => $wechatAccountId,
];
$this->getMomentSourceRealUrl($snsData);
}
}
//Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条');

View File

@@ -7,5 +7,27 @@ use think\Model;
class WechatAccountModel extends Model
{
// 设置表名
protected $table = 's2_wechat_account';
protected $table = 's2_wechat_account';
// 定义字段类型
protected $type = [
'healthScore' => 'integer',
'baseScore' => 'integer',
'dynamicScore' => 'integer',
'isModifiedAlias' => 'integer',
'frequentCount' => 'integer',
'consecutiveNoFrequentDays' => 'integer',
'lastFrequentTime' => 'integer',
'lastNoFrequentTime' => 'integer',
'scoreUpdateTime' => 'integer',
];
// 允许批量赋值的字段
protected $field = [
'id', 'wechatId', 'alias', 'nickname', 'avatar', 'gender', 'region', 'signature',
'healthScore', 'baseScore', 'dynamicScore', 'isModifiedAlias',
'lastFrequentTime', 'frequentCount', 'lastNoFrequentTime',
'consecutiveNoFrequentDays', 'scoreUpdateTime',
'createTime', 'updateTime', 'status', 'isDeleted'
];
}

View File

@@ -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', // 更新微信账号评分记录
];

View File

@@ -0,0 +1,558 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\facade\Log;
use app\common\service\WechatAccountHealthScoreService;
/**
* 统一计算微信账号健康分命令
* 一个命令完成所有评分工作:
* 1. 初始化未计算的账号(基础分只计算一次)
* 2. 更新评分记录根据wechatId和alias不一致情况
* 3. 批量更新健康分(只更新动态分)
*/
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('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)')
->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();
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: 初始化未计算基础分的账号
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不一致情况
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: 批量更新健康分(只更新动态分,不重新计算基础分)
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();
$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']}");
// 记录命令执行完成的日志
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 (\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 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function initUncalculatedAccounts($service, $output, $accountId = null, $batchSize = 50)
{
$stats = [
'total' => 0,
'success' => 0,
'failed' => 0,
'errors' => []
];
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']} 个需要初始化的账号");
// 优化批处理:使用传入的批处理大小
$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'] % 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()
];
}
}
$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;
}
/**
* 更新评分记录根据wechatId和alias不一致情况
*
* @param WechatAccountHealthScoreService $service 健康分服务实例
* @param Output $output 输出对象
* @return array 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function updateScoreRecords($service, $output, $accountId = null, $batchSize = 50)
{
$stats = ['total' => 0];
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;
// 优化批处理:使用传入的批处理大小
$batches = array_chunk($allAccounts, $batchSize);
$batchCount = count($batches);
Log::info("将分 {$batchCount} 批更新评分记录,每批 {$batchSize} 个账号");
foreach ($batches as $batchIndex => $batch) {
$batchStartTime = microtime(true);
$batchUpdatedCount = 0;
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) {
$output->writeln("");
}
return $stats;
}
/**
* 批量更新健康分(只更新动态分)
*
* @param WechatAccountHealthScoreService $service 健康分服务实例
* @param Output $output 输出对象
* @return array 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function batchUpdateHealthScore($service, $output, $accountId = null, $batchSize = 50, $forceRecalculate = false)
{
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} 个需要更新动态分的账号");
// 使用传入的批处理大小和强制重新计算标志
Log::info("使用批量大小 {$batchSize} 进行批量更新健康分,强制重新计算基础分: " . ($forceRecalculate ? 'true' : 'false'));
$stats = $service->batchCalculateAndUpdate($accountIds, $batchSize, $forceRecalculate);
return $stats;
}
/**
* 更新评分记录
*
* @param int $accountId 账号ID
* @param bool $isModifiedAlias 是否已修改微信号
* @param WechatAccountHealthScoreService $service 评分服务
*/
/**
* 更新评分记录
*
* @param int $accountId 账号ID
* @param bool $isModifiedAlias 是否已修改微信号
* @param WechatAccountHealthScoreService $service 评分服务
* @return bool 是否成功更新
*/
private function updateScoreRecord($accountId, $isModifiedAlias, $service)
{
Log::debug("开始更新账号 {$accountId} 的评分记录isModifiedAlias: " . ($isModifiedAlias ? 'true' : 'false'));
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(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->find();
}
if (empty($scoreRecord)) {
Log::warning("账号 {$accountId} 的评分记录创建失败,跳过更新");
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);
Log::info("账号 {$accountId} 的基础信息分从 {$oldBaseInfoScore} 更新为 {$newBaseInfoScore}" .
"基础分从 {$oldBaseScore} 更新为 {$newBaseScore},健康分更新为 {$healthScore}");
}
} else {
// 基础分未计算,只更新标记和基础信息分
$updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0;
}
$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;
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use app\common\service\WechatAccountHealthScoreService;
/**
* 更新微信账号评分记录
* 根据wechatId和alias是否不一致来更新isModifiedAlias字段仅用于评分不修复数据
*/
class UpdateWechatAccountScoreCommand extends Command
{
protected function configure()
{
$this->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);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace app\common\model;
use think\Model;
/**
* 微信账号评分记录模型类
*/
class WechatAccountScore extends Model
{
// 设置表名
protected $name = 'wechat_account_score';
protected $table = 's2_wechat_account_score';
// 自动写入时间戳
protected $autoWriteTimestamp = false;
// 定义字段类型
protected $type = [
'accountId' => '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',
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1070,7 +1070,7 @@ class ContentLibraryController extends Controller
->select()->toArray();
if (empty($libraries)) {
return json(['code' => 200, 'msg' => '没有可用的内容库配置']);
return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'],256);
}
$successCount = 0;
@@ -1159,7 +1159,7 @@ class ContentLibraryController extends Controller
}
// 返回采集结果
return json([
return json_encode([
'code' => 200,
'msg' => '采集任务执行完成',
'data' => [
@@ -1169,7 +1169,7 @@ class ContentLibraryController extends Controller
'skipped' => $totalLibraries - $successCount - $failCount,
'results' => $results
]
]);
],256);
}
/**
@@ -1206,7 +1206,7 @@ class ContentLibraryController extends Controller
->whereIn('id', $friendIds)
->where('isDeleted', 0)
->select();
if (empty($friends)) {
return [
'status' => 'failed',

View File

@@ -2816,6 +2816,8 @@ class WorkbenchController extends Controller
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$keyword = $this->request->param('keyword', '');
$pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
$status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
$userId = $this->request->userInfo['id'];
// 构建工作台查询条件
@@ -2840,10 +2842,11 @@ class WorkbenchController extends Controller
$workbenchWhere[] = ['w.id', '=', $workbenchId];
}
// 按内容ID、工作台ID和时间分组统计每次推送
$query = Db::name('workbench_group_push_item')
// 1. 先查询所有已执行的推送记录(按推送时间分组)
$pushHistoryQuery = Db::name('workbench_group_push_item')
->alias('wgpi')
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
->where($workbenchWhere)
@@ -2853,52 +2856,57 @@ class WorkbenchController extends Controller
'wgpi.contentId',
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
'wgpi.targetType',
'wgp.groupPushSubType',
'MIN(wgpi.createTime) as createTime',
'COUNT(DISTINCT wgpi.id) as totalCount',
'cl.name as contentLibraryName'
])
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType');
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
if (!empty($keyword)) {
$query->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
$pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
}
// 获取分页数据
$list = $query->order('createTime', 'desc')
->page($page, $limit)
->select();
// 对于有 group by 的查询,统计总数需要重新查询
$totalQuery = Db::name('workbench_group_push_item')
->alias('wgpi')
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
->where($workbenchWhere);
if (!empty($keyword)) {
$totalQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
}
// 统计分组后的记录数(使用子查询)
$subQuery = $totalQuery
$pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
// 2. 查询所有任务(包括未执行的)
$allTasksQuery = Db::name('workbench')
->alias('w')
->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
->where($workbenchWhere)
->field([
'wgpi.workbenchId',
'wgpi.contentId',
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
'wgpi.targetType'
])
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType')
->buildSql();
$total = Db::table('(' . $subQuery . ') as temp')->count();
'w.id as workbenchId',
'w.name as workbenchName',
'w.createTime',
'wgp.targetType',
'wgp.groupPushSubType',
'wgp.groups',
'wgp.friends',
'wgp.trafficPools'
]);
// 处理每条记录
foreach ($list as &$item) {
if (!empty($keyword)) {
$allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
}
$allTasks = $allTasksQuery->select();
// 3. 合并数据:已执行的推送记录 + 未执行的任务
$resultList = [];
$executedWorkbenchIds = [];
// 处理已执行的推送记录
foreach ($pushHistoryList as $item) {
$itemWorkbenchId = $item['workbenchId'];
$contentId = $item['contentId'];
$pushTime = $item['pushTime'];
$targetType = intval($item['targetType']);
$groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
// 标记该工作台已有执行记录
if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
$executedWorkbenchIds[] = $itemWorkbenchId;
}
// 将时间字符串转换为时间戳范围(小时级别)
$pushTimeStart = strtotime($pushTime);
@@ -2937,23 +2945,149 @@ class WorkbenchController extends Controller
$failCount = 0; // 简化处理,实际需要从发送状态获取
// 状态判断
$status = $successCount > 0 ? 'success' : 'failed';
$itemStatus = $successCount > 0 ? 'success' : 'failed';
if ($failCount > 0 && $successCount > 0) {
$status = 'partial';
$itemStatus = 'partial';
}
$item['pushType'] = $targetType == 1 ? '群推送' : '好友推送';
$item['pushTypeCode'] = $targetType;
$item['targetCount'] = $targetCount;
$item['successCount'] = $successCount;
$item['failCount'] = $failCount;
$item['status'] = $status;
$item['statusText'] = $status == 'success' ? '成功' : ($status == 'partial' ? '部分成功' : '失败');
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
// 任务名称(工作台名称)
$item['taskName'] = $item['workbenchName'] ?? '';
// 推送类型判断
$pushTypeText = '';
$pushTypeCode = '';
if ($targetType == 1) {
// 群推送
if ($groupPushSubType == 2) {
$pushTypeText = '群公告';
$pushTypeCode = 'announcement';
} else {
$pushTypeText = '群消息';
$pushTypeCode = 'group';
}
} else {
// 好友推送
$pushTypeText = '好友消息';
$pushTypeCode = 'friend';
}
$resultList[] = [
'workbenchId' => $itemWorkbenchId,
'taskName' => $item['workbenchName'] ?? '',
'pushType' => $pushTypeText,
'pushTypeCode' => $pushTypeCode,
'targetCount' => $targetCount,
'successCount' => $successCount,
'failCount' => $failCount,
'status' => $itemStatus,
'statusText' => $this->getStatusText($itemStatus),
'createTime' => date('Y-m-d H:i:s', $item['createTime']),
'contentLibraryName' => $item['contentLibraryName'] ?? ''
];
}
unset($item);
// 处理未执行的任务
foreach ($allTasks as $task) {
$taskWorkbenchId = $task['workbenchId'];
// 如果该任务已有执行记录,跳过(避免重复)
if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
continue;
}
$targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
$groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
// 计算目标数量(从配置中获取)
$targetCount = 0;
if ($targetType == 1) {
// 群推送:统计配置的群数量
$groups = json_decode($task['groups'] ?? '[]', true);
$targetCount = is_array($groups) ? count($groups) : 0;
} else {
// 好友推送:统计配置的好友数量或流量池数量
$friends = json_decode($task['friends'] ?? '[]', true);
$trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
$friendCount = is_array($friends) ? count($friends) : 0;
$poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
// 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
$targetCount = $friendCount > 0 ? $friendCount : $poolCount;
}
// 推送类型判断
$pushTypeText = '';
$pushTypeCode = '';
if ($targetType == 1) {
// 群推送
if ($groupPushSubType == 2) {
$pushTypeText = '群公告';
$pushTypeCode = 'announcement';
} else {
$pushTypeText = '群消息';
$pushTypeCode = 'group';
}
} else {
// 好友推送
$pushTypeText = '好友消息';
$pushTypeCode = 'friend';
}
$resultList[] = [
'workbenchId' => $taskWorkbenchId,
'taskName' => $task['workbenchName'] ?? '',
'pushType' => $pushTypeText,
'pushTypeCode' => $pushTypeCode,
'targetCount' => $targetCount,
'successCount' => 0,
'failCount' => 0,
'status' => 'pending',
'statusText' => '进行中',
'createTime' => date('Y-m-d H:i:s', $task['createTime']),
'contentLibraryName' => ''
];
}
// 应用筛选条件
$filteredList = [];
foreach ($resultList as $item) {
// 推送类型筛选
if (!empty($pushType)) {
if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
continue;
}
if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
continue;
}
if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
continue;
}
}
// 状态筛选
if (!empty($status)) {
if ($status === 'success' && $item['status'] !== 'success') {
continue;
}
if ($status === 'progress') {
// 进行中:包括 partial 和 pending
if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
continue;
}
}
if ($status === 'failed' && $item['status'] !== 'failed') {
continue;
}
}
$filteredList[] = $item;
}
// 按创建时间倒序排序
usort($filteredList, function($a, $b) {
return strtotime($b['createTime']) - strtotime($a['createTime']);
});
// 分页处理
$total = count($filteredList);
$offset = ($page - 1) * $limit;
$list = array_slice($filteredList, $offset, $limit);
return json([
'code' => 200,
@@ -2967,5 +3101,21 @@ class WorkbenchController extends Controller
]);
}
/**
* 获取状态文本
* @param string $status 状态码
* @return string 状态文本
*/
private function getStatusText($status)
{
$statusMap = [
'success' => '已完成',
'partial' => '进行中',
'pending' => '进行中',
'failed' => '失败'
];
return $statusMap[$status] ?? '未知';
}
}

View File

@@ -1,70 +1,72 @@
{
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
],
"homepage": "http://thinkphp.cn/",
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
},
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"php": ">=5.6.0",
"topthink/framework": "5.1.41",
"topthink/think-installer": "2.*",
"topthink/think-captcha": "^2.0",
"topthink/think-helper": "^3.0",
"topthink/think-image": "^1.0",
"topthink/think-queue": "^2.0",
"topthink/think-worker": "^2.0",
"textalk/websocket": "^1.5",
"aliyuncs/oss-sdk-php": "^2.6",
"monolog/monolog": "^1.27",
"guzzlehttp/guzzle": "^6.5",
"overtrue/wechat": "~4.6",
"endroid/qr-code": "^3.9",
"phpoffice/phpspreadsheet": "^1.29",
"workerman/workerman": "^3.5",
"workerman/gateway-worker": "^3.0",
"hashids/hashids": "^2.0",
"khanamiryan/qrcode-detector-decoder": "^1.0",
"lizhichao/word": "^2.0",
"adbario/php-dot-notation": "^2.2"
},
"require-dev": {
"symfony/var-dumper": "^3.4|^4.4",
"topthink/think-migration": "^2.0",
"phpunit/phpunit": "^5.0|^6.0"
},
"autoload": {
"psr-4": {
"app\\": "application",
"Eison\\": "extend/Eison"
},
"files": [
"application/common.php"
],
"homepage": "http://thinkphp.cn/",
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
},
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"php": ">=5.6.0",
"topthink/framework": "5.1.*",
"topthink/think-installer": "~1.0",
"topthink/think-captcha": "^2.0",
"topthink/think-helper": "^3.0",
"topthink/think-image": "^1.0",
"topthink/think-queue": "^2.0",
"topthink/think-worker": "^2.0",
"textalk/websocket": "^1.2",
"aliyuncs/oss-sdk-php": "^2.3",
"monolog/monolog": "^1.24",
"guzzlehttp/guzzle": "^6.3",
"overtrue/wechat": "~4.0",
"endroid/qr-code": "^3.5",
"phpoffice/phpspreadsheet": "^1.8",
"workerman/workerman": "^3.5",
"workerman/gateway-worker": "^3.0",
"hashids/hashids": "^2.0",
"khanamiryan/qrcode-detector-decoder": "^1.0",
"lizhichao/word": "^2.0",
"adbario/php-dot-notation": "^2.2"
},
"require-dev": {
"symfony/var-dumper": "^3.4",
"topthink/think-migration": "^2.0"
},
"autoload": {
"psr-4": {
"app\\": "application"
},
"files": [
"application/common.php"
],
"classmap": []
},
"extra": {
"think-path": "thinkphp"
},
"config": {
"preferred-install": "dist",
"allow-plugins": {
"topthink/think-installer": true,
"easywechat-composer/easywechat-composer": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
"classmap": []
},
"extra": {
"think-path": "thinkphp"
},
"config": {
"preferred-install": "dist",
"allow-plugins": {
"topthink/think-installer": true,
"easywechat-composer/easywechat-composer": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -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
```

View File

@@ -0,0 +1,125 @@
<?php
namespace Eison\Utils\Helper;
/**
* 数组辅助类
*/
class ArrHelper
{
/**
* 从数组中提取指定的键值
*
* @param string $keys 键名多个用逗号分隔支持键名映射account=userName
* @param array $array 源数组
* @param mixed $default 默认值,如果键不存在时返回此值
* @return array
*/
public static function getValue(string $keys, array $array, $default = null): array
{
$result = [];
$keyList = explode(',', $keys);
foreach ($keyList as $key) {
$key = trim($key);
// 支持键名映射account=userName
if (strpos($key, '=') !== false) {
list($sourceKey, $targetKey) = explode('=', $key, 2);
$sourceKey = trim($sourceKey);
$targetKey = trim($targetKey);
// 如果源键存在,使用源键的值;否则使用目标键的值;都不存在则使用默认值
if (isset($array[$sourceKey])) {
$result[$targetKey] = $array[$sourceKey];
} elseif (isset($array[$targetKey])) {
$result[$targetKey] = $array[$targetKey];
} else {
// 如果提供了默认值,使用默认值;否则不添加该键
if ($default !== null) {
$result[$targetKey] = $default;
}
}
} else {
// 普通键名
if (isset($array[$key])) {
$result[$key] = $array[$key];
} else {
// 如果提供了默认值,使用默认值;否则不添加该键
if ($default !== null) {
$result[$key] = $default;
}
}
}
}
return $result;
}
/**
* 移除数组中的空值null、空字符串、空数组
*
* @param array $array 源数组
* @return array
*/
public static function rmValue(array $array): array
{
return array_filter($array, function($value) {
if (is_array($value)) {
return !empty($value);
}
return $value !== null && $value !== '';
});
}
/**
* 左连接两个数组
*
* @param array $leftArray 左数组
* @param array $rightArray 右数组
* @param string $key 关联键名
* @return array
*/
public static function leftJoin(array $leftArray, array $rightArray, string $key): array
{
// 将右数组按关联键索引
$rightIndexed = [];
foreach ($rightArray as $item) {
if (isset($item[$key])) {
$rightIndexed[$item[$key]] = $item;
}
}
// 左连接
$result = [];
foreach ($leftArray as $leftItem) {
$leftKeyValue = $leftItem[$key] ?? null;
if ($leftKeyValue !== null && isset($rightIndexed[$leftKeyValue])) {
$result[] = array_merge($leftItem, $rightIndexed[$leftKeyValue]);
} else {
$result[] = $leftItem;
}
}
return $result;
}
/**
* 将数组的某一列作为键,重新组织数组
*
* @param string $key 作为键的列名
* @param array $array 源数组
* @return array
*/
public static function columnTokey(string $key, array $array): array
{
$result = [];
foreach ($array as $item) {
if (isset($item[$key])) {
$result[$item[$key]] = $item;
}
}
return $result;
}
}

View File

@@ -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)

2301
Server/sql.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
# 微信健康分规则 v2
## 一、定义
当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。
**健康分 = 基础分 + 动态分**
健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\
通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\
后期还可将健康分作为标签属性,用于快速筛选微信号。
**公式:每日最大加人次数 = 健康分 × 0.2**
## 二、基础分
基础分为 **60--100 分**
`60 + 40基础加成分` 四个维度参数组成,每个参数具有不同权重。
### 基础分组成
类型 权重 分数
------------ ------ ------
基础信息 0.2 10
好友数量 0.3 30
默认基础分 --- 60
### 1. 基础信息(权重 0.2,满分 10
类型 权重 分数
-------------- ------ ------
已修改微信号 1 10
### 2. 好友数量(权重 0.3,满分 30
好友数量范围 权重 分数
-------------- ------ ------
0--50 0.1 3
51--500 0.2 6
501--3000 0.3 8
3001 以上 0.4 12
## 三、动态分规则
### 扣分规则
场景 扣分 处罚
---------- ------ --------------
首次频繁 15 暂停 24 小时
再次频繁 25 暂停 24 小时
封号 60 暂停 72 小时
### 加分规则
场景 加分
--------------------- ------
连续 3 天不触发频繁 5/日