diff --git a/SimpleAdmin.Application/Services/Statistics/Dto/StatisticsDto.cs b/SimpleAdmin.Application/Services/Statistics/Dto/StatisticsDto.cs new file mode 100644 index 0000000..93c7653 --- /dev/null +++ b/SimpleAdmin.Application/Services/Statistics/Dto/StatisticsDto.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// +// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: +// 1.请不要删除和修改根目录下的LICENSE文件。 +// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 +// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin + +namespace SimpleAdmin.Application.Services.Statistics +{ + /// + /// 年度统计数据 DTO(对应前端 YearlyStatisticsDto) + /// + public class YearlyStatisticsDto + { + /// 统计年份(如 "2025") + public string Year { get; set; } + + /// 处方总花费(元) + public decimal TotalCost { get; set; } + + /// 处方总数量(张) + public int TotalQty { get; set; } + + /// 员工处方花费(元) + public decimal StaffCost { get; set; } + + /// 家属处方花费(元) + public decimal FamilyCost { get; set; } + + /// 员工处方数量(张) + public int StaffQty { get; set; } + + /// 家属处方数量(张) + public int FamilyQty { get; set; } + + /// 药材耗材花费(元)—— 当年所有入库药材总价 + public decimal MaterialCost { get; set; } + + /// 服务人数(当年有过处方记录的不重复人员数) + public int ServiceCount { get; set; } + + // ---------- 同比趋势(百分比,正数为增长,负数为下降) ---------- + + /// 处方总花费同比趋势 (%) + public decimal TotalCostTrend { get; set; } + + /// 处方总数量同比趋势 (%) + public decimal TotalQtyTrend { get; set; } + + /// 药材花费同比趋势 (%) + public decimal MaterialCostTrend { get; set; } + + /// 服务人数同比趋势 (%) + public decimal ServiceCountTrend { get; set; } + } + + /// + /// 月度明细数据 DTO(对应前端 MonthlyDetailDto) + /// + public class MonthlyDetailDto + { + /// 月份显示(如 "1月") + public string Month { get; set; } + + /// 员工处方花费(元) + public decimal StaffCost { get; set; } + + /// 家属处方花费(元) + public decimal FamilyCost { get; set; } + + /// 员工处方数量(张) + public int StaffQty { get; set; } + + /// 家属处方数量(张) + public int FamilyQty { get; set; } + + /// 药材耗材花费(元)—— 当月入库总价 + public decimal MaterialCost { get; set; } + } + + /// + /// 全量总览 DTO(不区分年份,含剩余药材价值) + /// + public class TotalOverviewDto + { + /// 处方总花费(元,含所有年份) + public decimal TotalCost { get; set; } + + /// 处方总数量(张) + public int TotalQty { get; set; } + + /// 员工处方花费(元) + public decimal StaffCost { get; set; } + + /// 家属处方花费(元) + public decimal FamilyCost { get; set; } + + /// 员工处方数量(张) + public int StaffQty { get; set; } + + /// 家属处方数量(张) + public int FamilyQty { get; set; } + + /// 药材采购总花费(元,历史所有入库总额) + public decimal MaterialCost { get; set; } + + /// 累计服务人数(不重复人员) + public int ServiceCount { get; set; } + + /// 当前剩余药材库存价值(元,按最新入库单价估算) + public decimal RemainingMedicinalValue { get; set; } + } +} diff --git a/SimpleAdmin.Application/Services/Statistics/IStatisticsService.cs b/SimpleAdmin.Application/Services/Statistics/IStatisticsService.cs new file mode 100644 index 0000000..0c3a3ed --- /dev/null +++ b/SimpleAdmin.Application/Services/Statistics/IStatisticsService.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// +// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: +// 1.请不要删除和修改根目录下的LICENSE文件。 +// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 +// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin + +namespace SimpleAdmin.Application.Services.Statistics +{ + /// + /// 统计分析 Service 接口 + /// + public interface IStatisticsService : ITransient + { + /// + /// 获取全量总览数据(不区分年份,含剩余药材库存价值) + /// + Task GetTotalOverview(); + + /// + /// 获取指定年份的年度汇总统计数据(含同比趋势) + /// + /// 年份,如 2025 + Task GetYearlyStatistics(int year); + + /// + /// 获取指定年份的逐月明细数据(返回 12 条,无数据月份补零) + /// + /// 年份,如 2025 + Task> GetMonthlyDetails(int year); + } +} diff --git a/SimpleAdmin.Application/Services/Statistics/StatisticsService.cs b/SimpleAdmin.Application/Services/Statistics/StatisticsService.cs new file mode 100644 index 0000000..870b41f --- /dev/null +++ b/SimpleAdmin.Application/Services/Statistics/StatisticsService.cs @@ -0,0 +1,526 @@ +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// +// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: +// 1.请不要删除和修改根目录下的LICENSE文件。 +// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 +// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin + +using SimpleAdmin.Application.Entity; +using SimpleAdmin.Application.Services.Statistics; + +/// +/// 统计分析 Service 实现 +/// +public class StatisticsService : DbRepository, IStatisticsService +{ + // ───────────────────────────────────────────────────────────── + // 公开接口 + // ───────────────────────────────────────────────────────────── + + /// + public async Task GetTotalOverview() + { + // 1) 所有处方(按创建时间升序,FIFO 需要) + var prescriptionInfos = await Context.Queryable() + .LeftJoin((p, pe) => p.PersonId == pe.Id) + .OrderBy((p, pe) => p.CreateTime) + .Select((p, pe) => new + { + p.Id, + p.PersonId, + IsRelative = pe.EmployeeId != null + }) + .ToListAsync(); + + decimal staffCost = 0, familyCost = 0; + int staffQty = 0, familyQty = 0; + + if (prescriptionInfos.Any()) + { + var allIds = prescriptionInfos.Select(x => x.Id).ToList(); + + var allHerbs = await Context.Queryable() + .Where(ph => allIds.Contains(ph.PrescriptionId)) + .ToListAsync(); + + var usageGroups = allHerbs + .GroupBy(h => new { h.PrescriptionId, h.MedicinalId }) + .Select(g => new + { + g.Key.PrescriptionId, + g.Key.MedicinalId, + TotalUsedWeight = g.Sum(h => h.AmountG) + }) + .ToList(); + + var allMedicinalIds = usageGroups.Select(u => u.MedicinalId).Distinct().ToList(); + var fifoData = await GetFifoData(allMedicinalIds); + var fifoOffsets = new Dictionary(); + + var usageByPres = usageGroups + .GroupBy(u => u.PrescriptionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var pInfo in prescriptionInfos) + { + decimal cost = 0; + if (usageByPres.TryGetValue(pInfo.Id, out var usages)) + foreach (var u in usages) + cost += CalculateFifoCost(fifoData, fifoOffsets, u.MedicinalId, u.TotalUsedWeight); + + cost = Math.Round(cost, 4, MidpointRounding.AwayFromZero); + + if (pInfo.IsRelative) + { + familyCost += Math.Round(cost * 1.1m, 2, MidpointRounding.AwayFromZero); + familyQty++; + } + else + { + staffCost += Math.Round(cost, 2, MidpointRounding.AwayFromZero); + staffQty++; + } + } + } + + // 2) 药材采购总花费(历史所有入库总额) + var materialCost = await Context.Queryable() + .SumAsync(l => l.TotalPrice); + + // 3) 累计服务人数(不重复人员) + var serviceCount = prescriptionInfos.Select(x => x.PersonId).Distinct().Count(); + + // 4) 剩余药材库存价值:每种药材当前库存 × 最新入库单价 + var medicinals = await Context.Queryable() + .Where(m => m.Number > 0) + .Select(m => new { m.Id, m.Number }) + .ToListAsync(); + + decimal remainingValue = 0; + if (medicinals.Any()) + { + var medIds = medicinals.Select(m => m.Id).ToList(); + + // 取每种药材最新的一条入库记录单价 + var latestPrices = await Context.Queryable() + .Where(l => medIds.Contains(l.MedicinalId)) + .GroupBy(l => l.MedicinalId) + .Select(l => new + { + MedicinalId = l.MedicinalId, + LatestPrice = SqlFunc.AggregateMax(l.UnitPrice) + }) + .ToListAsync(); + + var priceDict = latestPrices.ToDictionary(x => x.MedicinalId, x => x.LatestPrice); + + foreach (var med in medicinals) + { + if (priceDict.TryGetValue(med.Id, out var price)) + remainingValue += Math.Round(med.Number * price, 2, MidpointRounding.AwayFromZero); + } + } + + return new TotalOverviewDto + { + TotalCost = Math.Round(staffCost + familyCost, 2), + TotalQty = staffQty + familyQty, + StaffCost = Math.Round(staffCost, 2), + FamilyCost = Math.Round(familyCost, 2), + StaffQty = staffQty, + FamilyQty = familyQty, + MaterialCost = Math.Round(materialCost, 2), + ServiceCount = serviceCount, + RemainingMedicinalValue = Math.Round(remainingValue, 2), + }; + } + + /// + public async Task GetYearlyStatistics(int year) + { + var currentYearData = await BuildYearData(year); + var lastYearData = await BuildYearData(year - 1); + + return new YearlyStatisticsDto + { + Year = year.ToString(), + TotalCost = currentYearData.TotalCost, + TotalQty = currentYearData.TotalQty, + StaffCost = currentYearData.StaffCost, + FamilyCost = currentYearData.FamilyCost, + StaffQty = currentYearData.StaffQty, + FamilyQty = currentYearData.FamilyQty, + MaterialCost = currentYearData.MaterialCost, + ServiceCount = currentYearData.ServiceCount, + + TotalCostTrend = CalcTrend(lastYearData.TotalCost, currentYearData.TotalCost), + TotalQtyTrend = CalcTrend(lastYearData.TotalQty, currentYearData.TotalQty), + MaterialCostTrend = CalcTrend(lastYearData.MaterialCost, currentYearData.MaterialCost), + ServiceCountTrend = CalcTrend(lastYearData.ServiceCount, currentYearData.ServiceCount), + }; + } + + /// + public async Task> GetMonthlyDetails(int year) + { + var start = new DateTime(year, 1, 1); + var end = new DateTime(year, 12, 31, 23, 59, 59); + + // 1) 当年所有处方(按时间升序,供 FIFO 用) + var prescriptionInfos = await Context.Queryable() + .LeftJoin((p, pe) => p.PersonId == pe.Id) + .Where((p, pe) => p.VisitDate >= start && p.VisitDate <= end) + .OrderBy((p, pe) => p.CreateTime) + .Select((p, pe) => new + { + p.Id, + p.VisitDate, + p.CreateTime, + IsRelative = pe.EmployeeId != null + }) + .ToListAsync(); + + if (!prescriptionInfos.Any()) + return BuildEmptyMonthList(); + + var allPrescriptionIds = prescriptionInfos.Select(x => x.Id).ToList(); + + // 2) 批量拉取处方药材明细 + var allHerbs = await Context.Queryable() + .Where(ph => allPrescriptionIds.Contains(ph.PrescriptionId)) + .ToListAsync(); + + // 3) 按 处方ID+药材ID 分组,得到每张处方消耗每种药材的总量 + var usageGroups = allHerbs + .GroupBy(h => new { h.PrescriptionId, h.MedicinalId }) + .Select(g => new + { + g.Key.PrescriptionId, + g.Key.MedicinalId, + TotalUsedWeight = g.Sum(h => h.AmountG) + }) + .ToList(); + + // 4) FIFO 入库数据 + var allMedicinalIds = usageGroups.Select(u => u.MedicinalId).Distinct().ToList(); + var fifoData = await GetFifoData(allMedicinalIds); + + // 5) 需要在"所有历史处方"维度跑 FIFO,才能保证成本正确(与 PrescriptionService 逻辑一致) + // 先把当年之前的"历史"也加进来跑偏移 + var historyIds = await Context.Queryable() + .Where(p => p.VisitDate < start) + .OrderBy(p => p.CreateTime) + .Select(p => p.Id) + .ToListAsync(); + + var fifoOffsets = new Dictionary(); + + // 先消耗历史处方(建立偏移量),不需要计入结果 + if (historyIds.Any()) + { + var historyHerbs = await Context.Queryable() + .Where(ph => historyIds.Contains(ph.PrescriptionId)) + .ToListAsync(); + + var historyUsage = historyHerbs + .GroupBy(h => new { h.PrescriptionId, h.MedicinalId }) + .Select(g => new + { + g.Key.PrescriptionId, + g.Key.MedicinalId, + TotalUsedWeight = g.Sum(h => h.AmountG) + }) + .ToList(); + + var historyByPres = historyUsage + .GroupBy(u => u.PrescriptionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var hid in historyIds) + { + if (!historyByPres.TryGetValue(hid, out var usages)) continue; + foreach (var u in usages) + CalculateFifoCost(fifoData, fifoOffsets, u.MedicinalId, u.TotalUsedWeight); + } + } + + // 6) 计算当年每张处方的成本,按月汇总 + var usageByPres = usageGroups + .GroupBy(u => u.PrescriptionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // 月度桶:1–12 + var monthlyStaffCost = new decimal[13]; + var monthlyFamilyCost = new decimal[13]; + var monthlyStaffQty = new int[13]; + var monthlyFamilyQty = new int[13]; + + var presInfoDict = prescriptionInfos.ToDictionary(x => x.Id); + + foreach (var pInfo in prescriptionInfos) + { + int month = pInfo.VisitDate?.Month ?? pInfo.CreateTime?.Month ?? 0; + if (month < 1 || month > 12) continue; + + decimal cost = 0m; + if (usageByPres.TryGetValue(pInfo.Id, out var usages)) + { + foreach (var u in usages) + cost += CalculateFifoCost(fifoData, fifoOffsets, u.MedicinalId, u.TotalUsedWeight); + } + cost = Math.Round(cost, 4, MidpointRounding.AwayFromZero); + + if (pInfo.IsRelative) + { + cost = Math.Round(cost * 1.1m, 2, MidpointRounding.AwayFromZero); + monthlyFamilyCost[month] += cost; + monthlyFamilyQty[month]++; + } + else + { + cost = Math.Round(cost, 2, MidpointRounding.AwayFromZero); + monthlyStaffCost[month] += cost; + monthlyStaffQty[month]++; + } + } + + // 7) 药材花费:按入库记录的月份汇总当年花费 + var materialLogs = await Context.Queryable() + .Where(l => l.CreateTime >= start && l.CreateTime <= end) + .Select(l => new { l.CreateTime, l.TotalPrice }) + .ToListAsync(); + + var monthlyMaterialCost = new decimal[13]; + foreach (var log in materialLogs) + { + int month = log.CreateTime?.Month ?? 0; + if (month >= 1 && month <= 12) + monthlyMaterialCost[month] += log.TotalPrice; + } + + // 8) 组装返回结果 + var result = new List(12); + for (int m = 1; m <= 12; m++) + { + result.Add(new MonthlyDetailDto + { + Month = $"{m}月", + StaffCost = Math.Round(monthlyStaffCost[m], 2), + FamilyCost = Math.Round(monthlyFamilyCost[m], 2), + StaffQty = monthlyStaffQty[m], + FamilyQty = monthlyFamilyQty[m], + MaterialCost = Math.Round(monthlyMaterialCost[m], 2), + }); + } + return result; + } + + // ───────────────────────────────────────────────────────────── + // 私有辅助方法 + // ───────────────────────────────────────────────────────────── + + /// + /// 计算指定年份的聚合数据(用于年度统计与同比计算) + /// + private async Task<(decimal TotalCost, int TotalQty, decimal StaffCost, decimal FamilyCost, + int StaffQty, int FamilyQty, decimal MaterialCost, int ServiceCount)> BuildYearData(int year) + { + var start = new DateTime(year, 1, 1); + var end = new DateTime(year, 12, 31, 23, 59, 59); + + // 当年处方(按时间升序,FIFO 需要) + var prescriptionInfos = await Context.Queryable() + .LeftJoin((p, pe) => p.PersonId == pe.Id) + .Where((p, pe) => p.VisitDate >= start && p.VisitDate <= end) + .OrderBy((p, pe) => p.CreateTime) + .Select((p, pe) => new + { + p.Id, + p.PersonId, + IsRelative = pe.EmployeeId != null + }) + .ToListAsync(); + + if (!prescriptionInfos.Any()) + return (0, 0, 0, 0, 0, 0, 0, 0); + + var allPrescriptionIds = prescriptionInfos.Select(x => x.Id).ToList(); + + var allHerbs = await Context.Queryable() + .Where(ph => allPrescriptionIds.Contains(ph.PrescriptionId)) + .ToListAsync(); + + var usageGroups = allHerbs + .GroupBy(h => new { h.PrescriptionId, h.MedicinalId }) + .Select(g => new + { + g.Key.PrescriptionId, + g.Key.MedicinalId, + TotalUsedWeight = g.Sum(h => h.AmountG) + }) + .ToList(); + + var allMedicinalIds = usageGroups.Select(u => u.MedicinalId).Distinct().ToList(); + var fifoData = await GetFifoData(allMedicinalIds); + + // 历史偏移量(当年之前的处方) + var historyIds = await Context.Queryable() + .Where(p => p.VisitDate < start) + .OrderBy(p => p.CreateTime) + .Select(p => p.Id) + .ToListAsync(); + + var fifoOffsets = new Dictionary(); + if (historyIds.Any()) + { + var historyHerbs = await Context.Queryable() + .Where(ph => historyIds.Contains(ph.PrescriptionId)) + .ToListAsync(); + + var historyUsage = historyHerbs + .GroupBy(h => new { h.PrescriptionId, h.MedicinalId }) + .Select(g => new + { + g.Key.PrescriptionId, + g.Key.MedicinalId, + TotalUsedWeight = g.Sum(h => h.AmountG) + }) + .GroupBy(u => u.PrescriptionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var hid in historyIds) + { + if (!historyUsage.TryGetValue(hid, out var usages)) continue; + foreach (var u in usages) + CalculateFifoCost(fifoData, fifoOffsets, u.MedicinalId, u.TotalUsedWeight); + } + } + + // 按处方计算成本 + var usageByPres = usageGroups + .GroupBy(u => u.PrescriptionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + decimal staffCost = 0, familyCost = 0; + int staffQty = 0, familyQty = 0; + + foreach (var pInfo in prescriptionInfos) + { + decimal cost = 0; + if (usageByPres.TryGetValue(pInfo.Id, out var usages)) + foreach (var u in usages) + cost += CalculateFifoCost(fifoData, fifoOffsets, u.MedicinalId, u.TotalUsedWeight); + + cost = Math.Round(cost, 4, MidpointRounding.AwayFromZero); + + if (pInfo.IsRelative) + { + familyCost += Math.Round(cost * 1.1m, 2, MidpointRounding.AwayFromZero); + familyQty++; + } + else + { + staffCost += Math.Round(cost, 2, MidpointRounding.AwayFromZero); + staffQty++; + } + } + + // 药材花费:当年所有入库记录的总额 + var materialCost = await Context.Queryable() + .Where(l => l.CreateTime >= start && l.CreateTime <= end) + .SumAsync(l => l.TotalPrice); + + // 服务人数:当年有处方的不重复人员 + var serviceCount = prescriptionInfos.Select(x => x.PersonId).Distinct().Count(); + + return ( + Math.Round(staffCost + familyCost, 2), + staffQty + familyQty, + Math.Round(staffCost, 2), + Math.Round(familyCost, 2), + staffQty, + familyQty, + Math.Round(materialCost, 2), + serviceCount + ); + } + + /// + /// 读取 FIFO 入库数据(按时间升序) + /// + private async Task>> GetFifoData(List medicinalIds) + { + if (!medicinalIds.Any()) + return new Dictionary>(); + + var logs = await Context.Queryable() + .Where(l => medicinalIds.Contains(l.MedicinalId)) + .OrderBy(l => l.CreateTime) + .Select(l => new MedicinalFifoRecord + { + MedicinalId = l.MedicinalId, + AddWeight = l.AddWeight, + UnitPrice = l.UnitPrice + }) + .ToListAsync(); + + return logs.GroupBy(l => l.MedicinalId) + .ToDictionary(g => g.Key, g => g.ToList()); + } + + /// + /// 按 FIFO 规则(含累计偏移)计算单次消耗成本,并推进偏移量 + /// + private static decimal CalculateFifoCost( + Dictionary> fifoData, + Dictionary fifoOffsets, + long medicinalId, + decimal usedWeight) + { + if (usedWeight <= 0 || !fifoData.TryGetValue(medicinalId, out var records) || !records.Any()) + return 0; + + decimal alreadyUsed = fifoOffsets.GetValueOrDefault(medicinalId, 0); + decimal remainingToUse = usedWeight; + decimal totalCost = 0; + + foreach (var record in records) + { + if (remainingToUse <= 0) break; + + if (alreadyUsed >= record.AddWeight) + { + alreadyUsed -= record.AddWeight; + continue; + } + + decimal available = record.AddWeight - alreadyUsed; + decimal toConsume = Math.Min(available, remainingToUse); + + totalCost += toConsume * record.UnitPrice; + remainingToUse -= toConsume; + alreadyUsed = 0; + } + + fifoOffsets[medicinalId] = fifoOffsets.GetValueOrDefault(medicinalId, 0) + usedWeight; + return Math.Round(totalCost, 4, MidpointRounding.AwayFromZero); + } + + /// 计算同比增涨率(%),基期为 0 时返回 0 + private static decimal CalcTrend(T baseVal, T currentVal) where T : struct, IConvertible + { + decimal b = Convert.ToDecimal(baseVal); + decimal c = Convert.ToDecimal(currentVal); + if (b == 0) return 0; + return Math.Round((c - b) / b * 100, 1, MidpointRounding.AwayFromZero); + } + + /// 返回全零的 12 个月明细列表 + private static List BuildEmptyMonthList() + { + var list = new List(12); + for (int m = 1; m <= 12; m++) + list.Add(new MonthlyDetailDto { Month = $"{m}月" }); + return list; + } +} diff --git a/SimpleAdmin.Application/SimpleAdmin.Application.xml b/SimpleAdmin.Application/SimpleAdmin.Application.xml index 4676701..939e70e 100644 --- a/SimpleAdmin.Application/SimpleAdmin.Application.xml +++ b/SimpleAdmin.Application/SimpleAdmin.Application.xml @@ -215,6 +215,127 @@ + + + 年度统计数据 DTO(对应前端 YearlyStatisticsDto) + + + + 统计年份(如 "2025") + + + 处方总花费(元) + + + 处方总数量(张) + + + 员工处方花费(元) + + + 家属处方花费(元) + + + 员工处方数量(张) + + + 家属处方数量(张) + + + 药材耗材花费(元)—— 当年所有入库药材总价 + + + 服务人数(当年有过处方记录的不重复人员数) + + + 处方总花费同比趋势 (%) + + + 处方总数量同比趋势 (%) + + + 药材花费同比趋势 (%) + + + 服务人数同比趋势 (%) + + + + 月度明细数据 DTO(对应前端 MonthlyDetailDto) + + + + 月份显示(如 "1月") + + + 员工处方花费(元) + + + 家属处方花费(元) + + + 员工处方数量(张) + + + 家属处方数量(张) + + + 药材耗材花费(元)—— 当月入库总价 + + + + 全量总览 DTO(不区分年份,含剩余药材价值) + + + + 处方总花费(元,含所有年份) + + + 处方总数量(张) + + + 员工处方花费(元) + + + 家属处方花费(元) + + + 员工处方数量(张) + + + 家属处方数量(张) + + + 药材采购总花费(元,历史所有入库总额) + + + 累计服务人数(不重复人员) + + + 当前剩余药材库存价值(元,按最新入库单价估算) + + + + 统计分析 Service 接口 + + + + + 获取全量总览数据(不区分年份,含剩余药材库存价值) + + + + + 获取指定年份的年度汇总统计数据(含同比趋势) + + 年份,如 2025 + + + + 获取指定年份的逐月明细数据(返回 12 条,无数据月份补零) + + 年份,如 2025 + 机构服务 @@ -845,5 +966,40 @@ + + + 统计分析 Service 实现 + + + + + + + + + + + + + + 计算指定年份的聚合数据(用于年度统计与同比计算) + + + + + 读取 FIFO 入库数据(按时间升序) + + + + + 按 FIFO 规则(含累计偏移)计算单次消耗成本,并推进偏移量 + + + + 计算同比增涨率(%),基期为 0 时返回 0 + + + 返回全零的 12 个月明细列表 + diff --git a/SimpleAdmin.SqlSugar/SqlSugar.Development.json b/SimpleAdmin.SqlSugar/SqlSugar.Development.json index b9f6e60..a945d35 100644 --- a/SimpleAdmin.SqlSugar/SqlSugar.Development.json +++ b/SimpleAdmin.SqlSugar/SqlSugar.Development.json @@ -5,7 +5,7 @@ "ConnectionStrings": [ { "ConfigId": "SimpleAdmin", //租户ID - "ConnectionString": "AllowLoadLocalInfile=true;Server=58.17.132.2;User ID=admin;Password=so^867z$4S;Port=16306;Database=WeiBingJu;CharSet=utf8mb4;pooling=true;SslMode=None;min pool size=1;Connection Timeout=30;Pooling=false", //连接字符串 + "ConnectionString": "AllowLoadLocalInfile=true;Server=116.168.197.2;User ID=admin;Password=so^867z$4S;Port=16306;Database=WeiBingJu;CharSet=utf8mb4;pooling=true;SslMode=None;min pool size=1;Connection Timeout=30;Pooling=false", //连接字符串 "DbType": "MySql", //数据库类型 WeiBingJu "IsAutoCloseConnection": true //是否自动释放 weibingjutest } diff --git a/SimpleAdmin.SqlSugar/SqlSugar.Production.json b/SimpleAdmin.SqlSugar/SqlSugar.Production.json index 90b9151..01d1779 100644 --- a/SimpleAdmin.SqlSugar/SqlSugar.Production.json +++ b/SimpleAdmin.SqlSugar/SqlSugar.Production.json @@ -5,7 +5,7 @@ "ConnectionStrings": [ { "ConfigId": "SimpleAdmin", //租户ID - "ConnectionString": "AllowLoadLocalInfile=true;Server=58.17.132.2;User ID=admin;Password=so^867z$4S;Port=16306;Database=WeiBingJu;CharSet=utf8mb4;pooling=true;SslMode=None;min pool size=1;Connection Timeout=30;Pooling=false", //连接字符串 + "ConnectionString": "AllowLoadLocalInfile=true;Server=116.168.197.2;User ID=admin;Password=so^867z$4S;Port=16306;Database=WeiBingJu;CharSet=utf8mb4;pooling=true;SslMode=None;min pool size=1;Connection Timeout=30;Pooling=false", //连接字符串 "DbType": "MySql", //数据库类型 "IsAutoCloseConnection": true //是否自动释放 } diff --git a/SimpleAdmin.Web.Core/AntDeploy.json b/SimpleAdmin.Web.Core/AntDeploy.json new file mode 100644 index 0000000..c8a7e49 --- /dev/null +++ b/SimpleAdmin.Web.Core/AntDeploy.json @@ -0,0 +1,57 @@ +{ + "Env": [], + "IIsConfig": { + "SdkType": null, + "WebSiteName": "", + "LastEnvName": null, + "EnvPairList": [] + }, + "WindowsServiveConfig": { + "ServiceName": "", + "SdkType": null, + "LastEnvName": null, + "EnvPairList": [] + }, + "LinuxServiveConfig": { + "ServiceName": "", + "EnvParam": "", + "LastEnvName": null, + "EnvPairList": [] + }, + "DockerConfig": { + "Prot": "", + "AspNetCoreEnv": "", + "LastEnvName": null, + "RemoveDaysFromPublished": "10", + "WorkDir": "", + "Volume": "", + "Other": "", + "EnvPairList": [] + }, + "DockerImageConfig": { + "BaseHttpProxy": "", + "BaseImage": "", + "BaseImageCredential": { + "UserName": "", + "Password": "" + }, + "TargetImage": "", + "TargetHttpProxy": "", + "TargetTags": [ + "" + ], + "TargetImageCredential": { + "UserName": "", + "Password": "" + }, + "ImageFormat": "Docker", + "Entrypoint": [ + "" + ], + "Cmd": [ + "" + ], + "IgnoreList": [], + "SkipExistingImages": false + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/WeiBingJu/StatisticsController.cs b/SimpleAdmin.Web.Core/Controllers/Application/WeiBingJu/StatisticsController.cs new file mode 100644 index 0000000..2bfd80f --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/WeiBingJu/StatisticsController.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// +// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: +// 1.请不要删除和修改根目录下的LICENSE文件。 +// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 +// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin + +using MoYu.FriendlyException; +using SimpleAdmin.Application.Services.Statistics; + +namespace SimpleAdmin.Web.Core; + +/// +/// 统计分析 +/// +[ApiDescriptionSettings("Application", Tag = "统计分析")] +[Route("biz/weibingju/statistics")] +[RolePermission] +public class StatisticsController : IDynamicApiController +{ + private readonly IStatisticsService _service; + + public StatisticsController(IStatisticsService service) + { + _service = service; + } + + /// + /// 获取全量总览数据(不区分年份,含剩余药材库存价值) + /// + [HttpGet("overview")] + [DisplayName("获取全量总览数据")] + public async Task GetTotalOverview() + { + return await _service.GetTotalOverview(); + } + + /// + /// 获取年度统计数据(含同比趋势) + /// + [HttpGet("yearly")] + [DisplayName("获取年度统计数据")] + public async Task GetYearlyStatistics([FromQuery] string year) + { + if (!int.TryParse(year, out int yearInt) || yearInt < 2000 || yearInt > 2100) + throw Oops.Bah("年份参数不合法"); + + return await _service.GetYearlyStatistics(yearInt); + } + + /// + /// 获取月度明细数据(返回 12 个月,无数据月份自动补零) + /// + [HttpGet("monthly")] + [DisplayName("获取月度明细数据")] + public async Task GetMonthlyDetails([FromQuery] string year) + { + if (!int.TryParse(year, out int yearInt) || yearInt < 2000 || yearInt > 2100) + throw Oops.Bah("年份参数不合法"); + + return await _service.GetMonthlyDetails(yearInt); + } +} diff --git a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml index 5ecb907..d5f8800 100644 --- a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml +++ b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml @@ -420,6 +420,26 @@ 获取关系类型 + + + 统计分析 + + + + + 获取全量总览数据(不区分年份,含剩余药材库存价值) + + + + + 获取年度统计数据(含同比趋势) + + + + + 获取月度明细数据(返回 12 个月,无数据月份自动补零) + + B端登录控制器 diff --git a/SimpleAdmin.Web.Entry/AntDeploy.json b/SimpleAdmin.Web.Entry/AntDeploy.json index f46bc36..3212d13 100644 --- a/SimpleAdmin.Web.Entry/AntDeploy.json +++ b/SimpleAdmin.Web.Entry/AntDeploy.json @@ -7,7 +7,7 @@ { "UserName": "root", "Pwd": "DA9C12691417F47DE6F7FED4AC9DC2B0BFFAA5D2C8E3609D29A2595461736381", - "Host": "58.17.132.2:12005", + "Host": "116.168.197.2:12005", "NickName": "10楼liunx未病局测试环境", "IIsFireUrl": null, "DockerFireUrl": "",