using System.Security.Claims; using Admin.NET.Plugin.AiDOP.DataPlatform.Inbound; using Microsoft.AspNetCore.Http; namespace Admin.NET.Plugin.AiDOP.Controllers; /// 第三方标准 API 推数入站。类级仅指定 InboundSignature,不加裸 [Authorize]、不加 [AllowAnonymous]。 [ApiController] [Route("api/mdp/inbound")] [NonUnify] [Authorize(AuthenticationSchemes = InboundSignatureDefaults.AuthenticationScheme)] [ApiDescriptionSettings(Order = 331, Description = "MDP API_INBOUND 推数入站")] public class MdpInboundController : ControllerBase { private readonly MdpInboundReceiveService _receive; private readonly MdpInboundSnapshotService _snapshots; public MdpInboundController(MdpInboundReceiveService receive, MdpInboundSnapshotService snapshots) { _receive = receive; _snapshots = snapshots; } /// 接收推数。签名串 POST&{path}&...(方案 §5.2)。 [HttpPost("{entityCode}")] public async Task Receive(string entityCode, CancellationToken ct) { Request.EnableBuffering(); using var ms = new MemoryStream(); await Request.Body.CopyToAsync(ms); Request.Body.Position = 0; var outcome = await _receive.ReceiveAsync(new MdpInboundReceiveArgs { EntityCode = entityCode, RawBody = ms.ToArray(), AccessKey = AccessKey(), TenantId = TenantId(), IdempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader), ClientIp = ClientIp(), Path = Request.Path.Value ?? string.Empty, Headers = Request.Headers.ToDictionary( h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase) }, ct); return JsonOutcome(outcome); } /// 对外规范字段 JSON Schema。签名串 GET&{path}&...,须带与 POST 相同的签名头(空 body 摘要)。 [HttpGet("{entityCode}/schema")] public async Task Schema(string entityCode, CancellationToken ct) { var outcome = await _receive.SchemaAsync(entityCode, AccessKey(), TenantId(), ClientIp(), ct); return JsonOutcome(outcome); } /// 按批次查回执。非本 AccessKey 的批次返回 404。 [HttpGet("receipts/{syncBatchId}")] public async Task Receipt(string syncBatchId, CancellationToken ct) { var outcome = await _receive.ReceiptAsync(syncBatchId, AccessKey(), ct); return JsonOutcome(outcome); } /// 开启全量快照。同一 (tenant, access_key, entity) 已有 OPEN → 409。 [HttpPost("{entityCode}/snapshots")] public async Task OpenSnapshot(string entityCode, CancellationToken ct) { var outcome = await _snapshots.OpenAsync(entityCode, AccessKey(), TenantId(), ClientIp(), ct); return JsonOutcome(outcome); } /// 提交快照差集。?force=true 仅当状态为 DIFF_BLOCKED。 [HttpPost("{entityCode}/snapshots/{snapshotId}/commit")] public async Task CommitSnapshot(string entityCode, string snapshotId, [FromQuery] bool force, CancellationToken ct) { try { var outcome = await _snapshots.CommitAsync( entityCode, snapshotId, AccessKey(), TenantId(), ClientIp(), force, ct); return JsonOutcome(outcome); } catch (MdpInboundBatchException ex) { return JsonOutcome(new MdpInboundOutcome { HttpStatus = ex.HttpStatus, Body = new { code = ex.HttpStatus, message = ex.Message, data = (object)null } }); } } /// NDJSON 分块推送。每行独立 idempotencyKey,整包仍走一次入站签名。 [HttpPost("{entityCode}/bulk")] public async Task Bulk(string entityCode, CancellationToken ct) { Request.EnableBuffering(); using var ms = new MemoryStream(); await Request.Body.CopyToAsync(ms); Request.Body.Position = 0; var outcome = await _receive.BulkAsync(new MdpInboundReceiveArgs { EntityCode = entityCode, RawBody = ms.ToArray(), AccessKey = AccessKey(), TenantId = TenantId(), IdempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader), ClientIp = ClientIp(), Path = Request.Path.Value ?? string.Empty, Headers = Request.Headers.ToDictionary( h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase) }, ct); return JsonOutcome(outcome); } /// 日终对账。query 用 date 或 bizDate(yyyy-MM-dd)。 [HttpGet("{entityCode}/digest")] public async Task Digest(string entityCode, [FromQuery] string date, [FromQuery] string bizDate, CancellationToken ct) { var raw = !string.IsNullOrWhiteSpace(date) ? date : bizDate; var outcome = await _receive.DigestAsync(entityCode, AccessKey(), TenantId(), ClientIp(), raw, ct); return JsonOutcome(outcome); } private IActionResult JsonOutcome(MdpInboundOutcome outcome) => new JsonResult(outcome.Body, MdpInboundJson.Options) { StatusCode = outcome.HttpStatus }; private string AccessKey() => User.FindFirstValue(InboundSignatureDefaults.AccessKeyClaim) ?? string.Empty; private long TenantId() { var raw = User.FindFirstValue(ClaimConst.TenantId); return long.TryParse(raw, out var id) ? id : 0; } private string Header(string name) => Request.Headers[name].FirstOrDefault() ?? string.Empty; private string ClientIp() { var xff = Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(xff)) return xff.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty; return HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty; } }