陈昶聿
15 小时以前 2d42d335a0abdabf02601b63b85dda82f5b0781b
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;
@@ -448,7 +448,7 @@
            if (StringUtils.isEmpty(serviceSubtask.getCurrentPreachform())) {
                if (StringUtils.isNotEmpty(serviceSubtask.getPreachform())) {
                    //默认取第一个
                    serviceSubtaskRes.setCurrentPreachform(serviceSubtask.getPreachform().split(",")[0]);
                    serviceSubtaskRes.setCurrentPreachform(serviceSubtask.getPreachform().split(",")[0].trim());
                }
            }
            PatMedInhosp inhosp = patMedInhospMapper.selectPatMedInhospByInhospid(serviceSubtask.getInhospid());
@@ -827,7 +827,7 @@
        }
        //丽水中医院根据下次任务ID生成再次随访
        if ("47231022633110211A2101".equals(serviceSubtask1.getOrgid())) {
            return addSubTaskAgainNextTaskId(serviceSubtask1);
            return addSubTaskAgainNextTaskId(serviceSubtask1, serviceSubtask);
        }
        //新增再次随访任务
        ServiceSubtask serviceSubtask2 = DtoConversionUtils.sourceToTarget(serviceSubtask, ServiceSubtask.class);
@@ -850,7 +850,7 @@
        return "再次随访任务创建失败,再请次重试";
    }
    public String addSubTaskAgainNextTaskId(ServiceSubtask serviceSubtask) {
    public String addSubTaskAgainNextTaskId(ServiceSubtask serviceSubtask, ServiceSubtask request) {
        if (ObjectUtils.isEmpty(serviceSubtask.getTaskid())) {
            log.error("再次随访任务创建失败,缺少TaskId, 再请次重试");
            return "再次随访任务创建失败,缺少TaskId, 再请次重试";
@@ -865,6 +865,11 @@
            if (ObjectUtils.isEmpty(serviceTask)) {
                log.error("再次随访任务创建失败,找不到对应任务 taskid = {}, 再请次重试", serviceSubtask.getTaskid());
                return "再次随访任务创建失败,找不到对应任务 taskid = " + serviceSubtask.getTaskid() + ", 再请次重试";
            }
            // nexttaskflag=0 表示任务链终止,不允许再生成
            if ("0".equals(serviceTask.getNexttaskflag())) {
                log.info("无需再次随访,任务已标记无后续随访, taskid = {}", serviceSubtask.getTaskid());
                return "无需再次随访,该任务无后续随访安排";
            }
            // 幂等校验:同一原任务只允许生成一条二次随访
            ServiceSubtaskEntity againQuery = new ServiceSubtaskEntity();
@@ -885,14 +890,16 @@
                log.error("再次随访任务创建失败,任务缺少 对应的出院记录, taskid = {}, 再请次重试", serviceSubtask.getTaskid());
                return "再次随访任务创建失败,任务缺少 对应的出院记录, taskid = " + serviceSubtask.getTaskid() + ", 再请次重试";
            }
            if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) {
                log.error("再次随访任务创建失败,任务缺少 nexttaskid, taskid = {}, 再请次重试", serviceSubtask.getTaskid());
                return "再次随访任务创建失败,任务缺少 nexttaskid, taskid = " + serviceSubtask.getTaskid() + ", 再请次重试";
            // 未配置 nexttaskid 时复用当前任务配置,防止历史任务直接失败
            Long nexttaskid = serviceTask.getNexttaskid();
            if (ObjectUtils.isEmpty(nexttaskid) || Long.valueOf(0L).equals(nexttaskid)) {
                nexttaskid = serviceTask.getTaskid();
                log.warn("再次随访任务未配置 nexttaskid,按当前任务配置生成, taskid = {}", nexttaskid);
            }
            ServiceTask nextServiceTask = serviceTaskService.selectServiceTaskByTaskid(serviceTask.getNexttaskid());
            ServiceTask nextServiceTask = serviceTaskService.selectServiceTaskByTaskid(nexttaskid);
            if (ObjectUtils.isEmpty(nextServiceTask)) {
                log.error("再次随访任务创建失败,找不到对应任务 nexttaskid = {}, 再请次重试", serviceTask.getNexttaskid());
                return "再次随访任务创建失败,找不到对应任务 nexttaskid = " + serviceTask.getNexttaskid() + ", 再请次重试";
                log.error("再次随访任务创建失败,找不到对应任务 nexttaskid = {}, 再请次重试", nexttaskid);
                return "再次随访任务创建失败,找不到对应任务 nexttaskid = " + nexttaskid + ", 再请次重试";
            }
            PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid());
            if (ObjectUtils.isEmpty(patArchive)) {
@@ -901,54 +908,83 @@
            }
            String config = configService.selectConfigByKey("visit.early.day");
            Integer type = 1;
            // 随访中心科室
            String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", serviceSubtask.getOrgid());
            if (ObjectUtils.isEmpty(centerDept)) {
                log.error("再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = {}, 再请次重试", serviceSubtask.getId());
                return "再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = " + serviceSubtask.getId() + ", 再请次重试";
            String resource = resolveAgainResource(request);
            if (StringUtils.isEmpty(resource)) {
                String requestResource = request == null ? null : request.getResource();
                log.error("再次随访任务创建失败,再次随访方式不合法, resource = {}, subTaskId = {}", requestResource, serviceSubtask.getId());
                return "再次随访任务创建失败,再次随访方式不合法, resource = " + requestResource + ", 再请次重试";
            }
            SysDept sysDeptVo = new SysDept();
            sysDeptVo.setDeptType("1");
            sysDeptVo.setOrgid(serviceSubtask.getOrgid());
            sysDeptVo.setDeptCode(centerDept);
            List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo);
            SysDept sysDept = new SysDept();
            if (CollectionUtils.isNotEmpty(sysDepts)) {
                sysDept = sysDepts.get(0);
            String visitDeptCode;
            String visitDeptName;
            if ("1".equals(resource)) {
                // 本科室随访:请求科室优先,未传时使用原任务出院科室;不依赖原任务 visitType
                String requestDeptCode = firstNonBlank(request == null ? null : request.getVisitDeptCode());
                String requestDeptName = firstNonBlank(request == null ? null : request.getVisitDeptName());
                if (StringUtils.isNotEmpty(requestDeptCode)) {
                    visitDeptCode = requestDeptCode;
                    visitDeptName = requestDeptName;
                } else {
                    visitDeptCode = serviceSubtask.getDeptcode();
                    visitDeptName = serviceSubtask.getDeptname();
                }
                if (StringUtils.isEmpty(visitDeptCode)) {
                    log.error("再次随访任务创建失败,本科室随访未找到随访科室, subTaskId = {}", serviceSubtask.getId());
                    return "再次随访任务创建失败,本科室随访未找到随访科室, subTaskId = " + serviceSubtask.getId() + ", 再请次重试";
                }
                SysDept ownDept = selectDeptByCode(serviceSubtask.getOrgid(), visitDeptCode);
                if (ObjectUtils.isNotEmpty(ownDept)) {
                    visitDeptCode = ownDept.getDeptCode();
                    visitDeptName = ownDept.getDeptName();
                } else if (StringUtils.isNotEmpty(requestDeptCode) || StringUtils.isEmpty(visitDeptName)) {
                    log.error("再次随访任务创建失败,本科室随访科室不存在, deptCode = {}, subTaskId = {}", visitDeptCode, serviceSubtask.getId());
                    return "再次随访任务创建失败,本科室随访科室不存在, deptCode = " + visitDeptCode + ", 再请次重试";
                }
            } else {
                log.error("再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = {}, 再请次重试", centerDept);
                return "再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = " + centerDept + ", 再请次重试";
                // 随访中心科室随访
                String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", serviceSubtask.getOrgid());
                if (ObjectUtils.isEmpty(centerDept)) {
                    log.error("再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = {}, 再请次重试", serviceSubtask.getId());
                    return "再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = " + serviceSubtask.getId() + ", 再请次重试";
                }
                SysDept sysDeptVo = new SysDept();
                sysDeptVo.setDeptType("1");
                sysDeptVo.setOrgid(serviceSubtask.getOrgid());
                sysDeptVo.setDeptCode(centerDept);
                List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo);
                SysDept sysDept = new SysDept();
                if (CollectionUtils.isNotEmpty(sysDepts)) {
                    sysDept = sysDepts.get(0);
                } else {
                    log.error("再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = {}, 再请次重试", centerDept);
                    return "再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = " + centerDept + ", 再请次重试";
                }
                visitDeptCode = sysDept.getDeptCode();
                visitDeptName = sysDept.getDeptName();
            }
            // 按下一任务生成二次随访子任务(表单、发送方式取自下一任务)
            ServiceSubtask serviceSubtaskAgain = patMedInhospService.boxedServiceSubtask(nextServiceTask,
                    patMedInhosp, patArchive, config, type);
            if (ObjectUtils.isEmpty(serviceSubtaskAgain) || ObjectUtils.isEmpty(serviceSubtaskAgain.getTaskid())) {
                log.error("再次随访任务创建失败,下一任务生成子任务为空, subTaskId = {}, nextTaskId = {}", serviceSubtask.getId(), serviceTask.getNexttaskid());
                log.error("再次随访任务创建失败,下一任务生成子任务为空, subTaskId = {}, nextTaskId = {}", serviceSubtask.getId(), nexttaskid);
                return "再次随访任务创建失败,患者出院记录不完整或下一任务配置异常, 再请次重试";
            }
            Date now = new Date();
            serviceSubtaskAgain.setId(null);
            serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
            serviceSubtaskAgain.setVisitCount(ObjectUtils.isNotEmpty(serviceSubtask.getVisitCount())
                    ? serviceSubtask.getVisitCount() + 1 : 1);
                    ? serviceSubtask.getVisitCount() + 1 : 2);
            serviceSubtaskAgain.setSendstate(2L);
            serviceSubtaskAgain.setIsVisitAgain(1);
            serviceSubtaskAgain.setCreateTime(now);
            serviceSubtaskAgain.setUpdateTime(now);
