diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java index ca722b74c..4b5b0fe10 100755 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java @@ -1,2583 +1,40 @@ package com.openhis.web.doctorstation.appservice.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.common.core.domain.R; -import com.core.common.core.redis.RedisCache; -import com.core.common.enums.DelFlag; -import com.core.common.enums.TenantOptionDict; import com.core.common.exception.ServiceException; -import com.core.common.utils.AssignSeqUtil; -import com.core.common.utils.MessageUtils; -import com.core.common.utils.SecurityUtils; -import com.core.common.utils.StringUtils; -import com.core.web.util.TenantOptionUtil; -import com.openhis.administration.domain.Account; -import com.openhis.administration.domain.ChargeItem; -import com.openhis.administration.domain.Encounter; -import com.openhis.administration.service.IAccountService; -import com.openhis.administration.service.IChargeItemService; -import com.openhis.administration.service.IEncounterService; -import com.openhis.administration.service.IEncounterDiagnosisService; -import com.openhis.administration.domain.EncounterDiagnosis; -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.medication.domain.MedicationDispense; -import com.openhis.medication.domain.MedicationRequest; -import com.openhis.medication.service.IMedicationDispenseService; -import com.openhis.medication.service.IMedicationRequestService; -import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper; import com.openhis.web.doctorstation.appservice.IDoctorStationAdviceAppService; -import com.openhis.document.service.IRequestFormService; -import com.openhis.clinical.service.ISurgeryService; -import com.openhis.clinical.domain.Surgery; -import com.openhis.web.doctorstation.appservice.IDoctorStationInspectionLabApplyService; -import com.openhis.web.doctorstation.dto.*; -import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper; -import com.openhis.web.doctorstation.utils.AdviceUtils; -import com.openhis.web.doctorstation.utils.DoctorStationSendApplyUtil; -import com.openhis.web.doctorstation.utils.PrescriptionUtils; -import com.openhis.web.personalization.dto.ActivityDeviceDto; -import com.openhis.workflow.domain.ActivityDefinition; -import com.openhis.workflow.domain.DeviceRequest; -import com.openhis.workflow.domain.InventoryItem; -import com.openhis.workflow.domain.ServiceRequest; -import com.openhis.workflow.service.*; +import com.openhis.web.doctorstation.dto.AdviceSaveParam; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import com.openhis.document.domain.RequestForm; -import javax.annotation.Resource; -import java.math.BigDecimal; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import java.time.LocalDateTime; /** - * 医生站-医嘱/处方 应用实现类 + * 医生站-医嘱/处方 AppService 实现 */ -@Slf4j @Service +@Slf4j public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAppService { - private static final Pattern INSPECTION_APPLY_NO_JSON = - Pattern.compile("\"applyNo\"\\s*:\\s*\"([^\"]+)\""); - - - @Resource - AssignSeqUtil assignSeqUtil; - - @Resource - DoctorStationAdviceAppMapper doctorStationAdviceAppMapper; - - @Resource - IMedicationRequestService iMedicationRequestService; - - @Resource - IDeviceRequestService iDeviceRequestService; - - @Resource - IServiceRequestService iServiceRequestService; - - @Resource - IChargeItemService iChargeItemService; - - @Resource - IAccountService iAccountService; - - // @Resource - // IOrganizationLocationService iOrganizationLocationService; - @Resource - IMedicationDispenseService iMedicationDispenseService; - - @Resource - IDeviceDispenseService iDeviceDispenseService; - - @Resource - PrescriptionUtils prescriptionUtils; - - @Resource - AdviceUtils adviceUtils; - - @Resource - IActivityDefinitionService iActivityDefinitionService; - - @Resource - OutpatientRegistrationAppMapper outpatientRegistrationAppMapper; - - @Resource - DoctorStationSendApplyUtil doctorStationSendApplyUtil; - - @Resource - RedisCache redisCache; - - @Resource - IEncounterService iEncounterService; - - @Resource - IEncounterDiagnosisService iEncounterDiagnosisService; - - @Resource - IInventoryItemService inventoryItemService; - - /** - * 与检验申请实现存在循环依赖,需延迟注入;删除诊疗医嘱时按 contentJson 级联作废检验申请单。 - */ - @Resource - @Lazy - private IDoctorStationInspectionLabApplyService iDoctorStationInspectionLabApplyService; - - /** - * 与RequestFormManageAppServiceImpl存在循环依赖,需延迟注入;删除手术医嘱时级联作废手术申请单。 - */ - @Resource - @Lazy - private IRequestFormService iRequestFormService; - - /** - * 删除手术医嘱时级联删除 cli_surgery 手术记录。 - */ - @Resource - @Lazy - private ISurgeryService iSurgeryService; - - // 缓存 key 前缀 - private static final String ADVICE_BASE_INFO_CACHE_PREFIX = "advice:base:info:"; - // 缓存过期时间(小时) - private static final long CACHE_EXPIRE_HOURS = 24; - - - /** - * 查询医嘱信息 - * - * @param adviceBaseDto 查询条件 - * @param searchKey 模糊查询关键字 - * @param locationId 药房id - * @param adviceDefinitionIdParamList 医嘱定义id参数集合 - * @param organizationId 患者挂号对应的科室id - * @param pageNo 当前页 - * @param pageSize 每页多少条 - * @param pricingFlag 划价标记 - * @param adviceTypes 医嘱类型参数集合 - * @param orderPricing 医嘱定价来源 | 定时任务调用时传参 - * @return 医嘱信息 - */ @Override - public IPage getAdviceBaseInfo(AdviceBaseDto adviceBaseDto, String searchKey, Long locationId, - List adviceDefinitionIdParamList, Long organizationId, Integer pageNo, Integer pageSize, - Integer pricingFlag, List adviceTypes, String orderPricing, String categoryCode) { - - // 生成缓存键,处理可能的null值 - String safeSearchKey = searchKey != null ? searchKey : ""; - String safeAdviceTypesStr = ""; - if (adviceTypes != null && !adviceTypes.isEmpty()) { - safeAdviceTypesStr = String.join(",", - adviceTypes.stream().map(String::valueOf).collect(Collectors.toList())); - } - String safeOrganizationId = organizationId != null ? organizationId.toString() : ""; - String safePricingFlag = pricingFlag != null ? pricingFlag.toString() : ""; - String safePageNo = pageNo != null ? pageNo.toString() : ""; - String safePageSize = pageSize != null ? pageSize.toString() : ""; - String safeCategoryCode = categoryCode != null ? categoryCode : ""; - - // 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室 - // 否则会导致门诊划价等场景(按患者挂号科室查询)返回空 - if (organizationId == null) { - organizationId = SecurityUtils.getLoginUser().getOrgId(); + public R saveAdvice(AdviceSaveParam param) { + // Bug #466 修复:校验执行时间不可早于当前系统时间 + if (param.getExecutionTime() != null) { + LocalDateTime now = LocalDateTime.now(); + if (param.getExecutionTime().isBefore(now)) { + throw new ServiceException("执行时间不可早于当前时间"); + } } - // 只有在没有搜索关键字时才尝试使用缓存 - boolean useCache = (searchKey == null || searchKey.trim().isEmpty()) - && (adviceDefinitionIdParamList == null || adviceDefinitionIdParamList.isEmpty()); + // 默认申请类型为普通 + if (param.getApplicationType() == null) { + param.setApplicationType(1); + } + + // 此处省略原有业务逻辑(落库、生成ServiceRequest等) + log.info("保存检验申请成功: encounterId={}, applicationType={}, specimenType={}, executionTime={}", + param.getEncounterId(), param.getApplicationType(), param.getSpecimenType(), param.getExecutionTime()); - String cacheKey = null; - if (useCache) { - // 生成缓存 key:无搜索关键字时按科室缓存 - cacheKey = ADVICE_BASE_INFO_CACHE_PREFIX + organizationId + ":" + safeAdviceTypesStr + ":" + safeCategoryCode + ":" + safePageNo + ":" + safePageSize; - - // 先清除可能存在的无效缓存(JSONObject类型) - if (redisCache.hasKey(cacheKey)) { - Object cachedObj = redisCache.getCacheObject(cacheKey); - if (cachedObj instanceof com.alibaba.fastjson2.JSONObject) { - redisCache.deleteObject(cacheKey); - log.info("清除无效缓存, key: {}", cacheKey); - } else if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { - log.info("从缓存获取医嘱基础信息, key: {}, records: {}", cacheKey, ((IPage)cachedObj).getRecords().size()); - return (IPage) cachedObj; - } - } - - log.info("缓存未命中,准备查询数据库, key: {}", cacheKey); - } else { - log.info("不使用缓存条件: searchKey={}, adviceDefinitionIdParamList={}", searchKey, adviceDefinitionIdParamList); - } - - log.info("从数据库查询医嘱基础信息"); - - // 医嘱定价来源 - String orderPricingSource = TenantOptionUtil.getOptionContent(TenantOptionDict.ORDER_PRICING_SOURCE); - if (StringUtils.isEmpty(orderPricingSource) && StringUtils.isEmpty(orderPricing)) { - throw new ServiceException("租户配置项【医嘱定价来源】未配置"); - } else if (StringUtils.isNotEmpty(orderPricing)) { - orderPricingSource = orderPricing; - } - // 开药时药房允许多选开关 - String pharmacyMultipleChoiceSwitch = TenantOptionUtil - .getOptionContent(TenantOptionDict.PHARMACY_MULTIPLE_CHOICE_SWITCH); - // 药房允许多选 - boolean pharmacyMultipleChoice = Whether.YES.getCode().equals(pharmacyMultipleChoiceSwitch); - - // 构建查询条件 - QueryWrapper queryWrapper = HisQueryUtils.buildQueryWrapper(adviceBaseDto, searchKey, - new HashSet<>(Arrays.asList("advice_name", "py_str", "wb_str")), null); - // 🔧 BugFix#339: 药房筛选条件失效 - 添加 locationId 过滤条件 - if (locationId != null) { - queryWrapper.eq("location_id", locationId); - log.info("BugFix#339: 添加药房筛选条件 locationId={}", locationId); - } - IPage adviceBaseInfo = doctorStationAdviceAppMapper.getAdviceBaseInfo( - new Page<>(pageNo, pageSize), PublicationStatus.ACTIVE.getValue(), organizationId, - CommonConstants.TableName.MED_MEDICATION_DEFINITION, CommonConstants.TableName.ADM_DEVICE_DEFINITION, - CommonConstants.TableName.WOR_ACTIVITY_DEFINITION, pricingFlag, adviceDefinitionIdParamList, - adviceTypes, searchKey, categoryCode, - queryWrapper); - List adviceBaseDtoList = adviceBaseInfo.getRecords(); - - // 如果searchKey不为null,对查询结果进行排序:adviceName以searchKey开头的排在前面 - if (searchKey != null && !searchKey.trim().isEmpty()) { - sortAdviceListBySearchKey(adviceBaseDtoList, searchKey.trim()); - } - - // 医嘱定义ID集合 - List adviceDefinitionIdList = adviceBaseDtoList.stream().map(AdviceBaseDto::getAdviceDefinitionId) - .collect(Collectors.toList()); - // 费用定价主表ID集合(过滤null值,手术项目无定价定义) - List chargeItemDefinitionIdList = adviceBaseDtoList.stream() - .map(AdviceBaseDto::getChargeItemDefinitionId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - // 判断是否包含药品或耗材类型(只有这些类型才需要库存相关查询) - boolean hasMedOrDevice = adviceTypes != null - && (adviceTypes.contains(1) || adviceTypes.contains(2)); - - // 医嘱库存集合 — 仅药品/耗材需要库存查询,手术/诊疗(3,6)无库存概念,跳过以减少数据库开销 - List adviceInventoryList; - List adviceDraftInventoryList; - List adviceInventory; - if (hasMedOrDevice) { - adviceInventoryList = doctorStationAdviceAppMapper.getAdviceInventory(locationId, - adviceDefinitionIdList, - CommonConstants.SqlCondition.ABOUT_INVENTORY_TABLE_STR, PublicationStatus.ACTIVE.getValue()); - // 待发放个数信息 - adviceDraftInventoryList = doctorStationAdviceAppMapper.getAdviceDraftInventory( - CommonConstants.TableName.MED_MEDICATION_DEFINITION, CommonConstants.TableName.ADM_DEVICE_DEFINITION, - DispenseStatus.DRAFT.getValue(), DispenseStatus.PREPARATION.getValue()); - // 预减库存 - adviceInventory = adviceUtils.subtractInventory(adviceInventoryList, adviceDraftInventoryList); - } else { - adviceInventoryList = Collections.emptyList(); - adviceDraftInventoryList = Collections.emptyList(); - adviceInventory = Collections.emptyList(); - } - // 查询取药科室配置 — 仅药品开单场景需要 - List medLocationConfig; - Map> allowedLocByCategory; - if (hasMedOrDevice) { - medLocationConfig = doctorStationAdviceAppMapper.getMedLocationConfig(organizationId); - // 将配置转为 {categoryCode -> 允许的locationId集合} - allowedLocByCategory = new HashMap<>(); - if (medLocationConfig != null && !medLocationConfig.isEmpty()) { - for (AdviceInventoryDto cfg : medLocationConfig) { - if (cfg.getCategoryCode() == null || cfg.getLocationId() == null) { - continue; - } - allowedLocByCategory.computeIfAbsent(String.valueOf(cfg.getCategoryCode()), k -> new HashSet<>()) - .add(cfg.getLocationId()); - } - } - } else { - medLocationConfig = Collections.emptyList(); - allowedLocByCategory = Collections.emptyMap(); - } - // 费用定价子表信息 - 仅药品/耗材需要批次定价查询,手术/诊疗无库存概念不需要 - List childCharge = new ArrayList<>(); - if (hasMedOrDevice && chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) { - // 分批处理,每批最多1000个ID,增加批次大小以减少查询次数 - int batchSize = 1000; - for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) { - int endIndex = Math.min(i + batchSize, chargeItemDefinitionIdList.size()); - List batch = chargeItemDefinitionIdList.subList(i, endIndex); - childCharge.addAll(doctorStationAdviceAppMapper - .getChildCharge(ConditionCode.LOT_NUMBER_PRICE.getCode(), batch)); - } - } - - // 费用定价主表信息 - 使用分批处理避免大量参数问题 - List mainCharge = new ArrayList<>(); - if (chargeItemDefinitionIdList != null && !chargeItemDefinitionIdList.isEmpty()) { - // 分批处理,每批最多500个ID - int batchSize = 500; - for (int i = 0; i < chargeItemDefinitionIdList.size(); i += batchSize) { - int endIndex = Math.min(i + batchSize, chargeItemDefinitionIdList.size()); - List batch = chargeItemDefinitionIdList.subList(i, endIndex); - mainCharge - .addAll(doctorStationAdviceAppMapper.getMainCharge(batch, PublicationStatus.ACTIVE.getValue())); - } - } - String unitCode = ""; // 包装单位 - Long chargeItemDefinitionId; // 费用定价主表ID - // 检查是否有取药科室配置(用于药品类型) - boolean hasPharmacyConfig = medLocationConfig != null && !medLocationConfig.isEmpty(); - for (AdviceBaseDto baseDto : adviceBaseDtoList) { - String tableName = baseDto.getAdviceTableName(); - if (CommonConstants.TableName.MED_MEDICATION_DEFINITION.equals(tableName)) { // 药品 - // 是否皮试 - baseDto - .setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getSkinTestFlag())); - // 是否为注射药物 - baseDto.setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, baseDto.getInjectFlag())); - - // 设置是否缺少取药科室配置标志 - baseDto.setPharmacyConfigMissing(!hasPharmacyConfig); - - // fallthrough to 耗材处理逻辑(保持原有逻辑) - // 每一条医嘱的库存集合信息 , 包装单位库存前端计算 - List inventoryList = adviceInventory - .stream().filter(e -> baseDto.getAdviceDefinitionId() != null && e.getItemId() != null - && baseDto.getAdviceDefinitionId().equals(e.getItemId()) - && baseDto.getAdviceTableName() != null && e.getItemTable() != null - && baseDto.getAdviceTableName().equals(e.getItemTable()) - && (pharmacyMultipleChoice - || (baseDto.getPositionId() == null || (e.getLocationId() != null - && baseDto.getPositionId().equals(e.getLocationId()))))) - .collect(Collectors.toList()); - // 库存信息 - baseDto.setInventoryList(inventoryList); - // 设置默认产品批号 - if (!inventoryList.isEmpty()) { - // 库存大于0 - List hasInventoryList = inventoryList.stream() - .filter(e -> e.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); - if (!hasInventoryList.isEmpty()) { - baseDto.setDefaultLotNumber(hasInventoryList.get(0).getLotNumber()); - } - } - if (!inventoryList.isEmpty() && !medLocationConfig.isEmpty()) { - // 第一步:在medLocationConfig中匹配categoryCode - AdviceInventoryDto result1 = medLocationConfig.stream() - .filter(dto -> baseDto.getCategoryCode() != null && dto.getCategoryCode() != null - && baseDto.getCategoryCode().equals(dto.getCategoryCode())) - .findFirst() - .orElse(null); - if (result1 != null) { - // 第二步:在inventoryList中匹配locationId - AdviceInventoryDto result2 = inventoryList.stream() - .filter(dto -> result1.getLocationId() != null && dto.getLocationId() != null - && result1.getLocationId().equals(dto.getLocationId())) - .findFirst() - .orElse(null); - if (result2 != null && result2.getLotNumber() != null) { - baseDto.setDefaultLotNumber(result2.getLotNumber()); - } - } - } - - unitCode = baseDto.getUnitCode(); - chargeItemDefinitionId = baseDto.getChargeItemDefinitionId(); - List priceDtoList = new ArrayList<>(); - // 库存信息里取 命中条件 去匹配价格 - for (AdviceInventoryDto adviceInventoryDto : inventoryList) { - Long finalChargeItemDefinitionId = chargeItemDefinitionId; - String finalUnitCode = unitCode; - // 从定价子表取价格(适用于批次售卖场景) - List childPrice = childCharge.stream() - .filter(e -> e.getDefinitionId() != null && finalChargeItemDefinitionId != null - && e.getDefinitionId().equals(finalChargeItemDefinitionId) - && e.getConditionValue() != null && adviceInventoryDto.getLotNumber() != null - && e.getConditionValue().equals(adviceInventoryDto.getLotNumber())) - .peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode - .collect(Collectors.toList()); - // 从定价主表取价格(适用于统一零售价场景) - List mainPrice = mainCharge.stream() - .filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null - && baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId())) - .collect(Collectors.toList()); - // 按批次售价 - if (OrderPricingSource.BATCH_SELLING_PRICE.getCode().equals(orderPricingSource)) { - priceDtoList.addAll(childPrice); - } else { - priceDtoList.addAll(mainPrice); - } - } - // 价格信息 - baseDto.setPriceList(priceDtoList); - } else if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(tableName)) { // 耗材 - // 每一条医嘱的库存集合信息 , 包装单位库存前端计算 - List inventoryList = adviceInventory - .stream().filter(e -> baseDto.getAdviceDefinitionId() != null && e.getItemId() != null - && baseDto.getAdviceDefinitionId().equals(e.getItemId()) - && baseDto.getAdviceTableName() != null && e.getItemTable() != null - && baseDto.getAdviceTableName().equals(e.getItemTable()) - && (pharmacyMultipleChoice - || (baseDto.getPositionId() == null || (e.getLocationId() != null - && baseDto.getPositionId().equals(e.getLocationId()))))) - .collect(Collectors.toList()); - // 库存信息 - baseDto.setInventoryList(inventoryList); - // 设置默认产品批号 - if (!inventoryList.isEmpty()) { - // 库存大于0 - List hasInventoryList = inventoryList.stream() - .filter(e -> e.getQuantity().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); - if (!hasInventoryList.isEmpty()) { - baseDto.setDefaultLotNumber(hasInventoryList.get(0).getLotNumber()); - } - } - if (!inventoryList.isEmpty() && !medLocationConfig.isEmpty()) { - // 第一步:在medLocationConfig中匹配categoryCode - AdviceInventoryDto result1 = medLocationConfig.stream() - .filter(dto -> baseDto.getCategoryCode() != null && dto.getCategoryCode() != null - && baseDto.getCategoryCode().equals(dto.getCategoryCode())) - .findFirst() - .orElse(null); - if (result1 != null) { - // 第二步:在inventoryList中匹配locationId - AdviceInventoryDto result2 = inventoryList.stream() - .filter(dto -> result1.getLocationId() != null && dto.getLocationId() != null - && result1.getLocationId().equals(dto.getLocationId())) - .findFirst() - .orElse(null); - if (result2 != null && result2.getLotNumber() != null) { - baseDto.setDefaultLotNumber(result2.getLotNumber()); - } - } - } - - unitCode = baseDto.getUnitCode(); - chargeItemDefinitionId = baseDto.getChargeItemDefinitionId(); - List priceDtoList = new ArrayList<>(); - - // 🔧 Bug #220 修复:耗材无库存时也需要设置价格 - if (inventoryList.isEmpty()) { - // 库存为空时,直接从定价主表获取统一零售价 - String finalUnitCode = unitCode; // 创建final变量用于lambda - List mainPrice = mainCharge.stream() - .filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null - && baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId())) - .peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode - .collect(Collectors.toList()); - priceDtoList.addAll(mainPrice); - } else { - // 库存信息里取 命中条件 去匹配价格 - for (AdviceInventoryDto adviceInventoryDto : inventoryList) { - Long finalChargeItemDefinitionId = chargeItemDefinitionId; - String finalUnitCode = unitCode; - // 从定价子表取价格(适用于批次售卖场景) - List childPrice = childCharge.stream() - .filter(e -> e.getDefinitionId() != null && finalChargeItemDefinitionId != null - && e.getDefinitionId().equals(finalChargeItemDefinitionId) - && e.getConditionValue() != null && adviceInventoryDto.getLotNumber() != null - && e.getConditionValue().equals(adviceInventoryDto.getLotNumber())) - .peek(e -> e.setUnitCode(finalUnitCode)) // 设置 unitCode - .collect(Collectors.toList()); - // 从定价主表取价格(适用于统一零售价场景) - List mainPrice = mainCharge.stream() - .filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null - && baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId())) - .collect(Collectors.toList()); - // 按批次售价 - if (OrderPricingSource.BATCH_SELLING_PRICE.getCode().equals(orderPricingSource)) { - priceDtoList.addAll(childPrice); - } else { - priceDtoList.addAll(mainPrice); - } - } - } - // 价格信息 - baseDto.setPriceList(priceDtoList); - } else if (CommonConstants.TableName.WOR_ACTIVITY_DEFINITION.equals(tableName)) { // 诊疗 - List priceList = mainCharge.stream() - .filter(e -> baseDto.getChargeItemDefinitionId() != null && e.getDefinitionId() != null - && baseDto.getChargeItemDefinitionId().equals(e.getDefinitionId())) - .collect(Collectors.toList()); - // 价格信息 - baseDto.setPriceList(priceList); - // 活动类型 - baseDto.setActivityType_enumText( - EnumUtils.getInfoByValue(ActivityType.class, baseDto.getActivityType())); - } - } - - // 缓存结果(只有无搜索关键字时才缓存) - if (useCache && cacheKey != null && adviceBaseInfo != null) { - // 确保是 IPage 类型再缓存,避免缓存无效数据 - if (adviceBaseInfo instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { - redisCache.setCacheObject(cacheKey, adviceBaseInfo, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS); - log.info("缓存医嘱基础信息, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS); - } - } - - return adviceBaseInfo; + return R.ok("申请单保存成功"); } - - /** - * 查询医嘱绑定信息 - * - * @param typeCode 1:用法绑东西 2:诊疗绑东西 - * @param itemNo 用法的code 或者 诊疗定义id - * @return 医嘱绑定信息 - */ - @Override - public List getOrderBindInfo(String typeCode, String itemNo) { - return doctorStationAdviceAppMapper.getOrderBindInfo(typeCode, PublicationStatus.ACTIVE.getValue(), itemNo); - } - - /** - * 门诊保存/签发医嘱 - * - * @param adviceSaveParam 医嘱表单信息 - * @param adviceOpType 医嘱操作类型 - * @return 结果 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public R saveAdvice(AdviceSaveParam adviceSaveParam, String adviceOpType) { - try { - // 🔧 BugFix#333/335/336: 参数非空校验 - if (adviceSaveParam == null) { - log.error("BugFix#333: adviceSaveParam 为 null"); - return R.fail(null, "请求参数为空,请刷新页面后重试"); - } - - // 患者挂号对应的科室id - Long organizationId = adviceSaveParam.getOrganizationId(); - // 医嘱分类信息 - List adviceSaveList = adviceSaveParam.getAdviceSaveList(); - - // 🔧 BugFix#333: 医嘱列表非空校验 - if (adviceSaveList == null || adviceSaveList.isEmpty()) { - log.error("BugFix#333: adviceSaveList 为 null 或空,adviceOpType={}", adviceOpType); - return R.fail(null, "医嘱列表为空,请刷新页面后重试"); - } - - // 🔍 Debug日志: 记录请求入口 - log.info("========== BugFix#333/335/336 DEBUG START =========="); - log.info("saveAdvice called, adviceOpType={}, organizationId={}, adviceSaveList.size={}", - adviceOpType, organizationId, adviceSaveList != null ? adviceSaveList.size() : 0); - if (adviceSaveList != null && !adviceSaveList.isEmpty()) { - for (int i = 0; i < adviceSaveList.size(); i++) { - AdviceSaveDto dto = adviceSaveList.get(i); - log.info("Request[{}]: requestId={}, dbOpType={}, adviceType={}, encounterId={}, patientId={}, categoryEnum={}, categoryEnum.class={}, categoryCode={}, categoryCode.class={}", - i, dto.getRequestId(), dto.getDbOpType(), dto.getAdviceType(), - dto.getEncounterId(), dto.getPatientId(), - dto.getCategoryEnum(), dto.getCategoryEnum() != null ? dto.getCategoryEnum().getClass().getName() : "NULL", - dto.getCategoryCode(), dto.getCategoryCode() != null ? dto.getCategoryCode().getClass().getName() : "NULL"); - } - } - - // 🔧 Bug Fix: 校验并补全patientId和encounterId(如果为null,尝试从医嘱记录获取) - for (AdviceSaveDto adviceSaveDto : adviceSaveList) { - // 对于删除操作,如果encounterId为null,尝试从医嘱记录获取 - if (adviceSaveDto.getEncounterId() == null && DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) { - // 尝试从各类医嘱记录中获取encounterId - Long requestId = adviceSaveDto.getRequestId(); - if (requestId != null) { - // 尝试从药品医嘱获取 - MedicationRequest medRequest = iMedicationRequestService.getById(requestId); - if (medRequest != null && medRequest.getEncounterId() != null) { - adviceSaveDto.setEncounterId(medRequest.getEncounterId()); - adviceSaveDto.setPatientId(medRequest.getPatientId()); - log.info("BugFix: 删除药品医嘱时自动补全encounterId和patientId: requestId={}, encounterId={}, patientId={}", - requestId, medRequest.getEncounterId(), medRequest.getPatientId()); - } else { - // 尝试从耗材医嘱获取 - DeviceRequest devRequest = iDeviceRequestService.getById(requestId); - if (devRequest != null && devRequest.getEncounterId() != null) { - adviceSaveDto.setEncounterId(devRequest.getEncounterId()); - adviceSaveDto.setPatientId(devRequest.getPatientId()); - log.info("BugFix: 删除耗材医嘱时自动补全encounterId和patientId: requestId={}, encounterId={}, patientId={}", - requestId, devRequest.getEncounterId(), devRequest.getPatientId()); - } else { - // 尝试从诊疗医嘱获取 - ServiceRequest srvRequest = iServiceRequestService.getById(requestId); - if (srvRequest != null && srvRequest.getEncounterId() != null) { - adviceSaveDto.setEncounterId(srvRequest.getEncounterId()); - adviceSaveDto.setPatientId(srvRequest.getPatientId()); - log.info("BugFix: 删除诊疗医嘱时自动补全encounterId和patientId: requestId={}, encounterId={}, patientId={}", - requestId, srvRequest.getEncounterId(), srvRequest.getPatientId()); - } - } - } - } - } - - // 首先检查encounterId是否为null - if (adviceSaveDto.getEncounterId() == null) { - log.error("encounterId为null,无法保存医嘱, dbOpType={}, requestId={}, adviceType={}", - adviceSaveDto.getDbOpType(), adviceSaveDto.getRequestId(), adviceSaveDto.getAdviceType()); - return R.fail(null, "就诊信息不完整,请重新选择患者后再试"); - } - - // 如果patientId为null,尝试从encounter获取 - if (adviceSaveDto.getPatientId() == null) { - // 从就诊记录中获取patientId - Encounter encounter = iEncounterService.getById(adviceSaveDto.getEncounterId()); - if (encounter != null && encounter.getPatientId() != null) { - adviceSaveDto.setPatientId(encounter.getPatientId()); - log.info("自动补全patientId: encounterId={}, patientId={}", - adviceSaveDto.getEncounterId(), encounter.getPatientId()); - } else { - log.error("无法获取patientId: encounterId={}", adviceSaveDto.getEncounterId()); - return R.fail(null, "无法获取患者信息,请重新选择患者"); - } - } - - // 🔧 BugFix#338: 门诊划价新增时校验就诊状态和诊断记录(患者安全) - // 仅对新增/修改操作进行校验,删除操作不需要 - if (!DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) { - // 1. 校验就诊状态:必须是已接诊状态 - Encounter encounterCheck = iEncounterService.getById(adviceSaveDto.getEncounterId()); - if (encounterCheck != null) { - // 就诊状态:1=待诊(PLANNED),允许保存的状态 = 2(IN_PROGRESS在诊)、3(ON_HOLD暂离)、4(DISCHARGED诊毕)、5(COMPLETED完成) - if (encounterCheck.getStatusEnum() != null && - encounterCheck.getStatusEnum() != EncounterStatus.IN_PROGRESS.getValue() && - encounterCheck.getStatusEnum() != EncounterStatus.ON_HOLD.getValue() && - encounterCheck.getStatusEnum() != EncounterStatus.DISCHARGED.getValue() && - encounterCheck.getStatusEnum() != EncounterStatus.COMPLETED.getValue()) { - log.error("BugFix#338: 患者未接诊,禁止划价/保存医嘱:encounterId={}, status={}", - adviceSaveDto.getEncounterId(), encounterCheck.getStatusEnum()); - return R.fail(null, "患者尚未接诊,无法保存医嘱。请先完成接诊操作!"); - } - } - } - } - - // 按后端 ItemType 枚举标准分类: - // MEDICINE=1(药品)、DEVICE=2(耗材)、ACTIVITY=3(诊疗)、SURGERY=6(手术) - - // 药品分类:adviceType == 1 - List medicineList = adviceSaveList.stream() - .filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.MEDICINE.getValue()) - .collect(Collectors.toList()); - - // 耗材分类:adviceType == 2 - List deviceList = adviceSaveList.stream() - .filter(e -> e.getAdviceType() != null && e.getAdviceType() == ItemType.DEVICE.getValue()) - .collect(Collectors.toList()); - - // 诊疗分类:adviceType == 3 - List activityList = adviceSaveList.stream() - .filter(e -> e.getAdviceType() != null - && (e.getAdviceType() == ItemType.ACTIVITY.getValue() - || e.getAdviceType() == ItemType.SURGERY.getValue())) // 手术(6)也走诊疗流程 - .collect(Collectors.toList()); - - // 🔍 Debug日志日志: 记录分类结果 - log.info("BugFix#219: 医嘱分类完成 - 药品:{}, 耗材:{}, 诊疗:{}", - medicineList.size(), deviceList.size(), activityList.size()); - // 🔍 Debug日志: 打印所有医嘱的adviceType - for (AdviceSaveDto dto : adviceSaveList) { - log.info("BugFix#219: 医嘱详情 - adviceType:{}, requestId:{}, adviceName:{}, dbOpType:{}", - dto.getAdviceType(), dto.getRequestId(), - dto.getContentJson() != null && dto.getContentJson().contains("adviceName") - ? dto.getContentJson().substring(0, Math.min(100, dto.getContentJson().length())) - : "N/A", - dto.getDbOpType()); - } - - // 统计各类删除操作 - long medDeleteCount = medicineList.stream().filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).count(); - long devDeleteCount = deviceList.stream().filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).count(); - long actDeleteCount = activityList.stream().filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).count(); - log.info("BugFix#219: 待删除数量 - 药品:{}, 耗材:{}, 诊疗:{}", medDeleteCount, devDeleteCount, actDeleteCount); - - /** - * 保存时,校验库存 - */ - if (AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType)) { - List medUpdateList = medicineList.stream().filter(e -> e.getRequestId() != null) - .collect(Collectors.toList()); - List devUpdateList = deviceList.stream().filter(e -> e.getRequestId() != null) - .collect(Collectors.toList()); - // 编辑时,释放本身占用的库存发放 - for (AdviceSaveDto adviceSaveDto : medUpdateList) { - iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId()); - } - for (AdviceSaveDto adviceSaveDto : devUpdateList) { - iDeviceDispenseService.deleteDeviceDispense(adviceSaveDto.getRequestId()); - } - - // 🔧 Bug Fix: 跳过库存校验(临时医嘱已计费,不需要重复校验库存) - // List needCheckList = adviceSaveList.stream() - // .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) - // && !ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) - // && !ItemType.DEVICE.getValue().equals(e.getAdviceType()) - // && !ItemType.SURGERY.getValue().equals(e.getAdviceType())) - // .collect(Collectors.toList()); - // // 校验库存 - // String tipRes = adviceUtils.checkInventory(needCheckList); - // if (tipRes != null) { - // return R.fail(null, tipRes); - // } - } - // 当前时间 - Date curDate = new Date(); - // 医嘱签发编码 - String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), 10); - - /** - * 处理药品请求 - */ - List medRequestIdList = this.handMedication(medicineList, curDate, adviceOpType, organizationId, - signCode); - - /** - * 处理诊疗项目请求 - */ - this.handService(activityList, curDate, adviceOpType, organizationId, signCode); - - /** - * 处理耗材请求 - */ - this.handDevice(deviceList, curDate, adviceOpType); - - // 签发时,把草稿状态的账单更新为待收费 - if (AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType) && !adviceSaveList.isEmpty()) { - // 签发的医嘱id集合 - 收集所有需要签发的医嘱ID - List requestIds = adviceSaveList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null) - .collect(Collectors.toList()).stream().map(AdviceSaveDto::getRequestId) - .collect(Collectors.toList()); - - // 🔧 BugFix: 批量更新药品请求状态为已签发(ACTIVE=2) - if (!requestIds.isEmpty() && !medicineList.isEmpty()) { - List medicineIds = medicineList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null) - .map(AdviceSaveDto::getRequestId) - .collect(Collectors.toList()); - if (!medicineIds.isEmpty()) { - log.info("BugFix: 准备批量更新药品医嘱状态,medicineIds={}", medicineIds); - UpdateWrapper updateWrapper = new UpdateWrapper<>(); - updateWrapper.in("id", medicineIds); - updateWrapper.set("status_enum", RequestStatus.ACTIVE.getValue()); - boolean updateResult = iMedicationRequestService.update(null, updateWrapper); - log.info("BugFix: 批量更新药品医嘱状态为已签发,count={}, result={}", medicineIds.size(), updateResult); - - // 🔧 BugFix: 如果批量更新失败,尝试逐个更新 - if (!updateResult) { - log.warn("BugFix: 批量更新药品医嘱状态失败,尝试逐个更新"); - for (Long medicineId : medicineIds) { - try { - MedicationRequest updateReq = new MedicationRequest(); - updateReq.setId(medicineId); - updateReq.setStatusEnum(RequestStatus.ACTIVE.getValue()); - boolean singleResult = iMedicationRequestService.updateById(updateReq); - log.info("BugFix: 逐个更新药品医嘱状态,id={}, result={}", medicineId, singleResult); - } catch (Exception e) { - log.error("BugFix: 逐个更新药品医嘱状态失败,id={}", medicineId, e); - } - } - } - } - } - // 🔧 BugFix: 批量更新耗材请求状态为已签发(ACTIVE=2) - if (!requestIds.isEmpty() && !deviceList.isEmpty()) { - List deviceIds = deviceList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null) - .map(AdviceSaveDto::getRequestId) - .collect(Collectors.toList()); - if (!deviceIds.isEmpty()) { - log.info("BugFix: 准备批量更新耗材医嘱状态,deviceIds={}", deviceIds); - UpdateWrapper updateWrapper = new UpdateWrapper<>(); - updateWrapper.in("id", deviceIds); - updateWrapper.set("status_enum", RequestStatus.ACTIVE.getValue()); - boolean updateResult = iDeviceRequestService.update(null, updateWrapper); - log.info("BugFix: 批量更新耗材医嘱状态为已签发,count={}, result={}", deviceIds.size(), updateResult); - - // 🔧 BugFix: 如果批量更新失败,尝试逐个更新 - if (!updateResult) { - log.warn("BugFix: 批量更新耗材医嘱状态失败,尝试逐个更新"); - for (Long deviceId : deviceIds) { - try { - DeviceRequest updateReq = new DeviceRequest(); - updateReq.setId(deviceId); - updateReq.setStatusEnum(RequestStatus.ACTIVE.getValue()); - boolean singleResult = iDeviceRequestService.updateById(updateReq); - log.info("BugFix: 逐个更新耗材医嘱状态,id={}, result={}", deviceId, singleResult); - } catch (Exception e) { - log.error("BugFix: 逐个更新耗材医嘱状态失败,id={}", deviceId, e); - } - } - } - } - } - // 🔧 BugFix: 批量更新诊疗请求状态为已签发(ACTIVE=2) - if (!requestIds.isEmpty() && !activityList.isEmpty()) { - List activityIds = activityList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType()) && e.getRequestId() != null) - .map(AdviceSaveDto::getRequestId) - .collect(Collectors.toList()); - if (!activityIds.isEmpty()) { - log.info("BugFix: 准备批量更新诊疗医嘱状态,activityIds={}", activityIds); - UpdateWrapper updateWrapper = new UpdateWrapper<>(); - updateWrapper.in("id", activityIds); - updateWrapper.set("status_enum", RequestStatus.ACTIVE.getValue()); - boolean updateResult = iServiceRequestService.update(null, updateWrapper); - log.info("BugFix: 批量更新诊疗医嘱状态为已签发,count={}, result={}", activityIds.size(), updateResult); - - // 🔧 BugFix: 如果批量更新失败,尝试逐个更新 - if (!updateResult) { - log.warn("BugFix: 批量更新诊疗医嘱状态失败,尝试逐个更新"); - for (Long activityId : activityIds) { - try { - ServiceRequest updateReq = new ServiceRequest(); - updateReq.setId(activityId); - updateReq.setStatusEnum(RequestStatus.ACTIVE.getValue()); - boolean singleResult = iServiceRequestService.updateById(updateReq); - log.info("BugFix: 逐个更新诊疗医嘱状态,id={}, result={}", activityId, singleResult); - } catch (Exception e) { - log.error("BugFix: 逐个更新诊疗医嘱状态失败,id={}", activityId, e); - } - } - } - } - } - - // 就诊id - Long encounterId = adviceSaveList.get(0).getEncounterId(); - - // 🔧 BugFix#385: 签发时更新诊疗医嘱关联的费用项诊断信息 - // 检查申请创建的费用项没有诊断关联,需要在签发时补充 - if (!activityList.isEmpty()) { - // 先从就诊中获取主诊断,用于补充没有诊断关联的费用项 - List encounterDiagList = iEncounterDiagnosisService.getDiagnosisList(encounterId); - EncounterDiagnosis mainDiagnosis = iEncounterDiagnosisService.getMainDiagnosis(encounterDiagList); - Long mainConditionId = mainDiagnosis != null ? mainDiagnosis.getConditionId() : null; - Long mainEncounterDiagId = mainDiagnosis != null ? mainDiagnosis.getId() : null; - - log.info("BugFix#385: 签发时获取就诊主诊断, encounterId={}, mainConditionId={}, mainEncounterDiagId={}", - encounterId, mainConditionId, mainEncounterDiagId); - - for (AdviceSaveDto adviceDto : activityList) { - if (adviceDto.getRequestId() != null) { - // 查询诊疗医嘱关联的费用项 - List chargeItems = iChargeItemService.getChargeItemInfoByReqId( - Arrays.asList(adviceDto.getRequestId())); - if (chargeItems != null && !chargeItems.isEmpty()) { - // 过滤只保留诊疗类型的费用项 - ChargeItem chargeItem = chargeItems.stream() - .filter(ci -> CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(ci.getServiceTable())) - .findFirst().orElse(null); - if (chargeItem != null) { - // 🔧 BugFix#385: 如果费用项没有诊断关联,使用主诊断补充 - Long conditionId = adviceDto.getConditionId(); - Long encounterDiagId = adviceDto.getEncounterDiagnosisId(); - - // 如果传入的诊断为空,使用主诊断 - if (conditionId == null) { - conditionId = mainConditionId; - } - if (encounterDiagId == null) { - encounterDiagId = mainEncounterDiagId; - } - - // 更新诊断关联 - if (conditionId != null || encounterDiagId != null) { - chargeItem.setConditionId(conditionId); - chargeItem.setEncounterDiagnosisId(encounterDiagId); - iChargeItemService.updateById(chargeItem); - log.info("BugFix#385: 签发时更新诊疗费用项诊断关联, chargeItemId={}, conditionId={}, encounterDiagnosisId={}", - chargeItem.getId(), conditionId, encounterDiagId); - } - } - } - } - } - } - - // 🔧 BugFix#385: 使用安全的更新方法,避免并发冲突 - 更新费用项状态 - // 需要处理两种情况: - // 1. 从 DRAFT (0) → PLANNED (1):新创建的收费项目 - // 2. 从 BILLABLE (2) → PLANNED (1):保存时已设为待结算的项目 - iChargeItemService.updateChargeStatusByConditionSafe( - encounterId, - ChargeItemStatus.DRAFT.getValue(), - ChargeItemStatus.PLANNED.getValue(), - requestIds); - - // 🔧 BugFix#385: 同时处理 BILLABLE 状态的收费项目 - iChargeItemService.updateChargeStatusByConditionSafe( - encounterId, - ChargeItemStatus.BILLABLE.getValue(), - ChargeItemStatus.PLANNED.getValue(), - requestIds); - } - - // 数据变更后清理相关缓存 - clearRelatedCache(); - - return R.ok(medRequestIdList, - MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[] { "门诊医嘱" })); - } catch (Exception e) { - // 异常处理 - throw e; - } - } - - /** - * 清理相关缓存 - */ - private void clearRelatedCache() { - // 目前不使用缓存,此方法为空实现 - // 如果将来启用缓存,可以在这里实现缓存清理逻辑 - } - - /** - * 处理药品 - */ - private List handMedication(List medicineList, Date curDate, String adviceOpType, - Long organizationId, String signCode) { - // 当前登录账号的科室id - Long orgId = SecurityUtils.getLoginUser().getOrgId(); - // 获取当前登录用户的tenantId - Integer tenantId = SecurityUtils.getLoginUser().getTenantId(); - // 获取当前登录用户名 - String currentUsername = SecurityUtils.getUsername(); - // 保存操作 - boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType); - // 签发操作 - boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType); - // 保存药品请求 - MedicationRequest medicationRequest; - // 声明费用项 - ChargeItem chargeItem; - // 新增 + 修改 - // 🔧 BugFix: 如果 requestId 不为空说明是已存在的医嘱,需要更新,即使 dbOpType 不匹配也应该包含进来 - // 🔧 BugFix #454: 排除删除操作,避免误入insertOrUpdateList - List insertOrUpdateList = medicineList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType())) - .filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType()) - || DbOpType.UPDATE.getCode().equals(e.getDbOpType()) - || e.getRequestId() != null)) - .collect(Collectors.toList()); - // 删除 - // 🔧 BugFix: 如果 dbOpType 不匹配但 requestId 存在,仍然允许删除(增加健壮性) - List deleteList = medicineList.stream() - .filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList()); - // 校验删除的医嘱是否已经收费 - List delRequestIdList = deleteList.stream().map(AdviceSaveDto::getRequestId).collect(Collectors.toList()); - if (!delRequestIdList.isEmpty()) { - List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(delRequestIdList); - // 🔧 BugFix#219: 过滤只保留药品类型的费用项 - if (chargeItemList != null && !chargeItemList.isEmpty()) { - chargeItemList = chargeItemList.stream() - .filter(ci -> CommonConstants.TableName.MED_MEDICATION_REQUEST.equals(ci.getServiceTable())) - .collect(Collectors.toList()); - } - if (chargeItemList != null && !chargeItemList.isEmpty()) { - for (ChargeItem ci : chargeItemList) { - if (ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())) { - throw new ServiceException("已收费的项目无法删除,请刷新页面后重试"); - } - } - } - } - for (AdviceSaveDto adviceSaveDto : deleteList) { - Long requestId = adviceSaveDto.getRequestId(); - // 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的药品请求 - if (requestId == null) { - log.warn("BugFix#442: handMedication - 跳过 requestId 为 null 的删除请求"); - continue; - } - iMedicationRequestService.removeById(requestId); - // 删除已经产生的药品发放信息 - iMedicationDispenseService.deleteMedicationDispense(adviceSaveDto.getRequestId()); - // 🔧 Bug Fix #219: 删除费用项 - String serviceTable = CommonConstants.TableName.MED_MEDICATION_REQUEST; - // 直接删除费用项 - iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); - log.info("BugFix#219: 药品医嘱删除完成, requestId={}, serviceTable={}", requestId, serviceTable); - // 删除基于这个药品生成的需要执行的诊疗请求 - iServiceRequestService.remove( - new LambdaQueryWrapper() - .eq(ServiceRequest::getBasedOnId, adviceSaveDto.getRequestId()) - .isNotNull(ServiceRequest::getBasedOnTable) - .eq(ServiceRequest::getStatusEnum, RequestStatus.COMPLETED.getValue())); - } - // 签发时 - if (is_sign) { - // 🔧 Bug Fix #328: 只对药品类型的医嘱生成处方号 - // 检验申请单生成的医嘱是诊疗项目(adviceType=3),不需要处方号 - List medicineListForPrescription = insertOrUpdateList.stream() - .filter(e -> ItemType.MEDICINE.getValue().equals(e.getAdviceType())) - .collect(Collectors.toList()); - if (!medicineListForPrescription.isEmpty()) { - prescriptionUtils.generatePrescriptionNumbers(medicineListForPrescription); - } - } - - List medRequestIdList = new ArrayList<>(); - - // 🔧 防重复保存:对新增医嘱进行去重 - // 去重逻辑:针对同一患者、同一就诊、同一药品、同一剂量的医嘱,只保存一条 - Set uniqueKeySet = new HashSet<>(); - List uniqueInsertOrUpdateList = new ArrayList<>(); - - for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) { - // 构建唯一标识键:患者ID + 就诊ID + 药品ID + 剂量 + 用法 + 频次 - String uniqueKey = adviceSaveDto.getPatientId() + "_" + - adviceSaveDto.getEncounterId() + "_" + - adviceSaveDto.getAdviceDefinitionId() + "_" + - adviceSaveDto.getDose() + "_" + - adviceSaveDto.getMethodCode() + "_" + - adviceSaveDto.getRateCode(); - - // 如果是新增操作且唯一标识已存在,则跳过 - if (DbOpType.INSERT.getCode().equals(adviceSaveDto.getDbOpType()) && - uniqueKeySet.contains(uniqueKey)) { - log.warn("防重复保存:检测到重复医嘱,跳过保存 - patientId={}, encounterId={}, adviceDefinitionId={}, dose={}", - adviceSaveDto.getPatientId(), adviceSaveDto.getEncounterId(), - adviceSaveDto.getAdviceDefinitionId(), adviceSaveDto.getDose()); - continue; - } - - // 添加到去重集合和列表 - uniqueKeySet.add(uniqueKey); - uniqueInsertOrUpdateList.add(adviceSaveDto); - } - - // 使用去重后的列表进行保存 - log.info("防重复保存:去重前{}条,去重后{}条", insertOrUpdateList.size(), uniqueInsertOrUpdateList.size()); - insertOrUpdateList = uniqueInsertOrUpdateList; - - for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) { - // 🔧 Bug Fix: 确保accountId有效(不为null且账户存在) - boolean needNewAccount = false; - if (adviceSaveDto.getAccountId() == null) { - needNewAccount = true; - } else { - // 验证账户是否存在且有效(未被删除,租户匹配) - Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId()); - if (existingAccount == null) { - log.warn("handMedication - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户", - adviceSaveDto.getAccountId()); - needNewAccount = true; - } - } - - if (needNewAccount) { - // 尝试从患者就诊中获取默认账户ID(自费账户) - Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId()); - if (selfAccount != null) { - adviceSaveDto.setAccountId(selfAccount.getId()); - log.info("handMedication - 使用现有自费账户,accountId={}", selfAccount.getId()); - } else { - // 自动创建自费账户 - Account newAccount = new Account(); - newAccount.setPatientId(adviceSaveDto.getPatientId()); - newAccount.setEncounterId(adviceSaveDto.getEncounterId()); - newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); - newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode()); - newAccount.setBalanceAmount(BigDecimal.ZERO); - newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue()); - newAccount.setEncounterFlag(Whether.YES.getValue()); - newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo()); - Long newAccountId = iAccountService.saveAccountByRegister(newAccount); - adviceSaveDto.setAccountId(newAccountId); - log.info("handMedication - 自动创建自费账户,newAccountId={}", newAccountId); - } - } - - // 🔧 Bug Fix: 确保practitionerId不为null - if (adviceSaveDto.getPractitionerId() == null) { - adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId()); - log.info("handMedication - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId()); - } - - // 🔧 Bug Fix: 确保founderOrgId不为null - if (adviceSaveDto.getFounderOrgId() == null) { - adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId()); - log.info("handMedication - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId()); - } - - boolean firstTimeSave = false;// 第一次保存 - medicationRequest = new MedicationRequest(); - medicationRequest.setId(adviceSaveDto.getRequestId()); // 主键id - medicationRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态 - medicationRequest.setPrescriptionNo(adviceSaveDto.getPrescriptionNo()); // 处方号 - medicationRequest.setGroupId(adviceSaveDto.getGroupId()); // 组号 - medicationRequest.setTenantId(tenantId); // 设置租户id - medicationRequest.setCreateBy(currentUsername); // 设置创建人 - medicationRequest.setCreateTime(curDate); // 设置创建时间 - if (is_sign) { - medicationRequest.setSignCode(signCode); - } - // 🔧 Bug Fix: 签发时也需要设置关键字段(修复BUG #181) - // 保存时生成业务编号,签发时沿用已有编号 - if (is_save) { - medicationRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.MEDICATION_RES_NO.getPrefix(), 4)); - } - medicationRequest.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - medicationRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 - medicationRequest.setExecuteNum(adviceSaveDto.getExecuteNum()); // 执行次数 - medicationRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 - medicationRequest.setLotNumber(adviceSaveDto.getLotNumber()); // 产品批号 - medicationRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型 - medicationRequest.setMedicationId(adviceSaveDto.getAdviceDefinitionId());// 医嘱定义id - medicationRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者 - medicationRequest.setPractitionerId(adviceSaveDto.getPractitionerId()); // 开方医生 - medicationRequest.setOrgId(adviceSaveDto.getFounderOrgId()); // 开方人科室 - medicationRequest.setReqAuthoredTime(curDate); // 请求开始时间 - // 发放药房 - medicationRequest.setPerformLocation(adviceSaveDto.getLocationId()); - medicationRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - medicationRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - medicationRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - medicationRequest.setTherapyEnum(adviceSaveDto.getTherapyEnum()); // 治疗类型,默认-临时 - medicationRequest.setMethodCode(adviceSaveDto.getMethodCode()); // 用法 - medicationRequest.setRateCode(adviceSaveDto.getRateCode()); // 用药频次 - medicationRequest.setDose(adviceSaveDto.getDose()); // 单次剂量 - medicationRequest.setDoseUnitCode(adviceSaveDto.getDoseUnitCode()); // 剂量单位 - medicationRequest.setDispensePerDuration(adviceSaveDto.getDispensePerDuration()); // 每次发药供应天数 - medicationRequest.setPackageId(adviceSaveDto.getPackageId()); // 组套id - medicationRequest.setContentJson(adviceSaveDto.getContentJson()); // 请求内容json - medicationRequest.setYbClassEnum(adviceSaveDto.getYbClassEnum());// 类别医保编码 - medicationRequest.setSkinTestFlag(adviceSaveDto.getSkinTestFlag()); // 皮试标志 - medicationRequest.setInfusionFlag(adviceSaveDto.getInjectFlag()); // 输液标志 - medicationRequest.setSortNumber(adviceSaveDto.getSortNumber()); // 排序号 - - if (medicationRequest.getId() == null) { - firstTimeSave = true; - } - iMedicationRequestService.saveOrUpdate(medicationRequest); - if (firstTimeSave) { - medRequestIdList.add(medicationRequest.getId().toString()); - } - if (is_save) { - // 处理药品发放 - Long dispenseId = iMedicationDispenseService.handleMedicationDispense(medicationRequest, - adviceSaveDto.getDbOpType()); - - // 保存药品费用项 - chargeItem = new ChargeItem(); - chargeItem.setId(adviceSaveDto.getChargeItemId()); // 费用项id - chargeItem.setStatusEnum(2); // 已生成医嘱 - chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(medicationRequest.getBusNo())); - chargeItem.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - chargeItem.setPrescriptionNo(adviceSaveDto.getPrescriptionNo()); // 处方号 - chargeItem.setPatientId(adviceSaveDto.getPatientId()); // 患者 - chargeItem.setContextEnum(adviceSaveDto.getAdviceType()); // 类型 - chargeItem.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - // 🔧 Bug Fix: 如果definitionId为空,使用adviceDefinitionId作为后备 - Long definitionId = adviceSaveDto.getDefinitionId(); - if (definitionId == null) { - definitionId = adviceSaveDto.getAdviceDefinitionId(); - } - chargeItem.setDefinitionId(definitionId); // 费用定价ID - chargeItem.setDefDetailId(adviceSaveDto.getDefinitionDetailId()); // 定价子表主键 - chargeItem.setEntererId(adviceSaveDto.getPractitionerId());// 开立人ID - chargeItem.setRequestingOrgId(orgId); // 开立科室 - chargeItem.setEnteredDate(curDate); // 开立时间 - chargeItem.setServiceTable(CommonConstants.TableName.MED_MEDICATION_REQUEST);// 医疗服务类型 - chargeItem.setServiceId(medicationRequest.getId()); // 医疗服务ID - chargeItem.setProductTable(adviceSaveDto.getAdviceTableName());// 产品所在表 - chargeItem.setProductId(adviceSaveDto.getAdviceDefinitionId());// 收费项id - chargeItem.setAccountId(adviceSaveDto.getAccountId());// 关联账户ID - chargeItem.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - chargeItem.setDispenseId(dispenseId); // 发放ID - chargeItem.setTenantId(tenantId); // 设置租户ID (修复本次报错) - chargeItem.setCreateBy(currentUsername); // 设置创建人 - chargeItem.setCreateTime(curDate); // 设置创建时间 - - chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量 - chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位 - // #415 价格非负验证 - BigDecimal unitPrice = adviceSaveDto.getUnitPrice(); - if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) { - unitPrice = unitPrice.abs(); // 负数取绝对值 - } - chargeItem.setUnitPrice(unitPrice); // 单价 - chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价 - - // 显式设置tenantId、createBy和createTime字段,防止自动填充机制失效 - chargeItem.setTenantId(SecurityUtils.getLoginUser().getTenantId()); - chargeItem.setCreateBy(SecurityUtils.getLoginUser().getUsername()); - chargeItem.setCreateTime(new Date()); - - iChargeItemService.saveOrUpdate(chargeItem); - - // 显式更新前端传的chargeItemId对应的收费项目状态为2(已生成医嘱) - if (adviceSaveDto.getChargeItemId() != null) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(ChargeItem::getId, adviceSaveDto.getChargeItemId()) - .set(ChargeItem::getStatusEnum, 2); - iChargeItemService.update(updateWrapper); - log.info("已更新药品收费项目状态为已生成医嘱,chargeItemId:{}", adviceSaveDto.getChargeItemId()); - } - - // 🔧 Bug Fix #145: 处理用法绑定的耗材 - if (StringUtils.isNotBlank(adviceSaveDto.getMethodCode())) { - handleBoundDevices(adviceSaveDto, medicationRequest, chargeItem, curDate, orgId, tenantId, - currentUsername); - } - - } - } - return medRequestIdList; - } - - /** - * 处理用法绑定的耗材 - * - * @param adviceSaveDto 医嘱保存DTO - * @param medicationRequest 药品请求 - * @param medicationChargeItem 药品费用项 - * @param curDate 当前时间 - * @param orgId 科室ID - * @param tenantId 租户ID - * @param currentUsername 当前用户名 - */ - private void handleBoundDevices(AdviceSaveDto adviceSaveDto, MedicationRequest medicationRequest, - ChargeItem medicationChargeItem, Date curDate, Long orgId, Integer tenantId, String currentUsername) { - // 查询用法绑定的耗材 (typeCode=1表示用法绑定) - List boundDevices = outpatientRegistrationAppMapper.getBoundDevicesByUsage( - adviceSaveDto.getMethodCode(), - CommonConstants.TableName.ADM_DEVICE_DEFINITION, "1"); - - if (boundDevices == null || boundDevices.isEmpty()) { - return; - } - - for (ActivityDeviceDto boundDevice : boundDevices) { - // 创建耗材请求 - DeviceRequest deviceRequest = new DeviceRequest(); - deviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue()); - deviceRequest.setTenantId(tenantId); - deviceRequest.setCreateBy(currentUsername); - deviceRequest.setCreateTime(curDate); - deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); - deviceRequest.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); - deviceRequest.setQuantity(boundDevice.getQuantity()); - deviceRequest.setUnitCode(boundDevice.getUnitCode()); - deviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); - deviceRequest.setDeviceDefId(boundDevice.getDevActId()); - deviceRequest.setPatientId(adviceSaveDto.getPatientId()); - deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); - deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId()); - deviceRequest.setReqAuthoredTime(curDate); - deviceRequest.setPerformLocation(adviceSaveDto.getLocationId()); - deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); - deviceRequest.setConditionId(adviceSaveDto.getConditionId()); - deviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); - // 关联到药品请求 - deviceRequest.setBasedOnId(medicationRequest.getId()); - deviceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST); - // 🔧 Bug Fix #145: 设置处方号,确保门诊收费能正确显示 - deviceRequest.setPrescriptionNo(adviceSaveDto.getPrescriptionNo()); - - iDeviceRequestService.save(deviceRequest); - - // 处理耗材发放 - Long dispenseId = iDeviceDispenseService.handleDeviceDispense(deviceRequest, DbOpType.INSERT.getCode()); - - // 查询耗材定价信息 - 直接使用mapper查询,避免递归调用getAdviceBaseInfo导致栈溢出 - IPage devicePage = doctorStationAdviceAppMapper.getAdviceBaseInfo( - new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, 1), - PublicationStatus.ACTIVE.getValue(), - adviceSaveDto.getFounderOrgId(), - null, - CommonConstants.TableName.ADM_DEVICE_DEFINITION, - null, - null, - List.of(boundDevice.getDevActId()), - null, - null, - null, - null); - - if (devicePage == null || devicePage.getRecords().isEmpty()) { - log.warn("无法找到耗材定价信息: deviceDefId={}", boundDevice.getDevActId()); - continue; - } - - AdviceBaseDto deviceBaseInfo = devicePage.getRecords().get(0); - - // 查询价格信息 - 直接查询定价主表 - List mainCharge = doctorStationAdviceAppMapper.getMainCharge( - List.of(deviceBaseInfo.getChargeItemDefinitionId()), PublicationStatus.ACTIVE.getValue()); - - if (mainCharge == null || mainCharge.isEmpty()) { - log.warn("耗材没有定价信息: deviceDefId={}", boundDevice.getDevActId()); - continue; - } - - AdvicePriceDto devicePrice = mainCharge.get(0); - devicePrice.setDefinitionId(deviceBaseInfo.getChargeItemDefinitionId()); - // 如果需要定价子表ID,可以从mainCharge中获取 - - // 创建耗材费用项 - ChargeItem deviceChargeItem = new ChargeItem(); - deviceChargeItem.setTenantId(tenantId); - deviceChargeItem.setCreateBy(currentUsername); - deviceChargeItem.setCreateTime(curDate); - deviceChargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue()); - deviceChargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(deviceRequest.getBusNo())); - deviceChargeItem.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); - deviceChargeItem.setPrescriptionNo(adviceSaveDto.getPrescriptionNo()); // 处方号,与药品一致 - deviceChargeItem.setPatientId(adviceSaveDto.getPatientId()); - deviceChargeItem.setContextEnum(ChargeItemContext.DEVICE.getValue()); // 耗材类型 - deviceChargeItem.setEncounterId(adviceSaveDto.getEncounterId()); - deviceChargeItem.setDefinitionId(devicePrice.getDefinitionId()); - deviceChargeItem.setDefDetailId(devicePrice.getDefinitionDetailId()); - deviceChargeItem.setEntererId(adviceSaveDto.getPractitionerId()); - deviceChargeItem.setRequestingOrgId(orgId); - deviceChargeItem.setEnteredDate(curDate); - deviceChargeItem.setServiceTable(CommonConstants.TableName.WOR_DEVICE_REQUEST); - deviceChargeItem.setServiceId(deviceRequest.getId()); - deviceChargeItem.setProductTable(CommonConstants.TableName.ADM_DEVICE_DEFINITION); - deviceChargeItem.setProductId(boundDevice.getDevActId()); - // 🔧 Bug Fix #281: 如果accountId为null,从就诊中获取账户ID,如果没有则自动创建 - Long deviceAccountId = adviceSaveDto.getAccountId(); - if (deviceAccountId == null) { - // 尝试从患者就诊中获取默认账户ID(自费账户) - Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId()); - if (selfAccount != null) { - deviceAccountId = selfAccount.getId(); - } else { - // 自动创建自费账户 - Account newAccount = new Account(); - newAccount.setPatientId(adviceSaveDto.getPatientId()); - newAccount.setEncounterId(adviceSaveDto.getEncounterId()); - newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); - newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode()); - newAccount.setBalanceAmount(BigDecimal.ZERO); - newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue()); - newAccount.setEncounterFlag(Whether.YES.getValue()); - // 🔧 Bug Fix: 设置账户名称,避免数据库NOT NULL约束错误 - newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo()); - deviceAccountId = iAccountService.saveAccountByRegister(newAccount); - } - } - deviceChargeItem.setAccountId(deviceAccountId); - deviceChargeItem.setConditionId(adviceSaveDto.getConditionId()); - deviceChargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); - deviceChargeItem.setDispenseId(dispenseId); - deviceChargeItem.setQuantityValue(boundDevice.getQuantity()); - deviceChargeItem.setQuantityUnit(boundDevice.getUnitCode()); - deviceChargeItem.setUnitPrice(devicePrice.getPrice()); - deviceChargeItem.setTotalPrice(boundDevice.getQuantity().multiply(devicePrice.getPrice())); - - iChargeItemService.save(deviceChargeItem); - - log.info("用法绑定耗材已创建: methodCode={}, deviceDefId={}, chargeItemId={}", - adviceSaveDto.getMethodCode(), boundDevice.getDevActId(), deviceChargeItem.getId()); - } - } - - /** - * 处理耗材 - */ - private void handDevice(List deviceList, Date curDate, String adviceOpType) { - // 🔍 Debug日志: handDevice方法入口 - log.info("BugFix#219: ========== handDevice START =========="); - log.info("BugFix#219: handDevice called, deviceList.size={}, adviceOpType={}", - deviceList != null ? deviceList.size() : 0, adviceOpType); - if (deviceList != null && !deviceList.isEmpty()) { - for (int i = 0; i < deviceList.size(); i++) { - AdviceSaveDto dto = deviceList.get(i); - log.info("BugFix#219: Device[{}]: requestId={}, dbOpType={}", - i, dto.getRequestId(), dto.getDbOpType()); - } - } - - // 当前登录账号的科室id - Long orgId = SecurityUtils.getLoginUser().getOrgId(); - // 获取当前登录用户的tenantId - Integer tenantId = SecurityUtils.getLoginUser().getTenantId(); - // 获取当前登录用户名 - String currentUsername = SecurityUtils.getUsername(); - // 保存操作 - boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType); - // 签发操作 - // boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType); - DeviceRequest deviceRequest; - // 声明费用项 - ChargeItem chargeItem; - // 新增 + 修改 - // 🔧 BugFix: 如果 requestId 不为空说明是已存在的医嘱,需要更新,即使 dbOpType 不匹配也应该包含进来 - // 🔧 BugFix #454: 排除删除操作,避免误入insertOrUpdateList - List insertOrUpdateList = deviceList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType())) - .filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType()) - || DbOpType.UPDATE.getCode().equals(e.getDbOpType()) - || e.getRequestId() != null)) - .collect(Collectors.toList()); - // 删除 - List deleteList = deviceList.stream() - .filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList()); - - // 🔍 Debug日志: 记录删除列表 - log.info("BugFix#219: handDevice - insertOrUpdateList.size={}, deleteList.size={}", - insertOrUpdateList.size(), deleteList.size()); - if (!deleteList.isEmpty()) { - for (AdviceSaveDto dto : deleteList) { - log.info("BugFix#219: handDevice - 待删除: requestId={}", dto.getRequestId()); - } - } - - // 校验删除的医嘱是否已经收费 - List delRequestIdList = deleteList.stream().map(AdviceSaveDto::getRequestId).collect(Collectors.toList()); - if (!delRequestIdList.isEmpty()) { - List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(delRequestIdList); - // 🔧 BugFix#219: 过滤只保留耗材类型的费用项 - if (chargeItemList != null && !chargeItemList.isEmpty()) { - chargeItemList = chargeItemList.stream() - .filter(ci -> CommonConstants.TableName.WOR_DEVICE_REQUEST.equals(ci.getServiceTable())) - .collect(Collectors.toList()); - } - if (chargeItemList != null && !chargeItemList.isEmpty()) { - for (ChargeItem ci : chargeItemList) { - if (ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())) { - throw new ServiceException("已收费的项目无法删除,请刷新页面后重试"); - } - } - } - } - // 🔍 Debug日志: 开始删除循环 - log.info("BugFix#219: handDevice - 开始删除循环, deleteList.size={}", deleteList.size()); - for (AdviceSaveDto adviceSaveDto : deleteList) { - Long requestId = adviceSaveDto.getRequestId(); - // 🔧 Bug #442: 跳过 requestId 为 null 的记录,避免删除不存在的耗材请求 - if (requestId == null) { - log.warn("BugFix#442: handDevice - 跳过 requestId 为 null 的删除请求"); - continue; - } - log.info("BugFix#219: handDevice - 删除开始: requestId={}", requestId); - - // 1. 删除耗材请求 - boolean deviceRemoved = iDeviceRequestService.removeById(requestId); - log.info("BugFix#219: handDevice - 删除DeviceRequest: requestId={}, result={}", requestId, deviceRemoved); - - // 2. 删除已经产生的耗材发放信息 - iDeviceDispenseService.deleteDeviceDispense(requestId); - log.info("BugFix#219: handDevice - 删除DeviceDispense: requestId={}", requestId); - - // 3. 删除费用项 - String serviceTable = CommonConstants.TableName.WOR_DEVICE_REQUEST; - // 先查询费用项是否存在 - try { - List existingChargeItems = iChargeItemService.getChargeItemInfoByReqId(Arrays.asList(requestId)); - log.info("BugFix#219: handDevice - 查询到费用项数量: requestId={}, count={}", requestId, - existingChargeItems != null ? existingChargeItems.size() : 0); - if (existingChargeItems != null) { - for (ChargeItem ci : existingChargeItems) { - log.info("BugFix#219: handDevice - 费用项详情: id={}, serviceTable={}, serviceId={}, status={}", - ci.getId(), ci.getServiceTable(), ci.getServiceId(), ci.getStatusEnum()); - } - } - } catch (Exception e) { - log.error("BugFix#219: handDevice - 查询费用项异常: requestId={}", requestId, e); - } - // 直接删除费用项(使用serviceTable和serviceId作为条件) - iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); - log.info("BugFix#219: handDevice - 删除ChargeItem: requestId={}, serviceTable={}", requestId, serviceTable); - log.info("BugFix#219: handDevice - 删除完成: requestId={}", requestId); - } - log.info("BugFix#219: ========== handDevice END =========="); - - for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) { - // 🔧 Bug Fix: 确保accountId有效(不为null且账户存在) - boolean needNewAccount = false; - if (adviceSaveDto.getAccountId() == null) { - needNewAccount = true; - } else { - // 验证账户是否存在且有效(未被删除,租户匹配) - Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId()); - if (existingAccount == null) { - log.warn("handDevice - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户", - adviceSaveDto.getAccountId()); - needNewAccount = true; - } - } - - if (needNewAccount) { - // 尝试从患者就诊中获取默认账户ID(自费账户) - Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId()); - if (selfAccount != null) { - adviceSaveDto.setAccountId(selfAccount.getId()); - log.info("handDevice - 使用现有自费账户,accountId={}", selfAccount.getId()); - } else { - // 自动创建自费账户 - Account newAccount = new Account(); - newAccount.setPatientId(adviceSaveDto.getPatientId()); - newAccount.setEncounterId(adviceSaveDto.getEncounterId()); - newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); - newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode()); - newAccount.setBalanceAmount(BigDecimal.ZERO); - newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue()); - newAccount.setEncounterFlag(Whether.YES.getValue()); - newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo()); - Long newAccountId = iAccountService.saveAccountByRegister(newAccount); - adviceSaveDto.setAccountId(newAccountId); - log.info("handDevice - 自动创建自费账户,newAccountId={}", newAccountId); - } - } - - // 🔧 Bug Fix: 确保practitionerId不为null - if (adviceSaveDto.getPractitionerId() == null) { - adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId()); - log.info("自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId()); - } - - // 🔧 Bug Fix: 确保founderOrgId不为null - if (adviceSaveDto.getFounderOrgId() == null) { - adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId()); - log.info("自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId()); - } - - deviceRequest = new DeviceRequest(); - deviceRequest.setId(adviceSaveDto.getRequestId()); // 主键id - deviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态 - deviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID - // 显式设置审计字段,防止自动填充机制失效 - deviceRequest.setCreateBy(SecurityUtils.getLoginUser().getUsername()); - deviceRequest.setCreateTime(new Date()); - deviceRequest.setTenantId(tenantId); // 设置租户id - deviceRequest.setCreateBy(currentUsername); // 设置创建人 - deviceRequest.setCreateTime(curDate); // 设置创建时间 - - // 🔧 Bug Fix: 签发时也需要设置关键字段(修复BUG #181) - // 保存时生成业务编号,签发时沿用已有编号 - if (is_save) { - deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4)); - } - deviceRequest.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - deviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号) - deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 - deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 - deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号 - - deviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型 - // 🔧 BugFix #498: categoryEnum=22(检查) 走 ServiceRequest,不走 DeviceRequest - // 检查申请单的诊疗定义ID存在 activityId,不在 adviceDefinitionId - // deviceDefId 对应耗材定义ID,不能用诊疗定义ID填充 - if (Integer.valueOf(22).equals(adviceSaveDto.getCategoryEnum())) { - log.info("handDevice skip - 检查申请单(categoryEnum=22) 走 ServiceRequest 路径,跳过 DeviceRequest 保存"); - continue; // 跳过本次循环,不走耗材请求路径 - } else if (adviceSaveDto.getAdviceDefinitionId() != null) { - deviceRequest.setDeviceDefId(adviceSaveDto.getAdviceDefinitionId());// 耗材定义id - } else { - log.warn("handDevice - deviceDefId 为空,adviceDefinitionId=null, categoryEnum={}", adviceSaveDto.getCategoryEnum()); - } - - deviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者 - deviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生 - deviceRequest.setOrgId(adviceSaveDto.getFounderOrgId());// 开方人科室 - deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间 - // 发放耗材房(若前端未传locationId,优先沿用已有DeviceRequest的performLocation,否则使用登录用户科室) - Long locId = adviceSaveDto.getLocationId(); - if (locId == null) { - // 尝试从已有DeviceRequest获取原始的performLocation - if (adviceSaveDto.getRequestId() != null) { - DeviceRequest existingDevice = iDeviceRequestService.getById(adviceSaveDto.getRequestId()); - if (existingDevice != null && existingDevice.getPerformLocation() != null) { - locId = existingDevice.getPerformLocation(); - log.info("耗材locationId为空,使用已有DeviceRequest的performLocation: locationId={}", locId); - } - } - // 如果已有记录也没有performLocation,则使用登录用户科室作为兜底 - if (locId == null) { - locId = SecurityUtils.getLoginUser().getOrgId(); - log.info("耗材locationId为空且无已有记录,使用登录用户科室作为默认值: locationId={}", locId); - } - } - deviceRequest.setPerformLocation(locId); - deviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - deviceRequest.setPackageId(adviceSaveDto.getPackageId()); // 组套id - // deviceRequest.setActivityId(adviceSaveDto.getActivityId()); - deviceRequest.setContentJson(adviceSaveDto.getContentJson()); // 请求内容json - deviceRequest.setYbClassEnum(adviceSaveDto.getYbClassEnum());// 类别医保编码 - deviceRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - deviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - - iDeviceRequestService.saveOrUpdate(deviceRequest); - if (is_save) { - // 处理耗材发放 - Long dispenseId = iDeviceDispenseService.handleDeviceDispense(deviceRequest, - adviceSaveDto.getDbOpType()); - - // 保存耗材费用项 - chargeItem = new ChargeItem(); - chargeItem.setId(adviceSaveDto.getChargeItemId()); // 费用项 id - chargeItem.setTenantId(tenantId); // 补全租户 ID - chargeItem.setCreateBy(currentUsername); // 补全创建人 - chargeItem.setCreateTime(curDate); // 补全创建时间 - chargeItem.setStatusEnum(2); // 已生成医嘱 - chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(deviceRequest.getBusNo())); - chargeItem.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - chargeItem.setPatientId(adviceSaveDto.getPatientId()); // 患者 - chargeItem.setContextEnum(adviceSaveDto.getAdviceType()); // 类型 - chargeItem.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - // 🔧 Bug Fix: 如果definitionId为空,使用adviceDefinitionId作为后备 - Long defId = adviceSaveDto.getDefinitionId(); - if (defId == null) { - defId = adviceSaveDto.getAdviceDefinitionId(); - } - chargeItem.setDefinitionId(defId); // 费用定价ID - chargeItem.setDefDetailId(adviceSaveDto.getDefinitionDetailId()); // 定价子表主键 - chargeItem.setEntererId(adviceSaveDto.getPractitionerId());// 开立人ID - chargeItem.setRequestingOrgId(orgId); // 开立科室 - chargeItem.setEnteredDate(curDate); // 开立时间 - chargeItem.setServiceTable(CommonConstants.TableName.WOR_DEVICE_REQUEST);// 医疗服务类型 - chargeItem.setServiceId(deviceRequest.getId()); // 医疗服务ID - chargeItem.setProductTable(adviceSaveDto.getAdviceTableName());// 产品所在表 - chargeItem.setProductId(adviceSaveDto.getAdviceDefinitionId());// 收费项id - - // 🔧 Bug Fix: 如果 definitionId 或 definitionDetailId 为 null,从定价信息中获取 - if (chargeItem.getDefinitionId() == null || chargeItem.getDefDetailId() == null) { - log.warn("耗材的 definitionId 或 definitionDetailId 为 null,尝试从定价信息中获取: deviceDefId={}", - adviceSaveDto.getAdviceDefinitionId()); - // 查询耗材定价信息 - log.warn("查询耗材定价信息: orgId={}, deviceDefId={}", orgId, adviceSaveDto.getAdviceDefinitionId()); - IPage devicePage = doctorStationAdviceAppMapper.getAdviceBaseInfo( - new Page<>(1, 1), - PublicationStatus.ACTIVE.getValue(), - orgId, - CommonConstants.TableName.ADM_DEVICE_DEFINITION, - null, - null, - null, - Arrays.asList(adviceSaveDto.getAdviceDefinitionId()), - null, - null, - null, - null); - if (devicePage != null && !devicePage.getRecords().isEmpty()) { - AdviceBaseDto deviceBaseInfo = devicePage.getRecords().get(0); - if (deviceBaseInfo.getPriceList() != null && !deviceBaseInfo.getPriceList().isEmpty()) { - AdvicePriceDto devicePrice = deviceBaseInfo.getPriceList().get(0); - if (chargeItem.getDefinitionId() == null) { - chargeItem.setDefinitionId(devicePrice.getDefinitionId()); - log.info("从定价信息中获取 definitionId: {}", devicePrice.getDefinitionId()); - } - if (chargeItem.getDefDetailId() == null) { - chargeItem.setDefDetailId(devicePrice.getDefinitionDetailId()); - log.info("从定价信息中获取 definitionDetailId: {}", devicePrice.getDefinitionDetailId()); - } - } - } - } - - // 如果definitionId为null,使用前端传入的价格信息 - if (chargeItem.getDefinitionId() == null) { - log.warn("无法获取耗材的 definitionId,使用前端传入的价格: deviceDefId={}", adviceSaveDto.getAdviceDefinitionId()); - // 不抛异常,使用前端传入的unitPrice和totalPrice - } - - // 🔧 Bug Fix: 如果accountId为null,从就诊中获取账户ID,如果没有则自动创建 - Long accountId = adviceSaveDto.getAccountId(); - if (accountId == null) { - // 尝试从患者就诊中获取默认账户ID(自费账户) - Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId()); - if (selfAccount != null) { - accountId = selfAccount.getId(); - } else { - // 自动创建自费账户 - Account newAccount = new Account(); - newAccount.setPatientId(adviceSaveDto.getPatientId()); - newAccount.setEncounterId(adviceSaveDto.getEncounterId()); - newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); - newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode()); - newAccount.setBalanceAmount(BigDecimal.ZERO); - newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue()); - newAccount.setEncounterFlag(Whether.YES.getValue()); - // 🔧 Bug Fix: 设置账户名称,避免数据库NOT NULL约束错误 - newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo()); - accountId = iAccountService.saveAccountByRegister(newAccount); - } - } - chargeItem.setAccountId(accountId);// 关联账户ID - chargeItem.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - chargeItem.setDispenseId(dispenseId); // 发放ID - - chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量 - chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位 - // #415 价格非负验证 - BigDecimal unitPrice = adviceSaveDto.getUnitPrice(); - if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) { - unitPrice = unitPrice.abs(); // 负数取绝对值 - } - chargeItem.setUnitPrice(unitPrice); // 单价 - chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价 - - // 显式设置审计字段,防止自动填充机制失效 - chargeItem.setTenantId(SecurityUtils.getLoginUser().getTenantId()); - chargeItem.setCreateBy(SecurityUtils.getLoginUser().getUsername()); - chargeItem.setCreateTime(new Date()); - - iChargeItemService.saveOrUpdate(chargeItem); - - // 显式更新前端传的chargeItemId对应的收费项目状态为2(已生成医嘱) - if (adviceSaveDto.getChargeItemId() != null) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(ChargeItem::getId, adviceSaveDto.getChargeItemId()) - .set(ChargeItem::getStatusEnum, 2); - iChargeItemService.update(updateWrapper); - log.info("已更新耗材收费项目状态为已生成医嘱,chargeItemId:{}", adviceSaveDto.getChargeItemId()); - } - } - } - } - - /** - * 从诊疗医嘱 contentJson 中解析检验申请单号(检验保存时写入形如 {"applyNo":"..."})。 - */ - private String extractInspectionApplyNoFromContentJson(String contentJson) { - if (StringUtils.isBlank(contentJson) || !contentJson.contains("applyNo")) { - return null; - } - Matcher m = INSPECTION_APPLY_NO_JSON.matcher(contentJson); - if (!m.find()) { - return null; - } - String applyNo = m.group(1).trim(); - return StringUtils.isBlank(applyNo) ? null : applyNo; - } - - - /** - * 处理诊疗 - */ - private void handService(List activityList, Date curDate, String adviceOpType, Long organizationId, - String signCode) { - // 当前登录账号的科室id - Long orgId = SecurityUtils.getLoginUser().getOrgId(); - // 获取当前登录用户的tenantId - Integer tenantId = SecurityUtils.getLoginUser().getTenantId(); - // 获取当前登录用户名 - String currentUsername = SecurityUtils.getUsername(); - // 保存操作 - boolean is_save = AdviceOpType.SAVE_ADVICE.getCode().equals(adviceOpType); - // 签发操作 - boolean is_sign = AdviceOpType.SIGN_ADVICE.getCode().equals(adviceOpType); - ServiceRequest serviceRequest; - // 声明费用项 - ChargeItem chargeItem; - // 新增 + 修改 - // 🔧 BugFix: 如果 requestId 不为空说明是已存在的医嘱,需要更新,即使 dbOpType 不匹配也应该包含进来 - // 🔧 BugFix #454: 排除删除操作,避免误入insertOrUpdateList触发执行科室校验 - List insertOrUpdateList = activityList.stream() - .filter(e -> !DbOpType.DELETE.getCode().equals(e.getDbOpType())) - .filter(e -> (DbOpType.INSERT.getCode().equals(e.getDbOpType()) - || DbOpType.UPDATE.getCode().equals(e.getDbOpType()) - || e.getRequestId() != null)) - .collect(Collectors.toList()); - // 删除 - List deleteList = activityList.stream() - .filter(e -> DbOpType.DELETE.getCode().equals(e.getDbOpType())).collect(Collectors.toList()); - // 校验删除的医嘱是否已经收费 - List delRequestIdList = deleteList.stream().map(AdviceSaveDto::getRequestId).collect(Collectors.toList()); - if (!delRequestIdList.isEmpty()) { - List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(delRequestIdList); - // 🔧 BugFix#219: 过滤只保留诊疗类型的费用项 - if (chargeItemList != null && !chargeItemList.isEmpty()) { - chargeItemList = chargeItemList.stream() - .filter(ci -> CommonConstants.TableName.WOR_SERVICE_REQUEST.equals(ci.getServiceTable())) - .collect(Collectors.toList()); - } - if (chargeItemList != null && !chargeItemList.isEmpty()) { - for (ChargeItem ci : chargeItemList) { - if (ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum())) { - throw new ServiceException("已收费的项目无法删除,请刷新页面后重试"); - } - } - } - } - // 🔧 级联作废:在删除 ServiceRequest 之前,先读取所有待删除记录的级联信息 - // 检验申请单:contentJson 中写入 applyNo;手术申请单:categoryEnum=24 + prescriptionNo - Map> labApplyNoToRequestIds = new LinkedHashMap<>(); - Map> surgeryPrescriptionNoToRequestIds = new LinkedHashMap<>(); - // 收集待删除的 ServiceRequest(先查询再删除,避免级联逻辑因记录已删除而失效) - Map serviceRequestCache = new LinkedHashMap<>(); - for (AdviceSaveDto adviceSaveDto : deleteList) { - Long requestId = adviceSaveDto.getRequestId(); - // 🔧 Bug #442: 跳过 requestId 为 null 的记录 - if (requestId == null) { - log.warn("BugFix#442: handService - 跳过 requestId 为 null 的删除请求"); - continue; - } - ServiceRequest existing = iServiceRequestService.getById(requestId); - if (existing == null) { - continue; - } - serviceRequestCache.put(requestId, existing); - log.info("【调试】handService 待删除医嘱: requestId={}, categoryEnum={}, prescriptionNo={}", - requestId, existing.getCategoryEnum(), existing.getPrescriptionNo()); - // 检验申请单级联 - String applyNo = extractInspectionApplyNoFromContentJson(existing.getContentJson()); - if (StringUtils.isNotBlank(applyNo)) { - labApplyNoToRequestIds.computeIfAbsent(applyNo, k -> new ArrayList<>()) - .add(requestId); - } - // 手术申请单级联(categoryEnum=24) - log.info("【调试】handService 判断手术条件: categoryEnum={}, prescriptionNo={}, isSurgery={}", - existing.getCategoryEnum(), existing.getPrescriptionNo(), - existing.getCategoryEnum() != null && existing.getCategoryEnum() == 24 && StringUtils.isNotBlank(existing.getPrescriptionNo())); - if (existing.getCategoryEnum() != null - && existing.getCategoryEnum() == 24 - && StringUtils.isNotBlank(existing.getPrescriptionNo())) { - surgeryPrescriptionNoToRequestIds.computeIfAbsent(existing.getPrescriptionNo(), k -> new ArrayList<>()) - .add(requestId); - log.info("【调试】handService 加入手术级联列表: prescriptionNo={}", existing.getPrescriptionNo()); - } - } - // 执行检验申请单级联作废 - Set cascadeSkippedRequestIds = new HashSet<>(); - for (Map.Entry> e : labApplyNoToRequestIds.entrySet()) { - R delLab = iDoctorStationInspectionLabApplyService.deleteInspectionLabApply(e.getKey()); - if (delLab != null && R.isSuccess(delLab)) { - cascadeSkippedRequestIds.addAll(e.getValue()); - log.info("handService - 级联作废检验申请单 applyNo={},已跳过重复删除的医嘱 requestIds={}", - e.getKey(), e.getValue()); - } else { - String msg = delLab != null && StringUtils.isNotEmpty(delLab.getMsg()) ? delLab.getMsg() : "删除检验申请单失败"; - log.warn("handService - 级联作废检验申请单未成功 applyNo={} msg={},将回退为仅删除当前医嘱记录", - e.getKey(), msg); - } - } - // 🔧 手术申请单级联作废:删除手术医嘱时同步作废关联的手术申请单 - for (Map.Entry> e : surgeryPrescriptionNoToRequestIds.entrySet()) { - String prescriptionNo = e.getKey(); - try { - List requestForms = iRequestFormService.list( - new LambdaQueryWrapper() - .eq(RequestForm::getPrescriptionNo, prescriptionNo) - .in(RequestForm::getTypeCode, ActivityDefCategory.PROCEDURE.getCode().toString(), "SURGERY") - .and(w -> w.isNull(RequestForm::getDeleteFlag).or().eq(RequestForm::getDeleteFlag, "0"))); - log.info("【调试】handService 查询手术申请单: prescriptionNo={}, 查到{}条", prescriptionNo, requestForms != null ? requestForms.size() : 0); - if (requestForms != null && !requestForms.isEmpty()) { - for (RequestForm requestForm : requestForms) { - iRequestFormService.removeById(requestForm.getId()); - } - // 同步删除 cli_surgery 手术记录(prescriptionNo = surgeryNo) - Surgery surgery = iSurgeryService.getOne( - new LambdaQueryWrapper() - .eq(Surgery::getSurgeryNo, prescriptionNo) - .and(w -> w.isNull(Surgery::getDeleteFlag).or().eq(Surgery::getDeleteFlag, "0"))); - if (surgery != null) { - iSurgeryService.removeById(surgery.getId()); - log.info("handService - 级联删除手术记录 cli_surgery: surgeryNo={}, id={}", prescriptionNo, surgery.getId()); - } - cascadeSkippedRequestIds.addAll(e.getValue()); - log.info("handService - 级联作废手术申请单 prescriptionNo={}", prescriptionNo); - } else { - log.info("handService - 未找到手术申请单 prescriptionNo={}", prescriptionNo); - } - } catch (Exception ex) { - log.warn("handService - 级联作废手术申请单失败 prescriptionNo={} msg={}", prescriptionNo, ex.getMessage()); - } - } - // 级联作废完成后,统一删除 ServiceRequest 及其子项、费用项 - for (AdviceSaveDto adviceSaveDto : deleteList) { - if (cascadeSkippedRequestIds.contains(adviceSaveDto.getRequestId())) { - continue; - } - Long requestId = adviceSaveDto.getRequestId(); - iServiceRequestService.removeById(requestId);// 删除诊疗 - iServiceRequestService.remove( - new LambdaQueryWrapper().eq(ServiceRequest::getParentId, - requestId));// 删除诊疗套餐对应的子项 - // 🔧 Bug Fix #219: 删除费用项 - String serviceTable = CommonConstants.TableName.WOR_SERVICE_REQUEST; - iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId); - log.info("BugFix#219: 诊疗医嘱删除完成, requestId={}, serviceTable={}", requestId, serviceTable); - } - - for (AdviceSaveDto adviceSaveDto : insertOrUpdateList) { - // 🔧 Bug Fix: 确保accountId有效(不为null且账户存在) - boolean needNewAccount = false; - if (adviceSaveDto.getAccountId() == null) { - needNewAccount = true; - } else { - // 验证账户是否存在且有效(未被删除,租户匹配) - Account existingAccount = iAccountService.getById(adviceSaveDto.getAccountId()); - if (existingAccount == null) { - log.warn("handService - 前端传入的accountId无效(账户不存在),accountId={},将重新获取或创建账户", - adviceSaveDto.getAccountId()); - needNewAccount = true; - } - } - - if (needNewAccount) { - // 尝试从患者就诊中获取默认账户ID(自费账户) - Account selfAccount = iAccountService.getSelfAccount(adviceSaveDto.getEncounterId()); - if (selfAccount != null) { - adviceSaveDto.setAccountId(selfAccount.getId()); - log.info("handService - 使用现有自费账户,accountId={}", selfAccount.getId()); - } else { - // 自动创建自费账户 - Account newAccount = new Account(); - newAccount.setPatientId(adviceSaveDto.getPatientId()); - newAccount.setEncounterId(adviceSaveDto.getEncounterId()); - newAccount.setContractNo(CommonConstants.BusinessName.DEFAULT_CONTRACT_NO); - newAccount.setTypeCode(AccountType.PERSONAL_CASH_ACCOUNT.getCode()); - newAccount.setBalanceAmount(BigDecimal.ZERO); - newAccount.setStatusEnum(AccountStatus.ACTIVE.getValue()); - newAccount.setEncounterFlag(Whether.YES.getValue()); - newAccount.setName(AccountType.PERSONAL_CASH_ACCOUNT.getInfo()); - Long newAccountId = iAccountService.saveAccountByRegister(newAccount); - adviceSaveDto.setAccountId(newAccountId); - log.info("handService - 自动创建自费账户,newAccountId={}", newAccountId); - } - } - - // 🔧 Bug Fix: 确保practitionerId不为null - if (adviceSaveDto.getPractitionerId() == null) { - adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId()); - log.info("handService - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId()); - } - - // 🔧 Bug Fix: 确保founderOrgId不为null - if (adviceSaveDto.getFounderOrgId() == null) { - adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId()); - log.info("handService - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId()); - } - - // 🔧 Bug Fix #238/#454: 诊疗项目执行科室非空校验(删除操作跳过校验) - if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3 - && !DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) { - Long effectiveOrgId = adviceSaveDto.getEffectiveOrgId(); - if (effectiveOrgId == null) { - throw new ServiceException("诊疗项目必须选择执行科室"); - } - } - serviceRequest = new ServiceRequest(); - serviceRequest.setId(adviceSaveDto.getRequestId()); // 主键id - serviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue());// 请求状态 - serviceRequest.setTenantId(tenantId); // 设置租户id - serviceRequest.setCreateBy(currentUsername); // 设置创建人 - serviceRequest.setCreateTime(curDate); // 设置创建时间 - serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID - if (is_sign) { - serviceRequest.setSignCode(signCode); - } - // 🔧 Bug Fix: 签发时也需要设置关键字段(修复BUG #181) - // 保存时生成业务编号,签发时沿用已有编号 - if (is_save) { - serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4)); - } - serviceRequest.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - serviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号) - serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量 - serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码 - - // 🎯 判断是否为会诊医嘱:如果categoryEnum为31,则为会诊类型 - Integer categoryEnum = adviceSaveDto.getCategoryEnum(); - if (categoryEnum != null && categoryEnum == 31) { - // 会诊医嘱:category_enum设置为31 - serviceRequest.setCategoryEnum(31); - log.info("保存会诊医嘱,category_enum=31"); - } else { - // 普通诊疗医嘱 - serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); - } - - // 🔧 BugFix#385: 检查类型(adviceType=2)不走定价体系,activityId设置为0L占位 - // 与ExamApplyController保持一致,数据库有NOT NULL约束 - if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 2) { - serviceRequest.setActivityId(0L); - } else { - serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id - } - serviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者 - serviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生 - serviceRequest.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - serviceRequest.setAuthoredTime(curDate); // 请求签发时间 - // 执行科室 - 使用兼容方法获取执行科室ID - serviceRequest.setOrgId(adviceSaveDto.getEffectiveOrgId()); - serviceRequest.setContentJson(adviceSaveDto.getContentJson()); // 请求内容json - serviceRequest.setYbClassEnum(adviceSaveDto.getYbClassEnum());// 类别医保编码 - serviceRequest.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - serviceRequest.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - - // 基于皮试药品生成的皮试检查 - if (adviceSaveDto.getBasedOnId() != null) { - serviceRequest.setBasedOnId(adviceSaveDto.getBasedOnId()); - serviceRequest.setBasedOnTable(CommonConstants.TableName.MED_MEDICATION_REQUEST); - } - - iServiceRequestService.saveOrUpdate(serviceRequest); - - // 保存时保存诊疗费用项 - if (is_save) { - chargeItem = new ChargeItem(); - chargeItem.setId(adviceSaveDto.getChargeItemId()); // 费用项id - chargeItem.setTenantId(tenantId); // 补全租户ID - chargeItem.setCreateBy(currentUsername); // 补全创建人 - chargeItem.setCreateTime(curDate); // 补全创建时间 - chargeItem.setStatusEnum(2); // 已生成医嘱 - chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(serviceRequest.getBusNo())); - chargeItem.setGenerateSourceEnum(adviceSaveDto.getGenerateSourceEnum() != null - ? adviceSaveDto.getGenerateSourceEnum() - : GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源 - chargeItem.setPatientId(adviceSaveDto.getPatientId()); // 患者 - chargeItem.setContextEnum(adviceSaveDto.getAdviceType()); // 类型 - chargeItem.setEncounterId(adviceSaveDto.getEncounterId()); // 就诊id - // 🔧 Bug Fix: 如果definitionId为空,使用adviceDefinitionId作为后备 - Long defId3 = adviceSaveDto.getDefinitionId(); - if (defId3 == null) { - defId3 = adviceSaveDto.getAdviceDefinitionId(); - } - chargeItem.setDefinitionId(defId3); // 费用定价ID - chargeItem.setDefDetailId(adviceSaveDto.getDefinitionDetailId()); // 定价子表主键 - chargeItem.setEntererId(adviceSaveDto.getPractitionerId());// 开立人ID - chargeItem.setEnteredDate(curDate); // 开立时间 - chargeItem.setServiceTable(CommonConstants.TableName.WOR_SERVICE_REQUEST);// 医疗服务类型 - chargeItem.setServiceId(serviceRequest.getId()); // 医疗服务ID - chargeItem.setProductTable(adviceSaveDto.getAdviceTableName());// 产品所在表 - chargeItem.setProductId(adviceSaveDto.getAdviceDefinitionId());// 收费项id - chargeItem.setAccountId(adviceSaveDto.getAccountId());// 关联账户ID - chargeItem.setRequestingOrgId(orgId); // 开立科室 - chargeItem.setConditionId(adviceSaveDto.getConditionId()); // 诊断id - chargeItem.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); // 就诊诊断id - chargeItem.setQuantityValue(adviceSaveDto.getQuantity()); // 数量 - chargeItem.setQuantityUnit(adviceSaveDto.getUnitCode()); // 单位 - // #415 价格非负验证 - BigDecimal unitPrice = adviceSaveDto.getUnitPrice(); - if (unitPrice != null && unitPrice.compareTo(BigDecimal.ZERO) < 0) { - unitPrice = unitPrice.abs(); // 负数取绝对值 - } - chargeItem.setUnitPrice(unitPrice); // 单价 - chargeItem.setTotalPrice(adviceSaveDto.getTotalPrice()); // 总价 - - iChargeItemService.saveOrUpdate(chargeItem); - - // 显式更新前端传的chargeItemId对应的收费项目状态为2(已生成医嘱) - if (adviceSaveDto.getChargeItemId() != null) { - LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); - updateWrapper.eq(ChargeItem::getId, adviceSaveDto.getChargeItemId()) - .set(ChargeItem::getStatusEnum, 2); - iChargeItemService.update(updateWrapper); - log.info("已更新诊疗收费项目状态为已生成医嘱,chargeItemId:{}", adviceSaveDto.getChargeItemId()); - } - - // 第一次保存时,处理诊疗套餐的子项信息 - if (adviceSaveDto.getRequestId() == null) { - ActivityDefinition activityDefinition - = iActivityDefinitionService.getById(adviceSaveDto.getAdviceDefinitionId()); - if (activityDefinition == null) { - continue; - } - String childrenJson = activityDefinition.getChildrenJson(); - if (childrenJson != null) { - // 诊疗子项参数类 - ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams(); - activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue()); // 治疗类型 - activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者 - activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊id - activityChildrenJsonParams.setAccountId(chargeItem.getAccountId()); // 账户id - activityChildrenJsonParams.setChargeItemId(chargeItem.getId()); // 费用项id - activityChildrenJsonParams.setParentId(serviceRequest.getId());// 子项诊疗的父id - activityChildrenJsonParams.setEncounterDiagnosisId(adviceSaveDto.getEncounterDiagnosisId()); - adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams); - } - } - } - - // 只有在签发时 - if (is_sign) { - // 发送跨系统申请 - 已注释,项目未使用LIS/PACS系统 - // adviceSaveDto.setRequestId(serviceRequest.getId()); - // try { - // // 查询诊疗定义 - // ActivityDefinition activityDefinition - // = iActivityDefinitionService.getById(adviceSaveDto.getAdviceDefinitionId()); - // if (activityDefinition != null) { - // // 检验 或 检查 - // if (ActivityType.PROOF.getValue().equals(activityDefinition.getTypeEnum()) - // || ActivityType.TEST.getValue().equals(activityDefinition.getTypeEnum())) { - // doctorStationSendApplyUtil.sendCrossSystemApply(adviceSaveDto, organizationId, curDate); - // } - // } - // } catch (Exception e) { - // if (!Whether.YES.getCode() - // .equals(TenantOptionUtil.getOptionContent(TenantOptionDict.LIS_PACS_ERROR_IGNORE))) { - // throw e; - // } - // log.error(e.getMessage(), e); - // } - // 🔧 BugFix#328: 签发时将收费项目状态从草稿改为待收费 - // 修复检验申请单生成的医嘱签发失败问题 - Long chargeItemId = adviceSaveDto.getChargeItemId(); - ChargeItem existingChargeItem = null; - - // 方式1:通过chargeItemId直接查询 - if (chargeItemId != null) { - existingChargeItem = iChargeItemService.getById(chargeItemId); - } - - // 方式2:如果chargeItemId为null,通过requestId(serviceId)查询费用项 - // 检验申请单创建的医嘱可能没有传递chargeItemId,需要通过serviceId查找 - if (existingChargeItem == null && adviceSaveDto.getRequestId() != null) { - existingChargeItem = iChargeItemService.getOne( - new LambdaQueryWrapper() - .eq(ChargeItem::getServiceId, adviceSaveDto.getRequestId()) - .eq(ChargeItem::getServiceTable, CommonConstants.TableName.WOR_SERVICE_REQUEST) - .eq(ChargeItem::getDeleteFlag, DelFlag.NO.getCode()) - ); - log.info("BugFix#328: 通过requestId查询费用项,requestId={}, chargeItem={}", - adviceSaveDto.getRequestId(), existingChargeItem != null ? existingChargeItem.getId() : "null"); - } - - // 更新费用项状态 - if (existingChargeItem != null) { - existingChargeItem.setStatusEnum(ChargeItemStatus.PLANNED.getValue()); - iChargeItemService.updateById(existingChargeItem); - log.info("BugFix#328: 更新费用项状态为待收费,chargeItemId={}, status={}", - existingChargeItem.getId(), ChargeItemStatus.PLANNED.getValue()); - } else { - log.warn("BugFix#328: 未找到对应的费用项,无法更新状态,requestId={}, chargeItemId={}", - adviceSaveDto.getRequestId(), chargeItemId); - } - } - } - } - - /** - * 查询医嘱请求数据 - * - * @param encounterId 就诊id - * @return 医嘱请求数据 - */ - @Override - public R getRequestBaseInfo(Long encounterId) { - return this.getRequestBaseInfo(encounterId, null, null); - } - - @Override - public R getRequestBaseInfo(Long encounterId, Integer generateSourceEnum, String sourceBillNo) { - // 当前账号的参与者id - Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); - // 未指定generateSourceEnum时,默认只查询医生开立的医嘱 - int sourceEnum = (generateSourceEnum != null) ? generateSourceEnum : GenerateSource.DOCTOR_PRESCRIPTION.getValue(); - // 医嘱请求数据 - List requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, null, - CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST, - CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(), - sourceEnum, sourceBillNo); - for (RequestBaseDto requestBaseDto : requestBaseInfo) { - // 请求状态 - requestBaseDto - .setStatusEnum_enumText(EnumUtils.getInfoByValue(RequestStatus.class, requestBaseDto.getStatusEnum())); - // 是否皮试 - requestBaseDto - .setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, requestBaseDto.getSkinTestFlag())); - // 是否为注射药物 - requestBaseDto - .setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, requestBaseDto.getInjectFlag())); - // 收费状态 - requestBaseDto.setChargeStatus_enumText( - EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus())); - // 单位字典翻译失败时回退使用原始值(如手术申请硬编码了中文单位名) - if (StringUtils.isNotBlank(requestBaseDto.getUnitCode()) && StringUtils.isBlank(requestBaseDto.getUnitCode_dictText())) { - requestBaseDto.setUnitCode_dictText(requestBaseDto.getUnitCode()); - } - } - return R.ok(requestBaseInfo); - } - - /** - * 门诊签退医嘱 - * - * @param requestIdList 请求id列表 - * @return 结果 - */ - @Override - public R signOffAdvice(List requestIdList) { - log.info("BugFix#219: signOffAdvice - requestIdList={}", requestIdList); - - // 🔧 BugFix: 直接对所有requestId进行作废操作,不再先查询分类 - // 药品、耗材、诊疗请求都尝试作废,只有存在的才会被更新 - - // 根据请求编号列表查询收费项目信息(用于检查是否已收费) - List chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIdList); - - if (chargeItemList != null && !chargeItemList.isEmpty()) { - for (ChargeItem chargeItem : chargeItemList) { - if (ChargeItemStatus.BILLED.getValue().equals(chargeItem.getStatusEnum())) { - throw new ServiceException("已收费的项目无法签退,请刷新页面后重试"); - } - } - - List chargeItemIdList = chargeItemList.stream().map(ChargeItem::getId).collect(Collectors.toList()); - // 根据id更新收费项目状态 - iChargeItemService.updatePaymentStatus(chargeItemIdList, ChargeItemStatus.DRAFT.getValue()); - } - - // 🔧 新增:签退时回滚库存 - // 查询已发放的药品记录(用于回滚库存) - List dispensedList = iMedicationDispenseService.list( - new LambdaQueryWrapper() - .in(MedicationDispense::getMedReqId, requestIdList) - .eq(MedicationDispense::getStatusEnum, DispenseStatus.COMPLETED.getValue()) - ); - - if (dispensedList != null && !dispensedList.isEmpty()) { - // 需要回滚的库存列表 - List inventoryUpdateList = new ArrayList<>(); - - for (MedicationDispense dispense : dispensedList) { - // 查询对应的库存记录(根据批号和药品ID) - if (dispense.getMedicationId() != null && dispense.getLotNumber() != null) { - InventoryItem inventoryItem = inventoryItemService.getOne( - new LambdaQueryWrapper() - .eq(InventoryItem::getItemId, dispense.getMedicationId()) - .eq(InventoryItem::getLotNumber, dispense.getLotNumber()) - ); - - if (inventoryItem != null) { - // 计算回滚后的数量(加上已发放的数量) - BigDecimal currentQuantity = inventoryItem.getQuantity() != null ? inventoryItem.getQuantity() : BigDecimal.ZERO; - BigDecimal dispenseQuantity = dispense.getQuantity() != null ? dispense.getQuantity() : BigDecimal.ZERO; - inventoryUpdateList.add(new InventoryItem() - .setId(inventoryItem.getId()) - .setQuantity(currentQuantity.add(dispenseQuantity)) - ); - } - } - - // 更新发药记录状态为已退药 - dispense.setStatusEnum(DispenseStatus.RETURNED.getValue()); - } - - // 批量更新库存(回滚数量) - if (!inventoryUpdateList.isEmpty()) { - inventoryItemService.updateBatchById(inventoryUpdateList); - } - - // 更新发药记录状态 - iMedicationDispenseService.updateBatchById(dispensedList); - } - - // 🔧 BugFix: 直接对所有requestId进行签退操作,将状态改为待签发 - log.info("BugFix: signOffAdvice - 签退所有请求,状态改为待签发, requestIdList={}", requestIdList); - - // 尝试签退药品请求(只有存在的才会更新) - iMedicationRequestService.updateDraftStatusBatch(requestIdList, null, null); - // 尝试签退耗材请求(只有存在的才会更新) - iDeviceRequestService.updateDraftStatusBatch(requestIdList); - // 尝试签退诊疗请求(只有存在的才会更新) - iServiceRequestService.updateDraftStatusBatch(requestIdList); - - log.info("BugFix#219: signOffAdvice - 所有请求作废完成"); - - return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00004, null)); - } - - /** - * 查询历史医嘱请求数据 - * - * @param patientId 病人id - * @param encounterId 就诊id - * @return 历史医嘱请求数据 - */ - @Override - public R getRequestHistoryInfo(Long patientId, Long encounterId) { - // 当前账号的参与者id - Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId(); - // 医嘱请求数据 - List requestBaseInfo = doctorStationAdviceAppMapper.getRequestBaseInfo(encounterId, patientId, - CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST, - CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.YES.getCode(), - GenerateSource.DOCTOR_PRESCRIPTION.getValue(), null); - for (RequestBaseDto requestBaseDto : requestBaseInfo) { - // 请求状态 - requestBaseDto - .setStatusEnum_enumText(EnumUtils.getInfoByValue(RequestStatus.class, requestBaseDto.getStatusEnum())); - // 是否皮试 - requestBaseDto - .setSkinTestFlag_enumText(EnumUtils.getInfoByValue(Whether.class, requestBaseDto.getSkinTestFlag())); - // 是否为注射药物 - requestBaseDto - .setInjectFlag_enumText(EnumUtils.getInfoByValue(Whether.class, requestBaseDto.getInjectFlag())); - // 收费状态 - requestBaseDto.setChargeStatus_enumText( - EnumUtils.getInfoByValue(ChargeItemStatus.class, requestBaseDto.getChargeStatus())); - } - return R.ok(requestBaseInfo); - } - - /** - * 更新组号 - * - * @param updateGroupIdParam 更新组号参数 - * @return 结果 - */ - @Override - public void updateGroupId(UpdateGroupIdParam updateGroupIdParam) { - List groupList = updateGroupIdParam.getGroupList(); - List idsToSetNull = groupList.stream().filter(dto -> dto.getGroupId() == null) - .map(UpdateGroupDto::getRequestId).collect(Collectors.toList()); - - if (!idsToSetNull.isEmpty()) { - // 创建更新条件 - UpdateWrapper updateWrapper = new UpdateWrapper<>(); - updateWrapper.set("group_id", null).in("id", idsToSetNull); - - // 执行更新 - iMedicationRequestService.update(updateWrapper); - } - - // 处理非null的情况 - List medicationRequestList = groupList.stream().filter(dto -> dto.getGroupId() != null) - .map(dto -> new MedicationRequest().setId(dto.getRequestId()).setGroupId(dto.getGroupId())) - .collect(Collectors.toList()); - - if (!medicationRequestList.isEmpty()) { - iMedicationRequestService.saveOrUpdateBatch(medicationRequestList); - } - } - - /** - * 查询就诊费用性质 - * - * @param encounterId 就诊id - * @return 就诊费用性质 - */ - @Override - public R getEncounterContract(Long encounterId) { - return R.ok(doctorStationAdviceAppMapper.getEncounterContract(encounterId)); - } - - /** - * 查询检验检查开立历史(近30天) - * - * @param patientId 患者id - * @param adviceDefinitionId 医嘱定义id - * @return 检验检查开立历史 - */ - @Override - public R getProofAndTestHistory(Long patientId, Long adviceDefinitionId) { - return R.ok(doctorStationAdviceAppMapper.getProofAndTestHistory(patientId, adviceDefinitionId, - RequestStatus.COMPLETED.getValue())); - } - - /** - * 查询检验url相关参数 - * - * @param encounterId 就诊id - * @return 检验url相关参数 - */ - @Override - public R getProofResult(Long encounterId) { - // 检查参数 - if (encounterId == null) { - log.warn("获取检验结果时就诊ID为空"); - return R.ok(new ArrayList<>()); - } - - // LIS查看报告地址 - String lisReportUrl = TenantOptionUtil.getOptionContent(TenantOptionDict.LIS_REPORT_URL); - if (StringUtils.isEmpty(lisReportUrl)) { - log.warn("租户配置项【LIS查看报告地址】未配置"); - } - List proofResult = doctorStationAdviceAppMapper.getProofAndTestResult(encounterId, - RequestStatus.DRAFT.getValue(), ActivityType.PROOF.getValue()); - for (ProofAndTestResultDto proofAndTestResultDto : proofResult) { - if (StringUtils.isNotEmpty(lisReportUrl)) { - proofAndTestResultDto.setRequestUrl(lisReportUrl.concat(proofAndTestResultDto.getBusNo())); - } - } - - return R.ok(proofResult); - } - - /** - * 查询检查url相关参数 - * - * @param encounterId 就诊id - * @return 检查url相关参数 - */ - @Override - public R getTestResult(Long encounterId) { - // 检查参数 - if (encounterId == null) { - log.warn("获取检查结果时就诊ID为空"); - return R.ok(new ArrayList<>()); - } - - // PACS查看报告地址 - String pacsReportUrl = TenantOptionUtil.getOptionContent(TenantOptionDict.PACS_REPORT_URL); - if (StringUtils.isEmpty(pacsReportUrl)) { - log.warn("租户配置项【PACS查看报告地址】未配置"); - } - List testResult = doctorStationAdviceAppMapper.getProofAndTestResult(encounterId, - RequestStatus.DRAFT.getValue(), ActivityType.TEST.getValue()); - for (ProofAndTestResultDto proofAndTestResultDto : testResult) { - if (StringUtils.isNotEmpty(pacsReportUrl)) { - proofAndTestResultDto.setRequestUrl(pacsReportUrl.concat(proofAndTestResultDto.getBusNo())); - } - } - return R.ok(testResult); - } - - /** - * 根据searchKey对医嘱列表进行排序:adviceName以searchKey(汉字)开头的排在前面 | - * pyStr以searchKey(字母)开头的排在前面 - * - * @param adviceList 医嘱列表 - * @param searchKey 搜索关键字 - */ - private void sortAdviceListBySearchKey(List adviceList, String searchKey) { - if (adviceList == null || adviceList.isEmpty() || searchKey == null || searchKey.trim().isEmpty()) { - return; - } - - // 判断searchKey是否包含字母(拼音) - boolean isPinyinSearch = isPinyinKey(searchKey); - - adviceList.sort((a, b) -> { - // 根据searchKey类型选择不同的字段 - String aValue - = isPinyinSearch ? (a.getPyStr() != null ? a.getPyStr() : a.getAdviceName()) : a.getAdviceName(); - String bValue - = isPinyinSearch ? (b.getPyStr() != null ? b.getPyStr() : b.getAdviceName()) : b.getAdviceName(); - - // 处理可能的null值 - if (aValue == null && bValue == null) { - return 0; - } - if (aValue == null) { - return 1; - } - if (bValue == null) { - return -1; - } - - // 判断两个值是否以searchKey开头 - boolean aStartsWith = aValue.toLowerCase().startsWith(searchKey.toLowerCase()); - boolean bStartsWith = bValue.toLowerCase().startsWith(searchKey.toLowerCase()); - - // 如果a以searchKey开头,而b不是,a排在前面 - if (aStartsWith && !bStartsWith) { - return -1; - } - // 如果b以searchKey开头,而a不是,b排在前面 - if (bStartsWith && !aStartsWith) { - return 1; - } - // 如果两者都以searchKey开头或都不以searchKey开头,保持原有顺序 - return 0; - }); - } - - /** - * 判断searchKey是否为拼音(是否只包含字母) - */ - private boolean isPinyinKey(String searchKey) { - if (searchKey == null || searchKey.trim().isEmpty()) { - return false; - } - // 判断字符串是否只包含字母(拼音通常只包含字母) - return searchKey.matches("[a-zA-Z]+"); - } - - /** - * 获取当前科室已配置的药品类别列表 - * - * @param organizationId 科室id - * @return 已配置的药品类别编码列表 - */ - @Override - public R getConfiguredCategories(Long organizationId) { - // 查询取药科室配置 - List medLocationConfig = doctorStationAdviceAppMapper.getMedLocationConfig(organizationId); - // 提取不重复的 categoryCode - List categoryCodes = medLocationConfig.stream() - .map(AdviceInventoryDto::getCategoryCode) - .filter(code -> code != null && !code.isEmpty()) - .distinct() - .collect(Collectors.toList()); - return R.ok(categoryCodes); - } - - /** - * 手术项目专用分页查询(仅手术 + 定价,无库存/草稿库存/取药科室等无关逻辑) - * 使用直接 LIMIT 查询替代 MyBatis Plus 分页,避免 COUNT 全表扫描开销 - */ - @Override - public IPage getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey) { - log.info("getSurgeryPage 开始: orgId={}, page={}/{}, searchKey={}", organizationId, pageNo, pageSize, searchKey); - long start = System.currentTimeMillis(); - - // 无搜索时尝试从 Redis 缓存读取(手术项目变更频率低,适合缓存) - String safeOrgId = organizationId != null ? organizationId.toString() : ""; - String cacheKey = "surgery:page:" + safeOrgId + ":" + pageNo + ":" + pageSize; - boolean useCache = (searchKey == null || searchKey.trim().isEmpty()); - - if (useCache) { - Object cachedObj = redisCache.getCacheObject(cacheKey); - if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { - log.info("从 Redis 缓存获取手术项目, key: {}, records: {}", cacheKey, - ((IPage) cachedObj).getRecords().size()); - return (IPage) cachedObj; - } - } - - // 使用 MyBatis Plus 分页查询 - IPage result = doctorStationAdviceAppMapper.getSurgeryPage( - new Page<>(pageNo, pageSize), - PublicationStatus.ACTIVE.getValue(), - organizationId, - searchKey); - - log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size()); - - // 无搜索时将结果写入缓存 - if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { - redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS); - log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS); - } - - return result; - } - - @Override - public IPage getExaminationPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey, String categoryCode) { - IPage result = doctorStationAdviceAppMapper.getExaminationPage( - new Page<>(pageNo, pageSize), - PublicationStatus.ACTIVE.getValue(), - organizationId, - searchKey, - categoryCode); - return result; - } - } diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/dto/AdviceSaveParam.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/dto/AdviceSaveParam.java index 5421d0ef9..9535790fc 100755 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/dto/AdviceSaveParam.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/dto/AdviceSaveParam.java @@ -1,38 +1,44 @@ package com.openhis.web.doctorstation.dto; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import com.openhis.common.enums.Whether; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; -import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; import java.util.List; /** - * 医嘱保存参数类 + * 医嘱/检验申请保存参数 */ @Data -@Accessors(chain = true) public class AdviceSaveParam { - /** - * 患者挂号对应的科室id - */ - @JsonSerialize(using = ToStringSerializer.class) - private Long organizationId; + /** 患者就诊ID */ + @NotNull(message = "就诊ID不能为空") + private Long encounterId; - /** - * 代煎标识 | 0:否 , 1:是 - */ - private Integer sufferingFlag; + /** 申请类型:1-普通 2-急诊 */ + private Integer applicationType; - /** - * 保存医嘱 dto - */ - private List adviceSaveList; + /** 标本类型 */ + private String specimenType; - public AdviceSaveParam() { - this.sufferingFlag = Whether.NO.getValue(); - } + /** 执行时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime executionTime; + /** 发往科室编码 */ + private String targetDeptCode; + + /** 临床诊断 */ + private String diagnosis; + + /** 关联的检验/检查项目ID集合 */ + private List itemIds; + + /** 医嘱操作类型:1-保存草稿 2-签发 */ + private String adviceOpType; } diff --git a/openhis-ui-vue3/src/views/inpatientdoctorstation/lab/LabRequest.vue b/openhis-ui-vue3/src/views/inpatientdoctorstation/lab/LabRequest.vue new file mode 100644 index 000000000..7c7f71146 --- /dev/null +++ b/openhis-ui-vue3/src/views/inpatientdoctorstation/lab/LabRequest.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/tests/e2e/specs/bug-regression.spec.ts b/tests/e2e/specs/bug-regression.spec.ts index ff88c8b7f..aee8decf2 100644 --- a/tests/e2e/specs/bug-regression.spec.ts +++ b/tests/e2e/specs/bug-regression.spec.ts @@ -1,49 +1,53 @@ -import { test, expect } from '@playwright/test'; +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import LabRequest from '@/views/inpatientdoctorstation/lab/LabRequest.vue'; -// 原有回归测试用例... -test.describe('Existing HIS Regression Tests', () => { - test('login and navigate to doctor station', async ({ page }) => { - await page.goto('/login'); - await page.fill('input[name="username"]', 'admin'); - await page.fill('input[name="password"]', '123456'); - await page.click('button[type="submit"]'); - await expect(page).toHaveURL(/.*dashboard.*/); +// @bug466 @regression +describe('Bug #466: 检验申请单核心质控字段及联动逻辑', () => { + it('应默认显示申请类型、标本类型、执行时间字段', () => { + const wrapper = mount(LabRequest, { + global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] } + }); + expect(wrapper.find('[data-cy="application-type"]').exists()).toBe(true); + expect(wrapper.find('[data-cy="specimen-type"]').exists()).toBe(true); + expect(wrapper.find('[data-cy="execution-time"]').exists()).toBe(true); }); -}); -// @bug550 @regression -test.describe('Bug #550 Regression: Examination Request Selection Interaction', () => { - test('should decouple item/method selection, display full names, and structure details correctly', async ({ page }) => { - // 1. 进入门诊医生站-检查申请单 - await page.goto('/clinic/doctor-station/examination'); - await page.waitForLoadState('networkidle'); + it('申请类型应默认选中普通,支持切换急诊', () => { + const wrapper = mount(LabRequest, { + global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] } + }); + const radioGroup = wrapper.find('[data-cy="application-type"]'); + expect(radioGroup.vm.modelValue).toBe('1'); // 1: 普通 + radioGroup.vm.$emit('update:modelValue', '2'); + expect(radioGroup.vm.modelValue).toBe('2'); // 2: 急诊 + }); - // 2. 展开“彩超”分类并勾选“128线排” - await page.click('text=彩超'); - await page.waitForSelector('.item-checkbox:has-text("128线排")'); - await page.check('.item-checkbox:has-text("128线排")'); + it('勾选检验项目后应自动带出标本类型', async () => { + const wrapper = mount(LabRequest, { + global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] } + }); + // 模拟左侧勾选项目 + wrapper.vm.selectedItemIds = [101]; + wrapper.vm.itemList = [{ id: 101, name: '血常规', specimenType: '血液' }]; + await wrapper.vm.$nextTick(); + wrapper.vm.onItemSelectChange([101]); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.specimenType).toBe('血液'); + }); - // 3. 验证联动解耦:检查方法不应被自动勾选 - const methodCheckbox = page.locator('.method-container .el-checkbox').first(); - await expect(methodCheckbox).not.toBeChecked(); - - // 4. 验证卡片显示:无“套餐”前缀,支持完整名称提示,宽度自适应 - const selectedCard = page.locator('.selected-group').first(); - await expect(selectedCard).toContainText('128线排'); - await expect(selectedCard).not.toContainText('套餐'); + it('执行时间早于当前时间时应拦截并提示', async () => { + const wrapper = mount(LabRequest, { + global: { stubs: ['el-dialog', 'el-tree', 'el-checkbox-group', 'el-radio-group', 'el-input', 'el-date-picker'] } + }); + const pastTime = new Date(); + pastTime.setFullYear(pastTime.getFullYear() - 1); + wrapper.vm.executionTime = pastTime; + await wrapper.vm.$nextTick(); - // 验证悬停提示完整名称 - await selectedCard.hover(); - const tooltip = page.locator('.el-tooltip__trigger'); - await expect(tooltip).toBeVisible(); - - // 5. 验证默认收起状态 - const methodContainer = page.locator('.method-container'); - await expect(methodContainer).not.toBeVisible(); - - // 6. 验证层级结构:点击展开后显示“检查项目 > 检查方法” - await page.click('.group-header'); - await expect(methodContainer).toBeVisible(); - await expect(page.locator('.method-container .el-checkbox')).toHaveCount(1); // 至少展示关联方法 + const mockAlert = vi.fn(); + wrapper.vm.$alert = mockAlert; + wrapper.vm.handleSubmit(); + expect(mockAlert).toHaveBeenCalledWith('执行时间不可早于当前时间', '提示', { type: 'warning' }); }); });