liusheng
8 小时以前 18f876d637cf1e9b0b264a4586e4a676a21f8166
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceSubtaskServiceImpl.java
@@ -1060,10 +1060,10 @@
                return "再次随访任务创建失败,找不到对应任务 taskid = " + serviceSubtask.getTaskid() + ", 再请次重试";
            }
            // nexttaskflag=0 表示任务链终止,不允许再生成
            if ("0".equals(serviceTask.getNexttaskflag())) {
                log.info("无需再次随访,任务已标记无后续随访, taskid = {}", serviceSubtask.getTaskid());
                return "无需再次随访,该任务无后续随访安排";
            }
//            if ("0".equals(serviceTask.getNexttaskflag())) {
//                log.info("无需再次随访,任务已标记无后续随访, taskid = {}", serviceSubtask.getTaskid());
//                return "无需再次随访,该任务无后续随访安排";
//            }
            // 幂等校验:同一原任务只允许生成一条二次随访
            ServiceSubtaskEntity againQuery = new ServiceSubtaskEntity();
            againQuery.setUpid(serviceSubtask.getId().intValue());
@@ -1165,6 +1165,18 @@
                return "再次随访任务创建失败,患者出院记录不完整或下一任务配置异常, 再请次重试";
            }
            Date now = new Date();
            applyAgainVisitTime(serviceSubtaskAgain, serviceSubtask, nextServiceTask, config, now);
            // 医生处理意见取自本次请求,按下一任务重建子任务时需显式带入,否则不会落库
            if (request != null) {
                serviceSubtaskAgain.setDoctorOpinion(request.getDoctorOpinion());
                serviceSubtaskAgain.setRemark(request.getRemark());
                serviceSubtaskAgain.setDrcode(request.getDrcode());
                serviceSubtaskAgain.setDrname(request.getDrname());
                serviceSubtaskAgain.setNurseId(request.getNurseId());
                serviceSubtaskAgain.setNurseName(request.getNurseName());
                serviceSubtaskAgain.setManagementDoctor(request.getManagementDoctor());
                serviceSubtaskAgain.setManagementDoctorCode(request.getManagementDoctorCode());
            }
            serviceSubtaskAgain.setId(null);
            serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue());
            serviceSubtaskAgain.setVisitCount(ObjectUtils.isNotEmpty(serviceSubtask.getVisitCount())
@@ -1173,9 +1185,6 @@
            serviceSubtaskAgain.setIsVisitAgain(1);
            serviceSubtaskAgain.setCreateTime(now);
            serviceSubtaskAgain.setUpdateTime(now);
            // 二次随访时间:上次随访完成时间(未完成则当前时间)+ 下一任务发送天数;过去时间兜底为当前时间
            applyAgainVisitTime(serviceSubtaskAgain, serviceSubtask, nextServiceTask, config, now);
            // 新任务的随访方式记录本次选择的再次随访方式;resource 本身不落库
            serviceSubtaskAgain.setVisitType(resource);
            serviceSubtaskAgain.setVisitDeptCode(visitDeptCode);
@@ -1253,6 +1262,10 @@
        if (dueTime.before(now)) {
            dueTime = now;
        }
        if(ObjectUtils.isNotEmpty(origin.getVisitTime())){
            dueTime = ObjectUtils.isNotEmpty(origin.getVisitTime()) ? origin.getVisitTime() : now;
        }
        target.setLongSendTime(dueTime);
        Date visitTime = dueTime;
        if ("3".equals(nextTask.getType())) {
@@ -1586,6 +1599,61 @@
    }
    /**
     * 取逗号串里的第一个值(任务上科室/病区是多选,而 {@code service_subtask} 上都是单值列)。
     */
    private String firstValue(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        for (String item : value.split(",")) {
            if (StringUtils.isNotEmpty(item.trim())) {
                return item.trim();
            }
        }
        return null;
    }
    /**
     * 任务配置({@link ServiceTaskVO})→ 子任务({@link ServiceSubtask})的字段搬运。
     * <p>
     * <b>为什么不能直接 {@code DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class)}:</b>
     * 它是纯同名拷贝,而两边同名的 {@code deptcode / deptname} <b>语义不同</b>:
     * <ul>
     *   <li>{@code ServiceTaskVO.deptcode / deptname} = 任务的「适用科室」,
     *       出院随访(serviceType=2)/ 出院宣教(4)/ 门诊随访(3)等场景下就是<b>离院科室</b>;</li>
     *   <li>{@code ServiceSubtask.deptcode / deptname} = <b>在院科室</b>
     *       (2026-11 由原 sendlimitpbegin/pend 复用而来,见 {@code ServiceSubtask} 字段注释)。</li>
     * </ul>
     * 于是纯同名拷贝会把「离院科室」写进「在院科室」列,后果有两个:
     * <ol>
     *   <li>按在院维度筛({@code deptcodes} → {@code service_subtask.deptcode})会误命中这些子任务;</li>
     *   <li>按离院维度筛({@code leaveldeptcodes} → {@code service_subtask.leaveldeptcode})时,
     *       患者自己没带科室的那些(前端手动勾选患者很常见)会因为 {@code leaveldeptcode} 为空而查不到。</li>
     * </ol>
     * 因此这里按项目既定口径({@code doc/service_subtask_字段改名_改动清单.md} §8:这几条
     * 「长期任务 / 任务组 / 手动建任务」路径<b>不填在院维度</b>)统一处理:
     * <b>在院维度 4 列清空</b>(在院科室/在院病区只有 HIS 链路能从 {@code pat_med_inhosp} 拿到),
     * <b>科室 / 病区一律落离院维度</b>({@code leaveldeptcode/name}、{@code leavehospitaldistrictcode/name})。
     *
     * @param serviceTaskVO 建任务入参
     * @return 已按离院维度归位的子任务对象(在院维度为空)
     */
    private ServiceSubtask toSubtaskFromTaskVO(ServiceTaskVO serviceTaskVO) {
        ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
        // ① 在院维度必须清空:任务配置的科室不是患者本人的在院科室
        serviceSubtask.setDeptcode(null);
        serviceSubtask.setDeptname(null);
        serviceSubtask.setHospitaldistrictcode(null);
        serviceSubtask.setHospitaldistrictname(null);
        // ② 科室 / 病区 → 离院维度(多选串取第一个,子任务是单值列)
        serviceSubtask.setLeaveldeptcode(firstValue(serviceTaskVO.getDeptcode()));
        serviceSubtask.setLeaveldeptname(firstValue(serviceTaskVO.getDeptname()));
        serviceSubtask.setLeavehospitaldistrictcode(firstValue(serviceTaskVO.getLeavehospitaldistrictcode()));
        serviceSubtask.setLeavehospitaldistrictname(firstValue(serviceTaskVO.getLeavehospitaldistrictname()));
        return serviceSubtask;
    }
    /**
     * 单一任务
     *
     * @return 结果
@@ -1802,6 +1870,25 @@
                            serviceTaskdeptService.insertServiceTaskdept(serviceTaskdept);
                        }
                    }
                    // 点位行(2026-11,appltype=7):科室 / 病区 / 点位在 service_taskdept 里是
                    // **各自独立的行**,可以共存也可以不共存 —— 所以点位行是"额外追加",
                    // 不替换上面的科室/病区行。点位行的 dept_code/dept_name 留空,编码只落 point_location。
                    if ("7".equals(serviceTaskVO.getAppltype())) {
                        String pointLocation = firstNonBlank(serviceTaskVO.getPointLocation(),
                                serviceTask.getPointLocation());
                        if (StringUtils.isNotEmpty(pointLocation)) {
                            ServiceTaskdept pointDept = new ServiceTaskdept();
                            pointDept.setTaskId(serviceTask.getTaskid());
                            pointDept.setTaskName(serviceTask.getTaskName());
                            pointDept.setOrgid(serviceTask.getOrgid());
                            pointDept.setLongtask(serviceTask.getLongTask() == null ? null
                                    : Long.valueOf(serviceTask.getLongTask()));
                            pointDept.setServiceType(serviceTaskVO.getServiceType());
                            pointDept.setDeptType("3");
                            pointDept.setPointLocation(pointLocation);
                            serviceTaskdeptService.insertServiceTaskdept(pointDept);
                        }
                    }
                }
            }
//长期任务,也允许加人了
@@ -1812,7 +1899,8 @@
//            }
            //将任务信息放到服务表中
            ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
            // 科室/病区按「离院维度」归位(出院随访的科室病区就是离院科室/病区),见 toSubtaskFromTaskVO
            ServiceSubtask serviceSubtask = toSubtaskFromTaskVO(serviceTaskVO);
            serviceSubtask.setTaskid(serviceTask.getTaskid().longValue());
            serviceSubtask.setTemplatename(ObjectUtils.isNotEmpty(tempid) ? tempName : serviceTask.getTemplatename());
            serviceSubtask.setTemplateid(ObjectUtils.isNotEmpty(tempid) ? tempid : serviceTask.getTemplateid());
@@ -1833,15 +1921,18 @@
                    serviceSubtask.setInhospid(patTaskRelevance.getHospid());
                    //新建完成就是待发送状态
                    serviceSubtask.setSendstate(2L);
                    String subtaskDeptCode = patTaskRelevance.getDeptcode();
                    if (StringUtils.isEmpty(subtaskDeptCode) && StringUtils.isNotEmpty(serviceTaskVO.getDeptcode())) {
                        // service_subtask.leaveldeptcode 为单值字段,不能写入任务的多科室拼接串,取第一个科室兜底
                        subtaskDeptCode = serviceTaskVO.getDeptcode().split(",")[0].trim();
                    }
                    serviceSubtask.setLeaveldeptcode(subtaskDeptCode);
                    serviceSubtask.setLeaveldeptname(StringUtils.isNotEmpty(patTaskRelevance.getDeptName()) ? patTaskRelevance.getDeptName() : serviceTaskVO.getDeptname());
                    serviceSubtask.setLeavehospitaldistrictcode(patTaskRelevance.getLeavehospitaldistrictcode());
                    serviceSubtask.setLeavehospitaldistrictname(patTaskRelevance.getLeavehospitaldistrictname());
                    // 离院维度(出院随访 = 出院科室 / 出院病区):
                    // 患者自带的出院科室/病区优先,患者侧没带(前端手动勾选很常见)时回退到任务配置,
                    // 不能无条件覆盖 —— 否则会把 toSubtaskFromTaskVO 按任务配置兜好的离院维度抹成 null,
                    // 结果「任务配了科室病区、生成出来的子任务离院科室/病区却是空」。
                    serviceSubtask.setLeaveldeptcode(firstNonBlank(patTaskRelevance.getDeptcode(),
                            firstValue(serviceTaskVO.getDeptcode())));
                    serviceSubtask.setLeaveldeptname(firstNonBlank(patTaskRelevance.getDeptName(),
                            firstValue(serviceTaskVO.getDeptname())));
                    serviceSubtask.setLeavehospitaldistrictcode(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictcode(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictcode())));
                    serviceSubtask.setLeavehospitaldistrictname(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictname(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictname())));
                    serviceSubtask.setType(serviceTaskVO.getType());
                    serviceSubtask.setHospType(patTaskRelevance.getHospType());
                    serviceSubtask.setOpenid(patTaskRelevance.getOpenid());
@@ -2177,6 +2268,27 @@
                    scheduleVO.setOplevelCodes(serviceTaskVO.getOplevelcode());
                    scheduleVO.setCreateBy(serviceTask.getUpdateBy());
                    serviceTaskScheduleService.saveScheduleWithDetail(scheduleVO);
                } else if (serviceTaskVO.getAppltype().equals("7")) {
                    // 按点位(同德需求):先软删该任务旧的 dept_type='3' 行,再按入参重建
                    //   (与科室/病区「先删后插」同口径;点位行 dept_code/dept_name 留空)
                    ServiceTaskdept oldPoint = new ServiceTaskdept();
                    oldPoint.setTaskId(serviceTask.getTaskid());
                    oldPoint.setDeptType("3");
                    serviceTaskdeptService.deleteServiceTaskdeptByTaskId(oldPoint);
                    String pointLocation = StringUtils.isNotEmpty(serviceTaskVO.getPointLocation())
                            ? serviceTaskVO.getPointLocation() : serviceTask.getPointLocation();
                    if (StringUtils.isNotEmpty(pointLocation)) {
                        ServiceTaskdept pointDept = new ServiceTaskdept();
                        pointDept.setTaskId(serviceTask.getTaskid());
                        pointDept.setTaskName(serviceTask.getTaskName());
                        pointDept.setOrgid(serviceTaskVO.getOrgid());
                        pointDept.setLongtask(serviceTask.getLongTask() == null ? null
                                : Long.valueOf(serviceTask.getLongTask()));
                        pointDept.setServiceType(serviceTaskVO.getServiceType());
                        pointDept.setDeptType("3");
                        pointDept.setPointLocation(pointLocation);
                        serviceTaskdeptService.insertServiceTaskdept(pointDept);
                    }
                }
            }
            if (ObjectUtils.isNotEmpty(serviceTaskVO.getIvrTaskTemplateVO())) {
@@ -2199,7 +2311,8 @@
            }
            if (CollectionUtils.isNotEmpty(serviceTaskVO.getPatTaskRelevances())) {
                for (PatTaskRelevance patTaskRelevance : serviceTaskVO.getPatTaskRelevances()) {
                    ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
                    // 科室/病区按「离院维度」归位(出院随访的科室病区就是离院科室/病区),见 toSubtaskFromTaskVO
                    ServiceSubtask serviceSubtask = toSubtaskFromTaskVO(serviceTaskVO);
                    serviceSubtask.setLibtemplateid(ObjectUtils.isEmpty(serviceTaskVO.getLibtemplateid()) ? null : Long.valueOf(serviceTaskVO.getLibtemplateid()));
                    serviceSubtask.setLibtemplatename(serviceTaskVO.getLibtemplatename());
                    serviceSubtask.setTemplateid(serviceTask.getTemplateid());
@@ -2211,10 +2324,16 @@
                    serviceSubtask.setAddr(patTaskRelevance.getAddr());
                    serviceSubtask.setPatid(patTaskRelevance.getPatid());
                    serviceSubtask.setOpenid(patTaskRelevance.getOpenid());
                    serviceSubtask.setLeaveldeptcode(patTaskRelevance.getDeptcode());
                    serviceSubtask.setLeavehospitaldistrictname(patTaskRelevance.getLeavehospitaldistrictname());
                    serviceSubtask.setLeavehospitaldistrictcode(patTaskRelevance.getLeavehospitaldistrictcode());
                    serviceSubtask.setLeaveldeptname(patTaskRelevance.getDeptName());
                    // 离院维度:患者自带的出院科室/病区优先,没带时回退到任务配置(否则会把
                    // toSubtaskFromTaskVO 按任务配置兜好的值抹成 null,出院随访就成「离院科室/病区为空」)
                    serviceSubtask.setLeaveldeptcode(firstNonBlank(patTaskRelevance.getDeptcode(),
                            firstValue(serviceTaskVO.getDeptcode())));
                    serviceSubtask.setLeaveldeptname(firstNonBlank(patTaskRelevance.getDeptName(),
                            firstValue(serviceTaskVO.getDeptname())));
                    serviceSubtask.setLeavehospitaldistrictcode(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictcode(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictcode())));
                    serviceSubtask.setLeavehospitaldistrictname(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictname(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictname())));
                    serviceSubtask.setType(serviceTaskVO.getType());
                    serviceSubtask.setCreateTime(DateUtils.getNowDate());
                    serviceSubtask.setDrname(patTaskRelevance.getDrname());
@@ -2448,6 +2567,9 @@
            } else {
                groupKey = "deptcode";
            }
        } else if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 3) {
            // 病种统计:按任务ID分组,每个任务即一个病种随访任务
            groupKey = "taskid";
        }
        List<ServiceSubtaskStatistic> serviceSubtaskStatistics = new ArrayList<>();
        // 根据条件进行分组
@@ -2473,6 +2595,9 @@
            case "leavehospitaldistrictcode":
                collect = rawData.stream().collect(Collectors.groupingBy(subtask -> Optional.ofNullable(subtask.getLeavehospitaldistrictcode()).orElse("Unknown")));
                break;
            case "taskid":
                collect = rawData.stream().collect(Collectors.groupingBy(subtask -> Optional.ofNullable(subtask.getTaskid()).map(String::valueOf).orElse("Unknown")));
                break;
            default:
                collect = rawData.stream().collect(Collectors.groupingBy(subtask -> Optional.ofNullable(subtask.getDrcode()).orElse("Unknown")));
                break;
@@ -2485,6 +2610,9 @@
                continue;
            }
            ServiceSubtaskStatistic statistic = getStatistic(serviceSubtaskList, serviceSubtaskCountReq.getFollowUpCountStyle(), serviceSubtaskCountReq.getVisitCount());
            if ("taskid".equals(groupKey)) {
                applyTaskGroupInfo(statistic, serviceSubtaskList);
            }
            if (ObjectUtils.isNotEmpty(jsRates)) {
                Map<String, Object> jsRateMap = jsRates.get(collectKey);
                BigDecimal jsRate = new BigDecimal("0");
@@ -2573,7 +2701,16 @@
        ssRate.setGroupKey(groupKey);
        ssRate.setGroupKeyList(groupKeyList);
        ssRate.setRateDay(serviceSubtaskCountReq.getRateDay());
        return serviceSubtaskMapper.selectTimelyRateBatch(ssRate);
        Map<String, Map<String, Object>> rateResult = serviceSubtaskMapper.selectTimelyRateBatch(ssRate);
        if (rateResult == null || rateResult.isEmpty()) {
            return rateResult;
        }
        // taskid 为数值类型,MyBatis 返回的 Map key 可能是 Long,这里统一转成 String 与分组 key 对齐
        Map<String, Map<String, Object>> jsRates = new HashMap<>();
        for (Map.Entry<String, Map<String, Object>> entry : rateResult.entrySet()) {
            jsRates.put(String.valueOf(entry.getKey()), entry.getValue());
        }
        return jsRates;
    }
    /**
@@ -2597,6 +2734,9 @@
            ServiceSubtaskStatistic totalRow = calculateTotalRow(sfStatistics);
            totalRow.setLeavehospitaldistrictname("总计");
            totalRow.setDeptname("总计");
            if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 3) {
                totalRow.setTaskName("总计");
            }
            sfStatistics.add(totalRow);
        }
@@ -2693,6 +2833,22 @@
        }
        return serviceSubtaskStatistics;
    }
    /**
     * 病种统计(按 taskid 分组)时补齐任务ID、任务名称和疾病病种。
     *
     * @param statistic          统计结果
     * @param serviceSubtaskList 当前任务分组下的子任务
     */
    private void applyTaskGroupInfo(ServiceSubtaskStatistic statistic, List<ServiceSubtask> serviceSubtaskList) {
        if (statistic == null || CollectionUtils.isEmpty(serviceSubtaskList)) {
            return;
        }
        ServiceSubtask first = serviceSubtaskList.get(0);
        statistic.setTaskId(first.getTaskid());
        statistic.setTaskName(first.getTaskName());
        statistic.setDiagType(first.getDiagType());
    }
    private ServiceSubtaskStatistic getStatistic(List<ServiceSubtask> serviceSubtaskList, String followUpCountStyle, Integer visitCount) {
@@ -4006,6 +4162,9 @@
        } else if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 2) {
            // 历史入参:2 = 科室统计(原 deptcode 即现在的离院科室 leaveldeptcode)
            groupKey = hasDrcode ? "drcode" : "leaveldeptcode";
        } else if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 3) {
            // 病种统计:按任务ID分组,每个任务即一个病种随访任务
            groupKey = "taskid";
        }
        serviceSubtaskCountReq.setGroupKey(groupKey);
        // 分组列与过滤列保持一致:离院科室 leaveldeptcode、在院科室 deptcode
