liusheng
2026-09-28 278a56025b6e64e49a3453f0b9be84832de96066
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java
@@ -1,13 +1,22 @@
package com.ruoyi.web.controller.smartor;
import com.ruoyi.common.annotation.AddOrgId;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.DtoConversionUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysConfigService;
import com.smartor.domain.DTO.QuestionResultDTO;
import com.smartor.domain.*;
import com.smartor.domain.DTO.ServiceSubtaskDetailDTO;
import com.smartor.domain.VO.MydTrendReqVO;
import com.smartor.domain.VO.MzMydScoreRankVO;
import com.smartor.domain.VO.QuestionCountKsOrBqVO;
import com.smartor.domain.VO.SltdMydTotalVO;
import com.smartor.domain.entity.ServiceSubtaskEntity;
import com.smartor.service.IServiceSubtaskDetailService;
import com.smartor.service.IServiceSubtaskService;
import io.swagger.annotations.Api;
@@ -16,7 +25,12 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
import static com.ruoyi.framework.datasource.DynamicDataSourceContextHolder.log;
/**
 * 语音任务结果明细Controller
@@ -32,6 +46,9 @@
    private IServiceSubtaskDetailService ServiceSubtaskDetailService;
    @Autowired
    private IServiceSubtaskService serviceSubtaskService;
    @Autowired
    private ISysConfigService configService;
    /**
     * 查询单个人的语音信息
@@ -156,11 +173,223 @@
    /**
     * 获取随访详情-指标详情(超链接)
     */
    @ApiOperation("获取随访详情-指标详情(超链接)")
    @ApiOperation("获取随访详情-指标详情")
    @PostMapping("/getServiceSubtaskDetails")
    public List<Map<String, Object>> getServiceSubtaskDetails(@RequestBody ServiceSubtaskDetailVO serviceSubtaskDetailVO) {
        List<Map<String, Object>> map = new ArrayList<>();
        map = ServiceSubtaskDetailService.getServiceSubtaskDetails(serviceSubtaskDetailVO);
        return map;
    }
    /**
     * 获取随访详情-指标详情(超链接)
     */
    @ApiOperation("获取随访详情-指标详情(超链接)")
    @PostMapping("/getServiceSubtaskDetailsHyperlink")
    public List<Map<String, Object>> getServiceSubtaskDetailsHyperlink(@RequestBody ServiceSubtaskDetailVO serviceSubtaskDetailVO) {
        LoginUser loginUser = getLoginUser();
        String orgid = loginUser.getUser().getOrgid();
        String configKey = serviceSubtaskDetailVO.getConfigKey();
        if (StringUtils.isNotEmpty(configKey)) {
            String configValue = configService.selectConfigByKey(configKey, orgid);
            serviceSubtaskDetailVO.setConfigValue(configValue);
            if (StringUtils.isEmpty(configValue)) {
                String logInfo = "getSfStatisticsCount-参数 " + configKey + " 未配置, 请配置好后重试";
                log.error(logInfo);
            }
        }
        List<Map<String, Object>> map = new ArrayList<>();
        map = ServiceSubtaskDetailService.getServiceSubtaskDetails(serviceSubtaskDetailVO);
        return map;
    }
    /**
     * 省立同德满意度数据统计(按题目统计选项数量/占比,并给出该题的综合得分)
     * <p>
     * 综合满意度 = (很满意项数×1 + 满意项数×0.8 + 一般项数×0.6 + 不满意项数×0.4 + 很不满意项数×0.2)
     * / (很满意项数 + 满意项数 + 一般项数 + 不满意项数 + 很不满意项数) × 100%;
     * 「未经历」「不适用」等不在五档里的选项既不进分子也不进分母。
     */
    @ApiOperation("省立同德满意度数据统计")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
    @PostMapping("/sltdMydTotalByScore")
    public Map<String, Object> sltdMydTotalByScore(@RequestBody SltdMydTotalVO sltdMydTotalVO) {
        List<ServiceSubtaskRes> serviceSubtaskResList = null;
        if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList())) {
            //serviceType可能会出现多个,不想多写方法了,一个一个查询吧
            for (String st : sltdMydTotalVO.getServiceTypeList()) {
                ServiceSubtaskEntity serviceSubtaskEntity = new ServiceSubtaskEntity();
                serviceSubtaskEntity.setServiceType(st);
                serviceSubtaskEntity.setEndOutHospTime(sltdMydTotalVO.getEndOutHospTime());
                serviceSubtaskEntity.setStartOutHospTime(sltdMydTotalVO.getStartOutHospTime());
                serviceSubtaskEntity.setDeptOrDistrict(sltdMydTotalVO.getDeptOrDistrict());
                serviceSubtaskEntity.setLeavehospitaldistrictcodes(sltdMydTotalVO.getLeavehospitaldistrictcodes());
                serviceSubtaskEntity.setLeaveldeptcodes(sltdMydTotalVO.getLeaveldeptcodes());
                //机构/院区由 @AddOrgId 注入,落到查询条件上,避免跨机构取数
                serviceSubtaskEntity.setOrgid(sltdMydTotalVO.getOrgid());
                serviceSubtaskEntity.setCampusid(sltdMydTotalVO.getCampusid());
                if (serviceSubtaskEntity != null) {
                    List<ServiceSubtask> serviceSubtasks = serviceSubtaskService.getServiceSubtasks(serviceSubtaskEntity);
                    List<ServiceSubtaskRes> serviceSubtaskRes = DtoConversionUtils.sourceToTarget(serviceSubtasks, ServiceSubtaskRes.class);
                    if (CollectionUtils.isNotEmpty(serviceSubtaskResList)) {
                        serviceSubtaskResList.addAll(serviceSubtaskRes);
                    } else {
                        if (CollectionUtils.isEmpty(serviceSubtaskResList)) serviceSubtaskResList = serviceSubtaskRes;
                    }
                }
            }
        }
        // 按 questiontext 分组后转换为 QuestionResultVO
        List<QuestionResultDTO> result = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(serviceSubtaskResList)) {
            List<ServiceSubtaskDetailRatioExport> detailList = serviceSubtaskService.getDetailRatio(sltdMydTotalVO, serviceSubtaskResList);
            if (detailList == null) {
                //serviceType 组合不在支持范围内(如同时含随访 2 和满意度 6/14),给明确提示而不是悄悄返回空数组
                return error("当前 serviceType 组合暂不支持统计问题占比,请分开查询(仅支持随访 2 或满意度 6/14)");
            }
            if (CollectionUtils.isNotEmpty(detailList)) {
                Map<String, List<ServiceSubtaskDetailRatioExport>> groupedMap = detailList.stream().collect(Collectors.groupingBy(ServiceSubtaskDetailRatioExport::getQuestiontext));
                groupedMap.forEach((questionText, exportList) -> {
                    QuestionResultDTO questionResultDTO = new QuestionResultDTO();
                    questionResultDTO.setQeustionText(questionText);
                    questionResultDTO.setSubtaskDetailRatioExportList(exportList);
                    //综合得分:(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / 五档项数之和 × 100%
                    questionResultDTO.setAvgScore(calcMydScore(exportList));
                    result.add(questionResultDTO);
                });
            }
        }
        return success(result);
    }
    @ApiOperation("省立同德满意度数据统计通过科室或病区")
    @PostMapping("/statQuestionOptionByKsOrBq")
    public Map<String, Object> statQuestionOptionByKsOrBq(@RequestBody QuestionCountKsOrBqVO questionResultVO) {
        return success(serviceSubtaskService.statQuestionOptionByKsOrBq(questionResultVO));
    }
    @ApiOperation("省立同德患者满意度趋势")
    @PostMapping("/mydTrend")
    public Map<String, Object> mydTrend(@RequestBody MydTrendReqVO mydTrendReqVO) {
        return success(serviceSubtaskService.mydTrend(mydTrendReqVO));
    }
    @ApiOperation("省立同德患者满意度维护")
    @PostMapping("/sltdMydTotalByDimension")
    public Map<String, Object> sltdMydTotalByDimension(@RequestBody MydTrendReqVO mydTrendReqVO) {
        return success(serviceSubtaskService.sltdMydTotalByDimension(mydTrendReqVO));
    }
    @ApiOperation("门诊满意度综合得分排名(按科室或病区)")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
    @PostMapping("/mzMydScoreRank")
    public Map<String, Object> mzMydScoreRank(@RequestBody MzMydScoreRankVO mzMydScoreRankVO) {
        return success(serviceSubtaskService.mzMydScoreRank(mzMydScoreRankVO));
    }
    /**
     * 多选题的问题类型
     */
    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>
     * 综合满意度 = (很满意项数×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"
     */
    private String calcMydScore(List<ServiceSubtaskDetailRatioExport> exportList) {
        if (CollectionUtils.isEmpty(exportList)) return "0.00";
        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 "0.00";
        return scoreSum.divide(countSum, 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) {
        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;
        }
    }
}