MdpOutboxAdminService.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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. nextRetryTime = x.NextRetryTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  70. lastErrorCode = x.LastErrorCode,
  71. responseJson = x.ResponseJson,
  72. errorMsg = x.ErrorMsg,
  73. createTime = x.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  74. updateTime = x.UpdateTime.ToString("yyyy-MM-dd HH:mm:ss"),
  75. })
  76. };
  77. }
  78. [DisplayName("出站回写队列统计")]
  79. [HttpGet("stats")]
  80. public async Task<object> Stats()
  81. {
  82. var pending = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 0);
  83. var success = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 1);
  84. var dead = await _db.Queryable<MdpOutbox>().CountAsync(x => x.Status == 2);
  85. var since = DateTime.Now.AddHours(-24);
  86. var deadLast24h = await _db.Queryable<MdpOutbox>()
  87. .CountAsync(x => x.Status == 2 && x.UpdateTime >= since);
  88. double oldestPendingMinutes = 0;
  89. if (pending > 0)
  90. {
  91. var oldest = await _db.Queryable<MdpOutbox>()
  92. .Where(x => x.Status == 0)
  93. .OrderBy(x => x.CreateTime)
  94. .Select(x => x.CreateTime)
  95. .FirstAsync();
  96. oldestPendingMinutes = Math.Max(0, (DateTime.Now - oldest).TotalMinutes);
  97. oldestPendingMinutes = Math.Round(oldestPendingMinutes, 1);
  98. }
  99. return new
  100. {
  101. pending,
  102. success,
  103. dead,
  104. oldestPendingMinutes,
  105. deadLast24h,
  106. };
  107. }
  108. [DisplayName("出站回写手工重推")]
  109. [HttpPost("retry")]
  110. public async Task<object> Retry([FromBody] RetryInput input, CancellationToken ct = default)
  111. {
  112. if (input == null)
  113. throw Oops.Oh("body 不能为空");
  114. var hasIds = input.Ids is { Length: > 0 };
  115. var allDead = input.AllDead == true;
  116. if (!hasIds && !allDead)
  117. throw Oops.Oh("请指定 ids 或 allDead=true,禁止全表重置");
  118. int reset;
  119. if (allDead)
  120. {
  121. reset = await _db.Updateable<MdpOutbox>()
  122. .SetColumns(x => x.Status == 0)
  123. .SetColumns(x => x.RetryCount == 0)
  124. .SetColumns(x => x.ErrorMsg == null)
  125. .SetColumns(x => x.NextRetryTime == null)
  126. .SetColumns(x => x.LastErrorCode == null)
  127. .SetColumns(x => x.UpdateTime == DateTime.Now)
  128. .Where(x => x.Status == 2)
  129. .ExecuteCommandAsync(ct);
  130. }
  131. else
  132. {
  133. var ids = input.Ids!.Distinct().ToArray();
  134. reset = await _db.Updateable<MdpOutbox>()
  135. .SetColumns(x => x.Status == 0)
  136. .SetColumns(x => x.RetryCount == 0)
  137. .SetColumns(x => x.ErrorMsg == null)
  138. .SetColumns(x => x.NextRetryTime == null)
  139. .SetColumns(x => x.LastErrorCode == null)
  140. .SetColumns(x => x.UpdateTime == DateTime.Now)
  141. .Where(x => ids.Contains(x.Id))
  142. .ExecuteCommandAsync(ct);
  143. }
  144. if (reset > 0)
  145. _wake.Pulse();
  146. return new { reset };
  147. }
  148. }