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) |
*
*
* ⚠️ 与增量口径有关的两个说明(都不影响结果,但必须写下来)
*
* - 患者列表被查了两次而不是一次。
* 原实现只查一次 {@code pmks}(L325-329)并复用于三个分支;
* 拆成两个方法后各查一次。两次查询的条件完全相同
* ({@code deptcheck_flag='0'} + {@code inhospstate='1'} + {@code fuflag='1'}),
* 且三个分支都只是"读该列表 + 匹配任务",不会修改 {@code deptcheck_flag},
* 所以列表内容一致、结果等价。代价是多一次轻量查询。
* - "科室/病区未配宣教任务"分支里对标志位的回写,原样保留了一个历史笔误:
* 病区维度(原 L400)写的是 {@code inDeptcheckFlag} 而不是 {@code inWardcheckFlag}。
* 这意味着病区维度的失败标记落在科室那一列上。
* 不修 —— 修了会让"入院宣教"的重复扫描行为发生变化。
*
*
* 依赖全部可降级
* {@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)。
*
* ⚠️ 两个必须原样保留的点:
*
* - 查患者用的是 {@code in_wardcheck_flag='0'};
* - 但"未配宣教任务"时回写的仍是 {@code in_deptcheck_flag}(原 L400 的笔误)。
*
*/
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,该方法已删除(保留此注释以避免后人重复踩这个误判)。
}