From 1deebf3f5c69ed10cef2c3b5755ba5fbd9a72a7c Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 14 Apr 2026 09:37:00 +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 --- .gitignore | 3 + Server/README_scheduler.md | 2 +- .../api/controller/MessageController.php | 36 ++ .../api/controller/WebSocketController.php | 91 +---- .../chukebao/controller/DataProcessing.php | 30 ++ Server/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 +- Server/application/job/MediaArchiveJob.php | 57 +++ Server/crontab_tasks.md | 116 +++--- .../components/AudioMessage/AudioMessage.tsx | 4 +- .../components/FileMessage/index.tsx | 12 +- .../components/VideoMessage/index.tsx | 12 +- .../messageTypes/ImageMessage.tsx | 14 +- .../FriendsCicle/components/friendCard.tsx | 19 +- .../src/store/module/websocket/msgManage.ts | 29 +- 19 files changed, 1046 insertions(+), 151 deletions(-) create mode 100644 Server/application/command/BackfillMediaOssCommand.php create mode 100644 Server/application/common/service/MediaOssArchiveService.php create mode 100644 Server/application/common/service/WechatMediaArchiveService.php create mode 100644 Server/application/job/MediaArchiveJob.php diff --git a/.gitignore b/.gitignore index 3048ae356..68fcda8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ Touchkebao/.specstory/ Serverruntime/ Moncter/提示词/ *.log +*.code-workspace +项目整体分析报告.md +Server/docs/traffic_pool_design.md diff --git a/Server/README_scheduler.md b/Server/README_scheduler.md index 4c485684f..64d7d6ebd 100644 --- a/Server/README_scheduler.md +++ b/Server/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/Server/application/api/controller/MessageController.php b/Server/application/api/controller/MessageController.php index 8e4dd7e4e..ff521bb98 100644 --- a/Server/application/api/controller/MessageController.php +++ b/Server/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/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 2c6424507..bbc954263 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/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/Server/application/chukebao/controller/DataProcessing.php b/Server/application/chukebao/controller/DataProcessing.php index 5f0121cc6..56d3bd514 100644 --- a/Server/application/chukebao/controller/DataProcessing.php +++ b/Server/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/Server/application/command.php b/Server/application/command.php index 45b6d7da8..65e4966ce 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -53,4 +53,5 @@ return [ // V2 流量池数据迁移 'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统 + 'media:archive' => 'app\command\BackfillMediaOssCommand', // 历史媒体资源归档到OSS ]; diff --git a/Server/application/command/BackfillMediaOssCommand.php b/Server/application/command/BackfillMediaOssCommand.php new file mode 100644 index 000000000..3c1a71e75 --- /dev/null +++ b/Server/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/Server/application/command/SyncWechatDataToCkbTask.php b/Server/application/command/SyncWechatDataToCkbTask.php index 8d0409cbe..671fd0b48 100644 --- a/Server/application/command/SyncWechatDataToCkbTask.php +++ b/Server/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/Server/application/common/service/MediaOssArchiveService.php b/Server/application/common/service/MediaOssArchiveService.php new file mode 100644 index 000000000..1f150da63 --- /dev/null +++ b/Server/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/Server/application/common/service/WechatMediaArchiveService.php b/Server/application/common/service/WechatMediaArchiveService.php new file mode 100644 index 000000000..c587d9a19 --- /dev/null +++ b/Server/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/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php index 06f6c3be9..414a5d85b 100644 --- a/Server/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php +++ b/Server/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/Server/application/job/MediaArchiveJob.php b/Server/application/job/MediaArchiveJob.php new file mode 100644 index 000000000..5a1e7ff6d --- /dev/null +++ b/Server/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/Server/crontab_tasks.md b/Server/crontab_tasks.md index 81def4327..b89b3043b 100644 --- a/Server/crontab_tasks.md +++ b/Server/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 diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx index d9840279b..52e4145f7 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx @@ -18,10 +18,10 @@ const parseAudioUrl = (audioUrl: string): AudioData => { try { // 尝试解析为JSON const parsed = JSON.parse(audioUrl); - if (parsed.url) { + if (parsed.url || parsed.ossUrl) { return { durationMs: parsed.durationMs, - url: parsed.url, + url: parsed.ossUrl || parsed.url, text: parsed.text, }; } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/FileMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/FileMessage/index.tsx index afc769df4..625a6cd81 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/FileMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/FileMessage/index.tsx @@ -226,20 +226,22 @@ export const FileMessage: React.FC<FileMessageProps> = ({ // 提取文件信息 const { url = "", + ossUrl = "", title, fileName, filename, fileext, isDownloading = false, } = fileMessageData; + const resolvedUrl = ossUrl || url; // 解析文件名(优先级:title > fileName > filename > URL中提取) const resolvedFileName = title || fileName || filename || - (typeof url === "string" && url - ? url.split("/").pop()?.split("?")[0] + (typeof resolvedUrl === "string" && resolvedUrl + ? resolvedUrl.split("/").pop()?.split("?")[0] : "") || "文件"; @@ -278,7 +280,7 @@ export const FileMessage: React.FC<FileMessageProps> = ({ // 判断是否有可用的文件URL const isUrlAvailable = - typeof url === "string" && url.trim().length > 0; + typeof resolvedUrl === "string" && resolvedUrl.trim().length > 0; // 文件下载处理函数 const handleFileDownload = () => { @@ -305,7 +307,7 @@ export const FileMessage: React.FC<FileMessageProps> = ({ event.stopPropagation(); if (isUrlAvailable) { try { - window.open(url, "_blank"); + window.open(resolvedUrl, "_blank"); } catch (e) { console.error("文件打开失败:", e); } @@ -320,7 +322,7 @@ export const FileMessage: React.FC<FileMessageProps> = ({ className={styles.fileCard} onClick={() => { if (isUrlAvailable) { - window.open(url, "_blank"); + window.open(resolvedUrl, "_blank"); } else if (!isDownloading) { handleFileDownload(); } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx index a2c5252b9..c328b2872 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx @@ -88,34 +88,36 @@ const VideoMessage: React.FC<VideoMessageProps> = ({ videoData && typeof videoData === "object" && videoData.previewImage && - videoData.tencentUrl + (videoData.tencentUrl || videoData.videoUrl || videoData.ossUrl) ) { const previewImageUrl = String(videoData.previewImage).replace( /[`"']/g, "", ); + const resolvedVideoUrl = + videoData.videoUrl || videoData.ossUrl || videoData.tencentUrl; // 创建点击处理函数 const handlePlayClick = (e: React.MouseEvent, msg: ChatRecord) => { e.stopPropagation(); // 如果没有视频URL且不在加载中,则发起下载请求 - if (!videoData.videoUrl && !videoData.isLoading) { + if (!resolvedVideoUrl && !videoData.isLoading) { handleVideoPlayRequest(videoData.tencentUrl, msg.id); } }; // 如果已有视频URL,显示视频播放器 - if (videoData.videoUrl) { + if (videoData.videoUrl || videoData.ossUrl) { return ( <div className={styles.videoMessage}> <div className={styles.videoContainer}> <video controls - src={videoData.videoUrl} + src={resolvedVideoUrl} style={{ maxWidth: "100%", borderRadius: "8px" }} /> <a - href={videoData.videoUrl} + href={resolvedVideoUrl} download className={styles.downloadButton} style={{ display: "flex" }} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/messageTypes/ImageMessage.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/messageTypes/ImageMessage.tsx index 173daf754..4319ec2e8 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/messageTypes/ImageMessage.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/messageTypes/ImageMessage.tsx @@ -10,6 +10,16 @@ interface ImageMessageProps { * msgType = 3 */ export const ImageMessage: React.FC<ImageMessageProps> = ({ content }) => { + let imageUrl = content; + try { + const parsed = JSON.parse(content); + if (parsed && typeof parsed === "object") { + imageUrl = parsed.ossUrl || parsed.url || parsed.originUrl || content; + } + } catch (error) { + imageUrl = content; + } + const handleImageError = (event: React.SyntheticEvent<HTMLImageElement>) => { const target = event.target as HTMLImageElement; const parent = target.parentElement; @@ -22,14 +32,14 @@ export const ImageMessage: React.FC<ImageMessageProps> = ({ content }) => { <div className={styles.messageBubble}> <div className={styles.imageMessage}> <img - src={content} + src={imageUrl} alt="图片消息" style={{ maxWidth: "200px", maxHeight: "200px", borderRadius: "8px", }} - onClick={() => window.open(content, "_blank")} + onClick={() => window.open(imageUrl, "_blank")} onError={handleImageError} /> </div> diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx index b9429fe1e..df00aa8b4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx @@ -33,7 +33,11 @@ export const FriendCard: React.FC<FriendCardProps> = ({ formatTime, }) => { const content = monent?.momentEntity?.content || ""; - const images = monent?.momentEntity?.resUrls || []; + const mediaUrls = monent?.momentEntity?.resUrls || []; + const isVideoUrl = (url: string) => + /\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i.test(url || ""); + const videos = mediaUrls.filter((url: string) => isVideoUrl(url)); + const images = mediaUrls.filter((url: string) => !isVideoUrl(url)); const time = formatTime(monent.createTime); const likesCount = monent?.likeList?.length || 0; const commentsCount = monent?.commentList?.length || 0; @@ -153,6 +157,19 @@ export const FriendCard: React.FC<FriendCardProps> = ({ <div className={styles.itemContent}> <div className={styles.contentText}>{content}</div> + {videos.length > 0 && ( + <div className={styles.imageContainer}> + {videos.map((video, index) => ( + <video + key={`video-${index}`} + src={video} + controls + className={styles.contentImage} + style={{ background: "#000" }} + /> + ))} + </div> + )} {images && images.length > 0 && ( <div className={styles.imageContainer}> {images.map((image, index) => ( diff --git a/Touchkebao/src/store/module/websocket/msgManage.ts b/Touchkebao/src/store/module/websocket/msgManage.ts index 2ba36b3a1..963a60d8e 100644 --- a/Touchkebao/src/store/module/websocket/msgManage.ts +++ b/Touchkebao/src/store/module/websocket/msgManage.ts @@ -35,6 +35,7 @@ const getWeChatStoreMethods = () => { updateMessage: state.updateMessage, updateMomentCommonLoading: state.updateMomentCommonLoading, addMomentCommon: state.addMomentCommon, + setVideoUrl: state.setVideoUrl, setFileDownloadUrl: state.setFileDownloadUrl, setFileDownloading: state.setFileDownloading, }; @@ -508,9 +509,24 @@ const messageHandlers: Record<string, MessageHandler> = { }, CmdDownloadVideoResult: message => { - // 在这里添加具体的处理逻辑 + const { setVideoUrl } = getWeChatStoreMethods(); + const messageId = message.friendMessageId || message.chatroomMessageId; + console.log("视频下载结果:", message); - // setVideoUrl(message.friendMessageId, message.url); + if (!messageId || !message.url) { + return; + } + + setVideoUrl(messageId, message.url); + dataProcessing({ + type: "CmdDownloadVideoResult", + wechatAccountId: message.wechatAccountId || 1, + friendMessageId: message.friendMessageId, + chatroomMessageId: message.chatroomMessageId, + url: message.url, + }).catch(error => { + console.error("回写视频下载地址失败:", error); + }); }, CmdDownloadFileResult: message => { const { setFileDownloadUrl, setFileDownloading } = getWeChatStoreMethods(); @@ -528,6 +544,15 @@ const messageHandlers: Record<string, MessageHandler> = { } setFileDownloadUrl(messageId, message.url); + dataProcessing({ + type: "CmdDownloadFileResult", + wechatAccountId: message.wechatAccountId || 1, + friendMessageId: message.friendMessageId, + chatroomMessageId: message.chatroomMessageId, + url: message.url, + }).catch(error => { + console.error("回写文件下载地址失败:", error); + }); }, CmdFetchMomentResult: message => {