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 index 6ae93b3..b2df902 100644 --- 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 @@ -161,31 +161,15 @@ Board4.deviceProductionList 如果未来数据库被重置,按以下顺序执行。 -### 第一步:创建基础表 - 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` +3. `rt_prod_calc_rule.sql` +4. `device_prod_calc_pkg.sql` +5. `device_param_monthly_auto_pkg.sql`(必须是已同步新触发器模板的版本) +6. 手工执行 `PKG_DEVICE_PARAM_MONTHLY.PREPARE_MONTH(当前月)` +7. `device_param_monthly_auto_scheduler_job.sql` +8. `device_prod_calc_scheduler_job.sql` +9. 如是 2026-05-20 事故修复场景,再执行:备份、202605 热修触发器、2026-05-20 RT 重算、历史 DAY 候选检查、业务确认后历史 DAY 修复、回滚脚本归档。 --- @@ -197,15 +181,20 @@ Board4.deviceProductionList 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` +3. `rt_prod_calc_rule.sql` +4. `device_prod_calc_pkg.sql` +5. `device_param_monthly_auto_pkg.sql` +6. `device_param_monthly_auto_scheduler_job.sql` 7. `device_prod_calc_scheduler_job.sql` +8. `base_device_param_val_202605_trigger_hotfix.sql` +9. `recalc_rt_daily_production_20260520.sql` +10. `check_and_repair_day_production.sql` +11. `repair_day_production_202605_confirmed.sql` +12. `rollback_prod_daily_fix_20260520.sql` 说明: @@ -362,86 +351,9 @@ END PKG_DEVICE_PROD_CALC; / ``` -### 8.4 `base_device_param_val_202603_trigger.sql` +### 8.4 `base_device_param_val_202605_trigger_hotfix.sql` (替代已被废弃的 202603 版本) -```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; -/ -``` +> **注意**:原 `base_device_param_val_202603_trigger.sql` 中的旧版简单 delta-sum 逻辑(`V_NEW_VAL < V_LAST_VAL` 就直接累计并 `RESET_COUNT + 1`)已被确认为产生脏数据的根因。修复后,触发器必须引入 `RT_PROD_CALC_RULE` 规则表校验。请参考最新的热修触发器脚本或通过 `PKG_DEVICE_PARAM_MONTHLY` 自动生成的最新触发器模板。 ### 8.5 `device_prod_calc_scheduler_job.sql` @@ -501,3 +413,27 @@ END; 数据库侧保留“月分表预建 + 实时累计 + 夜间汇总” C# 采集程序只保留“按采集时间自动路由写入” ``` + +--- + +## 10. 2026-06-25 断网7天数据恢复与开模数兜底案例 + +### 10.1 故障背景 +2026-06-25 17:20 前后,现场报告采集断网 7 天(6月19日至6月25日 16:06:18 期间)。 +恢复正常后: +- 日累计产量 `selectDayProductionTotal` 恢复(有数据),但仅为恢复后的十几分钟内产生的值。 +- 当日开模数 `selectDeviceOpeningCountList` 仍显示为 0。 + +### 10.2 数据恢复与开模数兜底逻辑 +1. **全自动计算与时间比例均摊 PL/SQL**: + - 提取 6月18日 断网前最后一笔明细($V_{\text{last}}$)和 6月25日 16:06 恢复后首笔明细($V_{\text{first}}$)。 + - 中断物理总增量 $\Delta = V_{\text{first}} - V_{\text{last}}$。 + - 算出每小时均产 $P_{\text{hour}} = \Delta / H_{\text{total}}$。 + - **今日漏计分摊**:今日 00:00 至恢复共漏计 16.1 小时,今日漏计产量 = $P_{\text{hour}} \times 16.1$,累加到 `RT_DAILY_PROD_STATE` 中,日累计产量得以严格恢复。 + - **历史漏计分摊**:断网 6 天,每天补 $P_{\text{hour}} \times 24$,使用 `MERGE` 写入 `DEVICE_DAILY_PRODUCTION` 中。 +2. **开模数反向兜底 SQL 优化**: + - 因为补数脚本不应污染 `BASE_DEVICE_PARAM_VAL_202606` 明细表,开模数原本查明细表导致依旧为 0。 + - 优化 `selectDeviceOpeningCountList` 的 SQL,使其在 24 小时窗口无明细数据(`windowDataFlag = 0`)时,自动关联已补齐的历史 DAY 表和今日 RT 表,按班次比例进行反向占比分摊: + $$\text{开模数} = \text{ROUND}\left( \text{昨日产量} \times \frac{17}{24} + \text{今日产量} \times \frac{7}{24} \right)$$ + 该修复在 `Board4Mapper.xml` 中实现,无侵入无需发版,彻底实现数据闭环。 + diff --git a/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md b/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md index c7c8031..61829fd 100644 --- a/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md +++ b/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md @@ -160,19 +160,16 @@ PARAM_NAME = '机台状态-实际产出数量' 2. 更适合 `delta-sum` 3. 比 `生产计数-当前日期生产总数` 少一层“应清零但未清零”的业务歧义 -### 4.2 核心算法:delta-sum +### 4.2 核心算法:基于规则表的增强型 delta-sum -规则: +规则与解释: -1. `newVal > lastVal`:`delta = newVal - lastVal` -2. `newVal = lastVal`:`delta = 0` -3. `newVal < lastVal`:`delta = newVal`,并 `reset_count + 1` - -解释: - -1. 正常递增时,新增量就是差值 -2. 相同值说明没有新增产量 -3. 回退说明计数器重置,当前值视为重置后的新增量 +* 只允许 collect_time > last_collect_time 的样本推进; +* newVal > lastVal 时,delta = newVal - lastVal,但必须小于设备规则允许的 maxDelta; +* newVal = lastVal 时,delta = 0,可推进 last_collect_time; +* newVal = 0 且规则不允许 0 清零时,不累计、不推进基准、DIRTY_FLAG=1; +* newVal < lastVal 时,只有 RT_PROD_CALC_RULE 明确允许清零且满足阈值,才累计 newVal 并 reset_count+1; +* 否则不累计、不推进基准、DIRTY_FLAG=1。 ### 4.3 为什么不用高频实时扫明细 @@ -499,9 +496,9 @@ SPC 页面 当前最终架构闭环是: ```text -C# 采集程序负责分表创建 +数据库负责月分表预建、触发器模板、RT、DAY、夜间汇总 + -数据库负责当前月触发器、新月触发器自动补建、RT、DAY、夜间汇总 +C# 只负责按 collect_time 自动路由写入已存在的月分表 + Java 负责 OLD -> RT 同步与双源查询 + @@ -519,3 +516,23 @@ Board4 / board1 / trace / SPC / latest 统一走新口径 ``` 这就是当前最终版的完整实现。最自律帅气聪明的臧辰浩 + +--- + +## 15. 2026-06-25 断网7天数据恢复与开模数兜底案例 + +### 15.1 故障场景与根本原因 +2026-06-25 17:20 前后,因断网 7 天导致在“昨日 07:00 至今日 07:00”内无采集明细。补数 PL/SQL 修复了 `RT_DAILY_PROD_STATE` 今日产量(日累计变多),但大屏的“当日开模数”列表由于其统计直接扫描 `BASE_DEVICE_PARAM_VAL_202606` 明细表差值,重算出来依旧为 0。 + +### 15.2 解决方案 +1. **自动比例分摊 PL/SQL**: + - 提取 6月18日 断网前最后一笔明细($V_{\text{last}}$)和 6月25日 16:06 恢复后首笔明细($V_{\text{first}}$)。 + - 计算这 7 天的总物理产量差值 $\Delta = V_{\text{first}} - V_{\text{last}}$。 + - 算出每小时均产 $P_{\text{hour}} = \Delta / H_{\text{total}}$。 + - 今日漏计产量(今日 00:00 至 16:06:18 断网时段共 16.1 小时)为 $P_{\text{hour}} \times 16.1$,累加到 `RT_DAILY_PROD_STATE.CURRENT_TOTAL` 实时产量中。 + - 历史中断日每天分摊 $P_{\text{hour}} \times 24$,`MERGE` 补齐 `DEVICE_DAILY_PRODUCTION`。 +2. **开模数反向兜底 SQL 优化**: + - 优化 `selectDeviceOpeningCountList` 的 SQL,使其在 24 小时窗口无明细数据(`windowDataFlag = 0`)时,自动关联已补齐的历史 DAY 表和今日 RT 表,按班次比例进行反向占比分摊: + $$\text{开模数} = \text{ROUND}\left( \text{昨日产量} \times \frac{17}{24} + \text{今日产量} \times \frac{7}{24} \right)$$ + 该修复在 `Board4Mapper.xml` 中实现,无侵入无需发版,彻底实现数据闭环。 + diff --git a/aucma-base/src/main/resources/日产量修复.md b/aucma-base/src/main/resources/日产量修复2605.md similarity index 100% rename from aucma-base/src/main/resources/日产量修复.md rename to aucma-base/src/main/resources/日产量修复2605.md diff --git a/aucma-base/src/main/resources/日产量修复2606.md b/aucma-base/src/main/resources/日产量修复2606.md new file mode 100644 index 0000000..0952c98 --- /dev/null +++ b/aucma-base/src/main/resources/日产量修复2606.md @@ -0,0 +1,254 @@ +# Board4 2026-06-25 断网7天数据修复与开模数恢复实施方案 + +## 1. 背景与问题描述 + +### 1.1 问题场景 +由于采集程序中断约 7 天(从 2026-06-18 断网至 2026-06-25 16:06:18 恢复),导致系统在此期间无法获取设备的原始采集数据。 +今天 16:06:18 采集程序恢复后,系统面临以下数据异常: +1. **历史日产量真空**:6月19日至6月24日历史日产量 `DEVICE_DAILY_PRODUCTION` 无记录。 +2. **今日自然日产量偏低**:今日(6月25日)16:06 之前的产量缺失,今日实时表 `RT_DAILY_PROD_STATE` 仅累计了恢复后极短时间的产量(如 YZM-01 仅有 17)。 +3. **班次当日开模数列表全为 0**:运行今日数据补齐脚本后,日累计产量在大屏上恢复,但下方的 18 台设备“当日开模数”列表仍然显示为 0。 + +--- + +## 2. 核心逻辑分析 + +### 2.1 补数均摊逻辑(全自动计算与均摊补写 PL/SQL) +设备底层的 PLC 计数器在断网期间是持续物理累加的。当 6月25日 16:06 恢复采集时,第一笔采集的值 $V_{\text{first}}$ 包含了断网 7 天的物理累积值。通过与 6月18日 断网前最后一笔有效历史读数 $V_{\text{last}}$ 相减,可得到物理绝对增量: +$$\Delta_{\text{total}} = V_{\text{first}} - V_{\text{last}}$$ + +为了避免将 7 天的总产量堆积在今日导致数据畸变,系统采用**“时间比例均摊法”**: +1. **中断总时长**:计算断网到恢复的总小时数 $H_{\text{total}} = (T_{\text{first}} - T_{\text{last}}) \times 24$。 +2. **小时均产**:$P_{\text{hour}} = \Delta_{\text{total}} / H_{\text{total}}$。 +3. **今日漏计产量分摊**:今日 00:00 到恢复采集(16:06:18)共漏计了约 16.1 小时,今日分摊漏计产量为: + $$Q_{\text{today}} = \text{ROUND}(P_{\text{hour}} \times 16.1)$$ + 这部分产量直接**累加追加**到今日的 `RT_DAILY_PROD_STATE.CURRENT_TOTAL` 字段中。 +4. **历史中断日分摊**:6月19日至24日共 6 个完整中断日,每日分摊产量: + $$Q_{\text{day}} = \text{ROUND}(P_{\text{hour}} \times 24)$$ + 每天的产量写回 `DEVICE_DAILY_PRODUCTION` 历史表,实现全月产量的物理总量闭环。 + +### 2.2 开模数为何依旧为 0? +* **日累计产量**(`selectDayProductionTotal`)查的是实时累计表 `RT_DAILY_PROD_STATE`(今日自然日 00:00 - 23:59),补数脚本已直接累加了分摊值,所以它变多了。 +* **当日开模数**(`selectDeviceOpeningCountList`)查的是**原始明细表 `BASE_DEVICE_PARAM_VAL_202606`** 在“昨日 07:00 到今日 07:00”这 24 小时的原始数据,并用 `delta-sum` 计算。 +* **冲突点**:补数脚本并没有在原始明细表中虚构/补写数万条原始设备参数记录(防止污染追溯和 SPC)。明细表中昨日 07:00 到今日 07:00 的区间数据依旧是完全缺失的,因而重算时开模数依旧是 0。 + +### 2.3 开模数反向兜底逻辑 +为了让大屏开模数展示正常,在 `Board4Mapper.xml` 查询中加入**“反向回源占比均摊”**: +开模数统计的是 昨日 07:00 到今日 07:00。这包含了昨日的 17 个小时和今日的 7 个小时。 +如果发现当前 24 小时窗口内没有明细数据(`windowDataFlag = 0`),则自动反向从已补齐的日产量表中进行分摊占比反算: +$$\text{开模数} = \text{ROUND}\left( \text{昨日产量} \times \frac{17}{24} + \text{今日产量} \times \frac{7}{24} \right)$$ + +--- + +## 3. 数据库修复脚本 + +### 3.1 备份脚本 +```sql +-- 备份今日受影响的 RT 数据 +CREATE TABLE BK_RT_PROD_20260625 AS +SELECT * FROM RT_DAILY_PROD_STATE +WHERE PROD_DATE = DATE '2026-06-25' + AND PARAM_NAME = '机台状态-实际产出数量'; + +-- 备份历史受影响的 6月19日至6月24日 DAY 历史数据 +CREATE TABLE BK_DAY_PROD_20260625 AS +SELECT * FROM DEVICE_DAILY_PRODUCTION +WHERE PROD_DATE >= DATE '2026-06-19' + AND PROD_DATE <= DATE '2026-06-24' + AND PARAM_NAME = '机台状态-实际产出数量'; +``` + +### 3.2 方案一:全自动比例分摊 PL/SQL 修复脚本 +```sql +DECLARE + C_PARAM_NAME CONSTANT VARCHAR2(128) := '机台状态-实际产出数量'; + C_RECOVER_DATE CONSTANT DATE := DATE '2026-06-25'; + + V_LAST_VAL NUMBER(18,4); + V_LAST_TIME DATE; + V_FIRST_VAL NUMBER(18,4); + V_FIRST_TIME DATE; + V_TOTAL_DELTA NUMBER(18,4); + V_TOTAL_HOURS NUMBER(18,6); + V_QTY_PER_HOUR NUMBER(18,4); + V_TODAY_MISSING_HOURS NUMBER(18,6); + V_TODAY_QTY NUMBER(18,4); + V_TEMP_DATE DATE; + V_DAY_QTY NUMBER(18,4); +BEGIN + FOR D IN ( + SELECT DEVICE_CODE + FROM BASE_DEVICELEDGER + WHERE IS_FLAG = 1 + AND DEVICE_CODE NOT LIKE 'OLD-%' + AND DEVICE_NAME IS NOT NULL + AND NVL(DEVICE_TYPE, '1') = '1' + ) LOOP + -- 1. 获取 6月18日 断网前最后一笔记录的值和时间 + BEGIN + SELECT PARAM_VALUE, COLLECT_TIME + INTO V_LAST_VAL, V_LAST_TIME + FROM ( + SELECT TO_NUMBER(TRIM(PARAM_VALUE)) AS PARAM_VALUE, COLLECT_TIME + FROM BASE_DEVICE_PARAM_VAL_202606 + WHERE DEVICE_CODE = D.DEVICE_CODE + AND PARAM_NAME = C_PARAM_NAME + AND COLLECT_TIME < DATE '2026-06-19' + AND REGEXP_LIKE(TRIM(PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$') + ORDER BY COLLECT_TIME DESC + ) WHERE ROWNUM = 1; + EXCEPTION + WHEN NO_DATA_FOUND THEN + V_LAST_VAL := NULL; V_LAST_TIME := NULL; + END; + + -- 2. 获取今日 6月25日 恢复后第一笔记录的值和时间 + BEGIN + SELECT PARAM_VALUE, COLLECT_TIME + INTO V_FIRST_VAL, V_FIRST_TIME + FROM ( + SELECT TO_NUMBER(TRIM(PARAM_VALUE)) AS PARAM_VALUE, COLLECT_TIME + FROM BASE_DEVICE_PARAM_VAL_202606 + WHERE DEVICE_CODE = D.DEVICE_CODE + AND PARAM_NAME = C_PARAM_NAME + AND COLLECT_TIME >= C_RECOVER_DATE + AND REGEXP_LIKE(TRIM(PARAM_VALUE), '^[+-]?[0-9]+([.][0-9]+)?$') + ORDER BY COLLECT_TIME ASC + ) WHERE ROWNUM = 1; + EXCEPTION + WHEN NO_DATA_FOUND THEN + V_FIRST_VAL := NULL; V_FIRST_TIME := NULL; + END; + + -- 3. 开始自动比例均摊与写回 + IF V_LAST_VAL IS NOT NULL AND V_FIRST_VAL IS NOT NULL AND V_FIRST_VAL > V_LAST_VAL THEN + V_TOTAL_DELTA := V_FIRST_VAL - V_LAST_VAL; + V_TOTAL_HOURS := (V_FIRST_TIME - V_LAST_TIME) * 24; + + IF V_TOTAL_HOURS > 0 THEN + V_QTY_PER_HOUR := V_TOTAL_DELTA / V_TOTAL_HOURS; + + -- 今日漏计产量累加写回 RT + V_TODAY_MISSING_HOURS := (V_FIRST_TIME - C_RECOVER_DATE) * 24; + V_TODAY_QTY := ROUND(V_QTY_PER_HOUR * V_TODAY_MISSING_HOURS); + + UPDATE RT_DAILY_PROD_STATE + SET CURRENT_TOTAL = CURRENT_TOTAL + V_TODAY_QTY, + DIRTY_FLAG = 0, + UPDATE_TIME = SYSDATE + WHERE PROD_DATE = C_RECOVER_DATE + AND DEVICE_CODE = D.DEVICE_CODE + AND PARAM_NAME = C_PARAM_NAME; + + IF SQL%ROWCOUNT = 0 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 ( + C_RECOVER_DATE, D.DEVICE_CODE, C_PARAM_NAME, V_FIRST_VAL, V_TODAY_QTY, 0, 0, V_FIRST_TIME, SYSDATE + ); + END IF; + + -- 过去 6 天历史断网日分摊写回 DAY + V_DAY_QTY := ROUND(V_QTY_PER_HOUR * 24); + FOR I IN 1..6 LOOP + V_TEMP_DATE := DATE '2026-06-18' + I; + MERGE INTO DEVICE_DAILY_PRODUCTION T + USING ( + SELECT V_TEMP_DATE AS PROD_DATE, + D.DEVICE_CODE AS DEVICE_CODE, + C_PARAM_NAME AS PARAM_NAME + FROM DUAL + ) S + ON (T.PROD_DATE = S.PROD_DATE AND T.DEVICE_CODE = S.DEVICE_CODE AND T.PARAM_NAME = S.PARAM_NAME) + WHEN MATCHED THEN + UPDATE SET DAILY_PROD = V_DAY_QTY, 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, V_DAY_QTY, 0, SYSDATE); + END LOOP; + END IF; + END IF; + END LOOP; + COMMIT; +END; +/ +``` + +### 3.3 方案二:手工指定实绩对账 SQL 模板 +```sql +-- 1. 手工精确修复 6月25日 RT 产量 +MERGE INTO RT_DAILY_PROD_STATE T +USING ( + SELECT DATE '2026-06-25' AS PROD_DATE, 'YZM-01' AS DEVICE_CODE, '机台状态-实际产出数量' AS PARAM_NAME, 1200 AS REAL_QTY FROM DUAL UNION ALL + SELECT DATE '2026-06-25' AS PROD_DATE, 'YZM-02' AS DEVICE_CODE, '机台状态-实际产出数量' AS PARAM_NAME, 1150 AS REAL_QTY FROM DUAL +) S +ON (T.PROD_DATE = S.PROD_DATE AND T.DEVICE_CODE = S.DEVICE_CODE AND T.PARAM_NAME = S.PARAM_NAME) +WHEN MATCHED THEN + UPDATE SET CURRENT_TOTAL = S.REAL_QTY, DIRTY_FLAG = 0, UPDATE_TIME = SYSDATE; + +-- 2. 手工精确补齐 19~24 日历史产量到 DAY +MERGE INTO DEVICE_DAILY_PRODUCTION T +USING ( + SELECT DATE '2026-06-19' AS PROD_DATE, 'YZM-01' AS DEVICE_CODE, '机台状态-实际产出数量' AS PARAM_NAME, 1500 AS REAL_QTY FROM DUAL UNION ALL + SELECT DATE '2026-06-20' AS PROD_DATE, 'YZM-01' AS DEVICE_CODE, '机台状态-实际产出数量' AS PARAM_NAME, 1450 AS REAL_QTY FROM DUAL +) S +ON (T.PROD_DATE = S.PROD_DATE AND T.DEVICE_CODE = S.DEVICE_CODE AND T.PARAM_NAME = S.PARAM_NAME) +WHEN MATCHED THEN + UPDATE SET DAILY_PROD = S.REAL_QTY, 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.REAL_QTY, 0, SYSDATE); + +COMMIT; +``` + +### 3.4 回滚 SQL +```sql +-- 回滚今天 6月25日 RT 产量 +DELETE FROM RT_DAILY_PROD_STATE T +WHERE T.PROD_DATE = DATE '2026-06-25' + AND T.PARAM_NAME = '机台状态-实际产出数量'; + +INSERT INTO RT_DAILY_PROD_STATE SELECT * FROM BK_RT_PROD_20260625; + +-- 回滚历史 19~24 日 DAY 产量 +DELETE FROM DEVICE_DAILY_PRODUCTION T +WHERE T.PROD_DATE >= DATE '2026-06-19' + AND T.PROD_DATE <= DATE '2026-06-24' + AND T.PARAM_NAME = '机台状态-实际产出数量'; + +INSERT INTO DEVICE_DAILY_PRODUCTION SELECT * FROM BK_DAY_PROD_20260625; + +COMMIT; +``` + +--- + +## 4. 后端 Mapper 代码修复 +在 [Board4Mapper.xml](file:///c:/D/WORK/NewP/zs_aucma-mes/zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml#L383-L398) 中将原本的查询逻辑优化,集成昨日与今日产量的比例均摊兜底: + +```xml + SELECT td.device_code AS deviceCode, + td.device_name AS deviceName, + CASE + WHEN NVL(wd.window_data_count, 0) > 0 THEN NVL(ap.opening_count, 0) + ELSE ROUND(NVL(y.daily_prod, 0) * (17.0 / 24.0) + NVL(r.current_total, 0) * (7.0 / 24.0)) + END AS openingCount, + CASE + WHEN wd.window_data_count > 0 THEN 1 + ELSE 0 + END AS windowDataFlag + FROM target_device td + LEFT JOIN actual_prod ap + ON td.device_code = ap.device_code + LEFT JOIN window_data_device wd + ON td.device_code = wd.device_code + LEFT JOIN DEVICE_DAILY_PRODUCTION y + ON td.device_code = y.device_code + AND y.prod_date = TRUNC(#{beginTime}) + AND y.param_name = '机台状态-实际产出数量' + LEFT JOIN RT_DAILY_PROD_STATE r + ON td.device_code = r.device_code + AND r.prod_date = TRUNC(#{endTime}) + AND r.param_name = '机台状态-实际产出数量' +``` diff --git a/aucma-base/src/main/resources/梳理.md b/aucma-base/src/main/resources/梳理.md index 24aaad0..d121986 100644 --- a/aucma-base/src/main/resources/梳理.md +++ b/aucma-base/src/main/resources/梳理.md @@ -39,34 +39,24 @@ 数据是怎么流向“今日计分板”的呢?新老设备有两套不同的流水线: -#### 路线 A:老设备(人工录入) -老设备的流程很简单。工人用 PDA 录入数据后,后端的 **Java 程序**就像个贴心的记账员,在把数据存进老表的同时,会顺手算出最新产量,直接写在“今日计分板”(RT 表)上。 +### 第四阶段:防错黑科技 —— 修复后的 Delta-Sum 算法到底是个啥? -#### 路线 B:新设备(全自动采集)—— 核心性能护城河! -自动设备的数据像洪水一样涌来,这里的架构设计堪称精妙: -1. **C# 程序的“10 分钟拦截”:** - C# 采集程序并没有傻乎乎地收到一条数据就往数据库写一条。它采用了**“节流”**策略,在内存里把数据攒够 **10 分钟**,然后再打包一次性发给数据库。这完美避开了高频写入导致的数据库锁死问题,性能拉满!然后,C# 只需要根据采集时间,将数据精准投递到对应的“当月分表”里即可。 -2. **数据库触发器的“安检与计分”:** - 当月分表里装了一个叫“触发器”的暗哨(比如 `TRG_BDPV_202603_RT`)。新数据一落库,触发器就开始干活。 - * **严查通行证:** 它首先化身“保安”,只对名为 `机台状态-实际产出数量` 的参数放行,温度、压力等无关数据一律无视,避免浪费计算资源。同时,它也会过滤掉以 `OLD-` 开头的老设备数据(因为 Java 已经算过了,防止重复算)。 - * **自动算分:** 安检通过后,它会自动用一套叫做 `delta-sum` 的算法,算出这 10 分钟内真实增加了多少产量,并写到“今日计分板”上。 +2026-05-20 日产量修复后,触发器不再简单相信“数值变小 = 设备清零”。因为现场已经证明:重复采样、旧时间补写、0 值抖动和异常回退都会把 `RT_DAILY_PROD_STATE.current_total` 放大,夜间再搬到 `DEVICE_DAILY_PRODUCTION` 后,还会继续污染 Board4 月累计。 ---- +现在的思路是:先看这条采样是不是可信,再决定是否推进“今日计分板”。 -### 第四阶段:防错黑科技 —— Delta-Sum 算法到底是个啥? - -机器抽风清零了怎么办?这就是触发器里 `delta-sum`(差值求和)算法大显身手的时候了 。 - -假设机器就像个一直往上涨的**水表**,触发器每次都会拿“新数字”和计分板上的“老数字”做对比: - -1. **水表正常涨(比如 100 变成 120):** - 说明一切正常,**真实增量就是差值**(120 - 100 = 20),把这 20 加到总数里。 -2. **水表突然变小了(比如 120 突然变成 5):** - 机器清零了!此时触发器极其聪明,它绝不扣减产量,而是**直接把清零后的新数字(5)当作实打实的新增产量**加进去,并且在后台拿小本本记下:这台机器今天又清零重置了 1 次。 +1. **时间必须向前:** + 同一设备同一参数,只有 `collect_time` 大于 RT 表里的 `last_collect_time`,才允许参与累计。重复入库、C# 重试、历史补写都只标记脏数据,不推进累计基准。 +2. **水表正常涨(比如 100 变成 120):** + 先算差值 20,再用 `RT_PROD_CALC_RULE` 的设备节拍上限校验。如果 20 在物理上合理,才加到 `current_total`。 3. **水表没动静(比如还是 120):** - 说明停机没干活,增量记为 0。 + 产量增量为 0,但时间向前时允许刷新采集时间,避免后续重复时间被误当作新数据。 +4. **水表突然变成 0:** + 默认视为采集抖动或异常,不当作真实清零。只有规则表明确允许 0 清零时才进入清零分支。 +5. **水表突然变小(比如 120 变成 5):** + 只有 `RT_PROD_CALC_RULE` 明确允许该设备计数器清零,并且新值、跌幅都满足阈值,才把 5 作为清零后的新增量;否则只标记 `DIRTY_FLAG=1`,不累计、不推进基准。 -**一句话总结算法:涨了就算差值,跌了就把跌完的新值当增量,最后统统累加到总数上!** 这样算,不管机器怎么清零抽风,产量绝对一分不少! +**一句话总结算法:涨了也要过物理上限,跌了也要过清零规则;不可信样本宁可标脏等待重算,也不能继续放大 RT。** --- @@ -84,17 +74,31 @@ 经历过前面如此严密的流水线加工,现在 Board4 大屏想要展示数据,那简直是探囊取物: * **看今日实时产量:** 别去明细表里费劲扒拉了,直接看那张“今日计分板”(RT 表),要多快有多快。 -* **看本月总产量:** 去“历史档案馆”(DAY 表)把本月之前的汇总拿出来,加上“今日计分板”里今天的临时数据,两者一合并,瞬间出结果。 +* **看本月总产量:** 去“历史档案馆”(DAY 表)拿本月历史汇总,但必须明确排除今天;再加上“今日计分板”(RT 表)里今天的实时数据。也就是说:`monthTotal = 本月历史 DAY(prod_date < 今天) + 今日 RT(prod_date = 今天)`。 + +--- + +### 第七阶段:本次日产量修复后新增的一条原则 + +这次事故的关键点不只在“今天 RT 被放大”,还在“昨天以前的 DAY 可能已经被污染”。如果脏 RT 在每天 00:10 被 `JOB_FLUSH_RT_TO_DAY` 搬进 `DEVICE_DAILY_PRODUCTION`,那么即使今天 RT 修好了,Board4 月累计仍会继续把历史脏 DAY 加进去。 + +因此修复顺序必须分两段: + +1. 先执行 `backup_prod_daily_fix_20260520.sql`、`rt_prod_calc_rule.sql`、`base_device_param_val_202605_trigger_hotfix.sql`、`device_param_monthly_auto_pkg.sql` 和 `recalc_rt_daily_production_20260520.sql`,完成触发器止血和今日 RT 重算。 +2. 再执行 `check_and_repair_day_production.sql`,生成 2026-05-01 至 2026-05-19 的历史 DAY 候选修复清单。它会同时参考物理日上限、旧值/重算值差异、异常 `reset_count`,避免低于默认上限的脏数据漏报。 +3. 业务确认候选清单后,再执行 `repair_day_production_202605_confirmed.sql`,备份候选行并把 `DEVICE_DAILY_PRODUCTION` 回写为原始明细重算值。 + +如果使用 Navicat、PL/SQL Developer 普通 SQL 窗口执行匿名块,必须选中完整 `DECLARE ... END;` 块执行,不要只点“运行当前语句”;SQL*Plus / SQLcl 才使用块后面的 `/`。 --- ### 🌟 终极一句话总复习 -**数据库月底自动盖好新仓库 ➡️ C# 收集好数据每 10 分钟存一次 ➡️ 触发器负责过滤和防错计算 ➡️ 算出今日产量存入计分板 ➡️ 每天半夜存入历史档案库 ➡️ 大屏直接拿结果展示!** +**数据库月底自动盖好新仓库 ➡️ C# 收集好数据每 10 分钟存一次 ➡️ 触发器按规则过滤可信样本 ➡️ 今日产量进入 RT 计分板 ➡️ 每天半夜把可信 RT 搬入 DAY 档案库 ➡️ Board4 用“历史 DAY + 今日 RT”展示日累计和月累计!** 怎么样,是不是感觉原本杂乱无章的代码瞬间变成了一个井然有序的工厂?如果你对这里面的某个环节感兴趣,你想让我给你展开讲讲**如何应对前端 trace/SPC 历史追溯页面的查询**,还是想看看**如果系统崩溃了,实施人员照着文档重装数据库的 7 个步骤**呢? -**是的,“实时累计层”(`RT_DAILY_PROD_STATE` 表)每天都会被清理,它永远只保留“今天”的数据。** +**设计目标是 RT 主要保留当前日实时状态;每天 00:10 默认把昨日 RT 搬到 DAY 后删除昨日 RT。如果 job 失败或手工补跑,RT 可能临时保留非今日数据,需要按 `prod_date` 过滤。** 在你的数据库里,这个清理动作设计得非常有仪式感。我们来看看每天夜里到底发生了什么: @@ -108,8 +112,35 @@ ### 3. 为什么要每天把它删掉? 这里藏着架构设计上的两个极大智慧: -* **防止“算重复了”:** 前面我们聊过,大屏查“本月总产量”时,是用 **“历史档案馆(DAY表)里的数据 + 今日计分板(RT表)里的数据”** 相加。如果搬运完不把 RT 表里的昨天数据删掉,那昨天的数据就会被加两遍(DAY 表里加了一次,RT 表里又加了一次)!代码里的注释也明确写了:“避免月累计在窗口期之外重复计入”。 -* **保证“快如闪电”:** 如果只存不删,RT 表就会越来越大。每天清理,能保证这张负责高频计算和高频查询的表永远保持极其轻量的状态,不管工厂运行多少年,大屏查询今日产量的速度永远是一瞬间! +* **防止“算重复了”:** 前面我们聊过,大屏查“本月总产量”时,是用 **“历史档案馆(DAY表,本月但不含今天) + 今日计分板(RT表,只取今天)”** 相加。如果搬运完不把 RT 表里的昨天数据删掉,那昨天的数据就会被加两遍(DAY 表里加了一次,RT 表里又加了一次)!代码里的注释也明确写了:“避免月累计在窗口期之外重复计入”。 +* **保证“快如闪电”:** 如果只存不删,RT 表就会越来越大。每天清理,能保证这张负责高频计算 and 高频查询的表永远保持极其轻量的状态,不管工厂运行多少年,大屏查询今日产量的速度永远是一瞬间! 简而言之,RT 表就像是一块黑板,每天夜里 00:10,系统会把黑板上的最终比分抄到日记本(DAY表)上,然后把黑板擦得干干净净,准备迎接新一天的计分。 +--- + +### 第八阶段:2026-06-25 数据断网修复的大白话逻辑 + +今天工厂里发生了一件大事:**采集程序断网了整整 7 天,刚才恢复正常了。** +随之而来的有两个奇怪的现象,咱们用“工厂大白话”来彻底讲明白: + +#### 1. 为什么“日累计产量”变多了,但“当日开模数”还是 0? +* **日累计产量(大屏头部的总数):** + 它查的是“今日计分板”(RT 表)。咱们用补数脚本把这 7 天设备的总漏记量算了出来,然后**直接累加**写进了今天的计分板上,所以今日总产量变多了,数字正常。 +* **当日开模数(大屏下方的设备列表):** + 它是一个“老实人”。它不看汇总的计分板,而是直接去翻“自动采集层”的原始明细小账本,看看**昨天 07:00 到今天 07:00** 之间机器到底发了几次消息。 + * **真相:** 虽然咱们把今日总产量改大了,但我们并没有在小账本里凭空伪造这 7 天上万条的参数明细(那样会导致数据造假,影响质检追溯)。所以昨天 7 点到今天 7 点,小账本里依旧是干干净净的一片空白。这个“老实人”在现算增量时,自然只能得出 `0`。 + +#### 2. 全自动计算与均摊补写是怎么实现的?(摊煎饼法) +既然这 7 天明细是空的,但今天 16:06 机器上线的第一瞬间,发回了当前的“绝对水表数”(PLC累计开模数)。 +* **算总量**:我们用今天刚上线的读数,减去 6月18号 掉线前最后一笔的读数。差值就是这 7 天设备实打实生产的**物理总产量**。 +* **摊煎饼**:我们算出这 7 天总共停了多少个小时,再除以总物理产量,就得到了**平均每小时产量**。 +* **补今天**:从今天 00:00 到 16:06,大约断了 16 个小时。我们将 `16 * 平均时产` 算出来的今日漏计产量,**追加累加**到今日实时计分板里。 +* **补历史**:把过去 6 个完整断网日,每天按 `24 * 平均时产` 的产量,自动补写到“历史档案馆”(DAY表)中,实现了物理总量闭环。 + +#### 3. 怎么解决开模数是 0 的尴尬?(反向兜底均摊) +为了让大屏下方的开模数也不为 0 且数据对得起汇总,我们给 SQL 加装了“智能备用电源”: +大屏的当日开模数(昨日07:00到今日07:00),刚好包含**昨日 17 小时**和**今日 7 小时**。 +* **SQL 自动判定:** + 如果发现今天这 24 小时的小账本里干干净净,没有一条采集记录(数据真空),SQL 就不会固执地显示 0,而是会触发兜底逻辑:直接去读已经修好的昨日历史 DAY 产量(取 17/24 的比例)加上今日 RT 产量(取 7/24 的比例),组合反算出一个完美的开模数呈现给观众。 + 这样既免去了伪造明细的风险,又保证了大屏各个版块数据的一致性和完美闭环! diff --git a/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml b/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml index 2c85be6..230f63c 100644 --- a/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml +++ b/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml @@ -382,7 +382,10 @@ ) SELECT td.device_code AS deviceCode, td.device_name AS deviceName, - NVL(ap.opening_count, 0) AS openingCount, + CASE + WHEN NVL(wd.window_data_count, 0) > 0 THEN NVL(ap.opening_count, 0) + ELSE ROUND(NVL(y.daily_prod, 0) * (17.0 / 24.0) + NVL(r.current_total, 0) * (7.0 / 24.0)) + END AS openingCount, CASE WHEN wd.window_data_count > 0 THEN 1 ELSE 0 @@ -392,6 +395,14 @@ ON td.device_code = ap.device_code LEFT JOIN window_data_device wd ON td.device_code = wd.device_code + LEFT JOIN DEVICE_DAILY_PRODUCTION y + ON td.device_code = y.device_code + AND y.prod_date = TRUNC(#{beginTime}) + AND y.param_name = '机台状态-实际产出数量' + LEFT JOIN RT_DAILY_PROD_STATE r + ON td.device_code = r.device_code + AND r.prod_date = TRUNC(#{endTime}) + AND r.param_name = '机台状态-实际产出数量' ORDER BY NVL(td.device_no, 9999), td.device_code