// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 // // SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: // 1.请不要删除和修改根目录下的LICENSE文件。 // 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 // 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin // 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。 // 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。 // 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。 using System.Linq.Dynamic.Core; using SimpleAdmin.Application.Entity; using Yitter.IdGenerator; public class MedicinalService : DbRepository, IMedicinalService { private readonly ISysUserService _sysUserService; private readonly IDictService _dictService; public MedicinalService(ISysUserService sysUserService, IDictService dictService) { _sysUserService = sysUserService; _dictService = dictService; } public async Task> GetMedicinalList(string medicinalName, int page, int size) { // 1. 获取数据范围(原有逻辑不变) //var dataScope = await _sysUserService.GetLoginUserApiDataScope(); //dataScope?.Remove(UserManager.OrgId); // 2. 分页查询药材基本信息(1次查询,原有逻辑不变) var query = this.AsQueryable() //.WhereIF(dataScope.Any(), m => dataScope.Contains(m.Id)) .WhereIF(!string.IsNullOrEmpty(medicinalName), m => m.MedicinalName.Contains(medicinalName)) .OrderBy(p => p.Number) .Select(m => new MedicinalDto { Id = m.Id, MedicinalName = m.MedicinalName, Stock = m.Number, // 当前库存 }); var pageInfo = await query.ToPagedListAsync(page, size); if (!pageInfo.List.Any()) return pageInfo; // 3. 批量计算:成本、剩余价值、总使用量(仅2次查询,无循环查询) var medicinalIds = pageInfo.List.Select(dto => dto.Id).ToList(); var costAndUsageDict = await BatchCalculateCostAndValue(medicinalIds); // 4. 内存中赋值所有指标(无数据库查询) foreach (var item in pageInfo.List) { if (costAndUsageDict.TryGetValue(item.Id, out var metrics)) { item.UsedCost = metrics.usedCost; // 使用成本 item.RemainingValue = metrics.remainingValue; // 剩余价值 item.TotalUsedWeight = metrics.totalUsedWeight; // 新增:总使用量 } else { // 无数据时默认值 item.UsedCost = 0; item.RemainingValue = 0; item.TotalUsedWeight = 0; } } return pageInfo; } /// /// 获取药材下拉 /// /// public async Task> GetMedicinalDictList() { ////获取数据范围 //var dataScope = await _sysUserService.GetLoginUserApiDataScope(); //dataScope?.Remove(UserManager.OrgId); //药材查询 return await this.AsQueryable() //.WhereIF(dataScope.Any(), m => dataScope.Contains(m.Id)) //机构ID查询 .OrderBy(p => p.Number) .Select(m => new MedicinalDto { Id = m.Id, MedicinalName = m.MedicinalName, Stock = m.Number, }).ToListAsync(); } /// /// 获取药材消耗记录 /// /// /// /// public async Task> GetConsumptionRecords(long medicinalId, int page, int size) { return await Context.Queryable() // 先关联处方 .LeftJoin((ph, p) => ph.PrescriptionId == p.Id) // 再关联就诊人 .LeftJoin((ph, p, pe) => p.PersonId == pe.Id) // 再关联员工 .LeftJoin((ph, p, pe, emp) => pe.EmployeeId != null && pe.EmployeeId == emp.Id) // 条件也使用多参数 .Where((ph, p, pe, emp) => ph.MedicinalId == medicinalId) .OrderByDescending((ph, p, pe, emp) => p.VisitDate) // 投影使用所有表别名 .Select((ph, p, pe, emp) => new MedicinalConsumptionRecordDto { AmountG = ph.AmountG, PersonName = pe.Name, IsRelative = pe.EmployeeId != null, EmployeeName = emp == null ? null : emp.Name, UseTime = p.VisitDate }) // 分页 .ToPagedListAsync(page, size); } /// /// 获取药材添加记录 /// /// /// public async Task> GetAdditionRecords(long medicinalId, int page, int size) { var returnList = await Context.Queryable() .Where(log => log.MedicinalId == medicinalId) .OrderByDescending(dto => dto.CreateTime) .Select(log => new MedicinalAddLogDto { Id = log.Id, AddWeight = log.AddWeight, UnitPrice = log.UnitPrice, AddTime = log.CreateTime.Value, AddUserName = log.CreateUser, TotalPrice = log.TotalPrice, CanEdit = false }) .ToPagedListAsync(page, size); foreach (var item in returnList.List) { if (item.AddTime.Date == DateTime.Now.Date) { item.CanEdit = true; } } return returnList; } /// /// 添加药材 /// /// /// public async Task AddMedicinal(AddMedicinalInput input) { if (input.TotalPrice <= 0 || input.AddWeight <= 0) throw Oops.Bah("请填写正确的金额或者重量!"); // 开启事务 await Context.Ado.BeginTranAsync(); try { // 检查药材是否已存在 var existing = await Context.Queryable() .Where(m => m.Id == input.Id) .FirstAsync(); if (existing != null) { // 更新库存 existing.Number += input.AddWeight; await Context.Updateable(existing).ExecuteCommandAsync(); } else { var medicinalModel = await Context.Queryable() .Where(m => m.MedicinalName == input.MedicinalName) .FirstAsync(); if (medicinalModel != null) throw Oops.Bah("已存在该药材,请在下拉框选择!"); input.Id = YitIdHelper.NextId(); // 新增药材记录 await Context.Insertable(new Medicinal { Id = input.Id.Value, Number = input.AddWeight, MedicinalName = input.MedicinalName, }).ExecuteCommandAsync(); } decimal unitPrice = 0m; if (input.AddWeight > 0) { unitPrice = Decimal.Round(input.TotalPrice / input.AddWeight, 2, MidpointRounding.AwayFromZero); } // 添加记录 await Context.Insertable(new MedicinalAddLog { MedicinalId = input.Id.Value, AddWeight = input.AddWeight, UnitPrice = unitPrice, TotalPrice = input.TotalPrice }).ExecuteCommandAsync(); await Context.Ado.CommitTranAsync(); } catch { await Context.Ado.RollbackTranAsync(); throw; } return true; } /// /// 修改药材添加记录 /// /// /// /// /// public async Task UpdateAdditionRecord(UpdateMedicinalAddLogInput input) { if(input.TotalPrice<=0 || input.AddWeight<=0) throw Oops.Bah("请填写正确的金额或者重量!"); var log = await Context.Queryable().Where(l => l.Id == input.Id).FirstAsync(); if (log == null) { throw Oops.Bah("记录不存在!"); } // 检查是否是当天记录 if (log.CreateTime.Value.Date != DateTime.Now.Date) { throw Oops.Bah("只能修改当天的记录!"); } // 计算重量变化 var weightDiff = input.AddWeight - log.AddWeight; // 开启事务 await Context.Ado.BeginTranAsync(); try { // 更新记录 log.AddWeight = input.AddWeight; log.TotalPrice = input.TotalPrice; log.UnitPrice = Decimal.Round(input.TotalPrice / input.AddWeight, 2, MidpointRounding.AwayFromZero); await Context.Updateable(log).ExecuteCommandAsync(); // 更新药材库存 var medicinal = await Context.Queryable() .Where(m => m.Id == log.MedicinalId) .FirstAsync(); if (medicinal != null) { medicinal.Number += weightDiff; if(medicinal.Number<0) throw Oops.Bah("无法修改,库存不足!"); await Context.Updateable(medicinal).ExecuteCommandAsync(); } await Context.Ado.CommitTranAsync(); } catch { await Context.Ado.RollbackTranAsync(); throw; } return true; } /// /// 删除药材添加记录 /// /// /// /// public async Task DeleteAdditionRecord(long id) { var log = await Context.Queryable().Where(l => l.Id == id).FirstAsync(); if (log == null) { throw Oops.Bah("记录不存在!"); } // 检查是否是当天记录 if (log.CreateTime.Value.Date != DateTime.Now.Date) { throw Oops.Bah("只能删除当天的记录!"); } // 开启事务 await Context.Ado.BeginTranAsync(); try { // 删除记录 log.IsDelete = true; await Context.Updateable(log).ExecuteCommandAsync(); // 更新药材库存 var medicinal = await Context.Queryable() .Where(m => m.Id == log.MedicinalId) .FirstAsync(); if (medicinal != null) { medicinal.Number -= log.AddWeight; if(medicinal.Number<0) throw Oops.Bah("无法删除,库存不足!"); await Context.Updateable(medicinal).ExecuteCommandAsync(); } await Context.Ado.CommitTranAsync(); } catch { await Context.Ado.RollbackTranAsync(); throw; } return true; } /// /// 批量计算:使用成本、剩余价值、总使用量(先进先出法) /// /// 需要计算的药材ID列表 private async Task> BatchCalculateCostAndValue(List medicinalIds) { var result = new Dictionary(); if (!medicinalIds.Any()) return result; // 1. 批量查询所有药材的【当前库存】(1次查询) var medicinals = await Context.Queryable() .Where(m => medicinalIds.Contains(m.Id)) .Select(m => new { m.Id, m.Number }) // Number = 当前库存 .ToListAsync(); // 2. 批量查询所有药材的【入库记录】(1次查询,仅查需要的字段) var allAddLogs = await Context.Queryable() .Where(l => medicinalIds.Contains(l.MedicinalId)) .OrderBy(l => l.CreateTime) // 先进先出:按入库时间排序 .Select(l => new { l.MedicinalId, l.AddWeight, // 入库量 l.UnitPrice, // 单价(用于计算成本) l.TotalPrice // 总成本(用于计算成本) }) .ToListAsync(); // 3. 按【药材ID】分组入库记录(内存处理,无数据库查询) var groupedLogs = allAddLogs .GroupBy(l => l.MedicinalId) .ToDictionary(g => g.Key, g => g.ToList()); // 4. 遍历每个药材,内存中计算所有指标 foreach (var med in medicinals) { // 初始化默认值(无入库记录时) decimal totalIn = 0; // 总入库量(AddWeight 总和) decimal totalCost = 0; // 总入库成本 decimal totalUsedWeight = 0; // 总使用量 decimal usedCost = 0; // 使用成本 decimal remainingValue = 0;// 剩余价值 // 若有入库记录,计算总入库量、总成本 if (groupedLogs.TryGetValue(med.Id, out var addLogs) && addLogs.Any()) { totalIn = addLogs.Sum(l => l.AddWeight); // 总入库量 = 所有 AddWeight 求和 totalCost = addLogs.Sum(l => l.TotalPrice); // 总入库成本 } // 计算总使用量:总入库量 - 当前库存(若结果为负,说明数据异常,按0处理) totalUsedWeight = Math.Max(0, totalIn - med.Number); // 若有使用量,继续用先进先出法计算使用成本 if (totalUsedWeight > 0 && groupedLogs.TryGetValue(med.Id, out var validLogs)) { decimal remainingToCalculate = totalUsedWeight; foreach (var log in validLogs) { if (remainingToCalculate <= 0) break; // 当前入库记录的量 <= 剩余未计算的使用量:全部计入成本 if (log.AddWeight <= remainingToCalculate) { usedCost += log.AddWeight * log.UnitPrice; remainingToCalculate -= log.AddWeight; } // 当前入库记录的量 > 剩余未计算的使用量:仅计算剩余部分 else { usedCost += remainingToCalculate * log.UnitPrice; remainingToCalculate = 0; } } } // 剩余价值 = 总入库成本 - 使用成本(若为负,按0处理) remainingValue = Math.Max(0, totalCost - usedCost); // 存入结果字典 result[med.Id] = (usedCost, remainingValue, totalUsedWeight); } return result; } }