liusheng
8 天以前 f0a09ee996ab2605869844d6a3d812709e2bf0fa
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -11,30 +11,29 @@
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.service.IDingTalkService;
import com.ruoyi.common.dx.MessageSend;
import com.ruoyi.common.enums.MsgLSEnum;
import com.ruoyi.common.enums.ServiceFromEnum;
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.ruoyi.system.service.ISysUserService;
import com.smartor.common.LSHospTokenUtil;
import com.smartor.common.MtSubmitSmUtil;
import com.ruoyi.message.sms.provider.sltd.ws.MessageSend;
import com.smartor.common.QwenLLMUtil;
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;
@@ -49,7 +48,6 @@
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;
@@ -60,8 +58,24 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static cn.hutool.core.convert.Convert.toHex;
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.mapper.ServiceSubtaskMapper;
import com.ruoyi.task.mapper.ServiceTaskMapper;
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;
/**
 * 定时任务调度测试
 *
@@ -73,6 +87,12 @@
    @Autowired
    private IServiceThirdDataService iServiceThirdDataService;
    /**
     * 模板数据一律经 ruoyi-template 的门面取用(跨模块数据访问原则)。
     */
    @Autowired
    private TemplateFacade templateFacade;
    @Autowired
    private IServiceOutPathService iServiceOutPathService;
@@ -82,11 +102,15 @@
    @Autowired
    private IPatMedInhospService iPatMedInhospService;
    /**
     * 手术随访模块的<b>常驻门面</b>(原注入 {@code IPatMedOperationService})。
     * <p>
     * 手术随访编排已整体迁入 {@code ruoyi-othervisit},本类只做调用。
     * ⚠️ 注入门面而不是模块内部实现:门面永远存在,模块未打进应用或
     * {@code othervisit.enabled=false} 时只是降级并打 WARN,不会启动失败。
     */
    @Autowired
    private IPatMedOuthospService iPatMedOuthospService;
    @Autowired
    private IPatMedOperationService iPatMedOperationService;
    private com.ruoyi.othervisit.api.OtherVisitFacade otherVisitFacade;
    @Autowired
    private IServiceSubtaskService iServiceSubtaskService;
@@ -104,9 +128,6 @@
    private SysUserImportMapper sysUserImportMapper;
    @Autowired
    private HeLibraryMapper heLibraryMapper;
    @Autowired
    private ICollectHISService ichService;
    @Autowired
@@ -118,21 +139,8 @@
    @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;
@@ -168,12 +176,6 @@
    IPatMedOuthospService patMedOuthospService;
    @Autowired
    ISvyTaskTemplateService svyTaskTemplateService;
    @Autowired
    IIvrTaskTemplateService iIvrTaskTemplateService;
    @Autowired
    IXHGatherPatArchiveService ixhGatherPatArchiveService;
    @Autowired
@@ -182,9 +184,48 @@
    @Autowired
    ICollectHISService icollectHis;
    /**
     * 门诊分表维护已整体迁入 {@code ruoyi-outpatientvisit},这里只持有它的常驻门面。
     * <p>
     * ⚠️ 注入门面而不是内部实现:门面<b>永远存在</b>,即使模块没被打进应用本类也不会启动失败。
     */
    @Autowired
    CreateDataTable createDataTable;
    private com.ruoyi.outpatientvisit.api.OuthospVisitFacade outhospVisitFacade;
    /**
     * 出入院随访模块的<b>常驻门面</b>。
     * <p>
     * ⚠️ 2025 修复:本类的 <b>3 个随访入口原先不判开关</b> ——
     * 它们调用 {@code IPatMedInhospService}(smartor 里的遗留实现,见
     * {@code PatMedInhospServiceImpl}),而那个实现用门面<b>只是为了取数据</b>
     * ({@code selectPatMedInhospList} / {@code updatePatMedInhosp}),
     * <b>从不调用门面的编排方法</b>。于是 {@code inhospvisit.enabled} /
     * {@code publicity.enabled} / {@code othervisit.enabled} 对定时任务<b>完全失效</b>:
     * 门面的编排守卫({@code InhospVisitFacade#dealOutHospInfo} 等)虽然有实现、
     * 有测试,却只有 HTTP 手动触发那一条路走得到。
     * <p>
     * 现在改为与手术随访(手术随访早已走 {@code otherVisitFacade},见 {@link #dealOperationInfo()})
     * 完全同形:定时任务走门面,开关在门面里统一判。模块实现内部会回调
     * {@code PublicityApi} / {@code OtherVisitApi},那两处各自判自己的开关,
     * 所以一次切换让 3 个开关同时生效。
     * <p>
     * ⚠️ <b>不能注入 {@code InhospVisitApi} 接口</b>:容器里有两个实现
     * (本门面 + 内部实现 {@code DefaultInhospVisitApi}),按类型注入会歧义;
     * 而且只有门面才判开关。Quartz 依赖本模块、不存在反向依赖,所以直接注入门面是安全的。
     */
    @Autowired
    private com.ruoyi.inhospvisit.api.InhospVisitFacade inhospVisitFacade;
    /**
     * 宣教模块的<b>常驻门面</b>,用于判 {@code sys_config: publicity.enabled}。
     * <p>
     * ⚠️ 2025 修复:本类原本只在**生成**宣教时受开关管(走 inhospvisit 门面回调 PublicityApi),
     * 但**发送**宣教({@link #sfHandlle} 的 {@code type == 3} 分支)从来不读宣教开关 ——
     * 所以关掉 {@code publicity.enabled} 后,已生成的宣教子任务照样通过
     * 短信/公众号/电话发出去。
     */
    @Autowired
    private com.ruoyi.publicity.api.PublicityFacade publicityFacade;
    @Value("${appid}")
    private String appid;
