package com.ruoyi.publicity.service.impl; import com.ruoyi.common.core.service.IConfigService; import com.ruoyi.common.core.service.PublicityDataPort; import com.ruoyi.common.core.service.PublicitySubtaskApi; import com.ruoyi.common.core.service.dto.InHospPublicityRowDto; import com.ruoyi.common.core.service.dto.PublicityTaskMatchDto; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.publicity.service.PublicityService; import lombok.extern.slf4j.Slf4j; 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.List; /** * 宣教业务实现 —— 从 {@code PatMedInhospServiceImpl#dealOutHospInfo} 摘出的两段,逐行照抄。 * *

来源对照(@ b25c3b51,迁移前)

* * * * * * *
本类方法原实现位置说明
{@link #dealInHospPublicity(String)}L331-409入院宣教:科室维度 L335-372(type=4)、病区维度 L375-408(type=5)
{@link #dealMedicalPublicity(String)}L411-444药品宣教(仅 sltd,type=0)
* *

⚠️ 与增量口径有关的两个说明(都不影响结果,但必须写下来)

*
    *
  1. 患者列表被查了两次而不是一次。 * 原实现只查一次 {@code pmks}(L325-329)并复用于三个分支; * 拆成两个方法后各查一次。两次查询的条件完全相同 * ({@code deptcheck_flag='0'} + {@code inhospstate='1'} + {@code fuflag='1'}), * 且三个分支都只是"读该列表 + 匹配任务",不会修改 {@code deptcheck_flag}, * 所以列表内容一致、结果等价。代价是多一次轻量查询。
  2. *
  3. "科室/病区未配宣教任务"分支里对标志位的回写,原样保留了一个历史笔误: * 病区维度(原 L400)写的是 {@code inDeptcheckFlag} 而不是 {@code inWardcheckFlag}。 * 这意味着病区维度的失败标记落在科室那一列上。 * 不修 —— 修了会让"入院宣教"的重复扫描行为发生变化。
  4. *
* *

依赖全部可降级

* {@link PublicityDataPort}(smartor 提供的过渡实现)与 {@link PublicitySubtaskApi}(inhospvisit 门面) * 都用 {@code ObjectProvider} 惰性获取;取不到就按"没有宣教要做"处理并打 WARN, * 绝不让出入院随访主流程抛异常。 */ @Slf4j @Service public class PublicityServiceImpl implements PublicityService { /** 入院宣教自身的业务开关({@code sys_config}),只有等于 "2" 才执行 */ private static final String HELIBRARY_SWITCH_KEY = "in.helibrary.switch"; /** 这个值才开启入院宣教 */ private static final String HELIBRARY_SWITCH_ON = "2"; /** 入院宣教长期任务的 service_type */ private static final String IN_HOSP_PUBLICITY_SERVICE_TYPE = "17"; /** 药品宣教只在省立同德执行 */ private static final String SITE_SLTD = "sltd"; /** 维度:科室 */ private static final String DEPT_TYPE_DEPT = "1"; /** 维度:病区 */ private static final String DEPT_TYPE_WARD = "2"; /** 科室维度用的标志位列(与原实现 L340/L357/L368 一致) */ private static final String FLAG_COL_IN_DEPT = "in_deptcheck_flag"; /** * 病区维度用的标志位列。 *

* ⚠️ 注意:原实现病区维度的回写也用 in_deptcheck_flag(L384/L400), * 所以这里两个常量值相同 —— 刻意保留,不是笔误。 * 拆成两个常量是为了让 {@code verify_inhospvisit_semantics.py} 能分别断言。 */ private static final String FLAG_COL_IN_DEPT_FOR_WARD_QUIRK = "in_deptcheck_flag"; /** 标志位:未处理 */ private static final String FLAG_PENDING = "0"; /** 标志位:跳过/失败 */ private static final String FLAG_SKIPPED = "2"; /** 宣教子任务类型:药品宣教(走出院维度) */ private static final int TYPE_MEDICAL = 0; /** 宣教子任务类型:入院科室 */ private static final int TYPE_IN_DEPT = 4; /** 宣教子任务类型:入院病区 */ private static final int TYPE_IN_WARD = 5; @Value("${spring.profiles.active}") private String active; @Autowired private IConfigService configService; /** * 数据端口(smartor 提供过渡实现)。取不到时两个方法都直接返回 0。 */ @Autowired private ObjectProvider dataPortProvider; /** * inhospvisit 的下沉接口:由它代建子任务(含 type=0/4/5 分支与 check_flag 回写)。 */ @Autowired private ObjectProvider subtaskApiProvider; // ========================================================================== // 1. 入院宣教(原 L331-409) // ========================================================================== @Override public int dealInHospPublicity(String config) { // 原 L332-333:受 in.helibrary.switch 控制,不是 "2" 就整块不执行 String helibrarySwitch = configService == null ? null : readHelibrarySwitch(); if (StringUtils.isEmpty(helibrarySwitch) || !HELIBRARY_SWITCH_ON.equals(helibrarySwitch)) { log.debug("【宣教】in.helibrary.switch={},不是 \"2\",跳过入院宣教", helibrarySwitch); return 0; } PublicityDataPort port = dataPort(); PublicitySubtaskApi subtaskApi = subtaskApi(); if (port == null || subtaskApi == null) { return 0; } int handled = 0; handled += dealInHospPublicityByDept(port, subtaskApi, config); handled += dealInHospPublicityByWard(port, subtaskApi, config); return handled; } /** * 1.1 科室维度(原 L335-372)。 *

* 逐行对应: *

     * 原 L336   for (PatMedInhosp patMedInhosp1 : patMedInhosps)
     * 原 L338   PatArchive patArchive = patArchiveMapper.selectPatArchiveByPatid(...)
     * 原 L339-344 为空 → setInDeptcheckFlag("2") + setLongTaskReason("患者基本信息为空")
     *                     + update,continue
     * 原 L347-353  按 dept_type="1" + service_type="17" 查长期宣教任务
     * 原 L355-359  为空 → 重查记录、拼接 reason、写 in_deptcheck_flag="2"
     * 原 L361-363  逐任务 writeInSubTask(..., 4, config)
     * 原 L366-371  异常 → setInDeptcheckFlag("2") + "人员入院科室处理出异常了" + update
     * 
*/ private int dealInHospPublicityByDept(PublicityDataPort port, PublicitySubtaskApi subtaskApi, String config) { List rows = port.findInHospForPublicity(FLAG_COL_IN_DEPT); int handled = 0; for (InHospPublicityRowDto row : rows) { // 原 L338-344:患者基本信息为空 → in_deptcheck_flag="2" + reason 覆盖 if (!port.existsPatArchive(row.getPatid())) { port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT, FLAG_SKIPPED, "患者基本信息为空", false); continue; } try { // 原 L347-353:按患者所在科室查该科室的入院长期宣教任务 List tasks = port.findDeptPublicityTaskIds( row.getDeptcode(), DEPT_TYPE_DEPT, row.getOrgid(), row.getInhospid()); if (tasks == null || tasks.isEmpty()) { // 原 L355-359:未配置 → in_deptcheck_flag="2" + 追加 reason // 文案照抄(注意:原代码在旧值前不补分隔符,这里也不补) port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT, FLAG_SKIPPED, "该患者所在科室未配置入院长期宣教任务;", true); } else { for (PublicityTaskMatchDto task : tasks) { // 原 L362:writeInSubTask(taskId, true, patMedInhosp1, patArchive, 4, config) subtaskApi.generatePublicitySubtask(task.getTaskId(), row.getPatid(), row.getInhospid(), TYPE_IN_DEPT, config); handled++; } } } catch (Exception e) { // 原 L366-371:异常 → in_deptcheck_flag="2" + reason 覆盖 log.error("【宣教】人员入院科室处理出异常了:{}", e.getMessage()); port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT, FLAG_SKIPPED, "人员入院科室处理出异常了", false); } } return handled; } /** * 1.2 病区维度(原 L375-408)。 *

* ⚠️ 两个必须原样保留的点: *

*/ private int dealInHospPublicityByWard(PublicityDataPort port, PublicitySubtaskApi subtaskApi, String config) { List rows = port.findInHospForPublicity("in_wardcheck_flag"); int handled = 0; for (InHospPublicityRowDto row : rows) { // 原 L382-388:患者基本信息为空 → setInDeptcheckFlag("2") + reason 覆盖 if (!port.existsPatArchive(row.getPatid())) { port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT, FLAG_SKIPPED, "患者基本信息为空", false); continue; } // 原 L389 注释:「会出现一个问题,科室和病区发的同一个模板」—— 原样保留该行为 List tasks; try { tasks = port.findDeptPublicityTaskIds( row.getHospitaldistrictcode(), DEPT_TYPE_WARD, row.getOrgid(), row.getInhospid()); } catch (Exception e) { // ⚠️ 原实现这里【没有 try/catch】—— 病区分支是裸露的调用。 // 为了不让一次查询异常打断整个随访主流程(这属于"健壮性"而非"行为"), // 这里选择捕获并记录,同时把该条标记为跳过。 // 该差异已登记在 verify_inhospvisit_semantics.py 的"刻意差异"清单里。 log.error("【宣教】人员入院病区处理出异常了:{}", e.getMessage()); port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT_FOR_WARD_QUIRK, FLAG_SKIPPED, "人员入院病区处理出异常了", false); continue; } if (tasks == null || tasks.isEmpty()) { // 原 L398-402:⚠️ 回写的是 in_deptcheck_flag(笔误),文案同样不补分隔符、不判旧值 null port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT_FOR_WARD_QUIRK, FLAG_SKIPPED, "该患者所在病区未配置入院长期宣教任务;", true); } else { for (PublicityTaskMatchDto task : tasks) { // 原 L405:writeInSubTask(taskId, true, patMedInhosp1, patArchive, 5, config) subtaskApi.generatePublicitySubtask(task.getTaskId(), row.getPatid(), row.getInhospid(), TYPE_IN_WARD, config); handled++; } } } return handled; } // ========================================================================== // 2. 药品宣教(原 L411-444) // ========================================================================== @Override public int dealMedicalPublicity(String config) { // 原 L412:仅 sltd 站点 if (!SITE_SLTD.equals(active)) { log.debug("【宣教】active={},非 sltd,跳过药品宣教", active); return 0; } PublicityDataPort port = dataPort(); PublicitySubtaskApi subtaskApi = subtaskApi(); if (port == null || subtaskApi == null) { return 0; } // ⚠️ 原实现复用的是"科室维度未处理"的那份列表(L325-329 + L413), // 所以这里也用 deptcheck_flag='0' 维度取。 List rows = port.findInHospForPublicity(FLAG_COL_IN_DEPT); int handled = 0; for (InHospPublicityRowDto row : rows) { String medicalCode = row.getMedicalCode(); if (StringUtils.isEmpty(medicalCode)) { continue; } String[] split = medicalCode.split(","); // 原 L417-421:防御性编程,确保两个串不为 null String medicalCompanyStr = row.getMedicalCompany() != null ? row.getMedicalCompany() : ""; String medicalNameStr = row.getMedicalName() != null ? row.getMedicalName() : ""; String[] medicalCompanyArray = medicalCompanyStr.split(","); // 原实现同样解析了 name 数组但【从未使用】——保留解析以提示这一点 @SuppressWarnings("unused") String[] medicalNameArray = medicalNameStr.split(","); for (int i = 0; i < split.length; i++) { String medicalNameForQuery = null; String companyForQuery; if (i < medicalCompanyArray.length) { // 原 L428-429:正常分支只设 company companyForQuery = medicalCompanyArray[i]; } else { // 原 L430-433:越界分支 —— 打 WARN,并【把 medicalName 置为空串】、 // 【不设 medicalCompany】(保持 null)。 // 这两个值会真实影响 SQL:name 空串 → 跳过 name 条件; // company null → 跳过 company 条件。即越界时只按 medical_code 查。 // 【原样保留】—— 改了就会改变查询条件(这正是原代码的历史笔误)。 log.warn("【药品宣教】药品编码{}对应的公司信息下标越界,medicalCompany 数组长度={}, 当前索引={}", split[i], medicalCompanyArray.length, i); medicalNameForQuery = ""; companyForQuery = null; } List tasks = port.findMedicalPublicityTaskIds( split[i], medicalNameForQuery, companyForQuery, row.getInhospid()); if (tasks != null && !tasks.isEmpty()) { // 原 L435-438:命中就取第一条建子任务(check=true,由 writeInSubTask 内部去重) subtaskApi.generatePublicitySubtask(tasks.get(0).getTaskId(), row.getPatid(), row.getInhospid(), TYPE_MEDICAL, config); handled++; } // 原 L440 注释:查不到的药品就是不需要宣教的,就不管了 } } return handled; } // ========================================================================== // 依赖获取(全部可降级) // ========================================================================== private String readHelibrarySwitch() { try { return configService.selectConfigByKey(HELIBRARY_SWITCH_KEY); } catch (Exception e) { log.warn("【宣教】读取 sys_config 的 {} 失败,本次按未开启处理:{}", HELIBRARY_SWITCH_KEY, e.toString()); return null; } } /** * 取数据端口;取不到时打 WARN 并返回 null(调用方据此返回 0) */ private PublicityDataPort dataPort() { if (dataPortProvider == null) { return null; } try { PublicityDataPort port = dataPortProvider.stream().findFirst().orElse(null); if (port == null) { log.warn("【宣教】容器里没有 PublicityDataPort 实现(smartor 的 PublicityDataPortBridge 未装配)," + "本次不做任何宣教。"); } return port; } catch (Exception e) { log.warn("【宣教】获取 PublicityDataPort 失败,本次不做任何宣教:{}", e.toString()); return null; } } /** * 取 inhospvisit 的下沉接口;取不到时打 WARN 并返回 null(调用方据此返回 0) */ private PublicitySubtaskApi subtaskApi() { if (subtaskApiProvider == null) { return null; } try { PublicitySubtaskApi api = subtaskApiProvider.stream().findFirst().orElse(null); if (api == null) { log.warn("【宣教】容器里没有 PublicitySubtaskApi 实现(ruoyi-inhospvisit 的门面未装配)," + "本次不做任何宣教。"); } return api; } catch (Exception e) { log.warn("【宣教】获取 PublicitySubtaskApi 失败,本次不做任何宣教:{}", e.toString()); return null; } } /** * 原实现里 {@code PatMedInhosp.patid} 与 {@code PatArchive.id} 都是 {@code Long} * ({@code PatMedInhosp.java:186} / {@code PatArchive.java:31}), * 而 {@code generatePublicitySubtask} 的入参也是 {@code Long},所以这里是直接透传, * 不存在类型转换。 */ // 说明:早期草稿曾以为 patid 是 String 而写过一个 toLong() 帮助方法; // 核实源码后确认是 Long,该方法已删除(保留此注释以避免后人重复踩这个误判)。 }