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;
|
|
/**
|
* 门急诊随访编排:扫描未处理门诊记录 → 按科室/诊断维度匹配长期任务 → 写子任务。
|
*
|
* <h3>迁移对应(@ b25c3b51)</h3>
|
* <table border="1">
|
* <tr><th>本类方法</th><th>原实现</th></tr>
|
* <tr><td>{@link #dealOutpatientInfo()}</td>
|
* <td>{@code PatMedOuthospServiceImpl#dealOutpatientInfo} L385-427(43 行)</td></tr>
|
* <tr><td>{@link #writeInSubTask}</td>
|
* <td>同文件 L436-521(86 行)</td></tr>
|
* <tr><td>{@link #boxedServiceSubtask}</td>
|
* <td>同文件 L525-581(35 个 setter)</td></tr>
|
* </table>
|
*
|
* <h3>跨模块能力怎么拿(本模块只依赖 ruoyi-common)</h3>
|
* <ul>
|
* <li>{@link ServiceTaskMatchApi} —— 按科室({@code service_type="3"})/ 诊断维度匹配长期任务;</li>
|
* <li>{@link VisitTaskCreateApi} —— 查已发布长期任务、查待执行子任务、停旧、新增;</li>
|
* <li>{@link ArchiveReadApi} —— 患者档案只读({@code pat_archive})。</li>
|
* </ul>
|
*
|
* <h3>⚠️ 保留的原实现行为(勿"顺手优化")</h3>
|
* <ol>
|
* <li>{@code dealOutpatientInfo} 返回<b>固定 {@code 1}</b>(不是处理条数);</li>
|
* <li>匹配不到时的 flag 与 remark 逐字照抄:
|
* <b>科室</b>维度 → {@code deptcheckFlag="2"} + "通过部门,没有找到门诊随访任务ID";
|
* <b>诊断</b>维度 → {@code diagcheckFlag="2"} + "通过icd10,没有找到门诊随访任务ID";</li>
|
* <li>⚠️ 但 {@link #writeInSubTask} <b>内部</b>的 flag 映射是<b>交叉的</b>:
|
* 任务不存在时 {@code type==1 → diagcheckFlag="2"}、{@code type==2 → deptcheckFlag="2"}
|
* —— 与上面第 2 条正好相反。这是原实现的写法(出入院侧也有同类交叉痕迹),原样保留;</li>
|
* <li>{@code check=true} 且存在待执行子任务时,<b>先停旧再新增</b>,
|
* 且"停旧"要设 {@code finishtime = new Date()}(出入院侧那行是被注释掉的,门诊侧是活的);</li>
|
* <li>不需要随访({@code notrequiredFlag == "1"})时置 {@code sendstate=4} + {@code result="error"}
|
* 并把原因写进 remark,但<b>仍然继续 insert</b>(原实现没有提前 return);</li>
|
* <li>{@code setHospType("1")} 是<b>硬编码</b>,不读 {@code serviceTask.getHospType()}。</li>
|
* </ol>
|
*
|
* <h3>⚠️ 与"原 selectPatMedOuthospList"的一处有意差异(已登记)</h3>
|
* 原 {@code selectPatMedOuthospList} 除了调存储过程,还会对每条记录做<b>展示用富化</b>
|
* (用档案算年龄、补 sex/telcode/idcardno、查子任务置 {@code serverState})。
|
* 本方法的调用方是<b>后台扫描</b>而非列表展示,它随后自己要读 {@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<ArchiveReadApi> archiveApiProvider;
|
|
@Autowired
|
private ObjectProvider<ServiceTaskMatchApi> matchApiProvider;
|
|
@Autowired
|
private ObjectProvider<VisitTaskCreateApi> taskApiProvider;
|
|
/**
|
* 门诊随访主扫描。
|
* <p>
|
* 逐行对应原 {@code dealOutpatientInfo}(L385-427):
|
* <pre>
|
* 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; // 固定值
|
* </pre>
|
*
|
* @return 固定 {@code 1}(与原实现一致,<b>不是</b>处理条数)
|
*/
|
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<PatMedOuthosp> 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<ServiceTaskMatchDto> 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<ServiceTaskMatchDto> 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;
|
}
|
|
/**
|
* 把一条门诊记录写入随访子任务。
|
* <p>
|
* 逐行对应原 {@code PatMedOuthospServiceImpl#writeInSubTask}(L436-521)。
|
*
|
* @param type 1 = 科室维度;2 = 诊断维度
|
* ⚠️ 原实现的 flag 映射是<b>交叉的</b>(见类注释第 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<VisitPendingSubtaskDto> 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);
|
}
|
}
|
|
/**
|
* 新增子任务,并复刻原实现在"手机号为空"时的处理。
|
* <p>
|
* 原 L484-490 / L492-499 两处逻辑相同:
|
* <pre>
|
* 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);
|
* </pre>
|
*/
|
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);
|
}
|
|
/**
|
* 门诊版子任务组装。
|
* <p>
|
* 逐字段对应原 {@code boxedServiceSubtask}(L525-581)。
|
* 35 个赋值里绝大多数落在 {@code VisitSubtaskDraft} 已有字段上;
|
* 只有 {@code admitdate} / {@code fudate} / {@code finishtime} / {@code hospType}
|
* 落在 {@link OuthospSubtaskDraft}(见该类注释的对照表)。
|
*
|
* <h3>⚠️ 刻意保留的原行为</h3>
|
* <ul>
|
* <li>{@code setSfzh} 被调用两次(同值),原样保留;</li>
|
* <li>{@code setUpdateTime(new Date())} 被调用三次,原样保留;</li>
|
* <li>{@code serviceTask.getSendDay() == null} 时按 1 天处理,且<b>写回 serviceTask</b>;</li>
|
* <li>{@code addDays} 的基准是门诊的 {@code admitdate}(可能为 null,
|
* 原实现同样会 NPE —— 不额外加保护)。</li>
|
* </ul>
|
*/
|
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 天。
|
* <p>
|
* 逐行对应原 {@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();
|
}
|
|
/**
|
* 按"传什么更新什么"更新门诊记录的标志位。
|
* <p>
|
* 原实现调 {@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());
|
}
|
}
|
|
/**
|
* 门诊实体 → 存储过程入参。
|
* <p>
|
* 本路径只用到 {@code diagcheckFlag} 与 {@code fuflag} 两个筛选条件,
|
* 故按最小必要字段设置;<b>不用 DtoConversionUtils</b>——
|
* 它会把全部同名字段带过去,而多带的字段可能改变存储过程的 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> T providerGet(ObjectProvider<T> 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;
|
}
|
}
|
}
|