@@ -192,16 +233,29 @@
    @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;
    // ⚠️ 2025 移除:原 `IServiceTaskScheduleService serviceTaskScheduleService` 字段。
    //    它在本类里**只**被 rollTaskGroupCycle() 用来调 rollCycleTasks()(死字段检查:
    //    改前全文件仅 1 处使用)。该调用已切到 inhospVisitFacade.rollTaskGroupCycle(),
    //    所以字段连带失效并删除 —— 留着它会让人误以为 smartor 的 rollCycleTasks 还有人用。
    //    smartor 侧的 ServiceTaskScheduleServiceImpl.rollCycleTasks() 本身**保留不删**
    //    (它可能仍被现网其它入口/接口调用,删除属另一件事)。
    public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
        System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
@@ -357,7 +411,6 @@
        }
    }
    /**
     * 定时处理外链的数据
     */
@@ -384,7 +437,6 @@
        String formattedNow = now.format(formatter);
        dealHisData(formattedNow);
    }
    /**
     * HIS数据采集
@@ -1077,7 +1129,6 @@
        }
    }
    public void dealHisDataSLTD() {
        log.info("【dealHisData】开始执行HIS数据采集任务");
@@ -1135,27 +1186,50 @@
    /**
     * 处理患者信息,进入子任务表
     * <p>
     * ⚠️ 2025 修复(开关失效):本方法原走 {@code iPatMedInhospService.dealOutHospInfo(config)},
     * 那是 smartor 里的遗留实现,**不判任何模块开关** —— 于是:
     * <ul>
     *   <li>关 {@code inhospvisit.enabled} 照样生成出院/长期随访;</li>
     *   <li>关 {@code publicity.enabled} 照样发入院宣教与药品宣教;</li>
     *   <li>关 {@code othervisit.enabled} 照样生成手术随访。</li>
     * </ul>
     * 现改走模块门面:门面判 {@code inhospvisit.enabled},模块实现内部再回调
     * {@code PublicityApi} / {@code OtherVisitApi},三处各自判自己的开关。
     */
    public void dealOutHospInfo() {
        String config = configService.selectConfigByKey("visit.early.day");
        if (port.equals("8095")) {
            //出院表
            iPatMedInhospService.dealOutHospInfo(config);
            //出院表(含入院宣教 / 药品宣教 / 手术随访 —— 均由模块实现回调对应模块门面)
            inhospVisitFacade.dealOutHospInfo(config);
            //门诊表(新华的暂时不做门诊随访)
            if (!active.equals("xh")) {
                iPatMedOuthospService.dealOutpatientInfo();
                // 【门急诊模块化切换】门面内部判 sys_config: outpatientvisit.enabled(运行时热读),
                // 关闭时返回 0 并打 WARN。
                //
                // ⚠️ 2025 修复:这里原先是 iPatMedOuthospService.dealOutpatientInfo(),
                // 它经 IPatMedOuthospService → PatMedOuthospServiceDelegatingImpl
                // → OuthospDataFacade.dealOutpatientInfo(),而**那条路上没有开关守卫** ——
                // OuthospVisitFacade.dealOutpatientInfo() 才是带守卫的入口,却全仓零调用者。
                // 结果是 outpatientvisit.enabled=false 对定时任务**完全失效**。
                // 现在与下面 dealOperationInfo() 走 otherVisitFacade 的形状一致。
                outhospVisitFacade.dealOutpatientInfo();
            }
        }
    }
    /**
     * 处理患者信息,进入子任务表(任务组)
     * <p>
     * ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 {@link #dealOutHospInfo()})。
     * 现走模块门面。注意模块侧的 {@code dealOutHospInfoBySchedule} 内部仍会转发回 smartor
     * ({@code OtherVisitApiBridge},见该类注释),但<b>开关在门面这一层判</b>,所以关闭时不会走到那里。
     */
    public void dealOutHospInfoBySchedule() {
        String config = configService.selectConfigByKey("visit.early.day");
        if (port.equals("8095")) {
            //出院表
            iPatMedInhospService.dealOutHospInfoBySchedule(config);
            inhospVisitFacade.dealOutHospInfoBySchedule(config);
            //门诊表(新华的暂时不做门诊随访)
//            if (!active.equals("xh")) {
//                //iPatMedOuthospService.dealOutpatientInfo();
@@ -1163,12 +1237,13 @@
        }
    }
    /**
     * 处理手术信息,进入子任务表
     */
    public void dealOperationInfo() {
        if (port.equals("8095")) iPatMedOperationService.dealOperationInfo();
        // 【手术模块化切换】出口改为调用 ruoyi-othervisit 的常驻门面。
        // 门面内部判 sys_config: othervisit.enabled(运行时热读),关闭时返回 0 并打 WARN。
        if (port.equals("8095")) otherVisitFacade.dealOperationInfo();
    }
