MdpInboundController.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. using System.Security.Claims;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
  3. using Microsoft.AspNetCore.Http;
  4. namespace Admin.NET.Plugin.AiDOP.Controllers;
  5. /// <summary>第三方标准 API 推数入站。类级仅指定 InboundSignature,不加裸 [Authorize]、不加 [AllowAnonymous]。</summary>
  6. [ApiController]
  7. [Route("api/mdp/inbound")]
  8. [NonUnify]
  9. [Authorize(AuthenticationSchemes = InboundSignatureDefaults.AuthenticationScheme)]
  10. [ApiDescriptionSettings(Order = 331, Description = "MDP API_INBOUND 推数入站")]
  11. public class MdpInboundController : ControllerBase
  12. {
  13. private readonly MdpInboundReceiveService _receive;
  14. private readonly MdpInboundSnapshotService _snapshots;
  15. public MdpInboundController(MdpInboundReceiveService receive, MdpInboundSnapshotService snapshots)
  16. {
  17. _receive = receive;
  18. _snapshots = snapshots;
  19. }
  20. /// <summary>接收推数。签名串 POST&amp;{path}&amp;...(方案 §5.2)。</summary>
  21. [HttpPost("{entityCode}")]
  22. public async Task<IActionResult> Receive(string entityCode, CancellationToken ct)
  23. {
  24. Request.EnableBuffering();
  25. using var ms = new MemoryStream();
  26. await Request.Body.CopyToAsync(ms);
  27. Request.Body.Position = 0;
  28. var outcome = await _receive.ReceiveAsync(new MdpInboundReceiveArgs
  29. {
  30. EntityCode = entityCode,
  31. RawBody = ms.ToArray(),
  32. AccessKey = AccessKey(),
  33. TenantId = TenantId(),
  34. IdempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader),
  35. ClientIp = ClientIp(),
  36. Path = Request.Path.Value ?? string.Empty,
  37. Headers = Request.Headers.ToDictionary(
  38. h => h.Key,
  39. h => h.Value.ToString(),
  40. StringComparer.OrdinalIgnoreCase)
  41. }, ct);
  42. return JsonOutcome(outcome);
  43. }
  44. /// <summary>对外规范字段 JSON Schema。签名串 GET&amp;{path}&amp;...,须带与 POST 相同的签名头(空 body 摘要)。</summary>
  45. [HttpGet("{entityCode}/schema")]
  46. public async Task<IActionResult> Schema(string entityCode, CancellationToken ct)
  47. {
  48. var outcome = await _receive.SchemaAsync(entityCode, AccessKey(), TenantId(), ClientIp(), ct);
  49. return JsonOutcome(outcome);
  50. }
  51. /// <summary>按批次查回执。非本 AccessKey 的批次返回 404。</summary>
  52. [HttpGet("receipts/{syncBatchId}")]
  53. public async Task<IActionResult> Receipt(string syncBatchId, CancellationToken ct)
  54. {
  55. var outcome = await _receive.ReceiptAsync(syncBatchId, AccessKey(), ct);
  56. return JsonOutcome(outcome);
  57. }
  58. /// <summary>开启全量快照。同一 (tenant, access_key, entity) 已有 OPEN → 409。</summary>
  59. [HttpPost("{entityCode}/snapshots")]
  60. public async Task<IActionResult> OpenSnapshot(string entityCode, CancellationToken ct)
  61. {
  62. var outcome = await _snapshots.OpenAsync(entityCode, AccessKey(), TenantId(), ClientIp(), ct);
  63. return JsonOutcome(outcome);
  64. }
  65. /// <summary>提交快照差集。?force=true 仅当状态为 DIFF_BLOCKED。</summary>
  66. [HttpPost("{entityCode}/snapshots/{snapshotId}/commit")]
  67. public async Task<IActionResult> CommitSnapshot(string entityCode, string snapshotId, [FromQuery] bool force, CancellationToken ct)
  68. {
  69. try
  70. {
  71. var outcome = await _snapshots.CommitAsync(
  72. entityCode, snapshotId, AccessKey(), TenantId(), ClientIp(), force, ct);
  73. return JsonOutcome(outcome);
  74. }
  75. catch (MdpInboundBatchException ex)
  76. {
  77. return JsonOutcome(new MdpInboundOutcome
  78. {
  79. HttpStatus = ex.HttpStatus,
  80. Body = new { code = ex.HttpStatus, message = ex.Message, data = (object)null }
  81. });
  82. }
  83. }
  84. /// <summary>NDJSON 分块推送。每行独立 idempotencyKey,整包仍走一次入站签名。</summary>
  85. [HttpPost("{entityCode}/bulk")]
  86. public async Task<IActionResult> Bulk(string entityCode, CancellationToken ct)
  87. {
  88. Request.EnableBuffering();
  89. using var ms = new MemoryStream();
  90. await Request.Body.CopyToAsync(ms);
  91. Request.Body.Position = 0;
  92. var outcome = await _receive.BulkAsync(new MdpInboundReceiveArgs
  93. {
  94. EntityCode = entityCode,
  95. RawBody = ms.ToArray(),
  96. AccessKey = AccessKey(),
  97. TenantId = TenantId(),
  98. IdempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader),
  99. ClientIp = ClientIp(),
  100. Path = Request.Path.Value ?? string.Empty,
  101. Headers = Request.Headers.ToDictionary(
  102. h => h.Key,
  103. h => h.Value.ToString(),
  104. StringComparer.OrdinalIgnoreCase)
  105. }, ct);
  106. return JsonOutcome(outcome);
  107. }
  108. /// <summary>日终对账。query 用 date 或 bizDate(yyyy-MM-dd)。</summary>
  109. [HttpGet("{entityCode}/digest")]
  110. public async Task<IActionResult> Digest(string entityCode, [FromQuery] string date, [FromQuery] string bizDate, CancellationToken ct)
  111. {
  112. var raw = !string.IsNullOrWhiteSpace(date) ? date : bizDate;
  113. var outcome = await _receive.DigestAsync(entityCode, AccessKey(), TenantId(), ClientIp(), raw, ct);
  114. return JsonOutcome(outcome);
  115. }
  116. private IActionResult JsonOutcome(MdpInboundOutcome outcome) =>
  117. new JsonResult(outcome.Body, MdpInboundJson.Options) { StatusCode = outcome.HttpStatus };
  118. private string AccessKey() =>
  119. User.FindFirstValue(InboundSignatureDefaults.AccessKeyClaim) ?? string.Empty;
  120. private long TenantId()
  121. {
  122. var raw = User.FindFirstValue(ClaimConst.TenantId);
  123. return long.TryParse(raw, out var id) ? id : 0;
  124. }
  125. private string Header(string name) => Request.Headers[name].FirstOrDefault() ?? string.Empty;
  126. private string ClientIp()
  127. {
  128. var xff = Request.Headers["X-Forwarded-For"].FirstOrDefault();
  129. if (!string.IsNullOrWhiteSpace(xff))
  130. return xff.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
  131. ?? string.Empty;
  132. return HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
  133. }
  134. }