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;
|
|
/**
|
* 满意度「子任务维度」统计服务。
|
* <p>
|
* 迁移说明:本类方法原散落在随访主干的大文件 ServiceSubtaskServiceImpl 中
|
* (被视为随访主干的大文件),现整段搬入满意度模块,业务逻辑一行未改。
|
* <p>
|
* 与 {@link PatSatisfactionServiceImpl} 的分工:
|
* <ul>
|
* <li>{@code PatSatisfactionServiceImpl} —— 通用满意度统计(南华与同德共用);
|
* <li>本类 —— <b>省立同德导向</b>的统计:选项占比、按科室/病区、趋势、门诊得分排名。
|
* 这些入口原本挂在随访明细 Controller 上,迁移后仍由满意度模块统一提供。</li>
|
* </ul>
|
* 本模块自带 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<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds) {
|
//先清理一下重复数据
|
List<Long> repeatIds = satisfactionMapper.selectRepeatDetailIds();
|
if (!CollectionUtils.isEmpty(repeatIds)) {
|
satisfactionMapper.deleteRepeatDetailByIds(repeatIds);
|
}
|
List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds);
|
|
// 处理 targetvalue:按 & 分割成多条记录
|
List<ServiceSubtaskDetailRatioExport> 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<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext) {
|
List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
|
if (CollectionUtils.isEmpty(subtaskIds)) {
|
return result;
|
}
|
List<ServiceSubtaskOptionStatDTO> statList = satisfactionMapper.statMydQuestionOptionForExport(subtaskIds, questiontext);
|
if (CollectionUtils.isEmpty(statList)) {
|
return result;
|
}
|
return buildMydOptionRows(statList);
|
}
|
|
/**
|
* 把「任务+题目+选项」的作答统计拼成导出的「问题详情占比」行
|
* <ul>
|
* <li>选项清单:题目配置过的选项在 targetvalue(用 & 连接)里,全部列出,没人选的显示 0</li>
|
* <li>数量:患者选中的选项在 asrtext 里(多选题是「A&B」),每个选中项各记一次人数,一个患者只算一条明细</li>
|
* <li>比例:数量 / 该题作答患者数,所以单选题各选项数量之和 = 该题作答患者数</li>
|
* <li>分数:该选项明细的 score 之和;多选行按选中项份数均分,保证各选项分数之和 = 明细分数之和</li>
|
* </ul>
|
*/
|
private static List<ServiceSubtaskDetailRatioExport> buildMydOptionRows(List<ServiceSubtaskOptionStatDTO> statList) {
|
List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
|
|
// 按「任务 + 题目」分组,保持 SQL 的顺序(先题目后任务)
|
Map<String, List<ServiceSubtaskOptionStatDTO>> grouped = new LinkedHashMap<>();
|
for (ServiceSubtaskOptionStatDTO stat : statList) {
|
String key = stat.getTaskid() + "\u0001" + stat.getQuestiontext();
|
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(stat);
|
}
|
|
for (List<ServiceSubtaskOptionStatDTO> group : grouped.values()) {
|
ServiceSubtaskOptionStatDTO first = group.get(0);
|
// 选项 -> 人数(long[1] 便于累加),顺序即出现顺序
|
Map<String, long[]> countMap = new LinkedHashMap<>();
|
Map<String, BigDecimal> scoreMap = new LinkedHashMap<>();
|
|
for (ServiceSubtaskOptionStatDTO stat : group) {
|
// targetvalue = 本题所有选项的清单,没人选的也要列出来
|
List<String> options = splitOptions(stat.getTargetvalue());
|
// asrtext = 患者实际选中的选项,多选题会是「选中项1&选中项2」
|
List<String> chosenOptions = splitOptions(stat.getOptionresult());
|
for (String option : options) {
|
countMap.computeIfAbsent(option, k -> new long[1]);
|
scoreMap.computeIfAbsent(option, k -> BigDecimal.ZERO);
|
}
|
|
long patientCount = stat.getPatientCount() == null ? 0L : stat.getPatientCount();
|
BigDecimal scoreSum = stat.getScoreSum() == null ? BigDecimal.ZERO : stat.getScoreSum();
|
|
if (!chosenOptions.isEmpty()) {
|
BigDecimal[] scoreParts = splitScore(scoreSum, chosenOptions.size());
|
for (int i = 0; i < chosenOptions.size(); i++) {
|
String chosen = chosenOptions.get(i);
|
// 选中的选项可能不在配置清单里(历史数据),补一行
|
countMap.computeIfAbsent(chosen, k -> new long[1]);
|
scoreMap.computeIfAbsent(chosen, k -> BigDecimal.ZERO);
|
countMap.get(chosen)[0] += patientCount;
|
scoreMap.put(chosen, scoreMap.get(chosen).add(scoreParts[i]));
|
}
|
} else if (options.size() == 1) {
|
// 没有 asrtext(未答/无应答)时,只有唯一选项才能确定归属
|
countMap.get(options.get(0))[0] += patientCount;
|
scoreMap.put(options.get(0), scoreMap.get(options.get(0)).add(scoreSum));
|
}
|
}
|
|
long total = 0L;
|
for (long[] count : countMap.values()) total += count[0];
|
|
for (Map.Entry<String, long[]> entry : countMap.entrySet()) {
|
long count = entry.getValue()[0];
|
ServiceSubtaskDetailRatioExport row = new ServiceSubtaskDetailRatioExport();
|
row.setQuestiontext(first.getQuestiontext());
|
row.setOptionresult(entry.getKey());
|
row.setTargetvalue(entry.getKey());
|
row.setCount(String.valueOf(count));
|
row.setRatio(formatRatio(count, total));
|
row.setScore(formatScore(scoreMap.get(entry.getKey())));
|
row.setTaskname(first.getTaskName());
|
row.setTaskid(first.getTaskid());
|
row.setValueType(first.getValueType());
|
result.add(row);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* 把一条明细的分数按选中项份数均分,余数补给第一个,保证各份之和 = 原分数
|
*/
|
private static BigDecimal[] splitScore(BigDecimal total, int parts) {
|
BigDecimal[] result = new BigDecimal[parts];
|
if (parts <= 0) {
|
return result;
|
}
|
BigDecimal each = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.HALF_UP);
|
for (int i = 0; i < parts; i++) {
|
result[i] = each;
|
}
|
result[0] = result[0].add(total.subtract(each.multiply(BigDecimal.valueOf(parts))));
|
return result;
|
}
|
|
/**
|
* 拆分题目配置的选项(多个选项用 & 连接),顺带去掉全角/半角空白
|
*/
|
private static List<String> splitOptions(String targetvalue) {
|
List<String> options = new ArrayList<>();
|
if (StringUtils.isEmpty(targetvalue)) {
|
return options;
|
}
|
for (String part : targetvalue.split("&")) {
|
String option = trimAll(part);
|
if (StringUtils.isNotEmpty(option) && !options.contains(option)) {
|
options.add(option);
|
}
|
}
|
return options;
|
}
|
|
/**
|
* 比例:数量 / 该题作答患者数,保留两位小数
|
*/
|
private static String formatRatio(long count, long total) {
|
if (total <= 0) {
|
return "0.00%";
|
}
|
return BigDecimal.valueOf(count * 100.0 / total).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
|
}
|
|
/**
|
* 分数之和:去掉多余的小数位(0.00 -> 0)
|
*/
|
private static String formatScore(BigDecimal score) {
|
if (score == null) {
|
return "0";
|
}
|
BigDecimal value = score.stripTrailingZeros();
|
if (value.scale() < 0) {
|
value = value.setScale(0);
|
}
|
return value.toPlainString();
|
}
|
|
public Map<String, Map<String, List<QuestionCountKsOrBqDTO>>> statQuestionOptionByKsOrBq(QuestionCountKsOrBqVO vo) {
|
|
// 1. 查明细
|
List<QuestionCountKsOrBqDTO> 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<String, QuestionCountKsOrBqDTO> 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<String, Map<String, List<QuestionCountKsOrBqDTO>>>
|
// 第一层 key:科室/病区
|
// 第二层 key:题目
|
// 第二层 value:该题目下的所有选项列表
|
Map<String, Map<String, List<QuestionCountKsOrBqDTO>>> 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<String, List<QuestionCountKsOrBqDTO>> questionMap =
|
result.computeIfAbsent(groupName, k -> new LinkedHashMap<>());
|
|
// 第二层:题目
|
List<QuestionCountKsOrBqDTO> optionList =
|
questionMap.computeIfAbsent(questionText, k -> new ArrayList<>());
|
|
optionList.add(dto);
|
}
|
|
return result;
|
}
|
|
public List<ServiceSubtaskDetailRatioExport> statQuestionOptionBySubtaskIds(List<Long> subtaskIds, Date startOutHospTime, Date endOutHospTime, Date startFinishTime, Date endFinishTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType) {
|
//先清理一下重复数据
|
List<Long> repeatIds = satisfactionMapper.selectRepeatDetailIds();
|
if (!CollectionUtils.isEmpty(repeatIds)) {
|
satisfactionMapper.deleteRepeatDetailByIds(repeatIds);
|
}
|
|
List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOptionBySubtaskIds(subtaskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
|
// SQL 已按「题目 + 选项」汇总数量,这里再按题目合并(不同模板的 targetvalue 选项顺序不同,同一选项会拆成多行),
|
// 并把没人选的选项补成 0 行、按各选项数量之和重算比例
|
return mergeMydOptionRows(serviceSubtaskDetailRatioExports);
|
}
|
|
/**
|
* 把 SQL 返回的「题目 + 选项」明细合并成「一行一个选项」
|
* <ul>
|
* <li>同一题目下相同 optionresult 的数量累加:原实现遇到同名选项直接 continue,
|
* 每个选项只会保留第一条明细的数量,多任务/多患者的项数全被丢掉,前端「数量」永远只有 1</li>
|
* <li>选项清单取 targetvalue(用 & 连接),没人选的选项补 0 行,前端固定六列才能渲染</li>
|
* <li>比例 = 该选项数量 / 该题各选项数量之和,保证一道题各选项比例合计 100%</li>
|
* </ul>
|
*
|
* @param rows SQL 明细
|
*/
|
private List<ServiceSubtaskDetailRatioExport> mergeMydOptionRows(List<ServiceSubtaskDetailRatioExport> rows) {
|
List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
|
if (CollectionUtils.isEmpty(rows)) {
|
return result;
|
}
|
|
// key -> 选项 -> 数量,用 LinkedHashMap 保序:选项顺序沿用问卷配置 targetvalue 的顺序
|
Map<String, Map<String, long[]>> countMap = new LinkedHashMap<>();
|
Map<String, Map<String, String>> scoreMap = new LinkedHashMap<>();
|
Map<String, ServiceSubtaskDetailRatioExport> templateMap = new LinkedHashMap<>();
|
|
for (ServiceSubtaskDetailRatioExport row : rows) {
|
if (row == null) {
|
continue;
|
}
|
String key = row.getQuestiontext();
|
if (key == null) {
|
continue;
|
}
|
Map<String, long[]> counts = countMap.computeIfAbsent(key, k -> new LinkedHashMap<>());
|
Map<String, String> 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<String> 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<String, Map<String, long[]>> entry : countMap.entrySet()) {
|
ServiceSubtaskDetailRatioExport template = templateMap.get(entry.getKey());
|
Map<String, String> scores = scoreMap.get(entry.getKey());
|
long total = 0L;
|
for (long[] count : entry.getValue().values()) {
|
total += count[0];
|
}
|
for (Map.Entry<String, long[]> 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<String> singleOption(String answer) {
|
List<String> 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<MydTrendReqDTO> mydTrend(MydTrendReqVO mydTrendReqVO) {
|
List<MydTrendReqDTO> 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<MydTrendReqDTO> sltdMydTotalByDimension(MydTrendReqVO mydTrendReqVO) {
|
List<MydTrendReqDTO> 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<ServiceSubtaskDetailRatioExport> getDetailRatio(SltdMydTotalVO sltdMydTotalVO, List<SatisfactionSubtaskRef> subtaskRefs) {
|
List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = null;
|
List<String> 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<Long> taskIds = subtaskRefs.stream().map(SatisfactionSubtaskRef::getTaskid).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
|
//同时带上本次导出的子任务 id:让「问题详情占比」只统计这批患者,与「患者随访信息表单」口径一致
|
List<Long> 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<Long> 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<MydTrendReqDTO> getWeekDate(MydTrendReqVO mydTrendReqVO, List<MydTrendReqDTO> 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<Integer, String> 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<MydTrendReqDTO> 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<MydTrendReqDTO> getYearDate(MydTrendReqVO mydTrendReqVO, List<MydTrendReqDTO> 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<String, String> 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<MydTrendReqDTO> 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<MydTrendReqDTO> getMonthDate(MydTrendReqVO mydTrendReqVO, List<MydTrendReqDTO> 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<String, String> 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<MydTrendReqDTO> 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<MydTrendReqDTO> getDayDate(MydTrendReqVO mydTrendReqVO, List<MydTrendReqDTO> 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<String, String> 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<MydTrendReqDTO> 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<MzMydScoreRankDTO> mzMydScoreRank(MzMydScoreRankVO mzMydScoreRankVO) {
|
if (StringUtils.isEmpty(mzMydScoreRankVO.getDimensionType())) {
|
mzMydScoreRankVO.setDimensionType("1");
|
}
|
if (CollectionUtils.isEmpty(mzMydScoreRankVO.getServiceTypes())) {
|
List<String> list=new ArrayList<>(Arrays.asList("14"));
|
mzMydScoreRankVO.setServiceTypes(list);
|
}
|
List<MzMydScoreRankDTO> 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;
|
}
|
/**
|
* 是否空白字符(含全角空格)。
|
* <p>
|
* 迁移说明:原为 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)。
|
* <p>
|
* 原实现走 {@code serviceSubtaskService.getServiceSubtasks(ServiceSubtaskEntity)},
|
* 该方法位于随访主干 Service 且依赖 patArchiveService,不能搬;
|
* 这里用满意度专用查询复现其<b>实际用到的条件口径</b>(逐条见 mapper XML 注释)。
|
*/
|
@Override
|
public List<SatisfactionSubtaskRef> getMydSubtaskRefs(SatisfactionSubtaskQuery query) {
|
if (query == null) {
|
return new ArrayList<>();
|
}
|
List<SatisfactionSubtaskRef> 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 缓存。
|
* <p>
|
* 迁移说明:原为 {@code smartor.UserExtInfoServiceImpl.getSatisfactionCategories},
|
* 因属于满意度逻辑,按模块化要求搬入本模块;缓存 key 与 TTL 保持原值。
|
*/
|
@Override
|
@SuppressWarnings("unchecked")
|
public Map<String, Object> getSatisfactionCategories(String orgid) {
|
// 先查缓存
|
String cacheKey = SATISFACTION_CACHE_KEY + orgid;
|
Map<String, Object> cached = redisCache.getCacheObject(cacheKey);
|
if (cached != null) {
|
return cached;
|
}
|
|
Map<String, Object> satisfactionCategories = new HashMap<>();
|
|
// 获取语音问题分类
|
List<SatisfactionCategoryRow> ivrLibaScriptAssorts =
|
satisfactionScriptMapper.selectIvrCategoryList(categoryQueryByOrgid(orgid));
|
List<Map<String, Object>> categoryList = new ArrayList<>();
|
if (!CollectionUtils.isEmpty(ivrLibaScriptAssorts)) {
|
for (SatisfactionCategoryRow assort : ivrLibaScriptAssorts) {
|
Map<String, Object> map = new HashMap<>();
|
map.put("categoryid", assort.getId());
|
map.put("categoryName", assort.getName());
|
categoryList.add(map);
|
}
|
}
|
satisfactionCategories.put("voiceCategories", categoryList);
|
|
// 获取问卷问题分类
|
List<SatisfactionCategoryRow> svyLibScriptCategories =
|
satisfactionScriptMapper.selectSvyCategoryList(categoryQueryByOrgid(orgid));
|
List<Map<String, Object>> questionnaireCategorys = new ArrayList<>();
|
if (!CollectionUtils.isEmpty(svyLibScriptCategories)) {
|
for (SatisfactionCategoryRow scriptCategory : svyLibScriptCategories) {
|
Map<String, Object> 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<String, Object> target, List<Map<String, Object>> 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;
|
}
|
|
// ==================== 满意度异常配置题目 ====================
|
|
/**
|
* 获取满意度异常配置题目(语音 + 问卷)。
|
* <p>
|
* 迁移说明:原实现位于 {@code com.smartor.service.impl.ServiceSubtaskDetailTraceServiceImpl},
|
* 现整段搬入本模块。原来的调用链是:
|
* <pre>
|
* 语音分类:IvrLibaScriptAssortServiceImpl.selectIvrLibaScriptAssortList ← 服务方法,会把分类【拼成树】,只返回顶层
|
* 语音题目:IvrLibaTemplateScriptServiceImpl.selectIvrLibaTemplateScriptList(scriptAssortid=X) ← 逐个分类调用
|
* 问卷分类:SvyLibScriptCategoryMapper.selectSvyLibScriptCategoryList ← 直接 Mapper,返回【全部】分类
|
* 问卷题目:SvyLibTemplateScriptServiceImpl.selectSvyLibTemplateScriptList(categoryid=Y) ← 逐个分类调用
|
* </pre>
|
* 为不依赖 smartor 的模板/分类实体,这里改成本模块的只读查询复现,并且:
|
* <ul>
|
* <li>语音分类:仍然按顶层过滤({@code pid == null || pid == -1}),与原服务方法的结果集一致;</li>
|
* <li>问卷分类:不过滤顶层,取全部,与原 Mapper 调用一致;</li>
|
* <li>题目:原文是按分类逐个查询后累加,两条 list statement 都<b>没有 order by</b>,
|
* 所以合并成一次 {@code in (...)} 查询的结果集与逐次查询的并集一致
|
* (集合相同、顺序本来就未定义);</li>
|
* <li>分类 id 列表为空时<b>不查</b>(原文就是在 {@code if (isNotEmpty)} 里才查),
|
* 对应本模块 SQL 里"ids 为空则不拼 in 条件"的写法必须靠这个判断兜住,
|
* 否则会退化成"全表查询"。</li>
|
* </ul>
|
*/
|
@Override
|
public Map<String, Object> getSatisfactionExceptConfigQuestion(SatisfactionExceptConfigQuestionReqVO questionReqVO) {
|
// 初始化结果集
|
Map<String, Object> result = new HashMap<>();
|
|
// ---------------------------------------------------------------- 语音
|
if (StringUtils.isEmpty(questionReqVO.getScriptType()) || questionReqVO.getScriptType().equals("1")) {
|
List<Map<String, Object>> voicetDataList = new ArrayList<>();
|
|
// 获取满意度的分类(type 默认 myd,入参有值则覆盖 —— 与原文一致)
|
List<Long> assortIdList = selectIvrTopCategoryIds(questionReqVO.getType());
|
|
// 用来记录满意度的所有问题
|
if (CollectionUtils.isNotEmpty(assortIdList)) {
|
SatisfactionScriptQuery scriptQuery = new SatisfactionScriptQuery();
|
scriptQuery.setScriptAssortids(assortIdList);
|
List<SatisfactionIvrScriptRow> ivrScripts =
|
satisfactionScriptMapper.queryIvrScriptByAssort(scriptQuery);
|
if (CollectionUtils.isNotEmpty(ivrScripts)) {
|
for (SatisfactionIvrScriptRow ivrScript : ivrScripts) {
|
Map<String, Object> 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<Map<String, Object>> questionnaireDataList = new ArrayList<>();
|
|
// 获取满意度的分类(问卷侧原文直接查 Mapper,取全部分类,不拼树、不筛顶层)
|
List<Long> categoryIdList = selectSvyCategoryIds(questionReqVO.getType());
|
|
// 用来记录满意度的所有问题
|
if (CollectionUtils.isNotEmpty(categoryIdList)) {
|
SatisfactionScriptQuery scriptQuery = new SatisfactionScriptQuery();
|
scriptQuery.setScriptCategoryids(categoryIdList);
|
List<SatisfactionSvyScriptRow> svyScripts =
|
satisfactionScriptMapper.querySvyScriptByCategory(scriptQuery);
|
if (CollectionUtils.isNotEmpty(svyScripts)) {
|
for (SatisfactionSvyScriptRow svyScript : svyScripts) {
|
Map<String, Object> 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(<b>只取顶层</b>,复现原服务方法拼树后的返回值)。
|
*
|
* @param type 满意度类型;为空时用 {@code "myd"}
|
*/
|
private List<Long> selectIvrTopCategoryIds(String type) {
|
SatisfactionScriptQuery query = new SatisfactionScriptQuery();
|
query.setType(StringUtils.isEmpty(type) ? "myd" : type);
|
// 原文没有传 orgid,这里保持一致(orgid 为空时 SQL 不拼该条件)
|
List<SatisfactionCategoryRow> rows = satisfactionScriptMapper.selectIvrCategoryList(query);
|
List<Long> 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(<b>取全部</b>,复现原 Mapper 调用的返回值)。
|
*
|
* @param type 满意度类型;为空时用 {@code "myd"}
|
*/
|
private List<Long> selectSvyCategoryIds(String type) {
|
SatisfactionScriptQuery query = new SatisfactionScriptQuery();
|
query.setType(StringUtils.isEmpty(type) ? "myd" : type);
|
List<SatisfactionCategoryRow> rows = satisfactionScriptMapper.selectSvyCategoryList(query);
|
List<Long> ids = new ArrayList<>();
|
if (CollectionUtils.isNotEmpty(rows)) {
|
for (SatisfactionCategoryRow row : rows) {
|
ids.add(row.getId());
|
}
|
}
|
return ids;
|
}
|
|
// ==================== 随访「满意度指数」 ====================
|
|
/**
|
* 「满意度题目发送量」统计时使用的子任务发送状态(与原实现一致:3、5、6)
|
*/
|
private static final List<Long> SATISFACTION_INDEX_SENDSTATES =
|
Collections.unmodifiableList(Arrays.asList(3L, 5L, 6L));
|
|
/**
|
* 随访统计页的「满意度指数」:满意度题目总量 / 填报量 / 完成比例。
|
* <p>
|
* 迁移说明:原为 {@code com.smartor.service.impl.ServiceSubtaskServiceImpl#getSatisfactionIndex}
|
* 的私有方法,被 {@code getSfStatisticsJoy} 调用。它的取数对象是
|
* 「满意度题目」(按 {@code ivrCategoryid} / {@code svyCategoryid} 识别)与
|
* 「满意度填报明细」,属于满意度口径,故搬入本模块。
|
* <p>
|
* <b>与原实现逐条对应:</b>
|
* <ol>
|
* <li>{@code type="1"} 用 {@code deptcode} 匹配任务;{@code type="2"} 用
|
* {@code leavehospitaldistrictcode} 匹配任务(互斥,都带 orgid);</li>
|
* <li>语音任务:题目数 = 该模板下 {@code script_assortid = ivrCategoryid} 的题目数;
|
* 总量 += 题目数 × 发送量({@code sendstate in (3,5,6)});</li>
|
* <li>问卷任务:同上,用 {@code categoryid = svyCategoryid};</li>
|
* <li>填报量:按 {@code taskid + categoryid(按任务类型取语音/问卷) + orgid} 数明细条数;</li>
|
* <li>完成比例 = 填报量 / 总量(保留 2 位,HALF_UP),总量为 0 时为 {@code 0.0}。</li>
|
* </ol>
|
*
|
* <h3>⚠️ 原实现已知缺陷(本次<b>原样保留</b>,未修复)</h3>
|
* 原代码用的是 {@code mydtmzlsum.add(new BigDecimal(n))} ——
|
* {@link BigDecimal} 是不可变类,{@code add} 的返回值没有被赋回变量,
|
* 因此<b>所有累加都是无效的</b>,本方法实际上<b>恒定返回</b>
|
* {@code {mydtmzl: 0, mydtbl: 0, wcbl: 0.0}};
|
* 同时因为累加语句仍在循环里,<b>每次调用依旧会真实执行那 5 条查询</b>。
|
* <p>
|
* 迁移时按"行为不变"原则一字未改(连无效累加也保留):
|
* 若直接把查询结果改成常量,虽然返回值相同,但会改变数据库压力;
|
* 若把 {@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<String, Object> getSatisfactionIndex(String type, String code, String orgid,
|
Long ivrCategoryid, Long svyCategoryid) {
|
// 原文是 type.equals("1");调用方只传字面量 "1"/"2",这里用常量在前等价且更安全
|
List<SatisfactionTaskRow> 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<String, Object> 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;
|
}
|
}
|