@@ -1182,7 +1257,10 @@
     */
    public void createOuthospQuarterTable() {
        log.info("【createOuthospQuarterTable】开始执行门诊分表任务");
        createDataTable.createOuthospQuarterTable();
        // 【门诊模块化】CreateDataTable 已整体迁入 ruoyi-outpatientvisit
        // (实现为 OuthospPartitionMaintainer,经 OuthospVisitFacade 暴露)。
        // 这里只做调用;建表逻辑一行未动。
        outhospVisitFacade.createOuthospQuarterTable();
    }
    /**
@@ -1195,7 +1273,6 @@
            List<ServicePatientTemp> servicePatientTemps = iServicePatientTempService.selectServicePatientTempList(new ServicePatientTemp());
        }
    }
    /**
     * 长期任务发送
@@ -1294,7 +1371,6 @@
        if (StringUtils.isNotEmpty(failDay)) iServiceSubtaskService.setFailSubTaskDefalutVal(Long.valueOf(failDay));
    }
    /**
     * 判断日期是否是今天
     *
@@ -1346,6 +1422,22 @@
        }
        String descByCode = ServiceFromEnum.getDescByCode(Integer.valueOf(sendPreachform));
        // ⚠️ 2025 修复(宣教开关失效):type == 3 是**宣教/通知**(纯发送、无作答 ——
        // 见 setSuccessPreachForm 里「宣教或通知…直接改成 9」、以及本方法后面
        // 「宣教或通知,发送成功则直接改成 6L」的判定),整条派发的所有通道
        // (电话/短信/公众号/人工)发的都是宣教内容。关掉 publicity.enabled 时应**一个通道都不发**。
        //
        // ⚠️ 刻意**不推进 subtask 状态、也不记失败**(与 RedisMqReceiver 同口径):
        // 关闭是"暂时不发",不是"这个患者不需要宣教"。标成失败/不需要会让这条宣教
        // 在开关恢复后永远丢失。保持 sendstate 不动,开关打开后下一次 longTaskSend/compensateTask
        // 会照常把它捞出来发出。
        if ("3".equals(serviceSubtask.getType()) && !publicityFacade.isEnabled()) {
            log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),跳过宣教发送。subtaskId={}, patid={}",
                    serviceSubtask.getId(), serviceSubtask.getPatid());
            return;
        }
        if (descByCode.equals("电话")) {
            List<String> list = new ArrayList<>();
            ServiceTask ivrTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
@@ -1455,7 +1547,13 @@
                            sendMagParam.setContent(smsContemt.replace("${wjurl}", sendMagParam.getUrl()));
                        }
                    } else {
                        HeLibrary heLibrary = heLibraryMapper.selectHeLibraryById(serviceSubtask.getLibtemplateid());
                        // ⚠️ 这里**刻意不**为 TemplateModuleDisabledException 做降级:
                        // 模板模块关闭(sys_config: template.enabled=false)时,宣教内容确实取不到,
                        // 短信也就确实发不出去。外层 try-catch 会如实记为"短信发送失败",
                        // 并通过 setSuccessPreachForm(..., "5") 按既有机制升级到下一个通道(公众号/人工)。
                        // 若在这里硬记成"发送成功",等于用一条假记录掩盖"没发出去",
                        // 事后从任务记录上完全看不出问题 —— 那是更坏的降级。
                        HeLibrary heLibrary = templateFacade.findHeLibraryById(serviceSubtask.getLibtemplateid());
                        if (heLibrary.getHetype().equals("1")) {
                            sendMagParam.setPhone(serviceSubtask.getPhone());
                            url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/xj?p=" + format : localIP + "/xj?p=" + format;
@@ -1483,124 +1581,11 @@
                    serviceOutPath.setUpdateTime(new Date());
                    serviceOutPath.setUrl(url);
                    iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    String isSuccess = null;
                    //短信发送:各站点对接方式差异较大(报文/签名/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());
@@ -1659,29 +1644,11 @@
                    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());
@@ -1690,22 +1657,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);
                    //发送完成后,修改状态
@@ -1714,7 +1666,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) {
@@ -1729,6 +1681,71 @@
    }
    /**
     * 按 {@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";
    }
    private Date getNextVisitTime(Long subid, Long taskid, Date visitTime) {
        ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
@@ -1863,74 +1880,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 {
@@ -2119,7 +2072,9 @@
        }
        try {
            String config = configService.selectConfigByKey("visit.early.day");
            iPatMedInhospService.dealOutHospInfoBySchedule(config);
            // ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 dealOutHospInfo)。
            // 现走模块门面,与 RyTask#dealOutHospInfo 一致。
            inhospVisitFacade.dealOutHospInfoBySchedule(config);
            log.info("【dealTaskGroupOutHosp】任务组首轮纳入扫描完成");
        } catch (Exception e) {
            log.error("【dealTaskGroupOutHosp】任务组扫描异常", e);
@@ -2137,7 +2092,18 @@
    public void rollTaskGroupCycle() {
        log.info("【rollTaskGroupCycle】开始扫描循环任务组");
        try {
            int rolled = serviceTaskScheduleService.rollCycleTasks();
            // ⚠️ 2025 修复(开关失效):原走 smartor 的 ServiceTaskScheduleServiceImpl.rollCycleTasks(),
            // 不判开关 —— 关掉 inhospvisit.enabled 照样「推进轮次 + 新建 subtask」。
            //
            // 现走模块门面,完成模块化方案里第 10 步一直没做的那一步切换:
            //   InhospVisitFacade#rollTaskGroupCycle(判开关)
            //     → DefaultInhospVisitApi#rollTaskGroupCycle
            //     → TaskGroupCycleRoller#rollCycleTasks(逐行搬自原 L170-190)
            //     → ScheduleRollPortBridge(smartor 侧端口实现,116 字段深拷贝等留在实现侧)
            // 该类的注释明确写了「第 10 步切过来之前不能被挂到任何调度上,否则双跑」——
            // 本行就是那个切换点,切完老的 rollCycleTasks 即退役(全仓只有本方法调它)。
            int rolled = inhospVisitFacade.rollTaskGroupCycle();
            // 门面内部的 TaskGroupCycleRoller 已打「本次推进 N 个计划」,这里只补一句总览
            log.info("【rollTaskGroupCycle】本次推进 {} 个计划", rolled);
        } catch (Exception e) {
            log.error("【rollTaskGroupCycle】滚动调度异常", e);
@@ -2176,10 +2142,26 @@
    }
    /**
     * 处理超时随访任务
     * @return
     * 处理超时随访任务(随访超时 → 生成"再次随访")
     * <p>
     * ⚠️ 2025 修复(开关失效):本方法原不判任何模块开关 ——
     * 关掉 {@code inhospvisit.enabled} 照样建"再次随访"子任务。
     * <p>
     * <b>为什么这里是"入口判开关"而不是像 {@link #dealOutHospInfo()} 那样切到门面</b>:
     * 模块侧只提供了 {@code InhospVisitApi#buildAgainVisitDraft}(组装草稿),
     * <b>没有</b>完整的"扫描超时 → 落库 → 回写原任务状态"入口,
     * 而本方法自己组装并 insert(见下方 {@code insertServiceSubtask})。
     * 造一个模块入口属于新增能力、超出"只解决开关"的范围,所以取最小改动:入口守卫。
     * <p>
     * ⚠️ 全仓只有 {@code ServiceSubtaskController#dealSubtaskDeadline} 一个调用者。
     */
    public void dealSubtaskDeadline(String orgid, Integer deadlineDay){
        // ⚠️ 开关守卫:先判再干活,与 dealOutHospInfo / rollTaskGroupCycle 同口径。
        // 关闭时直接返回,不扫描、不建"再次随访"子任务。
        if (!inhospVisitFacade.isEnabled()) {
            log.warn("【dealSubtaskDeadline】模块未启用(参数配置 inhospvisit.enabled=false),跳过超时随访处理。orgid={}", orgid);
            return;
        }
        //获取任务信息
        ServiceTask st = new ServiceTask();
        st.setDelFlag("0");
@@ -2205,6 +2187,7 @@
                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()) {