liusheng
8 小时以前 c24d16a37523a49080392610357872ce580cbc38
ruoyi-satisfaction/src/main/java/com/ruoyi/satisfaction/service/impl/SatisfactionSubtaskServiceImpl.java
@@ -42,6 +42,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
@@ -69,12 +70,58 @@
    @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) {
        //先清理一下重复数据
    /**
     * 重复明细清理的“最近一次执行时间”(毫秒)。见 {@link #cleanRepeatDetailsIfDue()}。
     */
    private static final AtomicLong LAST_REPEAT_CLEAN_AT = new AtomicLong(0L);
    /**
     * 重复明细清理的最小间隔:10 分钟。
     */
    private static final long REPEAT_CLEAN_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10);
    /**
     * 清理 service_subtask_detail 里的重复明细(同一 sub_id + questiontext 只留最新一条)。
     * <p>
     * ⚠️ <b>为什么要有节流</b>:{@code selectRepeatDetailIds} 是一条
     * 「对整张 service_subtask_detail 按 (sub_id, questiontext) 分组 + HAVING COUNT(*) &gt; 1」的
     * 全表聚合,且<b>没有任何日期/病区/子任务范围</b>;一旦查出重复还会执行逻辑删除(写操作)。
     * 原实现在每次满意度统计请求前都无条件跑一遍,于是:
     * <ul>
     *   <li>统计接口的性能与用户所选条件<b>完全无关</b>,明细表越大越慢;</li>
     *   <li>一个只读接口在偷偷做 DML。</li>
     * </ul>
     * 而统计 SQL 本身已经用 {@code d.id = (SELECT MAX(...))} 做了“同题只取最新一条”的去重,
     * 所以这个前置清理对本次统计结果<b>不是必需</b>的。这里改成“每个实例最多 10 分钟清一次”,
     * 既保留清理能力,又不让每个请求都为它买单。
     * <p>
     * 更彻底的做法是把它挪到定时任务里(只留一个维护入口),需要的话可以再调整。
     */
    private void cleanRepeatDetailsIfDue() {
        long now = System.currentTimeMillis();
        long last = LAST_REPEAT_CLEAN_AT.get();
        if (now - last < REPEAT_CLEAN_INTERVAL_MS) {
            return;
        }
        // CAS 抢占:并发请求里只放一个线程去清理,其余直接返回
        if (!LAST_REPEAT_CLEAN_AT.compareAndSet(last, now)) {
            return;
        }
        try {
        List<Long> repeatIds = satisfactionMapper.selectRepeatDetailIds();
        if (!CollectionUtils.isEmpty(repeatIds)) {
            satisfactionMapper.deleteRepeatDetailByIds(repeatIds);
                log.info("满意度统计前置清理:逻辑删除重复明细 {} 条", repeatIds.size());
        }
        } catch (Exception e) {
            // 清理失败不能影响统计本身
            log.warn("满意度统计前置清理重复明细失败,已跳过本次清理", e);
        }
    }
    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) {
        //前置清理重复明细(已节流:每个实例最多 10 分钟一次,不再每个请求都跑全表 GROUP BY)
        cleanRepeatDetailsIfDue();
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds);
        // 处理 targetvalue:按 & 分割成多条记录
@@ -271,9 +318,10 @@
            return Collections.emptyMap();
        }
        // 2. 判断按科室还是按病区
        boolean byDept = vo.getLeaveldeptcodes() != null;
        boolean byDistrict = !byDept && vo.getLeavehospitaldistrictcodes() != null;
        // 2. 判断按科室还是按病区(离院 / 在院两组参数都算)
        boolean byDept = vo.getLeaveldeptcodes() != null || vo.getDeptcodes() != null;
        boolean byDistrict = !byDept
                && (vo.getLeavehospitaldistrictcodes() != null || vo.getHospitaldistrictcodes() != null);
        if (!byDept && !byDistrict) {
            return Collections.emptyMap();
        }
@@ -384,11 +432,8 @@
    }
    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);
        }
        //前置清理重复明细(已节流:每个实例最多 10 分钟一次,不再每个请求都跑全表 GROUP BY)
        cleanRepeatDetailsIfDue();
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = satisfactionMapper.statQuestionOptionBySubtaskIds(subtaskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
        // SQL 已按「题目 + 选项」汇总数量,这里再按题目合并(不同模板的 targetvalue 选项顺序不同,同一选项会拆成多行),