//            // 二次随访时间:上次随访完成时间(未完成则当前时间)+ 下一任务发送天数;过去时间兜底为当前时间
//            Date baseTime = ObjectUtils.isNotEmpty(serviceSubtask.getFinishtime()) ? serviceSubtask.getFinishtime() : now;
//            int intervalDays = ObjectUtils.isNotEmpty(nextServiceTask.getSendDay())
//                    ? nextServiceTask.getSendDay().intValue() : 0;
//            Date nextVisitTime = DateUtils.addDays(baseTime, intervalDays);
//            if (nextVisitTime.before(now)) {
//                nextVisitTime = now;
//            }
//            serviceSubtaskAgain.setVisitTime(nextVisitTime);
//            serviceSubtaskAgain.setLongSendTime(nextVisitTime);
            serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType());
            serviceSubtaskAgain.setVisitDeptCode(sysDept.getDeptCode());
            serviceSubtaskAgain.setVisitDeptName(sysDept.getDeptName());
            // 二次随访时间:上次随访完成时间(未完成则当前时间)+ 下一任务发送天数;过去时间兜底为当前时间
            applyAgainVisitTime(serviceSubtaskAgain, serviceSubtask, nextServiceTask, config, now);
            // 新任务的随访方式记录本次选择的再次随访方式;resource 本身不落库
            serviceSubtaskAgain.setVisitType(resource);
            serviceSubtaskAgain.setVisitDeptCode(visitDeptCode);
            serviceSubtaskAgain.setVisitDeptName(visitDeptName);
            int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain);
            if (i != 1) {
                return "再次随访任务创建失败,再请次重试";
@@ -964,6 +1000,230 @@
        } finally {
            unlockAgainTask(serviceSubtask.getId(), lockToken);
        }
    }
    /**
     * 解析再次随访方式:仅取请求参数 resource,不使用原任务 visitType;未传时默认随访中心。
     *
     * @return 1-本科室随访;2-随访中心随访;null-非法值
     */
    private String resolveAgainResource(ServiceSubtask request) {
        String resource = request == null ? null : request.getResource();
        if (StringUtils.isEmpty(resource) || StringUtils.isEmpty(resource.trim())) {
            return "2";
        }
        resource = resource.trim();
        return ("1".equals(resource) || "2".equals(resource)) ? resource : null;
    }
    private String firstNonBlank(String... values) {
        if (values == null) {
            return null;
        }
        for (String value : values) {
            if (StringUtils.isNotEmpty(value) && StringUtils.isNotEmpty(value.trim())) {
                return value.trim();
            }
        }
        return null;
    }
    /**
     * 按 orgid + deptCode 查询启用科室,查不到返回 null。
     */
    private SysDept selectDeptByCode(String orgid, String deptCode) {
        if (StringUtils.isEmpty(orgid) || StringUtils.isEmpty(deptCode)) {
            return null;
        }
        SysDept query = new SysDept();
        query.setDeptType("1");
        query.setOrgid(orgid);
        query.setDeptCode(deptCode);
        List<SysDept> deptList = sysDeptService.selectDeptList(query);
        return CollectionUtils.isEmpty(deptList) ? null : deptList.get(0);
    }
    /**
     * 二次随访时间:首次随访完成时间(未完成取当前时间)+ 下一任务发送天数,过去时间兜底为当前时间。
     * 通知类任务按 visit.early.day 提前发送。
     */
    private void applyAgainVisitTime(ServiceSubtask target, ServiceSubtask origin, ServiceTask nextTask,
                                     String earlyDayConfig, Date now) {
        if (target == null || origin == null || nextTask == null || now == null) {
            return;
        }
        Date baseTime = ObjectUtils.isNotEmpty(origin.getFinishtime()) ? origin.getFinishtime() : now;
        int intervalDays = ObjectUtils.isNotEmpty(nextTask.getSendDay()) ? nextTask.getSendDay().intValue() : 0;
        Date dueTime = DateUtils.addDays(baseTime, intervalDays);
        if (dueTime.before(now)) {
            dueTime = now;
        }
        target.setLongSendTime(dueTime);
        Date visitTime = dueTime;
        if ("3".equals(nextTask.getType())) {
            visitTime = DateUtils.addDays(dueTime, -parseIntConfig(earlyDayConfig));
            if (visitTime.before(now)) {
                visitTime = now;
            }
        }
        target.setVisitTime(visitTime);
    }
    private int parseIntConfig(String value) {
        if (StringUtils.isEmpty(value)) {
            return 0;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            log.warn("配置值不是合法整数,按0处理, value={}", value);
            return 0;
        }
    }
    /**
     * 首次随访任务超时后,按下一任务自动生成再次随访任务。
     * <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 ("0".equals(serviceTask.getNexttaskflag())) {
                return "无需再次随访,该任务无后续随访安排";
            }
            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();
            }
            String config = configService.selectConfigByKey("visit.early.day");
            String visitDeptCode;
            String visitDeptName;
            // 随访中心科室:未配置(或科室不存在)时回退原任务科室,保持超时入口原有行为
            SysDept centerDept = selectCenterDept(serviceSubtask.getOrgid());
            if (ObjectUtils.isNotEmpty(centerDept)) {
                visitDeptCode = centerDept.getDeptCode();
                visitDeptName = centerDept.getDeptName();
            } else {
                String fallbackDeptCode = firstNonBlank(serviceSubtask.getVisitDeptCode());
                if (StringUtils.isNotEmpty(fallbackDeptCode)) {
                    visitDeptCode = fallbackDeptCode;
                    visitDeptName = serviceSubtask.getVisitDeptName();
                } else {
                    visitDeptCode = serviceSubtask.getDeptcode();
                    visitDeptName = serviceSubtask.getDeptname();
                }
            }
            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("2");
            serviceSubtaskAgain.setVisitDeptCode(visitDeptCode);
            serviceSubtaskAgain.setVisitDeptName(visitDeptName);
            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);
        // 定时任务线程没有登录上下文,不能调用带 @DataScope 的 selectDeptList,否则会抛“获取用户信息异常”。
        List<SysDept> sysDepts = sysDeptService.selectSysDeptList(sysDeptVo);
        return CollectionUtils.isEmpty(sysDepts) ? null : sysDepts.get(0);
    }
    /**
@@ -1053,6 +1313,20 @@
    }
    /**
     * 科室编码去重,保持原顺序,避免 service_task.deptcode 超长和 service_taskdept 重复绑定
     */
    private String normalizeDeptCodes(String deptCodes) {
        if (StringUtils.isEmpty(deptCodes)) {
            return deptCodes;
        }
        return Arrays.stream(deptCodes.split(","))
                .map(String::trim)
                .filter(code -> StringUtils.isNotEmpty(code))
                .distinct()
                .collect(Collectors.joining(","));
    }
    /**
     * 单一任务
     *
     * @return 结果
@@ -1064,6 +1338,8 @@
            log.info("任务入参为空,请检查入参");
            throw new BaseException("任务入参为空,请检查入参");
        }
        // 科室编码去重,避免 service_task.deptcode 超长及 service_taskdept 重复绑定
        serviceTaskVO.setDeptcode(normalizeDeptCodes(serviceTaskVO.getDeptcode()));
        ServiceTask serviceTask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceTask.class);
        if (CollectionUtils.isEmpty(serviceTaskVO.getPreachformList())) throw new BaseException("任务的发送方式为空");
        //将preachfromList放到serviceTask中的preachfrom中
@@ -1298,7 +1574,12 @@
                    serviceSubtask.setInhospid(patTaskRelevance.getHospid());
                    //新建完成就是待发送状态
                    serviceSubtask.setSendstate(2L);
                    serviceSubtask.setDeptcode(StringUtils.isNotEmpty(patTaskRelevance.getDeptcode()) ? patTaskRelevance.getDeptcode() : serviceTaskVO.getDeptcode());
                    String subtaskDeptCode = patTaskRelevance.getDeptcode();
                    if (StringUtils.isEmpty(subtaskDeptCode) && StringUtils.isNotEmpty(serviceTaskVO.getDeptcode())) {
                        // service_subtask.deptcode 为单值字段,不能写入任务的多科室拼接串,取第一个科室兜底
                        subtaskDeptCode = serviceTaskVO.getDeptcode().split(",")[0].trim();
                    }
                    serviceSubtask.setDeptcode(subtaskDeptCode);
                    serviceSubtask.setDeptname(StringUtils.isNotEmpty(patTaskRelevance.getDeptName()) ? patTaskRelevance.getDeptName() : serviceTaskVO.getDeptname());
                    serviceSubtask.setLeavehospitaldistrictcode(patTaskRelevance.getLeavehospitaldistrictcode());
                    serviceSubtask.setLeavehospitaldistrictname(patTaskRelevance.getLeavehospitaldistrictname());
@@ -1371,6 +1652,8 @@
            ServiceTask serviceTask1 = serviceTaskService.selectServiceTaskByTaskid(serviceTask.getTaskid());
            ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity();
            serviceSubtaskVO.setTaskid(serviceTask1.getTaskid());
            //只删除首次随访未完成的
            serviceSubtaskVO.setVisitCount(1);
            serviceSubtaskVO.setSendstate(2L);
            List<ServiceSubtask> serviceSubtaskList = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO);
            serviceSubtaskVO.setSendstate(1L);
@@ -1436,6 +1719,11 @@
            if (StringUtils.isNotEmpty(serviceTaskVO.getAppltype())) {
                if (serviceTaskVO.getAppltype().equals("1") || serviceTaskVO.getAppltype().equals("2")) {
                    if (StringUtils.isNotEmpty(serviceTaskVO.getDeptcode())) {
                        // 修改任务时先软删除旧的科室绑定,避免旧科室残留
                        ServiceTaskdept oldDept = new ServiceTaskdept();
                        oldDept.setTaskId(serviceTask.getTaskid());
                        oldDept.setDeptType("1");
                        serviceTaskdeptService.deleteServiceTaskdeptByTaskId(oldDept);
                        String[] deptcodes = serviceTaskVO.getDeptcode().split(",");
                        for (String deptCode : deptcodes) {
                            ServiceTaskdept serviceTaskdept = new ServiceTaskdept();
@@ -3474,6 +3762,8 @@
            serviceSubtaskStatistic.setLeavehospitaldistrictcode(serviceSubtaskList.get(0).getLeavehospitaldistrictcode());
            serviceSubtaskStatistic.setDrcode(serviceSubtaskList.get(0).getDrcode());
            serviceSubtaskStatistic.setDrname(serviceSubtaskList.get(0).getDrname());
            //随访中心科室,用于统计再次随访推送到随访中心的人数
            String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", serviceSubtaskList.get(0).getOrgid());
            //出院人次
            serviceSubtaskStatistic.setDischargeCount(serviceSubtaskList.size());
            for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
@@ -3488,6 +3778,21 @@
                if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                    serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
                }
                //及时随访人次:已完成且未超时
                if (isTimelyFollowUp(serviceSubtask)) {
                    serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
                }
                //首次出院人次:visitCount = 1,作为再次随访推送率的分母
                if (serviceSubtask.getVisitCount() != null && serviceSubtask.getVisitCount() == 1) {
                    serviceSubtaskStatistic.setFirstDischargeCount(serviceSubtaskStatistic.getFirstDischargeCount() + 1L);
                }
                //再次随访推送人数:首个再次随访(visitCount = 2)且执行科室为随访中心科室
                if (serviceSubtask.getVisitCount() != null && serviceSubtask.getVisitCount() == 2
                        && StringUtils.isNotEmpty(centerDept)
                        && StringUtils.equals(StringUtils.trim(centerDept), StringUtils.trim(serviceSubtask.getVisitDeptCode()))) {
                    serviceSubtaskStatistic.setAgainFollowUpPushCount(serviceSubtaskStatistic.getAgainFollowUpPushCount() + 1L);
                }
                //人工已拨通总量
                if (ObjectUtils.isNotEmpty(serviceSubtask.getIsManuallyCalled())
@@ -3497,6 +3802,7 @@
                //首次出院随访
                if (serviceSubtask.getVisitCount() != null && serviceSubtask.getVisitCount() == 1) {
                    applyFirstFollowUpExtraStatistics(serviceSubtaskStatistic, serviceSubtask);
                    //首次应随访
                    if (serviceSubtask.getSendstate() != null && !serviceSubtask.getSendstate().equals(4L)) {
                        serviceSubtaskStatistic.setNeedFollowUp(serviceSubtaskStatistic.getNeedFollowUp() + 1L);
@@ -3618,6 +3924,7 @@
                    }
                }//二次出院随访
                else if (serviceSubtask.getVisitCount() != null && serviceSubtask.getVisitCount() > 1) {
                    applyAgainFollowUpExtraStatistics(serviceSubtaskStatistic, serviceSubtask);
                    //再次应随访
                    if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                        serviceSubtaskStatistic.setNeedFollowUpAgain(serviceSubtaskStatistic.getNeedFollowUpAgain() + 1L);
@@ -3756,6 +4063,80 @@
    }
    /**
     * 是否属于「及时随访」:随访已完成且完成时状态为正常完成(未超时)。
     * <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);
    }
    /**
     * 市一统计口径:首次待随访、已发送及人工电话超时。
     */
    private void applyFirstFollowUpExtraStatistics(ServiceSubtaskStatistic statistic, ServiceSubtask serviceSubtask) {
        if (serviceSubtask.getVisitCount() == null || serviceSubtask.getVisitCount() != 1 || serviceSubtask.getSendstate() == null) {
            return;
        }
        Long sendstate = serviceSubtask.getSendstate();
        if (sendstate.equals(3L)) {
            statistic.setSent(statistic.getSent() + 1L);
        }
        if (sendstate.equals(7L) && isManuallyNotCalled(serviceSubtask)) {
            statistic.setManualCallTimeout(statistic.getManualCallTimeout() + 1L);
        }
        if (sendstate.equals(1L) || sendstate.equals(2L) || sendstate.equals(3L) || sendstate.equals(7L)) {
            statistic.setPendingFollowUpAll(statistic.getPendingFollowUpAll() + 1L);
        }
        if (sendstate.equals(1L) || sendstate.equals(2L)) {
            statistic.setPendingFollowUpWaitSend(statistic.getPendingFollowUpWaitSend() + 1L);
        }
        if (sendstate.equals(3L) || sendstate.equals(7L)) {
            statistic.setPendingFollowUpSent(statistic.getPendingFollowUpSent() + 1L);
        }
    }
    /**
     * 再次随访附加统计:待随访、已发送、人工电话超时及回院复诊复查(suggest=6)。
     */
    private void applyAgainFollowUpExtraStatistics(ServiceSubtaskStatistic statistic, ServiceSubtask serviceSubtask) {
        if (serviceSubtask.getVisitCount() == null || serviceSubtask.getVisitCount() <= 1) {
            return;
        }
        if (serviceSubtask.getSendstate() == null) {
            return;
        }
        Long sendstate = serviceSubtask.getSendstate();
        // 回院复诊复查:再次随访中按处理意见 suggest=6 统计
        if (StringUtils.equals("6", StringUtils.trim(serviceSubtask.getSuggest()))) {
            statistic.setSuggest6CountAgain(statistic.getSuggest6CountAgain() + 1L);
        }
        if (sendstate.equals(3L)) {
            statistic.setSentAgain(statistic.getSentAgain() + 1L);
        }
        if (sendstate.equals(7L) && isManuallyNotCalled(serviceSubtask)) {
            statistic.setManualCallTimeoutAgain(statistic.getManualCallTimeoutAgain() + 1L);
        }
        if (sendstate.equals(1L) || sendstate.equals(2L) || sendstate.equals(3L) || sendstate.equals(7L)) {
            statistic.setPendingFollowUpAllAgain(statistic.getPendingFollowUpAllAgain() + 1L);
        }
        if (sendstate.equals(1L) || sendstate.equals(2L)) {
            statistic.setPendingFollowUpWaitSendAgain(statistic.getPendingFollowUpWaitSendAgain() + 1L);
        }
        if (sendstate.equals(3L) || sendstate.equals(7L)) {
            statistic.setPendingFollowUpSentAgain(statistic.getPendingFollowUpSentAgain() + 1L);
        }
    }
    private boolean isManuallyNotCalled(ServiceSubtask serviceSubtask) {
        return serviceSubtask.getIsManuallyCalled() != null && serviceSubtask.getIsManuallyCalled().equals(0L);
    }
    /**
     * 首次随访统计
     *
     * @param serviceSubtaskList
@@ -3776,6 +4157,7 @@
        serviceSubtaskStatistic.setDischargeCount(serviceSubtaskList.size());
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            applyFirstFollowUpExtraStatistics(serviceSubtaskStatistic, serviceSubtask);
            //无需随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() == 4) {
                serviceSubtaskStatistic.setNonFollowUp(serviceSubtaskStatistic.getNonFollowUp() + 1L);
@@ -3786,6 +4168,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //首次出院随访
@@ -3945,6 +4331,7 @@
        //出院人次
        serviceSubtaskStatistic.setDischargeCount(serviceSubtaskList.size());
        for (ServiceSubtask serviceSubtask : serviceSubtaskList) {
            applyAgainFollowUpExtraStatistics(serviceSubtaskStatistic, serviceSubtask);
            //无需随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() == 4) {
                serviceSubtaskStatistic.setNonFollowUp(serviceSubtaskStatistic.getNonFollowUp() + 1L);
@@ -3955,6 +4342,10 @@
            //应随访人次
            if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) {
                serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L);
            }
            //及时随访人次:已完成且未超时
            if (isTimelyFollowUp(serviceSubtask)) {
                serviceSubtaskStatistic.setTimelyFollowUp(serviceSubtaskStatistic.getTimelyFollowUp() + 1L);
            }
            //再次出院随访
@@ -4212,7 +4603,8 @@
        serviceSubtaskStatistic.setDrname(serviceSubtaskList.get(0).getDrname());
        serviceSubtaskStatistic.setJoyCount(0);
        serviceSubtaskStatistic.setJoyAllCount(0);
        //出院人次
        serviceSubtaskStatistic.setDischargeCount(serviceSubtaskList.size());
        //获取随访统计
        if (subCounts == null) {
            subCounts = new ArrayList<>();
@@ -4481,11 +4873,14 @@
     * 获取随访统计超链接
     * dischargeCountInfo
     * followUpNeededAllInfo - sendstate != 4 (1,2,3,5,6,7)
     * firstDischargeCountInfo - visitCount = 1
     * againFollowUpPushCountInfo - visitCount = 2 && visitDeptCode = sys.smartor.center.dept(随访中心科室)
     * nonFollowUpInfo - sendstate = 4
     * needFollowUpInfo - visitCount = 1 && sendstate != 4
     * 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"
@@ -4515,6 +4910,17 @@
     * taskSituation5Info - taskSituation = 5
     * taskSituation6Info - taskSituation = 7
     * manuallyCalledInfo - isManuallyCalled = 1L
     * manualCallTimeoutInfo - visitCount = 1 && sendstate = 7 && isManuallyCalled = 0
     * manualCallTimeoutAgainInfo - visitCount > 1 && sendstate = 7 && isManuallyCalled = 0
     * pendingFollowUpAllInfo - visitCount = 1 && sendstate in (1,2,3,7)
     * pendingFollowUpAllAgainInfo - visitCount > 1 && sendstate in (1,2,3,7)
     * pendingFollowUpWaitSendInfo - visitCount = 1 && sendstate in (1,2)
     * pendingFollowUpWaitSendAgainInfo - visitCount > 1 && sendstate in (1,2)
     * pendingFollowUpSentInfo - visitCount = 1 && sendstate in (3,7)
     * pendingFollowUpSentAgainInfo - visitCount > 1 && sendstate in (3,7)
     * sentInfo - visitCount = 1 && sendstate = 3
     * sentAgainInfo - visitCount > 1 && sendstate = 3
     * suggest6CountAgainInfo - visitCount > 1 && suggest = 6
     * filterCountList - taskSituation = 6 && sendstate = 4
     *
     */
