liusheng
4 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospTaskOrchestrator.java
@@ -21,12 +21,26 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * 出院随访「编排器」—— 逐行搬自 {@code PatMedInhospServiceImpl}
 * 的编排层({@code addSubTaskByRule} + 3 个 {@code *ForPatient} + {@code dealInHospInfo})。
 *
 * <h3>⚠️ 2026-10 口径变更:一个患者(一次出院)只生成 <b>一条</b> 随访子任务</h3>
 * 搬迁时本类是"逐行等价"的(原实现按维度全生成:每个 {@code *ForPatient} 把命中的
 * <b>所有</b>长期任务各写一条)。业务确认改为:
 * <ol>
 *   <li>维度按 {@code service_task_rule} 优先级排序尝试:首要 → 次要 → 通用兜底(科室、病区);</li>
 *   <li>第一个真正落库成功的维度即停止 → 只留一条;</li>
 *   <li>无规则时按 {@code service_taskdept} 的科室/病区"通用模块"生成;</li>
 *   <li>标志位统一回写,保证 {@code deptcheck_flag} 不再停在 0(防重复扫描);
 *       并在写库前按 {@code patid+inhospid+taskid} 判重(见 {@code VisitTaskCreateApi#existsSubtask})。</li>
 * </ol>
 * 因此 {@code verify_inhospvisit_orchestration_semantics.py} 里 O18~O23 那 6 条
 * "搬迁等价"断言已被改为"口径变更"断言(脚本里单独分组并注明依据)。
 *
 * <h3>来源对照(@ b25c3b51)</h3>
 * <table border="1">
@@ -74,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}")
@@ -122,165 +146,190 @@
    /**
     * 按 {@code service_task_rule} 的「首要(1)/次要(2)」规则编排科室/病区/疾病三个维度。
     * <p>
     * 逐行对应原 {@code addSubTaskByRule(String config, List<PatMedInhosp> patMedInhosps)}。
     * <p>
     * ⚠️ 规则查询 {@code serviceTaskRuleMapper.selectServiceTaskRuleByDeptOrWard} 仍在 smartor,
     * 所以本方法接收<b>已查好的规则</b>(由调用方 {@code InhospVisitTaskService} 通过
     * {@code PublicityDataPort} 取——该端口已含"按科室/病区查规则"的能力,见下)。
     * 这样做是为了避免为"规则表"再加一个 Mapper 或端口。
     *
     * @param inhosp 待处理的出院患者
     * <h3>⚠️ 2026-10 口径变更:一个患者(一次出院)只生成 <b>一条</b> 随访子任务</h3>
     * 原实现(以及搬迁后的等价版)是按维度"全生成":先跑所有「首要」维度的
     * {@code *ForPatient},而每个 {@code *ForPatient} 又把命中的<b>所有</b>长期任务各写一条。
     * 结果是同一患者一次出院能生成几十条(线上实测 27 条),而且因为扫描键
     * {@code deptcheck_flag} 在"只有病区/疾病维度"的规则下不会被置 1,
     * 患者每轮扫描都被重新处理 → 重复子任务持续累积。
     * <p>
     * 现口径(业务确认):
     * <ol>
     *   <li>把三个维度按规则优先级排成一个<b>尝试顺序</b>:
     *       首要(1) → 次要(2) → 通用兜底(科室、病区);同优先级内固定 科室 &gt; 病区 &gt; 疾病。
     *       {@code service_task_rule} 没有该患者科室/病区对应的规则时 → 顺序为 科室 → 病区
     *       (即"按 {@code service_taskdept} 里的通用模块生成")。</li>
     *   <li>按顺序逐个维度尝试,<b>第一个真正落库成功(或已存在)的维度即停止</b> —— 只留一条。</li>
     *   <li>最后统一回写标志位:命中维度 = {@code 1};其余维度 = {@code 2}(跳过),
     *       且<b>无论命中哪个维度,{@code deptcheck_flag} 一定会被写成非 0</b>
     *       —— 这就是"不再被下一轮扫描重复生成"的保证。</li>
     * </ol>
     * 另有一道兜底在 {@code InhospSubtaskWriter#writeInSubTask}:落库前按
     * {@code patid + inhospid + taskid} 判重,见 {@link VisitTaskCreateApi#existsSubtask}。
     *
     * <h3>站点差异(保留)</h3>
     * {@code xh} 只做科室维度 —— {@link #addDimension} 里统一挡掉病区/疾病。
     *
     * @param inhosp  待处理的出院患者
     * @param archive 患者档案
     * @param rule 已查好的规则;{@code null} = 未配置规则(全部生成)
     * @param config {@code sys_config: visit.early.day}
     * @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 {
            if (rule == null) {
                // 原 L1709-1716:规则为空,全部生成
                log.info("【基于规则】患者{}未配置规则,执行全部生成逻辑", inhosp.getInhospid());
                addDeptSubTaskForPatient(inhosp, archive, config);
                if (!SITE_XH.equals(active)) {
                    addWardSubTaskForPatient(inhosp, archive, config);
                    addIcd10SubTaskForPatient(inhosp, archive, config);
                }
                return;
            }
            // 原 L1717-1726:有规则,按规则执行
            log.info("【基于规则】患者{}查询到规则:deptRule={}, wradRule={}, diagRule={}",
                    inhosp.getInhospid(), rule.getDeptRule(), rule.getWradRule(), rule.getDiagRule());
            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);
            boolean deptIsPrimary = "1".equals(rule.getDeptRule());
            boolean wardIsPrimary = "1".equals(rule.getWradRule());
            boolean diagIsPrimary = "1".equals(rule.getDiagRule());
            boolean deptIsSecondary = "2".equals(rule.getDeptRule());
            boolean wardIsSecondary = "2".equals(rule.getWradRule());
            boolean diagIsSecondary = "2".equals(rule.getDiagRule());
            boolean hasPrimaryRule = deptIsPrimary || wardIsPrimary || diagIsPrimary;
            if (!hasPrimaryRule) {
                // 原 L1829-1837:没有首要规则,全部生成
                log.info("【基于规则】无首要规则,执行全部生成逻辑");
                addDeptSubTaskForPatient(inhosp, archive, config);
                if (!SITE_XH.equals(active)) {
                    addWardSubTaskForPatient(inhosp, archive, config);
                    addIcd10SubTaskForPatient(inhosp, archive, config);
                }
                return;
            }
            // 原 L1730-1754:先执行所有规则=1 的方法
            int deptGenCount = 0;
            int wardGenCount = 0;
            int diagGenCount = 0;
            if (deptIsPrimary) {
                log.info("【基于规则】执行科室首要规则");
                deptGenCount = addDeptSubTaskForPatient(inhosp, archive, config);
            }
            if (!SITE_XH.equals(active)) {
                if (wardIsPrimary) {
                    log.info("【基于规则】执行病区首要规则");
                    wardGenCount = addWardSubTaskForPatient(inhosp, archive, config);
                }
                if (diagIsPrimary) {
                    log.info("【基于规则】执行疾病首要规则");
                    diagGenCount = addIcd10SubTaskForPatient(inhosp, archive, config);
            Integer hit = null;
            for (Integer type : order) {
                log.info("【基于规则】患者{}尝试维度{}({})", inhosp.getInhospid(), type, dimensionName(type));
                if (generateOneForDimension(inhosp, archive, type, config)) {
                    hit = type;
                    break;
                }
            }
            int primaryGeneratedCount = deptGenCount + wardGenCount + diagGenCount;
            log.info("【基于规则】首要规则执行结果: deptGen={}, wardGen={}, diagGen={}, total={}",
                    deptGenCount, wardGenCount, diagGenCount, primaryGeneratedCount);
            String remark = inhosp.getRemark();
            if (primaryGeneratedCount > 0) {
                // 原 L1758-1796:至少一个首要规则成功
                log.info("【基于规则】首要规则有成功(count={}),将次要规则对应flag设为2", primaryGeneratedCount);
                if (deptIsSecondary) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsSecondary) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                    }
                    if (diagIsSecondary) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                    }
                }
                if (deptIsPrimary && deptGenCount > 0) {
                    updateCheckFlag(inhosp, 1, "1", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则生成成功" : "科室首要规则生成成功");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary && wardGenCount > 0) {
                        updateCheckFlag(inhosp, 2, "1", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则生成成功" : "病区首要规则生成成功");
                    }
                    if (diagIsPrimary && diagGenCount > 0) {
                        updateCheckFlag(inhosp, 3, "1", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则生成成功" : "疾病首要规则生成成功");
                    }
                }
                if (deptIsPrimary && deptGenCount == 0) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则生成失败" : "科室首要规则生成失败");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary && wardGenCount == 0) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则生成失败" : "病区首要规则生成失败");
                    }
                    if (diagIsPrimary && diagGenCount == 0) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则生成失败" : "疾病首要规则生成失败");
                    }
                }
            } else {
                // 原 L1797-1828:所有首要规则全部失败 → 把 =1 的置 2,然后执行次要规则
                log.info("【基于规则】首要规则全部失败,将=1的flag设为2,开始执行次要规则");
                if (deptIsPrimary) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则全部失败" : "科室首要规则全部失败");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则全部失败" : "病区首要规则全部失败");
                    }
                    if (diagIsPrimary) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则全部失败" : "疾病首要规则全部失败");
                    }
                }
                if (deptIsSecondary) {
                    log.info("【基于规则】执行科室次要规则");
                    addDeptSubTaskForPatient(inhosp, archive, config);
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsSecondary) {
                        log.info("【基于规则】执行病区次要规则");
                        addWardSubTaskForPatient(inhosp, archive, config);
                    }
                    if (diagIsSecondary) {
                        log.info("【基于规则】执行疾病次要规则");
                        addIcd10SubTaskForPatient(inhosp, archive, config);
                    }
                }
            }
            writeFlagsAfterRun(inhosp, hit);
        } catch (Exception e) {
            // 原 L1839-1845
            e.printStackTrace();
            log.error("【基于规则】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            updateCheckFlag(inhosp, 1, "2", "基于规则处理异常");
        }
    }
    /**
     * 维度尝试顺序(2026-10 口径)。
     * <p>
     * 规则为 {@code null} → {@code [科室, 病区]}:按 {@code service_taskdept} 里该患者的
     * 科室/病区"通用模块"生成;这是业务明确要求的兜底口径(此时<b>不进入疾病维度</b>)。
     * <p>
     * 有规则 → {@code [首要维度…, 次要维度…, 科室, 病区]}:
     * 首要优先、次要其后,最后用通用兜底保证"能生成一条就生成一条"。
     */
    private List<Integer> resolveDimensionOrder(VisitTaskRule rule) {
        boolean xhSite = SITE_XH.equals(active);
        List<Integer> order = new ArrayList<>(4);
        if (rule == null) {
            addDimension(order, 1, xhSite);
            addDimension(order, 2, xhSite);
            return order;
        }
        boolean deptIsPrimary = "1".equals(rule.getDeptRule());
        boolean wardIsPrimary = "1".equals(rule.getWradRule());
        boolean diagIsPrimary = "1".equals(rule.getDiagRule());
        boolean deptIsSecondary = "2".equals(rule.getDeptRule());
        boolean wardIsSecondary = "2".equals(rule.getWradRule());
        boolean diagIsSecondary = "2".equals(rule.getDiagRule());
        // 同优先级内固定顺序:科室 → 病区 → 疾病
        if (deptIsPrimary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsPrimary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsPrimary) {
            addDimension(order, 3, xhSite);
        }
        if (deptIsSecondary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsSecondary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsSecondary) {
            addDimension(order, 3, xhSite);
        }
        // 通用兜底:规则里配的维度全都取不到任务时,仍按 service_taskdept 的 科室/病区 生成一条
        addDimension(order, 1, xhSite);
        addDimension(order, 2, xhSite);
        return order;
    }
    /**
     * 追加一个候选维度;{@code xh} 站点只允许科室维度(原 {@code !active.equals("xh")} 的等价写法)
     */
    private void addDimension(List<Integer> order, int type, boolean xhSite) {
        if (xhSite && type != 1) {
            return;
        }
        if (!order.contains(type)) {
            order.add(type);
        }
    }
    private String dimensionName(int type) {
        switch (type) {
            case 1:
                return "科室";
            case 2:
                return "病区";
            case 3:
                return "疾病";
            default:
                return "未知(" + type + ")";
        }
    }
    /**
     * 尝试一个维度,返回"是否真的生成了(或本来就已经有)一条子任务"
     */
    private boolean generateOneForDimension(VisitInhospRow inhosp, VisitArchiveRow archive, int type, String config) {
        switch (type) {
            case 1:
                return addDeptSubTaskForPatient(inhosp, archive, config) > 0;
            case 2:
                return addWardSubTaskForPatient(inhosp, archive, config) > 0;
            case 3:
                return addIcd10SubTaskForPatient(inhosp, archive, config) > 0;
            default:
                return false;
        }
    }
    /**
     * 一轮生成结束后的标志位回写。
     * <p>
     * ⚠️ 关键点:扫描键是 {@code pat_med_inhosp.deptcheck_flag = '0'},
     * 所以<b>不论命中哪个维度,deptcheck_flag 都必须被写成 1 或 2</b>,
     * 否则下一轮扫描会把这个患者再处理一遍(2026-10 重复生成的直接原因)。
     * <p>
     * 没生成时({@code hit == null})刻意<b>只补扫描键</b>:各维度方法已经把更具体的原因
     * ("该患者所在科室未配置离院长期任务;" / "生成子任务失败 1" …)写进
     * {@code long_task_reason} 了,这里先用一次重读判断 deptcheck 是否已经非 0,
     * 避免把它覆盖成泛泛的"未生成随访任务"。
     *
     * @param hit 命中的维度;{@code null} = 三个维度都没能生成
     */
    private void writeFlagsAfterRun(VisitInhospRow inhosp, Integer hit) {
        boolean xhSite = SITE_XH.equals(active);
        if (hit == null) {
            VisitInhospRow fresh = inhospMapper.selectByInhospid(inhosp.getInhospid());
            String current = fresh == null ? null : fresh.getDeptcheckFlag();
            if (current == null || "0".equals(current)) {
                updateCheckFlag(inhosp, 1, "2", composeRemark(inhosp, "未生成随访任务(无可用长期任务)"));
            }
            return;
        }
        for (int type = 1; type <= 3; type++) {
            if (xhSite && type != 1) {
                continue;
            }
            if (hit == type) {
                updateCheckFlag(inhosp, type, "1", composeRemark(inhosp, "按优先级生成一条随访任务(维度=" + dimensionName(type) + ")"));
            } else {
                updateCheckFlag(inhosp, type, "2", composeRemark(inhosp, "已生成一条随访任务(维度=" + dimensionName(hit) + "),本维度跳过"));
            }
        }
    }
    /**
     * 拼 reason:沿用原实现的 {@code remark + ";xxx"} 形式
     */
    private String composeRemark(VisitInhospRow inhosp, String text) {
        String remark = inhosp.getRemark();
        return StringUtils.isNotEmpty(remark) ? remark + ";" + text : text;
    }
    /**
@@ -296,11 +345,14 @@
    /**
     * 为单个患者生成科室维度的子任务。
     * <p>
     * ⚠️ 2026-10 口径变更:命中多个长期任务时<b>只生成第一条真正落库的任务</b>
     * (原实现是 {@code for (t : taskdepts) writeInSubTask(...); generatedCount++;},
     * 一个科室配 N 个长期任务就会生成 N 条 —— 是"同一患者几十条"的主因)。
     *
     * @return 生成的子任务数量
     * @return 生成的子任务数量(0 或 1)
     */
    public int addDeptSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            // 原 L1857-1864:按患者所在科室查该科室的长期任务(dept_type=1 + service_type=2)
            VisitTaskdeptRow query = new VisitTaskdeptRow();
@@ -320,13 +372,15 @@
                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) {
                    subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config);
                    generatedCount++;
                    // 一个患者只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config)) {
                        return 1;
                    }
                    log.info("【科室任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -339,7 +393,7 @@
            u.setLongTaskReason("科室任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
@@ -348,11 +402,12 @@
    /**
     * 为单个患者生成病区维度的子任务。
     * <p>
     * ⚠️ 同上:命中多个长期任务时只生成第一条真正落库的(原实现是全生成)。
     *
     * @return 生成的子任务数量
     * @return 生成的子任务数量(0 或 1)
     */
    public int addWardSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            String errorreason = null;
            List<VisitTaskdeptRow> taskdeptList = null;
@@ -381,13 +436,15 @@
                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) {
                    subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config);
                    generatedCount++;
                    // 一个患者只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config)) {
                        return 1;
                    }
                    log.info("【病区任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -400,7 +457,7 @@
            u.setLongTaskReason("病区任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
@@ -409,11 +466,12 @@
    /**
     * 为单个患者生成疾病维度的子任务。
     * <p>
     * ⚠️ 同上:命中多个长期任务时只生成第一条真正落库的(原实现是全生成)。
     *
     * @return 生成的子任务数量
     * @return 生成的子任务数量(0 或 1)
     */
    public int addIcd10SubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            // 原 L1945-1947
            if (StringUtils.isEmpty(inhosp.getLeaveicd10code())) {
@@ -452,16 +510,19 @@
                int flag = 0;
                for (VisitTaskdiagRow t : taskdiags) {
                    // 原 L1975-1983:必须科室或病区能匹配上才建
                    if (StringUtils.isNotEmpty(t.getDeptCode())
                            && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                        subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config);
                        generatedCount++;
                    if (StringUtils.isNotEmpty(t.getDeptCode()) && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                        flag = 1;
                    } else if (StringUtils.isNotEmpty(t.getWardCode())
                            && t.getWardCode().equals(inhosp.getLeavehospitaldistrictcode())) {
                        subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config);
                        generatedCount++;
                        // 一个患者只留一条:第一条真正落库的即返回
                        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())) {
                        flag = 1;
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选", inhosp.getInhospid(), t.getTaskId());
                    }
                }
                if (flag == 0) {
@@ -484,7 +545,7 @@
            u.setLongTaskReason("疾病任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
@@ -510,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;
            }
@@ -531,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("患者再入院");
@@ -546,7 +604,9 @@
        }
    }
    /** 景宁机构ID(原 L1338;与 InhospSubtaskWriter 里那份是同一常量,已逐字核对) */
    /**
     * 景宁机构ID(原 L1338;与 InhospSubtaskWriter 里那份是同一常量,已逐字核对)
     */
    private static final String ORG_JINGNING = "47255004333112711A1001";
    // ==========================================================================
@@ -594,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() {
@@ -628,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();