feat(media): 微信消息媒体 OSS 归档与下载地址回写
- 新增 MediaArchiveJob、MediaOssArchiveService、WechatMediaArchiveService 与回填命令 - Message/DataProcessing 等支持归档调度与持久化下载 URL - WebSocket 控制器整理;朋友圈与文档/定时任务说明更新 Made-with: Cursor
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ Touchkebao/.specstory/
|
||||
Serverruntime/
|
||||
Moncter/提示词/
|
||||
*.log
|
||||
*.code-workspace
|
||||
项目整体分析报告.md
|
||||
Server/docs/traffic_pool_design.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. 系统要求
|
||||
|
||||
@@ -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 原始消息内容
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -53,4 +53,5 @@ return [
|
||||
|
||||
// V2 流量池数据迁移
|
||||
'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统
|
||||
'media:archive' => 'app\command\BackfillMediaOssCommand', // 历史媒体资源归档到OSS
|
||||
];
|
||||
|
||||
121
Server/application/command/BackfillMediaOssCommand.php
Normal file
121
Server/application/command/BackfillMediaOssCommand.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\WechatMediaArchiveService;
|
||||
use app\job\MediaArchiveJob;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Option;
|
||||
use think\Db;
|
||||
|
||||
class BackfillMediaOssCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
261
Server/application/common/service/MediaOssArchiveService.php
Normal file
261
Server/application/common/service/MediaOssArchiveService.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\util\AliyunOSS;
|
||||
use think\facade\Log;
|
||||
|
||||
class MediaOssArchiveService
|
||||
{
|
||||
protected static $mimeExtensionMap = [
|
||||
'image/jpeg' => '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;
|
||||
}
|
||||
}
|
||||
372
Server/application/common/service/WechatMediaArchiveService.php
Normal file
372
Server/application/common/service/WechatMediaArchiveService.php
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
class WechatMediaArchiveService
|
||||
{
|
||||
public static function archiveMessageById($messageId)
|
||||
{
|
||||
$messageId = (int)$messageId;
|
||||
if ($messageId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = Db::table('s2_wechat_message')->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('/<title><!\[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);
|
||||
}
|
||||
}
|
||||
@@ -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字段解析
|
||||
*
|
||||
|
||||
57
Server/application/job/MediaArchiveJob.php
Normal file
57
Server/application/job/MediaArchiveJob.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user