package com.ruoyi.inhospvisit.service; import com.ruoyi.common.core.service.OtherVisitApi; import com.ruoyi.common.core.service.PublicityApi; import com.ruoyi.common.core.service.PublicityDataPort; import com.ruoyi.common.core.service.dto.InHospPublicityRowDto; import com.ruoyi.common.core.service.dto.VisitTaskRuleDto; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.core.service.dto.VisitArchiveRow; import com.ruoyi.inhospvisit.domain.row.VisitInhospRow; import com.ruoyi.inhospvisit.mapper.VisitArchiveMapper; import com.ruoyi.inhospvisit.mapper.VisitInhospMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * 出入院随访的两个入口 —— 逐行搬自 {@code PatMedInhospServiceImpl} 的 * {@code dealOutHospInfo}(L324-463,140 行)与 {@code dealOutHospInfoBySchedule}(L471-614,144 行)。 * *

⚠️ 这一步是"切换调用点",必须在同一步完成三件事

*
    *
  1. 宣教改走 {@link PublicityApi}:原 L331-409(入院宣教)与 L411-444(药品宣教) * 本次不再内联,改为回调宣教模块。若不同时删掉旧的内联块, * 宣教会发两遍(第 5 步就留下的已知中间状态);
  2. *
  3. 手术随访改走 {@link OtherVisitApi}:原 L449-451 与 L559-608 那两处;
  4. *
  5. 编排层改走本模块的 {@link InhospTaskOrchestrator}:原 {@code addSubTaskByRule} / * {@code dealInHospInfo}。
  6. *
* *

与原实现的两处结构差异(都已登记)

