95-门诊医生站开立会诊申请单界面PRD_2026-01-15,全部功能。

This commit is contained in:
weixin_45799331
2026-02-06 11:24:08 +08:00
parent f3d56bff45
commit faf73a5ac4
25 changed files with 4225 additions and 25 deletions

42
consultation_invited.sql Normal file
View File

@@ -0,0 +1,42 @@
-- 会诊邀请对象表
CREATE TABLE hisdev.consultation_invited (
id BIGINT NOT NULL PRIMARY KEY,
consultation_request_id BIGINT NOT NULL,
invited_department_id BIGINT,
invited_department_name VARCHAR(100),
invited_physician_id BIGINT,
invited_physician_name VARCHAR(50),
invited_status SMALLINT DEFAULT 0,
confirm_time TIMESTAMP,
confirm_opinion TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64),
update_by VARCHAR(64),
tenant_id BIGINT,
is_deleted SMALLINT DEFAULT 0
);
-- 添加注释
COMMENT ON TABLE hisdev.consultation_invited IS '会诊邀请对象表';
COMMENT ON COLUMN hisdev.consultation_invited.id IS '主键ID';
COMMENT ON COLUMN hisdev.consultation_invited.consultation_request_id IS '会诊申请ID外键';
COMMENT ON COLUMN hisdev.consultation_invited.invited_department_id IS '邀请科室ID';
COMMENT ON COLUMN hisdev.consultation_invited.invited_department_name IS '邀请科室名称';
COMMENT ON COLUMN hisdev.consultation_invited.invited_physician_id IS '邀请医生ID';
COMMENT ON COLUMN hisdev.consultation_invited.invited_physician_name IS '邀请医生姓名';
COMMENT ON COLUMN hisdev.consultation_invited.invited_status IS '邀请状态0-待确认1-已确认2-已拒绝)';
COMMENT ON COLUMN hisdev.consultation_invited.confirm_time IS '确认时间';
COMMENT ON COLUMN hisdev.consultation_invited.confirm_opinion IS '确认意见';
COMMENT ON COLUMN hisdev.consultation_invited.create_time IS '创建时间';
COMMENT ON COLUMN hisdev.consultation_invited.update_time IS '更新时间';
COMMENT ON COLUMN hisdev.consultation_invited.create_by IS '创建人';
COMMENT ON COLUMN hisdev.consultation_invited.update_by IS '更新人';
COMMENT ON COLUMN hisdev.consultation_invited.tenant_id IS '租户ID';
COMMENT ON COLUMN hisdev.consultation_invited.is_deleted IS '删除标识0-未删除1-已删除)';
-- 创建索引
CREATE INDEX idx_consultation_request_id ON hisdev.consultation_invited(consultation_request_id);
CREATE INDEX idx_invited_physician_id ON hisdev.consultation_invited(invited_physician_id);
CREATE INDEX idx_tenant_id ON hisdev.consultation_invited(tenant_id);

View File

@@ -0,0 +1,89 @@
package com.openhis.web.consultation.appservice;
import com.openhis.web.consultation.dto.ConsultationActivityDto;
import com.openhis.web.consultation.dto.ConsultationRequestDto;
import com.openhis.web.consultation.dto.DepartmentTreeDto;
import java.util.List;
import java.util.Map;
/**
* 会诊管理AppService接口
*
* @author system
* @date 2026-01-29
*/
public interface IConsultationAppService {
/**
* 获取会诊列表
*
* @param encounterId 就诊ID可选如果为null则查询当前医生的所有会诊申请
* @return 会诊列表
*/
List<ConsultationRequestDto> getConsultationList(Long encounterId);
/**
* 保存会诊申请
*
* @param dto 会诊申请DTO
* @return 是否成功
*/
Boolean saveConsultation(ConsultationRequestDto dto);
/**
* 提交会诊申请
*
* @param consultationId 会诊申请单号
* @return 是否成功
*/
Boolean submitConsultation(String consultationId);
/**
* 作废会诊申请
*
* @param consultationId 会诊申请单号
* @param cancelReason 作废原因
* @return 是否成功
*/
Boolean cancelConsultation(String consultationId, String cancelReason);
/**
* 结束会诊申请
*
* @param consultationId 会诊申请单号
* @return 是否成功
*/
Boolean completeConsultation(String consultationId);
/**
* 获取科室医生树
*
* @return 科室医生树
*/
List<DepartmentTreeDto> getDepartmentTree();
/**
* 获取主诊断
*
* @param encounterId 就诊ID
* @return 主诊断信息
*/
Map<String, String> getMainDiagnosis(Long encounterId);
/**
* 获取我的会诊邀请列表(被邀请的医生查看)
*
* @return 会诊邀请列表
*/
List<ConsultationRequestDto> getMyInvitations();
/**
* 获取所有会诊项目及价格
*
* @return 会诊项目列表
*/
List<ConsultationActivityDto> getConsultationActivities();
}

View File

