liusheng
5 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
smartor/src/main/java/com/smartor/service/impl/ServiceSLTDHealthcareRecordServiceImpl.java
@@ -8,13 +8,20 @@
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.SysUserDept;
import com.ruoyi.common.core.domain.entity.SysUserRole;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.service.dto.InhospPositionRecord;
import com.smartor.common.enums.EmergencyLevelEnum;
import com.smartor.common.enums.ImportFlagEnum;
import com.ruoyi.common.utils.http.HttpUtils;
import com.smartor.common.DistinctByProperty;
import com.smartor.domain.*;
import com.ruoyi.outpatientvisit.domain.PatMedOuthospError;
import com.ruoyi.outpatientvisit.domain.PatMedOuthosp;
import com.ruoyi.inhospvisit.api.InhospVisitFacade;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
import com.smartor.mapper.*;
import com.ruoyi.outpatientvisit.mapper.PatMedOuthospErrorMapper;
import com.smartor.service.IPatArchiveService;
import com.smartor.service.IPatMedInhospService;
import com.smartor.service.IPatMedOuthospService;
@@ -36,12 +43,33 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.ruoyi.common.core.domain.entity.PatArchive;
import com.ruoyi.task.domain.ServiceSubtask;
import com.ruoyi.task.mapper.ServiceSubtaskMapper;
import com.ruoyi.task.service.impl.ServiceSubtaskServiceImpl;
@Slf4j
@Service
public class ServiceSLTDHealthcareRecordServiceImpl implements IServiceSLTDHealthcareRecordService {
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * 在院患者当前位置查询地址(网关根路径 {@code sltd_pub_path} + 固定后缀)。
     * <p>
     * 与"入院采集"是<b>同一个接口</b>({@code queryHealthcareRecordList} 用的也是它),
     * 区别只是本方法按 {@code healthcareRecordNo} 单查、只取位置字段、不落库。
     */
    private static final String INHOSP_POSITION_PATH =
            "/osj/hbos-thirdparty-integration/standard/common/healthcareRecord/dtcQueryHealthcareRecordList";
    /**
     * 当前位置查询的连接/读超时:一轮要按患者调用几千次,
     * 不能让一次无响应把采集线程无限期挂住({@code HttpUtils} 老方法没有超时)
     */
    private static final int INHOSP_POSITION_CONNECT_TIMEOUT_MS = 5000;
    private static final int INHOSP_POSITION_READ_TIMEOUT_MS = 15000;
    @Autowired
    private IPatArchiveService patArchiveService;
@@ -85,6 +113,15 @@
    @Value("${spring.profiles.active}")
    private String active;
    @Autowired
    private ServiceSubtaskServiceImpl serviceSubtaskServiceImpl;
    @Autowired
    private ServiceSubtaskMapper serviceSubtaskMapper;
    @Autowired
    private InhospVisitFacade inhospVisitFacade;
    @Autowired
    private PatMedOuthospErrorMapper patMedOuthospErrorMapper;
    @Override
    public List<ServiceSLTDInhospResDTO> queryHealthcareRecordList(ServiceSLTDInhospReqVO reqVO) {
@@ -407,6 +444,47 @@
        return true;
    }
    @Override
    public List<InhospPositionRecord> queryInhospPosition(ServiceSLTDInhospReqVO reqVO) {
        // 与"入院采集"共用同一套入参构造:orgId + campusIds + statusList + healthcareRecordNo
        Map<String, Object> requestParams = buildRequestParams(reqVO);
        Map<String, String> headers = buildRequestHeaders();
        String url = sltdPubPath + INHOSP_POSITION_PATH;
        String result = HttpUtils.sendPostByHeader(url, new Gson().toJson(requestParams), headers,
                INHOSP_POSITION_CONNECT_TIMEOUT_MS, INHOSP_POSITION_READ_TIMEOUT_MS);
        if (StringUtils.isEmpty(result)) {
            // sendPostByHeader 把异常吞掉后返回空串:这里绝不能当成"该患者已不在院",
            // 否则接口整体不可用时会被记成"几千个患者都出院了",与真相反。
            throw new RuntimeException("查询在院患者当前位置接口无响应,healthcareRecordNo=" + reqVO.getHealthcareRecordNo());
        }
        // getDataList 在返回码非 200 时抛 RuntimeException;data 为 [](该患者已不在入院状态)时返回空列表
        List<Map<String, Object>> dataList = getDataList(result, 1);
        List<InhospPositionRecord> records = new ArrayList<>(dataList.size());
        for (Map<String, Object> dataItem : dataList) {
            records.add(convertToInhospPosition(dataItem));
        }
        return records;
    }
    /**
     * 健康记录 JSON → 契约 DTO 的逐字段搬运(只取"当前位置"要用的字段)。
     * <p>
     * 一律走 {@code getStringValue} / {@code getLongValue}:接口对空值可能返回 {@code null}、
     * {@code ""} 甚至缺字段,直接让 Gson 反序列化到 Long 字段会在空串上抛 NumberFormatException。
     */
    private InhospPositionRecord convertToInhospPosition(Map<String, Object> dataItem) {
        InhospPositionRecord record = new InhospPositionRecord();
        record.setHealthcareRecordNo(getStringValue(dataItem, "healthcareRecordNo"));
        record.setPatientName(getStringValue(dataItem, "patientName"));
        record.setDeptId(getLongValue(dataItem, "deptId"));
        record.setDeptName(getStringValue(dataItem, "deptName"));
        record.setAreaId(getLongValue(dataItem, "areaId"));
        record.setAreaName(getStringValue(dataItem, "areaName"));
        record.setBedNumber(getStringValue(dataItem, "bedNumber"));
        record.setHealthcareRecordStatus(getStringValue(dataItem, "healthcareRecordStatus"));
        return record;
    }
    private String determineCry(ServiceSLTDInhospReqVO reqVO) {
        if (CollectionUtils.isEmpty(reqVO.getStatusList()) && CollectionUtils.isEmpty(reqVO.getHealthcareRecordTypeList()))
            return null;
@@ -496,7 +574,12 @@
            log.info("【parseResponseData】成功解析dataList:{}条健康记录数据", dataList.size());
            List<ServiceSLTDInhospResDTO> resultList = new ArrayList<>();
            for (Map<String, Object> dataItem : dataList) {
                resultList.add(convertToDTO(dataItem));
                try {
                    resultList.add(convertToDTO(dataItem));
                } catch (Exception e) {
                    // 单条数据转换失败不再让整页失败:写错误表交给人工处理,其余记录照常入库
                    saveOuthospError(dataItem, "CONVERT", e.getMessage(), new Gson().toJson(dataItem), e);
                }
            }
            log.info("【parseResponseData】成功解析resultList:{}条健康记录数据", resultList.size());
@@ -606,34 +689,184 @@
    }
    private Boolean outHospitalDate(List<ServiceSLTDInhospResDTO> resultList) {
        List<PatMedOuthosp> batchList = new ArrayList<>();
        // 门急诊在 HIS 侧只能按天查,采集任务每半小时都会把当天上万个诊疗记录重新拉一遍,
        // 其中绝大多数上一轮已经入库。这里先整页批量查重,把已入库的直接跳过,
        // 只对新增记录做"档案处理 + 入库",否则每条重复数据都要走一遍
        // Redis加锁 + 档案查重 + 档案更新 + countBySerialnum + insert,一万条就是几万次 SQL,半小时跑不完。
        Set<String> pageSerialnums = new HashSet<>();
        List<ServiceSLTDInhospResDTO> uniqueList = new ArrayList<>();
        List<ServiceSLTDInhospResDTO> noSerialnumList = new ArrayList<>();
        Map<String, List<String>> serialnumsByDate = new LinkedHashMap<>();
        for (ServiceSLTDInhospResDTO dto : resultList) {
            if (Objects.isNull(dto)) {
                continue;
            }
            PatArchive patArchive = processPatientArchive(dto);
            PatMedOuthosp patMedOuthosp = buildPatMedOuthosp(dto, patArchive);
            batchList.add(patMedOuthosp);
            String serialnum = dto.getHealthcareRecordNo();
            if (StringUtils.isEmpty(serialnum)) {
                // 没有诊疗记录号就没法查重,保持原有行为照常处理(入库环节还会再兜一次)
                log.warn("【outHospitalDate】诊疗记录号为空,无法查重,按新增处理:patientName={}", dto.getPatientName());
                noSerialnumList.add(dto);
                continue;
            }
            if (!pageSerialnums.add(serialnum)) {
                // 同一页内重复的记录,只处理一条
                continue;
            }
            uniqueList.add(dto);
            // 按就诊日期分组(跨零点时最多两个日期),保证查重的分表和入库时分表路由一致
            String admitDateStr = formatAdmitDate(dto.getLastEncounterTime());
            serialnumsByDate.computeIfAbsent(admitDateStr == null ? "" : admitDateStr, k -> new ArrayList<>()).add(serialnum);
        }
        // 一次 SQL 查出这一页里已经入库的 serialnum(取代逐条 countBySerialnum)
        Set<String> existingSerialnums = new HashSet<>();
        for (Map.Entry<String, List<String>> entry : serialnumsByDate.entrySet()) {
            try {
                existingSerialnums.addAll(patMedOuthospService.selectExistingSerialnums(entry.getKey(), entry.getValue()));
            } catch (Exception e) {
                // 批量查重失败不影响正确性:下面入库时每条还会再查一次重,只是慢一些
                log.error("【outHospitalDate】批量查重失败,退化为逐条查重,就诊日期:{}", entry.getKey(), e);
            }
        }
        List<PatMedOuthosp> batchList = new ArrayList<>();
        int existSkipCount = 0;
        for (ServiceSLTDInhospResDTO dto : uniqueList) {
            if (existingSerialnums.contains(dto.getHealthcareRecordNo())) {
                existSkipCount++;
                continue;
            }
            PatMedOuthosp patMedOuthosp = buildNewOuthosp(dto);
            if (patMedOuthosp != null) {
                batchList.add(patMedOuthosp);
            }
        }
        for (ServiceSLTDInhospResDTO dto : noSerialnumList) {
            PatMedOuthosp patMedOuthosp = buildNewOuthosp(dto);
            if (patMedOuthosp != null) {
                batchList.add(patMedOuthosp);
            }
        }
        log.info("【outHospitalDate】本页{}条,去重后{}条,已入库跳过{}条,待入库{}条",
                resultList.size(), uniqueList.size(), existSkipCount, batchList.size());
        // 批量插入,自动忽略重复
        if (CollectionUtils.isNotEmpty(batchList)) {
            try {
                //这里用自动忽略重复会有问题,如果表重新建,serialnum索引会丢失。就无法去重,所以还是需要内存里去重
                batchList = DistinctByProperty.distinctByProperty(batchList, PatMedOuthosp::getSerialnum);
                Integer insertCount = 0;
                for (PatMedOuthosp patMedOuthosp : batchList) {
                    insertCount += patMedOuthospService.insertPatMedOuthosp(patMedOuthosp);
            //这里用自动忽略重复会有问题,如果表重新建,serialnum索引会丢失。就无法去重,所以还是需要内存里去重
            batchList = DistinctByProperty.distinctByProperty(batchList, PatMedOuthosp::getSerialnum);
            int insertCount = 0;
            int skipCount = 0;
            List<String> failSerialnums = new ArrayList<>();
            for (PatMedOuthosp patMedOuthosp : batchList) {
                try {
                    int inserted = patMedOuthospService.insertPatMedOuthosp(patMedOuthosp);
                    if (inserted > 0) {
                        insertCount += inserted;
                    } else {
                        // 返回0表示已存在(重复采集)或就诊时间为空,属于正常跳过,不算失败
                        skipCount++;
                    }
                } catch (Exception e) {
                    // 单条入库失败不再中断采集:落错误表交给人工处理,采集断点照常推进
                    failSerialnums.add(patMedOuthosp.getSerialnum());
                    saveOuthospError(null, patMedOuthosp, "INSERT", e.getMessage(), new Gson().toJson(patMedOuthosp), e);
                }
            }
            log.info("批量插入门急诊记录,总数:{}, 实际插入:{}, 跳过(已存在/无就诊时间):{}, 落错误表:{}",
                    batchList.size(), insertCount, skipCount, failSerialnums.size());
                log.info("批量插入门急诊记录,总数:{}, 实际插入:{}", batchList.size(), insertCount);
            } catch (Exception e) {
                log.error("批量插入门急诊记录失败", e);
            if (!failSerialnums.isEmpty()) {
                // 失败明细已逐条写进 pat_med_outhosp_error,这里只提示,不再抛异常阻断采集
                log.error("门急诊记录入库失败 {} 条,已写入 pat_med_outhosp_error 等人工处理,serialnum:{}",
                        failSerialnums.size(), failSerialnums);
            }
        }
        return true;
    }
    /**
     * 处理单条新增记录的患者档案并构建门急诊记录;
     * 失败时写错误表交给人工处理并返回 null,不影响同一页里其他记录
     */
    private PatMedOuthosp buildNewOuthosp(ServiceSLTDInhospResDTO dto) {
        try {
            PatArchive patArchive = processPatientArchive(dto);
            return buildPatMedOuthosp(dto, patArchive);
        } catch (Exception e) {
            saveOuthospError(dto, null, "ARCHIVE", e.getMessage(), new Gson().toJson(dto), e);
            return null;
        }
    }
    /** 采集失败的原始记录来自接口返回的 Map(数据转换失败时还没有 DTO) */
    private void saveOuthospError(Map<String, Object> dataItem, String errorType, String errorMsg,
                                  String rawData, Throwable cause) {
        PatMedOuthospError error = new PatMedOuthospError();
        error.setSerialnum(getStringValue(dataItem, "healthcareRecordNo"));
        error.setOuthospno(getStringValue(dataItem, "healthcareRecordNo"));
        error.setPatname(getStringValue(dataItem, "patientName"));
        error.setPatno(getStringValue(dataItem, "medicalRecordNo"));
        error.setCampusid(getStringValue(dataItem, "campusId"));
        error.setOrgid(getStringValue(dataItem, "orgId"));
        doSaveOuthospError(error, errorType, errorMsg, rawData, cause);
    }
    /** 采集失败的原始记录来自 DTO / 待入库的 PatMedOuthosp */
    private void saveOuthospError(ServiceSLTDInhospResDTO dto, PatMedOuthosp patMedOuthosp, String errorType,
                                  String errorMsg, String rawData, Throwable cause) {
        PatMedOuthospError error = new PatMedOuthospError();
        if (dto != null) {
            error.setSerialnum(dto.getHealthcareRecordNo());
            error.setOuthospno(dto.getHealthcareRecordNo());
            error.setPatname(dto.getPatientName());
            error.setPatno(dto.getMedicalRecordNo());
            error.setCampusid(dto.getCampusId());
            error.setOrgid(dto.getOrgId());
            error.setAdmitdate(parseDate(dto.getLastEncounterTime()));
        }
        if (patMedOuthosp != null) {
            if (StringUtils.isEmpty(error.getSerialnum())) {
                error.setSerialnum(patMedOuthosp.getSerialnum());
            }
            error.setOuthospno(patMedOuthosp.getOuthospno());
            error.setPatname(patMedOuthosp.getPatname());
            error.setPatno(patMedOuthosp.getPatno());
            error.setCampusid(patMedOuthosp.getCampusid());
            error.setOrgid(patMedOuthosp.getOrgid());
            error.setAdmitdate(patMedOuthosp.getAdmitdate());
        }
        doSaveOuthospError(error, errorType, errorMsg, rawData, cause);
    }
    /**
     * 写门急诊采集失败记录表,交给人工处理。
     * 这里兜住一切异常:连错误表都写不进去时只打日志(带原始数据),绝不因为写错误表失败而中断采集
     */
    private void doSaveOuthospError(PatMedOuthospError error, String errorType, String errorMsg,
                                    String rawData, Throwable cause) {
        error.setErrorType(errorType);
        error.setErrorMsg(truncate(errorMsg == null || errorMsg.isEmpty() ? String.valueOf(cause) : errorMsg, 1000));
        error.setRawData(rawData);
        error.setFailCount(1);
        error.setHandleStatus("0");
        error.setGatherDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
        error.setCreateTime(DateUtils.getNowDate());
        try {
            patMedOuthospErrorMapper.insertPatMedOuthospError(error);
            log.error("【门急诊采集】{} 失败,已写入 pat_med_outhosp_error 等人工处理,serialnum={},患者={},原因={}",
                    errorType, error.getSerialnum(), error.getPatname(), error.getErrorMsg(), cause);
        } catch (Exception ex) {
            log.error("【门急诊采集】{} 失败,且写入错误表也失败,serialnum={},原始数据:{}",
                    errorType, error.getSerialnum(), rawData, ex);
        }
    }
    /** 截断超长文本,避免超出错误表字段长度导致落表失败 */
    private String truncate(String text, int maxLength) {
        if (text == null) {
            return null;
        }
        return text.length() <= maxLength ? text : text.substring(0, maxLength);
    }
    private PatMedOuthosp buildPatMedOuthosp(ServiceSLTDInhospResDTO dto, PatArchive patArchive) {
@@ -674,11 +907,35 @@
        }
    }
    /**
     * 取就诊日期的 yyyy-MM-dd 部分,与 buildPatMedOuthosp 里的 admitdate 保持完全一致的解析规则,
     * 保证批量查重用的分表和入库时路由到的分表是同一张
     */
    private String formatAdmitDate(String lastEncounterTime) {
        Date admitDate = parseDate(lastEncounterTime);
        if (admitDate == null) {
            return null;
        }
        return new SimpleDateFormat("yyyy-MM-dd").format(admitDate);
    }
    private Boolean inHospitalDate(List<ServiceSLTDInhospResDTO> resultList, String cry) {
        int failCount = 0;
        for (ServiceSLTDInhospResDTO dto : resultList) {
            if (Objects.isNull(dto)) continue;
            PatArchive patArchive = processPatientArchive(dto);
            processPatientInhospInfo(dto, patArchive, cry);
            try {
                PatArchive patArchive = processPatientArchive(dto);
                processPatientInhospInfo(dto, patArchive, cry);
            } catch (Exception e) {
                failCount++;
                log.error("【inHospitalDate】单条记录处理失败,已跳过继续采集。state={}, serialnum={}, patname={}, patno={}, orgid={}, campusId={}",
                        cry, dto.getHealthcareRecordNo(), dto.getPatientName(), dto.getMedicalRecordNo(),
                        dto.getOrgId(), dto.getCampusId(), e);
            }
        }
        if (failCount > 0) {
            log.warn("【inHospitalDate】本页共{}条,失败{}条已跳过,其余{}条处理完成,state={}",
                    resultList.size(), failCount, resultList.size() - failCount, cry);
        }
        return true;
    }
@@ -709,6 +966,44 @@
                    queryInhosp.setInhospstate("1");
                    patMedInhospList = patMedInhospService.selectPatMedInhosp(queryInhosp);
                    if (CollectionUtils.isNotEmpty(patMedInhospList)) {
                        //如果不为空,则将入院时间进行更新
                        PatMedInhosp pmOut = patMedInhospList.get(0);
                        //本次入院数据(这里 existingInhosps 必为空,取 existingInhosps.get(0) 会下标越界)
                        PatMedInhosp pmIn = patMedInhosp;
                        pmOut.setStarttime(pmIn.getStarttime());
                        pmOut.setUpdateTime(new Date());
                        patMedInhospService.updatePatMedInhosp(pmOut);
                        //省立同德需要将开始日期与入院日期相等的任务,改成不执行
                        try {
                            if ("sltd".equals(active) && DateUtils.isSameDay(pmOut.getStarttime(), pmIn.getEndtime())) {
                                ServiceSubtask serviceSubtask = new ServiceSubtask();
                                serviceSubtask.setInhospid(pmOut.getInhospid());
                                serviceSubtask.setSendstate(4L);
                                serviceSubtaskMapper.updateServiceSubtaskByCondition(serviceSubtask);
                            }
                            //通过入院时间,和患者的编号去查一下出院时间,是否存在,如果存在,则该患者是再入院的,需要改成不执行
                            //出院时间只比较到天,不比较时分秒
                            if (pmIn.getStarttime() != null) {
                                Date dayStart = DateUtils.truncate(pmIn.getStarttime(), Calendar.DAY_OF_MONTH);
                                Date dayEnd = DateUtils.addDays(dayStart, 1);
                                List<PatMedInhosp> outInhosps = inhospVisitFacade.selectOutInhospByPatnoAndOutDate(
                                        StringUtils.trim(pmIn.getPatno()), dayStart, dayEnd);
                                if (CollectionUtils.isNotEmpty(outInhosps)) {
                                    //存在出院记录,说明该患者是再入院,把上一次出院产生的子任务改成不执行
                                    ServiceSubtask outSubtask = new ServiceSubtask();
                                    outSubtask.setInhospid(outInhosps.get(0).getInhospid());
                                    outSubtask.setSendstate(4L);
                                    outSubtask.setRemark("患者再入院");
                                    outSubtask.setUpdateTime(new Date());
                                    serviceSubtaskMapper.updateServiceSubtaskByCondition(outSubtask);
                                    log.info("患者再入院,将上次出院记录的子任务改为不执行:patno={}, 出院inhospid={}, 出院时间={}, 本次入院时间={}",
                                            pmIn.getPatno(), outInhosps.get(0).getInhospid(), outInhosps.get(0).getEndtime(), pmIn.getStarttime());
                                }
                            }
                        } catch (Exception e) {
                            log.error("将出院时间等于入院时间的inhospid的任务更新失败了, inhospid={}", pmOut.getInhospid(), e);
                        }
                        return;
                    }
                }
@@ -900,7 +1195,8 @@
            }
        }
        if (active.equals("nhfy") || active.equals("sltd")) {
        //目前只有省立同德
        if (active.equals("sltd")) {
            Map<String, String> drugInfoMap = queryMedicalOrders(dto);
            if (drugInfoMap != null && !drugInfoMap.isEmpty()) {
                patMedInhosp.setMedicalName(drugInfoMap.get("drugItemNames"));