using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
using SqlSugar;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
///
/// DOP-L3 库存加锁加载 + 写入的原始 SQL 工具(**不管理事务**;由调用方在既有事务内使用)。
/// 供独立 C5 UoW 与 IQC 收货外层 UoW 复用,避免重复 SQL。**Phase 5B 未实库验证**(目标库 aidopdev)。
/// 并发策略:既有行 FOR UPDATE 锁定 + 相对 UPDATE(set x=x+delta) 防 lost-update;首建用 INSERT…ON DUPLICATE KEY(noop)。
///
public static class InventorySqlWriter
{
public static async Task LoadLockedAsync(ISqlSugarClient db, IReadOnlyList cmds)
{
var ctx = new InventoryContext();
if (cmds == null || cmds.Count == 0) return ctx;
var tenantId = cmds[0].TenantId;
var domain = cmds[0].DomainCode ?? string.Empty;
var items = cmds.Select(c => c.ItemNum).Distinct(StringComparer.Ordinal).ToList();
var locations = cmds.Select(c => c.Location).Distinct(StringComparer.Ordinal).ToList();
foreach (var it in await db.Ado.SqlQueryAsync(
"SELECT ItemNum FROM ItemMaster WHERE Domain=@d AND ItemNum IN (@items)",
new SugarParameter("@d", domain), new SugarParameter("@items", items)))
ctx.Items.Add(it);
foreach (var lo in await db.Ado.SqlQueryAsync(
"SELECT Location FROM LocationMaster WHERE Domain=@d AND Location IN (@locs)",
new SugarParameter("@d", domain), new SugarParameter("@locs", locations)))
ctx.Locations.Add(lo);
var invRows = await db.Ado.SqlQueryAsync(
"""
SELECT item_num AS ItemNum, location AS Location, qty_on_hand AS QtyOnHand,
avail_status_qty AS AvailStatusQty, assay_qty AS AssayQty, freeze_qty AS FreezeQty
FROM ado_inventory_master
WHERE tenant_id=@t AND domain_code=@d AND item_num IN (@items) AND location IN (@locs)
FOR UPDATE
""",
new SugarParameter("@t", tenantId), new SugarParameter("@d", domain),
new SugarParameter("@items", items), new SugarParameter("@locs", locations));
foreach (var r in invRows)
ctx.InvMasters[InventoryContext.InvKey(r.ItemNum, r.Location)] =
new InventoryBalance { QtyOnHand = r.QtyOnHand, AvailStatusQty = r.AvailStatusQty, Assay = r.AssayQty, FreezeQty = r.FreezeQty };
var detRows = await db.Ado.SqlQueryAsync(
"""
SELECT item_num AS ItemNum, location AS Location, lot_serial AS LotSerial, refs AS Refs,
qty_on_hand AS QtyOnHand, avail_status_qty AS AvailStatusQty, assay_qty AS AssayQty, freeze_qty AS FreezeQty
FROM ado_inventory_location_detail
WHERE tenant_id=@t AND domain_code=@d AND item_num IN (@items) AND location IN (@locs)
FOR UPDATE
""",
new SugarParameter("@t", tenantId), new SugarParameter("@d", domain),
new SugarParameter("@items", items), new SugarParameter("@locs", locations));
foreach (var r in detRows)
ctx.LocationDetails[InventoryContext.DetailKey(r.ItemNum, r.Location, r.LotSerial, r.Refs)] =
new InventoryBalance { QtyOnHand = r.QtyOnHand, AvailStatusQty = r.AvailStatusQty, Assay = r.AssayQty, FreezeQty = r.FreezeQty };
foreach (var s in await db.Ado.SqlQueryAsync(
"SELECT item_num AS ItemNum, default_location AS DefaultLocation FROM ado_item_location_state WHERE tenant_id=@t AND domain_code=@d AND item_num IN (@items) FOR UPDATE",
new SugarParameter("@t", tenantId), new SugarParameter("@d", domain), new SugarParameter("@items", items)))
ctx.ItemDefaultLocation[s.ItemNum] = s.DefaultLocation ?? string.Empty;
return ctx;
}
public static async Task WriteAsync(ISqlSugarClient db, InventoryWriteSet ws)
{
if (ws == null) throw new ArgumentNullException(nameof(ws));
var now = DateTime.Now;
foreach (var w in ws.InvMasterWrites)
{
await db.Ado.ExecuteCommandAsync(
"""
INSERT INTO ado_inventory_master (id,tenant_id,domain_code,item_num,location,qty_on_hand,avail_status_qty,assay_qty,freeze_qty,is_active,create_time,create_user)
VALUES (@id,@t,@d,@i,@l,0,0,0,0,1,@now,@u)
ON DUPLICATE KEY UPDATE update_time=@now
""",
new SugarParameter("@id", YitIdHelper.NextId()), new SugarParameter("@t", w.TenantId), new SugarParameter("@d", w.DomainCode ?? ""),
new SugarParameter("@i", w.ItemNum), new SugarParameter("@l", w.Location), new SugarParameter("@now", now), new SugarParameter("@u", w.User ?? ""));
await db.Ado.ExecuteCommandAsync(
"""
UPDATE ado_inventory_master
SET qty_on_hand=qty_on_hand+@dq, avail_status_qty=avail_status_qty+@da, assay_qty=assay_qty+@das, freeze_qty=freeze_qty+@df,
last_rct_time=@now, is_active=1, update_time=@now, update_user=@u
WHERE tenant_id=@t AND domain_code=@d AND item_num=@i AND location=@l
""",
new SugarParameter("@dq", w.Delta.DeltaQtyOnHand), new SugarParameter("@da", w.Delta.DeltaAvailStatusQty),
new SugarParameter("@das", w.Delta.DeltaAssay), new SugarParameter("@df", w.Delta.DeltaFreezeQty),
new SugarParameter("@now", now), new SugarParameter("@u", w.User ?? ""),
new SugarParameter("@t", w.TenantId), new SugarParameter("@d", w.DomainCode ?? ""),
new SugarParameter("@i", w.ItemNum), new SugarParameter("@l", w.Location));
}
foreach (var w in ws.ItemLocationWrites)
await db.Ado.ExecuteCommandAsync(
"""
INSERT INTO ado_item_location_state (id,tenant_id,domain_code,item_num,default_location,create_time,create_user)
VALUES (@id,@t,@d,@i,@loc,@now,@u)
ON DUPLICATE KEY UPDATE default_location=IF(IFNULL(default_location,'')='',@loc,default_location), update_time=@now, update_user=@u
""",
new SugarParameter("@id", YitIdHelper.NextId()), new SugarParameter("@t", w.TenantId), new SugarParameter("@d", w.DomainCode ?? ""),
new SugarParameter("@i", w.ItemNum), new SugarParameter("@loc", w.DefaultLocation), new SugarParameter("@now", now), new SugarParameter("@u", w.User ?? ""));
foreach (var w in ws.LocationDetailWrites)
{
await db.Ado.ExecuteCommandAsync(
"""
INSERT INTO ado_inventory_location_detail (id,tenant_id,domain_code,item_num,location,lot_serial,refs,qty_on_hand,avail_status_qty,assay_qty,freeze_qty,create_time,create_user)
VALUES (@id,@t,@d,@i,@l,@lot,@refs,0,0,0,0,@now,@u)
ON DUPLICATE KEY UPDATE update_time=@now
""",
new SugarParameter("@id", YitIdHelper.NextId()), new SugarParameter("@t", w.TenantId), new SugarParameter("@d", w.DomainCode ?? ""),
new SugarParameter("@i", w.ItemNum), new SugarParameter("@l", w.Location), new SugarParameter("@lot", w.LotSerial ?? ""), new SugarParameter("@refs", w.Refs ?? ""),
new SugarParameter("@now", now), new SugarParameter("@u", w.User ?? ""));
await db.Ado.ExecuteCommandAsync(
"""
UPDATE ado_inventory_location_detail
SET qty_on_hand=qty_on_hand+@dq, avail_status_qty=avail_status_qty+@da, assay_qty=assay_qty+@das, freeze_qty=freeze_qty+@df, update_time=@now, update_user=@u
WHERE tenant_id=@t AND domain_code=@d AND item_num=@i AND location=@l AND lot_serial=@lot AND refs=@refs
""",
new SugarParameter("@dq", w.Delta.DeltaQtyOnHand), new SugarParameter("@da", w.Delta.DeltaAvailStatusQty),
new SugarParameter("@das", w.Delta.DeltaAssay), new SugarParameter("@df", w.Delta.DeltaFreezeQty),
new SugarParameter("@now", now), new SugarParameter("@u", w.User ?? ""),
new SugarParameter("@t", w.TenantId), new SugarParameter("@d", w.DomainCode ?? ""),
new SugarParameter("@i", w.ItemNum), new SugarParameter("@l", w.Location),
new SugarParameter("@lot", w.LotSerial ?? ""), new SugarParameter("@refs", w.Refs ?? ""));
}
foreach (var r in ws.TransactionRows)
await db.Insertable(ToEntity(r)).ExecuteCommandAsync();
}
private static AdoInventoryTransaction ToEntity(InventoryTransactionRow r)
=> new AdoInventoryTransaction
{
Id = YitIdHelper.NextId(), TenantId = r.TenantId, DomainCode = r.DomainCode ?? "",
ItemNum = r.ItemNum, Location = r.Location, LotSerial = r.LotSerial ?? "",
TransType = r.TransType ?? "", QtyChange = r.QtyChange, BeginBalance = r.BeginBalance,
WorkOrd = r.WorkOrd, ShipperNum = r.ShipperNum, Remark = r.Remark,
FbillNo = r.Fbillno, Receiver = r.Receiver, RctQcNbr = r.RctQcNbr,
PostingId = r.PostingId == 0 ? (long?)null : r.PostingId,
TransactionGroupId = r.TransactionGroupId, Seq = r.Seq,
CreateTime = DateTime.Now, CreateUser = r.User,
};
private sealed class InvRow { public string ItemNum { get; set; } public string Location { get; set; } public decimal QtyOnHand { get; set; } public decimal AvailStatusQty { get; set; } public decimal AssayQty { get; set; } public decimal FreezeQty { get; set; } }
private sealed class DetailRow { public string ItemNum { get; set; } public string Location { get; set; } public string LotSerial { get; set; } public string Refs { get; set; } public decimal QtyOnHand { get; set; } public decimal AvailStatusQty { get; set; } public decimal AssayQty { get; set; } public decimal FreezeQty { get; set; } }
private sealed class StateRow { public string ItemNum { get; set; } public string DefaultLocation { get; set; } }
}