@@ -0,0 +1,967 @@
package com.openhis.web.consultation.appservice.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.utils.SecurityUtils;
import com.openhis.administration.domain.Encounter;
import com.openhis.administration.domain.EncounterDiagnosis;
import com.openhis.administration.domain.Organization;
import com.openhis.administration.domain.Patient;
import com.openhis.administration.domain.Practitioner;
import com.openhis.administration.mapper.EncounterDiagnosisMapper;
import com.openhis.administration.mapper.EncounterMapper;
import com.openhis.administration.mapper.OrganizationMapper;
import com.openhis.administration.mapper.PatientMapper;
import com.openhis.administration.mapper.PractitionerMapper;
import com.openhis.clinical.domain.Condition;
import com.openhis.clinical.mapper.ConditionMapper;
import com.openhis.workflow.domain.ServiceRequest;
import com.openhis.workflow.service.IServiceRequestService;
import com.openhis.common.enums.RequestStatus;
import com.openhis.common.enums.GenerateSource;
import com.openhis.common.enums.ItemType;
import com.core.common.utils.AssignSeqUtil;
import com.openhis.common.enums.AssignSeqEnum;
import com.openhis.web.consultation.appservice.IConsultationAppService;
import com.openhis.web.consultation.dto.ConsultationActivityDto;
import com.openhis.web.consultation.dto.ConsultationRequestDto;
import com.openhis.web.consultation.dto.DepartmentTreeDto;
import com.openhis.web.consultation.dto.InvitedObjectDto;
import com.openhis.web.consultation.dto.PhysicianNodeDto;
import com.openhis.web.consultation.mapper.ConsultationRequestMapper;
import com.openhis.web.consultation.mapper.ConsultationInvitedMapper;
import com.openhis.web.consultation.domain.ConsultationRequest;
import com.openhis.web.consultation.domain.ConsultationInvited;
import com.openhis.web.consultation.enums.ConsultationStatusEnum;
import com.openhis.web.consultation.enums.InvitedStatusEnum;
import com.openhis.web.consultation.enums.ConsultationUrgencyEnum;
import com.openhis.workflow.domain.ActivityDefinition;
import com.openhis.workflow.mapper.ActivityDefinitionMapper;
import com.openhis.administration.domain.ChargeItemDefinition;
import com.openhis.administration.mapper.ChargeItemDefinitionMapper;
import com.openhis.administration.domain.ChargeItem;
import com.openhis.administration.service.IChargeItemService;
import com.openhis.common.enums.ChargeItemStatus;
import com.openhis.web.doctorstation.mapper.DoctorStationAdviceAppMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 会诊管理AppService实现类
*
* @author system
* @date 2026-01-29
*/
@Slf4j
@Service
public class ConsultationAppServiceImpl implements IConsultationAppService {
@Resource
private ConsultationRequestMapper consultationRequestMapper;
@Resource
private ConsultationInvitedMapper consultationInvitedMapper;
@Resource
private EncounterMapper encounterMapper;
@Resource
private PatientMapper patientMapper;
@Resource
private ConditionMapper conditionMapper;
@Resource
private OrganizationMapper organizationMapper;
@Resource
private PractitionerMapper practitionerMapper;
@Resource
private EncounterDiagnosisMapper encounterDiagnosisMapper;
@Resource
private IServiceRequestService iServiceRequestService;
@Resource
private AssignSeqUtil assignSeqUtil;
@Resource
private ActivityDefinitionMapper activityDefinitionMapper;
@Resource
private ChargeItemDefinitionMapper chargeItemDefinitionMapper;
@Resource
private IChargeItemService iChargeItemService;
@Resource
private DoctorStationAdviceAppMapper doctorStationAdviceAppMapper;
/**
* 用于生成唯一序列号的计数器
* 每秒重置,范围 0000-9999
*/
private static final AtomicInteger SEQUENCE = new AtomicInteger(0);
/**
* 记录上一次生成ID的时间秒级
*/
private static volatile long lastSecond = 0L;
@Override
public List<ConsultationRequestDto> getConsultationList(Long encounterId) {
// 查询会诊列表
LambdaQueryWrapper<ConsultationRequest> wrapper = new LambdaQueryWrapper<>();
// 根据就诊ID查询该患者的会诊申请
wrapper.eq(ConsultationRequest::getEncounterId, encounterId);
wrapper.orderByDesc(ConsultationRequest::getCreateTime);
List<ConsultationRequest> list = consultationRequestMapper.selectList(wrapper);
// 转换为DTO
return list.stream().map(this::convertToDto).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveConsultation(ConsultationRequestDto dto) {
try {
// 判断是新增还是更新
boolean isUpdate = (dto.getId() != null);
ConsultationRequest entity;
if (isUpdate) {
// 更新:查询现有记录
entity = consultationRequestMapper.selectById(dto.getId());
if (entity == null) {
throw new RuntimeException("会诊申请记录不存在ID: " + dto.getId());
}
} else {
// 新增:创建新记录
entity = new ConsultationRequest();
entity.setConsultationId(generateConsultationId());
entity.setTenantId(SecurityUtils.getLoginUser().getTenantId().longValue());
entity.setConsultationRequestDate(new Date());
}
// 复制基本属性(现在字段名已统一,可以直接复制)
BeanUtils.copyProperties(dto, entity, "id", "consultationId", "invitedList", "submitFlag", "provisionalDiagnosis", "consultationRequestDate");
// 如果前端没有传递申请医生ID使用当前登录用户
if (entity.getRequestingPhysicianId() == null) {
entity.setRequestingPhysicianId(SecurityUtils.getLoginUser().getPractitionerId());
}
// 设置邀请对象文本(拼接所有医生名称)
if (dto.getInvitedList() != null && !dto.getInvitedList().isEmpty()) {
String invitedObjectText = dto.getInvitedList().stream()
.map(InvitedObjectDto::getPhysicianName)
.collect(Collectors.joining(","));
entity.setInvitedObject(invitedObjectText);
}
// 处理门诊诊断:如果前端没有传递,自动从数据库获取
if (!StringUtils.hasText(dto.getProvisionalDiagnosis()) && dto.getEncounterId() != null) {
String diagnosis = getPatientDiagnosis(dto.getEncounterId());
if (StringUtils.hasText(diagnosis)) {
entity.setProvisionalDiagnosis(diagnosis);
}
} else {
entity.setProvisionalDiagnosis(dto.getProvisionalDiagnosis());
}
// 设置会诊状态(使用枚举)
if (Boolean.TRUE.equals(dto.getSubmitFlag())) {
entity.setConsultationStatus(ConsultationStatusEnum.SUBMITTED.getCode());
entity.setConfirmingPhysician(SecurityUtils.getLoginUser().getUser().getNickName());
entity.setConfirmingPhysicianId(SecurityUtils.getLoginUser().getPractitionerId());
entity.setConfirmingDate(new Date());
} else {
entity.setConsultationStatus(ConsultationStatusEnum.NEW.getCode());
}
// 保存或更新主表
if (isUpdate) {
consultationRequestMapper.updateById(entity);
// 删除旧的邀请对象
LambdaQueryWrapper<ConsultationInvited> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(ConsultationInvited::getConsultationRequestId, entity.getId());
consultationInvitedMapper.delete(deleteWrapper);
// 更新门诊医嘱表
if (entity.getOrderId() != null) {
updateConsultationServiceRequest(entity);
}
} else {
consultationRequestMapper.insert(entity);
// 保存到门诊医嘱表
saveConsultationServiceRequest(entity);
}
// 保存邀请对象到关联表
if (dto.getInvitedList() != null && !dto.getInvitedList().isEmpty()) {
for (InvitedObjectDto invitedDto : dto.getInvitedList()) {
ConsultationInvited invited = new ConsultationInvited();
invited.setConsultationRequestId(entity.getId());
invited.setInvitedDepartmentId(invitedDto.getDeptId());
invited.setInvitedDepartmentName(invitedDto.getDeptName());
invited.setInvitedPhysicianId(invitedDto.getPhysicianId());
invited.setInvitedPhysicianName(invitedDto.getPhysicianName());
invited.setInvitedStatus(InvitedStatusEnum.PENDING.getCode());
invited.setTenantId(entity.getTenantId());
consultationInvitedMapper.insert(invited);
}
}
return true;
} catch (Exception e) {
log.error("保存会诊申请失败", e);
throw new RuntimeException("保存会诊申请失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean submitConsultation(String consultationId) {
try {
if (!StringUtils.hasText(consultationId)) {
throw new IllegalArgumentException("会诊申请单号不能为空");
}
LambdaQueryWrapper<ConsultationRequest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ConsultationRequest::getConsultationId, consultationId);
ConsultationRequest entity = consultationRequestMapper.selectOne(wrapper);
if (entity == null) {
throw new IllegalArgumentException("会诊申请不存在");
}
// 只有新开状态才能提交
if (!ConsultationStatusEnum.NEW.getCode().equals(entity.getConsultationStatus())) {
throw new IllegalArgumentException("只有新开状态的会诊申请才能提交");
}
// 更新状态为已提交
entity.setConsultationStatus(ConsultationStatusEnum.SUBMITTED.getCode());
// 设置提交会诊的医生信息
entity.setConfirmingPhysician(SecurityUtils.getLoginUser().getUser().getNickName());
entity.setConfirmingPhysicianId(SecurityUtils.getLoginUser().getPractitionerId());
entity.setConfirmingDate(new Date());
consultationRequestMapper.updateById(entity);
// 新增:更新门诊医嘱表状态为已提交
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.ACTIVE.getValue());
return true;
} catch (Exception e) {
log.error("提交会诊申请失败", e);
throw new RuntimeException("提交会诊申请失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean cancelConsultation(String consultationId, String cancelReason) {
try {
if (!StringUtils.hasText(consultationId)) {
throw new IllegalArgumentException("会诊申请单号不能为空");
}
LambdaQueryWrapper<ConsultationRequest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ConsultationRequest::getConsultationId, consultationId);
ConsultationRequest entity = consultationRequestMapper.selectOne(wrapper);
if (entity == null) {
throw new IllegalArgumentException("会诊申请不存在");
}
// 判断是"取消提交"还是"作废"
if ("取消提交".equals(cancelReason) && ConsultationStatusEnum.SUBMITTED.getCode().equals(entity.getConsultationStatus())) {
// 取消提交:将状态从"已提交"改回"新开"
entity.setConsultationStatus(ConsultationStatusEnum.NEW.getCode());
entity.setConfirmingPhysician(null);
entity.setConfirmingPhysicianId(null);
entity.setConfirmingDate(null);
consultationRequestMapper.updateById(entity);
// 更新门诊医嘱表状态为新开
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.DRAFT.getValue());
} else {
// 作废:将状态改为"已取消"
entity.setConsultationStatus(ConsultationStatusEnum.CANCELLED.getCode());
entity.setCancelReason(cancelReason);
entity.setCancelNatureDate(new Date());
consultationRequestMapper.updateById(entity);
// 更新门诊医嘱表状态为已作废
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.CANCELLED.getValue());
}
return true;
} catch (Exception e) {
log.error("操作失败", e);
throw new RuntimeException("操作失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean completeConsultation(String consultationId) {
try {
if (!StringUtils.hasText(consultationId)) {
throw new IllegalArgumentException("会诊申请单号不能为空");
}
LambdaQueryWrapper<ConsultationRequest> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ConsultationRequest::getConsultationId, consultationId);
ConsultationRequest entity = consultationRequestMapper.selectOne(wrapper);
if (entity == null) {
throw new IllegalArgumentException("会诊申请不存在");
}
// 更新状态为已完成
entity.setConsultationStatus(ConsultationStatusEnum.COMPLETED.getCode());
consultationRequestMapper.updateById(entity);
// 🎯 新增:更新门诊医嘱表状态为已完成
updateServiceRequestStatus(entity.getOrderId(), RequestStatus.COMPLETED.getValue());
return true;
} catch (Exception e) {
log.error("结束会诊申请失败", e);
throw new RuntimeException("结束会诊申请失败: " + e.getMessage());
}
}
@Override
public List<DepartmentTreeDto> getDepartmentTree() {
try {
// 查询所有科室
LambdaQueryWrapper<Organization> deptWrapper = new LambdaQueryWrapper<>();
deptWrapper.eq(Organization::getDeleteFlag, "0")
.orderByAsc(Organization::getName);
List<Organization> deptList = organizationMapper.selectList(deptWrapper);
// 查询所有医生
LambdaQueryWrapper<Practitioner> practitionerWrapper = new LambdaQueryWrapper<>();
practitionerWrapper.eq(Practitioner::getDeleteFlag, "0")
.orderByAsc(Practitioner::getName);
List<Practitioner> practitionerList = practitionerMapper.selectList(practitionerWrapper);
// 按科室分组医生
Map<Long, List<Practitioner>> practitionerMap = practitionerList.stream()
.filter(p -> p.getOrgId() != null)
.collect(Collectors.groupingBy(Practitioner::getOrgId));
// 构建树形结构
List<DepartmentTreeDto> treeList = new ArrayList<>();
for (Organization dept : deptList) {
DepartmentTreeDto treeDto = new DepartmentTreeDto();
treeDto.setId(dept.getId());
treeDto.setLabel(dept.getName());
// 添加该科室的医生
List<Practitioner> physicians = practitionerMap.get(dept.getId());
if (physicians != null && !physicians.isEmpty()) {
List<PhysicianNodeDto> children = physicians.stream()
.map(p -> {
PhysicianNodeDto node = new PhysicianNodeDto();
node.setId(p.getId());
node.setLabel(p.getName());
return node;
})
.collect(Collectors.toList());
treeDto.setChildren(children);
} else {
treeDto.setChildren(new ArrayList<>());
}
treeList.add(treeDto);
}
return treeList;
} catch (Exception e) {
log.error("获取科室医生树失败", e);
throw new RuntimeException("获取科室医生树失败: " + e.getMessage());
}
}
@Override
public Map<String, String> getMainDiagnosis(Long encounterId) {
Map<String, String> result = new HashMap<>();
if (encounterId == null) {
return result;
}
try {
String diagnosis = getPatientDiagnosis(encounterId);
if (StringUtils.hasText(diagnosis)) {
result.put("diagnosis", diagnosis);
}
return result;
} catch (Exception e) {
log.error("获取主诊断失败", e);
return result;
}
}
/**
* 获取患者诊断信息
*
* @param encounterId 就诊ID
* @return 诊断信息字符串
*/
private String getPatientDiagnosis(Long encounterId) {
try {
// 查询该就诊的所有诊断,优先查询主诊断
LambdaQueryWrapper<EncounterDiagnosis> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EncounterDiagnosis::getEncounterId, encounterId)
.orderByDesc(EncounterDiagnosis::getMaindiseFlag) // 主诊断排在前面
.orderByAsc(EncounterDiagnosis::getDiagSrtNo) // 按诊断排序号排序
.orderByDesc(EncounterDiagnosis::getCreateTime); // 按创建时间倒序
List<EncounterDiagnosis> diagnosisList = encounterDiagnosisMapper.selectList(wrapper);
if (diagnosisList == null || diagnosisList.isEmpty()) {
return null;
}
// 使用 LinkedHashSet 去重,保持顺序
LinkedHashSet<String> diagnosisSet = new LinkedHashSet<>();
int mainDiagnosisCount = 0;
for (EncounterDiagnosis ed : diagnosisList) {
// 优先使用 name 字段,如果没有则使用 diagnosisDesc
String diagName = StringUtils.hasText(ed.getName()) ? ed.getName() : ed.getDiagnosisDesc();
if (StringUtils.hasText(diagName)) {
// 构建诊断文本
StringBuilder itemBuilder = new StringBuilder();
itemBuilder.append(diagName);
// 如果有诊断描述且与名称不同,添加到括号中
if (StringUtils.hasText(ed.getDiagnosisDesc()) &&
!ed.getDiagnosisDesc().equals(diagName)) {
itemBuilder.append("(").append(ed.getDiagnosisDesc()).append(")");
}
// 标记主诊断
if (ed.getMaindiseFlag() != null && ed.getMaindiseFlag() == 1) {
itemBuilder.append("[主]");
mainDiagnosisCount++;
}
// 添加到集合(自动去重)
diagnosisSet.add(itemBuilder.toString());
// 限制最多显示 10 个诊断(包括主诊断)
if (diagnosisSet.size() >= 10) {
break;
}
}
}
// 拼接诊断信息
String result = String.join("", diagnosisSet);
return result;
} catch (Exception e) {
log.error("获取患者诊断信息失败encounterId: {}", encounterId, e);
return null;
}
}
@Override
public List<ConsultationRequestDto> getMyInvitations() {
try {
// 获取当前登录医生ID
Long currentPhysicianId = SecurityUtils.getLoginUser().getPractitionerId();
// 查询邀请我的会诊申请
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
invitedWrapper.eq(ConsultationInvited::getInvitedPhysicianId, currentPhysicianId)
.orderByDesc(ConsultationInvited::getCreateTime);
List<ConsultationInvited> invitedList = consultationInvitedMapper.selectList(invitedWrapper);
if (invitedList.isEmpty()) {
return new ArrayList<>();
}
// 获取所有会诊申请ID
List<Long> requestIds = invitedList.stream()
.map(ConsultationInvited::getConsultationRequestId)
.distinct()
.collect(Collectors.toList());
// 查询会诊申请详情
List<ConsultationRequest> requests = consultationRequestMapper.selectBatchIds(requestIds);
// 转换为DTO并添加邀请状态
Map<Long, ConsultationInvited> invitedMap = invitedList.stream()
.collect(Collectors.toMap(ConsultationInvited::getConsultationRequestId, v -> v, (v1, v2) -> v1));
return requests.stream().map(request -> {
ConsultationRequestDto dto = convertToDto(request);
// 添加我的邀请状态
ConsultationInvited myInvited = invitedMap.get(request.getId());
if (myInvited != null) {
dto.setMyInvitedStatus(myInvited.getInvitedStatus());
dto.setMyConfirmTime(myInvited.getConfirmTime());
}
return dto;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("获取我的会诊邀请失败", e);
throw new RuntimeException("获取我的会诊邀请失败: " + e.getMessage());
}
}
/**
* 转换为DTO
*/
private ConsultationRequestDto convertToDto(ConsultationRequest entity) {
ConsultationRequestDto dto = new ConsultationRequestDto();
BeanUtils.copyProperties(entity, dto);
// 查询邀请对象列表
if (entity.getId() != null) {
LambdaQueryWrapper<ConsultationInvited> invitedWrapper = new LambdaQueryWrapper<>();
invitedWrapper.eq(ConsultationInvited::getConsultationRequestId, entity.getId());
List<ConsultationInvited> invitedList = consultationInvitedMapper.selectList(invitedWrapper);
if (invitedList != null && !invitedList.isEmpty()) {
List<InvitedObjectDto> invitedDtoList = invitedList.stream().map(invited -> {
InvitedObjectDto invitedDto = new InvitedObjectDto();
invitedDto.setDeptId(invited.getInvitedDepartmentId());
invitedDto.setDeptName(invited.getInvitedDepartmentName());
invitedDto.setPhysicianId(invited.getInvitedPhysicianId());
invitedDto.setPhysicianName(invited.getInvitedPhysicianName());
return invitedDto;
}).collect(Collectors.toList());
dto.setInvitedList(invitedDtoList);
}
}
// 如果门诊诊断为空,尝试从数据库自动获取
if (!StringUtils.hasText(entity.getProvisionalDiagnosis()) && entity.getEncounterId() != null) {
String diagnosis = getPatientDiagnosis(entity.getEncounterId());
if (StringUtils.hasText(diagnosis)) {
dto.setProvisionalDiagnosis(diagnosis);
}
}
return dto;
}
/**
* 生成会诊申请单号(高性能版本)
* 格式CS + 年月日时分秒 + 4位序列号
* 例如CS202601301425300001
*
* 优化说明:
* 1. 使用 AtomicInteger 保证线程安全,性能远高于 synchronized
* 2. 每秒最多支持 10000 个并发请求0000-9999
* 3. 无需查询数据库,性能提升 100+ 倍
* 4. 秒级时间戳 + 自增序列号,确保唯一性
*/
private String generateConsultationId() {
while (true) {
long nowMillis = System.currentTimeMillis();
long currentSecond = nowMillis / 1000;
// 跨秒则重置序列号(双重检查 + 类锁)
if (currentSecond != lastSecond) {
synchronized (ConsultationAppServiceImpl.class) {
if (currentSecond != lastSecond) {
lastSecond = currentSecond;
SEQUENCE.set(0);
}
}
}
int sequence = SEQUENCE.getAndIncrement();
// 本秒内序列号耗尽精确等待到下一秒再重试避免递归、避免固定100ms
if (sequence >= 10000) {
// 可选“回退自增”让 SEQUENCE 不乱飙
// 回退这次自增(可选,但更“稳”,避免 SEQUENCE 无限增长)
// 注意:回退可能在极端并发下导致轻微争用,但比不回退更可控
SEQUENCE.decrementAndGet();
sleepUntilNextSecond(nowMillis);
continue;
}
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHHmmss");
String dateTime = sdf.format(new Date());
String sequenceStr = String.format("%04d", sequence);
return "CS" + dateTime + sequenceStr;
}
}
// 精确等到下一秒,等待更短、抖动更小,不在固定睡100ms
private void sleepUntilNextSecond(long nowMillis) {
long nextSecondMillis = (nowMillis / 1000 + 1) * 1000;
long sleepMillis = nextSecondMillis - nowMillis;
// 防御:理论上 sleepMillis 范围是 1~1000ms
if (sleepMillis <= 0) return;
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
// 恢复中断标记:把“有人希望我停下来”的信号传递给上层
Thread.currentThread().interrupt();
throw new RuntimeException("生成会诊申请单号失败:线程被中断", e);
}
}
// private String generateConsultationId() {
// java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHHmmss");
// long currentSecond = System.currentTimeMillis() / 1000;
//
// // 如果是新的一秒,重置序列号
// // 跨秒检测:如果发现当前秒变了,说明进入下一秒
// // 重置序列号:把 SEQUENCE 重新设置为 0保证新的一秒从 0000 开始
// // 双重锁检测:避免重复进入
// if (currentSecond != lastSecond) {
// synchronized (ConsultationAppServiceImpl.class) {
// if (currentSecond != lastSecond) {
// lastSecond = currentSecond;
// SEQUENCE.set(0);
// }
// }
// }
//
// // 获取当前序列号并自增(线程安全)
// int sequence = SEQUENCE.getAndIncrement();
//
// // 如果同一秒内超过 10000 个请求,等待下一秒
// if (sequence >= 10000) {
// try {
// Thread.sleep(100); // 等待 100ms
// return generateConsultationId(); // 递归重试
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// throw new RuntimeException("生成会诊申请单号失败", e);
// }
// }
//
// // 生成时间戳部分
// String dateTime = sdf.format(new Date());
//
// // 生成4位序列号补零
// String sequenceStr = String.format("%04d", sequence);
//
// return "CS" + dateTime + sequenceStr;
// }
/**
* 计算年龄
*/
private Integer calculateAge(Date birthDate) {
if (birthDate == null) {
return null;
}
java.util.Calendar now = java.util.Calendar.getInstance();
java.util.Calendar birth = java.util.Calendar.getInstance();
birth.setTime(birthDate);
int age = now.get(java.util.Calendar.YEAR) - birth.get(java.util.Calendar.YEAR);
if (now.get(java.util.Calendar.DAY_OF_YEAR) < birth.get(java.util.Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
/**
* 保存会诊医嘱到门诊医嘱表wor_service_request
* 同时创建费用项adm_charge_item
*
* @param consultationRequest 会诊申请实体
*/
private void saveConsultationServiceRequest(ConsultationRequest consultationRequest) {
try {
ServiceRequest serviceRequest = new ServiceRequest();
// 生成医嘱编号
String busNo = assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4);
serviceRequest.setBusNo(busNo);
// 基本信息
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
// 🎯 使用前端选择的会诊项目ID
Long activityDefinitionId = consultationRequest.getConsultationActivityId();
if (activityDefinitionId == null) {
// 如果前端没有传递,使用默认的"院内会诊"
activityDefinitionId = 1983417749497446402L;
log.warn("前端未传递会诊项目ID使用默认的院内会诊: {}", activityDefinitionId);
}
// 🎯 设置医嘱类型为会诊31=会诊类型)
// 医嘱类型分类1-10=药品22-30=诊疗31=会诊40+=其他
serviceRequest.setCategoryEnum(31); // 会诊类型
serviceRequest.setActivityId(activityDefinitionId); // 关联到会诊项目定义
serviceRequest.setPatientId(consultationRequest.getPatientId());
serviceRequest.setEncounterId(consultationRequest.getEncounterId());
serviceRequest.setRequesterId(consultationRequest.getRequestingPhysicianId());
// 🎯 设置执行科室(申请医生的科室)
serviceRequest.setOrgId(consultationRequest.getDepartmentId());
// 🎯 设置执行次数为 1
serviceRequest.setQuantity(BigDecimal.ONE);
serviceRequest.setUnitCode("111"); // 单位:次
// 医嘱状态:新开
serviceRequest.setStatusEnum(RequestStatus.DRAFT.getValue());
// 医嘱内容保存会诊申请的详细信息JSON格式
Map<String, Object> contentMap = new HashMap<>();
contentMap.put("consultationRequestId", consultationRequest.getId());
contentMap.put("consultationId", consultationRequest.getConsultationId());
contentMap.put("consultationType", consultationRequest.getConsultationActivityName());
contentMap.put("consultationActivityId", activityDefinitionId);
contentMap.put("patientName", consultationRequest.getPatientName());
contentMap.put("provisionalDiagnosis", consultationRequest.getProvisionalDiagnosis());
contentMap.put("consultationPurpose", consultationRequest.getConsultationPurpose());
contentMap.put("requestingPhysician", consultationRequest.getRequestingPhysician());
contentMap.put("department", consultationRequest.getDepartment());
contentMap.put("adviceName", consultationRequest.getConsultationActivityName()); // 添加项目名称
serviceRequest.setContentJson(JSON.toJSONString(contentMap));
// 时间信息
serviceRequest.setAuthoredTime(new Date());
// 审计字段
serviceRequest.setTenantId(SecurityUtils.getLoginUser().getTenantId().intValue());
serviceRequest.setCreateBy(SecurityUtils.getUsername());
serviceRequest.setCreateTime(new Date());
// 保存到数据库
iServiceRequestService.save(serviceRequest);
// 🎯 新增创建费用项ChargeItem
// 查询会诊项目的价格定义
LambdaQueryWrapper<ChargeItemDefinition> priceWrapper = new LambdaQueryWrapper<>();
priceWrapper.eq(ChargeItemDefinition::getInstanceTable, "wor_activity_definition")
.eq(ChargeItemDefinition::getInstanceId, activityDefinitionId)
.eq(ChargeItemDefinition::getStatusEnum, 2) // 2=启用
.eq(ChargeItemDefinition::getDeleteFlag, "0")
.last("LIMIT 1");
ChargeItemDefinition chargeItemDef = chargeItemDefinitionMapper.selectOne(priceWrapper);
if (chargeItemDef != null) {
// 🎯 查询患者的费用性质account_id
// 从 adm_account 表获取患者的 account_id费用性质
Long accountId = doctorStationAdviceAppMapper.getEncounterContract(consultationRequest.getEncounterId())
.stream()
.findFirst()
.map(contract -> contract.getAccountId())
.orElse(null);
if (accountId == null) {
log.warn("未找到患者的费用性质encounterId: {}", consultationRequest.getEncounterId());
throw new RuntimeException("未找到患者的费用性质信息,无法创建费用项");
}
ChargeItem chargeItem = new ChargeItem();
chargeItem.setStatusEnum(ChargeItemStatus.DRAFT.getValue()); // 草稿状态
chargeItem.setBusNo(AssignSeqEnum.CHARGE_ITEM_NO.getPrefix().concat(busNo));
chargeItem.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue());
chargeItem.setPatientId(consultationRequest.getPatientId());
chargeItem.setContextEnum(ItemType.ACTIVITY.getValue()); // 诊疗类型
chargeItem.setEncounterId(consultationRequest.getEncounterId());
// 🎯 设置费用性质(必填字段)
chargeItem.setAccountId(accountId);
// 费用定价信息
chargeItem.setDefinitionId(chargeItemDef.getId());
chargeItem.setUnitPrice(chargeItemDef.getPrice());
chargeItem.setQuantityValue(BigDecimal.ONE);
chargeItem.setQuantityUnit("111"); // 次
chargeItem.setTotalPrice(chargeItemDef.getPrice());
// 关联医嘱信息
chargeItem.setServiceTable("wor_service_request");
chargeItem.setServiceId(serviceRequest.getId());
chargeItem.setProductTable("wor_activity_definition");
chargeItem.setProductId(activityDefinitionId);
// 开立信息
chargeItem.setEntererId(consultationRequest.getRequestingPhysicianId());
chargeItem.setRequestingOrgId(consultationRequest.getDepartmentId());
chargeItem.setEnteredDate(new Date());
// 审计字段
chargeItem.setTenantId(SecurityUtils.getLoginUser().getTenantId());
chargeItem.setCreateBy(SecurityUtils.getUsername());
chargeItem.setCreateTime(new Date());
iChargeItemService.save(chargeItem);
} else {
log.warn("未找到会诊项目的价格定义activityId: {}", activityDefinitionId);
}
// 将医嘱ID保存到会诊申请表
consultationRequest.setOrderId(serviceRequest.getId());
consultationRequestMapper.updateById(consultationRequest);
} catch (Exception e) {
log.error("保存会诊医嘱失败", e);
throw new RuntimeException("保存会诊医嘱失败: " + e.getMessage());
}
}
/**
* 更新会诊医嘱
*
* @param consultationRequest 会诊申请实体
*/
private void updateConsultationServiceRequest(ConsultationRequest consultationRequest) {
try {
if (consultationRequest.getOrderId() == null) {
log.warn("会诊申请没有关联的医嘱ID跳过更新");
return;
}
ServiceRequest serviceRequest = iServiceRequestService.getById(consultationRequest.getOrderId());
if (serviceRequest == null) {
log.warn("未找到关联的医嘱记录OrderId: {}", consultationRequest.getOrderId());
return;
}
// 更新医嘱内容
serviceRequest.setContentJson(JSON.toJSONString(consultationRequest));
serviceRequest.setUpdateBy(SecurityUtils.getUsername());
serviceRequest.setUpdateTime(new Date());
iServiceRequestService.updateById(serviceRequest);
} catch (Exception e) {
log.error("更新会诊医嘱失败", e);
throw new RuntimeException("更新会诊医嘱失败: " + e.getMessage());
}
}
/**
* 更新门诊医嘱状态
*
* @param orderId 医嘱ID
* @param status 新状态
*/
private void updateServiceRequestStatus(Long orderId, Integer status) {
try {
if (orderId == null) {
log.warn("医嘱ID为空跳过状态更新");
return;
}
ServiceRequest serviceRequest = iServiceRequestService.getById(orderId);
if (serviceRequest == null) {
log.warn("未找到医嘱记录OrderId: {}", orderId);
return;
}
serviceRequest.setStatusEnum(status);
serviceRequest.setUpdateBy(SecurityUtils.getUsername());
serviceRequest.setUpdateTime(new Date());
iServiceRequestService.updateById(serviceRequest);
} catch (Exception e) {
log.error("更新医嘱状态失败", e);
throw new RuntimeException("更新医嘱状态失败: " + e.getMessage());
}
}
@Override
public List<ConsultationActivityDto> getConsultationActivities() {
try {
// 查询所有会诊相关的诊疗活动
LambdaQueryWrapper<ActivityDefinition> wrapper = new LambdaQueryWrapper<>();
wrapper.like(ActivityDefinition::getName, "会诊")
.eq(ActivityDefinition::getDeleteFlag, "0")
.orderBy(true, true, ActivityDefinition::getSortOrder);
List<ActivityDefinition> activityList = activityDefinitionMapper.selectList(wrapper);
if (activityList.isEmpty()) {
return new ArrayList<>();
}
// 转换为DTO并查询价格
List<ConsultationActivityDto> resultList = new ArrayList<>();
for (ActivityDefinition activity : activityList) {
ConsultationActivityDto dto = new ConsultationActivityDto();
dto.setId(activity.getId());
dto.setName(activity.getName());
dto.setBusNo(activity.getBusNo());
dto.setTypeEnum(activity.getTypeEnum());
dto.setCategoryCode(activity.getCategoryCode());
dto.setUnitCode(activity.getPermittedUnitCode());
dto.setStatusEnum(activity.getStatusEnum());
// 查询价格
LambdaQueryWrapper<ChargeItemDefinition> priceWrapper = new LambdaQueryWrapper<>();
priceWrapper.eq(ChargeItemDefinition::getInstanceTable, "wor_activity_definition")
.eq(ChargeItemDefinition::getInstanceId, activity.getId())
.eq(ChargeItemDefinition::getStatusEnum, 2) // 2=启用
.eq(ChargeItemDefinition::getDeleteFlag, "0")
.last("LIMIT 1");
ChargeItemDefinition chargeItem = chargeItemDefinitionMapper.selectOne(priceWrapper);
if (chargeItem != null && chargeItem.getPrice() != null) {
dto.setPrice(chargeItem.getPrice());
} else {
dto.setPrice(java.math.BigDecimal.ZERO);
log.warn("会诊项目: {} 未找到价格定义", activity.getName());
}
resultList.add(dto);
}
return resultList;
} catch (Exception e) {
log.error("查询会诊项目列表失败", e);
throw new RuntimeException("查询会诊项目列表失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,175 @@
package com.openhis.web.consultation.controller;
import com.core.common.core.domain.R;
import com.openhis.web.consultation.appservice.IConsultationAppService;
import com.openhis.web.consultation.dto.ConsultationActivityDto;
import com.openhis.web.consultation.dto.ConsultationRequestDto;
import com.openhis.web.consultation.dto.DepartmentTreeDto;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 会诊管理Controller
*
* @author system
* @date 2026-01-29
*/
@Slf4j
@Api(tags = "会诊管理")
@RestController
@RequestMapping("/consultation")
public class ConsultationController {
@Resource
private IConsultationAppService consultationAppService;
/**
* 获取会诊列表
*/
@ApiOperation("获取会诊列表")
@GetMapping("/list")
public R<List<ConsultationRequestDto>> getConsultationList(
@ApiParam("就诊ID可选不传则查询当前医生的所有会诊申请")
@RequestParam(required = false) Long encounterId) {
try {
List<ConsultationRequestDto> list = consultationAppService.getConsultationList(encounterId);
return R.ok(list);
} catch (Exception e) {
log.error("获取会诊列表失败", e);
return R.fail("获取会诊列表失败: " + e.getMessage());
}
}
/**
* 保存会诊申请
*/
@ApiOperation("保存会诊申请")
@PostMapping("/save")
public R<String> saveConsultation(@RequestBody ConsultationRequestDto dto) {
try {
Boolean result = consultationAppService.saveConsultation(dto);
return result ? R.ok("保存成功") : R.fail("保存失败");
} catch (Exception e) {
log.error("保存会诊申请失败", e);
return R.fail("保存会诊申请失败: " + e.getMessage());
}
}
/**
* 提交会诊申请
*/
@ApiOperation("提交会诊申请")
@PostMapping("/submit")
public R<String> submitConsultation(
@ApiParam("会诊申请单号") @RequestParam String consultationId) {
try {
Boolean result = consultationAppService.submitConsultation(consultationId);
return result ? R.ok("提交成功") : R.fail("提交失败");
} catch (Exception e) {
log.error("提交会诊申请失败", e);
return R.fail("提交会诊申请失败: " + e.getMessage());
}
}
/**
* 作废会诊申请
*/
@ApiOperation("作废会诊申请")
@PostMapping("/cancel")
public R<String> cancelConsultation(
@ApiParam("会诊申请单号") @RequestParam String consultationId,
@ApiParam("作废原因") @RequestParam(required = false) String cancelReason) {
try {
Boolean result = consultationAppService.cancelConsultation(consultationId, cancelReason);
return result ? R.ok("作废成功") : R.fail("作废失败");
} catch (Exception e) {
log.error("作废会诊申请失败", e);
return R.fail("作废会诊申请失败: " + e.getMessage());
}
}
/**
* 结束会诊申请
*/
@ApiOperation("结束会诊申请")
@PostMapping("/complete")
public R<String> completeConsultation(
@ApiParam("会诊申请单号") @RequestParam String consultationId) {
try {
Boolean result = consultationAppService.completeConsultation(consultationId);
return result ? R.ok("结束成功") : R.fail("结束失败");
} catch (Exception e) {
log.error("结束会诊申请失败", e);
return R.fail("结束会诊申请失败: " + e.getMessage());
}
}
/**
* 获取科室医生树
*/
@ApiOperation("获取科室医生树")
@GetMapping("/departmentTree")
public R<List<DepartmentTreeDto>> getDepartmentTree() {
try {
List<DepartmentTreeDto> tree = consultationAppService.getDepartmentTree();
return R.ok(tree);
} catch (Exception e) {
log.error("获取科室医生树失败", e);
return R.fail("获取科室医生树失败: " + e.getMessage());
}
}
/**
* 获取主诊断
*/
@ApiOperation("获取主诊断")
@GetMapping("/mainDiagnosis")
public R<Map<String, String>> getMainDiagnosis(
@ApiParam("就诊ID") @RequestParam Long encounterId) {
try {
Map<String, String> diagnosis = consultationAppService.getMainDiagnosis(encounterId);
return R.ok(diagnosis);
} catch (Exception e) {
log.error("获取主诊断失败", e);
return R.fail("获取主诊断失败: " + e.getMessage());
}
}
/**
* 获取我的会诊邀请列表
*/
@ApiOperation("获取我的会诊邀请列表")
@GetMapping("/myInvitations")
public R<List<ConsultationRequestDto>> getMyInvitations() {
try {
List<ConsultationRequestDto> list = consultationAppService.getMyInvitations();
return R.ok(list);
} catch (Exception e) {
log.error("获取我的会诊邀请失败", e);
return R.fail("获取我的会诊邀请失败: " + e.getMessage());
}
}
/**
* 获取所有会诊项目及价格
*/
@ApiOperation("获取所有会诊项目及价格")
@GetMapping("/activities")
public R<List<ConsultationActivityDto>> getConsultationActivities() {
try {
List<ConsultationActivityDto> list = consultationAppService.getConsultationActivities();
return R.ok(list);
} catch (Exception e) {
log.error("获取会诊项目列表失败", e);
return R.fail("获取会诊项目列表失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,116 @@
package com.openhis.web.consultation.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 会诊邀请对象实体类
*
* @author system
* @date 2026-01-30
*/
@Data
@TableName("consultation_invited")
public class ConsultationInvited implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 会诊申请ID外键
*/
@TableField("consultation_request_id")
private Long consultationRequestId;
/**
* 邀请科室ID
*/
@TableField("invited_department_id")
private Long invitedDepartmentId;
/**
* 邀请科室名称
*/
@TableField("invited_department_name")
private String invitedDepartmentName;
/**
* 邀请医生ID
*/
@TableField("invited_physician_id")
private Long invitedPhysicianId;
/**
* 邀请医生姓名
*/
@TableField("invited_physician_name")
private String invitedPhysicianName;
/**
* 邀请状态0-待确认1-已确认2-已拒绝)
*/
@TableField("invited_status")
private Integer invitedStatus;
/**
* 确认时间
*/
@TableField("confirm_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date confirmTime;
/**
* 确认意见
*/
@TableField("confirm_opinion")
private String confirmOpinion;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 创建人
*/
@TableField(value = "create_by", fill = FieldFill.INSERT)
private String createBy;
/**
* 更新人
*/
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 租户ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 删除标识0-未删除1-已删除)
*/
@TableField("is_deleted")
@TableLogic
private Integer isDeleted;
}

View File

@@ -0,0 +1,264 @@
package com.openhis.web.consultation.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 会诊申请单实体类
*
* @author system
* @date 2026-01-29
*/
@Data
@TableName("consultation_request")
public class ConsultationRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 会诊申请单号
*/
@TableField("consultation_id")
private String consultationId;
/**
* 患者ID
*/
@TableField("patient_id")
private Long patientId;
/**
* 就诊ID
*/
@TableField("encounter_id")
private Long encounterId;
/**
* 订单ID关联门诊医嘱表 wor_service_request.id
*/
@TableField("order_id")
private Long orderId;
/**
* 患者姓名
*/
@TableField("patient_name")
private String patientName;
/**
* 患者病历号
*/
@TableField("patient_bus_no")
private String patientBusNo;
/**
* 患者就诊卡号
*/
@TableField("patient_identifier_no")
private String patientIdentifierNo;
/**
* 性别
*/
@TableField("gender_enum")
private Integer genderEnum;
/**
* 年龄
*/
@TableField("age")
private Integer age;
/**
* 申请科室名称
*/
@TableField("department")
private String department;
/**
* 申请科室ID
*/
@TableField("department_id")
private Long departmentId;
/**
* 申请医生姓名
*/
@TableField("requesting_physician")
private String requestingPhysician;
/**
* 申请医生ID
*/
@TableField("requesting_physician_id")
private Long requestingPhysicianId;
/**
* 申请时间
*/
@TableField("consultation_request_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date consultationRequestDate;
/**
* 邀请对象显示文本
*/
@TableField("invited_object")
private String invitedObject;
/**
* 会诊时间
*/
@TableField("consultation_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date consultationDate;
/**
* 会诊目的
*/
@TableField("consultation_purpose")
private String consultationPurpose;
/**
* 门诊诊断
*/
@TableField("provisional_diagnosis")
private String provisionalDiagnosis;
/**
* 会诊项目ID关联wor_activity_definition表
*/
@TableField("consultation_activity_id")
private Long consultationActivityId;
/**
* 会诊项目名称(如:院内会诊、远程会诊等)
*/
@TableField("consultation_activity_name")
private String consultationActivityName;
/**
* 会诊意见
*/
@TableField("consultation_opinion")
private String consultationOpinion;
/**
* 会诊状态0-新开10-已提交20-已确认30-已签名40-已完成50-已取消)
*/
@TableField("consultation_status")
private Integer consultationStatus;
/**
* 紧急程度(一般/紧急)
*/
@TableField("consultation_urgency")
private String consultationUrgency;
/**
* 确认医生
*/
@TableField("confirming_physician")
private String confirmingPhysician;
/**
* 确认医生ID
*/
@TableField("confirming_physician_id")
private Long confirmingPhysicianId;
/**
* 确认时间
*/
@TableField("confirming_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date confirmingDate;
/**
* 签名
*/
@TableField("signature")
private String signature;
/**
* 签名医生ID
*/
@TableField("signature_physician_id")
private Long signaturePhysicianId;
/**
* 签名时间
*/
@TableField("signature_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date signatureDate;
/**
* 取消时间
*/
@TableField("cancel_nature_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date cancelNatureDate;
/**
* 作废原因
*/
@TableField("cancel_reason")
private String cancelReason;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 创建人
*/
@TableField(value = "create_by", fill = FieldFill.INSERT)
private String createBy;
/**
* 更新人
*/
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 租户ID
*/
@TableField("tenant_id")
private Long tenantId;
/**
* 删除标识0-未删除1-已删除)
*/
@TableField("is_deleted")
@TableLogic
private Integer isDeleted;
/**
* 备注
*/
@TableField("remark")
private String remark;
}

View File

@@ -0,0 +1,68 @@
package com.openhis.web.consultation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 会诊项目DTO
*
* @author system
* @date 2026-02-02
*/
@Data
public class ConsultationActivityDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 项目ID
* 使用 @JsonSerialize 注解将 Long 类型转换为 String避免前端 JavaScript 精度丢失
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 项目名称
*/
private String name;
/**
* 项目编号
*/
private String busNo;
/**
* 价格
*/
private BigDecimal price;
/**
* 类型枚举
*/
private Integer typeEnum;
/**
* 分类编码
*/
private String categoryCode;
/**
* 单位编码
*/
private String unitCode;
/**
* 单位名称
*/
private String unitCodeText;
/**
* 状态
*/
private Integer statusEnum;
}

View File

@@ -0,0 +1,190 @@
package com.openhis.web.consultation.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 会诊申请DTO
*
* @author system
* @date 2026-01-29
*/
@Data
public class ConsultationRequestDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 会诊申请单号
*/
private String consultationId;
/**
* 患者ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long patientId;
/**
* 患者姓名
*/
private String patientName;
/**
* 患者性别
*/
private Integer genderEnum;
/**
* 患者年龄
*/
private Integer age;
/**
* 病历号
*/
private String patientBusNo;
/**
* 就诊卡号
*/
private String patientIdentifierNo;
/**
* 就诊ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long encounterId;
/**
* 申请科室ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long departmentId;
/**
* 申请科室名称
*/
private String department;
/**
* 申请医生ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long requestingPhysicianId;
/**
* 申请医生姓名
*/
private String requestingPhysician;
/**
* 邀请对象显示文本
*/
private String invitedObject;
/**
* 邀请对象列表
*/
private List<InvitedObjectDto> invitedList;
/**
* 会诊时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date consultationDate;
/**
* 申请时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date consultationRequestDate;
/**
* 会诊目的
*/
private String consultationPurpose;
/**
* 门诊诊断
*/
private String provisionalDiagnosis;
/**
* 会诊项目ID关联wor_activity_definition表
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long consultationActivityId;
/**
* 会诊项目名称(如:院内会诊、远程会诊等)
*/
private String consultationActivityName;
/**
* 紧急程度1=普通2=紧急)
*/
private String consultationUrgency;
/**
* 会诊状态
*/
private Integer consultationStatus;
/**
* 会诊意见
*/
private String consultationOpinion;
/**
* 参与会诊医师
*/
private String consultingPhysicians;
/**
* 签名医生ID
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long signingPhysicianId;
/**
* 签名医生姓名
*/
private String signingPhysicianName;
/**
* 签名时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date signingTime;
/**
* 是否提交(保存时使用)
*/
private Boolean submitFlag;
/**
* 我的邀请状态查询我的邀请时使用0-待确认1-已确认2-已拒绝)
*/
private Integer myInvitedStatus;
/**
* 我的确认时间(查询我的邀请时使用)
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date myConfirmTime;
}

View File

@@ -0,0 +1,38 @@
package com.openhis.web.consultation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 科室医生树DTO
*
* @author system
* @date 2026-01-29
*/
@Data
public class DepartmentTreeDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 科室ID序列化为字符串避免JavaScript精度丢失
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 科室名称
*/
private String label;
/**
* 子节点(医生列表)
*/
private List<PhysicianNodeDto> children;
}

View File

@@ -0,0 +1,43 @@
package com.openhis.web.consultation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
/**
* 邀请对象DTO
*
* @author system
* @date 2026-01-29
*/
@Data
public class InvitedObjectDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 科室ID序列化为字符串避免JavaScript精度丢失
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long deptId;
/**
* 科室名称
*/
private String deptName;
/**
* 医生ID为空表示邀请整个科室序列化为字符串避免JavaScript精度丢失
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long physicianId;
/**
* 医生姓名
*/
private String physicianName;
}

View File

@@ -0,0 +1,32 @@
package com.openhis.web.consultation.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
/**
* 医生节点DTO
*
* @author system
* @date 2026-01-29
*/
@Data
public class PhysicianNodeDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 医生ID序列化为字符串避免JavaScript精度丢失
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 医生姓名
*/
private String label;
}

View File

@@ -0,0 +1,85 @@
package com.openhis.web.consultation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 会诊状态枚举
*
* @author system
* @date 2026-02-05
*/
@Getter
@AllArgsConstructor
public enum ConsultationStatusEnum {
/**
* 新开(草稿)
*/
NEW(0, "新开"),
/**
* 已提交
*/
SUBMITTED(10, "已提交"),
/**
* 已确认
*/
CONFIRMED(20, "已确认"),
/**
* 已签名
*/
SIGNED(30, "已签名"),
/**
* 已完成
*/
COMPLETED(40, "已完成"),
/**
* 已取消
*/
CANCELLED(50, "已取消");
/**
* 状态码
*/
private final Integer code;
/**
* 状态描述
*/
private final String description;
/**
* 根据状态码获取枚举
*/
public static ConsultationStatusEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (ConsultationStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
/**
* 判断是否可以编辑
*/
public boolean canEdit() {
return this == NEW || this == SUBMITTED;
}
/**
* 判断是否可以取消
*/
public boolean canCancel() {
return this == NEW || this == SUBMITTED || this == CONFIRMED;
}
}

View File

@@ -0,0 +1,51 @@
package com.openhis.web.consultation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 会诊紧急程度枚举
*
* @author system
* @date 2026-02-05
*/
@Getter
@AllArgsConstructor
public enum ConsultationUrgencyEnum {
/**
* 普通
*/
NORMAL("1", "普通"),
/**
* 紧急
*/
URGENT("2", "紧急");
/**
* 类型码
*/
private final String code;
/**
* 类型描述
*/
private final String description;
/**
* 根据类型码获取枚举
*/
public static ConsultationUrgencyEnum getByCode(String code) {
if (code == null) {
return null;
}
for (ConsultationUrgencyEnum urgency : values()) {
if (urgency.getCode().equals(code)) {
return urgency;
}
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
package com.openhis.web.consultation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 邀请状态枚举
*
* @author system
* @date 2026-02-05
*/
@Getter
@AllArgsConstructor
public enum InvitedStatusEnum {
/**
* 待确认
*/
PENDING(0, "待确认"),
/**
* 已确认
*/
CONFIRMED(1, "已确认"),
/**
* 已拒绝
*/
REJECTED(2, "已拒绝");
/**
* 状态码
*/
private final Integer code;
/**
* 状态描述
*/
private final String description;
/**
* 根据状态码获取枚举
*/
public static InvitedStatusEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (InvitedStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
}

View File

@@ -0,0 +1,17 @@
package com.openhis.web.consultation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.openhis.web.consultation.domain.ConsultationInvited;
import org.apache.ibatis.annotations.Mapper;
/**
* 会诊邀请对象Mapper接口
*
* @author system
* @date 2026-01-30
*/
@Mapper
public interface ConsultationInvitedMapper extends BaseMapper<ConsultationInvited> {
}

View File

@@ -0,0 +1,17 @@
package com.openhis.web.consultation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.openhis.web.consultation.domain.ConsultationRequest;
import org.apache.ibatis.annotations.Mapper;
/**
* 会诊申请Mapper接口
*
* @author system
* @date 2026-01-29
*/
@Mapper
public interface ConsultationRequestMapper extends BaseMapper<ConsultationRequest> {
}

View File

@@ -864,7 +864,17 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum()); // 请求类型
// 🎯 判断是否为会诊医嘱如果categoryEnum为31则为会诊类型
Integer categoryEnum = adviceSaveDto.getCategoryEnum();
if (categoryEnum != null && categoryEnum == 31) {
// 会诊医嘱category_enum设置为31
serviceRequest.setCategoryEnum(31);
log.info("保存会诊医嘱category_enum=31");
} else {
// 普通诊疗医嘱
serviceRequest.setCategoryEnum(adviceSaveDto.getCategoryEnum());
}
serviceRequest.setActivityId(adviceSaveDto.getAdviceDefinitionId());// 诊疗定义id
serviceRequest.setPatientId(adviceSaveDto.getPatientId()); // 患者
serviceRequest.setRequesterId(adviceSaveDto.getPractitionerId()); // 开方医生

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.web.consultation.mapper.ConsultationRequestMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.openhis.web.consultation.domain.ConsultationRequest">
<id column="id" property="id" />
<result column="consultation_id" property="consultationId" />
<result column="patient_id" property="patientId" />
<result column="encounter_id" property="encounterId" />
<result column="order_id" property="orderId" />
<result column="patient_name" property="patientName" />
<result column="patient_bus_no" property="patientBusNo" />
<result column="patient_identifier_no" property="patientIdentifierNo" />
<result column="gender_enum" property="genderEnum" />
<result column="age" property="age" />
<result column="department" property="department" />
<result column="department_id" property="departmentId" />
<result column="requesting_physician" property="requestingPhysician" />
<result column="requesting_physician_id" property="requestingPhysicianId" />
<result column="consultation_request_date" property="consultationRequestDate" />
<result column="invited_object" property="invitedObject" />
<result column="invited_department_id" property="invitedDepartmentId" />
<result column="invited_physician_id" property="invitedPhysicianId" />
<result column="consultation_date" property="consultationDate" />
<result column="consultation_purpose" property="consultationPurpose" />
<result column="provisional_diagnosis" property="provisionalDiagnosis" />
<result column="consultation_opinion" property="consultationOpinion" />
<result column="consultation_status" property="consultationStatus" />
<result column="consultation_urgency" property="consultationUrgency" />
<result column="confirming_physician" property="confirmingPhysician" />
<result column="confirming_physician_id" property="confirmingPhysicianId" />
<result column="confirming_date" property="confirmingDate" />
<result column="signature" property="signature" />
<result column="signature_physician_id" property="signaturePhysicianId" />
<result column="signature_date" property="signatureDate" />
<result column="cancel_nature_date" property="cancelNatureDate" />
<result column="cancel_reason" property="cancelReason" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="create_by" property="createBy" />
<result column="update_by" property="updateBy" />
<result column="tenant_id" property="tenantId" />
<result column="is_deleted" property="isDeleted" />
<result column="remark" property="remark" />
</resultMap>
</mapper>

View File

@@ -465,7 +465,8 @@
T2.part_percent AS part_percent,
ccd.name AS condition_definition_name,
T1.sort_number AS sort_number,
T1.based_on_id AS based_on_id
T1.based_on_id AS based_on_id,
T1.category_enum AS category_enum
FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2 ON T2.ID = T1.medication_id
AND T2.delete_flag = '0'
@@ -516,7 +517,8 @@
T2.part_percent AS part_percent,
'' AS condition_definition_name,
99 AS sort_number,
T1.based_on_id AS based_on_id
T1.based_on_id AS based_on_id,
T1.category_enum AS category_enum
FROM wor_device_request AS T1
LEFT JOIN adm_device_definition AS T2 ON T2.ID = T1.device_def_id
AND T2.delete_flag = '0'
@@ -564,7 +566,8 @@
1 AS part_percent,
'' AS condition_definition_name,
99 AS sort_number,
T1.based_on_id AS based_on_id
T1.based_on_id AS based_on_id,
T1.category_enum AS category_enum
FROM wor_service_request AS T1
LEFT JOIN wor_activity_definition AS T2
ON T2.ID = T1.activity_id

View File

@@ -0,0 +1,402 @@
<template>
<div class="app-container consultation-confirmation">
<div class="page-header">
<span class="tab-title">会诊确认</span>
</div>
<div class="action-bar no-print">
<el-button type="success" @click="handlePrint">打印</el-button>
<el-button @click="handleRefresh">刷新</el-button>
<el-button
type="primary"
:disabled="!currentRow"
@click="handleConfirm"
>
{{ confirmButtonText }}
</el-button>
<el-button
type="primary"
:disabled="!canSign"
@click="handleSign"
>
签名
</el-button>
</div>
<div class="list-section no-print">
<el-table
:data="tableData"
border
stripe
highlight-current-row
@current-change="handleRowChange"
>
<el-table-column type="index" width="60" label="序号" align="center" />
<el-table-column label="紧急" width="70" align="center">
<template #default="scope">
<el-checkbox v-model="scope.row.urgent" />
</template>
</el-table-column>
<el-table-column prop="consultationId" label="申请单号" min-width="160" />
<el-table-column prop="patientName" label="病人姓名" min-width="100" />
<el-table-column prop="consultationTime" label="会诊时间" min-width="160" />
<el-table-column prop="invitee" label="邀请对象" min-width="120" />
<el-table-column prop="applyDept" label="申请科室" min-width="120" />
<el-table-column prop="applyDoctor" label="申请医师" min-width="120" />
<el-table-column prop="applyTime" label="申请时间" min-width="160" />
<el-table-column label="确认" width="70" align="center">
<template #default="scope">
<el-checkbox v-model="scope.row.confirmed" disabled />
</template>
</el-table-column>
<el-table-column label="签名" width="70" align="center">
<template #default="scope">
<el-checkbox v-model="scope.row.signed" disabled />
</template>
</el-table-column>
</el-table>
</div>
<div class="form-section">
<div class="section-title">会诊记录单</div>
<el-form :model="formData" label-width="110px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="病人姓名">
<el-input v-model="formData.patientName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="性别">
<el-input v-model="formData.gender" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="年龄">
<el-input v-model="formData.age" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="就诊卡号">
<el-input v-model="formData.cardNo" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请单号">
<el-input v-model="formData.consultationId" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请科室">
<el-input v-model="formData.applyDept" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="会诊时间">
<el-input v-model="formData.consultationTime" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="紧急标志">
<el-input :model-value="formData.urgent ? '' : ''" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="会诊邀请对象">
<el-input v-model="formData.invitee" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="提交医生">
<el-input v-model="formData.applyDoctor" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="提交时间">
<el-input v-model="formData.applyTime" disabled />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="病史及目的">
<el-input v-model="formData.history" type="textarea" :rows="3" disabled />
</el-form-item>
<el-form-item label="会诊确认参加医师" required>
<el-input v-model="formData.confirmingPhysician" placeholder="请输入会诊确认参加医师" />
</el-form-item>
<el-form-item label="会诊意见" required>
<el-input v-model="formData.opinion" type="textarea" :rows="4" placeholder="请输入会诊意见" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="所属医生">
<el-input v-model="formData.confirmDoctor" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="代表科室">
<el-input v-model="formData.confirmDept" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="签名医生">
<el-input v-model="formData.signDoctor" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="签名时间">
<el-input v-model="formData.signTime" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
</template>
<script setup name="consultationConfirmation">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const buildMockRows = () => ([
{
consultationId: 'CS20250812001',
urgent: false,
patientName: '陈明',
gender: '男',
age: '45',
cardNo: 'CARD20250812001',
consultationTime: '2025-08-12 17:48',
invitee: '演示测试',
applyDept: '内科',
applyDoctor: '徐斌',
applyTime: '2025-08-12 16:30',
history: '胸闷、气短一周,既往有高血压史。',
confirmingPhysician: '',
opinion: '',
confirmed: false,
signed: false
},
{
consultationId: 'CS20250812002',
urgent: true,
patientName: '赵敏',
gender: '女',
age: '32',
cardNo: 'CARD20250812002',
consultationTime: '2025-08-12 18:10',
invitee: '李医生',
applyDept: '神经内科',
applyDoctor: '王婷',
applyTime: '2025-08-12 16:50',
history: '头晕伴视物模糊3天。',
confirmingPhysician: '',
opinion: '',
confirmed: false,
signed: false
}
])
const tableData = ref(buildMockRows())
const currentRow = ref(null)
const formData = ref({
consultationId: '',
urgent: false,
patientName: '',
gender: '',
age: '',
cardNo: '',
consultationTime: '',
invitee: '',
applyDept: '',
applyDoctor: '',
applyTime: '',
history: '',
confirmingPhysician: '',
opinion: '',
confirmDoctor: '',
confirmDept: '',
signDoctor: '',
signTime: ''
})
const confirmButtonText = computed(() => {
if (!currentRow.value) {
return '确认'
}
return currentRow.value.confirmed ? '取消确认' : '确认'
})
const canSign = computed(() => {
return !!currentRow.value && currentRow.value.confirmed && !currentRow.value.signed
})
const formatDateTime = (date = new Date()) => {
const pad = (value) => String(value).padStart(2, '0')
const yyyy = date.getFullYear()
const mm = pad(date.getMonth() + 1)
const dd = pad(date.getDate())
const hh = pad(date.getHours())
const mi = pad(date.getMinutes())
const ss = pad(date.getSeconds())
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`
}
const getDoctorName = () => userStore.nickName || userStore.name || '当前医生'
const getDoctorDept = () => userStore.orgName || '当前科室'
const applyRowToForm = (row) => {
if (!row) {
Object.keys(formData.value).forEach((key) => {
formData.value[key] = key === 'urgent' ? false : ''
})
return
}
formData.value.consultationId = row.consultationId
formData.value.urgent = row.urgent
formData.value.patientName = row.patientName
formData.value.gender = row.gender
formData.value.age = row.age
formData.value.cardNo = row.cardNo
formData.value.consultationTime = row.consultationTime
formData.value.invitee = row.invitee
formData.value.applyDept = row.applyDept
formData.value.applyDoctor = row.applyDoctor
formData.value.applyTime = row.applyTime
formData.value.history = row.history
formData.value.confirmingPhysician = row.confirmingPhysician
formData.value.opinion = row.opinion
formData.value.confirmDoctor = row.confirmDoctor || ''
formData.value.confirmDept = row.confirmDept || ''
formData.value.signDoctor = row.signDoctor || ''
formData.value.signTime = row.signTime || ''
}
const handleRowChange = (row) => {
currentRow.value = row
applyRowToForm(row)
}
const handleConfirm = () => {
if (!currentRow.value) {
return
}
if (!currentRow.value.confirmed) {
if (!formData.value.opinion.trim()) {
ElMessage.warning('请先填写会诊意见')
return
}
if (!formData.value.confirmingPhysician.trim()) {
ElMessage.warning('请先填写会诊确认参加医师')
return
}
currentRow.value.confirmed = true
currentRow.value.confirmingPhysician = formData.value.confirmingPhysician
currentRow.value.opinion = formData.value.opinion
currentRow.value.confirmDoctor = getDoctorName()
currentRow.value.confirmDept = getDoctorDept()
formData.value.confirmDoctor = currentRow.value.confirmDoctor
formData.value.confirmDept = currentRow.value.confirmDept
ElMessage.success('会诊已确认')
return
}
if (currentRow.value.signed) {
ElMessage.warning('已签名的会诊无法取消确认')
return
}
currentRow.value.confirmed = false
currentRow.value.confirmDoctor = ''
currentRow.value.confirmDept = ''
formData.value.confirmDoctor = ''
formData.value.confirmDept = ''
ElMessage.success('已取消确认')
}
const handleSign = () => {
if (!currentRow.value) {
return
}
if (!currentRow.value.confirmed) {
ElMessage.warning('请先确认会诊申请')
return
}
currentRow.value.signed = true
currentRow.value.signDoctor = getDoctorName()
currentRow.value.signTime = formatDateTime()
formData.value.signDoctor = currentRow.value.signDoctor
formData.value.signTime = currentRow.value.signTime
ElMessage.success('签名完成')
}
const handlePrint = () => {
if (!currentRow.value) {
ElMessage.warning('请先选择会诊申请')
return
}
window.print()
}
const handleRefresh = () => {
tableData.value = buildMockRows()
currentRow.value = null
applyRowToForm(null)
ElMessage.success('已刷新')
}
</script>
<style scoped>
.consultation-confirmation .page-header {
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.consultation-confirmation .tab-title {
display: inline-block;
font-size: 16px;
font-weight: 700;
color: #4a89dc;
border-bottom: 2px solid #4a89dc;
padding-bottom: 6px;
}
.action-bar {
margin: 16px 0;
display: flex;
gap: 12px;
}
.list-section {
margin-bottom: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
}
@media print {
.no-print {
display: none !important;
}
.consultation-confirmation {
padding: 0;
}
}
</style>

View File

@@ -86,7 +86,7 @@
<script setup>
import { getCurrentInstance, nextTick, onMounted, ref, computed, watch } from 'vue';
import { getAdviceBaseInfo, getDeviceList } from './api';
import { getAdviceBaseInfo, getDeviceList, getConsultationActivities } from './api';
import { throttle, debounce } from 'lodash-es';
const { proxy } = getCurrentInstance();
@@ -274,8 +274,50 @@ async function getList() {
};
const isConsumables = queryParams.adviceTypes === '2' || queryParams.adviceTypes === 2;
const isConsultation = queryParams.adviceTypes === '5' || queryParams.adviceTypes === 5;
if (isConsumables) {
if (isConsultation) {
// 会诊类型:调用会诊项目接口
const res = await getConsultationActivities();
if (res.data && Array.isArray(res.data)) {
const result = res.data.map((item) => ({
adviceName: item.name || item.activityName,
adviceType: 5, // 会诊类型
unitCode: '111', // 次
unitCode_dictText: '次',
minUnitCode: '111',
minUnitCode_dictText: '次',
volume: '',
partPercent: 1,
priceList: item.price ? [{ price: item.price }] : [],
inventoryList: [],
adviceDefinitionId: item.id || item.activityId,
chargeItemDefinitionId: item.id || item.activityId,
positionId: '',
positionName: '',
dose: 0,
doseUnitCode: '111',
doseUnitCode_dictText: '次',
injectFlag: 0,
injectFlag_enumText: '否',
skinTestFlag: 0,
skinTestFlag_enumText: '否',
categoryCode: 31, // 会诊的category_enum
unitPrice: item.price || 0,
...item,
}));
// 缓存结果
searchCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
adviceBaseList.value = result;
} else {
adviceBaseList.value = [];
}
} else if (isConsumables) {
const deviceQueryParams = {
pageNo: queryParams.pageNum || 1,
pageSize: queryParams.pageSize || 1000,

View File

@@ -894,3 +894,90 @@ export function deleteInspectionApplication(id) {
method: 'delete',
});
}
// ========== 会诊相关接口 ==========
/**
* 获取会诊列表
*/
export function getConsultationList(queryParams) {
return request({
url: '/consultation/list',
method: 'get',
params: queryParams,
});
}
/**
* 保存会诊申请
*/
export function saveConsultation(data) {
return request({
url: '/consultation/save',
method: 'post',
data: data,
});
}
/**
* 提交会诊申请
*/
export function submitConsultation(queryParams) {
return request({
url: '/consultation/submit',
method: 'post',
params: queryParams,
});
}
/**
* 作废会诊申请
*/
export function cancelConsultation(queryParams) {
return request({
url: '/consultation/cancel',
method: 'post',
params: queryParams,
});
}
/**
* 结束会诊申请
*/
export function completeConsultation(consultationId) {
return request({
url: '/consultation/complete',
method: 'post',
params: { consultationId },
});
}
/**
* 获取科室医生树
*/
export function getDepartmentTree() {
return request({
url: '/consultation/departmentTree',
method: 'get',
});
}
/**
* 获取主诊断
*/
export function getMainDiagnosis(params) {
return request({
url: '/consultation/mainDiagnosis',
method: 'get',
params: params,
});
}
/**
* 获取会诊项目列表及价格
*/
export function getConsultationActivities() {
return request({
url: '/consultation/activities',
method: 'get',
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -346,7 +346,7 @@
expandOrder = [];
// 当医嘱类型改变时,清空当前选择的项目名称,因为不同类型项目的数据结构可能不兼容
prescriptionList[scope.$index].adviceName = undefined;
adviceQueryParams.adviceType = value;
adviceQueryParams.adviceTypes = value; // 🎯 修复:改为 adviceTypes复数
// 根据选择的类型设置categoryCode用于药品分类筛选
if (value == 1) { // 西药
@@ -357,6 +357,8 @@
adviceQueryParams.categoryCode = ''; // 诊疗不需要categoryCode筛选
} else if (value == 4) { // 耗材
adviceQueryParams.categoryCode = ''; // 耗材不需要categoryCode筛选
} else if (value == 5) { // 会诊
adviceQueryParams.categoryCode = ''; // 会诊不需要categoryCode筛选
} else {
adviceQueryParams.categoryCode = ''; // 全部类型
}
@@ -438,6 +440,7 @@
<el-tag v-if="scope.row.chargeStatus == 5" type="info">
{{ scope.row.chargeStatus_enumText }}
</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 5" type="danger">已作废</el-tag>
<el-tag v-else-if="scope.row.statusEnum == 2" type="success">已签发</el-tag>
<el-tag v-else-if="!scope.row.requestId && scope.row.statusEnum == 1" type="warning">
待保存
@@ -569,7 +572,7 @@ import {
} from '../api';
import { advicePrint, getAdjustPriceSwitchState } from '@/api/public';
import adviceBaseList from '../adviceBaseList.vue';
import { computed, getCurrentInstance, nextTick, ref, watch } from 'vue';
import { computed, getCurrentInstance, nextTick, ref, watch, onMounted, onBeforeUnmount } from 'vue';
import { calculateQuantityByDays } from '@/utils/his';
import OrderGroupDrawer from './orderGroupDrawer';
import PrescriptionHistory from './prescriptionHistory';
@@ -591,7 +594,7 @@ const form = ref({
});
const adviceQueryParams = ref({
searchKey: '',
adviceType: '',
adviceTypes: '', // 🎯 修复:改为 adviceTypes复数
categoryCode: '' // 用于筛选西药(2)和中成药(1)
});
const rowIndex = ref(-1);
@@ -664,7 +667,7 @@ const { method_code, unit_code, rate_code, distribution_category_code, drord_doc
);
// 删除硬编码的adviceTypeList直接使用drord_doctor_type字典
// drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=全部
// drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=全部
const adviceTypeList = ref([
{
label: '西药',
@@ -682,6 +685,10 @@ const adviceTypeList = ref([
label: '耗材',
value: 4,
},
{
label: '会诊',
value: 5,
},
{
label: '全部',
value: '',
@@ -742,6 +749,8 @@ const allPrescriptionsData = ref({}); // 存储所有处方的数据,格式: {
const allPrescriptionCheckStates = ref({}); // 存储每个处方的选中状态,格式: { prescriptionId: { checkedIndexes: [], checkAll: false } }
onMounted(() => {
document.addEventListener('keydown', escKeyListener);
// 初始化时预加载组织数据,避免选择诊疗/会诊项目时显示"无数据"
getOrgList();
// 初始化时自动创建第一个西药处方
if (westernPrescriptions.value.length === 0) {
createNewPrescription();
@@ -1144,7 +1153,7 @@ async function disposalPrint() {
function handleTotalAmount() {
totalAmount.value = prescriptionList.value.reduce((accumulator, currentRow) => {
if (currentRow.chargeStatus != 8) {
if (currentRow.chargeStatus != 8 && currentRow.statusEnum != 5) {
return new Decimal(accumulator).add(currentRow.totalPrice || 0);
} else {
// 跳过已退费项目,保持累加结果不变
@@ -1183,13 +1192,41 @@ function getListInfo(addNewRow) {
// 关键:先等待处方列表数据获取完成
isAdding.value = false;
const res = await getPrescriptionList(props.patientInfo.encounterId);
prescriptionList.value = res.data.map((item) => {
return {
...JSON.parse(item.contentJson),
const contentJson = JSON.parse(item.contentJson);
// 🎯 判断是否为会诊医嘱:
// 方法1检查 category_enum 字段(需要后端重新编译)
// 方法2检查 contentJson 中是否包含会诊相关字段(临时方案)
const categoryEnum = contentJson?.categoryEnum || contentJson?.category_enum || item.category_enum;
const isConsultation = categoryEnum === 31 || categoryEnum === '31' ||
contentJson?.consultationType ||
contentJson?.consultationId ||
contentJson?.consultationRequestId;
let adviceType = item.adviceType;
let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(item.adviceType);
// 如果是会诊类型,设置为会诊类型
if (isConsultation) {
adviceType = 5; // 前端会诊类型值为 5
adviceType_dictText = '会诊';
}
const result = {
...contentJson,
...item,
doseQuantity: JSON.parse(item.contentJson)?.doseQuantity,
doseUnitCode_dictText: JSON.parse(item.contentJson)?.doseUnitCode_dictText,
doseQuantity: contentJson?.doseQuantity,
doseUnitCode_dictText: contentJson?.doseUnitCode_dictText,
// 🎯 修复:将 adviceType 和 adviceType_dictText 放在最后,确保不被 item 覆盖
adviceType: adviceType,
adviceType_dictText: adviceType_dictText,
// 🎯 修复:确保 orgId 被正确设置(从 positionId 映射)
orgId: item.positionId || item.orgId,
};
return result;
});
getGroupMarkers(); // 更新标记
if (props.activeTab == 'prescription' && addNewRow) {
@@ -1283,7 +1320,7 @@ function handleAddPrescription(prescriptionId, showWarning = true) {
// 重置查询参数
adviceQueryParams.value = {
searchKey: '',
adviceType: '',
adviceTypes: '', // 🎯 修复:改为 adviceTypes复数
categoryCode: ''
};
@@ -1363,7 +1400,7 @@ function handleFocus(row, index) {
let adviceType = row.adviceType || '';
// 根据医嘱类型设置筛选条件
// drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=全部
// drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊
if (row.adviceType == 1) {
// 西药
categoryCode = '2';
@@ -1380,6 +1417,10 @@ function handleFocus(row, index) {
// 耗材adviceType = 2后端接口中耗材的adviceType是2
categoryCode = '';
adviceType = 2; // 耗材类型
} else if (row.adviceType == 5) {
// 🎯 会诊adviceType = 5
categoryCode = '';
adviceType = 5; // 会诊类型
} else {
// 全部(5)或其他:显示所有类型
categoryCode = '';
@@ -1387,8 +1428,7 @@ function handleFocus(row, index) {
}
adviceQueryParams.value = {
adviceType: adviceType,
adviceTypes: adviceType ? adviceType.toString() : '1,2,3', // 根据当前类型设置查询类型,避免显示其他类型的数据
adviceTypes: adviceType ? adviceType.toString() : '1,2,3', // 🎯 修复:只保留 adviceTypes复数
categoryCode: categoryCode,
searchKey: adviceQueryParams.value.searchKey || ''
};
@@ -1566,7 +1606,7 @@ function handleDelete() {
handleEmrTreatment();
updateExpandOrder([]);
isAdding.value = false;
adviceQueryParams.value.adviceType = undefined;
adviceQueryParams.value.adviceTypes = undefined; // 🎯 修复:改为 adviceTypes复数
if (sum == selectRows.length) {
proxy.$modal.msgSuccess('删除成功');
return;
@@ -1732,6 +1772,8 @@ function handleSave(prescriptionId) {
saveAdviceType = 2; // 耗材前端4 -> 后端2
} else if (item.adviceType == 2) {
saveAdviceType = 1; // 中成药前端2 -> 后端1
} else if (item.adviceType == 5) {
saveAdviceType = 3; // 会诊前端5 -> 后端3诊疗类
}
// 构造请求参数
@@ -2105,6 +2147,10 @@ function handleSaveSign(row, index, prescriptionId) {
else if (params.adviceType == 2) {
params.adviceType = 1;
}
// 前端5(会诊) -> 后端3 (诊疗类)
else if (params.adviceType == 5) {
params.adviceType = 3;
}
// 发送处理后的 params而不是原始 row
savePrescription({ adviceSaveList: [params] }).then((res) => {
@@ -2122,7 +2168,7 @@ function handleSaveSign(row, index, prescriptionId) {
isAdding.value = false;
// --- 修改结束 ---
}
adviceQueryParams.value.adviceType = undefined;
adviceQueryParams.value.adviceTypes = undefined; // 🎯 修复:改为 adviceTypes复数
}
});
}
@@ -2211,6 +2257,8 @@ function handleSaveBatch(prescriptionId) {
saveAdviceType = 2; // 耗材前端4 -> 后端2
} else if (item.adviceType == 2) {
saveAdviceType = 1; // 中成药前端2 -> 后端1
} else if (item.adviceType == 5) {
saveAdviceType = 3; // 会诊前端5 -> 后端3诊疗类
}
// 构造 contentJson (保持前端UI原始数据)
@@ -2385,10 +2433,21 @@ function setValue(row) {
}
} else {
getOrgList();
prescriptionList.value[rowIndex.value].orgId = JSON.parse(JSON.stringify(row)).positionId;
prescriptionList.value[rowIndex.value].quantity = 1;
prescriptionList.value[rowIndex.value].unitPrice = row.priceList[0].price;
prescriptionList.value[rowIndex.value].totalPrice = row.priceList[0].price;
// 会诊类型adviceType == 5和诊疗类型adviceType == 3的处理
if (row.adviceType == 5) {
// 会诊类型:设置默认值
prescriptionList.value[rowIndex.value].orgId = props.patientInfo.orgId; // 执行科室默认为申请医生的科室
prescriptionList.value[rowIndex.value].quantity = 1; // 执行次数默认1次
prescriptionList.value[rowIndex.value].unitPrice = row.priceList && row.priceList[0] ? row.priceList[0].price : (row.unitPrice || 0);
prescriptionList.value[rowIndex.value].totalPrice = prescriptionList.value[rowIndex.value].unitPrice;
prescriptionList.value[rowIndex.value].categoryEnum = 31; // 会诊的category_enum设置为31
} else {
// 诊疗类型adviceType == 3
prescriptionList.value[rowIndex.value].orgId = JSON.parse(JSON.stringify(row)).positionId;
prescriptionList.value[rowIndex.value].quantity = 1;
prescriptionList.value[rowIndex.value].unitPrice = row.priceList[0].price;
prescriptionList.value[rowIndex.value].totalPrice = row.priceList[0].price;
}
}
}

View File

@@ -169,6 +169,9 @@
<el-tab-pane label="报告查询" name="reportQuery">
<ReportQuery :patientInfo="patientInfo" ref="reportQueryRef" />
</el-tab-pane>
<el-tab-pane label="会诊" name="consultation">
<Consultation :patientInfo="patientInfo" :activeTab="activeTab" ref="consultationRef" />
</el-tab-pane>
</el-tabs>
<div class="overlay" :class="{ 'overlay-disabled': disabled }" v-if="disabled"></div>
</div>
@@ -203,6 +206,7 @@ import {
import prescriptionlist from './components/prescription/prescriptionlist.vue';
import RefundListDialog from './components/prescription/refundListDialog.vue';
import ReportQuery from './components/reportQuery.vue';
import Consultation from './components/consultation.vue';
import PatientList from './components/patientList.vue';
import Diagnosis from './components/diagnosis/diagnosis.vue';
import PrescriptionInfo from './components/prescription/prescriptionInfo.vue';
@@ -294,6 +298,7 @@ const inspectionRef = ref();
const surgeryRef = ref();
const emrRef = ref();
const diagnosisRef = ref();
const consultationRef = ref();
const waitCount = ref(0);
const loading = ref(false);
const { proxy } = getCurrentInstance();
@@ -486,6 +491,9 @@ function handleClick(tab) {
case 'eprescription':
eprescriptionRef.value.getList();
break;
case 'consultation':
consultationRef.value.fetchConsultationList();
break;
}
// if (tab != 'emr') {
// if (!saveStatus.value) {
@@ -586,6 +594,7 @@ function handleCardClick(item, index) {
surgeryRef.value.getList();
diagnosisRef.value.getList();
eprescriptionRef.value.getList();
consultationRef.value.fetchConsultationList();
// emrRef.value.getDetail(item.encounterId);
setTimeout(() => {
loading.value = false;