From 41f4b8f6fbfa2b96fc6d24781fc72902dfe5f1d8 Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 16 Mar 2026 16:56:27 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Board4=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=8E=92=E6=9F=A5=E4=B8=8E=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现在 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 新增内容包括: 按顺序执行的总表 每个脚本是否必须执行 每个脚本具体作用 首次重置恢复时的推荐执行顺序 为什么当前月触发器首次仍需手工补一次 后续月份为什么可以自动化 --- .../base/support/DeviceParamTableRouter.java | 118 ++ .../base_device_param_val_202603_trigger.sql | 77 ++ ...abase_troubleshooting_and_rebuild_guide.md | 1018 +++++++++++++++++ .../device_param_partition_auto_pkg.sql | 190 +++ ...ice_param_partition_auto_scheduler_job.sql | 37 + .../device_prod_calc_scheduler_job.sql | 39 + 6 files changed, 1479 insertions(+) create mode 100644 aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java create mode 100644 aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql create mode 100644 aucma-base/src/main/resources/board4_database_troubleshooting_and_rebuild_guide.md create mode 100644 aucma-base/src/main/resources/device_param_partition_auto_pkg.sql create mode 100644 aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql create mode 100644 aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql diff --git a/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java b/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java new file mode 100644 index 0000000..6221792 --- /dev/null +++ b/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java @@ -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 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 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 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; + } +} diff --git a/aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql b/aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql new file mode 100644 index 0000000..e1d28fd --- /dev/null +++ b/aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql @@ -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; +/ diff --git a/aucma-base/src/main/resources/board4_database_troubleshooting_and_rebuild_guide.md b/aucma-base/src/main/resources/board4_database_troubleshooting_and_rebuild_guide.md new file mode 100644 index 0000000..a4b4ff0 --- /dev/null +++ b/aucma-base/src/main/resources/board4_database_troubleshooting_and_rebuild_guide.md @@ -0,0 +1,1018 @@ +# Board4 数据库排查与自动化重建说明 + +## 1. 文档目的 + +本文档用于沉淀本次 `Board4` 产量统计方案在数据库侧的完整排查过程、最终状态和自动化 SQL 备份,避免未来数据库被重置后无法快速恢复。 + +本文档覆盖: + +1. 本次数据库对象排查过程 +2. 当前数据库对象最终状态 +3. 自动化部署链路说明 +4. 数据库重建顺序 +5. 所有关键 SQL 原文备份 + +--- + +## 2. 背景与目标 + +本次数据库侧改造围绕以下目标展开: + +1. 为 `Board4.dayTotal`、`Board4.monthTotal`、`deviceProductionList` 引入实时表 `RT_DAILY_PROD_STATE` +2. 为月累计提供权威日汇总表 `DEVICE_DAILY_PRODUCTION` +3. 让自动采集设备写入月分表 `BASE_DEVICE_PARAM_VAL_YYYYMM` +4. 让自动采集分表通过触发器自动更新 `RT_DAILY_PROD_STATE` +5. 让数据库夜间自动执行 `RT -> DAY` 搬迁 +6. 让数据库在月底自动创建下月分表和下月触发器 + +当前分表规则已统一为: + +```text +BASE_DEVICE_PARAM_VAL_YYYYMM +``` + +例如: + +```text +BASE_DEVICE_PARAM_VAL_202603 +BASE_DEVICE_PARAM_VAL_202604 +``` + +--- + +## 3. 本次排查过程 + +### 3.1 第一轮检查 + +最开始执行了基于 `USER_TABLES / USER_OBJECTS / USER_SCHEDULER_JOBS` 的检查 SQL,结果全部为空。 + +最初判断有两种可能: + +1. 当前登录用户不是实际业务 schema +2. 业务 schema 尚未部署任何对象 + +后续用户确认: + +```text +BASE_DEVICE_PARAM_VAL_202603 已经建了 +``` + +这说明: + +1. 之前查询使用的视角不对 +2. 需要改用 `ALL_TABLES / ALL_OBJECTS / ALL_SCHEDULER_JOBS` 继续排查 + +--- + +### 3.2 第二轮检查:确认实际对象所在 schema + +通过 `ALL_TABLES / ALL_OBJECTS / ALL_TRIGGERS / ALL_SCHEDULER_JOBS` 排查后,确认对象位于: + +```text +HAIWEI +``` + +确认已存在对象: + +1. `HAIWEI.BASE_DEVICE_PARAM_VAL_202603` +2. `HAIWEI.RT_DAILY_PROD_STATE` +3. `HAIWEI.DEVICE_DAILY_PRODUCTION` +4. `HAIWEI.PKG_DEVICE_PROD_CALC` +5. `HAIWEI.PKG_DEVICE_PROD_CALC BODY` + +当时仍缺失: + +1. `TRG_BDPV_202603_RT` +2. `JOB_FLUSH_RT_TO_DAY` +3. `PKG_DEVICE_PARAM_PARTITION` +4. `JOB_ENSURE_DEVICE_PARAM_PARTITION` + +--- + +### 3.3 当前月触发器与调度任务部署 + +后续补部署后,再次查询,确认以下对象已存在: + +1. `HAIWEI.TRG_BDPV_202603_RT` +2. `HAIWEI.JOB_FLUSH_RT_TO_DAY` +3. `HAIWEI.JOB_ENSURE_DEVICE_PARAM_PARTITION` + +当时 `JOB` 状态为: + +```text +JOB_ENSURE_DEVICE_PARAM_PARTITION TRUE SCHEDULED RUN_COUNT=0 FAILURE_COUNT=0 +JOB_FLUSH_RT_TO_DAY TRUE SCHEDULED RUN_COUNT=0 FAILURE_COUNT=0 +``` + +这说明: + +1. 任务已经创建并启用 +2. 但尚未到首次实际执行时间 +3. 仍需继续验证“自动建下月分表+触发器”能力 + +--- + +### 3.4 手工验证自动建下月分表能力时出现 ORA-01031 + +执行: + +```sql +BEGIN + HAIWEI.PKG_DEVICE_PARAM_PARTITION.ENSURE_MONTH_TABLE_AND_TRIGGER('202604', '202603'); +END; +/ +``` + +返回: + +```text +ORA-01031: 权限不足 +ORA-06512: 在 "HAIWEI.PKG_DEVICE_PARAM_PARTITION", line 52 +ORA-06512: 在 "HAIWEI.PKG_DEVICE_PARAM_PARTITION", line 164 +``` + +问题定位: + +1. 不是 SQL 逻辑问题 +2. 是 Oracle 包内执行 DDL 时的权限问题 +3. `PKG_DEVICE_PARAM_PARTITION` 内部需要执行: + - `CREATE TABLE` + - `CREATE INDEX` + - `CREATE OR REPLACE TRIGGER` + +Oracle 特性说明: + +1. 包里执行 DDL 依赖的是 **直接授予的系统权限** +2. 不是依赖角色权限 +3. Oracle 没有 `GRANT CREATE INDEX TO user` 这种系统权限,索引能力依赖表对象和表空间配额 + +--- + +### 3.5 权限排查与修正 + +在权限排查过程中发现一个执行细节: + +1. 当前 SQL 客户端执行 `GRANT` 语句时,**不能带尾部分号** +2. 带分号时会报: + +```text +ORA-00933: SQL 命令未正确结束 +``` + +最终应采用“不带分号、单条执行”的方式,例如: + +```sql +GRANT CREATE TRIGGER TO HAIWEI +``` + +本次建议的最小权限模型为: + +```sql +GRANT CREATE TABLE TO HAIWEI +GRANT CREATE TRIGGER TO HAIWEI +GRANT CREATE JOB TO HAIWEI +GRANT CREATE PROCEDURE TO HAIWEI +``` + +注意: + +1. 不需要 `CREATE ANY TABLE` +2. 不需要 `CREATE ANY TRIGGER` +3. 不需要维护其它 schema +4. 只允许 `HAIWEI` 在自己 schema 下自动维护本系统对象 + +--- + +### 3.6 表空间配额检查 + +后续补查了 `HAIWEI` 的表空间信息。 + +用户反馈的关键结果为: + +```text +HAIWEI_DATA -1 10215620608 +``` + +可解释为: + +1. `HAIWEI` 在业务表空间 `HAIWEI_DATA` 上已有配额 +2. `MAX_BYTES = -1` 一般表示无限配额 +3. `BYTES = 10215620608` 表示已经占用约 9.5GB + +结论: + +1. 当前自动建表失败的根因不是表空间配额 +2. 核心根因是 DDL 权限不足 + +--- + +### 3.7 自动建下月分表能力最终验证通过 + +在权限与环境调整完成后,再次执行: + +```sql +BEGIN + HAIWEI.PKG_DEVICE_PARAM_PARTITION.ENSURE_MONTH_TABLE_AND_TRIGGER('202604', '202603'); +END; +/ +``` + +执行成功。 + +这意味着: + +1. `PKG_DEVICE_PARAM_PARTITION` 可以在 `HAIWEI` schema 内自动建下月分表 +2. 也可以自动创建对应下月触发器 +3. 后续不需要每个月手工执行触发器模板 + +--- + +## 4. 当前数据库最终状态 + +根据本次排查与执行结果,当前数据库已经具备以下能力。 + +### 4.1 已存在的关键表 + +1. `HAIWEI.BASE_DEVICE_PARAM_VAL_202603` +2. `HAIWEI.RT_DAILY_PROD_STATE` +3. `HAIWEI.DEVICE_DAILY_PRODUCTION` + +### 4.2 已存在的关键包 + +1. `HAIWEI.PKG_DEVICE_PROD_CALC` +2. `HAIWEI.PKG_DEVICE_PROD_CALC BODY` +3. `HAIWEI.PKG_DEVICE_PARAM_PARTITION` +4. `HAIWEI.PKG_DEVICE_PARAM_PARTITION BODY` + +### 4.3 已存在的关键触发器 + +1. `HAIWEI.TRG_BDPV_202603_RT` + +### 4.4 已存在的关键调度任务 + +1. `HAIWEI.JOB_FLUSH_RT_TO_DAY` +2. `HAIWEI.JOB_ENSURE_DEVICE_PARAM_PARTITION` + +--- + +## 5. 自动化链路说明 + +### 5.1 当前月自动采集链路 + +```text +自动采集数据 + -> BASE_DEVICE_PARAM_VAL_202603 + -> TRG_BDPV_202603_RT + -> RT_DAILY_PROD_STATE +``` + +说明: + +1. 仅自动采集设备会走当前月分表 +2. 触发器只处理: + - `PARAM_NAME = '机台状态-实际产出数量'` +3. 触发器会执行 delta-sum 累计逻辑 + +### 5.2 OLD 设备链路 + +```text +PDA / Java 写入 + -> BASE_DEVICE_PARAM_VAL + -> BaseDeviceParamValServiceImpl.syncRtStateIfNeeded(...) + -> RT_DAILY_PROD_STATE +``` + +说明: + +1. OLD 设备不依赖数据库触发器 +2. OLD 的 RT 更新由 Java 服务层同步完成 + +### 5.3 夜间汇总链路 + +```text +JOB_FLUSH_RT_TO_DAY + -> PKG_DEVICE_PROD_CALC.FLUSH_RT_TO_DAY + -> RT_DAILY_PROD_STATE(昨天) + -> DEVICE_DAILY_PRODUCTION + -> 删除昨天 RT +``` + +调度时间: + +```text +每天 00:10:00 +``` + +### 5.4 下月分表自动准备链路 + +```text +JOB_ENSURE_DEVICE_PARAM_PARTITION + -> PKG_DEVICE_PARAM_PARTITION.ENSURE_MONTH_TABLE_AND_TRIGGER + -> 用当前月表克隆下月表 + -> 创建下月索引 + -> 创建下月 RT 触发器 +``` + +调度时间: + +```text +每月 28/29/30/31 日 23:55:00 +``` + +这样设计的原因: + +1. 不同月份天数不同 +2. 过程本身是幂等的 +3. 重复触发也不会重复建对象 + +--- + +## 6. 现在是否已经“完全自动运行” + +从机制角度看,当前已经具备: + +1. 当前月自动采集数据自动进 RT +2. 夜间自动 RT -> DAY +3. 月底自动准备下月分表与触发器 + +因此,当前可以认为: + +```text +数据库自动化链路已经部署完成 +``` + +但仍建议在以下两个时间点做一次运维验证: + +1. `JOB_FLUSH_RT_TO_DAY` 首次实际运行后,确认 `RUN_COUNT > 0` +2. 月底任务首次自动执行后,确认已自动生成: + - `BASE_DEVICE_PARAM_VAL_202604` + - `TRG_BDPV_202604_RT` + +--- + +## 7. 数据库重建顺序 + +如果未来数据库被重置,可按以下顺序恢复: + +### 第一步:创建基础表 + +1. `rt_daily_prod_state.sql` +2. `device_daily_production.sql` + +### 第二步:创建夜间搬迁包 + +3. `device_prod_calc_pkg.sql` + +### 第三步:确保当前月分表存在 + +4. 手工创建当前月分表,例如: + `BASE_DEVICE_PARAM_VAL_202603` + +说明: + +1. 自动建分表包默认是“用当前月分表去克隆下月分表” +2. 所以首次上线时,当前月分表仍需人工准备 + +### 第四步:为当前月分表创建触发器 + +5. `base_device_param_val_202603_trigger.sql` + +### 第五步:创建夜间调度任务 + +6. `device_prod_calc_scheduler_job.sql` + +### 第六步:创建自动建下月分表包 + +7. `device_param_partition_auto_pkg.sql` + +### 第七步:创建自动建下月分表调度任务 + +8. `device_param_partition_auto_scheduler_job.sql` + +--- + +## 8. 脚本执行顺序与作用总表 + +为避免数据库重置后执行顺序混乱,建议严格按下表顺序执行。 + +| 顺序 | 文件名 | 文件位置 | 是否必须 | 作用 | +|------|--------|----------|----------|------| +| 1 | `rt_daily_prod_state.sql` | `aucma-base/src/main/resources` | 必须 | 创建实时产量状态表 `RT_DAILY_PROD_STATE`,承接自动采集触发器与 OLD 设备 Java 同步累计 | +| 2 | `device_daily_production.sql` | `aucma-base/src/main/resources` | 必须 | 创建设备日产量权威汇总表 `DEVICE_DAILY_PRODUCTION`,承接夜间 RT->DAY 搬迁 | +| 3 | `device_prod_calc_pkg.sql` | `aucma-base/src/main/resources` | 必须 | 创建夜间搬迁包 `PKG_DEVICE_PROD_CALC`,负责把昨日 RT 数据搬迁到 DAY | +| 4 | `base_device_param_val_202603_trigger.sql` | `aucma-base/src/main/resources` | 当前月必须 | 为当前月分表 `BASE_DEVICE_PARAM_VAL_202603` 创建触发器 `TRG_BDPV_202603_RT`,让自动采集写入实时更新 RT | +| 5 | `device_prod_calc_scheduler_job.sql` | `aucma-base/src/main/resources` | 必须 | 创建夜间调度任务 `JOB_FLUSH_RT_TO_DAY`,每天 00:10 自动执行 `PKG_DEVICE_PROD_CALC.FLUSH_RT_TO_DAY` | +| 6 | `device_param_partition_auto_pkg.sql` | `aucma-base/src/main/resources` | 推荐且建议必须 | 创建自动建下月分表/触发器包 `PKG_DEVICE_PARAM_PARTITION`,避免每月手工建新分表 | +| 7 | `device_param_partition_auto_scheduler_job.sql` | `aucma-base/src/main/resources` | 推荐且建议必须 | 创建月底自动建下月分表与触发器的任务 `JOB_ENSURE_DEVICE_PARAM_PARTITION` | + +补充说明: + +1. 第 4 步之所以仍要“当前月必须手工执行一次”,是因为自动建月分表包默认以“当前月分表”为源克隆“下月分表”,所以首次上线必须先把当前月表和当前月触发器补齐。 +2. 第 6、7 步执行完成后,后续月份无需再手工创建 `BASE_DEVICE_PARAM_VAL_YYYYMM` 和 `TRG_BDPV_YYYYMM_RT`。 +3. 如果数据库不是首次重置,而只是补装夜间任务,可以在确认基础表和当前月触发器都存在后,从第 5 步开始执行。 + +--- + +## 9. 自动化 SQL 文件清单 + +本次自动化相关 SQL 一共 7 个: + +1. `zs_aucma-mes-back/aucma-base/src/main/resources/rt_daily_prod_state.sql` +2. `zs_aucma-mes-back/aucma-base/src/main/resources/device_daily_production.sql` +3. `zs_aucma-mes-back/aucma-base/src/main/resources/device_prod_calc_pkg.sql` +4. `sql/base_device_param_val_202603_trigger.sql` +5. `sql/device_prod_calc_scheduler_job.sql` +6. `sql/device_param_partition_auto_pkg.sql` +7. `sql/device_param_partition_auto_scheduler_job.sql` + +以下为全部 SQL 原文备份。 + +--- + +## 10. SQL 原文备份 + +### 9.1 `rt_daily_prod_state.sql` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/rt_daily_prod_state.sql +``` + +```sql +-- RT_DAILY_PROD_STATE:设备当日实时产量状态表 +-- 兼容:Oracle 11 / Oracle 19 + +CREATE TABLE RT_DAILY_PROD_STATE ( + PROD_DATE DATE NOT NULL, + DEVICE_CODE VARCHAR2(64) NOT NULL, + PARAM_NAME VARCHAR2(128) NOT NULL, + LAST_PARAM_VAL NUMBER(18,4) DEFAULT 0 NOT NULL, + CURRENT_TOTAL NUMBER(18,4) DEFAULT 0 NOT NULL, + RESET_COUNT NUMBER(10) DEFAULT 0 NOT NULL, + DIRTY_FLAG NUMBER(1) DEFAULT 0 NOT NULL, + LAST_COLLECT_TIME DATE, + UPDATE_TIME DATE DEFAULT SYSDATE NOT NULL +); + +COMMENT ON TABLE RT_DAILY_PROD_STATE IS '设备当日实时产量状态表'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.PROD_DATE IS '生产日期,按自然日截断'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.DEVICE_CODE IS '设备编码'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.PARAM_NAME IS '产量参数名称,当前固定为机台状态-实际产出数量'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.LAST_PARAM_VAL IS '最近一次采集到的参数值'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.CURRENT_TOTAL IS '当日累计产量'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.RESET_COUNT IS '当天计数器回退/清零次数'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.DIRTY_FLAG IS '脏标记,预留给人工修正/对账'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.LAST_COLLECT_TIME IS '最近一次参与累计的采集时间'; +COMMENT ON COLUMN RT_DAILY_PROD_STATE.UPDATE_TIME IS '最近更新时间'; + +ALTER TABLE RT_DAILY_PROD_STATE + ADD CONSTRAINT PK_RT_DAILY_PROD_STATE + PRIMARY KEY (PROD_DATE, DEVICE_CODE, PARAM_NAME); + +CREATE INDEX IDX_RT_DAILY_PROD_STATE_COLLECT + ON RT_DAILY_PROD_STATE (LAST_COLLECT_TIME); + +CREATE INDEX IDX_RT_DAILY_PROD_STATE_DEVICE + ON RT_DAILY_PROD_STATE (DEVICE_CODE, PROD_DATE); +``` + +--- + +### 9.2 `device_daily_production.sql` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_daily_production.sql +``` + +```sql +-- DEVICE_DAILY_PRODUCTION:设备日产量权威汇总表 +-- 兼容:Oracle 11 / Oracle 19 + +CREATE TABLE DEVICE_DAILY_PRODUCTION ( + PROD_DATE DATE NOT NULL, + DEVICE_CODE VARCHAR2(64) NOT NULL, + PARAM_NAME VARCHAR2(128) NOT NULL, + DAILY_PROD NUMBER(18,4) DEFAULT 0 NOT NULL, + RESET_COUNT NUMBER(10) DEFAULT 0 NOT NULL, + CALC_TIME DATE DEFAULT SYSDATE NOT NULL +); + +COMMENT ON TABLE DEVICE_DAILY_PRODUCTION IS '设备日产量权威汇总表'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.PROD_DATE IS '生产日期,按自然日'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.DEVICE_CODE IS '设备编码'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.PARAM_NAME IS '产量参数名称'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.DAILY_PROD IS '该设备该日最终日产量'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.RESET_COUNT IS '该设备该日计数器回退次数'; +COMMENT ON COLUMN DEVICE_DAILY_PRODUCTION.CALC_TIME IS '汇总计算时间'; + +ALTER TABLE DEVICE_DAILY_PRODUCTION + ADD CONSTRAINT PK_DEVICE_DAILY_PRODUCTION + PRIMARY KEY (PROD_DATE, DEVICE_CODE, PARAM_NAME); + +CREATE INDEX IDX_DEVICE_DAILY_PRODUCTION_MONTH + ON DEVICE_DAILY_PRODUCTION (PROD_DATE, PARAM_NAME); + +CREATE INDEX IDX_DEVICE_DAILY_PRODUCTION_DEVICE + ON DEVICE_DAILY_PRODUCTION (DEVICE_CODE, PROD_DATE); +``` + +--- + +### 9.3 `device_prod_calc_pkg.sql` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_prod_calc_pkg.sql +``` + +```sql +-- 设备产量夜间搬迁过程 +-- 兼容:Oracle 11 / Oracle 19 +-- 说明: +-- 1. 只做 RT -> DAY 搬迁,不再夜间重扫明细表。 +-- 2. 搬迁完成后删除目标日期 RT 数据,等待新一天自然重建。 + +CREATE OR REPLACE PACKAGE PKG_DEVICE_PROD_CALC AS + + PROCEDURE FLUSH_RT_TO_DAY( + P_PROD_DATE IN DATE DEFAULT TRUNC(SYSDATE) - 1, + P_PARAM_NAME IN VARCHAR2 DEFAULT '机台状态-实际产出数量' + ); + +END PKG_DEVICE_PROD_CALC; +/ + +CREATE OR REPLACE PACKAGE BODY PKG_DEVICE_PROD_CALC AS + + PROCEDURE FLUSH_RT_TO_DAY( + P_PROD_DATE IN DATE DEFAULT TRUNC(SYSDATE) - 1, + P_PARAM_NAME IN VARCHAR2 DEFAULT '机台状态-实际产出数量' + ) IS + V_TARGET_DATE DATE := TRUNC(P_PROD_DATE); + BEGIN + MERGE INTO DEVICE_DAILY_PRODUCTION D + USING ( + SELECT PROD_DATE, + DEVICE_CODE, + PARAM_NAME, + CURRENT_TOTAL, + RESET_COUNT + FROM RT_DAILY_PROD_STATE + WHERE PROD_DATE = V_TARGET_DATE + AND PARAM_NAME = P_PARAM_NAME + ) S + ON ( + D.PROD_DATE = S.PROD_DATE + AND D.DEVICE_CODE = S.DEVICE_CODE + AND D.PARAM_NAME = S.PARAM_NAME + ) + WHEN MATCHED THEN + UPDATE SET + D.DAILY_PROD = S.CURRENT_TOTAL, + D.RESET_COUNT = S.RESET_COUNT, + D.CALC_TIME = SYSDATE + WHEN NOT MATCHED THEN + INSERT ( + PROD_DATE, DEVICE_CODE, PARAM_NAME, DAILY_PROD, RESET_COUNT, CALC_TIME + ) VALUES ( + S.PROD_DATE, S.DEVICE_CODE, S.PARAM_NAME, S.CURRENT_TOTAL, S.RESET_COUNT, SYSDATE + ); + + DELETE FROM RT_DAILY_PROD_STATE + WHERE PROD_DATE = V_TARGET_DATE + AND PARAM_NAME = P_PARAM_NAME; + + COMMIT; + EXCEPTION + WHEN OTHERS THEN + ROLLBACK; + RAISE; + END FLUSH_RT_TO_DAY; + +END PKG_DEVICE_PROD_CALC; +/ +``` + +--- + +### 9.4 `base_device_param_val_202603_trigger.sql` + +文件位置: + +```text +sql/base_device_param_val_202603_trigger.sql +``` + +```sql +-- 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; +/ +``` + +--- + +### 9.5 `device_prod_calc_scheduler_job.sql` + +文件位置: + +```text +sql/device_prod_calc_scheduler_job.sql +``` + +```sql +-- 夜间 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; +/ +``` + +--- + +### 9.6 `device_param_partition_auto_pkg.sql` + +文件位置: + +```text +sql/device_param_partition_auto_pkg.sql +``` + +```sql +-- 自动创建设备参数月分表与对应触发器 +-- 说明: +-- 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; +/ +``` + +--- + +### 9.7 `device_param_partition_auto_scheduler_job.sql` + +文件位置: + +```text +sql/device_param_partition_auto_scheduler_job.sql +``` + +```sql +-- 自动创建下月分表与触发器的数据库任务 +-- 说明: +-- 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; +/ +``` + +--- + +## 11. 推荐保留策略 + +建议: + +1. 本文档长期保留在: + `zs_aucma-mes-back/aucma-base/src/main/resources` +2. 根目录 `sql/` 中的自动化 SQL 文件不要删除 +3. 数据库重置、迁移、容灾恢复时,以本文档为准恢复对象 + +--- + +## 12. 最终结论 + +本次排查后,数据库侧已经完成: + +1. 当前月自动采集分表 `BASE_DEVICE_PARAM_VAL_202603` +2. 当前月触发器 `TRG_BDPV_202603_RT` +3. 实时表 `RT_DAILY_PROD_STATE` +4. 日汇总表 `DEVICE_DAILY_PRODUCTION` +5. 夜间搬迁包 `PKG_DEVICE_PROD_CALC` +6. 夜间搬迁任务 `JOB_FLUSH_RT_TO_DAY` +7. 自动建下月分表包 `PKG_DEVICE_PARAM_PARTITION` +8. 自动建下月分表任务 `JOB_ENSURE_DEVICE_PARAM_PARTITION` + +从架构上看,数据库已具备: + +```text +当前月自动采集入 RT ++ 夜间自动 RT -> DAY ++ 月底自动准备下月分表与下月触发器 +``` + +后续只需要常规运维检查 job 实际运行情况,不需要再每个月手工执行触发器模板。最自律帅气聪明的臧辰浩 diff --git a/aucma-base/src/main/resources/device_param_partition_auto_pkg.sql b/aucma-base/src/main/resources/device_param_partition_auto_pkg.sql new file mode 100644 index 0000000..e2a9192 --- /dev/null +++ b/aucma-base/src/main/resources/device_param_partition_auto_pkg.sql @@ -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; +/ diff --git a/aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql b/aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql new file mode 100644 index 0000000..94dd10e --- /dev/null +++ b/aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql @@ -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'; diff --git a/aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql b/aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql new file mode 100644 index 0000000..d178d45 --- /dev/null +++ b/aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql @@ -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';