using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
using Microsoft.Extensions.Logging;
namespace Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
///
/// 出站分发:扫描 mdp_outbox,按 source_type 路由到 API / DB 执行器。
///
public sealed class MdpTargetPushDispatcher : ITransient
{
public const int DefaultTake = 500;
private const int MaxRetry = 3;
private readonly ISqlSugarClient _db;
private readonly MdpApiPushExecutor _api;
private readonly MdpDbPushExecutor _dbPush;
private readonly ILogger _logger;
public MdpTargetPushDispatcher(
ISqlSugarClient db,
MdpApiPushExecutor api,
MdpDbPushExecutor dbPush,
ILoggerFactory loggerFactory)
{
_db = db;
_api = api;
_dbPush = dbPush;
_logger = loggerFactory.CreateLogger(nameof(MdpTargetPushDispatcher));
}
public async Task<(int success, int failed, int skipped)> PushPendingAsync(
int take = DefaultTake, CancellationToken cancellationToken = default)
{
var pending = await _db.Queryable()
.Where(x => x.Status == 0 && x.RetryCount < MaxRetry)
.OrderBy(x => x.Id)
.Take(take > 0 ? take : DefaultTake)
.ToListAsync(cancellationToken);
var success = 0;
var failed = 0;
var skipped = 0;
foreach (var item in pending)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var sources = await _db.Queryable()
.Where(x => x.SourceCode == item.TargetSourceCode && x.Status == 1)
.Take(1)
.ToListAsync(cancellationToken);
var source = sources.FirstOrDefault();
if (source == null)
{
await MarkAsync(item, 2, null, $"目标源 {item.TargetSourceCode} 未启用", cancellationToken);
failed++;
continue;
}
var executor = Resolve(source);
_logger.LogInformation(
"[MdpTargetPushDispatcher] outbox id={Id} source={Source} type={SourceType} via={Executor}",
item.Id, source.SourceCode, source.SourceType, executor.SupportedType);
var result = await executor.PushAsync(source, item, cancellationToken);
if (result.Success)
{
await MarkAsync(item, 1, result.ResponseJson, null, cancellationToken);
success++;
}
else
{
item.RetryCount++;
var status = item.RetryCount >= MaxRetry ? 2 : 0;
await MarkAsync(item, status, result.ResponseJson, Truncate(result.ErrorMessage, 900), cancellationToken);
if (status == 2) failed++; else skipped++;
}
}
catch (Exception ex)
{
item.RetryCount++;
var status = item.RetryCount >= MaxRetry ? 2 : 0;
await MarkAsync(item, status, null, Truncate(ex.Message, 900), cancellationToken);
if (status == 2) failed++; else skipped++;
_logger.LogWarning(ex, "[MdpTargetPushDispatcher] outbox id={Id} failed", item.Id);
}
}
return (success, failed, skipped);
}
private IMdpTargetPushExecutor Resolve(MdpSource source)
{
var t = (source.SourceType ?? "").Trim().ToUpperInvariant();
if (t is "DB" or "SQLSERVER" or "MYSQL" or "DATABASE")
return _dbPush;
if (t == "API" || t == "HTTP")
return _api;
// 未标注类型时:有库连接信息则走 DB,否则走 API(兼容历史 HTTP 源)
if (!string.IsNullOrWhiteSpace(source.DbHost) && !string.IsNullOrWhiteSpace(source.DbName))
return _dbPush;
return _api;
}
private async Task MarkAsync(MdpOutbox item, int status, string? responseJson, string? error, CancellationToken ct)
{
item.Status = status;
item.ResponseJson = Truncate(responseJson, 4000);
item.ErrorMsg = error;
item.UpdateTime = DateTime.Now;
await _db.Updateable(item)
.UpdateColumns(x => new { x.Status, x.RetryCount, x.ResponseJson, x.ErrorMsg, x.UpdateTime })
.ExecuteCommandAsync(ct);
}
private static string? Truncate(string? s, int max) =>
string.IsNullOrEmpty(s) ? s : (s.Length <= max ? s : s[..max]);
}