liusheng
2026-10-10 0afac42fb2e9d6e5291304dd0044ce3042b45d2b
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java
@@ -207,9 +207,9 @@
    /**
     * 省立同德满意度数据统计(按题目统计选项数量/占比,并给出该题的综合得分)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%,
     * 分值取 1、0.8、0.6、0.4、0.2 五档(很满意、满意、一般、不满意、很不满意);
     * 「不适用」、0 分、空值等既不进分子也不进分母。
     * 综合满意度 = (很满意项数×1 + 满意项数×0.8 + 一般项数×0.6 + 不满意项数×0.4 + 很不满意项数×0.2)
     * / (很满意项数 + 满意项数 + 一般项数 + 不满意项数 + 很不满意项数) × 100%;
     * 「未经历」「不适用」等不在五档里的选项既不进分子也不进分母。
     */
    @ApiOperation("省立同德满意度数据统计")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
@@ -256,7 +256,7 @@
                    QuestionResultDTO questionResultDTO = new QuestionResultDTO();
                    questionResultDTO.setQeustionText(questionText);
                    questionResultDTO.setSubtaskDetailRatioExportList(exportList);
                    //综合得分:Σ(分值×项数) / Σ项数 / 本题最高分值 × 100%,不适用/0分/空值不进分子也不进分母
                    //综合得分:(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / 五档项数之和 × 100%
                    questionResultDTO.setAvgScore(calcMydScore(exportList));
                    result.add(questionResultDTO);
                });
@@ -297,11 +297,25 @@
    private static final String VALUE_TYPE_MULTIPLE = "2";
    /**
     * 满意度五档选项(顺序与权重顺序一致)
     */
    private static final String[] MYD_SCORE_OPTIONS = {"很满意", "满意", "一般", "不满意", "很不满意"};
    /**
     * 满意度五档权重:很满意 1、满意 0.8、一般 0.6、不满意 0.4、很不满意 0.2
     */
    private static final BigDecimal[] MYD_SCORE_WEIGHTS = {
            new BigDecimal("1"), new BigDecimal("0.8"), new BigDecimal("0.6"), new BigDecimal("0.4"), new BigDecimal("0.2")};
    /**
     * 计算一道题的满意度综合得分(百分制,保留两位小数)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%
     * 等价于 Σ(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / Σ(五档项数) × 100%。
     * 用本题最高分值归一化,兼容明细分值存 0-1 权重(1、0.8…)和存 5 分制(5、4…)两种口径。
     * 综合满意度 = (很满意项数×1 + 满意项数×0.8 + 一般项数×0.6 + 不满意项数×0.4 + 很不满意项数×0.2)
     * / (很满意项数 + 满意项数 + 一般项数 + 不满意项数 + 很不满意项数) × 100%
     * <p>
     * 权重按选项文案判定(见 {@link #mydScoreOptionIndex(String)}),不再依赖明细的 score 字段:
     * 历史数据里 score 有 0、「0000000001」这类脏值,按 score 算会把得分固定成 0 或 100。
     * 「未经历」「不适用」等不在五档里的选项既不进分子也不进分母。
     *
     * @param exportList 同一道题目的选项统计明细
     * @return 百分制综合得分,无有效项数时返回 "0.00"
@@ -309,9 +323,8 @@
    private String calcMydScore(List<ServiceSubtaskDetailRatioExport> exportList) {
        if (CollectionUtils.isEmpty(exportList)) return "0.00";
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(选项分值 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:Σ项数
        BigDecimal maxScore = null;              //本题最高分值,用于归一化
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(档位权重 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:五档项数之和
        for (ServiceSubtaskDetailRatioExport detail : exportList) {
            //多选题一行是多个选项的分值之和,分母只算 1 项,会把得分算高,不计入综合得分
@@ -319,27 +332,42 @@
            //「不适用」不在公式的五档里,不计入综合得分
            if (isNotApplicable(detail)) continue;
            BigDecimal score = parseDecimal(detail.getScore());
            int optionIndex = mydScoreOptionIndex(detail.getOptionresult());
            BigDecimal count = parseDecimal(detail.getCount());
            if (score == null || count == null) continue;
            //0 分(未答/空答)、空数量:既不进分子也不进分母
            if (score.signum() <= 0 || count.signum() <= 0) continue;
            if (optionIndex < 0 || count == null || count.signum() <= 0) continue;
            if (maxScore == null || score.compareTo(maxScore) > 0) maxScore = score;
            scoreSum = scoreSum.add(score.multiply(count));
            scoreSum = scoreSum.add(MYD_SCORE_WEIGHTS[optionIndex].multiply(count));
            countSum = countSum.add(count);
        }
        if (maxScore == null || countSum.signum() <= 0) return "0.00";
        if (countSum.signum() <= 0) return "0.00";
        return scoreSum.divide(countSum, 6, RoundingMode.HALF_UP)
                .divide(maxScore, 6, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP)
                .toPlainString();
    }
    /**
     * 选项文案 → 满意度五档下标(很满意 0、满意 1、一般 2、不满意 3、很不满意 4),不在五档里返回 -1
     */
    private int mydScoreOptionIndex(String optionresult) {
        if (StringUtils.isEmpty(optionresult)) return -1;
        String text = optionresult.trim();
        for (int i = 0; i < MYD_SCORE_OPTIONS.length; i++) {
            if (MYD_SCORE_OPTIONS[i].equals(text)) return i;
        }
        //兼容「A 很满意」「B.满意」这类带选项序号前缀的历史数据
        if (text.length() > 2 && (text.charAt(1) == ' ' || text.charAt(1) == '.' || text.charAt(1) == '、')) {
            String stripped = text.substring(2).trim();
            for (int i = 0; i < MYD_SCORE_OPTIONS.length; i++) {
                if (MYD_SCORE_OPTIONS[i].equals(stripped)) return i;
            }
        }
        return -1;
    }
    /**
     * 是否「不适用」选项
     */
    private boolean isNotApplicable(ServiceSubtaskDetailRatioExport detail) {