WorkCamera/client/WorkCameraExport/Services/CosService.cs
2026-01-06 22:23:29 +08:00

268 lines
9.4 KiB
C#

using COSXML;
using COSXML.Auth;
using COSXML.Model.Object;
using COSXML.Transfer;
using WorkCameraExport.Models;
using WorkCameraExport.Services.Interfaces;
namespace WorkCameraExport.Services
{
/// <summary>
/// COS 服务类 - 处理腾讯云 COS 图片上传
/// </summary>
public class CosService : IDisposable
{
private CosXml? _cosXml;
private CosTempCredentials? _credentials;
private readonly ILogService? _logService;
private bool _disposed;
public CosService(ILogService? logService = null)
{
_logService = logService;
}
/// <summary>
/// 初始化 COS 客户端
/// </summary>
public void Initialize(CosTempCredentials credentials)
{
_credentials = credentials;
_logService?.Info($"[COS] 初始化, Region={credentials.Region}, Bucket={credentials.Bucket}");
var config = new CosXmlConfig.Builder()
.IsHttps(true)
.SetRegion(credentials.Region)
.SetDebugLog(false)
.Build();
var credentialProvider = new DefaultSessionQCloudCredentialProvider(
credentials.TmpSecretId,
credentials.TmpSecretKey,
credentials.ExpiredTime,
credentials.SessionToken);
_cosXml = new CosXmlServer(config, credentialProvider);
_logService?.Info("[COS] 初始化完成");
}
/// <summary>
/// 检查是否已初始化
/// </summary>
public bool IsInitialized => _cosXml != null && _credentials != null;
/// <summary>
/// 上传图片到 COS
/// </summary>
/// <param name="localFilePath">本地文件路径</param>
/// <param name="cosKey">COS 对象键(路径)</param>
/// <param name="progress">进度回调</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>上传结果</returns>
public async Task<(bool Success, string Message, string? CosUrl)> UploadFileAsync(
string localFilePath,
string cosKey,
Action<long, long>? progress = null,
CancellationToken cancellationToken = default)
{
if (!IsInitialized || _cosXml == null || _credentials == null)
{
_logService?.Error("[COS] 上传失败: 服务未初始化");
return (false, "COS 服务未初始化", null);
}
_logService?.Info($"[COS] 开始上传文件: {localFilePath} -> {cosKey}");
try
{
var transferConfig = new TransferConfig();
var transferManager = new TransferManager(_cosXml, transferConfig);
var uploadTask = new COSXMLUploadTask(_credentials.Bucket, cosKey);
uploadTask.SetSrcPath(localFilePath);
if (progress != null)
{
uploadTask.progressCallback = (completed, total) =>
{
progress(completed, total);
};
}
// 使用新的异步 API
var result = await transferManager.UploadAsync(uploadTask);
if (result.IsSuccessful())
{
var cosUrl = $"https://{_credentials.Bucket}.cos.{_credentials.Region}.myqcloud.com/{cosKey}";
_logService?.Info($"[COS] 上传成功: {cosUrl}");
return (true, "上传成功", cosUrl);
}
else
{
_logService?.Warn($"[COS] 上传失败: result.IsSuccessful()=false");
return (false, "上传失败", null);
}
}
catch (OperationCanceledException)
{
_logService?.Info("[COS] 上传已取消");
return (false, "上传已取消", null);
}
catch (COSXML.CosException.CosClientException clientEx)
{
_logService?.Error($"[COS] 客户端异常: {clientEx.Message}", clientEx);
return (false, $"客户端异常: {clientEx.Message}", null);
}
catch (COSXML.CosException.CosServerException serverEx)
{
_logService?.Error($"[COS] 服务端异常: {serverEx.GetInfo()}");
return (false, $"服务端异常: {serverEx.GetInfo()}", null);
}
catch (Exception ex)
{
_logService?.Error($"[COS] 上传异常: {ex.Message}", ex);
return (false, $"上传异常: {ex.Message}", null);
}
}
/// <summary>
/// 上传字节数组到 COS
/// </summary>
public async Task<(bool Success, string Message, string? CosUrl)> UploadBytesAsync(
byte[] data,
string cosKey,
Action<long, long>? progress = null,
CancellationToken cancellationToken = default)
{
if (!IsInitialized || _cosXml == null || _credentials == null)
{
return (false, "COS 服务未初始化", null);
}
// 创建临时文件
var tempFile = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempFile, data, cancellationToken);
return await UploadFileAsync(tempFile, cosKey, progress, cancellationToken);
}
finally
{
// 清理临时文件
try
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
catch
{
// 忽略清理错误
}
}
}
/// <summary>
/// 批量上传图片
/// </summary>
/// <param name="uploadTasks">上传任务列表 (本地路径, COS键)</param>
/// <param name="maxConcurrency">最大并发数</param>
/// <param name="progress">进度回调 (已完成数, 总数)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>上传结果列表</returns>
public async Task<List<(string LocalPath, bool Success, string? CosUrl, string Message)>>
BatchUploadAsync(
List<(string LocalPath, string CosKey)> uploadTasks,
int maxConcurrency = 5,
Action<int, int>? progress = null,
CancellationToken cancellationToken = default)
{
var results = new List<(string LocalPath, bool Success, string? CosUrl, string Message)>();
var semaphore = new SemaphoreSlim(maxConcurrency);
var completed = 0;
var total = uploadTasks.Count;
var tasks = uploadTasks.Select(async task =>
{
await semaphore.WaitAsync(cancellationToken);
try
{
var result = await UploadFileAsync(task.LocalPath, task.CosKey,
cancellationToken: cancellationToken);
lock (results)
{
results.Add((task.LocalPath, result.Success, result.CosUrl, result.Message));
completed++;
progress?.Invoke(completed, total);
}
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
return results;
}
/// <summary>
/// 生成 COS 对象键(路径)
/// </summary>
/// <param name="recordTime">记录时间</param>
/// <param name="category">分类(当日照片/参与人员/工作内容/部门)</param>
/// <param name="subCategory">子分类</param>
/// <param name="fileName">文件名</param>
/// <returns>COS 对象键</returns>
public static string GenerateCosKey(DateTime recordTime, string category,
string subCategory, string fileName)
{
var yearMonth = recordTime.ToString("yyyyMM");
var date = recordTime.ToString("yyyyMMdd");
if (string.IsNullOrEmpty(subCategory))
{
return $"workfiles/{yearMonth}/{date}/{category}/{fileName}";
}
return $"workfiles/{yearMonth}/{date}/{category}/{subCategory}/{fileName}";
}
/// <summary>
/// 生成唯一文件名
/// </summary>
public static string GenerateFileName(string extension = ".jpg")
{
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff");
var random = new Random().Next(1000, 9999);
return $"{timestamp}_{random}{extension}";
}
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_cosXml = null;
_credentials = null;
}
_disposed = true;
}
}
#endregion
}
}