陈昶聿
7 天以前 599beddc80e7e50b2099bacdbf7df2d89df00ef2
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -14,6 +14,7 @@
import com.ruoyi.common.enums.ServiceFromEnum;
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;
@@ -32,14 +33,15 @@
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.message.sms.provider.sltd.ws.MessageSend;
import com.smartor.common.QwenLLMUtil;
import com.smartor.common.SpecificArchiveConstants;
import com.smartor.domain.*;
import com.smartor.domain.VO.ServiceSubtaskDetailTraceVO;
import com.smartor.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.smartor.mapper.*;
import com.smartor.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -62,6 +64,33 @@
import com.ruoyi.template.domain.HeLibrary;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
import com.ruoyi.template.api.TemplateFacade;
import com.ruoyi.common.core.domain.entity.PatArchive;
import com.ruoyi.task.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.ServiceSubtask;
import com.ruoyi.task.domain.ServiceSubtaskRecord;
import com.ruoyi.task.domain.ServiceTask;
import com.ruoyi.task.domain.ServiceTaskSchedule;
import com.ruoyi.task.domain.ServiceTaskScheduleDetail;
import com.ruoyi.task.mapper.ServiceSubtaskMapper;
import com.ruoyi.task.mapper.ServiceTaskMapper;
import com.ruoyi.task.mapper.ServiceTaskScheduleMapper;
import com.ruoyi.task.mapper.ServiceTaskScheduleDetailMapper;
import com.ruoyi.task.domain.ServiceSubtaskDetailTrace;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.ruoyi.task.service.IServiceSubtaskDetailTraceService;
import com.ruoyi.task.domain.ServiceOutPath;
import com.ruoyi.task.service.IServiceOutPathService;
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
import com.ruoyi.task.service.IServiceSubtaskService;
import com.ruoyi.task.domain.SendMagParam;
import com.ruoyi.task.service.IServiceSubtaskRecordService;
import com.ruoyi.task.mapper.ServiceSubtaskPreachformMapper;
import com.ruoyi.patarchive.service.IPatArchiveService;
import com.ruoyi.datacollection.service.IXHGatherPatArchiveService;
import com.ruoyi.datacollection.service.IServiceSLTDHealthcareRecordService;
import com.ruoyi.datacollection.domain.ServiceSLTDInhospReqVO;
import com.ruoyi.datacollection.domain.ServiceSLTDInhospResDTO;
/**
 * 定时任务调度测试
 *
@@ -104,8 +133,11 @@
    @Autowired
    private IServicePatientTempService iServicePatientTempService;
    @Autowired
    private IHNGatherPatArchiveService ihnGatherPatArchiveService;
    // ⚠️ 2026-10 移除:原 `IHNGatherPatArchiveService ihnGatherPatArchiveService` 字段。
    //    它在本类里**只**被 dealHisData 的河南分支使用(死字段检查:改前全文件仅 1 处使用),
    //    该分支已随方法体搬进 ruoyi-datacollection,故字段连带删除 ——
    //    留着它会让人误以为 smartor 的河南采集还有本类这个使用方
    //    (真正的使用方已是数据采集模块经契约 HisGatherApi 调用的 Bridge)。
    @Autowired
    private IServiceSLTDHealthcareRecordService serviceSLTDHealthcareRecordService;
@@ -141,13 +173,20 @@
    private ServiceSubtaskMapper serviceSubtaskMapper;
    @Autowired
    private ServiceTaskScheduleMapper serviceTaskScheduleMapper;
    @Autowired
    private ServiceTaskScheduleDetailMapper serviceTaskScheduleDetailMapper;
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private IServiceSubtaskRecordService serviceSubtaskRecordService;
    @Autowired
    IIcd10Service iIcd10Service;
    // ⚠️ 2026-10 移除:原 `IIcd10Service iIcd10Service` 字段。
    //    它在本类里**只**被 dealHisData 的"疾病字典增量采集"一行调用,已随方法体搬进
    //    ruoyi-datacollection(经契约 HisGatherApi#gatherIcd10 调回 smartor 的实现)。
    @Autowired
    IPatArchiveService patArchiveService;
@@ -213,8 +252,27 @@
    @Autowired
    private com.ruoyi.publicity.api.PublicityFacade publicityFacade;
    @Value("${appid}")
    private String appid;
    /**
     * 数据采集模块的<b>常驻门面</b>。
     * <p>
     * ⚠️ 2026-10 模块化:本类的 {@code dealHisData(String)} 方法体(连同私有帮手
     * {@code collectSegmentData})已整体搬进 {@code ruoyi-datacollection},
     * 本类只保留定时任务入口(sys_job 的 invoke_target 是 {@code ryTask.dealHisData('...')},
     * 入口名不能变)。站点分支、时间窗口、分段回溯、日志文案都在模块里,一行未改。
     * <p>
     * 注入门面而不是模块内部 Service:门面<b>永远存在</b>,且只有门面判
     * {@code sys_config: datacollection.enabled} 开关。
     */
    @Autowired
    private com.ruoyi.datacollection.api.DataCollectionFacade dataCollectionFacade;
    // ⚠️ 2026-10 删除:原 `@Value("${appid}") private String appid;`
    //    它是「新华微信公众号推送」的遗留字段(提交 8590ef6a 引入,只被
    //    xinHuaWXReq.setAppid(appid) 用过)。该推送逻辑已搬进 ruoyi-message
    //    (公众号凭据改由 sys_config.wx.mp.config.* 下发),本类里已无引用;
    //    但它的 @Value 没有默认值,而 application-nhfy.yml / -xh.yml / -ls.yml
    //    都没定义 appid 这个 key,这些 profile 启动时会直接报
    //    `Could not resolve placeholder 'appid'`。留着是定时炸弹,故删除。
    @Value("${server.port}")
    private String port;
@@ -330,8 +388,7 @@
        //1.获取发送人工号
        ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO();
        subtaskDetailTraceVO.setHandleFlag("0");
        List<ServiceSubtaskDetailTrace> serviceSubtaskDetailTraces =
                serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO);
        List<ServiceSubtaskDetailTrace> serviceSubtaskDetailTraces = serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO);
        // 获取“异常内容提醒"
        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("send.except.content");
        String exceptContent = sysConfig.getConfigValue();
