# Board4 数据库排查与重建说明

## 1. 文档目的

本文档用于沉淀本次 `Board4` 产量统计改造在数据库侧的完整排查过程、最终数据库对象状态、重建顺序和 SQL 备份。

重点说明：

1. 数据库负责：
   - `RT_DAILY_PROD_STATE`
   - `DEVICE_DAILY_PRODUCTION`
   - 月分表 `BASE_DEVICE_PARAM_VAL_YYYYMM`
   - 月分表触发器 `TRG_BDPV_YYYYMM_RT`
   - `PKG_DEVICE_PARAM_MONTHLY`
   - `PKG_DEVICE_PROD_CALC`
   - `JOB_AUTO_PREPARE_MONTHLY`
   - `JOB_FLUSH_RT_TO_DAY`
2. **数据库在每月 26 日提前创建下个月分表与触发器**
3. **C# 采集程序只负责按采集时间自动路由写入**

---

## 2. 当前最终职责边界

### 2.1 数据库负责的内容

1. 保存实时产量状态：`RT_DAILY_PROD_STATE`
2. 保存日产量汇总：`DEVICE_DAILY_PRODUCTION`
3. 在每月 26 日提前创建下个月分表 `BASE_DEVICE_PARAM_VAL_YYYYMM`
4. 在每月 26 日提前创建下个月触发器 `TRG_BDPV_YYYYMM_RT`
5. 通过月分表触发器把自动采集明细同步到 RT
6. 通过 `PKG_DEVICE_PROD_CALC` 把昨日 RT 搬迁到 DAY
7. 通过 `JOB_FLUSH_RT_TO_DAY` 每天 00:10 自动执行 RT -> DAY

### 2.2 C# 采集程序负责的内容

1. 使用 sqlSugar 按 `collect_time` 自动路由到目标月分表
2. 仅负责向已存在的 `BASE_DEVICE_PARAM_VAL_YYYYMM` 写入采集数据
3. 不再负责建表
4. 不再负责补触发器

### 2.3 Java 负责的内容

1. OLD 设备写 `BASE_DEVICE_PARAM_VAL`
2. OLD 设备在写入成功后，由 Java Service 同步更新 `RT_DAILY_PROD_STATE`

---

## 3. 数据库排查过程总结

### 3.1 初始排查

最初通过 `USER_TABLES / USER_OBJECTS / USER_SCHEDULER_JOBS` 查询关键对象时结果为空。

后续确认：

1. 实际业务 schema 是 `HAIWEI`
2. 需要通过 `ALL_TABLES / ALL_OBJECTS / ALL_TRIGGERS / ALL_SCHEDULER_JOBS` 查询全局对象

### 3.2 关键对象定位结果

确认在 `HAIWEI` schema 下已存在：

1. `BASE_DEVICE_PARAM_VAL_202603`
2. `RT_DAILY_PROD_STATE`
3. `DEVICE_DAILY_PRODUCTION`
4. `PKG_DEVICE_PROD_CALC`
5. `TRG_BDPV_202603_RT`
6. `JOB_FLUSH_RT_TO_DAY`

说明：

1. 当前月分表已经存在
2. 当前月触发器已经存在
3. RT / DAY 基础表已经存在
4. 夜间 RT->DAY 搬迁包和 job 已经存在

### 3.3 自动建分表方案最终确认

本次最终决策为：

1. 由数据库在每月 26 日提前创建下个月分表
2. 由数据库在每月 26 日提前创建下个月触发器
3. C# 采集程序仅按采集时间自动路由到目标月分表写入

原因：

1. 分表和触发器是自动采集链路的数据库基础设施，应由数据库统一托管
2. 月底前预建下个月对象，能规避月初切月瞬间的建表/补触发器空窗
3. sqlSugar 已经能按 `collect_time` 自动路由，C# 没必要再承担 DDL 职责
4. 统一在数据库侧建表和建触发器，职责边界更稳定

---

## 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.TRG_BDPV_202603_RT`

### 4.3 包

1. `HAIWEI.PKG_DEVICE_PROD_CALC`
2. `HAIWEI.PKG_DEVICE_PROD_CALC BODY`
3. `HAIWEI.PKG_DEVICE_PARAM_MONTHLY`
4. `HAIWEI.PKG_DEVICE_PARAM_MONTHLY BODY`

