liusheng
6 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.ruoyi.satisfaction.service.strategy;
 
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.satisfaction.domain.ServiceSubtaskDetailRatioExport;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
 
/**
 * 省立同德口径的综合满意度得分策略(五档加权,百分制)。
 * <p>
 * 算法从原 {@code ServiceSubtaskDetailController#calcMydScore} 及其 4 个私有助手
 * <b>原样搬入</b>(一行未改,仅把返回值由 {@code String} 改为 {@link BigDecimal} 以便统一策略签名;
 * "无有效项数"这类情况由返回 {@code null} 表达,调用方仍回退为 {@code "0.00"},对外结果不变)。
 * <p>
 * 口径:
 * <pre>
 *   综合满意度 = (很满意数×1 + 满意数×0.8 + 一般数×0.6 + 不满意数×0.4 + 很不满意数×0.2)
 *              / (五档项数之和) × 100%,保留 2 位
 * </pre>
 * <b>为什么按选项文案判档、而不是用明细的 {@code score} 字段</b>:
 * 历史数据里 {@code score} 有 {@code 0}、{@code 0000000001} 这类脏值,
 * 按 {@code score} 算会把得分固定成 0 或 100。
 * <p>
 * 跳过的行:多选行({@code valueType=2})、以及「不适用 / 未答 / 拒答」(不在五档里,
 * 既不进分子也不进分母)。
 */
@Component
public class SltdSatisfactionScoreStrategy implements SatisfactionScoreStrategy {
 
    /**
     * 多选题的问题类型
     */
    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")};
 
    @Override
    public String site() {
        return SatisfactionSites.SLTD;
    }
 
    @Override
    public String siteName() {
        return "省立同德口径";
    }
 
    /**
     * 计算一道题的满意度综合得分(百分制,保留两位小数)。
     *
     * @param exportList 同一道题目的选项统计明细
     * @return 百分制综合得分;无有效项数时返回 {@code null}(调用方呈现为 "0.00")
     */
    @Override
    public BigDecimal comprehensiveScore(List<ServiceSubtaskDetailRatioExport> exportList) {
        if (CollectionUtils.isEmpty(exportList)) return null;
 
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(档位权重 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:五档项数之和
 
        for (ServiceSubtaskDetailRatioExport detail : exportList) {
            //多选题一行是多个选项的分值之和,分母只算 1 项,会把得分算高,不计入综合得分
            if (VALUE_TYPE_MULTIPLE.equals(detail.getValueType())) continue;
            //「不适用」不在公式的五档里,不计入综合得分
            if (isNotApplicable(detail)) continue;
 
            int optionIndex = mydScoreOptionIndex(detail.getOptionresult());
            BigDecimal count = parseDecimal(detail.getCount());
            if (optionIndex < 0 || count == null || count.signum() <= 0) continue;
 
            scoreSum = scoreSum.add(MYD_SCORE_WEIGHTS[optionIndex].multiply(count));
            countSum = countSum.add(count);
        }
 
        if (countSum.signum() <= 0) return null;
 
        return scoreSum.divide(countSum, 6, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP);
    }
 
    /**
     * 选项文案 → 满意度五档下标(很满意 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) {
        return containsNotApplicable(detail.getOptionresult()) || containsNotApplicable(detail.getTargetvalue());
    }
 
    private boolean containsNotApplicable(String optionText) {
        if (StringUtils.isEmpty(optionText)) return false;
        String text = optionText.trim();
        return text.contains("不适用") || text.contains("未答") || text.contains("拒答");
    }
 
    /**
     * 字符串转 BigDecimal,空值/非法值返回 null
     */
    private BigDecimal parseDecimal(String value) {
        if (StringUtils.isEmpty(value)) return null;
        try {
            return new BigDecimal(value.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
}