陈昶聿
2 天以前 3d047130b059efd46f56be37465a751ab16ee8cb
ruoyi-task/src/main/java/com/ruoyi/task/service/impl/ServiceSubtaskServiceImpl.java
@@ -126,6 +126,7 @@
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.mapper.ext.SysDept2Mapper;
import com.ruoyi.task.api.contract.OperationSubtaskWriter;
import com.ruoyi.task.event.FollowupCompletedEventPublisher;
/**
 * 单一任务(随访)Service业务层处理
@@ -145,6 +146,9 @@
     */
    @Autowired
    private OperationSubtaskWriter operationSubtaskWriter;
    @Autowired
    private FollowupCompletedEventPublisher followupCompletedEventPublisher;
    /**
     * 满意度模块对外门面(常驻 Bean)。
@@ -1544,7 +1548,12 @@
        if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() == 6)
            serviceSubtask.setFinishtime(new Date());
        return serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
        Boolean updated = serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
        if (Boolean.TRUE.equals(updated) && serviceSubtask.getSendstate() != null
                && serviceSubtask.getSendstate() == 6 && serviceSubtask.getId() != null) {
            followupCompletedEventPublisher.publish(serviceSubtask.getId());
        }
        return updated;
    }
    @Override
@@ -1596,6 +1605,61 @@
                .filter(code -> StringUtils.isNotEmpty(code))
                .distinct()
                .collect(Collectors.joining(","));
    }
    /**
     * 取逗号串里的第一个值(任务上科室/病区是多选,而 {@code service_subtask} 上都是单值列)。
     */
    private String firstValue(String value) {
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        for (String item : value.split(",")) {
            if (StringUtils.isNotEmpty(item.trim())) {
                return item.trim();
            }
        }
        return null;
    }
    /**
     * 任务配置({@link ServiceTaskVO})→ 子任务({@link ServiceSubtask})的字段搬运。
     * <p>
     * <b>为什么不能直接 {@code DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class)}:</b>
     * 它是纯同名拷贝,而两边同名的 {@code deptcode / deptname} <b>语义不同</b>:
     * <ul>
     *   <li>{@code ServiceTaskVO.deptcode / deptname} = 任务的「适用科室」,
     *       出院随访(serviceType=2)/ 出院宣教(4)/ 门诊随访(3)等场景下就是<b>离院科室</b>;</li>
     *   <li>{@code ServiceSubtask.deptcode / deptname} = <b>在院科室</b>
     *       (2026-11 由原 sendlimitpbegin/pend 复用而来,见 {@code ServiceSubtask} 字段注释)。</li>
     * </ul>
     * 于是纯同名拷贝会把「离院科室」写进「在院科室」列,后果有两个:
     * <ol>
     *   <li>按在院维度筛({@code deptcodes} → {@code service_subtask.deptcode})会误命中这些子任务;</li>
     *   <li>按离院维度筛({@code leaveldeptcodes} → {@code service_subtask.leaveldeptcode})时,
     *       患者自己没带科室的那些(前端手动勾选患者很常见)会因为 {@code leaveldeptcode} 为空而查不到。</li>
     * </ol>
     * 因此这里按项目既定口径({@code doc/service_subtask_字段改名_改动清单.md} §8:这几条
     * 「长期任务 / 任务组 / 手动建任务」路径<b>不填在院维度</b>)统一处理:
     * <b>在院维度 4 列清空</b>(在院科室/在院病区只有 HIS 链路能从 {@code pat_med_inhosp} 拿到),
     * <b>科室 / 病区一律落离院维度</b>({@code leaveldeptcode/name}、{@code leavehospitaldistrictcode/name})。
     *
     * @param serviceTaskVO 建任务入参
     * @return 已按离院维度归位的子任务对象(在院维度为空)
     */
    private ServiceSubtask toSubtaskFromTaskVO(ServiceTaskVO serviceTaskVO) {
        ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
        // ① 在院维度必须清空:任务配置的科室不是患者本人的在院科室
        serviceSubtask.setDeptcode(null);
        serviceSubtask.setDeptname(null);
        serviceSubtask.setHospitaldistrictcode(null);
        serviceSubtask.setHospitaldistrictname(null);
        // ② 科室 / 病区 → 离院维度(多选串取第一个,子任务是单值列)
        serviceSubtask.setLeaveldeptcode(firstValue(serviceTaskVO.getDeptcode()));
        serviceSubtask.setLeaveldeptname(firstValue(serviceTaskVO.getDeptname()));
        serviceSubtask.setLeavehospitaldistrictcode(firstValue(serviceTaskVO.getLeavehospitaldistrictcode()));
        serviceSubtask.setLeavehospitaldistrictname(firstValue(serviceTaskVO.getLeavehospitaldistrictname()));
        return serviceSubtask;
    }
    /**
@@ -1825,7 +1889,8 @@
//            }
            //将任务信息放到服务表中
            ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
            // 科室/病区按「离院维度」归位(出院随访的科室病区就是离院科室/病区),见 toSubtaskFromTaskVO
            ServiceSubtask serviceSubtask = toSubtaskFromTaskVO(serviceTaskVO);
            serviceSubtask.setTaskid(serviceTask.getTaskid().longValue());
            serviceSubtask.setTemplatename(ObjectUtils.isNotEmpty(tempid) ? tempName : serviceTask.getTemplatename());
            serviceSubtask.setTemplateid(ObjectUtils.isNotEmpty(tempid) ? tempid : serviceTask.getTemplateid());
@@ -1846,15 +1911,18 @@
                    serviceSubtask.setInhospid(patTaskRelevance.getHospid());
                    //新建完成就是待发送状态
                    serviceSubtask.setSendstate(2L);
                    String subtaskDeptCode = patTaskRelevance.getDeptcode();
                    if (StringUtils.isEmpty(subtaskDeptCode) && StringUtils.isNotEmpty(serviceTaskVO.getDeptcode())) {
                        // service_subtask.leaveldeptcode 为单值字段,不能写入任务的多科室拼接串,取第一个科室兜底
                        subtaskDeptCode = serviceTaskVO.getDeptcode().split(",")[0].trim();
                    }
                    serviceSubtask.setLeaveldeptcode(subtaskDeptCode);
                    serviceSubtask.setLeaveldeptname(StringUtils.isNotEmpty(patTaskRelevance.getDeptName()) ? patTaskRelevance.getDeptName() : serviceTaskVO.getDeptname());
                    serviceSubtask.setLeavehospitaldistrictcode(patTaskRelevance.getLeavehospitaldistrictcode());
                    serviceSubtask.setLeavehospitaldistrictname(patTaskRelevance.getLeavehospitaldistrictname());
                    // 离院维度(出院随访 = 出院科室 / 出院病区):
                    // 患者自带的出院科室/病区优先,患者侧没带(前端手动勾选很常见)时回退到任务配置,
                    // 不能无条件覆盖 —— 否则会把 toSubtaskFromTaskVO 按任务配置兜好的离院维度抹成 null,
                    // 结果「任务配了科室病区、生成出来的子任务离院科室/病区却是空」。
                    serviceSubtask.setLeaveldeptcode(firstNonBlank(patTaskRelevance.getDeptcode(),
                            firstValue(serviceTaskVO.getDeptcode())));
                    serviceSubtask.setLeaveldeptname(firstNonBlank(patTaskRelevance.getDeptName(),
                            firstValue(serviceTaskVO.getDeptname())));
                    serviceSubtask.setLeavehospitaldistrictcode(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictcode(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictcode())));
                    serviceSubtask.setLeavehospitaldistrictname(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictname(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictname())));
                    serviceSubtask.setType(serviceTaskVO.getType());
                    serviceSubtask.setHospType(patTaskRelevance.getHospType());
                    serviceSubtask.setOpenid(patTaskRelevance.getOpenid());
@@ -2212,7 +2280,8 @@
            }
            if (CollectionUtils.isNotEmpty(serviceTaskVO.getPatTaskRelevances())) {
                for (PatTaskRelevance patTaskRelevance : serviceTaskVO.getPatTaskRelevances()) {
                    ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
                    // 科室/病区按「离院维度」归位(出院随访的科室病区就是离院科室/病区),见 toSubtaskFromTaskVO
                    ServiceSubtask serviceSubtask = toSubtaskFromTaskVO(serviceTaskVO);
                    serviceSubtask.setLibtemplateid(ObjectUtils.isEmpty(serviceTaskVO.getLibtemplateid()) ? null : Long.valueOf(serviceTaskVO.getLibtemplateid()));
                    serviceSubtask.setLibtemplatename(serviceTaskVO.getLibtemplatename());
                    serviceSubtask.setTemplateid(serviceTask.getTemplateid());
@@ -2224,10 +2293,16 @@
                    serviceSubtask.setAddr(patTaskRelevance.getAddr());
                    serviceSubtask.setPatid(patTaskRelevance.getPatid());
                    serviceSubtask.setOpenid(patTaskRelevance.getOpenid());
                    serviceSubtask.setLeaveldeptcode(patTaskRelevance.getDeptcode());
                    serviceSubtask.setLeavehospitaldistrictname(patTaskRelevance.getLeavehospitaldistrictname());
                    serviceSubtask.setLeavehospitaldistrictcode(patTaskRelevance.getLeavehospitaldistrictcode());
                    serviceSubtask.setLeaveldeptname(patTaskRelevance.getDeptName());
                    // 离院维度:患者自带的出院科室/病区优先,没带时回退到任务配置(否则会把
                    // toSubtaskFromTaskVO 按任务配置兜好的值抹成 null,出院随访就成「离院科室/病区为空」)
                    serviceSubtask.setLeaveldeptcode(firstNonBlank(patTaskRelevance.getDeptcode(),
                            firstValue(serviceTaskVO.getDeptcode())));
                    serviceSubtask.setLeaveldeptname(firstNonBlank(patTaskRelevance.getDeptName(),
                            firstValue(serviceTaskVO.getDeptname())));
                    serviceSubtask.setLeavehospitaldistrictcode(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictcode(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictcode())));
                    serviceSubtask.setLeavehospitaldistrictname(firstNonBlank(patTaskRelevance.getLeavehospitaldistrictname(),
                            firstValue(serviceTaskVO.getLeavehospitaldistrictname())));
                    serviceSubtask.setType(serviceTaskVO.getType());
                    serviceSubtask.setCreateTime(DateUtils.getNowDate());
                    serviceSubtask.setDrname(patTaskRelevance.getDrname());
@@ -2459,7 +2534,10 @@
            if (serviceSubtaskCountReq.getDrcode() != null && !serviceSubtaskCountReq.getDrcode().isEmpty()) {
                groupKey = drcode;
            } else {
                groupKey = "deptcode";
                // 2026-11 字段重命名后:deptcode 是在院科室(历史数据为空),
                // 出院科室已改名为 leaveldeptcode,分组必须用后者,
                // 否则 getSfStatisticsGroupKey 会返回 NULL,拼成 IN (NULL) 导致整表查空。
                groupKey = "leaveldeptcode";
            }
        } else if (serviceSubtaskCountReq.getStatisticaltype() != null && serviceSubtaskCountReq.getStatisticaltype() == 3) {
            // 病种统计:按任务ID分组,每个任务即一个病种随访任务
@@ -2470,6 +2548,17 @@
        Map<String, List<ServiceSubtask>> collect = new HashMap<>();
        serviceSubtaskCountReq.setGroupKey(groupKey);
        List<String> groupKeyList = serviceSubtaskMapper.getSfStatisticsGroupKey(serviceSubtaskCountReq);
        /*
         * 分组键可能为 NULL(历史数据、或重命名后尚未回填的在院维度列),
         * 直接拼进 SQL 会变成 `AND IFNULL(列,'Unknown') IN (NULL)`,恒不成立,整张统计表为空。
         * 这里统一成 'Unknown',与聚合 SQL 的 IFNULL(列,'Unknown') 口径对齐。
         */
        if (groupKeyList != null) {
            groupKeyList = groupKeyList.stream()
                    .map(key -> key == null ? "Unknown" : key)
                    .distinct()
                    .collect(Collectors.toList());
        }
        serviceSubtaskCountReq.setGroupKeyList(groupKeyList);
        List<ServiceSubtask> rawData = serviceSubtaskMapper.getSfStatistics(serviceSubtaskCountReq);
        switch (groupKey) {
@@ -2484,6 +2573,7 @@
                collect = rawData.stream().collect(Collectors.groupingBy(subtask -> Optional.ofNullable(subtask.getManagementDoctorCode()).orElse("Unknown")));
                break;
            case "deptcode":
            case "leaveldeptcode":
                collect = rawData.stream().collect(Collectors.groupingBy(subtask -> Optional.ofNullable(subtask.getLeaveldeptcode()).orElse("Unknown")));
                break;
            case "leavehospitaldistrictcode":
@@ -3100,7 +3190,7 @@
    }
    /**
     * 再次随访附加统计:待随访、已发送、人工电话超时及回院复诊复查(suggest=6)。
     * 再次随访附加统计:待随访、已发送、人工电话超时及回院复诊复查(suggest=7)。
     */
    private void applyAgainFollowUpExtraStatistics(ServiceSubtaskStatistic statistic, ServiceSubtask serviceSubtask) {
        if (serviceSubtask.getVisitCount() == null || serviceSubtask.getVisitCount() <= 1) {
@@ -3110,9 +3200,9 @@
            return;
        }
        Long sendstate = serviceSubtask.getSendstate();
        // 回院复诊复查:再次随访中按处理意见 suggest=6 统计
        if (StringUtils.equals("6", StringUtils.trim(serviceSubtask.getSuggest()))) {
            statistic.setSuggest6CountAgain(statistic.getSuggest6CountAgain() + 1L);
        // 回院复诊复查:再次随访中按处理意见 suggest=7 统计
        if (StringUtils.equals("7", StringUtils.trim(serviceSubtask.getSuggest()))) {
            statistic.setSuggest7CountAgain(statistic.getSuggest7CountAgain() + 1L);
        }
        if (sendstate.equals(3L)) {
            statistic.setSentAgain(statistic.getSentAgain() + 1L);
@@ -3758,7 +3848,7 @@
     * pendingFollowUpSentAgainInfo - visitCount > 1 && sendstate in (3,7)
     * sentInfo - visitCount = 1 && sendstate = 3
     * sentAgainInfo - visitCount > 1 && sendstate = 3
     * suggest6CountAgainInfo - visitCount > 1 && suggest = 6
     * suggest7CountAgainInfo - visitCount > 1 && suggest = 7
     * filterCountList - taskSituation = 6 && sendstate = 4
     *
     */
@@ -3943,10 +4033,10 @@
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setExcep("1");
                break;
            case "suggest6CountAgainInfo":
                // 再次随访回院复诊复查:visitCount > 1 且 suggest = 6
            case "suggest7CountAgainInfo":
                // 再次随访回院复诊复查:visitCount > 1 且 suggest = 7
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setSuggest("6");
                serviceSubtaskCountReq.setSuggest("7");
                break;
            case "taskSituation1Info":
                serviceSubtaskCountReq.setTaskSituation(1);
@@ -4505,7 +4595,7 @@
        total.setSentAgain(0L);
        total.setFollowUpSuccessAgain(0L);
        total.setFollowUpFailAgain(0L);
        total.setSuggest6CountAgain(0L);
        total.setSuggest7CountAgain(0L);
        total.setManualAgain(0L);
        total.setSmsAgain(0L);
        total.setWeChatAgain(0L);
@@ -4550,7 +4640,7 @@
            total.setSentAgain(total.getSentAgain() + (stat.getSentAgain() != null ? stat.getSentAgain() : 0L));
            total.setFollowUpSuccessAgain(total.getFollowUpSuccessAgain() + (stat.getFollowUpSuccessAgain() != null ? stat.getFollowUpSuccessAgain() : 0L));
            total.setFollowUpFailAgain(total.getFollowUpFailAgain() + (stat.getFollowUpFailAgain() != null ? stat.getFollowUpFailAgain() : 0L));
            total.setSuggest6CountAgain(total.getSuggest6CountAgain() + (stat.getSuggest6CountAgain() != null ? stat.getSuggest6CountAgain() : 0L));
            total.setSuggest7CountAgain(total.getSuggest7CountAgain() + (stat.getSuggest7CountAgain() != null ? stat.getSuggest7CountAgain() : 0L));
            total.setManualAgain(total.getManualAgain() + (stat.getManualAgain() != null ? stat.getManualAgain() : 0L));
            total.setSmsAgain(total.getSmsAgain() + (stat.getSmsAgain() != null ? stat.getSmsAgain() : 0L));
            total.setWeChatAgain(total.getWeChatAgain() + (stat.getWeChatAgain() != null ? stat.getWeChatAgain() : 0L));
@@ -4802,6 +4892,7 @@
            serviceSubtask.setSendstate(6L);
            serviceSubtask.setPreachform("1");
            serviceSubtaskMapper.updateServiceSubtask(serviceSubtask);
            followupCompletedEventPublisher.publish(serviceSubtask.getId());
        }
    }
@@ -4946,6 +5037,7 @@
        if (subtaskUpdated == 0) {
            throw new BaseException("微信公众号默认完成失败:子任务状态已变化,subid=" + current.getId());
        }
        followupCompletedEventPublisher.publish(current.getId());
        log.info("【微信公众号默认完成】subid:{},已按默认选项完成,填充题目数:{}", current.getId(), filled);
        return true;