docs: 添加 Board4 数据库排查与重建文档

现在 zs_aucma-mes-back\aucma-base\src\main\resources 目录下,已经有本次全部自动化相关脚本:

rt_daily_prod_state.sql
device_daily_production.sql
device_prod_calc_pkg.sql
base_device_param_val_202603_trigger.sql
device_prod_calc_scheduler_job.sql
device_param_partition_auto_pkg.sql
device_param_partition_auto_scheduler_job.sql
我也已经把“脚本执行顺序 + 每个脚本作用”写进文档了,文档位置还是:

board4_database_troubleshooting_and_rebuild_guide.md

新增内容包括:

按顺序执行的总表
每个脚本是否必须执行
每个脚本具体作用
首次重置恢复时的推荐执行顺序
为什么当前月触发器首次仍需手工补一次
后续月份为什么可以自动化
master
zangch@mesnac.com 4 weeks ago
parent bc50d4a6d1
commit 41f4b8f6fb

@ -0,0 +1,118 @@
package com.aucma.base.support;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
/**
*
*/
@Component
public class DeviceParamTableRouter {
private static final String BASE_TABLE_NAME = "BASE_DEVICE_PARAM_VAL";
private static final String PARTITION_TABLE_PREFIX = "BASE_DEVICE_PARAM_VAL_";
private static final DateTimeFormatter YM_FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");
private static final Pattern SUFFIX_PATTERN = Pattern.compile("^\\d{6}$");
private final JdbcTemplate jdbcTemplate;
private final Set<String> confirmedTables = ConcurrentHashMap.newKeySet();
@Autowired
public DeviceParamTableRouter(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public boolean isOldDevice(String deviceCode) {
return deviceCode != null && deviceCode.startsWith("OLD-");
}
public String resolveWriteTable(String deviceCode, Date collectTime) {
if (isOldDevice(deviceCode)) {
return BASE_TABLE_NAME;
}
String suffix = toSuffix(collectTime == null ? new Date() : collectTime);
String tableName = buildPartitionTableName(suffix);
ensureTableExists(tableName);
return tableName;
}
public List<String> resolveReadTableSuffixes(Date startTime, Date endTime) {
if (startTime == null || endTime == null) {
return Collections.emptyList();
}
LocalDate startMonth = toLocalDate(startTime).withDayOfMonth(1);
LocalDate endMonth = toLocalDate(endTime).withDayOfMonth(1);
if (startMonth.isAfter(endMonth)) {
LocalDate temp = startMonth;
startMonth = endMonth;
endMonth = temp;
}
List<String> suffixes = new ArrayList<>();
LocalDate cursor = startMonth;
while (!cursor.isAfter(endMonth)) {
String suffix = YM_FORMATTER.format(cursor);
if (tableExists(buildPartitionTableName(suffix))) {
suffixes.add(suffix);
}
cursor = cursor.plusMonths(1);
}
return suffixes;
}
private LocalDate toLocalDate(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
private String toSuffix(Date collectTime) {
// 为什么这样做:物理分表已按自然月完整年月命名,使用 yyyyMM 才能避免跨年份时表名歧义。
return YM_FORMATTER.format(toLocalDate(collectTime));
}
private String buildPartitionTableName(String suffix) {
validateSuffix(suffix);
return PARTITION_TABLE_PREFIX + suffix;
}
private void validateSuffix(String suffix) {
if (suffix == null || !SUFFIX_PATTERN.matcher(suffix).matches()) {
throw new IllegalArgumentException("非法分表后缀: " + suffix);
}
}
private void ensureTableExists(String tableName) {
if (!tableExists(tableName)) {
throw new IllegalArgumentException("采集月分表不存在: " + tableName);
}
}
private boolean tableExists(String tableName) {
String normalizedName = tableName.toUpperCase(Locale.ROOT);
if (confirmedTables.contains(normalizedName)) {
return true;
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM USER_TABLES WHERE TABLE_NAME = ?",
Integer.class,
normalizedName
);
boolean exists = count != null && count > 0;
if (exists) {
confirmedTables.add(normalizedName);
}
return exists;
}
}

@ -0,0 +1,77 @@
-- 2026年03月自动采集分表触发器
-- 说明:
-- 1. 本脚本是 base_device_param_val_partition_trigger_template.sql 的 202603 实例化版本。
-- 2. 挂载表BASE_DEVICE_PARAM_VAL_202603
-- 3. 仅处理自动采集设备OLD 设备 RT 累计由 Java Service 同步完成。
CREATE OR REPLACE TRIGGER TRG_BDPV_202603_RT
AFTER INSERT ON BASE_DEVICE_PARAM_VAL_202603
FOR EACH ROW
DECLARE
V_PROD_DATE DATE;
V_NEW_VAL NUMBER(18,4);
V_LAST_VAL NUMBER(18,4);
V_CUR_TOTAL NUMBER(18,4);
V_RESET_COUNT NUMBER(10);
V_DELTA NUMBER(18,4);
BEGIN
IF :NEW.PARAM_NAME <> '机台状态-实际产出数量' THEN
RETURN;
END IF;
IF :NEW.DEVICE_CODE LIKE 'OLD-%' THEN
RETURN;
END IF;
BEGIN
V_NEW_VAL := TO_NUMBER(:NEW.PARAM_VALUE);
EXCEPTION
WHEN OTHERS THEN
RETURN;
END;
V_PROD_DATE := TRUNC(:NEW.COLLECT_TIME);
BEGIN
SELECT LAST_PARAM_VAL,
CURRENT_TOTAL,
RESET_COUNT
INTO V_LAST_VAL,
V_CUR_TOTAL,
V_RESET_COUNT
FROM RT_DAILY_PROD_STATE
WHERE PROD_DATE = V_PROD_DATE
AND DEVICE_CODE = :NEW.DEVICE_CODE
AND PARAM_NAME = :NEW.PARAM_NAME
FOR UPDATE;
IF V_NEW_VAL > V_LAST_VAL THEN
V_DELTA := V_NEW_VAL - V_LAST_VAL;
ELSIF V_NEW_VAL < V_LAST_VAL THEN
V_DELTA := V_NEW_VAL;
V_RESET_COUNT := NVL(V_RESET_COUNT, 0) + 1;
ELSE
V_DELTA := 0;
END IF;
UPDATE RT_DAILY_PROD_STATE
SET LAST_PARAM_VAL = V_NEW_VAL,
CURRENT_TOTAL = NVL(V_CUR_TOTAL, 0) + NVL(V_DELTA, 0),
RESET_COUNT = NVL(V_RESET_COUNT, 0),
LAST_COLLECT_TIME = :NEW.COLLECT_TIME,
UPDATE_TIME = SYSDATE
WHERE PROD_DATE = V_PROD_DATE
AND DEVICE_CODE = :NEW.DEVICE_CODE
AND PARAM_NAME = :NEW.PARAM_NAME;
EXCEPTION
WHEN NO_DATA_FOUND THEN
INSERT INTO RT_DAILY_PROD_STATE (
PROD_DATE, DEVICE_CODE, PARAM_NAME, LAST_PARAM_VAL,
CURRENT_TOTAL, RESET_COUNT, DIRTY_FLAG, LAST_COLLECT_TIME, UPDATE_TIME
) VALUES (
V_PROD_DATE, :NEW.DEVICE_CODE, :NEW.PARAM_NAME, V_NEW_VAL,
0, 0, 0, :NEW.COLLECT_TIME, SYSDATE
);
END;
END;
/

@ -0,0 +1,190 @@
-- 自动创建设备参数月分表与对应触发器
-- 说明:
-- 1. 仅需部署一次本包,后续可由 DBMS_SCHEDULER 自动调用。
-- 2. 默认使用“当前月分表”克隆出“下月分表”,并自动创建下月 RT 触发器。
-- 3. 本包是幂等的:目标分表或目标触发器已存在时会自动跳过。
-- 4. 首次上线前,仍需人工确保“当前月分表”已经存在,例如 BASE_DEVICE_PARAM_VAL_202603。
CREATE OR REPLACE PACKAGE PKG_DEVICE_PARAM_PARTITION AS
PROCEDURE ENSURE_MONTH_TABLE_AND_TRIGGER(
P_TARGET_SUFFIX IN VARCHAR2 DEFAULT TO_CHAR(ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1), 'YYYYMM'),
P_SOURCE_SUFFIX IN VARCHAR2 DEFAULT TO_CHAR(TRUNC(SYSDATE, 'MM'), 'YYYYMM')
);
END PKG_DEVICE_PARAM_PARTITION;
/
CREATE OR REPLACE PACKAGE BODY PKG_DEVICE_PARAM_PARTITION AS
C_TABLE_PREFIX CONSTANT VARCHAR2(64) := 'BASE_DEVICE_PARAM_VAL_';
C_TRIGGER_PREFIX CONSTANT VARCHAR2(64) := 'TRG_BDPV_';
PROCEDURE VALIDATE_SUFFIX(P_SUFFIX IN VARCHAR2) IS
BEGIN
IF P_SUFFIX IS NULL OR NOT REGEXP_LIKE(P_SUFFIX, '^\d{6}$') THEN
RAISE_APPLICATION_ERROR(-20001, '非法分表后缀: ' || NVL(P_SUFFIX, 'NULL'));
END IF;
END VALIDATE_SUFFIX;
FUNCTION OBJECT_EXISTS(P_OBJECT_NAME IN VARCHAR2, P_OBJECT_TYPE IN VARCHAR2) RETURN NUMBER IS
V_COUNT NUMBER;
BEGIN
SELECT COUNT(1)
INTO V_COUNT
FROM USER_OBJECTS
WHERE OBJECT_NAME = UPPER(P_OBJECT_NAME)
AND OBJECT_TYPE = UPPER(P_OBJECT_TYPE);
RETURN V_COUNT;
END OBJECT_EXISTS;
FUNCTION TABLE_EXISTS(P_TABLE_NAME IN VARCHAR2) RETURN NUMBER IS
V_COUNT NUMBER;
BEGIN
SELECT COUNT(1)
INTO V_COUNT
FROM USER_TABLES
WHERE TABLE_NAME = UPPER(P_TABLE_NAME);
RETURN V_COUNT;
END TABLE_EXISTS;
PROCEDURE EXECUTE_DDL(P_SQL IN CLOB) IS
V_CURSOR INTEGER;
BEGIN
V_CURSOR := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(V_CURSOR, P_SQL, DBMS_SQL.NATIVE);
DBMS_SQL.CLOSE_CURSOR(V_CURSOR);
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(V_CURSOR) THEN
DBMS_SQL.CLOSE_CURSOR(V_CURSOR);
END IF;
RAISE;
END EXECUTE_DDL;
PROCEDURE CREATE_TARGET_TABLE(P_SOURCE_TABLE IN VARCHAR2, P_TARGET_TABLE IN VARCHAR2) IS
V_SQL CLOB;
BEGIN
-- 为什么这样做:用当前月物理表结构克隆下月表,能保证字段类型与现网保持一致。
V_SQL := 'CREATE TABLE ' || P_TARGET_TABLE || ' AS SELECT * FROM ' || P_SOURCE_TABLE || ' WHERE 1 = 0';
EXECUTE IMMEDIATE V_SQL;
END CREATE_TARGET_TABLE;
PROCEDURE CREATE_TARGET_INDEXES(P_TARGET_TABLE IN VARCHAR2, P_TARGET_SUFFIX IN VARCHAR2) IS
V_INDEX_NAME_1 VARCHAR2(128);
V_INDEX_NAME_2 VARCHAR2(128);
BEGIN
V_INDEX_NAME_1 := 'IDX_BDPV_' || P_TARGET_SUFFIX || '_PCD';
V_INDEX_NAME_2 := 'IDX_BDPV_' || P_TARGET_SUFFIX || '_DCT';
IF OBJECT_EXISTS(V_INDEX_NAME_1, 'INDEX') = 0 THEN
EXECUTE IMMEDIATE 'CREATE INDEX ' || V_INDEX_NAME_1 ||
' ON ' || P_TARGET_TABLE || ' (PARAM_NAME, COLLECT_TIME, DEVICE_CODE, RECORD_ID)';
END IF;
IF OBJECT_EXISTS(V_INDEX_NAME_2, 'INDEX') = 0 THEN
EXECUTE IMMEDIATE 'CREATE INDEX ' || V_INDEX_NAME_2 ||
' ON ' || P_TARGET_TABLE || ' (DEVICE_CODE, COLLECT_TIME)';
END IF;
END CREATE_TARGET_INDEXES;
PROCEDURE CREATE_TARGET_TRIGGER(P_TARGET_TABLE IN VARCHAR2, P_TARGET_SUFFIX IN VARCHAR2) IS
V_TRIGGER_NAME VARCHAR2(128);
V_SQL CLOB;
BEGIN
V_TRIGGER_NAME := C_TRIGGER_PREFIX || P_TARGET_SUFFIX || '_RT';
IF OBJECT_EXISTS(V_TRIGGER_NAME, 'TRIGGER') > 0 THEN
RETURN;
END IF;
V_SQL := 'CREATE OR REPLACE TRIGGER ' || V_TRIGGER_NAME || CHR(10) ||
'AFTER INSERT ON ' || P_TARGET_TABLE || CHR(10) ||
'FOR EACH ROW' || CHR(10) ||
'DECLARE' || CHR(10) ||
' V_PROD_DATE DATE;' || CHR(10) ||
' V_NEW_VAL NUMBER(18,4);' || CHR(10) ||
' V_LAST_VAL NUMBER(18,4);' || CHR(10) ||
' V_CUR_TOTAL NUMBER(18,4);' || CHR(10) ||
' V_RESET_COUNT NUMBER(10);' || CHR(10) ||
' V_DELTA NUMBER(18,4);' || CHR(10) ||
'BEGIN' || CHR(10) ||
' IF :NEW.PARAM_NAME <> ''机台状态-实际产出数量'' THEN' || CHR(10) ||
' RETURN;' || CHR(10) ||
' END IF;' || CHR(10) ||
' IF :NEW.DEVICE_CODE LIKE ''OLD-%'' THEN' || CHR(10) ||
' RETURN;' || CHR(10) ||
' END IF;' || CHR(10) ||
' BEGIN' || CHR(10) ||
' V_NEW_VAL := TO_NUMBER(:NEW.PARAM_VALUE);' || CHR(10) ||
' EXCEPTION' || CHR(10) ||
' WHEN OTHERS THEN' || CHR(10) ||
' RETURN;' || CHR(10) ||
' END;' || CHR(10) ||
' V_PROD_DATE := TRUNC(:NEW.COLLECT_TIME);' || CHR(10) ||
' BEGIN' || CHR(10) ||
' SELECT LAST_PARAM_VAL, CURRENT_TOTAL, RESET_COUNT' || CHR(10) ||
' INTO V_LAST_VAL, V_CUR_TOTAL, V_RESET_COUNT' || CHR(10) ||
' FROM RT_DAILY_PROD_STATE' || CHR(10) ||
' WHERE PROD_DATE = V_PROD_DATE' || CHR(10) ||
' AND DEVICE_CODE = :NEW.DEVICE_CODE' || CHR(10) ||
' AND PARAM_NAME = :NEW.PARAM_NAME' || CHR(10) ||
' FOR UPDATE;' || CHR(10) ||
' IF V_NEW_VAL > V_LAST_VAL THEN' || CHR(10) ||
' V_DELTA := V_NEW_VAL - V_LAST_VAL;' || CHR(10) ||
' ELSIF V_NEW_VAL < V_LAST_VAL THEN' || CHR(10) ||
' V_DELTA := V_NEW_VAL;' || CHR(10) ||
' V_RESET_COUNT := NVL(V_RESET_COUNT, 0) + 1;' || CHR(10) ||
' ELSE' || CHR(10) ||
' V_DELTA := 0;' || CHR(10) ||
' END IF;' || CHR(10) ||
' UPDATE RT_DAILY_PROD_STATE' || CHR(10) ||
' SET LAST_PARAM_VAL = V_NEW_VAL,' || CHR(10) ||
' CURRENT_TOTAL = NVL(V_CUR_TOTAL, 0) + NVL(V_DELTA, 0),' || CHR(10) ||
' RESET_COUNT = NVL(V_RESET_COUNT, 0),' || CHR(10) ||
' LAST_COLLECT_TIME = :NEW.COLLECT_TIME,' || CHR(10) ||
' UPDATE_TIME = SYSDATE' || CHR(10) ||
' WHERE PROD_DATE = V_PROD_DATE' || CHR(10) ||
' AND DEVICE_CODE = :NEW.DEVICE_CODE' || CHR(10) ||
' AND PARAM_NAME = :NEW.PARAM_NAME;' || CHR(10) ||
' EXCEPTION' || CHR(10) ||
' WHEN NO_DATA_FOUND THEN' || CHR(10) ||
' INSERT INTO RT_DAILY_PROD_STATE (' || CHR(10) ||
' PROD_DATE, DEVICE_CODE, PARAM_NAME, LAST_PARAM_VAL,' || CHR(10) ||
' CURRENT_TOTAL, RESET_COUNT, DIRTY_FLAG, LAST_COLLECT_TIME, UPDATE_TIME' || CHR(10) ||
' ) VALUES (' || CHR(10) ||
' V_PROD_DATE, :NEW.DEVICE_CODE, :NEW.PARAM_NAME, V_NEW_VAL,' || CHR(10) ||
' 0, 0, 0, :NEW.COLLECT_TIME, SYSDATE' || CHR(10) ||
' );' || CHR(10) ||
' END;' || CHR(10) ||
'END;';
EXECUTE_DDL(V_SQL);
END CREATE_TARGET_TRIGGER;
PROCEDURE ENSURE_MONTH_TABLE_AND_TRIGGER(
P_TARGET_SUFFIX IN VARCHAR2 DEFAULT TO_CHAR(ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1), 'YYYYMM'),
P_SOURCE_SUFFIX IN VARCHAR2 DEFAULT TO_CHAR(TRUNC(SYSDATE, 'MM'), 'YYYYMM')
) IS
V_SOURCE_TABLE VARCHAR2(128);
V_TARGET_TABLE VARCHAR2(128);
BEGIN
VALIDATE_SUFFIX(P_TARGET_SUFFIX);
VALIDATE_SUFFIX(P_SOURCE_SUFFIX);
V_SOURCE_TABLE := C_TABLE_PREFIX || P_SOURCE_SUFFIX;
V_TARGET_TABLE := C_TABLE_PREFIX || P_TARGET_SUFFIX;
IF TABLE_EXISTS(V_SOURCE_TABLE) = 0 THEN
RAISE_APPLICATION_ERROR(-20002, '源分表不存在: ' || V_SOURCE_TABLE);
END IF;
IF TABLE_EXISTS(V_TARGET_TABLE) = 0 THEN
CREATE_TARGET_TABLE(V_SOURCE_TABLE, V_TARGET_TABLE);
END IF;
CREATE_TARGET_INDEXES(V_TARGET_TABLE, P_TARGET_SUFFIX);
CREATE_TARGET_TRIGGER(V_TARGET_TABLE, P_TARGET_SUFFIX);
END ENSURE_MONTH_TABLE_AND_TRIGGER;
END PKG_DEVICE_PARAM_PARTITION;
/

