smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -14,7 +14,7 @@
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.sms.SmsUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
@@ -967,6 +967,136 @@
    }
    /**
     * 首次随访任务超时后,按下一任务自动生成再次随访任务(随访中心)。
     * <p>
     * 定时任务入口 {@code RyTask#dealSubtaskDeadline} 使用。与手动入口一致的部分:
     * 表单/发送方式取下一任务、链路 upid + visit_count、发送方式明细初始化;
     * 差异:随访中心科室未配置时回退原任务科室,原任务置为发送失败(sendstate=5)。
     * <p>
     * 幂等:同一原任务只允许生成一条再次随访(Redis 锁 + upid 校验 + 原任务条件更新),
     * 多节点/多线程同时扫描时,后到的事务会在条件更新处拿到 0 行并回滚本次插入。
     *
     * @param serviceSubtask 已超时的首次随访子任务
     * @return 结果描述
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String addSubTaskAgainByDeadline(ServiceSubtask serviceSubtask) {
        if (ObjectUtils.isEmpty(serviceSubtask) || ObjectUtils.isEmpty(serviceSubtask.getId())) {
            return "再次随访任务创建失败,原任务不存在";
        }
        String lockToken = tryLockAgainTask(serviceSubtask.getId());
        if (lockToken == null) {
            return "再次随访任务正在创建中,请勿重复提交";
        }
        try {
            // 幂等校验:同一原任务只允许生成一条再次随访
            ServiceSubtaskEntity againQuery = new ServiceSubtaskEntity();
            againQuery.setUpid(serviceSubtask.getId().intValue());
            if (CollectionUtils.isNotEmpty(serviceSubtaskMapper.selectServiceSubtaskList(againQuery))) {
                return "无需再次随访,该任务已经再次随访过了";
            }
            // 原任务已被标记为不再随访(手动入口写过 is_visit_again=2)
            if (ObjectUtils.isNotEmpty(serviceSubtask.getIsVisitAgain()) && serviceSubtask.getIsVisitAgain() == 2) {
                return "无需再次随访,该任务已经再次随访过了";
            }
            ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
            if (ObjectUtils.isEmpty(serviceTask)) {
                return "再次随访任务创建失败,找不到对应任务 taskid = " + serviceSubtask.getTaskid();
            }
            if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) {
                return "再次随访任务创建失败,任务缺少 nexttaskid, taskid = " + serviceSubtask.getTaskid();
            }
            ServiceTask nextServiceTask = serviceTaskService.selectServiceTaskByTaskid(serviceTask.getNexttaskid());
            if (ObjectUtils.isEmpty(nextServiceTask)) {
                return "再次随访任务创建失败,找不到对应任务 nexttaskid = " + serviceTask.getNexttaskid();
            }
            PatMedInhosp patMedInhosp = selectOutHospRecord(serviceSubtask);
            if (ObjectUtils.isEmpty(patMedInhosp)) {
                return "再次随访任务创建失败,任务缺少对应的出院记录, taskid = " + serviceSubtask.getTaskid();
            }
            PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
            if (ObjectUtils.isEmpty(patArchive)) {
                return "再次随访任务创建失败,未找到患者档案, subTaskId = " + serviceSubtask.getId();
            }
            // 随访中心科室:未配置(或科室不存在)时回退原任务科室,保持超时入口原有行为
            SysDept visitDept = selectCenterDept(serviceSubtask.getOrgid());
            String config = configService.selectConfigByKey("visit.early.day");
            ServiceSubtask serviceSubtaskAgain = patMedInhospService.boxedServiceSubtask(nextServiceTask,
                    patMedInhosp, patArchive, config, 1);
            if (ObjectUtils.isEmpty(serviceSubtaskAgain) || ObjectUtils.isEmpty(serviceSubtaskAgain.getTaskid())) {
                return "再次随访任务创建失败,患者出院记录不完整或下一任务配置异常, subTaskId = " + serviceSubtask.getId();
            }
            Date now = new Date();
            serviceSubtaskAgain.setId(null);
            serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
            serviceSubtaskAgain.setVisitCount(ObjectUtils.isNotEmpty(serviceSubtask.getVisitCount())
                    ? serviceSubtask.getVisitCount() + 1 : 2);
            serviceSubtaskAgain.setSendstate(2L);
            serviceSubtaskAgain.setIsVisitAgain(1);
            serviceSubtaskAgain.setCreateTime(now);
            serviceSubtaskAgain.setUpdateTime(now);
            serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
            if (ObjectUtils.isNotEmpty(visitDept)) {
                serviceSubtaskAgain.setVisitDeptCode(visitDept.getDeptCode());
                serviceSubtaskAgain.setVisitDeptName(visitDept.getDeptName());
            } else {
                serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
                serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName());
            }
            if (serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain) != 1) {
                return "再次随访任务创建失败,再请次重试";
            }
            // 初始化发送方式明细,补偿(compensateTask)和超时判断依赖该表
            patMedInhospService.addServiceSubtaskPreachform(serviceSubtaskAgain, null);
            // 条件更新原任务:0 行说明已被其他请求生成,抛异常回滚本次插入的重复数据
            if (serviceSubtaskMapper.updateIsVisitAgainById(serviceSubtask.getId(), 2) != 1) {
                throw new BaseException("无需再次随访,该任务已经再次随访过了");
            }
            // 原任务超时,置为发送失败
            ServiceSubtask originUpdate = new ServiceSubtask();
            originUpdate.setId(serviceSubtask.getId());
            originUpdate.setSendstate(5L);
            originUpdate.setUpdateTime(now);
            serviceSubtaskMapper.updateServiceSubtask(originUpdate);
            return "再次随访任务创建成功";
        } finally {
            unlockAgainTask(serviceSubtask.getId(), lockToken);
        }
    }
    /**
     * 按 orgid + inhospid 取出院记录(超时再次随访用)。
     */
    private PatMedInhosp selectOutHospRecord(ServiceSubtask serviceSubtask) {
        PatMedInhosp patMedInhospVo = new PatMedInhosp();
        patMedInhospVo.setOrgid(serviceSubtask.getOrgid());
        patMedInhospVo.setInhospid(serviceSubtask.getInhospid());
        patMedInhospVo.setCry(1);
        List<PatMedInhosp> patMedInhospList = patMedInhospService.selectPatMedInhospList(patMedInhospVo);
        if (CollectionUtils.isEmpty(patMedInhospList)) {
            return null;
        }
        return patMedInhospList.get(0);
    }
    /**
     * 取随访中心科室,未配置或科室不存在时返回 null(由调用方回退原任务科室)。
     */
    private SysDept selectCenterDept(String orgid) {
        String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", orgid);
        if (StringUtils.isEmpty(centerDept)) {
            return null;
        }
        SysDept sysDeptVo = new SysDept();
        sysDeptVo.setDeptType("1");
        sysDeptVo.setOrgid(orgid);
        sysDeptVo.setDeptCode(centerDept);
        List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo);
        return CollectionUtils.isEmpty(sysDepts) ? null : sysDepts.get(0);
    }
    /**
     * 获取二次随访生成锁。Redis 异常时降级为数据库幂等校验,不阻断业务。
     */
    private String tryLockAgainTask(Long subId) {