package com.ruoyi.outpatientvisit.service; import com.ruoyi.common.core.service.ArchiveReadApi; import com.ruoyi.common.core.service.ServiceTaskMatchApi; import com.ruoyi.common.core.service.VisitTaskCreateApi; import com.ruoyi.common.core.service.dto.OuthospSubtaskDraft; import com.ruoyi.common.core.service.dto.ServiceTaskMatchDto; import com.ruoyi.common.core.service.dto.VisitArchiveRow; import com.ruoyi.common.core.service.dto.VisitLongTaskDto; import com.ruoyi.common.core.service.dto.VisitPendingSubtaskDto; import com.ruoyi.outpatientvisit.domain.PatMedOuthosp; import com.ruoyi.outpatientvisit.domain.PatMedOuthospQueryReq; import com.ruoyi.outpatientvisit.mapper.PatMedOuthospMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Objects; /** * 门急诊随访编排:扫描未处理门诊记录 → 按科室/诊断维度匹配长期任务 → 写子任务。 * *

迁移对应(@ b25c3b51)

* * * * * * * * *
本类方法原实现
{@link #dealOutpatientInfo()}{@code PatMedOuthospServiceImpl#dealOutpatientInfo} L385-427(43 行)
{@link #writeInSubTask}同文件 L436-521(86 行)
{@link #boxedServiceSubtask}同文件 L525-581(35 个 setter)
* *

跨模块能力怎么拿(本模块只依赖 ruoyi-common)

* * *

⚠️ 保留的原实现行为(勿"顺手优化")

*
    *
  1. {@code dealOutpatientInfo} 返回固定 {@code 1}(不是处理条数);
  2. *
  3. 匹配不到时的 flag 与 remark 逐字照抄: * 科室维度 → {@code deptcheckFlag="2"} + "通过部门,没有找到门诊随访任务ID"; * 诊断维度 → {@code diagcheckFlag="2"} + "通过icd10,没有找到门诊随访任务ID";
  4. *
  5. ⚠️ 但 {@link #writeInSubTask} 内部的 flag 映射是交叉的: * 任务不存在时 {@code type==1 → diagcheckFlag="2"}、{@code type==2 → deptcheckFlag="2"} * —— 与上面第 2 条正好相反。这是原实现的写法(出入院侧也有同类交叉痕迹),原样保留;
  6. *
  7. {@code check=true} 且存在待执行子任务时,先停旧再新增, * 且"停旧"要设 {@code finishtime = new Date()}(出入院侧那行是被注释掉的,门诊侧是活的);
  8. *
  9. 不需要随访({@code notrequiredFlag == "1"})时置 {@code sendstate=4} + {@code result="error"} * 并把原因写进 remark,但仍然继续 insert(原实现没有提前 return);
  10. *
  11. {@code setHospType("1")} 是硬编码,不读 {@code serviceTask.getHospType()}。
  12. *
* *

⚠️ 与"原 selectPatMedOuthospList"的一处有意差异(已登记)

* 原 {@code selectPatMedOuthospList} 除了调存储过程,还会对每条记录做展示用富化 * (用档案算年龄、补 sex/telcode/idcardno、查子任务置 {@code serverState})。 * 本方法的调用方是后台扫描而非列表展示,它随后自己要读 {@code patid/deptcode/icd10code} 等, * 富化字段并不参与判断 —— 故本方法直接调 {@code callSpQueryOuthosp},不做那层富化 * (少一次档案查询 + 少一次子任务查询)。如需与列表接口完全一致,应另走列表服务。 */ @Slf4j @Service public class OuthospVisitTaskService { /** 科室维度的服务类型(原实现硬编码 "3") */ private static final String SERVICE_TYPE_DEPT = "3"; /** 待执行状态(原实现硬编码 2L) */ private static final Long SENDSTATE_PENDING = 2L; /** 已停止状态(原实现硬编码 6L) */ private static final Long SENDSTATE_STOPPED = 6L; /** 不执行状态(原实现硬编码 4L:"手机号为空" / "不需要随访"时用) */ private static final Long SENDSTATE_SKIP = 4L; /** 标记"不需要随访"的值(原 {@code patArchive.notrequiredFlag == "1"}) */ private static final String NOT_REQUIRED_YES = "1"; /** 门诊侧 {@code hospType} 的固定值(原实现硬编码) */ private static final String HOSP_TYPE_OUTHOSP = "1"; @Autowired private PatMedOuthospMapper patMedOuthospMapper; @Autowired private ObjectProvider archiveApiProvider; @Autowired private ObjectProvider matchApiProvider; @Autowired private ObjectProvider taskApiProvider; /** * 门诊随访主扫描。 *

* 逐行对应原 {@code dealOutpatientInfo}(L385-427): *

     * PatMedOuthosp q = new PatMedOuthosp();
     * q.setDiagcheckFlag("0");
     * q.setFuflag("1");
     * for (每条未处理门诊记录) {
     *     查档案;
     *     科室维度:longtask=1 + deptCode + service_type="3" -> writeInSubTask(taskId, true, ..., 1)
     *     诊断维度:longtask=1 + icd10code                 -> writeInSubTask(taskId, true, ..., 2)
     * }
     * return 1;   // 固定值
     * 
* * @return 固定 {@code 1}(与原实现一致,不是处理条数) */ public Integer dealOutpatientInfo() { ServiceTaskMatchApi matchApi = matchApi(); VisitTaskCreateApi taskApi = taskApi(); ArchiveReadApi archiveApi = archiveApi(); if (matchApi == null || taskApi == null) { log.warn("【门急诊随访】跨模块契约不可用(ServiceTaskMatchApi={}, VisitTaskCreateApi={}),跳过本次门诊随访。", matchApi != null, taskApi != null); return 1; } PatMedOuthosp query = new PatMedOuthosp(); query.setDiagcheckFlag("0"); query.setFuflag("1"); List list = patMedOuthospMapper.callSpQueryOuthosp(toQueryReq(query)); if (CollectionUtils.isEmpty(list)) { return 1; } for (PatMedOuthosp row : list) { VisitArchiveRow archive = archiveApi == null ? null : archiveApi.findByPatid(row.getPatid()); // ---- 科室维度(原 L396-410)---- List deptTasks = matchApi.findLongTasksByDept(row.getDeptcode(), SERVICE_TYPE_DEPT); if (CollectionUtils.isEmpty(deptTasks)) { // ⚠️ 原实现此处置的是 deptcheckFlag(不是 diagcheckFlag) row.setDeptcheckFlag("2"); row.setRemark("通过部门,没有找到门诊随访任务ID"); updateFlag(row); } else { for (ServiceTaskMatchDto t : deptTasks) { writeInSubTask(taskApi, t.getTaskId(), Boolean.TRUE, row, archive, 1); } } // ---- 诊断维度(原 L412-426)---- List diagTasks = matchApi.findLongTasksByDiag(row.getIcd10code()); if (CollectionUtils.isEmpty(diagTasks)) { // 原实现此处置的是 diagcheckFlag row.setDiagcheckFlag("2"); row.setRemark("通过icd10,没有找到门诊随访任务ID"); updateFlag(row); } else { for (ServiceTaskMatchDto t : diagTasks) { writeInSubTask(taskApi, t.getTaskId(), Boolean.TRUE, row, archive, 2); } } } return 1; } /** * 把一条门诊记录写入随访子任务。 *

* 逐行对应原 {@code PatMedOuthospServiceImpl#writeInSubTask}(L436-521)。 * * @param type 1 = 科室维度;2 = 诊断维度 * ⚠️ 原实现的 flag 映射是交叉的(见类注释第 2 条),原样保留。 */ private void writeInSubTask(VisitTaskCreateApi taskApi, Long taskid, Boolean check, PatMedOuthosp row, VisitArchiveRow archive, Integer type) { // 原 L438-441:查「已发布的长期任务」(taskid + sendState=2) VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid); if (serviceTask == null) { log.info("该患者疾病随访长期任务不存在,任务ID为:{}", taskid); // ⚠️ 原实现的交叉映射,原样保留 if (type == 1) row.setDiagcheckFlag("2"); if (type == 2) row.setDeptcheckFlag("2"); row.setRemark("该患者门诊随访长期任务不存在,任务ID为:" + taskid); updateFlag(row); return; } // 原 L452:封装 serviceSubtask OuthospSubtaskDraft draft = boxedServiceSubtask(serviceTask, row, archive); boolean ok = false; // 原 L455:先判断一下,是否需要校验 if (Boolean.TRUE.equals(check)) { // 原 L457-464:按 patid + sendstate=2 + taskid 查待执行子任务 List pendings = taskApi.findPendingSubtasks(archive == null ? null : archive.getId(), taskid); log.info("该患者待执行的任务:{}", CollectionUtils.isEmpty(pendings) ? null : pendings.size()); if (CollectionUtils.isNotEmpty(pendings)) { for (VisitPendingSubtaskDto pending : pendings) { if (Objects.isNull(pending.getLongSendTime())) { // 不是长期任务,不处理(原 L465-468) continue; } // 将之前的停掉(原 L470-479) pending.setSendstate(SENDSTATE_STOPPED); pending.setRemark("患者已经回院复诊"); pending.setResult("success"); // ⚠️ 门诊侧设了 finishtime(出入院侧那行是被注释掉的),必须带上 pending.setFinishtime(new Date()); pending.setUpdateBy(serviceTask.getUpdateBy()); taskApi.updatePendingSubtask(pending); // 重新新增子任务(原 L481) ok = insertSubtask(taskApi, draft, serviceTask); } } else { // 原 L483-490:没有待执行任务 -> 直接新增 ok = insertSubtask(taskApi, draft, serviceTask); } } else { // 原 L491-502:不校验,直接新增 ok = insertSubtask(taskApi, draft, serviceTask); } if (ok) { // 原 L503-509:将 check_flag 改成已处理(按 id 更新) PatMedOuthosp upd = new PatMedOuthosp(); upd.setId(row.getId()); if (type == 1) upd.setDiagcheckFlag("2"); if (type == 2) upd.setDeptcheckFlag("2"); updateFlag(upd); } else { // 原 L510-520:生成子任务失败 log.info("生成子任务失败serviceSubtask的taskid为:{},patid为:{}", draft.getTaskid(), draft.getPatid()); PatMedOuthosp upd = new PatMedOuthosp(); upd.setGuid(row.getGuid()); upd.setSerialnum(row.getSerialnum()); if (type == 1) upd.setDiagcheckFlag("2"); if (type == 2) upd.setDeptcheckFlag("2"); upd.setRemark("生成子任务失败"); updateFlag(upd); } } /** * 新增子任务,并复刻原实现在"手机号为空"时的处理。 *

* 原 L484-490 / L492-499 两处逻辑相同: *

     * if (StringUtils.isEmpty(serviceSubtask.getPhone())) {
     *     serviceSubtask.setRemark("手机号为空");
     *     serviceSubtask.setSendstate(4L);
     *     serviceSubtask.setResult("error");
     * }
     * serviceSubtask.setCreateBy(serviceTask.getCreateBy());
     * serviceSubtask.setCreateTime(new Date());
     * i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
     * 
*/ private boolean insertSubtask(VisitTaskCreateApi taskApi, OuthospSubtaskDraft draft, VisitLongTaskDto serviceTask) { if (StringUtils.isEmpty(draft.getPhone())) { draft.setRemark("手机号为空"); draft.setSendstate(SENDSTATE_SKIP); draft.setResult("error"); } draft.setCreateBy(serviceTask.getCreateBy()); draft.setCreateTime(new Date()); return taskApi.insertSubtask(draft); } /** * 门诊版子任务组装。 *

* 逐字段对应原 {@code boxedServiceSubtask}(L525-581)。 * 35 个赋值里绝大多数落在 {@code VisitSubtaskDraft} 已有字段上; * 只有 {@code admitdate} / {@code fudate} / {@code finishtime} / {@code hospType} * 落在 {@link OuthospSubtaskDraft}(见该类注释的对照表)。 * *

⚠️ 刻意保留的原行为

*
    *
  • {@code setSfzh} 被调用两次(同值),原样保留;
  • *
  • {@code setUpdateTime(new Date())} 被调用三次,原样保留;
  • *
  • {@code serviceTask.getSendDay() == null} 时按 1 天处理,且写回 serviceTask;
  • *
  • {@code addDays} 的基准是门诊的 {@code admitdate}(可能为 null, * 原实现同样会 NPE —— 不额外加保护)。
  • *
*/ private OuthospSubtaskDraft boxedServiceSubtask(VisitLongTaskDto serviceTask, PatMedOuthosp row, VisitArchiveRow archive) { OuthospSubtaskDraft d = new OuthospSubtaskDraft(); // ---- 来自 serviceTask ---- d.setTaskid(serviceTask.getTaskid()); d.setOrgid(serviceTask.getOrgid()); d.setCampusid(serviceTask.getCampusid()); // 原 L533-534:libtemplateid 在 ServiceTask 里是 String,落库列/草稿里是 Long, // 原实现显式 Long.valueOf 转换,且【仅在非空时】设值(为空则保持 null -> 列不写)。原样保留。 if (StringUtils.isNotEmpty(serviceTask.getLibtemplateid())) { d.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid())); } d.setTemplateid(serviceTask.getTemplateid()); d.setTemplatename(serviceTask.getTemplatename()); d.setServiceType(serviceTask.getServiceType()); d.setPreachform(serviceTask.getPreachform()); d.setUpdateBy(serviceTask.getUpdateBy()); // ---- 来自门诊记录 ---- d.setDrcode(row.getDrcode()); d.setDrname(row.getDrname()); d.setDeptcode(row.getDeptcode()); d.setInhospid(row.getId()); d.setHospno(row.getOuthospno()); d.setDeptname(row.getDeptname()); d.setLeavediagname(row.getDiagname()); d.setLeaveicd10code(row.getIcd10code()); d.setVisitDeptCode(row.getDeptcode()); d.setVisitDeptName(row.getDeptname()); // 门诊侧独有(父类没有的字段) d.setAdmitdate(row.getAdmitdate()); d.setFudate(row.getFudate()); // ---- 来自患者档案 ---- if (archive != null) { d.setPatid(archive.getId()); d.setSendname(archive.getName()); d.setSfzh(archive.getIdcardno()); // 原实现:phone 取 telcode;为空则取 relativetelcode d.setPhone(StringUtils.isBlank(archive.getTelcode()) ? archive.getRelativetelcode() : archive.getTelcode()); d.setSex(archive.getSex()); d.setAge(archive.getAge()); } // ---- 固定值 / 状态 ---- d.setSendstate(SENDSTATE_PENDING); d.setSfzh(archive == null ? null : archive.getIdcardno()); // ⚠️ 硬编码 "1"(原实现不读 serviceTask.getHospType()),原样保留 d.setHospType(HOSP_TYPE_OUTHOSP); d.setCreateTime(new Date()); d.setUpdateBy(serviceTask.getUpdateBy()); d.setUpdateTime(new Date()); d.setUpdateTime(new Date()); // ---- 发送时间(原 L562-570)---- if (serviceTask.getSendDay() == null) { serviceTask.setSendDay(1L); } Date newDate = addDays(row.getAdmitdate(), serviceTask.getSendDay().intValue()); if (row.getFudate() != null) { // 如果门诊表有指定随访时间,那就用指定的 newDate = row.getFudate(); } d.setLongSendTime(newDate); d.setVisitTime(newDate); // ---- 患者不需要随访(原 L571-579)---- if (archive != null && StringUtils.isNotEmpty(archive.getNotrequiredFlag()) && NOT_REQUIRED_YES.equals(archive.getNotrequiredFlag())) { d.setRemark(archive.getNotrequiredreason()); d.setResult("error"); d.setSendstate(SENDSTATE_SKIP); } return d; } /** * 在 {@code date} 上加 {@code days} 天;{@code days} 为 null 时按 1 天。 *

* 逐行对应原 {@code PatMedOuthospServiceImpl#addDays}(L561-568)。 * ⚠️ 与出入院侧的 {@code minusDay} 不是同一个方法(那个硬编码减 2 天并忽略入参)。 */ private Date addDays(Date date, Integer days) { if (days == null) { days = 1; } Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.DAY_OF_MONTH, days); return calendar.getTime(); } /** * 按"传什么更新什么"更新门诊记录的标志位。 *

* 原实现调 {@code updatePatMedOuthosp},其 SQL 走 {@code updateInTable} * (动态表名 + 全列可选更新),这里保持同样的入口与表名。 */ private void updateFlag(PatMedOuthosp row) { try { patMedOuthospMapper.updateInTable("pat_med_outhosp", row); } catch (Exception e) { log.warn("【门急诊随访】更新门诊记录标志位失败,row.id={}:{}", row.getId(), e.toString()); } } /** * 门诊实体 → 存储过程入参。 *

* 本路径只用到 {@code diagcheckFlag} 与 {@code fuflag} 两个筛选条件, * 故按最小必要字段设置;不用 DtoConversionUtils—— * 它会把全部同名字段带过去,而多带的字段可能改变存储过程的 WHERE 分支。 */ private PatMedOuthospQueryReq toQueryReq(PatMedOuthosp q) { PatMedOuthospQueryReq req = new PatMedOuthospQueryReq(); req.setDiagcheckFlag(q.getDiagcheckFlag()); req.setFuflag(q.getFuflag()); return req; } private ServiceTaskMatchApi matchApi() { return providerGet(matchApiProvider, "ServiceTaskMatchApi"); } private ArchiveReadApi archiveApi() { return providerGet(archiveApiProvider, "ArchiveReadApi"); } private VisitTaskCreateApi taskApi() { return providerGet(taskApiProvider, "VisitTaskCreateApi"); } private T providerGet(ObjectProvider p, String name) { if (p == null) { return null; } try { return p.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【门急诊随访】获取 {} 失败,按不可用处理:{}", name, e.toString()); return null; } } }