@ -0,0 +1,37 @@
-- 自动创建下月分表与触发器的数据库任务
-- 说明:
-- 1. 每月 28 号到 31 号 23:55 都会尝试执行一次。
-- 2. 由于 ENSURE_MONTH_TABLE_AND_TRIGGER 是幂等的,所以重复执行不会重复建表。
-- 3. 采用这种写法是为了兼容不同月份天数。
BEGIN
DBMS_SCHEDULER.DROP_JOB(
JOB_NAME => 'JOB_ENSURE_DEVICE_PARAM_PARTITION',
FORCE => TRUE
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -27475 THEN
RAISE;
END IF;
END;
/
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
JOB_NAME => 'JOB_ENSURE_DEVICE_PARAM_PARTITION',
JOB_TYPE => 'PLSQL_BLOCK',
JOB_ACTION => 'BEGIN PKG_DEVICE_PARAM_PARTITION.ENSURE_MONTH_TABLE_AND_TRIGGER; END;',
START_DATE => SYSTIMESTAMP,
REPEAT_INTERVAL => 'FREQ=MONTHLY;BYMONTHDAY=28,29,30,31;BYHOUR=23;BYMINUTE=55;BYSECOND=0',
ENABLED => TRUE,
AUTO_DROP => FALSE,
COMMENTS => '自动创建下月设备参数分表与 RT 触发器'
);
END;
/
-- 查看状态:
-- SELECT JOB_NAME, ENABLED, STATE, LAST_START_DATE, NEXT_RUN_DATE
-- FROM USER_SCHEDULER_JOBS
-- WHERE JOB_NAME = 'JOB_ENSURE_DEVICE_PARAM_PARTITION';

