IMdpSourcePullExecutor.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  2. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  3. /// <summary>统一入站抽数执行器(方式甲 DB / 方式乙 API)。</summary>
  4. public interface IMdpSourcePullExecutor
  5. {
  6. /// <summary>DB_SYNC / API_PULL</summary>
  7. string SupportedType { get; }
  8. Task<MdpPullResult> PullAsync(MdpSource source, MdpEntity entity, MdpPullContext ctx, CancellationToken cancellationToken = default);
  9. }
  10. public sealed class MdpPullContext
  11. {
  12. public long TenantId { get; set; } = 1300000000001L;
  13. public string BatchId { get; set; } = "";
  14. /// <summary>强制全量(忽略水位与滚动下界)。</summary>
  15. public bool FullRefresh { get; set; }
  16. /// <summary>关联 mdp_sync_task.task_code,用于读取调度上的同步窗口。</summary>
  17. public string? TaskCode { get; set; }
  18. /// <summary>FULL / INCR / ROLLING(可由调度表覆盖)。</summary>
  19. public string? SyncWindowType { get; set; }
  20. /// <summary>如 7d / 24h / 2026-01-01。</summary>
  21. public string? SyncWindowValue { get; set; }
  22. /// <summary>ROLLING 解析后的下界(含)。</summary>
  23. public DateTime? WindowFrom { get; set; }
  24. /// <summary>全量分页偏移(仅 FullRefresh / 无 incr 时由 PullAll 推进)。</summary>
  25. public int Offset { get; set; }
  26. }
  27. public sealed class MdpPullResult
  28. {
  29. public int RowsPulled { get; set; }
  30. public int RowsWritten { get; set; }
  31. public string? NewCursor { get; set; }
  32. public string? Message { get; set; }
  33. }
  34. /// <summary>按实体/源类型选择 DB 或 API 执行器。</summary>
  35. public sealed class MdpSourcePullDispatcher : ITransient
  36. {
  37. private readonly MdpDbPullExecutor _dbExecutor;
  38. private readonly MdpApiPullExecutor _apiExecutor;
  39. private readonly ISqlSugarClient _db;
  40. public MdpSourcePullDispatcher(
  41. MdpDbPullExecutor dbExecutor,
  42. MdpApiPullExecutor apiExecutor,
  43. ISqlSugarClient db)
  44. {
  45. _dbExecutor = dbExecutor;
  46. _apiExecutor = apiExecutor;
  47. _db = db;
  48. }
  49. public async Task<MdpPullResult> PullByEntityCodeAsync(string entityCode, MdpPullContext ctx, CancellationToken cancellationToken = default)
  50. {
  51. var entity = await _db.Queryable<MdpEntity>()
  52. .Where(x => x.EntityCode == entityCode && x.Status == 1)
  53. .FirstAsync(cancellationToken)
  54. ?? throw new InvalidOperationException($"mdp_entity 未找到启用实体:{entityCode}");
  55. var source = await _db.Queryable<MdpSource>()
  56. .Where(x => x.Id == entity.SourceId && x.Status == 1)
  57. .FirstAsync(cancellationToken)
  58. ?? throw new InvalidOperationException($"mdp_source id={entity.SourceId} 未找到或未启用");
  59. if (string.IsNullOrWhiteSpace(ctx.BatchId))
  60. ctx.BatchId = $"MDP_PULL_{DateTime.Now:yyyyMMddHHmmss}";
  61. await ApplyScheduleWindowAsync(ctx, cancellationToken);
  62. MdpSyncWindowResolver.Apply(ctx);
  63. IMdpSourcePullExecutor executor =
  64. !string.IsNullOrWhiteSpace(entity.SourceApiPath)
  65. || string.Equals(source.SourceType, "API", StringComparison.OrdinalIgnoreCase)
  66. ? _apiExecutor
  67. : _dbExecutor;
  68. return await executor.PullAsync(source, entity, ctx, cancellationToken);
  69. }
  70. /// <summary>
  71. /// 分页抽尽:FullRefresh 用 OFFSET;增量依赖实体 LastCursor 在页间推进。
  72. /// </summary>
  73. public async Task<MdpPullResult> PullAllByEntityCodeAsync(
  74. string entityCode,
  75. MdpPullContext ctx,
  76. CancellationToken cancellationToken = default,
  77. int maxPages = 500)
  78. {
  79. var totalPulled = 0;
  80. var totalWritten = 0;
  81. string? lastCursor = null;
  82. string? lastMessage = null;
  83. ctx.Offset = 0;
  84. for (var page = 0; page < maxPages; page++)
  85. {
  86. cancellationToken.ThrowIfCancellationRequested();
  87. var pageResult = await PullByEntityCodeAsync(entityCode, ctx, cancellationToken);
  88. totalPulled += pageResult.RowsPulled;
  89. totalWritten += pageResult.RowsWritten;
  90. lastCursor = pageResult.NewCursor ?? lastCursor;
  91. lastMessage = pageResult.Message;
  92. var entity = await _db.Queryable<MdpEntity>()
  93. .Where(x => x.EntityCode == entityCode && x.Status == 1)
  94. .FirstAsync(cancellationToken);
  95. var batchSize = entity?.BatchSize > 0 ? entity.BatchSize : 1000;
  96. if (pageResult.RowsPulled <= 0 || pageResult.RowsPulled < batchSize)
  97. break;
  98. if (ctx.FullRefresh)
  99. ctx.Offset += pageResult.RowsPulled;
  100. // 增量:实体 LastCursor 已在执行器内更新,下一页自动收窄
  101. }
  102. return new MdpPullResult
  103. {
  104. RowsPulled = totalPulled,
  105. RowsWritten = totalWritten,
  106. NewCursor = lastCursor,
  107. Message = $"OK pages pulled={totalPulled} written={totalWritten}; {lastMessage}"
  108. };
  109. }
  110. /// <summary>若上下文带 TaskCode 且未显式指定窗口,则从 mdp_sync_task_schedule 读取。</summary>
  111. private async Task ApplyScheduleWindowAsync(MdpPullContext ctx, CancellationToken cancellationToken)
  112. {
  113. if (ctx.FullRefresh) return;
  114. if (string.IsNullOrWhiteSpace(ctx.TaskCode)) return;
  115. if (!string.IsNullOrWhiteSpace(ctx.SyncWindowType)) return;
  116. var schedules = await _db.Queryable<MdpSyncTaskSchedule>()
  117. .Where(x => x.TaskCode == ctx.TaskCode && (x.TenantId == ctx.TenantId || x.TenantId == 0))
  118. .OrderByDescending(x => x.TenantId)
  119. .Take(1)
  120. .ToListAsync(cancellationToken);
  121. var schedule = schedules.FirstOrDefault();
  122. if (schedule == null) return;
  123. ctx.SyncWindowType = schedule.SyncWindowType;
  124. ctx.SyncWindowValue = schedule.SyncWindowValue;
  125. }
  126. }