wangxinhui
2025-12-31 6f8b21411a2a56f6a33fe0112c2ed0eeca407e9c
´úÂë¹ÜÀí/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -1,10 +1,13 @@
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;
@@ -55,7 +58,7 @@
                    throw new Exception($"未找到出库单信息");
                }
                List<string> locationCodes = _basicRepository.LocationInfoRepository.GetCanOutLocationCodes(outboundOrder.WarehouseId);
                List<string> locationCodes = _basicRepository.LocationInfoRepository.PPGetCanOutLocationCodes(outboundOrder.WarehouseId);
                return BaseDal.QueryTabs<Dt_StockInfo, Dt_StockInfoDetail, StockSelectViewDTO>((a, b) => a.Id == b.StockId, (a, b) => new StockSelectViewDTO
                {
@@ -115,6 +118,92 @@
                return null;
            }
        }
        /// <summary>
        /// æŸ¥è¯¢è®¢å•PP立库库存视图
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="materielCode"></param>
        /// <returns></returns>
        public List<PPStockSelectViewDTO> PPGetStockSelectViews(int orderId, string materielCode)
        {
            try
            {
                Dt_MesPPCutOutboundOrder mesPPCutOutboundOrder = _outboundRepository.MesPPCutOutboundOrderRepository.QueryFirst(x => x.Id == orderId);
                if (mesPPCutOutboundOrder == null)
                {
                    throw new Exception($"未找到出库单信息");
                }
                List<string> locationCodes = _basicRepository.LocationInfoRepository.PPGetCanOutLocationCodes(mesPPCutOutboundOrder.WarehouseId);
                return BaseDal.QueryTabs<Dt_StockInfo, Dt_StockInfoDetail, PPStockSelectViewDTO>((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;
            }
        }
        /// <summary>
        /// æŸ¥è¯¢è®¢å•PP平库库存视图
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="materielCode"></param>
        /// <returns></returns>
        public List<PPStockSelectViewDTO> 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<Dt_StockInfo, Dt_StockInfoDetail, PPStockSelectViewDTO>((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)
        {
@@ -150,11 +239,19 @@
                while (needQuantity > 0)
                {
                    Dt_StockInfo stockInfo = stockInfos[index];
                    float useableStockQuantity = stockInfo.Details.Where(x => x.MaterielCode == materielCode).Sum(x => x.StockQuantity - x.OutboundQuantity);
                    if (useableStockQuantity < needQuantity)
                    // è®¡ç®—可用库存时转换为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);
                        needQuantity -= useableStockQuantity;
                        stockInfo.Details.ForEach(x =>
                            x.OutboundQuantity = x.StockQuantity);
                        // ä½¿ç”¨decimal进行计算后再转回float
                        needQuantity = (float)((decimal)needQuantity - useableStockQuantity);
                    }
                    else
                    {
@@ -162,14 +259,20 @@
                        {
                            if (x.StockQuantity > x.OutboundQuantity && x.MaterielCode == materielCode)
                            {
                                if (x.StockQuantity - x.OutboundQuantity >= needQuantity)
                                // å°†ç›¸å…³å€¼è½¬æ¢ä¸ºdecimal进行精确计算
                                decimal currentStock = (decimal)x.StockQuantity;
                                decimal currentOutbound = (decimal)x.OutboundQuantity;
                                decimal currentNeed = (decimal)needQuantity;
                                decimal available = currentStock - currentOutbound;
                                if (available >= currentNeed)
                                {
                                    x.OutboundQuantity += needQuantity;
                                    x.OutboundQuantity = (float)(currentOutbound + currentNeed);
                                    needQuantity = 0;
                                }
                                else
                                {
                                    needQuantity -= (x.StockQuantity - x.OutboundQuantity);
                                    needQuantity = (float)(currentNeed - available);
                                    x.OutboundQuantity = x.StockQuantity;
                                }
                            }
@@ -178,39 +281,11 @@
                    outStocks.Add(stockInfo);
                    index++;
                }
            }
            else
            {
                for (int i = 0; i < stockInfos.Count; i++)
                {
                    Dt_StockInfo stockInfo = stockInfos[i];
                    float useableStockQuantity = stockInfo.Details.Where(x => x.MaterielCode == materielCode).Sum(x => x.StockQuantity - x.OutboundQuantity);
                    if (useableStockQuantity < needQuantity)
                    {
                        stockInfo.Details.ForEach(x => x.OutboundQuantity = x.StockQuantity);
                        needQuantity -= useableStockQuantity;
                    }
                    else
                    {
                        stockInfo.Details.ForEach(x =>
                        {
                            if (x.StockQuantity > x.OutboundQuantity && x.MaterielCode == materielCode)
                            {
                                if (x.StockQuantity - x.OutboundQuantity >= needQuantity)
                                {
                                    x.OutboundQuantity += needQuantity;
                                    needQuantity = 0;
                                }
                                else
                                {
                                    needQuantity -= (x.StockQuantity - x.OutboundQuantity);
                                    x.OutboundQuantity = x.StockQuantity;
                                }
                            }
                        });
                    }
                    outStocks.Add(stockInfo);
                }
                throw new Exception("库存不足");
            }
            residueQuantity = needQuantity;
            return outStocks;
@@ -218,6 +293,10 @@
        public List<Dt_StockInfo> 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<string> locationCodes = _basicRepository.LocationInfoRepository.GetCanOutLocationCodes(warehoseId);
            return BaseDal.GetStockInfos(materielCode, batchNo, locationCodes);
@@ -232,5 +311,437 @@
            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<Dt_StockInfoDetail>()
                    .InnerJoin<Dt_StockInfo>((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<long, int>();
                    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<Dt_StockInfo>();
                        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("更新失败,请联系管理员");
            }
        }
        /// <summary>
        /// é’‰é’‰æœºå™¨äººæ¶ˆæ¯æŽ¨é€æµ‹è¯•
        /// </summary>
        /// <returns></returns>
        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<int, string>
        {
            { 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<Dt_StockInfo>()
                    .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<Dt_StockInfo>()
                    .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<Dt_ProStockInfo>();
                if (warehouseIdToName.ContainsKey(7))
                {
                    proOver90DaysStocks = BaseDal.Db.Queryable<Dt_ProStockInfo>()
                        .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<Dt_StockInfoDetail>()
                        .Where(d => expirationLabel3StockIds.Contains(d.StockId))
                        .ToList()
                    : new List<Dt_StockInfoDetail>();
                var over90DaysDetails = over90DaysStockIds.Any()
                    ? BaseDal.Db.Queryable<Dt_StockInfoDetail>()
                        .Where(d => over90DaysStockIds.Contains(d.StockId))
                        .ToList()
                    : new List<Dt_StockInfoDetail>();
                // 7. å¤„理成品仓明细数据
                var proOver90DaysDetails = new List<Dt_ProStockInfoDetail>();
                if (proOver90DaysStocks.Any())
                {
                    var proStockIds = proOver90DaysStocks.Select(s => s.Id).ToList();
                    proOver90DaysDetails = BaseDal.Db.Queryable<Dt_ProStockInfoDetail>()
                        .Where(d => proStockIds.Contains(d.ProStockId))
                        .ToList();
                }
                // 8. å»ºç«‹ä¸»è¡¨ID到仓库ID的映射(包括成品仓)
                var stockIdToWarehouseId = new Dictionary<int, int>();
                // åŽŸææ–™ä»“æ˜ å°„
                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}&timestamp={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);
            }
        }
    }
}