package com.ruoyi.outpatientvisit.support;
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.outpatientvisit.mapper.PatMedOuthospMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import java.time.LocalDate;
import java.util.*;
/**
* 门诊分表维护 + 存储过程重建(从 {@code ruoyi-quartz} 的
* {@code com.ruoyi.quartz.util.CreateDataTable} 整体迁入)。
*
*
迁移对应关系
*
* | 本类方法 | 原实现 |
* | {@link #createOuthospQuarterTable()} |
* {@code CreateDataTable#createOuthospQuarterTable}(原 L35-58) |
* | {@link #createPartitionTableAndRefreshView(String)} |
* 原 L69-128(按 季度/年/月 建分表 + 刷视图) |
* | {@link #refreshOuthospView()} |
* 原 L163-420(重建存储过程 {@code sp_query_outhosp}) |
* | {@link #refreshOuthospCountProc(List, List, String)} |
* 原 L426-584(重建存储过程 {@code sp_count_outhosp}) |
*
* 业务逻辑一行未改,包括几处刻意保留的行为(见 "保留的原行为")。
*
* 迁移时唯一改动的两处(都是为了让本类能进"只依赖 ruoyi-common"的模块)
*
* - 配置服务换接口:原来注入 {@code com.ruoyi.system.service.ISysConfigService}
* (属 ruoyi-system,本模块不能依赖)。改为
* {@link IConfigService}(ruoyi-common)——同名方法
* {@code selectConfigByKey(String)},语义一致;
* 并用 {@code ObjectProvider} 惰性取,容器里没有实现时按"未配置"处理而不是启动失败。
* - 删除未使用字段:原类有一个 {@code @Autowired IPatMedOuthospService
* iPatMedOuthospService} 字段,全类从未被使用(已逐行确认)。
* 它属 smartor,是本类进模块的唯一硬阻碍;既然无用,直接不搬。
*
*
* 保留的原行为(勿"顺手优化")
*
* - {@code createOuthospQuarterTable} 把所有异常吞掉只打日志(原 L55-57),
* 不向上抛 —— 这是定时任务的容错方式,原样保留;
* - 存储过程"已存在时不覆盖、只把新分表 UNION ALL 追加进去"(原 L346-413),
* 以及"DROP 前先存原 DDL、CREATE 失败立刻恢复"的保护;
* - {@code sp_query_outhosp} 与 {@code sp_count_outhosp} 的参数完全一致,
* 只是后者不分页;两处的条件片段拼接逻辑刻意分别保留(不抽公共方法),
* 以免改动引入差异;
* - {@code refreshOuthospCountProc} 里存在未被使用的中间变量
* {@code concatArgs}(原 L486-496 拼了但没用到)—— 原样保留。
*
*
* 调用方
* 定时任务 {@code RyTask#createOuthospQuarterTable} 经本模块门面调用本类,
* 不再直接依赖 ruoyi-quartz 里的建表工具。
*/
@Slf4j
@Component
public class OuthospPartitionMaintainer {
/**
* 分表模式配置键(原样保留)
*/
private static final String SPLIT_MODE_KEY = "outhosp.split.mode";
/**
* 参数配置服务。
*
* ⚠️ 用 {@code ObjectProvider} 惰性取:本模块只依赖 ruoyi-common,
* 在独立部署/测试等"容器里没有实现"的场景下不能因此启动失败。
* 取不到时按"未配置"处理(与原来 {@code selectConfigByKey} 查不到返回 null 的效果一致)。
*/
@Autowired
private ObjectProvider configServiceProvider;
@Autowired
private PatMedOuthospMapper patMedOuthospMapper;
@Autowired
private DataSource dataSource;
/**
* 定时任务入口:按 {@code sys_config: outhosp.split.mode} 建门诊分表并刷新视图。
*
* 逐行对应原 {@code CreateDataTable#createOuthospQuarterTable}(L35-58)。
*/
public void createOuthospQuarterTable() {
log.info("【createOuthospQuarterTable】开始执行门诊分表任务");
try {
// 从 sys_config 查询分表模式, key = outhosp.split.mode
String splitMode = readConfig(SPLIT_MODE_KEY);
if (org.apache.commons.lang3.StringUtils.isBlank(splitMode)) {
log.info("【createOuthospQuarterTable】未配置 outhosp.split.mode,不执行分表");
return;
}
splitMode = splitMode.trim();
if (!"季度".equals(splitMode) && !"年".equals(splitMode) && !"月".equals(splitMode)) {
log.warn("【createOuthospQuarterTable】分表模式配置非法:{},应为 quarter/year/month,不执行分表", splitMode);
return;
}
String newTable = createPartitionTableAndRefreshView(splitMode);
if (newTable == null) {
log.info("【createOuthospQuarterTable】当前周期表已存在,无需操作");
} else {
log.info("【createOuthospQuarterTable】新建分表成功:{},已刷新视图 v_pat_med_outhosp", newTable);
}
} catch (Exception e) {
// ⚠️ 原行为:吞掉异常只打日志(定时任务容错方式),原样保留
log.error("【createOuthospQuarterTable】建表异常", e);
}
}
/**
* 读 {@code sys_config} 键值;没有实现或读取失败返回 {@code null}
* (与原来 {@code ISysConfigService.selectConfigByKey} 查不到返回 null 一致)。
*/
private String readConfig(String key) {
if (configServiceProvider == null) {
return null;
}
try {
IConfigService svc = configServiceProvider.stream().findFirst().orElse(null);
if (svc == null) {
log.warn("【门诊分表】容器里没有 IConfigService 实现,{} 按未配置处理。", key);
return null;
}
return svc.selectConfigByKey(key);
} catch (Exception e) {
log.warn("【门诊分表】读取 {} 失败,按未配置处理:{}", key, e.toString());
return null;
}
}
/**
* 按指定模式建门诊分表并更新动态视图
* splitMode: 季度 / 年 / 月 / 不分表
* 表名:
* quarter -> pat_med_outhosp_20260103
* year -> pat_med_outhosp_2026
* month -> pat_med_outhosp_202601
*/
public String createPartitionTableAndRefreshView(String splitMode) {
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
// 1. 根据模式计算新表名和模板表名
String newTableName;
String templateName;
if ("年".equals(splitMode)) {
// 按年:pat_med_outhosp_2026
newTableName = "pat_med_outhosp_" + year;
// 模板表:优先上一年分表
String prevYearTable = "pat_med_outhosp_" + (year - 1);
templateName = patMedOuthospMapper.tableExists(prevYearTable) > 0 ? prevYearTable : "pat_med_outhosp";
} else if ("月".equals(splitMode)) {
// 按月:pat_med_outhosp_202601
newTableName = String.format("pat_med_outhosp_%d%02d", year, month);
// 模板表:优先上一月分表
LocalDate prevMonth = today.minusMonths(1);
String prevMonthTable = String.format("pat_med_outhosp_%d%02d", prevMonth.getYear(), prevMonth.getMonthValue());
templateName = patMedOuthospMapper.tableExists(prevMonthTable) > 0 ? prevMonthTable : "pat_med_outhosp";
} else {
// 默认按季度:pat_med_outhosp_20260103
String quarterSuffix;
if (month <= 3) {
quarterSuffix = "0103";
} else if (month <= 6) {
quarterSuffix = "0406";
} else if (month <= 9) {
quarterSuffix = "0709";
} else {
quarterSuffix = "1012";
}
newTableName = "pat_med_outhosp_" + year + quarterSuffix;
templateName = resolveQuarterTemplateName(year, quarterSuffix);
}
// 2. 检查表是否已存在
if (patMedOuthospMapper.tableExists(newTableName) > 0) {
log.info("[建表] 表 {} 已存在,无需重复建表,但仍然尝试刷新存储过程(确保分表已纳入查询)", newTableName);
refreshOuthospView();
return null;
}
log.info("[建表] 模式={},使用模板表 {} 创建 {}", splitMode, templateName, newTableName);
// 3. 创建新表(LIKE 模板表,只复制结构+索引,不含数据)
patMedOuthospMapper.createPatMedOuthosp(templateName, newTableName);
// 4. 设置自增主键
patMedOuthospMapper.createPatMedOuthospAutoAdd(newTableName);
// 5. 重建动态视图
refreshOuthospView();
log.info("[建表] 建表并刷新视图完成,新表:{}", newTableName);
return newTableName;
}
/**
* 季度分表模板确定:优先上一季度分表,没有则用主表
*/
private String resolveQuarterTemplateName(int year, String currentQuarterSuffix) {
String[][] quarters = {{"0103", "1012"}, {"0406", "0103"}, {"0709", "0406"}, {"1012", "0709"}};
String prevSuffix = null;
int prevYear = year;
for (String[] q : quarters) {
if (q[0].equals(currentQuarterSuffix)) {
prevSuffix = q[1];
if (currentQuarterSuffix.equals("0103")) {
prevYear = year - 1;
}
break;
}
}
if (prevSuffix != null) {
String prevTableName = "pat_med_outhosp_" + prevYear + prevSuffix;
if (patMedOuthospMapper.tableExists(prevTableName) > 0) {
return prevTableName;
}
}
return "pat_med_outhosp";
}
/**
* 刷新存储过程 sp_query_outhosp
* 逻辑:
* 1. 从 information_schema 动态获取 pat_med_outhosp 字段列表
* 2. 查出所有分表(后缀为4-8位纯数字)拼 UNION ALL
* 3. 根据字段是否存在动态决定存储过程参数和 WHERE 条件
* 4. 用原生 JDBC 执行(CREATE PROCEDURE 内含分号,MyBatis 无法处理)
*/
private void refreshOuthospView() {
// 1. 获取所有分表
List allTables = patMedOuthospMapper.getAllOuthospTableNames();
List partTables = allTables.stream().filter(t -> t.matches("pat_med_outhosp_[0-9]{4,8}")).sorted().collect(java.util.stream.Collectors.toList());
// 2. 动态获取主表字段列表
List columns = patMedOuthospMapper.getTableColumns("pat_med_outhosp");
Set cs = new HashSet<>(columns);
String colList = String.join(",", columns);
// 3. 拼 UNION ALL(del_flag 内嵌入 SET @sql='...' 中,用 ''0'' 转义)
StringBuilder union = new StringBuilder();
for (String tbl : partTables) {
union.append("SELECT ").append(colList).append(" FROM ").append(tbl).append(" WHERE del_flag=''0'' UNION ALL ");
}
union.append("SELECT ").append(colList).append(" FROM pat_med_outhosp WHERE del_flag=''0''");
// 4. 从 DB 元数据动态构建参数列表
// 排除无业务意义的管理字段
Set excludeCols = new HashSet<>(java.util.Arrays.asList("id", "del_flag", "create_by", "create_time", "update_by", "update_time", "remark"));
// 强制精确匹配的字段(字段名不符合 eqPattern 规则但业务上需精确查询)
Set forceEqCols = new HashSet<>(java.util.Arrays.asList("orgid", "guid", "campusid"));
// 精确匹配字段的关键词(含 no/code/flag/status/state/num/type/level/grade/sex/gender/phone/tel/id_)
java.util.regex.Pattern eqPattern = java.util.regex.Pattern.compile("(no|code|flag|status|state|num|type|level|grade|sex|gender|phone|tel|id_.*|.*_id)$", java.util.regex.Pattern.CASE_INSENSITIVE);
List