xiazhengtongxue
12 小时以前 af4404160a9e8d14c09f1e6acab2ba00cb7fc91b
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
@@ -1,272 +1,373 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// 仪表盘
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// 总览数据
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // 今日入库数
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 今日出库数
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 本月入库数
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 本月出库数
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 当前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每日统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按日期分组。
        /// SqlSugar 的 GroupBy 不支持对 .Date 这样的计算列直接生成 SQL GROUP BY,
        /// 因此采用此方式以确保跨数据库兼容性。
        /// </remarks>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key.ToString("yyyy-MM-dd"),
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Date)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每周统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按 ISO 8601 周键分组。
        /// 周键为 "YYYY-Www" 格式,无法直接在 SQL 层用 GROUP BY 实现。
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // 获取周一开始的周 (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// 每月统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按年月分组。
        /// SqlSugar 的 GroupBy 不支持匿名对象 (Year, Month) 直接映射到 SQL GROUP BY,
        /// 因此采用此方式以确保跨数据库兼容性。
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(g => new
                    {
                        Month = $"{g.Key.Year}-{g.Key.Month:D2}",
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Month)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 库存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // 使用 SQL 直接分组统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 各仓库库存分布
        /// </summary>
        /// <remarks>
        /// 使用 SQL GROUP BY 在数据库层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // 查询仓库名称
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                var warehouseDict = warehouses.ToDictionary(w => w.WarehouseId, w => w.WarehouseName);
                // 使用 SQL GROUP BY 在数据库层面聚合,仅返回聚合结果
                var stockGroups = await _db.Queryable<Dt_StockInfo>()
                    .GroupBy(s => s.WarehouseId)
                    .Select(s => new { s.WarehouseId, Count = SqlFunc.AggregateCount(s.Id) })
                    .ToListAsync();
                var result = stockGroups
                    .Select(g => new
                    {
                        Warehouse = warehouseDict.TryGetValue(g.WarehouseId, out var name) ? name : $"仓库{g.WarehouseId}",
                        Count = g.Count
                    })
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// 仪表盘
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// 总览数据
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // 今日入库数
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 今日出库数
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 本月入库数
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 本月出库数
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 当前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每日统计
        /// </summary>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var endDate = DateTime.Today; // 包含今天
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate && t.InsertTime <= endDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                // 生成日期范围
                var allDates = new List<DateTime>();
                for (var date = startDate; date <= endDate; date = date.AddDays(1))
                {
                    allDates.Add(date);
                }
                // 按日期分组统计
                var groupedData = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .ToDictionary(x => x.Date, x => x);
                // 补全缺失日期
                var result = allDates.Select(date =>
                {
                    if (groupedData.TryGetValue(date, out var data))
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = data.Inbound,
                            Outbound = data.Outbound
                        };
                    }
                    else
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = 0,
                            Outbound = 0
                        };
                    }
                })
                .OrderBy(x => x.Date)
                .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每周统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按 ISO 8601 周键分组。
        /// 周键为 "YYYY-Www" 格式,无法直接在 SQL 层用 GROUP BY 实现。
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // 获取周一开始的周 (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// 每月统计
        /// </summary>
        /// <remarks>
        /// 按年月统计入站和出站任务数量
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var monthlyStats = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(t => new
                    {
                        Year = t.InsertTime.Year,
                        Month = t.InsertTime.Month,
                        Inbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 200 && t.TaskType < 300, 1, 0)
                        ),
                        Outbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 100 && t.TaskType < 200, 1, 0)
                        )
                    })
                    .OrderBy(t => t.Year)
                    .OrderBy(t => t.Month)
                    .ToListAsync();
                // 生成所有需要统计的月份列表
                var allMonths = new List<DateTime>();
                var currentMonth = startDate;
                var endMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
                while (currentMonth <= endMonth)
                {
                    allMonths.Add(currentMonth);
                    currentMonth = currentMonth.AddMonths(1);
                }
                // 将查询结果转换为字典,方便查找
                var statsDict = monthlyStats.ToDictionary(
                    s => $"{s.Year}-{s.Month:D2}",
                    s => new { s.Inbound, s.Outbound }
                );
                // 构建完整的结果列表,包含所有月份
                var result = new List<object>();
                foreach (var month in allMonths)
                {
                    var monthKey = $"{month.Year}-{month.Month:D2}";
                    if (statsDict.TryGetValue(monthKey, out var stat))
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = stat.Inbound,
                            Outbound = stat.Outbound
                        });
                    }
                    else
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = 0,
                            Outbound = 0
                        });
                    }
                }
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                // 记录异常日志(实际项目中建议使用日志框架)
                // _logger.LogError(ex, "每月统计获取失败");
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 库存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // 使用 SQL 直接分组统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 各仓库库存分布
        /// </summary>
        /// <remarks>
        /// 使用 SQL GROUP BY 在数据库层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // 查询所有仓库信息
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                // 查询所有货位信息,按仓库分组统计总数
                var locationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        TotalLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // 查询状态不为Free的货位信息(有货货位),按仓库分组统计
                var occupiedLocationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .Where(l => l.LocationStatus != (int)LocationStatusEnum.Free)
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        OccupiedLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // 将仓库信息与货位统计信息合并
                var result = warehouses.Select(w =>
                {
                    var totalLocations = locationGroups.FirstOrDefault(lg => lg.WarehouseId == w.WarehouseId)?.TotalLocations ?? 0;
                    var occupiedLocations = occupiedLocationGroups.FirstOrDefault(og => og.WarehouseId == w.WarehouseId)?.OccupiedLocations ?? 0;
                    var emptyLocations = totalLocations - occupiedLocations;
                    var occupiedPercentage = totalLocations > 0 ? Math.Round((double)occupiedLocations / totalLocations * 100, 2) : 0.0;
                    var emptyPercentage = totalLocations > 0 ? Math.Round((double)emptyLocations / totalLocations * 100, 2) : 0.0;
                    return new
                    {
                        Warehouse = w.WarehouseName,
                        Total = totalLocations,
                        HasStock = occupiedLocations,
                        NoStock = emptyLocations,
                        HasStockPercentage = $"{occupiedPercentage}%",
                        NoStockPercentage = $"{emptyPercentage}%"
                    };
                }).ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}