@@ -4049,6 +4208,35 @@
        List<ServiceSubtaskStatistic> sumStatistics = serviceSubtaskMapper.getSfStatisticsScriptSum(serviceSubtaskCountReq);
        if (CollectionUtils.isEmpty(sumStatistics)) {
            return serviceSubtaskStatistics;
        }
        /*
         * 病种统计(按 taskid 分组)时补齐任务ID、任务名称和疾病病种。
         * 聚合 SQL 已返回 taskid/task_name,这里再补 diag_type。
         */
        if ("taskid".equals(groupKey)) {
            Map<Long, String> diagTypeCache = new HashMap<>();
            for (ServiceSubtaskStatistic statistic : sumStatistics) {
                Long taskId = statistic.getTaskId();
                if (taskId == null && StringUtils.isNotEmpty(statistic.getGroupCode())) {
                    try {
                        taskId = Long.valueOf(statistic.getGroupCode());
                    } catch (NumberFormatException ignore) {
                        // 历史数据可能不是数值 taskid,保持原分组键即可
                    }
                }
                statistic.setTaskId(taskId);
                if (StringUtils.isEmpty(statistic.getTaskName())) {
                    statistic.setTaskName(statistic.getGroupName());
                }
                if (taskId != null) {
                    if (!diagTypeCache.containsKey(taskId)) {
                        ServiceTask task = serviceTaskService.selectServiceTaskByTaskid(taskId);
                        diagTypeCache.put(taskId, task == null ? null : task.getDiagType());
                    }
                    statistic.setDiagType(diagTypeCache.get(taskId));
                }
            }
        }
        /*
@@ -4300,6 +4488,88 @@
        result.put("rows", new ArrayList<>(pageCounts));
        result.put("data", pageData);
        return result;
    }
    /**
     * 问题选项数字下钻:返回选择了指定选项的 service_subtask 列表。
     * <p>
     * 统计明细中的选项数量来自 service_subtask_detail 的
     * IFNULL(matchedtext, asrtext),因此下钻必须使用同一题目的相同选项进行匹配。
     *
     * @param serviceSubtaskCountReq 下钻条件
     * @return service_subtask 列表
     */
    @Override
    public List<ServiceSubtask> getSfStatisticsScriptDetailsHyperlink(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        log.info("getSfStatisticsScriptDetailsHyperlink的入参为:{}", serviceSubtaskCountReq);
        if (serviceSubtaskCountReq == null) {
            return new ArrayList<>();
        }
        // 兼容前端直接回传统计明细行中的 subtaskType
        if (StringUtils.isBlank(serviceSubtaskCountReq.getType())
                && StringUtils.isNotBlank(serviceSubtaskCountReq.getSubtaskType())) {
            serviceSubtaskCountReq.setType(serviceSubtaskCountReq.getSubtaskType());
        }
        resolveScriptContentAndType(serviceSubtaskCountReq);
        if (StringUtils.isBlank(serviceSubtaskCountReq.getScriptContent())
                && serviceSubtaskCountReq.getScriptid() == null) {
            log.warn("getSfStatisticsScriptDetailsHyperlink-题目scriptid和scriptContent均为空,返回空列表");
            return new ArrayList<>();
        }
        if (serviceSubtaskCountReq.getOptionText() == null) {
            log.warn("getSfStatisticsScriptDetailsHyperlink-选项optionText为空,返回空列表");
            return new ArrayList<>();
        }
        return serviceSubtaskMapper.getSfStatisticsScriptDetailsHyperlink(serviceSubtaskCountReq);
    }
    /**
     * 根据 scriptid 补齐题目内容和任务类型。
     * <p>
     * 统计明细按“题目内容 + 任务类型”合并,同一道题复制到多个任务模板时 scriptid 可能不同,
     * 因此优先按题目内容下钻;只有查不到题目内容时才退化为 scriptid 精确匹配。
     */
    private void resolveScriptContentAndType(ServiceSubtaskCountReq serviceSubtaskCountReq) {
        if (serviceSubtaskCountReq.getScriptid() == null) {
            return;
        }
        // 已有题目内容和任务类型时无需再查模板问题表
        if (StringUtils.isNotBlank(serviceSubtaskCountReq.getScriptContent())
                && StringUtils.isNotBlank(serviceSubtaskCountReq.getType())) {
            return;
        }
        String type = serviceSubtaskCountReq.getType();
        if (StringUtils.isBlank(type) || "1".equals(type)) {
            IvrTaskTemplateScript ivrTaskTemplateScript =
                    iIvrTaskTemplateScriptService.selectIvrTaskTemplateScriptByID(serviceSubtaskCountReq.getScriptid());
            if (ObjectUtils.isNotEmpty(ivrTaskTemplateScript)) {
                if (StringUtils.isBlank(serviceSubtaskCountReq.getType())) {
                    serviceSubtaskCountReq.setType("1");
                }
                if (StringUtils.isBlank(serviceSubtaskCountReq.getScriptContent())) {
                    serviceSubtaskCountReq.setScriptContent(ivrTaskTemplateScript.getScriptContent());
                }
                return;
            }
            // 明确指定了语音类型时不再尝试问卷表
            if (StringUtils.isNotBlank(type)) {
                return;
            }
        }
        if (StringUtils.isBlank(serviceSubtaskCountReq.getType()) || "2".equals(type)) {
            SvyTaskTemplateScript svyTaskTemplateScript =
                    svyTaskTemplateScriptService.selectSvyTaskTemplateScriptByID(serviceSubtaskCountReq.getScriptid());
            if (ObjectUtils.isNotEmpty(svyTaskTemplateScript)) {
                if (StringUtils.isBlank(serviceSubtaskCountReq.getType())) {
                    serviceSubtaskCountReq.setType("2");
                }
                if (StringUtils.isBlank(serviceSubtaskCountReq.getScriptContent())) {
                    serviceSubtaskCountReq.setScriptContent(svyTaskTemplateScript.getScriptContent());
                }
            }
        }
    }
    /**
@@ -4709,6 +4979,184 @@
        }
    }
    /**
     * 微信公众号已领取未填写:按题目默认选项补齐当前子任务,并将当前子任务完成。
     * <p>
     * 所有查询、明细写入、状态更新都只使用当前 subid,避免影响其他任务/子任务。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean completeWechatClaimedByDefault(ServiceSubtask serviceSubtask) {
        if (serviceSubtask == null || serviceSubtask.getId() == null) {
            return false;
        }
        // 重新读取当前子任务,所有判断只针对这一个 subid
        ServiceSubtask current = serviceSubtaskMapper.selectServiceSubtaskById(serviceSubtask.getId());
        if (current == null) {
            return false;
        }
        // 只处理:丽水中医院、问卷、当前方式公众号、已领取、未提交
        // 其他医院 orgid 不匹配时直接返回 false,继续走原补偿逻辑。
        if (!IServiceSubtaskService.WECHAT_DEFAULT_COMPLETE_ORGID.equals(current.getOrgid())
                || !"2".equals(current.getType())
                || !"5".equals(current.getCurrentPreachform())
                || !Long.valueOf(1L).equals(current.getSendstate())
                || Long.valueOf(1L).equals(current.getSubmit())) {
            return false;
        }
        // 查询当前子任务自己的公众号发送方式,必须是已发送(2)或已领取(3)
        ServiceSubtaskPreachform query = new ServiceSubtaskPreachform();
        query.setSubid(current.getId());
        query.setTaskid(current.getTaskid());
        query.setPreachform("5");
        List<ServiceSubtaskPreachform> preachforms = taskPreachformPort.selectServiceSubtaskPreachformList(query);
        if (CollectionUtils.isEmpty(preachforms)) {
            return false;
        }
        ServiceSubtaskPreachform currentPreachform = preachforms.stream()
                .filter(item -> "2".equals(item.getSendstate()) || "3".equals(item.getSendstate()))
                .findFirst().orElse(null);
        if (currentPreachform == null) {
            return false;
        }
        // 只给当前 subid 补齐默认选项;至少有一题默认选项才继续
        int filled = fillWechatDefaultDetailForCurrentSubtask(current);
        if (filled <= 0) {
            log.info("【微信公众号默认完成】subid:{} 没有可填充的默认选项,跳过", current.getId());
            return false;
        }
        // 当前公众号发送方式:已发送/已领取 -> 完成(9)
        ServiceSubtaskPreachform updatePreachform = new ServiceSubtaskPreachform();
        updatePreachform.setId(currentPreachform.getId());
        updatePreachform.setSubid(current.getId());
        updatePreachform.setPreachform("5");
        updatePreachform.setOrgid(current.getOrgid());
        updatePreachform.setRemark("已领取未填写,按默认选项完成");
        int preachformUpdated = taskPreachformPort.updateWechatPreachformComplete(updatePreachform);
        if (preachformUpdated == 0) {
            throw new BaseException("微信公众号默认完成失败:发送方式状态已变化,subid=" + current.getId());
        }
        // 当前子任务:已领取(1) -> 完成(6),只更新这一个 subid
        ServiceSubtask complete = new ServiceSubtask();
        complete.setId(current.getId());
        complete.setOrgid(current.getOrgid());
        complete.setFinishtime(new Date());
        complete.setResult("success");
        complete.setRemark("已领取未填写,按默认选项完成");
        int subtaskUpdated = serviceSubtaskMapper.updateWechatDefaultComplete(complete);
        if (subtaskUpdated == 0) {
            throw new BaseException("微信公众号默认完成失败:子任务状态已变化,subid=" + current.getId());
        }
        log.info("【微信公众号默认完成】subid:{},已按默认选项完成,填充题目数:{}", current.getId(), filled);
        return true;
    }
    /**
     * 仅给当前子任务补齐问卷单选/多选题中 default_value=2 的选项。
     * 已有非空答案的题目跳过,不覆盖患者已填写内容。
     *
     * @return 实际填充(或此前已由系统默认填充)的题目数
     */
    private int fillWechatDefaultDetailForCurrentSubtask(ServiceSubtask serviceSubtask) {
        Long templateId = serviceSubtask.getTemplateid();
        ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid());
        if (templateId == null && serviceTask != null) {
            templateId = serviceTask.getTemplateid();
        }
        if (templateId == null) {
            return 0;
        }
        SvyTaskTemplateScript scriptQuery = new SvyTaskTemplateScript();
        scriptQuery.setTemplateID(templateId);
        List<SvyTaskTemplateScript> scripts = svyTaskTemplateScriptService.selectSvyTaskTemplateScriptList(scriptQuery);
        if (CollectionUtils.isEmpty(scripts)) {
            return 0;
        }
        int filled = 0;
        for (SvyTaskTemplateScript script : scripts) {
            SvyTaskTemplateScriptVO scriptVO = DtoConversionUtils.sourceToTarget(script, SvyTaskTemplateScriptVO.class);
            SvyTaskTemplateScriptVO scriptWithOptions = svyTaskTemplateScriptService.selectInfoByCondition(scriptVO);
            if (scriptWithOptions == null
                    || scriptWithOptions.getId() == null
                    || CollectionUtils.isEmpty(scriptWithOptions.getSvyTaskTemplateTargetoptions())
                    || (!"1".equals(scriptWithOptions.getScriptType()) && !"2".equals(scriptWithOptions.getScriptType()))) {
                continue;
            }
            List<SvyTaskTemplateTargetoption> options = scriptWithOptions.getSvyTaskTemplateTargetoptions();
            List<SvyTaskTemplateTargetoption> defaultOptions = options.stream()
                    .filter(option -> option.getDefaultValue() != null && option.getDefaultValue() == 2L)
                    .filter(option -> StringUtils.isNotBlank(option.getOptioncontent()))
                    .collect(Collectors.toList());
            if (CollectionUtils.isEmpty(defaultOptions)) {
                continue;
            }
            // 已有非空答案则不覆盖,避免影响患者已填写内容
            ServiceSubtaskDetail detailQuery = new ServiceSubtaskDetail();
            detailQuery.setSubId(serviceSubtask.getId());
            detailQuery.setScriptid(scriptWithOptions.getId());
            List<ServiceSubtaskDetail> existingDetails = serviceSubtaskDetailMapper.selectServiceSubtaskDetailList(detailQuery);
            ServiceSubtaskDetail existing = CollectionUtils.isNotEmpty(existingDetails) ? existingDetails.get(0) : null;
            if (existing != null
                    && (StringUtils.isNotBlank(existing.getAsrtext()) || StringUtils.isNotBlank(existing.getMatchedtext()))) {
                // 之前已由系统默认填充过的,计入已填充,保证重试幂等
                if ("DEFAULT".equals(existing.getOperate())) {
                    filled++;
                }
                continue;
            }
            String targetValue = options.stream()
                    .map(SvyTaskTemplateTargetoption::getOptioncontent)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.joining("&"));
            String defaultValue = defaultOptions.stream()
                    .map(SvyTaskTemplateTargetoption::getOptioncontent)
                    .collect(Collectors.joining("&"));
            ServiceSubtaskDetail detail = new ServiceSubtaskDetail();
            detail.setSubId(serviceSubtask.getId());
            detail.setTaskid(serviceSubtask.getTaskid());
            detail.setPatid(serviceSubtask.getPatid());
            detail.setScriptid(scriptWithOptions.getId());
            detail.setTemplatequestionnum(scriptWithOptions.getId());
            detail.setQuestiontext(scriptWithOptions.getScriptContent());
            detail.setCategoryid(scriptWithOptions.getCategoryid());
            detail.setTargetid(scriptWithOptions.getTargetid() != null ? scriptWithOptions.getTargetid().intValue() : null);
            detail.setValueType(scriptWithOptions.getScriptType());
            detail.setTargetvalue(targetValue);
            detail.setAsrtext(defaultValue);
            detail.setMatchedtext(defaultValue);
            detail.setOperate("DEFAULT");
            detail.setTemplateType(2);
            detail.setLibTemplateid(serviceSubtask.getLibtemplateid() != null ? serviceSubtask.getLibtemplateid().intValue() : null);
            detail.setTemplateid(templateId.toString());
            detail.setGuid(scriptWithOptions.getGuid());
            detail.setOrgid(StringUtils.isNotEmpty(scriptWithOptions.getOrgid()) ? scriptWithOptions.getOrgid()
                    : (serviceTask != null ? serviceTask.getOrgid() : null));
            detail.setCreateTime(new Date());
            detail.setUpdateTime(new Date());
            if (existing != null) {
                detail.setId(existing.getId());
                serviceSubtaskDetailMapper.updateServiceSubtaskDetail(detail);
            } else {
                serviceSubtaskDetailMapper.insertServiceSubtaskDetail(detail);
            }
            filled++;
        }
        return filled;
    }
    @Override
    public Map<String, Object> getHeLibraryCount(@RequestBody HeLibraryCountVO heLibraryCountVO) {
        if (ObjectUtils.isEmpty(heLibraryCountVO)) {
@@ -5048,16 +5496,46 @@
        String ip = localIP;
        ServiceTask serviceTask = new ServiceTask();
        serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceTaskVo.getTaskid());
        if (ObjectUtils.isEmpty(serviceTask)) {
            // 任务不存在 → 不可能有 data,直接抛业务异常(接口层会转成 code=500)
            throw new BaseException("扫码生成任务失败:任务不存在或已删除(taskid=" + serviceTaskVo.getTaskid() + ")");
        }
        if (ObjectUtils.isNotEmpty(serviceTask)) {
            try {
                ServiceTaskdept serviceTaskdept = new ServiceTaskdept();
                if (ObjectUtils.isNotEmpty(serviceTaskVo.getDeptcode())) {
                    serviceTaskdept.setDeptType("1");
                    serviceTaskdept.setDeptCode(serviceTaskVo.getDeptcode());
                }
                if (ObjectUtils.isNotEmpty(serviceTaskVo.getLeavehospitaldistrictcode())) {
                // 维度(科室/病区/点位)由任务的 appltype 决定:1=科室、2=病区、7=点位。
                //   三种维度在 service_taskdept 里是**各自独立的行**(各自的列),可以共存、也可以不共存;
                //   但查行时必须只带一个维度 —— 否则会用 A 维度的编码去撞 B 维度的行,
                //   而点位行的 dept_code 本来就是空的。
                String appltype = serviceTask.getAppltype();
                if ("7".equals(appltype)) {
                    serviceTaskdept.setDeptType("3");
                    serviceTaskdept.setPointLocation(firstNonBlank(serviceTaskVo.getPointLocation(),
                            serviceTask.getPointLocation()));
                } else if ("2".equals(appltype)) {
                    serviceTaskdept.setDeptType("2");
                    serviceTaskdept.setDeptCode(serviceTaskVo.getLeavehospitaldistrictcode());
                    // 任务上的科室/病区可能是逗号多选,查 service_taskdept 要用单值(取第一个)
                    serviceTaskdept.setDeptCode(firstNonBlank(serviceTaskVo.getLeavehospitaldistrictcode(),
                            firstValue(serviceTask.getLeavehospitaldistrictcode())));
                } else if ("1".equals(appltype)) {
                    serviceTaskdept.setDeptType("1");
                    serviceTaskdept.setDeptCode(firstNonBlank(serviceTaskVo.getDeptcode(),
                            firstValue(serviceTask.getDeptcode())));
                } else {
                    // appltype 为空的历史任务:沿用改造前的口径(科室 → 病区,后写的覆盖先写的),
                    // 点位作为第三维,只在传了点位时补上。
                    if (ObjectUtils.isNotEmpty(serviceTaskVo.getDeptcode())) {
                        serviceTaskdept.setDeptType("1");
                        serviceTaskdept.setDeptCode(serviceTaskVo.getDeptcode());
                    }
                    if (ObjectUtils.isNotEmpty(serviceTaskVo.getLeavehospitaldistrictcode())) {
                        serviceTaskdept.setDeptType("2");
                        serviceTaskdept.setDeptCode(serviceTaskVo.getLeavehospitaldistrictcode());
                    }
                    if (ObjectUtils.isNotEmpty(serviceTaskVo.getPointLocation())) {
                        serviceTaskdept.setDeptType("3");
                        serviceTaskdept.setPointLocation(serviceTaskVo.getPointLocation());
                    }
                }
                serviceTaskdept.setServiceType(serviceTask.getServiceType());
                serviceTaskdept.setOrgid(serviceTask.getOrgid());
@@ -5094,10 +5572,21 @@
                        iServiceOutPathService.updateServiceOutPath(serviceOutPath);
                    }
                } else {
                    // 配置缺失属「业务可预期」情况:带上排查信息抛出去(不吞),
                    // 接口层据此返回 code=500 + 明确 msg,避免「200 + 空 data」。
                    throw new BaseException("扫码生成任务失败:未匹配到任务维度配置(taskid=" + serviceTaskVo.getTaskid()
                            + ",appltype=" + appltype
                            + ",deptType=" + serviceTaskdept.getDeptType()
                            + ",deptCode=" + serviceTaskdept.getDeptCode()
                            + ",pointLocation=" + serviceTaskdept.getPointLocation() + ")");
                }
            } catch (BaseException be) {
                // ⚠️ 必须排在 catch (Exception) 之前:BaseException 是 RuntimeException,
                //    否则刚抛出的业务异常会被下面整段吞掉,接口又变成 200 + 空 data。
                throw be;
            } catch (Exception e) {
                e.printStackTrace();
                log.error("【扫码生成任务】处理异常:{}", e.getMessage());
                log.error("【扫码生成任务】处理异常:taskid={},{}", serviceTaskVo.getTaskid(), e.getMessage(), e);
            }
        }
        return content;
@@ -5115,6 +5604,9 @@
        if (StringUtils.isNotEmpty(serviceTaskdept.getDeptType()) && serviceTaskdept.getDeptType().equals("1")) {
            serviceSubtask.setLeaveldeptcode(serviceTaskdept.getDeptCode());
            serviceSubtask.setLeaveldeptname(serviceTaskdept.getDeptName());
        } else if (StringUtils.isNotEmpty(serviceTaskdept.getDeptType()) && serviceTaskdept.getDeptType().equals("3")) {
            // 点位行(dept_type='3'):dept_code/dept_name 为空,点位只落 point_location
            serviceSubtask.setPointLocation(serviceTaskdept.getPointLocation());
        } else {
            serviceSubtask.setLeavehospitaldistrictcode(serviceTaskdept.getDeptCode());
            serviceSubtask.setLeavehospitaldistrictname(serviceTaskdept.getDeptName());