using AutoMapper; using MailKit.Search; using Newtonsoft.Json; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using SqlSugar; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using WIDESEA_Common.StockEnum; using WIDESEA_Core; using WIDESEA_Core.BaseServices; using WIDESEA_Core.Enums; using WIDESEA_Core.Helper; using WIDESEA_DTO.Stock; using WIDESEA_IBasicRepository; using WIDESEA_IOutboundRepository; using WIDESEA_IRecordRepository; using WIDESEA_IRecordService; using WIDESEA_IStockRepository; using WIDESEA_IStockService; using WIDESEA_Model.Models; using WIDESEA_StockRepository; namespace WIDESEA_StockService { public partial class StockInfoService : ServiceBase, IStockInfoService { private readonly IMapper _mapper; private readonly IBasicRepository _basicRepository; private readonly IOutboundRepository _outboundRepository; public IStockInfoRepository Repository => BaseDal; public StockInfoService(IStockInfoRepository BaseDal, IMapper mapper, IBasicRepository basicRepository, IOutboundRepository outboundRepository) : base(BaseDal) { _mapper = mapper; _basicRepository = basicRepository; _outboundRepository = outboundRepository; } /// /// 查询订单立库库存视图 /// /// /// /// public List GetStockSelectViews(int orderId, string materielCode) { try { Dt_OutboundOrder outboundOrder = _outboundRepository.OutboundOrderRepository.QueryFirst(x => x.Id == orderId); if (outboundOrder == null) { throw new Exception($"未找到出库单信息"); } List locationCodes = _basicRepository.LocationInfoRepository.PPGetCanOutLocationCodes(outboundOrder.WarehouseId); return BaseDal.QueryTabs((a, b) => a.Id == b.StockId, (a, b) => new StockSelectViewDTO { LocationCode = a.LocationCode, MaterielCode = b.MaterielCode, MaterielName = b.MaterielName, PalletCode = a.PalletCode, UseableQuantity = b.StockQuantity - b.OutboundQuantity }, a => locationCodes.Contains(a.LocationCode), b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new StockSelectViewDTO { LocationCode = x.FirstOrDefault()?.LocationCode ?? "", MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "", MaterielName = x.FirstOrDefault()?.MaterielName ?? "", PalletCode = x.Key, UseableQuantity = x.Sum(x => x.UseableQuantity) }).ToList(); } catch (Exception ex) { return null; } } /// /// 查询订单平库库存视图 /// /// /// /// public List GetPKStockSelectViews(int orderId, string materielCode) { try { Dt_OutboundOrder outboundOrder = _outboundRepository.OutboundOrderRepository.QueryFirst(x => x.Id == orderId); if (outboundOrder == null) { throw new Exception($"未找到出库单信息"); } return BaseDal.QueryTabs((a, b) => a.Id == b.StockId && a.WarehouseId == outboundOrder.WarehouseId, (a, b) => new StockSelectViewDTO { LocationCode = a.LocationCode, MaterielCode = b.MaterielCode, MaterielName = b.MaterielName, PalletCode = a.PalletCode, UseableQuantity = b.StockQuantity - b.OutboundQuantity }, a => a.LocationCode == "平库位", b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new StockSelectViewDTO { LocationCode = x.FirstOrDefault()?.LocationCode ?? "", MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "", MaterielName = x.FirstOrDefault()?.MaterielName ?? "", PalletCode = x.Key, UseableQuantity = x.Sum(x => x.UseableQuantity) }).ToList(); } catch (Exception ex) { return null; } } /// /// 查询订单PP立库库存视图 /// /// /// /// public List PPGetStockSelectViews(int orderId, string materielCode) { try { Dt_MesPPCutOutboundOrder mesPPCutOutboundOrder = _outboundRepository.MesPPCutOutboundOrderRepository.QueryFirst(x => x.Id == orderId); if (mesPPCutOutboundOrder == null) { throw new Exception($"未找到出库单信息"); } List locationCodes = _basicRepository.LocationInfoRepository.PPGetCanOutLocationCodes(mesPPCutOutboundOrder.WarehouseId); return BaseDal.QueryTabs((a, b) => a.Id == b.StockId, (a, b) => new PPStockSelectViewDTO { LocationCode = a.LocationCode, MaterielCode = b.MaterielCode, MaterielName = b.MaterielName, PalletCode = a.PalletCode, Unit = b.Unit, CutedWidth = b.CutedWidth, UseableQuantity = b.StockQuantity - b.OutboundQuantity }, a => locationCodes.Contains(a.LocationCode), b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new PPStockSelectViewDTO { LocationCode = x.FirstOrDefault()?.LocationCode ?? "", MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "", MaterielName = x.FirstOrDefault()?.MaterielName ?? "", Unit = x.FirstOrDefault()?.Unit ?? "", CutedWidth = x.Sum(x => x.CutedWidth), PalletCode = x.Key, UseableQuantity = x.Sum(x => x.UseableQuantity) }).ToList(); } catch (Exception ex) { return null; } } /// /// 查询订单PP平库库存视图 /// /// /// /// public List PPGetPKStockSelectViews(int orderId, string materielCode) { try { Dt_MesPPCutOutboundOrder outboundOrder = _outboundRepository.MesPPCutOutboundOrderRepository.QueryFirst(x => x.Id == orderId); if (outboundOrder == null) { throw new Exception($"未找到出库单信息"); } return BaseDal.QueryTabs((a, b) => a.Id == b.StockId && a.WarehouseId == outboundOrder.WarehouseId, (a, b) => new PPStockSelectViewDTO { LocationCode = a.LocationCode, MaterielCode = b.MaterielCode, MaterielName = b.MaterielName, PalletCode = a.PalletCode, Unit = b.Unit, CutedWidth = b.CutedWidth, UseableQuantity = b.StockQuantity - b.OutboundQuantity }, a => a.LocationCode.Contains("AGV_PP"), b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new PPStockSelectViewDTO { LocationCode = x.FirstOrDefault()?.LocationCode ?? "", MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "", MaterielName = x.FirstOrDefault()?.MaterielName ?? "", Unit = x.FirstOrDefault()?.Unit ?? "", CutedWidth = x.Sum(x => x.CutedWidth), PalletCode = x.Key, UseableQuantity = x.Sum(x => x.UseableQuantity) }).ToList(); } catch (Exception ex) { return null; } } public WebResponseContent StockQueryData(SaveModel saveModel) { try { var barcode = saveModel.MainData["barcode"].ToString(); var warehouseId = saveModel.MainData["warehouseId"].ObjToInt(); Dt_StockInfo stockInfo = BaseDal.Db.Queryable().Where(x => x.PalletCode == barcode && x.WarehouseId == warehouseId).Includes(x => x.Details).First(); if (stockInfo == null) throw new Exception("未找到托盘信息"); return WebResponseContent.Instance.OK(data: stockInfo); } catch (Exception ex) { return WebResponseContent.Instance.Error(ex.Message); } } /// /// /// /// /// /// /// /// public List GetOutboundStocks(List stockInfos, string materielCode, float needQuantity, out float residueQuantity) { List outStocks = new List(); float stockTotalQuantity = stockInfos.Select(x => x.Details.Sum(v => v.StockQuantity - v.OutboundQuantity)).Sum(x => x); //stockInfos = stockInfos.OrderBy(x => x.Id).ToList(); if (stockTotalQuantity >= needQuantity)//库存够 { int index = 0; while (needQuantity > 0) { Dt_StockInfo stockInfo = stockInfos[index]; // 计算可用库存时转换为decimal decimal useableStockQuantity = stockInfo.Details .Where(x => x.MaterielCode == materielCode) .Sum(x => (decimal)x.StockQuantity - (decimal)x.OutboundQuantity); // 将needQuantity转换为decimal进行比较 if (useableStockQuantity < (decimal)needQuantity && useableStockQuantity > 0) { stockInfo.Details.ForEach(x => x.OutboundQuantity = x.StockQuantity); // 使用decimal进行计算后再转回float needQuantity = (float)((decimal)needQuantity - useableStockQuantity); } else { stockInfo.Details.ForEach(x => { if (x.StockQuantity > x.OutboundQuantity && x.MaterielCode == materielCode) { // 将相关值转换为decimal进行精确计算 decimal currentStock = (decimal)x.StockQuantity; decimal currentOutbound = (decimal)x.OutboundQuantity; decimal currentNeed = (decimal)needQuantity; decimal available = currentStock - currentOutbound; if (available >= currentNeed) { x.OutboundQuantity = (float)(currentOutbound + currentNeed); needQuantity = 0; } else { needQuantity = (float)(currentNeed - available); x.OutboundQuantity = x.StockQuantity; } } }); } outStocks.Add(stockInfo); index++; } } else { throw new Exception("库存不足"); } residueQuantity = needQuantity; return outStocks; } public List GetUseableStocks(string materielCode, string batchNo, int warehoseId) { if ((materielCode.Equals("405000585")|| materielCode.Equals("405000831") || materielCode.Equals("405005565") || materielCode.Equals("405405097") || materielCode.Equals("405005461")) && warehoseId == 5) { warehoseId = 3; } List locationCodes = _basicRepository.LocationInfoRepository.GetCanOutLocationCodes(warehoseId); return BaseDal.GetStockInfos(materielCode, batchNo, locationCodes); } public List GetUseableStocks(string materielCode, string batchNo, string palletcode, int warehoseId) { Dt_StockInfo stockInfo = BaseDal.Db.Queryable().Where(x => x.PalletCode == palletcode && x.WarehouseId == warehoseId).Includes(x => x.Details).First(); List locationCodes = _basicRepository.LocationInfoRepository.GetCanOutLocationCodes(stockInfo.LocationCode); return BaseDal.GetStockInfos(materielCode, batchNo, locationCodes); } public WebResponseContent UpdateExpirationlabel() { try { var today = DateTime.Today; int batchSize = 1000; int totalUpdated = 0; int skipCount = 0; // 只查询需要的字段,减少数据传输和内存占用 var query = BaseDal.Db.Queryable() .InnerJoin((detail, master) => detail.StockId == master.Id) .Select((detail, master) => new { MasterId = master.Id, master.WarehouseId, detail.EffectiveDate, CurrentExpirationlabel = master.Expirationlabel }); while (true) { var batchData = query.Skip(skipCount).Take(batchSize).ToList(); if (!batchData.Any()) break; var groupedData = batchData.GroupBy(item => item.MasterId) .Select(g => new { MasterId = g.Key, WarehouseId = g.First().WarehouseId, // 取最早的有效日期 EarliestEffectiveDate = g.Min(item => { DateTime.TryParse(item.EffectiveDate, out DateTime date); return date; }), CurrentExpirationlabel = g.First().CurrentExpirationlabel }) .ToList(); var updateDic = new Dictionary(); foreach (var group in groupedData) { DateTime effectiveDate = group.EarliestEffectiveDate; if (effectiveDate == default(DateTime)) // 处理解析失败的情况 { Console.WriteLine($"主表ID {group.MasterId} 下无有效日期,跳过"); continue; } int newLabel; if (effectiveDate < today) { newLabel = ExpirationlabelEnum.过期.ObjToInt(); } else if (group.WarehouseId == 3) { int daysDiff = (effectiveDate - today).Days; newLabel = daysDiff < 60 ? ExpirationlabelEnum.临期预警.ObjToInt() : ExpirationlabelEnum.未临期.ObjToInt(); } else { int daysDiff = (effectiveDate - today).Days; newLabel = daysDiff < 30 ? ExpirationlabelEnum.临期预警.ObjToInt() : ExpirationlabelEnum.未临期.ObjToInt(); } if (newLabel != group.CurrentExpirationlabel && !updateDic.ContainsKey(group.MasterId)) { updateDic[group.MasterId] = newLabel; } } if (updateDic.Any()) { var updateBuilder = BaseDal.Db.Updateable(); var idsToUpdate = updateDic.Keys.ToList(); int updateValue = updateDic.First().Value; updateBuilder.SetColumns(m => m.Expirationlabel == updateValue) .Where(m => idsToUpdate.Contains(m.Id)); int batchUpdated = updateBuilder.ExecuteCommand(); totalUpdated += batchUpdated; Console.WriteLine($"批次更新:{batchUpdated} 条,累计更新:{totalUpdated} 条,更新条件:{JsonConvert.SerializeObject(idsToUpdate)}"); } skipCount += batchSize; } return WebResponseContent.Instance.OK($"更新成功,共更新 {totalUpdated} 条记录"); } catch (Exception ex) { return WebResponseContent.Instance.Error("更新失败,请联系管理员"); } } /// /// 钉钉机器人消息推送测试 /// /// public async Task T0DingTalkText(string webhookUrl, string secret) { try { if (webhookUrl == null || secret == null) { webhookUrl = "https://oapi.dingtalk.com/robot/send?access_token=c3e05f2c6bcd595383ee02e713446174b9201bad91db216590620fe0acd4e75e"; secret = "SEC617f06140fc7cbd8b91d3e203f270826320637af41e7423d756e62df40f62282"; } // 1. 定义仓库ID与仓库名称的映射关系(增加成品仓) var warehouseIdToName = new Dictionary { { 2, "油墨仓" }, { 3, "板材仓" }, { 4, "PP仓" }, { 6, "测试架仓" }, { 7, "成品仓" }, // 新增成品仓 { 11, "干膜仓" }, { 12, "阻焊仓" } }; // 2. 时间相关配置,用于筛选超过90天的数据 var currentTime = DateTime.Now; var ninetyDaysAgo = currentTime.AddDays(-90); // 3. 处理原材料仓(仓库ID ≠ 7) // 3.1 查询标识码为3(过期)的主表数据(排除仓库ID=5和7) var expirationLabel3Stocks = BaseDal.Db.Queryable() .Where(s => s.Expirationlabel == 3 && s.WarehouseId != 5 && s.WarehouseId != 7 && s.LocationCode != "" && s.LocationCode != null) .ToList(); // 3.2 查询超过90天未修改(未使用)的主表数据(排除仓库ID=5和7) var over90DaysStocks = BaseDal.Db.Queryable() .Where(s => s.ModifyDate <= ninetyDaysAgo && s.Expirationlabel != 3 && s.WarehouseId != 5 && s.WarehouseId != 7 && s.LocationCode != "" && s.LocationCode != null) .ToList(); // 4. 处理成品仓(仓库ID = 7) var proOver90DaysStocks = new List(); if (warehouseIdToName.ContainsKey(7)) { proOver90DaysStocks = BaseDal.Db.Queryable() .Where(s => s.WarehouseId == 7 && s.ModifyDate <= ninetyDaysAgo && s.LocationCode != "" && s.LocationCode != null) .ToList(); } // 无符合条件数据时直接返回 if (!expirationLabel3Stocks.Any() && !over90DaysStocks.Any() && !proOver90DaysStocks.Any()) { return; } // 5. 提取原材料主表数据的ID var expirationLabel3StockIds = expirationLabel3Stocks.Select(s => s.Id).ToList(); var over90DaysStockIds = over90DaysStocks.Select(s => s.Id).ToList(); // 6. 关联查询原材料明细表数据 var expirationLabel3Details = expirationLabel3StockIds.Any() ? BaseDal.Db.Queryable() .Where(d => expirationLabel3StockIds.Contains(d.StockId)) .ToList() : new List(); var over90DaysDetails = over90DaysStockIds.Any() ? BaseDal.Db.Queryable() .Where(d => over90DaysStockIds.Contains(d.StockId)) .ToList() : new List(); // 7. 处理成品仓明细数据 var proOver90DaysDetails = new List(); if (proOver90DaysStocks.Any()) { var proStockIds = proOver90DaysStocks.Select(s => s.Id).ToList(); proOver90DaysDetails = BaseDal.Db.Queryable() .Where(d => proStockIds.Contains(d.ProStockId)) .ToList(); } // 8. 建立主表ID到仓库ID的映射(包括成品仓) var stockIdToWarehouseId = new Dictionary(); // 原材料仓映射 foreach (var stock in expirationLabel3Stocks.Concat(over90DaysStocks)) { if (!stockIdToWarehouseId.ContainsKey(stock.Id)) { stockIdToWarehouseId[stock.Id] = stock.WarehouseId; } } // 成品仓映射 foreach (var proStock in proOver90DaysStocks) { if (!stockIdToWarehouseId.ContainsKey(proStock.Id)) { stockIdToWarehouseId[proStock.Id] = proStock.WarehouseId; } } // 9. 按仓库分组处理物料数据 // 9.1 原材料过期物料 var expirationLabel3Groups = expirationLabel3Details .GroupBy(d => stockIdToWarehouseId[d.StockId]) .ToDictionary(g => g.Key, g => g.Select(d => new { MaterielCode = d.MaterielCode, BatchNo = d.BatchNo, IsProStock = false // 标记为非成品仓 }).Distinct().ToList()); // 9.2 原材料超过90天未使用物料 var over90DaysGroups = over90DaysDetails .GroupBy(d => stockIdToWarehouseId[d.StockId]) .ToDictionary(g => g.Key, g => g.Select(d => new { MaterielCode = d.MaterielCode, BatchNo = d.BatchNo, IsProStock = false // 标记为非成品仓 }).Distinct().ToList()); // 9.3 成品仓超过90天未使用物料 if (proOver90DaysDetails.Any()) { var proWarehouseId = 7; var proGroup = proOver90DaysDetails .GroupBy(d => stockIdToWarehouseId[d.ProStockId]) .Select(g => new { WarehouseId = g.Key, Materials = g.Select(d => new { MaterielCode = d.ProductCode, // 成品仓使用ProductCode字段 BatchNo = d.LotNumber, // 成品仓使用LotNumber字段 IsProStock = true // 标记为成品仓 }).Distinct().ToList() }) .FirstOrDefault(); if (proGroup != null) { // 添加到over90DaysGroups中 if (over90DaysGroups.ContainsKey(proWarehouseId)) { over90DaysGroups[proWarehouseId].AddRange(proGroup.Materials); } else { over90DaysGroups[proWarehouseId] = proGroup.Materials; } } } // 10. 获取所有涉及的仓库ID var allWarehouseIds = expirationLabel3Groups.Keys .Union(over90DaysGroups.Keys) .ToList(); // 11. 发送钉钉消息 using (HttpClient httpClient = new HttpClient()) { foreach (var warehouseId in allWarehouseIds) { var warehouseName = warehouseIdToName.TryGetValue(warehouseId, out var name) ? name : $"仓库{warehouseId}"; // 11.1 构建markdown格式消息 var markdownContent = new StringBuilder(); markdownContent.AppendLine($"## {warehouseName}物料提醒通知\n"); // 统计信息 var expiredCount = expirationLabel3Groups.TryGetValue(warehouseId, out var expList) ? expList.Count : 0; var over90Count = over90DaysGroups.TryGetValue(warehouseId, out var over90List) ? over90List.Count : 0; // 如果是成品仓,需要过滤出成品仓的数据 if (warehouseId == 7) { over90Count = over90List?.Count(m => m.IsProStock) ?? 0; } markdownContent.AppendLine($"**统计概览:**"); if (warehouseId == 7) { markdownContent.AppendLine($"- 超过90天未使用成品:{over90Count}条"); } else { markdownContent.AppendLine($"- 过期物料:{expiredCount}条"); markdownContent.AppendLine($"- 超过90天未使用物料:{over90Count}条"); } markdownContent.AppendLine(); // 11.2 添加过期物料表格(如果有数据且不是成品仓) if (expiredCount > 0 && warehouseId != 7) { markdownContent.AppendLine("### 一、过期物料"); markdownContent.AppendLine("| 序号 | 物料编码 | 批次号 |"); markdownContent.AppendLine("| :--- | :--- | :--- |"); int index = 1; var expiredToShow = expList.Take(200); foreach (var material in expiredToShow) { markdownContent.AppendLine($"| {index} | {material.MaterielCode} | {material.BatchNo} |"); index++; } if (expiredCount > 200) { markdownContent.AppendLine($"| ... | 共{expiredCount}条,仅显示前200条 | ... |"); } markdownContent.AppendLine(); } // 11.3 添加超过90天未使用物料表格(如果有数据) if (over90Count > 0) { if (warehouseId == 7) { markdownContent.AppendLine("### 超过90天未使用成品"); } else { markdownContent.AppendLine("### 二、超过90天未使用物料"); } markdownContent.AppendLine("| 序号 | 物料编码 | 批次号 |"); markdownContent.AppendLine("| :--- | :--- | :--- |"); int index = 1; var over90ToShow = warehouseId == 7 ? over90List?.Where(m => m.IsProStock).Take(500) : over90List?.Take(500); if (over90ToShow != null) { foreach (var material in over90ToShow) { markdownContent.AppendLine($"| {index} | {material.MaterielCode} | {material.BatchNo} |"); index++; } if (over90Count > (warehouseId == 7 ? 500 : 200)) { markdownContent.AppendLine($"| ... | 共{over90Count}条,仅显示前{(warehouseId == 7 ? 500 : 200)}条 | ... |"); } } markdownContent.AppendLine(); } // 11.4 添加时间戳和提示信息 markdownContent.AppendLine($"**报告时间:** {currentTime:yyyy-MM-dd HH:mm:ss}"); if (warehouseId == 7) { markdownContent.AppendLine("**备注:** 请成品仓管理人员及时处理超过90天未使用的成品。"); } else { markdownContent.AppendLine("**备注:** 请相关仓库管理人员及时处理以上物料。"); } var messageContent = markdownContent.ToString(); // 11.5 生成钉钉消息所需的时间戳和签名 var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var sign = GenerateSign(timestamp, secret); // 11.6 构建钉钉请求URL var uri = new Uri(webhookUrl); var token = System.Web.HttpUtility.ParseQueryString(uri.Query)["access_token"]; var baseUrl = uri.GetLeftPart(UriPartial.Path); var url = $"{baseUrl}?access_token={token}×tamp={timestamp}&sign={sign}"; // 11.7 构建请求体(使用markdown格式) var requestBody = new { msgtype = "markdown", markdown = new { title = $"{warehouseName}物料提醒", text = messageContent }, at = new { // 可以指定@某些人,如果不需要可以删除这部分 // atMobiles = new[] { "138xxxx8888" }, // isAtAll = false } }; var jsonBody = JsonConvert.SerializeObject(requestBody); var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); // 11.8 发送POST请求并处理响应 var response = await httpClient.PostAsync(url, content); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); throw new Exception($"【{warehouseName}】消息发送失败,状态码:{response.StatusCode},错误信息:{errorContent}"); } // 避免发送频率过快 await Task.Delay(1000); } } } catch (Exception ex) { // 捕获异常并补充上下文,便于问题定位 throw new Exception($"钉钉消息推送整体失败,错误详情:{ex.Message}", ex); } } // 钉钉签名生成方法 private string GenerateSign(long timestamp, string secret) { var stringToSign = $"{timestamp}\n{secret}"; using (var hmac = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(secret))) { var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); return Convert.ToBase64String(hash); } } } }