package com.ruoyi.satisfaction.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.ruoyi.satisfaction.domain.vo.PatSatisfactionReqVO;
|
import com.ruoyi.satisfaction.domain.vo.PatSatisfactionResVO;
|
import com.ruoyi.satisfaction.domain.entity.PatSatisfactionDetailEntity;
|
import com.ruoyi.satisfaction.domain.entity.SatisfactionStatQuery;
|
import com.ruoyi.satisfaction.domain.entity.ServiceSubtaskSatisfactionEntity;
|
import com.ruoyi.satisfaction.mapper.PatSatisfactionMapper;
|
import com.ruoyi.satisfaction.mapper.SatisfactionMapper;
|
import com.ruoyi.satisfaction.mapper.SatisfactionScriptMapper;
|
import com.ruoyi.satisfaction.domain.row.SatisfactionCategoryRow;
|
import com.ruoyi.satisfaction.domain.row.SatisfactionIvrScriptRow;
|
import com.ruoyi.satisfaction.domain.row.SatisfactionOptionScoreRow;
|
import com.ruoyi.satisfaction.domain.row.SatisfactionScriptQuery;
|
import com.ruoyi.satisfaction.domain.row.SatisfactionSvyScriptRow;
|
import com.ruoyi.satisfaction.domain.ServiceSubtaskDetailRatioExport;
|
import com.ruoyi.satisfaction.service.strategy.SatisfactionScoreStrategyResolver;
|
import com.ruoyi.satisfaction.service.strategy.SatisfactionSites;
|
import org.apache.commons.collections4.CollectionUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import com.ruoyi.satisfaction.domain.PatSatisfaction;
|
import com.ruoyi.satisfaction.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 SatisfactionMapper satisfactionMapper;
|
|
@Autowired
|
private SatisfactionScriptMapper satisfactionScriptMapper;
|
|
/**
|
* 站点口径策略解析器。
|
* <p>
|
* 本类的 {@code averageScoreByStat} 就是「通用口径(南华)」的综合满意度得分口径,
|
* 现改为委托 {@link com.ruoyi.satisfaction.service.strategy.CommonSatisfactionScoreStrategy},
|
* 与省立同德口径({@code SltdSatisfactionScoreStrategy})通过同一策略接口并列,
|
* 避免同一个概念在两处各写一份实现。
|
*/
|
@Autowired
|
private SatisfactionScoreStrategyResolver satisfactionScoreStrategyResolver;
|
|
/**
|
* 查询患者满意度
|
*
|
* @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<PatSatisfaction> selectPatSatisfactionList(PatSatisfaction patSatisfaction) {
|
List<PatSatisfaction> 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<PatSatisfaction> patSatisfactions) {
|
if(CollectionUtils.isEmpty(patSatisfactions)){
|
return 0;
|
}
|
List<Long> ids = patSatisfactions.stream().map(PatSatisfaction::getId).
|
distinct().collect(Collectors.toList());
|
List<PatSatisfaction> patSatisfactionsOld = new ArrayList<>();
|
PatSatisfaction patSatisfactionVo = new PatSatisfaction();
|
patSatisfactionVo.setIds(ids);
|
patSatisfactionsOld = patSatisfactionMapper.selectPatSatisfactionList(patSatisfactionVo);
|
Map<String, List<PatSatisfaction>> maps = ArrayListUtils.splitListsById(patSatisfactions,
|
patSatisfactionsOld, PatSatisfaction::getId);
|
//只在patSatisfactions的数据,执行新增操作
|
List<PatSatisfaction> addItems = maps.get("onlyIn1");
|
//只在patSatisfactionsOld的数据,执行删除操作
|
// List<PatSatisfaction> delItems = maps.get("onlyIn2");
|
//重叠的数据,执行更新操作
|
List<PatSatisfaction> 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<PatSatisfactionDetailEntity> patSatisfactionDetailEntities = new ArrayList<>();
|
// [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的)
|
int[] personTotals = new int[2];
|
|
// 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空)
|
List<String> types = satisfactionMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO);
|
boolean hasVoice = types != null && types.contains("1");
|
boolean hasQuestionnaire = types != null && types.contains("2");
|
|
if (hasVoice) {
|
List<SatisfactionCategoryRow> ivrLibaScriptAssorts = satisfactionScriptMapper.selectIvrCategoryList(ivrCategoryQuery(patSatisfactionReqVO));
|
List<Long> mydIdList = ivrLibaScriptAssorts.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SatisfactionCategoryRow::getId).collect(Collectors.toList());
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScripts = satisfactionScriptMapper.queryIvrScriptByAssort(assortQuery(mydIdList));
|
|
//3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
|
List<SatScriptRef> scriptRefs = new ArrayList<>();
|
for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScripts) {
|
scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent(), ivrLibaTemplateScript.getDimension()));
|
}
|
buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals);
|
}
|
|
if (hasQuestionnaire) {
|
List<SatisfactionCategoryRow> svyLibScriptCategories = satisfactionScriptMapper.selectSvyCategoryList(svyCategoryQuery(patSatisfactionReqVO));
|
List<Long> mydIdList = svyLibScriptCategories.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SatisfactionCategoryRow::getId).collect(Collectors.toList());
|
|
List<SatisfactionSvyScriptRow> svyLibTemplateScripts = satisfactionScriptMapper.querySvyScriptByCategory(categoryQuery(mydIdList));
|
|
//3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
|
List<SatScriptRef> scriptRefs = new ArrayList<>();
|
for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScripts) {
|
// 问卷的选项表按 svy_lib_template_script 的主键关联
|
scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent(), svyLibTemplateScript.getDimension()));
|
}
|
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<PatSatisfactionDetailEntity> patSatisfactionDetailEntities = new ArrayList<>();
|
// [0]=答题总人数(已经回答问题的) [1]=总人数(包含未回答的)
|
int[] personTotals = new int[2];
|
|
// 只查本次条件下实际存在哪些 type(原来是把接近全表的上百个字段查回来,仅用于判断下面两个分支是否为空)
|
List<String> types = satisfactionMapper.selectSatisfactionSubtaskTypes(patSatisfactionReqVO);
|
boolean hasVoice = types != null && types.contains("1");
|
boolean hasQuestionnaire = types != null && types.contains("2");
|
|
if (hasVoice) {
|
//1. 先获取入参中的维护集合
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScripts = satisfactionScriptMapper.selectIvrScriptByDimension(dimensionQuery(patSatisfactionReqVO));
|
if (CollectionUtils.isNotEmpty(ivrLibaTemplateScripts)) {
|
//3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
|
List<SatScriptRef> scriptRefs = new ArrayList<>();
|
for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScripts) {
|
scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent(), ivrLibaTemplateScript.getDimension()));
|
}
|
buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals);
|
}
|
}
|
|
if (hasQuestionnaire) {
|
//1. 先获取入参中的满意度维度
|
List<SatisfactionSvyScriptRow> svyLibTemplateScripts = satisfactionScriptMapper.selectSvyScriptByDimension(dimensionQuery(patSatisfactionReqVO));
|
if (CollectionUtils.isNotEmpty(svyLibTemplateScripts)) {
|
//3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次)
|
List<SatScriptRef> scriptRefs = new ArrayList<>();
|
for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScripts) {
|
// 问卷的选项表按 svy_lib_template_script 的主键关联
|
scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent(), svyLibTemplateScript.getDimension()));
|
}
|
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<String, Object> satisfactionGraph(PatSatisfactionReqVO patSatisfactionReqVO) {
|
//用来记录柱状图信息(key=满意度分类名称,value=满意度分类下的总人数和填报人数)
|
Map<String, Object> zzt = new HashMap<>();
|
|
// 记录一下每种分类的填报情况(只记分值与条数,不再把明细行全部查回 Java)
|
Map<String, List<ScoreCount>> assortNameFillCountMap = new HashMap<>();
|
|
if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) {
|
List<SatisfactionCategoryRow> ivrLibaScriptAssorts = satisfactionScriptMapper.selectIvrCategoryList(ivrCategoryQuery(patSatisfactionReqVO));
|
List<Long> mydIdList = ivrLibaScriptAssorts.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SatisfactionCategoryRow::getId).collect(Collectors.toList());
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScripts = satisfactionScriptMapper.queryIvrScriptByAssort(assortQuery(mydIdList));
|
|
//2.1 一次性把本次请求全部模板的发送量、回收人数查回来(原来是每个模板问题各查两次)
|
SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1",
|
ivrLibaTemplateScripts.stream().map(SatisfactionIvrScriptRow::getTemplateid).collect(Collectors.toList()));
|
Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
|
SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
|
Map<Long, Long> fillPersonCountMap = mapSatisfactionFillPersonCount(statQuery);
|
|
//3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
|
Map<String, List<SatisfactionIvrScriptRow>> scriptGroupByAssortName = ivrLibaTemplateScripts.stream().filter(item -> item.getScriptAssortname() != null).collect(Collectors.groupingBy(SatisfactionIvrScriptRow::getScriptAssortname));
|
if (scriptGroupByAssortName != null && !scriptGroupByAssortName.isEmpty()) {
|
for (String assortNameKey : scriptGroupByAssortName.keySet()) {
|
// 该分类涉及的去重模板:发送人数/回收人数都按模板统计,
|
// 按题目循环累加会把同一个模板的量重复计成「题量」
|
Set<Long> categoryTemplateIds = new LinkedHashSet<>();
|
//获取该分类的填报总数量(只用于算平均分,按题目聚合的行数)
|
List<ScoreCount> allFillCountList = new ArrayList<>();
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScriptList = scriptGroupByAssortName.get(assortNameKey);
|
for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScriptList) {
|
Long templateId = ivrLibaTemplateScript.getTemplateid();
|
if (templateId != null) {
|
categoryTemplateIds.add(templateId);
|
}
|
//获取填报分值分布(只用于平均分)
|
List<ScoreCount> fillCount = fillStat.get(templateId, ivrLibaTemplateScript.getScriptContent());
|
if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
|
}
|
|
//记录发送总人数、回收总人数
|
Double subidAll = 0.0;
|
Double fillCountAll = 0.0;
|
for (Long templateId : categoryTemplateIds) {
|
subidAll += sendCountMap.getOrDefault(templateId, 0L);
|
fillCountAll += fillPersonCountMap.getOrDefault(templateId, 0L);
|
}
|
|
// 这里先记录一下每个分类的总人数和填报人数
|
Map<String, Object> 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) {
|
List<SatisfactionCategoryRow> svyLibScriptCategories = satisfactionScriptMapper.selectSvyCategoryList(svyCategoryQuery(patSatisfactionReqVO));
|
List<Long> mydIdList = svyLibScriptCategories.stream().filter(item -> item.getType() != null && item.getType().contains("myd")).map(SatisfactionCategoryRow::getId).collect(Collectors.toList());
|
|
List<SatisfactionSvyScriptRow> svyLibTemplateScripts = satisfactionScriptMapper.querySvyScriptByCategory(categoryQuery(mydIdList));
|
|
//2.1 一次性把本次请求全部模板的发送量、回收人数查回来(原来是每个模板问题各查两次)
|
SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2",
|
svyLibTemplateScripts.stream().map(SatisfactionSvyScriptRow::getSvyid).collect(Collectors.toList()));
|
Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
|
SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
|
Map<Long, Long> fillPersonCountMap = mapSatisfactionFillPersonCount(statQuery);
|
|
//3 按scriptAssortname分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
|
Map<String, List<SatisfactionSvyScriptRow>> scriptGroupByatCegoryName = svyLibTemplateScripts.stream().filter(item -> item.getCategoryName() != null).collect(Collectors.groupingBy(SatisfactionSvyScriptRow::getCategoryName));
|
if (scriptGroupByatCegoryName != null && !scriptGroupByatCegoryName.isEmpty()) {
|
for (String categoryNameKey : scriptGroupByatCegoryName.keySet()) {
|
// 该分类涉及的去重模板:发送人数/回收人数都按模板统计,
|
// 按题目循环累加会把同一个模板的量重复计成「题量」
|
Set<Long> categoryTemplateIds = new LinkedHashSet<>();
|
//获取该分类的填报总数量(只用于算平均分,按题目聚合的行数)
|
List<ScoreCount> allFillCountList = new ArrayList<>();
|
|
List<SatisfactionSvyScriptRow> svyLibTemplateScriptList = scriptGroupByatCegoryName.get(categoryNameKey);
|
for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScriptList) {
|
Long templateId = svyLibTemplateScript.getSvyid();
|
if (templateId != null) {
|
categoryTemplateIds.add(templateId);
|
}
|
//获取填报分值分布(只用于平均分)
|
List<ScoreCount> fillCount = fillStat.get(templateId, svyLibTemplateScript.getScriptContent());
|
if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
|
}
|
|
//记录发送总人数、回收总人数
|
Double subidAll = 0.0;
|
Double fillCountAll = 0.0;
|
for (Long templateId : categoryTemplateIds) {
|
subidAll += sendCountMap.getOrDefault(templateId, 0L);
|
fillCountAll += fillPersonCountMap.getOrDefault(templateId, 0L);
|
}
|
|
// 这里先记录一下每个分类的总人数和填报人数
|
Map<String, Object> personCount = (Map<String, Object>) 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<ScoreCount> 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<String, Object> satisfactionGraphDimension(PatSatisfactionReqVO patSatisfactionReqVO) {
|
//用来记录柱状图信息(key=满意度维度名称,value=该维度下的总人数和填报人数)
|
Map<String, Object> zzt = new HashMap<>();
|
|
// 记录一下每个维度的填报情况(只记分值与条数,不再把明细行全部查回 Java)
|
Map<String, List<ScoreCount>> assortNameFillCountMap = new HashMap<>();
|
|
if (patSatisfactionReqVO.getType() == null || patSatisfactionReqVO.getType() == 1) {
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScripts = satisfactionScriptMapper.selectIvrScriptByDimension(dimensionQuery(patSatisfactionReqVO));
|
if (ivrLibaTemplateScripts != null && !ivrLibaTemplateScripts.isEmpty()) {
|
//3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
|
List<SatisfactionIvrScriptRow> dimensionScripts = ivrLibaTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList());
|
Map<String, List<SatisfactionIvrScriptRow>> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(SatisfactionIvrScriptRow::getDimension));
|
|
if (groupByDimension != null && !groupByDimension.isEmpty()) {
|
//3.1 一次性把本次请求全部模板的发送量、回收人数查回来(原来是每个模板问题各查两次)
|
SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "1",
|
dimensionScripts.stream().map(SatisfactionIvrScriptRow::getTemplateid).collect(Collectors.toList()));
|
Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
|
SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
|
Map<Long, Long> fillPersonCountMap = mapSatisfactionFillPersonCount(statQuery);
|
|
for (String dimension : groupByDimension.keySet()) {
|
// 该维度涉及的去重模板:发送人数/回收人数都按模板统计,按题目累加会变成题量
|
Set<Long> dimensionTemplateIds = new LinkedHashSet<>();
|
//获取该维度的填报分值分布(只用于算平均分)
|
List<ScoreCount> allFillCountList = new ArrayList<>();
|
|
List<SatisfactionIvrScriptRow> ivrLibaTemplateScriptList = groupByDimension.get(dimension);
|
for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScriptList) {
|
Long templateId = ivrLibaTemplateScript.getTemplateid();
|
if (templateId != null) {
|
dimensionTemplateIds.add(templateId);
|
}
|
//获取填报分值分布(只用于平均分)
|
List<ScoreCount> fillCount = fillStat.get(templateId, ivrLibaTemplateScript.getScriptContent());
|
if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
|
}
|
|
//记录发送总人数、回收总人数
|
Double subidAll = 0.0;
|
Double fillCountAll = 0.0;
|
for (Long templateId : dimensionTemplateIds) {
|
subidAll += sendCountMap.getOrDefault(templateId, 0L);
|
fillCountAll += fillPersonCountMap.getOrDefault(templateId, 0L);
|
}
|
|
// 这里先记录一下每个分类的总人数和填报人数
|
Map<String, Object> 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. 从入参中获取满意度维维度
|
|
List<SatisfactionSvyScriptRow> svyLibTemplateScripts = satisfactionScriptMapper.selectSvyScriptByDimension(dimensionQuery(patSatisfactionReqVO));
|
if (svyLibTemplateScripts != null && !svyLibTemplateScripts.isEmpty()) {
|
//3 按dimension维度分组,并获取每个集合进行遍历,计算每个分类的总人数和填报人数
|
List<SatisfactionSvyScriptRow> dimensionScripts = svyLibTemplateScripts.stream().filter(item -> item.getDimension() != null).collect(Collectors.toList());
|
Map<String, List<SatisfactionSvyScriptRow>> groupByDimension = dimensionScripts.stream().collect(Collectors.groupingBy(SatisfactionSvyScriptRow::getDimension));
|
if (groupByDimension != null && !groupByDimension.isEmpty()) {
|
//3.1 一次性把本次请求全部模板的发送量、回收人数查回来(原来是每个模板问题各查两次)
|
SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(patSatisfactionReqVO, "2",
|
dimensionScripts.stream().map(SatisfactionSvyScriptRow::getSvyid).collect(Collectors.toList()));
|
Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
|
SatisfactionFillStat fillStat = querySatisfactionFillStat(statQuery);
|
Map<Long, Long> fillPersonCountMap = mapSatisfactionFillPersonCount(statQuery);
|
|
for (String dimension : groupByDimension.keySet()) {
|
// 该维度涉及的去重模板:发送人数/回收人数都按模板统计,按题目累加会变成题量
|
Set<Long> dimensionTemplateIds = new LinkedHashSet<>();
|
//获取该分类的填报分值分布(只用于算平均分)
|
List<ScoreCount> allFillCountList = new ArrayList<>();
|
|
List<SatisfactionSvyScriptRow> svyLibTemplateScriptList = groupByDimension.get(dimension);
|
for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScriptList) {
|
Long templateId = svyLibTemplateScript.getSvyid();
|
if (templateId != null) {
|
dimensionTemplateIds.add(templateId);
|
}
|
//获取填报分值分布(只用于平均分)
|
List<ScoreCount> fillCount = fillStat.get(templateId, svyLibTemplateScript.getScriptContent());
|
if (CollectionUtils.isNotEmpty(fillCount)) allFillCountList.addAll(fillCount);
|
}
|
|
//记录发送总人数、回收总人数
|
Double subidAll = 0.0;
|
Double fillCountAll = 0.0;
|
for (Long templateId : dimensionTemplateIds) {
|
subidAll += sendCountMap.getOrDefault(templateId, 0L);
|
fillCountAll += fillPersonCountMap.getOrDefault(templateId, 0L);
|
}
|
|
// 这里先记录一下每个分类的总人数和填报人数
|
Map<String, Object> personCount = (Map<String, Object>) 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<ScoreCount> 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;
|
}
|
|
// ==================== 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<Long> 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<Long, Long> mapSatisfactionSendCount(SatisfactionStatQuery statQuery) {
|
Map<Long, Long> sendCountMap = new HashMap<>();
|
if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return sendCountMap;
|
List<Map<String, Object>> rows = satisfactionMapper.selectSatisfactionSendCount(statQuery);
|
if (CollectionUtils.isEmpty(rows)) return sendCountMap;
|
for (Map<String, Object> row : rows) {
|
Long templateId = toLong(row.get("libtemplateid"));
|
if (templateId == null) continue;
|
sendCountMap.put(templateId, toLong(row.get("cnt"), 0L));
|
}
|
return sendCountMap;
|
}
|
|
/**
|
* 一次查出全部模板的「回收人数」(按模板去重,同一个人答多道题只算一人)
|
*
|
* @return key=模板id,value=回收人数
|
*/
|
private Map<Long, Long> mapSatisfactionFillPersonCount(SatisfactionStatQuery statQuery) {
|
Map<Long, Long> fillPersonCountMap = new HashMap<>();
|
if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillPersonCountMap;
|
List<Map<String, Object>> rows = satisfactionMapper.selectSatisfactionFillPersonCount(statQuery);
|
if (CollectionUtils.isEmpty(rows)) return fillPersonCountMap;
|
for (Map<String, Object> row : rows) {
|
Long templateId = toLong(row.get("libtemplateid"));
|
if (templateId == null) continue;
|
fillPersonCountMap.put(templateId, toLong(row.get("cnt"), 0L));
|
}
|
return fillPersonCountMap;
|
}
|
|
/**
|
* 一次查出全部模板的填报量(按 模板+问题+分值 分组)
|
*/
|
private SatisfactionFillStat querySatisfactionFillStat(SatisfactionStatQuery statQuery) {
|
SatisfactionFillStat fillStat = new SatisfactionFillStat();
|
if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat;
|
List<Map<String, Object>> rows = satisfactionMapper.selectSatisfactionFillStat(statQuery);
|
if (CollectionUtils.isEmpty(rows)) return fillStat;
|
for (Map<String, Object> 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;
|
}
|
|
/**
|
* 获取平均分(聚合版:分值与条数)
|
* <p>
|
* 口径与原 {@code averageScore(List)} 完全一致:分子为可解析分值之和,分母为明细行数(包含分值为空的行)。
|
* 注意不能改成 sql 的 avg() 或「有分值的行数」做分母,否则返回的 averageScore 会变。
|
* <p>
|
* <b>策略化(2026)</b>:真正的算法已收敛到「通用口径(南华)」策略
|
* {@link com.ruoyi.satisfaction.service.strategy.CommonSatisfactionScoreStrategy},
|
* 本方法只做入参适配(把内部 {@code ScoreCount} 转成统一的占比行)。
|
* <b>计算结果与迁移前逐位相同</b>(保留 1 位、HALF_UP;无行时 0.0)。
|
*/
|
private Double averageScoreByStat(List<ScoreCount> fillCount) {
|
if (CollectionUtils.isEmpty(fillCount)) return 0.0;
|
|
// 适配成策略的统一入参:只带 score 与 count 两个字段(策略只用到这两个)
|
List<ServiceSubtaskDetailRatioExport> rows = new ArrayList<>(fillCount.size());
|
for (ScoreCount scoreCount : fillCount) {
|
if (scoreCount.getCount() <= 0L) continue; // 与原文一致:非正数的聚合行不参与
|
ServiceSubtaskDetailRatioExport row = new ServiceSubtaskDetailRatioExport();
|
row.setScore(scoreCount.getScore());
|
row.setCount(String.valueOf(scoreCount.getCount()));
|
rows.add(row);
|
}
|
|
BigDecimal average = satisfactionScoreStrategyResolver
|
.resolve(SatisfactionSites.COMMON)
|
.comprehensiveScore(rows);
|
return average == null ? 0.0 : average.doubleValue();
|
}
|
|
/**
|
* 分值分组键:模板id + 分隔符 + 规范化后的问题内容
|
* <p>
|
* 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;
|
}
|
}
|
|
/**
|
* 满意度填报量聚合结果
|
* <p>
|
* byTemplateQuestion:按 模板+问题 分组;
|
* byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在
|
* script_content 为 null 时不拼 questiontext 条件的行为。
|
*/
|
private static class SatisfactionFillStat {
|
private final Map<String, List<ScoreCount>> byTemplateQuestion = new HashMap<>();
|
private final Map<Long, List<ScoreCount>> 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<ScoreCount> 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 共用)
|
* <p>
|
* 把原来「每个问题 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<SatScriptRef> scriptRefs,
|
List<PatSatisfactionDetailEntity> out, int[] totals) {
|
if (CollectionUtils.isEmpty(scriptRefs)) return;
|
|
List<Long> templateIds = scriptRefs.stream().map(SatScriptRef::getTemplateId)
|
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
SatisfactionStatQuery statQuery = buildSatisfactionStatQuery(reqVO, type, templateIds);
|
Map<Long, Long> sendCountMap = mapSatisfactionSendCount(statQuery);
|
SatisfactionFillStatByMatchedtext fillStat = querySatisfactionFillStatByMatchedtext(statQuery);
|
//答题【人数】:按 模板+问题 的 distinct sub_id(与「发送人数」同口径)
|
Map<String, Long> answerPersonMap = mapSatisfactionFillPersonCountByQuestion(statQuery);
|
Map<Long, List<String>> 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);
|
//填报【行数】:用于平均分 / 最高分 / 最低分 / 各选项占比(这些口径本来就按明细行计)
|
List<MatchedtextCount> fillRows = fillStat.get(templateId, scriptRef.getScriptContent());
|
long fillCount = countFillRowsByMatchedtext(fillRows);
|
//答题【人数】:用于 答题人数 / 未答题人数 / 答题率
|
long answerPersonCount = answerPersonMap.getOrDefault(
|
statKey(templateId, scriptRef.getScriptContent()), 0L);
|
|
PatSatisfactionDetailEntity detail = new PatSatisfactionDetailEntity();
|
detail.setScriptContent(scriptRef.getScriptContent());
|
//维度:原本查询里已取到,但构造 SatScriptRef 时被丢掉,导致前端「维度」列一直为空
|
detail.setDimension(scriptRef.getDimension());
|
// ⚠️ 人数口径修正:原来这里用的是 fillCount(明细行数),
|
// 而发送人数是 count(distinct id)(人数),两者相减会出现负数(实测 -120 = 15 − 135)。
|
// 一条 (sub_id, questiontext) 可能有多行明细,所以答题人数必须取 distinct sub_id。
|
detail.setAnswerPerson((int) answerPersonCount);
|
detail.setNoAnswerPerson(answerPersonCount > 0L && sendCount > 0L ? (int) (sendCount - answerPersonCount)
|
: sendCount > 0L ? (int) sendCount : 0);
|
detail.setAverageScore(String.valueOf(averageScoreByMatchedtext(fillRows)));
|
List<String> scores = optionScores.get(scriptRef.getOptionScriptId());
|
detail.setMaxScore(String.valueOf(maxScoreOf(scores)));
|
detail.setMinScore(String.valueOf(minScoreOf(scores)));
|
detail.setAnswerRate(sendCount == 0L ? 0 : answerPersonCount == 0L ? 0 : answerPersonCount * 1.0 / sendCount);
|
detail.setMatchedtextStats(buildMatchedtextStats(fillRows, fillCount));
|
out.add(detail);
|
|
totals[0] += (int) answerPersonCount;
|
totals[1] += (int) sendCount;
|
}
|
}
|
|
/**
|
* 一次查出「全部 模板+问题」的答题人数({@code count(distinct ssd.sub_id)})。
|
* <p>
|
* key 用与 {@link #statKey(Long, String)} 相同的规则构造,保证与调用方的查找一致
|
* ({@code statKey} 已做 trim + 小写,等价于 MySQL 的忽略大小写与尾部空格比较)。
|
*/
|
private Map<String, Long> mapSatisfactionFillPersonCountByQuestion(SatisfactionStatQuery statQuery) {
|
Map<String, Long> answerPersonMap = new HashMap<>();
|
if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return answerPersonMap;
|
List<Map<String, Object>> rows = satisfactionMapper.selectSatisfactionFillPersonCountByQuestion(statQuery);
|
if (CollectionUtils.isEmpty(rows)) return answerPersonMap;
|
for (Map<String, Object> 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");
|
answerPersonMap.merge(
|
statKey(templateId, questiontext == null ? null : String.valueOf(questiontext)),
|
cnt, Long::sum);
|
}
|
return answerPersonMap;
|
}
|
|
/**
|
* 一次查出全部问题的填报量(按 模板+问题+matchedtext+score 分组,含 targetvalue)
|
*/
|
private SatisfactionFillStatByMatchedtext querySatisfactionFillStatByMatchedtext(SatisfactionStatQuery statQuery) {
|
SatisfactionFillStatByMatchedtext fillStat = new SatisfactionFillStatByMatchedtext();
|
if (statQuery == null || CollectionUtils.isEmpty(statQuery.getLibTemplateIds())) return fillStat;
|
List<Map<String, Object>> rows = satisfactionMapper.selectSatisfactionFillStatByMatchedtext(statQuery);
|
if (CollectionUtils.isEmpty(rows)) return fillStat;
|
for (Map<String, Object> 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 两列)
|
* <p>
|
* 原实现是每个问题查一次整行,而调用方只用到 score 算最高/最低分;
|
* 问题 id 为 null 时原实现不拼 scriptid 条件(等于取该表全部分值),这里用 null 作为 key 存这一份。
|
*/
|
private Map<Long, List<String>> loadTargetOptionScores(boolean voice, List<SatScriptRef> scriptRefs) {
|
Map<Long, List<String>> optionScores = new HashMap<>();
|
List<Long> scriptIds = scriptRefs.stream().map(SatScriptRef::getOptionScriptId)
|
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
if (CollectionUtils.isNotEmpty(scriptIds)) {
|
addTargetOptionScores(optionScores, voice
|
? satisfactionScriptMapper.selectIvrTargetOptionScores(scriptIds)
|
: satisfactionScriptMapper.selectSvyTargetOptionScores(scriptIds));
|
}
|
if (scriptRefs.stream().anyMatch(ref -> ref.getOptionScriptId() == null)) {
|
optionScores.put(null, collectTargetOptionScores(voice
|
? satisfactionScriptMapper.selectIvrTargetOptionScores(null)
|
: satisfactionScriptMapper.selectSvyTargetOptionScores(null)));
|
}
|
return optionScores;
|
}
|
|
private void addTargetOptionScores(Map<Long, List<String>> optionScores,
|
List<SatisfactionOptionScoreRow> rows) {
|
if (CollectionUtils.isEmpty(rows)) return;
|
for (SatisfactionOptionScoreRow row : rows) {
|
Long scriptId = row.getScriptid();
|
if (scriptId == null) continue;
|
optionScores.computeIfAbsent(scriptId, key -> new ArrayList<>())
|
.add(row.getScore());
|
}
|
}
|
|
private List<String> collectTargetOptionScores(List<SatisfactionOptionScoreRow> rows) {
|
List<String> scores = new ArrayList<>();
|
if (CollectionUtils.isEmpty(rows)) return scores;
|
for (SatisfactionOptionScoreRow row : rows) {
|
scores.add(row.getScore());
|
}
|
return scores;
|
}
|
|
/**
|
* 各选项(matchedtext)数量及占比
|
* <p>
|
* 口径与原实现一致:分母是全部明细行数(含 matchedtext 为 null 的行);
|
* targetvalue 用于补全未出现的选项(count=0、ratio=0)。
|
*/
|
private Map<String, Map<String, Object>> buildMatchedtextStats(List<MatchedtextCount> fillRows, long totalFill) {
|
Map<String, Map<String, Object>> matchedtextStats = new HashMap<>();
|
if (CollectionUtils.isEmpty(fillRows) || totalFill <= 0L) return matchedtextStats;
|
|
Map<String, Long> 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<String, Long> entry : matchedtextCounts.entrySet()) {
|
Map<String, Object> 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<String, Object> emptyStat = new HashMap<>();
|
emptyStat.put("count", 0L);
|
emptyStat.put("ratio", 0.0);
|
matchedtextStats.put(optionTrim, emptyStat);
|
}
|
}
|
}
|
return matchedtextStats;
|
}
|
|
/**
|
* 取填报明细行数(原 fillCount.size())
|
*/
|
private long countFillRowsByMatchedtext(List<MatchedtextCount> fillRows) {
|
if (CollectionUtils.isEmpty(fillRows)) return 0L;
|
long rows = 0L;
|
for (MatchedtextCount row : fillRows) {
|
rows += row.getCount();
|
}
|
return rows;
|
}
|
|
/**
|
* 获取平均分(聚合版:分值与条数)
|
* <p>
|
* 分子为可解析分值之和,分母为明细行数(含分值为空的行)。
|
*/
|
private Double averageScoreByMatchedtext(List<MatchedtextCount> 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();
|
}
|
|
/**
|
* 获取最高分(聚合版,空列表返回 0.0)
|
*/
|
private Double maxScoreOf(List<String> scores) {
|
if (CollectionUtils.isEmpty(scores)) return 0.0;
|
return scores.stream().filter(scoreStr -> scoreStr != null && !scoreStr.isEmpty())
|
.mapToDouble(Double::parseDouble).max().orElse(0.0);
|
}
|
|
/**
|
* 获取最低分(聚合版,口径同 {空列表返回 0.0)
|
*/
|
private Double minScoreOf(List<String> 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;
|
private final String dimension;
|
|
SatScriptRef(Long templateId, Long optionScriptId, String scriptContent, String dimension) {
|
this.templateId = templateId;
|
this.optionScriptId = optionScriptId;
|
this.scriptContent = scriptContent;
|
this.dimension = dimension;
|
}
|
|
Long getTemplateId() {
|
return templateId;
|
}
|
|
Long getOptionScriptId() {
|
return optionScriptId;
|
}
|
|
String getScriptContent() {
|
return scriptContent;
|
}
|
|
String getDimension() {
|
return dimension;
|
}
|
}
|
|
/**
|
* 满意度填报聚合结果: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 版)
|
* <p>
|
* byTemplateQuestion:按 模板+问题 分组;
|
* byTemplate:按模板分组(该模板全部问题),用于兼容原 queryFillCount 在
|
* script_content 为 null 时不拼 questiontext 条件的行为。
|
*/
|
private static class SatisfactionFillStatByMatchedtext {
|
private final Map<String, List<MatchedtextCount>> byTemplateQuestion = new HashMap<>();
|
private final Map<Long, List<MatchedtextCount>> 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<MatchedtextCount> get(Long templateId, String scriptContent) {
|
if (templateId == null) return null;
|
if (scriptContent == null) return byTemplate.get(templateId);
|
return byTemplateQuestion.get(statKey(templateId, scriptContent));
|
}
|
}
|
|
// ==================== 满意度模块化:查询对象构造 ====================
|
// 原实现直接 new smartor 的实体当入参;模块化后统一用 SatisfactionScriptQuery,
|
// 这里把三处构造收敛为帮助方法,条件与原来逐字对应。
|
|
/**
|
* 满意度分类查询(语音侧)。
|
* <p>
|
* ⚠️ <b>与原文一致:只传 orgid,<u>不传 type</u></b>。原实现是
|
* <pre>
|
* IvrLibaScriptAssort e = new IvrLibaScriptAssort();
|
* e.setOrgid(reqVO.getOrgid()); // ← 只有 orgid
|
* mapper.selectIvrLibaScriptAssortList(e);
|
* </pre>
|
* 分类的「是不是满意度(myd)」是在 <b>Java 侧</b>用
|
* {@code item.getType().contains("myd")} 过滤的(见 {@link #statistics} /
|
* {@link #satisfactionGraph})。
|
* <p>
|
* 如果在这里顺手加上 {@code type="myd"},SQL 就会多出一个
|
* {@code type like '%myd%'} 条件 —— 而 MySQL 的 LIKE 默认<b>不区分大小写</b>,
|
* 会比 Java 的 {@code contains("myd")} 多命中分类,导致统计结果出现差异。
|
*/
|
private SatisfactionScriptQuery ivrCategoryQuery(PatSatisfactionReqVO reqVO) {
|
SatisfactionScriptQuery q = new SatisfactionScriptQuery();
|
q.setOrgid(reqVO.getOrgid());
|
return q;
|
}
|
|
/** 满意度分类查询(问卷侧,同样只传 orgid,见 {@link #ivrCategoryQuery}) */
|
private SatisfactionScriptQuery svyCategoryQuery(PatSatisfactionReqVO reqVO) {
|
SatisfactionScriptQuery q = new SatisfactionScriptQuery();
|
q.setOrgid(reqVO.getOrgid());
|
return q;
|
}
|
|
/** 按语音分类 id 取问题 */
|
private SatisfactionScriptQuery assortQuery(List<Long> scriptAssortids) {
|
SatisfactionScriptQuery q = new SatisfactionScriptQuery();
|
q.setScriptAssortids(scriptAssortids);
|
return q;
|
}
|
|
/** 按问卷分类 id 取问题 */
|
private SatisfactionScriptQuery categoryQuery(List<Long> scriptCategoryids) {
|
SatisfactionScriptQuery q = new SatisfactionScriptQuery();
|
q.setScriptCategoryids(scriptCategoryids);
|
return q;
|
}
|
|
/** 按维度取问题(语音/问卷共用;两侧「dimensionList 为空时是否拼 dimension IS NOT NULL」的差异在 SQL 里保留) */
|
private SatisfactionScriptQuery dimensionQuery(PatSatisfactionReqVO reqVO) {
|
SatisfactionScriptQuery q = new SatisfactionScriptQuery();
|
q.setDimensionList(reqVO.getDimensionList());
|
return q;
|
}
|
|
}
|