liusheng
4 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
处理长期任务发送问题
已修改9个文件
517 ■■■■■ 文件已修改
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitTaskRuleDto.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospSubtaskWriter.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospTaskOrchestrator.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospVisitTaskService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-message/pom.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-task/src/main/java/com/ruoyi/task/api/contract/VisitTaskCreateApiImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceTaskServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml
@@ -75,7 +75,7 @@
    # 国际化资源文件路径
    basename: i18n/messages
  profiles:
    active: sltd
    active: nhfy
  # 文件上传
  servlet:
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitTaskRuleDto.java
@@ -1,5 +1,7 @@
package com.ruoyi.common.core.service.dto;
import lombok.Data;
import java.io.Serializable;
/**
@@ -21,84 +23,45 @@
 * 其它/null = 不参与
 * </pre>
 */
@Data
public class VisitTaskRuleDto implements Serializable {
    private static final long serialVersionUID = 1L;
    /** 规则ID */
    /**
     * 规则ID
     */
    private Long id;
    /** 科室规则:"1"=首要 "2"=次要 */
    /**
     * 科室规则:"1"=首要 "2"=次要
     */
    private String deptRule;
    /** 病区规则(⚠️ 原实体拼写即为 {@code wradRule}) */
    /**
     * 病区规则(⚠️ 原实体拼写即为 {@code wradRule})
     */
    private String wradRule;
    /** 疾病规则 */
    /**
     * 疾病规则
     */
    private String diagRule;
    /** 离院科室编码 */
    /**
     * 离院科室编码
     */
    private String deptcode;
    /** 离院病区编码 */
    /**
     * 离院病区编码
     */
    private String wardcode;
    /** 机构ID */
    /**
     * 机构ID
     */
    private String orgid;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getDeptRule() {
        return deptRule;
    }
    public void setDeptRule(String deptRule) {
        this.deptRule = deptRule;
    }
    public String getWradRule() {
        return wradRule;
    }
    public void setWradRule(String wradRule) {
        this.wradRule = wradRule;
    }
    public String getDiagRule() {
        return diagRule;
    }
    public void setDiagRule(String diagRule) {
        this.diagRule = diagRule;
    }
    public String getDeptcode() {
        return deptcode;
    }
    public void setDeptcode(String deptcode) {
        this.deptcode = deptcode;
    }
    public String getWardcode() {
        return wardcode;
    }
    public void setWardcode(String wardcode) {
        this.wardcode = wardcode;
    }
    public String getOrgid() {
        return orgid;
    }
    public void setOrgid(String orgid) {
        this.orgid = orgid;
    }
}
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospSubtaskWriter.java
@@ -61,19 +61,29 @@
@Service
public class InhospSubtaskWriter {
    /** 景宁机构ID(原 L798 硬编码;已逐字核对原实现 4 处) */
    /**
     * 景宁机构ID(原 L798 硬编码;已逐字核对原实现 4 处)
     */
    private static final String ORG_JINGNING = "47255004333112711A1001";
    /** 出院随访的服务类型(再入院停机分支的必要条件,原 L803) */
    /**
     * 出院随访的服务类型(再入院停机分支的必要条件,原 L803)
     */
    private static final String SERVICE_TYPE_DISCHARGE = "2";
    /** 待执行状态 */
    /**
     * 待执行状态
     */
    private static final long SENDSTATE_PENDING = 2L;
    /** 停机状态(原实现把 sendstate 置 4 表示不再执行) */
    /**
     * 停机状态(原实现把 sendstate 置 4 表示不再执行)
     */
    private static final long SENDSTATE_STOPPED = 4L;
    /** 疾病维度(type=3)专用:停旧任务后把执行方式全置失败 */
    /**
     * 疾病维度(type=3)专用:停旧任务后把执行方式全置失败
     */
    private static final String PREACHFORM_STATE_FAIL = "5";
    @Autowired
@@ -111,20 +121,17 @@
     *         原实现是 {@code void},编排层用"调用了几次"当成功数 —— 那是错的
     *         (任务未发布/被去重时同样是"生成了一条")。
     */
    public boolean writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp,
                                  VisitArchiveRow archive, Integer type, String config) {
        // 原 L683-685
    public boolean writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp, VisitArchiveRow archive, Integer type, String config) {
        String longTaskReason = inhosp.getLongTaskReason();
        String orgId = inhosp.getOrgid();
        if (StringUtils.isEmpty(longTaskReason)) {
            longTaskReason = "";
        }
        // 原 L686-728:查"已发布的长期任务";查不到时按 type 写不同的标志位后 return
        // 查任务创建模块的方法是否可用
        VisitTaskCreateApi taskApi = taskCreateApi();
        if (taskApi == null) {
            log.warn("【出入院随访】VisitTaskCreateApi 不可用(任务创建模块未装配),本患者不生成子任务。"
                    + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid);
            log.warn("【出入院随访】VisitTaskCreateApi 不可用(任务创建模块未装配),本患者不生成子任务。" + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid);
            return false;
        }
        VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid);
