refactor: 重构书籍实体与消息模型,新增OSS文件复制功能
1. 更新Book实体的Source字段为SourceId并修改数据库列映射 2. 修正BookCreatedMessage的中文注释与字段类型 3. 新增阿里云OSS配置类、OssService实现与接口 4. 新增OtherSystem实体用于存储外部系统配置 5. 在BookCreatedHandler中添加OSS文件跨桶复制逻辑 6. 新增Aliyun.OSS.SDK.NetCore依赖包 7. 配置OSS服务依赖注入
This commit is contained in:
17
QuestionLibraryMQConsumer/Configuration/AliyunOSSOptions.cs
Normal file
17
QuestionLibraryMQConsumer/Configuration/AliyunOSSOptions.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace QuestionLibraryMQConsumer.Configuration;
|
||||||
|
|
||||||
|
public class AliyunOSSOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "AliyunOSSConfigs";
|
||||||
|
|
||||||
|
public string AccessKeyID { get; set; } = string.Empty;
|
||||||
|
public string AccessKeySecret { get; set; } = string.Empty;
|
||||||
|
public string VodBucketName { get; set; } = string.Empty;
|
||||||
|
public string BucketName { get; set; } = string.Empty;
|
||||||
|
public string Region { get; set; } = string.Empty;
|
||||||
|
public string RoleArn { get; set; } = string.Empty;
|
||||||
|
public int DurationSeconds { get; set; }
|
||||||
|
public string Endpoint { get; set; } = string.Empty;
|
||||||
|
public string ProjectName { get; set; } = string.Empty;
|
||||||
|
public string Domain { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -194,7 +194,7 @@ namespace QuestionLibraryMQConsumer.Entities
|
|||||||
/// Default:
|
/// Default:
|
||||||
/// Nullable:True
|
/// Nullable:True
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(ColumnName = "source")]
|
[SugarColumn(ColumnName = "source_id")]
|
||||||
public string Source { get; set; }
|
public long SourceId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
QuestionLibraryMQConsumer/Entities/OtherSystem.cs
Normal file
50
QuestionLibraryMQConsumer/Entities/OtherSystem.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace QuestionLibraryMQConsumer.Entities
|
||||||
|
{
|
||||||
|
///<summary>
|
||||||
|
///其他系统
|
||||||
|
///</summary>
|
||||||
|
[SugarTable("other_system")]
|
||||||
|
public partial class OtherSystem : SqlSugarBaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:主键id
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsPrimaryKey = true, ColumnName = "id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:Code
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "code")]
|
||||||
|
public string Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:请求域名
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "domain")]
|
||||||
|
public string Domain { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:桶名称
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "bucket_name")]
|
||||||
|
public string BucketName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ using QuestionLibraryMQConsumer.Configuration;
|
|||||||
using QuestionLibraryMQConsumer.Consumers;
|
using QuestionLibraryMQConsumer.Consumers;
|
||||||
using QuestionLibraryMQConsumer.Data;
|
using QuestionLibraryMQConsumer.Data;
|
||||||
using QuestionLibraryMQConsumer.Handlers;
|
using QuestionLibraryMQConsumer.Handlers;
|
||||||
|
using QuestionLibraryMQConsumer.Services;
|
||||||
|
|
||||||
namespace QuestionLibraryMQConsumer.Extensions;
|
namespace QuestionLibraryMQConsumer.Extensions;
|
||||||
|
|
||||||
@ -19,6 +20,9 @@ public static class ServiceCollectionExtensions
|
|||||||
var connectionString = configuration.GetConnectionString("zybDb") ?? throw new InvalidOperationException("Connection string 'zybDb' is not configured");
|
var connectionString = configuration.GetConnectionString("zybDb") ?? throw new InvalidOperationException("Connection string 'zybDb' is not configured");
|
||||||
services.AddSingleton<QuestionLibraryDb>(new QuestionLibraryDb(connectionString));
|
services.AddSingleton<QuestionLibraryDb>(new QuestionLibraryDb(connectionString));
|
||||||
|
|
||||||
|
services.Configure<AliyunOSSOptions>(configuration.GetSection(AliyunOSSOptions.SectionName));
|
||||||
|
services.AddSingleton<IOssService, OssService>();
|
||||||
|
|
||||||
services.AddSingleton<MessageHandlerRegistry>();
|
services.AddSingleton<MessageHandlerRegistry>();
|
||||||
|
|
||||||
services.AddTransient<IMessageHandler, BookCreatedHandler>();
|
services.AddTransient<IMessageHandler, BookCreatedHandler>();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using QuestionLibraryMQConsumer.Data;
|
using QuestionLibraryMQConsumer.Data;
|
||||||
using QuestionLibraryMQConsumer.Entities;
|
using QuestionLibraryMQConsumer.Entities;
|
||||||
using QuestionLibraryMQConsumer.Models;
|
using QuestionLibraryMQConsumer.Models;
|
||||||
|
using QuestionLibraryMQConsumer.Services;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -20,6 +21,7 @@ namespace QuestionLibraryMQConsumer.Handlers
|
|||||||
|
|
||||||
private readonly ILogger<BookCreatedHandler> _logger;
|
private readonly ILogger<BookCreatedHandler> _logger;
|
||||||
private readonly QuestionLibraryDb _db;
|
private readonly QuestionLibraryDb _db;
|
||||||
|
private readonly IOssService _ossService;
|
||||||
|
|
||||||
public string RoutingKey => "questionLibrary.book.created";
|
public string RoutingKey => "questionLibrary.book.created";
|
||||||
public Type MessageType => typeof(BookCreatedMessage);
|
public Type MessageType => typeof(BookCreatedMessage);
|
||||||
@ -27,15 +29,17 @@ namespace QuestionLibraryMQConsumer.Handlers
|
|||||||
|
|
||||||
public BookCreatedHandler(
|
public BookCreatedHandler(
|
||||||
ILogger<BookCreatedHandler> logger,
|
ILogger<BookCreatedHandler> logger,
|
||||||
QuestionLibraryDb db)
|
QuestionLibraryDb db,
|
||||||
|
IOssService ossService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_ossService = ossService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleJsonAsync(string messageJson, CancellationToken cancellationToken)
|
public async Task HandleJsonAsync(string messageJson, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[Handler入口] HandleJsonAsync被调用, messageJson长度: {Length}", messageJson?.Length ?? 0);
|
_logger.LogInformation("[Handler入口] HandleJsonAsync被调用,messageJson长度:{msg} messageJson长度: {Length}", messageJson, messageJson?.Length ?? 0);
|
||||||
|
|
||||||
var message = JsonSerializer.Deserialize<BookCreatedMessage>(messageJson, _jsonOptions);
|
var message = JsonSerializer.Deserialize<BookCreatedMessage>(messageJson, _jsonOptions);
|
||||||
|
|
||||||
@ -63,6 +67,48 @@ namespace QuestionLibraryMQConsumer.Handlers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询外部系统配置获取源bucket信息
|
||||||
|
var otherSystem = await _db.Db.Queryable<OtherSystem>()
|
||||||
|
.FirstAsync(s => s.Id == message.SourceId);
|
||||||
|
|
||||||
|
string pdfUrl = message.PdfUrl ?? string.Empty;
|
||||||
|
string coverUrl = message.CoverUrl ?? string.Empty;
|
||||||
|
string backCoverUrl = message.BackCoverUrl ?? string.Empty;
|
||||||
|
|
||||||
|
// 如果找到外部系统配置,执行OSS跨bucket复制
|
||||||
|
if (otherSystem != null && !string.IsNullOrEmpty(otherSystem.BucketName) && !string.IsNullOrEmpty(otherSystem.Domain))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("找到外部系统配置 SourceId: {SourceId}, Bucket: {Bucket}, Domain: {Domain}",
|
||||||
|
message.SourceId, otherSystem.BucketName, otherSystem.Domain);
|
||||||
|
|
||||||
|
// 并行复制所有URL
|
||||||
|
var copyTasks = new List<Task>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(pdfUrl))
|
||||||
|
{
|
||||||
|
copyTasks.Add(CopyUrlAsync("PdfUrl", pdfUrl, otherSystem, newUrl => pdfUrl = newUrl, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(coverUrl))
|
||||||
|
{
|
||||||
|
copyTasks.Add(CopyUrlAsync("CoverUrl", coverUrl, otherSystem, newUrl => coverUrl = newUrl, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(backCoverUrl))
|
||||||
|
{
|
||||||
|
copyTasks.Add(CopyUrlAsync("BackCoverUrl", backCoverUrl, otherSystem, newUrl => backCoverUrl = newUrl, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyTasks.Count > 0)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(copyTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("未找到外部系统配置 SourceId: {SourceId},使用原始URL", message.SourceId);
|
||||||
|
}
|
||||||
|
|
||||||
var book = new Book
|
var book = new Book
|
||||||
{
|
{
|
||||||
Id = message.BookId,
|
Id = message.BookId,
|
||||||
@ -70,12 +116,12 @@ namespace QuestionLibraryMQConsumer.Handlers
|
|||||||
Title = message.Subtitle,
|
Title = message.Subtitle,
|
||||||
Width = message.PaperWidth,
|
Width = message.PaperWidth,
|
||||||
Height = message.PaperHeight,
|
Height = message.PaperHeight,
|
||||||
Cover = message.CoverUrl,
|
Cover = coverUrl,
|
||||||
PdfUrl = message.PdfUrl,
|
PdfUrl = pdfUrl,
|
||||||
BackCover = message.BackCoverUrl,
|
BackCover = backCoverUrl,
|
||||||
CreatedTime = DateTime.UtcNow,
|
CreatedTime = DateTime.UtcNow,
|
||||||
UpdatedTime = DateTime.UtcNow,
|
UpdatedTime = DateTime.UtcNow,
|
||||||
Source = message.Source,
|
SourceId = message.SourceId,
|
||||||
Status = (int)BookStatusEnum.MissCatalog
|
Status = (int)BookStatusEnum.MissCatalog
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,6 +130,21 @@ namespace QuestionLibraryMQConsumer.Handlers
|
|||||||
_logger.LogInformation("书籍 {BookId} 创建成功", message.BookId);
|
_logger.LogInformation("书籍 {BookId} 创建成功", message.BookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CopyUrlAsync(string fieldName, string originalPath, OtherSystem otherSystem, Action<string> setNewPath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newPath = await _ossService.CopyToOwnBucketAsync(originalPath, otherSystem.BucketName, cancellationToken);
|
||||||
|
setNewPath(newPath);
|
||||||
|
_logger.LogInformation("{FieldName} OSS复制成功: {OriginalPath} -> {NewPath}", fieldName, originalPath, newPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{FieldName} OSS复制失败,使用原始路径: {OriginalPath}", fieldName, originalPath);
|
||||||
|
// 保持原始路径不变
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task HandleWithManualAckAsync(string messageJson, IChannel channel, ulong deliveryTag, CancellationToken cancellationToken)
|
public async Task HandleWithManualAckAsync(string messageJson, IChannel channel, ulong deliveryTag, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var message = JsonSerializer.Deserialize<BookCreatedMessage>(messageJson, _jsonOptions);
|
var message = JsonSerializer.Deserialize<BookCreatedMessage>(messageJson, _jsonOptions);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ namespace QuestionLibraryMQConsumer.Models;
|
|||||||
public class BookCreatedMessage
|
public class BookCreatedMessage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <EFBFBD><EFBFBD>ϢId
|
/// 消息Id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MessageId { get; set; } = string.Empty;
|
public string MessageId { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -15,31 +15,31 @@ public class BookCreatedMessage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string BookName { get; set; } = string.Empty;
|
public string BookName { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
/// 副标题
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Subtitle { get; set; } = string.Empty;
|
public string Subtitle { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ֽ<EFBFBD>ſ<EFBFBD><EFBFBD><EFBFBD>
|
/// 纸张宽度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float PaperWidth { get; set; }
|
public float PaperWidth { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ֽ<EFBFBD>Ÿ߶<EFBFBD>
|
/// 纸张高度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float PaperHeight { get; set; }
|
public float PaperHeight { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
/// 封面
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CoverUrl { get; set; }
|
public string? CoverUrl { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <EFBFBD><EFBFBD><EFBFBD>
|
/// 封底
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? BackCoverUrl { get; set; }
|
public string? BackCoverUrl { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <EFBFBD>鼮PDF
|
/// 书籍PDF
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PdfUrl { get; set; }
|
public string? PdfUrl { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ԴʶжϻشMQϢַ
|
/// 来源识别判断回MQ消息地址
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Source { get; set; }
|
public long SourceId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,5 +12,6 @@
|
|||||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
|
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.214" />
|
<PackageReference Include="SqlSugarCore" Version="5.1.4.214" />
|
||||||
|
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
13
QuestionLibraryMQConsumer/Services/IOssService.cs
Normal file
13
QuestionLibraryMQConsumer/Services/IOssService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace QuestionLibraryMQConsumer.Services;
|
||||||
|
|
||||||
|
public interface IOssService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将文件从外部bucket复制到自己的bucket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourcePath">源文件相对路径(如 book/807248715010117/book.pdf)</param>
|
||||||
|
/// <param name="sourceBucket">源bucket名称</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>复制后的新URL</returns>
|
||||||
|
Task<string> CopyToOwnBucketAsync(string sourcePath, string sourceBucket, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
51
QuestionLibraryMQConsumer/Services/OssService.cs
Normal file
51
QuestionLibraryMQConsumer/Services/OssService.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using Aliyun.OSS;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using QuestionLibraryMQConsumer.Configuration;
|
||||||
|
|
||||||
|
namespace QuestionLibraryMQConsumer.Services;
|
||||||
|
|
||||||
|
public class OssService : IOssService
|
||||||
|
{
|
||||||
|
private readonly ILogger<OssService> _logger;
|
||||||
|
private readonly AliyunOSSOptions _ossOptions;
|
||||||
|
|
||||||
|
public OssService(
|
||||||
|
ILogger<OssService> logger,
|
||||||
|
IOptions<AliyunOSSOptions> ossOptions)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_ossOptions = ossOptions.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> CopyToOwnBucketAsync(string sourcePath, string sourceBucket, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sourcePath))
|
||||||
|
{
|
||||||
|
return sourcePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// sourcePath 是相对路径,直接作为 object key 使用
|
||||||
|
var objectKey = sourcePath.TrimStart('/');
|
||||||
|
|
||||||
|
// 创建OSS客户端
|
||||||
|
var client = new OssClient(_ossOptions.Endpoint, _ossOptions.AccessKeyID, _ossOptions.AccessKeySecret);
|
||||||
|
|
||||||
|
// 执行跨bucket复制
|
||||||
|
var request = new CopyObjectRequest(sourceBucket, objectKey, _ossOptions.BucketName, objectKey);
|
||||||
|
var result = client.CopyObject(request);
|
||||||
|
|
||||||
|
_logger.LogInformation("OSS跨bucket复制成功: {SourceBucket}/{ObjectKey} -> {TargetBucket}/{TargetKey}, ETag: {ETag}",
|
||||||
|
sourceBucket, objectKey, _ossOptions.BucketName, objectKey, result.ETag);
|
||||||
|
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "OSS跨bucket复制失败: {SourcePath}", sourcePath);
|
||||||
|
return sourcePath; // 失败时返回原始路径
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -52,5 +52,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"AliyunOSSConfigs": {
|
||||||
|
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
|
||||||
|
"AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf",
|
||||||
|
"VodBucketName": "outin-5277bbb52bec11f08dbd00163e169e2b.oss-cn-beijing.aliyuncs.com",
|
||||||
|
"BucketName": "qyzh-qb",
|
||||||
|
"Region": "beijing",
|
||||||
|
"RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole",
|
||||||
|
"DurationSeconds": 3600, //过期时间(秒)
|
||||||
|
"Endpoint": "oss-cn-beijing.aliyuncs.com",
|
||||||
|
"ProjectName": "scs",
|
||||||
|
"Domain": "https://qyzh-qb.oss-cn-beijing.aliyuncs.com/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user