| | |
| | | import com.ruoyi.common.utils.*; |
| | | import com.ruoyi.common.utils.http.HttpUtils; |
| | | import com.ruoyi.common.utils.sms.smsUtils; |
| | | import com.ruoyi.quartz.constant.MzGatherConstant; |
| | | import com.ruoyi.quartz.enums.MzGatherParamEnum; |
| | | import com.ruoyi.quartz.enums.MzGatherSlotEnum; |
| | | import com.ruoyi.quartz.enums.MzGatherStatusEnum; |
| | | import com.ruoyi.quartz.service.ICollectHISService; |
| | | import com.ruoyi.quartz.util.CreateDataTable; |
| | | import com.ruoyi.system.domain.SysConfig; |
| | |
| | | log.info("【sendExceptInfo】发送人工号:{}", dutyDeptPersonCode); |
| | | SysUser sysUser = new SysUser(); |
| | | SysUser sysUserVo = new SysUser(); |
| | | sysUserVo.setUserCode(dutyDeptPersonCode); |
| | | sysUserVo.setUserName(dutyDeptPersonCode); |
| | | sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid()); |
| | | List<SysUser> users = userService.getUserList(sysUserVo); |
| | | String phoneNumber = ""; |
| | |
| | | preAdmitStatus.add("FH0109.53"); |
| | | reqVO.setStatusList(preAdmitStatus); |
| | | log.info("【dealHisData】预入院采集数据入参:{}", reqVO); |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | try { |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】预入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | log.info("【dealHisData】预入院終了采集数据,院区:{}", cid); |
| | | |
| | | // 入院 |
| | |
| | | admitStatus.add("FH0109.24"); |
| | | reqVO.setStatusList(admitStatus); |
| | | log.info("【dealHisData】入院采集数据入参:{}", reqVO); |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | try { |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | log.info("【dealHisData】入院終了采集数据,院区:{}", cid); |
| | | |
| | | // 出院 |
| | |
| | | dischargeStatus.add("FH0109.25"); |
| | | reqVO.setStatusList(dischargeStatus); |
| | | log.info("【dealHisData】出院采集数据入参:{}", reqVO); |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | try { |
| | | serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】出院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | log.info("【dealHisData】出院结束采集数据,院区:{}", cid); |
| | | } |
| | | |
| | | /** |
| | | * HIS省立同德门急诊数据采集(这个只能一天执行一次,不能半个小时执行一个,数据量太大,影响性能) |
| | | * 定时任务:门诊数据 |
| | | * todayZeroStr 当前的时间 例如:2026-05-06 00:00:00 |
| | | * HIS省立同德门急诊数据采集(每半小时一轮,按天全量刷新 + 分页断点续采) |
| | | * <p> |
| | | * HIS 侧门急诊数据只能按天查,每轮拉到的都是当天全量(到晚上一万多条),这个取数成本改不了; |
| | | * 能改的是处理成本:当天数据里绝大多数上一轮已经入库,靠"整页批量查重 + 已入库直接跳过", |
| | | * 每轮真正需要做档案处理和入库的只有新出现的那几百条,一万多条也能在很短时间内跑完。 |
| | | * <p> |
| | | * 每一轮做三件事: |
| | | * <ol> |
| | | * <li>今天的整轮刷新:第 1 页翻到最后一页,只处理新记录;</li> |
| | | * <li>昨天的收尾采集:每天只做一次,兜住 23:xx 之后才写入 HIS 的记录和跨零点的数据;</li> |
| | | * <li>手工回溯:任务入参给了日期时,把那天补采一次(同一天只补一次,不会每轮重复补)。</li> |
| | | * </ol> |
| | | * 每轮最多跑 {@link MzGatherParamEnum#BUDGET_MINUTES}(默认 20 分钟),到点保存页码断点正常退出,下一轮从该页继续; |
| | | * 每采完一页都会落一次断点,进程被杀也不会从头重扫。 |
| | | * <p> |
| | | * 失败一律不阻断采集进度: |
| | | * <ul> |
| | | * <li>数据层失败(转换/档案/入库)落 pat_med_outhosp_error 表交人工处理;</li> |
| | | * <li>接口层失败(网络超时、来未来返回非 200)没有数据可落表,只写 error 日志(含院区、日期、页码、接口入参), |
| | | * 跳过该页继续往后采,本轮结束时把断点留在第一个失败页;因为每轮都是整天从头全量刷新, |
| | | * 被跳过的页下一轮必然重新采到,不会漏;</li> |
| | | * <li>连续 {@link MzGatherConstant#MAX_CONSECUTIVE_PAGE_FAIL} 页接口失败判定为接口整体不可用,本轮提前结束(避免空转几十页), |
| | | * 断点留在第一个失败页,下一轮再试。</li> |
| | | * </ul> |
| | | * |
| | | * @param todayZeroStr 可选,手工指定要一次性回溯的日期,支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd;为空时只刷新今天+收尾昨天 |
| | | */ |
| | | public void dealHisDataMZ(String todayZeroStr) { |
| | | log.info("【dealHisData】门急诊数据开始执行HIS数据采集任务"); |
| | | // 获取昨天0点到今天0点的时间范围 |
| | | LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN); |
| | | if (StringUtils.isNotBlank(todayZeroStr)) { |
| | | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | todayZero = LocalDateTime.parse(todayZeroStr, formatter); |
| | | log.info("【dealHisDataMZ】门急诊数据采集开始,入参:{}", todayZeroStr); |
| | | // 南华附一的先不采门急诊数据 |
| | | if (!"sltd".equals(active.trim())) { |
| | | log.info("【dealHisDataMZ】当前环境 {} 不采集门急诊数据,跳过", active.trim()); |
| | | return; |
| | | } |
| | | LocalDateTime tomorrowdayZero = todayZero.plusDays(1); |
| | | |
| | | if (active.trim().equals("sltd") || active.trim().equals("nhfy")) { |
| | | //省立同德与华南附一用同一套采集方法(都是来未来提供) |
| | | long budgetMinutes = getMzConfigLong(MzGatherParamEnum.BUDGET_MINUTES); |
| | | long pageSize = getMzConfigLong(MzGatherParamEnum.PAGE_SIZE); |
| | | long maxPage = getMzConfigLong(MzGatherParamEnum.MAX_PAGE); |
| | | |
| | | LocalDateTime now = LocalDateTime.now().withNano(0); |
| | | LocalDateTime deadline = now.plusMinutes(budgetMinutes); |
| | | |
| | | // 获取省立同德要采集的院区 |
| | | SysConfig cf = new SysConfig(); |
| | | cf.setConfigKey("sltd.gather.campusid"); |
| | | cf.setOrgid(MzGatherConstant.ORG_ID); |
| | | List<SysConfig> sysConfigcampusids = configService.selectConfigList(cf); |
| | | if (CollectionUtils.isEmpty(sysConfigcampusids) || StringUtils.isBlank(sysConfigcampusids.get(0).getConfigValue())) { |
| | | log.error("【dealHisDataMZ】SysConfig中的院区ID为空!"); |
| | | return; |
| | | } |
| | | List<Long> campusidList = Arrays.stream(sysConfigcampusids.get(0).getConfigValue().split(",")) |
| | | .map(String::trim).filter(StringUtils::isNotBlank).map(Long::valueOf).collect(Collectors.toList()); |
| | | log.info("【dealHisDataMZ】院区:{},单轮预算:{}分钟,每页:{}条", campusidList, budgetMinutes, pageSize); |
| | | |
| | | for (Long cid : campusidList) { |
| | | if (LocalDateTime.now().isAfter(deadline)) { |
| | | log.warn("【dealHisDataMZ】本轮时间预算已用尽,剩余院区留到下一轮继续,院区:{}", cid); |
| | | break; |
| | | } |
| | | // 防止上一轮还没跑完就被下一轮叠着跑(预算配置过大、或手工触发时兜底) |
| | | String lockKey = "mzRunning:" + cid; |
| | | if (Boolean.TRUE.equals(redisCache.hasKey(lockKey))) { |
| | | log.warn("【dealHisDataMZ】该院区上一轮采集尚未结束,本轮跳过,院区:{}(断点已保留,下一轮继续)", cid); |
| | | continue; |
| | | } |
| | | redisCache.setCacheObject(lockKey, MzGatherConstant.MINUTE_FMT.format(now), budgetMinutes * 2, TimeUnit.MINUTES); |
| | | try { |
| | | //获取省立同德要采集的院区 |
| | | SysConfig cf = new SysConfig(); |
| | | cf.setConfigKey("sltd.gather.campusid"); |
| | | cf.setOrgid("20001001"); |
| | | List<SysConfig> sysConfigcampusids = configService.selectConfigList(cf); |
| | | |
| | | List<Long> campusidList = new ArrayList<>(); |
| | | if (CollectionUtils.isNotEmpty(sysConfigcampusids)) { |
| | | String[] split = sysConfigcampusids.get(0).getConfigValue().split(","); |
| | | campusidList = Arrays.stream(split).map(Long::valueOf).collect(Collectors.toList()); |
| | | } else { |
| | | log.error("SysConfig中的院区ID为空!"); |
| | | return; |
| | | } |
| | | |
| | | // 省立同德数据采集 |
| | | log.info("--campusidList的值为:{}", campusidList); |
| | | for (Long cid : campusidList) { |
| | | String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); |
| | | ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO(); |
| | | serviceSLTDInhospReqVO.setOrgId("20001001"); |
| | | serviceSLTDInhospReqVO.setCampusId(cid); |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime); |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime); |
| | | |
| | | // 门急诊分页采集:每页40条,直到无数据为止(南华附一的先不采门急诊数据) |
| | | if (active.trim().equals("sltd")) { |
| | | log.info("【dealHisData】省立同德“门急诊”开始分页采集数据,院区:{}", cid); |
| | | serviceSLTDInhospReqVO.setStartHeadTime(null); |
| | | serviceSLTDInhospReqVO.setStartTailTime(null); |
| | | //查询当天的数据 |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(tomorrowdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | serviceSLTDInhospReqVO.setStatusList(null); |
| | | List<String> list1 = new ArrayList<>(); |
| | | list1.add("FH0108.01"); |
| | | list1.add("FH0108.03"); |
| | | serviceSLTDInhospReqVO.setHealthcareRecordTypeList(list1); |
| | | //从缓存里获取起始页(零点前10分钟内用昨天的日期,避免跨天漏采) |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | String mzCacheDate = (now.getHour() == 0 && now.getMinute() < 10) ? now.minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) : now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); |
| | | Long outpPage = redisCache.getCacheObject("mzCurrent" + cid + mzCacheDate); |
| | | if (outpPage == null) outpPage = 1L; |
| | | //那边暂时只能设置40 |
| | | final long OUTP_PAGE_SIZE = 500L; |
| | | while (true) { |
| | | serviceSLTDInhospReqVO.setCurrent(outpPage); |
| | | serviceSLTDInhospReqVO.setSize(OUTP_PAGE_SIZE); |
| | | log.info("【dealHisData】省立同德“门急诊”采集第{}页数据,院区:{}", outpPage, cid); |
| | | List<ServiceSLTDInhospResDTO> outpResult = serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO); |
| | | if (CollectionUtils.isEmpty(outpResult)) { |
| | | log.info("【dealHisData】省立同德“门急诊”第{}页无数据,采集结束,院区:{}", outpPage, cid); |
| | | break; |
| | | } |
| | | log.info("【dealHisData】省立同德“门急诊”第{}页采集{}条,院区:{}", outpPage, outpResult.size(), cid); |
| | | if (outpResult.size() < OUTP_PAGE_SIZE) { |
| | | //设置一个缓存,用来存在下次的起始页 |
| | | redisCache.setCacheObject("mzCurrent" + cid + mzCacheDate, outpPage, 24, TimeUnit.HOURS); |
| | | log.info("【dealHisData】省立同德“门急诊”的缓存key:{}下次的起始页{},院区:{}", "mzCurrent" + cid + mzCacheDate, outpPage, cid); |
| | | break; |
| | | } |
| | | outpPage++; |
| | | } |
| | | // 清除分页参数,避免影响后续出入院查询 |
| | | serviceSLTDInhospReqVO.setCurrent(null); |
| | | serviceSLTDInhospReqVO.setSize(null); |
| | | log.info("【dealHisData】省立同德“门急诊”分页采集完成,院区:{}", cid); |
| | | } |
| | | } |
| | | collectOutpatientDaily(cid, todayZeroStr, pageSize, maxPage, deadline); |
| | | } catch (Exception e) { |
| | | log.error("【dealHisData】省立同德数据门急诊采集异常", e); |
| | | log.error("【dealHisDataMZ】门急诊采集异常,院区:{},断点已保留,下一轮从断点继续", cid, e); |
| | | } finally { |
| | | redisCache.deleteObject(lockKey); |
| | | } |
| | | } |
| | | log.info("【dealHisDataMZ】门急诊采集本轮结束,耗时:{}秒", |
| | | java.time.Duration.between(now, LocalDateTime.now()).getSeconds()); |
| | | } |
| | | |
| | | /** |
| | | * 采集某个院区:先补一次性回溯日期,再做昨天的收尾采集,最后整轮刷新今天 |
| | | */ |
| | | private void collectOutpatientDaily(Long cid, String backfillParam, long pageSize, long maxPage, |
| | | LocalDateTime deadline) { |
| | | String cursorKey = "mzDayCursor:" + cid; |
| | | MzDayCursor cursor = loadMzDayCursor(cursorKey); |
| | | LocalDate today = LocalDate.now(); |
| | | LocalDate yesterday = today.minusDays(1); |
| | | |
| | | // 手工入参指定的一次性回溯:同一个日期只补一次,避免定时任务每轮重复补 |
| | | LocalDateTime backfillTime = parseMzStartTime(backfillParam); |
| | | if (backfillTime != null) { |
| | | LocalDate backfillDay = backfillTime.toLocalDate(); |
| | | if (backfillDay.toString().equals(cursor.backfillDone)) { |
| | | log.info("【dealHisDataMZ】入参日期 {} 已完成回溯,跳过,院区:{}", backfillDay, cid); |
| | | } else if (backfillDay.equals(today)) { |
| | | // 入参就是今天,交给下面的整轮刷新处理即可 |
| | | cursor.backfillDone = backfillDay.toString(); |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | } else { |
| | | cursor.backfillDay = backfillDay.toString(); |
| | | cursor.backfillPage = 1L; |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | log.info("【dealHisDataMZ】待回溯日期:{},院区:{}", backfillDay, cid); |
| | | } |
| | | } |
| | | |
| | | // 1) 一次性回溯 |
| | | if (cursor.backfillDay != null) { |
| | | LocalDate day = LocalDate.parse(cursor.backfillDay); |
| | | MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.BACKFILL, day, |
| | | cursor.backfillPage, pageSize, maxPage, deadline); |
| | | if (status != MzGatherStatusEnum.DONE) { |
| | | log.warn("【dealHisDataMZ】回溯日期 {} 本轮未完成({}),断点已保留,院区:{}", day, status.getDesc(), cid); |
| | | return; |
| | | } |
| | | cursor.backfillDone = cursor.backfillDay; |
| | | cursor.backfillDay = null; |
| | | cursor.backfillPage = 1L; |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | } |
| | | |
| | | // 2) 昨天收尾采集:每天一次,兜住 23:xx 之后才写进 HIS 的记录和跨零点的数据 |
| | | if (!yesterday.toString().equals(cursor.finalDay)) { |
| | | MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.FINAL, yesterday, |
| | | cursor.finalPage, pageSize, maxPage, deadline); |
| | | if (status != MzGatherStatusEnum.DONE) { |
| | | log.warn("【dealHisDataMZ】昨日收尾采集 {} 本轮未完成({}),断点已保留,院区:{}", yesterday, status.getDesc(), cid); |
| | | return; |
| | | } |
| | | cursor.finalDay = yesterday.toString(); |
| | | cursor.finalPage = 1L; |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | } |
| | | |
| | | // 3) 今天的整轮刷新 |
| | | if (!today.toString().equals(cursor.day)) { |
| | | cursor.day = today.toString(); |
| | | cursor.page = 1L; |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | } |
| | | MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.TODAY, today, |
| | | cursor.page, pageSize, maxPage, deadline); |
| | | if (status == MzGatherStatusEnum.DONE) { |
| | | // 整轮刷新完成,页码归位,下一轮重新从第一页整轮刷新 |
| | | cursor.page = 1L; |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | log.info("【dealHisDataMZ】{} 门急诊整轮刷新完成,院区:{}", today, cid); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 全量采集某一天(分页直到采完),每采完一页立即落一次断点 |
| | | * |
| | | * @param slot 断点槽位(今天整轮刷新 / 昨日收尾 / 手工回溯),决定中断页码记到哪个字段 |
| | | * @return {@link MzGatherStatusEnum} 采集完成 / 预算用尽 / 接口连续失败提前结束 |
| | | */ |
| | | private MzGatherStatusEnum runDayPass(Long cid, String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, |
| | | LocalDate day, long startPage, long pageSize, long maxPage, |
| | | LocalDateTime deadline) { |
| | | String startStr = day.format(MzGatherConstant.DAY_FMT); |
| | | String endStr = day.plusDays(1).format(MzGatherConstant.DAY_FMT); |
| | | |
| | | ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO(); |
| | | reqVO.setOrgId(MzGatherConstant.ORG_ID); |
| | | reqVO.setCampusId(cid); |
| | | reqVO.setStartHeadTime(null); |
| | | reqVO.setStartTailTime(null); |
| | | reqVO.setStatusList(null); |
| | | // 接口只支持按天查,起止都传日期 |
| | | reqVO.setEncounterTimeStart(startStr); |
| | | reqVO.setEncounterTimeEnd(endStr); |
| | | List<String> typeList = new ArrayList<>(); |
| | | typeList.add("FH0108.01"); // 门诊 |
| | | typeList.add("FH0108.03"); // 急诊 |
| | | reqVO.setHealthcareRecordTypeList(typeList); |
| | | reqVO.setSize(pageSize); |
| | | |
| | | long page = startPage < 1L ? 1L : startPage; |
| | | long total = 0L; |
| | | long pageCount = 0L; |
| | | // 接口层失败跳过的页数、第一个失败页、连续失败页数 |
| | | long skippedPages = 0L; |
| | | long firstFailPage = -1L; |
| | | int consecutiveFail = 0; |
| | | while (true) { |
| | | if (LocalDateTime.now().isAfter(deadline)) { |
| | | // 本轮有跳过的页时,断点回到第一个失败页,保证下一轮先把这些页补回来 |
| | | long resumePage = (skippedPages > 0 && firstFailPage > 0) ? firstFailPage : page; |
| | | saveMzDayPage(cursorKey, cursor, slot, resumePage); |
| | | log.warn("【dealHisDataMZ】本轮时间预算用尽,{} 本轮已采{}页{}条{},下次从第{}页继续,院区:{}", |
| | | day, pageCount, total, skippedLog(skippedPages), resumePage, cid); |
| | | return MzGatherStatusEnum.BUDGET_UP; |
| | | } |
| | | reqVO.setCurrent(page); |
| | | log.info("【dealHisDataMZ】采集门急诊数据,院区:{},日期:{},第{}页", cid, day, page); |
| | | |
| | | List<ServiceSLTDInhospResDTO> pageData = null; |
| | | Exception lastError = null; |
| | | for (int attempt = 1; attempt <= MzGatherConstant.PAGE_RETRY_TIMES; attempt++) { |
| | | try { |
| | | pageData = serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); |
| | | lastError = null; |
| | | break; |
| | | } catch (Exception e) { |
| | | lastError = e; |
| | | log.error("【dealHisDataMZ】门急诊第{}页第{}次采集失败,院区:{},日期:{}", page, attempt, cid, day, e); |
| | | if (attempt < MzGatherConstant.PAGE_RETRY_TIMES) { |
| | | try { |
| | | Thread.sleep(MzGatherConstant.PAGE_RETRY_SLEEP_MS); |
| | | } catch (InterruptedException ie) { |
| | | Thread.currentThread().interrupt(); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (lastError != null) { |
| | | // 接口层失败(网络超时 / 来未来返回非200)没有数据可以落表,只写 error 日志,然后跳过该页继续往后采,不阻断进度。 |
| | | // 每轮都是整天从头全量刷新,被跳过的页下一轮必然重新采到,所以不会漏数据。 |
| | | skippedPages++; |
| | | if (firstFailPage < 0) { |
| | | firstFailPage = page; |
| | | } |
| | | consecutiveFail++; |
| | | log.error("【dealHisDataMZ】接口层采集失败,已记 error 日志并跳过该页,本轮继续往后采;" |
| | | + "院区:{},日期:{},第{}页,接口入参:{},异常:{}", |
| | | cid, day, page, new Gson().toJson(reqVO), lastError.getMessage(), lastError); |
| | | if (consecutiveFail >= MzGatherConstant.MAX_CONSECUTIVE_PAGE_FAIL) { |
| | | // 连续多页接口失败,判定来未来接口整体不可用,本轮提前结束(不再空转后面几十页),断点留在第一个失败页 |
| | | log.error("【dealHisDataMZ】连续{}页接口失败,判定来未来接口不可用,本轮提前结束;院区:{},日期:{},断点保留在第{}页", |
| | | consecutiveFail, cid, day, firstFailPage); |
| | | saveMzDayPage(cursorKey, cursor, slot, firstFailPage); |
| | | return MzGatherStatusEnum.FAILED; |
| | | } |
| | | page++; |
| | | continue; |
| | | } |
| | | consecutiveFail = 0; |
| | | |
| | | if (CollectionUtils.isEmpty(pageData)) { |
| | | log.info("【dealHisDataMZ】{} 第{}页无数据,采集结束,院区:{},本轮{}页共{}条{}", |
| | | day, page, cid, pageCount, total, skippedLog(skippedPages)); |
| | | return MzGatherStatusEnum.DONE; |
| | | } |
| | | pageCount++; |
| | | total += pageData.size(); |
| | | log.info("【dealHisDataMZ】{} 第{}页采集{}条,院区:{}", day, page, pageData.size(), cid); |
| | | if (pageData.size() < pageSize) { |
| | | log.info("【dealHisDataMZ】{} 采集结束,院区:{},本轮{}页共{}条{}", day, cid, pageCount, total, skippedLog(skippedPages)); |
| | | return MzGatherStatusEnum.DONE; |
| | | } |
| | | page++; |
| | | if (page > maxPage) { |
| | | log.error("【dealHisDataMZ】{} 已翻到第{}页,超过单日上限{}页,按采完处理,院区:{}", day, page, maxPage, cid); |
| | | return MzGatherStatusEnum.DONE; |
| | | } |
| | | // 每采完一页立即落断点:进程被重启/强杀也能从这一页继续,不从头重扫,也不会漏 |
| | | saveMzDayPage(cursorKey, cursor, slot, page); |
| | | } |
| | | } |
| | | |
| | | /** 按槽位保存中断页码 */ |
| | | private void saveMzDayPage(String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, long page) { |
| | | if (slot == MzGatherSlotEnum.FINAL) { |
| | | cursor.finalPage = page; |
| | | } else if (slot == MzGatherSlotEnum.BACKFILL) { |
| | | cursor.backfillPage = page; |
| | | } else { |
| | | cursor.page = page; |
| | | } |
| | | saveMzDayCursor(cursorKey, cursor); |
| | | } |
| | | |
| | | /** 日志里提示接口层失败跳过了几页(下一轮整轮刷新会重新采到) */ |
| | | private String skippedLog(long skippedPages) { |
| | | return skippedPages > 0L ? ",接口失败跳过" + skippedPages + "页(下一轮整轮刷新会重新采到)" : ""; |
| | | } |
| | | |
| | | /** 读取 sys_config 里的门急诊采集参数(机构 orgid=20001001),取不到或超范围时用枚举里的默认值 */ |
| | | private long getMzConfigLong(MzGatherParamEnum param) { |
| | | try { |
| | | SysConfig config = new SysConfig(); |
| | | config.setConfigKey(param.getKey()); |
| | | config.setOrgid(MzGatherConstant.ORG_ID); |
| | | List<SysConfig> configs = configService.selectConfigList(config); |
| | | if (CollectionUtils.isNotEmpty(configs) && StringUtils.isNotBlank(configs.get(0).getConfigValue())) { |
| | | long value = Long.parseLong(configs.get(0).getConfigValue().trim()); |
| | | if (value >= param.getMin() && value <= param.getMax()) { |
| | | return value; |
| | | } |
| | | log.warn("【dealHisDataMZ】配置{}的值{}超出范围[{},{}],使用默认值{}", |
| | | param.getKey(), value, param.getMin(), param.getMax(), param.getDefaultValue()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("【dealHisDataMZ】读取配置{}失败,使用默认值{}", param.getKey(), param.getDefaultValue(), e); |
| | | } |
| | | return param.getDefaultValue(); |
| | | } |
| | | |
| | | /** 解析任务入参(要一次性回溯的日期),支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd */ |
| | | private LocalDateTime parseMzStartTime(String text) { |
| | | LocalDateTime time = parseMzTimeQuiet(text); |
| | | if (time == null && StringUtils.isNotBlank(text)) { |
| | | log.warn("【dealHisDataMZ】入参 {} 无法解析为日期,按未指定处理", text); |
| | | } |
| | | return time; |
| | | } |
| | | |
| | | /** 静默解析日期,支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd,格式不符返回 null(不刷日志) */ |
| | | private LocalDateTime parseMzTimeQuiet(String text) { |
| | | if (StringUtils.isBlank(text)) { |
| | | return null; |
| | | } |
| | | String value = text.trim(); |
| | | try { |
| | | return LocalDateTime.parse(value, MzGatherConstant.MINUTE_FMT); |
| | | } catch (Exception ignore) { |
| | | // 兼容只传日期的情况,落到下面按日期解析 |
| | | } |
| | | try { |
| | | return LocalDate.parse(value).atStartOfDay(); |
| | | } catch (Exception ignore) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** 保存门急诊采集断点(各轮次中断的页码) */ |
| | | private void saveMzDayCursor(String cursorKey, MzDayCursor cursor) { |
| | | JSONObject json = new JSONObject(); |
| | | json.put("day", cursor.day); |
| | | json.put("page", cursor.page); |
| | | json.put("finalDay", cursor.finalDay); |
| | | json.put("finalPage", cursor.finalPage); |
| | | json.put("backfillDay", cursor.backfillDay); |
| | | json.put("backfillPage", cursor.backfillPage); |
| | | json.put("backfillDone", cursor.backfillDone); |
| | | json.put("updateTime", MzGatherConstant.MINUTE_FMT.format(LocalDateTime.now().withNano(0))); |
| | | redisCache.setCacheObject(cursorKey, json.toJSONString(), MzGatherConstant.CACHE_TTL_HOURS, TimeUnit.HOURS); |
| | | } |
| | | |
| | | /** 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新) */ |
| | | private MzDayCursor loadMzDayCursor(String cursorKey) { |
| | | MzDayCursor cursor = new MzDayCursor(); |
| | | try { |
| | | String json = redisCache.getCacheObject(cursorKey); |
| | | if (StringUtils.isBlank(json)) { |
| | | return cursor; |
| | | } |
| | | JSONObject obj = JSONObject.parseObject(json); |
| | | cursor.day = obj.getString("day"); |
| | | cursor.page = obj.getLongValue("page"); |
| | | cursor.finalDay = obj.getString("finalDay"); |
| | | cursor.finalPage = obj.getLongValue("finalPage"); |
| | | cursor.backfillDay = obj.getString("backfillDay"); |
| | | cursor.backfillPage = obj.getLongValue("backfillPage"); |
| | | cursor.backfillDone = obj.getString("backfillDone"); |
| | | if (cursor.page < 1L) { |
| | | cursor.page = 1L; |
| | | } |
| | | if (cursor.finalPage < 1L) { |
| | | cursor.finalPage = 1L; |
| | | } |
| | | if (cursor.backfillPage < 1L) { |
| | | cursor.backfillPage = 1L; |
| | | } |
| | | return cursor; |
| | | } catch (Exception e) { |
| | | log.warn("【dealHisDataMZ】读取断点{}失败,按空断点处理", cursorKey, e); |
| | | return new MzDayCursor(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 门急诊采集断点:每天一轮全量刷新,这里分别记录三个轮次中断在第几页 |
| | | * (页码为 1 表示该轮次下一轮从第一页整轮开始) |
| | | */ |
| | | private static class MzDayCursor { |
| | | /** 今天的整轮刷新 */ |
| | | private String day; |
| | | private long page = 1L; |
| | | /** 昨天的收尾采集 */ |
| | | private String finalDay; |
| | | private long finalPage = 1L; |
| | | /** 待做的一次性回溯 */ |
| | | private String backfillDay; |
| | | private long backfillPage = 1L; |
| | | /** 已经补过的回溯日期,避免定时任务每轮重复补同一天 */ |
| | | private String backfillDone; |
| | | } |
| | | |
| | | /** |