@@ -4522,7 +4928,10 @@
    public List<ServiceSubtask> getSfStatisticsHyperlink(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        String hyperLinkInfoType = serviceSubtaskCountReq.getHyperLinkInfoType();
        if (StringUtils.isNotEmpty(hyperLinkInfoType)) {
            applyHyperLinkInfoType(serviceSubtaskCountReq, hyperLinkInfoType);
            if (!applyHyperLinkInfoType(serviceSubtaskCountReq, hyperLinkInfoType)) {
                // 下钻依赖的配置缺失时返回空明细,避免退化成无过滤条件的查询
                return new ArrayList<>();
            }
        }
        String followUpCountStyle = serviceSubtaskCountReq.getFollowUpCountStyle();
@@ -4543,15 +4952,40 @@
        return serviceSubtasks;
    }
    private void applyHyperLinkInfoType(ServiceSubtaskCountReq serviceSubtaskCountReq, String hyperLinkInfoType) {
    private boolean 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);
        serviceSubtaskCountReq.setSuggest(null);
        switch (hyperLinkInfoType) {
            case "dischargeCountInfo":
                break;
            case "firstDischargeCountInfo":
                //首次出院人次:visitCount = 1
                serviceSubtaskCountReq.setVisitCount(1);
                break;
            case "againFollowUpPushCountInfo": {
                //再次随访推送人数:visitCount = 2 且执行科室为随访中心科室
                String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", serviceSubtaskCountReq.getOrgid());
                if (StringUtils.isBlank(centerDept)) {
                    log.warn("获取再次随访推送明细失败,未配置随访中心科室参数 sys.smartor.center.dept, orgid={}", serviceSubtaskCountReq.getOrgid());
                    return false;
                }
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setVisitDeptCode(centerDept.trim());
                serviceSubtaskCountReq.setVisitDeptName(null);
                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)));
