liusheng
2026-09-28 93078044580c2781cb8cf3ac18d2ab90513267de
smartor/src/main/java/com/smartor/service/impl/PatSatisfactionServiceImpl.java
@@ -5,7 +5,11 @@
import java.util.*;
import java.util.stream.Collectors;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.SysUserDept;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.Arith;
import com.ruoyi.common.utils.ArrayListUtils;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.DtoConversionUtils;
import com.smartor.domain.*;
@@ -13,12 +17,15 @@
import com.smartor.domain.VO.PatSatisfactionReqVO;
import com.smartor.domain.VO.PatSatisfactionResVO;
import com.smartor.domain.entity.PatSatisfactionDetailEntity;
import com.smartor.domain.entity.SatisfactionStatQuery;
import com.smartor.domain.entity.ServiceSubtaskSatisfactionEntity;
import com.smartor.mapper.*;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.smartor.service.IPatSatisfactionService;
import static com.ruoyi.common.utils.SecurityUtils.getLoginUser;
/**
 * 患者满意度Service业务层处理
@@ -101,6 +108,50 @@
    }
    /**
     * 新增患者满意度
     *
     * @param patSatisfactions 患者满意度
     * @return 结果
     */
    @Override
    public int batchInsert(List<PatSatisfaction> patSatisfactions) {
        if(CollectionUtils.isEmpty(patSatisfactions)){
            return 0;
        }
        List<Long> ids = patSatisfactions.stream().map(PatSatisfaction::getId).
                distinct().collect(Collectors.toList());
        List<PatSatisfaction> patSatisfactionsOld = new ArrayList<>();
        PatSatisfaction patSatisfactionVo = new PatSatisfaction();
        patSatisfactionVo.setIds(ids);
        patSatisfactionsOld = patSatisfactionMapper.selectPatSatisfactionList(patSatisfactionVo);
        Map<String, List<PatSatisfaction>> maps = ArrayListUtils.splitListsById(patSatisfactions,
                patSatisfactionsOld, PatSatisfaction::getId);
        //只在patSatisfactions的数据,执行新增操作
        List<PatSatisfaction> addItems = maps.get("onlyIn1");
        //只在patSatisfactionsOld的数据,执行删除操作
//        List<PatSatisfaction> delItems = maps.get("onlyIn2");
        //重叠的数据,执行更新操作
        List<PatSatisfaction> editItems = maps.get("common");
        Integer count = 0;
        LoginUser loginUser = getLoginUser();
        SysUser user = loginUser.getUser();
        for (PatSatisfaction patSatisfaction0 : editItems) {
            patSatisfaction0.setUpdateBy(user.getNickName());
            patSatisfaction0.setUpdateTime(DateUtils.getNowDate());
            count = count + patSatisfactionMapper.updatePatSatisfaction(patSatisfaction0);
        }
        for (PatSatisfaction patSatisfaction1 : addItems) {
            patSatisfaction1.setCreateBy(user.getNickName());
            patSatisfaction1.setOrgid(user.getOrgid());
            patSatisfaction1.setCreateTime(DateUtils.getNowDate());
            patSatisfaction1.setUpdateTime(DateUtils.getNowDate());
            count = count + patSatisfactionMapper.insertPatSatisfaction(patSatisfaction1);
        }
        return count;
    }
    /**
     * 修改患者满意度
     *
     * @param patSatisfaction 患者满意度
@@ -144,95 +195,35 @@
    public PatSatisfactionResVO statistics(PatSatisfactionReqVO patSatisfactionReqVO) {
        //定义返参
        PatSatisfactionResVO result = new PatSatisfactionResVO();
        //答题总人数(已经回答问题的)
        int totalPerson = 0;
        // 总人数(包含未回答的)
        int allPerson = 0;
        //患者满意度统详情
        List<PatSatisfactionDetailEntity> patSatisfactionDetailEntities = new ArrayList<>();
        // [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的)
        int[] personTotals = new int[2];
        List<ServiceSubtask> serviceSubtasks = serviceSubtaskMapper.selectSatisfactionSubtaskList(patSatisfactionReqVO);
        // 按type分组:1-语音,2-问卷
        Map<String, List<ServiceSubtask>> groupByType = serviceSubtasks.stream().filter(s -> s.getType() != null).collect(Collectors.groupingBy(ServiceSubtask::getType));
        List<ServiceSubtask> voiceList = groupByType.getOrDefault("1", Collections.emptyList());
        List<ServiceSubtask> questionnaireList = groupByType.getOrDefault("2", Collections.emptyList());
        // 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空)
        List<String> types = serviceSubtaskMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO);
        boolean hasVoice = types != null && types.contains("1");
        boolean hasQuestionnaire = types != null && types.contains("2");
        if (CollectionUtils.isNotEmpty(voiceList)) {
        if (hasVoice) {
            //1. 先获取ivr_liba_script_assort的满意度分类ID
            IvrLibaScriptAssort ivrLibaScriptAssort = new IvrLibaScriptAssort();
            ivrLibaScriptAssort.setOrgid(patSatisfactionReqVO.getOrgid());
            List<IvrLibaScriptAssort> ivrLibaScriptAssorts = ivrLibaScriptAssortMapper.selectIvrLibaScriptAssortList(ivrLibaScriptAssort);
            List<Long> mydIdList = ivrLibaScriptAssorts.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(IvrLibaScriptAssort::getId).collect(Collectors.toList());
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<IvrLibaTemplateScript> ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.queryLibTemplateIds(mydIdList);
            //3 再通过单个ivr_liba_template获取到所有的满意度问题 和 使用该模板的subTask人数
            //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
            List<SatScriptRef> scriptRefs = new ArrayList<>();
            for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScripts) {
                //记录单个明细信息
                PatSatisfactionDetailEntity patSatisfactionDetailEntity = new PatSatisfactionDetailEntity();
                //通过问题ID,获取问题选项(为了获取选项上的最高分最低分)
                IvrLibaTemplateTargetoption ivrLibaTemplateTargetoption = new IvrLibaTemplateTargetoption();
                ivrLibaTemplateTargetoption.setScriptid(ivrLibaTemplateScript.getScriptid());
                List<IvrLibaTemplateTargetoption> ivrLibaTemplateTargetoptions = ivrLibaTemplateTargetoptionMapper.selectIvrLibaTemplateTargetoptionList(ivrLibaTemplateTargetoption);
                //获取libTemplateId
                List<Long> libTemplateIds = new ArrayList<>();
                libTemplateIds.add(ivrLibaTemplateScript.getTemplateid());
                // 再通过模板id获取问题发送总量
                ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                serviceSubtaskSatisfactionEntity.setType(1);
                List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                //获取填报数量
                List<ServiceSubtaskDetailDTO> fillCount = null;
                if (CollectionUtils.isNotEmpty(subids))
                    fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, ivrLibaTemplateScript.getScriptContent());
                // 根据matchedtext分组,统计各选项数量及占比
                Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
                if (CollectionUtils.isNotEmpty(fillCount)) {
                    int totalFill = fillCount.size();
                    fillCount.stream().filter(dto -> dto.getMatchedtext() != null).collect(Collectors.groupingBy(ServiceSubtaskDetailDTO::getMatchedtext, Collectors.counting())).forEach((matchedtext, count) -> {
                        Map<String, Object> stat = new HashMap<>();
                        stat.put("count", count);
                        stat.put("ratio", new BigDecimal((double) count / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                        matchedtextStats.put(matchedtext, stat);
                    });
                    // 从fillCount中取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0
                    fillCount.stream().filter(dto -> dto.getTargetvalue() != null).findFirst().ifPresent(dto -> {
                        for (String option : dto.getTargetvalue().split("&")) {
                            String optionTrim = option.trim();
                            if (!matchedtextStats.containsKey(optionTrim)) {
                                Map<String, Object> emptyStat = new HashMap<>();
                                emptyStat.put("count", 0L);
                                emptyStat.put("ratio", 0.0);
                                matchedtextStats.put(optionTrim, emptyStat);
                            }
                        }
                    });
                }
                patSatisfactionDetailEntity.setScriptContent(ivrLibaTemplateScript.getScriptContent());
                patSatisfactionDetailEntity.setAnswerPerson(CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size());
                patSatisfactionDetailEntity.setNoAnswerPerson(CollectionUtils.isNotEmpty(fillCount) && CollectionUtils.isNotEmpty(subids) ? (subids.size() - fillCount.size()) : CollectionUtils.isNotEmpty(subids) ? subids.size() : 0);
                patSatisfactionDetailEntity.setAverageScore(averageScore(fillCount));
                patSatisfactionDetailEntity.setMaxScore(maxScore(ivrLibaTemplateTargetoptions));
                patSatisfactionDetailEntity.setMinScore(minScore(ivrLibaTemplateTargetoptions));
                patSatisfactionDetailEntity.setAnswerRate(CollectionUtils.isEmpty(subids) ? 0 : CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size() * 1.0 / subids.size());
                patSatisfactionDetailEntity.setMatchedtextStats(matchedtextStats);
                patSatisfactionDetailEntities.add(patSatisfactionDetailEntity);
                totalPerson += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                allPerson += subids.size();
                scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent()));
            }
            buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals);
        }
        if (CollectionUtils.isNotEmpty(questionnaireList)) {
        if (hasQuestionnaire) {
            //1. 先获取svy_lib_script_category的满意度分类ID
            SvyLibScriptCategory svyLibScriptCategory = new SvyLibScriptCategory();
            svyLibScriptCategory.setOrgid(patSatisfactionReqVO.getOrgid());
@@ -242,75 +233,17 @@
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<SvyLibTemplateScript> svyLibTemplateScripts = svyLibTemplateScriptMapper.querySvyLibTemplateIds(mydIdList);
            //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
            List<SatScriptRef> scriptRefs = new ArrayList<>();
            for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScripts) {
                //记录单个明细信息
                PatSatisfactionDetailEntity patSatisfactionDetailEntity = new PatSatisfactionDetailEntity();
                //通过问题ID,获取问题选项(为了获取选项上的最高分最低分)
                SvyLibTemplateTargetoption svyLibTemplateTargetoption = new SvyLibTemplateTargetoption();
                svyLibTemplateTargetoption.setScriptid(svyLibTemplateScript.getId());
                List<SvyLibTemplateTargetoption> svyLibTemplateTargetoptions = svyLibTemplateTargetoptionMapper.selectSvyLibTemplateTargetoptionList(svyLibTemplateTargetoption);
                //获取libTemplateId
                List<Long> libTemplateIds = new ArrayList<>();
                libTemplateIds.add(svyLibTemplateScript.getSvyid());
                // 再通过模板id获取问题发送总量
                ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                serviceSubtaskSatisfactionEntity.setType(2);
                List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                //获取填报数量
                List<ServiceSubtaskDetailDTO> fillCount = null;
                if (CollectionUtils.isNotEmpty(subids))
                    fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, svyLibTemplateScript.getScriptContent());
                // 根据matchedtext分组,统计各选项数量及占比
                Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
                if (CollectionUtils.isNotEmpty(fillCount)) {
                    int totalFill = fillCount.size();
                    fillCount.stream().filter(dto -> dto.getMatchedtext() != null).collect(Collectors.groupingBy(ServiceSubtaskDetailDTO::getMatchedtext, Collectors.counting())).forEach((matchedtext, count) -> {
                        Map<String, Object> stat = new HashMap<>();
                        stat.put("count", count);
                        stat.put("ratio", new BigDecimal((double) count / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                        matchedtextStats.put(matchedtext, stat);
                    });
                    // 从fillCount中取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0
                    fillCount.stream().filter(dto -> dto.getTargetvalue() != null).findFirst().ifPresent(dto -> {
                        for (String option : dto.getTargetvalue().split("&")) {
                            String optionTrim = option.trim();
                            if (!matchedtextStats.containsKey(optionTrim)) {
                                Map<String, Object> emptyStat = new HashMap<>();
                                emptyStat.put("count", 0L);
                                emptyStat.put("ratio", 0.0);
                                matchedtextStats.put(optionTrim, emptyStat);
                            }
                        }
                    });
                }
                patSatisfactionDetailEntity.setScriptContent(svyLibTemplateScript.getScriptContent());
                patSatisfactionDetailEntity.setAnswerPerson(CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size());
                patSatisfactionDetailEntity.setNoAnswerPerson(CollectionUtils.isNotEmpty(fillCount) && CollectionUtils.isNotEmpty(subids) ? (subids.size() - fillCount.size()) : CollectionUtils.isNotEmpty(subids) ? subids.size() : 0);
                patSatisfactionDetailEntity.setAverageScore(averageScore(fillCount));
                OptionalDouble maxOpt = svyLibTemplateTargetoptions.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).max();
                patSatisfactionDetailEntity.setMaxScore(maxOpt.isPresent() ? new BigDecimal(maxOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0);
                OptionalDouble minOpt = svyLibTemplateTargetoptions.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).min();
                patSatisfactionDetailEntity.setMinScore(minOpt.isPresent() ? new BigDecimal(minOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0);
                patSatisfactionDetailEntity.setAnswerRate(CollectionUtils.isEmpty(subids) ? 0 : CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size() * 1.0 / subids.size());
                patSatisfactionDetailEntity.setMatchedtextStats(matchedtextStats);
                patSatisfactionDetailEntities.add(patSatisfactionDetailEntity);
                totalPerson += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                allPerson += subids.size();
                // 问卷的选项表按 svy_lib_template_script 的主键关联
                scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent()));
            }
            buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals);
        }
        result.setTotalAnswerRate(Arith.div(totalPerson, allPerson));
        result.setTotalAnswerRate(Arith.div(personTotals[0], personTotals[1]));
        result.setPatSatisfactionDetailEntities(patSatisfactionDetailEntities);
        result.setTotalPerson(totalPerson);
        result.setTotalPerson(personTotals[0]);
        return result;
    }
@@ -325,19 +258,17 @@
    public PatSatisfactionResVO statisticsByDimension(PatSatisfactionReqVO patSatisfactionReqVO) {
        //定义返参
        PatSatisfactionResVO result = new PatSatisfactionResVO();
        //答题总人数(已经回答问题的)
        int totalPerson = 0;
        // 总人数(包含未回答的)
        int allPerson = 0;
        //患者满意度统详情
        List<PatSatisfactionDetailEntity> patSatisfactionDetailEntities = new ArrayList<>();
        List<ServiceSubtask> serviceSubtasks = serviceSubtaskMapper.selectSatisfactionSubtaskList(patSatisfactionReqVO);
        // 按type分组:1-语音,2-问卷
        Map<String, List<ServiceSubtask>> groupByType = serviceSubtasks.stream().filter(s -> s.getType() != null).collect(Collectors.groupingBy(ServiceSubtask::getType));
        List<ServiceSubtask> voiceList = groupByType.getOrDefault("1", Collections.emptyList());
        List<ServiceSubtask> questionnaireList = groupByType.getOrDefault("2", Collections.emptyList());
        // [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的)
        int[] personTotals = new int[2];
        if (CollectionUtils.isNotEmpty(voiceList)) {
        // 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空)
        List<String> types = serviceSubtaskMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO);
        boolean hasVoice = types != null && types.contains("1");
        boolean hasQuestionnaire = types != null && types.contains("2");
        if (hasVoice) {
            //1. 先获取入参中的维护集合
            IvrLibaTemplateScript iltscript = new IvrLibaTemplateScript();
            iltscript.setDimensionList(patSatisfactionReqVO.getDimensionList());
@@ -345,148 +276,34 @@
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<IvrLibaTemplateScript> ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.selectILTSBydimension(iltscript);
            if (CollectionUtils.isNotEmpty(ivrLibaTemplateScripts)) {
                //3 再通过单个ivr_liba_template获取到所有的满意度问题 和 使用该模板的subTask人数
                //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
                List<SatScriptRef> scriptRefs = new ArrayList<>();
                for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScripts) {
                    //记录单个明细信息
                    PatSatisfactionDetailEntity patSatisfactionDetailEntity = new PatSatisfactionDetailEntity();
                    //通过问题ID,获取问题选项(为了获取选项上的最高分最低分)
                    IvrLibaTemplateTargetoption ivrLibaTemplateTargetoption = new IvrLibaTemplateTargetoption();
                    ivrLibaTemplateTargetoption.setScriptid(ivrLibaTemplateScript.getScriptid());
                    List<IvrLibaTemplateTargetoption> ivrLibaTemplateTargetoptions = ivrLibaTemplateTargetoptionMapper.selectIvrLibaTemplateTargetoptionList(ivrLibaTemplateTargetoption);
                    //获取libTemplateId
                    List<Long> libTemplateIds = new ArrayList<>();
                    libTemplateIds.add(ivrLibaTemplateScript.getTemplateid());
                    // 再通过模板id获取问题发送总量
                    ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                    serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                    serviceSubtaskSatisfactionEntity.setType(1);
                    List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                    //获取填报数量
                    List<ServiceSubtaskDetailDTO> fillCount = null;
                    if (CollectionUtils.isNotEmpty(subids))
                        fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, ivrLibaTemplateScript.getScriptContent());
                    // 根据matchedtext分组,统计各选项数量及占比
                    Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
                    if (CollectionUtils.isNotEmpty(fillCount)) {
                        int totalFill = fillCount.size();
                        fillCount.stream().filter(dto -> dto.getMatchedtext() != null).collect(Collectors.groupingBy(ServiceSubtaskDetailDTO::getMatchedtext, Collectors.counting())).forEach((matchedtext, count) -> {
                            Map<String, Object> stat = new HashMap<>();
                            stat.put("count", count);
                            stat.put("ratio", new BigDecimal((double) count / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                            matchedtextStats.put(matchedtext, stat);
                        });
                        // 从fillCount中取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0
                        fillCount.stream().filter(dto -> dto.getTargetvalue() != null).findFirst().ifPresent(dto -> {
                            for (String option : dto.getTargetvalue().split("&")) {
                                String optionTrim = option.trim();
                                if (!matchedtextStats.containsKey(optionTrim)) {
                                    Map<String, Object> emptyStat = new HashMap<>();
                                    emptyStat.put("count", 0L);
                                    emptyStat.put("ratio", 0.0);
                                    matchedtextStats.put(optionTrim, emptyStat);
                                }
                            }
                        });
                    }
                    patSatisfactionDetailEntity.setScriptContent(ivrLibaTemplateScript.getScriptContent());
                    patSatisfactionDetailEntity.setAnswerPerson(CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size());
                    patSatisfactionDetailEntity.setNoAnswerPerson(CollectionUtils.isNotEmpty(fillCount) && CollectionUtils.isNotEmpty(subids) ? (subids.size() - fillCount.size()) : CollectionUtils.isNotEmpty(subids) ? subids.size() : 0);
                    patSatisfactionDetailEntity.setAverageScore(averageScore(fillCount));
                    patSatisfactionDetailEntity.setMaxScore(maxScore(ivrLibaTemplateTargetoptions));
                    patSatisfactionDetailEntity.setMinScore(minScore(ivrLibaTemplateTargetoptions));
                    patSatisfactionDetailEntity.setAnswerRate(CollectionUtils.isEmpty(subids) ? 0 : CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size() * 1.0 / subids.size());
                    patSatisfactionDetailEntity.setMatchedtextStats(matchedtextStats);
                    patSatisfactionDetailEntities.add(patSatisfactionDetailEntity);
                    totalPerson += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                    allPerson += subids.size();
                    scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent()));
                }
                buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals);
            }
        }
        if (CollectionUtils.isNotEmpty(questionnaireList)) {
        if (hasQuestionnaire) {
            //1. 先获取入参中的满意度维度
            SvyLibTemplateScriptVO svyLibTemplateScriptVO = new SvyLibTemplateScriptVO();
            svyLibTemplateScriptVO.setDimensionList(patSatisfactionReqVO.getDimensionList());
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<SvyLibTemplateScript> svyLibTemplateScripts = svyLibTemplateScriptMapper.selectSLTScriptListBydimension(svyLibTemplateScriptVO);
            if (CollectionUtils.isNotEmpty(svyLibTemplateScripts)) {
                //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
                List<SatScriptRef> scriptRefs = new ArrayList<>();
                for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScripts) {
                    //记录单个明细信息
                    PatSatisfactionDetailEntity patSatisfactionDetailEntity = new PatSatisfactionDetailEntity();
                    //通过问题ID,获取问题选项(为了获取选项上的最高分最低分)
                    SvyLibTemplateTargetoption svyLibTemplateTargetoption = new SvyLibTemplateTargetoption();
                    svyLibTemplateTargetoption.setScriptid(svyLibTemplateScript.getId());
                    List<SvyLibTemplateTargetoption> svyLibTemplateTargetoptions = svyLibTemplateTargetoptionMapper.selectSvyLibTemplateTargetoptionList(svyLibTemplateTargetoption);
                    //获取libTemplateId
                    List<Long> libTemplateIds = new ArrayList<>();
                    libTemplateIds.add(svyLibTemplateScript.getSvyid());
                    // 再通过模板id获取问题发送总量
                    ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                    serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                    serviceSubtaskSatisfactionEntity.setType(2);
                    List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                    //获取填报数量
                    List<ServiceSubtaskDetailDTO> fillCount = null;
                    if (CollectionUtils.isNotEmpty(subids))
                        fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, svyLibTemplateScript.getScriptContent());
                    // 根据matchedtext分组,统计各选项数量及占比
                    Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
                    if (CollectionUtils.isNotEmpty(fillCount)) {
                        int totalFill = fillCount.size();
                        fillCount.stream().filter(dto -> dto.getMatchedtext() != null).collect(Collectors.groupingBy(ServiceSubtaskDetailDTO::getMatchedtext, Collectors.counting())).forEach((matchedtext, count) -> {
                            Map<String, Object> stat = new HashMap<>();
                            stat.put("count", count);
                            stat.put("ratio", new BigDecimal((double) count / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
                            matchedtextStats.put(matchedtext, stat);
                        });
                        // 从fillCount中取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0
                        fillCount.stream().filter(dto -> dto.getTargetvalue() != null).findFirst().ifPresent(dto -> {
                            for (String option : dto.getTargetvalue().split("&")) {
                                String optionTrim = option.trim();
                                if (!matchedtextStats.containsKey(optionTrim)) {
                                    Map<String, Object> emptyStat = new HashMap<>();
                                    emptyStat.put("count", 0L);
                                    emptyStat.put("ratio", 0.0);
                                    matchedtextStats.put(optionTrim, emptyStat);
                                }
                            }
                        });
                    }
                    patSatisfactionDetailEntity.setScriptContent(svyLibTemplateScript.getScriptContent());
                    patSatisfactionDetailEntity.setAnswerPerson(CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size());
                    patSatisfactionDetailEntity.setNoAnswerPerson(CollectionUtils.isNotEmpty(fillCount) && CollectionUtils.isNotEmpty(subids) ? (subids.size() - fillCount.size()) : CollectionUtils.isNotEmpty(subids) ? subids.size() : 0);
                    patSatisfactionDetailEntity.setAverageScore(averageScore(fillCount));
                    OptionalDouble maxOpt = svyLibTemplateTargetoptions.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).max();
                    patSatisfactionDetailEntity.setMaxScore(maxOpt.isPresent() ? new BigDecimal(maxOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0);
                    OptionalDouble minOpt = svyLibTemplateTargetoptions.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).min();
                    patSatisfactionDetailEntity.setMinScore(minOpt.isPresent() ? new BigDecimal(minOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0);
                    patSatisfactionDetailEntity.setAnswerRate(CollectionUtils.isEmpty(subids) ? 0 : CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size() * 1.0 / subids.size());
                    patSatisfactionDetailEntity.setMatchedtextStats(matchedtextStats);
                    patSatisfactionDetailEntities.add(patSatisfactionDetailEntity);
                    totalPerson += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                    allPerson += subids.size();
                    // 问卷的选项表按 svy_lib_template_script 的主键关联
                    scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent()));
                }
                buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals);
            }
        }
        result.setTotalAnswerRate(Arith.div(totalPerson, allPerson));
        result.setTotalAnswerRate(Arith.div(personTotals[0], personTotals[1]));
        result.setPatSatisfactionDetailEntities(patSatisfactionDetailEntities);
        result.setTotalPerson(totalPerson);
        result.setTotalPerson(personTotals[0]);
        return result;
    }
@@ -496,8 +313,8 @@
        //用来记录柱状图信息(key=满意度分类名称,value=满意度分类下的总人数和填报人数)
        Map<String, Object> zzt = new HashMap<>();
        // 记录一下每种分类的填报情况
        Map<String, List<ServiceSubtaskDetailDTO>> assortNameFillCountMap = new HashMap<>();
        // 记录一下每种分类的填报情况(只记分值与条数,不再把明细行全部查回 Java)
        Map<String, List<ScoreCount>> assortNameFillCountMap = new HashMap<>();
        if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) {
            //1. 先获取ivr_liba_script_assort的满意度分类ID
@@ -509,6 +326,11 @@
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<IvrLibaTemplateScript> ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.queryLibTemplateIds(mydIdList);
            //2.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次)
            SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1",
                    ivrLibaTemplateScripts.stream().map(IvrLibaTemplateScript::getTemplateid).collect(Collectors.toList()));
            Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
            SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
            //3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
            Map<String, List<IvrLibaTemplateScript>> scriptGroupByAssortName = ivrLibaTemplateScripts.stream().filter(item -> item.getScriptAssortname() != null).collect(Collectors.groupingBy(IvrLibaTemplateScript::getScriptAssortname));
@@ -519,27 +341,16 @@
                    //记录总的填报人数
                    Double fillCountAll = 0.0;
                    //获取该分类的填报总数量
                    List<ServiceSubtaskDetailDTO> allFillCountList = new ArrayList<>();
                    List<ScoreCount> allFillCountList = new ArrayList<>();
                    List<IvrLibaTemplateScript> ivrLibaTemplateScriptList = scriptGroupByAssortName.get(assortNameKey);
                    for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScriptList) {
                        //获取libTemplateId
                        List<Long> libTemplateIds = new ArrayList<>();
                        libTemplateIds.add(ivrLibaTemplateScript.getTemplateid());
                        // 再通过模板id获取问题发送总量
                        ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                        serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                        serviceSubtaskSatisfactionEntity.setType(1);
                        List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                        //获取填报数量
                        List<ServiceSubtaskDetailDTO> fillCount = null;
                        if (CollectionUtils.isNotEmpty(subids)) {
                            fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, ivrLibaTemplateScript.getScriptContent());
                            if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                        }
                        subidAll += subids.size();
                        fillCountAll += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                        //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size)
                        subidAll += sendCountMap.getOrDefault(ivrLibaTemplateScript.getTemplateid(), 0L);
                        //获取填报数量(原来是把明细行全部查回 Java 再取 size)
                        List<ScoreCount> fillCount = fillStat.get(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptContent());
                        if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                        fillCountAll += countFillRows(fillCount);
                    }
                    // 这里先记录一下每个分类的总人数和填报人数
@@ -547,7 +358,7 @@
                    personCount.put("fillCountAll", fillCountAll);
                    personCount.put("subidAll", subidAll);
                    personCount.put("receiveRate", Arith.div(fillCountAll, subidAll));
                    personCount.put("averageScore", averageScore(allFillCountList));
                    personCount.put("averageScore", averageScoreByStat(allFillCountList));
                    zzt.put(assortNameKey, personCount);
                    assortNameFillCountMap.put(assortNameKey, allFillCountList);
                }
@@ -564,6 +375,12 @@
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<SvyLibTemplateScript> svyLibTemplateScripts = svyLibTemplateScriptMapper.querySvyLibTemplateIds(mydIdList);
            //2.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次)
            SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2",
                    svyLibTemplateScripts.stream().map(SvyLibTemplateScript::getSvyid).collect(Collectors.toList()));
            Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
            SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
            //3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
            Map<String, List<SvyLibTemplateScript>> scriptGroupByatCegoryName = svyLibTemplateScripts.stream().filter(item -> item.getCategoryName() != null).collect(Collectors.groupingBy(SvyLibTemplateScript::getCategoryName));
            if (scriptGroupByatCegoryName != null && !scriptGroupByatCegoryName.isEmpty()) {
@@ -573,27 +390,16 @@
                    //记录总的填报人数
                    Double fillCountAll = 0.0;
                    //获取该分类的填报总数量
                    List<ServiceSubtaskDetailDTO> allFillCountList = new ArrayList<>();
                    List<ScoreCount> allFillCountList = new ArrayList<>();
                    List<SvyLibTemplateScript> svyLibTemplateScriptList = scriptGroupByatCegoryName.get(categoryNameKey);
                    for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScriptList) {
                        //获取libTemplateId
                        List<Long> libTemplateIds = new ArrayList<>();
                        libTemplateIds.add(svyLibTemplateScript.getSvyid());
                        // 再通过模板id获取问题发送总量
                        ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                        serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                        serviceSubtaskSatisfactionEntity.setType(2);
                        List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                        //获取填报数量
                        List<ServiceSubtaskDetailDTO> fillCount = null;
                        if (CollectionUtils.isNotEmpty(subids)) {
                            fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, svyLibTemplateScript.getScriptContent());
                            if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                        }
                        subidAll += subids.size();
                        fillCountAll += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                        //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size)
                        subidAll += sendCountMap.getOrDefault(svyLibTemplateScript.getSvyid(), 0L);
                        //获取填报数量(原来是把明细行全部查回 Java 再取 size)
                        List<ScoreCount> fillCount = fillStat.get(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getScriptContent());
                        if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                        fillCountAll += countFillRows(fillCount);
                    }
@@ -607,16 +413,16 @@
                        personCount.put("receiveRate", Arith.div((Double) personCount.get("fillCountAll"), (Double) personCount.get("subidAll")));
                        //看一下之前有没有该分类的填报量
                        List<ServiceSubtaskDetailDTO> serviceSubtaskDetailDTOS = assortNameFillCountMap.get(categoryNameKey);
                        List<ScoreCount> serviceSubtaskDetailDTOS = assortNameFillCountMap.get(categoryNameKey);
                        if (CollectionUtils.isNotEmpty(serviceSubtaskDetailDTOS))
                            allFillCountList.addAll(serviceSubtaskDetailDTOS);
                        personCount.put("averageScore", averageScore(allFillCountList));
                        personCount.put("averageScore", averageScoreByStat(allFillCountList));
                    } else {
                        personCount = new HashMap<>();
                        personCount.put("fillCountAll", fillCountAll);
                        personCount.put("subidAll", subidAll);
                        personCount.put("receiveRate", Arith.div(fillCountAll, subidAll));
                        personCount.put("averageScore", averageScore(allFillCountList));
                        personCount.put("averageScore", averageScoreByStat(allFillCountList));
                        zzt.put(categoryNameKey, personCount);
                    }
                }
@@ -628,11 +434,11 @@
    @Override
    public Map<String, Object> satisfactionGraphDimension(PatSatisfactionReqVO patSatisfactionReqVO) {
        //用来记录柱状图信息(key=满意度分类名称,value=满意度分类下的总人数和填报人数)
        //用来记录柱状图信息(key=满意度维度名称,value=该维度下的总人数和填报人数)
        Map<String, Object> zzt = new HashMap<>();
        // 记录一下每种分类的填报情况
        Map<String, List<ServiceSubtaskDetailDTO>> assortNameFillCountMap = new HashMap<>();
        // 记录一下每个维度的填报情况(只记分值与条数,不再把明细行全部查回 Java)
        Map<String, List<ScoreCount>> assortNameFillCountMap = new HashMap<>();
        if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) {
            //1. 从入参中获取满意度维维度
@@ -643,36 +449,32 @@
            List<IvrLibaTemplateScript> ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.selectILTSBydimension(ivrLibaTemplateScript2);
            if (ivrLibaTemplateScripts != null && !ivrLibaTemplateScripts.isEmpty()) {
                //3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
                Map<String, List<IvrLibaTemplateScript>> groupByDimension = ivrLibaTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.groupingBy(IvrLibaTemplateScript::getDimension));
                List<IvrLibaTemplateScript> dimensionScripts = ivrLibaTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList());
                Map<String, List<IvrLibaTemplateScript>> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(IvrLibaTemplateScript::getDimension));
                if (groupByDimension != null && !groupByDimension.isEmpty()) {
                    //3.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次)
                    SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1",
                            dimensionScripts.stream().map(IvrLibaTemplateScript::getTemplateid).collect(Collectors.toList()));
                    Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
                    SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
                    for (String dimension : groupByDimension.keySet()) {
                        //记录总人数
                        Double subidAll = 0.0;
                        //记录总的填报人数
                        Double fillCountAll = 0.0;
                        //获取该维度的填报总数量
                        List<ServiceSubtaskDetailDTO> allFillCountList = new ArrayList<>();
                        List<ScoreCount> allFillCountList = new ArrayList<>();
                        List<IvrLibaTemplateScript> ivrLibaTemplateScriptList = groupByDimension.get(dimension);
                        for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScriptList) {
                            //获取libTemplateId
                            List<Long> libTemplateIds = new ArrayList<>();
                            libTemplateIds.add(ivrLibaTemplateScript.getTemplateid());
                            // 再通过模板id获取问题发送总量
                            ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                            serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                            serviceSubtaskSatisfactionEntity.setType(1);
                            List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                            //获取填报数量
                            List<ServiceSubtaskDetailDTO> fillCount = null;
                            if (CollectionUtils.isNotEmpty(subids)) {
                                fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, ivrLibaTemplateScript.getScriptContent());
                                if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                            }
                            subidAll += subids.size();
                            fillCountAll += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                            //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size)
                            subidAll += sendCountMap.getOrDefault(ivrLibaTemplateScript.getTemplateid(), 0L);
                            //获取填报数量(原来是把明细行全部查回 Java 再取 size)
                            List<ScoreCount> fillCount = fillStat.get(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptContent());
                            if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                            fillCountAll += countFillRows(fillCount);
                        }
                        // 这里先记录一下每个分类的总人数和填报人数
@@ -680,7 +482,7 @@
                        personCount.put("fillCountAll", fillCountAll);
                        personCount.put("subidAll", subidAll);
                        personCount.put("receiveRate", Arith.div(fillCountAll, subidAll));
                        personCount.put("averageScore", averageScore(allFillCountList));
                        personCount.put("averageScore", averageScoreByStat(allFillCountList));
                        zzt.put(dimension, personCount);
                        assortNameFillCountMap.put(dimension, allFillCountList);
                    }
@@ -696,36 +498,32 @@
            //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合
            List<SvyLibTemplateScript> svyLibTemplateScripts = svyLibTemplateScriptMapper.selectSLTScriptListBydimension(svyLibTemplateScriptVO);
            if (svyLibTemplateScripts != null && !svyLibTemplateScripts.isEmpty()) {
                //3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
                Map<String, List<SvyLibTemplateScript>> groupByDimension = svyLibTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.groupingBy(SvyLibTemplateScript::getDimension));
                //3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
                List<SvyLibTemplateScript> dimensionScripts = svyLibTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList());
                Map<String, List<SvyLibTemplateScript>> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(SvyLibTemplateScript::getDimension));
                if (groupByDimension != null && !groupByDimension.isEmpty()) {
                    //3.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次)
                    SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2",
                            dimensionScripts.stream().map(SvyLibTemplateScript::getSvyid).collect(Collectors.toList()));
                    Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
                    SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
                    for (String dimension : groupByDimension.keySet()) {
                        //记录总人数
                        Double subidAll = 0.0;
                        //记录总的填报人数
                        Double fillCountAll = 0.0;
                        //获取该分类的填报总数量
                        List<ServiceSubtaskDetailDTO> allFillCountList = new ArrayList<>();
                        List<ScoreCount> allFillCountList = new ArrayList<>();
                        List<SvyLibTemplateScript> svyLibTemplateScriptList = groupByDimension.get(dimension);
                        for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScriptList) {
                            //获取libTemplateId
                            List<Long> libTemplateIds = new ArrayList<>();
                            libTemplateIds.add(svyLibTemplateScript.getSvyid());
                            // 再通过模板id获取问题发送总量
                            ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity = DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class);
                            serviceSubtaskSatisfactionEntity.setLibTemplateIds(libTemplateIds);
                            serviceSubtaskSatisfactionEntity.setType(2);
                            List<Long> subids = serviceSubtaskMapper.querySendCount(serviceSubtaskSatisfactionEntity);
                            //获取填报数量
                            List<ServiceSubtaskDetailDTO> fillCount = null;
                            if (CollectionUtils.isNotEmpty(subids)) {
                                fillCount = serviceSubtaskDetailMapper.queryFillCount(subids, svyLibTemplateScript.getScriptContent());
                                if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                            }
                            subidAll += subids.size();
                            fillCountAll += CollectionUtils.isEmpty(fillCount) ? 0 : fillCount.size();
                            //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size)
                            subidAll += sendCountMap.getOrDefault(svyLibTemplateScript.getSvyid(), 0L);
                            //获取填报数量(原来是把明细行全部查回 Java 再取 size)
                            List<ScoreCount> fillCount = fillStat.get(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getScriptContent());
                            if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
                            fillCountAll += countFillRows(fillCount);
                        }
@@ -739,16 +537,16 @@
                            personCount.put("receiveRate", Arith.div((Double) personCount.get("fillCountAll"), (Double) personCount.get("subidAll")));
                            //看一下之前有没有该分类的填报量
                            List<ServiceSubtaskDetailDTO> serviceSubtaskDetailDTOS = assortNameFillCountMap.get(dimension);
                            List<ScoreCount> serviceSubtaskDetailDTOS = assortNameFillCountMap.get(dimension);
                            if (CollectionUtils.isNotEmpty(serviceSubtaskDetailDTOS))
                                allFillCountList.addAll(serviceSubtaskDetailDTOS);
                            personCount.put("averageScore", averageScore(allFillCountList));
                            personCount.put("averageScore", averageScoreByStat(allFillCountList));
                        } else {
                            personCount = new HashMap<>();
                            personCount.put("fillCountAll", fillCountAll);
                            personCount.put("subidAll", subidAll);
                            personCount.put("receiveRate", Arith.div(fillCountAll, subidAll));
                            personCount.put("averageScore", averageScore(allFillCountList));
                            personCount.put("averageScore", averageScoreByStat(allFillCountList));
                            zzt.put(dimension, personCount);
                        }
                    }
@@ -766,8 +564,13 @@
     */
    private Double maxScore(List<IvrLibaTemplateTargetoption> options) {
        if (CollectionUtils.isEmpty(options)) return 0.0;
        OptionalDouble maxOpt = options.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).max();
        return maxOpt.isPresent() ? new BigDecimal(maxOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0;
        Double maxScore = options.stream()
                .map(IvrLibaTemplateTargetoption::getScore)
                .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                .mapToDouble(Double::parseDouble)
                .max()
                .orElse(0.0);
        return maxScore;
    }
    /**
@@ -778,8 +581,13 @@
     */
    private Double minScore(List<IvrLibaTemplateTargetoption> options) {
        if (CollectionUtils.isEmpty(options)) return 0.0;
        OptionalDouble minOpt = options.stream().filter(dto -> dto.getScore() != null).mapToDouble(dto -> dto.getScore().doubleValue()).min();
        return minOpt.isPresent() ? new BigDecimal(minOpt.getAsDouble()).setScale(1, RoundingMode.HALF_UP).doubleValue() : 0.0;
        Double minScore = options.stream()
                .map(IvrLibaTemplateTargetoption::getScore)
                .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                .mapToDouble(Double::parseDouble)
                .min()
                .orElse(0.0);
        return minScore;
    }
    /**
@@ -793,4 +601,464 @@
        double sum = fillCount.stream().filter(dto -> dto.getScore() != null && !dto.getScore().isEmpty()).mapToDouble(dto -> Double.parseDouble(dto.getScore())).sum();
        return new BigDecimal(sum / fillCount.size()).setScale(1, RoundingMode.HALF_UP).doubleValue();
    }
    // ==================== satisfactionGraph 聚合统计(替代逐个模板查库) ====================
    /**
     * 构建聚合统计入参(取值来源与原 DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class) 一致)
     *
     * @param reqVO       原始请求
     * @param type        service_subtask.type(1 语音、2 问卷),用字符串比较避免隐式转换
     * @param templateIds 本次请求涉及的模板id(ivr 用 templateid,问卷用 svyid)
     */
    private SatisfactionStatQuery buildSatisfactionStatQuery(PatSatisfactionReqVO reqVO, String type, List<Long> templateIds) {
        SatisfactionStatQuery statQuery = new SatisfactionStatQuery();
        statQuery.setOrgid(reqVO.getOrgid());
        statQuery.setCampusid(reqVO.getCampusid());
        statQuery.setType(type);
        statQuery.setStartTime(reqVO.getStartTime());
        statQuery.setEndTime(reqVO.getEndTime());
        statQuery.setDeptcodes(reqVO.getDeptcodes());
        statQuery.setHospitaldistrictcodes(reqVO.getHospitaldistrictcodes());
        statQuery.setLibTemplateIds(CollectionUtils.isEmpty(templateIds) ? templateIds
                : templateIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()));
        return statQuery;
    }
    /**
     * 一次查出全部模板的发送量
     *
     * @return key=模板id,value=发送量(等价于原 querySendCount 结果的 size)
     */
    private Map<Long, Long> mapSatisfactionSendCount(SatisfactionStatQuery statQuery) {
        Map<Long, Long> sendCountMap = new HashMap<>();
        if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return sendCountMap;
        List<Map<String, Object>> rows = serviceSubtaskMapper.selectSatisfactionSendCount(statQuery);
        if (CollectionUtils.isEmpty(rows)) return sendCountMap;
        for (Map<String, Object> row : rows) {
            Long templateId = toLong(row.get("libtemplateid"));
            if (templateId == null) continue;
            sendCountMap.put(templateId, toLong(row.get("cnt"), 0L));
        }
        return sendCountMap;
    }
    /**
     * 一次查出全部模板的填报量(按 模板+问题+分值 分组)
     */
    private SatisfactionFillStat querySatisfactionFillStat(SatisfactionStatQuery statQuery) {
        SatisfactionFillStat fillStat = new SatisfactionFillStat();
        if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat;
        List<Map<String, Object>> rows = serviceSubtaskDetailMapper.selectSatisfactionFillStat(statQuery);
        if (CollectionUtils.isEmpty(rows)) return fillStat;
        for (Map<String, Object> row : rows) {
            Long templateId = toLong(row.get("libtemplateid"));
            if (templateId == null) continue;
            long cnt = toLong(row.get("cnt"), 0L);
            if (cnt <= 0L) continue;
            Object scoreValue = row.get("score");
            Object questiontextValue = row.get("questiontext");
            ScoreCount scoreCount = new ScoreCount(scoreValue == null ? null : String.valueOf(scoreValue), cnt);
            fillStat.add(templateId, questiontextValue == null ? null : String.valueOf(questiontextValue), scoreCount);
        }
        return fillStat;
    }
    /**
     * 取填报明细行数(原 fillCount.size())
     */
    private long countFillRows(List<ScoreCount> fillCount) {
        if (CollectionUtils.isEmpty(fillCount)) return 0L;
        long rows = 0L;
        for (ScoreCount scoreCount : fillCount) {
            rows += scoreCount.getCount();
        }
        return rows;
    }
    /**
     * 获取平均分(聚合版:分值与条数)
     * <p>
     * 口径与 {@link #averageScore(List)} 完全一致:分子为可解析分值之和,分母为明细行数(包含分值为空的行)。
     * 注意不能改成 sql 的 avg() 或「有分值的行数」做分母,否则返回的 averageScore 会变。
     */
    private Double averageScoreByStat(List<ScoreCount> fillCount) {
        if (CollectionUtils.isEmpty(fillCount)) return 0.0;
        double sum = 0.0;
        long rows = 0L;
        for (ScoreCount scoreCount : fillCount) {
            if (scoreCount.getCount() <= 0L) continue;
            rows += scoreCount.getCount();
            if (scoreCount.getScore() != null && !scoreCount.getScore().isEmpty()) {
                sum += Double.parseDouble(scoreCount.getScore()) * scoreCount.getCount();
            }
        }
        if (rows == 0L) return 0.0;
        return new BigDecimal(sum / rows).setScale(1, RoundingMode.HALF_UP).doubleValue();
    }
    /**
     * 分值分组键:模板id + 分隔符 + 规范化后的问题内容
     * <p>
     * MySQL 里 questiontext = #{questiontext} 是「忽略大小写、忽略尾部空格」的比较,
     * 这里用 trim + 小写做等价匹配。
     */
    private static String statKey(Long templateId, String questiontext) {
        return templateId + "\u0001" + (questiontext == null ? "" : questiontext.trim().toLowerCase(Locale.ROOT));
    }
    private static Long toLong(Object value) {
        if (value == null) return null;
        if (value instanceof Number) return ((Number) value).longValue();
        String text = String.valueOf(value).trim();
        return text.isEmpty() ? null : Long.valueOf(text);
    }
    private static long toLong(Object value, long defaultValue) {
        Long result = toLong(value);
        return result == null ? defaultValue : result;
    }
    /**
     * 满意度分值聚合结果:分值 + 该分值对应的明细行数
     */
    private static class ScoreCount {
        private final String score;
        private final long count;
        ScoreCount(String score, long count) {
            this.score = score;
            this.count = count;
        }
        String getScore() {
            return score;
        }
        long getCount() {
            return count;
        }
    }
    /**
     * 满意度填报量聚合结果
     * <p>
     * byTemplateQuestion:按 模板+问题 分组;
     * byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在
     * script_content 为 null 时不拼 questiontext 条件的行为。
     */
    private static class SatisfactionFillStat {
        private final Map<String, List<ScoreCount>> byTemplateQuestion = new HashMap<>();
        private final Map<Long, List<ScoreCount>> byTemplate = new HashMap<>();
        void add(Long templateId, String questiontext, ScoreCount scoreCount) {
            byTemplate.computeIfAbsent(templateId, key -> new ArrayList<>()).add(scoreCount);
            if (questiontext != null) {
                byTemplateQuestion.computeIfAbsent(statKey(templateId, questiontext), key -> new ArrayList<>()).add(scoreCount);
            }
        }
        List<ScoreCount> get(Long templateId, String scriptContent) {
            if (templateId == null) return null;
            if (scriptContent == null) return byTemplate.get(templateId);
            return byTemplateQuestion.get(statKey(templateId, scriptContent));
        }
    }
    // ==================== statistics / statisticsByDimension 批量统计(替代逐个问题查库) ====================
    /**
     * 满意度明细统计(statistics、statisticsByDimension 共用)
     * <p>
     * 把原来「每个问题 3 条 SQL」改成「整个请求 3 条查询 + 内存组装」:
     * ① 发送量按 libtemplateid 分组;② 填报量按 模板+问题+matchedtext+score 分组;③ 选项分值按 scriptid 分组。
     *
     * @param reqVO      原始请求
     * @param type       service_subtask.type(1 语音、2 问卷)
     * @param voice      true=语音问题(选项分值取 ivr_liba_template_targetoption)
     * @param scriptRefs 本次要统计的问题(顺序即返回顺序)
     * @param out        明细结果
     * @param totals     [0]=totalPerson, [1]=allPerson
     */
    private void buildSatisfactionDetailEntities(PatSatisfactionReqVO reqVO, String type, boolean voice,
                                                 List<SatScriptRef> scriptRefs,
                                                 List<PatSatisfactionDetailEntity> out, int[] totals) {
        if (CollectionUtils.isEmpty(scriptRefs)) return;
        List<Long> templateIds = scriptRefs.stream().map(SatScriptRef::getTemplateId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(reqVO, type, templateIds);
        Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
        SatisfactionFillStatByMatchedtext fillStat = querySatisfactionFillStatByMatchedtext(statQuery);
        Map<Long, List<String>> optionScores = loadTargetOptionScores(voice, scriptRefs);
        for (SatScriptRef scriptRef : scriptRefs) {
            Long templateId = scriptRef.getTemplateId();
            //发送总量(原来是把全部 sub_id 查回 Java 再取 size)
            long sendCount = templateId == null ? 0L : sendCountMap.getOrDefault(templateId, 0L);
            //填报数量(原来是把明细行全部查回 Java 再统计)
            List<MatchedtextCount> fillRows = fillStat.get(templateId, scriptRef.getScriptContent());
            long fillCount = countFillRowsByMatchedtext(fillRows);
            PatSatisfactionDetailEntity detail = new PatSatisfactionDetailEntity();
            detail.setScriptContent(scriptRef.getScriptContent());
            detail.setAnswerPerson((int) fillCount);
            detail.setNoAnswerPerson(fillCount > 0L && sendCount > 0L ? (int) (sendCount - fillCount) : sendCount > 0L ? (int) sendCount : 0);
            detail.setAverageScore(String.valueOf(averageScoreByMatchedtext(fillRows)));
            List<String> scores = optionScores.get(scriptRef.getOptionScriptId());
            detail.setMaxScore(String.valueOf(maxScoreOf(scores)));
            detail.setMinScore(String.valueOf(minScoreOf(scores)));
            detail.setAnswerRate(sendCount == 0L ? 0 : fillCount == 0L ? 0 : fillCount * 1.0 / sendCount);
            detail.setMatchedtextStats(buildMatchedtextStats(fillRows, fillCount));
            out.add(detail);
            totals[0] += (int) fillCount;
            totals[1] += (int) sendCount;
        }
    }
    /**
     * 一次查出全部问题的填报量(按 模板+问题+matchedtext+score 分组,含 targetvalue)
     */
    private SatisfactionFillStatByMatchedtext querySatisfactionFillStatByMatchedtext(SatisfactionStatQuery statQuery) {
        SatisfactionFillStatByMatchedtext fillStat = new SatisfactionFillStatByMatchedtext();
        if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat;
        List<Map<String, Object>> rows = serviceSubtaskDetailMapper.selectSatisfactionFillStatByMatchedtext(statQuery);
        if (CollectionUtils.isEmpty(rows)) return fillStat;
        for (Map<String, Object> row : rows) {
            Long templateId = toLong(row.get("libtemplateid"));
            if (templateId == null) continue;
            long cnt = toLong(row.get("cnt"), 0L);
            if (cnt <= 0L) continue;
            Object questiontext = row.get("questiontext");
            Object matchedtext = row.get("matchedtext");
            Object score = row.get("score");
            Object targetvalue = row.get("targetvalue");
            fillStat.add(templateId, questiontext == null ? null : String.valueOf(questiontext),
                    new MatchedtextCount(matchedtext == null ? null : String.valueOf(matchedtext),
                            score == null ? null : String.valueOf(score),
                            targetvalue == null ? null : String.valueOf(targetvalue), cnt));
        }
        return fillStat;
    }
    /**
     * 一次查出全部问题的选项分值(只取 scriptid、score 两列)
     * <p>
     * 原实现是每个问题查一次整行,而调用方只用到 score 算最高/最低分;
     * 问题 id 为 null 时原实现不拼 scriptid 条件(等于取该表全部分值),这里用 null 作为 key 存这一份。
     */
    private Map<Long, List<String>> loadTargetOptionScores(boolean voice, List<SatScriptRef> scriptRefs) {
        Map<Long, List<String>> optionScores = new HashMap<>();
        List<Long> scriptIds = scriptRefs.stream().map(SatScriptRef::getOptionScriptId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (CollectionUtils.isNotEmpty(scriptIds)) {
            addTargetOptionScores(optionScores, voice
                    ? ivrLibaTemplateTargetoptionMapper.selectIvrLibaTargetoptionScores(scriptIds)
                    : svyLibTemplateTargetoptionMapper.selectSvyLibTargetoptionScores(scriptIds));
        }
        if (scriptRefs.stream().anyMatch(ref -> ref.getOptionScriptId() == null)) {
            optionScores.put(null, collectTargetOptionScores(voice
                    ? ivrLibaTemplateTargetoptionMapper.selectIvrLibaTargetoptionScores(null)
                    : svyLibTemplateTargetoptionMapper.selectSvyLibTargetoptionScores(null)));
        }
        return optionScores;
    }
    private void addTargetOptionScores(Map<Long, List<String>> optionScores, List<Map<String, Object>> rows) {
        if (CollectionUtils.isEmpty(rows)) return;
        for (Map<String, Object> row : rows) {
            Long scriptId = toLong(row.get("scriptid"));
            if (scriptId == null) continue;
            Object score = row.get("score");
            optionScores.computeIfAbsent(scriptId, key -> new ArrayList<>())
                    .add(score == null ? null : String.valueOf(score));
        }
    }
    private List<String> collectTargetOptionScores(List<Map<String, Object>> rows) {
        List<String> scores = new ArrayList<>();
        if (CollectionUtils.isEmpty(rows)) return scores;
        for (Map<String, Object> row : rows) {
            Object score = row.get("score");
            scores.add(score == null ? null : String.valueOf(score));
        }
        return scores;
    }
    /**
     * 各选项(matchedtext)数量及占比
     * <p>
     * 口径与原实现一致:分母是全部明细行数(含 matchedtext 为 null 的行);
     * targetvalue 用于补全未出现的选项(count=0、ratio=0)。
     */
    private Map<String, Map<String, Object>> buildMatchedtextStats(List<MatchedtextCount> fillRows, long totalFill) {
        Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
        if (CollectionUtils.isEmpty(fillRows) || totalFill <= 0L) return matchedtextStats;
        Map<String, Long> matchedtextCounts = new LinkedHashMap<>();
        String targetvalue = null;
        for (MatchedtextCount row : fillRows) {
            if (row.getMatchedtext() != null) {
                matchedtextCounts.merge(row.getMatchedtext(), row.getCount(), Long::sum);
            }
            if (targetvalue == null && row.getTargetvalue() != null) {
                targetvalue = row.getTargetvalue();
            }
        }
        for (Map.Entry<String, Long> entry : matchedtextCounts.entrySet()) {
            Map<String, Object> stat = new HashMap<>();
            stat.put("count", entry.getValue());
            stat.put("ratio", new BigDecimal((double) entry.getValue() / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue());
            matchedtextStats.put(entry.getKey(), stat);
        }
        // 从明细里取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0
        if (targetvalue != null) {
            for (String option : targetvalue.split("&")) {
                String optionTrim = option.trim();
                if (!matchedtextStats.containsKey(optionTrim)) {
                    Map<String, Object> emptyStat = new HashMap<>();
                    emptyStat.put("count", 0L);
                    emptyStat.put("ratio", 0.0);
                    matchedtextStats.put(optionTrim, emptyStat);
                }
            }
        }
        return matchedtextStats;
    }
    /**
     * 取填报明细行数(原 fillCount.size())
     */
    private long countFillRowsByMatchedtext(List<MatchedtextCount> fillRows) {
        if (CollectionUtils.isEmpty(fillRows)) return 0L;
        long rows = 0L;
        for (MatchedtextCount row : fillRows) {
            rows += row.getCount();
        }
        return rows;
    }
    /**
     * 获取平均分(聚合版:分值与条数)
     * <p>
     * 口径与 {@link #averageScore(List)} 一致:分子为可解析分值之和,分母为明细行数(含分值为空的行)。
     */
    private Double averageScoreByMatchedtext(List<MatchedtextCount> fillRows) {
        if (CollectionUtils.isEmpty(fillRows)) return 0.0;
        double sum = 0.0;
        long rows = 0L;
        for (MatchedtextCount row : fillRows) {
            if (row.getCount() <= 0L) continue;
            rows += row.getCount();
            if (row.getScore() != null && !row.getScore().isEmpty()) {
                sum += Double.parseDouble(row.getScore()) * row.getCount();
            }
        }
        if (rows == 0L) return 0.0;
        return new BigDecimal(sum / rows).setScale(1, RoundingMode.HALF_UP).doubleValue();
    }
    /**
     * 获取最高分(聚合版,口径同 {@link #maxScore(List)}:空列表返回 0.0)
     */
    private Double maxScoreOf(List<String> scores) {
        if (CollectionUtils.isEmpty(scores)) return 0.0;
        return scores.stream().filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                .mapToDouble(Double::parseDouble).max().orElse(0.0);
    }
    /**
     * 获取最低分(聚合版,口径同 {@link #minScore(List)}:空列表返回 0.0)
     */
    private Double minScoreOf(List<String> scores) {
        if (CollectionUtils.isEmpty(scores)) return 0.0;
        return scores.stream().filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                .mapToDouble(Double::parseDouble).min().orElse(0.0);
    }
    /**
     * 满意度统计用的问题引用:模板id、选项表关联的问题id(可能为 null)、问题内容
     */
    private static class SatScriptRef {
        private final Long templateId;
        private final Long optionScriptId;
        private final String scriptContent;
        SatScriptRef(Long templateId, Long optionScriptId, String scriptContent) {
            this.templateId = templateId;
            this.optionScriptId = optionScriptId;
            this.scriptContent = scriptContent;
        }
        Long getTemplateId() {
            return templateId;
        }
        Long getOptionScriptId() {
            return optionScriptId;
        }
        String getScriptContent() {
            return scriptContent;
        }
    }
    /**
     * 满意度填报聚合结果:matchedtext + score + targetvalue + 条数
     */
    private static class MatchedtextCount {
        private final String matchedtext;
        private final String score;
        private final String targetvalue;
        private final long count;
        MatchedtextCount(String matchedtext, String score, String targetvalue, long count) {
            this.matchedtext = matchedtext;
            this.score = score;
            this.targetvalue = targetvalue;
            this.count = count;
        }
        String getMatchedtext() {
            return matchedtext;
        }
        String getScore() {
            return score;
        }
        String getTargetvalue() {
            return targetvalue;
        }
        long getCount() {
            return count;
        }
    }
    /**
     * 满意度填报聚合结果(按 matchedtext 版)
     * <p>
     * byTemplateQuestion:按 模板+问题 分组;
     * byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在
     * script_content 为 null 时不拼 questiontext 条件的行为。
     */
    private static class SatisfactionFillStatByMatchedtext {
        private final Map<String, List<MatchedtextCount>> byTemplateQuestion = new HashMap<>();
        private final Map<Long, List<MatchedtextCount>> byTemplate = new HashMap<>();
        void add(Long templateId, String questiontext, MatchedtextCount row) {
            byTemplate.computeIfAbsent(templateId, key -> new ArrayList<>()).add(row);
            if (questiontext != null) {
                byTemplateQuestion.computeIfAbsent(statKey(templateId, questiontext), key -> new ArrayList<>()).add(row);
            }
        }
        List<MatchedtextCount> get(Long templateId, String scriptContent) {
            if (templateId == null) return null;
            if (scriptContent == null) return byTemplate.get(templateId);
            return byTemplateQuestion.get(statKey(templateId, scriptContent));
        }
    }
}