ccf5fc76de48e6add5b325b93f2b0ea54efbf2b8..93078044580c2781cb8cf3ac18d2ab90513267de
2026-09-28 liusheng
处理患者再入院状态代码提交
930780 对比 | 目录
2026-09-28 liusheng
处理患者再入院状态代码提交
138f31 对比 | 目录
2026-09-28 liusheng
1.代码提交
278a56 对比 | 目录
已修改16个文件
1740 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/IvrLibaTemplateTargetoptionMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/PatMedInhospMapper.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/ServiceSLTDHealthcareRecordServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateTargetoptionMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatMedInhospMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskDetailMapper.xml 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/SvyLibTemplateTargetoptionMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java
@@ -207,9 +207,9 @@
    /**
     * 省立同德满意度数据统计(按题目统计选项数量/占比,并给出该题的综合得分)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%,
     * 分值取 1、0.8、0.6、0.4、0.2 五档(很满意、满意、一般、不满意、很不满意);
     * 「不适用」、0 分、空值等既不进分子也不进分母。
     * 综合满意度 = (很满意项数×1 + 满意项数×0.8 + 一般项数×0.6 + 不满意项数×0.4 + 很不满意项数×0.2)
     * / (很满意项数 + 满意项数 + 一般项数 + 不满意项数 + 很不满意项数) × 100%;
     * 「未经历」「不适用」等不在五档里的选项既不进分子也不进分母。
     */
    @ApiOperation("省立同德满意度数据统计")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
@@ -256,7 +256,7 @@
                    QuestionResultDTO questionResultDTO = new QuestionResultDTO();
                    questionResultDTO.setQeustionText(questionText);
                    questionResultDTO.setSubtaskDetailRatioExportList(exportList);
                    //综合得分:Σ(分值×项数) / Σ项数 / 本题最高分值 × 100%,不适用/0分/空值不进分子也不进分母
                    //综合得分:(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / 五档项数之和 × 100%
                    questionResultDTO.setAvgScore(calcMydScore(exportList));
                    result.add(questionResultDTO);
                });