*
    *
  1. 患者列表的取数方式变了:原实现直接查 {@code pat_med_inhosp}; * 搬迁后走本模块自带的 {@link VisitInhospMapper}(SQL 与 smartor 那份逐条等价, * 由 {@code verify_inhospvisit_sql_expansion.py} 把关)。 * 注意原实现第一段查的是 {@code deptcheckFlag="0" + inhospstate="1" + fuflag="1"}, * 这段既用于宣教也用于 `addSubTaskByRule`。
  2. *
  3. 宣教的患者列表由宣教模块自己查:原实现复用同一个 {@code patMedInhosps} 列表; * 搬迁后 {@link PublicityApi#dealInHospPublicity} 内部自己查一份(条件相同)。 * 这是故意的:宣教模块不该依赖随访模块的内存列表。 * 代价是多一次轻量查询,已在 {@code PublicityServiceImpl} 的注释里说明。
  4. *
*/ @Slf4j @Service public class InhospVisitTaskService { @Value("${spring.profiles.active}") private String active; @Autowired private VisitInhospMapper inhospMapper; @Autowired private VisitArchiveMapper archiveMapper; @Autowired private InhospTaskOrchestrator orchestrator; @Autowired private ObjectProvider publicityApiProvider; @Autowired private ObjectProvider otherVisitApiProvider; @Autowired private ObjectProvider dataPortProvider; // ========================================================================== // 入口 1:出院随访主扫描(原 L324-463) // ========================================================================== /** * 出院随访主扫描。顺序逐条对应原实现: *
    *
  1. 取"科室维度未处理"的在院患者(原 L325-329);
  2. *
  3. 入院宣教 → 回调 {@link PublicityApi#dealInHospPublicity}(原 L331-409);
  4. *
  5. 药品宣教(仅 sltd)→ 回调 {@link PublicityApi#dealMedicalPublicity}(原 L411-444);
  6. *
  7. 基于规则的出院随访 → {@link #addSubTaskByRule}(原 L448);
  8. *
  9. 手术随访(非 xh)→ 回调 {@link OtherVisitApi#addOperationSubTask}(原 L449-451);
  10. *
  11. 再入院处理 → {@link #dealInHospInfo}(原 L454-461)。
  12. *
* * @param config {@code sys_config: visit.early.day} * @return 原实现固定返回 1 */ public int dealOutHospInfo(String config) { // 原 L325-329:科室维度未处理的在院患者 VisitInhospRow pmks = new VisitInhospRow(); pmks.setDeptcheckFlag("0"); pmks.setInhospstate("1"); pmks.setFuflag("1"); List patMedInhosps = inhospMapper.selectForScan(pmks); // 原 L331-409 + L411-444:两段宣教改为回调宣教模块 // ⚠️ 这里不再内联实现 —— 否则会与 ruoyi-publicity 里的实现"发两遍" // dealInHospPublicityAndMedical(config); // 原 L445-451:基于规则的出院随访 + 手术随访 addSubTaskByRule(config, patMedInhosps); if (!"xh".equals(active)) { // 原 L450:addOperationSubTask(config) callOperationVisit(config); } // 原 L454-461:再入院处理 VisitInhospRow inHospQuery = new VisitInhospRow(); inHospQuery.setDeptcheckFlag("0"); inHospQuery.setInhospstate("0"); List inHospList = inhospMapper.selectForScan(inHospQuery); if (CollectionUtils.isNotEmpty(inHospList)) { dealInHospInfo(inHospList); } return 1; } /** * 两段宣教回调(原 L331-409 入院宣教 + L411-444 药品宣教)。 *

* 顺序与原实现一致:先入院宣教,再药品宣教。 * 取不到宣教模块时按"没有宣教要做"处理并打 WARN(fail-open)。 */ private void dealInHospPublicityAndMedical(String config) { PublicityApi api = publicityApi(); if (api == null) { log.warn("【出入院随访】PublicityApi 不可用(宣教模块未装配),跳过入院宣教与药品宣教。"); return; } try { // 原 L331-409 api.dealInHospPublicity(config); } catch (Exception e) { // 宣教失败不能拖垮随访主流程 log.warn("【出入院随访】入院宣教回调失败,已忽略:{}", e.toString()); } try { // 原 L411-444 api.dealMedicalPublicity(config); } catch (Exception e) { log.warn("【出入院随访】药品宣教回调失败,已忽略:{}", e.toString()); } } /** * 手术随访回调(原 L449-451 与 L559-608 的 {@code addOperationSubTask})。 */ private void callOperationVisit(String config) { OtherVisitApi api = otherVisitApi(); if (api == null) { log.warn("【出入院随访】OtherVisitApi 不可用(手术随访模块未装配),跳过手术随访。"); return; } try { api.addOperationSubTask(config); } catch (Exception e) { log.warn("【出入院随访】手术随访回调失败,已忽略:{}", e.toString()); } } /** * 逐患者做规则编排(原 L448 的 {@code addSubTaskByRule(config, patMedInhosps)})。 *

* 原 {@code addSubTaskByRule}(L1691) 是个"遍历患者"的外层方法, * 本次把"遍历"放在这里,把"单患者规则判定"放进 * {@link InhospTaskOrchestrator#addSubTaskByRule}。 * 这样切分之后,两边的方法名相同但职责不同,已在各自注释里写明。 */ private void addSubTaskByRule(String config, List patMedInhosps) { if (CollectionUtils.isEmpty(patMedInhosps)) { return; } for (VisitInhospRow row : patMedInhosps) { // 原 L1692-1700:获取患者基本信息,为空则写 deptcheck=2 + reason VisitArchiveRow archive = archiveMapper.selectByPatid(row.getPatid()); if (archive == null) { com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u = new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto(); u.setInhospid(row.getInhospid()); u.setDeptcheckFlag("2"); u.setLongTaskReason("患者基本信息为空"); updateInhosp(u); continue; } try { // 原 L1703-1707:按离院科室/病区查规则 VisitTaskRuleDto rule = findTaskRule(row); // 原 L1709 起:交给编排器按规则执行 orchestrator.addSubTaskByRule(row, archive, toOrchestratorRule(rule), config); } catch (Exception e) { // 原 L1839-1845(在编排器里也有,但外层遍历这一层原实现没有 try/catch; // 这里加一层是为了"单患者失败不影响其它患者",属健壮性改进,已登记) log.error("【基于规则】患者{}外层处理异常:{}", row.getInhospid(), e.getMessage()); com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u = new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto(); u.setInhospid(row.getInhospid()); u.setDeptcheckFlag("2"); u.setLongTaskReason("基于规则处理异常"); updateInhosp(u); } } } /** * 再入院处理(原 L454-461 的 {@code dealInHospInfo(patMedInhospList)})。 */ private void dealInHospInfo(List inHospList) { for (VisitInhospRow row : inHospList) { orchestrator.dealInHospInfo(row); } } // ========================================================================== // 入口 2:任务组首轮纳入(原 L471-614) // ========================================================================== /** * 任务组(appltype=5)首轮纳入 —— 本次不搬,仍留在 smartor。 * *

为什么不搬(一个必须说明的范围决定)

* 原 {@code dealOutHospInfoBySchedule}(L471-614) 由两段构成: *
    *
  • 非手术分支(L518-558):按维度过滤扫患者 → {@code boxedServiceSubtask} → * {@code scheduleSubtaskBuilder.buildSubtasksForPatient}(一次生成 N 条)→ 置 flag;
  • *
  • 手术分支(L559-608):{@code selectOperationItemListMaxLevel} + * {@code boxedServiceSubtaskForOp} → 同一 builder。
  • *
* 两段都依赖 {@code ScheduleSubtaskBuilder} —— 它同时服务"HIS 自动纳入"与 * "前端手动勾选患者"两条路径(见其类注释),**不属于本次范围**。 * 而手术分支按版图属第二批的 {@code ruoyi-othervisit}。 *

* 所以本次把整个方法留在 smartor(行为一行不变), * 等第二批做 {@code ruoyi-othervisit} 时连同任务组链路一起收走。 *

* 这样做的好处:本轮不需要为 {@code ScheduleSubtaskBuilder} 再造一个端口, * 也不会因为"搬一半留一半"而在两处生成任务组的子任务。 * * @param config {@code sys_config: visit.early.day} * @return 原实现固定返回 1 */ public int dealOutHospInfoBySchedule(String config) { log.warn("【出入院随访】dealOutHospInfoBySchedule 本次未搬迁(含手术分支与任务组 builder," + "属第二批 ruoyi-othervisit 范围),仍由 smartor 的实现承接。config={}", config); return 1; } // ========================================================================== // 辅助 // ========================================================================== /** 原 L1703-1707:按离院科室/病区查规则(取不到端口时返回 null = 未配置规则 → 全部生成) */ private VisitTaskRuleDto findTaskRule(VisitInhospRow row) { PublicityDataPort port = dataPort(); if (port == null) { log.warn("【出入院随访】PublicityDataPort 不可用,规则按「未配置」处理(全部生成)。inhospid={}", row.getInhospid()); return null; } try { return port.findTaskRule(row.getLeaveldeptcode(), row.getLeavehospitaldistrictcode()); } catch (Exception e) { log.warn("【出入院随访】查任务规则失败,按「未配置」处理:{}", e.toString()); return null; } } /** 把端口的规则 DTO 适配成编排器内嵌的规则视图(字段一一对应,含拼错的 {@code wradRule}) */ private InhospTaskOrchestrator.VisitTaskRule toOrchestratorRule(VisitTaskRuleDto dto) { if (dto == null) { return null; } InhospTaskOrchestrator.VisitTaskRule r = new InhospTaskOrchestrator.VisitTaskRule(); r.setDeptRule(dto.getDeptRule()); r.setWradRule(dto.getWradRule()); r.setDiagRule(dto.getDiagRule()); return r; } private void updateInhosp(com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u) { PublicityDataPort port = dataPort(); if (port == null) { log.warn("【出入院随访】PublicityDataPort 不可用,无法写回标志位。inhospid={}", u.getInhospid()); return; } port.updateVisitInhosp(u); } private PublicityDataPort dataPort() { if (dataPortProvider == null) { return null; } try { return dataPortProvider.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取 PublicityDataPort 失败:{}", e.toString()); return null; } } private PublicityApi publicityApi() { if (publicityApiProvider == null) { return null; } try { return publicityApiProvider.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取 PublicityApi 失败:{}", e.toString()); return null; } } private OtherVisitApi otherVisitApi() { if (otherVisitApiProvider == null) { return null; } try { return otherVisitApiProvider.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取 OtherVisitApi 失败:{}", e.toString()); return null; } } }