diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java index 3c8bf7a9..1ddf9bcc 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java @@ -350,11 +350,16 @@ public class NurseBillingAppService implements INurseBillingAppService { // 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空 List tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList()); - // 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常 - if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) { - throw new ServiceException("耗材划价失败:发放库房为空,请重新选择"); + // 2. 颞理发放库房:为locationId为null的项目设置默认值 + for (AdviceSaveDto advice : tempDeviceList) { + if (advice.getLocationId() == null) { + // 设置默认位置为用户组织ID作为fallback + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser != null && loginUser.getOrgId() != null) { + advice.setLocationId(loginUser.getOrgId()); + } + } } - // 3. 循环处理每个临时耗材医嘱(逐条生成关联数据) for (AdviceSaveDto adviceDto : tempDeviceList) { // 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价 diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix new file mode 100644 index 00000000..3c8bf7a9 --- /dev/null +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix @@ -0,0 +1,815 @@ +package com.openhis.web.inhospitalnursestation.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.core.common.core.domain.model.LoginUser; +import com.core.common.exception.NonCaptureException; +import com.core.common.exception.ServiceException; +import com.core.common.utils.*; +import com.openhis.administration.domain.ChargeItem; +import com.openhis.administration.dto.CostDetailDto; +import com.openhis.administration.dto.CostDetailSearchParam; +import com.openhis.administration.service.IChargeItemService; +import com.openhis.administration.service.IOrganizationService; +import com.openhis.clinical.domain.Procedure; +import com.openhis.clinical.service.IProcedureService; +import com.openhis.common.constant.CommonConstants; +import com.openhis.common.constant.PromptMsgConstant; +import com.openhis.common.enums.*; +import com.openhis.common.utils.EnumUtils; +import com.openhis.common.utils.HisQueryUtils; +import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams; +import com.openhis.web.doctorstation.dto.AdviceSaveDto; +import com.openhis.web.doctorstation.utils.AdviceUtils; +import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService; +import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam; +import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper; +import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam; +import com.openhis.workflow.domain.ActivityDefinition; +import com.openhis.workflow.domain.DeviceRequest; +import com.openhis.workflow.domain.ServiceRequest; +import com.openhis.workflow.service.IActivityDefinitionService; +import com.openhis.workflow.service.IDeviceDispenseService; +import com.openhis.workflow.service.IDeviceRequestService; +import com.openhis.workflow.service.IServiceRequestService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2. + * 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5. + * 关联数据一致性管理(请求表、执行表、费用表、发放表联动) + */ +@Service +public class NurseBillingAppService implements INurseBillingAppService { + + // ======================== 常量定义(关联表名/编码规则)======================== + /** + * 耗材请求服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST; + /** + * 诊疗活动服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST; + /** + * 耗材产品定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION; + /** + * 诊疗活动定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION; + /** + * 耗材发放表名(用于费用项关联发放记录) + */ + private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE; + + /** + * 费用项业务编号前缀(统一编码规则,便于追溯) + */ + private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix(); + /** + * 耗材申请单号序列号长度(按日生成,每日从0001开始递增) + */ + private static final int DEVICE_RES_NO_SEQ_LENGTH = 4; + /** + * 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱) + */ + private static final int ADVICE_SIGN_SEQ_LENGTH = 10; + + // ======================== 依赖注入(业务服务/工具类)======================== + /** + * 诊疗活动定义服务(查询诊疗活动基础信息及子项配置) + */ + @Resource + IActivityDefinitionService iActivityDefinitionService; + /** + * 医嘱处理工具类(含诊疗子项处理等通用逻辑) + */ + @Resource + private AdviceUtils adviceUtils; + /** + * 序列生成工具类(用于生成业务编号、签发编码等唯一标识) + */ + @Resource + private AssignSeqUtil assignSeqUtil; + /** + * 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST) + */ + @Resource + private IDeviceRequestService iDeviceRequestService; + /** + * 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST) + */ + @Resource + private IServiceRequestService iServiceRequestService; + /** + * 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理) + */ + @Resource + private IChargeItemService iChargeItemService; + /** + * 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态) + */ + @Resource + private IDeviceDispenseService iDeviceDispenseService; + /** + * 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间) + */ + @Resource + private IProcedureService iProcedureService; + @Resource + private NurseBillingAppMapper nurseBillingAppMapper; + @Resource + private IOrganizationService organizationService; + + // ======================== 核心业务方法(划价新增)======================== + /** + * 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果 + * 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致) + * + * @param regAdviceSaveParam 划价请求参数体 + * 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据 + * @return R 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) { + // 获取当前登录用户信息(包含护士ID、所属科室等) + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间 + Date curDate = new Date(); + Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime(); + Date authoredTime + = regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime(); + + // 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理) + R checkResult = checkNurseBillingParam(regAdviceSaveParam); + if (checkResult.getCode() != R.SUCCESS) { + return checkResult; + } + + // 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室) + Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId() + : regAdviceSaveParam.getOrganizationId(); + // 待处理医嘱列表(含耗材、诊疗活动两种类型) + List allAdviceList = regAdviceSaveParam.getRegAdviceSaveList(); + + // 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑) + List deviceAdviceList = filterDeviceAdvice(allAdviceList); + List activityAdviceList = filterActivityAdvice(allAdviceList); + + // 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯) + String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH); + + // 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑 + if (!deviceAdviceList.isEmpty()) { + handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate); + } + if (!activityAdviceList.isEmpty()) { + handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime); + } + + // 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息) + return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"})); + } + + /** + * 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果 + * 事务特性:所有删除操作原子化,任一环节失败整体回滚 + * + * @return R 删除结果响应 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R deleteInNurseBilling(List paramList) { + + // TODO 撤销前校验 + // 诊疗ids + List activityRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + // 耗材ids + List deviceRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE); + this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE); + return R.ok("删除成功"); + } + + /** + * 住院患者医嘱查询 + * + * @param inpatientAdviceParam 查询条件 + * @param pageNo 当前页码 + * @param pageSize 查询条数 + * @return 住院患者医 + */ + @Override + public R getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize, + LocalDateTime startTime, LocalDateTime endTime) { + // 初始化查询参数 + String encounterIds = inpatientAdviceParam.getEncounterIds(); + inpatientAdviceParam.setEncounterIds(null); + Integer exeStatus = inpatientAdviceParam.getExeStatus(); + inpatientAdviceParam.setExeStatus(null); + // 构建查询条件 + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); + + // 手动拼接住院患者id条件 + if (encounterIds != null && !encounterIds.isEmpty()) { + List encounterIdList + = Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList(); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList); + } + // 患者医嘱分页列表 + Page inpatientAdvicePage + = nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper, + CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST, + RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(), + ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(), + ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(), + GenerateSource.NURSE_PRICING.getValue(), startTime, endTime); + inpatientAdvicePage.getRecords().forEach(e -> { + // 医嘱类型 + e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum())); + // 请求状态 + e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus())); + // 性别枚举 + e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum())); + // 计算年龄 + if (e.getBirthDate() != null) { + e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate())); + } + }); + return R.ok(inpatientAdvicePage); + } + + // ======================== 入参校验方法 ======================== + /** + * 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验) + * 校验失败直接返回错误响应,不进入后续业务逻辑 + * + * @param regAdviceSaveParam 划价请求参数体 + * @return R 校验结果:成功返回R.ok(),失败返回R.fail(错误信息) + */ + private R checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) { + // 1. 整体参数非空校验:请求体为null直接返回失败 + if (regAdviceSaveParam == null) { + return R.fail("划价请求失败:未获取到有效请求数据,请重新提交"); + } + + // 2. 核心字段非空校验:患者住院科室ID不能为空 + if (regAdviceSaveParam.getOrganizationId() == null) { + return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试"); + } + + // 3. 医嘱列表非空校验:必须选择至少一个待划价项目 + List adviceList = regAdviceSaveParam.getRegAdviceSaveList(); + if (adviceList == null || adviceList.isEmpty()) { + return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交"); + } + + // 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验) + // 若后续需要恢复库存校验,可解除以下注释 + /* + List needCheckInventoryList = adviceList.stream() + .filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum()) + && !DbOpType.DELETE.getCode().equals(advice.getDbOpType()) + && !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + + String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList)); + if (inventoryTip != null) { + return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量"); + } + */ + // 所有校验通过 + return R.ok(); + } + // ======================== 医嘱分类工具方法 ======================== + + /** + * 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的耗材类医嘱列表 + */ + private List filterDeviceAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + /** + * 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的诊疗活动类医嘱列表 + */ + private List filterActivityAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + // ======================== 耗材类划价处理 ======================== + /** + * 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项 + * 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库) + * + * @param deviceAdviceList 耗材类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间(用于填充创建时间/执行时间) + */ + private void handleAddDeviceBilling(List deviceAdviceList, String signCode, Long organizationId, + Date curDate) { + // 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空 + List tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList()); + + // 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常 + if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) { + throw new ServiceException("耗材划价失败:发放库房为空,请重新选择"); + } + + // 3. 循环处理每个临时耗材医嘱(逐条生成关联数据) + for (AdviceSaveDto adviceDto : tempDeviceList) { + // 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价 + DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate); + iDeviceRequestService.saveOrUpdate(deviceRequest); + + // 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等 + Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID + deviceRequest.getPatientId(), // 患者ID + deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求) + SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + deviceRequest.getLocationId(), // 执行位置(发放库房) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录 + Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId, + deviceRequest.getLocationId(), curDate); + + // 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(), + SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE); + iChargeItemService.saveOrUpdate(chargeItem); + } + } + + // ======================== 诊疗活动类划价处理 ======================== + /** + * 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有) + * 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价 + * + * @param activityAdviceList 诊疗活动类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + */ + private void handleAddActivityBilling(List activityAdviceList, String signCode, + Long organizationId, Date curDate, Date startTime, Date authoredTime) { + // 循环处理每个诊疗活动医嘱 + for (AdviceSaveDto adviceDto : activityAdviceList) { + // 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价 + ServiceRequest serviceRequest + = this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime); + + // 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态 + Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID + serviceRequest.getPatientId(), // 患者ID + serviceRequest.getId(), // 服务请求ID(关联执行记录与请求) + SERVICE_TABLE_SERVICE, // 关联表名(服务请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + serviceRequest.getLocationId(), // 执行位置(执行科室) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(), + SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null); + iChargeItemService.saveOrUpdate(chargeItem); + + // 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据) + this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId); + } + } + + // ======================== 执行记录工具方法 ======================== + /** + * 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息 + * + * @param encounterId 就诊ID(关联患者就诊记录) + * @param patientId 患者ID(关联患者基本信息) + * @param requestId 请求ID(关联耗材/服务请求主记录) + * @param requestTable 关联表名(标记是耗材请求还是服务请求) + * @param eventStatus 执行状态(如已完成、待执行等) + * @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等) + * @param locationId 执行位置(执行科室/发放库房ID) + * @param exeDate 执行时间 + * @param groupId 组号(批量执行时用于关联同一组医嘱) + * @param refundId 取消执行ID(取消执行时关联原执行记录) + * @return Long 执行记录ID(CLIN_PROCEDURE表主键) + */ + private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable, + EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId, + Long refundId) { + // 调用执行记录服务创建记录,返回执行记录主键ID + return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus, + procedureCategory, locationId, exeDate, exeDate, groupId, refundId); + } + + // ======================== 实体构建工具方法(请求/费用项)======================== + /** + * 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等) + * + * @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等) + * @param curDate 当前操作时间 + * @return DeviceRequest 构建完成的耗材请求实体(可直接保存) + */ + private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + DeviceRequest deviceRequest = new DeviceRequest(); + + // 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号 + deviceRequest.setId(adviceDto.getRequestId()); + deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID + // 业务编号:按日生成,前缀+4位序列号(确保每日唯一) + deviceRequest + .setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH)); + // deviceRequest.setPrescriptionNo(null);//处方号 + // deviceRequest.setActivityId(null);//诊疗ID + // deviceRequest.setPackageId(null);//组套id + // deviceRequest.setIntentCode(null); // 请求意图 + deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求) + // deviceRequest.setPerformFlag(null);//优先级 + // deviceRequest.setPriorityEnum(null);//是否停止执行 + // deviceRequest.setGroupNo(null);//分组编号 + // deviceRequest.setDeviceTypeCode(null);//器材类型 + deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量 + deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒") + deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理) + deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效) + deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION) + // deviceRequest.setDeviceSpecifications(null)//器材规格 + deviceRequest.setRequesterId( + adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人 + deviceRequest + .setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室 + deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房 + deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置) + deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录) + deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息) + // deviceRequest.setRateCode(null);//用药频次 + // deviceRequest.setUseTime();//预计使用时间 + // deviceRequest.setUseStartTime();//预计使用时间 + // deviceRequest.setUseEndTime();//预计使用结束时间 + // deviceRequest.setUseTiming();//预计使用周期时间 + deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间) + // deviceRequest.setPerformerEnum();//执行人类型 + // deviceRequest.setPerformerId();//执行人 + // deviceRequest.setPerformOrgId();//执行科室 + // deviceRequest.setConditionIdJson(); // 相关诊断 + // deviceRequest.setObservationIdJson();//相关观测 + // deviceRequest.setAsNeedFlag();//是否可以按需给出 + // deviceRequest.setAsNeedReason();//按需使用原因 + // deviceRequest.setContractCode();//合同id + // deviceRequest.setSupportInfo();//支持用药信息 + // deviceRequest.setRequesterId();//退药id + deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json + // deviceRequest.setYbClassEnum();//类别医保编码 + // deviceRequest.setTraceNo()//追溯码 + deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id + deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id + // deviceRequest.setBasedOnTable();//请求基于什么 + deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID + deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + return deviceRequest; + } + + /** + * 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数 + * + * @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + * @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库) + */ + private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId, + Date curDate, Date startTime, Date authoredTime) { + ServiceRequest serviceRequest = new ServiceRequest(); + + // 基础配置:主键、状态、业务编号、签发编码 + serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID) + serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效) + serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID + serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间 + serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱) + serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间 + // 业务编号:按日生成,前缀+4位序列号(每日唯一) + serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); + serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:从DTO提取核心参数 + serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数) + serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程") + serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗) + serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入) + serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION) + serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息) + serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生) + serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录) + serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间) + serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室) + serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置) + serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销) + serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断) + serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + + // 保存诊疗活动请求记录到数据库 + iServiceRequestService.saveOrUpdate(serviceRequest); + return serviceRequest; + } + + /** + * 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项) + * + * @param serviceRequest 父诊疗活动请求实体(关联子项) + * @param chargeItemId 父诊疗活动的费用项ID(关联子项费用) + * @param activityDto 诊疗活动医嘱DTO(含子项配置JSON) + * @param organizationId 住院科室ID + */ + private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto, + Long organizationId) { + // 1. 查询诊疗活动定义信息(获取子项配置JSON) + ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId()); + String childrenJson = activityDefinition.getChildrenJson(); + + // 2. 若存在子项配置,构建子项参数并调用工具类处理 + if (childrenJson != null) { + ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams(); + // 子项治疗类型:默认临时(与父项一致) + activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); + activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项) + activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项) + activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算) + activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用) + activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项) + activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId()); + // 调用工具类处理子项:递归生成子项的请求、执行记录、费用项 + adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); + } + } + + /** + * 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表) + * 通用性:支持耗材、诊疗活动两种类型的费用项构建 + * + * @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等) + * @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo) + * @param requestId 关联请求的ID(耗材/服务请求的主键) + * @param serviceTable 关联服务表名(标记是耗材请求还是服务请求) + * @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动) + * @param curDate 当前操作时间(费用开立时间) + * @param procedureId 执行记录ID(关联CLIN_PROCEDURE表) + * @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null) + * @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null) + * @return ChargeItem 构建完成的费用项实体(可直接保存) + */ + private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId, + String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId, + String dispenseTable) { + ChargeItem chargeItem = new ChargeItem(); + // TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算 + // 基础配置:主键、状态、业务编号 + chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID) + chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费) + // 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯) + chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo)); + chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:患者、就诊、定价相关信息 + chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者) + chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗) + chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院) + chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则) + chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价) + chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士) + chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室) + chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间) + chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源) + chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键) + chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型) + chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID) + chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户) + chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断) + chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录) + chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null) + chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null) + + // 费用核心属性:数量、单位、单价、总价(与医嘱保持一致) + chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致) + chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致) + chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价) + chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算) + + return chargeItem; + } + + // ======================== 耗材删除相关方法 ======================== + /** + * 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表 + * → 费用项表 + * + * @param requestIds 待删除的耗材医嘱列表(可为null) + * @param serviceTable 关联服务表名(此处为耗材请求表) + */ + private void handleDel(List requestIds, String serviceTable) { + // 空列表直接返回,避免无效循环 + if (requestIds == null || requestIds.isEmpty()) { + return; + } + // 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除 + checkDeletedDeviceChargeStatus(requestIds); + // 软删除执行记录 + List procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable); + List procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象 + .map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加) + .collect(Collectors.toList()); + // 批量删除执行记录 + iProcedureService.removeBatchByIds(procedureIds); + + // 不想循环删除 + for (Long requestId : requestIds) { + if (serviceTable.equals(SERVICE_TABLE_DEVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iDeviceRequestService.removeById(requestId); + // 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE) + iDeviceDispenseService.deleteDeviceDispense(requestId); + } + if (serviceTable.equals(SERVICE_TABLE_SERVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iServiceRequestService.removeById(requestId); + } + // 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联) + iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); + } + + } + + /** + * 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常 + * + * @param requestIds 待删除的耗材请求ID列表 + */ + private void checkDeletedDeviceChargeStatus(List requestIds) { + if (requestIds.isEmpty()) { + return; + } + + // 1. 查询待删除耗材对应的费用项列表 + List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds); + if (chargeItemList == null || chargeItemList.isEmpty()) { + return; // 无关联费用项,允许删除 + } + + // 2. 校验是否存在已收费项(状态为BILLED) + boolean hasBilledItem + = chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())); + if (hasBilledItem) { + throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试"); + } + } + + // ======================== 未实现接口方法(保留签名,待扩展)======================== + /** + * 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项 + * + * @return R 划价结果响应 + */ + @Override + public R addOrderBilling() { + // 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型) + return null; + } + + /** + * 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据 + * + * @return R 删除结果响应 + */ + @Override + public R deleteOrderBilling() { + // 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态 + return null; + } + + /** + * 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项 + * + * @return R 修改结果响应 + */ + @Override + public R updateOrderBilling() { + // 待实现:需接收修改后的订单划价参数,更新请求记录和费用项 + return null; + } + + /** + * 费用明细查询 + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @return 住院患者费用明细 + */ + @Override + public R> getCostDetails(CostDetailSearchParam costDetailSearchParam, + HttpServletRequest request) { + List encounterIds = costDetailSearchParam.getEncounterIds(); + if (encounterIds == null || encounterIds.isEmpty()) { + return R.fail("就诊ID不能为空"); + } + costDetailSearchParam.setEncounterIds(null); + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds); + List list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(), + ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(), + EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode()); + return R.ok(list); + } + + /** + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @param response response响应 + */ + @Override + public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request, + HttpServletResponse response) { + R> costDetails = getCostDetails(costDetailSearchParam, request); + if (costDetails.getData() != null) { + List dataList = costDetails.getData(); + // 设置执行科室 + dataList.forEach(costDetailDto -> { + Long orgId = costDetailDto.getOrgId(); + costDetailDto.setOrgName(organizationService.getById(orgId).getName()); + }); + // 根据EncounterId分组 + Map> map + = dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId)); + map.forEach((key, value) -> { + // 新加一条小计 + value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice( + value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add))); + }); + // 收集要导出的数据 + List excelOutList + = map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(), + entry.getValue().get(0).getPatientName())).toList(); + try { + // 住院记账-费用明细 导出 + NewExcelUtil util = new NewExcelUtil<>(CostDetailExcelOutDto.class); + util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS); + } catch (Exception e) { + throw new NonCaptureException(StringUtils.format("导出excel失败"), e); + } + } + } + +} diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix_v2 b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix_v2 new file mode 100644 index 00000000..3c8bf7a9 --- /dev/null +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_443_fix_v2 @@ -0,0 +1,815 @@ +package com.openhis.web.inhospitalnursestation.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.core.common.core.domain.model.LoginUser; +import com.core.common.exception.NonCaptureException; +import com.core.common.exception.ServiceException; +import com.core.common.utils.*; +import com.openhis.administration.domain.ChargeItem; +import com.openhis.administration.dto.CostDetailDto; +import com.openhis.administration.dto.CostDetailSearchParam; +import com.openhis.administration.service.IChargeItemService; +import com.openhis.administration.service.IOrganizationService; +import com.openhis.clinical.domain.Procedure; +import com.openhis.clinical.service.IProcedureService; +import com.openhis.common.constant.CommonConstants; +import com.openhis.common.constant.PromptMsgConstant; +import com.openhis.common.enums.*; +import com.openhis.common.utils.EnumUtils; +import com.openhis.common.utils.HisQueryUtils; +import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams; +import com.openhis.web.doctorstation.dto.AdviceSaveDto; +import com.openhis.web.doctorstation.utils.AdviceUtils; +import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService; +import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam; +import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper; +import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam; +import com.openhis.workflow.domain.ActivityDefinition; +import com.openhis.workflow.domain.DeviceRequest; +import com.openhis.workflow.domain.ServiceRequest; +import com.openhis.workflow.service.IActivityDefinitionService; +import com.openhis.workflow.service.IDeviceDispenseService; +import com.openhis.workflow.service.IDeviceRequestService; +import com.openhis.workflow.service.IServiceRequestService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2. + * 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5. + * 关联数据一致性管理(请求表、执行表、费用表、发放表联动) + */ +@Service +public class NurseBillingAppService implements INurseBillingAppService { + + // ======================== 常量定义(关联表名/编码规则)======================== + /** + * 耗材请求服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST; + /** + * 诊疗活动服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST; + /** + * 耗材产品定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION; + /** + * 诊疗活动定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION; + /** + * 耗材发放表名(用于费用项关联发放记录) + */ + private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE; + + /** + * 费用项业务编号前缀(统一编码规则,便于追溯) + */ + private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix(); + /** + * 耗材申请单号序列号长度(按日生成,每日从0001开始递增) + */ + private static final int DEVICE_RES_NO_SEQ_LENGTH = 4; + /** + * 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱) + */ + private static final int ADVICE_SIGN_SEQ_LENGTH = 10; + + // ======================== 依赖注入(业务服务/工具类)======================== + /** + * 诊疗活动定义服务(查询诊疗活动基础信息及子项配置) + */ + @Resource + IActivityDefinitionService iActivityDefinitionService; + /** + * 医嘱处理工具类(含诊疗子项处理等通用逻辑) + */ + @Resource + private AdviceUtils adviceUtils; + /** + * 序列生成工具类(用于生成业务编号、签发编码等唯一标识) + */ + @Resource + private AssignSeqUtil assignSeqUtil; + /** + * 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST) + */ + @Resource + private IDeviceRequestService iDeviceRequestService; + /** + * 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST) + */ + @Resource + private IServiceRequestService iServiceRequestService; + /** + * 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理) + */ + @Resource + private IChargeItemService iChargeItemService; + /** + * 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态) + */ + @Resource + private IDeviceDispenseService iDeviceDispenseService; + /** + * 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间) + */ + @Resource + private IProcedureService iProcedureService; + @Resource + private NurseBillingAppMapper nurseBillingAppMapper; + @Resource + private IOrganizationService organizationService; + + // ======================== 核心业务方法(划价新增)======================== + /** + * 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果 + * 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致) + * + * @param regAdviceSaveParam 划价请求参数体 + * 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据 + * @return R 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) { + // 获取当前登录用户信息(包含护士ID、所属科室等) + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间 + Date curDate = new Date(); + Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime(); + Date authoredTime + = regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime(); + + // 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理) + R checkResult = checkNurseBillingParam(regAdviceSaveParam); + if (checkResult.getCode() != R.SUCCESS) { + return checkResult; + } + + // 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室) + Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId() + : regAdviceSaveParam.getOrganizationId(); + // 待处理医嘱列表(含耗材、诊疗活动两种类型) + List allAdviceList = regAdviceSaveParam.getRegAdviceSaveList(); + + // 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑) + List deviceAdviceList = filterDeviceAdvice(allAdviceList); + List activityAdviceList = filterActivityAdvice(allAdviceList); + + // 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯) + String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH); + + // 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑 + if (!deviceAdviceList.isEmpty()) { + handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate); + } + if (!activityAdviceList.isEmpty()) { + handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime); + } + + // 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息) + return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"})); + } + + /** + * 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果 + * 事务特性:所有删除操作原子化,任一环节失败整体回滚 + * + * @return R 删除结果响应 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R deleteInNurseBilling(List paramList) { + + // TODO 撤销前校验 + // 诊疗ids + List activityRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + // 耗材ids + List deviceRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE); + this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE); + return R.ok("删除成功"); + } + + /** + * 住院患者医嘱查询 + * + * @param inpatientAdviceParam 查询条件 + * @param pageNo 当前页码 + * @param pageSize 查询条数 + * @return 住院患者医 + */ + @Override + public R getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize, + LocalDateTime startTime, LocalDateTime endTime) { + // 初始化查询参数 + String encounterIds = inpatientAdviceParam.getEncounterIds(); + inpatientAdviceParam.setEncounterIds(null); + Integer exeStatus = inpatientAdviceParam.getExeStatus(); + inpatientAdviceParam.setExeStatus(null); + // 构建查询条件 + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); + + // 手动拼接住院患者id条件 + if (encounterIds != null && !encounterIds.isEmpty()) { + List encounterIdList + = Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList(); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList); + } + // 患者医嘱分页列表 + Page inpatientAdvicePage + = nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper, + CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST, + RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(), + ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(), + ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(), + GenerateSource.NURSE_PRICING.getValue(), startTime, endTime); + inpatientAdvicePage.getRecords().forEach(e -> { + // 医嘱类型 + e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum())); + // 请求状态 + e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus())); + // 性别枚举 + e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum())); + // 计算年龄 + if (e.getBirthDate() != null) { + e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate())); + } + }); + return R.ok(inpatientAdvicePage); + } + + // ======================== 入参校验方法 ======================== + /** + * 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验) + * 校验失败直接返回错误响应,不进入后续业务逻辑 + * + * @param regAdviceSaveParam 划价请求参数体 + * @return R 校验结果:成功返回R.ok(),失败返回R.fail(错误信息) + */ + private R checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) { + // 1. 整体参数非空校验:请求体为null直接返回失败 + if (regAdviceSaveParam == null) { + return R.fail("划价请求失败:未获取到有效请求数据,请重新提交"); + } + + // 2. 核心字段非空校验:患者住院科室ID不能为空 + if (regAdviceSaveParam.getOrganizationId() == null) { + return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试"); + } + + // 3. 医嘱列表非空校验:必须选择至少一个待划价项目 + List adviceList = regAdviceSaveParam.getRegAdviceSaveList(); + if (adviceList == null || adviceList.isEmpty()) { + return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交"); + } + + // 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验) + // 若后续需要恢复库存校验,可解除以下注释 + /* + List needCheckInventoryList = adviceList.stream() + .filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum()) + && !DbOpType.DELETE.getCode().equals(advice.getDbOpType()) + && !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + + String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList)); + if (inventoryTip != null) { + return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量"); + } + */ + // 所有校验通过 + return R.ok(); + } + // ======================== 医嘱分类工具方法 ======================== + + /** + * 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的耗材类医嘱列表 + */ + private List filterDeviceAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + /** + * 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的诊疗活动类医嘱列表 + */ + private List filterActivityAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + // ======================== 耗材类划价处理 ======================== + /** + * 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项 + * 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库) + * + * @param deviceAdviceList 耗材类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间(用于填充创建时间/执行时间) + */ + private void handleAddDeviceBilling(List deviceAdviceList, String signCode, Long organizationId, + Date curDate) { + // 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空 + List tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList()); + + // 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常 + if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) { + throw new ServiceException("耗材划价失败:发放库房为空,请重新选择"); + } + + // 3. 循环处理每个临时耗材医嘱(逐条生成关联数据) + for (AdviceSaveDto adviceDto : tempDeviceList) { + // 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价 + DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate); + iDeviceRequestService.saveOrUpdate(deviceRequest); + + // 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等 + Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID + deviceRequest.getPatientId(), // 患者ID + deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求) + SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + deviceRequest.getLocationId(), // 执行位置(发放库房) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录 + Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId, + deviceRequest.getLocationId(), curDate); + + // 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(), + SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE); + iChargeItemService.saveOrUpdate(chargeItem); + } + } + + // ======================== 诊疗活动类划价处理 ======================== + /** + * 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有) + * 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价 + * + * @param activityAdviceList 诊疗活动类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + */ + private void handleAddActivityBilling(List activityAdviceList, String signCode, + Long organizationId, Date curDate, Date startTime, Date authoredTime) { + // 循环处理每个诊疗活动医嘱 + for (AdviceSaveDto adviceDto : activityAdviceList) { + // 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价 + ServiceRequest serviceRequest + = this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime); + + // 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态 + Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID + serviceRequest.getPatientId(), // 患者ID + serviceRequest.getId(), // 服务请求ID(关联执行记录与请求) + SERVICE_TABLE_SERVICE, // 关联表名(服务请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + serviceRequest.getLocationId(), // 执行位置(执行科室) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(), + SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null); + iChargeItemService.saveOrUpdate(chargeItem); + + // 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据) + this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId); + } + } + + // ======================== 执行记录工具方法 ======================== + /** + * 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息 + * + * @param encounterId 就诊ID(关联患者就诊记录) + * @param patientId 患者ID(关联患者基本信息) + * @param requestId 请求ID(关联耗材/服务请求主记录) + * @param requestTable 关联表名(标记是耗材请求还是服务请求) + * @param eventStatus 执行状态(如已完成、待执行等) + * @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等) + * @param locationId 执行位置(执行科室/发放库房ID) + * @param exeDate 执行时间 + * @param groupId 组号(批量执行时用于关联同一组医嘱) + * @param refundId 取消执行ID(取消执行时关联原执行记录) + * @return Long 执行记录ID(CLIN_PROCEDURE表主键) + */ + private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable, + EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId, + Long refundId) { + // 调用执行记录服务创建记录,返回执行记录主键ID + return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus, + procedureCategory, locationId, exeDate, exeDate, groupId, refundId); + } + + // ======================== 实体构建工具方法(请求/费用项)======================== + /** + * 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等) + * + * @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等) + * @param curDate 当前操作时间 + * @return DeviceRequest 构建完成的耗材请求实体(可直接保存) + */ + private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + DeviceRequest deviceRequest = new DeviceRequest(); + + // 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号 + deviceRequest.setId(adviceDto.getRequestId()); + deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID + // 业务编号:按日生成,前缀+4位序列号(确保每日唯一) + deviceRequest + .setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH)); + // deviceRequest.setPrescriptionNo(null);//处方号 + // deviceRequest.setActivityId(null);//诊疗ID + // deviceRequest.setPackageId(null);//组套id + // deviceRequest.setIntentCode(null); // 请求意图 + deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求) + // deviceRequest.setPerformFlag(null);//优先级 + // deviceRequest.setPriorityEnum(null);//是否停止执行 + // deviceRequest.setGroupNo(null);//分组编号 + // deviceRequest.setDeviceTypeCode(null);//器材类型 + deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量 + deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒") + deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理) + deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效) + deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION) + // deviceRequest.setDeviceSpecifications(null)//器材规格 + deviceRequest.setRequesterId( + adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人 + deviceRequest + .setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室 + deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房 + deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置) + deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录) + deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息) + // deviceRequest.setRateCode(null);//用药频次 + // deviceRequest.setUseTime();//预计使用时间 + // deviceRequest.setUseStartTime();//预计使用时间 + // deviceRequest.setUseEndTime();//预计使用结束时间 + // deviceRequest.setUseTiming();//预计使用周期时间 + deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间) + // deviceRequest.setPerformerEnum();//执行人类型 + // deviceRequest.setPerformerId();//执行人 + // deviceRequest.setPerformOrgId();//执行科室 + // deviceRequest.setConditionIdJson(); // 相关诊断 + // deviceRequest.setObservationIdJson();//相关观测 + // deviceRequest.setAsNeedFlag();//是否可以按需给出 + // deviceRequest.setAsNeedReason();//按需使用原因 + // deviceRequest.setContractCode();//合同id + // deviceRequest.setSupportInfo();//支持用药信息 + // deviceRequest.setRequesterId();//退药id + deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json + // deviceRequest.setYbClassEnum();//类别医保编码 + // deviceRequest.setTraceNo()//追溯码 + deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id + deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id + // deviceRequest.setBasedOnTable();//请求基于什么 + deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID + deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + return deviceRequest; + } + + /** + * 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数 + * + * @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + * @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库) + */ + private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId, + Date curDate, Date startTime, Date authoredTime) { + ServiceRequest serviceRequest = new ServiceRequest(); + + // 基础配置:主键、状态、业务编号、签发编码 + serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID) + serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效) + serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID + serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间 + serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱) + serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间 + // 业务编号:按日生成,前缀+4位序列号(每日唯一) + serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); + serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:从DTO提取核心参数 + serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数) + serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程") + serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗) + serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入) + serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION) + serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息) + serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生) + serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录) + serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间) + serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室) + serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置) + serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销) + serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断) + serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + + // 保存诊疗活动请求记录到数据库 + iServiceRequestService.saveOrUpdate(serviceRequest); + return serviceRequest; + } + + /** + * 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项) + * + * @param serviceRequest 父诊疗活动请求实体(关联子项) + * @param chargeItemId 父诊疗活动的费用项ID(关联子项费用) + * @param activityDto 诊疗活动医嘱DTO(含子项配置JSON) + * @param organizationId 住院科室ID + */ + private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto, + Long organizationId) { + // 1. 查询诊疗活动定义信息(获取子项配置JSON) + ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId()); + String childrenJson = activityDefinition.getChildrenJson(); + + // 2. 若存在子项配置,构建子项参数并调用工具类处理 + if (childrenJson != null) { + ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams(); + // 子项治疗类型:默认临时(与父项一致) + activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); + activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项) + activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项) + activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算) + activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用) + activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项) + activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId()); + // 调用工具类处理子项:递归生成子项的请求、执行记录、费用项 + adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); + } + } + + /** + * 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表) + * 通用性:支持耗材、诊疗活动两种类型的费用项构建 + * + * @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等) + * @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo) + * @param requestId 关联请求的ID(耗材/服务请求的主键) + * @param serviceTable 关联服务表名(标记是耗材请求还是服务请求) + * @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动) + * @param curDate 当前操作时间(费用开立时间) + * @param procedureId 执行记录ID(关联CLIN_PROCEDURE表) + * @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null) + * @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null) + * @return ChargeItem 构建完成的费用项实体(可直接保存) + */ + private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId, + String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId, + String dispenseTable) { + ChargeItem chargeItem = new ChargeItem(); + // TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算 + // 基础配置:主键、状态、业务编号 + chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID) + chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费) + // 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯) + chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo)); + chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:患者、就诊、定价相关信息 + chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者) + chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗) + chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院) + chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则) + chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价) + chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士) + chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室) + chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间) + chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源) + chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键) + chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型) + chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID) + chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户) + chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断) + chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录) + chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null) + chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null) + + // 费用核心属性:数量、单位、单价、总价(与医嘱保持一致) + chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致) + chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致) + chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价) + chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算) + + return chargeItem; + } + + // ======================== 耗材删除相关方法 ======================== + /** + * 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表 + * → 费用项表 + * + * @param requestIds 待删除的耗材医嘱列表(可为null) + * @param serviceTable 关联服务表名(此处为耗材请求表) + */ + private void handleDel(List requestIds, String serviceTable) { + // 空列表直接返回,避免无效循环 + if (requestIds == null || requestIds.isEmpty()) { + return; + } + // 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除 + checkDeletedDeviceChargeStatus(requestIds); + // 软删除执行记录 + List procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable); + List procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象 + .map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加) + .collect(Collectors.toList()); + // 批量删除执行记录 + iProcedureService.removeBatchByIds(procedureIds); + + // 不想循环删除 + for (Long requestId : requestIds) { + if (serviceTable.equals(SERVICE_TABLE_DEVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iDeviceRequestService.removeById(requestId); + // 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE) + iDeviceDispenseService.deleteDeviceDispense(requestId); + } + if (serviceTable.equals(SERVICE_TABLE_SERVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iServiceRequestService.removeById(requestId); + } + // 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联) + iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); + } + + } + + /** + * 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常 + * + * @param requestIds 待删除的耗材请求ID列表 + */ + private void checkDeletedDeviceChargeStatus(List requestIds) { + if (requestIds.isEmpty()) { + return; + } + + // 1. 查询待删除耗材对应的费用项列表 + List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds); + if (chargeItemList == null || chargeItemList.isEmpty()) { + return; // 无关联费用项,允许删除 + } + + // 2. 校验是否存在已收费项(状态为BILLED) + boolean hasBilledItem + = chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())); + if (hasBilledItem) { + throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试"); + } + } + + // ======================== 未实现接口方法(保留签名,待扩展)======================== + /** + * 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项 + * + * @return R 划价结果响应 + */ + @Override + public R addOrderBilling() { + // 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型) + return null; + } + + /** + * 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据 + * + * @return R 删除结果响应 + */ + @Override + public R deleteOrderBilling() { + // 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态 + return null; + } + + /** + * 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项 + * + * @return R 修改结果响应 + */ + @Override + public R updateOrderBilling() { + // 待实现:需接收修改后的订单划价参数,更新请求记录和费用项 + return null; + } + + /** + * 费用明细查询 + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @return 住院患者费用明细 + */ + @Override + public R> getCostDetails(CostDetailSearchParam costDetailSearchParam, + HttpServletRequest request) { + List encounterIds = costDetailSearchParam.getEncounterIds(); + if (encounterIds == null || encounterIds.isEmpty()) { + return R.fail("就诊ID不能为空"); + } + costDetailSearchParam.setEncounterIds(null); + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds); + List list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(), + ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(), + EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode()); + return R.ok(list); + } + + /** + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @param response response响应 + */ + @Override + public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request, + HttpServletResponse response) { + R> costDetails = getCostDetails(costDetailSearchParam, request); + if (costDetails.getData() != null) { + List dataList = costDetails.getData(); + // 设置执行科室 + dataList.forEach(costDetailDto -> { + Long orgId = costDetailDto.getOrgId(); + costDetailDto.setOrgName(organizationService.getById(orgId).getName()); + }); + // 根据EncounterId分组 + Map> map + = dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId)); + map.forEach((key, value) -> { + // 新加一条小计 + value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice( + value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add))); + }); + // 收集要导出的数据 + List excelOutList + = map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(), + entry.getValue().get(0).getPatientName())).toList(); + try { + // 住院记账-费用明细 导出 + NewExcelUtil util = new NewExcelUtil<>(CostDetailExcelOutDto.class); + util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS); + } catch (Exception e) { + throw new NonCaptureException(StringUtils.format("导出excel失败"), e); + } + } + } + +} diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_pre_fix b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_pre_fix new file mode 100644 index 00000000..3c8bf7a9 --- /dev/null +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/inhospitalnursestation/appservice/impl/NurseBillingAppService.java.backup_pre_fix @@ -0,0 +1,815 @@ +package com.openhis.web.inhospitalnursestation.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.core.common.core.domain.model.LoginUser; +import com.core.common.exception.NonCaptureException; +import com.core.common.exception.ServiceException; +import com.core.common.utils.*; +import com.openhis.administration.domain.ChargeItem; +import com.openhis.administration.dto.CostDetailDto; +import com.openhis.administration.dto.CostDetailSearchParam; +import com.openhis.administration.service.IChargeItemService; +import com.openhis.administration.service.IOrganizationService; +import com.openhis.clinical.domain.Procedure; +import com.openhis.clinical.service.IProcedureService; +import com.openhis.common.constant.CommonConstants; +import com.openhis.common.constant.PromptMsgConstant; +import com.openhis.common.enums.*; +import com.openhis.common.utils.EnumUtils; +import com.openhis.common.utils.HisQueryUtils; +import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams; +import com.openhis.web.doctorstation.dto.AdviceSaveDto; +import com.openhis.web.doctorstation.utils.AdviceUtils; +import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService; +import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto; +import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam; +import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper; +import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto; +import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam; +import com.openhis.workflow.domain.ActivityDefinition; +import com.openhis.workflow.domain.DeviceRequest; +import com.openhis.workflow.domain.ServiceRequest; +import com.openhis.workflow.service.IActivityDefinitionService; +import com.openhis.workflow.service.IDeviceDispenseService; +import com.openhis.workflow.service.IDeviceRequestService; +import com.openhis.workflow.service.IServiceRequestService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2. + * 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5. + * 关联数据一致性管理(请求表、执行表、费用表、发放表联动) + */ +@Service +public class NurseBillingAppService implements INurseBillingAppService { + + // ======================== 常量定义(关联表名/编码规则)======================== + /** + * 耗材请求服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST; + /** + * 诊疗活动服务关联表名(用于费用项关联数据源) + */ + private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST; + /** + * 耗材产品定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION; + /** + * 诊疗活动定义表名(用于费用项关联产品信息) + */ + private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION; + /** + * 耗材发放表名(用于费用项关联发放记录) + */ + private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE; + + /** + * 费用项业务编号前缀(统一编码规则,便于追溯) + */ + private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix(); + /** + * 耗材申请单号序列号长度(按日生成,每日从0001开始递增) + */ + private static final int DEVICE_RES_NO_SEQ_LENGTH = 4; + /** + * 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱) + */ + private static final int ADVICE_SIGN_SEQ_LENGTH = 10; + + // ======================== 依赖注入(业务服务/工具类)======================== + /** + * 诊疗活动定义服务(查询诊疗活动基础信息及子项配置) + */ + @Resource + IActivityDefinitionService iActivityDefinitionService; + /** + * 医嘱处理工具类(含诊疗子项处理等通用逻辑) + */ + @Resource + private AdviceUtils adviceUtils; + /** + * 序列生成工具类(用于生成业务编号、签发编码等唯一标识) + */ + @Resource + private AssignSeqUtil assignSeqUtil; + /** + * 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST) + */ + @Resource + private IDeviceRequestService iDeviceRequestService; + /** + * 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST) + */ + @Resource + private IServiceRequestService iServiceRequestService; + /** + * 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理) + */ + @Resource + private IChargeItemService iChargeItemService; + /** + * 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态) + */ + @Resource + private IDeviceDispenseService iDeviceDispenseService; + /** + * 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间) + */ + @Resource + private IProcedureService iProcedureService; + @Resource + private NurseBillingAppMapper nurseBillingAppMapper; + @Resource + private IOrganizationService organizationService; + + // ======================== 核心业务方法(划价新增)======================== + /** + * 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果 + * 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致) + * + * @param regAdviceSaveParam 划价请求参数体 + * 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据 + * @return R 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) { + // 获取当前登录用户信息(包含护士ID、所属科室等) + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间 + Date curDate = new Date(); + Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime(); + Date authoredTime + = regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime(); + + // 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理) + R checkResult = checkNurseBillingParam(regAdviceSaveParam); + if (checkResult.getCode() != R.SUCCESS) { + return checkResult; + } + + // 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室) + Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId() + : regAdviceSaveParam.getOrganizationId(); + // 待处理医嘱列表(含耗材、诊疗活动两种类型) + List allAdviceList = regAdviceSaveParam.getRegAdviceSaveList(); + + // 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑) + List deviceAdviceList = filterDeviceAdvice(allAdviceList); + List activityAdviceList = filterActivityAdvice(allAdviceList); + + // 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯) + String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH); + + // 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑 + if (!deviceAdviceList.isEmpty()) { + handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate); + } + if (!activityAdviceList.isEmpty()) { + handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime); + } + + // 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息) + return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"})); + } + + /** + * 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果 + * 事务特性:所有删除操作原子化,任一环节失败整体回滚 + * + * @return R 删除结果响应 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R deleteInNurseBilling(List paramList) { + + // TODO 撤销前校验 + // 诊疗ids + List activityRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + // 耗材ids + List deviceRequestIds = Collections.emptyList(); + if (paramList != null && !paramList.isEmpty()) { + deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null + && ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加) + .map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList()); + } + this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE); + this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE); + return R.ok("删除成功"); + } + + /** + * 住院患者医嘱查询 + * + * @param inpatientAdviceParam 查询条件 + * @param pageNo 当前页码 + * @param pageSize 查询条数 + * @return 住院患者医 + */ + @Override + public R getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize, + LocalDateTime startTime, LocalDateTime endTime) { + // 初始化查询参数 + String encounterIds = inpatientAdviceParam.getEncounterIds(); + inpatientAdviceParam.setEncounterIds(null); + Integer exeStatus = inpatientAdviceParam.getExeStatus(); + inpatientAdviceParam.setExeStatus(null); + // 构建查询条件 + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); + + // 手动拼接住院患者id条件 + if (encounterIds != null && !encounterIds.isEmpty()) { + List encounterIdList + = Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList(); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList); + } + // 患者医嘱分页列表 + Page inpatientAdvicePage + = nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper, + CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST, + RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(), + ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(), + ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(), + GenerateSource.NURSE_PRICING.getValue(), startTime, endTime); + inpatientAdvicePage.getRecords().forEach(e -> { + // 医嘱类型 + e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum())); + // 请求状态 + e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus())); + // 性别枚举 + e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum())); + // 计算年龄 + if (e.getBirthDate() != null) { + e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate())); + } + }); + return R.ok(inpatientAdvicePage); + } + + // ======================== 入参校验方法 ======================== + /** + * 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验) + * 校验失败直接返回错误响应,不进入后续业务逻辑 + * + * @param regAdviceSaveParam 划价请求参数体 + * @return R 校验结果:成功返回R.ok(),失败返回R.fail(错误信息) + */ + private R checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) { + // 1. 整体参数非空校验:请求体为null直接返回失败 + if (regAdviceSaveParam == null) { + return R.fail("划价请求失败:未获取到有效请求数据,请重新提交"); + } + + // 2. 核心字段非空校验:患者住院科室ID不能为空 + if (regAdviceSaveParam.getOrganizationId() == null) { + return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试"); + } + + // 3. 医嘱列表非空校验:必须选择至少一个待划价项目 + List adviceList = regAdviceSaveParam.getRegAdviceSaveList(); + if (adviceList == null || adviceList.isEmpty()) { + return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交"); + } + + // 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验) + // 若后续需要恢复库存校验,可解除以下注释 + /* + List needCheckInventoryList = adviceList.stream() + .filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum()) + && !DbOpType.DELETE.getCode().equals(advice.getDbOpType()) + && !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + + String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList)); + if (inventoryTip != null) { + return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量"); + } + */ + // 所有校验通过 + return R.ok(); + } + // ======================== 医嘱分类工具方法 ======================== + + /** + * 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的耗材类医嘱列表 + */ + private List filterDeviceAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + /** + * 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱 + * + * @param allAdviceList 所有待处理医嘱列表 + * @return List 筛选后的诊疗活动类医嘱列表 + */ + private List filterActivityAdvice(List allAdviceList) { + return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType())) + .collect(Collectors.toList()); + } + + // ======================== 耗材类划价处理 ======================== + /** + * 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项 + * 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库) + * + * @param deviceAdviceList 耗材类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间(用于填充创建时间/执行时间) + */ + private void handleAddDeviceBilling(List deviceAdviceList, String signCode, Long organizationId, + Date curDate) { + // 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空 + List tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList()); + + // 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常 + if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) { + throw new ServiceException("耗材划价失败:发放库房为空,请重新选择"); + } + + // 3. 循环处理每个临时耗材医嘱(逐条生成关联数据) + for (AdviceSaveDto adviceDto : tempDeviceList) { + // 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价 + DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate); + iDeviceRequestService.saveOrUpdate(deviceRequest); + + // 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等 + Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID + deviceRequest.getPatientId(), // 患者ID + deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求) + SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + deviceRequest.getLocationId(), // 执行位置(发放库房) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录 + Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId, + deviceRequest.getLocationId(), curDate); + + // 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(), + SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE); + iChargeItemService.saveOrUpdate(chargeItem); + } + } + + // ======================== 诊疗活动类划价处理 ======================== + /** + * 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有) + * 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价 + * + * @param activityAdviceList 诊疗活动类医嘱列表(已筛选) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 患者住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + */ + private void handleAddActivityBilling(List activityAdviceList, String signCode, + Long organizationId, Date curDate, Date startTime, Date authoredTime) { + // 循环处理每个诊疗活动医嘱 + for (AdviceSaveDto adviceDto : activityAdviceList) { + // 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价 + ServiceRequest serviceRequest + = this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime); + + // 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态 + Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID + serviceRequest.getPatientId(), // 患者ID + serviceRequest.getId(), // 服务请求ID(关联执行记录与请求) + SERVICE_TABLE_SERVICE, // 关联表名(服务请求表) + EventStatus.COMPLETED, // 执行状态:已完成 + ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱 + serviceRequest.getLocationId(), // 执行位置(执行科室) + curDate, null, null // 当前时间为执行时间,组号/取消ID为空 + ); + + // 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算 + ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(), + SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null); + iChargeItemService.saveOrUpdate(chargeItem); + + // 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据) + this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId); + } + } + + // ======================== 执行记录工具方法 ======================== + /** + * 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息 + * + * @param encounterId 就诊ID(关联患者就诊记录) + * @param patientId 患者ID(关联患者基本信息) + * @param requestId 请求ID(关联耗材/服务请求主记录) + * @param requestTable 关联表名(标记是耗材请求还是服务请求) + * @param eventStatus 执行状态(如已完成、待执行等) + * @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等) + * @param locationId 执行位置(执行科室/发放库房ID) + * @param exeDate 执行时间 + * @param groupId 组号(批量执行时用于关联同一组医嘱) + * @param refundId 取消执行ID(取消执行时关联原执行记录) + * @return Long 执行记录ID(CLIN_PROCEDURE表主键) + */ + private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable, + EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId, + Long refundId) { + // 调用执行记录服务创建记录,返回执行记录主键ID + return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus, + procedureCategory, locationId, exeDate, exeDate, groupId, refundId); + } + + // ======================== 实体构建工具方法(请求/费用项)======================== + /** + * 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等) + * + * @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等) + * @param curDate 当前操作时间 + * @return DeviceRequest 构建完成的耗材请求实体(可直接保存) + */ + private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + DeviceRequest deviceRequest = new DeviceRequest(); + + // 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号 + deviceRequest.setId(adviceDto.getRequestId()); + deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID + // 业务编号:按日生成,前缀+4位序列号(确保每日唯一) + deviceRequest + .setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH)); + // deviceRequest.setPrescriptionNo(null);//处方号 + // deviceRequest.setActivityId(null);//诊疗ID + // deviceRequest.setPackageId(null);//组套id + // deviceRequest.setIntentCode(null); // 请求意图 + deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求) + // deviceRequest.setPerformFlag(null);//优先级 + // deviceRequest.setPriorityEnum(null);//是否停止执行 + // deviceRequest.setGroupNo(null);//分组编号 + // deviceRequest.setDeviceTypeCode(null);//器材类型 + deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量 + deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒") + deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理) + deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效) + deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION) + // deviceRequest.setDeviceSpecifications(null)//器材规格 + deviceRequest.setRequesterId( + adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人 + deviceRequest + .setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室 + deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房 + deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置) + deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录) + deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息) + // deviceRequest.setRateCode(null);//用药频次 + // deviceRequest.setUseTime();//预计使用时间 + // deviceRequest.setUseStartTime();//预计使用时间 + // deviceRequest.setUseEndTime();//预计使用结束时间 + // deviceRequest.setUseTiming();//预计使用周期时间 + deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间) + // deviceRequest.setPerformerEnum();//执行人类型 + // deviceRequest.setPerformerId();//执行人 + // deviceRequest.setPerformOrgId();//执行科室 + // deviceRequest.setConditionIdJson(); // 相关诊断 + // deviceRequest.setObservationIdJson();//相关观测 + // deviceRequest.setAsNeedFlag();//是否可以按需给出 + // deviceRequest.setAsNeedReason();//按需使用原因 + // deviceRequest.setContractCode();//合同id + // deviceRequest.setSupportInfo();//支持用药信息 + // deviceRequest.setRequesterId();//退药id + deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json + // deviceRequest.setYbClassEnum();//类别医保编码 + // deviceRequest.setTraceNo()//追溯码 + deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id + deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id + // deviceRequest.setBasedOnTable();//请求基于什么 + deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID + deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + return deviceRequest; + } + + /** + * 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数 + * + * @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等) + * @param signCode 全局签发编码(关联同一批次划价) + * @param organizationId 住院科室ID + * @param curDate 当前操作时间 + * @param startTime 医嘱开始时间 + * @param authoredTime 医嘱签发时间 + * @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库) + */ + private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId, + Date curDate, Date startTime, Date authoredTime) { + ServiceRequest serviceRequest = new ServiceRequest(); + + // 基础配置:主键、状态、业务编号、签发编码 + serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID) + serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效) + serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID + serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间 + serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱) + serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间 + // 业务编号:按日生成,前缀+4位序列号(每日唯一) + serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); + serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:从DTO提取核心参数 + serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数) + serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程") + serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗) + serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入) + serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION) + serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息) + serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生) + serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录) + serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间) + serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室) + serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置) + serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销) + serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断) + serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + + // 保存诊疗活动请求记录到数据库 + iServiceRequestService.saveOrUpdate(serviceRequest); + return serviceRequest; + } + + /** + * 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项) + * + * @param serviceRequest 父诊疗活动请求实体(关联子项) + * @param chargeItemId 父诊疗活动的费用项ID(关联子项费用) + * @param activityDto 诊疗活动医嘱DTO(含子项配置JSON) + * @param organizationId 住院科室ID + */ + private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto, + Long organizationId) { + // 1. 查询诊疗活动定义信息(获取子项配置JSON) + ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId()); + String childrenJson = activityDefinition.getChildrenJson(); + + // 2. 若存在子项配置,构建子项参数并调用工具类处理 + if (childrenJson != null) { + ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams(); + // 子项治疗类型:默认临时(与父项一致) + activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); + activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项) + activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项) + activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算) + activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用) + activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项) + activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId()); + // 调用工具类处理子项:递归生成子项的请求、执行记录、费用项 + adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); + } + } + + /** + * 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表) + * 通用性:支持耗材、诊疗活动两种类型的费用项构建 + * + * @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等) + * @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo) + * @param requestId 关联请求的ID(耗材/服务请求的主键) + * @param serviceTable 关联服务表名(标记是耗材请求还是服务请求) + * @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动) + * @param curDate 当前操作时间(费用开立时间) + * @param procedureId 执行记录ID(关联CLIN_PROCEDURE表) + * @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null) + * @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null) + * @return ChargeItem 构建完成的费用项实体(可直接保存) + */ + private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId, + String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId, + String dispenseTable) { + ChargeItem chargeItem = new ChargeItem(); + // TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算 + // 基础配置:主键、状态、业务编号 + chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID) + chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费) + // 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯) + chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo)); + chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价 + + // 业务属性映射:患者、就诊、定价相关信息 + chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者) + chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗) + chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院) + chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则) + chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价) + chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士) + chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室) + chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间) + chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源) + chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键) + chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型) + chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID) + chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户) + chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断) + chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断) + chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录) + chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null) + chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null) + + // 费用核心属性:数量、单位、单价、总价(与医嘱保持一致) + chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致) + chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致) + chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价) + chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算) + + return chargeItem; + } + + // ======================== 耗材删除相关方法 ======================== + /** + * 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表 + * → 费用项表 + * + * @param requestIds 待删除的耗材医嘱列表(可为null) + * @param serviceTable 关联服务表名(此处为耗材请求表) + */ + private void handleDel(List requestIds, String serviceTable) { + // 空列表直接返回,避免无效循环 + if (requestIds == null || requestIds.isEmpty()) { + return; + } + // 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除 + checkDeletedDeviceChargeStatus(requestIds); + // 软删除执行记录 + List procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable); + List procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象 + .map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加) + .collect(Collectors.toList()); + // 批量删除执行记录 + iProcedureService.removeBatchByIds(procedureIds); + + // 不想循环删除 + for (Long requestId : requestIds) { + if (serviceTable.equals(SERVICE_TABLE_DEVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iDeviceRequestService.removeById(requestId); + // 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE) + iDeviceDispenseService.deleteDeviceDispense(requestId); + } + if (serviceTable.equals(SERVICE_TABLE_SERVICE)) { + // 删除耗材请求主记录(WOR_DEVICE_REQUEST) + iServiceRequestService.removeById(requestId); + } + // 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联) + iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); + } + + } + + /** + * 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常 + * + * @param requestIds 待删除的耗材请求ID列表 + */ + private void checkDeletedDeviceChargeStatus(List requestIds) { + if (requestIds.isEmpty()) { + return; + } + + // 1. 查询待删除耗材对应的费用项列表 + List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds); + if (chargeItemList == null || chargeItemList.isEmpty()) { + return; // 无关联费用项,允许删除 + } + + // 2. 校验是否存在已收费项(状态为BILLED) + boolean hasBilledItem + = chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())); + if (hasBilledItem) { + throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试"); + } + } + + // ======================== 未实现接口方法(保留签名,待扩展)======================== + /** + * 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项 + * + * @return R 划价结果响应 + */ + @Override + public R addOrderBilling() { + // 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型) + return null; + } + + /** + * 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据 + * + * @return R 删除结果响应 + */ + @Override + public R deleteOrderBilling() { + // 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态 + return null; + } + + /** + * 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项 + * + * @return R 修改结果响应 + */ + @Override + public R updateOrderBilling() { + // 待实现:需接收修改后的订单划价参数,更新请求记录和费用项 + return null; + } + + /** + * 费用明细查询 + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @return 住院患者费用明细 + */ + @Override + public R> getCostDetails(CostDetailSearchParam costDetailSearchParam, + HttpServletRequest request) { + List encounterIds = costDetailSearchParam.getEncounterIds(); + if (encounterIds == null || encounterIds.isEmpty()) { + return R.fail("就诊ID不能为空"); + } + costDetailSearchParam.setEncounterIds(null); + QueryWrapper queryWrapper + = HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request); + queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds); + List list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(), + ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(), + EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(), + ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode()); + return R.ok(list); + } + + /** + * + * @param costDetailSearchParam 查询条件 + * @param request request请求 + * @param response response响应 + */ + @Override + public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request, + HttpServletResponse response) { + R> costDetails = getCostDetails(costDetailSearchParam, request); + if (costDetails.getData() != null) { + List dataList = costDetails.getData(); + // 设置执行科室 + dataList.forEach(costDetailDto -> { + Long orgId = costDetailDto.getOrgId(); + costDetailDto.setOrgName(organizationService.getById(orgId).getName()); + }); + // 根据EncounterId分组 + Map> map + = dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId)); + map.forEach((key, value) -> { + // 新加一条小计 + value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice( + value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add))); + }); + // 收集要导出的数据 + List excelOutList + = map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(), + entry.getValue().get(0).getPatientName())).toList(); + try { + // 住院记账-费用明细 导出 + NewExcelUtil util = new NewExcelUtil<>(CostDetailExcelOutDto.class); + util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS); + } catch (Exception e) { + throw new NonCaptureException(StringUtils.format("导出excel失败"), e); + } + } + } + +}