# 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 实际运行情况，不需要再每个月手工执行触发器模板。最自律帅气聪明的臧辰浩
