wangxinhui
2026-01-19 3edc6956b30df3fc11025e0b719f320fcb1ec9c5
´úÂë¹ÜÀí/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -4,6 +4,7 @@
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using SqlSugar;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@@ -238,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 && useableStockQuantity>0)
                    // è®¡ç®—可用库存时转换为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
                    {
@@ -250,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;
                                }
                            }
@@ -266,6 +281,7 @@
                    outStocks.Add(stockInfo);
                    index++;
                }
            }
            else
            {
@@ -277,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);
@@ -401,52 +421,327 @@
            {
                if (webhookUrl == null || secret == null)
                {
                    webhookUrl = "https://oapi.dingtalk.com/robot/send?access_token=fbc3aaf4133ea650d8116fb86b3ebfd0c5e0d46775966ce87893a41886bdf9dc";
                    secret = "SECf221842b26356f22ccac84c4e60714e5287408ee8332a8f63503791382c3f5fb";
                    webhookUrl = "https://oapi.dingtalk.com/robot/send?access_token=c3e05f2c6bcd595383ee02e713446174b9201bad91db216590620fe0acd4e75e";
                    secret = "SEC617f06140fc7cbd8b91d3e203f270826320637af41e7423d756e62df40f62282";
                }
                HttpClient httpClient = new HttpClient();
                ///获取时间戳
                var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
                ///生成签名
                var sign = GenerateSign(timestamp,secret);
                // æž„建请求URL
                var url = $"{webhookUrl.Split('?')[0]}?access_token={new Uri(webhookUrl).Query.Split('=')[1]}&timestamp={timestamp}&sign={sign}";
                var requestBody = new
                {
                    msgtype = "text",
                    text = new { content = "小洋主人说: å°æ´‹è¦å’Œå°å¦å¤©ä¸‹ç¬¬ä¸€æœ€æœ€å¥½" },
                };
                var jsonBody = JsonConvert.SerializeObject(requestBody);
                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
                // å‘送POST请求
                var response = await httpClient.PostAsync(url, content);
                if (!response.IsSuccessStatusCode)
                // 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))
                {
                    // å¤„理请求失败的情况
                    var errorContent = await response.Content.ReadAsStringAsync();
                    throw new Exception($"钉钉消息发送失败,状态码: {response.StatusCode},错误内容: {errorContent}");
                    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)
            catch (Exception ex)
            {
                throw new Exception($"钉钉消息发送失败,错误内容: {ex.Message}");
                // æ•获异常并补充上下文,便于问题定位
                throw new Exception($"钉钉消息推送整体失败,错误详情:{ex.Message}", ex);
            }
        }
        /// <summary>
        /// ç”ŸæˆåŠ ç­¾ç­¾å
        /// </summary>
        /// <param name="timestamp">时间戳</param>
        /// <returns>签名</returns>
        private string GenerateSign(long timestamp,string secret)
        // é’‰é’‰ç­¾åç”Ÿæˆæ–¹æ³•
        private string GenerateSign(long timestamp, string secret)
        {
            var stringToSign = $"{timestamp}\n{secret}";
            using (var hmacsha256 = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(secret)))
            using (var hmac = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(secret)))
            {
                var hashBytes = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
                return Convert.ToBase64String(hashBytes).Replace("+", "%2B").Replace("/", "%2F");
                var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
                return Convert.ToBase64String(hash);
            }
        }
    }
}