package com.ruoyi.inhospvisit.support;
|
|
import com.ruoyi.common.core.service.dto.VisitSubtaskDraft;
|
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.domain.row.VisitTaskRow;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.stereotype.Component;
|
|
import java.util.Calendar;
|
import java.util.Date;
|
|
/**
|
* 随访子任务「组装器」—— 逐行搬自 {@code PatMedInhospServiceImpl} 的两个 boxed 方法。
|
*
|
* <h3>来源对照(@ b25c3b51)</h3>
|
* <table border="1">
|
* <tr><th>本类方法</th><th>原实现</th><th>行数</th></tr>
|
* <tr><td>{@link #boxedServiceSubtask}</td><td>{@code boxedServiceSubtask}(L1108-1222)</td><td>115</td></tr>
|
* <tr><td>{@link #boxedServiceSubtaskForOp}</td><td>{@code boxedServiceSubtaskForOp}(L1225-1300)</td><td>76</td></tr>
|
* <tr><td>{@link #addDays}</td><td>{@code addDays}(L663-671)</td><td>9</td></tr>
|
* <tr><td>{@link #minusDay}</td><td>{@code minusDay}(L1309-1318)</td><td>10</td></tr>
|
* <tr><td>{@link #updateCheckFlag}</td><td>{@code updateCheckFlag}(L1660-1684)</td><td>25</td></tr>
|
* </table>
|
*
|
* <h3>⚠️ 三个最容易被漏掉的语义(本类已逐个显式处理)</h3>
|
* <ol>
|
* <li><b>{@code DtoConversionUtils.sourceToTarget} 的隐性字段搬运</b>:
|
* 原实现第一行把 {@code ServiceTask}→{@code ServiceSubtask} 的 33 个同名字段整批拷过来,
|
* 其中 <b>18 个之后再也没被显式覆盖</b>({@code delFlag diagType endOutHospTime guid isupload
|
* leavehospitaldistrictcodes leaveldeptcodes libtemplatename pageNum pageSize pid sendType
|
* startOutHospTime taskDesc taskName textParam type uploadTime})。
|
* 本类把这 18 个<b>逐个显式</b>搬进 {@link VisitSubtaskDraft},见
|
* {@link #applyCarriedOverFieldsFromTask}。少一个就会静默丢值。</li>
|
* <li><b>两个方法失败时的返回语义不同</b>:
|
* {@code boxedServiceSubtask} 出院时间为空时返回 <b>{@code new ServiceSubtask()} 空对象</b>;
|
* {@code boxedServiceSubtaskForOp} 返回 <b>{@code null}</b>。
|
* 所以本类第一个返回"空草稿",第二个返回 {@code null}。</li>
|
* <li><b>第一个方法会<b>顺手写库</b></b>:出院时间为空时它把
|
* {@code deptcheckFlag="2"} 与 {@code remark} 写回 {@code pat_med_inhosp}。
|
* 这属于"副作用",本类通过 {@link BoxedSideEffect} 回调交回调用方执行,
|
* 避免组装器直接依赖 Mapper。</li>
|
* </ol>
|
*
|
* <h3>⚠️ 与方案的差异(已登记)</h3>
|
* 方案原计划叫 {@code VisitSubtaskFactory} 并同时承载 {@code writeInSubTask}。
|
* 实际实现时把它收窄为"纯组装 + 纯工具",因为 {@code writeInSubTask}(232 行)里有大量
|
* 写库与去重编排,混在一起会让本类的 A 档逐行等价探针失去意义。
|
*
|
* <h3>⚠️ 唯一的"非搬运"行:{@code hospno}</h3>
|
* {@link #boxedServiceSubtask} 里多了一句 {@code d.setHospno(inhosp.getInhospno())}。
|
* 原实现确实<b>没有</b>这一句,所以出院链路的 {@code service_subtask.hospno} 一直落空;
|
* 而 {@code ServiceSubtask.hospno} 的语义就是"就诊编号(对应的 outhospno、inhospno)"。
|
* 这是<b>有意补齐</b>,不是抄漏 —— 反向探针({@code verify_inhospvisit_boxed_fields.py})
|
* 只校验"原 setter 是否都有对应调用",不会因此报错。
|
*/
|
@Slf4j
|
@Component
|
public class VisitSubtaskFactory {
|
|
/** 出院随访(及专病随访)的任务类型值:通知类,需要提前 N 天发 */
|
private static final String TASK_TYPE_NOTICE = "3";
|
|
/** 入院宣教的服务类型 */
|
private static final String SERVICE_TYPE_IN_HOSP_PUBLICITY = "17";
|
|
/**
|
* 景宁机构ID(原实现里硬编码的白名单,原样保留)。
|
* <p>
|
* ⚠️ 这个值我第一版就抄错了一位(写成了 {@code ...1271011A1001})。
|
* 已逐字核对原实现出现过的 4 处:
|
* {@code PatMedInhospServiceImpl} L798 / L1186(注释) / L1195 / L1338,均为
|
* {@code 47255004333112711A1001}。抄错会导致"景宁的白名单分支永远不生效"。
|
*/
|
private static final String ORG_JINGNING = "47255004333112711A1001";
|
|
@Value("${spring.profiles.active}")
|
private String active;
|
|
/**
|
* 组装院外随访(科室/病区/疾病/宣教/药品)的子任务草稿。
|
* <p>
|
* 逐行对应 {@code boxedServiceSubtask(ServiceTask, PatMedInhosp, PatArchive, String, Integer)} L1108-1222。
|
*
|
* @param serviceTask 长期任务
|
* @param inhosp 出入院记录
|
* @param archive 患者档案
|
* @param config {@code sys_config: visit.early.day}
|
* @param type 0=药品宣教 / 1=离院科室 / 2=离院病区 / 3=疾病 / 4=入院科室 / 5=入院病区
|
* @param sideEffect 副作用回调(出院时间为空时要把标志位与备注写回库);可为 {@code null}(测试用)
|
* @return 组装好的草稿;<b>出院时间为空且 type∉{4,5} 时返回"空草稿"</b>(对应原实现 {@code new ServiceSubtask()})
|
*/
|
public VisitSubtaskDraft boxedServiceSubtask(VisitTaskRow serviceTask, VisitInhospRow inhosp,
|
VisitArchiveRow archive, String config, Integer type,
|
BoxedSideEffect sideEffect) {
|
VisitSubtaskDraft d = new VisitSubtaskDraft();
|
|
// ---- 原 L1113:DtoConversionUtils 的整批拷贝(18 个字段靠它带过来)----
|
applyCarriedOverFieldsFromTask(d, serviceTask);
|
|
// ---- 原 L1114-1118 ----
|
d.setTaskid(serviceTask.getTaskid());
|
d.setOrgid(serviceTask.getOrgid());
|
d.setCampusid(serviceTask.getCampusid());
|
if (StringUtils.isNotEmpty(serviceTask.getLibtemplateid())) {
|
d.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid()));
|
}
|
// ---- 原 L1119-1127 ----
|
d.setNurseId(inhosp.getNurseId());
|
d.setNurseName(inhosp.getNurseName());
|
d.setInhospid(inhosp.getInhospid());
|
// ⚠️ 这一行是【补的】,不是搬运:
|
// 原 boxedServiceSubtask 从未设 hospno,而 ServiceSubtask.hospno 的语义是
|
// "就诊编号(对应的 outhospno、inhospno)"(见 ServiceSubtask 字段注释)。
|
// 结果就是出院链路的 service_subtask.hospno 一直是 NULL,与门诊链路不一致。
|
// 取值:pat_med_inhosp.inhospno(门诊侧取 pat_med_outhosp.outhospno,见 OuthospSubtaskDraft)。
|
d.setHospno(inhosp.getInhospno());
|
d.setDrcode(inhosp.getDrcode());
|
d.setDrname(inhosp.getDrname());
|
d.setDeptcode(inhosp.getLeaveldeptcode());
|
d.setDeptname(inhosp.getLeaveldeptname());
|
d.setLeavehospitaldistrictcode(inhosp.getLeavehospitaldistrictcode());
|
d.setLeavehospitaldistrictname(inhosp.getLeavehospitaldistrictname());
|
// ---- 原 L1128-1134:type 4/5 改用入院维度 ----
|
if (type != null && (type == 4 || type == 5)) {
|
d.setDeptcode(inhosp.getDeptcode());
|
d.setDeptname(inhosp.getDeptname());
|
d.setLeavehospitaldistrictcode(inhosp.getHospitaldistrictcode());
|
d.setLeavehospitaldistrictname(inhosp.getHospitaldistrictname());
|
}
|
// ---- 原 L1135-1146 ----
|
d.setLeaveicd10code(inhosp.getLeaveicd10code());
|
d.setLeavediagname(inhosp.getLeavediagname());
|
d.setDiagname(inhosp.getDiagname());
|
d.setTemplateid(serviceTask.getTemplateid());
|
d.setTemplatename(serviceTask.getTemplatename());
|
d.setPatid(archive.getId());
|
d.setSendname(archive.getName());
|
d.setSfzh(archive.getIdcardno());
|
d.setPhone(archive.getTelcode());
|
if (StringUtils.isBlank(archive.getTelcode())) {
|
d.setPhone(archive.getRelativetelcode());
|
}
|
d.setSex(archive.getSex());
|
d.setAge(archive.getAge());
|
// ---- 原 L1147-1163 ----
|
d.setSendstate(2L);
|
d.setCampusid(inhosp.getCampusid());
|
d.setManagementDoctor(inhosp.getManagementDoctor());
|
d.setManagementDoctorCode(inhosp.getManagementDoctorCode());
|
d.setStarttime(inhosp.getStarttime());
|
d.setEndtime(inhosp.getEndtime());
|
d.setServiceType(serviceTask.getServiceType());
|
d.setPreachform(serviceTask.getPreachform());
|
d.setHospType("2");
|
if (type != null && (type == 4 || type == 5)) {
|
d.setHospType("6");
|
}
|
d.setCreateTime(new Date());
|
d.setUpdateTime(new Date());
|
d.setCreateBy(inhosp.getNurseName());
|
d.setLeavehospitaldistrictcode(inhosp.getLeavehospitaldistrictcode());
|
d.setLeavehospitaldistrictname(inhosp.getLeavehospitaldistrictname());
|
d.setUpdateBy(serviceTask.getUpdateBy());
|
d.setUpdateTime(new Date());
|
// 原 L1164-1165:如果是宣教就设置为7
|
// ⚠️ 原实现直接 getServiceType().equals("17"),serviceType 为 null 会 NPE —— 原样保留该风险
|
if (serviceTask.getServiceType() != null
|
&& SERVICE_TYPE_IN_HOSP_PUBLICITY.equals(serviceTask.getServiceType())) {
|
d.setTaskSituation(7);
|
}
|
// ---- 原 L1166-1182:发送时间 ----
|
if (serviceTask.getSendDay() == null) {
|
serviceTask.setSendDay(1L);
|
}
|
Date newDate;
|
if (inhosp.getEndtime() != null) {
|
newDate = addDays(inhosp.getEndtime(), serviceTask.getSendDay().intValue());
|
} else {
|
// 如果等于4/5说明是入院宣教,入院不可能有 endTime
|
if (type == null || (type != 4 && type != 5)) {
|
// 原 L1174-1178:★副作用:写回 deptcheckFlag="2" + remark,然后返回【空草稿】
|
if (sideEffect != null) {
|
sideEffect.onMissingOutHospTime(inhosp);
|
}
|
return new VisitSubtaskDraft();
|
}
|
// 入院宣教,基本当天或隔天就要发出
|
newDate = addDays(new Date(), serviceTask.getSendDay().intValue());
|
}
|
d.setLongSendTime(newDate);
|
d.setVisitTime(newDate);
|
|
// ---- 原 L1187-1203:通知类提前 N 天 ----
|
if (TASK_TYPE_NOTICE.equals(serviceTask.getType())) {
|
d.setVisitTime(minusDay(newDate, StringUtils.isNotEmpty(config) ? Integer.parseInt(config) : 0));
|
if (inhosp.getFudate() != null) {
|
d.setVisitTime(minusDay(inhosp.getFudate(), StringUtils.isNotEmpty(config) ? Integer.parseInt(config) : 0));
|
} else {
|
// 判断是不是景宁
|
if (ORG_JINGNING.equals(inhosp.getOrgid())) {
|
d.setVisitTime(null);
|
// 不执行
|
d.setSendstate(4L);
|
d.setRemark("景宁没有给随访时间,不发送通知短信");
|
}
|
}
|
}
|
// ---- 原 L1204-1210:南华附一:入院出院同一天不随访 ----
|
if ("nhfy".equals(active) && inhosp.getStarttime() != null && inhosp.getEndtime() != null
|
&& daysBetweenByMillisecond(inhosp.getStarttime(), inhosp.getEndtime()) == 0) {
|
d.setRemark("患者当天出院,不用随访");
|
d.setResult("error");
|
// 不执行
|
d.setSendstate(4L);
|
}
|
// ---- 原 L1212-1220:患者维度的"免随访"标记 ----
|
if (StringUtils.isNotEmpty(archive.getNotrequiredFlag()) && "1".equals(archive.getNotrequiredFlag())) {
|
d.setRemark(archive.getNotrequiredreason());
|
d.setResult("error");
|
d.setTaskSituation(6);
|
// 不执行
|
d.setSendstate(4L);
|
}
|
return d;
|
}
|
|
/**
|
* 组装手术随访的子任务草稿。
|
* <p>
|
* 逐行对应 {@code boxedServiceSubtaskForOp(ServiceTask, PatMedInhosp, PatMedOperationItem, PatArchive, String)}
|
* L1225-1300。
|
* <p>
|
* ⚠️ 与 {@link #boxedServiceSubtask} 的两处关键差异:
|
* <ol>
|
* <li>出院时间为空时返回 <b>{@code null}</b>(不是空草稿),并且写的是
|
* {@code pat_med_operation_item.opercheck_flag="2"} 而<b>不是</b> inhosp 的标志位;</li>
|
* <li>不使用 type 参数,也不设 {@code taskSituation}/{@code hospType="6"}。</li>
|
* </ol>
|
*
|
* @param operationItemId 手术明细ID(原 {@code patMedOperationItem.getId()})
|
* @param onMissingOutHospTime 出院时间为空时的副作用回调;可为 {@code null}
|
* @return 草稿;出院时间为空时返回 {@code null}
|
*/
|
public VisitSubtaskDraft boxedServiceSubtaskForOp(VisitTaskRow serviceTask, VisitInhospRow inhosp,
|
Long operationItemId, VisitArchiveRow archive,
|
String config,
|
java.util.function.Consumer<VisitInhospRow> onMissingOutHospTime) {
|
VisitSubtaskDraft d = new VisitSubtaskDraft();
|
|
// ---- 原 L1226:拷贝 ----
|
applyCarriedOverFieldsFromTask(d, serviceTask);
|
|
// ---- 原 L1227-1231 ----
|
d.setTaskid(serviceTask.getTaskid());
|
d.setOrgid(serviceTask.getOrgid());
|
d.setCampusid(serviceTask.getCampusid());
|
if (StringUtils.isNotEmpty(serviceTask.getLibtemplateid())) {
|
d.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid()));
|
}
|
// ---- 原 L1232-1240 ----
|
d.setNurseId(inhosp.getNurseId());
|
d.setNurseName(inhosp.getNurseName());
|
d.setInhospid(inhosp.getInhospid());
|
d.setDrcode(inhosp.getDrcode());
|
d.setDrname(inhosp.getDrname());
|
d.setDeptcode(inhosp.getLeaveldeptcode());
|
d.setDeptname(inhosp.getLeaveldeptname());
|
d.setLeavehospitaldistrictcode(inhosp.getLeavehospitaldistrictcode());
|
d.setLeavehospitaldistrictname(inhosp.getLeavehospitaldistrictname());
|
// ---- 原 L1242-1251 ----
|
d.setTemplateid(serviceTask.getTemplateid());
|
d.setTemplatename(serviceTask.getTemplatename());
|
d.setPatid(archive.getId());
|
d.setSendname(archive.getName());
|
d.setSfzh(archive.getIdcardno());
|
d.setPhone(archive.getTelcode());
|
if (StringUtils.isBlank(archive.getTelcode())) {
|
d.setPhone(archive.getRelativetelcode());
|
}
|
d.setSex(archive.getSex());
|
d.setAge(archive.getAge());
|
d.setSendstate(2L);
|
d.setManagementDoctor(inhosp.getDrname());
|
d.setManagementDoctorCode(inhosp.getDrcode());
|
// ---- 原 L1255-1266 ----
|
d.setStarttime(inhosp.getStarttime());
|
d.setEndtime(inhosp.getEndtime());
|
d.setServiceType(serviceTask.getServiceType());
|
d.setPreachform(serviceTask.getPreachform());
|
d.setHospType("2");
|
d.setCreateTime(new Date());
|
d.setUpdateTime(new Date());
|
d.setCreateBy(inhosp.getNurseName());
|
d.setOperationItemId(operationItemId);
|
d.setUpdateBy(serviceTask.getUpdateBy());
|
d.setUpdateTime(new Date());
|
// ---- 原 L1268-1282:发送时间 ----
|
if (serviceTask.getSendDay() == null) {
|
serviceTask.setSendDay(1L);
|
}
|
Date newDate;
|
if (inhosp.getEndtime() != null) {
|
newDate = addDays(inhosp.getEndtime(), serviceTask.getSendDay().intValue());
|
} else {
|
// 原 L1274-1281:★副作用落在 pat_med_operation_item 上,由调用方执行
|
if (onMissingOutHospTime != null) {
|
onMissingOutHospTime.accept(inhosp);
|
}
|
// 如果出院时间为空,则返回 null(注意:不是空草稿)
|
return null;
|
}
|
d.setLongSendTime(newDate);
|
d.setVisitTime(newDate);
|
// ---- 原 L1286-1289:通知类提前 N 天 ----
|
if (TASK_TYPE_NOTICE.equals(serviceTask.getType())) {
|
d.setVisitTime(minusDay(newDate, StringUtils.isNotEmpty(config) ? Integer.parseInt(config) : 0));
|
}
|
// ---- 原 L1290-1298:免随访标记 ----
|
if (StringUtils.isNotEmpty(archive.getNotrequiredFlag()) && "1".equals(archive.getNotrequiredFlag())) {
|
d.setRemark(archive.getNotrequiredreason());
|
d.setResult("error");
|
d.setTaskSituation(6);
|
d.setSendstate(4L);
|
}
|
return d;
|
}
|
|
/**
|
* 把 {@code DtoConversionUtils.sourceToTarget(serviceTask, ServiceSubtask.class)} 里
|
* <b>拷入但之后再未被显式覆盖</b>的 18 个字段显式搬过来。
|
* <p>
|
* ⚠️ 这 18 个字段是"隐式"进子任务的:原实现没有一行 set 它们,
|
* 全靠 DtoConversionUtils 的同名拷贝。搬迁后必须显式列出,否则值会丢。
|
* 清单由脚本从 {@code ServiceTask ∩ ServiceSubtask} 减去"被显式 set 过的"算出,
|
* 并由 {@code sql/_probe/verify_inhospvisit_semantics.py} 断言。
|
*/
|
private void applyCarriedOverFieldsFromTask(VisitSubtaskDraft d, VisitTaskRow t) {
|
d.setDelFlag(t.getDelFlag());
|
d.setDiagType(t.getDiagType());
|
d.setEndOutHospTime(t.getEndOutHospTime());
|
d.setGuid(t.getGuid());
|
d.setIsupload(t.getIsupload());
|
d.setLeavehospitaldistrictcodes(t.getLeavehospitaldistrictcodes());
|
d.setLeaveldeptcodes(t.getLeaveldeptcodes());
|
d.setLibtemplatename(t.getLibtemplatename());
|
d.setPageNum(t.getPageNum());
|
d.setPageSize(t.getPageSize());
|
d.setPid(t.getPid() == null ? null : t.getPid().longValue());
|
d.setSendType(t.getSendType() == null ? null : String.valueOf(t.getSendType()));
|
d.setStartOutHospTime(t.getStartOutHospTime());
|
d.setTaskDesc(t.getTaskDesc());
|
d.setTaskName(t.getTaskName());
|
d.setTextParam(t.getTextParam());
|
d.setType(t.getType());
|
d.setUploadTime(t.getUploadTime());
|
}
|
|
// ==========================================================================
|
// 工具方法(原样照抄,含已知缺陷)
|
// ==========================================================================
|
|
/**
|
* 日期加天数(原 {@code addDays},L663-671)。{@code days} 为 null 时按 1 处理。
|
*/
|
public 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();
|
}
|
|
/**
|
* 减去相应天数(原 {@code minusDay},L1309-1318)。
|
* <p>
|
* ⚠️ <b>已知缺陷,原样保留</b>:原实现写死 {@code instant.minus(2, ChronoUnit.DAYS)},
|
* <b>完全忽略 {@code day} 入参</b>。调用点传的是 {@code sys_config: visit.early.day},
|
* 但实际效果永远是"减 2 天"。
|
* <p>
|
* <b>不修</b>:修了会改变线上通知短信的发送时间,属可见行为变更。
|
*/
|
public Date minusDay(Date originalDate, Integer day) {
|
// 原样照抄:入参 day 被忽略,固定减 2 天
|
java.time.Instant instant = originalDate.toInstant();
|
java.time.Instant twoDaysAgoInstant = instant.minus(2, java.time.temporal.ChronoUnit.DAYS);
|
return Date.from(twoDaysAgoInstant);
|
}
|
|
/**
|
* 两个 {@code Date} 相差多少天(原 {@code DateUtils.differentDaysByMillisecond})。
|
* <p>
|
* 原实现 {@code boxedServiceSubtask} L1205 直接调 {@code DateUtils.differentDaysByMillisecond},
|
* 该工具在 ruoyi-common,新模块可以直接用 —— 这里只是把调用点收敛到一处,
|
* 便于探针断言"用的是同一个工具"。
|
*/
|
private int daysBetweenByMillisecond(Date start, Date end) {
|
return com.ruoyi.common.utils.DateUtils.differentDaysByMillisecond(start, end);
|
}
|
|
/**
|
* 组装器的副作用回调(原 {@code boxedServiceSubtask} 出院时间为空时会顺手写库)。
|
* <p>
|
* 抽成回调是为了让组装器<b>不直接依赖 Mapper</b>,保持"纯组装"可被逐行比对。
|
* 真正的写库由 {@code InhospVisitTaskService} 实现。
|
*/
|
public interface BoxedSideEffect {
|
|
/**
|
* 出院时间为空时的回写:原 L1174-1176
|
* <pre>
|
* patMedInhosp1.setDeptcheckFlag("2");
|
* patMedInhosp1.setRemark(patMedInhosp1.getRemark() + "& 该患者疾病出院时间为空,出院记录ID为:" + patMedInhosp1.getInhospid());
|
* patMedInhospMapper.updatePatMedInhosp(patMedInhosp1);
|
* </pre>
|
*/
|
void onMissingOutHospTime(VisitInhospRow inhosp);
|
}
|
}
|