package com.ruoyi.othervisit.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.ServiceTaskMatchDto; import com.ruoyi.common.core.service.dto.VisitArchiveRow; import com.ruoyi.common.core.service.dto.VisitLongTaskDto; import com.ruoyi.common.core.service.dto.VisitSubtaskDraft; import com.ruoyi.othervisit.domain.PatMedOperation; 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.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.stream.Collectors; /** * 手术随访编排:扫描未处理的手术记录 → 按手术编码匹配长期任务 → 写子任务。 * *

迁移对应(@ b25c3b51)

* * * * * * * * * *
本类方法原实现
{@link #dealOperationInfo()}{@code PatMedOperationServiceImpl#dealOperationInfo} L107-149(42 行)
{@link #writeInSubTask}同文件 L152-187(36 行)
{@link #boxedServiceSubtask}同文件 L191-238(48 行,29 个 setter)
{@link #addDays}同文件 L240-248
* *

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

* * 任务创建本身本次不迁:你要求它将来单独成一个模块,本模块只调上面这些接口, * 将来换实现方时本类一行都不用改。 * *

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

*
    *
  1. {@code dealOperationInfo} 返回固定 {@code 1}(不是处理条数);
  2. *
  3. 匹配不到任务 → {@code checkFlag="2"} + 原因逐字照抄 * 「该手术编码,未配置长期任务;」(注意末尾是分号,且与出入院/门诊的文案不同);
  4. *
  5. 匹配到多个任务 → 原因写「该手术编码,有多个随访长期任务:」+ taskIds 列表的 toString, * 先 update 一次,然后只用第一个继续;
  6. *
  7. ⚠️ 记录本身不参与"多任务"分支的 checkFlag 赋值:那一支只写 {@code longTaskReason}, * 不写 {@code checkFlag},随后由 {@code writeInSubTask} 成功时置 {@code "1"} —— 原样保留;
  8. *
  9. ⚠️ 每条记录的整段处理被 {@code try/catch(Exception)} 包住,异常只 * {@code printStackTrace()} + {@code log.error} 后继续下一条 * (原实现会吞掉异常);本实现把入参从 {@code PatArchive} 换成了 {@link VisitArchiveRow}, * 但 {@code getNotrequiredFlag()} 在 {@link VisitArchiveRow} 里仍是 {@code String}, * 所以"档案查不到 → NPE → 被吞 → 该条不处理"这一行为逐字保持;
  10. *
  11. {@code check=true} 是硬编码传入的({@link #dealOperationInfo()} 里写死), * 而 {@code writeInSubTask} 的 {@code check} 分支实际上没有任何判断 * —— 这是原实现的写法,不要为它补逻辑;
  12. *
  13. {@code setHospType("2")} 是硬编码,不读 {@code serviceTask.getHospType()}。
  14. *
* *

⚠️ 与门诊版的三处不同(别照抄门诊)

*
    *
  1. 没有"先停旧再新增":{@code check} 参数被接收但完全不使用, * 手术路径从不查/停待执行子任务;
  2. *
  3. 不使用 {@code OuthospSubtaskDraft}:手术的 {@code admitdate}/{@code fudate}/ * {@code finishtime} 三个字段在原实现里就没有被 set,故直接用 {@link VisitSubtaskDraft};
  4. *
  5. 入参是整个 {@link PatMedOperation}(不是明细), * 因为原实现对 {@code pat_med_operation} 逐条处理。
  6. *
* *

开关

* 本类不含开关判断 —— 由 {@link com.ruoyi.othervisit.api.OtherVisitFacade} 统一决定是否调用。 */ @Slf4j @Service public class OperationVisitTaskService { /** 待执行状态(原实现硬编码 2L) */ private static final Long SENDSTATE_PENDING = 2L; /** 不执行状态(原实现硬编码 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_OPERATION = "2"; @Autowired private OperationService operationService; @Autowired private ObjectProvider archiveApiProvider; @Autowired private ObjectProvider matchApiProvider; @Autowired private ObjectProvider taskApiProvider; /** * 手术随访主扫描。 *

* 逐行对应原 {@code dealOperationInfo}(L107-149): *

     * PatMedOperation q = new PatMedOperation();
     * q.setCheckFlag("0");                       // 未处理
     * List<PatMedOperation> list = selectPatMedOperationList(q);
     * log.error("dealOperationInfo需要处理的手术信息:{}", list);   // ⚠️ 原实现是 error 级
     * for (每条记录) {
     *     查档案;
     *     try {
     *         按 opcode 查 longtask=1 的任务;
     *         为空  -> checkFlag="2" + "该手术编码,未配置长期任务;" + update + continue
     *         多个  -> longTaskReason="该手术编码,有多个随访长期任务:"+taskIds + update,只留第一个
     *         writeInSubTask(第一个任务ID, true, 记录, 档案);
     *     } catch (Exception e) { e.printStackTrace(); log.error("人员处理出异常了:{}", e.getMessage()); }
     * }
     * return 1;   // 固定值
     * 
* * @return 固定 {@code 1}(与原实现一致,不是处理条数) */ public Integer dealOperationInfo() { ServiceTaskMatchApi matchApi = matchApi(); VisitTaskCreateApi taskApi = taskApi(); ArchiveReadApi archiveApi = archiveApi(); if (matchApi == null || taskApi == null) { log.warn("【手术随访】跨模块契约不可用(ServiceTaskMatchApi={}, VisitTaskCreateApi={}),跳过本次手术随访。", matchApi != null, taskApi != null); return 1; } PatMedOperation query = new PatMedOperation(); // 获取需要出院随访,未处理的数据 query.setCheckFlag("0"); List patMedOperations = operationService.selectList(query); // ⚠️ 原实现用的是 log.error(不是 info/warn),级别照抄 log.error("dealOperationInfo需要处理的手术信息:{}", patMedOperations); if (CollectionUtils.isEmpty(patMedOperations)) { return 1; } for (PatMedOperation patMedOperation1 : patMedOperations) { // 获取患者信息 VisitArchiveRow patArchive = archiveApi == null ? null : archiveApi.findByPatid(patMedOperation1.getPatid()); try { // 根据患者所在科室,获取该手术编码对应的长期任务 List serviceTaskopers = matchApi.findLongTasksByOpcode(patMedOperation1.getOpcode()); // 如果部门模板为空(暂定,将出入院表的check_flag设置为2 ,long_task_reason 记录原因) if (CollectionUtils.isEmpty(serviceTaskopers) || serviceTaskopers.size() == 0) { patMedOperation1.setCheckFlag("2"); patMedOperation1.setLongTaskReason("该手术编码,未配置长期任务;"); operationService.update(patMedOperation1); continue; } else if (serviceTaskopers.size() > 1) { // 如果查询出多个任务,那就取第一个,并在患者备注一下 List taskIds = serviceTaskopers.stream().map(ServiceTaskMatchDto::getTaskId).collect(Collectors.toList()); patMedOperation1.setLongTaskReason("该手术编码,有多个随访长期任务:" + taskIds); operationService.update(patMedOperation1); ServiceTaskMatchDto serviceTaskoper1 = serviceTaskopers.get(0); serviceTaskopers = new ArrayList<>(); serviceTaskopers.add(serviceTaskoper1); } writeInSubTask(taskApi, serviceTaskopers.get(0).getTaskId(), Boolean.TRUE, patMedOperation1, patArchive); } catch (Exception e) { e.printStackTrace(); log.error("人员处理出异常了:{}", e.getMessage()); } } return 1; } /** * 把一条手术记录写入随访子任务。 *

* 逐行对应原 {@code PatMedOperationServiceImpl#writeInSubTask}(L152-187)。 * *

⚠️ {@code check} 参数在原实现里没有被使用

* 原方法签名带 {@code Boolean check},但方法体里一次都没读它 * (不像门诊版会用它决定"先停旧再新增")。这里刻意保留同样的签名与"不读"的写法, * 以免将来对照代码时误以为漏了一段逻辑。 * * @param check 原实现传入但未使用;保留签名只为逐行可比 */ private void writeInSubTask(VisitTaskCreateApi taskApi, Long taskid, Boolean check, PatMedOperation patMedOperation1, VisitArchiveRow patArchive) { String longTaskReason = patMedOperation1.getLongTaskReason(); if (StringUtils.isEmpty(longTaskReason)) longTaskReason = ""; // 原 L156-159:查「已发布的长期任务」(taskid + sendState=2) VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid); if (serviceTask == null) { log.error("该手术编码长期任务不存在,任务ID为:{}", taskid); patMedOperation1.setCheckFlag("2"); patMedOperation1.setLongTaskReason(longTaskReason + " & 该手术编码长期任务不存在,任务ID为:" + taskid); operationService.update(patMedOperation1); return; } // 封装serviceSubtask VisitSubtaskDraft serviceSubtask = boxedServiceSubtask(serviceTask, patMedOperation1, patArchive); boolean i = taskApi.insertSubtask(serviceSubtask); if (i) { // 将check_flag改成1(已处理) PatMedOperation patMedOperation = new PatMedOperation(); patMedOperation.setId(patMedOperation1.getId()); patMedOperation.setCheckFlag("1"); operationService.update(patMedOperation); } else { // 生成子任务失败, log.error("生成子任务失败serviceSubtask为:{}", serviceSubtask); PatMedOperation patMedOperation = new PatMedOperation(); patMedOperation.setId(patMedOperation1.getId()); patMedOperation.setRemark("生成子任务失败"); operationService.update(patMedOperation); } } /** * 手术版子任务组装。 *

* 逐字段对应原 {@code boxedServiceSubtask}(L191-238,29 个 setter)。 * *

原实现第一行是 {@code DtoConversionUtils.sourceToTarget(serviceTask, ServiceSubtask.class)}

* 它整批拷了 {@code ServiceTask} 与 {@code ServiceSubtask} 的 33 个同名字段, * 其中 18 个之后再没被显式覆盖。本次换了数据来源—— * {@code serviceTask} 现在是 {@link VisitLongTaskDto}(15 个字段的任务快照), * 不是 {@code ServiceTask} 实体。 *

* ⚠️ 这是一个已登记的、与门诊版完全相同的口径限制: * 那 18 个"靠拷贝带进子任务"的字段里,{@code VisitLongTaskDto} 只承载 * {@code taskName}/{@code type}/{@code preachformDesc}/{@code createBy} 等已列出的几个, * 其余({@code guid}/{@code pid}/{@code sendType}/{@code isupload}/…) * 需要由将来的「随访任务创建模块」在实现 {@code insertSubtask} 时补齐。 * 本模块不自己补,也不把它们加到 {@code VisitSubtaskDraft} 上 —— * 那会让出入院/门诊两条已验收的链路多写列。 * 落地方式与门诊侧({@code OuthospVisitTaskService#boxedServiceSubtask})一致。 * *

⚠️ 刻意保留的原行为

*
    *
  • {@code setStarttime} 与 {@code setEndtime} 同值(都取 {@code opreqtime}),原样保留;
  • *
  • {@code serviceTask.getSendDay() == null} 时按 1 天处理,且写回 serviceTask;
  • *
  • {@code addDays} 的基准是 {@code opreqtime}(可能为 null, * 原实现同样会 NPE —— 不额外加保护);
  • *
  • 不需要随访时置 {@code sendstate=4} + {@code result="error"} 并把原因写进 remark, * 但仍然继续 insert(原实现没有提前 return);
  • *
  • {@code setPhone} 只在 {@code telcode} 为空时才回落到 {@code relativetelcode} * (与门诊版"三元表达式"写法等价,此处照抄原 if 写法)。
  • *
*/ public VisitSubtaskDraft boxedServiceSubtask(VisitLongTaskDto serviceTask, PatMedOperation patMedOperation1, VisitArchiveRow patArchive) { VisitSubtaskDraft serviceSubtask = new VisitSubtaskDraft(); serviceSubtask.setTaskid(serviceTask.getTaskid()); serviceSubtask.setOrgid(serviceTask.getOrgid()); serviceSubtask.setCampusid(serviceTask.getCampusid()); // 原 L194-195:libtemplateid 在 ServiceTask 里是 String,落库列/草稿里是 Long, // 原实现显式 Long.valueOf 转换,且【仅在非空时】设值(为空则保持 null -> 列不写)。原样保留。 if (StringUtils.isNotEmpty(serviceTask.getLibtemplateid())) serviceSubtask.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid())); serviceSubtask.setNurseId(patMedOperation1.getNurseId()); serviceSubtask.setNurseName(patMedOperation1.getNurseName()); // serviceSubtask.setInhospid(patMedOperation1.getInhospid()); serviceSubtask.setDrcode(patMedOperation1.getDrcode()); serviceSubtask.setDrname(patMedOperation1.getDrname()); serviceSubtask.setDeptcode(patMedOperation1.getDeptcode()); serviceSubtask.setDeptname(patMedOperation1.getDeptname()); serviceSubtask.setTemplateid(serviceTask.getTemplateid()); serviceSubtask.setTemplatename(serviceTask.getTemplatename()); serviceSubtask.setPatid(patArchive.getId()); serviceSubtask.setSendname(patArchive.getName()); serviceSubtask.setSfzh(patArchive.getIdcardno()); serviceSubtask.setPhone(patArchive.getTelcode()); if (StringUtils.isBlank(patArchive.getTelcode())) serviceSubtask.setPhone(patArchive.getRelativetelcode()); serviceSubtask.setSex(patArchive.getSex()); serviceSubtask.setAge(patArchive.getAge()); serviceSubtask.setSendstate(SENDSTATE_PENDING); serviceSubtask.setStarttime(patMedOperation1.getOpreqtime()); serviceSubtask.setEndtime(patMedOperation1.getOpreqtime()); serviceSubtask.setServiceType("2"); serviceSubtask.setPreachform(serviceTask.getPreachform()); serviceSubtask.setHospType(HOSP_TYPE_OPERATION); serviceSubtask.setCreateTime(new Date()); serviceSubtask.setUpdateTime(new Date()); // serviceSubtask.setCreateBy(patMedInhosp1.getNurseName()); // serviceSubtask.setLeavehospitaldistrictcode(patMedInhosp1.getLeavehospitaldistrictcode()); // serviceSubtask.setLeavehospitaldistrictname(patMedInhosp1.getLeavehospitaldistrictname()); // 设置发送时间 if (serviceTask.getSendDay() == null) serviceTask.setSendDay(1L); Date newDate = addDays(patMedOperation1.getOpreqtime(), serviceTask.getSendDay().intValue()); serviceSubtask.setLongSendTime(newDate); serviceSubtask.setVisitTime(newDate); // 患者发送时间 if (StringUtils.isNotEmpty(patArchive.getNotrequiredFlag()) && patArchive.getNotrequiredFlag().equals("1")) { String remark = patArchive.getNotrequiredreason(); serviceSubtask.setRemark(remark); serviceSubtask.setResult("error"); // serviceSubtask.setFinishtime(new Date()); // 不执行 serviceSubtask.setSendstate(SENDSTATE_SKIP); } return serviceSubtask; } /** * 在 {@code date} 上加 {@code days} 天;{@code days} 为 null 时按 1 天。 *

* 逐行对应原 {@code PatMedOperationServiceImpl#addDays}(L240-248)。 * ⚠️ 与出入院侧的 {@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(); } // ========================================================================== // 契约取用(与门诊/出入院模块同一写法:ObjectProvider + stream,取不到就降级) // ========================================================================== 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; } } }