@@ -4671,6 +5105,11 @@
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setExcep("1");
                break;
            case "suggest6CountAgainInfo":
                // 再次随访回院复诊复查:visitCount > 1 且 suggest = 6
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSuggest("6");
                break;
            case "taskSituation1Info":
                serviceSubtaskCountReq.setTaskSituation(1);
                break;
@@ -4696,9 +5135,52 @@
            case "manuallyCalledInfo":
                serviceSubtaskCountReq.setIsManuallyCalled(1L);
                break;
            case "manualCallTimeoutInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(7L)));
                serviceSubtaskCountReq.setIsManuallyCalled(0L);
                break;
            case "manualCallTimeoutAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(7L)));
                serviceSubtaskCountReq.setIsManuallyCalled(0L);
                break;
            case "pendingFollowUpAllInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 7L)));
                break;
            case "pendingFollowUpAllAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 7L)));
                break;
            case "pendingFollowUpWaitSendInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L)));
                break;
            case "pendingFollowUpWaitSendAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L)));
                break;
            case "pendingFollowUpSentInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(3L, 7L)));
                break;
            case "pendingFollowUpSentAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Arrays.asList(3L, 7L)));
                break;
            case "sentInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(3L)));
                break;
            case "sentAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(3L)));
                break;
            default:
                break;
        }
        return true;
    }
