liusheng
2026-11-03 b5d55e1dd793c01f08786f805f01345bca64c893
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceSubtaskServiceImpl.java
@@ -101,11 +101,8 @@
import com.ruoyi.task.mapper.ServiceOutPathMapper;
import com.ruoyi.task.service.IServiceOutPathService;
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
import com.ruoyi.task.api.spi.TaskPreachformPort;
import com.ruoyi.task.mapper.ServiceSubtaskPreachformMapper;
import com.ruoyi.task.api.spi.TaskArchivePort;
import com.ruoyi.task.api.spi.TaskFtpPort;
import com.ruoyi.task.api.spi.TaskSysConfigPort;
import com.ruoyi.task.api.spi.TaskSysDeptPort;
import com.ruoyi.task.common.ScheduleSubtaskBuilder;
import com.ruoyi.task.domain.ServiceTaskScheduleVO;
import com.ruoyi.task.service.IServiceSubtaskService;
@@ -124,6 +121,8 @@
import com.ruoyi.task.domain.ServiceSubtaskTemplateVO;
import com.ruoyi.task.domain.ServiceTaskVO;
import com.ruoyi.task.domain.VO.TransferDeptVO;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.mapper.ext.SysDept2Mapper;
/**
 * 单一任务(随访)Service业务层处理
@@ -166,7 +165,7 @@
    private ServiceSubtaskMapper serviceSubtaskMapper;
    @Autowired
    private TaskPreachformPort taskPreachformPort;
    private ServiceSubtaskPreachformMapper taskPreachformPort;
    @Autowired
    private ServiceSubtaskDetailMapper serviceSubtaskDetailMapper;
@@ -221,9 +220,6 @@
    @Autowired
    private ScheduleSubtaskBuilder scheduleSubtaskBuilder;
    @Autowired
    private TaskFtpPort taskFtpPort;
@@ -236,7 +232,7 @@
    @Autowired
    private TaskSysConfigPort taskSysConfigPort;
    private ISysConfigService sysConfigService;
    /**
     * 消息发送方工厂(短信/微信公众号),替代原来直接调用各站点发送代码的写法
@@ -255,7 +251,7 @@
    @Autowired
    private TaskSysDeptPort taskSysDeptPort;
    private SysDept2Mapper sysDept2Mapper;
    @Value("${spring.profiles.active}")
    private String active;
@@ -542,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)
@@ -813,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 单一任务(随访)
@@ -854,6 +894,8 @@
            }
            serviceSubtask.setCreateTime(DateUtils.getNowDate());
            // 口径(2026-10-29 业务确认):出院随访的科室/病区一律以出院记录为准
            alignDischargeWardFromInhosp(serviceSubtask);
            serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
            //修改住院满意度状态(避免重复查询)
            if (serviceSubtask.getServiceType() != null && "6".equals(serviceSubtask.getServiceType())) {
@@ -947,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) {
            //新增成功后,需要将之前的任务设置为不要再随访(防止重复点,然后重复生成)
@@ -1085,7 +1134,7 @@
            }
            serviceTask.setLeaveldeptcodes(serviceTaskVO.getLeaveldeptcodes());
            if (StringUtils.isEmpty(serviceTask.getLeavehospitaldistrictname()) && StringUtils.isNotEmpty(serviceTask.getLeavehospitaldistrictcode())) {
                SysDept sysDept = taskSysDeptPort.selectDeptByCode(serviceTask.getLeavehospitaldistrictcode(), null);
                SysDept sysDept = sysDept2Mapper.selectDeptByCode(serviceTask.getLeavehospitaldistrictcode(), null);
                serviceTask.setLeavehospitaldistrictname(sysDept.getDeptName());
            }
            if (serviceTaskVO.getLongTask() == 1) serviceTask.setSendState(2L);
@@ -3210,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);
        /*
@@ -3284,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) {
@@ -3313,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());
@@ -3574,6 +3657,55 @@
        return serviceSubtaskMapper.getCurrentUserServiceSubtaskCount(entity);
    }
    /**
     * 当前登录人待处理随访任务,按出院年月分组统计(出院随访、门诊随访分开计数)
     * <p>
     * 只返回有数据的月份,前端按「当月 + 前 3 个月」补齐,没有数据显示 0 个。
     */
    @Override
    public Map<String, Object> getPendingManualTaskMonthStats(ServiceSubtaskEntity entity) {
        List<Map<String, Object>> rows = serviceSubtaskMapper.countPendingManualTaskByMonth(entity);
        List<Map<String, Object>> months = new ArrayList<>();
        long total = 0L;
        if (rows != null) {
            for (Map<String, Object> row : rows) {
                if (row == null || row.get("month") == null) {
                    continue;
                }
                long dischargeCount = parsePendingCount(row.get("dischargeCnt"));
                long outpatientCount = parsePendingCount(row.get("outpatientCnt"));
                if (dischargeCount <= 0 && outpatientCount <= 0) {
                    continue;
                }
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("month", row.get("month").toString());
                item.put("dischargeCount", dischargeCount);
                item.put("outpatientCount", outpatientCount);
                months.add(item);
                total += dischargeCount + outpatientCount;
            }
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total", total);
        result.put("months", months);
        return result;
    }
    /**
     * 统计结果里的数量转 long,解析失败按 0 处理
     */
    private long parsePendingCount(Object value) {
        if (value == null) {
            return 0L;
        }
        try {
            return Long.parseLong(value.toString());
        } catch (NumberFormatException e) {
            log.warn("统计待处理随访任务失败,数量无法解析:{}", value);
            return 0L;
        }
    }
    @Override
    public Map<String, Object> getContinueNurseCount(ServiceSubtaskCotinueCountVO vo) {
        boolean hasDistrict = !CollectionUtils.isEmpty(vo.getLeavehospitaldistrictcodes());
@@ -3641,8 +3773,8 @@
    public void setFailSubTaskDefalutVal(Long failDay) {
        //1.先去sys_config中,获取哪些科室或病区需要默认问卷默认值
        IConfigService configService = SpringUtils.getBean(IConfigService.class);
        String deptConfigValue = taskSysConfigPort.selectConfigValue("hosp.default.dept");
        String wardConfigValue = taskSysConfigPort.selectConfigValue("hosp.default.ward");
        String deptConfigValue = sysConfigService.selectConfigByKey("hosp.default.dept");
        String wardConfigValue = sysConfigService.selectConfigByKey("hosp.default.ward");
        List<String> leaveWard = null;
        List<String> leaveDept = null;
        if (deptConfigValue != null && !deptConfigValue.isEmpty()) {
@@ -3870,8 +4002,8 @@
        // 参数值经端口取用(原为 sysConfigMapper.checkConfigKeyUnique + getConfigValue)。
        // 语义等价:原判定"行存在"、这里判定"值非 null",而 sys_config.config_value 是
        // varchar(500) NOT NULL DEFAULT '' ⇒ 二者不可能不同(详见 TaskSysConfigPort javadoc)。
        String smsContemt = taskSysConfigPort.findConfigValueByKey("wj.sms.template");
        // varchar(500) NOT NULL DEFAULT '' ⇒ 二者不可能不同(详见 ISysConfigService javadoc)。
        String smsContemt = sysConfigService.selectConfigByKey("wj.sms.template");
        if (smsContemt != null) {
            sendMagParam.setContent(smsContemt.replace("${wjurl}", sendMagParam.getUrl()));
@@ -4214,7 +4346,7 @@
            return null;
        }
        try {
            SysDept sysDept = taskSysDeptPort.selectDeptByCode(deptCode, orgid);
            SysDept sysDept = sysDept2Mapper.selectDeptByCode(deptCode, orgid);
            return sysDept == null ? null : sysDept.getCampusid();
        } catch (Exception e) {
            log.warn("按编码反查院区失败:deptCode={}, orgid={}, err={}", deptCode, orgid, e.getMessage());