陈昶聿
22 小时以前 a2dd5b42824eff9626d1f48e1561be85f17f6107
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;
@@ -964,6 +964,136 @@
        } finally {
            unlockAgainTask(serviceSubtask.getId(), lockToken);
        }
    }
    /**
     * 首次随访任务超时后,按下一任务自动生成再次随访任务(随访中心)。
     * <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);
    }
    /**
@@ -3488,6 +3618,10 @@
                if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                    serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
                }
                //及时随访人次:已完成且未超时
                if (isTimelyFollowUp(serviceSubtask)) {
                    serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
                }
                //人工已拨通总量
                if (ObjectUtils.isNotEmpty(serviceSubtask.getIsManuallyCalled())
@@ -3756,6 +3890,19 @@
    }
    /**
     * 是否属于「及时随访」:随访已完成且完成时状态为正常完成(未超时)。
     * <p>
     * finish_state:1-正常完成 2-延期完成 3-超时完成,只有 1 计入及时随访。
     *
     * @param serviceSubtask 随访子任务
     * @return true-计入及时随访人次
     */
    private boolean isTimelyFollowUp(ServiceSubtask serviceSubtask) {
        return serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate().equals(6L)
                && serviceSubtask.getFinishState() != null && serviceSubtask.getFinishState().equals(1L);
    }
    /**
     * 首次随访统计
     *
     * @param serviceSubtaskList
@@ -3786,6 +3933,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //首次出院随访
@@ -3955,6 +4106,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //再次出院随访
@@ -4212,7 +4367,8 @@
        serviceSubtaskStatistic.setDrname(serviceSubtaskList.get(0).getDrname());
        serviceSubtaskStatistic.setJoyCount(0);
        serviceSubtaskStatistic.setJoyAllCount(0);
        //出院人次
        serviceSubtaskStatistic.setDischargeCount(serviceSubtaskList.size());
        //获取随访统计
        if (subCounts == null) {
            subCounts = new ArrayList<>();
@@ -4486,6 +4642,7 @@
     * pendingFollowUpInfo - visitCount = 1 && sendstate = 2
     * followUpSuccessInfo - visitCount = 1 && sendstate = 6
     * followUpFailInfo - visitCount = 1 && (sendstate = 5 || sendstate = 7)
     * timelyFollowUpInfo - sendstate = 6 && finish_state = 1(沿用请求的 visitCount 口径)
     * manualInfo - visitCount = 1 && currentPreachForm = "1"
     * voiceInfo - visitCount = 1 && currentPreachForm = "3"
     * smsInfo - visitCount = 1 && currentPreachForm = "4"
@@ -4544,14 +4701,22 @@
    }
    private void applyHyperLinkInfoType(ServiceSubtaskCountReq serviceSubtaskCountReq, String hyperLinkInfoType) {
        //及时随访明细需要沿用请求的首次/再次口径
        Integer originVisitCount = serviceSubtaskCountReq.getVisitCount();
        serviceSubtaskCountReq.setVisitCount(null);
        serviceSubtaskCountReq.setSendstates(null);
        serviceSubtaskCountReq.setCurrentPreachform(null);
        serviceSubtaskCountReq.setExcep(null);
        serviceSubtaskCountReq.setTaskSituation(null);
        serviceSubtaskCountReq.setFinishState(null);
        switch (hyperLinkInfoType) {
            case "dischargeCountInfo":
                break;
            case "timelyFollowUpInfo":
                serviceSubtaskCountReq.setVisitCount(originVisitCount);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L)));
                serviceSubtaskCountReq.setFinishState(1L);
                break;
            case "followUpNeededAllInfo":
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 5L, 6L, 7L)));
@@ -4709,7 +4874,7 @@
     * @return
     */
    @Override
    @Cacheable(value = "getSfStatisticsScript", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#serviceSubtaskCountReq.toString().getBytes())", unless = "#result == null or #result.isEmpty()")
//    @Cacheable(value = "getSfStatisticsScript", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#serviceSubtaskCountReq.toString().getBytes())", unless = "#result == null or #result.isEmpty()")
    public List<ServiceSubtaskStatistic> getSfStatisticsScript(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        log.info("getSfStatisticsScript的入参为:{}", serviceSubtaskCountReq);
        List<ServiceSubtaskStatistic> serviceSubtaskStatistics = new ArrayList<>();
@@ -4717,13 +4882,13 @@
        // 根据条件进行分组
        Map<String, List<ServiceSubtask>> collect = new HashMap<>();
        String groupKey = "drcode";
        if (serviceSubtaskCountReq.getLeavehospitaldistrictcodes() != null && !serviceSubtaskCountReq.getLeavehospitaldistrictcodes().isEmpty()) {
        if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 1) {
            if (serviceSubtaskCountReq.getDrcode() != null && !serviceSubtaskCountReq.getDrcode().isEmpty()) {
                groupKey = "drcode";
            } else {
                groupKey = "leavehospitaldistrictcode";
            }
        } else if (serviceSubtaskCountReq.getDeptcodes() != null && !serviceSubtaskCountReq.getDeptcodes().isEmpty()) {
        } else if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 2) {
            if (serviceSubtaskCountReq.getDrcode() != null && !serviceSubtaskCountReq.getDrcode().isEmpty()) {
                groupKey = "drcode";
            } else {
@@ -4839,8 +5004,7 @@
    }
    @Override
    public Map<String, ServiceSubtaskScriptCount> getSfStatisticsScriptDetails(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        Map<String, ServiceSubtaskScriptCount> result = new HashMap<>();
    public Map<String, Object> getSfStatisticsScriptDetails(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        List<ServiceSubtaskScriptCount> allCounts = new ArrayList<>();
        List<ServiceSubtaskScriptCount> allItemCounts = new ArrayList<>();
        String type = serviceSubtaskCountReq.getType();
@@ -4864,7 +5028,23 @@
            allItemCounts = serviceSubtaskMapper.getSfStatisticsScriptItemDetails(serviceSubtaskCountReq);
        }
        for (ServiceSubtaskScriptCount count : allCounts) {
        //返回结构是 Map,且不传 type 时会执行两条 SQL,PageHelper 没法对整体分页,
        //所以在题目合并完成之后按题目(scriptid)在这里分页,保证 total 和每页数据都是题目维度的。
        allCounts.sort(Comparator
                .comparing(ServiceSubtaskScriptCount::getSubtaskType, Comparator.nullsLast(String::compareTo))
                .thenComparing(ServiceSubtaskScriptCount::getScriptid, Comparator.nullsLast(Long::compareTo)));
        int pageNum = ObjectUtils.isNotEmpty(serviceSubtaskCountReq.getPageNum()) && serviceSubtaskCountReq.getPageNum() > 0
                ? serviceSubtaskCountReq.getPageNum() : 1;
        int pageSize = ObjectUtils.isNotEmpty(serviceSubtaskCountReq.getPageSize()) && serviceSubtaskCountReq.getPageSize() > 0
                ? serviceSubtaskCountReq.getPageSize() : 10;
        int total = allCounts.size();
        int fromIndex = (int) Math.min((long) (pageNum - 1) * pageSize, total);
        int toIndex = Math.min(fromIndex + pageSize, total);
        List<ServiceSubtaskScriptCount> pageCounts = allCounts.subList(fromIndex, toIndex);
        Map<String, ServiceSubtaskScriptCount> pageData = new LinkedHashMap<>();
        for (ServiceSubtaskScriptCount count : pageCounts) {
            Map<String, ServiceSubtaskScriptCount> itemCountMap = new HashMap<>();
            //问卷加上空白选项
            if (ObjectUtils.isNotEmpty(count.getTemplateid()) && ObjectUtils.isNotEmpty(count.getSubtaskType()) && count.getSubtaskType().equals("2")) {
@@ -4895,31 +5075,42 @@
                }
            }
            allItemCounts.forEach(itemCount -> {
                if (ObjectUtils.isNotEmpty(count.getScriptid()) && ObjectUtils.isNotEmpty(itemCount.getScriptid())) {
                    if (count.getScriptid().equals(itemCount.getScriptid())) {
                        //放入统计信息
                        itemCount.setScriptContent(count.getScriptContent());
                        itemCount.setScriptType(count.getScriptType());
                        itemCount.setSubtaskType(count.getSubtaskType());
                        itemCount.setCompletedQuantity(count.getCompletedQuantity());
                        itemCount.setAllQuantity(count.getAllQuantity());
                        itemCount.setCompletedPercentage(count.getCompletedPercentage());
                        itemCount.setTemplateid(count.getTemplateid());
                        //计算选择比例
                        int chosenQuantity = ObjectUtils.isNotEmpty(itemCount.getChosenQuantity()) ? itemCount.getChosenQuantity() : 0;
                        int completedQuantity = itemCount.getCompletedQuantity();
                        if (ObjectUtils.isNotEmpty(completedQuantity) && completedQuantity != 0) {
                            BigDecimal chosenPercentage = (new BigDecimal(chosenQuantity)).divide(new BigDecimal(completedQuantity), 2, RoundingMode.HALF_UP);
                            itemCount.setChosenPercentage(chosenPercentage.toString());
                        }
                        itemCountMap.put(itemCount.getOptionText(), itemCount);
                //明细行已按“题目内容+任务类型”聚合并带出 script_content,不能用 scriptid 匹配:
                //同一道题会被复制到多个任务模板,每个模板的 scriptid 都不同
                boolean sameQuestion = ObjectUtils.isNotEmpty(count.getScriptContent())
                        && ObjectUtils.isNotEmpty(itemCount.getScriptContent())
                        && count.getScriptContent().equals(itemCount.getScriptContent())
                        && Objects.equals(count.getSubtaskType(), itemCount.getSubtaskType());
                if (sameQuestion) {
                    //放入统计信息
                    itemCount.setScriptContent(count.getScriptContent());
                    itemCount.setScriptType(count.getScriptType());
                    itemCount.setSubtaskType(count.getSubtaskType());
                    itemCount.setCompletedQuantity(count.getCompletedQuantity());
                    itemCount.setAllQuantity(count.getAllQuantity());
                    itemCount.setCompletedPercentage(count.getCompletedPercentage());
                    itemCount.setTemplateid(count.getTemplateid());
                    //计算选择比例
                    int chosenQuantity = ObjectUtils.isNotEmpty(itemCount.getChosenQuantity()) ? itemCount.getChosenQuantity() : 0;
                    int completedQuantity = itemCount.getCompletedQuantity();
                    if (ObjectUtils.isNotEmpty(completedQuantity) && completedQuantity != 0) {
                        BigDecimal chosenPercentage = (new BigDecimal(chosenQuantity)).divide(new BigDecimal(completedQuantity), 2, RoundingMode.HALF_UP);
                        itemCount.setChosenPercentage(chosenPercentage.toString());
                    }
                    itemCountMap.put(itemCount.getOptionText(), itemCount);
                }
            });
            List<ServiceSubtaskScriptCount> itemCounts = new ArrayList<>(itemCountMap.values());
            count.setDetails(itemCounts);
            result.put(count.getScriptid().toString(), count);
            pageData.put(count.getScriptid().toString(), count);
        }
        Map<String, Object> result = new HashMap<>();
        // total 是题目总数(已按题目合并),rows/data 是当前页数据
        result.put("total", total);
        result.put("pageNum", pageNum);
        result.put("pageSize", pageSize);
        result.put("rows", new ArrayList<>(pageCounts));
        result.put("data", pageData);
        return result;
    }
@@ -5021,6 +5212,7 @@
        total.setPendingFollowUp(0L);
        total.setFollowUpSuccess(0L);
        total.setFollowUpFail(0L);
        total.setTimelyFollowUp(0L);
        total.setManual(0L);
        total.setSms(0L);
        total.setWeChat(0L);
@@ -5052,6 +5244,7 @@
            total.setPendingFollowUp(total.getPendingFollowUp() + (stat.getPendingFollowUp() != null ? stat.getPendingFollowUp() : 0L));
            total.setFollowUpSuccess(total.getFollowUpSuccess() + (stat.getFollowUpSuccess() != null ? stat.getFollowUpSuccess() : 0L));
            total.setFollowUpFail(total.getFollowUpFail() + (stat.getFollowUpFail() != null ? stat.getFollowUpFail() : 0L));
            total.setTimelyFollowUp(total.getTimelyFollowUp() + (stat.getTimelyFollowUp() != null ? stat.getTimelyFollowUp() : 0L));
            total.setManual(total.getManual() + (stat.getManual() != null ? stat.getManual() : 0L));
            total.setSms(total.getSms() + (stat.getSms() != null ? stat.getSms() : 0L));
            total.setWeChat(total.getWeChat() + (stat.getWeChat() != null ? stat.getWeChat() : 0L));