@@ -339,12 +396,12 @@
        // 获取“内容提醒测试人工号”
        SysConfig testPersonConfig = sysConfigMapper.checkConfigKeyUnique("send.test.person");
        String testPerson = ObjectUtils.isNotEmpty(testPersonConfig) ? testPersonConfig.getConfigValue() : "";
        if(CollectionUtils.isNotEmpty(serviceSubtaskDetailTraces)){
        if (CollectionUtils.isNotEmpty(serviceSubtaskDetailTraces)) {
            for (ServiceSubtaskDetailTrace serviceSubtaskDetailTrace : serviceSubtaskDetailTraces) {
                List<String> dutyDeptPersonCodeList = new ArrayList<>();
                        // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重)
                // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重)
                String dutyDeptPersonCodeStr = serviceSubtaskDetailTrace.getDutyDeptPersonCode();
                if(StringUtils.isNotEmpty(dutyDeptPersonCodeStr)){
                if (StringUtils.isNotEmpty(dutyDeptPersonCodeStr)) {
                    String[] dutyDeptPersonCodeSplit = dutyDeptPersonCodeStr.split(",");
                    if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) {
                        dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(dutyDeptPersonCodeSplit));
@@ -372,18 +429,18 @@
                        sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid());
                        List<SysUser> users = userService.getUserList(sysUserVo);
                        String phoneNumber = "";
                        if(ObjectUtils.isNotEmpty(users)){
                        if (ObjectUtils.isNotEmpty(users)) {
                            sysUser = users.get(0);
                            phoneNumber = sysUser.getPhonenumber();
                        }else {
                        } else {
                            continue;
                        }
                        if(StringUtils.isEmpty(phoneNumber)){
                        if (StringUtils.isEmpty(phoneNumber)) {
                            continue;
                        }
                        String dutyDeptPerson = dingTalkService.getDingTalkUserByMobile(phoneNumber);
                        if(StringUtils.isNotBlank(dutyDeptPerson)){
                        if (StringUtils.isNotBlank(dutyDeptPerson)) {
                            String context = TemplateUtils.render(exceptContent, serviceSubtaskDetailTrace);
                            dingTalkService.sendDingTalkText(context, dutyDeptPerson);
                            //更新状态
@@ -428,262 +485,53 @@
     * HIS数据采集
     * 定时任务:采集患者信息、出院、门诊数据
     * todayZeroStr  当前的时间   例如:2026-05-06 00:00:00
     * <p>
     * ⚠️ <b>2026-10 模块化</b>:方法体(连同私有帮手 {@code collectSegmentData})已整体搬进
     * {@code ruoyi-datacollection},实现见
     * {@code com.ruoyi.datacollection.service.impl.HisDataCollectionServiceImpl}。
     * 这里只保留<b>定时任务入口</b>:sys_job 的 invoke_target 仍是
     * {@code ryTask.dealHisData('...')},入口名不能变,所以本方法是<b>纯转发</b>。
     * <p>
     * 关闭数据采集模块({@code sys_config: datacollection.enabled=false})时门面会抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException};
     * 本入口捕获后只打一条 WARN —— 那是"模块被主动关掉",不是采集失败,
     * 不该让定时任务记成执行异常(否则运维会看到一片红色失败记录)。
     */
    public void dealHisData(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);
        }
        LocalDateTime yesterdayZero = todayZero.minusDays(1);
        log.info("【dealHisData】采集时间范围:{} ~ {}", yesterdayZero, todayZero);
        if (active.trim().equals("xh")) {
            try {
                // 采集入院数据
                log.info("【dealHisData】新华开始采集入院数据");
                ixhGatherPatArchiveService.getInHospDataGather("0", yesterdayZero, todayZero);
                log.info("【dealHisData】新华入院数据采集完成");
                // 采集出院数据
                log.info("【dealHisData】新华开始采集出院数据");
                ixhGatherPatArchiveService.getInHospDataGather("1", yesterdayZero, todayZero);
                log.info("【dealHisData】新华出院数据采集完成");
            } catch (Exception e) {
                log.error("【dealHisData】新华数据采集异常", e);
            }
        } else if (active.trim().equals("hn")) {
            try {
                // 河南数据采集
                HnDataGatherVO hnDataGatherVO = new HnDataGatherVO();
                log.info("【dealHisData】河南开始采集数据");
                Date startTime = Date.from(yesterdayZero.atZone(ZoneId.systemDefault()).toInstant());
                Date endTime = Date.from(todayZero.atZone(ZoneId.systemDefault()).toInstant());
                hnDataGatherVO.setStartTime(startTime);
                hnDataGatherVO.setEndTime(endTime);
                ihnGatherPatArchiveService.hnDataGather(hnDataGatherVO);
                log.info("【dealHisData】河南结束采集数据");
            } catch (Exception e) {
                log.error("【dealHisData】河南数据采集异常", e);
            }
        } else if (active.trim().equals("sltd") || active.trim().equals("nhfy")) {
            //省立同德与华南附一用同一套采集方法(都是来未来提供)
            try {
                //获取是否需要采集用户、部门信息
                SysConfig config = new SysConfig();
                config.setConfigKey("sltd.gather.sysuser");
                config.setOrgid("20001001");
                List<SysConfig> sysConfigs = configService.selectConfigList(config);
                //获取省立同德要采集的院区
                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;
                }
                //configValue = 1 不需要采集       configValue = 2需要采集
                if (CollectionUtils.isNotEmpty(sysConfigs) && sysConfigs.get(0).getConfigValue().equals("2")) {
                    log.info("【dealHisData】省立同德“部门信息”开始采集数据");
                    ServiceSLTDDeptReqVO serviceSLTDDeptReqVO = new ServiceSLTDDeptReqVO();
                    serviceSLTDDeptReqVO.setOrgId("20001001");
                    serviceSLTDDeptReqVO.setCampusIds(campusidList);
                    Boolean aBoolean1 = serviceSLTDHealthcareRecordService.queryDeptWardAreaInfoList(serviceSLTDDeptReqVO);
                    log.info("【dealHisData】省立同德“部门信息”结束采集数据:{}", aBoolean1);
                    Boolean aBoolean = false;
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德“用户信息”开始采集数据,院区:{}", cid);
                        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
                        reqVO.setOrgId("20001001");
                        reqVO.setCampusId(cid);
                        ArrayList<String> statusList = new ArrayList<>();
                        statusList.add("1");
                        reqVO.setStatusList(statusList);
                        aBoolean = serviceSLTDHealthcareRecordService.queryHospUserInfoList(reqVO);
                    }
                    log.info("【dealHisData】省立同德“用户信息”结束采集数据:{}", aBoolean);
                    //疾病字典采集
                    iIcd10Service.queryAddIcd10();
                }
                // 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");
                    LocalDate segStart = todayZero.toLocalDate().minusDays(30);
                    LocalDate segEndDate = todayZero.toLocalDate();
                    int segCount = 0;
                    while (!segStart.isAfter(segEndDate)) {
                        LocalDate segEnd = segStart.plusDays(3);
                        if (segEnd.isAfter(segEndDate)) {
                            segEnd = segEndDate;
                        }
                        segCount++;
                        log.info("【dealHisData】{}回溯第{}段采集,日期范围:{} ~ {}", active.trim(), segCount, segStart.format(dateFmt), segEnd.format(dateFmt));
                        for (Long cid : campusidList) {
                            collectSegmentData(cid, segStart.format(dateFmt), segEnd.format(dateFmt));
                        }
                        segStart = segEnd.plusDays(1);
                    }
                    log.info("【dealHisData】{}回溯采集完成,共{}段", active.trim(), segCount);
                }
                // 省立同德数据采集(非回溯模式才执行)
                if (!isBackfill) {
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德预入院开始采集数据,院区:{}", cid);
                        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);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setEncounterTimeStart(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setEncounterTimeEnd(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> list = new ArrayList<>();
                        list.add("FH0109.22");
                        list.add("FH0109.23");
                        list.add("FH0109.53");
                        serviceSLTDInhospReqVO.setStatusList(list);
//                        log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“预入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“入院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setOrgId("20001001");
                        serviceSLTDInhospReqVO.setCampusId(cid);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setStartHeadTime(nowTime);
                        serviceSLTDInhospReqVO.setStartTailTime(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setStartHeadTime(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setStartTailTime(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> listStr = new ArrayList<>();
                        listStr.add("FH0109.24");
                        serviceSLTDInhospReqVO.setStatusList(listStr);
//                        log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“出院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setStartHeadTime(null);
                        serviceSLTDInhospReqVO.setStartTailTime(null);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(nowTime);
                        serviceSLTDInhospReqVO.setPreOutHospitalTailDate(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setPreOutHospitalTailDate(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        serviceSLTDInhospReqVO.setHealthcareRecordTypeList(null);
                        List<String> list2 = new ArrayList<>();
//                    list2.add("FH0108.02");
                        //出院
                        list2.add("FH0109.27");
                        //预出院
                        list2.add("FH0109.25");
                        serviceSLTDInhospReqVO.setStatusList(list2);
//                        log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“出院”束采集数据,院区:{}", cid);
                    }
                }
            } catch (Exception e) {
                log.error("【dealHisData】省立同德数据采集异常", e);
            }
        try {
            dataCollectionFacade.dealHisData(todayZeroStr);
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【dealHisData】{}(本轮定时采集跳过)", e.getMessage());
        }
    }
    /**
     * 分段采集数据(预入院、入院、出院)
     * 用于nhfy/sltd回溯采集,每次最多查询7天数据
     * 在院患者"科室 / 病区 / 床位"刷新(南华附一,每天一次)。
     * <p>
     * 在院患者会转科、转病区、换床,而 {@code pat_med_inhosp} 的位置字段只在入院时写过一次,
     * 之后不会自己更新 —— 这里每天按患者查一遍 HIS 里<b>该患者当前的在院信息</b>
     * ({@code dtcQueryHealthcareRecordList},与入院采集是同一个接口,按
     * {@code healthcareRecordNo} 单查 + {@code statusList=["FH0109.24"]}),
     * 与库内比对后只把变化的字段写回去。实现与判定口径见
     * {@code com.ruoyi.datacollection.service.impl.InhospDeptRefreshServiceImpl}。
     * <p>
     * 与 {@link #dealHisData(String)} 同样的降级口径:模块被关掉(
     * {@code sys_config: datacollection.enabled=false})时门面抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException},
     * 这里只打一条 WARN —— 那是"模块被主动关掉",不该记成定时任务执行异常。
     * <p>
     * sys_job 的 invoke_target 为 {@code ryTask.refreshInhospDeptInfo()},
     * 建议每天 03:30(避开 01:00 入院采集与 02:00 出院处理),并发策略选"禁止并发"。
     *
     * @param cid          院区ID
     * @param startDateStr 日期开始 yyyy-MM-dd
     * @param endDateStr   日期结束 yyyy-MM-dd
     * @return 本次实际更新的在院记录条数(0 = 无变化 / 站点不匹配 / 模块被关闭)
     */
    private void collectSegmentData(Long cid, String startDateStr, String endDateStr) {
//        // 预入院
        log.info("【dealHisData】预入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setEncounterTimeStart(startDateStr);
        reqVO.setEncounterTimeEnd(endDateStr);
        List<String> preAdmitStatus = new ArrayList<>();
        preAdmitStatus.add("FH0109.22");
        preAdmitStatus.add("FH0109.23");
        preAdmitStatus.add("FH0109.53");
        reqVO.setStatusList(preAdmitStatus);
        log.info("【dealHisData】预入院采集数据入参:{}", reqVO);
    public int refreshInhospDeptInfo() {
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】预入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
            return dataCollectionFacade.refreshInhospDeptInfo();
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【refreshInhospDeptInfo】{}(本轮在院信息刷新跳过)", e.getMessage());
            return 0;
        }
        log.info("【dealHisData】预入院終了采集数据,院区:{}", cid);
        // 入院
        log.info("【dealHisData】入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setStartHeadTime(startDateStr);
        reqVO.setStartTailTime(endDateStr);
        List<String> admitStatus = new ArrayList<>();
        admitStatus.add("FH0109.24");
        reqVO.setStatusList(admitStatus);
        log.info("【dealHisData】入院采集数据入参:{}", reqVO);
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】入院終了采集数据,院区:{}", cid);
        // 出院
        log.info("【dealHisData】出院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setPreOutHospitalHeadDate(startDateStr);
        reqVO.setPreOutHospitalTailDate(endDateStr);
        List<String> dischargeStatus = new ArrayList<>();
        //出院
        dischargeStatus.add("FH0109.27");
        //预出院
        dischargeStatus.add("FH0109.25");
        reqVO.setStatusList(dischargeStatus);
        log.info("【dealHisData】出院采集数据入参:{}", reqVO);
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // 单类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】出院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】出院结束采集数据,院区:{}", cid);
    }
    /**
@@ -738,8 +586,7 @@
            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());
        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) {
@@ -762,15 +609,13 @@
                redisCache.deleteObject(lockKey);
            }
        }
        log.info("【dealHisDataMZ】门急诊采集本轮结束,耗时:{}秒",
                java.time.Duration.between(now, LocalDateTime.now()).getSeconds());
        log.info("【dealHisDataMZ】门急诊采集本轮结束,耗时:{}秒", java.time.Duration.between(now, LocalDateTime.now()).getSeconds());
    }
    /**
     * 采集某个院区:先补一次性回溯日期,再做昨天的收尾采集,最后整轮刷新今天
     */
    private void collectOutpatientDaily(Long cid, String backfillParam, long pageSize, long maxPage,
                                        LocalDateTime deadline) {
    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();
@@ -797,8 +642,7 @@
        // 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);
            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;
@@ -811,8 +655,7 @@
        // 2) 昨天收尾采集:每天一次,兜住 23:xx 之后才写进 HIS 的记录和跨零点的数据
        if (!yesterday.toString().equals(cursor.finalDay)) {
            MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.FINAL, yesterday,
                    cursor.finalPage, pageSize, maxPage, deadline);
            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;
@@ -828,8 +671,7 @@
            cursor.page = 1L;
            saveMzDayCursor(cursorKey, cursor);
        }
        MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.TODAY, today,
                cursor.page, pageSize, maxPage, deadline);
        MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.TODAY, today, cursor.page, pageSize, maxPage, deadline);
        if (status == MzGatherStatusEnum.DONE) {
            // 整轮刷新完成,页码归位,下一轮重新从第一页整轮刷新
            cursor.page = 1L;
@@ -844,9 +686,7 @@
     * @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) {
    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);
@@ -877,8 +717,7 @@
                // 本轮有跳过的页时,断点回到第一个失败页,保证下一轮先把这些页补回来
                long resumePage = (skippedPages > 0 && firstFailPage > 0) ? firstFailPage : page;
                saveMzDayPage(cursorKey, cursor, slot, resumePage);
                log.warn("【dealHisDataMZ】本轮时间预算用尽,{} 本轮已采{}页{}条{},下次从第{}页继续,院区:{}",
                        day, pageCount, total, skippedLog(skippedPages), resumePage, cid);
                log.warn("【dealHisDataMZ】本轮时间预算用尽,{} 本轮已采{}页{}条{},下次从第{}页继续,院区:{}", day, pageCount, total, skippedLog(skippedPages), resumePage, cid);
                return MzGatherStatusEnum.BUDGET_UP;
            }
            reqVO.setCurrent(page);
@@ -912,13 +751,10 @@
                    firstFailPage = page;
                }
                consecutiveFail++;
                log.error("【dealHisDataMZ】接口层采集失败,已记 error 日志并跳过该页,本轮继续往后采;"
                                + "院区:{},日期:{},第{}页,接口入参:{},异常:{}",
                        cid, day, page, new Gson().toJson(reqVO), lastError.getMessage(), lastError);
                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);
                    log.error("【dealHisDataMZ】连续{}页接口失败,判定来未来接口不可用,本轮提前结束;院区:{},日期:{},断点保留在第{}页", consecutiveFail, cid, day, firstFailPage);
                    saveMzDayPage(cursorKey, cursor, slot, firstFailPage);
                    return MzGatherStatusEnum.FAILED;
                }
@@ -928,8 +764,7 @@
            consecutiveFail = 0;
            if (CollectionUtils.isEmpty(pageData)) {
                log.info("【dealHisDataMZ】{} 第{}页无数据,采集结束,院区:{},本轮{}页共{}条{}",
                        day, page, cid, pageCount, total, skippedLog(skippedPages));
                log.info("【dealHisDataMZ】{} 第{}页无数据,采集结束,院区:{},本轮{}页共{}条{}", day, page, cid, pageCount, total, skippedLog(skippedPages));
                return MzGatherStatusEnum.DONE;
            }
            pageCount++;
@@ -949,7 +784,9 @@
        }
    }
    /** 按槽位保存中断页码 */
    /**
     * 按槽位保存中断页码
     */
    private void saveMzDayPage(String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, long page) {
        if (slot == MzGatherSlotEnum.FINAL) {
            cursor.finalPage = page;
@@ -961,12 +798,16 @@
        saveMzDayCursor(cursorKey, cursor);
    }
    /** 日志里提示接口层失败跳过了几页(下一轮整轮刷新会重新采到) */
    /**
     * 日志里提示接口层失败跳过了几页(下一轮整轮刷新会重新采到)
     */
    private String skippedLog(long skippedPages) {
        return skippedPages > 0L ? ",接口失败跳过" + skippedPages + "页(下一轮整轮刷新会重新采到)" : "";
    }
    /** 读取 sys_config 里的门急诊采集参数(机构 orgid=20001001),取不到或超范围时用枚举里的默认值 */
    /**
     * 读取 sys_config 里的门急诊采集参数(机构 orgid=20001001),取不到或超范围时用枚举里的默认值
     */
    private long getMzConfigLong(MzGatherParamEnum param) {
        try {
            SysConfig config = new SysConfig();
@@ -978,8 +819,7 @@
                if (value >= param.getMin() && value <= param.getMax()) {
                    return value;
                }
                log.warn("【dealHisDataMZ】配置{}的值{}超出范围[{},{}],使用默认值{}",
                        param.getKey(), value, param.getMin(), param.getMax(), param.getDefaultValue());
                log.warn("【dealHisDataMZ】配置{}的值{}超出范围[{},{}],使用默认值{}", param.getKey(), value, param.getMin(), param.getMax(), param.getDefaultValue());
            }
        } catch (Exception e) {
            log.warn("【dealHisDataMZ】读取配置{}失败,使用默认值{}", param.getKey(), param.getDefaultValue(), e);
@@ -987,7 +827,9 @@
        return param.getDefaultValue();
    }
    /** 解析任务入参(要一次性回溯的日期),支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd */
    /**
     * 解析任务入参(要一次性回溯的日期),支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd
     */
    private LocalDateTime parseMzStartTime(String text) {
        LocalDateTime time = parseMzTimeQuiet(text);
        if (time == null && StringUtils.isNotBlank(text)) {
@@ -996,7 +838,9 @@
        return time;
    }
    /** 静默解析日期,支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd,格式不符返回 null(不刷日志) */
    /**
     * 静默解析日期,支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd,格式不符返回 null(不刷日志)
     */
    private LocalDateTime parseMzTimeQuiet(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
@@ -1014,7 +858,9 @@
        }
    }
    /** 保存门急诊采集断点(各轮次中断的页码) */
    /**
     * 保存门急诊采集断点(各轮次中断的页码)
     */
    private void saveMzDayCursor(String cursorKey, MzDayCursor cursor) {
        JSONObject json = new JSONObject();
        json.put("day", cursor.day);
@@ -1028,7 +874,9 @@
        redisCache.setCacheObject(cursorKey, json.toJSONString(), MzGatherConstant.CACHE_TTL_HOURS, TimeUnit.HOURS);
    }
    /** 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新) */
    /**
     * 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新)
     */
    private MzDayCursor loadMzDayCursor(String cursorKey) {
        MzDayCursor cursor = new MzDayCursor();
        try {
@@ -1065,16 +913,24 @@
     * (页码为 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;
    }
@@ -1208,8 +1064,10 @@
     * 处理患者信息,进入子任务表(任务组)
     * <p>
     * ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 {@link #dealOutHospInfo()})。
     * 现走模块门面。注意模块侧的 {@code dealOutHospInfoBySchedule} 内部仍会转发回 smartor
     * ({@code OtherVisitApiBridge},见该类注释),但<b>开关在门面这一层判</b>,所以关闭时不会走到那里。
     * 现走模块门面。模块侧 {@code dealOutHospInfoBySchedule} 经 {@code OtherVisitApi} 契约
     * 回调 {@code ruoyi-task/api/contract/OtherVisitApiImpl}(2026-11 起实现整簇搬进 task 模块,
     * smartor 侧的 {@code OtherVisitApiBridge} 已删除),但<b>开关在门面这一层判</b>,
     * 所以关闭时不会走到那里。
     */
    public void dealOutHospInfoBySchedule() {
        String config = configService.selectConfigByKey("visit.early.day");
@@ -1304,6 +1162,13 @@
            serviceSubtaskList.addAll(compensateServiceSubtaskList);
        }
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            //currentPreachform为空,说明该子任务还没有执行过任何一种发送方式(sendstate=1,属于首次补偿),
            //这里直接交给sfHandlle处理,它会按sort=1的第一种发送方式发送,与getSendPreachform中的处理保持一致
            if (StringUtils.isEmpty(serviceSubtask.getCurrentPreachform())) {
                log.warn("【compensateTask】subid:{},currentPreachform为空,按第一种发送方式补偿处理", serviceSubtask.getId());
                sfHandlle(serviceSubtask);
                continue;
            }
            //根据当前的执行方式,获取下一种执行方式
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
            serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
@@ -1314,6 +1179,25 @@
            Optional<Long> currentSort = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getSort).findFirst();
            Optional<Long> id = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getId).findFirst();
            if (currentSort.isPresent()) {
                // 方案B:只处理当前子任务——公众号已领取(service_subtask.sendstate=1 + current_preachform=5)
                // 且未填写时,按默认选项完成,避免进入下一种发送方式或最后一步超时。
                // 先用内存字段快速过滤,其他医院/任务/发送方式不会进入下面的默认完成逻辑。
                if (IServiceSubtaskService.WECHAT_DEFAULT_COMPLETE_ORGID.equals(serviceSubtask.getOrgid())
                        && "2".equals(serviceSubtask.getType())
                        && "5".equals(serviceSubtask.getCurrentPreachform())
                        && Long.valueOf(1L).equals(serviceSubtask.getSendstate())
                        && !Long.valueOf(1L).equals(serviceSubtask.getSubmit())) {
                    try {
                        if (iServiceSubtaskService.completeWechatClaimedByDefault(serviceSubtask)) {
                            log.info("【compensateTask】subid:{},微信公众号已领取未填写,按默认选项完成", serviceSubtask.getId());
                            continue;
                        }
                    } catch (Exception e) {
                        // 单个子任务失败不影响其他任务,回滚后继续原补偿流程
                        log.error("【compensateTask】微信公众号默认完成失败,subid:{},继续原补偿流程", serviceSubtask.getId(), e);
                    }
                }
                //1先检查一下,是不是有执行状态是完成的(怕之前已经有完的了,没有将servuce_subtask的状态改成功,这里再检查一下)
                boolean finishState = serviceSubtaskPreachforms.stream().allMatch(item -> item.getSendstate().equals("9"));
                if (finishState) {
@@ -1325,8 +1209,7 @@
                //2判断一下,当前的sort是不是等于需要执行的总个数,如果等于的话,说明是最后一个,直接将servuce_subtask的状态改成5,执行失败就行了
                Long cs = currentSort.get();
                if (cs.equals(serviceSubtaskPreachforms.size())
                        && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (cs.equals(serviceSubtaskPreachforms.size()) && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                    serviceSubtask.setSendstate(7L);
                    serviceSubtask.setRemark("处理补偿任务,当前处理最后补偿,全部执行失败(超时)");
@@ -1376,6 +1259,24 @@
    }
    /**
     * 按机构查询配置值(同一 key 在不同机构下可配置不同值)
     *
     * @param configKey 配置key
     * @param orgid     机构ID
     * @return 配置值,未配置返回 null
     */
    private String getConfigValue(String configKey, String orgid) {
        SysConfig query = new SysConfig();
        query.setConfigKey(configKey);
        query.setOrgid(orgid);
        List<SysConfig> sysConfigs = configService.selectConfigList(query);
        if (CollectionUtils.isNotEmpty(sysConfigs)) {
            return sysConfigs.get(0).getConfigValue();
        }
        return null;
    }
    /**
     * 随访-补偿处理
     *
     * @param serviceSubtask
@@ -1389,6 +1290,7 @@
        serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
        //获取发送方式
        String sendPreachform = getSendPreachform(serviceSubtaskPreachform, serviceSubtask);
        log.info("sendPreachform的发送方式是啥:{}", sendPreachform);
        if (sendPreachform.equals("-1") || serviceSubtask.getSendstate() == 6 || serviceSubtask.getSendstate() == 5 || serviceSubtask.getSendstate() == 4) {
            //说明已经全部遍历完了,当前的执行方式序号是最后一个了   或者  该患者已经随访结束(成功或失败了或不执行)
            log.error("说明已经全部遍历完了,当前的执行方式序号是最后一个了   或者  该患者已经随访结束(成功或失败了或不执行)");
@@ -1396,6 +1298,7 @@
        }
        //先判断患者是不是还要发送
        PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
        log.info("patArchive的值为:{}", patArchive);
        if (!Objects.isNull(patArchive) && patArchive.getNotrequiredFlag() == "1") {
            //不需要发送
            serviceSubtask.setResult("患者不需要随访");
@@ -1419,8 +1322,7 @@
        // 在开关恢复后永远丢失。保持 sendstate 不动,开关打开后下一次 longTaskSend/compensateTask
        // 会照常把它捞出来发出。
        if ("3".equals(serviceSubtask.getType()) && !publicityFacade.isEnabled()) {
            log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),跳过宣教发送。subtaskId={}, patid={}",
                    serviceSubtask.getId(), serviceSubtask.getPatid());
            log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),跳过宣教发送。subtaskId={}, patid={}", serviceSubtask.getId(), serviceSubtask.getPatid());
            return;
        }
@@ -1507,8 +1409,9 @@
                    serviceOutPath.setOrgid(serviceSubtask.getOrgid());
                    iServiceOutPathService.insertServiceOutPath(serviceOutPath);
                    String url = "";
                    String format = String.format("%03X", serviceOutPath.getId());
                    String format = ShortCode.encode(serviceOutPath.getId());
                    //先判断一下发的是不是宣教
                    log.info("serviceSubtask.getType的值为:{}", serviceSubtask.getType());
                    if (!"3".equals(serviceSubtask.getType())) {
                        sendMagParam.setPhone(serviceSubtask.getPhone());
                        url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/wt?p=" + format : localIP + "/wt?p=" + format;
@@ -1516,7 +1419,7 @@
                        //如果type是语音随访的话(说明补偿发送方式中有电话随访的方式,这里的外链就地址只能用/sf)
                        if (serviceSubtask.getType().equals("1"))
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/sf?p=" + format : localIP + "/sf?p=" + format;
                            sendMagParam.setUrl(url);
                        sendMagParam.setUrl(url);
//                        SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        SysConfig sysConfig = new SysConfig();
@@ -1527,7 +1430,8 @@
                        } else {
                            sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        }
                        if (!Objects.isNull(sysConfig)) {
                        log.info("sysConfig的值为:{}", sysConfig);
                        if (!Objects.isNull(sysConfig) && StringUtils.isNotEmpty(sysConfig.getConfigValue())) {
                            String smsContemt = sysConfig.getConfigValue();
//                            sendMagParam.setContent("您好,邀请您填写出院调查表,请点击" + sendMagParam.getUrl() + "填写。感谢您配合!");
                            sendMagParam.setContent(smsContemt.replace("${wjurl}", sendMagParam.getUrl()));
@@ -1547,7 +1451,7 @@
                            sendMagParam.setUrl(url);
                            SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("xj.sms.template");
                            if (!Objects.isNull(sysConfig)) {
                            if (!Objects.isNull(sysConfig) && StringUtils.isNotEmpty(sysConfig.getConfigValue())) {
                                String smsContemt = sysConfig.getConfigValue();
//                              sendMagParam.setContent("您好,邀请您填写出院宣教调查表,请点击" + sendMagParam.getUrl() + "查看。感谢您配合!");
                                sendMagParam.setContent(smsContemt.replace("${xjurl}", sendMagParam.getUrl()));
@@ -1563,6 +1467,13 @@
                    }
                    //内容为空说明短信模板未配置或宣教/通知内容为空,直接按发送失败处理,避免后续发送时出现空指针
                    if (StringUtils.isEmpty(sendMagParam.getContent())) {
                        log.error("【sfHandlle】长期任务短信内容为空,无法发送,subid:{},taskid:{},type:{},机构ID:{}", serviceSubtask.getId(), serviceSubtask.getTaskid(), serviceSubtask.getType(), serviceSubtask.getOrgid());
                        setSuccessPreachForm(serviceSubtask, sendPreachform, "长期任务短信发送失败:短信内容为空(短信模板未配置或内容为空), 时间: " + now, "5");
                        return;
                    }
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
@@ -1570,8 +1481,24 @@
                    //短信发送:各站点对接方式差异较大(报文/签名/token/返回判定都不同),
                    //已收口到 ruoyi-message 模块,这里只负责组装入参并路由
                    log.info("active的值为:{}", active);
                    SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam);
                    String isSuccess = smsSendResult.asLegacyFlag();
                    String isSuccess = "";
                    if ("ls".equals(active) && "47226079133110211G1001".equals(serviceSubtask.getOrgid())) {
                        //丽水妇幼走中国移动云MAS,不依赖 ls.msg.info 与 token;
                        //ruoyi-message 的 ls 通道尚未覆盖云MAS,故此处保留 SmsUtils 直连
                        SmsUtils.YunMasConfig yunMasConfig = SmsUtils.YunMasConfig.of(getConfigValue("ls.msg.yunmas", serviceSubtask.getOrgid()));
                        SmsUtils.SmsResult yunMasSmsResult = SmsUtils.sendYunMasSms(yunMasConfig, sendMagParam.getPhone(), sendMagParam.getContent());
                        log.info("【sfHandlle】云MAS短信发送结果:ok={},rspcod={},msgGroup={},message={}",
                                yunMasSmsResult.isOk(), yunMasSmsResult.getRspcod(), yunMasSmsResult.getMsgGroup(), yunMasSmsResult.getMessage());
                        isSuccess = yunMasSmsResult.isOk() ? "true" : "false";
                        if (!yunMasSmsResult.isOk()) {
                            log.error("【sfHandlle】云MAS短信发送失败,subid:{},手机号:{},原因:{}",
                                    serviceSubtask.getId(), sendMagParam.getPhone(), yunMasSmsResult.getMessage());
                        }
                    } else {
                        SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam);
                        log.info("smsSendResult的值为:{}", smsSendResult);
                        isSuccess = smsSendResult.asLegacyFlag();
                    }
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
@@ -1586,6 +1513,7 @@
                    serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
                    //发送完成后,修改状态
                    log.info("serviceSubtaskRecord的值为:{}", serviceSubtaskRecord);
                    serviceSubtask.setSendstate(3L);
                    if (StringUtils.isNotEmpty(isSuccess) && isSuccess.equals("true")) {
                        serviceSubtask.setResult("success");
@@ -1624,7 +1552,7 @@
                    serviceOutPath.setCreateTime(new Date());
                    serviceOutPath.setOrgid(serviceSubtask.getOrgid());
                    iServiceOutPathService.insertServiceOutPath(serviceOutPath);
                    String format = String.format("%03X", serviceOutPath.getId());
                    String format = ShortCode.encode(serviceOutPath.getId());
                    serviceOutPath.setRadix(format);
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
@@ -1638,6 +1566,7 @@
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
                    serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString());
                    serviceSubtaskRecord.setSubtaskId(serviceSubtask.getId());
                    serviceSubtaskRecord.setUuid(UUID.randomUUID().toString());
                    serviceSubtaskRecord.setTasktype(serviceSubtask.getType());
                    serviceSubtaskRecord.setPreachform("4");
@@ -1718,10 +1647,10 @@
    private String resolveShiyiCampus(ServiceSubtask serviceSubtask) {
        SysDept sysDeptVo = new SysDept();
        List<SysDept> sysDepts = new ArrayList<>();
        if (ObjectUtils.isNotEmpty(serviceSubtask.getDeptcode())) {
        if (ObjectUtils.isNotEmpty(serviceSubtask.getLeaveldeptcode())) {
            sysDeptVo.setDeptType("1");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(serviceSubtask.getDeptcode());
            sysDeptVo.setDeptCode(serviceSubtask.getLeaveldeptcode());
            sysDeptService.selectDeptList(sysDeptVo);
        } else if (ObjectUtils.isNotEmpty(serviceSubtask.getLeavehospitaldistrictcode())) {
            sysDeptVo.setDeptType("2");
@@ -1789,8 +1718,7 @@
                return "-1";
            }
            // 当sort等于列表size时,并且所有sendstate都不等于9,则返回-1
            if (currentSort.get().equals((long) serviceSubtaskPreachforms.size())
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
            if (currentSort.get().equals((long) serviceSubtaskPreachforms.size()) && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (allSendStateNot1AndNot9) {
                    //说明全发送失败了
                    serviceSubtask.setSendstate(7L);
@@ -1882,6 +1810,25 @@
        }
    }
    /**
     * 采集丽水市妇幼保健院门诊接诊完成记录,并按配置生成妇科/儿科档案。
     * 定时任务默认采集昨天 00:00:00 ~ 今天 00:00:00。
     */
    public void collectMchArchive() {
        try {
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime yesterdayZero = todayZero.minusDays(1);
            Date startTime = Date.from(yesterdayZero.atZone(ZoneId.systemDefault()).toInstant());
            Date endTime = Date.from(todayZero.atZone(ZoneId.systemDefault()).toInstant());
            log.info("【collectMchArchive】开始采集丽水妇幼门诊接诊完成记录");
            ichService.collectMchOuthospAndGenerateArchive(
                    SpecificArchiveConstants.LISHUI_MCH_ORG_ID, startTime, endTime);
            log.info("【collectMchArchive】丽水妇幼门诊接诊完成记录采集结束");
        } catch (Exception e) {
            log.error("【collectMchArchive】丽水妇幼门诊接诊完成记录采集异常", e);
        }
    }
    public void collectHISYesterday() {
        try {
            // 获取昨天0点到今天0点的时间范围
@@ -1907,23 +1854,24 @@
            // 获取daysBefore 0点到daysBefore-1 0点的时间范围
            LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
            LocalDateTime daysBeforeZeroStart = todayZero.minusDays(daysBefore);
            LocalDateTime daysBeforeZeroEnd = todayZero.minusDays(daysBefore-1L);
            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);
            log.info("【collectHISDaysBefore】HIS开始采集数据: startTime:{}, endtime:{}", startTime, endTime);
            // 立即返回,后台异步执行
            CompletableFuture.runAsync(() -> {
                ichService.syncMedInhosp(startTime, endTime);
            });
            log.info("【collectHISDaysBefore】HIS结束采集数据: startTime:{}, endtime:{}",startTime, endTime);
            log.info("【collectHISDaysBefore】HIS结束采集数据: startTime:{}, endtime:{}", startTime, endTime);
        } catch (Exception e) {
            log.error("【collectHISDaysBefore】HIS数据采集异常", e);
        }
    }
    public Boolean setSuccessPreachForm(ServiceSubtask serviceSubtask, String preachform, String remark, String failSendstate) {
        log.info("setSuccessPreachForm的入参为:serviceSubtask = {},  preachform = {},  remark = {}, failSendstate = {}", serviceSubtask, preachform, remark, failSendstate);
        if (preachform.equals("-1")) {
            //说明都不需要执行了
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
@@ -1941,7 +1889,7 @@
        serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid());
        serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid());
        List<ServiceSubtaskPreachform> sspCount = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
        log.info("sspCount的值为:{}", sspCount);
        serviceSubtaskPreachform.setPreachform(preachform);
        //1:未发送状态
        serviceSubtaskPreachform.setSendstate("1");
@@ -1951,7 +1899,7 @@
//        //人工的未发送状态,需要查2
//        if (preachform.equals("1") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2)
//            serviceSubtaskPreachform.setSendstate("2");
        log.error("-----serviceSubtaskPreachform的入参是?{}", serviceSubtaskPreachform);
        log.info("-----serviceSubtaskPreachform的入参是?{}", serviceSubtaskPreachform);
        List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
        if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) {
            ServiceSubtaskPreachform serviceSubtaskPreachform1 = serviceSubtaskPreachforms.get(0);
@@ -1962,10 +1910,9 @@
            }
            serviceSubtaskPreachform1.setRemark(remark);
            serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform1);
            log.info("---------serviceSubtaskPreachform1的入参是 :  {}", serviceSubtaskPreachform1);
            //如果当前的preachform已经是最后一个了,那直接将serviceSubtask的sendstate状态改成7就行了(全超时了)
            if (serviceSubtaskPreachform1.getSort() == sspCount.size()
                    && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
            if (serviceSubtaskPreachform1.getSort() == sspCount.size() && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) {
                if (failSendstate.equals("4") || failSendstate.equals("5")) {
                    serviceSubtask.setCurrentPreachform(preachform);
                    serviceSubtask.setSendstate(7L);
@@ -1980,7 +1927,7 @@
            log.error("【定时任务中该患者没有查询到属于他的发送方式,subid:{},prechform:{},orgid:{}】", serviceSubtask.getId(), preachform, serviceSubtask.getOrgid());
            return false;
        }
        log.info("到这里了不????");
        //将下一次的随访时间与当前执行方式记录一下
        serviceSubtask.setVisitTime(getNextVisitTime(serviceSubtask.getId(), serviceSubtask.getTaskid(), serviceSubtask.getVisitTime()));
        serviceSubtask.setCurrentPreachform(preachform);
@@ -1993,6 +1940,7 @@
            }
        }
        serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + remark : remark);
        log.info("serviceSubtask的入参是:{}",serviceSubtask);
        serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
        return true;
    }
@@ -2005,14 +1953,15 @@
     * @return
     */
    private String getObject(ServiceSubtask serviceSubtask, String scriptContent) {
        //内容为空时直接返回,避免下面的replace出现空指针
        if (StringUtils.isEmpty(scriptContent)) {
            return scriptContent;
        }
        Map<String, Map<String, String>> param = getParam(serviceSubtask.getTaskid());
        for (Map<String, String> map : param.values()) {
            for (String key : map.keySet()) {
                scriptContent = scriptContent.replace(key, StringUtils.isNotEmpty(map.get(key)) ? map.get(key) : "");
            }
        }
        if (StringUtils.isEmpty(scriptContent)) {
            return scriptContent;
        }
        scriptContent = scriptContent.replace("${name}", StringUtils.isNotEmpty(serviceSubtask.getSendname()) ? serviceSubtask.getSendname() : "");
        scriptContent = scriptContent.replace("${dzz}", StringUtils.isNotEmpty(serviceSubtask.getAddr()) ? serviceSubtask.getAddr() : "");
@@ -2098,50 +2047,106 @@
    /**
     * 随访任务是否超时
     *
     * @param serviceSubtask
     * @param serviceSubtaskPreachforms
     * @return
     */
    public boolean isSubtaskDeadline(ServiceSubtask serviceSubtask, List<ServiceSubtaskPreachform> serviceSubtaskPreachforms){
    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")){
            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()))
        Date deadline;
        if (serviceTask != null && "5".equals(serviceTask.getAppltype())) {
            // 任务组:基准 = schedule_benchmark 解析出的时间 + day_offset,不叠加 send_day
            deadline = resolveTaskGroupDeadlineBase(serviceSubtask);
            if (deadline == null) {
                log.warn("【isSubtaskDeadline】subid:{} 任务组基准时间为空,无法计算超时,按未超时处理", serviceSubtask.getId());
                return false;
            }
        } else {
            // 非任务组:基准 = 出院时间 + 发送日(sendDay 缺省按 1)
            if (serviceSubtask.getEndtime() == null) {
                log.warn("【isSubtaskDeadline】subid:{} endtime为空,无法计算超时,按未超时处理", serviceSubtask.getId());
                return false;
            }
            int sendDay = serviceTask != null && serviceTask.getSendDay() != null ? serviceTask.getSendDay().intValue() : 1;
            deadline = DateUtils.addDays(serviceSubtask.getEndtime(), sendDay);
        }
        int sumCompensateTime = serviceSubtaskPreachforms.stream()
                .mapToInt(x -> NumberUtils.toInt(x.getCompensateTime(), 0))
                .sum();
        sumCompensateTime = sumCompensateTime + 1;
        // 只补偿最后一步自身的补偿时间(天),脏值一律按 0 兜底
        // 补偿时间脏值一律按 0 兜底,最后再加 1 天宽限
        deadline = DateUtils.addDays(deadline, sumCompensateTime);
        // 出院(发送)时间 + 本步补偿时间 小于当前时间 -> 超时
        return deadline.getTime() < new Date().getTime();
    }
    /**
     * 任务组基准时间:按 schedule_benchmark 解析后加 day_offset。
     * 0/其它-自定义时间 1-入院 2-出院 3-就诊 4-入组
     */
    private Date resolveTaskGroupDeadlineBase(ServiceSubtask serviceSubtask) {
        if (serviceSubtask.getScheduleId() == null || serviceSubtask.getScheduleDetailId() == null) {
            return null;
        }
        ServiceTaskSchedule schedule = serviceTaskScheduleMapper.selectServiceTaskScheduleById(serviceSubtask.getScheduleId());
        ServiceTaskScheduleDetail detail = serviceTaskScheduleDetailMapper.selectServiceTaskScheduleDetailById(serviceSubtask.getScheduleDetailId());
        if (schedule == null || detail == null || detail.getDayOffset() == null) {
            return null;
        }
        Date base;
        Integer benchmark = schedule.getScheduleBenchmark() == null ? 0 : schedule.getScheduleBenchmark();
        switch (benchmark) {
            case 1:
                base = serviceSubtask.getStarttime();
                break;
            case 2:
                base = serviceSubtask.getEndtime();
                break;
            case 3:
                base = serviceSubtask.getAdmindate();
                break;
            case 4:
                base = serviceSubtask.getCreateTime();
                break;
            default:
                base = schedule.getScheduleBenchmarkCustomTime();
                break;
        }
        return base == null ? null : DateUtils.addDays(base, detail.getDayOffset());
    }
    /**
     * 处理超时随访任务(随访超时 → 生成"再次随访")
     * <p>
     * 扫描「出院满 deadlineDay 天仍未完成」的首次随访任务(sendstate=7 + visit_count=1),
     * 按当前任务的 nexttaskid 生成再次随访任务,科室固定为随访中心科室。
     * <p>
     * 注意:截止时间必须在循环外计算一次。历史实现把 {@code deadlineDay = -deadlineDay}
     * 写在循环体内,每轮取反一次,导致偶数轮用「今天 + deadlineDay」做截止时间,
     * 把出院不满 28 天的超时任务也扫了进来。
     * <p>
     * ⚠️ 2025 修复(开关失效):本方法原不判任何模块开关 ——
     * 关掉 {@code inhospvisit.enabled} 照样建"再次随访"子任务。
     * <p>
     * <b>为什么这里是"入口判开关"而不是像 {@link #dealOutHospInfo()} 那样切到门面</b>:
     * 模块侧只提供了 {@code InhospVisitApi#buildAgainVisitDraft}(组装草稿),
     * <b>没有</b>完整的"扫描超时 → 落库 → 回写原任务状态"入口,
     * 而本方法自己组装并 insert(见下方 {@code insertServiceSubtask})。
     * 造一个模块入口属于新增能力、超出"只解决开关"的范围,所以取最小改动:入口守卫。
     * 超时扫描与回写原任务状态的入口由 service 侧承接(见下方 {@code addSubTaskAgainByDeadline})。
     * <p>
     * ⚠️ 全仓只有 {@code ServiceSubtaskController#dealSubtaskDeadline} 一个调用者。
     *
     * @param orgid       机构ID
     * @param deadlineDay 出院满多少天算超时(正数,如 28)
     */
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay){
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay) {
        // ⚠️ 开关守卫:先判再干活,与 dealOutHospInfo / rollTaskGroupCycle 同口径。
        // 关闭时直接返回,不扫描、不建"再次随访"子任务。
        if (!inhospVisitFacade.isEnabled()) {
@@ -2155,9 +2160,17 @@
        st.setNexttaskflag("1");
        st.setOrgid(orgid);
        List<ServiceTask> serviceTasks = serviceTaskMapper.selectServiceTaskList(st);
        // 截止时间只算一次:出院时间 <= 今天 - deadlineDay
        int days = Math.abs(ObjectUtils.isEmpty(deadlineDay) ? 28 : deadlineDay);
        Date deadline = DateUtils.addDays(new Date(), -days);
        log.info("【dealSubtaskDeadline】开始扫描, orgid={}, day={}, deadline={}, 任务数={}",
                orgid, days, deadline, serviceTasks.size());
        for (ServiceTask serviceTask : serviceTasks) {
            deadlineDay = -deadlineDay;
            Date deadline = DateUtils.addDays(new Date(), deadlineDay);
            if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) {
                continue;
            }
            //通过任务ID拿到患者信息,并且随访时间得是今天之前的
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setOrgid(orgid);
@@ -2170,42 +2183,15 @@
                    = 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());
                patMedInhospVo.setCry(1);
                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);
                try {
                    // 生成逻辑统一收口到 service:锁 + 幂等 + 事务 + 发送方式明细初始化
                    String result = iServiceSubtaskService.addSubTaskAgainByDeadline(serviceSubtask);
                    log.info("【dealSubtaskDeadline】生成再次随访, subId={}, taskid={}, 结果={}",
                            serviceSubtask.getId(), serviceTask.getTaskid(), result);
                } catch (Exception e) {
                    // 单条失败不影响其他患者
                    log.error("【dealSubtaskDeadline】生成再次随访失败, subId=" + serviceSubtask.getId()
                            + ", taskid=" + serviceTask.getTaskid(), e);
                }
            }
        }