@ -0,0 +1,39 @@
-- 夜间 RT -> DAY 搬迁调度任务
-- 说明:
-- 1. 默认每天 00:10 执行一次,把“昨天”的 RT 数据搬迁到 DAY。
-- 2. 依赖已存在的包PKG_DEVICE_PROD_CALC
-- 3. 若 JOB 已存在,先删除再重建。
BEGIN
DBMS_SCHEDULER.DROP_JOB(
JOB_NAME => 'JOB_FLUSH_RT_TO_DAY',
FORCE => TRUE
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -27475 THEN
RAISE;
END IF;
END;
/
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
JOB_NAME => 'JOB_FLUSH_RT_TO_DAY',
JOB_TYPE => 'PLSQL_BLOCK',
JOB_ACTION => 'BEGIN PKG_DEVICE_PROD_CALC.FLUSH_RT_TO_DAY(TRUNC(SYSDATE) - 1, ''机台状态-实际产出数量''); END;',
START_DATE => SYSTIMESTAMP,
REPEAT_INTERVAL => 'FREQ=DAILY;BYHOUR=0;BYMINUTE=10;BYSECOND=0',
ENABLED => FALSE,
AUTO_DROP => FALSE,
COMMENTS => '设备产量 RT->DAY 夜间搬迁'
);
DBMS_SCHEDULER.ENABLE('JOB_FLUSH_RT_TO_DAY');
END;
/
-- 查看任务状态:
-- SELECT JOB_NAME, ENABLED, STATE, LAST_START_DATE, NEXT_RUN_DATE
-- FROM USER_SCHEDULER_JOBS
-- WHERE JOB_NAME = 'JOB_FLUSH_RT_TO_DAY';
Loading…
Cancel
Save