liusheng
2026-11-03 b46b8b8fee4d10e52dd882febc07974b02e2aed9
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceSubtaskServiceImpl.java
@@ -538,9 +538,9 @@
    public List<ServiceSubtask> getServiceSubtasks(ServiceSubtaskEntity serviceSubtaskEntity) {
        if (serviceSubtaskEntity.getSendstateView() != null) {
            //            1:待随访(1 被领取、2 待发送、3 已发送、5 发送失败、7、超时);2:已完成( 6 已完成)、3:无需随访(4 不执行)
            //            1:待随访(1 被领取、2 待发送、3 已发送、7 超时;发送失败(5) 单独统计);2:已完成( 6 已完成)、3:无需随访(4 不执行)
            if (serviceSubtaskEntity.getSendstateView() == 1)
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 5L, 7L)));
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 7L)));
            if (serviceSubtaskEntity.getSendstateView() == 2)
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(6L)));
            if (serviceSubtaskEntity.getSendstateView() == 3)
@@ -809,6 +809,50 @@
    }
    /**
     * 统一「随访病区」口径:一律以【出院记录】{@code pat_med_inhosp} 为准(2026-10-29 业务确认)。
     *
     * <h3>为什么需要它</h3>
     * 出院随访子任务的科室/病区本应来自出院记录,但「再次随访 / 手工新增」这两个入口是
     * <b>直接按前端传来的字段落库</b>的:前端不带这些字段时
     * {@code leaveldeptcode} / {@code leavehospitaldistrictcode} / {@code campusid} 就是空。
     * 而随访列表是按科室/病区过滤的({@code and leavehospitaldistrictcode = #{...}}),
     * 空值不匹配任何值 → 这些任务"生成了但页面上查不到"(线上 2026-10 实测 314 条)。
     *
     * <p>口径:<b>病区只认出院记录</b> —— 不按患者档案反推、也不用前端传的值。
     * 只对出院随访({@code service_type='2'})且能定位到出院记录({@code inhospid} 非空)的行生效;
     * 其余情况原样放行,对满意度(6)/宣教/门诊等其它类型没有任何影响。
     *
     * @param serviceSubtask 待落库的子任务(就地修改)
     */
    private void alignDischargeWardFromInhosp(ServiceSubtask serviceSubtask) {
        if (serviceSubtask == null || serviceSubtask.getInhospid() == null) {
            return;
        }
        if (!"2".equals(serviceSubtask.getServiceType())) {
            return;
        }
        PatMedInhosp inhosp;
        try {
            inhosp = inhospVisitFacade.selectPatMedInhospByInhospid(serviceSubtask.getInhospid());
        } catch (Exception e) {
            log.warn("【随访任务】按 inhospid={} 取出院记录失败,病区沿用入参:{}",
                    serviceSubtask.getInhospid(), e.toString());
            return;
        }
        if (inhosp == null) {
            log.warn("【随访任务】inhospid={} 查不到出院记录,病区沿用入参", serviceSubtask.getInhospid());
            return;
        }
        serviceSubtask.setLeaveldeptcode(inhosp.getLeaveldeptcode());
        serviceSubtask.setLeaveldeptname(inhosp.getLeaveldeptname());
        serviceSubtask.setLeavehospitaldistrictcode(inhosp.getLeavehospitaldistrictcode());
        serviceSubtask.setLeavehospitaldistrictname(inhosp.getLeavehospitaldistrictname());
        if (StringUtils.isNotEmpty(inhosp.getCampusid())) {
            serviceSubtask.setCampusid(inhosp.getCampusid());
        }
    }
    /**
     * 新增单一任务(随访)
     *
     * @param serviceSubtask 单一任务(随访)
@@ -850,6 +894,8 @@
            }
            serviceSubtask.setCreateTime(DateUtils.getNowDate());
            // 口径(2026-10-29 业务确认):出院随访的科室/病区一律以出院记录为准
            alignDischargeWardFromInhosp(serviceSubtask);
            serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
            //修改住院满意度状态(避免重复查询)
            if (serviceSubtask.getServiceType() != null && "6".equals(serviceSubtask.getServiceType())) {
@@ -943,6 +989,13 @@
        serviceSubtask2.setVisitType(serviceSubtask.getVisitType());
        serviceSubtask2.setVisitDeptCode(serviceSubtask.getVisitDeptCode());
        serviceSubtask2.setVisitDeptName(serviceSubtask.getVisitDeptName());
        // 口径(2026-10-29 业务确认):随访病区一律取出院记录的
        //   ① 再次随访是从请求 DTO 复制出来的,出院记录ID 不在请求里 → 先用原任务的上;
        //   ② 再按出院记录把科室/病区/院区统一过来(取不到就保持不变)。
        if (serviceSubtask2.getInhospid() == null) {
            serviceSubtask2.setInhospid(serviceSubtask1.getInhospid());
        }
        alignDischargeWardFromInhosp(serviceSubtask2);
        int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtask2);
        if (i == 1) {
            //新增成功后,需要将之前的任务设置为不要再随访(防止重复点,然后重复生成)
@@ -3206,6 +3259,20 @@
        serviceSubtaskCountReq.setGroupKey(groupKey);
        // 分组列与过滤列保持一致:离院科室 leaveldeptcode、在院科室 deptcode
        serviceSubtaskCountReq.setGroupColumn(groupKey);
        /*
         * 满意度「病区统计」(离院维度):出院病区为空则回退到在院病区。
         * 满意度子任务(service_type 6/14/15/16)的 leavehospitaldistrictcode 约三成为空,
         * 之前为了“查得到数据”把病区条件写成 `xxx IS NULL OR xxx IN (...)`,
         * 于是选了 A 病区也会把空病区的全量数据带进来,再统一归到 'Unknown' 组,
         * 该组的病区名又是 MAX(hospitaldistrictname) 随便挑的 —— 表现就是
         * “选了血液内科病区,却查出一行显示骨科中心三病区”。
         * 用“有效病区”列过滤 + 分组后:筛选真正生效,空病区的记录也回到它真实的在院病区。
         */
        if ("leavehospitaldistrictcode".equals(groupKey)) {
            String districtColumn = "IFNULL(leavehospitaldistrictcode, hospitaldistrictcode)";
            serviceSubtaskCountReq.setGroupColumn(districtColumn);
            serviceSubtaskCountReq.setDistrictColumn(districtColumn);
        }
        //分组键集合:这一步保持不变(分页参数照旧作用在该查询上),保证“参与统计的分组”与改造前一致
        List<String> groupKeyList = serviceSubtaskMapper.getSfStatisticsGroupKey(serviceSubtaskCountReq);
        /*
@@ -3280,6 +3347,24 @@
            serviceSubtaskStatistics.add(statistic);
        }
        /*
         * 病区统计:这一行的病区就是分组键本身,展示名取该组自己的病区名。
         * 聚合 SQL 里原来的 MAX(hospitaldistrictname) 在“一个组混了多个病区”时会挑出
         * 一个毫不相干的病区名(选血液内科病区显示成骨科中心三病区就是这么来的)。
         * 这里把分组键回填到在院/离院两套病区字段,保证前端不论读哪一列都是同一个病区,
         * 也让“查看详情”带上来的病区编码能查回同一批数据。
         */
        if (StringUtils.isNotEmpty(serviceSubtaskCountReq.getDistrictColumn())) {
            for (ServiceSubtaskStatistic statistic : serviceSubtaskStatistics) {
                String districtCode = statistic.getGroupCode() == null ? "Unknown" : statistic.getGroupCode();
                String districtName = StringUtils.isNotEmpty(statistic.getGroupName()) ? statistic.getGroupName() : districtCode;
                statistic.setLeavehospitaldistrictcode(districtCode);
                statistic.setLeavehospitaldistrictname(districtName);
                statistic.setHospitaldistrictcode(districtCode);
                statistic.setHospitaldistrictname(districtName);
            }
        }
        //获取及时率统计合集(一次批量查询,不按分组循环查)
        Map<String, Map<String, Object>> jsRates = getSsRateForSfStatistics(serviceSubtaskCountReq, statisticsGroupKeys);
        for (ServiceSubtaskStatistic statistic : serviceSubtaskStatistics) {
@@ -3309,7 +3394,9 @@
                    sum = new BigDecimal(serviceSubtaskStatistic.getJoyAllCount());
                }
                if (sum.intValue() != 0) {
                    finishedPercentage = finishedSum.divide(sum, 2, RoundingMode.HALF_UP).doubleValue();
                    // 原来只保留 2 位小数(23634/26106 会被截成 0.91),前端再 ×100
                    // 就显示成 91.00%,与实际的 90.53% 差了半个百分点。这里保留 4 位。
                    finishedPercentage = finishedSum.divide(sum, 4, RoundingMode.HALF_UP).doubleValue();
                }
                serviceSubtaskStatistic.setJoyAllCount(sum.intValue());
                serviceSubtaskStatistic.setJoyCount(finishedSum.intValue());