111
liusheng
2026-10-09 809f28b2efe947cb0987356cffa42de959e2aa4d
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -8,7 +8,9 @@
import com.google.gson.Gson;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.service.IDingTalkService;
import com.ruoyi.common.dx.MessageSend;
import com.ruoyi.common.enums.MsgLSEnum;
import com.ruoyi.common.enums.ServiceFromEnum;
@@ -16,12 +18,17 @@
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;
import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
import com.smartor.common.LSHospTokenUtil;
import com.smartor.common.MtSubmitSmUtil;
import com.smartor.common.QwenLLMUtil;
@@ -104,6 +111,12 @@
    @Autowired
    private ISysConfigService configService;
    @Autowired
    private IDingTalkService dingTalkService;
    @Autowired
    private ISysUserService userService;
    @Value("${localIP}")
@@ -270,6 +283,80 @@
        }
    }
    /**
     * 定时发送异常信息-景宁
     */
    public void sendExceptInfo() throws Exception {
        //1.获取发送人工号
        ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO();
        subtaskDetailTraceVO.setHandleFlag("0");
        List<ServiceSubtaskDetailTrace> serviceSubtaskDetailTraces =
                serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO);
        // 获取“异常内容提醒"
        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("send.except.content");
        String exceptContent = sysConfig.getConfigValue();
        // 获取“内容提醒测试人工号”
        SysConfig testPersonConfig = sysConfigMapper.checkConfigKeyUnique("send.test.person");
        String testPerson = ObjectUtils.isNotEmpty(testPersonConfig) ? testPersonConfig.getConfigValue() : "";
        if(CollectionUtils.isNotEmpty(serviceSubtaskDetailTraces)){
            for (ServiceSubtaskDetailTrace serviceSubtaskDetailTrace : serviceSubtaskDetailTraces) {
                List<String> dutyDeptPersonCodeList = new ArrayList<>();
                        // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重)
                String dutyDeptPersonCodeStr = serviceSubtaskDetailTrace.getDutyDeptPersonCode();
                if(StringUtils.isNotEmpty(dutyDeptPersonCodeStr)){
                    String[] dutyDeptPersonCodeSplit = dutyDeptPersonCodeStr.split(",");
                    if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) {
                        dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(dutyDeptPersonCodeSplit));
                    } else {
                        dutyDeptPersonCodeList.addAll(Arrays.asList(dutyDeptPersonCodeSplit));
                    }
                }
                if (StringUtils.isNotBlank(testPerson)) {
                    String[] split = testPerson.split(",");
                    if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) {
                        dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(split));
                    } else {
                        dutyDeptPersonCodeList.addAll(Arrays.asList(split));
                    }
                }
                if (CollectionUtils.isNotEmpty(dutyDeptPersonCodeList)) {
                    for (String dutyDeptPersonCode : dutyDeptPersonCodeList) {
                        log.info("【sendExceptInfo】发送人工号:{}", dutyDeptPersonCode);
                        SysUser sysUser = new SysUser();
                        SysUser sysUserVo = new SysUser();
                        sysUserVo.setUserName(dutyDeptPersonCode);
                        sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid());
                        List<SysUser> users = userService.getUserList(sysUserVo);
                        String phoneNumber = "";
                        if(ObjectUtils.isNotEmpty(users)){
                            sysUser = users.get(0);
                            phoneNumber = sysUser.getPhonenumber();
                        }else {
                            continue;
                        }
                        if(StringUtils.isEmpty(phoneNumber)){
                            continue;
                        }
                        String dutyDeptPerson = dingTalkService.getDingTalkUserByMobile(phoneNumber);
                        if(StringUtils.isNotBlank(dutyDeptPerson)){
                            String context = TemplateUtils.render(exceptContent, serviceSubtaskDetailTrace);
                            dingTalkService.sendDingTalkText(context, dutyDeptPerson);
                            //更新状态
                            serviceSubtaskDetailTrace.setHandleFlag("1");
                            serviceSubtaskDetailTrace.setUpdateTime(new Date());
                            serviceSubtaskDetailTraceService.updateServiceSubtaskDetailTtrace(serviceSubtaskDetailTrace);
                        }
                    }
                }
            }
        }
    }
    /**
     * 定时处理外链的数据
@@ -395,7 +482,7 @@
                    iIcd10Service.queryAddIcd10();
                }
                // nhfy/sltd回溯30天分段采集判断(每段最多7天)
                // nhfy/sltd回溯30天分段采集判断(每段最多4天)
                boolean isBackfill = (active.trim().equals("nhfy") || active.trim().equals("sltd")) && StringUtils.isNotBlank(todayZeroStr);
                if (isBackfill) {
                    DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@@ -403,7 +490,7 @@
                    LocalDate segEndDate = todayZero.toLocalDate();
                    int segCount = 0;
                    while (!segStart.isAfter(segEndDate)) {
                        LocalDate segEnd = segStart.plusDays(6);
                        LocalDate segEnd = segStart.plusDays(3);
                        if (segEnd.isAfter(segEndDate)) {
                            segEnd = segEndDate;
                        }
@@ -547,96 +634,395 @@
    }
    /**
     * 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;
    }
    /**
@@ -1027,18 +1413,17 @@
                    serviceOutPath.setCreateTime(new Date());
                    serviceOutPath.setOrgid(serviceSubtask.getOrgid());
                    iServiceOutPathService.insertServiceOutPath(serviceOutPath);
                    String url = "";
                    String format = String.format("%03X", serviceOutPath.getId());
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    //先判断一下发的是不是宣教
                    if (!"3".equals(serviceSubtask.getType())) {
                        sendMagParam.setPhone(serviceSubtask.getPhone());
                        sendMagParam.setUrl(StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/wt?p=" + format : localIP + "/wt?p=" + format);
                        url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/wt?p=" + format : localIP + "/wt?p=" + format;
                        sendMagParam.setUrl(url);
                        //如果type是语音随访的话(说明补偿发送方式中有电话随访的方式,这里的外链就地址只能用/sf)
                        if (serviceSubtask.getType().equals("1"))
                            sendMagParam.setUrl(StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/sf?p=" + format : localIP + "/sf?p=" + format);
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/sf?p=" + format : localIP + "/sf?p=" + format;
                            sendMagParam.setUrl(url);
//                        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        SysConfig sysConfig = new SysConfig();
@@ -1058,7 +1443,9 @@
                        HeLibrary heLibrary = heLibraryMapper.selectHeLibraryById(serviceSubtask.getLibtemplateid());
                        if (heLibrary.getHetype().equals("1")) {
                            sendMagParam.setPhone(serviceSubtask.getPhone());
                            sendMagParam.setUrl(StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/xj?p=" + format : localIP + "/xj?p=" + format);
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/xj?p=" + format : localIP + "/xj?p=" + format;
                            sendMagParam.setUrl(url);
                            SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("xj.sms.template");
                            if (!Objects.isNull(sysConfig)) {
@@ -1076,6 +1463,11 @@
                        }
                    }
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    String isSuccess = null;
                    log.info("active的值为:{}", active);
                    if (active.equals("xh")) {
@@ -1249,6 +1641,7 @@
                    String format = String.format("%03X", serviceOutPath.getId());
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    //获取微信公众号请求信息根据机构ID
@@ -1771,48 +2164,65 @@
     * 处理超时随访任务
     * @return
     */
    public void dealSubtaskDeadline(Integer deadlineDay){
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay){
        //获取任务信息
        ServiceTask st = new ServiceTask();
        st.setDelFlag("0");
        st.setLongTask(1);
        st.setNexttaskflag("1");
        st.setOrgid(orgid);
        List<ServiceTask> serviceTasks = serviceTaskMapper.selectServiceTaskList(st);
        for (ServiceTask serviceTask : serviceTasks) {
            deadlineDay = -deadlineDay;
            Date deadline = DateUtils.addDays(new Date(), deadlineDay);
            //通过任务ID拿到患者信息,并且随访时间得是今天之前的
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setOrgid(orgid);
            serviceSubtaskVO.setSendstate(7L);
            //只扫描首次随访的
            serviceSubtaskVO.setVisitCount(1);
            serviceSubtaskVO.setTaskid(serviceTask.getTaskid());
            serviceSubtaskVO.setEndOutHospTime(deadline);
            List<ServiceSubtask> serviceSubtaskList
                    = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
            log.info("【dealSubtaskDeadline】获取到{}个超时任务, taskid={}", serviceSubtaskList.size(), serviceTask.getTaskid());
            for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
                PatMedInhosp patMedInhospVo = new PatMedInhosp();
                patMedInhospVo.setOrgid(serviceSubtask.getOrgid());
                patMedInhospVo.setInhospid(serviceSubtask.getInhospid());
                List<PatMedInhosp> patMedInhospList = iPatMedInhospService.selectPatMedInhospList(patMedInhospVo);
                PatMedInhosp patMedInhosp = new PatMedInhosp();
                if (!patMedInhospList.isEmpty()) {
                    patMedInhosp = patMedInhospList.get(0);
                }else {
                    continue;
                }
                if(ObjectUtils.isEmpty(serviceTask.getNexttaskid())){
                    continue;
                }
                ServiceTask nextServiceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceTask.getNexttaskid());
                PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
                String config = configService.selectConfigByKey("visit.early.day");
                Integer type = 1;
        Date deadline = DateUtils.addDays(new Date(), deadlineDay);
        //通过任务ID拿到患者信息,并且随访时间得是今天之前的
        ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
        serviceSubtaskVO.setSendstate(7L);
        serviceSubtaskVO.setVisitCount(1);
        serviceSubtaskVO.setEndOutHospTime(deadline);
        List<ServiceSubtask> serviceSubtaskList
                = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
        log.info("【dealSubtaskDeadline】获取到{}个超时任务", serviceSubtaskList.size());
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            PatMedInhosp patMedInhosp = new PatMedInhosp();
            patMedInhosp.setInhospid(serviceSubtask.getInhospid());
            List<PatMedInhosp> patMedInhospList = iPatMedInhospService.selectPatMedInhospList(patMedInhosp);
            if (patMedInhospList.size() > 0) {
                serviceSubtask.setIsVisitAgain(2);
                serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ";患者再入院了" : "患者再入院了");
                serviceSubtask.setSendstate(4L);
                serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                //return "无需再次随访,患者再入院了";
            }else {
                continue;
            }
            //新增再次随访任务
            ServiceSubtask serviceSubtaskAgain = DtoConversionUtils.sourceToTarget(serviceSubtask, ServiceSubtask.class);
            serviceSubtaskAgain.setId(null);
            serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
            serviceSubtaskAgain.setVisitCount(serviceSubtask.getVisitCount() + 1);
            serviceSubtaskAgain.setSendstate(2L);
            serviceSubtaskAgain.setCreateTime(new Date());
            serviceSubtaskAgain.setUpdateTime(new Date());
            serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
            serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
            serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
            int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain);
            if (i == 1) {
                //再次随访任务创建成功后,原任务改为失败状态
                serviceSubtask.setIsVisitAgain(2);
                serviceSubtask.setSendstate(5L);
                serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                //新增再次随访任务
                ServiceSubtask serviceSubtaskAgain = iPatMedInhospService.boxedServiceSubtask(nextServiceTask,
                        patMedInhosp, patArchive, config, type);
                serviceSubtaskAgain.setId(null);
                serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
                serviceSubtaskAgain.setVisitCount(serviceSubtask.getVisitCount() + 1);
                serviceSubtaskAgain.setSendstate(2L);
                serviceSubtaskAgain.setCreateTime(new Date());
                serviceSubtaskAgain.setUpdateTime(new Date());
                serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
                serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
                serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
                int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain);
                if (i == 1) {
                    //再次随访任务创建成功后,原任务改为失败状态
                    serviceSubtask.setSendstate(5L);
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                }
            }
        }
    }