@@ -4709,7 +5191,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 +5199,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 +5321,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 +5345,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 +5392,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;
    }
@@ -5014,6 +5522,8 @@
        // 初始化数值字段
        total.setDischargeCount(0);
        total.setFirstDischargeCount(0L);
        total.setAgainFollowUpPushCount(0L);
        total.setNonFollowUp(0L);
        total.setFilterCount(0L);
        total.setFollowUpNeeded(0L);
@@ -5021,14 +5531,26 @@
        total.setPendingFollowUp(0L);
        total.setFollowUpSuccess(0L);
        total.setFollowUpFail(0L);
        total.setTimelyFollowUp(0L);
        total.setManual(0L);
        total.setSms(0L);
        total.setWeChat(0L);
        total.setAbnormal(0L);
        total.setNeedFollowUpAgain(0L);
        total.setPendingFollowUpAgain(0L);
        total.setManualCallTimeout(0L);
        total.setManualCallTimeoutAgain(0L);
        total.setPendingFollowUpAll(0L);
        total.setPendingFollowUpAllAgain(0L);
        total.setPendingFollowUpWaitSend(0L);
        total.setPendingFollowUpWaitSendAgain(0L);
        total.setPendingFollowUpSent(0L);
        total.setPendingFollowUpSentAgain(0L);
        total.setSent(0L);
        total.setSentAgain(0L);
        total.setFollowUpSuccessAgain(0L);
        total.setFollowUpFailAgain(0L);
        total.setSuggest6CountAgain(0L);
        total.setManualAgain(0L);
        total.setSmsAgain(0L);
        total.setWeChatAgain(0L);