### 4.4 调度任务

1. `HAIWEI.JOB_AUTO_PREPARE_MONTHLY`
2. `HAIWEI.JOB_FLUSH_RT_TO_DAY`

---

## 5. 数据库完整链路

### 5.1 自动采集设备链路

```text
C# 采集程序
    -> sqlSugar 按 collect_time 自动路由
    -> 插入 BASE_DEVICE_PARAM_VAL_202603
    -> TRG_BDPV_202603_RT
    -> RT_DAILY_PROD_STATE
    -> JOB_FLUSH_RT_TO_DAY
    -> PKG_DEVICE_PROD_CALC
    -> DEVICE_DAILY_PRODUCTION
```

### 5.2 OLD 设备链路

```text
PDA / Java
    -> BASE_DEVICE_PARAM_VAL
    -> Java Service 同步更新 RT_DAILY_PROD_STATE
```

### 5.3 Board4 查询链路

```text
Board4.dayTotal
    -> RT_DAILY_PROD_STATE(当天)

Board4.monthTotal
    -> DEVICE_DAILY_PRODUCTION(本月已汇总部分)
    + RT_DAILY_PROD_STATE(本月未入DAY部分)

Board4.deviceProductionList
    -> RT_DAILY_PROD_STATE(当天 current_total)
```

---

## 6. 数据库重建顺序

如果未来数据库被重置，按以下顺序执行。

### 第一步：创建基础表

1. `rt_daily_prod_state.sql`
2. `device_daily_production.sql`

### 第二步：创建夜间搬迁包

3. `device_prod_calc_pkg.sql`

### 第三步：创建月分表自动化包

4. `device_param_monthly_auto_pkg.sql`

### 第四步：首次补齐当前月对象

5. 手工执行：
   `BEGIN PKG_DEVICE_PARAM_MONTHLY.PREPARE_MONTH(TO_CHAR(TRUNC(SYSDATE, 'MM'), 'YYYYMM')); END; /`

### 第五步：创建月分表自动化 job

6. `device_param_monthly_auto_scheduler_job.sql`

### 第六步：创建夜间 RT->DAY job

7. `device_prod_calc_scheduler_job.sql`

---

## 7. 当前资源目录保留的 SQL 文件

当前保留在：

```text
zs_aucma-mes-back/aucma-base/src/main/resources
```

的数据库脚本有 7 个：

1. `rt_daily_prod_state.sql`
2. `device_daily_production.sql`
3. `device_prod_calc_pkg.sql`
4. `device_param_monthly_auto_pkg.sql`
5. `device_param_monthly_auto_scheduler_job.sql`
6. `base_device_param_val_202603_trigger.sql`
7. `device_prod_calc_scheduler_job.sql`

说明：

1. 这些脚本是数据库恢复必需的脚本
2. 月分表与触发器改为由数据库自动化统一托管
3. C# 采集程序通过 sqlSugar 按 `collect_time` 自动路由写入
4. 数据库在每月 26 日提前创建下个月分表与触发器

---

## 8. SQL 原文备份

### 8.1 `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);
```

### 8.2 `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);
```

### 8.3 `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;
/
```

### 8.4 `base_device_param_val_202603_trigger.sql`

```sql
-- 2026年03月自动采集分表触发器
-- 说明：
-- 1. 挂载表：BASE_DEVICE_PARAM_VAL_202603
-- 2. 仅处理自动采集设备；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;
/
```

### 8.5 `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. 最终结论

当前最终数据库方案是：

1. 数据库在每月 26 日提前创建下个月分表 `BASE_DEVICE_PARAM_VAL_YYYYMM`
2. 数据库在每月 26 日提前创建下个月触发器 `TRG_BDPV_YYYYMM_RT`
3. C# 采集程序通过 sqlSugar 按 `collect_time` 自动路由写入目标月分表
4. 自动采集明细通过触发器实时进入 RT
5. OLD 数据由 Java 同步进入 RT
6. 夜间由 `JOB_FLUSH_RT_TO_DAY` 驱动 `PKG_DEVICE_PROD_CALC` 搬迁到 DAY

换句话说：

```text
数据库侧保留“月分表预建 + 实时累计 + 夜间汇总”
C# 采集程序只保留“按采集时间自动路由写入”
```
