| | |
| | | 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业务层处理 |
| | |
| | | */ |
| | | @Autowired |
| | | private OperationSubtaskWriter operationSubtaskWriter; |
| | | |
| | | @Autowired |
| | | private FollowupCompletedEventPublisher followupCompletedEventPublisher; |
| | | |
| | | /** |
| | | * 满意度模块对外门面(常驻 Bean)。 |
| | |
| | | serviceOutPath.setSubId(serviceSubtask1.getId()); |
| | | List<ServiceOutPath> serviceOutPaths = serviceOutPathMapper.selectServiceOutPathList(serviceOutPath); |
| | | if (CollectionUtils.isNotEmpty(serviceOutPaths)) |
| | | format = String.format("%03X", serviceOutPaths.get(0).getId()); |
| | | format = ShortCode.encode(serviceOutPaths.get(0).getId()); |
| | | else format = getOutPath(serviceSubtask1, rsaPublicKeyExample); |
| | | serviceSubtask = serviceSubtask1; |
| | | } else { |
| | |
| | | serviceOutPath.setSubId(serviceSubtask.getId()); |
| | | iServiceOutPathService.insertServiceOutPath(serviceOutPath); |
| | | //转成16进制 |
| | | format = String.format("%03X", serviceOutPath.getId()); |
| | | format = ShortCode.encode(serviceOutPath.getId()); |
| | | serviceOutPath.setRadix(format); |
| | | serviceOutPath.setUpdateTime(new Date()); |
| | | iServiceOutPathService.updateServiceOutPath(serviceOutPath); |
| | |
| | | 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 |
| | |
| | | .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; |
| | | } |
| | | |
| | | /** |
| | |
| | | // } |
| | | |
| | | //将任务信息放到服务表中 |
| | | 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()); |
| | |
| | | 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()); |
| | |
| | | } |
| | | 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()); |
| | |
| | | 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()); |
| | |
| | | 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分组,每个任务即一个病种随访任务 |
| | |
| | | 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) { |
| | |
| | | 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": |
| | |
| | | } |
| | | |
| | | /** |
| | | * 再次随访附加统计:待随访、已发送、人工电话超时及回院复诊复查(suggest=6)。 |
| | | * 再次随访附加统计:待随访、已发送、人工电话超时及回院复诊复查(suggest=7)。 |
| | | */ |
| | | private void applyAgainFollowUpExtraStatistics(ServiceSubtaskStatistic statistic, ServiceSubtask serviceSubtask) { |
| | | if (serviceSubtask.getVisitCount() == null || serviceSubtask.getVisitCount() <= 1) { |
| | |
| | | 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); |
| | |
| | | * 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 |
| | | * |
| | | */ |
| | |
| | | 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); |
| | |
| | | total.setSentAgain(0L); |
| | | total.setFollowUpSuccessAgain(0L); |
| | | total.setFollowUpFailAgain(0L); |
| | | total.setSuggest6CountAgain(0L); |
| | | total.setSuggest7CountAgain(0L); |
| | | total.setManualAgain(0L); |
| | | total.setSmsAgain(0L); |
| | | total.setWeChatAgain(0L); |
| | |
| | | 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)); |
| | |
| | | serviceSubtask.setSendstate(6L); |
| | | serviceSubtask.setPreachform("1"); |
| | | serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); |
| | | followupCompletedEventPublisher.publish(serviceSubtask.getId()); |
| | | } |
| | | } |
| | | |
| | |
| | | if (subtaskUpdated == 0) { |
| | | throw new BaseException("微信公众号默认完成失败:子任务状态已变化,subid=" + current.getId()); |
| | | } |
| | | followupCompletedEventPublisher.publish(current.getId()); |
| | | |
| | | log.info("【微信公众号默认完成】subid:{},已按默认选项完成,填充题目数:{}", current.getId(), filled); |
| | | return true; |
| | |
| | | serviceOutPath.setCreateTime(new Date()); |
| | | serviceOutPath.setOrgid(serviceSubtask.getOrgid()); |
| | | iServiceOutPathService.insertServiceOutPath(serviceOutPath); |
| | | String format = String.format("%03X", serviceOutPath.getId()); |
| | | String format = ShortCode.encode(serviceOutPath.getId()); |
| | | String url = ""; |
| | | SendMagParam sendMagParam = new SendMagParam(); |
| | | sendMagParam.setType("4"); |
| | |
| | | serviceOutPath.setCreateTime(new Date()); |
| | | serviceOutPath.setOrgid(serviceSubtask.getOrgid()); |
| | | iServiceOutPathService.insertServiceOutPath(serviceOutPath); |
| | | String format = String.format("%03X", serviceOutPath.getId()); |
| | | String format = ShortCode.encode(serviceOutPath.getId()); |
| | | serviceOutPath.setRadix(format); |
| | | serviceOutPath.setUpdateTime(new Date()); |
| | | serviceOutPath.setUrl(url); |
| | |
| | | serviceOutPath.setCreateTime(new Date()); |
| | | serviceOutPath.setOrgid(serviceTask.getOrgid()); |
| | | iServiceOutPathService.insertServiceOutPath(serviceOutPath); |
| | | String format = String.format("%03X", serviceOutPath.getId()); |
| | | String format = ShortCode.encode(serviceOutPath.getId()); |
| | | serviceOutPath.setRadix(format); |
| | | serviceOutPath.setUpdateTime(new Date()); |
| | | String url = StringUtils.isNotEmpty(req_path) ? ip + ":" + req_path + "/wt?p=" + format : ip + "/wt?p=" + format; |