feat(media): 微信消息媒体 OSS 归档与下载地址回写

- 新增 MediaArchiveJob、MediaOssArchiveService、WechatMediaArchiveService 与回填命令
- Message/DataProcessing 等支持归档调度与持久化下载 URL
- WebSocket 控制器整理;朋友圈与文档/定时任务说明更新

Made-with: Cursor
This commit is contained in:
wong
2026-04-14 09:37:00 +08:00
parent ae1120f1e3
commit 1deebf3f5c
19 changed files with 1046 additions and 151 deletions

View File

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