liusheng
2026-10-10 0afac42fb2e9d6e5291304dd0044ce3042b45d2b
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -6,23 +6,32 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.dx.MessageSend;
import com.ruoyi.common.enums.MsgLSEnum;
import com.ruoyi.common.core.service.IDingTalkService;
import com.ruoyi.common.enums.ServiceFromEnum;
import com.ruoyi.common.enums.WxGZHEnum;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.message.SendResult;
import com.ruoyi.message.sms.SmsSendRequest;
import com.ruoyi.message.sms.SmsSenderFactory;
import com.ruoyi.message.sms.provider.xh.XinHuaSmsSender;
import com.ruoyi.message.wechat.WechatSendRequest;
import com.ruoyi.message.wechat.WechatSenderFactory;
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.smartor.common.LSHospTokenUtil;
import com.smartor.common.MtSubmitSmUtil;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.message.sms.provider.sltd.ws.MessageSend;
import com.smartor.common.QwenLLMUtil;
import com.smartor.domain.*;
import com.smartor.domain.VO.ServiceSubtaskDetailTraceVO;
@@ -41,17 +50,15 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static cn.hutool.core.convert.Convert.toHex;
/**
 * 定时任务调度测试
@@ -103,21 +110,15 @@
    @Autowired
    private ISysConfigService configService;
    @Autowired
    private IDingTalkService dingTalkService;
    @Autowired
    private ISysUserService userService;
    @Value("${localIP}")
    private String localIP;
    @Value("${xhsmsPath}")
    private String xhsmsPath;
    @Value("${xhsmsAccount}")
    private String xhsmsAccount;
    @Value("${xhsmsPwd}")
    private String xhsmsPwd;
    // 虚拟接入码
    @Value("${xhsmsjrm}")
    private String xhsmsjrm;
    @Value("${pub_key}")
    private String pub_key;
@@ -150,6 +151,9 @@
    IServiceSubtaskDetailTraceService serviceSubtaskDetailTraceService;
    @Autowired
    IPatMedOuthospService patMedOuthospService;
    @Autowired
    ISvyTaskTemplateService svyTaskTemplateService;
    @Autowired
@@ -174,13 +178,37 @@
    @Value("${server.port}")
    private String port;
    /**
     * 消息发送方工厂(短信/微信公众号)。
     * <p>
     * 原来短信与公众号的发送逻辑在本类里用 {@code if (active.equals("xx"))} 硬分支了约 300 行,
     * 现已收口到 {@code ruoyi-message} 模块,这里只负责按 {@code active} 路由。
     */
    @Autowired
    private MtSubmitSmUtil mtSubmitSmUtil;
    private SmsSenderFactory smsSenderFactory;
    @Autowired
    private WechatSenderFactory wechatSenderFactory;
    @Value("${spring.profiles.active}")
    private String active;
    @Autowired
    private SysConfigMapper sysConfigMapper;
    @Autowired
    private IServiceTaskScheduleService serviceTaskScheduleService;
    public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
        System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
    }
    public void ryParams(String params) {
        System.out.println("执行有参方法:" + params);
    }
    public void ryNoParams() {
        System.out.println("执行无参方法");
    }
    public void importData() {
        String filePath = "C:\\Users\\86176\\Desktop\\新建文本文档 4.txt"; // 替换为你的文件路径
@@ -250,6 +278,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);
                        }
                    }
                }
            }
        }
    }
    /**
     * 定时处理外链的数据
@@ -267,6 +369,17 @@
        LocalDateTime tomorrowEnd = tomorrowStart.plusDays(1).minusSeconds(1);     // 明天 23:59:59
        ixhGatherPatArchiveService.getAppointRecordInfo("2", tomorrowStart, tomorrowEnd);
    }
    /**
     * 每月15号和月底,查询并处理 HIS 数据(同德,南华)
     */
    public void dealHisDataHalfMonth() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedNow = now.format(formatter);
        dealHisData(formattedNow);
    }
    /**
     * HIS数据采集
@@ -364,7 +477,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");
@@ -372,7 +485,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;
                        }
@@ -405,7 +518,7 @@
                        list.add("FH0109.23");
                        list.add("FH0109.53");
                        serviceSLTDInhospReqVO.setStatusList(list);
                        log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO);
//                        log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“预入院”結束采集数据,院区:{}", cid);
@@ -423,7 +536,7 @@
                        List<String> listStr = new ArrayList<>();
                        listStr.add("FH0109.24");
                        serviceSLTDInhospReqVO.setStatusList(listStr);
                        log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO);
//                        log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“入院”結束采集数据,院区:{}", cid);
@@ -446,7 +559,7 @@
                        //预出院
                        list2.add("FH0109.25");
                        serviceSLTDInhospReqVO.setStatusList(list2);
                        log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO);
//                        log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“出院”束采集数据,院区:{}", cid);
                    }
@@ -467,7 +580,7 @@
     * @param endDateStr   日期结束 yyyy-MM-dd
     */
    private void collectSegmentData(Long cid, String startDateStr, String endDateStr) {
        // 预入院
//        // 预入院
        log.info("【dealHisData】预入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
@@ -480,7 +593,12 @@
        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);
        // 入院
@@ -494,7 +612,12 @@
        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);
        // 出院
