using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
using SqlSugar;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
///
/// 生产 IQC 过账头幂等存储:依赖 `ado_iqc_inventory_posting` 的 DB UNIQUE `uk_ado_iqc_posting_fbillno`
/// (tenant_id, domain_code, fbillno) 原子占位——**INSERT 直上,撞唯一约束(1062)时回读既有记录**,不做先查后插。
///
/// ⚠️ Phase 4B(未在本轮实库验证):目标库 = aidopdev(ConfigId 1300000000001);DB UNIQUE 实际拒绝与真实并发行为待补。
///
public sealed class SqlSugarIqcPostingStore : IIqcPostingStore, ITransient
{
private readonly ISqlSugarClient _db;
public SqlSugarIqcPostingStore(ISqlSugarClient db)
{
_db = db;
}
public async Task TryInsertAsync(AdoIqcInventoryPosting posting)
{
if (posting == null) throw new ArgumentNullException(nameof(posting));
if (posting.Id == 0) posting.Id = YitIdHelper.NextId();
if (posting.CreateTime == default) posting.CreateTime = DateTime.Now;
if (posting.UpdateTime == null) posting.UpdateTime = posting.CreateTime; // 认领即起租(lease 心跳基准)
try
{
await _db.Insertable(posting).ExecuteCommandAsync();
return new PostingInsertResult { Inserted = true };
}
catch (Exception ex) when (IsDuplicateKey(ex))
{
var existing = await GetAsync(posting.TenantId, posting.DomainCode, posting.FbillNo);
return new PostingInsertResult { Inserted = false, Existing = existing };
}
}
public async Task GetAsync(long tenantId, string domainCode, string fbillNo)
{
var domain = domainCode ?? string.Empty;
return await _db.Queryable()
.Where(x => x.TenantId == tenantId && x.DomainCode == domain && x.FbillNo == fbillNo)
.FirstAsync();
}
public async Task UpdateStatusAsync(AdoIqcInventoryPosting posting, string status)
{
if (posting == null) throw new ArgumentNullException(nameof(posting));
await _db.Ado.ExecuteCommandAsync(
"UPDATE ado_iqc_inventory_posting SET posting_status=@st, update_time=@now WHERE tenant_id=@t AND domain_code=@d AND fbillno=@fb",
new SugarParameter("@st", status), new SugarParameter("@now", DateTime.Now),
new SugarParameter("@t", posting.TenantId), new SugarParameter("@d", posting.DomainCode ?? ""),
new SugarParameter("@fb", posting.FbillNo));
posting.PostingStatus = status;
}
public async Task TryRecoverStaleAsync(long tenantId, string domainCode, string fbillNo, DateTime staleBefore, DateTime now)
{
// 单条件原子更新:仅 PROCESSING 且心跳过期者被抢占(rows=1);并发多路仅一路命中,无先查后改窗口。
var rows = await _db.Ado.ExecuteCommandAsync(
"UPDATE ado_iqc_inventory_posting SET update_time=@now " +
"WHERE tenant_id=@t AND domain_code=@d AND fbillno=@fb AND posting_status='PROCESSING' AND update_time < @stale",
new SugarParameter("@now", now), new SugarParameter("@t", tenantId),
new SugarParameter("@d", domainCode ?? ""), new SugarParameter("@fb", fbillNo),
new SugarParameter("@stale", staleBefore));
return rows == 1;
}
/// 沿异常链识别 MySQL 唯一键冲突(1062 / Duplicate entry / 目标唯一索引名)。
private static bool IsDuplicateKey(Exception ex)
{
for (var e = ex; e != null; e = e.InnerException)
{
var m = e.Message;
if (!string.IsNullOrEmpty(m) &&
(m.Contains("Duplicate entry") || m.Contains("1062") || m.Contains("uk_ado_iqc_posting_fbillno")))
return true;
}
return false;
}
}