新增统计功能

This commit is contained in:
fuenmao
2026-02-24 15:05:47 +08:00
parent f96ce8fb07
commit 589dd0305b
10 changed files with 970 additions and 3 deletions
@@ -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> </summary>
<returns></returns> <returns></returns>
</member> </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"> <member name="T:SimpleAdmin.Application.IOrgService">
<summary> <summary>
机构服务 机构服务
@@ -845,5 +966,40 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </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> </members>
</doc> </doc>
@@ -5,7 +5,7 @@
"ConnectionStrings": [ "ConnectionStrings": [
{ {
"ConfigId": "SimpleAdmin", //租户ID "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 "DbType": "MySql", //数据库类型 WeiBingJu
"IsAutoCloseConnection": true //是否自动释放 weibingjutest "IsAutoCloseConnection": true //是否自动释放 weibingjutest
} }
@@ -5,7 +5,7 @@
"ConnectionStrings": [ "ConnectionStrings": [
{ {
"ConfigId": "SimpleAdmin", //租户ID "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", //数据库类型 "DbType": "MySql", //数据库类型
"IsAutoCloseConnection": true //是否自动释放 "IsAutoCloseConnection": true //是否自动释放
} }
+57
View File
@@ -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
}
}
@@ -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;
/// <summary>
/// 统计分析
/// </summary>
[ApiDescriptionSettings("Application", Tag = "统计分析")]
[Route("biz/weibingju/statistics")]
[RolePermission]
public class StatisticsController : IDynamicApiController
{
private readonly IStatisticsService _service;
public StatisticsController(IStatisticsService service)
{
_service = service;
}
/// <summary>
/// 获取全量总览数据(不区分年份,含剩余药材库存价值)
/// </summary>
[HttpGet("overview")]
[DisplayName("获取全量总览数据")]
public async Task<dynamic> GetTotalOverview()
{
return await _service.GetTotalOverview();
}
/// <summary>
/// 获取年度统计数据(含同比趋势)
/// </summary>
[HttpGet("yearly")]
[DisplayName("获取年度统计数据")]
public async Task<dynamic> GetYearlyStatistics([FromQuery] string year)
{
if (!int.TryParse(year, out int yearInt) || yearInt < 2000 || yearInt > 2100)
throw Oops.Bah("年份参数不合法");
return await _service.GetYearlyStatistics(yearInt);
}
/// <summary>
/// 获取月度明细数据(返回 12 个月,无数据月份自动补零)
/// </summary>
[HttpGet("monthly")]
[DisplayName("获取月度明细数据")]
public async Task<dynamic> GetMonthlyDetails([FromQuery] string year)
{
if (!int.TryParse(year, out int yearInt) || yearInt < 2000 || yearInt > 2100)
throw Oops.Bah("年份参数不合法");
return await _service.GetMonthlyDetails(yearInt);
}
}
@@ -420,6 +420,26 @@
获取关系类型 获取关系类型
</summary> </summary>
</member> </member>
<member name="T:SimpleAdmin.Web.Core.StatisticsController">
<summary>
统计分析
</summary>
</member>
<member name="M:SimpleAdmin.Web.Core.StatisticsController.GetTotalOverview">
<summary>
获取全量总览数据(不区分年份,含剩余药材库存价值)
</summary>
</member>
<member name="M:SimpleAdmin.Web.Core.StatisticsController.GetYearlyStatistics(System.String)">
<summary>
获取年度统计数据(含同比趋势)
</summary>
</member>
<member name="M:SimpleAdmin.Web.Core.StatisticsController.GetMonthlyDetails(System.String)">
<summary>
获取月度明细数据(返回 12 个月,无数据月份自动补零)
</summary>
</member>
<member name="T:SimpleAdmin.Web.Core.AuthBController"> <member name="T:SimpleAdmin.Web.Core.AuthBController">
<summary> <summary>
B端登录控制器 B端登录控制器
+1 -1
View File
@@ -7,7 +7,7 @@
{ {
"UserName": "root", "UserName": "root",
"Pwd": "DA9C12691417F47DE6F7FED4AC9DC2B0BFFAA5D2C8E3609D29A2595461736381", "Pwd": "DA9C12691417F47DE6F7FED4AC9DC2B0BFFAA5D2C8E3609D29A2595461736381",
"Host": "58.17.132.2:12005", "Host": "116.168.197.2:12005",
"NickName": "10楼liunx未病局测试环境", "NickName": "10楼liunx未病局测试环境",
"IIsFireUrl": null, "IIsFireUrl": null,
"DockerFireUrl": "", "DockerFireUrl": "",