@@ -145,23 +152,19 @@
        //   患者被重复扫到时会走"停旧 + 再 insert 一条 sendstate=4"的分支,
        //   每跑一轮多一条,最终同一患者几十条。
        // ------------------------------------------------------------------
        if (taskApi.existsSubtask(archive == null ? null : archive.getId(),
                inhosp.getInhospid(), taskid)) {
            log.info("【出入院随访】患者{}出院记录{}任务{}已有子任务,跳过重复生成。",
                    inhosp.getPatid(), inhosp.getInhospid(), taskid);
        if (taskApi.existsSubtask(archive == null ? null : archive.getId(), inhosp.getInhospid(), taskid)) {
            log.info("【出入院随访】患者{}出院记录{}任务{}已有子任务,跳过重复生成。", inhosp.getPatid(), inhosp.getInhospid(), taskid);
            writeFlagByType(inhosp.getInhospid(), type, "1", null);
            return true;
        }
        // 原 L732-736:组装(副作用由本类实现,见 missingOutHospTime)
        VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(
                toTaskSource(serviceTask, inhosp), inhosp, archive, config, type,
                row -> onMissingOutHospTime(row));
        // serviceSubtask数据组装(副作用由本类实现,见 missingOutHospTime)
        VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(toTaskSource(serviceTask, inhosp), inhosp, archive, config, type, row -> onMissingOutHospTime(row));
        if (ObjectUtils.isEmpty(serviceSubtask)) {
            return false;
        }
        // 原 L737-753:就诊科室/病区填充
        // 就诊科室/病区填充
        if (type != null && type != 2) {
            // 科室和疾病 都用 科室填充
            if ("1".equals(inhosp.getInhospstate())) {
@@ -180,22 +183,19 @@
            serviceSubtask.setVisitDeptName(inhosp.getLeavehospitaldistrictname());
        }
        // 原 L754-755:记录 insertServiceSubtask 的返回状态
        // 记录 insertServiceSubtask 的返回状态
        int i = 0;
        try {
            // 原 L757-758
            if (Boolean.TRUE.equals(check)
                    && (serviceSubtask.getTaskSituation() == null || serviceSubtask.getTaskSituation() != 6)) {
                // 原 L759-764:查该患者 + 该 taskid 下所有待执行子任务
                List<VisitPendingSubtaskDto> pendingList =
                        taskApi.findPendingSubtasks(archive.getId(), taskid);
            if (Boolean.TRUE.equals(check) && (serviceSubtask.getTaskSituation() == null || serviceSubtask.getTaskSituation() != 6)) {
                // 查该患者 + 该 taskid 下所有待执行子任务
                List<VisitPendingSubtaskDto> pendingList = taskApi.findPendingSubtasks(archive.getId(), taskid);
                if (pendingList != null && !pendingList.isEmpty()) {
                    for (VisitPendingSubtaskDto pending : pendingList) {
                        // 原 L768-771:不是长期任务,不处理
                        // 不是长期任务,不处理
                        if (pending.getLongSendTime() == null) {
                            continue;
                        }
                        // 原 L772-773:type = 1或2 表示 "科室、病区"
                        //type = 1或2 表示 "科室、病区"
                        if (type != null && (type == 1 || type == 2)) {
                            Date endTime = serviceSubtask.getEndtime();
                            Date endTime1 = pending.getEndtime();
@@ -222,14 +222,12 @@
                                }
                                continue;
                            }
                            // 原 L795-800:景宁 出院首次(7天)随访一定要做
                            //景宁 出院首次(7天)随访一定要做
                            boolean firstVisit = ORG_JINGNING.equals(orgId);
                            if (!firstVisit) {
                                // 原 L802-803
                                // ⚠️ 原实现不判 serviceType 是否为 null(会 NPE),原样保留
                                if (endTime1.before(endTime)
                                        && SERVICE_TYPE_DISCHARGE.equals(pending.getServiceType())) {
                                    // 原 L804-813:把循环到的旧子任务停掉
                                if (endTime1.before(endTime) && SERVICE_TYPE_DISCHARGE.equals(pending.getServiceType())) {
                                    // 把循环到的旧子任务停掉
                                    pending.setSendstate(SENDSTATE_STOPPED);
                                    if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) {
                                        serviceSubtask.setRemark(serviceSubtask.getRemark() + ",科室/病区患者再入院");
@@ -243,12 +241,11 @@
                                        i = 1;
                                    }
                                } else {
                                    // 原 L814-824:相反,则把"当前的"数据停掉(置 error + sendstate=4 后 insert)
                                    //相反,则把"当前的"数据停掉(置 error + sendstate=4 后 insert)
                                    serviceSubtask.setResult("error");
                                    serviceSubtask.setSendstate(SENDSTATE_STOPPED);
                                    if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) {
                                        serviceSubtask.setRemark(serviceSubtask.getRemark()
                                                + ",当前患者出院时间后有待执行的出院随访长期任务");
                                        serviceSubtask.setRemark(serviceSubtask.getRemark() + ",当前患者出院时间后有待执行的出院随访长期任务");
                                    } else {
                                        serviceSubtask.setRemark("当前患者出院时间后有待执行的出院随访长期任务");
                                    }
@@ -258,7 +255,7 @@
                                }
                            }
                        } else if (type != null && type == 3) {
                            // 原 L826-841:type=3 疾病 —— 无条件停旧 + 重新新增
                            //type=3 疾病 —— 无条件停旧 + 重新新增
                            pending.setSendstate(SENDSTATE_STOPPED);
                            if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) {
                                serviceSubtask.setRemark(serviceSubtask.getRemark() + ",疾病患者再入院");
@@ -276,7 +273,7 @@
                        }
                    }
                } else {
                    // 原 L844-856:没有待执行任务
                    //没有待执行任务
                    markPhoneEmptyIfNeeded(serviceSubtask);
                    serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                    serviceSubtask.setCreateTime(new Date());
@@ -284,13 +281,13 @@
                    addServiceSubtaskPreachform(serviceSubtask, null);
                }
            } else if (serviceSubtask.getTaskSituation() != null && serviceSubtask.getTaskSituation() == 6) {
                // 原 L857-862:该患者被过滤,不用执行
                // 该患者被过滤,不用执行
                serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                serviceSubtask.setCreateTime(new Date());
                serviceSubtask.setSendstate(SENDSTATE_STOPPED);
                i = insert(taskApi, serviceSubtask) ? 1 : 0;
            } else {
                // 原 L863-875:不需要校验(check=false)
                // 不需要校验(check=false)
                markPhoneEmptyIfNeeded(serviceSubtask);
                serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                serviceSubtask.setCreateTime(new Date());
@@ -298,19 +295,17 @@
                addServiceSubtaskPreachform(serviceSubtask, null);
            }
            // 原 L877-899:把 check_flag 改成 1(已处理)/ 生成失败置 2
            //把 check_flag 改成 1(已处理)/ 生成失败置 2
            if (i == 1) {
                writeFlagByType(inhosp.getInhospid(), type, "1", null);
            } else {
                log.info("生成子任务失败serviceSubtask的taskid为:{},patid为:{}",
                        serviceSubtask.getTaskid(), serviceSubtask.getPatid());
                log.info("生成子任务失败serviceSubtask的taskid为:{},patid为:{}", serviceSubtask.getTaskid(), serviceSubtask.getPatid());
                writeFlagByType(inhosp.getInhospid(), type, "2", "生成子任务失败 " + type);
            }
            return i == 1;
        } catch (Exception e) {
            // 原 L900-912:出错回滚事务
            log.info("生成子任务报错回滚serviceSubtask的taskid为:{},patid为:{}",
                    serviceSubtask.getTaskid(), serviceSubtask.getPatid());
            // 出错回滚事务
            log.info("生成子任务报错回滚serviceSubtask的taskid为:{},patid为:{}", serviceSubtask.getTaskid(), serviceSubtask.getPatid());
            // ⚠️ 原实现的 catch 只处理 type 1/2/3/0 —— type=4/5 抛异常时不回写任何标志位。
            //     原样保留(见类注释第 1 条)。
            writeFlagByTypeForRollback(inhosp.getInhospid(), type, "2", "生成子任务报错回滚 " + type);
@@ -319,10 +314,7 @@
        }
    }
    // ==========================================================================
    // 原 L691-725:任务不存在时按 type 写标志位
    // ==========================================================================
    // 任务不存在时按 type 写标志位
    private void writeMissingTaskFlag(VisitInhospRow inhosp, Integer type, String longTaskReason, Long taskid) {
        if (type == null) {
            return;
@@ -331,42 +323,31 @@
        switch (type) {
            case 0:
                log.info("该患者药品宣教长期任务不存在,任务ID为:{}", taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "medcheck_flag", "2",
                        longTaskReason + " & 该患者药品宣教长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "medcheck_flag", "2", longTaskReason + " & 该患者药品宣教长期任务不存在,任务ID为:" + taskid);
                return;
            case 1:
                log.info("该患者出院随访长期任务不存在,任务ID为:{}", taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "deptcheck_flag", "2",
                        longTaskReason + " & 该患者离院科室长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "deptcheck_flag", "2", longTaskReason + " & 该患者离院科室长期任务不存在,任务ID为:" + taskid);
                return;
            case 2:
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "wardcheck_flag", "2",
                        longTaskReason + " & 该患者离院病区长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "wardcheck_flag", "2", longTaskReason + " & 该患者离院病区长期任务不存在,任务ID为:" + taskid);
                return;
            case 3:
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "diagcheck_flag", "2",
                        longTaskReason + " & 该患者疾病长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "diagcheck_flag", "2", longTaskReason + " & 该患者疾病长期任务不存在,任务ID为:" + taskid);
                return;
            case 4:
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_deptcheck_flag", "2",
                        longTaskReason + " & 该患者入院科室长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_deptcheck_flag", "2", longTaskReason + " & 该患者入院科室长期任务不存在,任务ID为:" + taskid);
                return;
            case 5:
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_wardcheck_flag", "2",
                        longTaskReason + " & 该患者入院病区长期任务不存在,任务ID为:" + taskid);
                flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_wardcheck_flag", "2", longTaskReason + " & 该患者入院病区长期任务不存在,任务ID为:" + taskid);
                return;
            default:
                // 原实现:type 不属于 0-5 时,if/else-if 链全部不命中 → 什么都不做、也不 return
                // → 继续往下走,此时 serviceTask 仍是 null,到 L729 之后会 NPE。
                // 这里【不复制】那个 NPE(它只会掩盖问题),改为记录并返回。
                log.warn("【出入院随访】任务不存在且 type={} 不属于 0-5,无法判定回写哪个标志位。"
                        + "inhospid={}, taskid={}", type, inhosp.getInhospid(), taskid);
                log.warn("【出入院随访】任务不存在且 type={} 不属于 0-5,无法判定回写哪个标志位。" + "inhospid={}, taskid={}", type, inhosp.getInhospid(), taskid);
        }
    }
    // ==========================================================================
    // 原 L877-912:标志位回写(成功 / 失败 / 异常回滚)
    // ==========================================================================
    /**
     * type → 标志位回写。<b>照抄原 L881-885 与 L892-896 的分支</b>。
@@ -450,12 +431,8 @@
        flagWriter.setFlagWithRemark(inhospid, column, flagValue, remark);
    }
    // ==========================================================================
    // 辅助
    // ==========================================================================
    /**
     * 原 L845-851 / L864-870:手机号为空时追加备注、置 sendstate=4、result=error。
     * 手机号为空时追加备注、置 sendstate=4、result=error。
     */
    private void markPhoneEmptyIfNeeded(VisitSubtaskDraft d) {
        if (StringUtils.isEmpty(d.getPhone())) {
@@ -480,9 +457,7 @@
     */
    private void onMissingOutHospTime(VisitInhospRow row) {
        flagWriter.setFlag(row.getInhospid(), "deptcheck_flag", "2");
        flagWriter.setFlagWithRemark(row.getInhospid(), "deptcheck_flag", "2",
                (row.getRemark() == null ? "" : row.getRemark())
                        + "& 该患者疾病出院时间为空,出院记录ID为:" + row.getInhospid());
        flagWriter.setFlagWithRemark(row.getInhospid(), "deptcheck_flag", "2", (row.getRemark() == null ? "" : row.getRemark()) + "& 该患者疾病出院时间为空,出院记录ID为:" + row.getInhospid());
    }
    /**
@@ -495,8 +470,7 @@
     * ⚠️ {@code type / sendDay / updateBy / createBy / serviceType / preachform} 等
     * 都在 {@link VisitLongTaskDto} 里有对应字段,直接搬。
     */
    private com.ruoyi.inhospvisit.domain.row.VisitTaskRow toTaskSource(VisitLongTaskDto dto,
                                                                       VisitInhospRow inhosp) {
    private com.ruoyi.inhospvisit.domain.row.VisitTaskRow toTaskSource(VisitLongTaskDto dto, VisitInhospRow inhosp) {
        com.ruoyi.inhospvisit.domain.row.VisitTaskRow t = new com.ruoyi.inhospvisit.domain.row.VisitTaskRow();
        t.setTaskid(dto.getTaskid());
        t.setTaskName(dto.getTaskName());
@@ -560,16 +534,14 @@
        try {
            SubtaskPreachformWriter writer = preachformWriterProvider.stream().findFirst().orElse(null);
            if (writer == null) {
                log.warn("【出入院随访】SubtaskPreachformWriter 未装配,跳过执行方式明细写入。subtaskId={}",
                        subtask.getId());
                log.warn("【出入院随访】SubtaskPreachformWriter 未装配,跳过执行方式明细写入。subtaskId={}", subtask.getId());
                return;
            }
            writer.addServiceSubtaskPreachform(subtask, sendstate);
        } catch (Exception e) {
            // 这一步失败不应影响主流程(原实现在同一个 try 里,失败会走 catch 回滚标志位;
            // 但那是因为它与 insert 同属一次事务。这里拆开后按"尽力而为"处理并记录)
            log.warn("【出入院随访】写执行方式明细失败,已忽略。subtaskId={},原因={}",
                    subtask.getId(), e.toString());
            log.warn("【出入院随访】写执行方式明细失败,已忽略。subtaskId={},原因={}", subtask.getId(), e.toString());
        }
    }
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospTaskOrchestrator.java
@@ -88,19 +88,29 @@
@Service
public class InhospTaskOrchestrator {
    /** 出院随访的服务类型 */
    /**
     * 出院随访的服务类型
     */
    private static final String SERVICE_TYPE_DISCHARGE = "2";
    /** 维度:科室 */
    /**
     * 维度:科室
     */
    private static final String DEPT_TYPE_DEPT = "1";
    /** 维度:病区 */
    /**
     * 维度:病区
     */
    private static final String DEPT_TYPE_WARD = "2";
    /** 站点:新华(跳过病区+疾病维度) */
    /**
     * 站点:新华(跳过病区+疾病维度)
     */
    private static final String SITE_XH = "xh";
    /** 站点:南华附一(疾病维度用诊断名) */
    /**
     * 站点:南华附一(疾病维度用诊断名)
     */
    private static final String SITE_NHFY = "nhfy";
    @Value("${spring.profiles.active}")
@@ -166,15 +176,10 @@
     * @param rule 已查好的规则;{@code null} = 该科室/病区未配置规则
     * @param config {@code sys_config: visit.early.day}
     */
    public void addSubTaskByRule(VisitInhospRow inhosp, VisitArchiveRow archive,
                                 VisitTaskRule rule, String config) {
    public void addSubTaskByRule(VisitInhospRow inhosp, VisitArchiveRow archive, VisitTaskRule rule, String config) {
        try {
            List<Integer> order = resolveDimensionOrder(rule);
            log.info("【基于规则】患者{}规则:deptRule={}, wradRule={}, diagRule={};维度尝试顺序={}",
                    inhosp.getInhospid(),
                    rule == null ? null : rule.getDeptRule(),
                    rule == null ? null : rule.getWradRule(),
                    rule == null ? null : rule.getDiagRule(), order);
            log.info("【基于规则】患者{}规则:deptRule={}, wradRule={}, diagRule={};维度尝试顺序={}", inhosp.getInhospid(), rule == null ? null : rule.getDeptRule(), rule == null ? null : rule.getWradRule(), rule == null ? null : rule.getDiagRule(), order);
            Integer hit = null;
            for (Integer type : order) {
@@ -242,7 +247,9 @@
        return order;
    }
    /** 追加一个候选维度;{@code xh} 站点只允许科室维度(原 {@code !active.equals("xh")} 的等价写法) */
    /**
     * 追加一个候选维度;{@code xh} 站点只允许科室维度(原 {@code !active.equals("xh")} 的等价写法)
     */
    private void addDimension(List<Integer> order, int type, boolean xhSite) {
        if (xhSite && type != 1) {
            return;
@@ -265,9 +272,10 @@
        }
    }
    /** 尝试一个维度,返回"是否真的生成了(或本来就已经有)一条子任务" */
    private boolean generateOneForDimension(VisitInhospRow inhosp, VisitArchiveRow archive,
                                            int type, String config) {
    /**
     * 尝试一个维度,返回"是否真的生成了(或本来就已经有)一条子任务"
     */
    private boolean generateOneForDimension(VisitInhospRow inhosp, VisitArchiveRow archive, int type, String config) {
        switch (type) {
            case 1:
                return addDeptSubTaskForPatient(inhosp, archive, config) > 0;
@@ -309,16 +317,16 @@
                continue;
            }
            if (hit == type) {
                updateCheckFlag(inhosp, type, "1", composeRemark(inhosp,
                        "按优先级生成一条随访任务(维度=" + dimensionName(type) + ")"));
                updateCheckFlag(inhosp, type, "1", composeRemark(inhosp, "按优先级生成一条随访任务(维度=" + dimensionName(type) + ")"));
            } else {
                updateCheckFlag(inhosp, type, "2", composeRemark(inhosp,
                        "已生成一条随访任务(维度=" + dimensionName(hit) + "),本维度跳过"));
                updateCheckFlag(inhosp, type, "2", composeRemark(inhosp, "已生成一条随访任务(维度=" + dimensionName(hit) + "),本维度跳过"));
            }
        }
    }
    /** 拼 reason:沿用原实现的 {@code remark + ";xxx"} 形式 */
    /**
     * 拼 reason:沿用原实现的 {@code remark + ";xxx"} 形式
     */
    private String composeRemark(VisitInhospRow inhosp, String text) {
        String remark = inhosp.getRemark();
        return StringUtils.isNotEmpty(remark) ? remark + ";" + text : text;
@@ -364,8 +372,7 @@
                u.setWardcheckFlag(fresh == null ? null : fresh.getWardcheckFlag());
                u.setDiagcheckFlag(fresh == null ? null : fresh.getDiagcheckFlag());
                // 原实现不判 null(fresh.getLongTaskReason() + "..."),原样保留
                u.setLongTaskReason((fresh == null ? null : fresh.getLongTaskReason())
                        + "该患者所在科室未配置离院长期任务;");
                u.setLongTaskReason((fresh == null ? null : fresh.getLongTaskReason()) + "该患者所在科室未配置离院长期任务;");
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdepts) {
@@ -373,8 +380,7 @@
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config)) {
                        return 1;
                    }
                    log.info("【科室任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                    log.info("【科室任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -430,8 +436,7 @@
                u.setDeptcheckFlag(fresh == null ? null : fresh.getDeptcheckFlag());
                u.setDiagcheckFlag(fresh == null ? null : fresh.getDiagcheckFlag());
                // 原有判空(与科室维度那处不同),原样保留
                u.setLongTaskReason(StringUtils.isNotEmpty(fresh == null ? null : fresh.getLongTaskReason())
                        ? fresh.getLongTaskReason() + errorreason : errorreason);
                u.setLongTaskReason(StringUtils.isNotEmpty(fresh == null ? null : fresh.getLongTaskReason()) ? fresh.getLongTaskReason() + errorreason : errorreason);
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdeptList) {
@@ -439,8 +444,7 @@
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config)) {
                        return 1;
                    }
                    log.info("【病区任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                    log.info("【病区任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -506,23 +510,19 @@
                int flag = 0;
                for (VisitTaskdiagRow t : taskdiags) {
                    // 原 L1975-1983:必须科室或病区能匹配上才建
                    if (StringUtils.isNotEmpty(t.getDeptCode())
                            && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                    if (StringUtils.isNotEmpty(t.getDeptCode()) && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                        flag = 1;
                        // 一个患者只留一条:第一条真正落库的即返回
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                    } else if (StringUtils.isNotEmpty(t.getWardCode())
                            && t.getWardCode().equals(inhosp.getLeavehospitaldistrictcode())) {
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                    } else if (StringUtils.isNotEmpty(t.getWardCode()) && t.getWardCode().equals(inhosp.getLeavehospitaldistrictcode())) {
                        flag = 1;
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                    }
                }
                if (flag == 0) {
@@ -571,13 +571,11 @@
        try {
            VisitTaskCreateApi api = taskApi();
            if (api == null) {
                log.warn("【出入院随访】VisitTaskCreateApi 不可用,跳过再入院处理。inhospid={}",
                        newInHosp.getInhospid());
                log.warn("【出入院随访】VisitTaskCreateApi 不可用,跳过再入院处理。inhospid={}", newInHosp.getInhospid());
                return;
            }
            // 原 L1324-1328:patid + sendstate=2(不设 taskid)
            List<VisitPendingSubtaskDto> pendingList =
                    api.findPendingSubtasks(newInHosp.getPatid(), null);
            List<VisitPendingSubtaskDto> pendingList = api.findPendingSubtasks(newInHosp.getPatid(), null);
            if (CollectionUtils.isEmpty(pendingList)) {
                return;
            }
@@ -592,8 +590,7 @@
                    continue;
                }
                // 原 L1342-1350:旧子任务的出院时间在新入院时间之前 → 停掉
                if (pending.getEndtime() != null && newInHosp.getStarttime() != null
                        && pending.getEndtime().before(newInHosp.getStarttime())) {
                if (pending.getEndtime() != null && newInHosp.getStarttime() != null && pending.getEndtime().before(newInHosp.getStarttime())) {
                    pending.setSendstate(4L);
                    pending.setIsVisitAgain(2);
                    pending.setRemark("患者再入院");
@@ -607,7 +604,9 @@
        }
    }
    /** 景宁机构ID(原 L1338;与 InhospSubtaskWriter 里那份是同一常量,已逐字核对) */
    /**
     * 景宁机构ID(原 L1338;与 InhospSubtaskWriter 里那份是同一常量,已逐字核对)
     */
    private static final String ORG_JINGNING = "47255004333112711A1001";
    // ==========================================================================
@@ -655,13 +654,19 @@
     */
    public static class VisitTaskRule {
        /** 科室规则:"1"=首要 "2"=次要,其它=不参与 */
        /**
         * 科室规则:"1"=首要 "2"=次要,其它=不参与
         */
        private String deptRule;
        /** 病区规则(原字段名就是拼错的 {@code wradRule},保持同名以免读错) */
        /**
         * 病区规则(原字段名就是拼错的 {@code wradRule},保持同名以免读错)
         */
        private String wradRule;
        /** 疾病规则 */
        /**
         * 疾病规则
         */
        private String diagRule;
        public String getDeptRule() {
@@ -689,13 +694,16 @@
        }
    }
    /** 供测试/日志:本类是否拿到了必需的 Mapper */
    /**
     * 供测试/日志:本类是否拿到了必需的 Mapper
     */
    boolean hasMappers() {
        return inhospMapper != null && archiveMapper != null
                && taskdeptMapper != null && taskdiagMapper != null;
        return inhospMapper != null && archiveMapper != null && taskdeptMapper != null && taskdiagMapper != null;
    }
    /** 预留:供日志使用的时间戳(避免 IDE 报未使用 import Date) */
    /**
     * 预留:供日志使用的时间戳(避免 IDE 报未使用 import Date)
     */
    @SuppressWarnings("unused")
    private Date now() {
        return new Date();
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospVisitTaskService.java
@@ -4,6 +4,7 @@
import com.ruoyi.common.core.service.PublicityApi;
import com.ruoyi.common.core.service.PublicityDataPort;
import com.ruoyi.common.core.service.dto.InHospPublicityRowDto;
import com.ruoyi.common.core.service.dto.VisitInhospUpdateDto;
import com.ruoyi.common.core.service.dto.VisitTaskRuleDto;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.service.dto.VisitArchiveRow;
@@ -179,8 +180,7 @@
            // 原 L1692-1700:获取患者基本信息,为空则写 deptcheck=2 + reason
            VisitArchiveRow archive = archiveMapper.selectByPatid(row.getPatid());
            if (archive == null) {
                com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u =
                        new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto();
                VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                u.setInhospid(row.getInhospid());
                u.setDeptcheckFlag("2");
                u.setLongTaskReason("患者基本信息为空");
@@ -197,8 +197,7 @@
                // 原 L1839-1845(在编排器里也有,但外层遍历这一层原实现没有 try/catch;
                // 这里加一层是为了"单患者失败不影响其它患者",属健壮性改进,已登记)
                log.error("【基于规则】患者{}外层处理异常:{}", row.getInhospid(), e.getMessage());
                com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u =
                        new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto();
                com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u = new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto();
                u.setInhospid(row.getInhospid());
                u.setDeptcheckFlag("2");
                u.setLongTaskReason("基于规则处理异常");
@@ -245,8 +244,7 @@
     * @return 原实现固定返回 1
     */
    public int dealOutHospInfoBySchedule(String config) {
        log.warn("【出入院随访】dealOutHospInfoBySchedule 本次未搬迁(含手术分支与任务组 builder,"
                + "属第二批 ruoyi-othervisit 范围),仍由 smartor 的实现承接。config={}", config);
        log.warn("【出入院随访】dealOutHospInfoBySchedule 本次未搬迁(含手术分支与任务组 builder," + "属第二批 ruoyi-othervisit 范围),仍由 smartor 的实现承接。config={}", config);
        return 1;
    }
@@ -254,12 +252,13 @@
    // 辅助
    // ==========================================================================
    /** 原 L1703-1707:按离院科室/病区查规则(取不到端口时返回 null = 未配置规则 → 全部生成) */
    /**
     * 原 L1703-1707:按离院科室/病区查规则(取不到端口时返回 null = 未配置规则 → 全部生成)
     */
    private VisitTaskRuleDto findTaskRule(VisitInhospRow row) {
        PublicityDataPort port = dataPort();
        if (port == null) {
            log.warn("【出入院随访】PublicityDataPort 不可用,规则按「未配置」处理(全部生成)。inhospid={}",
                    row.getInhospid());
            log.warn("【出入院随访】PublicityDataPort 不可用,规则按「未配置」处理(全部生成)。inhospid={}", row.getInhospid());
            return null;
        }
        try {
@@ -270,7 +269,9 @@
        }
    }
    /** 把端口的规则 DTO 适配成编排器内嵌的规则视图(字段一一对应,含拼错的 {@code wradRule}) */
    /**
     * 把端口的规则 DTO 适配成编排器内嵌的规则视图(字段一一对应,含拼错的 {@code wradRule})
     */
    private InhospTaskOrchestrator.VisitTaskRule toOrchestratorRule(VisitTaskRuleDto dto) {
        if (dto == null) {
            return null;
@@ -282,7 +283,7 @@
        return r;
    }
    private void updateInhosp(com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u) {
    private void updateInhosp(VisitInhospUpdateDto u) {
        PublicityDataPort port = dataPort();
        if (port == null) {
            log.warn("【出入院随访】PublicityDataPort 不可用,无法写回标志位。inhospid={}", u.getInhospid());
ruoyi-message/pom.xml
@@ -95,6 +95,16 @@
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <dependency>
            <groupId>jakarta.jws</groupId>
            <artifactId>jakarta.jws-api</artifactId>
            <version>2.1.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.xml.ws</groupId>
            <artifactId>jakarta.xml.ws-api</artifactId>
        </dependency>
    </dependencies>
</project>
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -76,6 +76,7 @@
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
import com.ruoyi.task.service.IServiceSubtaskService;
import com.ruoyi.task.domain.SendMagParam;
/**
 * 定时任务调度测试
 *
@@ -367,8 +368,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();
@@ -566,8 +566,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) {
@@ -590,15 +589,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();
@@ -625,8 +622,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;
@@ -639,8 +635,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;
@@ -656,8 +651,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;
@@ -672,9 +666,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);
@@ -705,8 +697,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);
@@ -740,13 +731,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;
                }
@@ -756,8 +744,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++;
@@ -777,7 +764,9 @@
        }
    }
    /** 按槽位保存中断页码 */
    /**
     * 按槽位保存中断页码
     */
    private void saveMzDayPage(String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, long page) {
        if (slot == MzGatherSlotEnum.FINAL) {
            cursor.finalPage = page;
@@ -789,12 +778,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();
@@ -806,8 +799,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);
@@ -815,7 +807,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)) {
@@ -824,7 +818,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;
@@ -842,7 +838,9 @@
        }
    }
    /** 保存门急诊采集断点(各轮次中断的页码) */
    /**
     * 保存门急诊采集断点(各轮次中断的页码)
     */
    private void saveMzDayCursor(String cursorKey, MzDayCursor cursor) {
        JSONObject json = new JSONObject();
        json.put("day", cursor.day);
@@ -856,7 +854,9 @@
        redisCache.setCacheObject(cursorKey, json.toJSONString(), MzGatherConstant.CACHE_TTL_HOURS, TimeUnit.HOURS);
    }
    /** 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新) */
    /**
     * 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新)
     */
    private MzDayCursor loadMzDayCursor(String cursorKey) {
        MzDayCursor cursor = new MzDayCursor();
        try {
@@ -893,16 +893,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;
    }
@@ -1153,8 +1161,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("处理补偿任务,当前处理最后补偿,全部执行失败(超时)");
@@ -1217,6 +1224,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("说明已经全部遍历完了,当前的执行方式序号是最后一个了   或者  该患者已经随访结束(成功或失败了或不执行)");
@@ -1224,6 +1232,7 @@
        }
        //先判断患者是不是还要发送
        PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
        log.info("patArchive的值为:{}", patArchive);
        if (!Objects.isNull(patArchive) && patArchive.getNotrequiredFlag() == "1") {
            //不需要发送
            serviceSubtask.setResult("患者不需要随访");
@@ -1247,8 +1256,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;
        }
@@ -1337,6 +1345,7 @@
                    String url = "";
                    String format = String.format("%03X", 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;
@@ -1355,6 +1364,7 @@
                        } else {
                            sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template");
                        }
                        log.info("sysConfig的值为:{}", sysConfig);
                        if (!Objects.isNull(sysConfig)) {
                            String smsContemt = sysConfig.getConfigValue();
//                            sendMagParam.setContent("您好,邀请您填写出院调查表,请点击" + sendMagParam.getUrl() + "填写。感谢您配合!");
@@ -1399,6 +1409,7 @@
                    //已收口到 ruoyi-message 模块,这里只负责组装入参并路由
                    log.info("active的值为:{}", active);
                    SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam);
                    log.info("smsSendResult的值为:{}", smsSendResult);
                    String isSuccess = smsSendResult.asLegacyFlag();
                    //任务发送记录
                    ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord();
@@ -1414,6 +1425,7 @@
                    serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord);
                    //发送完成后,修改状态
                    log.info("serviceSubtaskRecord的值为:{}", serviceSubtaskRecord);
                    serviceSubtask.setSendstate(3L);
                    if (StringUtils.isNotEmpty(isSuccess) && isSuccess.equals("true")) {
                        serviceSubtask.setResult("success");
@@ -1617,8 +1629,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);
@@ -1752,6 +1763,7 @@
    }
    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();
@@ -1769,7 +1781,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");
@@ -1779,7 +1791,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);
@@ -1790,10 +1802,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);
@@ -1808,7 +1819,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);
@@ -1821,6 +1832,7 @@
            }
        }
        serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + remark : remark);
        log.info("serviceSubtask的入参是:{}",serviceSubtask);
        serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
        return true;
    }
@@ -1926,6 +1938,7 @@
    /**
     * 随访任务是否超时
     *
     * @param serviceSubtask
     * @param serviceSubtaskPreachforms
     * @return
@@ -1933,8 +1946,7 @@
    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;
            }
        }
@@ -1945,9 +1957,7 @@
        ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        int sendDay = serviceTask != null && serviceTask.getSendDay() != null ? serviceTask.getSendDay().intValue() : 1;
        Date deadline = DateUtils.addDays(serviceSubtask.getEndtime(), sendDay);
        int sumCompensateTime = CollectionUtils.isEmpty(serviceSubtaskPreachforms) ? 0 : serviceSubtaskPreachforms.stream()
                .mapToInt(x -> x.getCompensateTime() == null ? 0 : Integer.parseInt(x.getCompensateTime()))
                .sum();
        int sumCompensateTime = CollectionUtils.isEmpty(serviceSubtaskPreachforms) ? 0 : serviceSubtaskPreachforms.stream().mapToInt(x -> x.getCompensateTime() == null ? 0 : Integer.parseInt(x.getCompensateTime())).sum();
        sumCompensateTime = sumCompensateTime + 1;
        // 只补偿最后一步自身的补偿时间(天),脏值一律按 0 兜底
        deadline = DateUtils.addDays(deadline, sumCompensateTime);
@@ -1994,8 +2004,7 @@
            serviceSubtaskVO.setVisitCount(1);
            serviceSubtaskVO.setTaskid(serviceTask.getTaskid());
            serviceSubtaskVO.setEndOutHospTime(deadline);
            List<ServiceSubtask> serviceSubtaskList
                    = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
            List<ServiceSubtask> serviceSubtaskList = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
            log.info("【dealSubtaskDeadline】获取到{}个超时任务, taskid={}", serviceSubtaskList.size(), serviceTask.getTaskid());
            for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
                PatMedInhosp patMedInhospVo = new PatMedInhosp();
@@ -2018,8 +2027,7 @@
                Integer type = 1;
                //新增再次随访任务
                ServiceSubtask serviceSubtaskAgain = iPatMedInhospService.boxedServiceSubtask(nextServiceTask,
                        patMedInhosp, patArchive, config, type);
                ServiceSubtask serviceSubtaskAgain = iPatMedInhospService.boxedServiceSubtask(nextServiceTask, patMedInhosp, patArchive, config, type);
                serviceSubtaskAgain.setId(null);
                serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
                serviceSubtaskAgain.setVisitCount(serviceSubtask.getVisitCount() + 1);
ruoyi-task/src/main/java/com/ruoyi/task/api/contract/VisitTaskCreateApiImpl.java
@@ -12,6 +12,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.ruoyi.task.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.ServiceSubtask;
import com.ruoyi.task.domain.ServiceTask;
@@ -334,8 +335,7 @@
        //    见 OuthospSubtaskDraft 的类注释。
        // ------------------------------------------------------------------
        if (d instanceof com.ruoyi.common.core.service.dto.OuthospSubtaskDraft) {
            com.ruoyi.common.core.service.dto.OuthospSubtaskDraft o =
                    (com.ruoyi.common.core.service.dto.OuthospSubtaskDraft) d;
            com.ruoyi.common.core.service.dto.OuthospSubtaskDraft o = (com.ruoyi.common.core.service.dto.OuthospSubtaskDraft) d;
            // hospno 已上提到父类 VisitSubtaskDraft(上面统一映射过了),这里不再重复
            // 只有父类没有的字段才在这里补映射(其余如 sendname/sfzh/phone/sex/age 父类已有)
            e.setAdmindate(o.getAdmitdate());
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceTaskServiceImpl.java
@@ -296,23 +296,6 @@
            }
        }
        if (CollectionUtils.isNotEmpty(selectServiceSubtaskList)) {
            //将当前方式的发送状态改成已领取
            ServiceSubtask serviceSubtask = selectServiceSubtaskList.get(0);
            ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
            serviceSubtaskPreachform.setPreachform(serviceSubtask.getCurrentPreachform());
            serviceSubtaskPreachform.setSubid(serviceSubtask.getId());
            serviceSubtaskPreachform.setTaskid(serviceTask.getTaskid());
            List<ServiceSubtaskPreachform> serviceSubtaskPreachforms = taskPreachformPort.selectServiceSubtaskPreachformList(serviceSubtaskPreachform);
            if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) {
                ServiceSubtaskPreachform ssp = serviceSubtaskPreachforms.get(0);
                //人工无-3已领取状态(service_subtask_preachform.sendstate = 3)
                if(StringUtils.isNotEmpty(ssp.getPreachform()) && !ssp.getPreachform().equals("1")){
                    ssp.setSendstate("3");
                    taskPreachformPort.updateServiceSubtaskPreachform(ssp);
                }
            }
        }
        if (serviceTask != null) {
            if (StringUtils.isNotEmpty(serviceTask.getKcb())) map.put("kcb", serviceTask.getKcb());
            map.put("script", info);