@@ -511,101 +634,405 @@
        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;
    }
    /**
@@ -617,13 +1044,31 @@
        ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO();
        serviceSLTDInhospReqVO.setOrgId("20001001");
        Long outpPage = 1L;
        int totalCount = 0;  // 统计总共采集的药品数量
        while (true) {
            serviceSLTDInhospReqVO.setCurrent(outpPage);
            serviceSLTDInhospReqVO.setSize(OUTP_PAGE_SIZE);
            log.info("【dealHisData】省立同德“药品数据”采集第{}页数据", outpPage);
            Boolean b = serviceSLTDHealthcareRecordService.queryMedicationItemList(serviceSLTDInhospReqVO);
            log.info("【dealHisData】省立同德“药品数据”采集结果:{}", b);
            outpPage++;
            log.info("【dealHisData】省立同德\"药品数据\"采集第{}页数据", outpPage);
            try {
                Integer fetchedCount = serviceSLTDHealthcareRecordService.queryMedicationItemList(serviceSLTDInhospReqVO);
                log.info("【dealHisData】省立同德\"药品数据\"第{}页采集到{}条记录", outpPage, fetchedCount);
                totalCount += fetchedCount != null ? fetchedCount : 0;
                // 如果返回的数据量小于 pageSize,说明已经是最后一页了,退出循环
                if (fetchedCount == null || fetchedCount < OUTP_PAGE_SIZE) {
                    log.info("【dealHisData】省立同德\"药品数据\"采集完成,总页数:{}, 总计{}条记录", outpPage, totalCount);
                    break;
                }
                outpPage++;
            } catch (Exception e) {
                log.error("【dealHisData】省立同德\"药品数据\"第{}页采集失败", outpPage, e);
                // 出错时也可以选择退出或继续,这里选择退出避免无限重试
                break;
            }
        }
    }
@@ -668,7 +1113,7 @@
    public void dealHisData2() {
        //将患者信息、出入院数据全部采集
        // 指定的开始日期
        LocalDate endDate = LocalDate.of(2025, 3, 3);
        LocalDate endDate = LocalDate.of(2026, 7, 6);
        // 当前日期
        LocalDate currentDate = LocalDate.now();
        // 定义日期格式
@@ -699,17 +1144,17 @@
    }
    /**
     * 处理患者信息,进入子任务表
     * 处理患者信息,进入子任务表(任务组)
     */
    public void dealOutHospInfoByDimension() {
    public void dealOutHospInfoBySchedule() {
        String config = configService.selectConfigByKey("visit.early.day");
        if (port.equals("8095")) {
            //出院表
            iPatMedInhospService.dealOutHospInfoByDimension(config);
            iPatMedInhospService.dealOutHospInfoBySchedule(config);
            //门诊表(新华的暂时不做门诊随访)
            if (!active.equals("xh")) {
                //iPatMedOuthospService.dealOutpatientInfo();
            }
//            if (!active.equals("xh")) {
//                //iPatMedOuthospService.dealOutpatientInfo();
//            }
        }
    }
@@ -780,11 +1225,16 @@
     * 处理补偿任务
     */
    public void compensateTask() {
        //获取到sendstate=3 并且 visit_time为小于等于今天的subtask
        //获取到sendstate=3、1 并且 visit_time为小于等于今天的subtask
        List<Long> list = Arrays.asList(3L, 1L);
        ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
        serviceSubtaskVO.setSendstate(3L);
        serviceSubtaskVO.setVisitTime(new Date());
        List<ServiceSubtask> serviceSubtaskList = serviceSubtaskMapper.getCompensateServiceSubtaskList(serviceSubtaskVO);
        List<ServiceSubtask> serviceSubtaskList = new ArrayList<>();
        for (Long sendstate : list) {
            serviceSubtaskVO.setSendstate(sendstate);
            serviceSubtaskVO.setVisitTime(new Date());
            List<ServiceSubtask> compensateServiceSubtaskList = serviceSubtaskMapper.getCompensateServiceSubtaskList(serviceSubtaskVO);
            serviceSubtaskList.addAll(compensateServiceSubtaskList);
        }
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            //根据当前的执行方式,获取下一种执行方式
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
@@ -806,7 +1256,9 @@
                //2判断一下,当前的sort是不是等于需要执行的总个数,如果等于的话,说明是最后一个,直接将servuce_subtask的状态改成5,执行失败就行了
                Long cs = currentSort.get();
                if (cs.equals(serviceSubtaskPreachforms.size())) {
                if (cs.equals(serviceSubtaskPreachforms.size())
                        && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                    serviceSubtask.setSendstate(7L);
                    serviceSubtask.setRemark("处理补偿任务,当前处理最后补偿,全部执行失败(超时)");
@@ -971,18 +1423,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();
@@ -1002,7 +1453,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)) {
@@ -1020,124 +1473,16 @@
                        }
                    }
                    String isSuccess = null;
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    //短信发送:各站点对接方式差异较大(报文/签名/token/返回判定都不同),
                    //已收口到 ruoyi-message 模块,这里只负责组装入参并路由
                    log.info("active的值为:{}", active);
                    if (active.equals("xh")) {
                        Map<String, String> req = new HashMap<>();
//                        req.put("phone", sendMagParam.getPhone());
//                        req.put("content", sendMagParam.getContent());
                        Map<String, String> map2 = new HashMap<String, String>();
                        map2.put("action", "send");
                        map2.put("account", xhsmsAccount);
                        map2.put("password", xhsmsPwd);
                        map2.put("mobile", sendMagParam.getPhone());
                        map2.put("content", sendMagParam.getContent());
                        map2.put("extno", xhsmsjrm);
                        map2.put("rt", "json");
                        StringBuilder encodedParams = new StringBuilder();
                        for (Map.Entry<String, String> entry : map2.entrySet()) {
                            if (encodedParams.length() > 0) {
                                encodedParams.append("&");
                            }
                            // 对键和值进行URL编码
                            String encodedKey = URLEncoder.encode(entry.getKey(), "UTF-8");
                            String encodedValue = URLEncoder.encode(entry.getValue(), "UTF-8");
                            encodedParams.append(encodedKey).append("=").append(encodedValue);
                        }
                        Boolean result = HttpUtils.sendPost(xhsmsPath, new Gson().toJson(req));
                        if (result) isSuccess = "true";
                    } else if (active.equals("ls")) {
//                        String url = configService.selectConfigByKey("token.360", serviceSubtask.getOrgid());
//                        //丽水的短信发送方式
//                        Map<String, String> map = MsgLSEnum.getAddressByCode(serviceSubtask.getOrgid());
                        SysConfig config = new SysConfig();
                        config.setConfigKey("ls.msg.info");
                        config.setOrgid(serviceSubtask.getOrgid());
                        List<SysConfig> sysConfigs = configService.selectConfigList(config);
                        Map<String, String> msgInfo = new HashMap<>();
                        if (CollectionUtils.isNotEmpty(sysConfigs)) {
                            String configValue = sysConfigs.get(0).getConfigValue();
                            ObjectMapper objectMapper = new ObjectMapper();
                            if (StringUtils.isNotEmpty(configValue)) {
                                msgInfo = objectMapper.readValue(configValue, Map.class);
                            }
                        }
                        log.info("-----------map的值为:{}", msgInfo);
                        String token = LSHospTokenUtil.getToken(serviceSubtask.getOrgid(), msgInfo.get("tokenUrl"));
                        if (ObjectUtils.isNotEmpty(msgInfo) && StringUtils.isEmpty(msgInfo.get("address"))) {
                            setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送失败,该机构没有配置短信地址, 时间:" + now, "5");
                            return;
                        }
                        log.info("【sfHandlle】丽水短信发送,地址:{},参数:{},机构ID:{}", msgInfo.get("address"), sendMagParam, serviceSubtask.getOrgid());
                        String dxCode = getDXCode(msgInfo.get("address"), sendMagParam.getPhone(), sendMagParam.getContent(), msgInfo.get("sendPersonId"), msgInfo.get("sendPersonName"), MsgLSEnum.getHeaderByCode(msgInfo), token);
                        log.info("【sfHandlle】丽水短信发送结果:{}", dxCode);
                        ObjectMapper objectMapper = new ObjectMapper();
                        Map<String, Object> textParam = objectMapper.readValue(dxCode, Map.class);
                        String code = textParam.get("Code").toString();
                        if (code.equals("0")) {
                            isSuccess = "true";
                        }
                    } else if (active.equals("sltd") || active.equals("nhfy")) {
                        log.info("sltd进来了吗?{}   ,---sendMagParam.getContent()的参数为:{}", active, sendMagParam.getContent());
                        if (active.equals("sltd")) {
                            //省立同德的短信发送方式
                            String data = MessageSend.sendMsg(sendMagParam.getContent(), "6", sendMagParam.getPhone(), LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
                            if (StringUtils.isNotEmpty(data)) {
                                // 解析 result=0&description=成功&faillist= 格式(result=0成功,1失败)
                                log.info("【sfHandlle】省立同德短信发送返回:{}", data);
                                String[] pairs = data.split("&");
                                String resultVal = null;
                                for (String pair : pairs) {
                                    String[] kv = pair.split("=", 2);
                                    if ("result".equals(kv[0])) {
                                        resultVal = kv.length > 1 ? kv[1] : null;
                                        break;
                                    }
                                }
                                if ("0".equals(resultVal)) {
                                    isSuccess = "true";
                                }
                            }
                        } else if (active.equals("nhfy")) {
                            Map<String, Object> smsResult = mtSubmitSmUtil.submitSm(sendMagParam.getPhone(), sendMagParam.getContent());
                            if (smsResult != null && smsResult.get("result") != null && smsResult.get("result").toString().equals("0")) {
                                isSuccess = "true";
                            }
                        }
                    } else if (active.equals("hzszlyy")) {//市一医院
                        String content = sendMagParam.getContent().replace("【新华医院】", "");
                        SysDept sysDeptVo = new SysDept();
                        List<SysDept> sysDepts = new ArrayList<>();
                        if (ObjectUtils.isNotEmpty(serviceSubtask.getDeptcode())) {
                            sysDeptVo.setDeptType("1");
                            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
                            sysDeptVo.setDeptCode(serviceSubtask.getDeptcode());
                            sysDeptService.selectDeptList(sysDeptVo);
                        } else if (ObjectUtils.isNotEmpty(serviceSubtask.getLeavehospitaldistrictcode())) {
                            sysDeptVo.setDeptType("2");
                            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
                            sysDeptVo.setDeptCode(serviceSubtask.getLeavehospitaldistrictcode());
                            sysDeptService.selectDeptList(sysDeptVo);
                        }
                        SysDept sysDept = CollectionUtils.isNotEmpty(sysDepts) ? sysDepts.get(0) : new SysDept();
                        String campusid = ObjectUtils.isNotEmpty(sysDept) ? sysDept.getCampusid() : "2";
                        //湖滨单独发送方式
                        if ("1".equals(campusid)) {
                            String result = smsUtils.sendSms1(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            //这里需要根据result返回的值去判断,是否成功,然后将值赋值给isSuccess
                            if (StringUtils.isNotEmpty(result) && result.contains("处理成功")) {
                                isSuccess = "true";
                            }
                        } else {
                            String result = smsUtils.sendSms(xhsmsPath, xhsmsAccount, xhsmsPwd, sendMagParam.getPhone(), content);
                            //这里需要根据result返回的值去判断,是否成功,然后将值赋值给isSuccess
                            if (StringUtils.isNotEmpty(result) && result.contains("处理成功")) {
                                isSuccess = "true";
                            }
                        }
                    }
                    SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam);
                    String isSuccess = smsSendResult.asLegacyFlag();
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
@@ -1193,31 +1538,14 @@
                    String format = String.format("%03X", serviceOutPath.getId());
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    //获取微信公众号请求信息根据机构ID
                    List<String> wxqqxx = WxGZHEnum.getDescByCode(patArchive.getOrgid());
                    //市一医院 不校验
                    //微信公众号发送:市一与新华是两套完全不同的网关与判定标准,
                    //机构配置校验、HIS患者ID校验都已收口到 ruoyi-message 的发送方实现里
                    SendResult wechatSendResult = sendWechatByActive(serviceSubtask, patArchive, url);
                    Boolean wxCodeSuccess = wechatSendResult.isOk();
                    if (CollectionUtils.isEmpty(wxqqxx) || wxqqxx.size() < 4) {
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】该机构的公众号配置信息不全,无法通过公众号发送", "4");
                        return;
                    }
                    if (StringUtils.isEmpty(patArchive.getPatidHis())) {
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送失败,his系统的患者id为空", "5");
                        return;
                    }
                    String wxCode = "";
                    //市一医院
                    if (active.equals("hzszlyy")) {
                        //处理中文乱码问题
                        wxCode = smsUtils.sendChat(url, patArchive.getTelcode(), serviceSubtask.getSfzh());
                    } else {
                        wxCode = getWXCode(serviceSubtask.getSfzh(), url, serviceSubtask.getTaskName(), serviceSubtask.getTaskDesc(), patArchive.getTelcode(), serviceSubtask.getSendname(), patArchive.getPatidHis(), wxqqxx);
                    }
                    Map map = JSONObject.parseObject(wxCode, Map.class);
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
@@ -1226,22 +1554,7 @@
                    serviceSubtaskRecord.setPreachform("4");
                    serviceSubtaskRecord.setStartTime(System.currentTimeMillis());
                    serviceSubtaskRecord.setResult("success");
                    Boolean wxCodeSuccess = false;
                    if (active.equals("hzszlyy")) {
                        if (!map.isEmpty() && map.get("success") != null) {
                            Integer wxCodeSuccessShiyi = (Integer) map.get("success");
                            if (ObjectUtils.isNotEmpty(wxCodeSuccessShiyi) && wxCodeSuccessShiyi == 1) {
                                wxCodeSuccess = true;
                            } else {
                                wxCodeSuccess = false;
                            }
                        }
                    } else {
                        if (!map.isEmpty() && map.get("succ") != null) {
                            wxCodeSuccess = (boolean) map.get("succ");
                        }
                    }
                    if (!map.isEmpty() && !wxCodeSuccess) serviceSubtaskRecord.setResult("error");
                    if (!wxCodeSuccess) serviceSubtaskRecord.setResult("error");
                    serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
                    //发送完成后,修改状态
@@ -1250,7 +1563,7 @@
                        serviceSubtask.setResult("success");
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送成功", "2");
                    } else {
                        log.error("【sfHandlle】公众号发送失败,患者信息:{},错误信息:{}", serviceSubtask, map.get("msg"));
                        log.error("【sfHandlle】公众号发送失败,患者信息:{},错误信息:{}", serviceSubtask, wechatSendResult.getMessage());
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送失败", "5");
                    }
                } catch (Exception e) {
@@ -1263,6 +1576,72 @@
            setSuccessPreachForm(serviceSubtask, sendPreachform, "人工发送成功", "2");
        }
    }
    /**
     * 按 {@code active}(站点)路由到对应的短信发送方。
     * <p>
     * 各站点差异见 {@code ruoyi-message} 模块各实现类;本方法只做「入参组装 + 路由 + 日志」。
     * <p>
     * ⚠️ 新华({@code xh})站点仍走「旧实现(发送空报文)」,与原 {@code RyTask} 行为一字不差,
     * 确认网关报文格式后应改为 {@code XinHuaSmsSender#sendForm}。详见该类的 TODO 说明。
     */
    private SendResult sendSmsByActive(ServiceSubtask serviceSubtask, SendMagParam sendMagParam) {
        SmsSendRequest request = SmsSendRequest.of(sendMagParam.getPhone(), sendMagParam.getContent(), serviceSubtask.getOrgid());
        //市一区分湖滨/吴山两套短信签名
        if ("hzszlyy".equals(active)) {
            request.setCampus(resolveShiyiCampus(serviceSubtask));
        }
        if ("xh".equals(active)) {
            XinHuaSmsSender xinHuaSmsSender = (XinHuaSmsSender) smsSenderFactory.get(active);
            return xinHuaSmsSender.sendJsonLegacy(request);
        }
        return smsSenderFactory.send(active, request);
    }
    /**
     * 按 {@code active}(站点)路由到对应的微信公众号发送方。
     * <p>
     * ⚠️ 新华公众号必须传患者信息(姓名 + HIS 患者ID),与原
     * {@code getWXCode(...)} 的入参保持一一对应。
     */
    private SendResult sendWechatByActive(ServiceSubtask serviceSubtask, PatArchive patArchive, String url) {
        WechatSendRequest request = new WechatSendRequest();
        request.setOrgid(patArchive.getOrgid());
        request.setIdCard(serviceSubtask.getSfzh());
        request.setUrl(url);
        request.setTitle(serviceSubtask.getTaskName());
        request.setContent(serviceSubtask.getTaskDesc());
        request.setPhone(patArchive.getTelcode());
        request.setPatientName(serviceSubtask.getSendname());
        request.setPatidHis(patArchive.getPatidHis());
        return wechatSenderFactory.send(active, request);
    }
    /**
     * 查询患者所在院区(1=湖滨,其他=吴山)。
     * <p>
     * TODO 待确认:原实现并未把 {@code sysDeptService.selectDeptList(...)} 的返回值赋给
     * {@code sysDepts},因此 {@code sysDepts} 恒为空列表、{@code campusid} 恒为 {@code "2"},
     * 湖滨院区实际上永远走不到 {@code sendSms1}。本次重构<b>保持该行为不变</b>,
     * 请在业务确认后再决定是否修成 {@code sysDepts = sysDeptService.selectDeptList(...)}。
     */
    private String resolveShiyiCampus(ServiceSubtask serviceSubtask) {
        SysDept sysDeptVo = new SysDept();
        List<SysDept> sysDepts = new ArrayList<>();
        if (ObjectUtils.isNotEmpty(serviceSubtask.getDeptcode())) {
            sysDeptVo.setDeptType("1");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(serviceSubtask.getDeptcode());
            sysDeptService.selectDeptList(sysDeptVo);
        } else if (ObjectUtils.isNotEmpty(serviceSubtask.getLeavehospitaldistrictcode())) {
            sysDeptVo.setDeptType("2");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(serviceSubtask.getLeavehospitaldistrictcode());
            sysDeptService.selectDeptList(sysDeptVo);
        }
        SysDept sysDept = CollectionUtils.isNotEmpty(sysDepts) ? sysDepts.get(0) : new SysDept();
        return ObjectUtils.isNotEmpty(sysDept) ? sysDept.getCampusid() : "2";
    }