@@ -5045,6 +5567,8 @@
            if (stat.getDischargeCount() != null) {
                total.setDischargeCount(total.getDischargeCount() + stat.getDischargeCount());
            }
            total.setFirstDischargeCount(total.getFirstDischargeCount() + (stat.getFirstDischargeCount() != null ? stat.getFirstDischargeCount() : 0L));
            total.setAgainFollowUpPushCount(total.getAgainFollowUpPushCount() + (stat.getAgainFollowUpPushCount() != null ? stat.getAgainFollowUpPushCount() : 0L));
            total.setNonFollowUp(total.getNonFollowUp() + (stat.getNonFollowUp() != null ? stat.getNonFollowUp() : 0L));
            total.setFilterCount(total.getFilterCount() + (stat.getFilterCount() != null ? stat.getFilterCount() : 0L));
            total.setFollowUpNeeded(total.getFollowUpNeeded() + (stat.getFollowUpNeeded() != null ? stat.getFollowUpNeeded() : 0L));
@@ -5052,14 +5576,26 @@
            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));
            total.setAbnormal(total.getAbnormal() + (stat.getAbnormal() != null ? stat.getAbnormal() : 0L));
            total.setNeedFollowUpAgain(total.getNeedFollowUpAgain() + (stat.getNeedFollowUpAgain() != null ? stat.getNeedFollowUpAgain() : 0L));
            total.setPendingFollowUpAgain(total.getPendingFollowUpAgain() + (stat.getPendingFollowUpAgain() != null ? stat.getPendingFollowUpAgain() : 0L));
            total.setManualCallTimeout(total.getManualCallTimeout() + (stat.getManualCallTimeout() != null ? stat.getManualCallTimeout() : 0L));
            total.setManualCallTimeoutAgain(total.getManualCallTimeoutAgain() + (stat.getManualCallTimeoutAgain() != null ? stat.getManualCallTimeoutAgain() : 0L));
            total.setPendingFollowUpAll(total.getPendingFollowUpAll() + (stat.getPendingFollowUpAll() != null ? stat.getPendingFollowUpAll() : 0L));
            total.setPendingFollowUpAllAgain(total.getPendingFollowUpAllAgain() + (stat.getPendingFollowUpAllAgain() != null ? stat.getPendingFollowUpAllAgain() : 0L));
            total.setPendingFollowUpWaitSend(total.getPendingFollowUpWaitSend() + (stat.getPendingFollowUpWaitSend() != null ? stat.getPendingFollowUpWaitSend() : 0L));
            total.setPendingFollowUpWaitSendAgain(total.getPendingFollowUpWaitSendAgain() + (stat.getPendingFollowUpWaitSendAgain() != null ? stat.getPendingFollowUpWaitSendAgain() : 0L));
            total.setPendingFollowUpSent(total.getPendingFollowUpSent() + (stat.getPendingFollowUpSent() != null ? stat.getPendingFollowUpSent() : 0L));
            total.setPendingFollowUpSentAgain(total.getPendingFollowUpSentAgain() + (stat.getPendingFollowUpSentAgain() != null ? stat.getPendingFollowUpSentAgain() : 0L));
            total.setSent(total.getSent() + (stat.getSent() != null ? stat.getSent() : 0L));
            total.setSentAgain(total.getSentAgain() + (stat.getSentAgain() != null ? stat.getSentAgain() : 0L));
            total.setFollowUpSuccessAgain(total.getFollowUpSuccessAgain() + (stat.getFollowUpSuccessAgain() != null ? stat.getFollowUpSuccessAgain() : 0L));
            total.setFollowUpFailAgain(total.getFollowUpFailAgain() + (stat.getFollowUpFailAgain() != null ? stat.getFollowUpFailAgain() : 0L));
            total.setSuggest6CountAgain(total.getSuggest6CountAgain() + (stat.getSuggest6CountAgain() != null ? stat.getSuggest6CountAgain() : 0L));
            total.setManualAgain(total.getManualAgain() + (stat.getManualAgain() != null ? stat.getManualAgain() : 0L));
            total.setSmsAgain(total.getSmsAgain() + (stat.getSmsAgain() != null ? stat.getSmsAgain() : 0L));
            total.setWeChatAgain(total.getWeChatAgain() + (stat.getWeChatAgain() != null ? stat.getWeChatAgain() : 0L));
@@ -5438,7 +5974,15 @@
                return resultMap;
            }
            if (StringUtils.isEmpty(serviceSubtask.getPatidHis())) {
            if (patArchive == null) {
                resultMap.put("code", -1);
                resultMap.put("message", "发送异常:" + "【公众号】未查询到患者档案信息,无法通过公众号发送");
                return resultMap;
            }
            // his系统的患者id取自患者档案pat_archive.patid_his,
            // service_subtask查询未关联该字段(serviceSubtask.getPatidHis()恒为空),不能用于校验
            if (StringUtils.isEmpty(patArchive.getPatidHis())) {
                resultMap.put("code", -1);
                resultMap.put("message", "发送异常:" + "【公众号】公众号发送失败,his系统的患者id为空");
                return resultMap;