using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse; /// 库存冷链手工补偿入口(管理员)。不接受前端 tenantId/domain/entityCode。 [ApiDescriptionSettings(Order = 310, Description = "S5库存冷链入站")] [Route("api/S5InventoryInbound")] [NonUnify] public sealed class InventoryInboundAdminService : IDynamicApiController, ITransient { private readonly InventoryMdpSyncService _sync; private readonly InventoryReconService _recon; private readonly ISqlSugarClient _db; private readonly ILogger _logger; public InventoryInboundAdminService( InventoryMdpSyncService sync, InventoryReconService recon, ISqlSugarClient db, ILoggerFactory loggerFactory) { _sync = sync; _recon = recon; _db = db; _logger = loggerFactory.CreateLogger(nameof(InventoryInboundAdminService)); } [DisplayName("库存冷链首刷")] [HttpPost("bootstrap")] [ApiDescriptionSettings(Name = "S5InventoryBootstrap")] public async Task Bootstrap(CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] bootstrap requested"); return await _sync.RunBootstrapAsync(ct); } [DisplayName("库存冷链跑一轮")] [HttpPost("run-once")] [ApiDescriptionSettings(Name = "S5InventoryRunOnce")] public async Task RunOnce(CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] run-once requested"); return await _sync.RunIncrementalAsync(ct); } [DisplayName("库存冷链全量校准")] [HttpPost("reconcile")] [ApiDescriptionSettings(Name = "S5InventoryReconcile")] public async Task Reconcile(CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] reconcile requested"); return await _sync.RunReconcileFullAsync(ct); } [DisplayName("库存流水 stg→std 补转换")] [HttpPost("transform-std")] [ApiDescriptionSettings(Name = "S5InventoryTransformStd")] public async Task TransformStd(CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] transform-std requested"); return await _sync.TransformTransStdFromStgAsync(ct); } /// /// 库存余额 stg→std 补物化:复用已完整落地的贴源批次,不访问源库。 /// UPSERT 语义,不删除既有标准层数据。 /// [DisplayName("库存余额 stg→std 补物化")] [HttpPost("transform-inventory-std")] [ApiDescriptionSettings(Name = "S5InventoryTransformInventoryStd")] public async Task TransformInventoryStd([FromQuery] string batchId, CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] transform-inventory-std requested batch={Batch}", batchId); return await _sync.TransformInventoryStdFromStgAsync(batchId, ct); } /// /// 日终对账与 bootstrap / reconcile / transform 共享同一互斥闸门: /// 对账读 mdp_std_inventory 并写差异,跑批期间的中间态会产生假差异,必须排他。 /// 锁在本入口而不是 InventoryReconService 内部,避免与 InventoryMdpSyncService 的自锁重入。 /// [DisplayName("库存日终对账(手工)")] [HttpPost("daily-recon")] [ApiDescriptionSettings(Name = "S5InventoryDailyRecon")] public async Task DailyRecon(CancellationToken ct) { _logger.LogInformation("[S5InventoryInbound] daily-recon requested"); await using var guard = await InventoryInboundLockGuard.TryAcquireAsync(_db, _logger, cancellationToken: ct); if (!guard.Acquired) { _logger.LogWarning("[S5InventoryInbound] daily-recon 跳过,互斥锁占用 reason={Reason}", guard.BusyReason); return new { diffs = 0, skipped = true, message = "lock busy" }; } var diffs = await _recon.RunDailyReconAsync(ct); return new { diffs, skipped = false, message = diffs == 0 ? "clean" : "diffs written" }; } }