liusheng
2026-09-28 93078044580c2781cb8cf3ac18d2ab90513267de
处理患者再入院状态代码提交
已修改11个文件
1319 ■■■■■ 文件已修改
smartor/src/main/java/com/smartor/mapper/IvrLibaTemplateTargetoptionMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskDetailMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/SvyLibTemplateTargetoptionMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/PatSatisfactionServiceImpl.java 950 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateTargetoptionMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskDetailMapper.xml 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/SvyLibTemplateTargetoptionMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/IvrLibaTemplateTargetoptionMapper.java
@@ -2,8 +2,10 @@
import com.smartor.domain.IvrLibaTemplateTargetoption;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
 * 模板指标选项库Mapper接口
@@ -30,6 +32,17 @@
    public List<IvrLibaTemplateTargetoption> selectIvrLibaTemplateTargetoptionList(IvrLibaTemplateTargetoption ivrLibaTemplateTargetoption);
    /**
     * 满意度统计:一次取回多个问题的选项分值(只取 scriptid、score 两列)
     * <p>
     * 原实现是每个问题查一次整行(40+ 列),而调用方只用到 score 算最高/最低分。
     * scriptIds 传空时表示不加 scriptid 过滤(等价于原查询在 scriptid 为 null 时不拼该条件)。
     *
     * @param scriptIds 问题id集合,可为空
     * @return 每行含 scriptid、score
     */
    public List<Map<String, Object>> selectIvrLibaTargetoptionScores(@Param("scriptIds") List<Long> scriptIds);
    /**
     * 新增模板指标选项库
     *
     * @param ivrLibaTemplateTargetoption 模板指标选项库
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskDetailMapper.java
@@ -4,6 +4,7 @@
import com.smartor.domain.ServiceSubtaskDetail;
import com.smartor.domain.ServiceSubtaskDetailTarget;
import com.smartor.domain.ServiceSubtaskDetailVO;
import com.smartor.domain.entity.SatisfactionStatQuery;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -28,6 +29,28 @@
    public List<ServiceSubtaskDetailDTO> queryFillCount(@Param("subids") List<Long> subids, @Param("questiontext") String questiontext);
    /**
     * 满意度柱状图:一次性统计本次请求全部模板的填报量
     * (按 libtemplateid + questiontext + score 分组,只回传分值与条数,不回传明细行)
     * <p>
     * 替代 queryFillCount 的 N+1 写法,见 PatSatisfactionServiceImpl#satisfactionGraph
     *
     * @param query 聚合查询条件
     * @return 每行含 libtemplateid、questiontext、score、cnt
     */
    public List<Map<String, Object>> selectSatisfactionFillStat(SatisfactionStatQuery query);
    /**
     * 满意度统计:一次性统计本次请求全部问题的填报量(按 模板+问题+matchedtext+score 分组)
     * <p>
     * 用于 statistics / statisticsByDimension 的「各选项数量及占比」,替代逐个问题执行的
     * queryFillCount(那一步会先查出几万个 sub_id 再拼进 IN(...),并把约十万行明细传回 Java)。
     *
     * @param query 聚合查询条件
     * @return 每行含 libtemplateid、questiontext、matchedtext、score、cnt、targetvalue
     */
    public List<Map<String, Object>> selectSatisfactionFillStatByMatchedtext(SatisfactionStatQuery query);
    public ServiceSubtaskDetailDTO isExistServiceSubtaskDetail(@Param("subid") Long subid, @Param("scriptid") Long scriptid);
    /**
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java
@@ -12,6 +12,7 @@
import com.smartor.domain.VO.ServiceSubtaskCotinueCountVO;
import com.smartor.domain.entity.ServiceSubtaskEntity;
import com.smartor.domain.entity.ServiceSubtaskSatisfactionEntity;
import com.smartor.domain.entity.SatisfactionStatQuery;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -240,6 +241,28 @@
    List<Long> querySendCount(ServiceSubtaskSatisfactionEntity serviceSubtaskSatisfactionEntity);
    /**
     * 满意度柱状图:一次性统计本次请求全部模板的发送量(按 libtemplateid 分组)
     * <p>
     * 替代逐个模板调用 querySendCount 的 N+1 写法,见 PatSatisfactionServiceImpl#satisfactionGraph
     *
     * @param query 聚合查询条件
     * @return 每行含 libtemplateid、cnt
     */
    List<Map<String, Object>> selectSatisfactionSendCount(SatisfactionStatQuery query);
    /**
     * 满意度统计:只查本次条件下实际存在哪些 type(1 语音 / 2 问卷)
     * <p>
     * 替代 selectSatisfactionSubtaskList —— 原方法会把接近全表的上百个字段查回来,
     * 调用方却只用它判断「语音分支/问卷分支是否为空」,见
     * PatSatisfactionServiceImpl#statistics / #statisticsByDimension
     *
     * @param patSatisfactionReqVO 查询条件(与原 selectSatisfactionSubtaskList 完全一致)
     * @return 去重后的 type 列表(可能含 null)
     */
    List<String> selectSatisfactionSubtaskTypes(PatSatisfactionReqVO patSatisfactionReqVO);
    /**
     * 按病区分组统计延续护理数量
     *
     * @param vo 查询条件
smartor/src/main/java/com/smartor/mapper/SvyLibTemplateTargetoptionMapper.java
@@ -5,6 +5,7 @@
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
 * 问卷问题指标选项库Mapper接口
@@ -31,6 +32,17 @@
    public List<SvyLibTemplateTargetoption> selectSvyLibTemplateTargetoptionList(SvyLibTemplateTargetoption svyLibTemplateTargetoption);
    /**
     * 满意度统计:一次取回多个问题的选项分值(只取 scriptid、score 两列)
     * <p>
     * 原实现是每个问题查一次整行,而调用方只用到 score 算最高/最低分。
     * scriptIds 传空时表示不加 scriptid 过滤(等价于原查询在 scriptid 为 null 时不拼该条件)。
     *
     * @param scriptIds 问题id集合(对应 svy_lib_template_script.id),可为空
     * @return 每行含 scriptid、score
     */
    public List<Map<String, Object>> selectSvyLibTargetoptionScores(@Param("scriptIds") List<Long> scriptIds);
    /**
     * 新增问卷问题指标选项库
     *
     * @param svyLibTemplateTargetoption 问卷问题指标选项库
smartor/src/main/java/com/smartor/service/impl/PatSatisfactionServiceImpl.java
@@ -17,6 +17,7 @@
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;
@@ -194,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(String.valueOf(averageScore(fillCount)));
                patSatisfactionDetailEntity.setMaxScore(String.valueOf(maxScore(ivrLibaTemplateTargetoptions)));
                patSatisfactionDetailEntity.setMinScore(String.valueOf(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());
@@ -292,85 +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(String.valueOf(averageScore(fillCount)));
                Double maxScore = svyLibTemplateTargetoptions.stream()
                        .map(dto -> dto.getScore())
                        .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                        .mapToDouble(Double::parseDouble)
                        .max()
                        .orElse(0.0);
                patSatisfactionDetailEntity.setMaxScore(String.valueOf(maxScore));
                Double minScore = svyLibTemplateTargetoptions.stream()
                        .map(dto -> dto.getScore())
                        .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                        .mapToDouble(Double::parseDouble)
                        .min()
                        .orElse(0.0);
                patSatisfactionDetailEntity.setMinScore(String.valueOf(minScore));
                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;
    }
@@ -385,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());
@@ -405,158 +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(String.valueOf(averageScore(fillCount)));
                    patSatisfactionDetailEntity.setMaxScore(String.valueOf(maxScore(ivrLibaTemplateTargetoptions)));
                    patSatisfactionDetailEntity.setMinScore(String.valueOf(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(String.valueOf(averageScore(fillCount)));
                    Double maxScore = svyLibTemplateTargetoptions.stream()
                            .map(dto -> dto.getScore())
                            .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                            .mapToDouble(Double::parseDouble)
                            .max()
                            .orElse(0.0);
                    patSatisfactionDetailEntity.setMaxScore(String.valueOf(maxScore));
                    Double minScore = svyLibTemplateTargetoptions.stream()
                            .map(dto -> dto.getScore())
                            .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
                            .mapToDouble(Double::parseDouble)
                            .min()
                            .orElse(0.0);
                    patSatisfactionDetailEntity.setMinScore(String.valueOf(minScore));
                    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;
    }
@@ -566,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
@@ -579,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));
@@ -589,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);
                    }
                    // 这里先记录一下每个分类的总人数和填报人数
@@ -617,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);
                }
@@ -634,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()) {
@@ -643,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);
                    }
@@ -677,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);
                    }
                }
@@ -698,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. 从入参中获取满意度维维度
@@ -713,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);
                        }
                        // 这里先记录一下每个分类的总人数和填报人数
@@ -750,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);
                    }
@@ -766,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);
                        }
@@ -809,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);
                        }
                    }
@@ -873,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));
        }
    }
}
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java
@@ -697,6 +697,7 @@
                    serviceSubtask.setNurseName(patMedInhospList.get(0).getNurseName());
                    serviceSubtask.setNurseId(patMedInhospList.get(0).getNurseId());
                    serviceSubtask.setInhospid(patMedInhospList.get(0).getInhospid());
                    serviceSubtask.setCampusid(patMedInhospList.get(0).getCampusid());
                    serviceSubtask.setServiceType("6");
                } else if (CollectionUtils.isNotEmpty(patMedOuthosps)) {
                    serviceSubtask.setHospType("1");
@@ -706,6 +707,7 @@
                    serviceSubtask.setDeptcode(patMedOuthosps.get(0).getDeptcode());
                    serviceSubtask.setDrname(patMedOuthosps.get(0).getDrname());
                    serviceSubtask.setDrcode(patMedOuthosps.get(0).getDrcode());
                    serviceSubtask.setCampusid(patMedOuthosps.get(0).getCampusid());
                    serviceSubtask.setServiceType("14");
                }
                if (!Objects.isNull(patArchive)) {
@@ -716,6 +718,11 @@
                    serviceSubtask.setSfzh(patArchive.getIdcardno());
                    serviceSubtask.setAddr(patArchive.getPlaceOfResidence());
                    serviceSubtask.setOrgid(patArchive.getOrgid());
                    // 院区兜底:住院/门急诊记录的院区在本库常为空,用患者档案的院区补上,
                    // 否则统计接口(@AddOrgId 按登录用户院区过滤 service_subtask.campusid)会把这些子任务整批丢掉
                    if (StringUtils.isEmpty(serviceSubtask.getCampusid())) {
                        serviceSubtask.setCampusid(patArchive.getCampusid());
                    }
                }
                if (StringUtils.isNotEmpty(dealDept)) {
                    //投诉建议的处理部门
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -14,7 +14,6 @@
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
@@ -23,11 +22,7 @@
import com.smartor.common.MtSubmitSmUtil;
import com.smartor.common.ScheduleSubtaskBuilder;
import com.smartor.config.PhoneUtils;
import com.smartor.domain.DTO.MydTrendReqDTO;
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.DTO.*;
import com.smartor.domain.*;
import com.smartor.domain.VO.*;
import com.smartor.domain.entity.ServiceSubtaskEntity;
@@ -5985,10 +5980,42 @@
        Date newDate = new Date();
        serviceSubtask.setLongSendTime(newDate);
        serviceSubtask.setVisitTime(newDate);
        // 院区:/scanGenerateSubtask 是匿名接口(SecurityConfig permitAll),取不到登录用户;
        // 而 service_task.campusid 在本库是占位值 '1',直接沿用会让统计接口(@AddOrgId 按院区过滤)查不到数据。
        // 这里用任务配置的病区(其次科室)反查 sys_dept.campusid 落库,保证问卷统计能按院区取到。
        String campusIdByDept = queryCampusIdByDeptCode(serviceSubtask.getLeavehospitaldistrictcode(), serviceTask.getOrgid());
        if (StringUtils.isEmpty(campusIdByDept)) {
            campusIdByDept = queryCampusIdByDeptCode(serviceSubtask.getDeptcode(), serviceTask.getOrgid());
        }
        if (StringUtils.isNotEmpty(campusIdByDept)) {
            serviceSubtask.setCampusid(campusIdByDept);
        }
        // 只配了科室(没有病区)的满意度任务:这道答卷在按病区筛选的报表里统计不到,留日志便于运维发现
        if (StringUtils.isEmpty(serviceSubtask.getLeavehospitaldistrictcode())
                && ("6".equals(serviceSubtask.getServiceType()) || "14".equals(serviceSubtask.getServiceType()))) {
            log.warn("【扫码生成任务】满意度子任务没有病区,按病区筛选的报表将统计不到该答卷:taskid={}, deptcode={}, campusid={}",
                    serviceSubtask.getTaskid(), serviceSubtask.getDeptcode(), serviceSubtask.getCampusid());
        }
        return serviceSubtask;
    }
    /**
     * 按科室/病区编码反查所属院区(查不到返回 null)
     */
    private String queryCampusIdByDeptCode(String deptCode, String orgid) {
        if (StringUtils.isEmpty(deptCode)) {
            return null;
        }
        try {
            SysDept sysDept = sysDept2Mapper.selectDeptByCode(deptCode, orgid);
            return sysDept == null ? null : sysDept.getCampusid();
        } catch (Exception e) {
            log.warn("按编码反查院区失败:deptCode={}, orgid={}, err={}", deptCode, orgid, e.getMessage());
            return null;
        }
    }
    /**
     * Ai 匹配选项
     *
     * @param questionText
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateTargetoptionMapper.xml
@@ -116,6 +116,23 @@
        where id = #{id}
    </select>
    <!--
        满意度统计:一次取回多个问题的选项分值(只取 scriptid、score)。
        等价于逐个问题执行 selectIvrLibaTemplateTargetoptionList(scriptid),但只取用得到的列;
        scriptIds 为空时不加 scriptid 过滤(对应原查询 scriptid 为 null 时丢弃该条件的行为)。
    -->
    <select id="selectIvrLibaTargetoptionScores" resultType="java.util.Map">
        select scriptid as scriptid, score as score
        from ivr_liba_template_targetoption
        where del_flag = 0
        <if test="scriptIds != null and scriptIds.size() > 0">
            and scriptid IN
            <foreach collection="scriptIds" item="scriptId" open="(" separator="," close=")">
                #{scriptId}
            </foreach>
        </if>
    </select>
    <insert id="insertIvrLibaTemplateTargetoption" parameterType="com.smartor.domain.IvrLibaTemplateTargetoption"
            useGeneratedKeys="true" keyProperty="id">
        insert into ivr_liba_template_targetoption
smartor/src/main/resources/mapper/smartor/ServiceSubtaskDetailMapper.xml
@@ -265,6 +265,133 @@
        <if test="questiontext != null">and questiontext = #{questiontext}</if>
    </select>
    <!--
        满意度柱状图:一次性统计本次请求全部模板的填报量。
        等价于对每个模板执行 queryFillCount(subids, questiontext),区别是:
          1) 用 join service_subtask 代替先把全部 sub_id 查回 Java 再拼 IN(...);
          2) 按 libtemplateid + questiontext + score 分组,只回传分值与条数;
          3) visit_time 改成可走索引的范围条件。
        注意:这里刻意不在 SQL 里过滤 questiontext,由 Java 按原 queryFillCount 的
        「questiontext 为 null 时不拼该条件」语义做匹配,保证结果口径完全一致。
    -->
    <select id="selectSatisfactionFillStat" parameterType="com.smartor.domain.entity.SatisfactionStatQuery"
            resultType="java.util.Map">
        select ss.libtemplateid as libtemplateid,
               ssd.questiontext as questiontext,
               ssd.score as score,
               count(*) as cnt
        from service_subtask_detail ssd
        join service_subtask ss on ss.id = ssd.sub_id
        where ssd.del_flag = 0
        and ss.del_flag = 0
        <if test="orgid != null and orgid != ''">
            and ss.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and ss.campusid = #{campusid}
        </if>
        <if test="type != null and type != ''">and ss.type = #{type}</if>
        <if test="startTime != null">
            and ss.visit_time &gt;= #{startTime}
        </if>
        <if test="endTime != null">
            and ss.visit_time &lt; date_add(#{endTime}, interval 1 day)
        </if>
        <if test="libTemplateIds != null and libTemplateIds.size() > 0">
            and ss.libtemplateid IN
            <foreach collection="libTemplateIds" item="libTemplateId" open="(" separator="," close=")">
                #{libTemplateId}
            </foreach>
        </if>
        <if test="(hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0) or (deptcodes != null and deptcodes.size() > 0)">
            AND (
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
                ss.leavehospitaldistrictcode IN
                <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{hospitaldistrictcode}
                </foreach>
            </if>
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0 and deptcodes != null and deptcodes.size() > 0">
                OR
            </if>
            <if test="deptcodes != null and deptcodes.size() > 0">
                ss.deptcode IN
                <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                    #{deptcode}
                </foreach>
            </if>
            )
        </if>
        group by ss.libtemplateid, ssd.questiontext, ssd.score
    </select>
    <!--
        满意度统计(statistics / statisticsByDimension):一次性统计本次请求全部问题的填报量。
        等价于对每个问题执行 queryFillCount(subids, questiontext),区别是:
          1) 用 join service_subtask 代替先把全部 sub_id 查回 Java 再拼 IN(...);
          2) 按 模板+问题+matchedtext+score 分组,只回传选项/分值与条数;
          3) targetvalue 用 min() 取该问题的一个非空选项串(同一个问题该值一致);
          4) visit_time 改成可走索引的范围条件。
        注意:这里刻意不在 SQL 里过滤 questiontext,由 Java 按原 queryFillCount 的
        「questiontext 为 null 时不拼该条件」语义做匹配,保证结果口径完全一致。
    -->
    <select id="selectSatisfactionFillStatByMatchedtext" parameterType="com.smartor.domain.entity.SatisfactionStatQuery"
            resultType="java.util.Map">
        select ss.libtemplateid as libtemplateid,
               ssd.questiontext as questiontext,
               ssd.matchedtext as matchedtext,
               ssd.score as score,
               count(*) as cnt,
               min(ssd.targetvalue) as targetvalue
        from service_subtask_detail ssd
        join service_subtask ss on ss.id = ssd.sub_id
        where ssd.del_flag = 0
        and ss.del_flag = 0
        <if test="orgid != null and orgid != ''">
            and ss.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and ss.campusid = #{campusid}
        </if>
        <if test="type != null and type != ''">and ss.type = #{type}</if>
        <if test="startTime != null">
            and ss.visit_time &gt;= #{startTime}
        </if>
        <if test="endTime != null">
            and ss.visit_time &lt; date_add(#{endTime}, interval 1 day)
        </if>
        <if test="libTemplateIds != null and libTemplateIds.size() > 0">
            and ss.libtemplateid IN
            <foreach collection="libTemplateIds" item="libTemplateId" open="(" separator="," close=")">
                #{libTemplateId}
            </foreach>
        </if>
        <if test="(hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0) or (deptcodes != null and deptcodes.size() > 0)">
            AND (
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
                ss.leavehospitaldistrictcode IN
                <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{hospitaldistrictcode}
                </foreach>
            </if>
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0 and deptcodes != null and deptcodes.size() > 0">
                OR
            </if>
            <if test="deptcodes != null and deptcodes.size() > 0">
                ss.deptcode IN
                <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                    #{deptcode}
                </foreach>
            </if>
            )
        </if>
        group by ss.libtemplateid, ssd.questiontext, ssd.matchedtext, ssd.score
    </select>
    <select id="isExistServiceSubtaskDetail" resultType="com.smartor.domain.DTO.ServiceSubtaskDetailDTO">
        select id, questiontext, matchedtext, score
        from service_subtask_detail
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -571,6 +571,43 @@
            AND date_format(visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
    </select>
    <!--
        满意度统计:只查本次条件下实际存在哪些 type。
        过滤条件与 selectSatisfactionSubtaskList 完全一致,只是:
          1) 只 select distinct type(原来是把上百个字段全查回来);
          2) visit_time 改成可走索引的范围条件(原来对列套 date_format,索引用不上)。
    -->
    <select id="selectSatisfactionSubtaskTypes" parameterType="com.smartor.domain.VO.PatSatisfactionReqVO"
            resultType="java.lang.String">
        select distinct type
        from service_subtask
        where del_flag = 0
        <if test="questionType != null">and type = #{questionType}</if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="serviceTypes != null and serviceTypes.size() > 0">
            AND service_type IN
            <foreach collection="serviceTypes" item="serviceType" open="(" separator="," close=")">
                #{serviceType}
            </foreach>
        </if>
        <if test="startTime != null and endTime != null">
            AND visit_time &gt;= #{startTime}
            AND visit_time &lt; date_add(#{endTime}, interval 1 day)
        </if>
    </select>
    <select id="getCompensateServiceSubtaskList" parameterType="com.smartor.domain.entity.ServiceSubtaskEntity"
            resultMap="ServiceSubtaskResult">
        <include refid="selectServiceSubtaskVo"/>
@@ -2896,6 +2933,60 @@
    </select>
    <!--
        满意度柱状图:一次性统计本次请求全部模板的发送量。
        与 querySendCount 的过滤条件完全一致,区别是:
          1) 按 libtemplateid 分组,一次返回所有模板(替代逐个模板查询);
          2) visit_time 改成可走索引的范围条件(原来对列套 date_format,索引用不上)。
    -->
    <select id="selectSatisfactionSendCount" parameterType="com.smartor.domain.entity.SatisfactionStatQuery"
            resultType="java.util.Map">
        select libtemplateid as libtemplateid, count(distinct id) as cnt
        from service_subtask
        where del_flag = 0
        <if test="orgid != null and orgid != ''">
            and orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <if test="type != null and type != ''">and type = #{type}</if>
        <if test="startTime != null">
            and visit_time &gt;= #{startTime}
        </if>
        <if test="endTime != null">
            and visit_time &lt; date_add(#{endTime}, interval 1 day)
        </if>
        <if test="libTemplateIds != null and libTemplateIds.size() > 0">
            and libtemplateid IN
            <foreach collection="libTemplateIds" item="libTemplateId" open="(" separator="," close=")">
                #{libTemplateId}
            </foreach>
        </if>
        <if test="(hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0) or (deptcodes != null and deptcodes.size() > 0)">
            AND (
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
                leavehospitaldistrictcode IN
                <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{hospitaldistrictcode}
                </foreach>
            </if>
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0 and deptcodes != null and deptcodes.size() > 0">
                OR
            </if>
            <if test="deptcodes != null and deptcodes.size() > 0">
                deptcode IN
                <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                    #{deptcode}
                </foreach>
            </if>
            )
        </if>
        group by libtemplateid
    </select>
    <!--
        按病区或科室分组统计 continue_flag 数量
        传了 leavehospitaldistrictcodes → 按病区维度
        传了 deptcodes              → 按科室维度
smartor/src/main/resources/mapper/smartor/SvyLibTemplateTargetoptionMapper.xml
@@ -126,6 +126,23 @@
        where id = #{id}
    </select>
    <!--
        满意度统计:一次取回多个问题的选项分值(只取 scriptid、score)。
        等价于逐个问题执行 selectSvyLibTemplateTargetoptionList(scriptid),但只取用得到的列;
        scriptIds 为空时不加 scriptid 过滤(对应原查询 scriptid 为 null 时丢弃该条件的行为)。
    -->
    <select id="selectSvyLibTargetoptionScores" resultType="java.util.Map">
        select scriptid as scriptid, score as score
        from svy_lib_template_targetoption
        where del_flag = 0
        <if test="scriptIds != null and scriptIds.size() > 0">
            and scriptid IN
            <foreach collection="scriptIds" item="scriptId" open="(" separator="," close=")">
                #{scriptId}
            </foreach>
        </if>
    </select>
    <insert id="insertSvyLibTemplateTargetoption" parameterType="com.smartor.domain.SvyLibTemplateTargetoption"
            useGeneratedKeys="true" keyProperty="id">
        insert into svy_lib_template_targetoption