package com.ruoyi.satisfaction.service.impl; import com.ruoyi.satisfaction.domain.ServiceSubtaskDetailRatioExport; import com.ruoyi.satisfaction.domain.dto.MydTrendReqDTO; import com.ruoyi.satisfaction.domain.dto.MzMydScoreRankDTO; import com.ruoyi.satisfaction.domain.dto.QuestionCountKsOrBqDTO; import com.ruoyi.satisfaction.domain.dto.ServiceSubtaskOptionStatDTO; import com.ruoyi.satisfaction.domain.vo.MydTrendReqVO; import com.ruoyi.satisfaction.domain.vo.MzMydScoreRankVO; import com.ruoyi.satisfaction.domain.vo.QuestionCountKsOrBqVO; import com.ruoyi.satisfaction.domain.row.SatisfactionScriptQuery; import com.ruoyi.satisfaction.domain.row.SatisfactionSubtaskQuery; import com.ruoyi.satisfaction.domain.row.SatisfactionSubtaskRef; import com.ruoyi.satisfaction.domain.row.SatisfactionIvrScriptRow; import com.ruoyi.satisfaction.domain.row.SatisfactionSvyScriptRow; import com.ruoyi.satisfaction.domain.row.SatisfactionTaskRow; import com.ruoyi.satisfaction.domain.vo.SatisfactionExceptConfigQuestionReqVO; import com.ruoyi.satisfaction.domain.vo.SltdMydTotalVO; import com.ruoyi.satisfaction.domain.row.SatisfactionCategoryRow; import com.ruoyi.satisfaction.mapper.SatisfactionMapper; import com.ruoyi.satisfaction.mapper.SatisfactionScriptMapper; import com.ruoyi.satisfaction.service.SatisfactionSubtaskService; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.utils.DtoConversionUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * 满意度「子任务维度」统计服务。 *

* 迁移说明:本类方法原散落在随访主干的大文件 ServiceSubtaskServiceImpl 中 * (被视为随访主干的大文件),现整段搬入满意度模块,业务逻辑一行未改。 *

* 与 {@link PatSatisfactionServiceImpl} 的分工: *

