package com.smartor.service.impl; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.stream.Collectors; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUserDept; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.utils.Arith; import com.ruoyi.common.utils.ArrayListUtils; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DtoConversionUtils; import com.smartor.domain.*; import com.smartor.domain.DTO.ServiceSubtaskDetailDTO; import com.smartor.domain.VO.PatSatisfactionReqVO; import com.smartor.domain.VO.PatSatisfactionResVO; import com.smartor.domain.entity.PatSatisfactionDetailEntity; import com.smartor.domain.entity.SatisfactionStatQuery; import com.smartor.domain.entity.ServiceSubtaskSatisfactionEntity; import com.smartor.mapper.*; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.smartor.service.IPatSatisfactionService; import static com.ruoyi.common.utils.SecurityUtils.getLoginUser; /** * 患者满意度Service业务层处理 * * @author lihu * @date 2025-06-30 */ @Service public class PatSatisfactionServiceImpl implements IPatSatisfactionService { @Autowired private PatSatisfactionMapper patSatisfactionMapper; @Autowired private ServiceSubtaskMapper serviceSubtaskMapper; @Autowired private ServiceSubtaskDetailMapper serviceSubtaskDetailMapper; @Autowired private IvrLibaTemplateScriptMapper ivrLibaTemplateScriptMapper; @Autowired private IvrLibaScriptAssortMapper ivrLibaScriptAssortMapper; @Autowired private SvyLibScriptCategoryMapper svyLibScriptCategoryMapper; @Autowired private IvrLibaTemplateTargetoptionMapper ivrLibaTemplateTargetoptionMapper; @Autowired private SvyLibTemplateTargetoptionMapper svyLibTemplateTargetoptionMapper; @Autowired private SvyLibTemplateScriptMapper svyLibTemplateScriptMapper; /** * 查询患者满意度 * * @param id 患者满意度主键 * @return 患者满意度 */ @Override public PatSatisfaction selectPatSatisfactionById(Long id) { PatSatisfaction ps = patSatisfactionMapper.selectPatSatisfactionById(id); double total = ps.getDoctorSatisfaction() + ps.getAuthenticity() + ps.getWeekFinish() + ps.getStandard() + ps.getTimeliness() + ps.getLibrary() + ps.getEnvironment() + ps.getNurseSatisfaction(); ps.setTotal(total); return ps; } /** * 查询患者满意度列表 * * @param patSatisfaction 患者满意度 * @return 患者满意度 */ @Override public List selectPatSatisfactionList(PatSatisfaction patSatisfaction) { List patSatisfactions = patSatisfactionMapper.selectPatSatisfactionList(patSatisfaction); if (CollectionUtils.isNotEmpty(patSatisfactions)) { patSatisfactions.forEach(ps -> { double total = ps.getDoctorSatisfaction() + ps.getAuthenticity() + ps.getWeekFinish() + ps.getStandard() + ps.getTimeliness() + ps.getLibrary() + ps.getEnvironment() + ps.getNurseSatisfaction(); ps.setTotal(total); }); } return patSatisfactions; } /** * 新增患者满意度 * * @param patSatisfaction 患者满意度 * @return 结果 */ @Override public int insertPatSatisfaction(PatSatisfaction patSatisfaction) { patSatisfaction.setCreateTime(DateUtils.getNowDate()); patSatisfaction.setUpdateTime(DateUtils.getNowDate()); return patSatisfactionMapper.insertPatSatisfaction(patSatisfaction); } /** * 新增患者满意度 * * @param patSatisfactions 患者满意度 * @return 结果 */ @Override public int batchInsert(List patSatisfactions) { if(CollectionUtils.isEmpty(patSatisfactions)){ return 0; } List ids = patSatisfactions.stream().map(PatSatisfaction::getId). distinct().collect(Collectors.toList()); List patSatisfactionsOld = new ArrayList<>(); PatSatisfaction patSatisfactionVo = new PatSatisfaction(); patSatisfactionVo.setIds(ids); patSatisfactionsOld = patSatisfactionMapper.selectPatSatisfactionList(patSatisfactionVo); Map> maps = ArrayListUtils.splitListsById(patSatisfactions, patSatisfactionsOld, PatSatisfaction::getId); //只在patSatisfactions的数据,执行新增操作 List addItems = maps.get("onlyIn1"); //只在patSatisfactionsOld的数据,执行删除操作 // List delItems = maps.get("onlyIn2"); //重叠的数据,执行更新操作 List editItems = maps.get("common"); Integer count = 0; LoginUser loginUser = getLoginUser(); SysUser user = loginUser.getUser(); for (PatSatisfaction patSatisfaction0 : editItems) { patSatisfaction0.setUpdateBy(user.getNickName()); patSatisfaction0.setUpdateTime(DateUtils.getNowDate()); count = count + patSatisfactionMapper.updatePatSatisfaction(patSatisfaction0); } for (PatSatisfaction patSatisfaction1 : addItems) { patSatisfaction1.setCreateBy(user.getNickName()); patSatisfaction1.setOrgid(user.getOrgid()); patSatisfaction1.setCreateTime(DateUtils.getNowDate()); patSatisfaction1.setUpdateTime(DateUtils.getNowDate()); count = count + patSatisfactionMapper.insertPatSatisfaction(patSatisfaction1); } return count; } /** * 修改患者满意度 * * @param patSatisfaction 患者满意度 * @return 结果 */ @Override public int updatePatSatisfaction(PatSatisfaction patSatisfaction) { patSatisfaction.setUpdateTime(DateUtils.getNowDate()); return patSatisfactionMapper.updatePatSatisfaction(patSatisfaction); } /** * 批量删除患者满意度 * * @param ids 需要删除的患者满意度主键 * @return 结果 */ @Override public int deletePatSatisfactionByIds(Long[] ids) { return patSatisfactionMapper.deletePatSatisfactionByIds(ids); } /** * 删除患者满意度信息 * * @param id 患者满意度主键 * @return 结果 */ @Override public int deletePatSatisfactionById(Long id) { return patSatisfactionMapper.deletePatSatisfactionById(id); } /** * 患者满意度统计 * * @param patSatisfactionReqVO 患者满意度统计条件 * @return 患者满意度统计结果 */ @Override public PatSatisfactionResVO statistics(PatSatisfactionReqVO patSatisfactionReqVO) { //定义返参 PatSatisfactionResVO result = new PatSatisfactionResVO(); //患者满意度统详情 List patSatisfactionDetailEntities = new ArrayList<>(); // [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的) int[] personTotals = new int[2]; // 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空) List types = serviceSubtaskMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO); boolean hasVoice = types != null && types.contains("1"); boolean hasQuestionnaire = types != null && types.contains("2"); if (hasVoice) { //1. 先获取ivr_liba_script_assort的满意度分类ID IvrLibaScriptAssort ivrLibaScriptAssort = new IvrLibaScriptAssort(); ivrLibaScriptAssort.setOrgid(patSatisfactionReqVO.getOrgid()); List ivrLibaScriptAssorts = ivrLibaScriptAssortMapper.selectIvrLibaScriptAssortList(ivrLibaScriptAssort); List mydIdList = ivrLibaScriptAssorts.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(IvrLibaScriptAssort::getId).collect(Collectors.toList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.queryLibTemplateIds(mydIdList); //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) List scriptRefs = new ArrayList<>(); for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScripts) { scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent())); } buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals); } if (hasQuestionnaire) { //1. 先获取svy_lib_script_category的满意度分类ID SvyLibScriptCategory svyLibScriptCategory = new SvyLibScriptCategory(); svyLibScriptCategory.setOrgid(patSatisfactionReqVO.getOrgid()); List svyLibScriptCategories = svyLibScriptCategoryMapper.selectSvyLibScriptCategoryList(svyLibScriptCategory); List mydIdList = svyLibScriptCategories.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SvyLibScriptCategory::getId).collect(Collectors.toList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List svyLibTemplateScripts = svyLibTemplateScriptMapper.querySvyLibTemplateIds(mydIdList); //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) List scriptRefs = new ArrayList<>(); for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScripts) { // 问卷的选项表按 svy_lib_template_script 的主键关联 scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent())); } buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals); } result.setTotalAnswerRate(Arith.div(personTotals[0], personTotals[1])); result.setPatSatisfactionDetailEntities(patSatisfactionDetailEntities); result.setTotalPerson(personTotals[0]); return result; } /** * 患者满意度统计(维度) * * @param patSatisfactionReqVO 患者满意度统计条件 * @return 患者满意度统计结果 */ @Override public PatSatisfactionResVO statisticsByDimension(PatSatisfactionReqVO patSatisfactionReqVO) { //定义返参 PatSatisfactionResVO result = new PatSatisfactionResVO(); //患者满意度统详情 List patSatisfactionDetailEntities = new ArrayList<>(); // [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的) int[] personTotals = new int[2]; // 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空) List types = serviceSubtaskMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO); boolean hasVoice = types != null && types.contains("1"); boolean hasQuestionnaire = types != null && types.contains("2"); if (hasVoice) { //1. 先获取入参中的维护集合 IvrLibaTemplateScript iltscript = new IvrLibaTemplateScript(); iltscript.setDimensionList(patSatisfactionReqVO.getDimensionList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.selectILTSBydimension(iltscript); if (CollectionUtils.isNotEmpty(ivrLibaTemplateScripts)) { //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) List scriptRefs = new ArrayList<>(); for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScripts) { scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent())); } buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals); } } if (hasQuestionnaire) { //1. 先获取入参中的满意度维度 SvyLibTemplateScriptVO svyLibTemplateScriptVO = new SvyLibTemplateScriptVO(); svyLibTemplateScriptVO.setDimensionList(patSatisfactionReqVO.getDimensionList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List svyLibTemplateScripts = svyLibTemplateScriptMapper.selectSLTScriptListBydimension(svyLibTemplateScriptVO); if (CollectionUtils.isNotEmpty(svyLibTemplateScripts)) { //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) List scriptRefs = new ArrayList<>(); for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScripts) { // 问卷的选项表按 svy_lib_template_script 的主键关联 scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent())); } buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals); } } result.setTotalAnswerRate(Arith.div(personTotals[0], personTotals[1])); result.setPatSatisfactionDetailEntities(patSatisfactionDetailEntities); result.setTotalPerson(personTotals[0]); return result; } @Override public Map satisfactionGraph(PatSatisfactionReqVO patSatisfactionReqVO) { //用来记录柱状图信息(key=满意度分类名称,value=满意度分类下的总人数和填报人数) Map zzt = new HashMap<>(); // 记录一下每种分类的填报情况(只记分值与条数,不再把明细行全部查回 Java) Map> assortNameFillCountMap = new HashMap<>(); if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) { //1. 先获取ivr_liba_script_assort的满意度分类ID IvrLibaScriptAssort ivrLibaScriptAssort = new IvrLibaScriptAssort(); ivrLibaScriptAssort.setOrgid(patSatisfactionReqVO.getOrgid()); List ivrLibaScriptAssorts = ivrLibaScriptAssortMapper.selectIvrLibaScriptAssortList(ivrLibaScriptAssort); List mydIdList = ivrLibaScriptAssorts.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(IvrLibaScriptAssort::getId).collect(Collectors.toList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.queryLibTemplateIds(mydIdList); //2.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次) SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1", ivrLibaTemplateScripts.stream().map(IvrLibaTemplateScript::getTemplateid).collect(Collectors.toList())); Map sendCountMap = mapSatisfactionSendCount(statQuery); SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery); //3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数 Map> scriptGroupByAssortName = ivrLibaTemplateScripts.stream().filter(item -> item.getScriptAssortname() != null).collect(Collectors.groupingBy(IvrLibaTemplateScript::getScriptAssortname)); if (scriptGroupByAssortName != null && !scriptGroupByAssortName.isEmpty()) { for (String assortNameKey : scriptGroupByAssortName.keySet()) { //记录总人数 Double subidAll = 0.0; //记录总的填报人数 Double fillCountAll = 0.0; //获取该分类的填报总数量 List allFillCountList = new ArrayList<>(); List ivrLibaTemplateScriptList = scriptGroupByAssortName.get(assortNameKey); for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScriptList) { //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size) subidAll += sendCountMap.getOrDefault(ivrLibaTemplateScript.getTemplateid(), 0L); //获取填报数量(原来是把明细行全部查回 Java 再取 size) List fillCount = fillStat.get(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptContent()); if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount); fillCountAll += countFillRows(fillCount); } // 这里先记录一下每个分类的总人数和填报人数 Map personCount = new HashMap<>(); personCount.put("fillCountAll", fillCountAll); personCount.put("subidAll", subidAll); personCount.put("receiveRate", Arith.div(fillCountAll, subidAll)); personCount.put("averageScore", averageScoreByStat(allFillCountList)); zzt.put(assortNameKey, personCount); assortNameFillCountMap.put(assortNameKey, allFillCountList); } } } if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 2) { //1. 先获取svy_lib_script_category的满意度分类ID SvyLibScriptCategory svyLibScriptCategory = new SvyLibScriptCategory(); svyLibScriptCategory.setOrgid(patSatisfactionReqVO.getOrgid()); List svyLibScriptCategories = svyLibScriptCategoryMapper.selectSvyLibScriptCategoryList(svyLibScriptCategory); List mydIdList = svyLibScriptCategories.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SvyLibScriptCategory::getId).collect(Collectors.toList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List svyLibTemplateScripts = svyLibTemplateScriptMapper.querySvyLibTemplateIds(mydIdList); //2.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次) SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2", svyLibTemplateScripts.stream().map(SvyLibTemplateScript::getSvyid).collect(Collectors.toList())); Map sendCountMap = mapSatisfactionSendCount(statQuery); SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery); //3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数 Map> scriptGroupByatCegoryName = svyLibTemplateScripts.stream().filter(item -> item.getCategoryName() != null).collect(Collectors.groupingBy(SvyLibTemplateScript::getCategoryName)); if (scriptGroupByatCegoryName != null && !scriptGroupByatCegoryName.isEmpty()) { for (String categoryNameKey : scriptGroupByatCegoryName.keySet()) { //记录总人数 Double subidAll = 0.0; //记录总的填报人数 Double fillCountAll = 0.0; //获取该分类的填报总数量 List allFillCountList = new ArrayList<>(); List svyLibTemplateScriptList = scriptGroupByatCegoryName.get(categoryNameKey); for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScriptList) { //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size) subidAll += sendCountMap.getOrDefault(svyLibTemplateScript.getSvyid(), 0L); //获取填报数量(原来是把明细行全部查回 Java 再取 size) List fillCount = fillStat.get(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getScriptContent()); if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount); fillCountAll += countFillRows(fillCount); } // 这里先记录一下每个分类的总人数和填报人数 Map personCount = (Map) zzt.get(categoryNameKey); if (!Objects.isNull(personCount)) { Double fillCountAll2 = (Double) personCount.get("fillCountAll"); Double subidAll2 = (Double) personCount.get("subidAll"); personCount.put("fillCountAll", fillCountAll2 + fillCountAll); personCount.put("subidAll", subidAll2 + subidAll); personCount.put("receiveRate", Arith.div((Double) personCount.get("fillCountAll"), (Double) personCount.get("subidAll"))); //看一下之前有没有该分类的填报量 List serviceSubtaskDetailDTOS = assortNameFillCountMap.get(categoryNameKey); if (CollectionUtils.isNotEmpty(serviceSubtaskDetailDTOS)) allFillCountList.addAll(serviceSubtaskDetailDTOS); personCount.put("averageScore", averageScoreByStat(allFillCountList)); } else { personCount = new HashMap<>(); personCount.put("fillCountAll", fillCountAll); personCount.put("subidAll", subidAll); personCount.put("receiveRate", Arith.div(fillCountAll, subidAll)); personCount.put("averageScore", averageScoreByStat(allFillCountList)); zzt.put(categoryNameKey, personCount); } } } } return zzt; } @Override public Map satisfactionGraphDimension(PatSatisfactionReqVO patSatisfactionReqVO) { //用来记录柱状图信息(key=满意度维度名称,value=该维度下的总人数和填报人数) Map zzt = new HashMap<>(); // 记录一下每个维度的填报情况(只记分值与条数,不再把明细行全部查回 Java) Map> assortNameFillCountMap = new HashMap<>(); if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) { //1. 从入参中获取满意度维维度 IvrLibaTemplateScript ivrLibaTemplateScript2 = new IvrLibaTemplateScript(); ivrLibaTemplateScript2.setDimensionList(patSatisfactionReqVO.getDimensionList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List ivrLibaTemplateScripts = ivrLibaTemplateScriptMapper.selectILTSBydimension(ivrLibaTemplateScript2); if (ivrLibaTemplateScripts != null && !ivrLibaTemplateScripts.isEmpty()) { //3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数 List dimensionScripts = ivrLibaTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList()); Map> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(IvrLibaTemplateScript::getDimension)); if (groupByDimension != null && !groupByDimension.isEmpty()) { //3.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次) SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1", dimensionScripts.stream().map(IvrLibaTemplateScript::getTemplateid).collect(Collectors.toList())); Map sendCountMap = mapSatisfactionSendCount(statQuery); SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery); for (String dimension : groupByDimension.keySet()) { //记录总人数 Double subidAll = 0.0; //记录总的填报人数 Double fillCountAll = 0.0; //获取该维度的填报总数量 List allFillCountList = new ArrayList<>(); List ivrLibaTemplateScriptList = groupByDimension.get(dimension); for (IvrLibaTemplateScript ivrLibaTemplateScript : ivrLibaTemplateScriptList) { //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size) subidAll += sendCountMap.getOrDefault(ivrLibaTemplateScript.getTemplateid(), 0L); //获取填报数量(原来是把明细行全部查回 Java 再取 size) List fillCount = fillStat.get(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptContent()); if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount); fillCountAll += countFillRows(fillCount); } // 这里先记录一下每个分类的总人数和填报人数 Map personCount = new HashMap<>(); personCount.put("fillCountAll", fillCountAll); personCount.put("subidAll", subidAll); personCount.put("receiveRate", Arith.div(fillCountAll, subidAll)); personCount.put("averageScore", averageScoreByStat(allFillCountList)); zzt.put(dimension, personCount); assortNameFillCountMap.put(dimension, allFillCountList); } } } } if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 2) { //1. 从入参中获取满意度维维度 SvyLibTemplateScriptVO svyLibTemplateScriptVO = new SvyLibTemplateScriptVO(); svyLibTemplateScriptVO.setDimensionList(patSatisfactionReqVO.getDimensionList()); //2 再获取该问题满意度分类下的IvrLibaTemplateScript的集合 List svyLibTemplateScripts = svyLibTemplateScriptMapper.selectSLTScriptListBydimension(svyLibTemplateScriptVO); if (svyLibTemplateScripts != null && !svyLibTemplateScripts.isEmpty()) { //3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数 List dimensionScripts = svyLibTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList()); Map> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(SvyLibTemplateScript::getDimension)); if (groupByDimension != null && !groupByDimension.isEmpty()) { //3.1 一次性把本次请求全部模板的发送量、填报量查回来(原来是每个模板问题各查两次) SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2", dimensionScripts.stream().map(SvyLibTemplateScript::getSvyid).collect(Collectors.toList())); Map sendCountMap = mapSatisfactionSendCount(statQuery); SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery); for (String dimension : groupByDimension.keySet()) { //记录总人数 Double subidAll = 0.0; //记录总的填报人数 Double fillCountAll = 0.0; //获取该分类的填报总数量 List allFillCountList = new ArrayList<>(); List svyLibTemplateScriptList = groupByDimension.get(dimension); for (SvyLibTemplateScript svyLibTemplateScript : svyLibTemplateScriptList) { //通过模板id取问题发送总量(原来是把全部 sub_id 查回 Java 再取 size) subidAll += sendCountMap.getOrDefault(svyLibTemplateScript.getSvyid(), 0L); //获取填报数量(原来是把明细行全部查回 Java 再取 size) List fillCount = fillStat.get(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getScriptContent()); if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount); fillCountAll += countFillRows(fillCount); } // 这里先记录一下每个分类的总人数和填报人数 Map personCount = (Map) zzt.get(dimension); if (!Objects.isNull(personCount)) { Double fillCountAll2 = (Double) personCount.get("fillCountAll"); Double subidAll2 = (Double) personCount.get("subidAll"); personCount.put("fillCountAll", fillCountAll2 + fillCountAll); personCount.put("subidAll", subidAll2 + subidAll); personCount.put("receiveRate", Arith.div((Double) personCount.get("fillCountAll"), (Double) personCount.get("subidAll"))); //看一下之前有没有该分类的填报量 List serviceSubtaskDetailDTOS = assortNameFillCountMap.get(dimension); if (CollectionUtils.isNotEmpty(serviceSubtaskDetailDTOS)) allFillCountList.addAll(serviceSubtaskDetailDTOS); personCount.put("averageScore", averageScoreByStat(allFillCountList)); } else { personCount = new HashMap<>(); personCount.put("fillCountAll", fillCountAll); personCount.put("subidAll", subidAll); personCount.put("receiveRate", Arith.div(fillCountAll, subidAll)); personCount.put("averageScore", averageScoreByStat(allFillCountList)); zzt.put(dimension, personCount); } } } } } return zzt; } /** * 获取最高分 * * @param options * @return */ private Double maxScore(List options) { if (CollectionUtils.isEmpty(options)) return 0.0; Double maxScore = options.stream() .map(IvrLibaTemplateTargetoption::getScore) .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty()) .mapToDouble(Double::parseDouble) .max() .orElse(0.0); return maxScore; } /** * 获取最低分 * * @param options * @return */ private Double minScore(List options) { if (CollectionUtils.isEmpty(options)) return 0.0; Double minScore = options.stream() .map(IvrLibaTemplateTargetoption::getScore) .filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty()) .mapToDouble(Double::parseDouble) .min() .orElse(0.0); return minScore; } /** * 获取平均分 * * @param fillCount * @return */ private Double averageScore(List fillCount) { if (CollectionUtils.isEmpty(fillCount)) return 0.0; double sum = fillCount.stream().filter(dto -> dto.getScore() != null && !dto.getScore().isEmpty()).mapToDouble(dto -> Double.parseDouble(dto.getScore())).sum(); return new BigDecimal(sum / fillCount.size()).setScale(1, RoundingMode.HALF_UP).doubleValue(); } // ==================== satisfactionGraph 聚合统计(替代逐个模板查库) ==================== /** * 构建聚合统计入参(取值来源与原 DtoConversionUtils.sourceToTarget(patSatisfactionReqVO, ServiceSubtaskSatisfactionEntity.class) 一致) * * @param reqVO 原始请求 * @param type service_subtask.type(1 语音、2 问卷),用字符串比较避免隐式转换 * @param templateIds 本次请求涉及的模板id(ivr 用 templateid,问卷用 svyid) */ private SatisfactionStatQuery buildSatisfactionStatQuery(PatSatisfactionReqVO reqVO, String type, List templateIds) { SatisfactionStatQuery statQuery = new SatisfactionStatQuery(); statQuery.setOrgid(reqVO.getOrgid()); statQuery.setCampusid(reqVO.getCampusid()); statQuery.setType(type); statQuery.setStartTime(reqVO.getStartTime()); statQuery.setEndTime(reqVO.getEndTime()); statQuery.setDeptcodes(reqVO.getDeptcodes()); statQuery.setHospitaldistrictcodes(reqVO.getHospitaldistrictcodes()); statQuery.setLibTemplateIds(CollectionUtils.isEmpty(templateIds) ? templateIds : templateIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList())); return statQuery; } /** * 一次查出全部模板的发送量 * * @return key=模板id,value=发送量(等价于原 querySendCount 结果的 size) */ private Map mapSatisfactionSendCount(SatisfactionStatQuery statQuery) { Map sendCountMap = new HashMap<>(); if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return sendCountMap; List> rows = serviceSubtaskMapper.selectSatisfactionSendCount(statQuery); if (CollectionUtils.isEmpty(rows)) return sendCountMap; for (Map row : rows) { Long templateId = toLong(row.get("libtemplateid")); if (templateId == null) continue; sendCountMap.put(templateId, toLong(row.get("cnt"), 0L)); } return sendCountMap; } /** * 一次查出全部模板的填报量(按 模板+问题+分值 分组) */ private SatisfactionFillStat querySatisfactionFillStat(SatisfactionStatQuery statQuery) { SatisfactionFillStat fillStat = new SatisfactionFillStat(); if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat; List> rows = serviceSubtaskDetailMapper.selectSatisfactionFillStat(statQuery); if (CollectionUtils.isEmpty(rows)) return fillStat; for (Map row : rows) { Long templateId = toLong(row.get("libtemplateid")); if (templateId == null) continue; long cnt = toLong(row.get("cnt"), 0L); if (cnt <= 0L) continue; Object scoreValue = row.get("score"); Object questiontextValue = row.get("questiontext"); ScoreCount scoreCount = new ScoreCount(scoreValue == null ? null : String.valueOf(scoreValue), cnt); fillStat.add(templateId, questiontextValue == null ? null : String.valueOf(questiontextValue), scoreCount); } return fillStat; } /** * 取填报明细行数(原 fillCount.size()) */ private long countFillRows(List fillCount) { if (CollectionUtils.isEmpty(fillCount)) return 0L; long rows = 0L; for (ScoreCount scoreCount : fillCount) { rows += scoreCount.getCount(); } return rows; } /** * 获取平均分(聚合版:分值与条数) *

* 口径与 {@link #averageScore(List)} 完全一致:分子为可解析分值之和,分母为明细行数(包含分值为空的行)。 * 注意不能改成 sql 的 avg() 或「有分值的行数」做分母,否则返回的 averageScore 会变。 */ private Double averageScoreByStat(List fillCount) { if (CollectionUtils.isEmpty(fillCount)) return 0.0; double sum = 0.0; long rows = 0L; for (ScoreCount scoreCount : fillCount) { if (scoreCount.getCount() <= 0L) continue; rows += scoreCount.getCount(); if (scoreCount.getScore() != null && !scoreCount.getScore().isEmpty()) { sum += Double.parseDouble(scoreCount.getScore()) * scoreCount.getCount(); } } if (rows == 0L) return 0.0; return new BigDecimal(sum / rows).setScale(1, RoundingMode.HALF_UP).doubleValue(); } /** * 分值分组键:模板id + 分隔符 + 规范化后的问题内容 *

* MySQL 里 questiontext = #{questiontext} 是「忽略大小写、忽略尾部空格」的比较, * 这里用 trim + 小写做等价匹配。 */ private static String statKey(Long templateId, String questiontext) { return templateId + "\u0001" + (questiontext == null ? "" : questiontext.trim().toLowerCase(Locale.ROOT)); } private static Long toLong(Object value) { if (value == null) return null; if (value instanceof Number) return ((Number) value).longValue(); String text = String.valueOf(value).trim(); return text.isEmpty() ? null : Long.valueOf(text); } private static long toLong(Object value, long defaultValue) { Long result = toLong(value); return result == null ? defaultValue : result; } /** * 满意度分值聚合结果:分值 + 该分值对应的明细行数 */ private static class ScoreCount { private final String score; private final long count; ScoreCount(String score, long count) { this.score = score; this.count = count; } String getScore() { return score; } long getCount() { return count; } } /** * 满意度填报量聚合结果 *

* byTemplateQuestion:按 模板+问题 分组; * byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在 * script_content 为 null 时不拼 questiontext 条件的行为。 */ private static class SatisfactionFillStat { private final Map> byTemplateQuestion = new HashMap<>(); private final Map> byTemplate = new HashMap<>(); void add(Long templateId, String questiontext, ScoreCount scoreCount) { byTemplate.computeIfAbsent(templateId, key -> new ArrayList<>()).add(scoreCount); if (questiontext != null) { byTemplateQuestion.computeIfAbsent(statKey(templateId, questiontext), key -> new ArrayList<>()).add(scoreCount); } } List get(Long templateId, String scriptContent) { if (templateId == null) return null; if (scriptContent == null) return byTemplate.get(templateId); return byTemplateQuestion.get(statKey(templateId, scriptContent)); } } // ==================== statistics / statisticsByDimension 批量统计(替代逐个问题查库) ==================== /** * 满意度明细统计(statistics、statisticsByDimension 共用) *

* 把原来「每个问题 3 条 SQL」改成「整个请求 3 条查询 + 内存组装」: * ① 发送量按 libtemplateid 分组;② 填报量按 模板+问题+matchedtext+score 分组;③ 选项分值按 scriptid 分组。 * * @param reqVO 原始请求 * @param type service_subtask.type(1 语音、2 问卷) * @param voice true=语音问题(选项分值取 ivr_liba_template_targetoption) * @param scriptRefs 本次要统计的问题(顺序即返回顺序) * @param out 明细结果 * @param totals [0]=totalPerson, [1]=allPerson */ private void buildSatisfactionDetailEntities(PatSatisfactionReqVO reqVO, String type, boolean voice, List scriptRefs, List out, int[] totals) { if (CollectionUtils.isEmpty(scriptRefs)) return; List templateIds = scriptRefs.stream().map(SatScriptRef::getTemplateId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(reqVO, type, templateIds); Map sendCountMap = mapSatisfactionSendCount(statQuery); SatisfactionFillStatByMatchedtext fillStat = querySatisfactionFillStatByMatchedtext(statQuery); Map> optionScores = loadTargetOptionScores(voice, scriptRefs); for (SatScriptRef scriptRef : scriptRefs) { Long templateId = scriptRef.getTemplateId(); //发送总量(原来是把全部 sub_id 查回 Java 再取 size) long sendCount = templateId == null ? 0L : sendCountMap.getOrDefault(templateId, 0L); //填报数量(原来是把明细行全部查回 Java 再统计) List fillRows = fillStat.get(templateId, scriptRef.getScriptContent()); long fillCount = countFillRowsByMatchedtext(fillRows); PatSatisfactionDetailEntity detail = new PatSatisfactionDetailEntity(); detail.setScriptContent(scriptRef.getScriptContent()); detail.setAnswerPerson((int) fillCount); detail.setNoAnswerPerson(fillCount > 0L && sendCount > 0L ? (int) (sendCount - fillCount) : sendCount > 0L ? (int) sendCount : 0); detail.setAverageScore(String.valueOf(averageScoreByMatchedtext(fillRows))); List scores = optionScores.get(scriptRef.getOptionScriptId()); detail.setMaxScore(String.valueOf(maxScoreOf(scores))); detail.setMinScore(String.valueOf(minScoreOf(scores))); detail.setAnswerRate(sendCount == 0L ? 0 : fillCount == 0L ? 0 : fillCount * 1.0 / sendCount); detail.setMatchedtextStats(buildMatchedtextStats(fillRows, fillCount)); out.add(detail); totals[0] += (int) fillCount; totals[1] += (int) sendCount; } } /** * 一次查出全部问题的填报量(按 模板+问题+matchedtext+score 分组,含 targetvalue) */ private SatisfactionFillStatByMatchedtext querySatisfactionFillStatByMatchedtext(SatisfactionStatQuery statQuery) { SatisfactionFillStatByMatchedtext fillStat = new SatisfactionFillStatByMatchedtext(); if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat; List> rows = serviceSubtaskDetailMapper.selectSatisfactionFillStatByMatchedtext(statQuery); if (CollectionUtils.isEmpty(rows)) return fillStat; for (Map row : rows) { Long templateId = toLong(row.get("libtemplateid")); if (templateId == null) continue; long cnt = toLong(row.get("cnt"), 0L); if (cnt <= 0L) continue; Object questiontext = row.get("questiontext"); Object matchedtext = row.get("matchedtext"); Object score = row.get("score"); Object targetvalue = row.get("targetvalue"); fillStat.add(templateId, questiontext == null ? null : String.valueOf(questiontext), new MatchedtextCount(matchedtext == null ? null : String.valueOf(matchedtext), score == null ? null : String.valueOf(score), targetvalue == null ? null : String.valueOf(targetvalue), cnt)); } return fillStat; } /** * 一次查出全部问题的选项分值(只取 scriptid、score 两列) *

* 原实现是每个问题查一次整行,而调用方只用到 score 算最高/最低分; * 问题 id 为 null 时原实现不拼 scriptid 条件(等于取该表全部分值),这里用 null 作为 key 存这一份。 */ private Map> loadTargetOptionScores(boolean voice, List scriptRefs) { Map> optionScores = new HashMap<>(); List scriptIds = scriptRefs.stream().map(SatScriptRef::getOptionScriptId) .filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(scriptIds)) { addTargetOptionScores(optionScores, voice ? ivrLibaTemplateTargetoptionMapper.selectIvrLibaTargetoptionScores(scriptIds) : svyLibTemplateTargetoptionMapper.selectSvyLibTargetoptionScores(scriptIds)); } if (scriptRefs.stream().anyMatch(ref -> ref.getOptionScriptId() == null)) { optionScores.put(null, collectTargetOptionScores(voice ? ivrLibaTemplateTargetoptionMapper.selectIvrLibaTargetoptionScores(null) : svyLibTemplateTargetoptionMapper.selectSvyLibTargetoptionScores(null))); } return optionScores; } private void addTargetOptionScores(Map> optionScores, List> rows) { if (CollectionUtils.isEmpty(rows)) return; for (Map row : rows) { Long scriptId = toLong(row.get("scriptid")); if (scriptId == null) continue; Object score = row.get("score"); optionScores.computeIfAbsent(scriptId, key -> new ArrayList<>()) .add(score == null ? null : String.valueOf(score)); } } private List collectTargetOptionScores(List> rows) { List scores = new ArrayList<>(); if (CollectionUtils.isEmpty(rows)) return scores; for (Map row : rows) { Object score = row.get("score"); scores.add(score == null ? null : String.valueOf(score)); } return scores; } /** * 各选项(matchedtext)数量及占比 *

* 口径与原实现一致:分母是全部明细行数(含 matchedtext 为 null 的行); * targetvalue 用于补全未出现的选项(count=0、ratio=0)。 */ private Map> buildMatchedtextStats(List fillRows, long totalFill) { Map> matchedtextStats = new HashMap<>(); if (CollectionUtils.isEmpty(fillRows) || totalFill <= 0L) return matchedtextStats; Map matchedtextCounts = new LinkedHashMap<>(); String targetvalue = null; for (MatchedtextCount row : fillRows) { if (row.getMatchedtext() != null) { matchedtextCounts.merge(row.getMatchedtext(), row.getCount(), Long::sum); } if (targetvalue == null && row.getTargetvalue() != null) { targetvalue = row.getTargetvalue(); } } for (Map.Entry entry : matchedtextCounts.entrySet()) { Map stat = new HashMap<>(); stat.put("count", entry.getValue()); stat.put("ratio", new BigDecimal((double) entry.getValue() / totalFill * 100).setScale(1, RoundingMode.HALF_UP).doubleValue()); matchedtextStats.put(entry.getKey(), stat); } // 从明细里取targetvalue(&分隔的当前问题的所有选项),补全未出现的选项,count和ratio均为0 if (targetvalue != null) { for (String option : targetvalue.split("&")) { String optionTrim = option.trim(); if (!matchedtextStats.containsKey(optionTrim)) { Map emptyStat = new HashMap<>(); emptyStat.put("count", 0L); emptyStat.put("ratio", 0.0); matchedtextStats.put(optionTrim, emptyStat); } } } return matchedtextStats; } /** * 取填报明细行数(原 fillCount.size()) */ private long countFillRowsByMatchedtext(List fillRows) { if (CollectionUtils.isEmpty(fillRows)) return 0L; long rows = 0L; for (MatchedtextCount row : fillRows) { rows += row.getCount(); } return rows; } /** * 获取平均分(聚合版:分值与条数) *

* 口径与 {@link #averageScore(List)} 一致:分子为可解析分值之和,分母为明细行数(含分值为空的行)。 */ private Double averageScoreByMatchedtext(List fillRows) { if (CollectionUtils.isEmpty(fillRows)) return 0.0; double sum = 0.0; long rows = 0L; for (MatchedtextCount row : fillRows) { if (row.getCount() <= 0L) continue; rows += row.getCount(); if (row.getScore() != null && !row.getScore().isEmpty()) { sum += Double.parseDouble(row.getScore()) * row.getCount(); } } if (rows == 0L) return 0.0; return new BigDecimal(sum / rows).setScale(1, RoundingMode.HALF_UP).doubleValue(); } /** * 获取最高分(聚合版,口径同 {@link #maxScore(List)}:空列表返回 0.0) */ private Double maxScoreOf(List scores) { if (CollectionUtils.isEmpty(scores)) return 0.0; return scores.stream().filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty()) .mapToDouble(Double::parseDouble).max().orElse(0.0); } /** * 获取最低分(聚合版,口径同 {@link #minScore(List)}:空列表返回 0.0) */ private Double minScoreOf(List scores) { if (CollectionUtils.isEmpty(scores)) return 0.0; return scores.stream().filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty()) .mapToDouble(Double::parseDouble).min().orElse(0.0); } /** * 满意度统计用的问题引用:模板id、选项表关联的问题id(可能为 null)、问题内容 */ private static class SatScriptRef { private final Long templateId; private final Long optionScriptId; private final String scriptContent; SatScriptRef(Long templateId, Long optionScriptId, String scriptContent) { this.templateId = templateId; this.optionScriptId = optionScriptId; this.scriptContent = scriptContent; } Long getTemplateId() { return templateId; } Long getOptionScriptId() { return optionScriptId; } String getScriptContent() { return scriptContent; } } /** * 满意度填报聚合结果:matchedtext + score + targetvalue + 条数 */ private static class MatchedtextCount { private final String matchedtext; private final String score; private final String targetvalue; private final long count; MatchedtextCount(String matchedtext, String score, String targetvalue, long count) { this.matchedtext = matchedtext; this.score = score; this.targetvalue = targetvalue; this.count = count; } String getMatchedtext() { return matchedtext; } String getScore() { return score; } String getTargetvalue() { return targetvalue; } long getCount() { return count; } } /** * 满意度填报聚合结果(按 matchedtext 版) *

* byTemplateQuestion:按 模板+问题 分组; * byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在 * script_content 为 null 时不拼 questiontext 条件的行为。 */ private static class SatisfactionFillStatByMatchedtext { private final Map> byTemplateQuestion = new HashMap<>(); private final Map> byTemplate = new HashMap<>(); void add(Long templateId, String questiontext, MatchedtextCount row) { byTemplate.computeIfAbsent(templateId, key -> new ArrayList<>()).add(row); if (questiontext != null) { byTemplateQuestion.computeIfAbsent(statKey(templateId, questiontext), key -> new ArrayList<>()).add(row); } } List get(Long templateId, String scriptContent) { if (templateId == null) return null; if (scriptContent == null) return byTemplate.get(templateId); return byTemplateQuestion.get(statKey(templateId, scriptContent)); } } }