Compare commits
25 Commits
7d1e50d045
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| b536eadd92 | |||
|
|
3472aa790e | ||
|
|
ec89ead14c | ||
|
|
136235fe4c | ||
|
|
c2cac12b9f | ||
|
|
b424d73542 | ||
|
|
decac542c8 | ||
|
|
783ee48ec8 | ||
|
|
e1ad4965eb | ||
|
|
fd1880f1c8 | ||
|
|
d4d05267ad | ||
| 2b0acce1db | |||
| 4312c0c557 | |||
|
|
caa45c3310 | ||
| 7fabad14f9 | |||
|
|
405a9dfb72 | ||
| d1be841688 | |||
|
|
9b8655748e | ||
| 00fd6c8710 | |||
| bbd9d48fa6 | |||
| 8fb1d3e583 | |||
| 34ba7cae6a | |||
| 305ab15436 | |||
| 46a7076460 | |||
| e0e6693897 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,3 +63,6 @@ public.sql
|
|||||||
发版记录/2025-11-12/发版日志.docx
|
发版记录/2025-11-12/发版日志.docx
|
||||||
.gitignore
|
.gitignore
|
||||||
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
||||||
|
.env.test.local
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.core.web.controller.system;
|
||||||
|
|
||||||
|
import com.core.common.config.CoreConfig;
|
||||||
|
import com.core.common.core.domain.AjaxResult;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统版本信息
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/system")
|
||||||
|
public class SysVersionController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CoreConfig coreConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后端版本号
|
||||||
|
*/
|
||||||
|
@GetMapping("/version")
|
||||||
|
public AjaxResult getVersion() {
|
||||||
|
AjaxResult ajax = AjaxResult.success();
|
||||||
|
ajax.put("backendVersion", coreConfig.getVersion());
|
||||||
|
return ajax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,9 @@ public class SecurityConfig {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.antMatchers("/patientmanage/information/**")
|
.antMatchers("/patientmanage/information/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
// 登录页展示用的系统版本信息,允许匿名访问
|
||||||
|
.antMatchers("/system/version")
|
||||||
|
.permitAll()
|
||||||
// 除上面外的所有请求全部需要鉴权认证
|
// 除上面外的所有请求全部需要鉴权认证
|
||||||
.anyRequest().authenticated();
|
.anyRequest().authenticated();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ import com.openhis.web.chargemanage.dto.PatientMetadata;
|
|||||||
import com.openhis.web.chargemanage.dto.PractitionerMetadata;
|
import com.openhis.web.chargemanage.dto.PractitionerMetadata;
|
||||||
import com.openhis.web.chargemanage.dto.ReprintRegistrationDto;
|
import com.openhis.web.chargemanage.dto.ReprintRegistrationDto;
|
||||||
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
|
import com.openhis.web.chargemanage.mapper.OutpatientRegistrationAppMapper;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
||||||
|
import com.openhis.triageandqueuemanage.service.DivLogService;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||||
import com.openhis.web.paymentmanage.appservice.IPaymentRecService;
|
import com.openhis.web.paymentmanage.appservice.IPaymentRecService;
|
||||||
import com.openhis.web.paymentmanage.dto.CancelPaymentDto;
|
import com.openhis.web.paymentmanage.dto.CancelPaymentDto;
|
||||||
@@ -111,6 +113,9 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
@Resource
|
@Resource
|
||||||
com.openhis.triageandqueuemanage.service.TriageQueueItemService triageQueueItemService;
|
com.openhis.triageandqueuemanage.service.TriageQueueItemService triageQueueItemService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
DivLogService divLogService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
ScheduleSlotMapper scheduleSlotMapper;
|
ScheduleSlotMapper scheduleSlotMapper;
|
||||||
|
|
||||||
@@ -807,6 +812,23 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
|
|||||||
queueItem.setDeleteFlag("1");
|
queueItem.setDeleteFlag("1");
|
||||||
queueItem.setUpdateTime(LocalDateTime.now());
|
queueItem.setUpdateTime(LocalDateTime.now());
|
||||||
triageQueueItemService.updateById(queueItem);
|
triageQueueItemService.updateById(queueItem);
|
||||||
|
|
||||||
|
// 写入分诊操作日志:诊前退号
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
DivLog divLog = new DivLog()
|
||||||
|
.setPoolId(queueItem.getPoolId())
|
||||||
|
.setSlotId(queueItem.getSlotId())
|
||||||
|
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
|
||||||
|
.setAction("REFUND")
|
||||||
|
.setCreateTime(LocalDateTime.now())
|
||||||
|
.setUpdateAt(LocalDateTime.now())
|
||||||
|
.setCreatedAt(LocalDateTime.now());
|
||||||
|
divLogService.save(divLog);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入分诊退号日志失败,encounterId={}", encounterId, e);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("退号成功,已移除分诊队列记录,encounterId={}, queueItemId={}", encounterId, queueItem.getId());
|
log.info("退号成功,已移除分诊队列记录,encounterId={}, queueItemId={}", encounterId, queueItem.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ public class OutpatientPricingController {
|
|||||||
@RequestParam(value = "organizationId") Long organizationId,
|
@RequestParam(value = "organizationId") Long organizationId,
|
||||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
@RequestParam(value = "categoryCode", required = false) String categoryCode) {
|
@RequestParam(value = "categoryCode", required = false) String categoryCode,
|
||||||
|
@RequestParam(value = "adviceType", required = false) Integer adviceType) {
|
||||||
// 将 categoryCode 设置到 adviceBaseDto 中
|
// 将 categoryCode 设置到 adviceBaseDto 中
|
||||||
|
// Bug #438 修复:接收并处理 adviceType 参数
|
||||||
|
if (adviceType != null) {
|
||||||
|
adviceBaseDto.setAdviceType(adviceType);
|
||||||
|
}
|
||||||
if (categoryCode != null && !categoryCode.isEmpty()) {
|
if (categoryCode != null && !categoryCode.isEmpty()) {
|
||||||
adviceBaseDto.setCategoryCode(categoryCode);
|
adviceBaseDto.setCategoryCode(categoryCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,4 +170,9 @@ public class CurrentDayEncounterDto {
|
|||||||
*/
|
*/
|
||||||
private String clinicRoom;
|
private String clinicRoom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约序号(来自 adm_schedule_slot.seq_no)
|
||||||
|
*/
|
||||||
|
private Integer seqNo;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,4 +209,14 @@ public class EncounterPatientPrescriptionDto {
|
|||||||
private String discountRate = "0";
|
private String discountRate = "0";
|
||||||
private String discountRate_dictText;
|
private String discountRate_dictText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账单生成来源
|
||||||
|
*/
|
||||||
|
private Integer generateSourceEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来源单据号(手术单号等)
|
||||||
|
*/
|
||||||
|
private String sourceBillNo;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.openhis.common.constant.CommonConstants;
|
|||||||
import com.openhis.common.enums.*;
|
import com.openhis.common.enums.*;
|
||||||
import com.openhis.common.utils.EnumUtils;
|
import com.openhis.common.utils.EnumUtils;
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
import com.openhis.common.utils.HisQueryUtils;
|
||||||
|
import com.openhis.web.doctorstation.appservice.IDoctorStationMainAppService;
|
||||||
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
||||||
@@ -32,6 +33,9 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TodayOutpatientMapper todayOutpatientMapper;
|
private TodayOutpatientMapper todayOutpatientMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDoctorStationMainAppService doctorStationMainAppService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
||||||
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
||||||
@@ -259,22 +263,19 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Override
|
@Override
|
||||||
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的接诊逻辑
|
// 调用现有的接诊逻辑
|
||||||
// 这里可以复用 DoctorStationMainAppServiceImpl 中的 receiveEncounter 方法
|
return doctorStationMainAppService.receiveEncounter(encounterId);
|
||||||
// 或者直接调用相应的服务
|
|
||||||
|
|
||||||
return R.ok("接诊成功");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的完诊逻辑
|
// 调用现有的完诊逻辑
|
||||||
return R.ok("就诊完成");
|
return doctorStationMainAppService.completeEncounter(encounterId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
||||||
// 调用现有的取消就诊逻辑
|
// 调用现有的取消就诊逻辑
|
||||||
return R.ok("就诊取消成功");
|
return doctorStationMainAppService.cancelEncounter(encounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,815 +0,0 @@
|
|||||||
package com.openhis.web.inhospitalnursestation.appservice.impl;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.core.common.core.domain.R;
|
|
||||||
import com.core.common.core.domain.model.LoginUser;
|
|
||||||
import com.core.common.exception.NonCaptureException;
|
|
||||||
import com.core.common.exception.ServiceException;
|
|
||||||
import com.core.common.utils.*;
|
|
||||||
import com.openhis.administration.domain.ChargeItem;
|
|
||||||
import com.openhis.administration.dto.CostDetailDto;
|
|
||||||
import com.openhis.administration.dto.CostDetailSearchParam;
|
|
||||||
import com.openhis.administration.service.IChargeItemService;
|
|
||||||
import com.openhis.administration.service.IOrganizationService;
|
|
||||||
import com.openhis.clinical.domain.Procedure;
|
|
||||||
import com.openhis.clinical.service.IProcedureService;
|
|
||||||
import com.openhis.common.constant.CommonConstants;
|
|
||||||
import com.openhis.common.constant.PromptMsgConstant;
|
|
||||||
import com.openhis.common.enums.*;
|
|
||||||
import com.openhis.common.utils.EnumUtils;
|
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
|
||||||
import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams;
|
|
||||||
import com.openhis.web.doctorstation.dto.AdviceSaveDto;
|
|
||||||
import com.openhis.web.doctorstation.utils.AdviceUtils;
|
|
||||||
import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam;
|
|
||||||
import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam;
|
|
||||||
import com.openhis.workflow.domain.ActivityDefinition;
|
|
||||||
import com.openhis.workflow.domain.DeviceRequest;
|
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
|
||||||
import com.openhis.workflow.service.IActivityDefinitionService;
|
|
||||||
import com.openhis.workflow.service.IDeviceDispenseService;
|
|
||||||
import com.openhis.workflow.service.IDeviceRequestService;
|
|
||||||
import com.openhis.workflow.service.IServiceRequestService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2.
|
|
||||||
* 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5.
|
|
||||||
* 关联数据一致性管理(请求表、执行表、费用表、发放表联动)
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class NurseBillingAppService implements INurseBillingAppService {
|
|
||||||
|
|
||||||
// ======================== 常量定义(关联表名/编码规则)========================
|
|
||||||
/**
|
|
||||||
* 耗材请求服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 诊疗活动服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 耗材产品定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 耗材发放表名(用于费用项关联发放记录)
|
|
||||||
*/
|
|
||||||
private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用项业务编号前缀(统一编码规则,便于追溯)
|
|
||||||
*/
|
|
||||||
private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix();
|
|
||||||
/**
|
|
||||||
* 耗材申请单号序列号长度(按日生成,每日从0001开始递增)
|
|
||||||
*/
|
|
||||||
private static final int DEVICE_RES_NO_SEQ_LENGTH = 4;
|
|
||||||
/**
|
|
||||||
* 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱)
|
|
||||||
*/
|
|
||||||
private static final int ADVICE_SIGN_SEQ_LENGTH = 10;
|
|
||||||
|
|
||||||
// ======================== 依赖注入(业务服务/工具类)========================
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义服务(查询诊疗活动基础信息及子项配置)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
IActivityDefinitionService iActivityDefinitionService;
|
|
||||||
/**
|
|
||||||
* 医嘱处理工具类(含诊疗子项处理等通用逻辑)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AdviceUtils adviceUtils;
|
|
||||||
/**
|
|
||||||
* 序列生成工具类(用于生成业务编号、签发编码等唯一标识)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AssignSeqUtil assignSeqUtil;
|
|
||||||
/**
|
|
||||||
* 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceRequestService iDeviceRequestService;
|
|
||||||
/**
|
|
||||||
* 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IServiceRequestService iServiceRequestService;
|
|
||||||
/**
|
|
||||||
* 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IChargeItemService iChargeItemService;
|
|
||||||
/**
|
|
||||||
* 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceDispenseService iDeviceDispenseService;
|
|
||||||
/**
|
|
||||||
* 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IProcedureService iProcedureService;
|
|
||||||
@Resource
|
|
||||||
private NurseBillingAppMapper nurseBillingAppMapper;
|
|
||||||
@Resource
|
|
||||||
private IOrganizationService organizationService;
|
|
||||||
|
|
||||||
// ======================== 核心业务方法(划价新增)========================
|
|
||||||
/**
|
|
||||||
* 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果
|
|
||||||
* 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致)
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据
|
|
||||||
* @return R<?> 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 获取当前登录用户信息(包含护士ID、所属科室等)
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
|
|
||||||
// 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间
|
|
||||||
Date curDate = new Date();
|
|
||||||
Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime();
|
|
||||||
Date authoredTime
|
|
||||||
= regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime();
|
|
||||||
|
|
||||||
// 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理)
|
|
||||||
R<?> checkResult = checkNurseBillingParam(regAdviceSaveParam);
|
|
||||||
if (checkResult.getCode() != R.SUCCESS) {
|
|
||||||
return checkResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室)
|
|
||||||
Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId()
|
|
||||||
: regAdviceSaveParam.getOrganizationId();
|
|
||||||
// 待处理医嘱列表(含耗材、诊疗活动两种类型)
|
|
||||||
List<RegAdviceSaveDto> allAdviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
|
|
||||||
// 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑)
|
|
||||||
List<RegAdviceSaveDto> deviceAdviceList = filterDeviceAdvice(allAdviceList);
|
|
||||||
List<RegAdviceSaveDto> activityAdviceList = filterActivityAdvice(allAdviceList);
|
|
||||||
|
|
||||||
// 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯)
|
|
||||||
String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH);
|
|
||||||
|
|
||||||
// 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑
|
|
||||||
if (!deviceAdviceList.isEmpty()) {
|
|
||||||
handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate);
|
|
||||||
}
|
|
||||||
if (!activityAdviceList.isEmpty()) {
|
|
||||||
handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息)
|
|
||||||
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果
|
|
||||||
* 事务特性:所有删除操作原子化,任一环节失败整体回滚
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> deleteInNurseBilling(List<AdviceBatchOpParam> paramList) {
|
|
||||||
|
|
||||||
// TODO 撤销前校验
|
|
||||||
// 诊疗ids
|
|
||||||
List<Long> activityRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
// 耗材ids
|
|
||||||
List<Long> deviceRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE);
|
|
||||||
this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE);
|
|
||||||
return R.ok("删除成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院患者医嘱查询
|
|
||||||
*
|
|
||||||
* @param inpatientAdviceParam 查询条件
|
|
||||||
* @param pageNo 当前页码
|
|
||||||
* @param pageSize 查询条数
|
|
||||||
* @return 住院患者医
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize,
|
|
||||||
LocalDateTime startTime, LocalDateTime endTime) {
|
|
||||||
// 初始化查询参数
|
|
||||||
String encounterIds = inpatientAdviceParam.getEncounterIds();
|
|
||||||
inpatientAdviceParam.setEncounterIds(null);
|
|
||||||
Integer exeStatus = inpatientAdviceParam.getExeStatus();
|
|
||||||
inpatientAdviceParam.setExeStatus(null);
|
|
||||||
// 构建查询条件
|
|
||||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
|
||||||
|
|
||||||
// 手动拼接住院患者id条件
|
|
||||||
if (encounterIds != null && !encounterIds.isEmpty()) {
|
|
||||||
List<Long> encounterIdList
|
|
||||||
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
|
||||||
}
|
|
||||||
// 患者医嘱分页列表
|
|
||||||
Page<InpatientAdviceDto> inpatientAdvicePage
|
|
||||||
= nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper,
|
|
||||||
CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST,
|
|
||||||
RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(),
|
|
||||||
ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(),
|
|
||||||
ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(),
|
|
||||||
GenerateSource.NURSE_PRICING.getValue(), startTime, endTime);
|
|
||||||
inpatientAdvicePage.getRecords().forEach(e -> {
|
|
||||||
// 医嘱类型
|
|
||||||
e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum()));
|
|
||||||
// 请求状态
|
|
||||||
e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus()));
|
|
||||||
// 性别枚举
|
|
||||||
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
|
|
||||||
// 计算年龄
|
|
||||||
if (e.getBirthDate() != null) {
|
|
||||||
e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return R.ok(inpatientAdvicePage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 入参校验方法 ========================
|
|
||||||
/**
|
|
||||||
* 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验)
|
|
||||||
* 校验失败直接返回错误响应,不进入后续业务逻辑
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* @return R<?> 校验结果:成功返回R.ok(),失败返回R.fail(错误信息)
|
|
||||||
*/
|
|
||||||
private R<?> checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 1. 整体参数非空校验:请求体为null直接返回失败
|
|
||||||
if (regAdviceSaveParam == null) {
|
|
||||||
return R.fail("划价请求失败:未获取到有效请求数据,请重新提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 核心字段非空校验:患者住院科室ID不能为空
|
|
||||||
if (regAdviceSaveParam.getOrganizationId() == null) {
|
|
||||||
return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 医嘱列表非空校验:必须选择至少一个待划价项目
|
|
||||||
List<RegAdviceSaveDto> adviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
if (adviceList == null || adviceList.isEmpty()) {
|
|
||||||
return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验)
|
|
||||||
// 若后续需要恢复库存校验,可解除以下注释
|
|
||||||
/*
|
|
||||||
List<AdviceSaveDto> needCheckInventoryList = adviceList.stream()
|
|
||||||
.filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum())
|
|
||||||
&& !DbOpType.DELETE.getCode().equals(advice.getDbOpType())
|
|
||||||
&& !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList));
|
|
||||||
if (inventoryTip != null) {
|
|
||||||
return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// 所有校验通过
|
|
||||||
return R.ok();
|
|
||||||
}
|
|
||||||
// ======================== 医嘱分类工具方法 ========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的耗材类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterDeviceAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的诊疗活动类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterActivityAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项
|
|
||||||
* 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库)
|
|
||||||
*
|
|
||||||
* @param deviceAdviceList 耗材类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间(用于填充创建时间/执行时间)
|
|
||||||
*/
|
|
||||||
private void handleAddDeviceBilling(List<RegAdviceSaveDto> deviceAdviceList, String signCode, Long organizationId,
|
|
||||||
Date curDate) {
|
|
||||||
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
|
||||||
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常
|
|
||||||
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
|
|
||||||
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
|
||||||
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
|
||||||
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate);
|
|
||||||
iDeviceRequestService.saveOrUpdate(deviceRequest);
|
|
||||||
|
|
||||||
// 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等
|
|
||||||
Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
deviceRequest.getPatientId(), // 患者ID
|
|
||||||
deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
deviceRequest.getLocationId(), // 执行位置(发放库房)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录
|
|
||||||
Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId,
|
|
||||||
deviceRequest.getLocationId(), curDate);
|
|
||||||
|
|
||||||
// 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 诊疗活动类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有)
|
|
||||||
* 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价
|
|
||||||
*
|
|
||||||
* @param activityAdviceList 诊疗活动类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
*/
|
|
||||||
private void handleAddActivityBilling(List<RegAdviceSaveDto> activityAdviceList, String signCode,
|
|
||||||
Long organizationId, Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
// 循环处理每个诊疗活动医嘱
|
|
||||||
for (AdviceSaveDto adviceDto : activityAdviceList) {
|
|
||||||
// 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
ServiceRequest serviceRequest
|
|
||||||
= this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
|
|
||||||
// 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态
|
|
||||||
Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
serviceRequest.getPatientId(), // 患者ID
|
|
||||||
serviceRequest.getId(), // 服务请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_SERVICE, // 关联表名(服务请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
serviceRequest.getLocationId(), // 执行位置(执行科室)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
|
|
||||||
// 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据)
|
|
||||||
this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 执行记录工具方法 ========================
|
|
||||||
/**
|
|
||||||
* 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息
|
|
||||||
*
|
|
||||||
* @param encounterId 就诊ID(关联患者就诊记录)
|
|
||||||
* @param patientId 患者ID(关联患者基本信息)
|
|
||||||
* @param requestId 请求ID(关联耗材/服务请求主记录)
|
|
||||||
* @param requestTable 关联表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param eventStatus 执行状态(如已完成、待执行等)
|
|
||||||
* @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等)
|
|
||||||
* @param locationId 执行位置(执行科室/发放库房ID)
|
|
||||||
* @param exeDate 执行时间
|
|
||||||
* @param groupId 组号(批量执行时用于关联同一组医嘱)
|
|
||||||
* @param refundId 取消执行ID(取消执行时关联原执行记录)
|
|
||||||
* @return Long 执行记录ID(CLIN_PROCEDURE表主键)
|
|
||||||
*/
|
|
||||||
private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable,
|
|
||||||
EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId,
|
|
||||||
Long refundId) {
|
|
||||||
// 调用执行记录服务创建记录,返回执行记录主键ID
|
|
||||||
return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus,
|
|
||||||
procedureCategory, locationId, exeDate, exeDate, groupId, refundId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 实体构建工具方法(请求/费用项)========================
|
|
||||||
/**
|
|
||||||
* 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等)
|
|
||||||
*
|
|
||||||
* @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等)
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @return DeviceRequest 构建完成的耗材请求实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) {
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
DeviceRequest deviceRequest = new DeviceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号
|
|
||||||
deviceRequest.setId(adviceDto.getRequestId());
|
|
||||||
deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(确保每日唯一)
|
|
||||||
deviceRequest
|
|
||||||
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH));
|
|
||||||
// deviceRequest.setPrescriptionNo(null);//处方号
|
|
||||||
// deviceRequest.setActivityId(null);//诊疗ID
|
|
||||||
// deviceRequest.setPackageId(null);//组套id
|
|
||||||
// deviceRequest.setIntentCode(null); // 请求意图
|
|
||||||
deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求)
|
|
||||||
// deviceRequest.setPerformFlag(null);//优先级
|
|
||||||
// deviceRequest.setPriorityEnum(null);//是否停止执行
|
|
||||||
// deviceRequest.setGroupNo(null);//分组编号
|
|
||||||
// deviceRequest.setDeviceTypeCode(null);//器材类型
|
|
||||||
deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量
|
|
||||||
deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒")
|
|
||||||
deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理)
|
|
||||||
deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效)
|
|
||||||
deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION)
|
|
||||||
// deviceRequest.setDeviceSpecifications(null)//器材规格
|
|
||||||
deviceRequest.setRequesterId(
|
|
||||||
adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人
|
|
||||||
deviceRequest
|
|
||||||
.setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室
|
|
||||||
deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房
|
|
||||||
deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置)
|
|
||||||
deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录)
|
|
||||||
deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
// deviceRequest.setRateCode(null);//用药频次
|
|
||||||
// deviceRequest.setUseTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseStartTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseEndTime();//预计使用结束时间
|
|
||||||
// deviceRequest.setUseTiming();//预计使用周期时间
|
|
||||||
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间)
|
|
||||||
// deviceRequest.setPerformerEnum();//执行人类型
|
|
||||||
// deviceRequest.setPerformerId();//执行人
|
|
||||||
// deviceRequest.setPerformOrgId();//执行科室
|
|
||||||
// deviceRequest.setConditionIdJson(); // 相关诊断
|
|
||||||
// deviceRequest.setObservationIdJson();//相关观测
|
|
||||||
// deviceRequest.setAsNeedFlag();//是否可以按需给出
|
|
||||||
// deviceRequest.setAsNeedReason();//按需使用原因
|
|
||||||
// deviceRequest.setContractCode();//合同id
|
|
||||||
// deviceRequest.setSupportInfo();//支持用药信息
|
|
||||||
// deviceRequest.setRequesterId();//退药id
|
|
||||||
deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json
|
|
||||||
// deviceRequest.setYbClassEnum();//类别医保编码
|
|
||||||
// deviceRequest.setTraceNo()//追溯码
|
|
||||||
deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id
|
|
||||||
deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id
|
|
||||||
// deviceRequest.setBasedOnTable();//请求基于什么
|
|
||||||
deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID
|
|
||||||
deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
return deviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数
|
|
||||||
*
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
* @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库)
|
|
||||||
*/
|
|
||||||
private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId,
|
|
||||||
Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
ServiceRequest serviceRequest = new ServiceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键、状态、业务编号、签发编码
|
|
||||||
serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID)
|
|
||||||
serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效)
|
|
||||||
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
|
|
||||||
serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间
|
|
||||||
serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱)
|
|
||||||
serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(每日唯一)
|
|
||||||
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
|
|
||||||
serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:从DTO提取核心参数
|
|
||||||
serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数)
|
|
||||||
serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程")
|
|
||||||
serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗)
|
|
||||||
serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入)
|
|
||||||
serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION)
|
|
||||||
serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生)
|
|
||||||
serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录)
|
|
||||||
serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间)
|
|
||||||
serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室)
|
|
||||||
serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置)
|
|
||||||
serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销)
|
|
||||||
serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
|
|
||||||
// 保存诊疗活动请求记录到数据库
|
|
||||||
iServiceRequestService.saveOrUpdate(serviceRequest);
|
|
||||||
return serviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项)
|
|
||||||
*
|
|
||||||
* @param serviceRequest 父诊疗活动请求实体(关联子项)
|
|
||||||
* @param chargeItemId 父诊疗活动的费用项ID(关联子项费用)
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含子项配置JSON)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
*/
|
|
||||||
private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto,
|
|
||||||
Long organizationId) {
|
|
||||||
// 1. 查询诊疗活动定义信息(获取子项配置JSON)
|
|
||||||
ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId());
|
|
||||||
String childrenJson = activityDefinition.getChildrenJson();
|
|
||||||
|
|
||||||
// 2. 若存在子项配置,构建子项参数并调用工具类处理
|
|
||||||
if (childrenJson != null) {
|
|
||||||
ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams();
|
|
||||||
// 子项治疗类型:默认临时(与父项一致)
|
|
||||||
activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
|
|
||||||
activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算)
|
|
||||||
activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用)
|
|
||||||
activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项)
|
|
||||||
activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId());
|
|
||||||
// 调用工具类处理子项:递归生成子项的请求、执行记录、费用项
|
|
||||||
adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表)
|
|
||||||
* 通用性:支持耗材、诊疗活动两种类型的费用项构建
|
|
||||||
*
|
|
||||||
* @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等)
|
|
||||||
* @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo)
|
|
||||||
* @param requestId 关联请求的ID(耗材/服务请求的主键)
|
|
||||||
* @param serviceTable 关联服务表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动)
|
|
||||||
* @param curDate 当前操作时间(费用开立时间)
|
|
||||||
* @param procedureId 执行记录ID(关联CLIN_PROCEDURE表)
|
|
||||||
* @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null)
|
|
||||||
* @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null)
|
|
||||||
* @return ChargeItem 构建完成的费用项实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId,
|
|
||||||
String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId,
|
|
||||||
String dispenseTable) {
|
|
||||||
ChargeItem chargeItem = new ChargeItem();
|
|
||||||
// TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算
|
|
||||||
// 基础配置:主键、状态、业务编号
|
|
||||||
chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID)
|
|
||||||
chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费)
|
|
||||||
// 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯)
|
|
||||||
chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo));
|
|
||||||
chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:患者、就诊、定价相关信息
|
|
||||||
chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者)
|
|
||||||
chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗)
|
|
||||||
chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院)
|
|
||||||
chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则)
|
|
||||||
chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价)
|
|
||||||
chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士)
|
|
||||||
chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室)
|
|
||||||
chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间)
|
|
||||||
chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源)
|
|
||||||
chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键)
|
|
||||||
chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型)
|
|
||||||
chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID)
|
|
||||||
chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户)
|
|
||||||
chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录)
|
|
||||||
chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null)
|
|
||||||
chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null)
|
|
||||||
|
|
||||||
// 费用核心属性:数量、单位、单价、总价(与医嘱保持一致)
|
|
||||||
chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致)
|
|
||||||
chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致)
|
|
||||||
chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价)
|
|
||||||
chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算)
|
|
||||||
|
|
||||||
return chargeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材删除相关方法 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表
|
|
||||||
* → 费用项表
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材医嘱列表(可为null)
|
|
||||||
* @param serviceTable 关联服务表名(此处为耗材请求表)
|
|
||||||
*/
|
|
||||||
private void handleDel(List<Long> requestIds, String serviceTable) {
|
|
||||||
// 空列表直接返回,避免无效循环
|
|
||||||
if (requestIds == null || requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
|
|
||||||
checkDeletedDeviceChargeStatus(requestIds);
|
|
||||||
// 软删除执行记录
|
|
||||||
List<Procedure> procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable);
|
|
||||||
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
|
|
||||||
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
// 批量删除执行记录
|
|
||||||
iProcedureService.removeBatchByIds(procedureIds);
|
|
||||||
|
|
||||||
// 不想循环删除
|
|
||||||
for (Long requestId : requestIds) {
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_DEVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iDeviceRequestService.removeById(requestId);
|
|
||||||
// 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE)
|
|
||||||
iDeviceDispenseService.deleteDeviceDispense(requestId);
|
|
||||||
}
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_SERVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iServiceRequestService.removeById(requestId);
|
|
||||||
}
|
|
||||||
// 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联)
|
|
||||||
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材请求ID列表
|
|
||||||
*/
|
|
||||||
private void checkDeletedDeviceChargeStatus(List<Long> requestIds) {
|
|
||||||
if (requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 查询待删除耗材对应的费用项列表
|
|
||||||
List<ChargeItem> chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds);
|
|
||||||
if (chargeItemList == null || chargeItemList.isEmpty()) {
|
|
||||||
return; // 无关联费用项,允许删除
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 校验是否存在已收费项(状态为BILLED)
|
|
||||||
boolean hasBilledItem
|
|
||||||
= chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum()));
|
|
||||||
if (hasBilledItem) {
|
|
||||||
throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 未实现接口方法(保留签名,待扩展)========================
|
|
||||||
/**
|
|
||||||
* 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 划价结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> addOrderBilling() {
|
|
||||||
// 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> deleteOrderBilling() {
|
|
||||||
// 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 修改结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> updateOrderBilling() {
|
|
||||||
// 待实现:需接收修改后的订单划价参数,更新请求记录和费用项
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用明细查询
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @return 住院患者费用明细
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<List<CostDetailDto>> getCostDetails(CostDetailSearchParam costDetailSearchParam,
|
|
||||||
HttpServletRequest request) {
|
|
||||||
List<Long> encounterIds = costDetailSearchParam.getEncounterIds();
|
|
||||||
if (encounterIds == null || encounterIds.isEmpty()) {
|
|
||||||
return R.fail("就诊ID不能为空");
|
|
||||||
}
|
|
||||||
costDetailSearchParam.setEncounterIds(null);
|
|
||||||
QueryWrapper<CostDetailSearchParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request);
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds);
|
|
||||||
List<CostDetailDto> list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(),
|
|
||||||
ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(),
|
|
||||||
EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode());
|
|
||||||
return R.ok(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @param response response响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request,
|
|
||||||
HttpServletResponse response) {
|
|
||||||
R<List<CostDetailDto>> costDetails = getCostDetails(costDetailSearchParam, request);
|
|
||||||
if (costDetails.getData() != null) {
|
|
||||||
List<CostDetailDto> dataList = costDetails.getData();
|
|
||||||
// 设置执行科室
|
|
||||||
dataList.forEach(costDetailDto -> {
|
|
||||||
Long orgId = costDetailDto.getOrgId();
|
|
||||||
costDetailDto.setOrgName(organizationService.getById(orgId).getName());
|
|
||||||
});
|
|
||||||
// 根据EncounterId分组
|
|
||||||
Map<Long, List<CostDetailDto>> map
|
|
||||||
= dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId));
|
|
||||||
map.forEach((key, value) -> {
|
|
||||||
// 新加一条小计
|
|
||||||
value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice(
|
|
||||||
value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
|
||||||
});
|
|
||||||
// 收集要导出的数据
|
|
||||||
List<CostDetailExcelOutDto> excelOutList
|
|
||||||
= map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(),
|
|
||||||
entry.getValue().get(0).getPatientName())).toList();
|
|
||||||
try {
|
|
||||||
// 住院记账-费用明细 导出
|
|
||||||
NewExcelUtil<CostDetailExcelOutDto> util = new NewExcelUtil<>(CostDetailExcelOutDto.class);
|
|
||||||
util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new NonCaptureException(StringUtils.format("导出excel失败"), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,815 +0,0 @@
|
|||||||
package com.openhis.web.inhospitalnursestation.appservice.impl;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.core.common.core.domain.R;
|
|
||||||
import com.core.common.core.domain.model.LoginUser;
|
|
||||||
import com.core.common.exception.NonCaptureException;
|
|
||||||
import com.core.common.exception.ServiceException;
|
|
||||||
import com.core.common.utils.*;
|
|
||||||
import com.openhis.administration.domain.ChargeItem;
|
|
||||||
import com.openhis.administration.dto.CostDetailDto;
|
|
||||||
import com.openhis.administration.dto.CostDetailSearchParam;
|
|
||||||
import com.openhis.administration.service.IChargeItemService;
|
|
||||||
import com.openhis.administration.service.IOrganizationService;
|
|
||||||
import com.openhis.clinical.domain.Procedure;
|
|
||||||
import com.openhis.clinical.service.IProcedureService;
|
|
||||||
import com.openhis.common.constant.CommonConstants;
|
|
||||||
import com.openhis.common.constant.PromptMsgConstant;
|
|
||||||
import com.openhis.common.enums.*;
|
|
||||||
import com.openhis.common.utils.EnumUtils;
|
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
|
||||||
import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams;
|
|
||||||
import com.openhis.web.doctorstation.dto.AdviceSaveDto;
|
|
||||||
import com.openhis.web.doctorstation.utils.AdviceUtils;
|
|
||||||
import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam;
|
|
||||||
import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam;
|
|
||||||
import com.openhis.workflow.domain.ActivityDefinition;
|
|
||||||
import com.openhis.workflow.domain.DeviceRequest;
|
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
|
||||||
import com.openhis.workflow.service.IActivityDefinitionService;
|
|
||||||
import com.openhis.workflow.service.IDeviceDispenseService;
|
|
||||||
import com.openhis.workflow.service.IDeviceRequestService;
|
|
||||||
import com.openhis.workflow.service.IServiceRequestService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2.
|
|
||||||
* 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5.
|
|
||||||
* 关联数据一致性管理(请求表、执行表、费用表、发放表联动)
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class NurseBillingAppService implements INurseBillingAppService {
|
|
||||||
|
|
||||||
// ======================== 常量定义(关联表名/编码规则)========================
|
|
||||||
/**
|
|
||||||
* 耗材请求服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 诊疗活动服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 耗材产品定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 耗材发放表名(用于费用项关联发放记录)
|
|
||||||
*/
|
|
||||||
private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用项业务编号前缀(统一编码规则,便于追溯)
|
|
||||||
*/
|
|
||||||
private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix();
|
|
||||||
/**
|
|
||||||
* 耗材申请单号序列号长度(按日生成,每日从0001开始递增)
|
|
||||||
*/
|
|
||||||
private static final int DEVICE_RES_NO_SEQ_LENGTH = 4;
|
|
||||||
/**
|
|
||||||
* 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱)
|
|
||||||
*/
|
|
||||||
private static final int ADVICE_SIGN_SEQ_LENGTH = 10;
|
|
||||||
|
|
||||||
// ======================== 依赖注入(业务服务/工具类)========================
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义服务(查询诊疗活动基础信息及子项配置)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
IActivityDefinitionService iActivityDefinitionService;
|
|
||||||
/**
|
|
||||||
* 医嘱处理工具类(含诊疗子项处理等通用逻辑)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AdviceUtils adviceUtils;
|
|
||||||
/**
|
|
||||||
* 序列生成工具类(用于生成业务编号、签发编码等唯一标识)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AssignSeqUtil assignSeqUtil;
|
|
||||||
/**
|
|
||||||
* 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceRequestService iDeviceRequestService;
|
|
||||||
/**
|
|
||||||
* 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IServiceRequestService iServiceRequestService;
|
|
||||||
/**
|
|
||||||
* 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IChargeItemService iChargeItemService;
|
|
||||||
/**
|
|
||||||
* 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceDispenseService iDeviceDispenseService;
|
|
||||||
/**
|
|
||||||
* 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IProcedureService iProcedureService;
|
|
||||||
@Resource
|
|
||||||
private NurseBillingAppMapper nurseBillingAppMapper;
|
|
||||||
@Resource
|
|
||||||
private IOrganizationService organizationService;
|
|
||||||
|
|
||||||
// ======================== 核心业务方法(划价新增)========================
|
|
||||||
/**
|
|
||||||
* 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果
|
|
||||||
* 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致)
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据
|
|
||||||
* @return R<?> 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 获取当前登录用户信息(包含护士ID、所属科室等)
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
|
|
||||||
// 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间
|
|
||||||
Date curDate = new Date();
|
|
||||||
Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime();
|
|
||||||
Date authoredTime
|
|
||||||
= regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime();
|
|
||||||
|
|
||||||
// 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理)
|
|
||||||
R<?> checkResult = checkNurseBillingParam(regAdviceSaveParam);
|
|
||||||
if (checkResult.getCode() != R.SUCCESS) {
|
|
||||||
return checkResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室)
|
|
||||||
Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId()
|
|
||||||
: regAdviceSaveParam.getOrganizationId();
|
|
||||||
// 待处理医嘱列表(含耗材、诊疗活动两种类型)
|
|
||||||
List<RegAdviceSaveDto> allAdviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
|
|
||||||
// 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑)
|
|
||||||
List<RegAdviceSaveDto> deviceAdviceList = filterDeviceAdvice(allAdviceList);
|
|
||||||
List<RegAdviceSaveDto> activityAdviceList = filterActivityAdvice(allAdviceList);
|
|
||||||
|
|
||||||
// 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯)
|
|
||||||
String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH);
|
|
||||||
|
|
||||||
// 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑
|
|
||||||
if (!deviceAdviceList.isEmpty()) {
|
|
||||||
handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate);
|
|
||||||
}
|
|
||||||
if (!activityAdviceList.isEmpty()) {
|
|
||||||
handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息)
|
|
||||||
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果
|
|
||||||
* 事务特性:所有删除操作原子化,任一环节失败整体回滚
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> deleteInNurseBilling(List<AdviceBatchOpParam> paramList) {
|
|
||||||
|
|
||||||
// TODO 撤销前校验
|
|
||||||
// 诊疗ids
|
|
||||||
List<Long> activityRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
// 耗材ids
|
|
||||||
List<Long> deviceRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE);
|
|
||||||
this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE);
|
|
||||||
return R.ok("删除成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院患者医嘱查询
|
|
||||||
*
|
|
||||||
* @param inpatientAdviceParam 查询条件
|
|
||||||
* @param pageNo 当前页码
|
|
||||||
* @param pageSize 查询条数
|
|
||||||
* @return 住院患者医
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize,
|
|
||||||
LocalDateTime startTime, LocalDateTime endTime) {
|
|
||||||
// 初始化查询参数
|
|
||||||
String encounterIds = inpatientAdviceParam.getEncounterIds();
|
|
||||||
inpatientAdviceParam.setEncounterIds(null);
|
|
||||||
Integer exeStatus = inpatientAdviceParam.getExeStatus();
|
|
||||||
inpatientAdviceParam.setExeStatus(null);
|
|
||||||
// 构建查询条件
|
|
||||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
|
||||||
|
|
||||||
// 手动拼接住院患者id条件
|
|
||||||
if (encounterIds != null && !encounterIds.isEmpty()) {
|
|
||||||
List<Long> encounterIdList
|
|
||||||
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
|
||||||
}
|
|
||||||
// 患者医嘱分页列表
|
|
||||||
Page<InpatientAdviceDto> inpatientAdvicePage
|
|
||||||
= nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper,
|
|
||||||
CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST,
|
|
||||||
RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(),
|
|
||||||
ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(),
|
|
||||||
ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(),
|
|
||||||
GenerateSource.NURSE_PRICING.getValue(), startTime, endTime);
|
|
||||||
inpatientAdvicePage.getRecords().forEach(e -> {
|
|
||||||
// 医嘱类型
|
|
||||||
e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum()));
|
|
||||||
// 请求状态
|
|
||||||
e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus()));
|
|
||||||
// 性别枚举
|
|
||||||
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
|
|
||||||
// 计算年龄
|
|
||||||
if (e.getBirthDate() != null) {
|
|
||||||
e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return R.ok(inpatientAdvicePage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 入参校验方法 ========================
|
|
||||||
/**
|
|
||||||
* 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验)
|
|
||||||
* 校验失败直接返回错误响应,不进入后续业务逻辑
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* @return R<?> 校验结果:成功返回R.ok(),失败返回R.fail(错误信息)
|
|
||||||
*/
|
|
||||||
private R<?> checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 1. 整体参数非空校验:请求体为null直接返回失败
|
|
||||||
if (regAdviceSaveParam == null) {
|
|
||||||
return R.fail("划价请求失败:未获取到有效请求数据,请重新提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 核心字段非空校验:患者住院科室ID不能为空
|
|
||||||
if (regAdviceSaveParam.getOrganizationId() == null) {
|
|
||||||
return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 医嘱列表非空校验:必须选择至少一个待划价项目
|
|
||||||
List<RegAdviceSaveDto> adviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
if (adviceList == null || adviceList.isEmpty()) {
|
|
||||||
return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验)
|
|
||||||
// 若后续需要恢复库存校验,可解除以下注释
|
|
||||||
/*
|
|
||||||
List<AdviceSaveDto> needCheckInventoryList = adviceList.stream()
|
|
||||||
.filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum())
|
|
||||||
&& !DbOpType.DELETE.getCode().equals(advice.getDbOpType())
|
|
||||||
&& !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList));
|
|
||||||
if (inventoryTip != null) {
|
|
||||||
return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// 所有校验通过
|
|
||||||
return R.ok();
|
|
||||||
}
|
|
||||||
// ======================== 医嘱分类工具方法 ========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的耗材类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterDeviceAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的诊疗活动类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterActivityAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项
|
|
||||||
* 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库)
|
|
||||||
*
|
|
||||||
* @param deviceAdviceList 耗材类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间(用于填充创建时间/执行时间)
|
|
||||||
*/
|
|
||||||
private void handleAddDeviceBilling(List<RegAdviceSaveDto> deviceAdviceList, String signCode, Long organizationId,
|
|
||||||
Date curDate) {
|
|
||||||
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
|
||||||
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常
|
|
||||||
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
|
|
||||||
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
|
||||||
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
|
||||||
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate);
|
|
||||||
iDeviceRequestService.saveOrUpdate(deviceRequest);
|
|
||||||
|
|
||||||
// 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等
|
|
||||||
Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
deviceRequest.getPatientId(), // 患者ID
|
|
||||||
deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
deviceRequest.getLocationId(), // 执行位置(发放库房)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录
|
|
||||||
Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId,
|
|
||||||
deviceRequest.getLocationId(), curDate);
|
|
||||||
|
|
||||||
// 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 诊疗活动类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有)
|
|
||||||
* 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价
|
|
||||||
*
|
|
||||||
* @param activityAdviceList 诊疗活动类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
*/
|
|
||||||
private void handleAddActivityBilling(List<RegAdviceSaveDto> activityAdviceList, String signCode,
|
|
||||||
Long organizationId, Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
// 循环处理每个诊疗活动医嘱
|
|
||||||
for (AdviceSaveDto adviceDto : activityAdviceList) {
|
|
||||||
// 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
ServiceRequest serviceRequest
|
|
||||||
= this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
|
|
||||||
// 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态
|
|
||||||
Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
serviceRequest.getPatientId(), // 患者ID
|
|
||||||
serviceRequest.getId(), // 服务请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_SERVICE, // 关联表名(服务请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
serviceRequest.getLocationId(), // 执行位置(执行科室)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
|
|
||||||
// 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据)
|
|
||||||
this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 执行记录工具方法 ========================
|
|
||||||
/**
|
|
||||||
* 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息
|
|
||||||
*
|
|
||||||
* @param encounterId 就诊ID(关联患者就诊记录)
|
|
||||||
* @param patientId 患者ID(关联患者基本信息)
|
|
||||||
* @param requestId 请求ID(关联耗材/服务请求主记录)
|
|
||||||
* @param requestTable 关联表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param eventStatus 执行状态(如已完成、待执行等)
|
|
||||||
* @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等)
|
|
||||||
* @param locationId 执行位置(执行科室/发放库房ID)
|
|
||||||
* @param exeDate 执行时间
|
|
||||||
* @param groupId 组号(批量执行时用于关联同一组医嘱)
|
|
||||||
* @param refundId 取消执行ID(取消执行时关联原执行记录)
|
|
||||||
* @return Long 执行记录ID(CLIN_PROCEDURE表主键)
|
|
||||||
*/
|
|
||||||
private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable,
|
|
||||||
EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId,
|
|
||||||
Long refundId) {
|
|
||||||
// 调用执行记录服务创建记录,返回执行记录主键ID
|
|
||||||
return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus,
|
|
||||||
procedureCategory, locationId, exeDate, exeDate, groupId, refundId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 实体构建工具方法(请求/费用项)========================
|
|
||||||
/**
|
|
||||||
* 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等)
|
|
||||||
*
|
|
||||||
* @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等)
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @return DeviceRequest 构建完成的耗材请求实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) {
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
DeviceRequest deviceRequest = new DeviceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号
|
|
||||||
deviceRequest.setId(adviceDto.getRequestId());
|
|
||||||
deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(确保每日唯一)
|
|
||||||
deviceRequest
|
|
||||||
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH));
|
|
||||||
// deviceRequest.setPrescriptionNo(null);//处方号
|
|
||||||
// deviceRequest.setActivityId(null);//诊疗ID
|
|
||||||
// deviceRequest.setPackageId(null);//组套id
|
|
||||||
// deviceRequest.setIntentCode(null); // 请求意图
|
|
||||||
deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求)
|
|
||||||
// deviceRequest.setPerformFlag(null);//优先级
|
|
||||||
// deviceRequest.setPriorityEnum(null);//是否停止执行
|
|
||||||
// deviceRequest.setGroupNo(null);//分组编号
|
|
||||||
// deviceRequest.setDeviceTypeCode(null);//器材类型
|
|
||||||
deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量
|
|
||||||
deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒")
|
|
||||||
deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理)
|
|
||||||
deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效)
|
|
||||||
deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION)
|
|
||||||
// deviceRequest.setDeviceSpecifications(null)//器材规格
|
|
||||||
deviceRequest.setRequesterId(
|
|
||||||
adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人
|
|
||||||
deviceRequest
|
|
||||||
.setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室
|
|
||||||
deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房
|
|
||||||
deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置)
|
|
||||||
deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录)
|
|
||||||
deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
// deviceRequest.setRateCode(null);//用药频次
|
|
||||||
// deviceRequest.setUseTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseStartTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseEndTime();//预计使用结束时间
|
|
||||||
// deviceRequest.setUseTiming();//预计使用周期时间
|
|
||||||
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间)
|
|
||||||
// deviceRequest.setPerformerEnum();//执行人类型
|
|
||||||
// deviceRequest.setPerformerId();//执行人
|
|
||||||
// deviceRequest.setPerformOrgId();//执行科室
|
|
||||||
// deviceRequest.setConditionIdJson(); // 相关诊断
|
|
||||||
// deviceRequest.setObservationIdJson();//相关观测
|
|
||||||
// deviceRequest.setAsNeedFlag();//是否可以按需给出
|
|
||||||
// deviceRequest.setAsNeedReason();//按需使用原因
|
|
||||||
// deviceRequest.setContractCode();//合同id
|
|
||||||
// deviceRequest.setSupportInfo();//支持用药信息
|
|
||||||
// deviceRequest.setRequesterId();//退药id
|
|
||||||
deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json
|
|
||||||
// deviceRequest.setYbClassEnum();//类别医保编码
|
|
||||||
// deviceRequest.setTraceNo()//追溯码
|
|
||||||
deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id
|
|
||||||
deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id
|
|
||||||
// deviceRequest.setBasedOnTable();//请求基于什么
|
|
||||||
deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID
|
|
||||||
deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
return deviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数
|
|
||||||
*
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
* @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库)
|
|
||||||
*/
|
|
||||||
private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId,
|
|
||||||
Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
ServiceRequest serviceRequest = new ServiceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键、状态、业务编号、签发编码
|
|
||||||
serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID)
|
|
||||||
serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效)
|
|
||||||
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
|
|
||||||
serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间
|
|
||||||
serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱)
|
|
||||||
serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(每日唯一)
|
|
||||||
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
|
|
||||||
serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:从DTO提取核心参数
|
|
||||||
serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数)
|
|
||||||
serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程")
|
|
||||||
serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗)
|
|
||||||
serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入)
|
|
||||||
serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION)
|
|
||||||
serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生)
|
|
||||||
serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录)
|
|
||||||
serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间)
|
|
||||||
serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室)
|
|
||||||
serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置)
|
|
||||||
serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销)
|
|
||||||
serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
|
|
||||||
// 保存诊疗活动请求记录到数据库
|
|
||||||
iServiceRequestService.saveOrUpdate(serviceRequest);
|
|
||||||
return serviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项)
|
|
||||||
*
|
|
||||||
* @param serviceRequest 父诊疗活动请求实体(关联子项)
|
|
||||||
* @param chargeItemId 父诊疗活动的费用项ID(关联子项费用)
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含子项配置JSON)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
*/
|
|
||||||
private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto,
|
|
||||||
Long organizationId) {
|
|
||||||
// 1. 查询诊疗活动定义信息(获取子项配置JSON)
|
|
||||||
ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId());
|
|
||||||
String childrenJson = activityDefinition.getChildrenJson();
|
|
||||||
|
|
||||||
// 2. 若存在子项配置,构建子项参数并调用工具类处理
|
|
||||||
if (childrenJson != null) {
|
|
||||||
ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams();
|
|
||||||
// 子项治疗类型:默认临时(与父项一致)
|
|
||||||
activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
|
|
||||||
activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算)
|
|
||||||
activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用)
|
|
||||||
activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项)
|
|
||||||
activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId());
|
|
||||||
// 调用工具类处理子项:递归生成子项的请求、执行记录、费用项
|
|
||||||
adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表)
|
|
||||||
* 通用性:支持耗材、诊疗活动两种类型的费用项构建
|
|
||||||
*
|
|
||||||
* @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等)
|
|
||||||
* @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo)
|
|
||||||
* @param requestId 关联请求的ID(耗材/服务请求的主键)
|
|
||||||
* @param serviceTable 关联服务表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动)
|
|
||||||
* @param curDate 当前操作时间(费用开立时间)
|
|
||||||
* @param procedureId 执行记录ID(关联CLIN_PROCEDURE表)
|
|
||||||
* @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null)
|
|
||||||
* @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null)
|
|
||||||
* @return ChargeItem 构建完成的费用项实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId,
|
|
||||||
String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId,
|
|
||||||
String dispenseTable) {
|
|
||||||
ChargeItem chargeItem = new ChargeItem();
|
|
||||||
// TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算
|
|
||||||
// 基础配置:主键、状态、业务编号
|
|
||||||
chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID)
|
|
||||||
chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费)
|
|
||||||
// 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯)
|
|
||||||
chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo));
|
|
||||||
chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:患者、就诊、定价相关信息
|
|
||||||
chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者)
|
|
||||||
chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗)
|
|
||||||
chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院)
|
|
||||||
chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则)
|
|
||||||
chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价)
|
|
||||||
chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士)
|
|
||||||
chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室)
|
|
||||||
chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间)
|
|
||||||
chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源)
|
|
||||||
chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键)
|
|
||||||
chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型)
|
|
||||||
chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID)
|
|
||||||
chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户)
|
|
||||||
chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录)
|
|
||||||
chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null)
|
|
||||||
chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null)
|
|
||||||
|
|
||||||
// 费用核心属性:数量、单位、单价、总价(与医嘱保持一致)
|
|
||||||
chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致)
|
|
||||||
chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致)
|
|
||||||
chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价)
|
|
||||||
chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算)
|
|
||||||
|
|
||||||
return chargeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材删除相关方法 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表
|
|
||||||
* → 费用项表
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材医嘱列表(可为null)
|
|
||||||
* @param serviceTable 关联服务表名(此处为耗材请求表)
|
|
||||||
*/
|
|
||||||
private void handleDel(List<Long> requestIds, String serviceTable) {
|
|
||||||
// 空列表直接返回,避免无效循环
|
|
||||||
if (requestIds == null || requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
|
|
||||||
checkDeletedDeviceChargeStatus(requestIds);
|
|
||||||
// 软删除执行记录
|
|
||||||
List<Procedure> procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable);
|
|
||||||
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
|
|
||||||
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
// 批量删除执行记录
|
|
||||||
iProcedureService.removeBatchByIds(procedureIds);
|
|
||||||
|
|
||||||
// 不想循环删除
|
|
||||||
for (Long requestId : requestIds) {
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_DEVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iDeviceRequestService.removeById(requestId);
|
|
||||||
// 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE)
|
|
||||||
iDeviceDispenseService.deleteDeviceDispense(requestId);
|
|
||||||
}
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_SERVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iServiceRequestService.removeById(requestId);
|
|
||||||
}
|
|
||||||
// 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联)
|
|
||||||
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材请求ID列表
|
|
||||||
*/
|
|
||||||
private void checkDeletedDeviceChargeStatus(List<Long> requestIds) {
|
|
||||||
if (requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 查询待删除耗材对应的费用项列表
|
|
||||||
List<ChargeItem> chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds);
|
|
||||||
if (chargeItemList == null || chargeItemList.isEmpty()) {
|
|
||||||
return; // 无关联费用项,允许删除
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 校验是否存在已收费项(状态为BILLED)
|
|
||||||
boolean hasBilledItem
|
|
||||||
= chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum()));
|
|
||||||
if (hasBilledItem) {
|
|
||||||
throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 未实现接口方法(保留签名,待扩展)========================
|
|
||||||
/**
|
|
||||||
* 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 划价结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> addOrderBilling() {
|
|
||||||
// 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> deleteOrderBilling() {
|
|
||||||
// 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 修改结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> updateOrderBilling() {
|
|
||||||
// 待实现:需接收修改后的订单划价参数,更新请求记录和费用项
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用明细查询
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @return 住院患者费用明细
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<List<CostDetailDto>> getCostDetails(CostDetailSearchParam costDetailSearchParam,
|
|
||||||
HttpServletRequest request) {
|
|
||||||
List<Long> encounterIds = costDetailSearchParam.getEncounterIds();
|
|
||||||
if (encounterIds == null || encounterIds.isEmpty()) {
|
|
||||||
return R.fail("就诊ID不能为空");
|
|
||||||
}
|
|
||||||
costDetailSearchParam.setEncounterIds(null);
|
|
||||||
QueryWrapper<CostDetailSearchParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request);
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds);
|
|
||||||
List<CostDetailDto> list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(),
|
|
||||||
ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(),
|
|
||||||
EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode());
|
|
||||||
return R.ok(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @param response response响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request,
|
|
||||||
HttpServletResponse response) {
|
|
||||||
R<List<CostDetailDto>> costDetails = getCostDetails(costDetailSearchParam, request);
|
|
||||||
if (costDetails.getData() != null) {
|
|
||||||
List<CostDetailDto> dataList = costDetails.getData();
|
|
||||||
// 设置执行科室
|
|
||||||
dataList.forEach(costDetailDto -> {
|
|
||||||
Long orgId = costDetailDto.getOrgId();
|
|
||||||
costDetailDto.setOrgName(organizationService.getById(orgId).getName());
|
|
||||||
});
|
|
||||||
// 根据EncounterId分组
|
|
||||||
Map<Long, List<CostDetailDto>> map
|
|
||||||
= dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId));
|
|
||||||
map.forEach((key, value) -> {
|
|
||||||
// 新加一条小计
|
|
||||||
value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice(
|
|
||||||
value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
|
||||||
});
|
|
||||||
// 收集要导出的数据
|
|
||||||
List<CostDetailExcelOutDto> excelOutList
|
|
||||||
= map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(),
|
|
||||||
entry.getValue().get(0).getPatientName())).toList();
|
|
||||||
try {
|
|
||||||
// 住院记账-费用明细 导出
|
|
||||||
NewExcelUtil<CostDetailExcelOutDto> util = new NewExcelUtil<>(CostDetailExcelOutDto.class);
|
|
||||||
util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new NonCaptureException(StringUtils.format("导出excel失败"), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,815 +0,0 @@
|
|||||||
package com.openhis.web.inhospitalnursestation.appservice.impl;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.core.common.core.domain.R;
|
|
||||||
import com.core.common.core.domain.model.LoginUser;
|
|
||||||
import com.core.common.exception.NonCaptureException;
|
|
||||||
import com.core.common.exception.ServiceException;
|
|
||||||
import com.core.common.utils.*;
|
|
||||||
import com.openhis.administration.domain.ChargeItem;
|
|
||||||
import com.openhis.administration.dto.CostDetailDto;
|
|
||||||
import com.openhis.administration.dto.CostDetailSearchParam;
|
|
||||||
import com.openhis.administration.service.IChargeItemService;
|
|
||||||
import com.openhis.administration.service.IOrganizationService;
|
|
||||||
import com.openhis.clinical.domain.Procedure;
|
|
||||||
import com.openhis.clinical.service.IProcedureService;
|
|
||||||
import com.openhis.common.constant.CommonConstants;
|
|
||||||
import com.openhis.common.constant.PromptMsgConstant;
|
|
||||||
import com.openhis.common.enums.*;
|
|
||||||
import com.openhis.common.utils.EnumUtils;
|
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
|
||||||
import com.openhis.web.doctorstation.dto.ActivityChildrenJsonParams;
|
|
||||||
import com.openhis.web.doctorstation.dto.AdviceSaveDto;
|
|
||||||
import com.openhis.web.doctorstation.utils.AdviceUtils;
|
|
||||||
import com.openhis.web.inhospitalnursestation.appservice.INurseBillingAppService;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.CostDetailExcelOutDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceDto;
|
|
||||||
import com.openhis.web.inhospitalnursestation.dto.InpatientAdviceParam;
|
|
||||||
import com.openhis.web.inhospitalnursestation.mapper.NurseBillingAppMapper;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.AdviceBatchOpParam;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveDto;
|
|
||||||
import com.openhis.web.regdoctorstation.dto.RegAdviceSaveParam;
|
|
||||||
import com.openhis.workflow.domain.ActivityDefinition;
|
|
||||||
import com.openhis.workflow.domain.DeviceRequest;
|
|
||||||
import com.openhis.workflow.domain.ServiceRequest;
|
|
||||||
import com.openhis.workflow.service.IActivityDefinitionService;
|
|
||||||
import com.openhis.workflow.service.IDeviceDispenseService;
|
|
||||||
import com.openhis.workflow.service.IDeviceRequestService;
|
|
||||||
import com.openhis.workflow.service.IServiceRequestService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院护士站划价服务实现类 核心职责: 1. 临时耗材划价签发(含耗材请求生成、发放记录创建、费用项关联) 2.
|
|
||||||
* 诊疗活动划价签发(含服务请求生成、子项处理、费用项关联) 3. 划价参数校验(非空校验、库房校验等) 4. 已收费项目删除限制(避免误删已结算数据) 5.
|
|
||||||
* 关联数据一致性管理(请求表、执行表、费用表、发放表联动)
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class NurseBillingAppService implements INurseBillingAppService {
|
|
||||||
|
|
||||||
// ======================== 常量定义(关联表名/编码规则)========================
|
|
||||||
/**
|
|
||||||
* 耗材请求服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_DEVICE = CommonConstants.TableName.WOR_DEVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 诊疗活动服务关联表名(用于费用项关联数据源)
|
|
||||||
*/
|
|
||||||
private static final String SERVICE_TABLE_SERVICE = CommonConstants.TableName.WOR_SERVICE_REQUEST;
|
|
||||||
/**
|
|
||||||
* 耗材产品定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_DEVICE = CommonConstants.TableName.ADM_DEVICE_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义表名(用于费用项关联产品信息)
|
|
||||||
*/
|
|
||||||
private static final String PRODUCT_TABLE_ACTIVITY = CommonConstants.TableName.WOR_ACTIVITY_DEFINITION;
|
|
||||||
/**
|
|
||||||
* 耗材发放表名(用于费用项关联发放记录)
|
|
||||||
*/
|
|
||||||
private static final String WOR_DEVICE_DISPENSE = CommonConstants.TableName.WOR_DEVICE_DISPENSE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用项业务编号前缀(统一编码规则,便于追溯)
|
|
||||||
*/
|
|
||||||
private static final String CHARGE_ITEM_BUS_NO_PREFIX = AssignSeqEnum.CHARGE_ITEM_NO.getPrefix();
|
|
||||||
/**
|
|
||||||
* 耗材申请单号序列号长度(按日生成,每日从0001开始递增)
|
|
||||||
*/
|
|
||||||
private static final int DEVICE_RES_NO_SEQ_LENGTH = 4;
|
|
||||||
/**
|
|
||||||
* 医嘱签发编码序列号长度(全局唯一,用于关联同一批次划价的医嘱)
|
|
||||||
*/
|
|
||||||
private static final int ADVICE_SIGN_SEQ_LENGTH = 10;
|
|
||||||
|
|
||||||
// ======================== 依赖注入(业务服务/工具类)========================
|
|
||||||
/**
|
|
||||||
* 诊疗活动定义服务(查询诊疗活动基础信息及子项配置)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
IActivityDefinitionService iActivityDefinitionService;
|
|
||||||
/**
|
|
||||||
* 医嘱处理工具类(含诊疗子项处理等通用逻辑)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AdviceUtils adviceUtils;
|
|
||||||
/**
|
|
||||||
* 序列生成工具类(用于生成业务编号、签发编码等唯一标识)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private AssignSeqUtil assignSeqUtil;
|
|
||||||
/**
|
|
||||||
* 耗材请求服务(CRUD耗材请求记录WOR_DEVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceRequestService iDeviceRequestService;
|
|
||||||
/**
|
|
||||||
* 服务请求服务(CRUD诊疗活动请求记录WOR_SERVICE_REQUEST)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IServiceRequestService iServiceRequestService;
|
|
||||||
/**
|
|
||||||
* 费用项服务(CRUD费用记录ADM_CHARGE_ITEM,含收费状态管理)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IChargeItemService iChargeItemService;
|
|
||||||
/**
|
|
||||||
* 耗材发放服务(生成耗材发放记录WOR_DEVICE_DISPENSE,管理发放状态)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IDeviceDispenseService iDeviceDispenseService;
|
|
||||||
/**
|
|
||||||
* 执行记录服务(生成医嘱执行记录CLIN_PROCEDURE,记录执行状态/人员/时间)
|
|
||||||
*/
|
|
||||||
@Resource
|
|
||||||
private IProcedureService iProcedureService;
|
|
||||||
@Resource
|
|
||||||
private NurseBillingAppMapper nurseBillingAppMapper;
|
|
||||||
@Resource
|
|
||||||
private IOrganizationService organizationService;
|
|
||||||
|
|
||||||
// ======================== 核心业务方法(划价新增)========================
|
|
||||||
/**
|
|
||||||
* 新增住院护士站划价(核心入口方法) 完整流程:参数初始化 → 入参校验 → 医嘱分类 → 生成全局签发编码 → 分类处理划价 → 返回结果
|
|
||||||
* 事务特性:所有操作原子化,任一环节失败则整体回滚(避免数据不一致)
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* 包含:患者ID、就诊ID、住院科室ID、医嘱列表(耗材/诊疗活动)、操作时间等核心数据
|
|
||||||
* @return R<?> 划价结果响应 成功:返回操作成功提示(编码M00002) 失败:返回具体错误信息(如参数缺失、库房为空等)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> addInNurseBilling(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 获取当前登录用户信息(包含护士ID、所属科室等)
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
|
|
||||||
// 1. 时间参数初始化:优先使用入参指定时间,无则默认当前系统时间
|
|
||||||
Date curDate = new Date();
|
|
||||||
Date startTime = regAdviceSaveParam.getStartTime() == null ? curDate : regAdviceSaveParam.getStartTime();
|
|
||||||
Date authoredTime
|
|
||||||
= regAdviceSaveParam.getAuthoredTime() == null ? curDate : regAdviceSaveParam.getAuthoredTime();
|
|
||||||
|
|
||||||
// 2. 入参校验:校验不通过直接返回错误响应(避免后续无效处理)
|
|
||||||
R<?> checkResult = checkNurseBillingParam(regAdviceSaveParam);
|
|
||||||
if (checkResult.getCode() != R.SUCCESS) {
|
|
||||||
return checkResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 提取核心业务参数:住院科室ID(优先入参,无则取登录用户所属科室)
|
|
||||||
Long organizationId = regAdviceSaveParam.getOrganizationId() == null ? loginUser.getOrgId()
|
|
||||||
: regAdviceSaveParam.getOrganizationId();
|
|
||||||
// 待处理医嘱列表(含耗材、诊疗活动两种类型)
|
|
||||||
List<RegAdviceSaveDto> allAdviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
|
|
||||||
// 4. 医嘱分类:按类型拆分为耗材类和诊疗活动类(分别执行不同划价逻辑)
|
|
||||||
List<RegAdviceSaveDto> deviceAdviceList = filterDeviceAdvice(allAdviceList);
|
|
||||||
List<RegAdviceSaveDto> activityAdviceList = filterActivityAdvice(allAdviceList);
|
|
||||||
|
|
||||||
// 5. 生成全局唯一签发编码:关联同一批次划价的所有医嘱(便于追溯)
|
|
||||||
String signCode = assignSeqUtil.getSeq(AssignSeqEnum.ADVICE_SIGN.getPrefix(), ADVICE_SIGN_SEQ_LENGTH);
|
|
||||||
|
|
||||||
// 6. 分类处理划价:耗材类、诊疗活动类分别执行对应逻辑
|
|
||||||
if (!deviceAdviceList.isEmpty()) {
|
|
||||||
handleAddDeviceBilling(deviceAdviceList, signCode, organizationId, curDate);
|
|
||||||
}
|
|
||||||
if (!activityAdviceList.isEmpty()) {
|
|
||||||
handleAddActivityBilling(activityAdviceList, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 划价成功:返回统一成功提示(通过国际化工具类拼接提示信息)
|
|
||||||
return R.ok(null, MessageUtils.createMessage(PromptMsgConstant.Common.M00002, new Object[]{"住院护士划价"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除住院划价记录(核心实现) 流程:参数校验 → 分类筛选医嘱 → 已收费状态校验 → 级联删除关联数据 → 返回结果
|
|
||||||
* 事务特性:所有删除操作原子化,任一环节失败整体回滚
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> deleteInNurseBilling(List<AdviceBatchOpParam> paramList) {
|
|
||||||
|
|
||||||
// TODO 撤销前校验
|
|
||||||
// 诊疗ids
|
|
||||||
List<Long> activityRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
activityRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.ACTIVITY.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
// 耗材ids
|
|
||||||
List<Long> deviceRequestIds = Collections.emptyList();
|
|
||||||
if (paramList != null && !paramList.isEmpty()) {
|
|
||||||
deviceRequestIds = paramList.stream().filter(e -> e != null // 避免单个参数对象为null
|
|
||||||
&& ItemType.DEVICE.getValue().equals(e.getAdviceType()) && e.getRequestId() != null) // 避免requestId为null(按需添加)
|
|
||||||
.map(AdviceBatchOpParam::getRequestId).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
this.handleDel(deviceRequestIds, SERVICE_TABLE_DEVICE);
|
|
||||||
this.handleDel(activityRequestIds, SERVICE_TABLE_SERVICE);
|
|
||||||
return R.ok("删除成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住院患者医嘱查询
|
|
||||||
*
|
|
||||||
* @param inpatientAdviceParam 查询条件
|
|
||||||
* @param pageNo 当前页码
|
|
||||||
* @param pageSize 查询条数
|
|
||||||
* @return 住院患者医
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> getInNurseBillingPage(InpatientAdviceParam inpatientAdviceParam, Integer pageNo, Integer pageSize,
|
|
||||||
LocalDateTime startTime, LocalDateTime endTime) {
|
|
||||||
// 初始化查询参数
|
|
||||||
String encounterIds = inpatientAdviceParam.getEncounterIds();
|
|
||||||
inpatientAdviceParam.setEncounterIds(null);
|
|
||||||
Integer exeStatus = inpatientAdviceParam.getExeStatus();
|
|
||||||
inpatientAdviceParam.setExeStatus(null);
|
|
||||||
// 构建查询条件
|
|
||||||
QueryWrapper<InpatientAdviceParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);
|
|
||||||
|
|
||||||
// 手动拼接住院患者id条件
|
|
||||||
if (encounterIds != null && !encounterIds.isEmpty()) {
|
|
||||||
List<Long> encounterIdList
|
|
||||||
= Arrays.stream(encounterIds.split(CommonConstants.Common.COMMA)).map(Long::parseLong).toList();
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIdList);
|
|
||||||
}
|
|
||||||
// 患者医嘱分页列表
|
|
||||||
Page<InpatientAdviceDto> inpatientAdvicePage
|
|
||||||
= nurseBillingAppMapper.getInNurseBillingPage(new Page<>(pageNo, pageSize), queryWrapper,
|
|
||||||
CommonConstants.TableName.WOR_DEVICE_REQUEST, CommonConstants.TableName.WOR_SERVICE_REQUEST,
|
|
||||||
RequestStatus.DRAFT.getValue(), EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode(),
|
|
||||||
ChargeItemStatus.BILLABLE.getValue(), ChargeItemStatus.BILLED.getValue(),
|
|
||||||
ChargeItemStatus.REFUNDED.getValue(), EncounterClass.IMP.getValue(),
|
|
||||||
GenerateSource.NURSE_PRICING.getValue(), startTime, endTime);
|
|
||||||
inpatientAdvicePage.getRecords().forEach(e -> {
|
|
||||||
// 医嘱类型
|
|
||||||
e.setTherapyEnum_enumText(EnumUtils.getInfoByValue(TherapyTimeType.class, e.getTherapyEnum()));
|
|
||||||
// 请求状态
|
|
||||||
e.setRequestStatus_enumText(EnumUtils.getInfoByValue(RequestStatus.class, e.getRequestStatus()));
|
|
||||||
// 性别枚举
|
|
||||||
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
|
|
||||||
// 计算年龄
|
|
||||||
if (e.getBirthDate() != null) {
|
|
||||||
e.setAge(AgeCalculatorUtil.getAge(e.getBirthDate()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return R.ok(inpatientAdvicePage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 入参校验方法 ========================
|
|
||||||
/**
|
|
||||||
* 划价入参校验(通用校验逻辑) 校验规则:1. 参数非空 2. 住院科室ID非空 3. 医嘱列表非空 4. 隐含库房校验(耗材类单独校验)
|
|
||||||
* 校验失败直接返回错误响应,不进入后续业务逻辑
|
|
||||||
*
|
|
||||||
* @param regAdviceSaveParam 划价请求参数体
|
|
||||||
* @return R<?> 校验结果:成功返回R.ok(),失败返回R.fail(错误信息)
|
|
||||||
*/
|
|
||||||
private R<?> checkNurseBillingParam(RegAdviceSaveParam regAdviceSaveParam) {
|
|
||||||
// 1. 整体参数非空校验:请求体为null直接返回失败
|
|
||||||
if (regAdviceSaveParam == null) {
|
|
||||||
return R.fail("划价请求失败:未获取到有效请求数据,请重新提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 核心字段非空校验:患者住院科室ID不能为空
|
|
||||||
if (regAdviceSaveParam.getOrganizationId() == null) {
|
|
||||||
return R.fail("划价请求失败:患者住院科室信息缺失,请确认患者科室后重试");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 医嘱列表非空校验:必须选择至少一个待划价项目
|
|
||||||
List<RegAdviceSaveDto> adviceList = regAdviceSaveParam.getRegAdviceSaveList();
|
|
||||||
if (adviceList == null || adviceList.isEmpty()) {
|
|
||||||
return R.fail("划价请求失败:未选择任何待划价项目,请添加医嘱后提交");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 库存校验:临时注释(当前需求:划价不校验库存,实际发放时校验)
|
|
||||||
// 若后续需要恢复库存校验,可解除以下注释
|
|
||||||
/*
|
|
||||||
List<AdviceSaveDto> needCheckInventoryList = adviceList.stream()
|
|
||||||
.filter(advice -> TherapyTimeType.TEMPORARY.getValue().equals(advice.getTherapyEnum())
|
|
||||||
&& !DbOpType.DELETE.getCode().equals(advice.getDbOpType())
|
|
||||||
&& !ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
String inventoryTip = adviceUtils.checkInventory(new ArrayList<>(needCheckInventoryList));
|
|
||||||
if (inventoryTip != null) {
|
|
||||||
return R.fail("划价失败:" + inventoryTip + ",请联系库房确认库存或调整申请数量");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// 所有校验通过
|
|
||||||
return R.ok();
|
|
||||||
}
|
|
||||||
// ======================== 医嘱分类工具方法 ========================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选耗材类医嘱 筛选规则:按医嘱类型枚举(ItemType.DEVICE)匹配,仅保留耗材相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的耗材类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterDeviceAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.DEVICE.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 筛选诊疗活动类医嘱 筛选规则:按医嘱类型枚举(ItemType.ACTIVITY)匹配,仅保留诊疗活动相关医嘱
|
|
||||||
*
|
|
||||||
* @param allAdviceList 所有待处理医嘱列表
|
|
||||||
* @return List<RegAdviceSaveDto> 筛选后的诊疗活动类医嘱列表
|
|
||||||
*/
|
|
||||||
private List<RegAdviceSaveDto> filterActivityAdvice(List<RegAdviceSaveDto> allAdviceList) {
|
|
||||||
return allAdviceList.stream().filter(advice -> ItemType.ACTIVITY.getValue().equals(advice.getAdviceType()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材类医嘱划价(核心子流程) 流程:筛选临时耗材 → 校验发放库房 → 生成耗材请求 → 生成执行记录 → 生成耗材发放 → 生成费用项
|
|
||||||
* 特殊规则:临时耗材划价不校验库存、不预减库存(仅记录发放需求,实际发放时扣库)
|
|
||||||
*
|
|
||||||
* @param deviceAdviceList 耗材类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间(用于填充创建时间/执行时间)
|
|
||||||
*/
|
|
||||||
private void handleAddDeviceBilling(List<RegAdviceSaveDto> deviceAdviceList, String signCode, Long organizationId,
|
|
||||||
Date curDate) {
|
|
||||||
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
|
||||||
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常
|
|
||||||
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
|
|
||||||
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
|
||||||
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
|
||||||
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
DeviceRequest deviceRequest = buildDeviceRequest(adviceDto, curDate);
|
|
||||||
iDeviceRequestService.saveOrUpdate(deviceRequest);
|
|
||||||
|
|
||||||
// 3.2 生成医嘱执行记录(CLIN_PROCEDURE):记录执行状态、执行科室、执行时间等
|
|
||||||
Long procedureId = this.addProcedureRecord(deviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
deviceRequest.getPatientId(), // 患者ID
|
|
||||||
deviceRequest.getId(), // 耗材请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_DEVICE, // 关联表名(耗材请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
deviceRequest.getLocationId(), // 执行位置(发放库房)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.3 生成耗材发放记录(WOR_DEVICE_DISPENSE):状态设为待发放,关联执行记录
|
|
||||||
Long dispenseId = iDeviceDispenseService.generateDeviceDispense(deviceRequest, procedureId,
|
|
||||||
deviceRequest.getLocationId(), curDate);
|
|
||||||
|
|
||||||
// 3.4 生成费用项记录(ADM_CHARGE_ITEM):关联耗材请求、发放记录、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, deviceRequest.getBusNo(), deviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_DEVICE, PRODUCT_TABLE_DEVICE, curDate, procedureId, dispenseId, WOR_DEVICE_DISPENSE);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 诊疗活动类划价处理 ========================
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动类医嘱划价(核心子流程) 流程:生成服务请求 → 生成执行记录 → 生成费用项 → 处理诊疗子项(如有)
|
|
||||||
* 特殊规则:诊疗活动可能包含子项(如套餐类活动),需递归处理子项划价
|
|
||||||
*
|
|
||||||
* @param activityAdviceList 诊疗活动类医嘱列表(已筛选)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 患者住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
*/
|
|
||||||
private void handleAddActivityBilling(List<RegAdviceSaveDto> activityAdviceList, String signCode,
|
|
||||||
Long organizationId, Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
// 循环处理每个诊疗活动医嘱
|
|
||||||
for (AdviceSaveDto adviceDto : activityAdviceList) {
|
|
||||||
// 1. 生成诊疗活动请求记录(WOR_SERVICE_REQUEST):状态设为激活,来源标记为护士划价
|
|
||||||
ServiceRequest serviceRequest
|
|
||||||
= this.buildActivityRequest(adviceDto, signCode, organizationId, curDate, startTime, authoredTime);
|
|
||||||
|
|
||||||
// 2. 生成医嘱执行记录(CLIN_PROCEDURE):关联服务请求,记录执行状态
|
|
||||||
Long procedureId = this.addProcedureRecord(serviceRequest.getEncounterId(), // 就诊ID
|
|
||||||
serviceRequest.getPatientId(), // 患者ID
|
|
||||||
serviceRequest.getId(), // 服务请求ID(关联执行记录与请求)
|
|
||||||
SERVICE_TABLE_SERVICE, // 关联表名(服务请求表)
|
|
||||||
EventStatus.COMPLETED, // 执行状态:已完成
|
|
||||||
ProcedureCategory.INPATIENT_NURSE_ADVICE, // 执行种类:住院护士医嘱
|
|
||||||
serviceRequest.getLocationId(), // 执行位置(执行科室)
|
|
||||||
curDate, null, null // 当前时间为执行时间,组号/取消ID为空
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 生成费用项记录(ADM_CHARGE_ITEM):关联服务请求、执行记录,状态设为待结算
|
|
||||||
ChargeItem chargeItem = buildChargeItem(adviceDto, serviceRequest.getBusNo(), serviceRequest.getId(),
|
|
||||||
SERVICE_TABLE_SERVICE, PRODUCT_TABLE_ACTIVITY, curDate, procedureId, null, null);
|
|
||||||
iChargeItemService.saveOrUpdate(chargeItem);
|
|
||||||
|
|
||||||
// 4. 处理诊疗子项(如活动包含子项配置,递归生成子项的划价数据)
|
|
||||||
this.buidActivityRequestChild(serviceRequest, chargeItem.getId(), adviceDto, organizationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 执行记录工具方法 ========================
|
|
||||||
/**
|
|
||||||
* 生成医嘱执行记录(通用方法,支持耗材/诊疗活动两种类型) 功能:调用执行记录服务,创建CLIN_PROCEDURE表记录,关联请求数据与执行信息
|
|
||||||
*
|
|
||||||
* @param encounterId 就诊ID(关联患者就诊记录)
|
|
||||||
* @param patientId 患者ID(关联患者基本信息)
|
|
||||||
* @param requestId 请求ID(关联耗材/服务请求主记录)
|
|
||||||
* @param requestTable 关联表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param eventStatus 执行状态(如已完成、待执行等)
|
|
||||||
* @param procedureCategory 执行种类(如住院护士医嘱、医生医嘱等)
|
|
||||||
* @param locationId 执行位置(执行科室/发放库房ID)
|
|
||||||
* @param exeDate 执行时间
|
|
||||||
* @param groupId 组号(批量执行时用于关联同一组医嘱)
|
|
||||||
* @param refundId 取消执行ID(取消执行时关联原执行记录)
|
|
||||||
* @return Long 执行记录ID(CLIN_PROCEDURE表主键)
|
|
||||||
*/
|
|
||||||
private Long addProcedureRecord(Long encounterId, Long patientId, Long requestId, String requestTable,
|
|
||||||
EventStatus eventStatus, ProcedureCategory procedureCategory, Long locationId, Date exeDate, Long groupId,
|
|
||||||
Long refundId) {
|
|
||||||
// 调用执行记录服务创建记录,返回执行记录主键ID
|
|
||||||
return iProcedureService.addProcedureRecord(encounterId, patientId, requestId, requestTable, eventStatus,
|
|
||||||
procedureCategory, locationId, exeDate, exeDate, groupId, refundId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 实体构建工具方法(请求/费用项)========================
|
|
||||||
/**
|
|
||||||
* 构建耗材请求实体(DeviceRequest) 功能:将医嘱DTO参数映射为耗材请求实体,填充默认配置(状态、业务编号、来源等)
|
|
||||||
*
|
|
||||||
* @param adviceDto 耗材医嘱DTO(含请求参数:数量、单位、耗材ID等)
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @return DeviceRequest 构建完成的耗材请求实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private DeviceRequest buildDeviceRequest(AdviceSaveDto adviceDto, Date curDate) {
|
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
|
||||||
DeviceRequest deviceRequest = new DeviceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键(新增为null,修改为已有ID)、状态、业务编号
|
|
||||||
deviceRequest.setId(adviceDto.getRequestId());
|
|
||||||
deviceRequest.setTenantId(loginUser.getTenantId()); // 显式设置租户ID
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(确保每日唯一)
|
|
||||||
deviceRequest
|
|
||||||
.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), DEVICE_RES_NO_SEQ_LENGTH));
|
|
||||||
// deviceRequest.setPrescriptionNo(null);//处方号
|
|
||||||
// deviceRequest.setActivityId(null);//诊疗ID
|
|
||||||
// deviceRequest.setPackageId(null);//组套id
|
|
||||||
// deviceRequest.setIntentCode(null); // 请求意图
|
|
||||||
deviceRequest.setCategoryEnum(adviceDto.getCategoryEnum()); // 请求类型(枚举,如常规请求)
|
|
||||||
// deviceRequest.setPerformFlag(null);//优先级
|
|
||||||
// deviceRequest.setPriorityEnum(null);//是否停止执行
|
|
||||||
// deviceRequest.setGroupNo(null);//分组编号
|
|
||||||
// deviceRequest.setDeviceTypeCode(null);//器材类型
|
|
||||||
deviceRequest.setQuantity(adviceDto.getQuantity()); // 耗材请求数量
|
|
||||||
deviceRequest.setUnitCode(adviceDto.getUnitCode()); // 单位编码(如"个"、"盒")
|
|
||||||
deviceRequest.setLotNumber(adviceDto.getLotNumber()); // 产品批号(可选,耗材批次管理)
|
|
||||||
deviceRequest.setStatusEnum(RequestStatus.COMPLETED.getValue()); // 状态:已完成(划价即生效)
|
|
||||||
deviceRequest.setDeviceDefId(adviceDto.getAdviceDefinitionId()); // 耗材定义ID(关联ADM_DEVICE_DEFINITION)
|
|
||||||
// deviceRequest.setDeviceSpecifications(null)//器材规格
|
|
||||||
deviceRequest.setRequesterId(
|
|
||||||
adviceDto.getPractitionerId() == null ? loginUser.getPractitionerId() : adviceDto.getPractitionerId());// 请求发起人
|
|
||||||
deviceRequest
|
|
||||||
.setOrgId(adviceDto.getFounderOrgId() == null ? loginUser.getOrgId() : adviceDto.getFounderOrgId());// 请求发起的科室
|
|
||||||
deviceRequest.setLocationId(adviceDto.getLocationId());// 默认器材房
|
|
||||||
deviceRequest.setPerformLocation(adviceDto.getLocationId()); // 发放库房ID(关联耗材发放位置)
|
|
||||||
deviceRequest.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联患者本次住院记录)
|
|
||||||
deviceRequest.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
// deviceRequest.setRateCode(null);//用药频次
|
|
||||||
// deviceRequest.setUseTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseStartTime();//预计使用时间
|
|
||||||
// deviceRequest.setUseEndTime();//预计使用结束时间
|
|
||||||
// deviceRequest.setUseTiming();//预计使用周期时间
|
|
||||||
deviceRequest.setReqAuthoredTime(curDate); // 请求开始时间(当前操作时间)
|
|
||||||
// deviceRequest.setPerformerEnum();//执行人类型
|
|
||||||
// deviceRequest.setPerformerId();//执行人
|
|
||||||
// deviceRequest.setPerformOrgId();//执行科室
|
|
||||||
// deviceRequest.setConditionIdJson(); // 相关诊断
|
|
||||||
// deviceRequest.setObservationIdJson();//相关观测
|
|
||||||
// deviceRequest.setAsNeedFlag();//是否可以按需给出
|
|
||||||
// deviceRequest.setAsNeedReason();//按需使用原因
|
|
||||||
// deviceRequest.setContractCode();//合同id
|
|
||||||
// deviceRequest.setSupportInfo();//支持用药信息
|
|
||||||
// deviceRequest.setRequesterId();//退药id
|
|
||||||
deviceRequest.setContentJson(adviceDto.getContentJson());// 请求内容json
|
|
||||||
// deviceRequest.setYbClassEnum();//类别医保编码
|
|
||||||
// deviceRequest.setTraceNo()//追溯码
|
|
||||||
deviceRequest.setConditionId(adviceDto.getConditionId());// 诊断id
|
|
||||||
deviceRequest.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId());// 就诊诊断id
|
|
||||||
// deviceRequest.setBasedOnTable();//请求基于什么
|
|
||||||
deviceRequest.setBasedOnId(adviceDto.getBasedOnId());// 请求基于什么的ID
|
|
||||||
deviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
return deviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建诊疗活动请求实体(ServiceRequest) 功能:将诊疗活动医嘱DTO映射为服务请求实体,填充默认配置和业务参数
|
|
||||||
*
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含活动ID、数量、执行科室等)
|
|
||||||
* @param signCode 全局签发编码(关联同一批次划价)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
* @param curDate 当前操作时间
|
|
||||||
* @param startTime 医嘱开始时间
|
|
||||||
* @param authoredTime 医嘱签发时间
|
|
||||||
* @return ServiceRequest 构建完成的诊疗活动请求实体(已保存到数据库)
|
|
||||||
*/
|
|
||||||
private ServiceRequest buildActivityRequest(AdviceSaveDto activityDto, String signCode, Long organizationId,
|
|
||||||
Date curDate, Date startTime, Date authoredTime) {
|
|
||||||
ServiceRequest serviceRequest = new ServiceRequest();
|
|
||||||
|
|
||||||
// 基础配置:主键、状态、业务编号、签发编码
|
|
||||||
serviceRequest.setId(activityDto.getRequestId()); // 主键ID(新增为null,修改为已有ID)
|
|
||||||
serviceRequest.setStatusEnum(RequestStatus.ACTIVE.getValue()); // 状态:激活(划价即生效)
|
|
||||||
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId()); // 显式设置租户ID
|
|
||||||
serviceRequest.setAuthoredTime(authoredTime); // 医嘱签发时间
|
|
||||||
serviceRequest.setSignCode(signCode); // 全局签发编码(关联同一批次划价的医嘱)
|
|
||||||
serviceRequest.setOccurrenceStartTime(startTime); // 医嘱开始执行时间
|
|
||||||
// 业务编号:按日生成,前缀+4位序列号(每日唯一)
|
|
||||||
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
|
|
||||||
serviceRequest.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:从DTO提取核心参数
|
|
||||||
serviceRequest.setQuantity(activityDto.getQuantity()); // 请求数量(如诊疗活动执行次数)
|
|
||||||
serviceRequest.setUnitCode(activityDto.getUnitCode()); // 单位编码(如"次"、"疗程")
|
|
||||||
serviceRequest.setCategoryEnum(activityDto.getCategoryEnum()); // 请求类型(枚举,如常规诊疗)
|
|
||||||
serviceRequest.setTherapyEnum(activityDto.getTherapyEnum()); // 治疗类型(如临时、长期,前端传入)
|
|
||||||
serviceRequest.setActivityId(activityDto.getAdviceDefinitionId()); // 诊疗活动定义ID(关联WOR_ACTIVITY_DEFINITION)
|
|
||||||
serviceRequest.setPatientId(activityDto.getPatientId()); // 患者ID(关联患者信息)
|
|
||||||
serviceRequest.setRequesterId(activityDto.getPractitionerId()); // 开方医生ID(诊疗活动的开单医生)
|
|
||||||
serviceRequest.setEncounterId(activityDto.getEncounterId()); // 就诊ID(关联本次住院记录)
|
|
||||||
serviceRequest.setAuthoredTime(curDate); // 请求签发时间(当前操作时间)
|
|
||||||
serviceRequest.setOrgId(activityDto.getPositionId()); // 执行科室ID(诊疗活动的执行科室)
|
|
||||||
serviceRequest.setContentJson(activityDto.getContentJson()); // 扩展信息JSON(额外配置)
|
|
||||||
serviceRequest.setYbClassEnum(activityDto.getYbClassEnum()); // 医保类别编码(关联医保报销)
|
|
||||||
serviceRequest.setConditionId(activityDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
serviceRequest.setEncounterDiagnosisId(activityDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
|
|
||||||
// 保存诊疗活动请求记录到数据库
|
|
||||||
iServiceRequestService.saveOrUpdate(serviceRequest);
|
|
||||||
return serviceRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理诊疗活动子项划价(如诊疗活动是套餐,包含多个子项) 功能:解析诊疗活动的子项配置JSON,递归生成子项的划价数据(请求、执行记录、费用项)
|
|
||||||
*
|
|
||||||
* @param serviceRequest 父诊疗活动请求实体(关联子项)
|
|
||||||
* @param chargeItemId 父诊疗活动的费用项ID(关联子项费用)
|
|
||||||
* @param activityDto 诊疗活动医嘱DTO(含子项配置JSON)
|
|
||||||
* @param organizationId 住院科室ID
|
|
||||||
*/
|
|
||||||
private void buidActivityRequestChild(ServiceRequest serviceRequest, Long chargeItemId, AdviceSaveDto activityDto,
|
|
||||||
Long organizationId) {
|
|
||||||
// 1. 查询诊疗活动定义信息(获取子项配置JSON)
|
|
||||||
ActivityDefinition activityDefinition = iActivityDefinitionService.getById(activityDto.getAdviceDefinitionId());
|
|
||||||
String childrenJson = activityDefinition.getChildrenJson();
|
|
||||||
|
|
||||||
// 2. 若存在子项配置,构建子项参数并调用工具类处理
|
|
||||||
if (childrenJson != null) {
|
|
||||||
ActivityChildrenJsonParams activityChildrenJsonParams = new ActivityChildrenJsonParams();
|
|
||||||
// 子项治疗类型:默认临时(与父项一致)
|
|
||||||
activityChildrenJsonParams.setTherapyEnum(TherapyTimeType.TEMPORARY.getValue());
|
|
||||||
activityChildrenJsonParams.setPatientId(serviceRequest.getPatientId()); // 患者ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setEncounterId(serviceRequest.getEncounterId()); // 就诊ID(继承父项)
|
|
||||||
activityChildrenJsonParams.setAccountId(activityDto.getAccountId()); // 患者账户ID(关联费用结算)
|
|
||||||
activityChildrenJsonParams.setChargeItemId(chargeItemId); // 父费用项ID(关联子项费用)
|
|
||||||
activityChildrenJsonParams.setParentId(serviceRequest.getId()); // 父诊疗请求ID(关联子项与父项)
|
|
||||||
activityChildrenJsonParams.setEncounterDiagnosisId(serviceRequest.getEncounterDiagnosisId());
|
|
||||||
// 调用工具类处理子项:递归生成子项的请求、执行记录、费用项
|
|
||||||
adviceUtils.handleActivityChild(childrenJson, organizationId, activityChildrenJsonParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建费用项实体(ChargeItem) 功能:关联请求记录(耗材/诊疗)与费用信息,生成待结算的费用项(ADM_CHARGE_ITEM表)
|
|
||||||
* 通用性:支持耗材、诊疗活动两种类型的费用项构建
|
|
||||||
*
|
|
||||||
* @param adviceDto 医嘱DTO(含费用相关参数:单价、总价等)
|
|
||||||
* @param requestBusNo 关联请求的业务编号(耗材/服务请求的busNo)
|
|
||||||
* @param requestId 关联请求的ID(耗材/服务请求的主键)
|
|
||||||
* @param serviceTable 关联服务表名(标记是耗材请求还是服务请求)
|
|
||||||
* @param productTable 关联产品表名(标记产品类型:耗材/诊疗活动)
|
|
||||||
* @param curDate 当前操作时间(费用开立时间)
|
|
||||||
* @param procedureId 执行记录ID(关联CLIN_PROCEDURE表)
|
|
||||||
* @param dispenseId 发放记录ID(关联耗材发放表,诊疗活动为null)
|
|
||||||
* @param dispenseTable 发放表名(耗材为WOR_DEVICE_DISPENSE,诊疗活动为null)
|
|
||||||
* @return ChargeItem 构建完成的费用项实体(可直接保存)
|
|
||||||
*/
|
|
||||||
private ChargeItem buildChargeItem(AdviceSaveDto adviceDto, String requestBusNo, Long requestId,
|
|
||||||
String serviceTable, String productTable, Date curDate, Long procedureId, Long dispenseId,
|
|
||||||
String dispenseTable) {
|
|
||||||
ChargeItem chargeItem = new ChargeItem();
|
|
||||||
// TODO1、是否需跨批次 2、金额 精确到小数点后6位 、数量 计算
|
|
||||||
// 基础配置:主键、状态、业务编号
|
|
||||||
chargeItem.setId(adviceDto.getChargeItemId()); // 费用项ID(新增为null,修改为已有ID)
|
|
||||||
chargeItem.setStatusEnum(ChargeItemStatus.BILLABLE.getValue()); // 状态:待结算(未收费)
|
|
||||||
// 业务编号:费用项前缀+关联请求的业务编号(确保与请求一一对应,便于追溯)
|
|
||||||
chargeItem.setBusNo(CHARGE_ITEM_BUS_NO_PREFIX.concat(requestBusNo));
|
|
||||||
chargeItem.setGenerateSourceEnum(GenerateSource.NURSE_PRICING.getValue()); // 生成来源:护士划价
|
|
||||||
|
|
||||||
// 业务属性映射:患者、就诊、定价相关信息
|
|
||||||
chargeItem.setPatientId(adviceDto.getPatientId()); // 患者ID(关联患者)
|
|
||||||
chargeItem.setContextEnum(adviceDto.getAdviceType()); // 费用类型(与医嘱类型一致:耗材/诊疗)
|
|
||||||
chargeItem.setEncounterId(adviceDto.getEncounterId()); // 就诊ID(关联本次住院)
|
|
||||||
chargeItem.setDefinitionId(adviceDto.getDefinitionId()); // 费用定价ID(关联定价规则)
|
|
||||||
chargeItem.setDefDetailId(adviceDto.getDefinitionDetailId()); // 定价子表ID(明细定价,如规格对应的单价)
|
|
||||||
chargeItem.setEntererId(adviceDto.getPractitionerId()); // 开立人ID(开方医生/护士)
|
|
||||||
chargeItem.setRequestingOrgId(SecurityUtils.getLoginUser().getOrgId()); // 开立科室ID(当前登录用户科室)
|
|
||||||
chargeItem.setEnteredDate(curDate); // 开立时间(当前操作时间)
|
|
||||||
chargeItem.setServiceTable(serviceTable); // 关联服务表名(标记数据源)
|
|
||||||
chargeItem.setServiceId(requestId); // 关联服务ID(耗材/服务请求的主键)
|
|
||||||
chargeItem.setProductTable(productTable); // 关联产品表名(标记产品类型)
|
|
||||||
chargeItem.setProductId(adviceDto.getAdviceDefinitionId()); // 产品ID(耗材/诊疗活动定义ID)
|
|
||||||
chargeItem.setAccountId(adviceDto.getAccountId()); // 患者账户ID(关联费用结算账户)
|
|
||||||
chargeItem.setConditionId(adviceDto.getConditionId()); // 诊断ID(关联患者诊断)
|
|
||||||
chargeItem.setEncounterDiagnosisId(adviceDto.getEncounterDiagnosisId()); // 就诊诊断ID(本次就诊具体诊断)
|
|
||||||
chargeItem.setProductId(procedureId); // 执行记录ID(关联执行记录)
|
|
||||||
chargeItem.setDispenseId(dispenseId); // 发放记录ID(耗材专属,诊疗活动为null)
|
|
||||||
chargeItem.setDispenseTable(dispenseTable); // 发放表名(耗材专属,诊疗活动为null)
|
|
||||||
|
|
||||||
// 费用核心属性:数量、单位、单价、总价(与医嘱保持一致)
|
|
||||||
chargeItem.setQuantityValue(adviceDto.getQuantity()); // 数量(与请求数量一致)
|
|
||||||
chargeItem.setQuantityUnit(adviceDto.getUnitCode()); // 单位(与请求单位一致)
|
|
||||||
chargeItem.setUnitPrice(adviceDto.getUnitPrice()); // 单价(从DTO传入,已定价)
|
|
||||||
chargeItem.setTotalPrice(adviceDto.getTotalPrice()); // 总价(数量×单价,DTO已计算,避免重复计算)
|
|
||||||
|
|
||||||
return chargeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 耗材删除相关方法 ========================
|
|
||||||
/**
|
|
||||||
* 处理耗材删除逻辑(级联删除关联数据) 核心规则:已收费的耗材项目不允许删除,未收费项目级联删除关联数据 级联删除顺序:耗材请求表 → 耗材发放表
|
|
||||||
* → 费用项表
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材医嘱列表(可为null)
|
|
||||||
* @param serviceTable 关联服务表名(此处为耗材请求表)
|
|
||||||
*/
|
|
||||||
private void handleDel(List<Long> requestIds, String serviceTable) {
|
|
||||||
// 空列表直接返回,避免无效循环
|
|
||||||
if (requestIds == null || requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 1. 校验:待删除项是否已收费,已收费则抛出异常阻止删除
|
|
||||||
checkDeletedDeviceChargeStatus(requestIds);
|
|
||||||
// 软删除执行记录
|
|
||||||
List<Procedure> procedureList = iProcedureService.getProcedureRecords(requestIds, serviceTable);
|
|
||||||
List<Long> procedureIds = procedureList.stream().filter(Objects::nonNull) // 过滤掉null的Procedure对象
|
|
||||||
.map(Procedure::getId).filter(Objects::nonNull) // 过滤掉id为null的记录(按需添加)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
// 批量删除执行记录
|
|
||||||
iProcedureService.removeBatchByIds(procedureIds);
|
|
||||||
|
|
||||||
// 不想循环删除
|
|
||||||
for (Long requestId : requestIds) {
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_DEVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iDeviceRequestService.removeById(requestId);
|
|
||||||
// 删除关联的耗材发放记录(WOR_DEVICE_DISPENSE)
|
|
||||||
iDeviceDispenseService.deleteDeviceDispense(requestId);
|
|
||||||
}
|
|
||||||
if (serviceTable.equals(SERVICE_TABLE_SERVICE)) {
|
|
||||||
// 删除耗材请求主记录(WOR_DEVICE_REQUEST)
|
|
||||||
iServiceRequestService.removeById(requestId);
|
|
||||||
}
|
|
||||||
// 删除关联的费用项记录(ADM_CHARGE_ITEM,按服务表+服务ID关联)
|
|
||||||
iChargeItemService.deleteByServiceTableAndId(serviceTable, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验待删除耗材的费用状态(防止删除已收费项目) 逻辑:查询待删除耗材对应的费用项,若存在"已收费"状态则抛出业务异常
|
|
||||||
*
|
|
||||||
* @param requestIds 待删除的耗材请求ID列表
|
|
||||||
*/
|
|
||||||
private void checkDeletedDeviceChargeStatus(List<Long> requestIds) {
|
|
||||||
if (requestIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 查询待删除耗材对应的费用项列表
|
|
||||||
List<ChargeItem> chargeItemList = iChargeItemService.getChargeItemInfoByReqId(requestIds);
|
|
||||||
if (chargeItemList == null || chargeItemList.isEmpty()) {
|
|
||||||
return; // 无关联费用项,允许删除
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 校验是否存在已收费项(状态为BILLED)
|
|
||||||
boolean hasBilledItem
|
|
||||||
= chargeItemList.stream().anyMatch(ci -> ChargeItemStatus.BILLED.getValue().equals(ci.getStatusEnum()));
|
|
||||||
if (hasBilledItem) {
|
|
||||||
throw new ServiceException("删除失败:部分项目已完成收费(结算),不支持直接删除,请联系收费人员处理后重试");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 未实现接口方法(保留签名,待扩展)========================
|
|
||||||
/**
|
|
||||||
* 新增订单划价(待实现) 功能:针对订单类型的划价(如患者自主购买耗材/服务),生成对应的费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 划价结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> addOrderBilling() {
|
|
||||||
// 待实现:需接收订单相关参数,构建订单划价逻辑(类似耗材/诊疗划价,差异在于来源类型)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除订单划价(待实现) 功能:删除未收费的订单划价记录,级联删除关联数据
|
|
||||||
*
|
|
||||||
* @return R<?> 删除结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> deleteOrderBilling() {
|
|
||||||
// 待实现:类似住院划价删除逻辑,需校验订单划价的费用状态
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改订单划价(待实现) 功能:支持修改未收费订单划价的数量、单价等信息,同步更新费用项
|
|
||||||
*
|
|
||||||
* @return R<?> 修改结果响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<?> updateOrderBilling() {
|
|
||||||
// 待实现:需接收修改后的订单划价参数,更新请求记录和费用项
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用明细查询
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @return 住院患者费用明细
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public R<List<CostDetailDto>> getCostDetails(CostDetailSearchParam costDetailSearchParam,
|
|
||||||
HttpServletRequest request) {
|
|
||||||
List<Long> encounterIds = costDetailSearchParam.getEncounterIds();
|
|
||||||
if (encounterIds == null || encounterIds.isEmpty()) {
|
|
||||||
return R.fail("就诊ID不能为空");
|
|
||||||
}
|
|
||||||
costDetailSearchParam.setEncounterIds(null);
|
|
||||||
QueryWrapper<CostDetailSearchParam> queryWrapper
|
|
||||||
= HisQueryUtils.buildQueryWrapper(costDetailSearchParam, null, null, request);
|
|
||||||
queryWrapper.in(CommonConstants.FieldName.EncounterId, encounterIds);
|
|
||||||
List<CostDetailDto> list = iChargeItemService.getCostDetails(queryWrapper, ChargeItemStatus.BILLABLE.getValue(),
|
|
||||||
ChargeItemStatus.BILLED.getValue(), ChargeItemStatus.REFUNDED.getValue(),
|
|
||||||
EncounterActivityStatus.ACTIVE.getValue(), LocationForm.BED.getValue(),
|
|
||||||
ParticipantType.ADMITTING_DOCTOR.getCode(), AccountType.PERSONAL_CASH_ACCOUNT.getCode());
|
|
||||||
return R.ok(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param costDetailSearchParam 查询条件
|
|
||||||
* @param request request请求
|
|
||||||
* @param response response响应
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void makeExcelFile(CostDetailSearchParam costDetailSearchParam, HttpServletRequest request,
|
|
||||||
HttpServletResponse response) {
|
|
||||||
R<List<CostDetailDto>> costDetails = getCostDetails(costDetailSearchParam, request);
|
|
||||||
if (costDetails.getData() != null) {
|
|
||||||
List<CostDetailDto> dataList = costDetails.getData();
|
|
||||||
// 设置执行科室
|
|
||||||
dataList.forEach(costDetailDto -> {
|
|
||||||
Long orgId = costDetailDto.getOrgId();
|
|
||||||
costDetailDto.setOrgName(organizationService.getById(orgId).getName());
|
|
||||||
});
|
|
||||||
// 根据EncounterId分组
|
|
||||||
Map<Long, List<CostDetailDto>> map
|
|
||||||
= dataList.stream().collect(Collectors.groupingBy(CostDetailDto::getEncounterId));
|
|
||||||
map.forEach((key, value) -> {
|
|
||||||
// 新加一条小计
|
|
||||||
value.add(new CostDetailDto().setEncounterId(key).setChargeName("小计").setTotalPrice(
|
|
||||||
value.stream().map(CostDetailDto::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
|
||||||
});
|
|
||||||
// 收集要导出的数据
|
|
||||||
List<CostDetailExcelOutDto> excelOutList
|
|
||||||
= map.entrySet().stream().map(entry -> new CostDetailExcelOutDto(entry.getKey(), entry.getValue(),
|
|
||||||
entry.getValue().get(0).getPatientName())).toList();
|
|
||||||
try {
|
|
||||||
// 住院记账-费用明细 导出
|
|
||||||
NewExcelUtil<CostDetailExcelOutDto> util = new NewExcelUtil<>(CostDetailExcelOutDto.class);
|
|
||||||
util.exportExcel(response, excelOutList, CommonConstants.SheetName.COST_DETAILS);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new NonCaptureException(StringUtils.format("导出excel失败"), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,7 @@ import com.openhis.workflow.service.IActivityDefinitionService;
|
|||||||
import com.openhis.workflow.service.IServiceRequestService;
|
import com.openhis.workflow.service.IServiceRequestService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -40,6 +41,7 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public class RequestFormManageAppServiceImpl implements IRequestFormManageAppService {
|
public class RequestFormManageAppServiceImpl implements IRequestFormManageAppService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -71,6 +73,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
|
public R<?> saveRequestForm(RequestFormSaveDto requestFormSaveDto, String typeCode) {
|
||||||
// 诊疗处方号
|
// 诊疗处方号
|
||||||
String prescriptionNo;
|
String prescriptionNo;
|
||||||
@@ -330,7 +333,7 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
|
|||||||
surgeryChargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(surgeryServiceRequest.getBusNo()));
|
surgeryChargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(surgeryServiceRequest.getBusNo()));
|
||||||
surgeryChargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
surgeryChargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
|
||||||
surgeryChargeItem.setPatientId(patientId);
|
surgeryChargeItem.setPatientId(patientId);
|
||||||
surgeryChargeItem.setContextEnum(6); // 6-手术
|
surgeryChargeItem.setContextEnum(3); // 3-项目(手术属于诊疗项目)
|
||||||
surgeryChargeItem.setEncounterId(encounterId);
|
surgeryChargeItem.setEncounterId(encounterId);
|
||||||
surgeryChargeItem.setEntererId(practitionerId);
|
surgeryChargeItem.setEntererId(practitionerId);
|
||||||
surgeryChargeItem.setEnteredDate(curDate);
|
surgeryChargeItem.setEnteredDate(curDate);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package com.openhis.web.reportmanage.dto;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +13,8 @@ import java.math.BigDecimal;
|
|||||||
* @author yuxj
|
* @author yuxj
|
||||||
* @date 2025/8/25
|
* @date 2025/8/25
|
||||||
*/
|
*/
|
||||||
@Data
|
@Getter
|
||||||
|
@Setter
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class InpatientMedicalRecordHomePageCollectionDto {
|
public class InpatientMedicalRecordHomePageCollectionDto {
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,32 @@ import cn.hutool.core.util.ObjectUtil;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.CallRecord;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
||||||
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
import com.openhis.triageandqueuemanage.domain.TriageQueueItem;
|
||||||
|
import com.openhis.triageandqueuemanage.service.CallRecordService;
|
||||||
|
import com.openhis.triageandqueuemanage.service.DivLogService;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||||
|
import com.openhis.appointmentmanage.domain.ScheduleSlot;
|
||||||
|
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
|
||||||
|
import com.openhis.common.enums.CallType;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.*;
|
import com.openhis.web.triageandqueuemanage.dto.*;
|
||||||
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
import com.openhis.web.triageandqueuemanage.sse.CallNumberSseManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import com.core.common.core.domain.model.LoginUser;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
||||||
|
|
||||||
@@ -46,6 +56,15 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TriageCandidateExclusionService triageCandidateExclusionService;
|
private TriageCandidateExclusionService triageCandidateExclusionService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DivLogService divLogService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CallRecordService callRecordService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ScheduleSlotMapper scheduleSlotMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> list(Long organizationId, LocalDate date) {
|
public R<?> list(Long organizationId, LocalDate date) {
|
||||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
@@ -65,6 +84,15 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<TriageQueueItem> list = triageQueueItemService.list(wrapper);
|
List<TriageQueueItem> list = triageQueueItemService.list(wrapper);
|
||||||
|
// 通过 slotId 查询 seqNo 并填充到返回结果中(用于选呼显示预约号)
|
||||||
|
if (list != null && !list.isEmpty()) {
|
||||||
|
Map<Long, Integer> seqNoMap = buildSlotSeqNoMap(list);
|
||||||
|
list.forEach(item -> {
|
||||||
|
if (item.getSlotId() != null && seqNoMap.containsKey(item.getSlotId())) {
|
||||||
|
item.setSeqNo(seqNoMap.get(item.getSlotId()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
|
// 双重保险:再次过滤掉 COMPLETED 状态的患者(防止数据库中有异常数据)
|
||||||
if (list != null && !list.isEmpty()) {
|
if (list != null && !list.isEmpty()) {
|
||||||
@@ -72,24 +100,17 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
list = list.stream()
|
list = list.stream()
|
||||||
.filter(item -> !STATUS_COMPLETED.equals(item.getStatus()))
|
.filter(item -> !STATUS_COMPLETED.equals(item.getStatus()))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
if (beforeSize != list.size()) {
|
|
||||||
System.out.println(">>> [TriageQueue] list() 警告:过滤掉了 " + (beforeSize - list.size()) + " 条 COMPLETED 状态的记录");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调试日志:检查状态值
|
|
||||||
if (list != null && !list.isEmpty()) {
|
|
||||||
System.out.println(">>> [TriageQueue] list() 返回 " + list.size() + " 条记录(已排除 COMPLETED)");
|
|
||||||
for (int i = 0; i < Math.min(3, list.size()); i++) {
|
|
||||||
TriageQueueItem item = list.get(i);
|
|
||||||
System.out.println(" [" + i + "] patientName=" + item.getPatientName()
|
|
||||||
+ ", status=" + item.getStatus()
|
|
||||||
+ ", organizationId=" + item.getOrganizationId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return R.ok(list);
|
return R.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将候选池患者的挂号移入队列操作
|
||||||
|
* 并同时写入移入队列操作日志
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> add(TriageQueueAddReq req) {
|
public R<?> add(TriageQueueAddReq req) {
|
||||||
@@ -133,6 +154,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
||||||
.setPoolId(it.getPoolId()) // ✅ 号源池ID(用于div_log审计)
|
.setPoolId(it.getPoolId()) // ✅ 号源池ID(用于div_log审计)
|
||||||
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID(用于div_log审计)
|
.setSlotId(it.getSlotId()) // ✅ 号源槽位ID(用于div_log审计)
|
||||||
|
.setSeqNo(it.getSeqNo()) // ✅ 预约序号(用于叫号显示)
|
||||||
.setStatus(STATUS_WAITING)
|
.setStatus(STATUS_WAITING)
|
||||||
.setQueueOrder(++maxOrder)
|
.setQueueOrder(++maxOrder)
|
||||||
.setDeleteFlag("0")
|
.setDeleteFlag("0")
|
||||||
@@ -140,7 +162,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.setUpdateTime(LocalDateTime.now());
|
.setUpdateTime(LocalDateTime.now());
|
||||||
|
|
||||||
triageQueueItemService.save(qi);
|
triageQueueItemService.save(qi);
|
||||||
|
// 写入分诊日志
|
||||||
|
writeDivLog(it.getPoolId(), it.getSlotId(), "ADD_QUEUE");
|
||||||
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
|
// 记录到候选池排除列表(避免刷新后重新出现在候选池)
|
||||||
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
|
TriageCandidateExclusion exclusion = triageCandidateExclusionService.getOne(
|
||||||
new LambdaQueryWrapper<TriageCandidateExclusion>()
|
new LambdaQueryWrapper<TriageCandidateExclusion>()
|
||||||
@@ -171,17 +194,26 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
return R.ok(added);
|
return R.ok(added);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除队列操作并同时写入移除操作日志
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> remove(Long id) {
|
public R<?> remove(Long id) {
|
||||||
if (id == null) return R.fail("id 不能为空");
|
if (id == null) return R.fail("id 不能为空");
|
||||||
TriageQueueItem item = triageQueueItemService.getById(id);
|
TriageQueueItem item = triageQueueItemService.getById(id);
|
||||||
if (item == null) return R.fail("队列项不存在");
|
if (item == null) return R.fail("队列项不存在");
|
||||||
|
if (item.getStatus() != null && item.getStatus() != 0) {
|
||||||
|
return R.fail("仅等待状态的患者可移出队列,当前状态码:" + item.getStatus());
|
||||||
|
}
|
||||||
// 逻辑删除队列项
|
// 逻辑删除队列项
|
||||||
item.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
|
item.setDeleteFlag("1").setUpdateTime(LocalDateTime.now());
|
||||||
triageQueueItemService.updateById(item);
|
triageQueueItemService.updateById(item);
|
||||||
|
// 写入分诊日志
|
||||||
|
writeDivLog(item.getPoolId(), item.getSlotId(), "REMOVE_QUEUE");
|
||||||
// 从排除列表中删除记录,使患者重新出现在候选池中
|
// 从排除列表中删除记录,使患者重新出现在候选池中
|
||||||
Integer tenantId = item.getTenantId();
|
Integer tenantId = item.getTenantId();
|
||||||
LocalDate exclusionDate = item.getQueueDate();
|
LocalDate exclusionDate = item.getQueueDate();
|
||||||
@@ -239,6 +271,12 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能分诊选呼功能,在进行选呼操作时同时写入叫号记录和选呼操作日志
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> call(TriageQueueActionReq req) {
|
public R<?> call(TriageQueueActionReq req) {
|
||||||
@@ -253,7 +291,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
|
// 叫号后推送 SSE 消息(实时通知显示屏刷新)
|
||||||
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
|
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
|
||||||
|
// 写入分诊日志和叫号记录
|
||||||
|
writeDivLog(selected.getPoolId(), selected.getSlotId(), "CALL");
|
||||||
|
writeCallRecord(selected.getId(), selected.getPractitionerId(), CallType.CALL, selected.getRoomNo());
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
||||||
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
||||||
@@ -264,6 +304,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能分诊完成操作
|
||||||
|
* 在进行完成操作后同时写入叫号记录和完成操作日志
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> complete(TriageQueueActionReq req) {
|
public R<?> complete(TriageQueueActionReq req) {
|
||||||
@@ -283,7 +330,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
} else {
|
} else {
|
||||||
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
||||||
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
|
Long orgId = req != null && req.getOrganizationId() != null ? req.getOrganizationId() : null;
|
||||||
System.out.println(">>> [TriageQueue] complete() 开始执行(不限制日期,通过查询条件), tenantId=" + tenantId + ", orgId=" + orgId);
|
|
||||||
|
|
||||||
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
LambdaQueryWrapper<TriageQueueItem> callingWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||||
@@ -298,8 +344,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
calling = triageQueueItemService.getOne(callingWrapper, false);
|
calling = triageQueueItemService.getOne(callingWrapper, false);
|
||||||
|
|
||||||
System.out.println(">>> [TriageQueue] complete() 查询叫号中患者(不限制日期): orgId=" + orgId + ", 结果=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null"));
|
|
||||||
|
|
||||||
if (calling == null) {
|
if (calling == null) {
|
||||||
return R.fail("当前没有叫号中的患者");
|
return R.fail("当前没有叫号中的患者");
|
||||||
}
|
}
|
||||||
@@ -329,8 +373,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||||
|
|
||||||
System.out.println(">>> [TriageQueue] complete() 查询等待患者(不限制日期): actualOrgId=" + actualOrgId + ", 结果=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ")" : "null"));
|
|
||||||
|
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
next.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
next.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
||||||
triageQueueItemService.updateById(next);
|
triageQueueItemService.updateById(next);
|
||||||
@@ -340,17 +382,46 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
// 完成后推送 SSE 消息(实时通知显示屏刷新)
|
// 完成后推送 SSE 消息(实时通知显示屏刷新)
|
||||||
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
// 写入分诊日志和叫号记录
|
||||||
|
writeDivLog(calling.getPoolId(), calling.getSlotId(), "COMPLETE");
|
||||||
|
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.COMPLETE, calling.getRoomNo());
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能队列重排序功能操作单元
|
||||||
|
* 在进行队列重排序时同时在div_log表中写入重排序日志和叫号记录
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> requeue(TriageQueueActionReq req) {
|
public R<?> requeue(TriageQueueActionReq req) {
|
||||||
|
return doRequeue(req, "REQUEUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能分诊跳过功能操作单元内部包含将队列正在叫号状态的号进行跳过
|
||||||
|
* 同时将跳过操作日志写入div_log表中,以及写入叫号记录表。
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public R<?> skip(TriageQueueActionReq req) {
|
||||||
|
// 当前业务"跳过"按"过号重排"处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
|
||||||
|
return doRequeue(req, "SKIP");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过号重排/跳过的核心逻辑
|
||||||
|
* @param action 日志动作:REQUEUE 或 SKIP
|
||||||
|
*/
|
||||||
|
private R<?> doRequeue(TriageQueueActionReq req, String action) {
|
||||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
TriageQueueItem calling = null;
|
TriageQueueItem calling = null;
|
||||||
|
|
||||||
|
|
||||||
if (req != null && req.getId() != null) {
|
if (req != null && req.getId() != null) {
|
||||||
calling = triageQueueItemService.getById(req.getId());
|
calling = triageQueueItemService.getById(req.getId());
|
||||||
if (calling == null) {
|
if (calling == null) {
|
||||||
@@ -358,7 +429,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
}
|
}
|
||||||
// 验证状态
|
// 验证状态
|
||||||
if (!STATUS_CALLING.equals(calling.getStatus())) {
|
if (!STATUS_CALLING.equals(calling.getStatus())) {
|
||||||
return R.fail("只能对\"叫号中\"状态的患者进行过号重排,当前患者状态为:" + calling.getStatus());
|
return R.fail("只能对\"叫号中\"状态的患者进行" + ("SKIP".equals(action) ? "跳过" : "过号重排") + ",当前患者状态为:" + calling.getStatus());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
// 如果没有提供 id,通过查询条件查找(兼容旧逻辑)
|
||||||
@@ -383,8 +454,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
// 使用实际找到的科室ID
|
// 使用实际找到的科室ID
|
||||||
Long actualOrgId = calling.getOrganizationId();
|
Long actualOrgId = calling.getOrganizationId();
|
||||||
|
|
||||||
// 关键改进:在执行"跳过"操作之前,先检查是否有等待中的患者(判断队列状态)
|
// 关键改进:在执行跳过/重排操作之前,先检查是否有等待中的患者(判断队列状态)
|
||||||
// 如果没有等待中的患者,就不应该执行"过号重排"操作
|
|
||||||
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
LambdaQueryWrapper<TriageQueueItem> nextWrapper = new LambdaQueryWrapper<TriageQueueItem>()
|
||||||
.eq(TriageQueueItem::getTenantId, tenantId)
|
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||||
.eq(TriageQueueItem::getDeleteFlag, "0")
|
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||||
@@ -394,21 +464,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||||
.last("LIMIT 1");
|
.last("LIMIT 1");
|
||||||
|
|
||||||
// 如果指定了科室ID,则按科室过滤;否则查询所有科室(全科模式)
|
|
||||||
if (actualOrgId != null) {
|
if (actualOrgId != null) {
|
||||||
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
|
nextWrapper.eq(TriageQueueItem::getOrganizationId, actualOrgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||||
|
|
||||||
// 调试日志:检查查询结果
|
|
||||||
System.out.println(">>> [TriageQueue] requeue() 查询等待中的患者:");
|
|
||||||
System.out.println(">>> - 科室ID: " + actualOrgId);
|
|
||||||
System.out.println(">>> - 找到的等待患者: " + (next != null ? next.getPatientName() + " (状态: " + next.getStatus() + ")" : "null"));
|
|
||||||
|
|
||||||
// 如果找不到等待中的患者,直接返回失败(不执行跳过操作)
|
|
||||||
if (next == null) {
|
if (next == null) {
|
||||||
System.out.println(">>> [TriageQueue] requeue() 失败:没有等待中的患者");
|
|
||||||
return R.fail("当前没有等待中的患者");
|
return R.fail("当前没有等待中的患者");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,28 +495,21 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
triageQueueItemService.updateById(next);
|
triageQueueItemService.updateById(next);
|
||||||
|
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
|
// 推送 SSE 消息
|
||||||
// ✅ 过号重排后推送 SSE 消息(实时通知显示屏刷新)
|
|
||||||
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
// 写入分诊日志和叫号记录
|
||||||
|
writeDivLog(calling.getPoolId(), calling.getSlotId(), action);
|
||||||
|
writeCallRecord(calling.getId(), calling.getPractitionerId(),
|
||||||
|
"SKIP".equals(action) ? CallType.SKIP : CallType.REQUEUE, calling.getRoomNo());
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public R<?> skip(TriageQueueActionReq req) {
|
|
||||||
// 当前业务“跳过”按“过号重排”处理:叫号中 -> 跳过并移到末尾,自动推进下一等待
|
|
||||||
return requeue(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public R<?> next(TriageQueueActionReq req) {
|
public R<?> next(TriageQueueActionReq req) {
|
||||||
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
Integer tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
TriageQueueItem calling = null;
|
TriageQueueItem calling = null;
|
||||||
|
|
||||||
System.out.println(">>> [TriageQueue] next() 开始执行(不限制日期), tenantId=" + tenantId);
|
|
||||||
|
|
||||||
// 关键改进:如果提供了 id,优先使用 id 直接查找(像 call 方法一样)
|
// 关键改进:如果提供了 id,优先使用 id 直接查找(像 call 方法一样)
|
||||||
if (req != null && req.getId() != null) {
|
if (req != null && req.getId() != null) {
|
||||||
calling = triageQueueItemService.getById(req.getId());
|
calling = triageQueueItemService.getById(req.getId());
|
||||||
@@ -514,14 +569,6 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
TriageQueueItem next = triageQueueItemService.getOne(nextWrapper, false);
|
||||||
|
|
||||||
// 调试日志:打印查询条件和结果
|
|
||||||
System.out.println(">>> [TriageQueue] next() 查询条件(不限制日期): tenantId=" + tenantId
|
|
||||||
+ ", actualOrgId=" + actualOrgId
|
|
||||||
+ ", deleteFlag=0"
|
|
||||||
+ ", status IN (WAITING, SKIPPED)");
|
|
||||||
System.out.println(">>> [TriageQueue] next() 查询结果: calling=" + (calling != null ? calling.getPatientName() + "(status=" + calling.getStatus() + ", queueDate=" + calling.getQueueDate() + ")" : "null")
|
|
||||||
+ ", next=" + (next != null ? next.getPatientName() + "(status=" + next.getStatus() + ", queueDate=" + next.getQueueDate() + ")" : "null"));
|
|
||||||
|
|
||||||
if (next == null) {
|
if (next == null) {
|
||||||
if (actualOrgId != null) {
|
if (actualOrgId != null) {
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
@@ -535,6 +582,13 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
if (next.getOrganizationId() != null) {
|
if (next.getOrganizationId() != null) {
|
||||||
recalcOrders(next.getOrganizationId(), null);
|
recalcOrders(next.getOrganizationId(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入叫号记录
|
||||||
|
if (calling != null) {
|
||||||
|
writeCallRecord(calling.getId(), calling.getPractitionerId(), CallType.NEXT, calling.getRoomNo());
|
||||||
|
}
|
||||||
|
writeCallRecord(next.getId(), next.getPractitionerId(), CallType.NEXT, next.getRoomNo());
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,6 +663,9 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.orderByAsc(TriageQueueItem::getQueueOrder)
|
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 通过 slotId 批量查询 seqNo(方案 B:不修改 triage_queue_item 表结构)
|
||||||
|
Map<Long, Integer> slotSeqNoMap = buildSlotSeqNoMap(allItems);
|
||||||
|
|
||||||
CallNumberDisplayResp resp = new CallNumberDisplayResp();
|
CallNumberDisplayResp resp = new CallNumberDisplayResp();
|
||||||
|
|
||||||
// 1. 获取科室名称(从第一条数据中取)
|
// 1. 获取科室名称(从第一条数据中取)
|
||||||
@@ -626,7 +683,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
|
|
||||||
if (callingItem != null) {
|
if (callingItem != null) {
|
||||||
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||||
currentCall.setNumber(callingItem.getQueueOrder());
|
Integer displayNo = resolveDisplayNumber(callingItem, slotSeqNoMap);
|
||||||
|
currentCall.setNumber(displayNo);
|
||||||
currentCall.setName(maskPatientName(callingItem.getPatientName()));
|
currentCall.setName(maskPatientName(callingItem.getPatientName()));
|
||||||
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
|
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
|
||||||
currentCall.setDoctor(callingItem.getPractitionerName());
|
currentCall.setDoctor(callingItem.getPractitionerName());
|
||||||
@@ -680,7 +738,7 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
patient.setId(item.getId());
|
patient.setId(item.getId());
|
||||||
patient.setName(maskPatientName(item.getPatientName()));
|
patient.setName(maskPatientName(item.getPatientName()));
|
||||||
patient.setStatus(item.getStatus());
|
patient.setStatus(item.getStatus());
|
||||||
patient.setQueueOrder(item.getQueueOrder());
|
patient.setQueueOrder(resolveDisplayNumber(item, slotSeqNoMap));
|
||||||
patients.add(patient);
|
patients.add(patient);
|
||||||
|
|
||||||
// 统计等待人数(不包括 CALLING 状态)
|
// 统计等待人数(不包括 CALLING 状态)
|
||||||
@@ -748,6 +806,82 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
System.err.println("推送显示屏更新失败:" + e.getMessage());
|
System.err.println("推送显示屏更新失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入分诊操作日志
|
||||||
|
*
|
||||||
|
* @param poolId 号源池ID
|
||||||
|
* @param slotId 号源槽位ID
|
||||||
|
* @param action 操作动作:ADD_QUEUE/REMOVE_QUEUE/CALL/COMPLETE/SKIP/REQUEUE
|
||||||
|
*/
|
||||||
|
private void writeDivLog(Long poolId, Long slotId, String action) {
|
||||||
|
try {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
DivLog log = new DivLog()
|
||||||
|
.setPoolId(poolId)
|
||||||
|
.setSlotId(slotId)
|
||||||
|
.setOpUserId(loginUser != null ? loginUser.getUserId() : null)
|
||||||
|
.setAction(action)
|
||||||
|
.setCreateTime(LocalDateTime.now())
|
||||||
|
.setUpdateAt(LocalDateTime.now())
|
||||||
|
.setCreatedAt(LocalDateTime.now());
|
||||||
|
divLogService.save(log);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入分诊日志失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入叫号记录
|
||||||
|
*
|
||||||
|
* @param queueId 队列ID
|
||||||
|
* @param doctorId 医生ID
|
||||||
|
* @param callType 叫号类型枚举
|
||||||
|
* @param room 诊室号
|
||||||
|
*/
|
||||||
|
private void writeCallRecord(Long queueId, Long doctorId, CallType callType, String room) {
|
||||||
|
try {
|
||||||
|
CallRecord record = new CallRecord()
|
||||||
|
.setQueueId(queueId)
|
||||||
|
.setDoctorId(doctorId)
|
||||||
|
.setCallTime(LocalDateTime.now())
|
||||||
|
.setCallType(callType.getValue().toString())
|
||||||
|
.setRoom(room)
|
||||||
|
.setCreateAt(LocalDateTime.now());
|
||||||
|
callRecordService.save(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入叫号记录失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 slotId 批量查询 seqNo,返回 slotId -> seqNo 映射
|
||||||
|
*/
|
||||||
|
private Map<Long, Integer> buildSlotSeqNoMap(List<TriageQueueItem> items) {
|
||||||
|
List<Long> slotIds = items.stream()
|
||||||
|
.map(TriageQueueItem::getSlotId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (slotIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<ScheduleSlot> slots = scheduleSlotMapper.selectSeqNoBySlotIds(slotIds);
|
||||||
|
return slots.stream()
|
||||||
|
.filter(s -> s.getId() != null && s.getSeqNo() != null)
|
||||||
|
.collect(Collectors.toMap(ScheduleSlot::getId, ScheduleSlot::getSeqNo, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算患者在叫号显示屏上应显示的号码:优先 seqNo(预约序号),否则 queueOrder(排队号)
|
||||||
|
*/
|
||||||
|
private Integer resolveDisplayNumber(TriageQueueItem item, Map<Long, Integer> slotSeqNoMap) {
|
||||||
|
if (item == null) return null;
|
||||||
|
if (item.getSlotId() != null && slotSeqNoMap.containsKey(item.getSlotId())) {
|
||||||
|
return slotSeqNoMap.get(item.getSlotId());
|
||||||
|
}
|
||||||
|
return item.getQueueOrder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public class TriageQueueEncounterItem {
|
|||||||
private Long poolId;
|
private Long poolId;
|
||||||
/** 号源槽位ID(关联 adm_schedule_slot.id,用于 div_log 审计日志) */
|
/** 号源槽位ID(关联 adm_schedule_slot.id,用于 div_log 审计日志) */
|
||||||
private Long slotId;
|
private Long slotId;
|
||||||
|
/** 预约序号(来自 adm_schedule_slot.seq_no,用于叫号显示) */
|
||||||
|
private Integer seqNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ core:
|
|||||||
# 名称
|
# 名称
|
||||||
name: HEALTHLINK-HIS
|
name: HEALTHLINK-HIS
|
||||||
# 版本
|
# 版本
|
||||||
version: 0.0.1
|
version: ${CORE_VERSION:0.0.1}
|
||||||
# 版权年份
|
# 版权年份
|
||||||
copyrightYear: 2025
|
copyrightYear: 2025
|
||||||
# 文件路径
|
# 文件路径
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
T1.quantity_unit,
|
T1.quantity_unit,
|
||||||
T1.unit_price,
|
T1.unit_price,
|
||||||
T1.total_price,
|
T1.total_price,
|
||||||
|
T1.generate_source_enum,
|
||||||
|
T1.prescription_no AS source_bill_no,
|
||||||
mmr.prescription_no,
|
mmr.prescription_no,
|
||||||
mmr.method_code AS method_code,
|
mmr.method_code AS method_code,
|
||||||
mmr.rate_code,
|
mmr.rate_code,
|
||||||
@@ -190,6 +192,8 @@
|
|||||||
T1.quantity_unit,
|
T1.quantity_unit,
|
||||||
T1.unit_price,
|
T1.unit_price,
|
||||||
T1.total_price,
|
T1.total_price,
|
||||||
|
T1.generate_source_enum,
|
||||||
|
T1.prescription_no AS source_bill_no,
|
||||||
mmr.prescription_no,
|
mmr.prescription_no,
|
||||||
mmr.method_code AS method_code,
|
mmr.method_code AS method_code,
|
||||||
mmr.rate_code,
|
mmr.rate_code,
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
|
COALESCE(T9.identifier_no, T9.patient_bus_no, '') AS identifierNo,
|
||||||
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment,
|
COALESCE(T9.order_id IS NOT NULL, false) AS isFromAppointment,
|
||||||
T9.slot_id AS slotId,
|
T9.slot_id AS slotId,
|
||||||
T9.pool_id AS poolId
|
T9.pool_id AS poolId,
|
||||||
|
T9.seq_no AS seqNo
|
||||||
from (
|
from (
|
||||||
SELECT T1.tenant_id AS tenant_id,
|
SELECT T1.tenant_id AS tenant_id,
|
||||||
T1.id AS encounter_id,
|
T1.id AS encounter_id,
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
T18.identifier_no AS identifier_no,
|
T18.identifier_no AS identifier_no,
|
||||||
T1.order_id AS order_id,
|
T1.order_id AS order_id,
|
||||||
om.slot_id AS slot_id,
|
om.slot_id AS slot_id,
|
||||||
|
ss.seq_no AS seq_no,
|
||||||
ss.pool_id AS pool_id,
|
ss.pool_id AS pool_id,
|
||||||
sp.clinic_room AS clinic_room -- Bug #410:从号源池获取诊室
|
sp.clinic_room AS clinic_room -- Bug #410:从号源池获取诊室
|
||||||
FROM adm_encounter AS T1
|
FROM adm_encounter AS T1
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
|
|||||||
@@ -143,10 +143,10 @@
|
|||||||
</if>
|
</if>
|
||||||
ORDER BY T1.id DESC
|
ORDER BY T1.id DESC
|
||||||
<if test="searchKey != null and searchKey != ''">
|
<if test="searchKey != null and searchKey != ''">
|
||||||
LIMIT 1500
|
LIMIT 10000
|
||||||
</if>
|
</if>
|
||||||
<if test="searchKey == null or searchKey == ''">
|
<if test="searchKey == null or searchKey == ''">
|
||||||
LIMIT 500
|
LIMIT 10000
|
||||||
</if>
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,9 @@
|
|||||||
fc.contract_name AS fee_type,
|
fc.contract_name AS fee_type,
|
||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifier_no
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifier_no
|
||||||
FROM doc_request_form drf
|
FROM doc_request_form drf
|
||||||
LEFT JOIN cli_surgery cs ON cs.surgery_no = drf.prescription_no
|
LEFT JOIN cli_surgery cs ON cs.surgery_no = drf.prescription_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_patient ap ON ap.id = cs.patient_id
|
LEFT JOIN adm_patient ap ON ap.id = cs.patient_id AND ap.delete_flag = '0'
|
||||||
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id
|
LEFT JOIN adm_encounter ae ON ae.id = cs.encounter_id AND ae.delete_flag = '0'
|
||||||
LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0'
|
LEFT JOIN adm_account aa ON aa.encounter_id = ae.id AND aa.delete_flag = '0'
|
||||||
LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0'
|
LEFT JOIN fin_contract fc ON fc.bus_no = aa.contract_no AND fc.delete_flag = '0'
|
||||||
LEFT JOIN op_schedule os ON os.apply_id = drf.id AND os.delete_flag = '0'
|
LEFT JOIN op_schedule os ON os.apply_id = drf.id AND os.delete_flag = '0'
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.openhis.common.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号类型
|
||||||
|
*
|
||||||
|
* @author wangjian963
|
||||||
|
* @date 2026-04-29
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum CallType implements HisEnumInterface {
|
||||||
|
|
||||||
|
/** 手动叫号(选呼) */
|
||||||
|
CALL(10, "CALL", "手动叫号(选呼)"),
|
||||||
|
|
||||||
|
/** 手动叫号(下一患者) */
|
||||||
|
NEXT(20, "NEXT", "手动叫号(下一患者)"),
|
||||||
|
|
||||||
|
/** 自动叫号 */
|
||||||
|
AUTO_CALL(30, "AUTO_CALL", "自动叫号"),
|
||||||
|
|
||||||
|
/** 跳过 */
|
||||||
|
SKIP(40, "SKIP", "跳过"),
|
||||||
|
|
||||||
|
/** 完成 */
|
||||||
|
COMPLETE(50, "COMPLETE", "完成"),
|
||||||
|
|
||||||
|
/** 重排 */
|
||||||
|
REQUEUE(60, "REQUEUE", "重排");
|
||||||
|
|
||||||
|
/** 状态码 */
|
||||||
|
private Integer value;
|
||||||
|
/** 英文标识 */
|
||||||
|
private String code;
|
||||||
|
/** 中文描述 */
|
||||||
|
private String info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态码获取对应的叫号类型枚举
|
||||||
|
*
|
||||||
|
* @param value 状态码
|
||||||
|
* @return 对应的枚举值,未匹配时返回 null
|
||||||
|
*/
|
||||||
|
public static CallType getByValue(Integer value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CallType val : values()) {
|
||||||
|
if (val.getValue().equals(value)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,4 +61,9 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
|
|||||||
*/
|
*/
|
||||||
List<DoctorAvailabilityDTO> selectDoctorAvailabilitySummary(@Param("query") TicketQueryDTO query);
|
List<DoctorAvailabilityDTO> selectDoctorAvailabilitySummary(@Param("query") TicketQueryDTO query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询槽位序号(用于分诊叫号显示)
|
||||||
|
*/
|
||||||
|
List<ScheduleSlot> selectSeqNoBySlotIds(@Param("slotIds") List<Long> slotIds);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@TableName(value = "call_record")
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
public class CallRecord {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
/** 队列ID (FK triage_queue_item.id) */
|
||||||
|
private Long queueId;
|
||||||
|
|
||||||
|
/** 医生ID */
|
||||||
|
private Long doctorId;
|
||||||
|
|
||||||
|
/** 叫号时间 */
|
||||||
|
private LocalDateTime callTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号类型,使用 {@link com.openhis.common.enums.CallType} 枚举值
|
||||||
|
* 10-CALL(选呼), 20-NEXT(下一患者), 30-AUTO_CALL(自动叫号),
|
||||||
|
* 40-SKIP(跳过), 50-COMPLETE(完成), 60-REQUEUE(重排)
|
||||||
|
*/
|
||||||
|
private String callType;
|
||||||
|
|
||||||
|
/** 诊室(冗余) */
|
||||||
|
private String room;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@TableName(value = "div_log")
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
public class DivLog {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long logId;
|
||||||
|
|
||||||
|
/** 号源池ID */
|
||||||
|
private Long poolId;
|
||||||
|
|
||||||
|
/** 号源槽位ID */
|
||||||
|
private Long slotId;
|
||||||
|
|
||||||
|
/** 操作人ID */
|
||||||
|
private Long opUserId;
|
||||||
|
|
||||||
|
/** 操作动作:ADD_QUEUE/REMOVE_QUEUE/CALL/REFUND/COMPLETE/SKIP/REQUEUE */
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
/** 操作时间 */
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
private LocalDateTime updateAt;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openhis.triageandqueuemanage.domain;
|
package com.openhis.triageandqueuemanage.domain;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -40,7 +41,10 @@ public class TriageQueueItem {
|
|||||||
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
|
* 30=COMPLETED(已完成), 40=SKIPPED(已跳过), 50=REFUNDED(已退费), 60=FOLLOW(已随访)
|
||||||
*/
|
*/
|
||||||
private Integer status;
|
private Integer status;
|
||||||
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
private Integer queueOrder; //”排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Integer seqNo; // 预约序号(来自 adm_schedule_slot.seq_no,非数据库字段,通过 JOIN 查询)
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.CallRecord;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface CallRecordMapper extends BaseMapper<CallRecord> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DivLogMapper extends BaseMapper<DivLog> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.CallRecord;
|
||||||
|
|
||||||
|
public interface CallRecordService extends IService<CallRecord> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||||
|
|
||||||
|
public interface DivLogService extends IService<DivLog> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.CallRecord;
|
||||||
|
import com.openhis.triageandqueuemanage.mapper.CallRecordMapper;
|
||||||
|
import com.openhis.triageandqueuemanage.service.CallRecordService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CallRecordServiceImpl extends ServiceImpl<CallRecordMapper, CallRecord> implements CallRecordService {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.openhis.triageandqueuemanage.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.openhis.triageandqueuemanage.domain.DivLog;
|
||||||
|
import com.openhis.triageandqueuemanage.mapper.DivLogMapper;
|
||||||
|
import com.openhis.triageandqueuemanage.service.DivLogService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DivLogServiceImpl extends ServiceImpl<DivLogMapper, DivLog> implements DivLogService {
|
||||||
|
}
|
||||||
@@ -437,5 +437,15 @@
|
|||||||
p.doctor_name ASC
|
p.doctor_name ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectSeqNoBySlotIds" resultType="com.openhis.appointmentmanage.domain.ScheduleSlot">
|
||||||
|
SELECT id, seq_no
|
||||||
|
FROM adm_schedule_slot
|
||||||
|
WHERE id IN
|
||||||
|
<foreach collection="slotIds" item="slotId" open="(" separator="," close=")">
|
||||||
|
#{slotId}
|
||||||
|
</foreach>
|
||||||
|
AND delete_flag = '0'
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
# 页面标题
|
# Playwright E2E 测试环境变量
|
||||||
VITE_APP_TITLE = 医院信息管理系统
|
# 注意:此文件仅用于本地开发,生产环境使用CI Secret管理
|
||||||
|
TEST_BASE_URL=http://localhost:80
|
||||||
# 测试环境配置
|
TEST_USERNAME=admin
|
||||||
VITE_APP_ENV = 'test'
|
TEST_PASSWORD=changeme_in_local_env
|
||||||
|
|
||||||
# OpenHIS管理系统/测试环境
|
|
||||||
|
|
||||||
VITE_APP_BASE_API = '/test-api'
|
|
||||||
|
|
||||||
# 租户ID配置
|
|
||||||
VITE_APP_TENANT_ID = '1'
|
|
||||||
|
|||||||
5
openhis-ui-vue3/.gitignore
vendored
5
openhis-ui-vue3/.gitignore
vendored
@@ -21,3 +21,8 @@ selenium-debug.log
|
|||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Playwright test results
|
||||||
|
test-results/
|
||||||
|
tests/e2e/report/
|
||||||
|
tests/tests/
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"lint": "eslint . --ext .js,.vue src/"
|
"lint": "eslint . --ext .js,.vue src/",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
28
openhis-ui-vue3/playwright.config.ts
Normal file
28
openhis-ui-vue3/playwright.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e/specs',
|
||||||
|
fullyParallel: true,
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
retries: process.env.CI ? 2 : 1,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
reporter: [
|
||||||
|
['html', { outputFolder: 'tests/e2e/report', open: 'never' }],
|
||||||
|
['list'],
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:81',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timezoneId: 'Asia/Shanghai',
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
navigationTimeout: 30_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
10
openhis-ui-vue3/src/api/system/info.js
Normal file
10
openhis-ui-vue3/src/api/system/info.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getSystemVersion(options = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/system/version',
|
||||||
|
method: 'get',
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -378,6 +378,7 @@ import {getCurrentInstance, nextTick, watch} from 'vue';
|
|||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const { unit_code, med_chrgitm_type, fin_type_code, activity_category_code, chrgitm_lv } =
|
const { unit_code, med_chrgitm_type, fin_type_code, activity_category_code, chrgitm_lv } =
|
||||||
proxy.useDict(
|
proxy.useDict(
|
||||||
|
'specimen_code',
|
||||||
'unit_code',
|
'unit_code',
|
||||||
'med_chrgitm_type',
|
'med_chrgitm_type',
|
||||||
'fin_type_code',
|
'fin_type_code',
|
||||||
|
|||||||
@@ -562,7 +562,7 @@
|
|||||||
prescriptionList[scope.$index].minUnitQuantity = prescriptionList[scope.$index].quantity || 1;
|
prescriptionList[scope.$index].minUnitQuantity = prescriptionList[scope.$index].quantity || 1;
|
||||||
prescriptionList[scope.$index].minUnitCode = prescriptionList[scope.$index].unitCode;
|
prescriptionList[scope.$index].minUnitCode = prescriptionList[scope.$index].unitCode;
|
||||||
prescriptionList[scope.$index].minUnitCode_dictText = prescriptionList[scope.$index].unitCode_dictText;
|
prescriptionList[scope.$index].minUnitCode_dictText = prescriptionList[scope.$index].unitCode_dictText;
|
||||||
adviceQueryParams.adviceTypes = value; // 🎯 修复:改为 adviceTypes(复数)
|
adviceQueryParams.adviceTypes = [value]; // 🎯 修复:改为 adviceTypes(复数)
|
||||||
|
|
||||||
// 根据选择的类型设置categoryCode,用于药品分类筛选
|
// 根据选择的类型设置categoryCode,用于药品分类筛选
|
||||||
if (value == 1) { // 西药
|
if (value == 1) { // 西药
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ const getList = () => {
|
|||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
categoryCode: '24',
|
categoryCode: '24',
|
||||||
organizationId: patientInfo.value.inHospitalOrgId,
|
organizationId: patientInfo.value.inHospitalOrgId,
|
||||||
adviceTypes: '3', //1 药品 2耗材 3诊疗
|
adviceTypes: [3], //1 药品 2耗材 3诊疗
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
|||||||
@@ -100,7 +100,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
© 2025 {{ currentTenantName || settings.systemName }}信息管理系统 | 版本 v2.5.1
|
© 2025 {{ currentTenantName || settings.systemName }}信息管理系统
|
||||||
|
| 前端版本 {{ formattedFrontendVersion }}
|
||||||
|
<span v-if="backendVersion">
|
||||||
|
| 后端版本 {{ formattedBackendVersion }}
|
||||||
|
</span>
|
||||||
<!-- 公司版权信息(新增) -->
|
<!-- 公司版权信息(新增) -->
|
||||||
<div class="company-copyright">
|
<div class="company-copyright">
|
||||||
技术支持:上海经创贺联信息技术有限公司
|
技术支持:上海经创贺联信息技术有限公司
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {getCurrentInstance, onMounted, ref, watch, nextTick} from 'vue';
|
import {computed, getCurrentInstance, onMounted, ref, watch, nextTick} from 'vue';
|
||||||
import settings from '@/settings';
|
import settings from '@/settings';
|
||||||
import {getCodeImg, getUserBindTenantList, sign} from '@/api/login';
|
import {getCodeImg, getUserBindTenantList, sign} from '@/api/login';
|
||||||
import {invokeYbPlugin5001} from '@/api/public';
|
import {invokeYbPlugin5001} from '@/api/public';
|
||||||
@@ -134,6 +138,7 @@ import Cookies from 'js-cookie';
|
|||||||
import {decrypt, encrypt} from '@/utils/jsencrypt';
|
import {decrypt, encrypt} from '@/utils/jsencrypt';
|
||||||
import useUserStore from '@/store/modules/user';
|
import useUserStore from '@/store/modules/user';
|
||||||
import {ElMessage} from 'element-plus';
|
import {ElMessage} from 'element-plus';
|
||||||
|
import {getSystemVersion} from '@/api/system/info';
|
||||||
import logoNew from '@/assets/logo/LOGO.jpg';
|
import logoNew from '@/assets/logo/LOGO.jpg';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -141,6 +146,31 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const env = import.meta.env.MODE;
|
const env = import.meta.env.MODE;
|
||||||
|
const loginVersion = import.meta.env.VITE_APP_BUILD_VERSION;
|
||||||
|
const backendVersion = ref('');
|
||||||
|
|
||||||
|
const formattedFrontendVersion = computed(() => {
|
||||||
|
if (!loginVersion) return '';
|
||||||
|
// 期望格式:YYYYMMDDHHmmss -> 显示 YYYY-MM-DD
|
||||||
|
if (loginVersion.length >= 8) {
|
||||||
|
const y = loginVersion.substring(0, 4);
|
||||||
|
const m = loginVersion.substring(4, 6);
|
||||||
|
const d = loginVersion.substring(6, 8);
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
return loginVersion;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedBackendVersion = computed(() => {
|
||||||
|
if (!backendVersion.value) return '';
|
||||||
|
if (backendVersion.value.length >= 8) {
|
||||||
|
const y = backendVersion.value.substring(0, 4);
|
||||||
|
const m = backendVersion.value.substring(4, 6);
|
||||||
|
const d = backendVersion.value.substring(6, 8);
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
return backendVersion.value;
|
||||||
|
});
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -236,6 +266,15 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取后端版本号
|
||||||
|
getSystemVersion().then((res) => {
|
||||||
|
if (res && res.backendVersion) {
|
||||||
|
backendVersion.value = res.backendVersion;
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
backendVersion.value = '';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
|
|||||||
@@ -1941,6 +1941,7 @@ function submitForm() {
|
|||||||
// 新增手术安排
|
// 新增手术安排
|
||||||
addSurgerySchedule(submitData).then((res) => {
|
addSurgerySchedule(submitData).then((res) => {
|
||||||
proxy.$modal.msgSuccess('新增成功')
|
proxy.$modal.msgSuccess('新增成功')
|
||||||
|
queryParams.pageNo = 1
|
||||||
open.value = false
|
open.value = false
|
||||||
getPageList()
|
getPageList()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function getCandidatePool(params) {
|
|||||||
pageNo: params?.pageNo || 1,
|
pageNo: params?.pageNo || 1,
|
||||||
pageSize: params?.pageSize || 10000,
|
pageSize: params?.pageSize || 10000,
|
||||||
searchKey: params?.searchKey || '',
|
searchKey: params?.searchKey || '',
|
||||||
statusEnum: params?.statusEnum || -1 // -1表示排除退号记录(正常挂号)
|
statusEnum: params?.statusEnum ?? 1 // 1=PLANNED(待诊),已挂号未接诊的患者;不传或传-1会返回已接诊的患者
|
||||||
},
|
},
|
||||||
skipErrorMsg: true // 跳过错误提示,由组件处理
|
skipErrorMsg: true // 跳过错误提示,由组件处理
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,12 +6,19 @@
|
|||||||
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
|
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<el-button type="primary" @click="handleRefresh">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleRefresh"
|
||||||
|
>
|
||||||
<el-icon><Refresh /></el-icon>
|
<el-icon><Refresh /></el-icon>
|
||||||
刷新
|
刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleExit">退出</el-button>
|
<el-button @click="handleExit">
|
||||||
<el-button @click="handleConfig">后台配置</el-button>
|
退出
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleConfig">
|
||||||
|
后台配置
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,28 +38,67 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@selection-change="handleCandidateSelectionChange"
|
@selection-change="handleCandidateSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column
|
||||||
<el-table-column prop="sequenceNo" label="序号" width="80" align="center" />
|
type="selection"
|
||||||
<el-table-column prop="patientName" label="患者" width="100" align="center" />
|
width="55"
|
||||||
<el-table-column prop="age" label="年龄" width="80" align="center" />
|
align="center"
|
||||||
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
|
/>
|
||||||
<el-table-column prop="room" label="诊室" width="120" align="center" />
|
<el-table-column
|
||||||
<el-table-column prop="doctor" label="医生" width="120" align="center" />
|
prop="sequenceNo"
|
||||||
<el-table-column prop="matchingRule" label="命中规则" min-width="150" align="center" />
|
label="序号"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="patientName"
|
||||||
|
label="患者"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="age"
|
||||||
|
label="年龄"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="appointmentType"
|
||||||
|
label="号别"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="room"
|
||||||
|
label="诊室"
|
||||||
|
width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="doctor"
|
||||||
|
label="医生"
|
||||||
|
width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="matchingRule"
|
||||||
|
label="命中规则"
|
||||||
|
min-width="150"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div class="candidate-actions">
|
<div class="candidate-actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAddToQueue"
|
|
||||||
:disabled="selectedCandidates.length === 0"
|
:disabled="selectedCandidates.length === 0"
|
||||||
|
@click="handleAddToQueue"
|
||||||
>
|
>
|
||||||
加入队列 >>
|
加入队列 >>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAddAllToQueue"
|
|
||||||
:disabled="filteredCandidatePoolList.length === 0"
|
:disabled="filteredCandidatePoolList.length === 0"
|
||||||
|
@click="handleAddAllToQueue"
|
||||||
>
|
>
|
||||||
一键加入队列
|
一键加入队列
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -75,13 +121,48 @@
|
|||||||
highlight-current-row
|
highlight-current-row
|
||||||
@row-click="handleQueueRowClick"
|
@row-click="handleQueueRowClick"
|
||||||
>
|
>
|
||||||
<el-table-column prop="queueOrder" label="队序" width="80" align="center" />
|
<el-table-column
|
||||||
<el-table-column prop="patientName" label="患者" width="100" align="center" />
|
prop="queueOrder"
|
||||||
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
|
label="队序"
|
||||||
<el-table-column prop="room" label="诊室" width="120" align="center" />
|
width="80"
|
||||||
<el-table-column prop="doctor" label="医生" width="120" align="center" />
|
align="center"
|
||||||
<el-table-column prop="waitingTime" label="等待" width="100" align="center" />
|
/>
|
||||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
<el-table-column
|
||||||
|
prop="patientName"
|
||||||
|
label="患者"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="appointmentType"
|
||||||
|
label="号别"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="room"
|
||||||
|
label="诊室"
|
||||||
|
width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="doctor"
|
||||||
|
label="医生"
|
||||||
|
width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="waitingTime"
|
||||||
|
label="等待"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="status"
|
||||||
|
label="状态"
|
||||||
|
width="100"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getStatusTagType(scope.row.status)">
|
<el-tag :type="getStatusTagType(scope.row.status)">
|
||||||
{{ scope.row.status }}
|
{{ scope.row.status }}
|
||||||
@@ -94,25 +175,25 @@
|
|||||||
<div class="queue-actions-left">
|
<div class="queue-actions-left">
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleRemoveFromQueue"
|
|
||||||
:disabled="!selectedQueueRow"
|
:disabled="!selectedQueueRow"
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="handleRemoveFromQueue"
|
||||||
>
|
>
|
||||||
<< 移出队列
|
<< 移出队列
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="info"
|
type="info"
|
||||||
@click="handleMoveUp"
|
|
||||||
:disabled="!selectedQueueRow || !canMoveUp"
|
:disabled="!selectedQueueRow || !canMoveUp"
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="handleMoveUp"
|
||||||
>
|
>
|
||||||
↑
|
↑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="info"
|
type="info"
|
||||||
@click="handleMoveDown"
|
|
||||||
:disabled="!selectedQueueRow || !canMoveDown"
|
:disabled="!selectedQueueRow || !canMoveDown"
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="handleMoveDown"
|
||||||
>
|
>
|
||||||
↓
|
↓
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -120,15 +201,15 @@
|
|||||||
<div class="queue-actions-right">
|
<div class="queue-actions-right">
|
||||||
<el-button
|
<el-button
|
||||||
:type="showOnlyWaiting ? 'primary' : ''"
|
:type="showOnlyWaiting ? 'primary' : ''"
|
||||||
@click="showOnlyWaiting = true"
|
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="showOnlyWaiting = true"
|
||||||
>
|
>
|
||||||
只显示等待
|
只显示等待
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
:type="!showOnlyWaiting ? 'primary' : ''"
|
:type="!showOnlyWaiting ? 'primary' : ''"
|
||||||
@click="showOnlyWaiting = false"
|
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="showOnlyWaiting = false"
|
||||||
>
|
>
|
||||||
显示全部状态
|
显示全部状态
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -141,7 +222,9 @@
|
|||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<!-- 就诊科室快速过滤栏 -->
|
<!-- 就诊科室快速过滤栏 -->
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<div class="filter-label">③ 就诊科室快速过滤栏</div>
|
<div class="filter-label">
|
||||||
|
③ 就诊科室快速过滤栏
|
||||||
|
</div>
|
||||||
<div class="filter-select-wrapper">
|
<div class="filter-select-wrapper">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedDept"
|
v-model="selectedDept"
|
||||||
@@ -167,24 +250,53 @@
|
|||||||
|
|
||||||
<!-- 叫号控制板 -->
|
<!-- 叫号控制板 -->
|
||||||
<div class="call-control-section">
|
<div class="call-control-section">
|
||||||
<div class="call-control-label">④ 叫号控制板</div>
|
<div class="call-control-label">
|
||||||
|
④ 叫号控制板
|
||||||
|
</div>
|
||||||
<div class="call-control-content">
|
<div class="call-control-content">
|
||||||
<div class="current-call-display">
|
<div class="current-call-display">
|
||||||
当前呼叫: {{ currentCall.number }} {{ currentCall.name }} 诊室: {{ currentCall.room }}
|
当前呼叫: {{ currentCall.number }} {{ currentCall.name }} 诊室: {{ currentCall.room }}
|
||||||
</div>
|
</div>
|
||||||
<div class="control-buttons">
|
<div class="control-buttons">
|
||||||
<el-button type="primary" @click="handleSelectCall">选呼</el-button>
|
<el-button
|
||||||
<el-button type="success" @click="handleNextPatient">下一患者</el-button>
|
type="primary"
|
||||||
<el-button type="warning" @click="handleSkip">跳过</el-button>
|
@click="handleSelectCall"
|
||||||
<el-button type="primary" @click="handleComplete">完成</el-button>
|
>
|
||||||
<el-button type="info" @click="handleRequeue">过号重排</el-button>
|
选呼
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="handleNextPatient"
|
||||||
|
>
|
||||||
|
下一患者
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
@click="handleSkip"
|
||||||
|
>
|
||||||
|
跳过
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleComplete"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="handleRequeue"
|
||||||
|
>
|
||||||
|
过号重排
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LED显示 -->
|
<!-- LED显示 -->
|
||||||
<div class="led-section">
|
<div class="led-section">
|
||||||
<div class="led-label">⑤ LED:</div>
|
<div class="led-label">
|
||||||
|
⑤ LED:
|
||||||
|
</div>
|
||||||
<div class="led-display">
|
<div class="led-display">
|
||||||
[{{ currentCall.number }}]{{ currentCall.name }}请到{{ currentCall.room }}({{ callType }})
|
[{{ currentCall.number }}]{{ currentCall.name }}请到{{ currentCall.room }}({{ callType }})
|
||||||
</div>
|
</div>
|
||||||
@@ -205,11 +317,25 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="config-dialog-header">
|
<div class="config-dialog-header">
|
||||||
<div class="config-dialog-title">智能分诊规则引擎配置 - 心内科</div>
|
<div class="config-dialog-title">
|
||||||
|
智能分诊规则引擎配置 - 心内科
|
||||||
|
</div>
|
||||||
<div class="config-topbar-actions">
|
<div class="config-topbar-actions">
|
||||||
<el-button type="primary" @click="handleAddRule">新增规则</el-button>
|
<el-button
|
||||||
<el-button type="primary" @click="handleSaveAllRules">保存全部</el-button>
|
type="primary"
|
||||||
<el-button @click="handleTestRule">测试规则</el-button>
|
@click="handleAddRule"
|
||||||
|
>
|
||||||
|
新增规则
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSaveAllRules"
|
||||||
|
>
|
||||||
|
保存全部
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleTestRule">
|
||||||
|
测试规则
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -224,42 +350,96 @@
|
|||||||
:class="{ active: idx === editingIndex }"
|
:class="{ active: idx === editingIndex }"
|
||||||
@click="handleSelectRule(idx)"
|
@click="handleSelectRule(idx)"
|
||||||
>
|
>
|
||||||
<div class="rule-title">规则{{ idx + 1 }}</div>
|
<div class="rule-title">
|
||||||
<div class="rule-sub">prio={{ item.priority }}</div>
|
规则{{ idx + 1 }}
|
||||||
<div class="rule-sub">{{ item.name }}</div>
|
</div>
|
||||||
|
<div class="rule-sub">
|
||||||
|
prio={{ item.priority }}
|
||||||
|
</div>
|
||||||
|
<div class="rule-sub">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
<div class="rule-actions">
|
<div class="rule-actions">
|
||||||
<el-button size="small" @click.stop="handleSelectRule(idx)">编辑</el-button>
|
<el-button
|
||||||
<el-button size="small" @click.stop="handleDeleteRule(idx)">删除</el-button>
|
size="small"
|
||||||
<el-button size="small" @click.stop="handleRuleMoveUp(idx)" :disabled="idx === 0">↑</el-button>
|
@click.stop="handleSelectRule(idx)"
|
||||||
<el-button size="small" @click.stop="handleRuleMoveDown(idx)" :disabled="idx === rules.length - 1">↓</el-button>
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click.stop="handleDeleteRule(idx)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:disabled="idx === 0"
|
||||||
|
@click.stop="handleRuleMoveUp(idx)"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:disabled="idx === rules.length - 1"
|
||||||
|
@click.stop="handleRuleMoveDown(idx)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-right">
|
<div class="config-right">
|
||||||
<el-form label-width="110px" class="config-form">
|
<el-form
|
||||||
<el-form-item label="规则名称:" required>
|
label-width="110px"
|
||||||
<el-input v-model="ruleForm.name" placeholder="请输入规则名称" />
|
class="config-form"
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
label="规则名称:"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.name"
|
||||||
|
placeholder="请输入规则名称"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="科室:">
|
<el-form-item label="科室:">
|
||||||
<el-select v-model="ruleForm.dept" class="config-fullwidth" disabled>
|
<el-select
|
||||||
<el-option label="心内科" value="心内科" />
|
v-model="ruleForm.dept"
|
||||||
|
class="config-fullwidth"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
label="心内科"
|
||||||
|
value="心内科"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="规则描述:">
|
<el-form-item label="规则描述:">
|
||||||
<el-input v-model="ruleForm.desc" placeholder="请输入规则描述" />
|
<el-input
|
||||||
|
v-model="ruleForm.desc"
|
||||||
|
placeholder="请输入规则描述"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="优先级:" required>
|
<el-form-item
|
||||||
|
label="优先级:"
|
||||||
|
required
|
||||||
|
>
|
||||||
<el-input v-model="ruleForm.priority" />
|
<el-input v-model="ruleForm.priority" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="周几生效:">
|
<el-form-item label="周几生效:">
|
||||||
<el-checkbox-group v-model="ruleForm.weeks">
|
<el-checkbox-group v-model="ruleForm.weeks">
|
||||||
<el-checkbox v-for="w in weekOptions" :key="w.value" :label="w.value">
|
<el-checkbox
|
||||||
|
v-for="w in weekOptions"
|
||||||
|
:key="w.value"
|
||||||
|
:label="w.value"
|
||||||
|
>
|
||||||
{{ w.label }}
|
{{ w.label }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
@@ -270,13 +450,17 @@
|
|||||||
v-model="ruleForm.expr"
|
v-model="ruleForm.expr"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="8"
|
||||||
placeholder='{"age":">=60","regType":"专家"}'
|
placeholder="{"age":">=60","regType":"专家"}"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="config-inline-actions">
|
<div class="config-inline-actions">
|
||||||
<el-button @click="handleQuickGenerate">快速生成器</el-button>
|
<el-button @click="handleQuickGenerate">
|
||||||
<el-button @click="handleValidateRule">语法检查</el-button>
|
快速生成器
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleValidateRule">
|
||||||
|
语法检查
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,8 +468,15 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="config-footer">
|
<div class="config-footer">
|
||||||
<el-button type="primary" @click="handleSaveCurrentRule">保存</el-button>
|
<el-button
|
||||||
<el-button @click="configDialogVisible = false">取消</el-button>
|
type="primary"
|
||||||
|
@click="handleSaveCurrentRule"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="configDialogVisible = false">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -297,15 +488,36 @@
|
|||||||
width="600px"
|
width="600px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<el-form :model="quickGeneratorForm" label-width="120px">
|
<el-form
|
||||||
|
:model="quickGeneratorForm"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<el-form-item label="年龄条件:">
|
<el-form-item label="年龄条件:">
|
||||||
<div style="display: flex; align-items: center; gap: 10px;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<el-select v-model="quickGeneratorForm.ageOperator" style="width: 100px;">
|
<el-select
|
||||||
<el-option label=">=" value=">=" />
|
v-model="quickGeneratorForm.ageOperator"
|
||||||
<el-option label="<=" value="<=" />
|
style="width: 100px;"
|
||||||
<el-option label="=" value="=" />
|
>
|
||||||
<el-option label=">" value=">" />
|
<el-option
|
||||||
<el-option label="<" value="<" />
|
label=">="
|
||||||
|
value=">="
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="<="
|
||||||
|
value="<="
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="="
|
||||||
|
value="="
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label=">"
|
||||||
|
value=">"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="<"
|
||||||
|
value="<"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="quickGeneratorForm.ageValue"
|
v-model="quickGeneratorForm.ageValue"
|
||||||
@@ -331,10 +543,22 @@
|
|||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option label="专家" value="专家" />
|
<el-option
|
||||||
<el-option label="普通" value="普通" />
|
label="专家"
|
||||||
<el-option label="特需" value="特需" />
|
value="专家"
|
||||||
<el-option label="急诊" value="急诊" />
|
/>
|
||||||
|
<el-option
|
||||||
|
label="普通"
|
||||||
|
value="普通"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="特需"
|
||||||
|
value="特需"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="急诊"
|
||||||
|
value="急诊"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -345,9 +569,18 @@
|
|||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option label="心内科" value="心内科" />
|
<el-option
|
||||||
<el-option label="心外科" value="心外科" />
|
label="心内科"
|
||||||
<el-option label="神经内科" value="神经内科" />
|
value="心内科"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="心外科"
|
||||||
|
value="心外科"
|
||||||
|
/>
|
||||||
|
<el-option
|
||||||
|
label="神经内科"
|
||||||
|
value="神经内科"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -391,8 +624,15 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<el-button @click="quickGeneratorDialogVisible = false">取消</el-button>
|
<el-button @click="quickGeneratorDialogVisible = false">
|
||||||
<el-button type="primary" @click="handleApplyQuickGenerate">应用</el-button>
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleApplyQuickGenerate"
|
||||||
|
>
|
||||||
|
应用
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -627,29 +867,40 @@ const parseAge = (ageStr) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 后端队列状态 -> 前端展示状态
|
// 后端队列状态 -> 前端展示状态
|
||||||
|
// 后端状态码:0=WAITING, 10=CALLING, 20=IN_CLINIC, 30=COMPLETED, 40=SKIPPED, 50=REFUNDED, 60=FOLLOW
|
||||||
const mapBackendStatusToFrontend = (status) => {
|
const mapBackendStatusToFrontend = (status) => {
|
||||||
if (!status) {
|
if (status === null || status === undefined) {
|
||||||
console.warn('【心内科】状态映射:收到空状态值')
|
console.warn('【心内科】状态映射:收到空状态值')
|
||||||
return '等待'
|
return '等待'
|
||||||
}
|
}
|
||||||
// 转换为大写并去除空格,确保匹配
|
const numStatus = Number(status)
|
||||||
const normalizedStatus = String(status).trim().toUpperCase()
|
switch (numStatus) {
|
||||||
if (normalizedStatus === 'CALLING') return '叫号中'
|
case 0: return '等待'
|
||||||
if (normalizedStatus === 'WAITING') return '等待'
|
case 10: return '叫号中'
|
||||||
if (normalizedStatus === 'SKIPPED') return '跳过'
|
case 20: return '诊中'
|
||||||
if (normalizedStatus === 'COMPLETED') return '已完成'
|
case 30: return '已完成'
|
||||||
|
case 40: return '跳过'
|
||||||
|
case 50: return '已退费'
|
||||||
|
case 60: return '已随访'
|
||||||
|
default:
|
||||||
console.warn('【心内科】状态映射:未知状态值', status, '-> 默认返回"等待"')
|
console.warn('【心内科】状态映射:未知状态值', status, '-> 默认返回"等待"')
|
||||||
return '等待'
|
return '等待'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前端状态 -> 后端状态(目前仅展示用)
|
// 前端状态 -> 后端状态码
|
||||||
const mapFrontendStatusToBackend = (status) => {
|
const mapFrontendStatusToBackend = (status) => {
|
||||||
if (!status) return 'WAITING'
|
if (!status) return 0
|
||||||
if (status === '叫号中') return 'CALLING'
|
switch (status) {
|
||||||
if (status === '等待') return 'WAITING'
|
case '叫号中': return 10
|
||||||
if (status === '跳过') return 'SKIPPED'
|
case '等待': return 0
|
||||||
if (status === '已完成') return 'COMPLETED'
|
case '诊中': return 20
|
||||||
return 'WAITING'
|
case '已完成': return 30
|
||||||
|
case '跳过': return 40
|
||||||
|
case '已退费': return 50
|
||||||
|
case '已随访': return 60
|
||||||
|
default: return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数据库加载队列
|
// 从数据库加载队列
|
||||||
|
|||||||
17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Normal file
17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
import { TEST_USERS } from '../utils/test-data';
|
||||||
|
|
||||||
|
export const test = base.extend({
|
||||||
|
async authenticatedPage({ page }, use) {
|
||||||
|
// 登录
|
||||||
|
await page.goto('/');
|
||||||
|
await page.fill('input[placeholder="请输入用户名"]', TEST_USERS.admin.username);
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', TEST_USERS.admin.password);
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await page.waitForURL(/.*(dashboard|home).*/);
|
||||||
|
|
||||||
|
await use(page);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect } from '@playwright/test';
|
||||||
26
openhis-ui-vue3/tests/e2e/pages/DoctorStationPage.ts
Normal file
26
openhis-ui-vue3/tests/e2e/pages/DoctorStationPage.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
export class DoctorStationPage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('/doctorstation');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async expandCategory(index: number = 0) {
|
||||||
|
const item = this.page.locator('.el-collapse-item, .category-item').nth(index);
|
||||||
|
await item.click();
|
||||||
|
await this.page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchPatient(name: string) {
|
||||||
|
await this.page.fill('input[placeholder*="患者"], input[placeholder*="姓名"]', name);
|
||||||
|
await this.page.click('button:has-text("搜索"), button:has-text("查询")');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
openhis-ui-vue3/tests/e2e/pages/LoginPage.ts
Normal file
46
openhis-ui-vue3/tests/e2e/pages/LoginPage.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
export class LoginPage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('/');
|
||||||
|
await this.page.waitForLoadState('domcontentloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
// Actual placeholders from login.vue: "账号" and "密码"
|
||||||
|
await this.page.fill('input[placeholder="账号"]', username);
|
||||||
|
await this.page.fill('input[placeholder="密码"]', password);
|
||||||
|
// Check for tenant selection if exists
|
||||||
|
const tenantSelect = this.page.locator('.el-select__wrapper, input[placeholder="请选择医疗机构"]').first();
|
||||||
|
if (await tenantSelect.isVisible().catch(() => false)) {
|
||||||
|
await tenantSelect.click();
|
||||||
|
await this.page.waitForTimeout(500);
|
||||||
|
// Select first option
|
||||||
|
const firstOption = this.page.locator('.el-select-dropdown__item, .el-option').first();
|
||||||
|
if (await firstOption.isVisible().catch(() => false)) {
|
||||||
|
await firstOption.click();
|
||||||
|
await this.page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.page.click('button:has-text("登 录")');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectLoginSuccess() {
|
||||||
|
await expect(this.page).toHaveURL(/.*(dashboard|home|index).*/, { timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectLoginFailed() {
|
||||||
|
await expect(this.page.locator('.el-message--error')).toBeVisible({ timeout: 5000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectOnLoginPage() {
|
||||||
|
await expect(this.page.locator('input[placeholder="账号"]')).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
34
openhis-ui-vue3/tests/e2e/pages/SurgeryBillingPage.ts
Normal file
34
openhis-ui-vue3/tests/e2e/pages/SurgeryBillingPage.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
export class SurgeryBillingPage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('/operatingroom');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async rapidClickGenerate(times: number = 5) {
|
||||||
|
const btn = this.page.locator('button:has-text("生成"), button:has-text("新增")');
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
await btn.click().catch(() => {});
|
||||||
|
}
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDialogCount(): Promise<number> {
|
||||||
|
return await this.page.locator('.el-dialog, .el-message-box').count();
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectNoLocationIdError() {
|
||||||
|
await expect(this.page.locator('text=发放库房为空')).toHaveCount(0, { timeout: 5000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectSaveSuccess() {
|
||||||
|
await expect(this.page.locator('.el-message--success')).toBeVisible({ timeout: 10000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
53
openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
Normal file
53
openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from '../pages/LoginPage';
|
||||||
|
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('🐛 Bug回归测试', () => {
|
||||||
|
let loginPage: LoginPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||||
|
await loginPage.expectLoginSuccess();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#437 手术计费防重复提交 @bug437 @regression', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.surgeryBilling);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const addBtn = page.locator('button:has-text("新增"), button:has-text("生成")');
|
||||||
|
if (await addBtn.isVisible()) {
|
||||||
|
await addBtn.click();
|
||||||
|
await addBtn.click();
|
||||||
|
await addBtn.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const dialogs = page.locator('.el-dialog, .el-message-box');
|
||||||
|
expect(await dialogs.count()).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#443 手术计费签发耗材 @bug443 @regression', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.surgeryBilling);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
const signBtn = page.locator('button:has-text("签发"), button:has-text("提交")');
|
||||||
|
if (await signBtn.isVisible()) {
|
||||||
|
await signBtn.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const errorMsg = page.locator('text=发放库房为空');
|
||||||
|
expect(await errorMsg.count()).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#427 检查项目分类手风琴展开 @regression', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.doctorStation);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
const categories = page.locator('.el-collapse-item, .category-item');
|
||||||
|
const count = await categories.count();
|
||||||
|
if (count > 0) {
|
||||||
|
await categories.first().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Normal file
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('🔄 并发操作测试', () => {
|
||||||
|
test('#437 多窗口同时操作手术计费 @bug437', async ({ browser }) => {
|
||||||
|
const context1 = await browser.newContext();
|
||||||
|
const context2 = await browser.newContext();
|
||||||
|
|
||||||
|
const page1 = await context1.newPage();
|
||||||
|
const page2 = await context2.newPage();
|
||||||
|
|
||||||
|
// Login on both pages
|
||||||
|
for (const page of [page1, page2]) {
|
||||||
|
await page.goto(TEST_URLS.login);
|
||||||
|
await page.fill('input[placeholder="账号"]', TEST_USERS.admin.username);
|
||||||
|
await page.fill('input[placeholder="密码"]', TEST_USERS.admin.password);
|
||||||
|
await page.click('button:has-text("登 录")');
|
||||||
|
await page.waitForURL(/.*(dashboard|home|index).*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page1.goto(TEST_URLS.surgeryBilling),
|
||||||
|
page2.goto(TEST_URLS.surgeryBilling),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page1.waitForLoadState('networkidle'),
|
||||||
|
page2.waitForLoadState('networkidle'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const genBtn1 = page1.locator('button:has-text("生成")');
|
||||||
|
const genBtn2 = page2.locator('button:has-text("生成")');
|
||||||
|
|
||||||
|
if (await genBtn1.isVisible() && await genBtn2.isVisible()) {
|
||||||
|
await Promise.all([
|
||||||
|
genBtn1.click().catch(() => {}),
|
||||||
|
genBtn2.click().catch(() => {}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page1.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const table1 = page1.locator('el-table__body tr, .el-table__row');
|
||||||
|
const table2 = page2.locator('el-table__body tr, .el-table__row');
|
||||||
|
|
||||||
|
const count1 = await table1.count();
|
||||||
|
const count2 = await table2.count();
|
||||||
|
|
||||||
|
expect(count1).toBe(count2);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context1.close();
|
||||||
|
await context2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
34
openhis-ui-vue3/tests/e2e/specs/doctor-station.spec.ts
Normal file
34
openhis-ui-vue3/tests/e2e/specs/doctor-station.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from '../pages/LoginPage';
|
||||||
|
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('🏥 门诊医生站', () => {
|
||||||
|
let loginPage: LoginPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||||
|
await loginPage.expectLoginSuccess();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#427 分类手风琴展开/收起 @regression', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.doctorStation);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const items = page.locator('.el-collapse-item, .category-item');
|
||||||
|
const count = await items.count();
|
||||||
|
|
||||||
|
if (count >= 2) {
|
||||||
|
await items.nth(0).click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await items.nth(1).click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-DOCTOR-001: 医生站页面加载 @smoke', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.doctorStation);
|
||||||
|
await expect(page).toHaveURL(/.*doctorstation.*/);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
openhis-ui-vue3/tests/e2e/specs/login.spec.ts
Normal file
38
openhis-ui-vue3/tests/e2e/specs/login.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from '../pages/LoginPage';
|
||||||
|
import { TEST_USERS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('🔐 登录模块', () => {
|
||||||
|
let loginPage: LoginPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-LOGIN-001: 管理员正常登录 @smoke', async ({ page }) => {
|
||||||
|
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||||
|
await loginPage.expectLoginSuccess();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-LOGIN-002: 错误密码登录 @smoke', async ({ page }) => {
|
||||||
|
await loginPage.login(TEST_USERS.admin.username, 'wrong_password_123');
|
||||||
|
// Check for any error indication (message, toast, or stayed on login page)
|
||||||
|
const hasError = await page.locator('.el-message--error, .el-message-box, text=密码错误, text=用户名或密码错误').isVisible().catch(() => false);
|
||||||
|
const stillOnLogin = page.url().includes('login') || page.url() === 'http://localhost:81/' || page.url() === 'http://localhost:81/index';
|
||||||
|
expect(hasError || stillOnLogin).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-LOGIN-003: 空用户名登录', async ({ page }) => {
|
||||||
|
await loginPage.login('', TEST_USERS.admin.password);
|
||||||
|
// Should show validation error or stay on login page
|
||||||
|
const hasError = await page.locator('.el-form-item__error, .el-message--error').isVisible().catch(() => false);
|
||||||
|
const stillOnLogin = page.url().includes('login') || page.url() === 'http://localhost:81/';
|
||||||
|
expect(hasError || stillOnLogin).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TC-LOGIN-004: 密码输入框可见性切换', async ({ page }) => {
|
||||||
|
const passwordInput = page.locator('input[placeholder="密码"]');
|
||||||
|
await expect(passwordInput).toHaveAttribute('type', 'password');
|
||||||
|
});
|
||||||
|
});
|
||||||
42
openhis-ui-vue3/tests/e2e/specs/surgery-billing.spec.ts
Normal file
42
openhis-ui-vue3/tests/e2e/specs/surgery-billing.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from '../pages/LoginPage';
|
||||||
|
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('💊 手术计费模块', () => {
|
||||||
|
let loginPage: LoginPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||||
|
await loginPage.expectLoginSuccess();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#437 快速连续点击防重复 @bug437 @smoke', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.surgeryBilling);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const genBtn = page.locator('button:has-text("生成"), button:has-text("新增")');
|
||||||
|
if (await genBtn.isVisible()) {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await genBtn.click().catch(() => {});
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const count = await page.locator('.el-dialog, .el-message-box').count();
|
||||||
|
expect(count).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#443 签发耗材不报库房错误 @bug443 @smoke', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.surgeryBilling);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const signBtn = page.locator('button:has-text("签发"), button:has-text("提交")');
|
||||||
|
if (await signBtn.isVisible()) {
|
||||||
|
await signBtn.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
await expect(page.locator('text=发放库房为空')).toHaveCount(0, { timeout: 5000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
export const TEST_USERS = {
|
export const TEST_USERS = {
|
||||||
admin: {
|
admin: {
|
||||||
username: process.env.TEST_USERNAME || 'admin',
|
username: process.env.TEST_USERNAME || 'admin',
|
||||||
password: process.env.TEST_PASSWORD || '123456',
|
password: process.env.TEST_PASSWORD || 'admin123',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TEST_URLS = {
|
export const TEST_URLS = {
|
||||||
login: '/',
|
login: '/',
|
||||||
dashboard: '/dashboard',
|
dashboard: '/index',
|
||||||
doctorStation: '/doctorstation',
|
doctorStation: '/doctorstation',
|
||||||
surgeryBilling: '/surgery-billing',
|
surgeryBilling: '/operatingroom',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests/e2e/specs',
|
testDir: './e2e/specs',
|
||||||
timeout: 60 * 1000,
|
fullyParallel: true,
|
||||||
expect: { timeout: 10000 },
|
timeout: 60_000,
|
||||||
fullyParallel: false,
|
expect: { timeout: 10_000 },
|
||||||
forbidOnly: !!process.env.CI,
|
retries: process.env.CI ? 2 : 1,
|
||||||
retries: process.env.CI ? 2 : 0,
|
workers: process.env.CI ? 2 : undefined,
|
||||||
workers: 1,
|
reporter: [
|
||||||
reporter: [['html', { outputFolder: 'playwright-report' }], ['list']],
|
['html', { outputFolder: 'tests/e2e/report', open: 'never' }],
|
||||||
|
['list'],
|
||||||
|
],
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.TEST_BASE_URL || 'http://localhost:80',
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:81',
|
||||||
trace: 'on-first-retry',
|
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timezoneId: 'Asia/Shanghai',
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
navigationTimeout: 30_000,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import createVitePlugins from './vite/plugins';
|
|||||||
export default defineConfig(({ mode, command }) => {
|
export default defineConfig(({ mode, command }) => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
const { VITE_APP_ENV } = env;
|
const { VITE_APP_ENV } = env;
|
||||||
|
const buildVersion = process.env.VITE_APP_VERSION || env.VITE_APP_VERSION || Date.now().toString();
|
||||||
return {
|
return {
|
||||||
// define: {
|
define: {
|
||||||
// // enable hydration mismatch details in production build
|
'import.meta.env.VITE_APP_BUILD_VERSION': JSON.stringify(buildVersion),
|
||||||
// __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
|
},
|
||||||
// },
|
|
||||||
// 部署生产环境和开发环境下的URL。
|
// 部署生产环境和开发环境下的URL。
|
||||||
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
||||||
// 例如 https://www.openHIS.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.openhis.vip/admin/,则设置 baseUrl 为 /admin/。
|
// 例如 https://www.openHIS.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.openhis.vip/admin/,则设置 baseUrl 为 /admin/。
|
||||||
|
|||||||
Reference in New Issue
Block a user