* 本模块自带 SQL,直接查 {@code service_subtask} / {@code service_subtask_detail}, * 不依赖 smartor 的任何实体(调用方只传 id 列表)。 */ @Slf4j @Service public class SatisfactionSubtaskServiceImpl implements SatisfactionSubtaskService { @Autowired private SatisfactionMapper satisfactionMapper; @Autowired private SatisfactionScriptMapper satisfactionScriptMapper; public List statQuestionOption(List taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List subtaskIds) { //先清理一下重复数据 List repeatIds = satisfactionMapper.selectRepeatDetailIds(); if (!CollectionUtils.isEmpty(repeatIds)) { satisfactionMapper.deleteRepeatDetailByIds(repeatIds); } List serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds); // 处理 targetvalue:按 & 分割成多条记录 List 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 //注意必须带上 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())) { existingExport.setRatio(export.getRatio()); existingExport.setScore(export.getScore()); existingExport.setCount(export.getCount()); existingExport.setOptionresult(export.getOptionresult()); } } else if (existingExport != null && !existingExport.getRatio().equals("0")) { 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); } } } else { // targetvalue 不包含 &,直接添加到结果 result.add(export); } } return result; } public List statMydQuestionOptionForExport(List subtaskIds, String questiontext) { List result = new ArrayList<>(); if (CollectionUtils.isEmpty(subtaskIds)) { return result; } List statList = satisfactionMapper.statMydQuestionOptionForExport(subtaskIds, questiontext); if (CollectionUtils.isEmpty(statList)) { return result; } return buildMydOptionRows(statList); } /** * 把「任务+题目+选项」的作答统计拼成导出的「问题详情占比」行 *
    *
  • 选项清单:题目配置过的选项在 targetvalue(用 & 连接)里,全部列出,没人选的显示 0
  • *
  • 数量:患者选中的选项在 asrtext 里(多选题是「A&B」),每个选中项各记一次人数,一个患者只算一条明细
  • *
  • 比例:数量 / 该题作答患者数,所以单选题各选项数量之和 = 该题作答患者数
  • *
  • 分数:该选项明细的 score 之和;多选行按选中项份数均分,保证各选项分数之和 = 明细分数之和
  • *
*/ private static List buildMydOptionRows(List statList) { List result = new ArrayList<>(); // 按「任务 + 题目」分组,保持 SQL 的顺序(先题目后任务) Map> grouped = new LinkedHashMap<>(); for (ServiceSubtaskOptionStatDTO stat : statList) { String key = stat.getTaskid() + "\u0001" + stat.getQuestiontext(); grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(stat); } for (List group : grouped.values()) { ServiceSubtaskOptionStatDTO first = group.get(0); // 选项 -> 人数(long[1] 便于累加),顺序即出现顺序 Map countMap = new LinkedHashMap<>(); Map scoreMap = new LinkedHashMap<>(); for (ServiceSubtaskOptionStatDTO stat : group) { // targetvalue = 本题所有选项的清单,没人选的也要列出来 List options = splitOptions(stat.getTargetvalue()); // asrtext = 患者实际选中的选项,多选题会是「选中项1&选中项2」 List 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 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 splitOptions(String targetvalue) { List 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(); } public Map>> statQuestionOptionByKsOrBq(QuestionCountKsOrBqVO vo) { // 1. 查明细 List sourceList = satisfactionMapper.statQuestionOptionByKsOrBq(vo); if (CollectionUtils.isEmpty(sourceList)) { return Collections.emptyMap(); } // 2. 判断按科室还是按病区 boolean byDept = vo.getLeaveldeptcodes() != null; boolean byDistrict = !byDept && vo.getLeavehospitaldistrictcodes() != null; if (!byDept && !byDistrict) { return Collections.emptyMap(); } // 3. key = 科室/病区 + 题目 + 选项 Map map = new LinkedHashMap<>(); for (QuestionCountKsOrBqDTO dto : sourceList) { if (StringUtils.isEmpty(dto.getTargetvalue())) { continue; } // 分组名:科室 或 病区 String groupName = byDept ? dto.getDeptname() : dto.getLeavehospitaldistrictname(); if (StringUtils.isEmpty(groupName)) { continue; } String asrtext = trimAll(dto.getAsrtext()); // 用户实际选中的选项 String questionText = trimAll(dto.getQuestionText()); // 题目 if (StringUtils.isEmpty(questionText)) { continue; } // 拆分 targetvalue,每个选项都要有一条记录 for (String raw : dto.getTargetvalue().split("&")) { String option = trimAll(raw); if (option.isEmpty()) { continue; } // key = 科室/病区 + 题目 + 选项 String key = buildKey(groupName, questionText, option); QuestionCountKsOrBqDTO item = map.get(key); if (item == null) { item = DtoConversionUtils.sourceToTarget(dto, QuestionCountKsOrBqDTO.class); item.setTargetvalue(option); item.setAsrtext(option); item.setQuestionText(dto.getQuestionText()); item.setAnswerCount("0"); item.setSingleScore("0"); map.put(key, item); } // 只有当前选项 == 用户实际选中的选项,才累加 if (option.equals(asrtext)) { int count = parseCount(item.getAnswerCount()) + 1; item.setAnswerCount(String.valueOf(count)); BigDecimal totalScore = new BigDecimal(item.getSingleScore()) .add(new BigDecimal(dto.getSingleScore() == null ? "0" : dto.getSingleScore())); item.setSingleScore(totalScore.toPlainString()); } } } // 4. 计算平均分 for (QuestionCountKsOrBqDTO dto : map.values()) { String answerCount = dto.getAnswerCount(); String singleScore = dto.getSingleScore(); BigDecimal avg; if (StringUtils.isEmpty(answerCount) || BigDecimal.ZERO.compareTo(new BigDecimal(answerCount.trim())) == 0) { avg = BigDecimal.ZERO; } else { avg = new BigDecimal(singleScore == null ? "0" : singleScore) .divide(new BigDecimal(answerCount.trim()), 2, RoundingMode.HALF_UP); } dto.setAvgScore(avg.toPlainString()); } // 5. 转成 Map>> // 第一层 key:科室/病区 // 第二层 key:题目 // 第二层 value:该题目下的所有选项列表 Map>> result = new LinkedHashMap<>(); for (QuestionCountKsOrBqDTO dto : map.values()) { // 分组名 String groupName = byDept ? dto.getDeptname() : dto.getLeavehospitaldistrictname(); if (StringUtils.isEmpty(groupName)) { continue; } // 题目名 String questionText = dto.getQuestionText(); if (StringUtils.isEmpty(questionText)) { continue; } // 第一层:科室/病区 Map> questionMap = result.computeIfAbsent(groupName, k -> new LinkedHashMap<>()); // 第二层:题目 List optionList = questionMap.computeIfAbsent(questionText, k -> new ArrayList<>()); optionList.add(dto); } return result; } public List statQuestionOptionBySubtaskIds(List subtaskIds, Date startOutHospTime, Date endOutHospTime, Date startFinishTime, Date endFinishTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType) { //先清理一下重复数据 List repeatIds = satisfactionMapper.selectRepeatDetailIds(); if (!CollectionUtils.isEmpty(repeatIds)) { satisfactionMapper.deleteRepeatDetailByIds(repeatIds); } List serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOptionBySubtaskIds(subtaskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType); // SQL 已按「题目 + 选项」汇总数量,这里再按题目合并(不同模板的 targetvalue 选项顺序不同,同一选项会拆成多行), // 并把没人选的选项补成 0 行、按各选项数量之和重算比例 return mergeMydOptionRows(serviceSubtaskDetailRatioExports); } /** * 把 SQL 返回的「题目 + 选项」明细合并成「一行一个选项」 *
    *
  • 同一题目下相同 optionresult 的数量累加:原实现遇到同名选项直接 continue, * 每个选项只会保留第一条明细的数量,多任务/多患者的项数全被丢掉,前端「数量」永远只有 1
  • *
  • 选项清单取 targetvalue(用 & 连接),没人选的选项补 0 行,前端固定六列才能渲染
  • *
  • 比例 = 该选项数量 / 该题各选项数量之和,保证一道题各选项比例合计 100%
  • *
* * @param rows SQL 明细 */ private List mergeMydOptionRows(List rows) { List result = new ArrayList<>(); if (CollectionUtils.isEmpty(rows)) { return result; } // key -> 选项 -> 数量,用 LinkedHashMap 保序:选项顺序沿用问卷配置 targetvalue 的顺序 Map> countMap = new LinkedHashMap<>(); Map> scoreMap = new LinkedHashMap<>(); Map templateMap = new LinkedHashMap<>(); for (ServiceSubtaskDetailRatioExport row : rows) { if (row == null) { continue; } String key = row.getQuestiontext(); if (key == null) { continue; } Map counts = countMap.computeIfAbsent(key, k -> new LinkedHashMap<>()); Map 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 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()); } } } for (Map.Entry> entry : countMap.entrySet()) { ServiceSubtaskDetailRatioExport template = templateMap.get(entry.getKey()); Map scores = scoreMap.get(entry.getKey()); long total = 0L; for (long[] count : entry.getValue().values()) { total += count[0]; } for (Map.Entry 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; } /** * 单选/问答题的作答文本:不按 & 拆,避免把问答题的文本答案拆坏 */ private static List singleOption(String answer) { List options = new ArrayList<>(); String text = trimAll(answer); if (StringUtils.isNotEmpty(text)) { options.add(text); } return options; } /** * 标准化问卷答案文本:去掉 A/B/C/D 单字母前缀和后续空格 * 例如 "A 是" -> "是","B 否" -> "否" */ private String normalizeOptionAnswer(String answer) { if (StringUtils.isBlank(answer)) { return ""; } String trimmed = answer.trim(); if (trimmed.length() >= 3 && trimmed.charAt(1) == ' ') { char firstChar = trimmed.charAt(0); if (firstChar >= 'A' && firstChar <= 'Z') { return trimmed.substring(2).trim(); } } return trimmed; } public List mydTrend(MydTrendReqVO mydTrendReqVO) { List list = satisfactionMapper.mydTrend(mydTrendReqVO.getStartFinishTime(), mydTrendReqVO.getEndFinishTime(), mydTrendReqVO.getType()); if (mydTrendReqVO.getType().equals("week")) list = getWeekDate(mydTrendReqVO, list); if (mydTrendReqVO.getType().equals("year")) list = getYearDate(mydTrendReqVO, list); if (mydTrendReqVO.getType().equals("month")) list = getMonthDate(mydTrendReqVO, list); if (mydTrendReqVO.getType().equals("day")) list = getDayDate(mydTrendReqVO, list); return list; } public List sltdMydTotalByDimension(MydTrendReqVO mydTrendReqVO) { List list = null; if (mydTrendReqVO.getDimensionType().equals("1")) list = satisfactionMapper.sltdMydTotalByDimension(mydTrendReqVO.getStartFinishTime(), mydTrendReqVO.getEndFinishTime(), mydTrendReqVO.getType()); else if (mydTrendReqVO.getDimensionType().equals("2")) list = satisfactionMapper.sltdMydTotalByYQ(mydTrendReqVO.getStartFinishTime(), mydTrendReqVO.getEndFinishTime(), mydTrendReqVO.getType()); return list; } /** * 问题结果占比 * * @param sltdMydTotalVO * @param taskIds 任务 id 列表(随访 serviceType=2 分支使用) * @param subtaskIds 子任务 id 列表(满意度 serviceType=6/14 分支使用) * @return */ public List getDetailRatio(SltdMydTotalVO sltdMydTotalVO, List subtaskRefs) { List serviceSubtaskDetailRatioExports = null; List serviceTypeList = sltdMydTotalVO.getServiceTypeList(); if (CollectionUtils.isEmpty(serviceTypeList)) { log.warn("getDetailRatio 未传 serviceTypeList,无法统计问题占比"); return serviceSubtaskDetailRatioExports; } // 判断口径: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 taskIds = subtaskRefs.stream().map(SatisfactionSubtaskRef::getTaskid).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList()); //同时带上本次导出的子任务 id:让「问题详情占比」只统计这批患者,与「患者随访信息表单」口径一致 List subtaskIds = subtaskRefs.stream().map(SatisfactionSubtaskRef::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 subtaskIds = subtaskRefs.stream().map(SatisfactionSubtaskRef::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; } private static String trimAll(String s) { if (s == null) { return ""; } int start = 0; int end = s.length(); while (start < end && isBlankChar(s.charAt(start))) { start++; } while (end > start && isBlankChar(s.charAt(end - 1))) { end--; } return s.substring(start, end); } private String buildKey(String groupName, String questionText, String option) { return groupName + "\u0001" + questionText + "\u0001" + option; } private static int parseCount(String s) { if (StringUtils.isEmpty(s)) { return 0; } try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { return 0; } } private List getWeekDate(MydTrendReqVO mydTrendReqVO, List list) { // 1. 计算总周数(和 SQL 里的公式保持一致) long days = (mydTrendReqVO.getEndFinishTime().getTime() - mydTrendReqVO.getStartFinishTime().getTime()) / (1000L * 60 * 60 * 24); int totalWeeks = (int) (days / 7) + 1; // 2. 已有数据:period -> totalScore Map exist = new HashMap<>(); for (MydTrendReqDTO row : list) { // period 是数字,可能是 Integer / Long / BigDecimal,统一转 int int p = Integer.parseInt(StringUtils.isNotEmpty(row.getPeriod()) ? row.getPeriod() : "0"); exist.put(p, row.getTotalScore() != null ? row.getTotalScore() : "0"); // 下面这行按你的原逻辑保留(虽然看起来有点多余) exist.put(p, String.valueOf(row.getTotalScore() != null ? row.getTotalScore() : 0)); } // 3. 补全 1..totalWeeks List result = new ArrayList<>(totalWeeks); // 获取整体的开始时间 long startTimeMillis = mydTrendReqVO.getStartFinishTime().getTime(); long endTimeMillis = mydTrendReqVO.getEndFinishTime().getTime(); long oneWeekMillis = 7L * 24 * 60 * 60 * 1000; // 一周的毫秒数 for (int i = 1; i <= totalWeeks; i++) { MydTrendReqDTO row = new MydTrendReqDTO(); row.setPeriod(String.valueOf(i)); row.setTotalScore(exist.getOrDefault(i, "0")); // -------- 新增:设置每周的 startDate 和 endDate -------- // 当前周开始时间 = 整体开始时间 + (i - 1) * 7天 long currentStartMillis = startTimeMillis + (long) (i - 1) * oneWeekMillis; // 当前周结束时间 = 当前周开始时间 + 7天 - 1毫秒(保证是这一周的最后一刻) long currentEndMillis = currentStartMillis + oneWeekMillis - 1; // 如果是最后一周,结束时间不能超过整体的 endTime if (i == totalWeeks) { currentEndMillis = endTimeMillis; } row.setStartDate(new Date(currentStartMillis)); row.setEndDate(new Date(currentEndMillis)); // ---------------------------------------------------- result.add(row); } return result; } private List getYearDate(MydTrendReqVO mydTrendReqVO, List list) { // 获取整体开始和结束时间 Date startTime = mydTrendReqVO.getStartFinishTime(); Date endTime = mydTrendReqVO.getEndFinishTime(); // 1. 计算年份范围 // 使用 Calendar 获取起始年份和结束年份 Calendar cal = Calendar.getInstance(); cal.setTime(startTime); int startYear = cal.get(Calendar.YEAR); cal.setTime(endTime); int endYear = cal.get(Calendar.YEAR); // 总年数 (包含首尾) int totalYears = endYear - startYear + 1; // 2. 已有数据:period -> totalScore (假设 period 存的是年份字符串,如 "2024") Map exist = new HashMap<>(); for (MydTrendReqDTO row : list) { String period = row.getPeriod() != null ? row.getPeriod() : ""; exist.put(period, row.getTotalScore() != null ? row.getTotalScore() : "0"); } // 3. 补全所有年份 List result = new ArrayList<>(totalYears); for (int i = 0; i < totalYears; i++) { int currentYear = startYear + i; String periodKey = String.valueOf(currentYear); MydTrendReqDTO row = new MydTrendReqDTO(); row.setPeriod(periodKey); // 如果没有该年份的数据,默认给 "0" row.setTotalScore(exist.getOrDefault(periodKey, "0")); // -------- 设置该年份的 startDate 和 endDate -------- Calendar yearCal = Calendar.getInstance(); // 设置为该年的 1月1日 00:00:00 yearCal.set(currentYear, Calendar.JANUARY, 1, 0, 0, 0); yearCal.set(Calendar.MILLISECOND, 0); row.setStartDate(yearCal.getTime()); // 设置为该年的 12月31日 23:59:59 yearCal.set(currentYear, Calendar.DECEMBER, 31, 23, 59, 59); yearCal.set(Calendar.MILLISECOND, 999); row.setEndDate(yearCal.getTime()); // ---------------------------------------------------- result.add(row); } return result; } private List getMonthDate(MydTrendReqVO mydTrendReqVO, List list) { // 获取整体开始和结束时间 Date startTime = mydTrendReqVO.getStartFinishTime(); Date endTime = mydTrendReqVO.getEndFinishTime(); // 1. 计算月份范围 Calendar cal = Calendar.getInstance(); cal.setTime(startTime); int startYear = cal.get(Calendar.YEAR); int startMonth = cal.get(Calendar.MONTH); // 注意:Calendar.MONTH 从 0 开始 cal.setTime(endTime); int endYear = cal.get(Calendar.YEAR); int endMonth = cal.get(Calendar.MONTH); // 总月数 = (结束年 - 开始年) * 12 + (结束月 - 开始月) + 1 int totalMonths = (endYear - startYear) * 12 + (endMonth - startMonth) + 1; // 2. 已有数据:period -> totalScore (period 存的是月份字符串,如 "2024-06") Map exist = new HashMap<>(); for (MydTrendReqDTO row : list) { String period = row.getPeriod() != null ? row.getPeriod() : ""; exist.put(period, row.getTotalScore() != null ? row.getTotalScore() : "0"); } // 3. 补全所有月份 List result = new ArrayList<>(totalMonths); Calendar monthCal = Calendar.getInstance(); monthCal.set(startYear, startMonth, 1, 0, 0, 0); monthCal.set(Calendar.MILLISECOND, 0); for (int i = 0; i < totalMonths; i++) { int currentYear = monthCal.get(Calendar.YEAR); int currentMonth = monthCal.get(Calendar.MONTH) + 1; // 转成 1-12 月 // 拼接 period,格式:yyyy-MM,比如 "2024-06" // 如果你希望是 "2024-6",可以把 String.format 改成 currentMonth String periodKey = String.format("%04d-%02d", currentYear, currentMonth); MydTrendReqDTO row = new MydTrendReqDTO(); row.setPeriod(periodKey); row.setTotalScore(exist.getOrDefault(periodKey, "0")); // -------- 设置该月的 startDate 和 endDate -------- // 当月开始时间:1号 00:00:00.000 Calendar startCal = (Calendar) monthCal.clone(); row.setStartDate(startCal.getTime()); // 当月结束时间:下个月1号 00:00:00.000 减 1 毫秒 Calendar endCal = (Calendar) monthCal.clone(); endCal.add(Calendar.MONTH, 1); endCal.add(Calendar.MILLISECOND, -1); row.setEndDate(endCal.getTime()); // ---------------------------------------------------- result.add(row); // 进入下一个月 monthCal.add(Calendar.MONTH, 1); } return result; } private List getDayDate(MydTrendReqVO mydTrendReqVO, List list) { // 获取整体开始和结束时间 Date startTime = mydTrendReqVO.getStartFinishTime(); Date endTime = mydTrendReqVO.getEndFinishTime(); // 1. 计算总天数 long oneDayMillis = 24L * 60 * 60 * 1000; // 注意:这里只算日期差,不考虑时分秒,否则可能差一天 Calendar startCal = Calendar.getInstance(); startCal.setTime(startTime); startCal.set(Calendar.HOUR_OF_DAY, 0); startCal.set(Calendar.MINUTE, 0); startCal.set(Calendar.SECOND, 0); startCal.set(Calendar.MILLISECOND, 0); Calendar endCal = Calendar.getInstance(); endCal.setTime(endTime); endCal.set(Calendar.HOUR_OF_DAY, 0); endCal.set(Calendar.MINUTE, 0); endCal.set(Calendar.SECOND, 0); endCal.set(Calendar.MILLISECOND, 0); long days = (endCal.getTimeInMillis() - startCal.getTimeInMillis()) / oneDayMillis; int totalDays = (int) days + 1; // 2. 已有数据:period -> totalScore (period 存的是日期字符串,如 "2024-06-01") Map exist = new HashMap<>(); for (MydTrendReqDTO row : list) { String period = row.getPeriod() != null ? row.getPeriod() : ""; exist.put(period, row.getTotalScore() != null ? row.getTotalScore() : "0"); } // 3. 补全所有日期 List result = new ArrayList<>(totalDays); Calendar dayCal = (Calendar) startCal.clone(); for (int i = 0; i < totalDays; i++) { // 拼接 period,格式:yyyy-MM-dd String periodKey = String.format("%04d-%02d-%02d", dayCal.get(Calendar.YEAR), dayCal.get(Calendar.MONTH) + 1, dayCal.get(Calendar.DAY_OF_MONTH)); MydTrendReqDTO row = new MydTrendReqDTO(); row.setPeriod(periodKey); row.setTotalScore(exist.getOrDefault(periodKey, "0")); // -------- 设置该天的 startDate 和 endDate -------- Calendar startOfDay = (Calendar) dayCal.clone(); row.setStartDate(startOfDay.getTime()); Calendar endOfDay = (Calendar) dayCal.clone(); endOfDay.set(Calendar.HOUR_OF_DAY, 23); endOfDay.set(Calendar.MINUTE, 59); endOfDay.set(Calendar.SECOND, 59); endOfDay.set(Calendar.MILLISECOND, 999); row.setEndDate(endOfDay.getTime()); // ---------------------------------------------------- result.add(row); // 进入下一天 dayCal.add(Calendar.DAY_OF_MONTH, 1); } return result; } public List mzMydScoreRank(MzMydScoreRankVO mzMydScoreRankVO) { if (StringUtils.isEmpty(mzMydScoreRankVO.getDimensionType())) { mzMydScoreRankVO.setDimensionType("1"); } if (CollectionUtils.isEmpty(mzMydScoreRankVO.getServiceTypes())) { List list=new ArrayList<>(Arrays.asList("14")); mzMydScoreRankVO.setServiceTypes(list); } List list = satisfactionMapper.selectMzMydScoreRank(mzMydScoreRankVO); if (CollectionUtils.isEmpty(list)) { return new ArrayList<>(); } // 名次:SQL 已按综合得分降序返回,这里顺序编号 for (int i = 0; i < list.size(); i++) { list.get(i).setRank(i + 1); } return list; } /** * 是否空白字符(含全角空格)。 *

* 迁移说明:原为 ServiceSubtaskServiceImpl 的私有静态方法,满意度方法用到(trimAll),一并搬入。 */ private static Boolean isBlankChar(char c) { return c == '\u3000' // 全角空格 || c == ' ' // 半角空格 || c == '\t' || c == '\n' || c == '\r' || c == '\f' || Character.isWhitespace(c); } /** * 按机构/院区/服务类型/出院时间/科室病区取子任务引用(只含 id、taskid)。 *

* 原实现走 {@code serviceSubtaskService.getServiceSubtasks(ServiceSubtaskEntity)}, * 该方法位于随访主干 Service 且依赖 patArchiveService,不能搬; * 这里用满意度专用查询复现其实际用到的条件口径(逐条见 mapper XML 注释)。 */ @Override public List getMydSubtaskRefs(SatisfactionSubtaskQuery query) { if (query == null) { return new ArrayList<>(); } List refs = satisfactionMapper.selectMydSubtaskRefs(query); return refs == null ? new ArrayList<>() : refs; } // ==================== 满意度分类(供登录链路,带缓存) ==================== /** * 满意度分类缓存 key 前缀(与迁移前一致) */ private static final String SATISFACTION_CACHE_KEY = "satisfaction:categories:"; /** * 满意度分类缓存时长(分钟,与迁移前一致) */ private static final int SATISFACTION_CACHE_TTL = 30; @Autowired private RedisCache redisCache; /** * 获取满意度问题分类(语音 + 问卷),带 Redis 缓存。 *

* 迁移说明:原为 {@code smartor.UserExtInfoServiceImpl.getSatisfactionCategories}, * 因属于满意度逻辑,按模块化要求搬入本模块;缓存 key 与 TTL 保持原值。 */ @Override @SuppressWarnings("unchecked") public Map getSatisfactionCategories(String orgid) { // 先查缓存 String cacheKey = SATISFACTION_CACHE_KEY + orgid; Map cached = redisCache.getCacheObject(cacheKey); if (cached != null) { return cached; } Map satisfactionCategories = new HashMap<>(); // 获取语音问题分类 List ivrLibaScriptAssorts = satisfactionScriptMapper.selectIvrCategoryList(categoryQueryByOrgid(orgid)); List> categoryList = new ArrayList<>(); if (!CollectionUtils.isEmpty(ivrLibaScriptAssorts)) { for (SatisfactionCategoryRow assort : ivrLibaScriptAssorts) { Map map = new HashMap<>(); map.put("categoryid", assort.getId()); map.put("categoryName", assort.getName()); categoryList.add(map); } } satisfactionCategories.put("voiceCategories", categoryList); // 获取问卷问题分类 List svyLibScriptCategories = satisfactionScriptMapper.selectSvyCategoryList(categoryQueryByOrgid(orgid)); List> questionnaireCategorys = new ArrayList<>(); if (!CollectionUtils.isEmpty(svyLibScriptCategories)) { for (SatisfactionCategoryRow scriptCategory : svyLibScriptCategories) { Map map = new HashMap<>(); map.put("categoryid", scriptCategory.getId()); map.put("categoryName", scriptCategory.getName()); questionnaireCategorys.add(map); } } satisfactionCategorysPut(satisfactionCategories, questionnaireCategorys); // 写入缓存 redisCache.setCacheObject(cacheKey, satisfactionCategories, SATISFACTION_CACHE_TTL, TimeUnit.MINUTES); return satisfactionCategories; } /** * 与原文一致:键名为 {@code questionnaireCategorys} */ private void satisfactionCategorysPut(Map target, List> value) { target.put("questionnaireCategorys", value); } /** * 按机构构造满意度分类查询(orgid + type=myd,与原文一致) */ private SatisfactionScriptQuery categoryQueryByOrgid(String orgid) { SatisfactionScriptQuery q = new SatisfactionScriptQuery(); q.setOrgid(orgid); q.setType("myd"); return q; } // ==================== 满意度异常配置题目 ==================== /** * 获取满意度异常配置题目(语音 + 问卷)。 *

* 迁移说明:原实现位于 {@code com.ruoyi.task.service.impl.ServiceSubtaskDetailTraceServiceImpl}, * 现整段搬入本模块。原来的调用链是: *

     * 语音分类:IvrLibaScriptAssortServiceImpl.selectIvrLibaScriptAssortList  ← 服务方法,会把分类【拼成树】,只返回顶层
     * 语音题目:IvrLibaTemplateScriptServiceImpl.selectIvrLibaTemplateScriptList(scriptAssortid=X)  ← 逐个分类调用
     * 问卷分类:SvyLibScriptCategoryMapper.selectSvyLibScriptCategoryList     ← 直接 Mapper,返回【全部】分类
     * 问卷题目:SvyLibTemplateScriptServiceImpl.selectSvyLibTemplateScriptList(categoryid=Y)       ← 逐个分类调用
     * 
* 为不依赖 smartor 的模板/分类实体,这里改成本模块的只读查询复现,并且: *
    *
  • 语音分类:仍然按顶层过滤({@code pid == null || pid == -1}),与原服务方法的结果集一致;
  • *
  • 问卷分类:不过滤顶层,取全部,与原 Mapper 调用一致;
  • *
  • 题目:原文是按分类逐个查询后累加,两条 list statement 都没有 order by, * 所以合并成一次 {@code in (...)} 查询的结果集与逐次查询的并集一致 * (集合相同、顺序本来就未定义);
  • *
  • 分类 id 列表为空时不查(原文就是在 {@code if (isNotEmpty)} 里才查), * 对应本模块 SQL 里"ids 为空则不拼 in 条件"的写法必须靠这个判断兜住, * 否则会退化成"全表查询"。
  • *
*/ @Override public Map getSatisfactionExceptConfigQuestion(SatisfactionExceptConfigQuestionReqVO questionReqVO) { // 初始化结果集 Map result = new HashMap<>(); // ---------------------------------------------------------------- 语音 if (StringUtils.isEmpty(questionReqVO.getScriptType()) || questionReqVO.getScriptType().equals("1")) { List> voicetDataList = new ArrayList<>(); // 获取满意度的分类(type 默认 myd,入参有值则覆盖 —— 与原文一致) List assortIdList = selectIvrTopCategoryIds(questionReqVO.getType()); // 用来记录满意度的所有问题 if (CollectionUtils.isNotEmpty(assortIdList)) { SatisfactionScriptQuery scriptQuery = new SatisfactionScriptQuery(); scriptQuery.setScriptAssortids(assortIdList); List ivrScripts = satisfactionScriptMapper.queryIvrScriptByAssort(scriptQuery); if (CollectionUtils.isNotEmpty(ivrScripts)) { for (SatisfactionIvrScriptRow ivrScript : ivrScripts) { Map voicetData = new HashMap<>(); voicetData.put("questionId", ivrScript.getId()); voicetData.put("libtemplateid", ivrScript.getTemplateid()); voicetData.put("questionContent", ivrScript.getScriptContent()); voicetData.put("questionTopic", ivrScript.getScriptTopic()); voicetData.put("questionType", ivrScript.getScriptType()); voicetData.put("categoryName", ivrScript.getScriptAssortname()); voicetData.put("categoryId", ivrScript.getScriptAssortid()); voicetDataList.add(voicetData); } } } // 语音数据返回集合 result.put("voiceDataList", voicetDataList); } // ---------------------------------------------------------------- 问卷 if (StringUtils.isEmpty(questionReqVO.getScriptType()) || questionReqVO.getScriptType().equals("2")) { List> questionnaireDataList = new ArrayList<>(); // 获取满意度的分类(问卷侧原文直接查 Mapper,取全部分类,不拼树、不筛顶层) List categoryIdList = selectSvyCategoryIds(questionReqVO.getType()); // 用来记录满意度的所有问题 if (CollectionUtils.isNotEmpty(categoryIdList)) { SatisfactionScriptQuery scriptQuery = new SatisfactionScriptQuery(); scriptQuery.setScriptCategoryids(categoryIdList); List svyScripts = satisfactionScriptMapper.querySvyScriptByCategory(scriptQuery); if (CollectionUtils.isNotEmpty(svyScripts)) { for (SatisfactionSvyScriptRow svyScript : svyScripts) { Map svyData = new HashMap<>(); svyData.put("questionId", svyScript.getId()); svyData.put("libtemplateid", svyScript.getSvyid()); svyData.put("questionContent", svyScript.getScriptContent()); svyData.put("questionTopic", svyScript.getScriptTopic()); svyData.put("questionType", svyScript.getScriptType()); svyData.put("categoryName", svyScript.getCategoryName()); svyData.put("categoryId", svyScript.getCategoryid()); questionnaireDataList.add(svyData); } } } // 问卷数据返回集合 result.put("questionnaireDataList", questionnaireDataList); } return result; } /** * 取语音侧满意度分类 id(只取顶层,复现原服务方法拼树后的返回值)。 * * @param type 满意度类型;为空时用 {@code "myd"} */ private List selectIvrTopCategoryIds(String type) { SatisfactionScriptQuery query = new SatisfactionScriptQuery(); query.setType(StringUtils.isEmpty(type) ? "myd" : type); // 原文没有传 orgid,这里保持一致(orgid 为空时 SQL 不拼该条件) List rows = satisfactionScriptMapper.selectIvrCategoryList(query); List ids = new ArrayList<>(); if (CollectionUtils.isNotEmpty(rows)) { for (SatisfactionCategoryRow row : rows) { // 与 IvrLibaScriptAssortServiceImpl 拼树时的顶层判断逐字一致 if (row.getPid() == null || row.getPid() == -1) { ids.add(row.getId()); } } } return ids; } /** * 取问卷侧满意度分类 id(取全部,复现原 Mapper 调用的返回值)。 * * @param type 满意度类型;为空时用 {@code "myd"} */ private List selectSvyCategoryIds(String type) { SatisfactionScriptQuery query = new SatisfactionScriptQuery(); query.setType(StringUtils.isEmpty(type) ? "myd" : type); List rows = satisfactionScriptMapper.selectSvyCategoryList(query); List ids = new ArrayList<>(); if (CollectionUtils.isNotEmpty(rows)) { for (SatisfactionCategoryRow row : rows) { ids.add(row.getId()); } } return ids; } // ==================== 随访「满意度指数」 ==================== /** * 「满意度题目发送量」统计时使用的子任务发送状态(与原实现一致:3、5、6) */ private static final List SATISFACTION_INDEX_SENDSTATES = Collections.unmodifiableList(Arrays.asList(3L, 5L, 6L)); /** * 随访统计页的「满意度指数」:满意度题目总量 / 填报量 / 完成比例。 *

* 迁移说明:原为 {@code com.ruoyi.task.service.impl.ServiceSubtaskServiceImpl#getSatisfactionIndex} * 的私有方法,被 {@code getSfStatisticsJoy} 调用。它的取数对象是 * 「满意度题目」(按 {@code ivrCategoryid} / {@code svyCategoryid} 识别)与 * 「满意度填报明细」,属于满意度口径,故搬入本模块。 *

* 与原实现逐条对应: *

    *
  1. {@code type="1"} 用 {@code deptcode} 匹配任务;{@code type="2"} 用 * {@code leavehospitaldistrictcode} 匹配任务(互斥,都带 orgid);
  2. *
  3. 语音任务:题目数 = 该模板下 {@code script_assortid = ivrCategoryid} 的题目数; * 总量 += 题目数 × 发送量({@code sendstate in (3,5,6)});
  4. *
  5. 问卷任务:同上,用 {@code categoryid = svyCategoryid};
  6. *
  7. 填报量:按 {@code taskid + categoryid(按任务类型取语音/问卷) + orgid} 数明细条数;
  8. *
  9. 完成比例 = 填报量 / 总量(保留 2 位,HALF_UP),总量为 0 时为 {@code 0.0}。
  10. *
* *

⚠️ 原实现已知缺陷(本次原样保留,未修复)

* 原代码用的是 {@code mydtmzlsum.add(new BigDecimal(n))} —— * {@link BigDecimal} 是不可变类,{@code add} 的返回值没有被赋回变量, * 因此所有累加都是无效的,本方法实际上恒定返回 * {@code {mydtmzl: 0, mydtbl: 0, wcbl: 0.0}}; * 同时因为累加语句仍在循环里,每次调用依旧会真实执行那 5 条查询。 *

* 迁移时按"行为不变"原则一字未改(连无效累加也保留): * 若直接把查询结果改成常量,虽然返回值相同,但会改变数据库压力; * 若把 {@code add} 改成 {@code xxx = xxx.add(...)},则会改变随访统计页的显示值。 * 两种都超出本次"模块化、不改行为"的范围,故留待单独确认。 * 详见 {@code doc/满意度模块化_执行进度.md} 的遗留项。 * * @param type "1"=按科室,"2"=按病区(与原文一致) * @param code 科室编码或病区编码 * @param orgid 机构 id * @param ivrCategoryid 语音满意度分类 id(可为 null,为 null 时该条件不生效) * @param svyCategoryid 问卷满意度分类 id(可为 null,为 null 时该条件不生效) * @return 含 {@code mydtmzl}(Integer) / {@code mydtbl}(Integer) / {@code wcbl}(Double) 的 Map */ @Override public Map getSatisfactionIndex(String type, String code, String orgid, Long ivrCategoryid, Long svyCategoryid) { // 原文是 type.equals("1");调用方只传字面量 "1"/"2",这里用常量在前等价且更安全 List serviceTasks = null; if ("1".equals(type)) { serviceTasks = satisfactionMapper.selectSatisfactionServiceTasks(orgid, code, null); } else if ("2".equals(type)) { serviceTasks = satisfactionMapper.selectSatisfactionServiceTasks(orgid, null, code); } // 获取满意度题目总量 BigDecimal mydtmzlsum = new BigDecimal(0); // 获取满意度题目填报量 BigDecimal mydtblsum = new BigDecimal(0); if (CollectionUtils.isNotEmpty(serviceTasks)) { for (SatisfactionTaskRow serviceTask : serviceTasks) { if (serviceTask.getType().equals("1")) { // 语音 Long templateid = serviceTask.getTemplateid(); int scriptCount = satisfactionMapper.countIvrTaskTemplateScripts( templateid, serviceTask.getOrgid(), ivrCategoryid); if (scriptCount > 0) { // 需要先计算出发送量,再乘以单个问券满意度题目数量,得到满意度题目总量(包含了未做的) int subtaskCount = satisfactionMapper.countSubtaskBySendstates( serviceTask.getTaskid(), SATISFACTION_INDEX_SENDSTATES); if (subtaskCount > 0) { int mydtmzlCount = scriptCount * subtaskCount; // ⚠️ 原实现缺陷:add 的返回值被丢弃(BigDecimal 不可变),此处原样保留 mydtmzlsum.add(new BigDecimal(mydtmzlCount)); } } } else if (serviceTask.getType().equals("2")) { // 问卷 int scriptCount = satisfactionMapper.countSvyTaskTemplateScripts( serviceTask.getTemplateid(), serviceTask.getOrgid(), svyCategoryid); if (scriptCount > 0) { int subtaskCount = satisfactionMapper.countSubtaskBySendstates( serviceTask.getTaskid(), SATISFACTION_INDEX_SENDSTATES); if (subtaskCount > 0) { int mydtmzlCount = scriptCount * subtaskCount; // ⚠️ 原实现缺陷:同上,原样保留 mydtmzlsum.add(new BigDecimal(mydtmzlCount)); } } } // 获取满意度题目填报量 Long categoryid = null; if (serviceTask.getType().equals("1")) categoryid = ivrCategoryid; if (serviceTask.getType().equals("2")) categoryid = svyCategoryid; int detailCount = satisfactionMapper.countSubtaskDetail( serviceTask.getTaskid(), categoryid, serviceTask.getOrgid()); if (detailCount > 0) { // ⚠️ 原实现缺陷:同上,原样保留 mydtblsum.add(new BigDecimal(detailCount)); } } } Map map = new HashMap<>(); // 满意度题目总量 map.put("mydtmzl", mydtmzlsum.intValue()); map.put("mydtbl", mydtblsum.intValue()); map.put("wcbl", 0.0); if (mydtmzlsum.intValue() != 0) { map.put("wcbl", mydtblsum.divide(mydtmzlsum, 2, RoundingMode.HALF_UP).doubleValue()); } return map; } }