陈昶聿
2 天以前 5bbd99ba550aa2e8cd8da36644018981ca1a1921
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -27,6 +27,7 @@
import com.smartor.domain.DTO.MzMydScoreRankDTO;
import com.smartor.domain.DTO.QuestionCountKsOrBqDTO;
import com.smartor.domain.DTO.ServiceSubtaskDetailDTO;
import com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO;
import com.smartor.domain.*;
import com.smartor.domain.VO.*;
import com.smartor.domain.entity.ServiceSubtaskEntity;
@@ -631,6 +632,43 @@
            result.putIfAbsent("wxsf", 0);
            result.putIfAbsent("xsf", 0);
            result.putIfAbsent("dsf", 0);
            result.putIfAbsent("ywc", 0);
            result.putIfAbsent("yc", 0);
            result.putIfAbsent("jg", 0);
        }
        return result;
    }
    /**
     * 统计任务各种状态的数量(已优化)
     * 优化说明:
     * 1. 使用数据库层聚合计算,替代应用层遍历
     * 2. 性能提升 10-100 倍(取决于数据量)
     * 3. 降低内存占用和网络传输
     *
     * @param serviceSubtaskEntity 查询条件
     * @return 各状态统计结果
     */
    @Override
    public Map<String, Object> countByCondition(ServiceSubtaskEntity serviceSubtaskEntity) {
        // 直接调用数据库统计方法,在数据库层完成聚合计算
        Map<String, Object> result = serviceSubtaskMapper.countByCondition(serviceSubtaskEntity);
        // 处理空异常
        if (MapUtils.isEmpty(result)) {
            result = new HashMap<>();
            // 确保所有 key 都有值,避免空指针异常
            // MyBatis 的 SUM 在没有匹配行时会返回 null
            result.putIfAbsent("wzx", 0);
            result.putIfAbsent("ysf", 0);
            result.putIfAbsent("fssb", 0);
            result.putIfAbsent("yfs", 0);
            result.putIfAbsent("blq", 0);
            result.putIfAbsent("total", 0);
            result.putIfAbsent("wxsf", 0);
            result.putIfAbsent("xsf", 0);
            result.putIfAbsent("dsf", 0);
            result.putIfAbsent("dfs", 0);
            result.putIfAbsent("yfs", 0);
            result.putIfAbsent("ywc", 0);
            result.putIfAbsent("yc", 0);
            result.putIfAbsent("jg", 0);
@@ -5230,13 +5268,13 @@
        return resultMap;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType) {
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds) {
        //先清理一下重复数据
        List<Long> repeatIds = serviceSubtaskDetailMapper.selectRepeatDetailIds();
        if (!CollectionUtils.isEmpty(repeatIds)) {
            serviceSubtaskDetailMapper.deleteRepeatDetailByIds(repeatIds);
        }
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds);
        // 处理 targetvalue:按 & 分割成多条记录
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
@@ -5246,7 +5284,9 @@
                // 如果有多条目标值,拆分成多行
                for (String value : values) {
                    //先去 result 中查一下,有没有这个 questiontext 和对应的 targetvalue
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    //注意必须带上 taskid 一起匹配:否则会把别的任务的数据填到本任务的行上,
                    //导致导出的「数量/比例」和该行的「任务名称/任务编号」对不上(同一任务同一题出现两个不同分母)
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> Objects.equals(e.getTaskid(), export.getTaskid()) && e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    if (existingExport != null && existingExport.getRatio().equals("0") && existingExport.getCount().equals("0")) {
                        //如果存在,则将 result 里的这个问题所对应的 Targetvalue=value 这条数据更新成当前这条数据
                        if (value.equals(export.getOptionresult())) {
@@ -5275,6 +5315,152 @@
        }
        return result;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        if (CollectionUtils.isEmpty(subtaskIds)) {
            return result;
        }
        List<ServiceSubtaskOptionStatDTO> statList = serviceSubtaskMapper.statMydQuestionOptionForExport(subtaskIds, questiontext);
        if (CollectionUtils.isEmpty(statList)) {
            return result;
        }
        return buildMydOptionRows(statList);
    }
    /**
     * 把「任务+题目+选项」的作答统计拼成导出的「问题详情占比」行
     * <ul>
     *     <li>选项清单:题目配置过的选项在 targetvalue(用 & 连接)里,全部列出,没人选的显示 0</li>
     *     <li>数量:患者选中的选项在 asrtext 里(多选题是「A&B」),每个选中项各记一次人数,一个患者只算一条明细</li>
     *     <li>比例:数量 / 该题作答患者数,所以单选题各选项数量之和 = 该题作答患者数</li>
     *     <li>分数:该选项明细的 score 之和;多选行按选中项份数均分,保证各选项分数之和 = 明细分数之和</li>
     * </ul>
     */
    private static List<ServiceSubtaskDetailRatioExport> buildMydOptionRows(List<ServiceSubtaskOptionStatDTO> statList) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        // 按「任务 + 题目」分组,保持 SQL 的顺序(先题目后任务)
        Map<String, List<ServiceSubtaskOptionStatDTO>> grouped = new LinkedHashMap<>();
        for (ServiceSubtaskOptionStatDTO stat : statList) {
            String key = stat.getTaskid() + "\u0001" + stat.getQuestiontext();
            grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(stat);
        }
        for (List<ServiceSubtaskOptionStatDTO> group : grouped.values()) {
            ServiceSubtaskOptionStatDTO first = group.get(0);
            // 选项 -> 人数(long[1] 便于累加),顺序即出现顺序
            Map<String, long[]> countMap = new LinkedHashMap<>();
            Map<String, BigDecimal> scoreMap = new LinkedHashMap<>();
            for (ServiceSubtaskOptionStatDTO stat : group) {
                // targetvalue = 本题所有选项的清单,没人选的也要列出来
                List<String> options = splitOptions(stat.getTargetvalue());
                // asrtext = 患者实际选中的选项,多选题会是「选中项1&选中项2」
                List<String> chosenOptions = splitOptions(stat.getOptionresult());
                for (String option : options) {
                    countMap.computeIfAbsent(option, k -> new long[1]);
                    scoreMap.computeIfAbsent(option, k -> BigDecimal.ZERO);
                }
                long patientCount = stat.getPatientCount() == null ? 0L : stat.getPatientCount();
                BigDecimal scoreSum = stat.getScoreSum() == null ? BigDecimal.ZERO : stat.getScoreSum();
                if (!chosenOptions.isEmpty()) {
                    BigDecimal[] scoreParts = splitScore(scoreSum, chosenOptions.size());
                    for (int i = 0; i < chosenOptions.size(); i++) {
                        String chosen = chosenOptions.get(i);
                        // 选中的选项可能不在配置清单里(历史数据),补一行
                        countMap.computeIfAbsent(chosen, k -> new long[1]);
                        scoreMap.computeIfAbsent(chosen, k -> BigDecimal.ZERO);
                        countMap.get(chosen)[0] += patientCount;
                        scoreMap.put(chosen, scoreMap.get(chosen).add(scoreParts[i]));
                    }
                } else if (options.size() == 1) {
                    // 没有 asrtext(未答/无应答)时,只有唯一选项才能确定归属
                    countMap.get(options.get(0))[0] += patientCount;
                    scoreMap.put(options.get(0), scoreMap.get(options.get(0)).add(scoreSum));
                }
            }
            long total = 0L;
            for (long[] count : countMap.values()) total += count[0];
            for (Map.Entry<String, long[]> entry : countMap.entrySet()) {
                long count = entry.getValue()[0];
                ServiceSubtaskDetailRatioExport row = new ServiceSubtaskDetailRatioExport();
                row.setQuestiontext(first.getQuestiontext());
                row.setOptionresult(entry.getKey());
                row.setTargetvalue(entry.getKey());
                row.setCount(String.valueOf(count));
                row.setRatio(formatRatio(count, total));
                row.setScore(formatScore(scoreMap.get(entry.getKey())));
                row.setTaskname(first.getTaskName());
                row.setTaskid(first.getTaskid());
                row.setValueType(first.getValueType());
                result.add(row);
            }
        }
        return result;
    }
    /**
     * 把一条明细的分数按选中项份数均分,余数补给第一个,保证各份之和 = 原分数
     */
    private static BigDecimal[] splitScore(BigDecimal total, int parts) {
        BigDecimal[] result = new BigDecimal[parts];
        if (parts <= 0) {
            return result;
        }
        BigDecimal each = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.HALF_UP);
        for (int i = 0; i < parts; i++) {
            result[i] = each;
        }
        result[0] = result[0].add(total.subtract(each.multiply(BigDecimal.valueOf(parts))));
        return result;
    }
    /**
     * 拆分题目配置的选项(多个选项用 & 连接),顺带去掉全角/半角空白
     */
    private static List<String> splitOptions(String targetvalue) {
        List<String> options = new ArrayList<>();
        if (StringUtils.isEmpty(targetvalue)) {
            return options;
        }
        for (String part : targetvalue.split("&")) {
            String option = trimAll(part);
            if (StringUtils.isNotEmpty(option) && !options.contains(option)) {
                options.add(option);
            }
        }
        return options;
    }
    /**
     * 比例:数量 / 该题作答患者数,保留两位小数
     */
    private static String formatRatio(long count, long total) {
        if (total <= 0) {
            return "0.00%";
        }
        return BigDecimal.valueOf(count * 100.0 / total).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
    }
    /**
     * 分数之和:去掉多余的小数位(0.00 -> 0)
     */
    private static String formatScore(BigDecimal score) {
        if (score == null) {
            return "0";
        }
        BigDecimal value = score.stripTrailingZeros();
        if (value.scale() < 0) {
            value = value.setScale(0);
        }
        return value.toPlainString();
    }
    /*
@@ -6052,9 +6238,10 @@
            redisCache.deleteObject(phoneCallReqYQVO.getUuid() + "PlayEventCallbackPlaystop");
//                        //获取下一题
            log.error("获取下一题的信息:{}", ivrTaskTemplateScriptVO);
            if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1") || ivrTaskTemplateScriptVO.getBranchFlag().equals("0") && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) {
            //branchFlag为null时按分支0处理,避免NPE
            if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag()) || "0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) {
                Long nextQuestion = null;
                if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1")) {
                if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag())) {
                    nextQuestion = chosenOption.getNextQuestion();
                    //更新分数
                    double score = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE");
@@ -6087,14 +6274,11 @@
                    redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "- jsy", serviceTask.getJsy(), 120, TimeUnit.MINUTES);
                    phoneCallBackYQVO.setType("text");
                    phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + serviceTask.getJsy());
                    //将结果写进detail表
                    ServiceSubTaskDetailReq ssdReq = new ServiceSubTaskDetailReq();
                    List<ServiceSubtaskDetail> serviceSubtaskDetailList2 = new ArrayList<>();
                    serviceSubtaskDetailList.add(getServiceSubtaskDetail(phoneCallReqYQVO, ivrTaskTemplateScriptVO, serviceSubtask, ivrTaskTemplate));
                    serviceSubTaskDetailReq.setServiceSubtaskDetailList(serviceSubtaskDetailList2);
                    ssdReq.setGuid(phoneCallReqYQVO.getGuid());
                    ssdReq.setOrgid(phoneCallReqYQVO.getOrgid());
                    saveQuestionAnswerPhone(ssdReq);
                    //将结果写进detail表(本题的明细在上面匹配成功时已经组装并存入serviceSubtaskDetailList,这里直接落库,避免重复新增)
                    serviceSubTaskDetailReq.setServiceSubtaskDetailList(serviceSubtaskDetailList);
                    serviceSubTaskDetailReq.setGuid(phoneCallReqYQVO.getGuid());
                    serviceSubTaskDetailReq.setOrgid(phoneCallReqYQVO.getOrgid());
                    saveQuestionAnswerPhone(serviceSubTaskDetailReq);
                    return phoneCallBackYQVO;
                }
@@ -6197,7 +6381,9 @@
        if (hasFollowUp) {
            //获取serviceSubtaskList中的所有taskid
            List<Long> taskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getTaskid).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType());
            //同时带上本次导出的子任务 id:让「问题详情占比」只统计这批患者,与「患者随访信息表单」口径一致
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType(), subtaskIds);
        } else if (hasSatisfaction) {
            //满意度统计 (通过 subtask 的 id 去统计)
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).collect(Collectors.toList());