kyy
6 天以前 dd3461812f84ac4f930d3c51e5b8eba92fea4dab
MES.Service/service/BasicData/MesInvItemArnManager.cs
@@ -7,257 +7,352 @@
using System.Collections.Generic;
using System.Linq;
namespace MES.Service.service;
/// <summary>
/// 收料单数据管理类(处理ERP收料单数据同步到MES收料单表)
/// 关联主表:MesInvItemArn,明细表:MesInvItemArnDetail
/// 关联ERP DTO:ErpSltz(聚合类)、ErpSltza(ERP主表)、ErpSltzBList(ERP明细)
/// </summary>
public class MesInvItemArnManager : Repository<MesInvItemArn>
namespace MES.Service.service
{
    /// <summary>
    /// 批量保存收料单(主表+明细)- 事务保证批量操作一致性
    /// 收料单数据管理类(处理ERP收料单数据同步到MES收料单表)
    /// 关联主表:MesInvItemArn(MES收料单主表)
    /// 关联明细表:MesInvItemArnDetail(MES收料单明细表)
    /// 关联ERP DTO:ErpSltz(聚合类,含主表+明细)、ErpSltza(ERP收料单主表)、ErpSltzBList(ERP收料单明细)
    /// </summary>
    /// <param name="erpSltzList">ERP收料单列表(含主表+明细)</param>
    /// <returns>全部成功返回true,否则false</returns>
    public bool SaveList(List<ErpSltz> erpSltzList)
    public class MesInvItemArnManager : Repository<MesInvItemArn>
    {
        if (erpSltzList == null || !erpSltzList.Any())
            throw new ArgumentNullException(nameof(erpSltzList), "待保存的收料单列表不能为空");
        // 修复:用事务包裹批量操作,确保数据一致性
        return UseTransaction(db =>
        /// <summary>
        /// 批量保存收料单(主表+明细)- 事务保证批量操作一致性
        /// 单条失败则整体回滚
        /// </summary>
        public bool SaveList(List<ErpSltz> erpSltzList)
        {
            foreach (var erpSltz in erpSltzList)
            if (erpSltzList == null || !erpSltzList.Any())
                throw new ArgumentNullException(nameof(erpSltzList), "待保存的收料单列表不能为空");
            return UseTransaction(db =>
            {
                // 修复:调用事务内的Save方法,传入db实例
                if (!Save(db, erpSltz))
                foreach (var erpSltz in erpSltzList)
                {
                    return 0; // 单条失败,事务回滚
                    if (!SaveInTransaction(db, erpSltz))
                        return 0; // 触发回滚
                }
            }
            return 1; // 全部成功
        }) > 0;
    }
    /// <summary>
    /// 单条保存收料单(主表+明细)- 事务内调用(修正访问修饰符,确保事务内可用)
    /// </summary>
    /// <param name="db">事务内的SqlSugar实例</param>
    /// <param name="erpSltz">ERP收料单(含主表+明细)</param>
    /// <returns>单条处理成功返回true</returns>
    private bool Save(SqlSugarScope db, ErpSltz erpSltz)
    {
        // 1. 参数校验
        if (erpSltz == null)
            throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空");
        if (erpSltz.ErpSltza == null)
            throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空");
        // 2. 提取ERP主表和明细数据
        var erpMain = erpSltz.ErpSltza;
        var erpDetails = erpSltz.ErpSltzBList ?? new List<ErpSltzBList>();
        // 3. 修复:用事务内的db查询,确保数据一致性
        Guid mainId = GetOrCreateMainId(db, erpMain);
        // 4. 映射ERP数据到MES实体(修复字段注释,补充关键属性)
        var mesMain = MapErpSltzaToMesInvItemArn(erpMain, mainId);
        var mesDetails = MapErpSltzBListToMesInvItemArnDetail(erpDetails, mainId);
        // 5. 根据操作类型处理数据
        switch (erpMain.TYPE?.Trim())
        {
            case "1": // 新增
            case "2": // 更新
                return HandleSaveOrUpdate(db, mesMain, mesDetails, mainId);
            case "3": // 删除
                return HandleDelete(db, mesMain, mesDetails, mainId);
            default:
                throw new NotImplementedException($"未实现的收料单操作类型:{erpMain.TYPE}(支持类型:1-新增,2-更新,3-删除)");
                return 1; // 提交
            }) > 0;
        }
    }
    /// <summary>
    /// 单条保存收料单(主表+明细)- 对外公开接口(供控制器调用)
    /// </summary>
    /// <param name="erpSltz">ERP收料单数据(含主表+明细)</param>
    /// <returns>保存成功返回true</returns>
    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 =>
        /// <summary>
        /// 对外单条保存(内部自动事务)
        /// </summary>
        public bool Save(ErpSltz erpSltz)
        {
            return Save(db, erpSltz) ? 1 : 0;
        }) > 0;
    }
            if (erpSltz == null) throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空");
            if (erpSltz.ErpSltza == null) throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空");
    /// <summary>
    /// 提前获取或生成MES主表ID(修复:用事务内db查询)
    /// </summary>
    private Guid GetOrCreateMainId(SqlSugarScope db, ErpSltza erpMain)
    {
        if (string.IsNullOrEmpty(erpMain.billNo))
            throw new ArgumentException("ERP收料单单据号不能为空,无法确定主表唯一性", nameof(erpMain.billNo));
            return UseTransaction(db => SaveInTransaction(db, erpSltz) ? 1 : 0) > 0;
        }
        // 修复:用事务内的db查询,而非Context
        var existingMain = db.Queryable<MesInvItemArn>()
            .Where(m => m.BillNo == erpMain.billNo)
            .First();
        return existingMain != null
            ? existingMain.Id
            : Guid.NewGuid();
    }
    /// <summary>
    /// 处理收料单新增/更新(主表+明细)
    /// </summary>
    private bool HandleSaveOrUpdate(SqlSugarScope db, MesInvItemArn mesMain, List<MesInvItemArnDetail> mesDetails, Guid mainId)
    {
        mesMain.Id = mainId;
        bool isMainExist = db.Queryable<MesInvItemArn>().Where(m => m.Id == mainId).Any();
        int mainOperateResult;
        if (isMainExist)
        /// <summary>
        /// 事务内单条保存
        /// </summary>
        private bool SaveInTransaction(SqlSugarScope db, ErpSltz erpSltz)
        {
            mainOperateResult = db.Updateable(mesMain)
                .IgnoreColumns(m => new { m.CreateDate })
            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<ErpSltzBList>();
            // 通过 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-删除)")
            };
        }
        /// <summary>
        /// 获取或生成主表ID(按 ERP DeliveryNo 唯一)
        /// </summary>
        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<MesInvItemArn>()
                .Where(m => m.BillNo == billNo)
                .First();
            return existingMain?.Id ?? Guid.NewGuid();
        }
        /// <summary>
        /// 新增/更新(主表+明细:先删旧明细再插新)
        /// </summary>
        private bool HandleSaveOrUpdate(SqlSugarScope db, MesInvItemArn mesMain, List<MesInvItemArnDetail> mesDetails,
            Guid mainId)
        {
            mesMain.Id = mainId;
            bool isMainExist = db.Queryable<MesInvItemArn>()
                .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<MesInvItemArnDetail>()
                .Where(d => d.Mid == mainId)
                .ExecuteCommand();
            int insertNewDetailsResult = mesDetails.Count > 0
                ? db.Insertable(mesDetails).ExecuteCommand()
                : 1;
            return mainOperateResult > 0 && deleteOldDetailsResult >= 0 && insertNewDetailsResult > 0;
        }
        /// <summary>
        /// 删除(先删明细再删主表)
        /// </summary>
        private bool HandleDelete(SqlSugarScope db, Guid mainId)
        {
            db.Deleteable<MesInvItemArnDetail>()
                .Where(d => d.Mid == mainId)
                .ExecuteCommand();
            int deleteMainResult = db.Deleteable<MesInvItemArn>()
                .Where(m => m.Id == mainId)
                .ExecuteCommand();
        }
        else
        {
            mesMain.CreateDate = DateTime.Now;
            mainOperateResult = db.Insertable(mesMain).ExecuteCommand();
            return deleteMainResult >= 0;
        }
        // 处理明细
        int deleteOldDetailResult = db.Deleteable<MesInvItemArnDetail>()
            .Where(d => d.Mid == mainId)
            .ExecuteCommand();
        int insertNewDetailResult = mesDetails.Count > 0
            ? db.Insertable(mesDetails).ExecuteCommand()
            : 1;
        return mainOperateResult > 0 && (deleteOldDetailResult >= 0 && insertNewDetailResult > 0);
    }
    /// <summary>
    /// 处理收料单删除(主表+明细)
    /// </summary>
    private bool HandleDelete(SqlSugarScope db, MesInvItemArn mesMain, List<MesInvItemArnDetail> mesDetails, Guid mainId)
    {
        db.Deleteable<MesInvItemArnDetail>().Where(d => d.Mid == mainId).ExecuteCommand();
        int mainDeleteResult = db.Deleteable<MesInvItemArn>().Where(m => m.Id == mainId).ExecuteCommand();
        return mainDeleteResult >= 0;
    }
    /// <summary>
    /// ERP主表映射到MES主表(修复:取消关键字段注释,修正FType逻辑)
    /// </summary>
    private MesInvItemArn MapErpSltzaToMesInvItemArn(ErpSltza erpMain, Guid mainId)
    {
        // 单据日期转换
        DateTime.TryParse(erpMain.FDate, out DateTime parsedDate);
        // 是否SRM转换(0-否,1-是)
        int.TryParse(erpMain.F_ZJXF_sfgx, out int isSrm);
        // 供应商ID转换(字符串直接赋值,匹配MES字段类型)
        string suppId = string.IsNullOrEmpty(erpMain.SupplierId) ? null : erpMain.SupplierId.Trim();
        // 修复:“是否委外”逻辑(假设ERP的fType是“1”=是,“0”=否,或bool字符串)
        bool isOutsourcing = false;
        if (!string.IsNullOrEmpty(erpMain.fType))
        /// <summary>
        /// ERP 主表 -> MES 主表
        /// </summary>
        private MesInvItemArn MapErpMainToMesMain(ErpSltza erpMain, Guid mainId, SqlSugarScope db)
        {
            isOutsourcing = erpMain.fType.Trim() == "1" || erpMain.fType.Trim().Equals("true", StringComparison.OrdinalIgnoreCase);
        }
        return new MesInvItemArn
        {
            // 修复:取消关键字段注释,补充赋值
            Id = mainId,
            BillNo = erpMain.billNo?.Trim() ?? throw new ArgumentException("收料单单据号不能为空"),
            SuppId = suppId,
            CreateDate = parsedDate == DateTime.MinValue ? null : (DateTime?)parsedDate,
            Remark = erpMain.Remark?.Trim(),
            IsSrm = isSrm,
            EbelnK3id = erpMain.erpId?.Trim(), // ERP主表ID(对应ebeln_k3id)
            CreateBy = erpMain.createBy?.Trim() ?? "SYSTEM", // 默认为系统
            FType = isOutsourcing, // 修复:正确的是否委外逻辑
            ReceiveOrgId = erpMain.ReceiveOrgId?.Trim(),
            // LastUpdateUser = erpMain.FCreatorId?.Trim() ?? "SYSTEM",
            // LastUpdateTime = DateTime.Now,
            // IsOut = false, // 初始未出库
            // Status = false // 初始未审核
        };
    }
    /// <summary>
    /// ERP明细映射到MES明细(修复:取消关键字段注释,修正EbelnK3id转换)
    /// </summary>
    private List<MesInvItemArnDetail> MapErpSltzBListToMesInvItemArnDetail(List<ErpSltzBList> erpDetails, Guid mainId)
    {
        return erpDetails.Select(erpDetail =>
        {
            // 数值类型安全转换
            int.TryParse(erpDetail.LineNo, out int lineNo); // 明细主键
            int.TryParse(erpDetail.ProductCode, out int itemId); // 物料编码
            decimal.TryParse(erpDetail.PurchaseQty, out decimal purchaseQty); // 采购订单数量
            decimal.TryParse(erpDetail.DeliveryQty, out decimal deliveryQty); // 本次应收数量
            decimal.TryParse(erpDetail.IncludeQty, out decimal includeQty); // 本次实收数量
            // 修复:用采购单ID字段(ebeln_k3id)转int,而非源单单号
            int.TryParse(erpDetail.FSrcBillNo, out int ebelnK3id);
            int.TryParse(erpDetail.FSrcBillLine, out int lineK3id); // 采购单行ID
            bool.TryParse(erpDetail.urgentFlag, out bool urgentFlag); // 急料标识
            return new MesInvItemArnDetail
            var billNo = TrimOrNull(erpMain.DeliveryNo)
                         ?? throw new ArgumentException("收料单单据号不能为空");
            // 获取供应商编号并查询对应的ID
            string suppNo = TrimOrNull(erpMain.SupplierId);
            decimal ? suppId = null;
            if (!string.IsNullOrEmpty(suppNo))
            {
                // 修复:取消关键字段注释,补充赋值
            //    LineNo = lineNo, // 明细主键(必须赋值)
                Mid = mainId, // 外键(关联主表ID,必须赋值)
                Ebeln = erpDetail.FBillNo?.Trim(), // 采购单号
                ItemId = itemId,
                EbelnQty = purchaseQty,
                SubQty = deliveryQty,
                Quantity = includeQty,
              //  PurchaseUnit = erpDetail.PurchaseUnit?.Trim(), // 采购单位(补充赋值)
              //  InventoryUnit = erpDetail.InventoryUnit?.Trim(), // 库存单位(补充赋值)
                Memo = erpDetail.Remark?.Trim(),
                EbelnK3id = ebelnK3id, // 修复:正确的采购单ID转换
                LineK3id = lineK3id,
             //   SalesOrderId = erpDetail.SalesOrderId?.Trim(), // 销售订单号(补充赋值)
              //  FMtoNo = erpDetail.FMtoNo?.Trim(), // 计划跟踪号(补充赋值)
             //   FLot = erpDetail.FLot?.Trim(), // 批号(补充赋值)
                UrgentFlag = urgentFlag
                //FStockId = erpDetail.DepotId?.Trim() // 收货仓库编号
                // 从数据库查询供应商ID(SuppNo唯一)
                suppId = db.Queryable<MesSupplier>()
                    .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)
            };
        }).ToList();
    }
        }
    /// <summary>
    /// 按单据号查询MES收料单(主表+明细)
    /// </summary>
    public (MesInvItemArn main, List<MesInvItemArnDetail> details) GetByBillNo(string billNo)
    {
        if (string.IsNullOrEmpty(billNo))
            throw new ArgumentNullException(nameof(billNo), "查询条件单据号不能为空");
        /// <summary>
        /// ERP 明细 -> MES 明细
        /// 核心:通过 ERP ProductCode 匹配 mes_items.item_no 获取 item_id
        /// </summary>
        private List<MesInvItemArnDetail> MapErpDetailsToMesDetails(SqlSugarScope db, List<ErpSltzBList> erpDetails,
            Guid mainId)
        {
            var productCodes = erpDetails
                .Where(d => !string.IsNullOrWhiteSpace(d.ProductCode))
                .Select(d => d.ProductCode.Trim())
                .Distinct()
                .ToList();
        var main = Context.Queryable<MesInvItemArn>()
            .Where(m => m.BillNo == billNo)
            .First();
            // 先 ToList 再 ToDictionary,避免直接对 ISugarQueryable 调用拓展方法失败
            var itemPairs = db.Queryable<MesItems>()
                .Where(item => productCodes.Contains(item.ItemNo))
                .Select(item => new { item.ItemNo, item.Id })
                .ToList();
        var details = main != null
            ? Context.Queryable<MesInvItemArnDetail>()
                .Where(d => d.Mid == main.Id)
                .ToList()
            : new List<MesInvItemArnDetail>();
            var itemMap = itemPairs.ToDictionary(k => k.ItemNo, v => v.Id);
        return (main, details);
            var list = new List<MesInvItemArnDetail>(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 = ToInt(erpDetail.DepotId)
                };
                list.Add(detail);
            }
            return list;
        }
        /// <summary>
        /// 按单据号查询(主+明细)
        /// </summary>
        public (MesInvItemArn main, List<MesInvItemArnDetail> details) GetByBillNo(string billNo)
        {
            if (string.IsNullOrWhiteSpace(billNo))
                throw new ArgumentNullException(nameof(billNo), "查询条件单据号不能为空");
            var no = billNo.Trim();
            // SqlSugar: First()
            var main = Context.Queryable<MesInvItemArn>()
                .Where(m => m.BillNo == no)
                .First();
            var details = main != null
                ? Context.Queryable<MesInvItemArnDetail>().Where(d => d.Mid == main.Id).ToList()
                : new List<MesInvItemArnDetail>();
            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
    }
}