using MES.Service.DB;
using MES.Service.Dto.webApi;
using MES.Service.Modes;
using MES.Service.util;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MES.Service.service
{
///
/// 收料单数据管理类(处理ERP收料单数据同步到MES收料单表)
/// 关联主表:MesInvItemArn(MES收料单主表)
/// 关联明细表:MesInvItemArnDetail(MES收料单明细表)
/// 关联ERP DTO:ErpSltz(聚合类,含主表+明细)、ErpSltza(ERP收料单主表)、ErpSltzBList(ERP收料单明细)
///
public class MesInvItemArnManager : Repository
{
///
/// 批量保存收料单(主表+明细)- 事务保证批量操作一致性
/// 单条失败则整体回滚
///
public bool SaveList(List erpSltzList)
{
if (erpSltzList == null || !erpSltzList.Any())
throw new ArgumentNullException(nameof(erpSltzList), "待保存的收料单列表不能为空");
return UseTransaction(db =>
{
foreach (var erpSltz in erpSltzList)
{
if (!SaveInTransaction(db, erpSltz))
return 0; // 触发回滚
}
return 1; // 提交
}) > 0;
}
///
/// 对外单条保存(内部自动事务)
///
public bool Save(ErpSltz erpSltz)
{
if (erpSltz == null) throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空");
if (erpSltz.ErpSltza == null) throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空");
return UseTransaction(db => SaveInTransaction(db, erpSltz) ? 1 : 0) > 0;
}
///
/// 事务内单条保存
///
private bool SaveInTransaction(SqlSugarScope db, ErpSltz erpSltz)
{
if (erpSltz == null) throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空");
if (erpSltz.ErpSltza == null) throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空");
var erpMain = erpSltz.ErpSltza;
var erpDetails = erpSltz.ErpSltzBList ?? new List();
// 通过 ERP 单号确定/生成主表ID
Guid mainId = GetOrCreateMainId(db, erpMain);
// 映射
var mesMain = MapErpMainToMesMain(erpMain, mainId,db);
var mesDetails = MapErpDetailsToMesDetails(db, erpDetails, mainId);
// 类型:1 新增,2 更新,3 删除
var type = TrimOrNull(erpMain.TYPE);
return type switch
{
"1" or "2" => HandleSaveOrUpdate(db, mesMain, mesDetails, mainId),
"3" => HandleDelete(db, mainId),
_ => throw new NotImplementedException($"未实现的收料单操作类型:{erpMain.TYPE}(支持:1-新增,2-更新,3-删除)")
};
}
///
/// 获取或生成主表ID(按 ERP DeliveryNo 唯一)
///
private Guid GetOrCreateMainId(SqlSugarScope db, ErpSltza erpMain)
{
var billNo = TrimOrNull(erpMain.DeliveryNo)
?? throw new ArgumentException("ERP收料单单据号(DeliveryNo)不能为空,无法确定主表唯一性",
nameof(erpMain.DeliveryNo));
// SqlSugar: 用 First(),无数据返回 null
var existingMain = db.Queryable()
.Where(m => m.BillNo == billNo)
.First();
return existingMain?.Id ?? Guid.NewGuid();
}
///
/// 新增/更新(主表+明细:先删旧明细再插新)
///
private bool HandleSaveOrUpdate(SqlSugarScope db, MesInvItemArn mesMain, List mesDetails,
Guid mainId)
{
mesMain.Id = mainId;
bool isMainExist = db.Queryable()
.Where(m => m.Id == mainId)
.Any();
int mainOperateResult;
if (isMainExist)
{
mainOperateResult = db.Updateable(mesMain)
.IgnoreColumns(m => new { m.CreateDate })
.Where(m => m.Id == mainId)
.ExecuteCommand();
}
else
{
mesMain.CreateDate = DateTime.Now;
mainOperateResult = db.Insertable(mesMain).ExecuteCommand();
}
// 明细同步:删旧 + 插新
int deleteOldDetailsResult = db.Deleteable()
.Where(d => d.Mid == mainId)
.ExecuteCommand();
int insertNewDetailsResult = mesDetails.Count > 0
? db.Insertable(mesDetails).ExecuteCommand()
: 1;
return mainOperateResult > 0 && deleteOldDetailsResult >= 0 && insertNewDetailsResult > 0;
}
///
/// 删除(先删明细再删主表)
///
private bool HandleDelete(SqlSugarScope db, Guid mainId)
{
db.Deleteable()
.Where(d => d.Mid == mainId)
.ExecuteCommand();
int deleteMainResult = db.Deleteable()
.Where(m => m.Id == mainId)
.ExecuteCommand();
return deleteMainResult >= 0;
}
///
/// ERP 主表 -> MES 主表
///
private MesInvItemArn MapErpMainToMesMain(ErpSltza erpMain, Guid mainId, SqlSugarScope db)
{
var billNo = TrimOrNull(erpMain.DeliveryNo)
?? throw new ArgumentException("收料单单据号不能为空");
// 获取供应商编号并查询对应的ID
string suppNo = TrimOrNull(erpMain.SupplierId);
decimal ? suppId = null;
if (!string.IsNullOrEmpty(suppNo))
{
// 从数据库查询供应商ID(SuppNo唯一)
suppId = db.Queryable()
.Where(s => s.SuppNo == suppNo)
.Select(s => s.Id)
.First();
// 可选:如果需要严格验证供应商存在性,取消下面注释
if (suppId == null)
{
throw new KeyNotFoundException($"未找到编号为【{suppNo}】的供应商信息");
}
}
// 是否 SRM
var isSrm = ToInt(erpMain.F_ZJXF_sfgx, 0);
// 是否委外:ERP的 fType 为 "1"/"true" 视为委外
var fTypeStr = TrimOrNull(erpMain.fType);
bool isOutsourcing = fTypeStr is not null &&
(fTypeStr == "1" || fTypeStr.Equals("true", StringComparison.OrdinalIgnoreCase));
return new MesInvItemArn
{
Id = mainId,
BillNo = billNo,
SuppId = suppId.ToString(),
CreateDate = ToDate(erpMain.FDate), // null 表示未知
Remark = TrimOrNull(erpMain.Remark),
IsSrm = isSrm,
EbelnK3id = TrimOrNull(erpMain.erpId),
CreateBy = TrimOrNull(erpMain.createBy) ?? "SYSTEM",
FType = isOutsourcing,
ReceiveOrgId = TrimOrNull(erpMain.ReceiveOrgId)
};
}
///
/// ERP 明细 -> MES 明细
/// 核心:通过 ERP ProductCode 匹配 mes_items.item_no 获取 item_id
///
private List MapErpDetailsToMesDetails(SqlSugarScope db, List erpDetails,
Guid mainId)
{
var productCodes = erpDetails
.Where(d => !string.IsNullOrWhiteSpace(d.ProductCode))
.Select(d => d.ProductCode.Trim())
.Distinct()
.ToList();
// 先 ToList 再 ToDictionary,避免直接对 ISugarQueryable 调用拓展方法失败
var itemPairs = db.Queryable()
.Where(item => productCodes.Contains(item.ItemNo))
.Select(item => new { item.ItemNo, item.Id })
.ToList();
var itemMap = itemPairs.ToDictionary(k => k.ItemNo, v => v.Id);
var list = new List(erpDetails.Count);
foreach (var erpDetail in erpDetails)
{
var lineNo = ToInt(erpDetail.LineNo);
var productCode = TrimOrNull(erpDetail.ProductCode);
if (productCode == null)
throw new ArgumentNullException(nameof(erpDetail.ProductCode),
$"ERP收料单明细【行:{lineNo}】的物料编码(ProductCode)不能为空,无法生成明细");
if (!itemMap.TryGetValue(productCode, out var itemId))
throw new KeyNotFoundException(
$"ERP收料单明细【行:{lineNo}】的物料编码【{productCode}】在 MES.mes_items 中未找到匹配的 item_no");
var detail = new MesInvItemArnDetail
{
DeliveryLineID = lineNo,
Eid = ToInt(erpDetail.ErpId),
Mid = mainId,
Ebeln = TrimOrNull(erpDetail.FBillNo),
ItemId = ToInt(itemId),
EbelnQty = ToDecimal(erpDetail.PurchaseQty),
SubQty = ToDecimal(erpDetail.DeliveryQty),
Quantity = ToDecimal(erpDetail.IncludeQty),
PurchaseUnit = TrimOrNull(erpDetail.PurchaseUnit),
InventoryUnit = TrimOrNull(erpDetail.InventoryUnit),
Memo = TrimOrNull(erpDetail.Remark),
EbelnK3id = ToInt(erpDetail.FSrcBillNo),
LineK3id = ToInt(erpDetail.FSrcBillLine),
SalesOrderId = TrimOrNull(erpDetail.SalesOrderId),
MtoNo = TrimOrNull(erpDetail.FMtoNo),
Lot = TrimOrNull(erpDetail.FLot),
UrgentFlag = ToBool(erpDetail.urgentFlag),
DepotId = erpDetail.DepotId
};
list.Add(detail);
}
return list;
}
///
/// 按单据号查询(主+明细)
///
public (MesInvItemArn main, List details) GetByBillNo(string billNo)
{
if (string.IsNullOrWhiteSpace(billNo))
throw new ArgumentNullException(nameof(billNo), "查询条件单据号不能为空");
var no = billNo.Trim();
// SqlSugar: First()
var main = Context.Queryable()
.Where(m => m.BillNo == no)
.First();
var details = main != null
? Context.Queryable().Where(d => d.Mid == main.Id).ToList()
: new List();
return (main, details);
}
#region 安全转换/工具方法
private static string TrimOrNull(string s) =>
string.IsNullOrWhiteSpace(s) ? null : s.Trim();
private static int ToInt(object value, int @default = 0)
{
switch (value)
{
case null: return @default;
case int i: return i;
case long l: return unchecked((int)l);
case short sh: return sh;
case byte b: return b;
case decimal dm: return (int)dm;
case double db: return (int)db;
case float f: return (int)f;
case string s when int.TryParse(s, out var n): return n;
default: return @default;
}
}
private static decimal ToDecimal(object value, decimal @default = 0m)
{
switch (value)
{
case null: return @default;
case decimal dm: return dm;
case double db: return (decimal)db;
case float f: return (decimal)f;
case int i: return i;
case long l: return l;
case string s when decimal.TryParse(s, out var n): return n;
default: return @default;
}
}
private static bool ToBool(object value, bool @default = false)
{
switch (value)
{
case null: return @default;
case bool b: return b;
case int i: return i != 0;
case long l: return l != 0;
case string s:
var t = s.Trim().ToLowerInvariant();
return t switch
{
"1" => true,
"0" => false,
"y" => true,
"n" => false,
"true" => true,
"false" => false,
_ => @default
};
default: return @default;
}
}
private static DateTime? ToDate(object value)
{
switch (value)
{
case null: return null;
case DateTime dt: return dt;
case string s when DateTime.TryParse(s, out var d): return d;
default: return null;
}
}
#endregion
}
}