From b1bfcec35b164b0b46325b60f79be827cc2aab6c Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 13 Apr 2026 17:23:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(media):=20=E5=BE=AE=E4=BF=A1=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=AA=92=E4=BD=93=20OSS=20=E5=BD=92=E6=A1=A3=E4=B8=8E?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=9C=B0=E5=9D=80=E5=9B=9E=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MediaArchiveJob、MediaOssArchiveService、WechatMediaArchiveService 与回填命令 - Message/DataProcessing 等支持归档调度与持久化下载 URL - WebSocket 控制器整理;朋友圈与文档/定时任务说明更新 Made-with: Cursor --- README_scheduler.md | 2 +- .../api/controller/MessageController.php | 36 ++ .../api/controller/WebSocketController.php | 91 +---- .../chukebao/controller/DataProcessing.php | 30 ++ application/command.php | 1 + .../command/BackfillMediaOssCommand.php | 121 ++++++ .../command/SyncWechatDataToCkbTask.php | 2 +- .../common/service/MediaOssArchiveService.php | 261 ++++++++++++ .../service/WechatMediaArchiveService.php | 372 ++++++++++++++++++ .../wechat/GetWechatMomentsV1Controller.php | 15 +- application/job/MediaArchiveJob.php | 57 +++ crontab_tasks.md | 116 +++--- 12 files changed, 970 insertions(+), 134 deletions(-) create mode 100644 application/command/BackfillMediaOssCommand.php create mode 100644 application/common/service/MediaOssArchiveService.php create mode 100644 application/common/service/WechatMediaArchiveService.php create mode 100644 application/job/MediaArchiveJob.php diff --git a/README_scheduler.md b/README_scheduler.md index 4c48568..64d7d6e 100644 --- a/README_scheduler.md +++ b/README_scheduler.md @@ -44,7 +44,7 @@ ```bash # 每分钟执行一次调度器(调度器内部会根据 cron 表达式判断哪些任务需要执行) -* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 +* * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think scheduler:run >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/scheduler.log 2>&1 ``` ### 4. 系统要求 diff --git a/application/api/controller/MessageController.php b/application/api/controller/MessageController.php index 8e4dd7e..ff521bb 100644 --- a/application/api/controller/MessageController.php +++ b/application/api/controller/MessageController.php @@ -4,7 +4,10 @@ namespace app\api\controller; use app\api\model\WechatMessageModel; use app\common\service\FriendTransferService; +use app\common\service\WechatMediaArchiveService; +use app\job\MediaArchiveJob; use think\Db; +use think\facade\Log; use think\facade\Request; class MessageController extends BaseController @@ -418,6 +421,7 @@ class MessageController extends BaseController 'type' => 1, 'accountId' => $item['accountId'], 'content' => $item['content'], + 'originalContent' => $item['content'], 'createTime' => $createTime, 'deleteTime' => $deleteTime, 'isDeleted' => $item['isDeleted'] ?? false, @@ -463,6 +467,9 @@ class MessageController extends BaseController }else{ $id = $data['id']; unset($data['id']); + if (!empty($exists['originalContent'])) { + unset($data['originalContent']); + } $res = $exists->save($data); } @@ -496,6 +503,9 @@ class MessageController extends BaseController } } } + if (in_array((int)($item['msgType'] ?? 0), [3, 34, 43, 47, 49], true) && !empty($id)) { + MediaArchiveJob::dispatch('message', $id, ['source' => 'saveMessage']); + } return true; } @@ -577,6 +587,9 @@ class MessageController extends BaseController throw new \Exception('更新群聊消息记录失败'); } } + if (in_array((int)($item['msgType'] ?? 0), [3, 34, 43, 47, 49], true)) { + MediaArchiveJob::dispatch('message', $item['id'], ['source' => 'saveChatroomMessage']); + } return true; } catch (\Exception $e) { // 记录错误日志,便于调试 @@ -588,6 +601,29 @@ class MessageController extends BaseController } } + /** + * 持久化视频/文件下载后的真实地址,并重新触发 OSS 归档 + * @param array $data + * @return bool + */ + public function updateDownloadedMessageMedia($data) + { + $messageId = (int)($data['friendMessageId'] ?? $data['chatroomMessageId'] ?? 0); + $downloadUrl = trim((string)($data['url'] ?? '')); + + if ($messageId <= 0 || empty($downloadUrl)) { + return false; + } + + $updated = WechatMediaArchiveService::updateDownloadedMessageMedia($messageId, $downloadUrl); + if (!$updated) { + return false; + } + + MediaArchiveJob::dispatch('message', $messageId, ['source' => $data['type'] ?? 'download_result']); + return true; + } + /** * 处理消息内容,提取发送者ID和消息内容 * @param string $content 原始消息内容 diff --git a/application/api/controller/WebSocketController.php b/application/api/controller/WebSocketController.php index 2c64245..bbc9542 100644 --- a/application/api/controller/WebSocketController.php +++ b/application/api/controller/WebSocketController.php @@ -12,7 +12,7 @@ use think\facade\Env; use app\api\model\WechatFriendModel as WechatFriend; use app\api\model\WechatMomentsModel as WechatMoments; use think\facade\Cache; -use app\common\util\AliyunOSS; +use app\common\service\MediaOssArchiveService; class WebSocketController extends BaseController @@ -468,7 +468,7 @@ class WebSocketController extends BaseController // 更新数据库:保存原始URL和OSS URL,并标记已上传 $updateData = [ 'resUrls' => $urls, - 'isOssUploaded' => 1, // 标识已上传到OSS + 'isOssUploaded' => !empty($ossUrls) ? 1 : 0, 'update_time' => time() ]; @@ -513,7 +513,7 @@ class WebSocketController extends BaseController // 更新数据库:保存原始URL和OSS URL,并标记已上传 $updateData = [ 'resUrls' => $urls, - 'isOssUploaded' => 1, // 标识已上传到OSS + 'isOssUploaded' => !empty($ossUrls) ? 1 : 0, 'update_time' => time() ]; @@ -557,76 +557,23 @@ class WebSocketController extends BaseController if (empty($urls) || !is_array($urls)) { return $ossUrls; } - - try { - // 创建临时目录(兼容无 runtime_path() 辅助函数的环境) - if (function_exists('runtime_path')) { - $baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } elseif (defined('RUNTIME_PATH')) { - $baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } else { - // 兜底:使用项目根目录下的 runtime 目录 - $baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR; + + foreach ($urls as $url) { + if (!MediaOssArchiveService::isRemoteHttpUrl($url)) { + continue; } - $tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'moments' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR; + $resourceType = preg_match('/\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i', $url) ? 'video' : 'image'; + $result = MediaOssArchiveService::archiveRemoteUrl($url, 'moments', $resourceType, (string)$snsId); + if (!empty($result['success']) && !empty($result['url'])) { + $ossUrls[] = $result['url']; + continue; + } - if (!is_dir($tempDir)) { - mkdir($tempDir, 0755, true); - } - - foreach ($urls as $index => $url) { - if (empty($url)) { - continue; - } - - try { - // 下载图片到临时文件 - $tempFile = $tempDir . md5($url . $snsId . $index) . '.jpg'; - - // 使用curl下载图片 - $ch = curl_init($url); - $fp = fopen($tempFile, 'wb'); - curl_setopt($ch, CURLOPT_FILE, $fp); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - fclose($fp); - - if ($httpCode != 200 || !file_exists($tempFile) || filesize($tempFile) == 0) { - Log::warning('下载朋友圈图片失败:' . $url . ', HTTP Code: ' . $httpCode); - @unlink($tempFile); - continue; - } - - // 生成OSS对象名称 - $objectName = 'moments/' . date('Y/m/d/') . md5($snsId . $index . time()) . '.jpg'; - - // 上传到OSS - $result = AliyunOSS::uploadFile($tempFile, $objectName); - if ($result['success']) { - $ossUrls[] = $result['url']; - } else { - Log::error('朋友圈图片上传OSS失败:' . $url . ', 错误:' . ($result['error'] ?? '未知错误')); - } - - // 删除临时文件 - @unlink($tempFile); - - } catch (\Exception $e) { - Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage() . ', URL: ' . $url); - if (isset($tempFile) && file_exists($tempFile)) { - @unlink($tempFile); - } - } - } - - } catch (\Exception $e) { - Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage()); + Log::error('朋友圈媒体上传OSS失败:' . ($result['error'] ?? '未知错误'), [ + 'snsId' => $snsId, + 'url' => $url, + ]); } return $ossUrls; @@ -696,8 +643,8 @@ class WebSocketController extends BaseController } // 获取资源链接(检查是否已上传到OSS,如果已上传则跳过) - if(empty($momentEntity['urls']) || $moment['type'] != 1) { - // 如果没有urls或类型不是1,跳过 + if(empty($momentEntity['urls'])) { + // 如果没有urls,跳过 } elseif ($isOssUploaded == 1) { // 如果已上传到OSS,跳过采集 } else { diff --git a/application/chukebao/controller/DataProcessing.php b/application/chukebao/controller/DataProcessing.php index 5f0121c..56d3bd5 100644 --- a/application/chukebao/controller/DataProcessing.php +++ b/application/chukebao/controller/DataProcessing.php @@ -37,6 +37,8 @@ class DataProcessing extends BaseController 'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId} 'CmdNewMessage', //接收消息 'CmdSendMessageResult', //更新消息状态 + 'CmdDownloadVideoResult', //视频下载结果回写 + 'CmdDownloadFileResult', //文件下载结果回写 'CmdPinToTop', //置顶 ]; @@ -168,6 +170,34 @@ class DataProcessing extends BaseController $msg = '更新消息状态成功'; break; + case 'CmdDownloadVideoResult': + case 'CmdDownloadFileResult': + $friendMessageId = $this->request->param('friendMessageId', 0); + $chatroomMessageId = $this->request->param('chatroomMessageId', 0); + $url = trim((string)$this->request->param('url', '')); + + if (empty($friendMessageId) && empty($chatroomMessageId)) { + return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个'); + } + + if (empty($url)) { + return ResponseHelper::error('url不能为空'); + } + + $messageController = new MessageController(); + $updated = $messageController->updateDownloadedMessageMedia([ + 'friendMessageId' => $friendMessageId, + 'chatroomMessageId' => $chatroomMessageId, + 'url' => $url, + 'type' => $type, + ]); + + if (!$updated) { + return ResponseHelper::error('媒体地址回写失败'); + } + + $msg = '媒体地址回写成功'; + break; case 'CmdPinToTop': //置顶 $wechatFriendId = $this->request->param('wechatFriendId', 0); $wechatChatroomId = $this->request->param('wechatChatroomId', 0); diff --git a/application/command.php b/application/command.php index 45b6d7d..65e4966 100644 --- a/application/command.php +++ b/application/command.php @@ -53,4 +53,5 @@ return [ // V2 流量池数据迁移 'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统 + 'media:archive' => 'app\command\BackfillMediaOssCommand', // 历史媒体资源归档到OSS ]; diff --git a/application/command/BackfillMediaOssCommand.php b/application/command/BackfillMediaOssCommand.php new file mode 100644 index 0000000..3c1a71e --- /dev/null +++ b/application/command/BackfillMediaOssCommand.php @@ -0,0 +1,121 @@ +setName('media:archive') + ->setDescription('扫描聊天记录和朋友圈媒体资源并加入 OSS 归档队列') + ->addOption('scope', null, Option::VALUE_OPTIONAL, '归档范围:chat|moments|all', 'all') + ->addOption('limit', null, Option::VALUE_OPTIONAL, '单次扫描数量', 100) + ->addOption('startId', null, Option::VALUE_OPTIONAL, '仅扫描大于该ID的记录', 0) + ->addOption('messageId', null, Option::VALUE_OPTIONAL, '仅同步归档单条聊天记录(本地调试用,不走队列)', 0); + } + + protected function execute(Input $input, Output $output) + { + $singleMessageId = (int)$input->getOption('messageId'); + if ($singleMessageId > 0) { + return $this->archiveOneChatMessage($singleMessageId, $output); + } + + $scope = strtolower((string)$input->getOption('scope')); + $limit = max(1, (int)$input->getOption('limit')); + $startId = max(0, (int)$input->getOption('startId')); + + $messageCount = 0; + $momentCount = 0; + + if (in_array($scope, ['all', 'chat'])) { + $messageIds = Db::table('s2_wechat_message') + ->where('id', '>', $startId) + ->whereIn('msgType', [3, 34, 43, 47, 49]) + ->where(function ($query) { + $query->whereLike('content', 'http%') + ->whereOrLike('content', '%"url"%') + ->whereOrLike('content', '%"previewImage"%') + ->whereOrLike('content', '%"tencentUrl"%'); + }) + ->order('id', 'asc') + ->limit($limit) + ->column('id'); + + foreach ($messageIds as $id) { + MediaArchiveJob::dispatch('message', $id, ['source' => 'backfill_command']); + $messageCount++; + } + } + + if (in_array($scope, ['all', 'moments'])) { + $momentIds = Db::table('s2_wechat_moments') + ->where('id', '>', $startId) + ->where(function ($query) { + $query->where('isOssUploaded', 0) + ->whereOr('ossUrls', 'null') + ->whereOr('ossUrls', '') + ->whereOr('ossUrls', '[]'); + }) + ->where(function ($query) { + $query->where('resUrls', '<>', '') + ->whereOr('urls', '<>', ''); + }) + ->order('id', 'asc') + ->limit($limit) + ->column('id'); + + foreach ($momentIds as $id) { + MediaArchiveJob::dispatch('moment', $id, ['source' => 'backfill_command']); + $momentCount++; + } + } + + $output->writeln(sprintf( + '已加入 OSS 归档队列:聊天记录 %d 条,朋友圈 %d 条。', + $messageCount, + $momentCount + )); + + return 0; + } + + /** + * 单条聊天消息同步归档(用于本地验证 AliyunOSS 与下载链路) + */ + protected function archiveOneChatMessage($messageId, Output $output) + { + $row = Db::table('s2_wechat_message')->where('id', $messageId)->find(); + if (empty($row)) { + $output->writeln('错误:未找到消息 id=' . $messageId); + return 1; + } + + $preview = function ($s, $len) { + $s = (string)$s; + return function_exists('mb_substr') ? mb_substr($s, 0, $len) : substr($s, 0, $len); + }; + + $output->writeln('msgType=' . ($row['msgType'] ?? '') . ' content(前200字): ' . $preview($row['content'] ?? '', 200)); + + $ok = WechatMediaArchiveService::archiveMessageById($messageId); + if (!$ok) { + $output->writeln('失败:归档未执行或无需更新(请确认 msgType 为 3/34/43/47/49 且 content 含可下载 http 资源)'); + return 1; + } + + $after = Db::table('s2_wechat_message')->where('id', $messageId)->find(); + $output->writeln('成功:归档完成。更新后 content(前300字):'); + $output->writeln($preview($after['content'] ?? '', 300)); + + return 0; + } +} diff --git a/application/command/SyncWechatDataToCkbTask.php b/application/command/SyncWechatDataToCkbTask.php index 8d0409c..671fd0b 100644 --- a/application/command/SyncWechatDataToCkbTask.php +++ b/application/command/SyncWechatDataToCkbTask.php @@ -9,7 +9,7 @@ use think\console\Command; use think\facade\App; use WeChatDeviceApi\Adapters\ChuKeBao\Adapter as ChuKeBaoAdapter; -// */7 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 +// */7 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/sync_wechat_data.log 2>&1 class SyncWechatDataToCkbTask extends Command { protected $lockFile; diff --git a/application/common/service/MediaOssArchiveService.php b/application/common/service/MediaOssArchiveService.php new file mode 100644 index 0000000..1f150da --- /dev/null +++ b/application/common/service/MediaOssArchiveService.php @@ -0,0 +1,261 @@ + 'jpg', + 'image/jpg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + 'image/bmp' => 'bmp', + 'image/svg+xml' => 'svg', + 'video/mp4' => 'mp4', + 'video/quicktime' => 'mov', + 'video/x-msvideo' => 'avi', + 'video/webm' => 'webm', + 'audio/mpeg' => 'mp3', + 'audio/mp3' => 'mp3', + 'audio/wav' => 'wav', + 'audio/x-wav' => 'wav', + 'audio/amr' => 'amr', + 'audio/aac' => 'aac', + 'audio/mp4' => 'm4a', + 'audio/ogg' => 'ogg', + 'application/pdf' => 'pdf', + 'application/msword' => 'doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.ms-excel' => 'xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/zip' => 'zip', + 'application/x-rar-compressed' => 'rar', + 'application/x-7z-compressed' => '7z', + 'text/plain' => 'txt', + ]; + + public static function isRemoteHttpUrl($url) + { + if (!is_string($url)) { + return false; + } + + return (bool)preg_match('/^https?:\/\//i', trim($url)); + } + + public static function isOwnOssUrl($url) + { + if (!self::isRemoteHttpUrl($url)) { + return false; + } + + $host = strtolower((string)parse_url($url, PHP_URL_HOST)); + if (empty($host)) { + return false; + } + + $customHost = strtolower((string)parse_url(AliyunOSS::ossUrl, PHP_URL_HOST)); + $bucketHost = strtolower(AliyunOSS::BUCKET . '.' . AliyunOSS::ENDPOINT); + + return $host === $customHost || $host === $bucketHost || strpos($host, strtolower(AliyunOSS::BUCKET . '.')) === 0; + } + + public static function archiveRemoteUrl($url, $bizType, $resourceType, $bizKey, array $options = []) + { + $url = trim((string)$url); + if (!self::isRemoteHttpUrl($url)) { + return [ + 'success' => false, + 'error' => 'URL不是有效的http(s)地址', + 'url' => '', + ]; + } + + if (self::isOwnOssUrl($url)) { + return [ + 'success' => true, + 'alreadyArchived' => true, + 'url' => self::normalizeOssUrl($url), + 'originalUrl' => $url, + 'object_name' => '', + ]; + } + + $tempFile = ''; + try { + $tempDir = self::ensureTempDirectory(); + $tempFile = $tempDir . md5($url . microtime(true) . mt_rand()) . '.tmp'; + + $downloadResult = self::downloadToLocal($url, $tempFile, (int)($options['timeout'] ?? 60)); + if (!$downloadResult['success']) { + return $downloadResult; + } + + $extension = self::detectExtension( + $url, + $tempFile, + $downloadResult['contentType'] ?? '', + $options['extension'] ?? '' + ); + + $objectName = self::buildObjectName($bizType, $resourceType, $bizKey, $extension); + $result = AliyunOSS::uploadFile($tempFile, $objectName); + if (!$result['success']) { + return [ + 'success' => false, + 'error' => $result['error'] ?? '上传OSS失败', + 'url' => '', + ]; + } + + return [ + 'success' => true, + 'alreadyArchived' => false, + 'url' => self::normalizeOssUrl($result['url'] ?? ''), + 'originalUrl' => $url, + 'object_name' => $objectName, + 'extension' => $extension, + 'mime_type' => $result['mime_type'] ?? ($downloadResult['contentType'] ?? ''), + 'size' => $result['size'] ?? 0, + ]; + } catch (\Exception $e) { + Log::error('媒体资源上传OSS失败:' . $e->getMessage(), [ + 'url' => $url, + 'bizType' => $bizType, + 'resourceType' => $resourceType, + 'bizKey' => $bizKey, + ]); + + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'url' => '', + ]; + } finally { + if (!empty($tempFile) && file_exists($tempFile)) { + @unlink($tempFile); + } + } + } + + protected static function ensureTempDirectory() + { + if (function_exists('runtime_path')) { + $baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } elseif (defined('RUNTIME_PATH')) { + $baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } else { + $baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR; + } + + $tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'media_archive' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + } + + return $tempDir; + } + + protected static function downloadToLocal($url, $tempFile, $timeout) + { + $ch = curl_init($url); + $fp = fopen($tempFile, 'wb'); + + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_USERAGENT, 'CKB-MediaArchive/1.0'); + curl_exec($ch); + + $curlError = curl_error($ch); + $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + curl_close($ch); + fclose($fp); + + if (!empty($curlError)) { + @unlink($tempFile); + return [ + 'success' => false, + 'error' => '下载资源失败:' . $curlError, + 'url' => '', + ]; + } + + if ($httpCode < 200 || $httpCode >= 300 || !file_exists($tempFile) || filesize($tempFile) <= 0) { + @unlink($tempFile); + return [ + 'success' => false, + 'error' => '下载资源失败,HTTP状态码:' . $httpCode, + 'url' => '', + ]; + } + + return [ + 'success' => true, + 'contentType' => $contentType, + 'httpCode' => $httpCode, + ]; + } + + protected static function detectExtension($url, $tempFile, $contentType = '', $forcedExtension = '') + { + $forcedExtension = strtolower(trim((string)$forcedExtension, '. ')); + if (!empty($forcedExtension)) { + return $forcedExtension; + } + + $pathExtension = strtolower((string)pathinfo((string)parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); + if (preg_match('/^[a-z0-9]{1,8}$/i', $pathExtension)) { + return $pathExtension; + } + + $contentType = strtolower(trim((string)$contentType)); + if (!empty($contentType)) { + $contentType = trim(explode(';', $contentType)[0]); + if (!empty(self::$mimeExtensionMap[$contentType])) { + return self::$mimeExtensionMap[$contentType]; + } + } + + $mimeType = ''; + if (function_exists('mime_content_type')) { + $mimeType = strtolower((string)mime_content_type($tempFile)); + } + if (!empty($mimeType) && !empty(self::$mimeExtensionMap[$mimeType])) { + return self::$mimeExtensionMap[$mimeType]; + } + + return 'bin'; + } + + protected static function buildObjectName($bizType, $resourceType, $bizKey, $extension) + { + $bizType = trim((string)$bizType, '/'); + $resourceType = trim((string)$resourceType, '/'); + $bizKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', (string)$bizKey); + $extension = trim((string)$extension, '.'); + + return $bizType . '/' . $resourceType . '/' . date('Y/m/d/') . $bizKey . '_' . substr(md5(uniqid('', true)), 0, 16) . '.' . $extension; + } + + protected static function normalizeOssUrl($url) + { + $url = trim((string)$url); + if (strpos($url, 'http://') === 0) { + return 'https://' . substr($url, 7); + } + + return $url; + } +} diff --git a/application/common/service/WechatMediaArchiveService.php b/application/common/service/WechatMediaArchiveService.php new file mode 100644 index 0000000..c587d9a --- /dev/null +++ b/application/common/service/WechatMediaArchiveService.php @@ -0,0 +1,372 @@ +where('id', $messageId)->find(); + if (empty($row)) { + return false; + } + + $update = self::buildMessageArchiveUpdate($row); + if (empty($update)) { + return false; + } + + Db::table('s2_wechat_message')->where('id', $messageId)->update($update); + return true; + } + + public static function archiveMomentById($momentId) + { + $momentId = (int)$momentId; + if ($momentId <= 0) { + return false; + } + + $row = Db::table('s2_wechat_moments')->where('id', $momentId)->find(); + if (empty($row)) { + return false; + } + + $displayUrls = self::decodeJsonArray($row['resUrls'] ?? ''); + $rawUrls = self::decodeJsonArray($row['urls'] ?? ''); + $candidateUrls = !empty($displayUrls) ? $displayUrls : $rawUrls; + if (empty($candidateUrls)) { + return false; + } + + $ossUrls = []; + foreach ($candidateUrls as $index => $url) { + if (!MediaOssArchiveService::isRemoteHttpUrl($url)) { + continue; + } + + $resourceType = self::isVideoUrl($url) ? 'video' : 'image'; + $result = MediaOssArchiveService::archiveRemoteUrl($url, 'moments', $resourceType, (string)($row['snsId'] ?? $momentId), [ + 'index' => $index, + ]); + if (!empty($result['success']) && !empty($result['url'])) { + $ossUrls[] = $result['url']; + } + } + + if (empty($ossUrls)) { + return false; + } + + Db::table('s2_wechat_moments')->where('id', $momentId)->update([ + 'ossUrls' => json_encode($ossUrls, JSON_UNESCAPED_UNICODE), + 'isOssUploaded' => 1, + 'update_time' => time(), + ]); + + return true; + } + + public static function updateDownloadedMessageMedia($messageId, $downloadUrl) + { + $messageId = (int)$messageId; + $downloadUrl = trim((string)$downloadUrl); + if ($messageId <= 0 || empty($downloadUrl)) { + return false; + } + + $row = Db::table('s2_wechat_message')->where('id', $messageId)->find(); + if (empty($row)) { + return false; + } + + $content = (string)($row['content'] ?? ''); + $originalContent = (string)($row['originalContent'] ?? ''); + $msgType = (int)($row['msgType'] ?? 0); + + $update = []; + if (empty($originalContent)) { + $update['originalContent'] = $content; + } + + if ($msgType === 43) { + $payload = self::decodeJsonObject($content); + if (empty($payload)) { + $payload = []; + } + $payload['videoUrl'] = $downloadUrl; + $payload['isLoading'] = false; + $update['content'] = self::encodeJson($payload); + } elseif ($msgType === 49) { + $payload = self::decodeJsonObject($content); + if (empty($payload) || strtolower((string)($payload['type'] ?? '')) !== 'file') { + $payload = [ + 'type' => 'file', + 'title' => self::extractFileTitle($content), + ]; + } + $payload['url'] = $downloadUrl; + $payload['isDownloading'] = false; + $update['content'] = self::encodeJson($payload); + } else { + return false; + } + + Db::table('s2_wechat_message')->where('id', $messageId)->update($update); + return true; + } + + protected static function buildMessageArchiveUpdate(array $row) + { + $msgType = (int)($row['msgType'] ?? 0); + $content = (string)($row['content'] ?? ''); + $originalContent = (string)($row['originalContent'] ?? ''); + $sourceOriginalContent = $originalContent !== '' ? $originalContent : $content; + + $update = []; + switch ($msgType) { + case 3: + case 47: + $imageUpdate = self::archiveImageLikeContent($content, $msgType, $row['id']); + if (!empty($imageUpdate)) { + $update = array_merge($update, $imageUpdate); + } + break; + case 34: + $audioUpdate = self::archiveAudioContent($content, $row['id']); + if (!empty($audioUpdate)) { + $update = array_merge($update, $audioUpdate); + } + break; + case 43: + $videoUpdate = self::archiveVideoContent($content, $row['id']); + if (!empty($videoUpdate)) { + $update = array_merge($update, $videoUpdate); + } + break; + case 49: + $fileUpdate = self::archiveFileContent($content, $row['id']); + if (!empty($fileUpdate)) { + $update = array_merge($update, $fileUpdate); + } + break; + default: + break; + } + + if (!empty($update) && empty($originalContent) && !empty($sourceOriginalContent)) { + $update['originalContent'] = $sourceOriginalContent; + } + + return $update; + } + + protected static function archiveImageLikeContent($content, $msgType, $messageId) + { + $payload = self::decodeJsonObject($content); + if (!empty($payload) && !empty($payload['url'])) { + $sourceUrl = (string)($payload['originUrl'] ?? $payload['url']); + $result = MediaOssArchiveService::archiveRemoteUrl( + $sourceUrl, + 'messages', + $msgType == 47 ? 'emoji' : 'image', + (string)$messageId + ); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + $payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl; + $payload['ossUrl'] = $result['url']; + $payload['url'] = $result['url']; + return ['content' => self::encodeJson($payload)]; + } + + if (!MediaOssArchiveService::isRemoteHttpUrl($content)) { + return []; + } + + $result = MediaOssArchiveService::archiveRemoteUrl( + $content, + 'messages', + $msgType == 47 ? 'emoji' : 'image', + (string)$messageId + ); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + return ['content' => $result['url']]; + } + + protected static function archiveAudioContent($content, $messageId) + { + $payload = self::decodeJsonObject($content); + if (!empty($payload) && !empty($payload['url'])) { + $sourceUrl = (string)($payload['originUrl'] ?? $payload['url']); + $result = MediaOssArchiveService::archiveRemoteUrl($sourceUrl, 'messages', 'audio', (string)$messageId); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + $payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl; + $payload['ossUrl'] = $result['url']; + $payload['url'] = $result['url']; + return ['content' => self::encodeJson($payload)]; + } + + if (!MediaOssArchiveService::isRemoteHttpUrl($content)) { + return []; + } + + $result = MediaOssArchiveService::archiveRemoteUrl($content, 'messages', 'audio', (string)$messageId); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + return ['content' => $result['url']]; + } + + protected static function archiveVideoContent($content, $messageId) + { + $payload = self::decodeJsonObject($content); + if (empty($payload)) { + if (!MediaOssArchiveService::isRemoteHttpUrl($content)) { + return []; + } + + $result = MediaOssArchiveService::archiveRemoteUrl($content, 'messages', 'video', (string)$messageId); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + return ['content' => $result['url']]; + } + + $changed = false; + $previewSource = (string)($payload['previewImageOriginUrl'] ?? $payload['previewImage'] ?? ''); + if (MediaOssArchiveService::isRemoteHttpUrl($previewSource)) { + $previewResult = MediaOssArchiveService::archiveRemoteUrl($previewSource, 'messages', 'video_cover', (string)$messageId); + if (!empty($previewResult['success']) && !empty($previewResult['url'])) { + $payload['previewImageOriginUrl'] = $payload['previewImageOriginUrl'] ?? $previewSource; + $payload['previewImageOssUrl'] = $previewResult['url']; + $payload['previewImage'] = $previewResult['url']; + $changed = true; + } + } + + $videoSource = (string)($payload['originUrl'] ?? $payload['videoUrl'] ?? $payload['ossUrl'] ?? $payload['tencentUrl'] ?? $payload['url'] ?? ''); + if (MediaOssArchiveService::isRemoteHttpUrl($videoSource)) { + $videoResult = MediaOssArchiveService::archiveRemoteUrl($videoSource, 'messages', 'video', (string)$messageId); + if (!empty($videoResult['success']) && !empty($videoResult['url'])) { + $payload['originUrl'] = $payload['originUrl'] ?? $videoSource; + $payload['ossUrl'] = $videoResult['url']; + $payload['videoUrl'] = $videoResult['url']; + $payload['url'] = $videoResult['url']; + $payload['isLoading'] = false; + $changed = true; + } + } + + if (!$changed) { + return []; + } + + return ['content' => self::encodeJson($payload)]; + } + + protected static function archiveFileContent($content, $messageId) + { + $payload = self::decodeJsonObject($content); + if (empty($payload) || strtolower((string)($payload['type'] ?? '')) !== 'file') { + return []; + } + + $sourceUrl = (string)($payload['originUrl'] ?? $payload['url'] ?? ''); + if (!MediaOssArchiveService::isRemoteHttpUrl($sourceUrl)) { + return []; + } + + $result = MediaOssArchiveService::archiveRemoteUrl( + $sourceUrl, + 'messages', + 'file', + (string)$messageId, + [ + 'extension' => $payload['fileext'] ?? '', + ] + ); + if (empty($result['success']) || empty($result['url'])) { + return []; + } + + $payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl; + $payload['ossUrl'] = $result['url']; + $payload['url'] = $result['url']; + $payload['isDownloading'] = false; + return ['content' => self::encodeJson($payload)]; + } + + protected static function decodeJsonObject($value) + { + if (!is_string($value)) { + return []; + } + + $decoded = json_decode(trim($value), true); + return is_array($decoded) ? $decoded : []; + } + + protected static function decodeJsonArray($value) + { + if (is_array($value)) { + return $value; + } + + if (!is_string($value) || trim($value) === '') { + return []; + } + + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + protected static function encodeJson(array $payload) + { + return json_encode($payload, JSON_UNESCAPED_UNICODE); + } + + protected static function extractFileTitle($rawContent) + { + if (!is_string($rawContent) || trim($rawContent) === '') { + return '文件'; + } + + if (preg_match('/<!\[CDATA\[(.*?)\]\]><\/title>/i', $rawContent, $matches) && !empty($matches[1])) { + return trim($matches[1]); + } + + if (preg_match('/<title>([^<]+)<\/title>/i', $rawContent, $matches) && !empty($matches[1])) { + return trim($matches[1]); + } + + return '文件'; + } + + protected static function isVideoUrl($url) + { + if (!is_string($url)) { + return false; + } + + return (bool)preg_match('/\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i', $url); + } +} diff --git a/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php index 06f6c3b..414a5d8 100644 --- a/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php +++ b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php @@ -201,7 +201,7 @@ class GetWechatMomentsV1Controller extends BaseController // 格式化数据 $rows = []; foreach ($moments as $moment) { - $resUrls = $this->decodeJson($moment['resUrls'] ?? null); + $resUrls = $this->preferredMediaUrls($moment); $imageUrls = is_array($resUrls) ? $resUrls : []; // 格式化日期和时间 @@ -300,7 +300,8 @@ class GetWechatMomentsV1Controller extends BaseController 'content' => $row['content'] ?? '', 'commentList' => $this->decodeJson($row['commentList'] ?? null), 'likeList' => $this->decodeJson($row['likeList'] ?? null), - 'resUrls' => $this->decodeJson($row['resUrls'] ?? null), + 'resUrls' => $this->preferredMediaUrls($row), + 'ossUrls' => $this->decodeJson($row['ossUrls'] ?? null), 'createTime' => $formatTime($row['createTime'] ?? null), 'momentEntity' => [ 'lat' => $row['lat'] ?? 0, @@ -312,6 +313,16 @@ class GetWechatMomentsV1Controller extends BaseController ]; } + protected function preferredMediaUrls(array $row): array + { + $ossUrls = $this->decodeJson($row['ossUrls'] ?? null); + if (!empty($ossUrls)) { + return $ossUrls; + } + + return $this->decodeJson($row['resUrls'] ?? null); + } + /** * JSON字段解析 * diff --git a/application/job/MediaArchiveJob.php b/application/job/MediaArchiveJob.php new file mode 100644 index 0000000..5a1e7ff --- /dev/null +++ b/application/job/MediaArchiveJob.php @@ -0,0 +1,57 @@ +<?php + +namespace app\job; + +use app\common\service\WechatMediaArchiveService; +use think\facade\Config; +use think\facade\Log; +use think\queue\Job; +use think\Queue; + +class MediaArchiveJob +{ + const QUEUE_NAME = 'media_archive'; + + public function fire(Job $job, $data) + { + try { + $scope = $data['scope'] ?? ''; + $id = (int)($data['id'] ?? 0); + if (empty($scope) || $id <= 0) { + $job->delete(); + return; + } + + $success = false; + if ($scope === 'message') { + $success = WechatMediaArchiveService::archiveMessageById($id); + } elseif ($scope === 'moment') { + $success = WechatMediaArchiveService::archiveMomentById($id); + } + + if ($success || $job->attempts() >= 3) { + $job->delete(); + return; + } + + $job->release(Config::get('queue.failed_delay', 10)); + } catch (\Exception $e) { + Log::error('媒体资源归档任务失败:' . $e->getMessage(), $data ?: []); + if ($job->attempts() >= 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + public static function dispatch($scope, $id, array $extra = []) + { + $payload = array_merge($extra, [ + 'scope' => $scope, + 'id' => (int)$id, + ]); + + return Queue::push(self::class, $payload, self::QUEUE_NAME); + } +} diff --git a/crontab_tasks.md b/crontab_tasks.md index 81def43..b89b304 100644 --- a/crontab_tasks.md +++ b/crontab_tasks.md @@ -4,85 +4,85 @@ ```bash # 设备列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/device_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think device:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/device_list.log 2>&1 # 微信好友列表 -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_friends_list.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/wechat_friends_list.log 2>&1 # 微信群列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_chatroom_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatChatroom:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/wechat_chatroom_list.log 2>&1 # 添加好友任务列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/friend_task_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think friendTask:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/friend_task_list.log 2>&1 # 微信客服列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatList:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/wechat_list.log 2>&1 # 公司账号列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/account_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think account:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/account_list.log 2>&1 # 微信好友消息列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_friends_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think message:friendsList >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/message_friends_list.log 2>&1 # 微信群聊消息列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_chatroom_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think message:chatroomList >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/message_chatroom_list.log 2>&1 # 部门列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think department:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/department_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think department:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/department_list.log 2>&1 # 同步内容库 -0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:sync >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_sync.log 2>&1 +0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think content:sync >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/content_sync.log 2>&1 # 微信群好友列表 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/group_friends_list.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think groupFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/group_friends_list.log 2>&1 # 获取通话记录 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think call-recording:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/call_recording.log 2>&1 # 分配规则列表 -0 3 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_list.log 2>&1 +0 3 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think allotrule:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/allot_rule_list.log 2>&1 # 自动创建分配规则 -0 4 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:autocreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_autocreate.log 2>&1 +0 4 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think allotrule:autocreate >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/allot_rule_autocreate.log 2>&1 # 内容采集任务 -0 5 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_collect.log 2>&1 +0 5 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think content:collect >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/content_collect.log 2>&1 # 朋友圈采集任务 -0 6 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/moments_collect.log 2>&1 +0 6 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think moments:collect >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/moments_collect.log 2>&1 # 工作台自动点赞任务 -0 7 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_auto_like.log 2>&1 +0 7 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:autoLike >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_auto_like.log 2>&1 # 工作台朋友圈同步任务 -0 8 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 +0 8 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:moments >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_moments.log 2>&1 # 同步微信数据到存客宝 -0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 +0 9 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/sync_wechat_data.log 2>&1 # 工作台群发消息 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:groupPush >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_groupPush.log 2>&1 # 工作台建群 -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:groupCreate >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_groupCreate.log 2>&1 # 工作台通讯录导入 -*/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 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:import-contact >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/import_contact.log 2>&1 # 工作台流量分发 -0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 +0 9 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:trafficDistribute >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/traffic_distribute.log 2>&1 # 预防性切换好友 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think switch:friends >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/switch_friends.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 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think kf:notice >> /www/wwwroot/ckbapi.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 +0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechat:calculate-score >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/calculate_score.log 2>&1 @@ -92,12 +92,12 @@ ## 说明 -- 所有命令都在 `/www/wwwroot/mckb_quwanzhi_com/Server` 目录下执行 +- 所有命令都在 `/www/wwwroot/ckbapi.quwanzhi.com/Server` 目录下执行 - 默认只获取未删除(活跃)的设备、微信好友和群聊 - 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用 - 每个命令的执行结果都会记录到对应的日志文件中 - 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped`) -- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下 +- 日志文件位于 `/www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/` 目录下 - 大部分任务每5分钟执行一次(`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行) - 设备列表的未删除设备任务每天凌晨1点执行一次(`0 1 * * *`) - 自动创建分配规则每小时整点执行一次(`0 * * * *`) @@ -120,64 +120,64 @@ crontab -l # 设备列表 - 未删除设备(每半小时执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_active.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think device:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_device_active.log 2>&1 # 设备列表 - 已删除设备(每天1点执行) -0 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_deleted.log 2>&1 +0 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think device:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_device_deleted.log 2>&1 # 设备列表 - 已停用设备(每天1:10执行) -10 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=2 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_stopped.log 2>&1 +10 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think device:list --isDel=2 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_device_stopped.log 2>&1 # 微信好友列表 - 未删除好友(每1分钟执行) -*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_active.log 2>&1 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatFriends:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_wechatFriends_active.log 2>&1 # 微信好友列表 - 已删除好友(每天1:30分执行) -30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_deleted.log 2>&1 +30 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatFriends:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_wechatFriends_deleted.log 2>&1 # 微信群列表 - 未删除群(每5分钟执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_active.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_wechatChatroom_active.log 2>&1 # 微信群列表 - 已删除群(每天1:30分执行) -30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_deleted.log 2>&1 +30 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_wechatChatroom_deleted.log 2>&1 # 微信群好友列表(没5分钟执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_groupFriends.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think groupFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_groupFriends.log 2>&1 # 添加好友任务列表(每1分钟执行) -*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_friendTask.log 2>&1 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think friendTask:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_friendTask.log 2>&1 # 微信客服列表(每5分钟执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatList.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechatList:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_wechatList.log 2>&1 # 公司账号列表(每5分钟执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_account.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think account:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_account.log 2>&1 # 微信好友消息列表(每30分钟执行) -*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageFriends.log 2>&1 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think message:friendsList >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_messageFriends.log 2>&1 # 微信群聊消息列表(每30分钟执行) -*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageChatroom.log 2>&1 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think message:chatroomList >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_messageChatroom.log 2>&1 # 获取通话记录 -*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 +*/30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think call-recording:list >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/call_recording.log 2>&1 # 清洗微信数据 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/sync_wechat_data.log 2>&1 # 内容采集任务(每5分钟执行) -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_contentCollect.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think content:collect >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_contentCollect.log 2>&1 # 工作台任务_自动点赞(每10分钟执行) -*/6 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_workbench_autoLike.log 2>&1 +*/6 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:autoLike >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/crontab_workbench_autoLike.log 2>&1 # 每3天的3点同步所有好友 -0 3 */3 * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:allFriends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/all_friends.log 2>&1 +0 3 */3 * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think sync:allFriends >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/all_friends.log 2>&1 # 工作台流量分发 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:trafficDistribute >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/traffic_distribute.log 2>&1 # 工作台朋友圈同步任务 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:moments >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_moments.log 2>&1 # 工作台群发消息 -#*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 +#*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:groupPush >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_groupPush.log 2>&1 # 预防性切换好友 -*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 +*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think switch:friends >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/switch_friends.log 2>&1 # 工作台建群 -*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:groupCreate >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/workbench_groupCreate.log 2>&1 # 工作台通讯录导入 -*/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 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think workbench:import-contact >> /www/wwwroot/ckbapi.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/ckbapi.quwanzhi.com/Server && php think workbench:groupWelcome >> /www/wwwroot/ckbapi.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 +*/1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think kf:notice >> /www/wwwroot/ckbapi.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 +0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think wechat:calculate-score >> /www/wwwroot/ckbapi.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/ckbapi.quwanzhi.com/Server && php think own:moments:collect >> /www/wwwroot/ckbapi.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 +*/5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think check:unread-message --minutes=30 >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/check_unread_message.log 2>&1 # 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行) -* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 +* * * * * cd /www/wwwroot/ckbapi.quwanzhi.com/Server && php think scheduler:run >> /www/wwwroot/ckbapi.quwanzhi.com/Server/runtime/log/scheduler.log 2>&1