@@ -297,11 +297,25 @@
    private static final String VALUE_TYPE_MULTIPLE = "2";
    /**
     * 满意度五档选项(顺序与权重顺序一致)
     */
    private static final String[] MYD_SCORE_OPTIONS = {"很满意", "满意", "一般", "不满意", "很不满意"};
    /**
     * 满意度五档权重:很满意 1、满意 0.8、一般 0.6、不满意 0.4、很不满意 0.2
     */
    private static final BigDecimal[] MYD_SCORE_WEIGHTS = {
            new BigDecimal("1"), new BigDecimal("0.8"), new BigDecimal("0.6"), new BigDecimal("0.4"), new BigDecimal("0.2")};
    /**
     * 计算一道题的满意度综合得分(百分制,保留两位小数)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%
     * 等价于 Σ(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / Σ(五档项数) × 100%。
     * 用本题最高分值归一化,兼容明细分值存 0-1 权重(1、0.8…)和存 5 分制(5、4…)两种口径。
     * 综合满意度 = (很满意项数×1 + 满意项数×0.8 + 一般项数×0.6 + 不满意项数×0.4 + 很不满意项数×0.2)
     * / (很满意项数 + 满意项数 + 一般项数 + 不满意项数 + 很不满意项数) × 100%
     * <p>
     * 权重按选项文案判定(见 {@link #mydScoreOptionIndex(String)}),不再依赖明细的 score 字段:
     * 历史数据里 score 有 0、「0000000001」这类脏值,按 score 算会把得分固定成 0 或 100。
     * 「未经历」「不适用」等不在五档里的选项既不进分子也不进分母。
     *
     * @param exportList 同一道题目的选项统计明细
     * @return 百分制综合得分,无有效项数时返回 "0.00"
@@ -309,9 +323,8 @@
    private String calcMydScore(List<ServiceSubtaskDetailRatioExport> exportList) {
        if (CollectionUtils.isEmpty(exportList)) return "0.00";
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(选项分值 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:Σ项数
        BigDecimal maxScore = null;              //本题最高分值,用于归一化
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(档位权重 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:五档项数之和
        for (ServiceSubtaskDetailRatioExport detail : exportList) {
            //多选题一行是多个选项的分值之和,分母只算 1 项,会把得分算高,不计入综合得分
@@ -319,27 +332,42 @@
            //「不适用」不在公式的五档里,不计入综合得分
            if (isNotApplicable(detail)) continue;
            BigDecimal score = parseDecimal(detail.getScore());
            int optionIndex = mydScoreOptionIndex(detail.getOptionresult());
            BigDecimal count = parseDecimal(detail.getCount());
            if (score == null || count == null) continue;
            //0 分(未答/空答)、空数量:既不进分子也不进分母
            if (score.signum() <= 0 || count.signum() <= 0) continue;
            if (optionIndex < 0 || count == null || count.signum() <= 0) continue;
            if (maxScore == null || score.compareTo(maxScore) > 0) maxScore = score;
            scoreSum = scoreSum.add(score.multiply(count));
            scoreSum = scoreSum.add(MYD_SCORE_WEIGHTS[optionIndex].multiply(count));
            countSum = countSum.add(count);
        }
        if (maxScore == null || countSum.signum() <= 0) return "0.00";
        if (countSum.signum() <= 0) return "0.00";
        return scoreSum.divide(countSum, 6, RoundingMode.HALF_UP)
                .divide(maxScore, 6, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP)
                .toPlainString();
    }
    /**
     * 选项文案 → 满意度五档下标(很满意 0、满意 1、一般 2、不满意 3、很不满意 4),不在五档里返回 -1
     */
    private int mydScoreOptionIndex(String optionresult) {
        if (StringUtils.isEmpty(optionresult)) return -1;
        String text = optionresult.trim();
        for (int i = 0; i < MYD_SCORE_OPTIONS.length; i++) {
            if (MYD_SCORE_OPTIONS[i].equals(text)) return i;
        }
        //兼容「A 很满意」「B.满意」这类带选项序号前缀的历史数据
        if (text.length() > 2 && (text.charAt(1) == ' ' || text.charAt(1) == '.' || text.charAt(1) == '、')) {
            String stripped = text.substring(2).trim();
            for (int i = 0; i < MYD_SCORE_OPTIONS.length; i++) {
                if (MYD_SCORE_OPTIONS[i].equals(stripped)) return i;
            }
        }
        return -1;
    }
    /**
     * 是否「不适用」选项
     */
    private boolean isNotApplicable(ServiceSubtaskDetailRatioExport detail) {
ruoyi-admin/src/main/resources/application.yml
@@ -75,7 +75,7 @@
    # 国际化资源文件路径
    basename: i18n/messages
  profiles:
    active: nhfy
    active: druid
  # 文件上传
  servlet:
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/PatMedInhospMapper.java
@@ -1,5 +1,6 @@
package com.smartor.mapper;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -28,6 +29,18 @@
    public List<PatMedInhosp> selectPatMedInhosp(PatMedInhosp patMedInhosp);
    /**
     * 按患者编号 + 出院日期查询出院记录(出院时间只比较到天,不比较时分秒)
     *
     * @param patno    患者编号
     * @param dayStart 查询日期(当天 00:00:00)
     * @param dayEnd   查询日期的次日 00:00:00
     * @return 出院记录集合,按 inhospid 倒序
     */
    public List<PatMedInhosp> selectOutInhospByPatnoAndOutDate(@Param("patno") String patno,
                                                               @Param("dayStart") Date dayStart,
                                                               @Param("dayEnd") Date dayEnd);
    public String getTagnameBypatid(Long patid);
    /**
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/ServiceSLTDHealthcareRecordServiceImpl.java
@@ -90,6 +90,8 @@
    private ServiceSubtaskServiceImpl serviceSubtaskServiceImpl;
    @Autowired
    private ServiceSubtaskMapper serviceSubtaskMapper;
    @Autowired
    private PatMedInhospMapper patMedInhospMapper;
    @Override
    public List<ServiceSLTDInhospResDTO> queryHealthcareRecordList(ServiceSLTDInhospReqVO reqVO) {
@@ -716,7 +718,8 @@
                    if (CollectionUtils.isNotEmpty(patMedInhospList)) {
                        //如果不为空,则将入院时间进行更新
                        PatMedInhosp pmOut = patMedInhospList.get(0);
                        PatMedInhosp pmIn = existingInhosps.get(0);
                        //本次入院数据(这里 existingInhosps 必为空,取 existingInhosps.get(0) 会下标越界)
                        PatMedInhosp pmIn = patMedInhosp;
                        pmOut.setStarttime(pmIn.getStarttime());
                        pmOut.setUpdateTime(new Date());
                        patMedInhospService.updatePatMedInhosp(pmOut);
@@ -728,6 +731,26 @@
                                serviceSubtask.setSendstate(4L);
                                serviceSubtaskMapper.updateServiceSubtaskByCondition(serviceSubtask);
                            }
                            //通过入院时间,和患者的编号去查一下出院时间,是否存在,如果存在,则该患者是再入院的,需要改成不执行
                            //出院时间只比较到天,不比较时分秒
                            if (pmIn.getStarttime() != null) {
                                Date dayStart = DateUtils.truncate(pmIn.getStarttime(), Calendar.DAY_OF_MONTH);
                                Date dayEnd = DateUtils.addDays(dayStart, 1);
                                List<PatMedInhosp> outInhosps = patMedInhospMapper.selectOutInhospByPatnoAndOutDate(
                                        StringUtils.trim(pmIn.getPatno()), dayStart, dayEnd);
                                if (CollectionUtils.isNotEmpty(outInhosps)) {
                                    //存在出院记录,说明该患者是再入院,把上一次出院产生的子任务改成不执行
                                    ServiceSubtask outSubtask = new ServiceSubtask();
                                    outSubtask.setInhospid(outInhosps.get(0).getInhospid());
                                    outSubtask.setSendstate(4L);
                                    outSubtask.setRemark("患者再入院");
                                    outSubtask.setUpdateTime(new Date());
                                    serviceSubtaskMapper.updateServiceSubtaskByCondition(outSubtask);
                                    log.info("患者再入院,将上次出院记录的子任务改为不执行:patno={}, 出院inhospid={}, 出院时间={}, 本次入院时间={}",
                                            pmIn.getPatno(), outInhosps.get(0).getInhospid(), outInhosps.get(0).getEndtime(), pmIn.getStarttime());
                                }
                            }
                        } catch (Exception e) {
                            log.error("将出院时间等于入院时间的inhospid的任务更新失败了, inhospid={}", pmOut.getInhospid(), e);
                        }
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java
@@ -97,6 +97,66 @@
    private String dealDeptCode;
    /**
     * 安全解析分数:null、空串、空白串以及非法格式一律按 0 处理,避免 Double.parseDouble 抛 NumberFormatException
     *
     * @param scoreValue 分数字符串
     * @return 解析后的分数,无法解析时返回 0
     */
    private double parseScore(String scoreValue) {
        if (StringUtils.isEmpty(scoreValue)) {
            return 0D;
        }
        try {
            return Double.parseDouble(scoreValue.trim());
        } catch (NumberFormatException e) {
            log.warn("分数格式非法,按 0 处理,原始值:{}", scoreValue);
            return 0D;
        }
    }
    /**
     * 将累加后的分值格式化为落库字符串,去掉多余的小数位(如 5.0 -> 5)
     *
     * @param scoreValue 分值
     * @return 分数字符串
     */
    private String formatScore(double scoreValue) {
        if (Double.isNaN(scoreValue) || Double.isInfinite(scoreValue)) {
            log.warn("分数计算结果非法,按 0 处理,原始值:{}", scoreValue);
            return "0";
        }
        return BigDecimal.valueOf(scoreValue).stripTrailingZeros().toPlainString();
    }
    /**
     * 安全解析异常标记:null、空串、非法格式一律按 0(正常)处理
     *
     * @param isabnormalFlag 异常标记字符串
     * @return 异常标记值
     */
    private Integer parseIsabnormal(String isabnormalFlag) {
        if (StringUtils.isEmpty(isabnormalFlag)) {
            return 0;
        }
        try {
            return Integer.valueOf(isabnormalFlag.trim());
        } catch (NumberFormatException e) {
            log.warn("异常标记格式非法,按 0 处理,原始值:{}", isabnormalFlag);
            return 0;
        }
    }
    /**
     * 归一化分数字符串,避免把 null 或空串(前端未传/未作答)放到 map 中传递给累加逻辑
     *
     * @param scoreValue 原始分值
     * @return 非空分值,默认 "0"
     */
    private String normalizeScore(String scoreValue) {
        return StringUtils.isEmpty(scoreValue) ? "0" : scoreValue.trim();
    }
    /**
     * 查询任务问卷问题选项
     *
     * @param id 任务问卷问题选项主键
@@ -237,7 +297,7 @@
                subtaskCache.put(subtaskCacheKey, initialSubtaskList.get(0));
            }
            String score = "0";
            double scoreTotal = 0D;
            Long nextScriptno = null;
            aa:
            for (String key : targetKeys) {
@@ -271,11 +331,7 @@
                                    if (map == null) continue aa;
                                    if (!Objects.isNull(map.get("nextScriptNo")))
                                        nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                                    if (score == null || score.isEmpty()) {
                                        score = map.get("score");
                                    } else {
                                        score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                                    }
                                    scoreTotal += parseScore(map.get("score"));
                                }
                            }
@@ -290,11 +346,7 @@
                                log.error("nextScriptNo的信息为:{}", map.get("nextScriptNo"));
                                if (!Objects.isNull(map.get("nextScriptNo")))
                                    nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                                if (score == null) {
                                    score = map.get("score");
                                } else {
                                    score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                                }
                                scoreTotal += parseScore(map.get("score"));
                            }
                        }
                    }
@@ -313,11 +365,7 @@
                                    Map<String, String> map = setSFInfo(ivrTaskTemplateScriptVO, tid, pid, subid);
                                    if (!Objects.isNull(map.get("nextScriptNo")))
                                        nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                                    if (score == null || score.isEmpty()) {
                                        score = map.get("score");
                                    } else {
                                        score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                                    }
                                    scoreTotal += parseScore(map.get("score"));
                                }
                            }
@@ -331,11 +379,7 @@
                                log.error("----nextScriptNo2的信息为:{}", map.get("nextScriptNo"));
                                if (!Objects.isNull(map.get("nextScriptNo")))
                                    nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                                if (score == null) {
                                    score = map.get("score");
                                } else {
                                    score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                                }
                                scoreTotal += parseScore(map.get("score"));
                            }
                        }
                    }
@@ -375,7 +419,7 @@
                serviceSubtask.setFinishtime(new Date());
                serviceSubtask.setUpdateTime(new Date());
                log.error("将该key和value从redis中,serviceSubtask的值为--------:{}", serviceSubtask);
                if (score != null && !score.isEmpty()) serviceSubtask.setScore(score);
                serviceSubtask.setScore(formatScore(scoreTotal));
                serviceSubtaskMapper.updateServiceSubtaskByCondition(serviceSubtask);
                //将当前发送状态改成 9 完成(缓存 preachform 查询)
@@ -416,7 +460,7 @@
        Long tid = Long.valueOf(serviceSubTaskCacheReq.getParam1());
        Long pid = Long.valueOf(serviceSubTaskCacheReq.getParam2());
        Long subid = Long.valueOf(serviceSubTaskCacheReq.getParam6());
        String score = "0";
        double scoreTotal = 0D;
        if (ObjectUtils.isNotEmpty(serviceSubTaskCacheReq)) {
            Long nextScriptno = null;
            String isabnormalFlag = "0";
@@ -463,18 +507,16 @@
                    if (shouldProcess) {
                        Map<String, String> map = setWJInfo(svyTaskTemplateScriptVO, tid, pid, subid, preServiceSubtaskList, preServiceTask, preDetailIdMap, preTraceIdMap);
                        log.error("nextScriptNo 的信息为:{}", map.get("nextScriptNo"));
                        if (map == null) continue;
                        log.info("nextScriptNo 的信息为:{}", map.get("nextScriptNo"));
                        if (!Objects.isNull(map.get("nextScriptNo")))
                            nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                        if (score == null) {
                            score = map.get("score");
                        } else {
                            score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                        scoreTotal += parseScore(map.get("score"));
                        if (StringUtils.isNotEmpty(map.get("isabnormal"))) {
                            isabnormalFlag = map.get("isabnormal");
                        }
                        isabnormalFlag = map.get("isabnormal");
                    }
                }
            } else if (serviceSubTaskCacheReq.getType() == 1) {
@@ -512,12 +554,10 @@
                        log.error("----nextScriptNo2 的信息为:{}", map.get("nextScriptNo"));
                        if (!Objects.isNull(map.get("nextScriptNo")))
                            nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                        if (score == null) {
                            score = map.get("score");
                        } else {
                            score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                        scoreTotal += parseScore(map.get("score"));
                        if (StringUtils.isNotEmpty(map.get("isabnormal"))) {
                            isabnormalFlag = map.get("isabnormal");
                        }
                        isabnormalFlag = map.get("isabnormal");
                    }
                }
            }
@@ -533,12 +573,12 @@
            serviceSubtask.setTaskid(tid);
            serviceSubtask.setPatid(pid);
            serviceSubtask.setSubmit(1L);
            serviceSubtask.setIsabnormal(Integer.valueOf(isabnormalFlag));
            serviceSubtask.setIsabnormal(parseIsabnormal(isabnormalFlag));
            serviceSubtask.setSendstate(6L);
            serviceSubtask.setExcep(isabnormalFlag);
            serviceSubtask.setFinishtime(new Date());
            serviceSubtask.setUpdateTime(new Date());
            if (score != null && !score.isEmpty()) serviceSubtask.setScore(score);
            serviceSubtask.setScore(formatScore(scoreTotal));
            serviceSubtaskMapper.updateServiceSubtaskByCondition(serviceSubtask);
            //将当前发送状态改成9完成
@@ -594,7 +634,7 @@
        }
        if (CollectionUtils.isNotEmpty(serviceSubTaskMYDAnswerReq.getSvyTaskTemplateScriptVOS())) {
            String score = "0";
            double scoreTotal = 0D;
            List<SvyTaskTemplateScriptVO> svyTaskTemplateScriptVOS = serviceSubTaskMYDAnswerReq.getSvyTaskTemplateScriptVOS();
            Long nextScriptno = null;
@@ -628,11 +668,7 @@
                    log.error("nextScriptNo 的信息为:{}", map.get("nextScriptNo"));
                    if (!Objects.isNull(map.get("nextScriptNo"))) nextScriptno = Long.valueOf(map.get("nextScriptNo"));
                    if (score == null || score.isEmpty()) {
                        score = map.get("score");
                    } else {
                        score = String.valueOf(Double.parseDouble(score) + Double.parseDouble(map.get("score")));
                    }
                    scoreTotal += parseScore(map.get("score"));
                    if (!Objects.isNull(map.get("id"))) {
                        idList.add(Long.valueOf(map.get("id")));
@@ -661,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");
@@ -670,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)) {
@@ -680,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)) {
                    //投诉建议的处理部门
@@ -694,7 +737,7 @@
                serviceSubtask.setUpdateTime(new Date());
                serviceSubtask.setCreateTime(new Date());
                serviceSubtask.setVisitTime(new Date());
                if (score != null && !score.isEmpty()) serviceSubtask.setScore(score);
                serviceSubtask.setScore(formatScore(scoreTotal));
                serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
                //将当前发送状态改成9完成
                ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform();
@@ -998,7 +1041,7 @@
                nextScriptNo = getNextScriptNo(svyTaskTemplateScriptVO.getTemplateID(), svyTaskTemplateScriptVO.getScriptContent());
            }
            if (svyTaskTemplateScriptVO.getScore() != null) {
                score = String.valueOf(svyTaskTemplateScriptVO.getScore());
                score = normalizeScore(String.valueOf(svyTaskTemplateScriptVO.getScore()));
            }
            serviceSubtaskDetailVO.setAsrtext(StringUtils.isEmpty(svyTaskTemplateScriptVO.getScriptResult()) ? "" : svyTaskTemplateScriptVO.getScriptResult().replaceAll("^\"|\"$", ""));
            serviceSubtaskDetailVO.setMatchedtext(StringUtils.isEmpty(svyTaskTemplateScriptVO.getScriptResult()) ? "" : svyTaskTemplateScriptVO.getScriptResult().replaceAll("^\"|\"$", ""));
@@ -1009,7 +1052,7 @@
            if (StringUtils.isNotEmpty(svyTaskTemplateScriptVO.getScriptResult())) {
                serviceSubtaskDetailVO.setAsrtext(svyTaskTemplateScriptVO.getScriptResult());
                if (svyTaskTemplateScriptVO.getScore() != null) {
                    score = String.valueOf(svyTaskTemplateScriptVO.getScore());
                    score = normalizeScore(String.valueOf(svyTaskTemplateScriptVO.getScore()));
                }
            }
            nextScriptNo = svyTaskTemplateScriptVO.getNextScriptno();
@@ -1153,7 +1196,7 @@
        Map<String, String> map = new HashMap<>();
        map.put("nextScriptNo", nextScriptNo);
        map.put("score", score);
        map.put("score", normalizeScore(score));
        map.put("id", "" + serviceSubtaskDetail.getId());
        map.put("isabnormal", isabnormalFlag.toString());
        return map;
@@ -1298,7 +1341,7 @@
            }
            if (ivrTaskTemplateScriptVO.getScore() != null) {
                score = String.valueOf(ivrTaskTemplateScriptVO.getScore());
                score = normalizeScore(String.valueOf(ivrTaskTemplateScriptVO.getScore()));
            }
            if (StringUtils.isNotEmpty(ivrTaskTemplateScriptVO.getQuestionResult())) {
                serviceSubtaskDetailVO.setAsrtext(ivrTaskTemplateScriptVO.getQuestionResult().replaceAll("^\"|\"$", ""));
@@ -1319,7 +1362,7 @@
                    nextScriptNo = "" + getVoiceNextScriptNo(ivrTaskTemplateScriptVO.getTemplateID(), ivrTaskTemplateScriptVO.getScriptContent());
                }
                if (ivrTaskTemplateScriptVO.getScore() != null) {
                    score = String.valueOf(ivrTaskTemplateScriptVO.getScore());
                    score = normalizeScore(String.valueOf(ivrTaskTemplateScriptVO.getScore()));
                }
            }
        }
@@ -1447,7 +1490,7 @@
        Map<String, String> map = new HashMap<>();
        map.put("nextScriptNo", nextScriptNo);
        map.put("score", score);
        map.put("score", normalizeScore(score));
        map.put("isabnormal", isabnormalFlag);
        return map;
    }
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;
@@ -5596,52 +5591,102 @@
        }
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOptionBySubtaskIds(subtaskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
        // 处理 targetvalue:按 & 分割成多条记录
        // SQL 已按「题目 + 选项」汇总数量,这里再按题目合并(不同模板的 targetvalue 选项顺序不同,同一选项会拆成多行),
        // 并把没人选的选项补成 0 行、按各选项数量之和重算比例
        return mergeMydOptionRows(serviceSubtaskDetailRatioExports);
    }
    /**
     * 把 SQL 返回的「题目 + 选项」明细合并成「一行一个选项」
     * <ul>
     *     <li>同一题目下相同 optionresult 的数量累加:原实现遇到同名选项直接 continue,
     *         每个选项只会保留第一条明细的数量,多任务/多患者的项数全被丢掉,前端「数量」永远只有 1</li>
     *     <li>选项清单取 targetvalue(用 &amp; 连接),没人选的选项补 0 行,前端固定六列才能渲染</li>
     *     <li>比例 = 该选项数量 / 该题各选项数量之和,保证一道题各选项比例合计 100%</li>
     * </ul>
     *
     * @param rows SQL 明细
     */
    private List<ServiceSubtaskDetailRatioExport> mergeMydOptionRows(List<ServiceSubtaskDetailRatioExport> rows) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        for (ServiceSubtaskDetailRatioExport export : serviceSubtaskDetailRatioExports) {
            if (StringUtils.isNotEmpty(export.getTargetvalue()) && export.getTargetvalue().contains("&")) {
                String[] values = export.getTargetvalue().split("&");
                // 如果有多条目标值,拆分成多行
                for (String value : values) {
                    //先去 result 中查一下,有没有这个 questiontext 和对应的 targetvalue
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> {
                        if (e == null || export == null) {
                            return false;
                        }
                        String q1 = e.getQuestiontext() != null ? e.getQuestiontext() : "";
                        String q2 = export.getQuestiontext() != null ? export.getQuestiontext() : "";
                        String t1 = value.trim();
                        String t2 = e.getTargetvalue() != null ? e.getTargetvalue() : "";
                        return q1.equals(q2) && t1.equals(t2);
                    }).findFirst().orElse(null);
                    if (existingExport != null && "0".equals(existingExport.getRatio()) && "0".equals(existingExport.getCount())) {
                        //如果存在,则将 result 里的这个问题所对应的 Targetvalue=value 这条数据更新成当前这条数据
                        if (value.equals(export.getOptionresult())) {
                            existingExport.setRatio(export.getRatio());
                            existingExport.setScore(export.getScore());
                            existingExport.setCount(export.getCount());
                            existingExport.setOptionresult(export.getOptionresult());
                        }
                    } else if (existingExport != null && !"0".equals(existingExport.getRatio())) {
                        continue;
                    } else {
                        ServiceSubtaskDetailRatioExport newExport = DtoConversionUtils.sourceToTarget(export, ServiceSubtaskDetailRatioExport.class);
                        newExport.setTargetvalue(value.trim()); // targetvalue 显示单个选项
                        if (!value.equals(export.getOptionresult())) {
                            newExport.setOptionresult(null);
                            newExport.setCount("0");
                            newExport.setRatio("0");
                        }
                        result.add(newExport);
                    }
        if (CollectionUtils.isEmpty(rows)) {
            return result;
        }
        // key -> 选项 -> 数量,用 LinkedHashMap 保序:选项顺序沿用问卷配置 targetvalue 的顺序
        Map<String, Map<String, long[]>> countMap = new LinkedHashMap<>();
        Map<String, Map<String, String>> scoreMap = new LinkedHashMap<>();
        Map<String, ServiceSubtaskDetailRatioExport> templateMap = new LinkedHashMap<>();
        for (ServiceSubtaskDetailRatioExport row : rows) {
            if (row == null) {
                continue;
            }
            String key = row.getQuestiontext();
            if (key == null) {
                continue;
            }
            Map<String, long[]> counts = countMap.computeIfAbsent(key, k -> new LinkedHashMap<>());
            Map<String, String> scores = scoreMap.computeIfAbsent(key, k -> new LinkedHashMap<>());
            templateMap.putIfAbsent(key, row);
            // 题目配置的选项清单:没人选的选项也要列出来(数量为 0)
            for (String option : splitOptions(row.getTargetvalue())) {
                counts.computeIfAbsent(option, k -> new long[1]);
            }
            // 患者实际选中的选项:多选题 asrtext 是「选项1&选项2」,单选/问答题是整段文本
            List<String> chosenOptions = "2".equals(row.getValueType())
                    ? splitOptions(row.getOptionresult())
                    : singleOption(row.getOptionresult());
            if (chosenOptions.isEmpty()) {
                // 未作答:不进任何选项,也不进分母
                continue;
            }
            long count = parseCount(row.getCount());
            for (String chosen : chosenOptions) {
                // 选中的选项可能不在配置清单里(历史数据),补一行,数量不能丢
                counts.computeIfAbsent(chosen, k -> new long[1])[0] += count;
                if (StringUtils.isNotEmpty(row.getScore()) && StringUtils.isEmpty(scores.get(chosen))) {
                    scores.put(chosen, row.getScore());
                }
            } else {
                // targetvalue 不包含 &,直接添加到结果
                result.add(export);
            }
        }
        for (Map.Entry<String, Map<String, long[]>> entry : countMap.entrySet()) {
            ServiceSubtaskDetailRatioExport template = templateMap.get(entry.getKey());
            Map<String, String> scores = scoreMap.get(entry.getKey());
            long total = 0L;
            for (long[] count : entry.getValue().values()) {
                total += count[0];
            }
            for (Map.Entry<String, long[]> optionEntry : entry.getValue().entrySet()) {
                String option = optionEntry.getKey();
                long count = optionEntry.getValue()[0];
                ServiceSubtaskDetailRatioExport merged = DtoConversionUtils.sourceToTarget(template, ServiceSubtaskDetailRatioExport.class);
                merged.setQuestiontext(template.getQuestiontext());
                merged.setTargetvalue(option);
                // 没人选的选项 optionresult 置空,前端按 targetvalue 兜底展示,保持原有展示口径
                merged.setOptionresult(count > 0 ? option : null);
                merged.setCount(String.valueOf(count));
                merged.setRatio(formatRatio(count, total));
                merged.setScore(StringUtils.isNotEmpty(scores.get(option)) ? scores.get(option) : template.getScore());
                result.add(merged);
            }
        }
        return result;
    }
    /**
     * 单选/问答题的作答文本:不按 &amp; 拆,避免把问答题的文本答案拆坏
     */
    private static List<String> singleOption(String answer) {
        List<String> options = new ArrayList<>();
        String text = trimAll(answer);
        if (StringUtils.isNotEmpty(text)) {
            options.add(text);
        }
        return options;
    }
    /**
@@ -5935,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/PatMedInhospMapper.xml
@@ -762,6 +762,18 @@
    </select>
    <!-- 按患者编号 + 出院日期查询出院记录(出院时间只比较到天,不比较时分秒),用于判断患者是否再入院 -->
    <select id="selectOutInhospByPatnoAndOutDate" resultMap="PatMedInhospResult">
        <include refid="selectPatMedInhospVo"/>
        where del_flag = 0
        and inhospstate = '1'
        <if test="patno != null and patno != ''">and patno = #{patno}</if>
        <if test="dayStart != null">and endtime &gt;= #{dayStart}</if>
        <if test="dayEnd != null">and endtime &lt; #{dayEnd}</if>
        order by inhospid desc
    </select>
    <insert id="insertPatMedInhosp" parameterType="com.smartor.domain.PatMedInhosp" useGeneratedKeys="true"
            keyProperty="inhospid">
        insert into pat_med_inhosp
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              → 按科室维度
@@ -3399,14 +3490,15 @@
    <select id="statQuestionOptionBySubtaskIds" resultType="com.smartor.domain.ServiceSubtaskDetailRatioExport">
        SELECT
        d.questiontext AS questiontext,
        t.taskid AS taskid,
        t.task_name AS taskName,
        <!-- 明细已按「题目 + 选项」汇总,任务/科室/病区只是附带信息,必须用聚合函数取值(MySQL ONLY_FULL_GROUP_BY) -->
        MAX(t.taskid) AS taskid,
        MAX(t.task_name) AS taskName,
        d.asrtext AS optionresult,
        d.score AS score,
        s.deptcode AS deptcode,
        s.deptname AS deptname,
        s.leavehospitaldistrictcode AS leavehospitaldistrictcode,
        s.leavehospitaldistrictname AS leavehospitaldistrictname,
        MAX(s.deptcode) AS deptcode,
        MAX(s.deptname) AS deptname,
        MAX(s.leavehospitaldistrictcode) AS leavehospitaldistrictcode,
        MAX(s.leavehospitaldistrictname) AS leavehospitaldistrictname,
        d.targetvalue AS targetvalue,
        d.value_type AS valueType,
        COUNT(*) AS count,
@@ -3424,9 +3516,15 @@
        AND d2.del_flag = '0'
        )
        INNER JOIN (
        <!-- 分母 = 该题各选项项数之和(未作答的空 asrtext 不进分母),这样一道题各选项比例合计才是 100% -->
        SELECT
        x.questiontext AS questiontext,
        SUM(x.option_count) AS total_count
        FROM (
        SELECT
        d.questiontext,
        COUNT(*) AS total_count
        d.asrtext,
        COUNT(*) AS option_count
        FROM
        service_task t
        INNER JOIN service_subtask s ON s.taskid = t.taskid
@@ -3467,7 +3565,11 @@
        <if test="endFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &lt;= date_format(#{endFinishTime},'%y%m%d')
        </if>
        GROUP BY d.questiontext
        <!-- 未作答(asrtext 为空)的明细不计入分母 -->
        AND d.asrtext IS NOT NULL AND TRIM(d.asrtext) &lt;&gt; ''
        GROUP BY d.questiontext, d.asrtext
        ) x
        GROUP BY x.questiontext
        ) t2 ON t2.questiontext = d.questiontext
        WHERE 1=1
        <!-- 去重:明细行同一子任务同一题目只保留最新一条,重答不重复计入项数 -->
@@ -3486,6 +3588,8 @@
        </if>
        AND s.sendstate = 6
        AND d.del_flag=0
        <!-- 未作答(asrtext 为空)不算任何选项,明细里直接排除,避免出现空选项行 -->
        AND d.asrtext IS NOT NULL AND TRIM(d.asrtext) &lt;&gt; ''
        <if test="valueType != null">AND d.value_type &gt;= #{valueType}</if>
        <if test="scoreStart != null">AND d.score &gt;= #{scoreStart}</if>
        <if test="scoreEnd != null">AND d.score &lt;= #{scoreEnd}</if>
@@ -3504,21 +3608,17 @@
        <if test="endFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &lt;= date_format(#{endFinishTime},'%y%m%d')
        </if>
        <!-- 只按「题目 + 选项」汇总:原来 GROUP BY 带上 taskid/科室/病区,同一个选项被拆成多行、每行数量都是 1,
             前端/导出拿到的「数量」就永远做不了累加 -->
        GROUP BY
        t.taskid,
        d.questiontext,
        d.asrtext,
        d.score,
        d.targetvalue,
        d.asrtext,
        t.task_name,
        s.deptcode,
        s.deptname,
        s.leavehospitaldistrictcode,
        s.leavehospitaldistrictname,
        d.value_type
        ORDER BY
        d.questiontext,
        t.taskid
        MIN(t.taskid)
    </select>
    <select id="statQuestionOptionByKsOrBq" resultType="com.smartor.domain.DTO.QuestionCountKsOrBqDTO"
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