| | |
| | | import java.time.LocalTime; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | |
| | | * —— 那是"顺手优化",会改变查询语义,回归时无法逐条比对。</li> |
| | | * </ul> |
| | | * |
| | | * <h3>2026-10 修正:接口 1500 条上限导致的静默丢数</h3> |
| | | * 来未来 {@code dtcQueryHealthcareRecordList} <b>单次最多返回 1500 条</b>,超出部分直接截断, |
| | | * 且 {@code code} 仍是 200、不报错。原"4 天一段"在出院高峰期(院内每天 380~500 条)必然超限, |
| | | * 于是每段悄悄少几十条 —— 表现为"回溯跑成功了,但某月病区人数比真实值少几条"。 |
| | | * <p> |
| | | * 现在的做法(见 {@link #gatherRange}): |
| | | * <ol> |
| | | * <li>契约方法改为返回条数,编排侧据此判断是否触顶;</li> |
| | | * <li>触顶就让<b>日期二分</b>重采(4 天 → 2 天 → 1 天);</li> |
| | | * <li>单日仍触顶就按 {@code statusList} <b>拆状态</b>重采(状态互斥,可无损拆分);</li> |
| | | * <li>连单日单状态都触顶时打 ERROR(参数已无法再拆),把"这次不完整"喊出来,而不是继续静默。</li> |
| | | * </ol> |
| | | * 二分只在触顶时发生,正常分段仍然只发 1 次请求,不会把日常回溯的请求量翻几倍。 |
| | | * |
| | | * |
| | | * <h3>它不管什么</h3> |
| | | * 真正的采集动作(HTTP 取数、入库)不在本类:那里仍在 smartor,经 |
| | | * {@link HisGatherApi} 契约调用,后续分批搬进本模块。 |
| | |
| | | * 省立同德 / 南华附一的机构ID(原实现里的字面量 {@code "20001001"}) |
| | | */ |
| | | private static final String SLTD_ORG_ID = "20001001"; |
| | | |
| | | /** |
| | | * 来未来查询接口的单次返回上限:超过这个条数<b>不报错、只截断</b>。 |
| | | * <p> |
| | | * 实测 {@code dtcQueryHealthcareRecordList} 有 1500 条硬上限({@code code} 仍为 200, |
| | | * 分页参数 {@code pageNo/pageSize/current/size} 均无效),所以只能靠调用方识别并拆分。 |
| | | */ |
| | | private static final int HIS_QUERY_LIMIT = 1500; |
| | | |
| | | /** |
| | | * 回溯时一段最多跨几天(4 天为历史值)。 |
| | | * <p> |
| | | * 段长只影响"平时要发几次请求",安全性由 {@link #gatherRange} 的触顶二分保证, |
| | | * 所以不必为了保守把它改成 1 天(那会让日常回溯请求量翻 4 倍)。 |
| | | */ |
| | | private static final int BACKFILL_SEGMENT_DAYS = 4; |
| | | |
| | | /** |
| | | * 预入院 / 入院 / 出院三类采集各自的状态码(与原实现一一对应,抽常量便于按状态拆分重采) |
| | | */ |
| | | private static final List<String> PRE_ADMIT_STATUS = Collections.unmodifiableList(Arrays.asList("FH0109.22", "FH0109.23", "FH0109.53")); |
| | | |
| | | private static final List<String> ADMIT_STATUS = Collections.singletonList("FH0109.24"); |
| | | |
| | | private static final List<String> DISCHARGE_STATUS = Collections.unmodifiableList(Arrays.asList("FH0109.27", "FH0109.25")); |
| | | |
| | | /** |
| | | * 诊疗记录的三类采集口径:决定日期条件落在 DTO 的哪一组字段上。 |
| | | */ |
| | | private enum RecordKind { |
| | | |
| | | PRE_ADMIT("预入院"), |
| | | ADMIT("入院"), |
| | | DISCHARGE("出院"); |
| | | |
| | | private final String label; |
| | | |
| | | RecordKind(String label) { |
| | | this.label = label; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 当时站点标识(原 {@code RyTask} 的 {@code @Value("${spring.profiles.active}")}) |
| | |
| | | hisGatherApi.gatherIcd10(); |
| | | } |
| | | |
| | | // nhfy/sltd回溯30天分段采集判断(每段最多4天) |
| | | // nhfy/sltd回溯30天分段采集判断(每段最多 BACKFILL_SEGMENT_DAYS 天,触顶自动二分) |
| | | boolean isBackfill = (active.trim().equals("nhfy") || active.trim().equals("sltd")) && StringUtils.isNotBlank(todayZeroStr); |
| | | if (isBackfill) { |
| | | DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | LocalDate segStart = todayZero.toLocalDate().minusDays(30); |
| | | LocalDate segEndDate = todayZero.toLocalDate(); |
| | | // 窗口 = [入参日期 - 30 天, 入参日期],两端都含。 |
| | | // ⚠️ 入参日期"之后"的数据不在窗口内:想补 2026-07-31 就要传 2026-07-31,传 07-30 不会带出 7/31。 |
| | | LocalDate segStart = segEndDate.minusDays(30); |
| | | int segCount = 0; |
| | | long fetchedTotal = 0L; |
| | | while (!segStart.isAfter(segEndDate)) { |
| | | LocalDate segEnd = segStart.plusDays(3); |
| | | LocalDate segEnd = segStart.plusDays(BACKFILL_SEGMENT_DAYS - 1L); |
| | | if (segEnd.isAfter(segEndDate)) { |
| | | segEnd = segEndDate; |
| | | } |
| | | segCount++; |
| | | log.info("【dealHisData】{}回溯第{}段采集,日期范围:{} ~ {}", active.trim(), segCount, segStart.format(dateFmt), segEnd.format(dateFmt)); |
| | | for (Long cid : campusidList) { |
| | | collectSegmentData(cid, segStart.format(dateFmt), segEnd.format(dateFmt)); |
| | | fetchedTotal += collectSegmentData(cid, segStart.format(dateFmt), segEnd.format(dateFmt)); |
| | | } |
| | | segStart = segEnd.plusDays(1); |
| | | } |
| | | log.info("【dealHisData】{}回溯采集完成,共{}段", active.trim(), segCount); |
| | | log.info("【dealHisData】{}回溯采集完成,共{}段,窗口{} ~ {},接口返回记录合计{}条", active.trim(), segCount, |
| | | segEndDate.minusDays(30).format(dateFmt), segEndDate.format(dateFmt), fetchedTotal); |
| | | } |
| | | |
| | | // 省立同德数据采集(非回溯模式才执行) |
| | | if (!isBackfill) { |
| | | DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | for (Long cid : campusidList) { |
| | | log.info("【dealHisData】省立同德预入院开始采集数据,院区:{}", cid); |
| | | String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); |
| | | SltdGatherReq serviceSLTDInhospReqVO = new SltdGatherReq(); |
| | | serviceSLTDInhospReqVO.setOrgId(SLTD_ORG_ID); |
| | | serviceSLTDInhospReqVO.setCampusId(cid); |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime); |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime); |
| | | // sltd 只采"今天";nhfy 采"昨天 ~ 今天"(与原实现一致) |
| | | String nowTime = LocalDate.now().format(dateFmt); |
| | | String head = nowTime; |
| | | String tail = nowTime; |
| | | if (active.trim().equals("nhfy")) { |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | head = yesterdayZero.format(dateFmt); |
| | | tail = todayZero.format(dateFmt); |
| | | } |
| | | List<String> list = new ArrayList<>(); |
| | | list.add("FH0109.22"); |
| | | list.add("FH0109.23"); |
| | | list.add("FH0109.53"); |
| | | serviceSLTDInhospReqVO.setStatusList(list); |
| | | // log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO); |
| | | hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO); |
| | | |
| | | log.info("【dealHisData】省立同德“预入院”开始采集数据,院区:{}", cid); |
| | | gatherRange(cid, RecordKind.PRE_ADMIT, head, tail, PRE_ADMIT_STATUS); |
| | | log.info("【dealHisData】省立同德“预入院”結束采集数据,院区:{}", cid); |
| | | |
| | | log.info("【dealHisData】省立同德“入院”开始采集数据,院区:{}", cid); |
| | | serviceSLTDInhospReqVO.setOrgId(SLTD_ORG_ID); |
| | | serviceSLTDInhospReqVO.setCampusId(cid); |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(null); |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(null); |
| | | serviceSLTDInhospReqVO.setStartHeadTime(nowTime); |
| | | serviceSLTDInhospReqVO.setStartTailTime(nowTime); |
| | | if (active.trim().equals("nhfy")) { |
| | | serviceSLTDInhospReqVO.setStartHeadTime(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | serviceSLTDInhospReqVO.setStartTailTime(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | } |
| | | List<String> listStr = new ArrayList<>(); |
| | | listStr.add("FH0109.24"); |
| | | serviceSLTDInhospReqVO.setStatusList(listStr); |
| | | // log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO); |
| | | hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO); |
| | | gatherRange(cid, RecordKind.ADMIT, head, tail, ADMIT_STATUS); |
| | | log.info("【dealHisData】省立同德“入院”結束采集数据,院区:{}", cid); |
| | | |
| | | log.info("【dealHisData】省立同德“出院”开始采集数据,院区:{}", cid); |
| | | serviceSLTDInhospReqVO.setStartHeadTime(null); |
| | | serviceSLTDInhospReqVO.setStartTailTime(null); |
| | | serviceSLTDInhospReqVO.setEncounterTimeStart(null); |
| | | serviceSLTDInhospReqVO.setEncounterTimeEnd(null); |
| | | serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(nowTime); |
| | | serviceSLTDInhospReqVO.setPreOutHospitalTailDate(nowTime); |
| | | if (active.trim().equals("nhfy")) { |
| | | serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | serviceSLTDInhospReqVO.setPreOutHospitalTailDate(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); |
| | | } |
| | | serviceSLTDInhospReqVO.setHealthcareRecordTypeList(null); |
| | | List<String> list2 = new ArrayList<>(); |
| | | // list2.add("FH0108.02"); |
| | | //出院 |
| | | list2.add("FH0109.27"); |
| | | //预出院 |
| | | list2.add("FH0109.25"); |
| | | serviceSLTDInhospReqVO.setStatusList(list2); |
| | | // log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO); |
| | | hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO); |
| | | log.info("【dealHisData】省立同德“出院”束采集数据,院区:{}", cid); |
| | | gatherRange(cid, RecordKind.DISCHARGE, head, tail, DISCHARGE_STATUS); |
| | | log.info("【dealHisData】省立同德“出院”結束采集数据,院区:{}", cid); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 分段采集数据(预入院、入院、出院) |
| | | * 用于nhfy/sltd回溯采集,每次最多查询7天数据 |
| | | * 分段采集数据(预入院、入院、出院)。 |
| | | * <p> |
| | | * 段长见 {@link #BACKFILL_SEGMENT_DAYS},每类采集内部还会做"触顶二分"(见 {@link #gatherRange}), |
| | | * 因此不会因为来未来接口 1500 条上限而悄悄少数据。 |
| | | * |
| | | * @param cid 院区ID |
| | | * @param startDateStr 日期开始 yyyy-MM-dd |
| | | * @param endDateStr 日期结束 yyyy-MM-dd |
| | | * @return 本段三类采集接口返回的记录数合计(回溯结束时用于总量核对) |
| | | */ |
| | | private void collectSegmentData(Long cid, String startDateStr, String endDateStr) { |
| | | // // 预入院 |
| | | private int collectSegmentData(Long cid, String startDateStr, String endDateStr) { |
| | | int count = 0; |
| | | |
| | | log.info("【dealHisData】预入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr); |
| | | SltdGatherReq reqVO = new SltdGatherReq(); |
| | | reqVO.setOrgId(SLTD_ORG_ID); |
| | | reqVO.setCampusId(cid); |
| | | reqVO.setEncounterTimeStart(startDateStr); |
| | | reqVO.setEncounterTimeEnd(endDateStr); |
| | | List<String> preAdmitStatus = new ArrayList<>(); |
| | | preAdmitStatus.add("FH0109.22"); |
| | | preAdmitStatus.add("FH0109.23"); |
| | | preAdmitStatus.add("FH0109.53"); |
| | | reqVO.setStatusList(preAdmitStatus); |
| | | log.info("【dealHisData】预入院采集数据入参:{}", reqVO); |
| | | try { |
| | | hisGatherApi.gatherSltdHealthcareRecords(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】预入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | count += gatherRange(cid, RecordKind.PRE_ADMIT, startDateStr, endDateStr, PRE_ADMIT_STATUS); |
| | | log.info("【dealHisData】预入院終了采集数据,院区:{}", cid); |
| | | |
| | | // 入院 |
| | | log.info("【dealHisData】入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr); |
| | | reqVO = new SltdGatherReq(); |
| | | reqVO.setOrgId(SLTD_ORG_ID); |
| | | reqVO.setCampusId(cid); |
| | | reqVO.setStartHeadTime(startDateStr); |
| | | reqVO.setStartTailTime(endDateStr); |
| | | List<String> admitStatus = new ArrayList<>(); |
| | | admitStatus.add("FH0109.24"); |
| | | reqVO.setStatusList(admitStatus); |
| | | log.info("【dealHisData】入院采集数据入参:{}", reqVO); |
| | | try { |
| | | hisGatherApi.gatherSltdHealthcareRecords(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | count += gatherRange(cid, RecordKind.ADMIT, startDateStr, endDateStr, ADMIT_STATUS); |
| | | log.info("【dealHisData】入院終了采集数据,院区:{}", cid); |
| | | |
| | | // 出院 |
| | | log.info("【dealHisData】出院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr); |
| | | reqVO = new SltdGatherReq(); |
| | | reqVO.setOrgId(SLTD_ORG_ID); |
| | | reqVO.setCampusId(cid); |
| | | reqVO.setPreOutHospitalHeadDate(startDateStr); |
| | | reqVO.setPreOutHospitalTailDate(endDateStr); |
| | | List<String> dischargeStatus = new ArrayList<>(); |
| | | //出院 |
| | | dischargeStatus.add("FH0109.27"); |
| | | //预出院 |
| | | dischargeStatus.add("FH0109.25"); |
| | | reqVO.setStatusList(dischargeStatus); |
| | | log.info("【dealHisData】出院采集数据入参:{}", reqVO); |
| | | try { |
| | | hisGatherApi.gatherSltdHealthcareRecords(reqVO); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】出院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e); |
| | | } |
| | | count += gatherRange(cid, RecordKind.DISCHARGE, startDateStr, endDateStr, DISCHARGE_STATUS); |
| | | log.info("【dealHisData】出院结束采集数据,院区:{}", cid); |
| | | |
| | | return count; |
| | | } |
| | | |
| | | /** |
| | | * 按采集口径组装入参:三类的区别只在"日期条件落在哪一组字段上"。 |
| | | * |
| | | * @param cid 院区ID |
| | | * @param kind 预入院 / 入院 / 出院 |
| | | * @param from 起始日期 yyyy-MM-dd |
| | | * @param to 截止日期 yyyy-MM-dd(含) |
| | | * @param statuses 该口径对应的状态码 |
| | | */ |
| | | private SltdGatherReq buildReq(Long cid, RecordKind kind, String from, String to, List<String> statuses) { |
| | | SltdGatherReq req = new SltdGatherReq(); |
| | | req.setOrgId(SLTD_ORG_ID); |
| | | req.setCampusId(cid); |
| | | switch (kind) { |
| | | case PRE_ADMIT: |
| | | req.setEncounterTimeStart(from); |
| | | req.setEncounterTimeEnd(to); |
| | | break; |
| | | case ADMIT: |
| | | req.setStartHeadTime(from); |
| | | req.setStartTailTime(to); |
| | | break; |
| | | case DISCHARGE: |
| | | req.setPreOutHospitalHeadDate(from); |
| | | req.setPreOutHospitalTailDate(to); |
| | | // 出院采集只按出院时间过滤,诊疗类型显式置 null(与原实现一致) |
| | | req.setHealthcareRecordTypeList(null); |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | req.setStatusList(statuses); |
| | | return req; |
| | | } |
| | | |
| | | /** |
| | | * 采一段,必要时拆小重采,返回这段实际取到的记录数。 |
| | | * |
| | | * <p>来未来接口单次最多返回 {@link #HIS_QUERY_LIMIT} 条,超出部分<b>直接截断且不报错</b>, |
| | | * 所以只能用"返回条数触顶"来识别,并按下面的顺序拆分(<b>只有触顶才会拆,平时仍是 1 次请求</b>): |
| | | * <ol> |
| | | * <li>跨多天:日期对半拆,递归重采;</li> |
| | | * <li>已缩到单天:按 {@code statusList} 拆(状态互斥,拆开既不漏也不重);</li> |
| | | * <li>单日单状态仍触顶:打 ERROR —— 请求参数维度已用尽,只能靠人看日志补采。</li> |
| | | * </ol> |
| | | * |
| | | * @param cid 院区ID |
| | | * @param kind 预入院 / 入院 / 出院 |
| | | * @param from 起始日期 yyyy-MM-dd |
| | | * @param to 截止日期 yyyy-MM-dd(含) |
| | | * @param statuses 该口径对应的状态码 |
| | | * @return 实际取到的记录数(拆分后为各子段之和) |
| | | */ |
| | | private int gatherRange(Long cid, RecordKind kind, String from, String to, List<String> statuses) { |
| | | SltdGatherReq req = buildReq(cid, kind, from, to, statuses); |
| | | log.info("【dealHisData】{}采集入参:{}", kind.label, req); |
| | | |
| | | int count; |
| | | try { |
| | | count = hisGatherApi.gatherSltdHealthcareRecords(req); |
| | | } catch (Exception e) { |
| | | // 单类数据失败不向上抛:否则一段失败会让本次回溯后面的所有段都不执行 |
| | | log.error("【dealHisData】{}采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", kind.label, cid, from, to, e); |
| | | return 0; |
| | | } |
| | | if (count < HIS_QUERY_LIMIT) { |
| | | return count; |
| | | } |
| | | |
| | | // 命中上限:接口不报错,只是把超出的数据丢掉,必须拆小重采 |
| | | LocalDate start = LocalDate.parse(from); |
| | | LocalDate end = LocalDate.parse(to); |
| | | if (start.isBefore(end)) { |
| | | LocalDate mid = start.plusDays(ChronoUnit.DAYS.between(start, end) / 2); |
| | | log.warn("【dealHisData】{}一次返回触达接口上限 {} 条,按日期二分重采。院区:{},{} ~ {} => {} ~ {} / {} ~ {}", |
| | | kind.label, HIS_QUERY_LIMIT, cid, from, to, from, mid, mid.plusDays(1), to); |
| | | return gatherRange(cid, kind, from, mid.toString(), statuses) |
| | | + gatherRange(cid, kind, mid.plusDays(1).toString(), to, statuses); |
| | | } |
| | | if (statuses.size() > 1) { |
| | | log.warn("【dealHisData】{}单日仍触达接口上限 {} 条,按状态拆分重采。院区:{},日期:{},状态:{}", |
| | | kind.label, HIS_QUERY_LIMIT, cid, from, statuses); |
| | | int sum = 0; |
| | | for (String status : statuses) { |
| | | sum += gatherRange(cid, kind, from, to, Collections.singletonList(status)); |
| | | } |
| | | return sum; |
| | | } |
| | | log.error("【dealHisData】{}单日单状态仍触达接口上限 {} 条,已无法再拆分,本次数据可能不完整!院区:{},日期:{},状态:{}", |
| | | kind.label, HIS_QUERY_LIMIT, cid, from, statuses); |
| | | return count; |
| | | } |
| | | } |