MdpOutboxAdminService.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. public MdpOutboxAdminService(ISqlSugarClient db, MdpOutboxWakeSignal wake)
  15. {
  16. _db = db;
  17. _wake = wake;
  18. }
  19. public sealed class PageInput
  20. {
  21. public int? Status { get; set; }
  22. public string? TargetSourceCode { get; set; }
  23. public string? ActionCode { get; set; }
  24. public string? IdemKey { get; set; }
  25. public DateTime? CreateTimeFrom { get; set; }
  26. public DateTime? CreateTimeTo { get; set; }
  27. public int Page { get; set; } = 1;
  28. public int PageSize { get; set; } = 20;
  29. }
  30. public sealed class RetryInput
  31. {
  32. public long[]? Ids { get; set; }
  33. public bool? AllDead { get; set; }
  34. }
  35. [DisplayName("出站回写队列分页")]
  36. [HttpGet("page")]
  37. public async Task<object> Page([FromQuery] PageInput input)
  38. {
  39. var page = input.Page <= 0 ? 1 : input.Page;
  40. var pageSize = input.PageSize <= 0 ? 20 : Math.Min(input.PageSize, 200);
  41. var q = _db.Queryable<MdpOutbox>()
  42. .WhereIF(input.Status is 0 or 1 or 2, x => x.Status == input.Status!.Value)
  43. .WhereIF(!string.IsNullOrWhiteSpace(input.TargetSourceCode),
  44. x => x.TargetSourceCode == input.TargetSourceCode!.Trim())
  45. .WhereIF(!string.IsNullOrWhiteSpace(input.ActionCode),
  46. x => x.ActionCode == input.ActionCode!.Trim())
  47. .WhereIF(!string.IsNullOrWhiteSpace(input.IdemKey),
  48. x => x.IdemKey.Contains(input.IdemKey!.Trim()))
  49. .WhereIF(input.CreateTimeFrom.HasValue, x => x.CreateTime >= input.CreateTimeFrom!.Value)
  50. .WhereIF(input.CreateTimeTo.HasValue, x => x.CreateTime <= input.CreateTimeTo!.Value);
  51. RefAsync<int> total = 0;
  52. var rows = await q.OrderByDescending(x => x.Id)
  53. .ToPageListAsync(page, pageSize, total);
  54. return new
  55. {
  56. total = total.Value,
  57. page,
  58. pageSize,
  59. list = rows.Select(x => new
  60. {
  61. id = x.Id,
  62. tenantId = x.TenantId,
  63. targetSourceCode = x.TargetSourceCode,
  64. actionCode = x.ActionCode,
  65. idemKey = x.IdemKey,
  66. payloadJson = x.PayloadJson,
  67. status = x.Status,
  68. retryCount = x.RetryCount,
  69. responseJson = x.ResponseJson,
  70. errorMsg = x.ErrorMsg,
  71. createTime = x.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  72. updateTime = x.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  73. })
  74. };
  75. }
  76. [DisplayName("出站回写队列统计")]
  77. [HttpGet("stats")]
  78. public async Task<object> Stats()
  79. {
  80. var pending = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 0);
  81. var success = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 1);
  82. var dead = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 2);
  83. var since = DateTime.Now.AddHours(-24);
  84. var deadLast24h = await _db.Queryable<MdpOutbox>()
  85. .CountAsync(x => x.Status == 2 && x.UpdateTime >= since);
  86. double oldestPendingMinutes = 0;
  87. if (pending > 0)
  88. {
  89. var oldest = await _db.Queryable<MdpOutbox>()
  90. .Where(x => x.Status == 0)
  91. .OrderBy(x => x.CreateTime)
  92. .Select(x => x.CreateTime)
  93. .FirstAsync();
  94. oldestPendingMinutes = Math.Max(0, (DateTime.Now - oldest).TotalMinutes);
  95. oldestPendingMinutes = Math.Round(oldestPendingMinutes, 1);
  96. }
  97. return new
  98. {
  99. pending,
  100. success,
  101. dead,
  102. oldestPendingMinutes,
  103. deadLast24h,
  104. };
  105. }
  106. [DisplayName("出站回写手工重推")]
  107. [HttpPost("retry")]
  108. public async Task<object> Retry([FromBody] RetryInput input, CancellationToken ct = default)
  109. {
  110. if (input == null)
  111. throw Oops.Oh("body 不能为空");
  112. var hasIds = input.Ids is { Length: > 0 };
  113. var allDead = input.AllDead == true;
  114. if (!hasIds && !allDead)
  115. throw Oops.Oh("请指定 ids 或 allDead=true,禁止全表重置");
  116. int reset;
  117. if (allDead)
  118. {
  119. reset = await _db.Updateable<MdpOutbox>()
  120. .SetColumns(x => x.Status == 0)
  121. .SetColumns(x => x.RetryCount == 0)
  122. .SetColumns(x => x.ErrorMsg == null)
  123. .SetColumns(x => x.UpdateTime == DateTime.Now)
  124. .Where(x => x.Status == 2)
  125. .ExecuteCommandAsync(ct);
  126. }
  127. else
  128. {
  129. var ids = input.Ids!.Distinct().ToArray();
  130. reset = await _db.Updateable<MdpOutbox>()
  131. .SetColumns(x => x.Status == 0)
  132. .SetColumns(x => x.RetryCount == 0)
  133. .SetColumns(x => x.ErrorMsg == null)
  134. .SetColumns(x => x.UpdateTime == DateTime.Now)
  135. .Where(x => ids.Contains(x.Id))
  136. .ExecuteCommandAsync(ct);
  137. }
  138. if (reset > 0)
  139. _wake.Pulse();
  140. return new { reset };
  141. }
  142. }