新增统计功能
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 年度统计数据 DTO(对应前端 YearlyStatisticsDto)
|
||||
/// </summary>
|
||||
public class YearlyStatisticsDto
|
||||
{
|
||||
/// <summary>统计年份(如 "2025")</summary>
|
||||
public string Year { get; set; }
|
||||
|
||||
/// <summary>处方总花费(元)</summary>
|
||||
public decimal TotalCost { get; set; }
|
||||
|
||||
/// <summary>处方总数量(张)</summary>
|
||||
public int TotalQty { get; set; }
|
||||
|
||||
/// <summary>员工处方花费(元)</summary>
|
||||
public decimal StaffCost { get; set; }
|
||||
|
||||
/// <summary>家属处方花费(元)</summary>
|
||||
public decimal FamilyCost { get; set; }
|
||||
|
||||
/// <summary>员工处方数量(张)</summary>
|
||||
public int StaffQty { get; set; }
|
||||
|
||||
/// <summary>家属处方数量(张)</summary>
|
||||
public int FamilyQty { get; set; }
|
||||
|
||||
/// <summary>药材耗材花费(元)—— 当年所有入库药材总价</summary>
|
||||
public decimal MaterialCost { get; set; }
|
||||
|
||||
/// <summary>服务人数(当年有过处方记录的不重复人员数)</summary>
|
||||
public int ServiceCount { get; set; }
|
||||
|
||||
// ---------- 同比趋势(百分比,正数为增长,负数为下降) ----------
|
||||
|
||||
/// <summary>处方总花费同比趋势 (%)</summary>
|
||||
public decimal TotalCostTrend { get; set; }
|
||||
|
||||
/// <summary>处方总数量同比趋势 (%)</summary>
|
||||
public decimal TotalQtyTrend { get; set; }
|
||||
|
||||
/// <summary>药材花费同比趋势 (%)</summary>
|
||||
public decimal MaterialCostTrend { get; set; }
|
||||
|
||||
/// <summary>服务人数同比趋势 (%)</summary>
|
||||
public decimal ServiceCountTrend { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 月度明细数据 DTO(对应前端 MonthlyDetailDto)
|
||||
/// </summary>
|
||||
public class MonthlyDetailDto
|
||||
{
|
||||
/// <summary>月份显示(如 "1月")</summary>
|
||||
public string Month { get; set; }
|
||||
|
||||
/// <summary>员工处方花费(元)</summary>
|
||||
public decimal StaffCost { get; set; }
|
||||
|
||||
/// <summary>家属处方花费(元)</summary>
|
||||
public decimal FamilyCost { get; set; }
|
||||
|
||||
/// <summary>员工处方数量(张)</summary>
|
||||
public int StaffQty { get; set; }
|
||||
|
||||
/// <summary>家属处方数量(张)</summary>
|
||||
public int FamilyQty { get; set; }
|
||||
|
||||
/// <summary>药材耗材花费(元)—— 当月入库总价</summary>
|
||||
public decimal MaterialCost { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全量总览 DTO(不区分年份,含剩余药材价值)
|
||||
/// </summary>
|
||||
public class TotalOverviewDto
|
||||
{
|
||||
/// <summary>处方总花费(元,含所有年份)</summary>
|
||||
public decimal TotalCost { get; set; }
|
||||
|
||||
/// <summary>处方总数量(张)</summary>
|
||||
public int TotalQty { get; set; }
|
||||
|
||||
/// <summary>员工处方花费(元)</summary>
|
||||
public decimal StaffCost { get; set; }
|
||||
|
||||
/// <summary>家属处方花费(元)</summary>
|
||||
public decimal FamilyCost { get; set; }
|
||||
|
||||
/// <summary>员工处方数量(张)</summary>
|
||||
public int StaffQty { get; set; }
|
||||
|
||||
/// <summary>家属处方数量(张)</summary>
|
||||
public int FamilyQty { get; set; }
|
||||
|
||||
/// <summary>药材采购总花费(元,历史所有入库总额)</summary>
|
||||
public decimal MaterialCost { get; set; }
|
||||
|
||||
/// <summary>累计服务人数(不重复人员)</summary>
|
||||
public int ServiceCount { get; set; }
|
||||
|
||||
/// <summary>当前剩余药材库存价值(元,按最新入库单价估算)</summary>
|
||||
public decimal RemainingMedicinalValue { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 统计分析 Service 接口
|
||||
/// </summary>
|
||||
public interface IStatisticsService : ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取全量总览数据(不区分年份,含剩余药材库存价值)
|
||||
/// </summary>
|
||||
Task<TotalOverviewDto> GetTotalOverview();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定年份的年度汇总统计数据(含同比趋势)
|
||||
/// </summary>
|
||||
/// <param name="year">年份,如 2025</param>
|
||||
Task<YearlyStatisticsDto> GetYearlyStatistics(int year);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定年份的逐月明细数据(返回 12 条,无数据月份补零)
|
||||
/// </summary>
|
||||
/// <param name="year">年份,如 2025</param>
|
||||
Task<List<MonthlyDetailDto>> GetMonthlyDetails(int year);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 统计分析 Service 实现
|
||||
/// </summary>
|
||||
public class StatisticsService : DbRepository<Prescription>, IStatisticsService
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 公开接口
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TotalOverviewDto> GetTotalOverview()
|
||||
{
|
||||
// 1) 所有处方(按创建时间升序,FIFO 需要)
|
||||
var prescriptionInfos = await Context.Queryable<Prescription>()
|
||||
.LeftJoin<Person>((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<PrescriptionHerb>()
|
||||
.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<long, decimal>();
|
||||
|
||||
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<MedicinalAddLog>()
|
||||
.SumAsync(l => l.TotalPrice);
|
||||
|
||||
// 3) 累计服务人数(不重复人员)
|
||||
var serviceCount = prescriptionInfos.Select(x => x.PersonId).Distinct().Count();
|
||||
|
||||
// 4) 剩余药材库存价值:每种药材当前库存 × 最新入库单价
|
||||
var medicinals = await Context.Queryable<Medicinal>()
|
||||
.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<MedicinalAddLog>()
|
||||
.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),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<YearlyStatisticsDto> 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),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<MonthlyDetailDto>> 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<Prescription>()
|
||||
.LeftJoin<Person>((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<PrescriptionHerb>()
|
||||
.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<Prescription>()
|
||||
.Where(p => p.VisitDate < start)
|
||||
.OrderBy(p => p.CreateTime)
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var fifoOffsets = new Dictionary<long, decimal>();
|
||||
|
||||
// 先消耗历史处方(建立偏移量),不需要计入结果
|
||||
if (historyIds.Any())
|
||||
{
|
||||
var historyHerbs = await Context.Queryable<PrescriptionHerb>()
|
||||
.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<MedicinalAddLog>()
|
||||
.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<MonthlyDetailDto>(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;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 私有辅助方法
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 计算指定年份的聚合数据(用于年度统计与同比计算)
|
||||
/// </summary>
|
||||
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<Prescription>()
|
||||
.LeftJoin<Person>((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<PrescriptionHerb>()
|
||||
.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<Prescription>()
|
||||
.Where(p => p.VisitDate < start)
|
||||
.OrderBy(p => p.CreateTime)
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var fifoOffsets = new Dictionary<long, decimal>();
|
||||
if (historyIds.Any())
|
||||
{
|
||||
var historyHerbs = await Context.Queryable<PrescriptionHerb>()
|
||||
.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<MedicinalAddLog>()
|
||||
.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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 FIFO 入库数据(按时间升序)
|
||||
/// </summary>
|
||||
private async Task<Dictionary<long, List<MedicinalFifoRecord>>> GetFifoData(List<long> medicinalIds)
|
||||
{
|
||||
if (!medicinalIds.Any())
|
||||
return new Dictionary<long, List<MedicinalFifoRecord>>();
|
||||
|
||||
var logs = await Context.Queryable<MedicinalAddLog>()
|
||||
.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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 FIFO 规则(含累计偏移)计算单次消耗成本,并推进偏移量
|
||||
/// </summary>
|
||||
private static decimal CalculateFifoCost(
|
||||
Dictionary<long, List<MedicinalFifoRecord>> fifoData,
|
||||
Dictionary<long, decimal> 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);
|
||||
}
|
||||
|
||||
/// <summary>计算同比增涨率(%),基期为 0 时返回 0</summary>
|
||||
private static decimal CalcTrend<T>(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);
|
||||
}
|
||||
|
||||
/// <summary>返回全零的 12 个月明细列表</summary>
|
||||
private static List<MonthlyDetailDto> BuildEmptyMonthList()
|
||||
{
|
||||
var list = new List<MonthlyDetailDto>(12);
|
||||
for (int m = 1; m <= 12; m++)
|
||||
list.Add(new MonthlyDetailDto { Month = $"{m}月" });
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -215,6 +215,127 @@
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto">
|
||||
<summary>
|
||||
年度统计数据 DTO(对应前端 YearlyStatisticsDto)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.Year">
|
||||
<summary>统计年份(如 "2025")</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.TotalCost">
|
||||
<summary>处方总花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.TotalQty">
|
||||
<summary>处方总数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.StaffCost">
|
||||
<summary>员工处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.FamilyCost">
|
||||
<summary>家属处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.StaffQty">
|
||||
<summary>员工处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.FamilyQty">
|
||||
<summary>家属处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.MaterialCost">
|
||||
<summary>药材耗材花费(元)—— 当年所有入库药材总价</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.ServiceCount">
|
||||
<summary>服务人数(当年有过处方记录的不重复人员数)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.TotalCostTrend">
|
||||
<summary>处方总花费同比趋势 (%)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.TotalQtyTrend">
|
||||
<summary>处方总数量同比趋势 (%)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.MaterialCostTrend">
|
||||
<summary>药材花费同比趋势 (%)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.YearlyStatisticsDto.ServiceCountTrend">
|
||||
<summary>服务人数同比趋势 (%)</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto">
|
||||
<summary>
|
||||
月度明细数据 DTO(对应前端 MonthlyDetailDto)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.Month">
|
||||
<summary>月份显示(如 "1月")</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.StaffCost">
|
||||
<summary>员工处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.FamilyCost">
|
||||
<summary>家属处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.StaffQty">
|
||||
<summary>员工处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.FamilyQty">
|
||||
<summary>家属处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.MonthlyDetailDto.MaterialCost">
|
||||
<summary>药材耗材花费(元)—— 当月入库总价</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto">
|
||||
<summary>
|
||||
全量总览 DTO(不区分年份,含剩余药材价值)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.TotalCost">
|
||||
<summary>处方总花费(元,含所有年份)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.TotalQty">
|
||||
<summary>处方总数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.StaffCost">
|
||||
<summary>员工处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.FamilyCost">
|
||||
<summary>家属处方花费(元)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.StaffQty">
|
||||
<summary>员工处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.FamilyQty">
|
||||
<summary>家属处方数量(张)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.MaterialCost">
|
||||
<summary>药材采购总花费(元,历史所有入库总额)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.ServiceCount">
|
||||
<summary>累计服务人数(不重复人员)</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.Application.Services.Statistics.TotalOverviewDto.RemainingMedicinalValue">
|
||||
<summary>当前剩余药材库存价值(元,按最新入库单价估算)</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Application.Services.Statistics.IStatisticsService">
|
||||
<summary>
|
||||
统计分析 Service 接口
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Application.Services.Statistics.IStatisticsService.GetTotalOverview">
|
||||
<summary>
|
||||
获取全量总览数据(不区分年份,含剩余药材库存价值)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Application.Services.Statistics.IStatisticsService.GetYearlyStatistics(System.Int32)">
|
||||
<summary>
|
||||
获取指定年份的年度汇总统计数据(含同比趋势)
|
||||
</summary>
|
||||
<param name="year">年份,如 2025</param>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Application.Services.Statistics.IStatisticsService.GetMonthlyDetails(System.Int32)">
|
||||
<summary>
|
||||
获取指定年份的逐月明细数据(返回 12 条,无数据月份补零)
|
||||
</summary>
|
||||
<param name="year">年份,如 2025</param>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Application.IOrgService">
|
||||
<summary>
|
||||
机构服务
|
||||
@@ -845,5 +966,40 @@
|
||||
</summary>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:StatisticsService">
|
||||
<summary>
|
||||
统计分析 Service 实现
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:StatisticsService.GetTotalOverview">
|
||||
<inheritdoc/>
|
||||
</member>
|
||||
<member name="M:StatisticsService.GetYearlyStatistics(System.Int32)">
|
||||
<inheritdoc/>
|
||||
</member>
|
||||
<member name="M:StatisticsService.GetMonthlyDetails(System.Int32)">
|
||||
<inheritdoc/>
|
||||
</member>
|
||||
<member name="M:StatisticsService.BuildYearData(System.Int32)">
|
||||
<summary>
|
||||
计算指定年份的聚合数据(用于年度统计与同比计算)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:StatisticsService.GetFifoData(System.Collections.Generic.List{System.Int64})">
|
||||
<summary>
|
||||
读取 FIFO 入库数据(按时间升序)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:StatisticsService.CalculateFifoCost(System.Collections.Generic.Dictionary{System.Int64,System.Collections.Generic.List{SimpleAdmin.Application.Services.Prescription.Dto.MedicinalFifoRecord}},System.Collections.Generic.Dictionary{System.Int64,System.Decimal},System.Int64,System.Decimal)">
|
||||
<summary>
|
||||
按 FIFO 规则(含累计偏移)计算单次消耗成本,并推进偏移量
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:StatisticsService.CalcTrend``1(``0,``0)">
|
||||
<summary>计算同比增涨率(%),基期为 0 时返回 0</summary>
|
||||
</member>
|
||||
<member name="M:StatisticsService.BuildEmptyMonthList">
|
||||
<summary>返回全零的 12 个月明细列表</summary>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
|
||||
Reference in New Issue
Block a user