ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java
@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.stream.Collectors; @@ -204,9 +205,14 @@ /** * 查询患者随访信息 * 省立同德满意度数据统计(按题目统计选项数量/占比,并给出该题的综合得分) * <p> * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%, * 分值取 1、0.8、0.6、0.4、0.2 五档(很满意、满意、一般、不满意、很不满意); * 「不适用」、0 分、空值等既不进分子也不进分母。 */ @ApiOperation("省立同德满意度数据统计") @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid") @PostMapping("/sltdMydTotalByScore") public Map<String, Object> sltdMydTotalByScore(@RequestBody SltdMydTotalVO sltdMydTotalVO) { List<ServiceSubtaskRes> serviceSubtaskResList = null; @@ -220,6 +226,9 @@ serviceSubtaskEntity.setDeptOrDistrict(sltdMydTotalVO.getDeptOrDistrict()); serviceSubtaskEntity.setLeavehospitaldistrictcodes(sltdMydTotalVO.getLeavehospitaldistrictcodes()); serviceSubtaskEntity.setLeaveldeptcodes(sltdMydTotalVO.getLeaveldeptcodes()); //机构/院区由 @AddOrgId 注入,落到查询条件上,避免跨机构取数 serviceSubtaskEntity.setOrgid(sltdMydTotalVO.getOrgid()); serviceSubtaskEntity.setCampusid(sltdMydTotalVO.getCampusid()); if (serviceSubtaskEntity != null) { List<ServiceSubtask> serviceSubtasks = serviceSubtaskService.getServiceSubtasks(serviceSubtaskEntity); List<ServiceSubtaskRes> serviceSubtaskRes = DtoConversionUtils.sourceToTarget(serviceSubtasks, ServiceSubtaskRes.class); @@ -236,6 +245,10 @@ if (CollectionUtils.isNotEmpty(serviceSubtaskResList)) { List<ServiceSubtaskDetailRatioExport> detailList = serviceSubtaskService.getDetailRatio(sltdMydTotalVO, serviceSubtaskResList); if (detailList == null) { //serviceType 组合不在支持范围内(如同时含随访 2 和满意度 6/14),给明确提示而不是悄悄返回空数组 return error("当前 serviceType 组合暂不支持统计问题占比,请分开查询(仅支持随访 2 或满意度 6/14)"); } if (CollectionUtils.isNotEmpty(detailList)) { Map<String, List<ServiceSubtaskDetailRatioExport>> groupedMap = detailList.stream().collect(Collectors.groupingBy(ServiceSubtaskDetailRatioExport::getQuestiontext)); @@ -243,17 +256,8 @@ QuestionResultDTO questionResultDTO = new QuestionResultDTO(); questionResultDTO.setQeustionText(questionText); questionResultDTO.setSubtaskDetailRatioExportList(exportList); BigDecimal allCount = new BigDecimal("0"); BigDecimal allScore = new BigDecimal("0.00"); for (ServiceSubtaskDetailRatioExport ssdre : exportList) { if (StringUtils.isNotEmpty(ssdre.getCount())) allCount.add(BigDecimal.valueOf(Long.valueOf(ssdre.getCount()))); allScore.add(BigDecimal.valueOf(Double.valueOf(ssdre.getScore()))); } if (allCount.compareTo(BigDecimal.ZERO) > 0) questionResultDTO.setAvgScore(allScore.divide(allCount, 2).toString()); //综合得分:Σ(分值×项数) / Σ项数 / 本题最高分值 × 100%,不适用/0分/空值不进分子也不进分母 questionResultDTO.setAvgScore(calcMydScore(exportList)); result.add(questionResultDTO); }); } @@ -287,4 +291,77 @@ return success(serviceSubtaskService.mzMydScoreRank(mzMydScoreRankVO)); } /** * 多选题的问题类型 */ private static final String VALUE_TYPE_MULTIPLE = "2"; /** * 计算一道题的满意度综合得分(百分制,保留两位小数) * <p> * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100% * 等价于 Σ(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / Σ(五档项数) × 100%。 * 用本题最高分值归一化,兼容明细分值存 0-1 权重(1、0.8…)和存 5 分制(5、4…)两种口径。 * * @param exportList 同一道题目的选项统计明细 * @return 百分制综合得分,无有效项数时返回 "0.00" */ private String calcMydScore(List<ServiceSubtaskDetailRatioExport> exportList) { if (CollectionUtils.isEmpty(exportList)) return "0.00"; BigDecimal scoreSum = BigDecimal.ZERO; //分子:Σ(选项分值 × 项数) BigDecimal countSum = BigDecimal.ZERO; //分母:Σ项数 BigDecimal maxScore = null; //本题最高分值,用于归一化 for (ServiceSubtaskDetailRatioExport detail : exportList) { //多选题一行是多个选项的分值之和,分母只算 1 项,会把得分算高,不计入综合得分 if (VALUE_TYPE_MULTIPLE.equals(detail.getValueType())) continue; //「不适用」不在公式的五档里,不计入综合得分 if (isNotApplicable(detail)) continue; BigDecimal score = parseDecimal(detail.getScore()); BigDecimal count = parseDecimal(detail.getCount()); if (score == null || count == null) continue; //0 分(未答/空答)、空数量:既不进分子也不进分母 if (score.signum() <= 0 || count.signum() <= 0) continue; if (maxScore == null || score.compareTo(maxScore) > 0) maxScore = score; scoreSum = scoreSum.add(score.multiply(count)); countSum = countSum.add(count); } if (maxScore == null || 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(); } /** * 是否「不适用」选项 */ private boolean isNotApplicable(ServiceSubtaskDetailRatioExport detail) { return containsNotApplicable(detail.getOptionresult()) || containsNotApplicable(detail.getTargetvalue()); } private boolean containsNotApplicable(String optionText) { if (StringUtils.isEmpty(optionText)) return false; String text = optionText.trim(); return text.contains("不适用") || text.contains("未答") || text.contains("拒答"); } /** * 字符串转 BigDecimal,空值/非法值返回 null */ private BigDecimal parseDecimal(String value) { if (StringUtils.isEmpty(value)) return null; try { return new BigDecimal(value.trim()); } catch (NumberFormatException e) { return null; } } } smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -6181,17 +6181,29 @@ */ public List<ServiceSubtaskDetailRatioExport> getDetailRatio(SltdMydTotalVO sltdMydTotalVO, List<ServiceSubtaskRes> serviceSubtaskRes) { List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = null; if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("2") && (sltdMydTotalVO.getServiceTypeList().contains("6") || sltdMydTotalVO.getServiceTypeList().contains("14"))) { //serviceType,即有随访又有满意度,暂时没法统计 List<String> serviceTypeList = sltdMydTotalVO.getServiceTypeList(); if (CollectionUtils.isEmpty(serviceTypeList)) { log.warn("getDetailRatio 未传 serviceTypeList,无法统计问题占比"); return serviceSubtaskDetailRatioExports; } else if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("2")) { } // 判断口径:2=随访(按 taskid 统计),6/14=满意度(按 subtask id 统计) boolean hasFollowUp = serviceTypeList.contains("2"); boolean hasSatisfaction = serviceTypeList.contains("6") || serviceTypeList.contains("14"); if (hasFollowUp && hasSatisfaction) { //serviceType,即有随访又有满意度,暂时没法统计 log.warn("getDetailRatio 暂不支持同时统计随访(2)与满意度(6/14),serviceTypeList={}", serviceTypeList); return serviceSubtaskDetailRatioExports; } 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()); } else if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("6") || sltdMydTotalVO.getServiceTypeList().contains("14")) { } else if (hasSatisfaction) { //满意度统计 (通过 subtask 的 id 去统计) List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).collect(Collectors.toList()); serviceSubtaskDetailRatioExports = statQuestionOptionBySubtaskIds(subtaskIds, sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType()); } else { log.warn("getDetailRatio 暂不支持的 serviceTypeList={}(仅支持 2 或 6/14)", serviceTypeList); } return serviceSubtaskDetailRatioExports; } smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -3329,6 +3329,14 @@ 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 <!-- 去重:同一子任务同一题目只保留最新一条,避免重答/重复回调把同一道题重复计入项数(与 mzMydScoreRank 口径一致) --> 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' ) INNER JOIN ( SELECT d.questiontext, @@ -3337,6 +3345,14 @@ 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 <!-- 去重:分母(该题有效答卷总数)与明细口径保持一致,保证各选项比例合计 100% --> AND d.id = ( SELECT MAX(d4.id) FROM service_subtask_detail d4 WHERE d4.sub_id = d.sub_id AND d4.questiontext <=> d.questiontext AND d4.del_flag = '0' ) WHERE 1=1 <if test="subtaskIds != null and subtaskIds.size() > 0"> AND s.id IN @@ -3345,16 +3361,37 @@ </foreach> </if> <if test="valueType != null">AND d.value_type >= #{valueType}</if> <if test="scoreStart != null">AND d.score = #{scoreStart}</if> <if test="scoreStart != null">AND d.score >= #{scoreStart}</if> <if test="scoreEnd != null">AND d.score <= #{scoreEnd}</if> <if test="questiontext != null and questiontext != ''">and d.questiontext like concat('%', #{questiontext}, '%') </if> AND s.sendstate = 6 AND d.del_flag=0 <!-- 分母与明细用同一套时间条件,否则选了日期区间后各选项比例合计不到 100% --> <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 d.questiontext ) t2 ON t2.questiontext = d.questiontext WHERE 1=1 <!-- 去重:明细行同一子任务同一题目只保留最新一条,重答不重复计入项数 --> AND d.id = ( SELECT MAX(d3.id) FROM service_subtask_detail d3 WHERE d3.sub_id = d.sub_id AND d3.questiontext <=> d.questiontext AND d3.del_flag = '0' ) <if test="subtaskIds != null and subtaskIds.size() > 0"> AND s.id IN <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">