MdpInboundController.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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(
  49. entityCode, AccessKey(), TenantId(), ClientIp(), ct, ContractVersion());
  50. return JsonOutcome(outcome);
  51. }
  52. /// <summary>按批次查回执。非本 AccessKey 的批次返回 404。</summary>
  53. [HttpGet("receipts/{syncBatchId}")]
  54. public async Task<IActionResult> Receipt(string syncBatchId, CancellationToken ct)
  55. {
  56. var outcome = await _receive.ReceiptAsync(syncBatchId, AccessKey(), ct);
  57. return JsonOutcome(outcome);
  58. }
  59. /// <summary>开启全量快照。同一 (tenant, access_key, entity) 已有 OPEN → 409。</summary>
  60. [HttpPost("{entityCode}/snapshots")]
  61. public async Task<IActionResult> OpenSnapshot(string entityCode, CancellationToken ct)
  62. {
  63. var outcome = await _snapshots.OpenAsync(entityCode, AccessKey(), TenantId(), ClientIp(), ct);
  64. return JsonOutcome(outcome);
  65. }
  66. /// <summary>提交快照差集。?force=true 仅当状态为 DIFF_BLOCKED。</summary>
  67. [HttpPost("{entityCode}/snapshots/{snapshotId}/commit")]
  68. public async Task<IActionResult> CommitSnapshot(string entityCode, string snapshotId, [FromQuery] bool force, CancellationToken ct)
  69. {
  70. try
  71. {
  72. var outcome = await _snapshots.CommitAsync(
  73. entityCode, snapshotId, AccessKey(), TenantId(), ClientIp(), force, ct);
  74. return JsonOutcome(outcome);
  75. }
  76. catch (MdpInboundBatchException ex)
  77. {
  78. return JsonOutcome(new MdpInboundOutcome
  79. {
  80. HttpStatus = ex.HttpStatus,
  81. Body = new { code = ex.HttpStatus, message = ex.Message, data = (object)null }
  82. });
  83. }
  84. }
  85. /// <summary>NDJSON 分块推送。每行独立 idempotencyKey,整包仍走一次入站签名。</summary>
  86. [HttpPost("{entityCode}/bulk")]
  87. public async Task<IActionResult> Bulk(string entityCode, CancellationToken ct)
  88. {
  89. Request.EnableBuffering();
  90. using var ms = new MemoryStream();
  91. await Request.Body.CopyToAsync(ms);
  92. Request.Body.Position = 0;
  93. var outcome = await _receive.BulkAsync(new MdpInboundReceiveArgs
  94. {
  95. EntityCode = entityCode,
  96. RawBody = ms.ToArray(),
  97. AccessKey = AccessKey(),
  98. TenantId = TenantId(),
  99. IdempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader),
  100. ClientIp = ClientIp(),
  101. Path = Request.Path.Value ?? string.Empty,
  102. Headers = Request.Headers.ToDictionary(
  103. h => h.Key,
  104. h => h.Value.ToString(),
  105. StringComparer.OrdinalIgnoreCase)
  106. }, ct);
  107. return JsonOutcome(outcome);
  108. }
  109. /// <summary>日终对账。query 用 date 或 bizDate(yyyy-MM-dd)。</summary>
  110. [HttpGet("{entityCode}/digest")]
  111. public async Task<IActionResult> Digest(string entityCode, [FromQuery] string date, [FromQuery] string bizDate, CancellationToken ct)
  112. {
  113. var raw = !string.IsNullOrWhiteSpace(date) ? date : bizDate;
  114. var outcome = await _receive.DigestAsync(entityCode, AccessKey(), TenantId(), ClientIp(), raw, ct);
  115. return JsonOutcome(outcome);
  116. }
  117. private IActionResult JsonOutcome(MdpInboundOutcome outcome) =>
  118. new JsonResult(outcome.Body, MdpInboundJson.Options) { StatusCode = outcome.HttpStatus };
  119. private string AccessKey() =>
  120. User.FindFirstValue(InboundSignatureDefaults.AccessKeyClaim) ?? string.Empty;
  121. private long TenantId()
  122. {
  123. var raw = User.FindFirstValue(ClaimConst.TenantId);
  124. return long.TryParse(raw, out var id) ? id : 0;
  125. }
  126. private string Header(string name) => Request.Headers[name].FirstOrDefault() ?? string.Empty;
  127. private string ContractVersion() =>
  128. MdpInboundFieldMapper.NormalizeVersion(Header(MdpInboundFieldMapper.ContractVersionHeader));
  129. private string ClientIp()
  130. {
  131. var xff = Request.Headers["X-Forwarded-For"].FirstOrDefault();
  132. if (!string.IsNullOrWhiteSpace(xff))
  133. return xff.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
  134. ?? string.Empty;
  135. return HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
  136. }
  137. }