From 443ef6ad2dbed39287ac2f0f996b8a4effe798b0 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 13 Jan 2026 15:22:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E8=BF=81=E7=A7=BB=20+=20?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E5=99=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/CheckUnreadMessageCommand.php | 12 +- .../command/TaskSchedulerCommand.php | 138 ++++++-- .../common/service/FriendTransferService.php | 328 +++++++++++++++--- Server/config/task_scheduler.php | 70 ++++ Server/crontab_tasks.md | 7 +- Server/thinkphp/library/think/Cache.php | 9 + 6 files changed, 477 insertions(+), 87 deletions(-) diff --git a/Server/application/command/CheckUnreadMessageCommand.php b/Server/application/command/CheckUnreadMessageCommand.php index 3d47b95e..b5fa3436 100644 --- a/Server/application/command/CheckUnreadMessageCommand.php +++ b/Server/application/command/CheckUnreadMessageCommand.php @@ -22,7 +22,8 @@ class CheckUnreadMessageCommand extends Command { $this->setName('check:unread-message') ->setDescription('检查未读/未回复消息并自动迁移好友') - ->addOption('minutes', 'm', \think\console\input\Option::VALUE_OPTIONAL, '未读/未回复分钟数,默认30分钟', 30); + ->addOption('minutes', 'm', \think\console\input\Option::VALUE_OPTIONAL, '未读/未回复分钟数,默认30分钟', 30) + ->addOption('page-size', 'p', \think\console\input\Option::VALUE_OPTIONAL, '每页处理数量,默认100条', 100); } protected function execute(Input $input, Output $output) @@ -32,11 +33,16 @@ class CheckUnreadMessageCommand extends Command $minutes = 30; } - $output->writeln("开始检查未读/未回复消息(超过{$minutes}分钟)..."); + $pageSize = intval($input->getOption('page-size')); + if ($pageSize <= 0) { + $pageSize = 100; + } + + $output->writeln("开始检查未读/未回复消息(超过{$minutes}分钟,每页处理{$pageSize}条)..."); try { $friendTransferService = new FriendTransferService(); - $result = $friendTransferService->checkAndTransferUnreadOrUnrepliedFriends($minutes); + $result = $friendTransferService->checkAndTransferUnreadOrUnrepliedFriends($minutes, $pageSize); $output->writeln("检查完成:"); $output->writeln(" 总计需要迁移的好友数:{$result['total']}"); diff --git a/Server/application/command/TaskSchedulerCommand.php b/Server/application/command/TaskSchedulerCommand.php index 9cad5c14..b23dae2d 100644 --- a/Server/application/command/TaskSchedulerCommand.php +++ b/Server/application/command/TaskSchedulerCommand.php @@ -29,7 +29,7 @@ class TaskSchedulerCommand extends Command /** * 最大并发进程数 */ - protected $maxConcurrent = 10; + protected $maxConcurrent = 20; /** * 当前运行的进程数 @@ -61,23 +61,48 @@ class TaskSchedulerCommand extends Command $this->maxConcurrent = 1; } - // 加载任务配置(优先使用框架配置,其次直接引入配置文件,避免加载失败) + // 加载任务配置 + // 方法1:尝试通过框架配置加载 $this->tasks = Config::get('task_scheduler', []); - - // 如果通过 Config 没有读到,再尝试直接 include 配置文件 + + // 方法2:如果框架配置没有,直接加载配置文件 if (empty($this->tasks)) { - // 以项目根目录为基准查找 config/task_scheduler.php - $configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php'; - if (is_file($configFile)) { - $config = include $configFile; - if (is_array($config) && !empty($config)) { - $this->tasks = $config; + // 获取项目根目录 + if (!defined('ROOT_PATH')) { + define('ROOT_PATH', dirname(__DIR__, 2)); + } + + // 尝试多个可能的路径 + $possiblePaths = [ + ROOT_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php', + __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php', + dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php', + ]; + + foreach ($possiblePaths as $configFile) { + if (is_file($configFile)) { + $output->writeln("找到配置文件:{$configFile}"); + $config = include $configFile; + if (is_array($config) && !empty($config)) { + $this->tasks = $config; + break; + } else { + $output->writeln("配置文件返回的不是数组或为空:{$configFile}"); + } } } } if (empty($this->tasks)) { - $output->writeln('错误:未找到任务配置(task_scheduler),请检查 config/task_scheduler.php 是否存在且返回数组'); + $output->writeln('错误:未找到任务配置(task_scheduler)'); + $output->writeln('请检查以下位置:'); + $output->writeln('1. config/task_scheduler.php 文件是否存在'); + $output->writeln('2. 文件是否返回有效的数组'); + $output->writeln('3. 文件权限是否正确'); + if (defined('ROOT_PATH')) { + $output->writeln('项目根目录:' . ROOT_PATH . ''); + $output->writeln('期望配置文件:' . ROOT_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php'); + } return false; } @@ -104,16 +129,24 @@ class TaskSchedulerCommand extends Command // 筛选需要执行的任务 $tasksToRun = []; + $enabledCount = 0; + $disabledCount = 0; + foreach ($this->tasks as $taskId => $task) { if (!isset($task['enabled']) || !$task['enabled']) { + $disabledCount++; continue; } + $enabledCount++; if ($this->shouldRun($task['schedule'], $currentMinute, $currentHour, $currentDay, $currentMonth, $currentWeekday)) { $tasksToRun[$taskId] = $task; + $output->writeln("任务 {$taskId} 符合执行条件(schedule: {$task['schedule']})"); } } + $output->writeln("已启用任务数: {$enabledCount},已禁用任务数: {$disabledCount}"); + if (empty($tasksToRun)) { $output->writeln('当前时间没有需要执行的任务'); return true; @@ -266,9 +299,36 @@ class TaskSchedulerCommand extends Command // 检查任务是否已经在运行(防止重复执行) $lockKey = "scheduler_task_lock:{$taskId}"; $lockTime = Cache::get($lockKey); - if ($lockTime && (time() - $lockTime) < 300) { // 5分钟内不重复执行 - $output->writeln("任务 {$taskId} 正在运行中,跳过"); - continue; + + // 如果锁存在,检查进程是否真的在运行 + if ($lockTime) { + $lockPid = Cache::get("scheduler_task_pid:{$taskId}"); + if ($lockPid) { + // 检查进程是否真的在运行 + if (function_exists('posix_kill')) { + // 使用 posix_kill(pid, 0) 检查进程是否存在(0信号不杀死进程,只检查) + if (@posix_kill($lockPid, 0)) { + $output->writeln("任务 {$taskId} 正在运行中(PID: {$lockPid}),跳过"); + continue; + } else { + // 进程不存在,清除锁 + Cache::rm($lockKey); + Cache::rm("scheduler_task_pid:{$taskId}"); + } + } else { + // 如果没有 posix_kill,使用时间判断(2分钟内不重复执行) + if ((time() - $lockTime) < 120) { + $output->writeln("任务 {$taskId} 可能在运行中(2分钟内执行过),跳过"); + continue; + } + } + } else { + // 如果没有PID记录,使用时间判断(2分钟内不重复执行) + if ((time() - $lockTime) < 120) { + $output->writeln("任务 {$taskId} 可能在运行中(2分钟内执行过),跳过"); + continue; + } + } } // 创建子进程 @@ -291,8 +351,9 @@ class TaskSchedulerCommand extends Command ]; $output->writeln("启动任务:{$taskId} (PID: {$pid})"); - // 设置任务锁 + // 设置任务锁和PID Cache::set($lockKey, time(), 600); // 10分钟过期 + Cache::set("scheduler_task_pid:{$taskId}", $pid, 600); // 保存PID,10分钟过期 } } @@ -337,37 +398,51 @@ class TaskSchedulerCommand extends Command } // 构建命令 - // 使用项目根目录下的 think 脚本(同命令行 php think) - if (!defined('ROOT_PATH')) { - define('ROOT_PATH', dirname(__DIR__, 2)); + // 使用指定的网站目录作为执行目录 + $executionPath = '/www/wwwroot/mckb_quwanzhi_com/Server'; + + // 获取 PHP 可执行文件路径 + $phpPath = PHP_BINARY ?: 'php'; + + // 获取 think 脚本路径(使用执行目录) + $thinkPath = $executionPath . DIRECTORY_SEPARATOR . 'think'; + + // 检查 think 文件是否存在 + if (!is_file($thinkPath)) { + $errorMsg = "错误:think 文件不存在:{$thinkPath}"; + Log::error($errorMsg); + file_put_contents($logFile, $errorMsg . "\n", FILE_APPEND); + return; } - $thinkPath = ROOT_PATH . DIRECTORY_SEPARATOR . 'think'; - $command = "php {$thinkPath} {$task['command']}"; + + // 构建命令(使用绝对路径,确保在 Linux 上能正确执行) + $command = escapeshellarg($phpPath) . ' ' . escapeshellarg($thinkPath) . ' ' . escapeshellarg($task['command']); if (!empty($task['options'])) { foreach ($task['options'] as $option) { $command .= ' ' . escapeshellarg($option); } } - // 添加日志重定向 + // 添加日志重定向(在后台执行) $command .= " >> " . escapeshellarg($logFile) . " 2>&1"; // 记录任务开始 $logMessage = "\n" . str_repeat('=', 60) . "\n"; $logMessage .= "任务开始执行: {$taskId}\n"; $logMessage .= "执行时间: " . date('Y-m-d H:i:s') . "\n"; + $logMessage .= "执行目录: {$executionPath}\n"; $logMessage .= "命令: {$command}\n"; $logMessage .= str_repeat('=', 60) . "\n"; file_put_contents($logFile, $logMessage, FILE_APPEND); - // 执行命令 + // 执行命令(使用指定的执行目录,Linux 环境) $descriptorspec = [ - 0 => ['file', (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null'), 'r'], // stdin + 0 => ['file', '/dev/null', 'r'], // stdin 1 => ['file', $logFile, 'a'], // stdout 2 => ['file', $logFile, 'a'], // stderr ]; - $process = @proc_open($command, $descriptorspec, $pipes, ROOT_PATH); + $process = @proc_open($command, $descriptorspec, $pipes, $executionPath); if (is_resource($process)) { // 关闭管道 @@ -412,12 +487,8 @@ class TaskSchedulerCommand extends Command // 关闭进程 proc_close($process); } else { - // 如果 proc_open 失败,尝试直接执行(后台执行) - if (PHP_OS_FAMILY === 'Windows') { - pclose(popen("start /B " . $command, "r")); - } else { - exec($command . ' > /dev/null 2>&1 &'); - } + // 如果 proc_open 失败,使用 exec 在后台执行(Linux 环境) + exec("cd " . escapeshellarg($executionPath) . " && " . $command . ' > /dev/null 2>&1 &'); } $endTime = microtime(true); @@ -448,12 +519,17 @@ class TaskSchedulerCommand extends Command if ($result == $pid || $result == -1) { // 进程已结束 + $taskId = $info['task_id']; unset($this->runningProcesses[$pid]); + // 清除任务锁和PID + Cache::rm("scheduler_task_lock:{$taskId}"); + Cache::rm("scheduler_task_pid:{$taskId}"); + $duration = time() - $info['start_time']; Log::info("子进程执行完成", [ 'pid' => $pid, - 'task' => $info['task_id'], + 'task' => $taskId, 'duration' => $duration, ]); } diff --git a/Server/application/common/service/FriendTransferService.php b/Server/application/common/service/FriendTransferService.php index 4382a656..6085c0b5 100644 --- a/Server/application/common/service/FriendTransferService.php +++ b/Server/application/common/service/FriendTransferService.php @@ -3,7 +3,6 @@ namespace app\common\service; use app\api\controller\AutomaticAssign; -use app\api\controller\AccountController; use think\Db; use think\facade\Log; @@ -44,20 +43,12 @@ class FriendTransferService } // 获取同部门的在线账号列表 - $accountController = new AccountController(); - $accountController->getlist([ - 'pageIndex' => 0, - 'pageSize' => 100, - 'departmentId' => $accountData['departmentId'] - ]); - $accountIds = Db::table('s2_company_account') ->where([ 'departmentId' => $accountData['departmentId'], 'alive' => 1 ]) ->column('id'); - if (empty($accountIds)) { return [ 'success' => false, @@ -157,82 +148,321 @@ class FriendTransferService } } + /** + * 批量迁移好友到其他账号(按账号分组处理) + * @param array $friends 好友列表,格式:[['friendId' => int, 'accountId' => int], ...] + * @param int $currentAccountId 当前账号ID + * @param string $reason 迁移原因 + * @return array ['transferred' => int, 'failed' => int] + */ + public function transferFriendsBatch($friends, $currentAccountId, $reason = '') + { + $transferred = 0; + $failed = 0; + + if (empty($friends)) { + return ['transferred' => 0, 'failed' => 0]; + } + + try { + // 获取当前账号的部门信息 + $accountData = Db::table('s2_company_account')->where('id', $currentAccountId)->find(); + if (empty($accountData)) { + Log::error("批量迁移失败:当前账号不存在,账号ID={$currentAccountId}"); + return ['transferred' => 0, 'failed' => count($friends)]; + } + + // 获取同部门的在线账号列表 + $accountIds = Db::table('s2_company_account') + ->where([ + 'departmentId' => $accountData['departmentId'], + 'alive' => 1 + ]) + ->column('id'); + + if (empty($accountIds)) { + Log::warning("批量迁移失败:没有可用的在线账号,账号ID={$currentAccountId}"); + return ['transferred' => 0, 'failed' => count($friends)]; + } + + // 排除当前账号,选择其他账号 + $availableAccountIds = array_filter($accountIds, function($id) use ($currentAccountId) { + return $id != $currentAccountId; + }); + + if (empty($availableAccountIds)) { + Log::warning("批量迁移失败:没有其他可用的在线账号,账号ID={$currentAccountId}"); + return ['transferred' => 0, 'failed' => count($friends)]; + } + + // 随机选择一个目标账号(同一批次使用同一个目标账号) + $availableAccountIds = array_values($availableAccountIds); + $randomKey = array_rand($availableAccountIds, 1); + $toAccountId = $availableAccountIds[$randomKey]; + + // 获取目标账号信息 + $toAccountData = Db::table('s2_company_account')->where('id', $toAccountId)->find(); + if (empty($toAccountData)) { + Log::error("批量迁移失败:目标账号不存在,账号ID={$toAccountId}"); + return ['transferred' => 0, 'failed' => count($friends)]; + } + + // 批量获取好友信息 + $friendIds = array_column($friends, 'friendId'); + $friendList = Db::table('s2_wechat_friend') + ->where('id', 'in', $friendIds) + ->select(); + + $friendMap = []; + foreach ($friendList as $friend) { + $friendMap[$friend['id']] = $friend; + } + + // 批量执行迁移 + $automaticAssign = new AutomaticAssign(); + $updateData = []; + + foreach ($friends as $friendItem) { + $wechatFriendId = $friendItem['friendId']; + + if (!isset($friendMap[$wechatFriendId])) { + $failed++; + Log::warning("批量迁移失败:好友不存在,好友ID={$wechatFriendId}"); + continue; + } + + $friend = $friendMap[$wechatFriendId]; + + // 如果好友当前账号不在可用账号列表中,或者需要迁移到其他账号 + $needTransfer = !in_array($friend['accountId'], $accountIds) || $currentAccountId != $friend['accountId']; + + if ($needTransfer) { + // 执行迁移 + $result = $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $wechatFriendId, + 'toAccountId' => $toAccountId + ], true); + + $resultData = json_decode($result, true); + + if (isset($resultData['code']) && $resultData['code'] == 200) { + // 收集需要更新的数据 + $updateData[] = [ + 'id' => $wechatFriendId, + 'accountId' => $toAccountId, + 'accountUserName' => $toAccountData['userName'], + 'accountRealName' => $toAccountData['realName'], + 'accountNickname' => $toAccountData['nickname'], + ]; + $transferred++; + } else { + $errorMsg = isset($resultData['msg']) ? $resultData['msg'] : '迁移失败'; + $failed++; + Log::warning("批量迁移失败:好友ID={$wechatFriendId},错误:{$errorMsg}"); + } + } else { + // 无需迁移 + $transferred++; + } + } + + // 批量更新好友的账号信息 + if (!empty($updateData)) { + foreach ($updateData as $data) { + Db::table('s2_wechat_friend') + ->where('id', $data['id']) + ->update([ + 'accountId' => $data['accountId'], + 'accountUserName' => $data['accountUserName'], + 'accountRealName' => $data['accountRealName'], + 'accountNickname' => $data['accountNickname'], + ]); + } + + $logMessage = "批量迁移成功:账号ID={$currentAccountId},共" . count($updateData) . "个好友迁移到账号{$toAccountId}"; + if (!empty($reason)) { + $logMessage .= ",原因:{$reason}"; + } + Log::info($logMessage); + } + + return [ + 'transferred' => $transferred, + 'failed' => $failed + ]; + + } catch (\Exception $e) { + Log::error("批量迁移异常:账号ID={$currentAccountId},错误:" . $e->getMessage()); + return [ + 'transferred' => $transferred, + 'failed' => count($friends) - $transferred + ]; + } + } + /** * 检查并迁移未读或未回复的好友 * @param int $unreadMinutes 未读分钟数,默认30分钟 + * @param int $pageSize 每页处理数量,默认100 * @return array ['total' => int, 'transferred' => int, 'failed' => int] */ - public function checkAndTransferUnreadOrUnrepliedFriends($unreadMinutes = 30) + public function checkAndTransferUnreadOrUnrepliedFriends($unreadMinutes = 30, $pageSize = 100) { $total = 0; $transferred = 0; $failed = 0; try { - $timeThreshold = time() - ($unreadMinutes * 60); + $currentTime = time(); + $timeThreshold = $currentTime - ($unreadMinutes * 60); // 超过指定分钟数的时间点 + $last24Hours = $currentTime - (24 * 60 * 60); // 近24小时的时间点 + + // 确保每页数量合理 + $pageSize = max(1, min(1000, intval($pageSize))); // 查询需要迁移的好友 - // 条件:最后一条消息是用户发送的消息(isSend=0),且超过指定分钟数,且客服在这之后没有回复 + // 条件:以消息表为主表,查询近24小时内的消息 + // 1. 最后一条消息是用户发送的消息(isSend=0) + // 2. 消息时间在近24小时内 + // 3. 消息时间超过指定分钟数(默认30分钟) + // 4. 在这条用户消息之后,客服没有发送任何回复 // 即:用户发送了消息,但客服超过30分钟没有回复,需要迁移给其他客服处理 - // 使用子查询找到每个好友的最后一条消息 - // SQL逻辑说明: - // 1. 找到每个好友的最后一条消息(通过MAX(id)) - // 2. 最后一条消息必须是用户发送的(isSend=0,即客服接收的消息) - // 3. 这条消息的时间超过30分钟前(wm.wechatTime <= timeThreshold) + // SQL逻辑说明(以消息表为主表): + // 1. 从消息表开始,筛选近24小时内的用户消息(isSend=0) + // 2. 找到每个好友的最后一条用户消息(通过MAX(id)) + // 3. 这条消息的时间超过指定分钟数(wm.wechatTime <= timeThreshold) // 4. 在这条用户消息之后,客服没有发送任何回复(NOT EXISTS isSend=1的消息) - // 5. 满足以上条件的好友,说明客服超过30分钟未回复,需要迁移给其他客服 - $sql = " - SELECT DISTINCT - wf.id as friendId, - wf.accountId, - wm.wechatAccountId, - wm.wechatTime, - wm.id as lastMessageId - FROM s2_wechat_friend wf + // 5. 关联好友表,确保好友未删除且已分配账号 + + // 先统计总数 + $countSql = " + SELECT COUNT(DISTINCT wm.wechatFriendId) as total + FROM s2_wechat_message wm INNER JOIN ( SELECT wechatFriendId, MAX(id) as maxId FROM s2_wechat_message WHERE type = 1 + AND isSend = 0 -- 用户发送的消息 + AND wechatTime >= ? -- 近24小时内的消息 GROUP BY wechatFriendId - ) last_msg ON wf.id = last_msg.wechatFriendId - INNER JOIN s2_wechat_message wm ON wm.id = last_msg.maxId - WHERE wf.isDeleted = 0 - AND wm.type = 1 + ) last_msg ON wm.id = last_msg.maxId + INNER JOIN s2_wechat_friend wf ON wf.id = wm.wechatFriendId + WHERE wm.type = 1 AND wm.isSend = 0 -- 最后一条消息是用户发送的(客服接收的) + AND wm.wechatTime >= ? -- 近24小时内的消息 AND wm.wechatTime <= ? -- 超过指定时间(默认30分钟) + AND wf.isDeleted = 0 + AND wf.accountId IS NOT NULL AND NOT EXISTS ( -- 检查在这条用户消息之后,是否有客服的回复 SELECT 1 FROM s2_wechat_message - WHERE wechatFriendId = wf.id + WHERE wechatFriendId = wm.wechatFriendId AND type = 1 AND isSend = 1 -- 客服发送的消息 AND wechatTime > wm.wechatTime -- 在用户消息之后 ) - AND wf.accountId IS NOT NULL "; - $friends = Db::query($sql, [$timeThreshold]); - $total = count($friends); + $countResult = Db::query($countSql, [$last24Hours, $last24Hours, $timeThreshold]); + $total = isset($countResult[0]['total']) ? intval($countResult[0]['total']) : 0; - Log::info("开始检查未读/未回复好友,共找到 {$total} 个需要迁移的好友"); - - foreach ($friends as $friend) { - $result = $this->transferFriend( - $friend['friendId'], - $friend['accountId'], - "消息未读或未回复超过{$unreadMinutes}分钟" - ); - - if ($result['success']) { - $transferred++; - } else { - $failed++; - Log::warning("好友迁移失败:好友ID={$friend['friendId']},原因:{$result['message']}"); - } + if ($total == 0) { + Log::info("未找到需要迁移的未读/未回复好友(近24小时内)"); + return [ + 'total' => 0, + 'transferred' => 0, + 'failed' => 0 + ]; } + Log::info("开始检查未读/未回复好友(近24小时内),共找到 {$total} 个需要迁移的好友,将分页处理(每页{$pageSize}条)"); + + // 分页处理 + $page = 1; + $processed = 0; + + do { + $offset = ($page - 1) * $pageSize; + + $sql = " + SELECT DISTINCT + wf.id as friendId, + wf.accountId, + wm.wechatAccountId, + wm.wechatTime, + wm.id as lastMessageId + FROM s2_wechat_message wm + INNER JOIN ( + SELECT wechatFriendId, MAX(id) as maxId + FROM s2_wechat_message + WHERE type = 1 + AND isSend = 0 -- 用户发送的消息 + AND wechatTime >= ? -- 近24小时内的消息 + GROUP BY wechatFriendId + ) last_msg ON wm.id = last_msg.maxId + INNER JOIN s2_wechat_friend wf ON wf.id = wm.wechatFriendId + WHERE wm.type = 1 + AND wm.isSend = 0 -- 最后一条消息是用户发送的(客服接收的) + AND wm.wechatTime >= ? -- 近24小时内的消息 + AND wm.wechatTime <= ? -- 超过指定时间(默认30分钟) + AND wf.isDeleted = 0 + AND wf.accountId IS NOT NULL + AND NOT EXISTS ( + -- 检查在这条用户消息之后,是否有客服的回复 + SELECT 1 + FROM s2_wechat_message + WHERE wechatFriendId = wm.wechatFriendId + AND type = 1 + AND isSend = 1 -- 客服发送的消息 + AND wechatTime > wm.wechatTime -- 在用户消息之后 + ) + ORDER BY wf.accountId ASC, wm.id ASC + LIMIT ? OFFSET ? + "; + + $friends = Db::query($sql, [$last24Hours, $last24Hours, $timeThreshold, $pageSize, $offset]); + $currentPageCount = count($friends); + + if ($currentPageCount == 0) { + break; + } + + Log::info("处理第 {$page} 页,本页 {$currentPageCount} 条记录"); + + // 按 accountId 分组 + $friendsByAccount = []; + foreach ($friends as $friend) { + $accountId = $friend['accountId']; + if (!isset($friendsByAccount[$accountId])) { + $friendsByAccount[$accountId] = []; + } + $friendsByAccount[$accountId][] = $friend; + } + + // 按账号分组批量处理 + foreach ($friendsByAccount as $accountId => $accountFriends) { + $batchResult = $this->transferFriendsBatch( + $accountFriends, + $accountId, + "消息未读或未回复超过{$unreadMinutes}分钟" + ); + + $transferred += $batchResult['transferred']; + $failed += $batchResult['failed']; + $processed += count($accountFriends); + + Log::info("账号 {$accountId} 批量迁移完成:成功{$batchResult['transferred']},失败{$batchResult['failed']},共" . count($accountFriends) . "个好友"); + } + + $page++; + + // 每处理一页后记录进度 + Log::info("已处理 {$processed}/{$total} 条记录,成功:{$transferred},失败:{$failed}"); + + } while ($currentPageCount == $pageSize && $processed < $total); + Log::info("未读/未回复好友迁移完成:总计{$total},成功{$transferred},失败{$failed}"); return [ diff --git a/Server/config/task_scheduler.php b/Server/config/task_scheduler.php index 6b6a4940..e0b64385 100644 --- a/Server/config/task_scheduler.php +++ b/Server/config/task_scheduler.php @@ -299,6 +299,76 @@ return [ 'log_file' => 'check_unread_message.log', ], + // 同步部门列表,用于部门管理与权限控制 + 'department_list' => [ + 'command' => 'department:list', + 'schedule' => '*/30 * * * *', // 每30分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_department.log', + ], + + // 同步内容库,将外部内容同步到系统内容库 + 'content_sync' => [ + 'command' => 'content:sync', + 'schedule' => '0 2 * * *', // 每天2点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_content_sync.log', + ], + + // 朋友圈采集任务,采集好友朋友圈内容 + 'moments_collect' => [ + 'command' => 'moments:collect', + 'schedule' => '0 6 * * *', // 每天6点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_moments_collect.log', + ], + + // 分配规则列表,同步分配规则数据 + 'allotrule_list' => [ + 'command' => 'allotrule:list', + 'schedule' => '0 3 * * *', // 每天3点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_allotrule_list.log', + ], + + // 自动创建分配规则,根据规则自动创建分配任务 + 'allotrule_autocreate' => [ + 'command' => 'allotrule:autocreate', + 'schedule' => '0 4 * * *', // 每天4点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_allotrule_autocreate.log', + ], + + // 工作台:入群欢迎语任务,自动发送入群欢迎消息 + 'workbench_group_welcome' => [ + 'command' => 'workbench:groupWelcome', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'workbench_groupWelcome.log', + ], + + // 采集客服自己的朋友圈,同步客服账号的朋友圈内容 + 'own_moments_collect' => [ + 'command' => 'own:moments:collect', + 'schedule' => '*/30 * * * *', // 每30分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'own_moments_collect.log', + ], + // 已禁用的任务(注释掉的任务) // 'workbench_group_push' => [ // 'command' => 'workbench:groupPush', diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md index 3f1bdba6..81def432 100644 --- a/Server/crontab_tasks.md +++ b/Server/crontab_tasks.md @@ -169,15 +169,14 @@ crontab -l */5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1 # 工作台入群欢迎语 */1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupWelcome >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupWelcome.log 2>&1 - # 消息提醒 */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 - # 采集客服自己的朋友圈 -*/30 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1 - +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1 +# 检查未读/未回复消息并自动迁移好友 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think check:unread-message --minutes=30 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/check_unread_message.log 2>&1 # 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行) diff --git a/Server/thinkphp/library/think/Cache.php b/Server/thinkphp/library/think/Cache.php index 162cb6d3..3e6aa1d1 100644 --- a/Server/thinkphp/library/think/Cache.php +++ b/Server/thinkphp/library/think/Cache.php @@ -83,6 +83,15 @@ class Cache public function init(array $options = [], $force = false) { if (is_null($this->handler) || $force) { + // 如果配置为空,使用默认配置 + if (empty($options)) { + $options = $this->config; + } + + // 确保有 type 配置 + if (empty($options['type'])) { + $options['type'] = 'File'; + } if ('complex' == $options['type']) { $default = $options['default'];