Compare commits

...

3 Commits

Author SHA1 Message Date
glz
a8955953c4 refactor: 优化项目配置、实体类和OSS服务
1. 调整RabbitMQ队列配置为新的业务队列
2. 修正Book实体类的列名映射为驼峰命名
3. 重构OSS服务的复制方法,支持自定义目标路径
4. 完善BookCreatedHandler,动态获取路由键并统一文件存储路径
2026-05-21 18:04:00 +08:00
glz
1a4087c27e chore: 修改配置文件中OSS项目名称为QuestionBank
将appsettings.json里的ProjectName从scs更新为QuestionBank,对齐业务项目命名
2026-05-20 17:36:16 +08:00
glz
dcfdf18e85 refactor: 重构书籍实体与消息模型,新增OSS文件复制功能
1. 更新Book实体的Source字段为SourceId并修改数据库列映射
2. 修正BookCreatedMessage的中文注释与字段类型
3. 新增阿里云OSS配置类、OssService实现与接口
4. 新增OtherSystem实体用于存储外部系统配置
5. 在BookCreatedHandler中添加OSS文件跨桶复制逻辑
6. 新增Aliyun.OSS.SDK.NetCore依赖包
7. 配置OSS服务依赖注入
2026-05-20 15:53:32 +08:00
10 changed files with 263 additions and 50 deletions

View 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;
}

View File

@ -13,6 +13,11 @@ namespace QuestionLibraryMQConsumer.Entities
[SugarTable("book")] [SugarTable("book")]
public partial class Book : SqlSugarBaseEntity public partial class Book : SqlSugarBaseEntity
{ {
public Book()
{
}
/// <summary> /// <summary>
/// Desc:主键id /// Desc:主键id
/// Default: /// Default:
@ -34,7 +39,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
[SugarColumn(ColumnName = "totalpage")] [SugarColumn(ColumnName = "totalPage")]
public int TotalPage { get; set; } public int TotalPage { get; set; }
/// <summary> /// <summary>
@ -42,7 +47,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
[SugarColumn(ColumnName = "verifypage")] [SugarColumn(ColumnName = "verifyPage")]
public int VerifyPage { get; set; } public int VerifyPage { get; set; }
/// <summary> /// <summary>
@ -58,7 +63,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "pdfurl")] [SugarColumn(ColumnName = "pdfUrl")]
public string PdfUrl { get; set; } public string PdfUrl { get; set; }
/// <summary> /// <summary>
@ -106,7 +111,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "backcover")] [SugarColumn(ColumnName = "backCover")]
public string BackCover { get; set; } public string BackCover { get; set; }
/// <summary> /// <summary>
@ -114,7 +119,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "pdfpreviewurl")] [SugarColumn(ColumnName = "pdfPreviewUrl")]
public string PdfPreviewUrl { get; set; } public string PdfPreviewUrl { get; set; }
/// <summary> /// <summary>
@ -130,7 +135,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "downloadbookpagepdfname")] [SugarColumn(ColumnName = "downloadBookPagePdfName")]
public string Downloadbookpagepdfname { get; set; } public string Downloadbookpagepdfname { get; set; }
/// <summary> /// <summary>
@ -138,7 +143,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default:b'0' /// Default:b'0'
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
[SugarColumn(ColumnName = "is_deleted")] [SugarColumn(ColumnName = "isDeleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
/// <summary> /// <summary>
@ -146,7 +151,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "created_by")] [SugarColumn(ColumnName = "createdBy")]
public long? CreatedBy { get; set; } public long? CreatedBy { get; set; }
/// <summary> /// <summary>
@ -154,7 +159,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
[SugarColumn(ColumnName = "created_time")] [SugarColumn(ColumnName = "createdTime")]
public DateTime CreatedTime { get; set; } public DateTime CreatedTime { get; set; }
/// <summary> /// <summary>
@ -162,7 +167,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "updated_by")] [SugarColumn(ColumnName = "updatedBy")]
public long? UpdatedBy { get; set; } public long? UpdatedBy { get; set; }
/// <summary> /// <summary>
@ -170,7 +175,7 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "updated_time")] [SugarColumn(ColumnName = "updatedTime")]
public DateTime? UpdatedTime { get; set; } public DateTime? UpdatedTime { get; set; }
/// <summary> /// <summary>
@ -194,7 +199,8 @@ namespace QuestionLibraryMQConsumer.Entities
/// Default: /// Default:
/// Nullable:True /// Nullable:True
/// </summary> /// </summary>
[SugarColumn(ColumnName = "source")] [SugarColumn(ColumnName = "sourceId")]
public string Source { get; set; } public long SourceId { get; set; }
} }
} }

