Files
WeiBingJuInterface/SimpleAdmin.Application/Services/Prescription/PrescriptionService.cs
T
2025-09-29 13:48:36 +08:00

552 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using SimpleAdmin.Application.Entity;
using SimpleAdmin.Application.Services.Prescription;
using Yitter.IdGenerator;
public class PrescriptionService : DbRepository<Prescription>, IPrescriptionService
{
public async Task<SqlSugarPagedList<PrescriptionDto>> GetPrescriptionList(PrescriptionQueryInput input)
{
// 原有查询逻辑保持不变
var query = Context.Queryable<Prescription>()
.LeftJoin<Person>((p, pe) => p.PersonId == pe.Id)
.LeftJoin<Person>((p, pe, emp) => pe.EmployeeId != null && pe.EmployeeId == emp.Id)
.WhereIF(!string.IsNullOrEmpty(input.PersonName), (p, pe, emp) =>
input.PersonName.Contains(pe.Name) || pe.Name.Contains(input.PersonName) ||
input.PersonName.Contains(emp.Name) || emp.Name.Contains(input.PersonName))
.WhereIF(input.PersonType.HasValue && input.PersonType == PersonType.Employee, (p, pe) =>
pe.EmployeeId == null)
.WhereIF(input.PersonType.HasValue && input.PersonType != PersonType.Employee, (p, pe) =>
pe.EmployeeId != null);
try
{
// 获取处方基础信息(添加总成本属性)
var prescriptionPage = await query.OrderByDescending((p, pe, emp) => p.CreateTime)
.Select((p, pe, emp) => new PrescriptionDto
{
Id = p.Id,
PersonId = p.PersonId,
PersonName = pe.Name,
PersonType = pe.EmployeeId != null ? PersonType.Relative : PersonType.Employee,
VisitDate = p.VisitDate,
CreateTime = p.CreateTime.Value,
EmployeeName = emp.Name,
CanEdit = false,
TotalMedicineCost = 0 // 初始化总成本
}).ToPagedListAsync(input.PageNum, input.PageSize);
// 设置可编辑状态
foreach (var item in prescriptionPage.List)
{
item.CanEdit = item.CreateTime.Date == DateTime.Now.Date;
}
// 获取当前页所有处方的药材明细
var pidList = prescriptionPage.List.Select(a => a.Id).ToList();
var prescriptionHerbList = await Context.Queryable<PrescriptionHerb>()
.Where(a => pidList.Contains(a.PrescriptionId))
.ToListAsync();
if (prescriptionHerbList.Any())
{
// 1. 按【处方ID+药材ID】分组,统计每个处方中每种药材的总使用量
var prescriptionMedicinalUsage = prescriptionHerbList
.GroupBy(h => new { h.PrescriptionId, h.MedicinalId })
.Select(g => new
{
PrescriptionId = g.Key.PrescriptionId,
MedicinalId = g.Key.MedicinalId,
TotalUsedWeight = g.Sum(h => h.AmountG) // 同一处方中同一药材的总用量
})
.ToList();
// 2. 收集所有涉及的药材ID,批量获取FIFO计算所需数据
var medicinalIds = prescriptionMedicinalUsage.Select(u => u.MedicinalId).Distinct().ToList();
var fifoData = await GetMedicinalFifoData(medicinalIds);
// 3. 按处方ID分组,计算每个处方的总成本
var groupedByPrescription = prescriptionMedicinalUsage
.GroupBy(u => u.PrescriptionId)
.ToDictionary(g => g.Key, g => g.ToList());
// 4. 遍历处方,计算总成本
foreach (var prescription in prescriptionPage.List)
{
if (groupedByPrescription.TryGetValue(prescription.Id, out var usages))
{
foreach (var usage in usages)
{
// 基于FIFO计算当前药材的使用成本
var herbCost = CalculateHerbCostByFifo(
fifoData,
usage.MedicinalId,
usage.TotalUsedWeight
);
// 累加到处方总成本
prescription.TotalMedicineCost += herbCost;
}
}
}
}
return prescriptionPage;
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
throw;
}
}
/// <summary>
/// 获取药材FIFO计算所需的基础数据(入库记录)
/// </summary>
private async Task<Dictionary<long, List<MedicinalFifoRecord>>> GetMedicinalFifoData(List<long> medicinalIds)
{
if (!medicinalIds.Any())
return new Dictionary<long, List<MedicinalFifoRecord>>();
// 批量查询所有涉及药材的入库记录(按时间排序)
var addLogs = await Context.Queryable<MedicinalAddLog>()
.Where(l => medicinalIds.Contains(l.MedicinalId))
.OrderBy(l => l.CreateTime) // FIFO核心:按入库时间排序
.Select(l => new MedicinalFifoRecord
{
MedicinalId = l.MedicinalId,
AddWeight = l.AddWeight,
UnitPrice = l.UnitPrice
})
.ToListAsync();
// 按药材ID分组
return addLogs.GroupBy(l => l.MedicinalId)
.ToDictionary(g => g.Key, g => g.ToList());
}
/// <summary>
/// 按FIFO规则计算单种药材的使用成本
/// </summary>
private decimal CalculateHerbCostByFifo(
Dictionary<long, List<MedicinalFifoRecord>> fifoData,
long medicinalId,
decimal usedWeight)
{
if (usedWeight <= 0 || !fifoData.TryGetValue(medicinalId, out var records) || !records.Any())
return 0;
decimal remainingToCalculate = usedWeight;
decimal totalCost = 0;
// 按入库顺序依次消耗
foreach (var record in records)
{
if (remainingToCalculate <= 0)
break;
if (record.AddWeight <= remainingToCalculate)
{
// 当前批次全部消耗
totalCost += record.AddWeight * record.UnitPrice;
remainingToCalculate -= record.AddWeight;
}
else
{
// 消耗当前批次的部分量
totalCost += remainingToCalculate * record.UnitPrice;
remainingToCalculate = 0;
}
}
return totalCost;
}
/// <summary>
/// 获取处方详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<PrescriptionDetailDto> GetPrescriptionDetail(long id)
{
var prescription = await Context.Queryable<Prescription>()
.Where(p => p.Id == id)
.FirstAsync();
if (prescription == null)
{
throw Oops.Bah("处方不存在");
}
var person = await Context.Queryable<Person>()
.Where(pe => pe.Id == prescription.PersonId)
.FirstAsync();
var herbs = await Context.Queryable<PrescriptionHerb, Medicinal>((ph, mt) => ph.MedicinalId == mt.Id)
.Where(ph => ph.PrescriptionId == id)
.Select((ph, mt) => new PrescriptionHerbDto
{
MedicinalId = mt.Id,
MedicinalName = mt.MedicinalName,
AmountG = ph.AmountG,
Note = ph.Note
})
.ToListAsync();
// 判断是否是员工还是家属
PersonDto employee = null;
if (person.EmployeeId.HasValue)
{
var empPerson = await Context.Queryable<Person>()
.Where(pe => pe.Id == person.EmployeeId)
.Select(pe => new PersonDto
{
Id = pe.Id,
Name = pe.Name
})
.FirstAsync();
employee = empPerson;
}
var RelationShipName = await GetRelationTypes();
return new PrescriptionDetailDto
{
Id = prescription.Id,
Person = new PersonDetailDto
{
Id = person.Id,
Name = person.Name,
Birthday = person.Birthday,
Weight = prescription.Weight,
Height = person.Height,
IsEmployee = !person.EmployeeId.HasValue,
EmployeeId = person.EmployeeId,
EmployeeName = employee?.Name,
RelationShip = person.RelationShip,
RelationShipName = RelationShipName.FirstOrDefault(t => t.Id == person.RelationShip)?.Name
},
DiagnosisResult = prescription.DiagnosisResult,
UserMedicinaAdvice = prescription.UserMedicinaAdvice,
VisitDate = prescription.VisitDate,
Herbs = herbs,
CanEdit = prescription.CreateTime.Value.Date == DateTime.Now.Date
};
}
/// <summary>
/// 添加编辑处方记录
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<long> SavePrescription(SavePrescriptionInput input)
{
// 基础参数验证
if (input == null)
throw Oops.Bah("输入参数不能为空");
if (input.Herbs == null || !input.Herbs.Any())
throw Oops.Bah("处方药材不能为空");
await Context.Ado.BeginTranAsync();
try
{
// 用于在方法内记录新创建的员工 id(如果需要)
long? createdEmployeeId = null;
// ---------- 1. 处理前端在 person 中携带的“临时员工”(如果存在) ----------
if (input.Person == null)
throw Oops.Bah("人员信息不能为空");
// 场景 A: 病人是家属(IsEmployee == false),但关联的员工是前端临时新增
if (!input.Person.IsEmployee)
{
// 如果前端明确标记关联员工为临时新增(EmployeeIsTemp),需要先创建员工
if (input.Person.EmployeeIsTemp)
{
if (string.IsNullOrWhiteSpace(input.Person.EmployeeName))
throw Oops.Bah("标记了临时关联员工,但未提供员工姓名");
// 创建新员工记录(作为 Person 表的一条记录,RelationShip=null, EmployeeId=null)
var newEmp = new Person
{
Id = YitIdHelper.NextId(),
Name = input.Person.EmployeeName?.Trim(),
};
await Context.Insertable(newEmp).ExecuteCommandAsync();
createdEmployeeId = newEmp.Id;
}
else if (input.Person.EmployeeId.HasValue && input.Person.EmployeeId.Value > 0)
{
// 如果前端传的是已有员工 ID,校验该员工是否存在
var exists = await Context.Queryable<Person>()
.AnyAsync(p => p.Id == input.Person.EmployeeId.Value);
if (!exists)
throw Oops.Bah($"关联的员工 ID={input.Person.EmployeeId.Value} 不存在");
}
else if (!string.IsNullOrWhiteSpace(input.Person.EmployeeName))
{
// 兼容:即便没有显式 EmployeeIsTemp,前端传了 EmployeeName,我们也可以选择创建员工
var newEmp = new Person
{
Id = YitIdHelper.NextId(),
Name = input.Person.EmployeeName.Trim(),
};
await Context.Insertable(newEmp).ExecuteCommandAsync();
createdEmployeeId = newEmp.Id;
}
else
{
// 没有绑定员工信息(既不是 temp,也没有 employeeId),则无法继续
if (input.Person.Id == null || input.Person.Id <= 0)
throw Oops.Bah("家属未关联员工,请选择或新增关联员工");
}
}
else
{
// 病人是公司员工(IsEmployee == true)
// 如果没有 Id,但前端同时将该员工标记为临时新增,或者传了 Name 而无 Id,则创建员工
if ((!input.Person.Id.HasValue || input.Person.Id.Value <= 0) &&
(!string.IsNullOrWhiteSpace(input.Person.Name) || input.Person.EmployeeIsTemp))
{
// 如果前端对员工也标记了 EmployeeIsTemp 且提供了 EmployeeName,则优先使用 EmployeeName(兼容性)
string empNameForCreate = !string.IsNullOrWhiteSpace(input.Person.Name) ? input.Person.Name.Trim() : input.Person.EmployeeName?.Trim();
if (string.IsNullOrWhiteSpace(empNameForCreate))
throw Oops.Bah("新增员工时姓名不能为空");
var newEmp = new Person
{
Id = YitIdHelper.NextId(),
Name = empNameForCreate,
Birthday = input.Person.Birthday,
Height = input.Person.Height,
};
await Context.Insertable(newEmp).ExecuteCommandAsync();
// 将新创建的员工 id 作为主 person id(病人本身)
input.Person.Id = newEmp.Id;
}
else if (input.Person.Id.HasValue && input.Person.Id.Value > 0)
{
// 校验存在性
var exists = await Context.Queryable<Person>().AnyAsync(p => p.Id == input.Person.Id.Value);
if (!exists)
throw Oops.Bah($"指定的人员 ID={input.Person.Id.Value} 不存在");
}
}
// ---------- 2. 确定主 personId(如果是临时创建的员工, input.Person.Id 已在上一步设置) ----------
long personId;
if (input.Person.Id.HasValue && input.Person.Id.Value > 0)
{
personId = input.Person.Id.Value;
// 已存在的人员(或上一步创建的员工),我们仍然可以继续使用 Storageable 更新信息
}
else
{
// 没有 Id,需新建主人员(可能是家属或员工)
if (string.IsNullOrWhiteSpace(input.Person.Name))
throw Oops.Bah("新增人员信息不完整,姓名为必填项");
personId = YitIdHelper.NextId();
}
// ---------- 3. 保存/更新主人员信息(使用 Storageable,兼容插入或更新) ----------
var mappedEmployeeId = input.Person.EmployeeId;
// 如果之前创建了员工(createdEmployeeId),优先使用该 id 作为家属的 EmployeeId
if (createdEmployeeId.HasValue)
mappedEmployeeId = createdEmployeeId.Value;
// 如果 person.IsEmployee == true,则 person.EmployeeId 应为 null(员工不需要关联员工)
if (input.Person.IsEmployee)
mappedEmployeeId = null;
var person = new Person
{
Id = personId,
Name = input.Person?.Name,
Birthday = input.Person?.Birthday,
Height = input.Person?.Height,
RelationShip = input.Person?.RelationShip,
EmployeeId = mappedEmployeeId
};
await Context.Storageable(person).ExecuteCommandAsync();
// ---------- 4. 处方保存/编辑逻辑(沿用原逻辑) ----------
Prescription prescription;
if (input.Id.HasValue) // 编辑
{
prescription = await Context.Queryable<Prescription>()
.FirstAsync(p => p.Id == input.Id.Value);
if (prescription == null)
throw Oops.Bah("处方不存在");
if (prescription.CreateTime.Value.Date != DateTime.Now.Date)
throw Oops.Bah("只能编辑当天的处方");
// 查询原有消耗记录并恢复库存
var oldHerbs = await Context.Queryable<PrescriptionHerb>()
.Where(ph => ph.PrescriptionId == input.Id.Value)
.ToListAsync();
if (oldHerbs.Count > 0)
{
foreach (var g in oldHerbs.GroupBy(x => x.MedicinalId))
{
// 恢复库存(加回已消耗)
// 注意:使用 Updateable 批量操作替换为 SetColumns 表达式累加数值
await Context.Updateable<Medicinal>()
.SetColumns(m => new Medicinal { Number = m.Number + g.Sum(x => x.AmountG) })
.Where(m => m.Id == g.Key)
.ExecuteCommandAsync();
}
}
// 删除原有处方药材(物理删除或软删按你系统策略)
await Context.Deleteable<PrescriptionHerb>()
.Where(ph => ph.PrescriptionId == input.Id.Value)
.ExecuteCommandAsync();
// 更新处方
prescription.PersonId = personId;
prescription.Weight = input.Person.Weight;
prescription.DiagnosisResult = input.DiagnosisResult;
prescription.UserMedicinaAdvice = input.UserMedicinaAdvice;
prescription.VisitDate = input.VisitDate ?? DateTime.Now;
await Context.Updateable(prescription).ExecuteCommandAsync();
}
else // 新增
{
var prescripId = YitIdHelper.NextId();
prescription = new Prescription
{
Id = prescripId,
Weight = input.Person.Weight,
PersonId = personId,
DiagnosisResult = input.DiagnosisResult,
UserMedicinaAdvice = input.UserMedicinaAdvice,
VisitDate = input.VisitDate ?? DateTime.Now,
CreateTime = DateTime.Now // 新增时设置创建时间
};
await Context.Insertable(prescription).ExecuteCommandAsync();
input.Id = prescripId;
}
// ---------- 5. 校验库存 & 扣减 & 插入消耗记录 ----------
var herbIds = input.Herbs.Select(h => h.Id).ToList();
var medicinalList = await Context.Queryable<Medicinal>()
.Where(m => herbIds.Contains(m.Id))
.ToListAsync();
List<long> HerbsId = new List<long>();
foreach (var herb in input.Herbs)
{
var medicinal = medicinalList.FirstOrDefault(m => m.Id == herb.Id);
if (medicinal == null)
throw Oops.Bah($"药材[{herb.Id}]不存在");
if (herb.AmountG<=0)
throw Oops.Bah($"请填写正确的消耗量,药材[{medicinal.MedicinalName}]");
if (medicinal.Number < herb.AmountG)
throw Oops.Bah($"药材[{medicinal.MedicinalName}]库存不足,当前库存: {medicinal.Number}g,需要: {herb.AmountG}g");
if(HerbsId.Contains(herb.Id)) throw Oops.Bah($"请勿重复添加同一个药材【{medicinal.MedicinalName}】");
HerbsId.Add(herb.Id);
medicinal.Number -= herb.AmountG;
}
// 批量更新药材库存
await Context.Updateable(medicinalList).ExecuteCommandAsync();
// 批量插入处方药材
var prescriptionHerbs = input.Herbs.Select(h => new PrescriptionHerb
{
PrescriptionId = input.Id.Value,
MedicinalId = h.Id,
AmountG = h.AmountG,
Note = h.Note
}).ToList();
await Context.Insertable(prescriptionHerbs).ExecuteCommandAsync();
await Context.Ado.CommitTranAsync();
return input.Id.Value;
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 获取 人员下拉
/// </summary>
/// <param name="type"></param>
/// <param name="employeeId"></param>
/// <returns></returns>
public async Task<List<PersonDto>> GetPersonList(PersonType? type, long? employeeId)
{
var query = Context.Queryable<Person>();
// 筛选员工或家属
if (type.HasValue)
{
if (type == PersonType.Employee)
{
query = query.Where(pe => pe.EmployeeId == null);
}
else if (type == PersonType.Relative && employeeId.HasValue)
{
query = query.Where(pe => pe.EmployeeId == employeeId);
}
}
return await query.Select(pe => new PersonDto
{
Id = pe.Id,
Name = pe.Name,
Birthday = pe.Birthday,
Height = pe.Height,
RelationShip = pe.RelationShip
}).ToListAsync();
}
/// <summary>
/// 获取关系下拉
/// </summary>
/// <returns></returns>
public async Task<List<RelationTypeDto>> GetRelationTypes()
{
var sysmodel = await Context.Queryable<SysDict>().FirstAsync(a => a.DictValue == "Relation");
if (sysmodel != null)
{
return await Context.Queryable<SysDict>().Where(a => a.ParentId == sysmodel.Id).Select(pe => new RelationTypeDto
{
Id = int.Parse(pe.DictValue),
Name = pe.DictLabel,
}).ToListAsync();
}
else
{
return new List<RelationTypeDto>
{
new RelationTypeDto { Id = 1, Name = "配偶" },
new RelationTypeDto { Id = 2, Name = "子女" },
new RelationTypeDto { Id = 3, Name = "父母" },
new RelationTypeDto { Id = 4, Name = "兄弟姐妹" },
new RelationTypeDto { Id = 5, Name = "其他" }
};
}
}
}