liusheng
3 天以前 d963031435c593b761436d819268f5493923c41d
1.导出新增图表
已修改10个文件
375 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateScriptMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatArchiveOutMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/SvyLibTemplateScriptMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskController.java
@@ -23,6 +23,7 @@
import com.ruoyi.quartz.task.RyTask;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserDeptService;
import com.smartor.common.MydOptionRatioChartBuilder;
import com.smartor.domain.*;
import com.smartor.domain.DTO.QuestionResultDTO;
import com.smartor.domain.VO.*;
@@ -221,16 +222,22 @@
                    serviceSubtaskExport.setEndDay(endDay);
                }
            }
            SltdMydTotalVO sltdMydTotalVO = new SltdMydTotalVO();
            sltdMydTotalVO.setServiceTypeList(Arrays.asList(serviceSubtaskVO.getServiceType()));
            sltdMydTotalVO.setEndOutHospTime(serviceSubtaskVO.getEndOutHospTime());
            sltdMydTotalVO.setStartOutHospTime(serviceSubtaskVO.getStartOutHospTime());
            serviceSubtaskDetailRatioExports = serviceSubtaskService.getDetailRatio(sltdMydTotalVO, serviceSubtaskList);
            //问题详情占比:只统计本次导出的这批患者,数量=选该选项的患者数,比例=占该题作答人数
            List<Long> subtaskIds = new ArrayList<>();
            for (ServiceSubtaskRes serviceSubtaskRes : serviceSubtaskList) {
                if (serviceSubtaskRes.getId() != null) {
                    subtaskIds.add(serviceSubtaskRes.getId());
                }
            }
            serviceSubtaskDetailRatioExports = serviceSubtaskService.statMydQuestionOptionForExport(subtaskIds, null);
        }
        ExcelUtil<ServiceSubtaskExport> util = new ExcelUtil<ServiceSubtaskExport>(ServiceSubtaskExport.class);
        util.exportExcelTwoSheet(response, serviceSubtaskExports, "患者随访信息表单", serviceSubtaskDetailRatioExports, "问题详情占比", ServiceSubtaskDetailRatioExport.class);
        //第三个Sheet:问题详情占比图表(按任务名称分组,一个任务一张柱状图)
        util.exportExcelTwoSheetWithChart(response, serviceSubtaskExports, "患者随访信息表单",
                serviceSubtaskDetailRatioExports, "问题详情占比", ServiceSubtaskDetailRatioExport.class,
                new MydOptionRatioChartBuilder(serviceSubtaskDetailRatioExports));
    }
    /**
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -474,6 +474,67 @@
    }
    /**
     * 图表Sheet构建回调:由调用方在给定工作簿上追加一个(或多个)带图表的Sheet
     */
    public interface ChartSheetBuilder {
        void build(XSSFWorkbook workbook);
    }
    /**
     * 取底层 XSSFWorkbook(SXSSF 流式工作簿也支持挂原生图表),不支持时返回 null
     */
    public XSSFWorkbook getXssfWorkbook() {
        if (wb instanceof SXSSFWorkbook) {
            return ((SXSSFWorkbook) wb).getXSSFWorkbook();
        }
        if (wb instanceof XSSFWorkbook) {
            return (XSSFWorkbook) wb;
        }
        return null;
    }
    /**
     * 导出两个数据Sheet + 一个图表Sheet(图表内容由调用方通过 builder 追加)
     *
     * @param response   返回数据
     * @param list1      第一个Sheet的数据集合
     * @param sheetName1 第一个Sheet的名称
     * @param list2      第二个Sheet的数据集合
     * @param sheetName2 第二个Sheet的名称
     * @param clazz2     第二个Sheet数据对象的Class
     * @param chartBuilder 图表Sheet构建回调,可为空(为空时与两Sheet导出完全一样)
     */
    public void exportExcelTwoSheetWithChart(HttpServletResponse response,
                                             List<T> list1, String sheetName1,
                                             List<?> list2, String sheetName2, Class<?> clazz2,
                                             ChartSheetBuilder chartBuilder) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 初始化并写入Sheet1
        this.init(list1, sheetName1, StringUtils.EMPTY, Type.EXPORT);
        writeSheet();
        // 追加写入Sheet2
        writeAdditionalSheet(list2, sheetName2, clazz2);
        // 追加图表Sheet
        if (chartBuilder != null) {
            XSSFWorkbook xssfWorkbook = getXssfWorkbook();
            if (xssfWorkbook != null) {
                chartBuilder.build(xssfWorkbook);
            } else {
                log.warn("当前工作簿不支持原生图表,跳过图表Sheet");
            }
        }
        // 输出到响应流
        try {
            wb.write(response.getOutputStream());
        } catch (Exception e) {
            log.error("导出Excel异常{}", e.getMessage());
        } finally {
            IOUtils.closeQuietly(wb);
        }
    }
    /**
     * 向当前工作簿追加写入一个Sheet
     *
     * @param dataList  数据集合
@@ -481,8 +542,7 @@
     * @param dataClazz 数据对象的Class
     */
    @SuppressWarnings("unchecked")
    public void writeAdditionalSheet(List<?> dataList, String sheetName, Class<?> dataClazz) {
        if (dataList == null) {
    public void writeAdditionalSheet(List<?> dataList, String sheetName, Class<?> dataClazz) {        if (dataList == null) {
            dataList = new ArrayList<>();
        }
        // 保存原始状态
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java
@@ -4,6 +4,7 @@
import com.smartor.domain.DTO.MydTrendReqDTO;
import com.smartor.domain.DTO.MzMydScoreRankDTO;
import com.smartor.domain.DTO.QuestionCountKsOrBqDTO;
import com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO;
import com.smartor.domain.VO.PatSatisfactionReqVO;
import com.smartor.domain.VO.HeLibraryCountVO;
import com.smartor.domain.VO.MzMydScoreRankVO;
@@ -269,11 +270,23 @@
     * 统计问题的选项的百分比
     *
     * @param taskIds          任务 ID 列表 (用于服务类型 2)
     * @param subtaskIds       子任务 ID 列表(可选)。传了就只统计这批子任务(即本次导出的患者),
     *                         不传则统计这些任务下的全部答卷,保持原行为
     * @param startOutHospTime
     * @param endOutHospTime
     * @return
     */
    List<ServiceSubtaskDetailRatioExport> statQuestionOption(@Param("taskIds") List<Long> taskIds, @Param("startOutHospTime") Date startOutHospTime, @Param("endOutHospTime") Date endOutHospTime, @Param("startFinishTime") Date startFinishTime, @Param("endFinishTime") Date endFinishTime, @Param("scoreStart") Double scoreStart, @Param("scoreEnd") Double scoreEnd, @Param("questiontext") String questiontext, @Param("valueType") String valueType);
    List<ServiceSubtaskDetailRatioExport> statQuestionOption(@Param("taskIds") List<Long> taskIds, @Param("startOutHospTime") Date startOutHospTime, @Param("endOutHospTime") Date endOutHospTime, @Param("startFinishTime") Date startFinishTime, @Param("endFinishTime") Date endFinishTime, @Param("scoreStart") Double scoreStart, @Param("scoreEnd") Double scoreEnd, @Param("questiontext") String questiontext, @Param("valueType") String valueType, @Param("subtaskIds") List<Long> subtaskIds);
    /**
     * 导出「问题详情占比」专用统计(按本次导出的子任务统计)
     * <p>
     * 数量 = 选择该选项的患者数(同一患者同一题只取最新一条明细),分数 = 该选项明细的 score 之和。
     *
     * @param subtaskIds   本次导出的子任务 ID(必传,为空则查不到数据)
     * @param questiontext 问题内容(可选,模糊匹配)
     */
    List<ServiceSubtaskOptionStatDTO> statMydQuestionOptionForExport(@Param("subtaskIds") List<Long> subtaskIds, @Param("questiontext") String questiontext);
    /**
     * 根据子任务 ID 统计问题的选项的百分比(用于满意度统计 serviceType=6)
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java
@@ -223,10 +223,22 @@
    /**
     * 统计问题的选项的百分比 (用于服务类型 2)
     *
     * @param taskIds
     * @param taskIds    任务 ID 列表
     * @param subtaskIds 子任务 ID 列表(可选):传了就只统计这批子任务(本次导出的患者),分母也用同一批数据
     * @return
     */
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType);
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds);
    /**
     * 导出「问题详情占比」专用统计(只统计本次导出的这批患者)
     * <p>
     * 数量 = 选择该选项的患者数;比例 = 数量 / 该题作答患者数;分数 = 该选项明细 score 之和;
     * 任务名称/任务编号 = service_task.task_name / taskid。
     *
     * @param subtaskIds   本次导出的子任务 ID
     * @param questiontext 问题内容(可选)
     */
    public List<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext);
    /**
     * 根据子任务 ID 统计问题的选项的百分比(用于满意度统计 serviceType=6)
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java
@@ -1146,7 +1146,7 @@
        //如何这需要服务补偿的服务ID不为空的话,则进行立即发送
        log.error("------svyTaskTemplateScriptVO.getSendTaskids()的值为:{},selectServiceSubtaskList的值为:{}", svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.size());
        log.info("------svyTaskTemplateScriptVO.getSendTaskids()的值为:{},selectServiceSubtaskList的值为:{}", svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.size());
        if (CollectionUtils.isNotEmpty(svyTaskTemplateScriptVO.getSendTaskids()) && CollectionUtils.isNotEmpty(selectServiceSubtaskList)) {
            sendTaskIds(svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.get(0));
        }
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -27,6 +27,7 @@
import com.smartor.domain.DTO.MzMydScoreRankDTO;
import com.smartor.domain.DTO.QuestionCountKsOrBqDTO;
import com.smartor.domain.DTO.ServiceSubtaskDetailDTO;
import com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO;
import com.smartor.domain.*;
import com.smartor.domain.VO.*;
import com.smartor.domain.entity.ServiceSubtaskEntity;
@@ -5230,13 +5231,13 @@
        return resultMap;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType) {
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds) {
        //先清理一下重复数据
        List<Long> repeatIds = serviceSubtaskDetailMapper.selectRepeatDetailIds();
        if (!CollectionUtils.isEmpty(repeatIds)) {
            serviceSubtaskDetailMapper.deleteRepeatDetailByIds(repeatIds);
        }
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds);
        // 处理 targetvalue:按 & 分割成多条记录
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
@@ -5246,7 +5247,9 @@
                // 如果有多条目标值,拆分成多行
                for (String value : values) {
                    //先去 result 中查一下,有没有这个 questiontext 和对应的 targetvalue
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    //注意必须带上 taskid 一起匹配:否则会把别的任务的数据填到本任务的行上,
                    //导致导出的「数量/比例」和该行的「任务名称/任务编号」对不上(同一任务同一题出现两个不同分母)
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> Objects.equals(e.getTaskid(), export.getTaskid()) && e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    if (existingExport != null && existingExport.getRatio().equals("0") && existingExport.getCount().equals("0")) {
                        //如果存在,则将 result 里的这个问题所对应的 Targetvalue=value 这条数据更新成当前这条数据
                        if (value.equals(export.getOptionresult())) {
@@ -5275,6 +5278,152 @@
        }
        return result;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        if (CollectionUtils.isEmpty(subtaskIds)) {
            return result;
        }
        List<ServiceSubtaskOptionStatDTO> statList = serviceSubtaskMapper.statMydQuestionOptionForExport(subtaskIds, questiontext);
        if (CollectionUtils.isEmpty(statList)) {
            return result;
        }
        return buildMydOptionRows(statList);
    }
    /**
     * 把「任务+题目+选项」的作答统计拼成导出的「问题详情占比」行
     * <ul>
     *     <li>选项清单:题目配置过的选项在 targetvalue(用 & 连接)里,全部列出,没人选的显示 0</li>
     *     <li>数量:患者选中的选项在 asrtext 里(多选题是「A&B」),每个选中项各记一次人数,一个患者只算一条明细</li>
     *     <li>比例:数量 / 该题作答患者数,所以单选题各选项数量之和 = 该题作答患者数</li>
     *     <li>分数:该选项明细的 score 之和;多选行按选中项份数均分,保证各选项分数之和 = 明细分数之和</li>
     * </ul>
     */
    private static List<ServiceSubtaskDetailRatioExport> buildMydOptionRows(List<ServiceSubtaskOptionStatDTO> statList) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        // 按「任务 + 题目」分组,保持 SQL 的顺序(先题目后任务)
        Map<String, List<ServiceSubtaskOptionStatDTO>> grouped = new LinkedHashMap<>();
        for (ServiceSubtaskOptionStatDTO stat : statList) {
            String key = stat.getTaskid() + "\u0001" + stat.getQuestiontext();
            grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(stat);
        }
        for (List<ServiceSubtaskOptionStatDTO> group : grouped.values()) {
            ServiceSubtaskOptionStatDTO first = group.get(0);
            // 选项 -> 人数(long[1] 便于累加),顺序即出现顺序
            Map<String, long[]> countMap = new LinkedHashMap<>();
            Map<String, BigDecimal> scoreMap = new LinkedHashMap<>();
            for (ServiceSubtaskOptionStatDTO stat : group) {
                // targetvalue = 本题所有选项的清单,没人选的也要列出来
                List<String> options = splitOptions(stat.getTargetvalue());
                // asrtext = 患者实际选中的选项,多选题会是「选中项1&选中项2」
                List<String> chosenOptions = splitOptions(stat.getOptionresult());
                for (String option : options) {
                    countMap.computeIfAbsent(option, k -> new long[1]);
                    scoreMap.computeIfAbsent(option, k -> BigDecimal.ZERO);
                }
                long patientCount = stat.getPatientCount() == null ? 0L : stat.getPatientCount();
                BigDecimal scoreSum = stat.getScoreSum() == null ? BigDecimal.ZERO : stat.getScoreSum();
                if (!chosenOptions.isEmpty()) {
                    BigDecimal[] scoreParts = splitScore(scoreSum, chosenOptions.size());
                    for (int i = 0; i < chosenOptions.size(); i++) {
                        String chosen = chosenOptions.get(i);
                        // 选中的选项可能不在配置清单里(历史数据),补一行
                        countMap.computeIfAbsent(chosen, k -> new long[1]);
                        scoreMap.computeIfAbsent(chosen, k -> BigDecimal.ZERO);
                        countMap.get(chosen)[0] += patientCount;
                        scoreMap.put(chosen, scoreMap.get(chosen).add(scoreParts[i]));
                    }
                } else if (options.size() == 1) {
                    // 没有 asrtext(未答/无应答)时,只有唯一选项才能确定归属
                    countMap.get(options.get(0))[0] += patientCount;
                    scoreMap.put(options.get(0), scoreMap.get(options.get(0)).add(scoreSum));
                }
            }
            long total = 0L;
            for (long[] count : countMap.values()) total += count[0];
            for (Map.Entry<String, long[]> entry : countMap.entrySet()) {
                long count = entry.getValue()[0];
                ServiceSubtaskDetailRatioExport row = new ServiceSubtaskDetailRatioExport();
                row.setQuestiontext(first.getQuestiontext());
                row.setOptionresult(entry.getKey());
                row.setTargetvalue(entry.getKey());
                row.setCount(String.valueOf(count));
                row.setRatio(formatRatio(count, total));
                row.setScore(formatScore(scoreMap.get(entry.getKey())));
                row.setTaskname(first.getTaskName());
                row.setTaskid(first.getTaskid());
                row.setValueType(first.getValueType());
                result.add(row);
            }
        }
        return result;
    }
    /**
     * 把一条明细的分数按选中项份数均分,余数补给第一个,保证各份之和 = 原分数
     */
    private static BigDecimal[] splitScore(BigDecimal total, int parts) {
        BigDecimal[] result = new BigDecimal[parts];
        if (parts <= 0) {
            return result;
        }
        BigDecimal each = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.HALF_UP);
        for (int i = 0; i < parts; i++) {
            result[i] = each;
        }
        result[0] = result[0].add(total.subtract(each.multiply(BigDecimal.valueOf(parts))));
        return result;
    }
    /**
     * 拆分题目配置的选项(多个选项用 & 连接),顺带去掉全角/半角空白
     */
    private static List<String> splitOptions(String targetvalue) {
        List<String> options = new ArrayList<>();
        if (StringUtils.isEmpty(targetvalue)) {
            return options;
        }
        for (String part : targetvalue.split("&")) {
            String option = trimAll(part);
            if (StringUtils.isNotEmpty(option) && !options.contains(option)) {
                options.add(option);
            }
        }
        return options;
    }
    /**
     * 比例:数量 / 该题作答患者数,保留两位小数
     */
    private static String formatRatio(long count, long total) {
        if (total <= 0) {
            return "0.00%";
        }
        return BigDecimal.valueOf(count * 100.0 / total).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
    }
    /**
     * 分数之和:去掉多余的小数位(0.00 -> 0)
     */
    private static String formatScore(BigDecimal score) {
        if (score == null) {
            return "0";
        }
        BigDecimal value = score.stripTrailingZeros();
        if (value.scale() < 0) {
            value = value.setScale(0);
        }
        return value.toPlainString();
    }
    /*
@@ -6197,7 +6346,9 @@
        if (hasFollowUp) {
            //获取serviceSubtaskList中的所有taskid
            List<Long> taskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getTaskid).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType());
            //同时带上本次导出的子任务 id:让「问题详情占比」只统计这批患者,与「患者随访信息表单」口径一致
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType(), subtaskIds);
        } else if (hasSatisfaction) {
            //满意度统计 (通过 subtask 的 id 去统计)
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).collect(Collectors.toList());
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateScriptMapper.xml
@@ -210,12 +210,12 @@
            resultMap="IvrLibaTemplateScriptResult">
        <include refid="selectIvrLibaTemplateScriptVo"/>
        <where>del_flag=0
            <if test="dimensionList != null ">and dimension in
            <if test="dimensionList != null and dimensionList.size() > 0">and dimension in
                <foreach item="dimension" collection="dimensionList" open="(" separator="," close=")">
                    #{dimension}
                </foreach>
            </if>
            <if test="dimensionList == null ">and dimension IS NOT NULL </if>
            <if test="dimensionList == null or dimensionList.size() == 0">and dimension IS NOT NULL </if>
            <if test="dutyDeptPersonName != null  and dutyDeptPersonName != ''">and duty_dept_person_name like concat('%', #{dutyDeptPersonName},
                '%')
            </if>
smartor/src/main/resources/mapper/smartor/PatArchiveOutMapper.xml
@@ -120,12 +120,12 @@
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
        <if test="deptcodes != null ">and deptcode in
        <if test="deptcodes != null and deptcodes.size() > 0">and deptcode in
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="drCodes != null ">and drcode in
        <if test="drCodes != null and drCodes.length > 0">and drcode in
            <foreach collection="drCodes" item="drCode" open="(" separator="," close=")">
                #{drCode}
            </foreach>
@@ -137,12 +137,12 @@
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
        <if test="deptcodes != null ">and leaveldeptcode in
        <if test="deptcodes != null and deptcodes.size() > 0">and leaveldeptcode in
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="drCodes != null ">and drcode in
        <if test="drCodes != null and drCodes.length > 0">and drcode in
            <foreach collection="drCodes" item="drCode" open="(" separator="," close=")">
                #{drCode}
            </foreach>
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -3258,6 +3258,13 @@
                #{taskId}
            </foreach>
        </if>
        <!-- 传了 subtaskIds 只统计这批子任务(本次导出的患者) -->
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <if test="valueType != null">AND d.value_type = #{valueType}</if>
        <if test="scoreStart != null">AND d.score &gt;= #{scoreStart}</if>
        <if test="scoreEnd != null">AND d.score &lt;= #{scoreEnd}</if>
@@ -3266,6 +3273,19 @@
        </if>
        and s.sendstate=6
        and d.del_flag=0
        <!-- 分母必须和明细用同一套时间条件,否则选了时间范围后比例会按全量历史被稀释 -->
        <if test="startOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="startFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &gt;= date_format(#{startFinishTime},'%y%m%d')
        </if>
        <if test="endFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &lt;= date_format(#{endFinishTime},'%y%m%d')
        </if>
        GROUP BY
        t.taskid,
        d.questiontext
@@ -3275,6 +3295,12 @@
            AND t.taskid IN
            <foreach collection="taskIds" item="taskId" open="(" separator="," close=")">
                #{taskId}
            </foreach>
        </if>
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <if test="valueType != null">AND d.value_type = #{valueType}</if>
@@ -3309,6 +3335,64 @@
        t.taskid
    </select>
    <!--
      导出「问题详情占比」专用统计(只统计本次导出的这批患者)
      数量 = 选择该选项的患者数(COUNT(DISTINCT s.id),并用 MAX(id) 保证同一患者同一题只算最新一条)
      分数 = 该选项明细的 score 之和
      比例 = 数量 / 该题作答患者数,在 Java 侧统一计算,因此「单题各选项数量之和 = 该题作答患者数」
    -->
    <select id="statMydQuestionOptionForExport" resultType="com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO">
        SELECT
        t.taskid AS taskid,
        t.task_name AS taskName,
        d.questiontext AS questiontext,
        d.asrtext AS optionresult,
        d.targetvalue AS targetvalue,
        d.value_type AS valueType,
        COUNT(DISTINCT s.id) AS patientCount,
        SUM(COALESCE(CAST(d.score AS DECIMAL(18,2)), 0)) AS scoreSum
        FROM
        service_task t
        INNER JOIN service_subtask s ON s.taskid = t.taskid
        INNER JOIN service_subtask_detail d ON d.sub_id = s.id
        WHERE 1=1
        AND s.del_flag = '0'
        AND d.del_flag = 0
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <!-- 不传子任务时直接查不到数据,避免误统计全量 -->
        <if test="subtaskIds == null or subtaskIds.size() == 0">
            AND 1 = 0
        </if>
        <if test="questiontext != null and questiontext != ''">
            AND d.questiontext LIKE CONCAT('%', #{questiontext}, '%')
        </if>
        <!-- 同一患者同一题目只保留最新一条,重答不重复计入数量 -->
        AND d.id = (
        SELECT MAX(d2.id)
        FROM service_subtask_detail d2
        WHERE d2.sub_id = d.sub_id
        AND d2.questiontext &lt;=&gt; d.questiontext
        AND d2.del_flag = '0'
        )
        GROUP BY
        t.taskid,
        t.task_name,
        d.questiontext,
        d.asrtext,
        d.targetvalue,
        d.value_type
        <!-- 按任务名称分组展示:先任务名称、再任务编号,同一任务的题目/选项连续排列 -->
        ORDER BY
        t.task_name,
        t.taskid,
        d.questiontext
    </select>
    <!-- 新方法:根据子任务 ID 统计问题选项百分比 (用于满意度统计 serviceType=6) -->
    <select id="statQuestionOptionBySubtaskIds" resultType="com.smartor.domain.ServiceSubtaskDetailRatioExport">
        SELECT
smartor/src/main/resources/mapper/smartor/SvyLibTemplateScriptMapper.xml
@@ -210,7 +210,7 @@
            resultMap="SvyLibTemplateScriptResult">
        <include refid="selectSvyLibTemplateScriptVo"/>
        <where>del_flag=0
            <if test="dimensionList != null ">and dimension in
            <if test="dimensionList != null and dimensionList.size() > 0">and dimension in
                <foreach item="dimension" collection="dimensionList" open="(" separator="," close=")">
                    #{dimension}
                </foreach>
@@ -239,7 +239,7 @@
                #{circulationDeptPersonName},
                '%')
            </if>
            <if test="dimensionList == null ">and dimension IS NOT NULL </if>
            <if test="dimensionList == null or dimensionList.size() == 0">and dimension IS NOT NULL </if>
        </where>
    </select>