package com.ruoyi.outpatientvisit.service;
|
|
import com.ruoyi.common.core.service.ArchiveReadApi;
|
import com.ruoyi.common.core.service.SubtaskStatApi;
|
import com.ruoyi.common.core.service.VisitTaskCreateApi;
|
import com.ruoyi.common.core.service.dto.PatMedReq;
|
import com.ruoyi.common.core.service.dto.PatMedRes;
|
import com.ruoyi.common.core.service.dto.VisitArchiveRow;
|
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.common.utils.DtoConversionUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
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.lang3.ObjectUtils;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.CollectionUtils;
|
|
import java.time.LocalDate;
|
import java.time.Period;
|
import java.time.ZoneId;
|
import java.time.temporal.ChronoUnit;
|
import java.util.*;
|
|
/**
|
* 门诊记录基础数据服务:查询(含展示富化)/ 统计 / 增删改 / 分表路由。
|
*
|
* <h3>迁移对应(@ b25c3b51 · {@code PatMedOuthospServiceImpl})</h3>
|
* <table border="1">
|
* <tr><th>本类方法</th><th>原方法</th></tr>
|
* <tr><td>{@link #selectList(PatMedOuthosp)}</td><td>{@code selectPatMedOuthospList} L83-133</td></tr>
|
* <tr><td>{@link #calculateAge}</td><td>{@code calculateAge} L135-185</td></tr>
|
* <tr><td>{@link #insert(PatMedOuthosp)}</td><td>{@code insertPatMedOuthosp} L187-204</td></tr>
|
* <tr><td>{@link #selectExistingSerialnums}</td><td>同名方法 L210-259</td></tr>
|
* <tr><td>{@link #update(PatMedOuthosp)}</td><td>{@code updatePatMedOuthosp} L268-291</td></tr>
|
* <tr><td>{@link #resolveTargetTable}</td><td>同名私有方法 L305-341</td></tr>
|
* <tr><td>{@link #selectCount(PatMedReq)}</td><td>{@code selectPatMedOuthospCount} L335-348</td></tr>
|
* <tr><td>{@link #getDeptCodeByPatId}</td><td>同名方法 L351-357</td></tr>
|
* <tr><td>{@link #selectById}</td><td>{@code selectPatMedOuthospById} L68-71(<b>空壳</b>)</td></tr>
|
* <tr><td>{@link #deleteByIds} / {@link #deleteById}</td>
|
* <td>L319-332(<b>均为空壳</b>)</td></tr>
|
* </table>
|
*
|
* <h3>⚠️ 保留的原实现行为(勿"顺手优化")</h3>
|
* <ol>
|
* <li>{@link #selectById} / {@link #deleteByIds} / {@link #deleteById} 是
|
* <b>空壳</b>(真实现被注释掉,直接返回 {@code null} / {@code 0})—— 原样保留;</li>
|
* <li>{@link #getDeptCodeByPatId} 在 {@code CollectionUtils.isEmpty} 通过后
|
* <b>仍直接 {@code get(0)}</b>:列表为空时会 {@code IndexOutOfBoundsException}。原样保留;</li>
|
* <li>{@link #insert} 在 {@code DuplicateKeyException} 时返回 0 并打 info(不当失败处理);</li>
|
* <li>{@link #update} 查不到记录时<b>转调 {@link #insert}</b>,但<b>丢弃其返回值</b>,最终仍 {@code return 0};</li>
|
* <li>{@link #selectList} 只在 {@code pageNum != null} 时才做富化(原实现如此)。</li>
|
* </ol>
|
*
|
* <h3>跨模块能力</h3>
|
* 富化与统计用到别人的数据,一律走 ruoyi-common 契约(都在 smartor 侧有过渡实现):
|
* {@link ArchiveReadApi}(档案)/ {@link VisitTaskCreateApi#existsSubtaskWithStates}(子任务存在性)
|
* / {@link SubtaskStatApi}(随访人次统计)。
|
*/
|
@Slf4j
|
@Service
|
public class OuthospDataService {
|
|
@Autowired
|
private PatMedOuthospMapper mapper;
|
|
@Autowired
|
private ObjectProvider<ArchiveReadApi> archiveApiProvider;
|
|
@Autowired
|
private ObjectProvider<VisitTaskCreateApi> taskApiProvider;
|
|
@Autowired
|
private ObjectProvider<SubtaskStatApi> statApiProvider;
|
|
// ==========================================================================
|
// 查询
|
// ==========================================================================
|
|
/**
|
* 按条件走存储过程查门诊记录(<b>纯转发,不做任何富化</b>)。
|
* <p>
|
* 与 {@link #selectList} 的区别:{@code selectList} 会在 {@code pageNum != null} 时
|
* 做年龄/性别/电话富化 + 子任务 {@code serverState} 查询;
|
* 本方法是"原样把存储过程结果给你",供<b>不需要富化</b>的内部调用方使用
|
* (如 smartor 的随访答复明细,它只需要原始记录)。
|
* <p>
|
* ⚠️ 入参请按<b>最小必要字段</b>构造,不要用 {@code DtoConversionUtils} 全量带字段 ——
|
* 多带的字段可能改变存储过程的 WHERE 分支。
|
*/
|
public List<PatMedOuthosp> callSpQueryOuthosp(PatMedOuthospQueryReq req) {
|
return mapper.callSpQueryOuthosp(req);
|
}
|
|
/**
|
* 按主键查。
|
* <p>
|
* ⚠️ <b>原实现是空壳</b>(L68-71):`// return patMedOuthospMapper.selectPatMedOuthospById(id); return null;`
|
* —— 本方法<b>原样保留空壳语义</b>,不要"顺手实现"它(那会改变线上行为)。
|
* 需要按主键取记录请用 {@link #selectList} 或存储过程。
|
*/
|
public PatMedOuthosp selectById(Long id) {
|
return null;
|
}
|
|
/**
|
* 按条件走存储过程查门诊记录<b>条数</b>(<b>纯转发</b>,原 {@code callSpQueryOuthospCount})。
|
* <p>
|
* 供不需要富化、只需要计数的跨模块调用方使用(如 smartor 的随访统计)。
|
*/
|
public Long callSpQueryOuthospCount(PatMedOuthospQueryReq req) {
|
return mapper.callSpQueryOuthospCount(req);
|
}
|
|
/**
|
* 门诊记录列表(含展示富化)。
|
* <p>
|
* 逐行对应原 {@code selectPatMedOuthospList}(L83-133):
|
* <ol>
|
* <li>{@code DtoConversionUtils.sourceToTarget} 转查询入参,
|
* 并用 {@code beginTime/endTime} 覆盖 {@code beginAdmitdate/endAdmitdate};
|
* {@code deptcodes} 非空时拼成逗号串塞进 {@code deptcode};</li>
|
* <li>调存储过程 {@code callSpQueryOuthosp};</li>
|
* <li><b>仅当 {@code pageNum != null}</b> 时逐条富化:
|
* 档案现算年龄(年/月/日三段拼成 {@code age} 字符串)+ 补 sex/telcode/idcardno,
|
* 并按 {@code patid + sendstates[1,2,3] + service_type="3"} 置 {@code serverState="1"}。</li>
|
* </ol>
|
*
|
* <h3>⚠️ 富化里有一处原实现的 NPE 风险(原样保留)</h3>
|
* {@code patArchive.getAge() + getAgeUnit() + getAge2() + getAgeUnit2()} 在
|
* {@code calculateAge} 返回 null(生日在未来)时会 NPE;原实现没有防护。
|
*/
|
public List<PatMedOuthosp> selectList(PatMedOuthosp query) {
|
PatMedOuthospQueryReq req = DtoConversionUtils.sourceToTarget(query, PatMedOuthospQueryReq.class);
|
req.setBeginAdmitdate(query.getBeginTime());
|
req.setEndAdmitdate(query.getEndTime());
|
if (!CollectionUtils.isEmpty(query.getDeptcodes())) {
|
req.setDeptcode(String.join(",", query.getDeptcodes()));
|
}
|
|
List<PatMedOuthosp> rows = mapper.callSpQueryOuthosp(req);
|
if (query.getPageNum() != null && rows != null) {
|
ArchiveReadApi archiveApi = archiveApi();
|
VisitTaskCreateApi taskApi = taskApi();
|
for (PatMedOuthosp row : rows) {
|
// ---- 档案富化 ----
|
VisitArchiveRow archive = archiveApi == null ? null : archiveApi.findByPatid(row.getPatid());
|
if (ObjectUtils.isNotEmpty(archive) && archive.getBirthdate() != null) {
|
Map<String, String> map = calculateAge(
|
archive.getBirthdate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(),
|
LocalDate.now());
|
archive.setAge(ObjectUtils.isNotEmpty(map.get("age")) ? Long.valueOf(map.get("age")) : null);
|
archive.setAgeUnit(map.get("ageUnit"));
|
archive.setAge2(ObjectUtils.isNotEmpty(map.get("age2")) ? Long.valueOf(map.get("age2")) : null);
|
archive.setAgeUnit2(map.get("ageUnit2"));
|
|
row.setAge(archive.getAge() + archive.getAgeUnit()
|
+ archive.getAge2() + archive.getAgeUnit2());
|
row.setSex(archive.getSex());
|
row.setTelcode(archive.getTelcode());
|
row.setIdcardno(archive.getIdcardno());
|
}
|
// ---- 子任务富化(口径见契约注释:与 findPendingSubtasks 不同)----
|
if (taskApi != null) {
|
List<Long> sendstates = new ArrayList<>();
|
sendstates.add(1L);
|
sendstates.add(2L);
|
sendstates.add(3L);
|
if (taskApi.existsSubtaskWithStates(row.getPatid(), sendstates, "3")) {
|
row.setServerState("1");
|
}
|
}
|
}
|
}
|
return rows;
|
}
|
|
/**
|
* 年龄段计算(原 {@code calculateAge} L135-185,逐行照抄)。
|
* <ul>
|
* <li>{@code < 30 天} → 天</li>
|
* <li>{@code < 12 月} → 月 + 天</li>
|
* <li>否则 → 岁 + 月</li>
|
* </ul>
|
*
|
* @return 含 {@code age/ageUnit/age2/ageUnit2} 的 map;
|
* {@code birthdate} 为 null 或晚于今天时返回 {@code null}
|
*/
|
public Map<String, String> calculateAge(LocalDate birthdate, LocalDate today) {
|
if (birthdate == null || today.isBefore(birthdate)) {
|
return null;
|
}
|
Map<String, String> ageMap = new HashMap<>();
|
|
Period period = Period.between(birthdate, today);
|
long totalDays = ChronoUnit.DAYS.between(birthdate, today);
|
long totalMonths = ChronoUnit.MONTHS.between(birthdate, today);
|
|
int years = period.getYears();
|
int months = period.getMonths();
|
int days = period.getDays();
|
|
String ageUnit;
|
Integer age;
|
String ageUnit2 = null;
|
Integer age2 = null;
|
|
if (totalDays < 30) {
|
ageUnit = "天";
|
age = (int) totalDays;
|
ageMap.put("age", age != null ? age.toString() : null);
|
ageMap.put("ageUnit", ageUnit);
|
ageMap.put("age2", null);
|
ageMap.put("ageUnit2", null);
|
} else if (totalMonths < 12) {
|
ageUnit = "月";
|
age = (int) totalMonths;
|
ageUnit2 = "天";
|
age2 = days;
|
ageMap.put("age", age != null ? age.toString() : null);
|
ageMap.put("ageUnit", ageUnit);
|
ageMap.put("age2", age2 != null ? age2.toString() : null);
|
ageMap.put("ageUnit2", ageUnit2);
|
} else {
|
ageUnit = "岁";
|
age = years;
|
ageUnit2 = "月";
|
age2 = months;
|
ageMap.put("age", age != null ? age.toString() : null);
|
ageMap.put("ageUnit", ageUnit);
|
ageMap.put("age2", age2 != null ? age2.toString() : null);
|
ageMap.put("ageUnit2", ageUnit2);
|
}
|
return ageMap;
|
}
|
|
/**
|
* 出院/门诊人数统计。
|
* <p>
|
* 逐行对应原 {@code selectPatMedOuthospCount}(L335-348):
|
* <pre>
|
* 本域 callSpQueryOuthospCount(req) -> count
|
* 随访域 countVisitForStat(patMedReq) -> PatMedRes
|
* patMedRes.setRc(count)
|
* </pre>
|
* ⚠️ 原实现不判 {@code patMedRes} 是否 null 就直接 setRc,此处保持原样。
|
*/
|
public PatMedRes selectCount(PatMedReq patMedReq) {
|
PatMedOuthospQueryReq req = new PatMedOuthospQueryReq();
|
String deptcodes = CollectionUtils.isEmpty(patMedReq.getDeptcodeList())
|
? null : String.join(",", patMedReq.getDeptcodeList());
|
req.setBeginAdmitdate(patMedReq.getStartDate());
|
req.setEndAdmitdate(DateUtils.addDays(patMedReq.getEndDate(), 1));
|
req.setDeptcode(deptcodes);
|
req.setOrgid(patMedReq.getOrgid());
|
// 原实现把下面这行注释掉了,原样保留(不设 campusid)
|
// req.setCampusid(patMedReq.getCampusid());
|
Long count = mapper.callSpQueryOuthospCount(req);
|
|
SubtaskStatApi statApi = statApi();
|
if (statApi == null) {
|
log.warn("【门急诊随访】SubtaskStatApi 不可用,selectCount 返回 null。");
|
return null;
|
}
|
PatMedRes patMedRes = statApi.countVisitForStat(patMedReq);
|
patMedRes.setRc(count == null ? 0 : count.intValue());
|
return patMedRes;
|
}
|
|
/**
|
* 按 patid 取最新一条门诊记录(按就诊日期倒序)。
|
* <p>
|
* ⚠️ <b>原实现的空列表越界缺陷原样保留</b>(L351-357):
|
* <pre>
|
* if (!CollectionUtils.isEmpty(list)) { Collections.sort(...); }
|
* return list.get(0); // ← 列表为空时在这里越界
|
* </pre>
|
*/
|
public PatMedOuthosp getDeptCodeByPatId(PatMedOuthosp patMedOuthosp) {
|
List<PatMedOuthosp> list = selectList(patMedOuthosp);
|
if (!CollectionUtils.isEmpty(list)) {
|
Collections.sort(list, Comparator.comparing(PatMedOuthosp::getAdmitdate).reversed());
|
}
|
return list.get(0);
|
}
|
|
// ==========================================================================
|
// 写入
|
// ==========================================================================
|
|
/**
|
* 新增门诊记录(按 {@code admitdate} 路由到对应分表)。
|
* <p>逐行对应原 {@code insertPatMedOuthosp}(L187-204)。
|
*/
|
public int insert(PatMedOuthosp row) {
|
if (row.getAdmitdate() == null) {
|
log.error("门急诊就诊时间不能为空");
|
return 0;
|
}
|
String targetTable = resolveTargetTable(row.getAdmitdate());
|
if (targetTable == null) {
|
return 0;
|
}
|
// 插入前按 serialnum 查重,防止重复采集导致重复入库
|
if (StringUtils.isNotBlank(row.getSerialnum())) {
|
int exists = mapper.countBySerialnum(targetTable, row.getSerialnum());
|
if (exists > 0) {
|
log.info("[insert] serialnum={} 已存在于表 {},跳过插入", row.getSerialnum(), targetTable);
|
return 0;
|
}
|
}
|
row.setCreateTime(DateUtils.getNowDate());
|
row.setUpdateTime(DateUtils.getNowDate());
|
row.setGuid(UUID.randomUUID().toString());
|
log.info("[insert] 路由到分表: {}", targetTable);
|
try {
|
return mapper.insertIntoTable(targetTable, row);
|
} catch (org.springframework.dao.DuplicateKeyException e) {
|
// 加了 serialnum 唯一索引后,极端并发或重复采集会被数据库直接拦下;
|
// 这属于"记录已存在",按正常跳过处理(返回 0),不要当成入库失败写进错误表
|
log.info("[insert] serialnum={} 已存在于表 {}(唯一索引拦截),跳过插入", row.getSerialnum(), targetTable);
|
return 0;
|
}
|
}
|
|
/**
|
* 批量查已入库的 serialnum(一次 IN 查询,避免逐条 count)。
|
* <p>逐行对应原 {@code selectExistingSerialnums}(L210-259)。
|
*/
|
public Set<String> selectExistingSerialnums(String admitDateStr, Collection<String> serialnums) {
|
Set<String> existing = new HashSet<>();
|
if (CollectionUtils.isEmpty(serialnums)) {
|
return existing;
|
}
|
Date admitDate = null;
|
if (StringUtils.isNotBlank(admitDateStr)) {
|
try {
|
admitDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(admitDateStr);
|
} catch (java.text.ParseException e) {
|
log.warn("[selectExistingSerialnums] 就诊日期解析失败:{},按主表查询", admitDateStr);
|
}
|
}
|
String targetTable = admitDate == null ? null : resolveTargetTable(admitDate);
|
if (targetTable == null) {
|
targetTable = "pat_med_outhosp";
|
}
|
List<String> existed = mapper.selectExistingSerialnums(targetTable, serialnums);
|
if (!CollectionUtils.isEmpty(existed)) {
|
existing.addAll(existed);
|
}
|
log.info("[selectExistingSerialnums] 日期={},分表={},待查{}条,已存在{}条",
|
admitDateStr, targetTable, serialnums.size(), existing.size());
|
return existing;
|
}
|
|
/**
|
* 修改门诊记录(按 {@code admitdate} 路由到分表;查不到记录则转新增)。
|
* <p>逐行对应原 {@code updatePatMedOuthosp}(L268-291)。
|
* ⚠️ 查不到时转调 {@link #insert} 但<b>丢弃其返回值</b>,最终仍 {@code return 0} —— 原样保留。
|
*/
|
public int update(PatMedOuthosp row) {
|
if (row.getSerialnum() == null) {
|
log.error("流水号不能为空");
|
return 0;
|
}
|
PatMedOuthospQueryReq req = new PatMedOuthospQueryReq();
|
req.setSerialnum(row.getSerialnum());
|
req.setGuid(row.getGuid());
|
req.setOuthospno(row.getOuthospno());
|
List<PatMedOuthosp> found = mapper.callSpQueryOuthosp(req);
|
|
if (found == null || found.isEmpty()) {
|
insert(row);
|
} else {
|
row.setUpdateTime(DateUtils.getNowDate());
|
String targetTable = resolveTargetTable(row.getAdmitdate());
|
if (targetTable != null) {
|
log.info("[update] 路由到分表: {}", targetTable);
|
return mapper.updateInTable(targetTable, row);
|
}
|
}
|
return 0;
|
}
|
|
/**
|
* 批量删除。
|
* <p>
|
* ⚠️ <b>原实现是空壳</b>(L319-322):`// return patMedOuthospMapper.deletePatMedOuthospByIds(ids); return 0;`
|
* —— 原样保留。
|
*/
|
public int deleteByIds(Long[] ids) {
|
return 0;
|
}
|
|
/**
|
* 按主键删除。
|
* <p>
|
* ⚠️ <b>原实现是空壳</b>(L328-332):`// return patMedOuthospMapper.deletePatMedOuthospById(id); return 0;`
|
* —— 原样保留。
|
*/
|
public int deleteById(Long id) {
|
return 0;
|
}
|
|
// ==========================================================================
|
// 分表路由
|
// ==========================================================================
|
|
/**
|
* 根据就诊时间推算目标分表名,与建表策略保持一致。
|
* <p>逐行对应原私有方法 {@code resolveTargetTable}(L305-341):
|
* <ul>
|
* <li>6 位后缀(YYYYMM)→ 按月分表</li>
|
* <li>8 位后缀(YYYYMMdd)→ 按季度分表,后缀固定 0103/0406/0709/1012</li>
|
* <li>4 位后缀(YYYY)→ 按年分表</li>
|
* </ul>
|
* 推算出表名后校验是否存在,<b>不存在则回落主表</b>(返回 {@code "pat_med_outhosp"},
|
* 不是 null)—— 注意与"参数为 null 时返回 null"的区别。
|
*/
|
private String resolveTargetTable(Date createTime) {
|
if (createTime == null) {
|
return null;
|
}
|
List<String> allTables = mapper.getAllOuthospTableNames();
|
if (allTables == null || allTables.isEmpty()) {
|
return null;
|
}
|
|
String sample = allTables.get(0);
|
String suffix = sample.replaceFirst("pat_med_outhosp_", "");
|
int suffixLen = suffix.length();
|
|
Calendar cal = Calendar.getInstance();
|
cal.setTime(createTime);
|
int year = cal.get(Calendar.YEAR);
|
int month = cal.get(Calendar.MONTH) + 1;
|
|
String targetTable;
|
if (suffixLen == 4) {
|
targetTable = "pat_med_outhosp_" + year;
|
} else if (suffixLen == 6) {
|
targetTable = String.format("pat_med_outhosp_%d%02d", year, month);
|
} else {
|
String quarterSuffix;
|
if (month <= 3) {
|
quarterSuffix = "0103";
|
} else if (month <= 6) {
|
quarterSuffix = "0406";
|
} else if (month <= 9) {
|
quarterSuffix = "0709";
|
} else {
|
quarterSuffix = "1012";
|
}
|
targetTable = "pat_med_outhosp_" + year + quarterSuffix;
|
}
|
|
boolean exists = allTables.contains(targetTable);
|
log.info("[路由] createTime={} → 目标表={} exists={}", createTime, targetTable, exists);
|
return exists ? targetTable : "pat_med_outhosp";
|
}
|
|
// ==========================================================================
|
// 跨模块契约取用
|
// ==========================================================================
|
|
private ArchiveReadApi archiveApi() {
|
return providerGet(archiveApiProvider, "ArchiveReadApi");
|
}
|
|
private VisitTaskCreateApi taskApi() {
|
return providerGet(taskApiProvider, "VisitTaskCreateApi");
|
}
|
|
private SubtaskStatApi statApi() {
|
return providerGet(statApiProvider, "SubtaskStatApi");
|
}
|
|
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;
|
}
|
}
|
}
|