liusheng
5 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
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
@@ -99,49 +109,62 @@
     * 把患者写进随访子任务表 —— 原 {@code writeInSubTask(Long taskid, Boolean check, PatMedInhosp,
     * PatArchive, Integer type, String config)}。
     *
     * @param taskid   命中的长期任务ID
     * @param check    是否需要做"再入院"去重校验
     * @param inhosp   出入院记录
     * @param archive  患者档案
     * @param type     0=药品宣教 / 1=离院科室 / 2=离院病区 / 3=疾病 / 4=入院科室 / 5=入院病区
     * @param config   {@code sys_config: visit.early.day}
     * @param taskid  命中的长期任务ID
     * @param check   是否需要做"再入院"去重校验
     * @param inhosp  出入院记录
     * @param archive 患者档案
     * @param type    0=药品宣教 / 1=离院科室 / 2=离院病区 / 3=疾病 / 4=入院科室 / 5=入院病区
     * @param config  {@code sys_config: visit.early.day}
     * @return <b>是否真的落了库</b>(或"本来就已经有")。
     * 返回值是 2026-10"一个患者只生成一条随访任务"改造的判据:
     * 编排层按维度优先级逐个尝试,只有拿到 {@code true} 才算该维度成功并停止。
     * 原实现是 {@code void},编排层用"调用了几次"当成功数 —— 那是错的
     * (任务未发布/被去重时同样是"生成了一条")。
     */
    public void 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);
            return;
            log.warn("【出入院随访】VisitTaskCreateApi 不可用(任务创建模块未装配),本患者不生成子任务。" + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid);
            return false;
        }
        VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid);
        if (serviceTask == null) {
            writeMissingTaskFlag(inhosp, type, longTaskReason, taskid);
            return;
            return false;
        }
        // 原 L729-731
        if ("1".equals(inhosp.getInhospstate()) && inhosp.getEndtime() == null) {
            return;
            return false;
        }
        // 原 L732-736:组装(副作用由本类实现,见 missingOutHospTime)
        VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(
                toTaskSource(serviceTask, inhosp), inhosp, archive, config, type,
                row -> onMissingOutHospTime(row));
        // ------------------------------------------------------------------
        // 【改造·2026-10】落库前最后一道判重:
        //   同一患者 + 同一次出院记录 + 同一任务,只要已经有一条未删除的子任务(含已停用的),
        //   就不再 insert。原因见 VisitTaskCreateApi#existsSubtask 的注释:
        //   患者被重复扫到时会走"停旧 + 再 insert 一条 sendstate=4"的分支,
        //   每跑一轮多一条,最终同一患者几十条。
        // ------------------------------------------------------------------
        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;
        }
        // serviceSubtask数据组装(副作用由本类实现,见 missingOutHospTime)
        VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(toTaskSource(serviceTask, inhosp), inhosp, archive, config, type, row -> onMissingOutHospTime(row));
        if (ObjectUtils.isEmpty(serviceSubtask)) {
            return;
            return false;
        }
        // 原 L737-753:就诊科室/病区填充
        // 就诊科室/病区填充
        if (type != null && type != 2) {
            // 科室和疾病 都用 科室填充
            if ("1".equals(inhosp.getInhospstate())) {
@@ -160,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();
@@ -202,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() + ",科室/病区患者再入院");
@@ -223,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("当前患者出院时间后有待执行的出院随访长期任务");
                                    }
@@ -238,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() + ",疾病患者再入院");
@@ -256,7 +273,7 @@
                        }
                    }
                } else {
                    // 原 L844-856:没有待执行任务
                    //没有待执行任务
                    markPhoneEmptyIfNeeded(serviceSubtask);
                    serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                    serviceSubtask.setCreateTime(new Date());
@@ -264,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());
@@ -278,29 +295,26 @@
                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);
            e.printStackTrace();
            return false;
        }
    }
    // ==========================================================================
    // 原 L691-725:任务不存在时按 type 写标志位
    // ==========================================================================
    // 任务不存在时按 type 写标志位
    private void writeMissingTaskFlag(VisitInhospRow inhosp, Integer type, String longTaskReason, Long taskid) {
        if (type == null) {
            return;
@@ -309,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>。
@@ -428,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())) {
@@ -458,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());
    }
    /**
@@ -473,21 +470,29 @@
     * ⚠️ {@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());
        t.setTaskDesc(dto.getTaskDesc());
        t.setType(dto.getType());
        t.setServiceType(dto.getServiceType());
        t.setTemplateid(dto.getTemplateid());
        t.setTemplatename(dto.getTemplatename());
        t.setLibtemplateid(dto.getLibtemplateid());
        t.setLibtemplatename(dto.getLibtemplatename());
        t.setSendDay(dto.getSendDay());
        t.setPreachform(dto.getPreachform());
        t.setPreachformDesc(dto.getPreachformDesc());
        t.setOrgid(dto.getOrgid());
        t.setCampusid(dto.getCampusid());
        // 以下 6 个同样属于"原 DtoConversionUtils 拷贝、随 insert 落库"的列
        t.setGuid(dto.getGuid());
        t.setPid(dto.getPid());
        t.setSendType(dto.getSendType());
        t.setIsupload(dto.getIsupload());
        t.setTextParam(dto.getTextParam());
        t.setUploadTime(dto.getUploadTime());
        t.setUpdateBy(dto.getUpdateBy());
        t.setCreateBy(dto.getCreateBy());
        return t;
@@ -529,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());
        }
    }