@@ -1321,15 +1700,15 @@
                serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                return "-1";
            }
            // 当sort等于列表size时,并且所有sendstate都不等于9,则返回-1
            if (currentSort.get().equals((long) serviceSubtaskPreachforms.size())) {
            if (currentSort.get().equals((long) serviceSubtaskPreachforms.size())
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (allSendStateNot1AndNot9) {
                    //说明全发送失败了
                    serviceSubtask.setSendstate(7L);
                    serviceSubtask.setFinishtime(null);
                    log.error("getSendPreachform方法 检查是否所有sendstate不等于9, 全失败了");
                    serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有发送方式均未获取结果,任务失败(超时)" : "所有发送方式均未获取结果,任务失败(超时)");
                    serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有执行方式均未获取结果,任务失败(超时)" : "所有执行方式均未获取结果,任务失败(超时)");
                    serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
                    //修改发送状态
                    setFail(serviceSubtaskPreachforms, serviceSubtask);
@@ -1399,74 +1778,10 @@
        return 0;
    }
    private String getDXCode(String address, String ShouJiHM, String FaSongNR, String FaSongRID, String FaSongRXM, Map<String, Object> headerMap, String token) {
        Map<String, Object> map = new LinkedHashMap<>();
        Map<String, Object> YeWuXX = new LinkedHashMap<>();
        Map<String, Object> DuanXinXX = new LinkedHashMap<>();
        DuanXinXX.put("ShouJiHM", ShouJiHM);
        DuanXinXX.put("FaSongNR", FaSongNR);
        DuanXinXX.put("FaSongRID", FaSongRID);
        DuanXinXX.put("FaSongRXM", FaSongRXM);
        YeWuXX.put("DuanXinXX", DuanXinXX);
        map.put("XiaoXiTou", headerMap);
        map.put("YeWuXX", YeWuXX);
        String body = new Gson().toJson(map);
//        String result = HttpUtils.sendPost(address, body);
        String result = null;
        try {
            result = OkHttpExample.sendPostRequest(address, body, token);
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info("【getDXCode】短信发送结果:{}", result);
        JSONObject jsonObject = JSONObject.parseObject(result);
        String code = (String) jsonObject.toString();
        return code;
    }
    private String getWXCode(String idcard, String url, String taskName, String taskDesc, String phone, String setPatientname, String setPatientid, List<String> wxqqxx) {
        XinHuaWXReq xinHuaWXReq = new XinHuaWXReq();
        xinHuaWXReq.setIdcard(idcard);
        xinHuaWXReq.setUrl(url);
        xinHuaWXReq.setPatientid(setPatientid);
        xinHuaWXReq.setPatientname(setPatientname);
        xinHuaWXReq.setMobile(phone);
        xinHuaWXReq.setMedcardno(null);
        xinHuaWXReq.setTitlename(taskName);
        xinHuaWXReq.setContent(taskDesc);
        xinHuaWXReq.setAppid(wxqqxx.get(1));
        log.info("【getWXCode】微信公众号请求参数:{}", xinHuaWXReq);
        String body = new Gson().toJson(xinHuaWXReq);
        String encode = encode(wxqqxx.get(2).concat(body));
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("sign", encode);
        String result = HttpUtils.sendPostByHeader(wxqqxx.get(3), body, headerMap);
        log.info("【getWXCode】微信公众号返回参数:{}", result);
        JSONObject jsonObject = JSONObject.parseObject(result);
        String code = (String) jsonObject.toString();
        return code;
    }
    public String encode(String arg) {
        if (arg == null) {
            arg = "";
        }
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            md5.update(arg.getBytes("UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return toHex(md5.digest());
    }
    // ---------------------------------------------------------------------
    // 说明:原 getDXCode / getWXCode / encode 三个私有方法已随发送方一起收口到
    // ruoyi-message 模块(LishuiSmsSender / XinHuaMpApi),此处已删除,避免重复实现。
    // ---------------------------------------------------------------------
    public void collectHIS() {
        try {
@@ -1476,6 +1791,47 @@
            log.info("【dealHisData】HIS结束采集数据");
        } catch (Exception e) {
            log.error("【dealHisData】HIS数据采集异常", e);
        }
    }
    public void collectHISYesterday() {
        try {
            // 获取昨天0点到今天0点的时间范围
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime yesterdayZero = todayZero.minusDays(1);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String startTime = yesterdayZero.format(formatter);
            String endTime = todayZero.format(formatter);
            // HIS数据采集
            log.info("【collectHISYesterday】HIS开始采集数据");
            // 立即返回,后台异步执行
            CompletableFuture.runAsync(() -> {
                ichService.syncMedInhosp(startTime, endTime);
            });
            log.info("【collectHISYesterday】HIS结束采集数据");
        } catch (Exception e) {
            log.error("【collectHISYesterday】HIS数据采集异常", e);
        }
    }
    public void collectHISDaysBefore(Long daysBefore) {
        try {
            // 获取daysBefore 0点到daysBefore-1 0点的时间范围
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime daysBeforeZeroStart = todayZero.minusDays(daysBefore);
            LocalDateTime daysBeforeZeroEnd = todayZero.minusDays(daysBefore-1L);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String startTime = daysBeforeZeroStart.format(formatter);
            String endTime = daysBeforeZeroEnd.format(formatter);
            // HIS数据采集
            log.info("【collectHISDaysBefore】HIS开始采集数据: startTime:{}, endtime:{}",startTime, endTime);
            // 立即返回,后台异步执行
            CompletableFuture.runAsync(() -> {
                ichService.syncMedInhosp(startTime, endTime);
            });
            log.info("【collectHISDaysBefore】HIS结束采集数据: startTime:{}, endtime:{}",startTime, endTime);
        } catch (Exception e) {
            log.error("【collectHISDaysBefore】HIS数据采集异常", e);
        }
    }
@@ -1507,7 +1863,7 @@
//        //人工的未发送状态,需要查2
//        if (preachform.equals("1") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2)
//            serviceSubtaskPreachform.setSendstate("2");
        log.error("-----serviceSubtaskPreachform的入参是?{}", serviceSubtaskPreachform);
        List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
        if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) {
            ServiceSubtaskPreachform serviceSubtaskPreachform1 = serviceSubtaskPreachforms.get(0);
@@ -1520,7 +1876,8 @@
            serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform1);
            //如果当前的preachform已经是最后一个了,那直接将serviceSubtask的sendstate状态改成7就行了(全超时了)
            if (serviceSubtaskPreachform1.getSort() == sspCount.size()) {
            if (serviceSubtaskPreachform1.getSort() == sspCount.size()
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (failSendstate.equals("4") || failSendstate.equals("5")) {
                    serviceSubtask.setCurrentPreachform(preachform);
                    serviceSubtask.setSendstate(7L);
@@ -1598,4 +1955,142 @@
        int result = QwenLLMUtil.matchRegex(questionText, voiceText, value, regexText);
        return result;
    }
    /**
     * 任务组(appltype=5)首轮纳入:扫描新出院患者,按 schedule 绑定的维度匹配任务组,
     * 为命中的患者按计划明细一次性生成第 0 轮 N 条 subtask,并置 schedulecheck_flag=1。
     * 后续轮次由 {@link #rollTaskGroupCycle()} 负责。
     * <p>
     * 与主扫描 dealOutHospInfo 分离,互不干扰。建议每 30 分钟执行一次。
     */
    public void dealTaskGroupOutHosp() {
        log.info("【dealTaskGroupOutHosp】开始扫描任务组新出院患者");
        if (!port.equals("8095")) {
            return;
        }
        try {
            String config = configService.selectConfigByKey("visit.early.day");
            iPatMedInhospService.dealOutHospInfoBySchedule(config);
            log.info("【dealTaskGroupOutHosp】任务组首轮纳入扫描完成");
        } catch (Exception e) {
            log.error("【dealTaskGroupOutHosp】任务组扫描异常", e);
        }
    }
    /**
     * 任务组(appltype=5)循环任务滚动调度。
     * <p>
     * 扫描所有 schedule_style=1 的计划,若本轮 subtask 全部 finalize(sendstate ∉ 1/2/3),
     * 则克隆本轮患者到下一轮,推进 current_loop_count 与 visit_time。
     * <p>
     * 建议每小时或每半小时执行一次,避免轮次积压。
     */
    public void rollTaskGroupCycle() {
        log.info("【rollTaskGroupCycle】开始扫描循环任务组");
        try {
            int rolled = serviceTaskScheduleService.rollCycleTasks();
            log.info("【rollTaskGroupCycle】本次推进 {} 个计划", rolled);
        } catch (Exception e) {
            log.error("【rollTaskGroupCycle】滚动调度异常", e);
        }
    }
    /**
     * 随访任务是否超时
     * @param serviceSubtask
     * @param serviceSubtaskPreachforms
     * @return
     */
    public boolean isSubtaskDeadline(ServiceSubtask serviceSubtask, List<ServiceSubtaskPreachform> serviceSubtaskPreachforms){
        if (ObjectUtils.isNotEmpty(serviceSubtask)) {
            //只有出院随访和专病随访有超时
            if(StringUtils.isNotEmpty(serviceSubtask.getServiceType()) &&
                    !serviceSubtask.getServiceType().equals("2") && !serviceSubtask.getServiceType().equals("13")){
                return true;
            }
        }
        if (Objects.isNull(serviceSubtask) || CollectionUtils.isEmpty(serviceSubtaskPreachforms)) {
            return false;
        }
        // 基准时刻:出院时间 + 发送日(sendDay 缺省按 1),并兜底避免 NPE/脏数据
        ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        int sendDay = serviceTask != null && serviceTask.getSendDay() != null ? serviceTask.getSendDay().intValue() : 1;
        Date deadline = DateUtils.addDays(serviceSubtask.getEndtime(), sendDay);
        int sumCompensateTime = CollectionUtils.isEmpty(serviceSubtaskPreachforms) ? 0 : serviceSubtaskPreachforms.stream()
                .mapToInt(x -> x.getCompensateTime() == null ? 0 : Integer.parseInt(x.getCompensateTime()))
                .sum();
        sumCompensateTime = sumCompensateTime + 1;
        // 只补偿最后一步自身的补偿时间(天),脏值一律按 0 兜底
        deadline = DateUtils.addDays(deadline, sumCompensateTime);
        // 出院(发送)时间 + 本步补偿时间 小于当前时间 -> 超时
        return deadline.getTime() < new Date().getTime();
    }
    /**
     * 处理超时随访任务
     * @return
     */
    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;
                //新增再次随访任务
                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);
                }
            }
        }
    }
}