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;
}
}