MdpOutboxAdminService.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
  2. using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
  3. namespace Admin.NET.Plugin.AiDOP.DataPlatform;
  4. /// <summary>
  5. /// WP10 S4a · Outbox 可观测与手工重推(本库运维数据,需登录;不加 AllowAnonymous)。
  6. /// </summary>
  7. [ApiDescriptionSettings(Order = 328, Description = "出站回写队列")]
  8. [Route("api/aidop/mdp-outbox")]
  9. [NonUnify]
  10. public class MdpOutboxAdminService : IDynamicApiController, ITransient
  11. {
  12. private readonly ISqlSugarClient _db;
  13. private readonly MdpOutboxWakeSignal _wake;
  14. private readonly MdpDbPushExecutor _dbPush;
  15. private readonly MdpApiPushExecutor _apiPush;
  16. public MdpOutboxAdminService(
  17. ISqlSugarClient db,
  18. MdpOutboxWakeSignal wake,
  19. MdpDbPushExecutor dbPush,
  20. MdpApiPushExecutor apiPush)
  21. {
  22. _db = db;
  23. _wake = wake;
  24. _dbPush = dbPush;
  25. _apiPush = apiPush;
  26. }
  27. public sealed class PageInput
  28. {
  29. public int? Status { get; set; }
  30. public string? TargetSourceCode { get; set; }
  31. public string? ActionCode { get; set; }
  32. public string? IdemKey { get; set; }
  33. public DateTime? CreateTimeFrom { get; set; }
  34. public DateTime? CreateTimeTo { get; set; }
  35. public int Page { get; set; } = 1;
  36. public int PageSize { get; set; } = 20;
  37. }
  38. public sealed class RetryInput
  39. {
  40. public long[]? Ids { get; set; }
  41. public bool? AllDead { get; set; }
  42. }
  43. [DisplayName("出站回写队列分页")]
  44. [HttpGet("page")]
  45. public async Task<object> Page([FromQuery] PageInput input)
  46. {
  47. var page = input.Page <= 0 ? 1 : input.Page;
  48. var pageSize = input.PageSize <= 0 ? 20 : Math.Min(input.PageSize, 200);
  49. var q = _db.Queryable<MdpOutbox>()
  50. .WhereIF(input.Status is 0 or 1 or 2, x => x.Status == input.Status!.Value)
  51. .WhereIF(!string.IsNullOrWhiteSpace(input.TargetSourceCode),
  52. x => x.TargetSourceCode == input.TargetSourceCode!.Trim())
  53. .WhereIF(!string.IsNullOrWhiteSpace(input.ActionCode),
  54. x => x.ActionCode == input.ActionCode!.Trim())
  55. .WhereIF(!string.IsNullOrWhiteSpace(input.IdemKey),
  56. x => x.IdemKey.Contains(input.IdemKey!.Trim()))
  57. .WhereIF(input.CreateTimeFrom.HasValue, x => x.CreateTime >= input.CreateTimeFrom!.Value)
  58. .WhereIF(input.CreateTimeTo.HasValue, x => x.CreateTime <= input.CreateTimeTo!.Value);
  59. RefAsync<int> total = 0;
  60. var rows = await q.OrderByDescending(x => x.Id)
  61. .ToPageListAsync(page, pageSize, total);
  62. return new
  63. {
  64. total = total.Value,
  65. page,
  66. pageSize,
  67. list = rows.Select(x => new
  68. {
  69. id = x.Id,
  70. tenantId = x.TenantId,
  71. targetSourceCode = x.TargetSourceCode,
  72. actionCode = x.ActionCode,
  73. idemKey = x.IdemKey,
  74. payloadJson = x.PayloadJson,
  75. status = x.Status,
  76. retryCount = x.RetryCount,
  77. nextRetryTime = x.NextRetryTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  78. lastErrorCode = x.LastErrorCode,
  79. responseJson = x.ResponseJson,
  80. errorMsg = x.ErrorMsg,
  81. createTime = x.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  82. updateTime = x.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  83. })
  84. };
  85. }
  86. [DisplayName("出站回写队列统计")]
  87. [HttpGet("stats")]
  88. public async Task<object> Stats()
  89. {
  90. var pending = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 0);
  91. var success = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 1);
  92. var dead = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 2);
  93. var since = DateTime.Now.AddHours(-24);
  94. var deadLast24h = await _db.Queryable<MdpOutbox>()
  95. .CountAsync(x => x.Status == 2 && x.UpdateTime >= since);
  96. double oldestPendingMinutes = 0;
  97. if (pending > 0)
  98. {
  99. var oldest = await _db.Queryable<MdpOutbox>()
  100. .Where(x => x.Status == 0)
  101. .OrderBy(x => x.CreateTime)
  102. .Select(x => x.CreateTime)
  103. .FirstAsync();
  104. oldestPendingMinutes = Math.Max(0, (DateTime.Now - oldest).TotalMinutes);
  105. oldestPendingMinutes = Math.Round(oldestPendingMinutes, 1);
  106. }
  107. return new
  108. {
  109. pending,
  110. success,
  111. dead,
  112. oldestPendingMinutes,
  113. deadLast24h,
  114. };
  115. }
  116. [DisplayName("出站回写手工重推")]
  117. [HttpPost("retry")]
  118. public async Task<object> Retry([FromBody] RetryInput input, CancellationToken ct = default)
  119. {
  120. if (input == null)
  121. throw Oops.Oh("body 不能为空");
  122. var hasIds = input.Ids is { Length: > 0 };
  123. var allDead = input.AllDead == true;
  124. if (!hasIds && !allDead)
  125. throw Oops.Oh("请指定 ids 或 allDead=true,禁止全表重置");
  126. int reset;
  127. if (allDead)
  128. {
  129. reset = await _db.Updateable<MdpOutbox>()
  130. .SetColumns(x => x.Status == 0)
  131. .SetColumns(x => x.RetryCount == 0)
  132. .SetColumns(x => x.ErrorMsg == null)
  133. .SetColumns(x => x.NextRetryTime == null)
  134. .SetColumns(x => x.LastErrorCode == null)
  135. .SetColumns(x => x.UpdateTime == DateTime.Now)
  136. .Where(x => x.Status == 2)
  137. .ExecuteCommandAsync(ct);
  138. }
  139. else
  140. {
  141. var ids = input.Ids!.Distinct().ToArray();
  142. reset = await _db.Updateable<MdpOutbox>()
  143. .SetColumns(x => x.Status == 0)
  144. .SetColumns(x => x.RetryCount == 0)
  145. .SetColumns(x => x.ErrorMsg == null)
  146. .SetColumns(x => x.NextRetryTime == null)
  147. .SetColumns(x => x.LastErrorCode == null)
  148. .SetColumns(x => x.UpdateTime == DateTime.Now)
  149. .Where(x => ids.Contains(x.Id))
  150. .ExecuteCommandAsync(ct);
  151. }
  152. if (reset > 0)
  153. _wake.Pulse();
  154. return new { reset };
  155. }
  156. /// <summary>
  157. /// 本进程内立刻执行一条 Outbox(不经集群/其它实例抢消费),用于 PROC 等新能力验收。
  158. /// </summary>
  159. [DisplayName("出站回写本机立即执行")]
  160. [HttpPost("push-now/{id:long}")]
  161. public async Task<object> PushNow(long id, CancellationToken ct = default)
  162. {
  163. var item = await _db.Queryable<MdpOutbox>().InSingleAsync(id);
  164. if (item == null)
  165. throw Oops.Oh($"outbox id={id} 不存在");
  166. var source = await _db.Queryable<MdpSource>()
  167. .Where(x => x.SourceCode == item.TargetSourceCode && x.Status == 1)
  168. .FirstAsync(ct);
  169. if (source == null)
  170. throw Oops.Oh($"目标源 {item.TargetSourceCode} 未启用");
  171. IMdpTargetPushExecutor executor = string.Equals(source.SourceType, "DB", StringComparison.OrdinalIgnoreCase)
  172. ? _dbPush
  173. : _apiPush;
  174. var result = await executor.PushAsync(source, item, ct);
  175. var now = DateTime.Now;
  176. if (result.Success)
  177. {
  178. await _db.Updateable<MdpOutbox>()
  179. .SetColumns(x => x.Status == 1)
  180. .SetColumns(x => x.ErrorMsg == null)
  181. .SetColumns(x => x.LastErrorCode == null)
  182. .SetColumns(x => x.NextRetryTime == null)
  183. .SetColumns(x => x.ResponseJson == result.ResponseJson)
  184. .SetColumns(x => x.UpdateTime == now)
  185. .Where(x => x.Id == id)
  186. .ExecuteCommandAsync(ct);
  187. }
  188. else
  189. {
  190. await _db.Updateable<MdpOutbox>()
  191. .SetColumns(x => x.Status == 2)
  192. .SetColumns(x => x.ErrorMsg == (result.ErrorMessage ?? "push failed"))
  193. .SetColumns(x => x.ResponseJson == result.ResponseJson)
  194. .SetColumns(x => x.NextRetryTime == null)
  195. .SetColumns(x => x.UpdateTime == now)
  196. .Where(x => x.Id == id)
  197. .ExecuteCommandAsync(ct);
  198. }
  199. return new
  200. {
  201. id,
  202. success = result.Success,
  203. idempotentSkip = result.IdempotentSkip,
  204. errorMessage = result.ErrorMessage,
  205. responseJson = result.ResponseJson,
  206. affectedRows = result.AffectedRows
  207. };
  208. }
  209. }