View 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
{
/// <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;
}
}

View File

@ -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>();

View File

@ -1,10 +1,14 @@
using QuestionLibraryMQConsumer.Data; using Microsoft.Extensions.Options;
using QuestionLibraryMQConsumer.Configuration;
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;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,23 +24,33 @@ 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;
private readonly string _routingKey;
public string RoutingKey => "questionLibrary.book.created"; public string RoutingKey => _routingKey;
public Type MessageType => typeof(BookCreatedMessage); public Type MessageType => typeof(BookCreatedMessage);
public bool RequiresManualAck => false; public bool RequiresManualAck => false;
public BookCreatedHandler( public BookCreatedHandler(
ILogger<BookCreatedHandler> logger, ILogger<BookCreatedHandler> logger,
QuestionLibraryDb db) QuestionLibraryDb db,
IOssService ossService,
IOptions<RabbitMqOptions> rabbitMqOptions)
{ {
_logger = logger; _logger = logger;
_db = db; _db = db;
_ossService = ossService;
var handlerTypeName = nameof(BookCreatedHandler);
_routingKey = rabbitMqOptions.Value.Queues
.FirstOrDefault(q => q.HandlerType == handlerTypeName)
?.RoutingKey ?? string.Empty;
} }
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);
if (message == null) if (message == null)
@ -63,6 +77,51 @@ 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))
{
var pdfExt = Path.GetExtension(pdfUrl);
copyTasks.Add(CopyUrlAsync("PdfUrl", pdfUrl, $"book/{message.BookId}/book{pdfExt}", otherSystem, newUrl => pdfUrl = newUrl, cancellationToken));
}
if (!string.IsNullOrEmpty(coverUrl))
{
var coverExt = Path.GetExtension(coverUrl);
copyTasks.Add(CopyUrlAsync("CoverUrl", coverUrl, $"book/{message.BookId}/cover{coverExt}", otherSystem, newUrl => coverUrl = newUrl, cancellationToken));
}
if (!string.IsNullOrEmpty(backCoverUrl))
{
var backCoverExt = Path.GetExtension(backCoverUrl);
copyTasks.Add(CopyUrlAsync("BackCoverUrl", backCoverUrl, $"book/{message.BookId}/backCover{backCoverExt}", 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 +129,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 +143,21 @@ namespace QuestionLibraryMQConsumer.Handlers
_logger.LogInformation("书籍 {BookId} 创建成功", message.BookId); _logger.LogInformation("书籍 {BookId} 创建成功", message.BookId);
} }
private async Task CopyUrlAsync(string fieldName, string originalPath, string targetPath, OtherSystem otherSystem, Action<string> setNewPath, CancellationToken cancellationToken)
{
try
{
var newPath = await _ossService.CopyToOwnBucketAsync(originalPath, otherSystem.BucketName, targetPath, 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);

View File

@ -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; }
} }

View File

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

View 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, string targetPath, CancellationToken cancellationToken = default);
}

View 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, string targetPath, 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, targetPath);
var result = client.CopyObject(request);
_logger.LogInformation("OSS跨bucket复制成功: {SourceBucket}/{ObjectKey} -> {TargetBucket}/{TargetKey}, ETag: {ETag}",
sourceBucket, objectKey, _ossOptions.BucketName, targetPath, result.ETag);
return targetPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "OSS跨bucket复制失败: {SourcePath}", sourcePath);
return sourcePath; // 失败时返回原始路径
}
}
}

View File

@ -10,25 +10,10 @@
"PrefetchCount": 10, "PrefetchCount": 10,
"Queues": [ "Queues": [
{ {
"QueueName": "questionLibrary.book.created.queue", "QueueName": "mq.book.push",
"ExchangeName": "icr.direct", "ExchangeName": "ex.book.direct",
"RoutingKey": "questionLibrary.book.created", "RoutingKey": "rk.book.push",
"Durable": true, "HandlerType": "BookCreatedHandler",
"Exclusive": false,
"AutoDelete": false
},
{
"QueueName": "questionLibrary.book.created.queue",
"ExchangeName": "question.library.exchange.dev",
"RoutingKey": "questionLibrary.book.created",
"Durable": true,
"Exclusive": false,
"AutoDelete": false
},
{
"QueueName": "questionLibrary.book.created.queue",
"ExchangeName": "question.library.exchange",
"RoutingKey": "questionLibrary.book.created",
"Durable": true, "Durable": true,
"Exclusive": false, "Exclusive": false,
"AutoDelete": false "AutoDelete": false
@ -52,5 +37,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": "QuestionBank",
"Domain": "https://qyzh-qb.oss-cn-beijing.aliyuncs.com/"
} }
} }