| | |
| | | //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) |
| | | List<SatScriptRef> scriptRefs = new ArrayList<>(); |
| | | for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScripts) { |
| | | scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent())); |
| | | scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent(), ivrLibaTemplateScript.getDimension())); |
| | | } |
| | | buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals); |
| | | } |
| | |
| | | List<SatScriptRef> scriptRefs = new ArrayList<>(); |
| | | for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScripts) { |
| | | // 问卷的选项表按 svy_lib_template_script 的主键关联 |
| | | scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent())); |
| | | scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent(), svyLibTemplateScript.getDimension())); |
| | | } |
| | | buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals); |
| | | } |
| | |
| | | //3 一次统计全部问题的发送量/填报量/选项分值(原来是每个问题各查 3 次) |
| | | List<SatScriptRef> scriptRefs = new ArrayList<>(); |
| | | for (SatisfactionIvrScriptRow ivrLibaTemplateScript : ivrLibaTemplateScripts) { |
| | | scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent())); |
| | | scriptRefs.add(new SatScriptRef(ivrLibaTemplateScript.getTemplateid(), ivrLibaTemplateScript.getScriptid(), ivrLibaTemplateScript.getScriptContent(), ivrLibaTemplateScript.getDimension())); |
| | | } |
| | | buildSatisfactionDetailEntities(patSatisfactionReqVO, "1", true, scriptRefs, patSatisfactionDetailEntities, personTotals); |
| | | } |
| | |
| | | List<SatScriptRef> scriptRefs = new ArrayList<>(); |
| | | for (SatisfactionSvyScriptRow svyLibTemplateScript : svyLibTemplateScripts) { |
| | | // 问卷的选项表按 svy_lib_template_script 的主键关联 |
| | | scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent())); |
| | | scriptRefs.add(new SatScriptRef(svyLibTemplateScript.getSvyid(), svyLibTemplateScript.getId(), svyLibTemplateScript.getScriptContent(), svyLibTemplateScript.getDimension())); |
| | | } |
| | | buildSatisfactionDetailEntities(patSatisfactionReqVO, "2", false, scriptRefs, patSatisfactionDetailEntities, personTotals); |
| | | } |
| | |
| | | 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); |
| | | //填报数量(原来是把明细行全部查回 Java 再统计) |
| | | //填报【行数】:用于平均分 / 最高分 / 最低分 / 各选项占比(这些口径本来就按明细行计) |
| | | 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()); |
| | | detail.setAnswerPerson((int) fillCount); |
| | | detail.setNoAnswerPerson(fillCount > 0L && sendCount > 0L ? (int) (sendCount - fillCount) : sendCount > 0L ? (int) sendCount : 0); |
| | | //维度:原本查询里已取到,但构造 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 : fillCount == 0L ? 0 : fillCount * 1.0 / sendCount); |
| | | detail.setAnswerRate(sendCount == 0L ? 0 : answerPersonCount == 0L ? 0 : answerPersonCount * 1.0 / sendCount); |
| | | detail.setMatchedtextStats(buildMatchedtextStats(fillRows, fillCount)); |
| | | out.add(detail); |
| | | |
| | | totals[0] += (int) fillCount; |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | /** |
| | | * 获取平均分(聚合版:分值与条数) |
| | | * <p> |
| | | * 口径与 {@link #averageScore(List)} 一致:分子为可解析分值之和,分母为明细行数(含分值为空的行)。 |
| | | * 分子为可解析分值之和,分母为明细行数(含分值为空的行)。 |
| | | */ |
| | | private Double averageScoreByMatchedtext(List<MatchedtextCount> fillRows) { |
| | | if (CollectionUtils.isEmpty(fillRows)) return 0.0; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 获取最高分(聚合版,口径同 {@link #maxScore(List)}:空列表返回 0.0) |
| | | * 获取最高分(聚合版,空列表返回 0.0) |
| | | */ |
| | | private Double maxScoreOf(List<String> scores) { |
| | | if (CollectionUtils.isEmpty(scores)) return 0.0; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 获取最低分(聚合版,口径同 {@link #minScore(List)}:空列表返回 0.0) |
| | | * 获取最低分(聚合版,口径同 {空列表返回 0.0) |
| | | */ |
| | | private Double minScoreOf(List<String> scores) { |
| | | if (CollectionUtils.isEmpty(scores)) return 0.0; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 满意度统计用的问题引用:模板id、选项表关联的问题id(可能为 null)、问题内容 |
| | | * 满意度统计用的问题引用:模板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) { |
| | | SatScriptRef(Long templateId, Long optionScriptId, String scriptContent, String dimension) { |
| | | this.templateId = templateId; |
| | | this.optionScriptId = optionScriptId; |
| | | this.scriptContent = scriptContent; |
| | | this.dimension = dimension; |
| | | } |
| | | |
| | | Long getTemplateId() { |
| | |
| | | |
| | | String getScriptContent() { |
| | | return scriptContent; |
| | | } |
| | | |
| | | String getDimension() { |
| | | return dimension; |
| | | } |
| | | } |
| | | |
| | |
| | | // 原实现直接 new smartor 的实体当入参;模块化后统一用 SatisfactionScriptQuery, |
| | | // 这里把三处构造收敛为帮助方法,条件与原来逐字对应。 |
| | | |
| | | /** 满意度分类查询(orgid + type=myd) */ |
| | | /** |
| | | * 满意度分类查询(语音侧)。 |
| | | * <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()); |
| | | q.setType("myd"); |
| | | return q; |
| | | } |
| | | |
| | | /** 满意度分类查询(问卷侧,条件同语音侧) */ |
| | | /** 满意度分类查询(问卷侧,同样只传 orgid,见 {@link #ivrCategoryQuery}) */ |
| | | private SatisfactionScriptQuery svyCategoryQuery(PatSatisfactionReqVO reqVO) { |
| | | SatisfactionScriptQuery q = new SatisfactionScriptQuery(); |
| | | q.setOrgid(reqVO.getOrgid()); |
| | | q.setType("myd"); |
| | | return q; |
| | | } |
| | | |