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 >= #{scoreStart}</if> <if test="scoreEnd != null">AND d.score <= #{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') >= date_format(#{startOutHospTime},'%y%m%d') </if> <if test="endOutHospTime != null"> AND date_format(s.endtime,'%y%m%d') <= date_format(#{endOutHospTime},'%y%m%d') </if> <if test="startFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') >= date_format(#{startFinishTime},'%y%m%d') </if> <if test="endFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') <= 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 <=> 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>