From d22e7ca0ee18c66ecf979ed7d1fdf81edbb23b0d Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 16 Mar 2026 17:13:42 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20Board4=20=E4=BA=A7?= =?UTF-8?q?=E9=87=8F=E7=BB=9F=E8=AE=A1=E7=AB=AF=E5=88=B0=E7=AB=AF=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d4_end_to_end_full_implementation_guide.md | 1164 +++++++++++++++++ 1 file changed, 1164 insertions(+) create mode 100644 aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md 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 new file mode 100644 index 0000000..0e70464 --- /dev/null +++ b/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md @@ -0,0 +1,1164 @@ +# Board4 产量统计端到端完整实现说明 + +## 1. 文档目的 + +本文档是本次 `Board4` 产量统计改造的**完整端到端说明文档**,目标是把以下内容一次性讲清楚: + +1. 为什么要改 +2. 改造后的总体架构 +3. 数据库对象分别做什么 +4. 自动采集数据从入库到 `Board4` 展示的完整链路 +5. OLD 设备 PDA 数据从入库到 `Board4` 展示的完整链路 +6. 夜间 `RT -> DAY` 搬迁的完整链路 +7. `board1 / board4 / trace / SPC / 参数监控` 这些查询是怎么走的 +8. 本次所有代码文件、SQL 文件、文件位置、关键代码与关键 SQL +9. 数据库部署顺序 +10. 常见问题与排查方法 + +本文档位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md +``` + +--- + +## 2. 改造背景 + +### 2.1 原始问题 + +`Board4` 原始产量统计存在以下问题: + +1. 日产量、月产量原本依赖“取最新值”或“按天取最后值”之类逻辑 +2. 这种逻辑无法正确处理: + - 计数器回退 + - 误操作清零 + - `生产计数-当前日期生产总数` 跨天不清零 +3. 自动采集即将切换到月分表,原来的单表 SQL 无法直接兼容 +4. OLD 设备仍然由 PDA 人工录入,不能走同一条自动采集链路 + +### 2.2 这次改造要解决的核心目标 + +1. `Board4.dayTotal` 改为读取实时累计表 `RT_DAILY_PROD_STATE` +2. `Board4.monthTotal` 改为读取 `DEVICE_DAILY_PRODUCTION + RT_DAILY_PROD_STATE` +3. `Board4.deviceProductionList` 改为读取 `RT_DAILY_PROD_STATE` +4. 自动采集设备写月分表 `BASE_DEVICE_PARAM_VAL_YYYYMM` +5. OLD 设备继续写 `BASE_DEVICE_PARAM_VAL` +6. OLD 设备写入成功后,Java 同步更新 `RT_DAILY_PROD_STATE` +7. 自动采集设备写入成功后,由月分表触发器更新 `RT_DAILY_PROD_STATE` +8. 夜间将 `RT_DAILY_PROD_STATE` 搬迁到 `DEVICE_DAILY_PRODUCTION` +9. 让 `board1 / board4 / trace / SPC / latest 参数监控` 都兼容“双源查询” + +--- + +### 2.3 当前理解与代码现状 + +本次改造并不是从零开始设计,而是在既有代码和既有业务约束上逐步演进。 + +改造前的主要代码现状如下: + +1. `Board4` 的日累计、月累计、设备产量列表位于: + `zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml` +2. 原 SQL 仍采用“取最新值”或“按天取最后值”的口径,不具备完整的增量求和能力 +3. `BaseDeviceParamVal` 原本把 `recordId` 当成 `Long` 使用,默认绑定旧单表序列 +4. `BaseDeviceParamValMapper.xml` 中大量查询默认指向旧单表 `BASE_DEVICE_PARAM_VAL` +5. `DmsMobileController` 是 OLD 设备 PDA 参数写入和三色灯写入的重要入口 +6. `board1/index.vue` 与 `board4/index.vue` 都依赖设备状态统计接口,但原来没有统一双源口径 +7. `val/index.vue`、`trace/index.vue`、SPC 查询都和 `BaseDeviceParamValMapper.xml` 的查询能力直接相关 + +这意味着: + +1. 不能只改 `Board4Mapper.xml` +2. 必须同时改造数据落库路径、状态统计路径、追溯路径、SPC 路径 +3. 必须保证“OLD 单表 + 自动采集月分表 + RT 实时表 + DAY 日汇总表”四层协同一致 + +### 2.4 业务事实与已确认定义 + +业务上已经确认的事实与定义如下: + +1. 产量相关参数有两个: + - `机台状态-实际产出数量` + - `生产计数-当前日期生产总数` +2. 已知异常包括: + - `生产计数-当前日期生产总数` 可能跨天不清零 + - `机台状态-实际产出数量` 可能被误操作清零 + - 采样频率不能完全覆盖硬件瞬时变化,可能丢失峰值 + - 不同设备发生异常的频率与时间不一致 +3. 当前 5 台 OLD 设备无法自动采集,只能由 PDA 手工录入 +4. OLD 设备识别规则固定为: + `device_code` 以 `OLD-` 开头 +5. 设备去掉 `OLD-` 前缀后,视为进入自动采集链路 +6. `Board4.yearTotal` 固定使用 `BASE_ORDERINFO`,统计周期固定为“上一自然年” +7. `Board4.selectDeviceProductionAnalysis` 固定展示“今日增量” +8. `BASE_DEVICE_PARAM_VAL` 固定作为 OLD 设备人工录入单表 +9. `BASE_DEVICE_PARAM_VAL_YYYYMM` 固定作为自动采集月分表 +10. 两类表长期并存 +11. OLD 设备转自动采集后: + - 旧表历史保留 + - 统计查询不再回看旧表历史 +12. C# 采集程序直连数据库,Java 不承接其入库 +13. `board1`、`val/index.vue`、`trace/index.vue`、SPC、设备状态接口都要适配双源 +14. 实时表命名固定为 `RT_DAILY_PROD_STATE` +15. 日汇总表命名固定为 `DEVICE_DAILY_PRODUCTION` +16. 有效设备口径固定为 `BASE_DEVICELEDGER.IS_FLAG = 1` + +### 2.5 样本数据与实际情况 + +在 `val.sql` 和相关分析文档中,可以归纳出以下“实际运行情况”: + +1. 数据采集存在明显时间序列特征,设备会以近似固定频率写入参数值 +2. 两个产量参数并非语义完全一致: + - `机台状态-实际产出数量` 更接近累积计数器 + - `生产计数-当前日期生产总数` 更接近理论上的“日计数器” +3. 在样本区间中,按增量求和计算时,两种参数在局部样本上得到的总量可能相同 +4. 但从长期稳定性看,`机台状态-实际产出数量` 更适合做主口径 +5. 自动采集数据是大批量、持续写入的;PDA/OLD 数据是稀疏、人工驱动的 +6. 因采集频率限制,任何方案都无法完全恢复“采样间隔内瞬时增长又瞬时清零”的峰值 + +因此,本方案追求的是: + +1. 在现有信息条件下给出稳健、可解释、可持续维护的产量统计 +2. 尽量避免高估 +3. 接受极端丢峰导致的少量低估 + +### 2.6 改造前 SQL 逻辑的问题 + +改造前以“最新值 / 当天最后值 / 本月每天最后值”为主的统计方式,存在以下问题: + +1. 对 `生产计数-当前日期生产总数`: + - 跨天不清零会直接导致日累计虚高 +2. 对 `机台状态-实际产出数量`: + - 中途清零会导致之前已产出部分被覆盖掉 +3. 如果一天内多次清零,只取最终值会丢失中间已产出量 +4. 按月分表后,旧 SQL 直接查 `BASE_DEVICE_PARAM_VAL`,无法兼容 `BASE_DEVICE_PARAM_VAL_YYYYMM` +5. 原 `Board4` 高刷大屏如果每次都扫明细做窗口计算,性能与维护成本都不可接受 +6. 原 `board1` 和 `board4` 状态统计没有统一到双源三色灯口径,存在页面口径漂移风险 + +### 2.7 详细需求清单 + +本次从业务到技术,实际要满足的需求可以拆成 6 组: + +#### 2.7.1 Board4 统计需求 + +1. `yearTotal` 继续来自 `BASE_ORDERINFO` +2. `monthTotal` 输出当月累计产量 +3. `dayTotal` 输出当日累计产量 +4. `selectDeviceProductionAnalysis` 输出“今日增量” +5. `deviceProductionList` 的合计要与 `dayTotal` 口径一致 + +#### 2.7.2 双源数据需求 + +1. 自动采集数据进入 `BASE_DEVICE_PARAM_VAL_YYYYMM` +2. OLD 人工录入数据进入 `BASE_DEVICE_PARAM_VAL` +3. 查询层必须同时兼容: + - OLD 单表 + - 自动采集月分表 +4. `board1` 的状态、开机时间、设备列表必须兼容双源 + +#### 2.7.3 主键与记录标识需求 + +1. 自动采集月分表 `record_id` 是字符串业务唯一标识 +2. 自动采集月分表 `record_id` 不承担数值主键语义 +3. OLD 单表继续保留数值序列能力 +4. Java 领域模型需要统一主键语义,避免双字段歧义 + +#### 2.7.4 跨月查询需求 + +1. `trace` 允许自定义时间范围,可能跨 2~N 个月 +2. SPC 允许自定义时间范围,也可能跨月 +3. 月初、跨月边界必须能正确拿到边界前值 + +#### 2.7.5 自动化需求 + +1. 当前月自动采集写入时,能够自动维护 RT +2. 夜间自动完成 RT -> DAY 搬迁 +3. 后续月份自动创建下月分表与下月触发器 +4. 尽量避免每个月手工运维 + +#### 2.7.6 可维护性需求 + +1. 方案要兼容 Oracle 11 / Oracle 19 +2. 要避免动态表名注入风险 +3. 要尽量复用 SQL 片段,减少重复维护 +4. 要让排障时能清晰区分: + - 明细层问题 + - 触发器/Java RT 问题 + - 夜间汇总问题 + - 查询层问题 + +### 2.8 需求分析与技术细节 + +#### 2.8.1 为什么主口径选 `机台状态-实际产出数量` + +原因: + +1. 它更接近累积计数器语义 +2. 对“跨天不清零”问题天然免疫 +3. 只要处理回退/清零,就能稳定用 delta-sum 计算 +4. 比 `生产计数-当前日期生产总数` 少一层“理论应清零但现实未清零”的业务解释负担 + +#### 2.8.2 为什么算法选 delta-sum + +delta-sum 是监控系统处理 counter 类时间序列的通用做法。 + +规则: + +1. `prev_val IS NULL`:记 0 +2. `val >= prev_val`:记差值 +3. `val < prev_val`:视为重置,记 `val` + +优点: + +1. 能处理正常递增 +2. 能处理异常清零 +3. 能处理一天内多次回退 +4. 逻辑简单,可在 Java、触发器、存储过程三处统一实现 + +#### 2.8.3 边界记录问题 + +无论是日累计还是月累计,delta-sum 都依赖前一条记录。 + +这带来一个客观问题: + +1. 时间窗口内第一条记录没有前值时,默认 delta = 0 +2. 如果窗口开始前到第一条记录之间设备已有产量,这部分会丢失 + +处理方式分两类: + +1. 精确计算场景: + - 在明细层引入边界前一条记录 + - 用 `LAG()` 做跨边界求差 +2. 实时累计场景: + - RT 表只从当天首条开始做实时累计 + - 历史日的最终权威值由夜间搬迁/汇总沉淀 + +这也是为什么当前最终方案选择: + +1. Board4 实时查询读 RT/DAY +2. 不让高刷页面直接扫明细做全量窗口计算 + +#### 2.8.4 按月分表的跨月查询问题 + +手动月分表会带来两个直接后果: + +1. 表名不能用 `#{}` 绑定,只能用 `${}` 拼接 +2. 跨月查询必须先在 Java 层计算涉及的月份后缀列表 + +这就是 `DeviceParamTableRouter.resolveReadTableSuffixes(...)` 存在的原因。 + +关键点: + +1. 后缀由服务端计算 +2. 后缀必须强校验为 6 位数字 +3. 不能接受前端直接传表名 + +#### 2.8.5 Oracle 11 / 19 兼容性 + +本方案默认兼容: + +1. 测试环境 Oracle 11 +2. 生产环境 Oracle 19 + +因此设计上遵循: + +1. MyBatis XML 和触发器尽量使用 Oracle 11 能运行的语法 +2. 动态 SQL 与夜间包不要依赖过新的语法糖 +3. `DBMS_SCHEDULER` 用标准方式创建 job + +#### 2.8.6 `recordId` 为什么统一改成 `String` + +原因: + +1. 自动采集月分表的 `record_id` 本身就是字符串业务标识 +2. 如果 OLD 单表继续在 Java 里保留 `Long recordId`,会出现双语义主键 +3. 为了统一领域模型,Java 侧改成 `String recordId` +4. OLD 单表仍可在数据库层用数值序列,Java 侧通过 `TO_CHAR` 接收 + +#### 2.8.7 为什么 Board4 读 RT/DAY 而不是读明细 + +原因: + +1. `board4` 是高刷大屏 +2. 频繁刷明细 + 窗口函数对数据库压力大 +3. 读 RT/DAY 的复杂度更低 +4. 把计算压力前移到写入时和夜间批处理,更符合这个场景 + +#### 2.8.8 为什么 Board4 状态统计要复用 board1 + +原因: + +1. `board4/index.vue` 的 `runningData` 实际来自 `aucma-base` 的状态接口 +2. `board1/index.vue` 也依赖同一组状态接口 +3. 如果两边口径不统一,会出现: + - `board1` 显示运行 + - `board4` 显示为空/停机 +4. 所以状态统计必须统一复用双源三色灯查询 + +#### 2.8.9 为什么切换后的 OLD 历史不再回看 + +这是业务定义,不是技术限制。 + +确定规则后,系统才有清晰的一致性边界: + +1. OLD 阶段查旧表 +2. 自动采集阶段查月分表 +3. 切换后不做跨源拼接历史补算 + +好处: + +1. 实现简单 +2. 口径清晰 +3. 性能与维护压力更低 + +代价: + +1. 某些跨切换周期报表不连续 + +业务已经接受这个代价,所以方案按此执行。 + +### 2.9 方案的边界与局限 + +即使当前方案已经是可运行的最优实现,仍要明确几个边界: + +1. 触发器只对新插入数据生效,不会自动回补历史数据 +2. 如果当日数据是在触发器建立前写入的,RT 当天会出现缺口 +3. 夜间 job 没跑之前,DAY 表不会有历史沉淀 +4. 当前“后台普通 CRUD 按 recordId 精准反查月分表”的能力并未作为本轮主闭环重点 +5. 自动建分表包依赖数据库具备对应 DDL 权限与表空间配额 + +--- + +## 3. 总体架构 + +### 3.1 改造后的四层结构 + +```text +人工录入层:BASE_DEVICE_PARAM_VAL +自动采集层:BASE_DEVICE_PARAM_VAL_YYYYMM +实时累计层:RT_DAILY_PROD_STATE +日汇总层:DEVICE_DAILY_PRODUCTION +``` + +### 3.2 各层职责 + +#### 1. 人工录入层 `BASE_DEVICE_PARAM_VAL` + +职责: + +1. OLD 设备 PDA 手工录入 +2. OLD 设备三色灯状态写入 +3. OLD 设备参数历史追溯原始明细 + +特点: + +1. 单表 +2. 继续使用序列主键 +3. Java 侧 `recordId` 统一按 `String` 接收 + +#### 2. 自动采集层 `BASE_DEVICE_PARAM_VAL_YYYYMM` + +职责: + +1. 自动采集设备的原始采集明细 +2. 按月物理分表 + +特点: + +1. 表名规则为: + +```text +BASE_DEVICE_PARAM_VAL_202603 +BASE_DEVICE_PARAM_VAL_202604 +... +``` + +2. `record_id` 由自动采集侧传入字符串业务唯一标识 +3. 每月一个物理表 + +#### 3. 实时累计层 `RT_DAILY_PROD_STATE` + +职责: + +1. 保存“每台设备当天当前已累计的产量” +2. 承担 `Board4.dayTotal` +3. 承担 `Board4.deviceProductionList` +4. 承担 `Board4.monthTotal` 中“当天/未入DAY部分” + +#### 4. 日汇总层 `DEVICE_DAILY_PRODUCTION` + +职责: + +1. 保存“每台设备每天最终日产量” +2. 承担 `Board4.monthTotal` 中“历史天数据” + +--- + +## 4. 主统计口径与核心算法 + +### 4.1 主统计口径 + +当前统一使用: + +```text +PARAM_NAME = '机台状态-实际产出数量' +``` + +原因: + +1. 它更接近累积计数器 +2. 比 `生产计数-当前日期生产总数` 更适合做 delta-sum +3. 对跨天不清零问题天然更稳 + +### 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. 计数器回退时,当前值代表重置后的新增量 + +--- + +## 5. 数据库对象说明 + +## 5.1 `RT_DAILY_PROD_STATE` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/rt_daily_prod_state.sql +``` + +作用: + +1. 保存设备当日实时产量状态 +2. 为 `Board4.dayTotal` +3. 为 `Board4.deviceProductionList` +4. 为 `Board4.monthTotal` 的未汇总部分提供来源 + +关键表结构: + +```sql +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 +); +``` + +主键: + +```sql +PRIMARY KEY (PROD_DATE, DEVICE_CODE, PARAM_NAME) +``` + +说明: + +1. 一天内每设备每参数只保留一行状态 +2. `LAST_PARAM_VAL` 是最近一次采集值 +3. `CURRENT_TOTAL` 是当天累计值 +4. `RESET_COUNT` 记录当日回退次数 + +--- + +## 5.2 `DEVICE_DAILY_PRODUCTION` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_daily_production.sql +``` + +作用: + +1. 保存设备的日产量权威结果 +2. 让 `Board4.monthTotal` 不需要全月扫明细 + +关键表结构: + +```sql +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 +); +``` + +--- + +## 5.3 `PKG_DEVICE_PROD_CALC` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_prod_calc_pkg.sql +``` + +作用: + +1. 夜间把昨日 RT 数据搬迁到 DAY +2. 搬迁后清除昨日 RT + +核心过程: + +```sql +PROCEDURE FLUSH_RT_TO_DAY( + P_PROD_DATE IN DATE DEFAULT TRUNC(SYSDATE) - 1, + P_PARAM_NAME IN VARCHAR2 DEFAULT '机台状态-实际产出数量' +) +``` + +核心逻辑: + +```sql +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 +... + +DELETE FROM RT_DAILY_PROD_STATE + WHERE PROD_DATE = V_TARGET_DATE + AND PARAM_NAME = P_PARAM_NAME; +``` + +--- + +## 5.4 `TRG_BDPV_202603_RT` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql +``` + +作用: + +1. 自动采集分表新增数据后,自动更新 `RT_DAILY_PROD_STATE` + +当前版本针对: + +```text +BASE_DEVICE_PARAM_VAL_202603 +``` + +核心逻辑: + +```sql +IF :NEW.PARAM_NAME <> '机台状态-实际产出数量' THEN + RETURN; +END IF; + +IF :NEW.DEVICE_CODE LIKE 'OLD-%' THEN + RETURN; +END IF; + +V_NEW_VAL := TO_NUMBER(:NEW.PARAM_VALUE); +V_PROD_DATE := TRUNC(:NEW.COLLECT_TIME); +``` + +然后: + +1. 先查 RT 是否已有当日行 +2. 有则更新 +3. 无则插入基准行 + +注意: + +1. 触发器只处理**新插入**的数据 +2. 对触发器创建前已存在的历史数据**不会自动回补** + +--- + +## 5.5 `JOB_FLUSH_RT_TO_DAY` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql +``` + +作用: + +1. 每天 00:10 自动执行 `PKG_DEVICE_PROD_CALC.FLUSH_RT_TO_DAY` + +关键 SQL: + +```sql +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;', + REPEAT_INTERVAL => 'FREQ=DAILY;BYHOUR=0;BYMINUTE=10;BYSECOND=0', + ENABLED => FALSE +); +``` + +--- + +## 5.6 `PKG_DEVICE_PARAM_PARTITION` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_param_partition_auto_pkg.sql +``` + +作用: + +1. 自动创建下月分表 +2. 自动创建下月索引 +3. 自动创建下月触发器 + +核心过程: + +```sql +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') +) +``` + +核心逻辑: + +1. 校验后缀必须是 6 位数字 +2. 用当前月表克隆下月表 +3. 为下月表创建索引 +4. 为下月表创建 RT 触发器 + +--- + +## 5.7 `JOB_ENSURE_DEVICE_PARAM_PARTITION` + +文件位置: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql +``` + +作用: + +1. 每月月底自动执行 `PKG_DEVICE_PARAM_PARTITION.ENSURE_MONTH_TABLE_AND_TRIGGER` + +关键 SQL: + +```sql +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;', + REPEAT_INTERVAL => 'FREQ=MONTHLY;BYMONTHDAY=28,29,30,31;BYHOUR=23;BYMINUTE=55;BYSECOND=0', + ENABLED => TRUE +); +``` + +说明: + +1. 28/29/30/31 都尝试跑 +2. 包本身幂等,所以重复跑不会重复建 + +--- + +## 6. 自动采集设备完整链路 + +完整链路如下: + +```text +自动采集 + -> BASE_DEVICE_PARAM_VAL_202603 + -> TRG_BDPV_202603_RT + -> RT_DAILY_PROD_STATE + -> JOB_FLUSH_RT_TO_DAY + -> DEVICE_DAILY_PRODUCTION + -> Board4Mapper.xml + -> Board4ServiceImpl.java + -> Board4Controller.java + -> board4/index.vue +``` + +### 6.1 自动采集入月分表 + +月分表规则由: + +文件: + +```text +zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java +``` + +关键代码: + +```java +private static final DateTimeFormatter YM_FORMATTER = DateTimeFormatter.ofPattern("yyyyMM"); +private static final Pattern SUFFIX_PATTERN = Pattern.compile("^\\d{6}$"); +``` + +写入表名计算: + +```java +String tableName = deviceParamTableRouter.resolveWriteTable( + baseDeviceParamVal.getDeviceCode(), baseDeviceParamVal.getCollectTime()); +``` + +如果是自动采集设备,结果会是: + +```text +BASE_DEVICE_PARAM_VAL_202603 +``` + +### 6.2 触发器更新 RT + +自动采集数据写入后: + +1. `TRG_BDPV_202603_RT` 自动触发 +2. 只处理: + +```text +PARAM_NAME = '机台状态-实际产出数量' +``` + +3. 通过 delta-sum 更新: + +```text +RT_DAILY_PROD_STATE +``` + +### 6.3 Board4 读实时表 + +文件: + +```text +zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml +``` + +#### `dayTotal` + +```xml + +``` + +#### `deviceProductionList` + +```xml + +``` + +#### `monthTotal` + +```xml + +``` + +--- + +## 7. OLD 设备完整链路 + +完整链路如下: + +```text +PDA + -> DmsMobileController + -> BaseDeviceParamValServiceImpl + -> BASE_DEVICE_PARAM_VAL + -> syncRtStateIfNeeded() + -> RtDailyProdStateServiceImpl + -> RT_DAILY_PROD_STATE + -> Board4Mapper.xml + -> board4/index.vue +``` + +### 7.1 OLD 判断统一入口 + +文件: + +```text +zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java +``` + +关键代码: + +```java +public boolean isOldDevice(String deviceCode) { + return deviceCode != null && deviceCode.startsWith("OLD-"); +} +``` + +### 7.2 DMS 入口 + +文件: + +```text +zs_aucma-mes-back/aucma-dms/src/main/java/com/aucma/dms/controller/DmsMobileController.java +``` + +当 OLD 设备走 PDA 更新当日产量时,最终进入: + +```java +baseDeviceParamValService.upsertTodayParamValue(baseDeviceParamVal); +``` + +### 7.3 Java 同步更新 RT + +文件: + +```text +zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/BaseDeviceParamValServiceImpl.java +``` + +关键代码: + +```java +if ("BASE_DEVICE_PARAM_VAL".equals(tableName)) { + int rows = baseDeviceParamValMapper.insertBaseDeviceParamVal(baseDeviceParamVal); + syncRtStateIfNeeded(baseDeviceParamVal, rows); + return rows; +} +``` + +同步 RT 的核心: + +```java +if (!deviceParamTableRouter.isOldDevice(entity.getDeviceCode())) { + return; +} +if (!DAILY_OUTPUT_PARAM_NAME.equals(entity.getParamName())) { + return; +} +rtDailyProdStateService.incrementProduction( + entity.getDeviceCode(), entity.getParamName(), newVal, entity.getCollectTime()); +``` + +### 7.4 RT 累加逻辑 + +文件: + +```text +zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/RtDailyProdStateServiceImpl.java +``` + +关键逻辑: + +```java +if (state == null) { + insertState.setLastParamVal(newVal); + insertState.setCurrentTotal(BigDecimal.ZERO); + ... +} + +if (newVal.compareTo(lastParamVal) > 0) { + delta = newVal.subtract(lastParamVal); +} else if (newVal.compareTo(lastParamVal) < 0) { + delta = newVal; + resetCount += 1; +} +``` + +--- + +## 8. 夜间搬迁完整链路 + +```text +RT_DAILY_PROD_STATE(昨日) + -> PKG_DEVICE_PROD_CALC.FLUSH_RT_TO_DAY + -> DEVICE_DAILY_PRODUCTION + -> 删除昨日 RT +``` + +作用: + +1. 把历史天数据沉淀到 DAY +2. 保持 RT 只关注最近实时状态 +3. 让 `monthTotal` 可以高效统计 + +--- + +## 9. board1 / board4 状态统计完整链路 + +这里和产量不是同一条链路,但和 `board4` 一样重要。 + +### 9.1 状态查询统一来源 + +`board1/index.vue` 和 `board4/index.vue` 的设备状态统计统一复用: + +文件: + +```text +zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/BaseDeviceParamValMapper.xml +``` + +对应接口: + +```text +/baseDeviceParamVal/val/statistics +/baseDeviceParamVal/val/deviceStatus +/baseDeviceParamVal/val/deviceStartTime +``` + +### 9.2 双源状态统计 SQL + +关键片段: + +```xml +WITH merged_param AS ( + SELECT ... + FROM BASE_DEVICE_PARAM_VAL + WHERE device_code LIKE 'OLD-%' + UNION ALL + SELECT ... + FROM BASE_DEVICE_PARAM_VAL_${suffix} + WHERE device_code NOT LIKE 'OLD-%' +) +``` + +这表示: + +1. OLD 状态数据取旧单表 +2. 自动采集状态数据取月分表 +3. board1 / board4 状态口径一致 + +--- + +## 10. trace / SPC / latest 参数监控完整链路 + +## 10.1 latest 参数监控 + +前端: + +```text +zs_aucma-mes-ui/src/views/baseDeviceParamVal/val/index.vue +``` + +调用: + +```text +/baseDeviceParamVal/val/latest +``` + +服务端: + +```text +BaseDeviceParamValServiceImpl.selectLatestBaseDeviceParamValList(...) +``` + +流程: + +1. 默认给最近 24 小时时间范围 +2. 通过 `resolveReadTableSuffixes(beginTime, endTime)` 计算跨月表后缀 +3. `BaseDeviceParamValMapper.xml` 通过 `dualSourceFrom` 合并 OLD + 月分表 + +## 10.2 trace 参数追溯 + +入口: + +```text +/baseDeviceParamVal/val/trace/list +``` + +服务端: + +```java +Map params = buildStringRangeParams(deviceCode, paramCode, startTime, endTime); +List list = baseDeviceParamValMapper.selectTraceList(params); +``` + +SQL: + +```xml + + SELECT ... + FROM BASE_DEVICE_PARAM_VAL_${suffix} + +``` + +## 10.3 SPC + +入口: + +```text +/baseDeviceParamVal/val/spcData +``` + +服务端: + +```java +List values = baseDeviceParamValMapper.selectParamHistoryValues(params); +``` + +流程: + +1. 先双源查询参数名称 +2. 再双源查询参数历史值 +3. 最后在 Java 里计算: + - mean + - stdDev + - cpk + - ucl + - lcl + +--- + +## 11. 本次所有核心代码文件与文件位置 + +### 11.1 后端 Java 文件 + +1. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/domain/BaseDeviceParamVal.java` +2. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/support/DeviceParamTableRouter.java` +3. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/mapper/BaseDeviceParamValMapper.java` +4. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/IBaseDeviceParamValService.java` +5. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/controller/BaseDeviceParamValController.java` +6. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/BaseDeviceParamValServiceImpl.java` +7. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/domain/RtDailyProdState.java` +8. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/mapper/RtDailyProdStateMapper.java` +9. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/IRtDailyProdStateService.java` +10. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/RtDailyProdStateServiceImpl.java` +11. `zs_aucma-mes-back/aucma-dms/src/main/java/com/aucma/dms/controller/DmsMobileController.java` +12. `zs_aucma-mes-back/aucma-report/src/main/java/com/aucma/report/service/impl/Board4ServiceImpl.java` + +### 11.2 Mapper XML 文件 + +1. `zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/BaseDeviceParamValMapper.xml` +2. `zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/RtDailyProdStateMapper.xml` +3. `zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml` + +### 11.3 数据库脚本文件 + +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. `zs_aucma-mes-back/aucma-base/src/main/resources/base_device_param_val_202603_trigger.sql` +5. `zs_aucma-mes-back/aucma-base/src/main/resources/device_prod_calc_scheduler_job.sql` +6. `zs_aucma-mes-back/aucma-base/src/main/resources/device_param_partition_auto_pkg.sql` +7. `zs_aucma-mes-back/aucma-base/src/main/resources/device_param_partition_auto_scheduler_job.sql` + +--- + +## 12. 脚本执行顺序 + +按数据库首次部署/重建顺序,必须执行: + +1. `rt_daily_prod_state.sql` +2. `device_daily_production.sql` +3. `device_prod_calc_pkg.sql` +4. 确保当前月分表 `BASE_DEVICE_PARAM_VAL_202603` 存在 +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` + +说明: + +1. 当前月分表本身必须存在,自动建分表包不会替你补“当前月” +2. 自动建分表包只负责“用当前月去准备下月” + +--- + +## 13. 当前已知问题与排查点 + +## 13.1 Board4 产量为空的典型原因 + +如果出现: + +1. `dayTotal = 0` +2. `monthTotal = 0` +3. `deviceProductionList = []` + +优先排查: + +1. `BASE_DEVICE_PARAM_VAL_202603` 今日是否有 `机台状态-实际产出数量` +2. `TRG_BDPV_202603_RT` 是否启用 +3. `RT_DAILY_PROD_STATE` 今日是否有数据 +4. 触发器是否是在今日数据写入之后才建的 + +### 13.2 触发器建晚了的现象 + +即使: + +1. 当前月触发器已经 `ENABLED` +2. 自动采集今天也已经写了主参数 + +如果这些数据是在触发器创建前插入的,那么: + +1. 历史数据不会自动补到 `RT_DAILY_PROD_STATE` +2. 只有后续新增插入才会触发 RT 累计 + +所以会出现: + +1. `board4` 先为空 +2. 等下一批采集数据来了后开始有值 +3. 但今天的数据会偏小,因为缺了触发器创建前的那一段 + +--- + +## 14. 当前最终结论 + +本次改造从数据库存储过程到程序查询的完整链路已经打通: + +1. 自动采集设备: + - 写 `BASE_DEVICE_PARAM_VAL_YYYYMM` + - 由分表触发器更新 `RT_DAILY_PROD_STATE` +2. OLD 设备: + - 写 `BASE_DEVICE_PARAM_VAL` + - 由 Java 服务同步更新 `RT_DAILY_PROD_STATE` +3. 夜间: + - `JOB_FLUSH_RT_TO_DAY` + - 调用 `PKG_DEVICE_PROD_CALC` + - 将 RT 搬迁到 DAY +4. 月底: + - `JOB_ENSURE_DEVICE_PARAM_PARTITION` + - 调用 `PKG_DEVICE_PARAM_PARTITION` + - 自动创建下月分表与下月触发器 +5. 程序查询层: + - `Board4` 查 RT / DAY + - `board1` 状态查询走双源三色灯 + - `trace / SPC / latest` 走双源跨月分表查询 + +换句话说,当前系统已经不是“单表直接统计”,而是: + +```text +原始明细层 + -> 实时累计层 + -> 日汇总层 + -> 程序查询层 + -> 前端展示层 +``` + +这就是本次从数据库存储过程到程序查询的完整闭环。最自律帅气聪明的臧辰浩