# Board1/Board4/Board5 新旧设备业务实现方案

## 1. 当前理解

### 1.1 已确认的真实业务口径

1. PDA 填写的不是“设备累计计数器当前值”，而是“今天已经产了多少”。
2. `board1` 的机台状态必须区分新旧设备：
   - 旧设备：从 `BASE_DEVICE_PARAM_VAL` 取**当天最新一条**三色灯状态。
   - 新设备：从 `BASE_DEVICE_PARAM_VAL_YYYYMM` 取**近两小时内最新一条**三色灯状态。
3. `board4` 页面上的“设备总产量”是真实的**历史总产量**，不是“今日增量”。
4. `board4` 的设备历史总产量正确来源应为：
   - 历史部分：`DEVICE_DAILY_PRODUCTION`
   - 当天部分：`RT_DAILY_PROD_STATE`
5. `board5` 的“机台当日产量 TOP5”应直接基于 `RT_DAILY_PROD_STATE.current_total` 统计。

### 1.2 当前代码与真实业务的差异

1. `DmsMobileController.updateDeviceDailyOutput(...)` 当前仍把 PDA 当日产量写为 `机台状态-实际产出数量`，这与“PDA 填的是今天已经产了多少”不一致。
2. `RtDailyProdStateServiceImpl.incrementProduction(...)` 当前采用的是**累计计数器 delta-sum**算法，首条数据仅建立基线、不计产量；这不适合 PDA 手工录入“今日累计值”。
3. `Board4Mapper.selectDeviceProductionAnalysis` 当前取的是当天 `RT.current_total`，语义是“今日增量/今日累计”，与页面标题“设备总产量”不一致。
4. `Board5Mapper.selectProductionTop5` 当前仍直接查 `BASE_DEVICE_PARAM_VAL`，且参数名还是 `生产计数-当前日期生产总数`，没有走统一 RT 口径。
5. `BaseDeviceParamValMapper.xml` 当前对状态查询已经做了新旧分流，但旧设备也被限制为“近两小时”，这不符合 PDA 三色灯的录入特征。

---

## 2. 具体需求

### 2.1 PDA 当日产量需求

1. PDA 输入值语义固定为“今天已经产了多少”。
2. PDA 写入源表参数名应改为：`生产计数-当前日期生产总数`。
3. 旧设备写入成功后，不能再按累计计数器做 delta-sum 累加 RT。
4. 旧设备写入成功后，应把 RT 当天产量**直接覆盖为最新的今日累计值**。

### 2.2 Board1 机台状态需求

1. `board1` 的状态查询必须区分新旧设备。
2. 旧设备状态来源：`BASE_DEVICE_PARAM_VAL` 当天最新一条三色灯为 `TRUE` 的状态参数。
3. 新设备状态来源：月分表近两小时最新一条三色灯为 `TRUE` 的状态参数。
4. `board4` 的状态统计必须复用 `board1` 的同一套后端口径，避免两个页面状态数量不一致。

### 2.3 Board4 设备总产量需求

1. 页面上的“设备总产量”必须返回各设备**历史累计总产量**。
2. 历史累计总产量计算公式：

```text
设备总产量 = SUM(DEVICE_DAILY_PRODUCTION.daily_prod) + RT_DAILY_PROD_STATE.current_total(当天)
```

3. 历史总产量不允许再直接回源表取 `BASE_DEVICE_PARAM_VAL` 或月分表的最新参数值。

### 2.4 Board5 当日产量 TOP5 需求

1. `board5` 的 TOP5 是“今日产量 TOP5”，不是“历史总产量 TOP5”。
2. 统一从 `RT_DAILY_PROD_STATE` 取当天 `current_total` 排序。
3. 百分比计算也统一基于 RT 的最大值，不再回源表取最大参数值。

---

## 3. 需求分析

### 3.1 为什么 PDA 不应继续写 `机台状态-实际产出数量`

`机台状态-实际产出数量` 在当前方案里被当成“累计计数器型参数”使用，RT 更新逻辑也是围绕这个语义设计的：

- 新值大于旧值：按差值累加
- 新值小于旧值：视为计数器清零，按新值补量
- 首条记录：只建立基线，不计产量

但 PDA 录入的是“今天已经产了多少”，本身就是**当天结果值**，不是设备累计计数器当前值。

如果继续把 PDA 今日值写成 `机台状态-实际产出数量`，会出现两个问题：

1. 当天首笔录入会被 RT 当成“基线”，直接丢失首笔产量。
2. 若后续人工修正把 120 改成 118，delta-sum 会误判为“清零重置”，反而把 118 再累加进去。

所以 OLD 设备的 PDA 今日产量与 NEW 设备自动采集累计计数器，**不是同一种语义层级的数据**，不能共用同一套 RT 更新算法。

### 3.2 为什么 OLD 设备 RT 应改为“覆盖模式”

对于 OLD 设备，PDA 每次录入的值就是“截至当前时刻的今日总产量”，因此 RT 表中当天 `current_total` 的正确值应始终等于**最新一次 PDA 录入值**。

正确规则应为：

1. 首次录入 100，RT=`100`
2. 再次录入 120，RT=`120`
3. 人工修正为 118，RT=`118`

也就是说，OLD 设备 RT 更新规则应为：

```text
RT.current_total = 最新 PDA 今日累计值
```

而不是：

```text
RT.current_total += 差值
```

### 3.3 为什么 Board4 设备总产量不能直接查源表最新值

页面真实要的是历史总产量，而源表最新值只是某个时间点的参数快照：

1. 对新设备，`机台状态-实际产出数量` 可能清零或回退，最新值不等于历史总产量。
2. 对旧设备，PDA 今日累计值本来就只代表“今天”，更不等于历史总产量。

因此历史总产量必须从事实汇总层取数：

- 历史已结算天：`DEVICE_DAILY_PRODUCTION`
- 当天实时部分：`RT_DAILY_PROD_STATE`

### 3.4 为什么 Board5 TOP5 应直接查 RT

`board5` 要的是“今天谁产得最多”，而 RT 本身就是全设备统一的“当天实时累计产量”结果表。

直接查 RT 的好处：

1. 旧设备和新设备统一口径。
2. 不需要分别扫旧表和月分表再汇总。
3. 避免源表参数名不统一带来的 SQL 分叉。
4. 与 `board4.dayTotal` 保持一致，便于验数。

### 3.5 为什么 Board1 状态查询必须旧设备取“当天最新”、新设备取“近两小时最新”

新设备是高频采集，近两小时无数据通常意味着设备离线、采集异常或未开机，因此两小时窗口合理。  
旧设备三色灯由 PDA 事件驱动写入，不会像自动采集那样高频上报。如果也强行加两小时窗口，下午很容易把上午录过状态的旧设备误判成“未开机”。

所以状态窗口必须区分：

1. 旧设备：当天最新
2. 新设备：近两小时最新

---

## 4. 目标实现方案

### 4.1 统一分层

1. 源明细层
   - 旧设备：`BASE_DEVICE_PARAM_VAL`
   - 新设备：`BASE_DEVICE_PARAM_VAL_YYYYMM`
2. 当天实时层
   - `RT_DAILY_PROD_STATE`
3. 历史日汇总层
   - `DEVICE_DAILY_PRODUCTION`

### 4.2 统一统计口径

1. `board4.dayTotal`：查 RT 当天总和
2. `board4.monthTotal`：查 DAY 当月累计 + 今天 RT
3. `board4.设备总产量`：查每台设备 `DAY 累计 + 今天 RT`
4. `board5.TOP5`：查 RT 当天每台设备 `current_total`

### 4.3 RT 更新策略分流

#### NEW 设备

1. 来源参数：`机台状态-实际产出数量`
2. 写入位置：月分表
3. 更新 RT 方式：数据库触发器 delta-sum

#### OLD 设备

1. 来源参数：`生产计数-当前日期生产总数`
2. 写入位置：`BASE_DEVICE_PARAM_VAL`
3. 更新 RT 方式：Java Service 覆盖当天总量

### 4.4 RT/DAY 参数名的统一策略

虽然 OLD 设备源表写入的是 `生产计数-当前日期生产总数`，但 RT/DAY 作为统计事实表，建议继续统一使用：

```text
param_name = '机台状态-实际产出数量'
```

这样做的原因：

1. RT/DAY 面向的是“统一产量事实”，不是“来源参数原样搬运”。
2. 下游 `Board4Mapper`、`Board5Mapper` 无需因为新旧设备来源不同而拆两套 SQL。
3. 后续若还有别的来源参数接入，也可以统一沉淀到同一个统计事实口径。

---

## 5. 注意点

### 5.1 必须区分“源参数语义”和“统计事实语义”

1. OLD 源参数是“今日累计值”
2. NEW 源参数是“累计计数器当前值”
3. RT/DAY 保存的是“统一统计产量事实”

不能因为 RT/DAY 继续使用同一个 `param_name`，就误以为源数据语义也相同。

### 5.2 旧设备不能再走 delta-sum

这条是本次方案最关键的边界。  
只要 PDA 输入值语义是“今天已经产了多少”，OLD 设备 RT 就必须走覆盖逻辑，而不能走差值累加逻辑。

### 5.3 Board4 文案与返回值必须一致

当前旧文档里把 `selectDeviceProductionAnalysis` 定义成“今日增量”，但页面标题是“设备总产量”。本次方案以页面真实语义为准，后端返回值改为历史总产量。

### 5.4 Board5 TOP5 不再直接扫源表

当 RT/DAY 链路跑通后，TOP5 不应再回源表按参数名硬查，否则会再次引入：

1. 新旧设备源不同
2. 参数名不同
3. 算法不同

这三个分叉点。

### 5.5 Board1 与 Board4 状态必须复用同一接口口径

`board4` 不能自己另写一套设备状态统计 SQL。  
应继续复用 `aucma-base` 的状态统计接口，只修改底层 SQL 口径。

### 5.6 Oracle 11 兼容约束

1. SQL 继续使用当前项目已采用的 `ROW_NUMBER()`、`NVL()`、`TRUNC(SYSDATE)` 风格。
2. 避免引入 Oracle 11 不支持的高级 JSON/递归查询写法。
3. 动态分表名仍必须经后端受控生成，禁止前端透传表名。

---

## 6. 具体修改代码方案

## 6.1 修改 PDA 当日产量写入参数

### 文件位置

`zs_aucma-mes-back/aucma-dms/src/main/java/com/aucma/dms/controller/DmsMobileController.java`

### 修改目标

1. 把 PDA 当日产量写入参数从 `机台状态-实际产出数量` 改为 `生产计数-当前日期生产总数`
2. 保留源表完整时间序列，供追溯查询使用

### 建议代码

```java
// 文件：zs_aucma-mes-back/aucma-dms/src/main/java/com/aucma/dms/controller/DmsMobileController.java

private static final String PARAM_NAME_PDA_DAILY_TOTAL = "生产计数-当前日期生产总数";

/**
 * PDA-选择设备后更新当日产量（写入参数：生产计数-当前日期生产总数）
 */
@PostMapping("/device/updateDailyOutput")
@Transactional(rollbackFor = Exception.class)
public AjaxResult updateDeviceDailyOutput(@RequestBody DmsPdaDeviceParamUpdateVo request) {
    if (request == null) {
        throw new ServiceException("请求体不能为空");
    }
    return updatePdaDeviceParamValue(
            request.getDeviceCode(),
            request.getDeviceId(),
            request.getParamValue(),
            PARAM_NAME_PDA_DAILY_TOTAL,
            true,
            "当日产量"
    );
}
```

### 行间注释要求

```java
// 为什么这样做：PDA 录入的是“今天已经产了多少”，
// 它在业务语义上就是“生产计数-当前日期生产总数”，不能再伪装成累计计数器参数。
```

## 6.2 新增 OLD 设备 RT 覆盖写入能力

### 文件位置

1. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/IRtDailyProdStateService.java`
2. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/RtDailyProdStateServiceImpl.java`
3. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/BaseDeviceParamValServiceImpl.java`

### 修改目标

1. OLD 设备写入 `生产计数-当前日期生产总数` 后，直接覆盖 RT 当天总量
2. RT 中的 `param_name` 继续统一写为 `机台状态-实际产出数量`

### 建议接口

```java
// 文件：IRtDailyProdStateService.java

void overwriteTodayProduction(String deviceCode, String paramName, BigDecimal todayTotal, Date collectTime);
```

### 建议实现

```java
// 文件：RtDailyProdStateServiceImpl.java

@Override
@Transactional(rollbackFor = Exception.class)
public void overwriteTodayProduction(String deviceCode, String paramName, BigDecimal todayTotal, Date collectTime) {
    if (deviceCode == null || paramName == null || todayTotal == null || collectTime == null) {
        return;
    }

    Date prodDate = truncateToDay(collectTime);
    RtDailyProdState state = rtDailyProdStateMapper.selectForUpdate(prodDate, deviceCode, paramName);
    Date now = new Date();

    if (state == null) {
        RtDailyProdState insertState = new RtDailyProdState();
        insertState.setProdDate(prodDate);
        insertState.setDeviceCode(deviceCode);
        insertState.setParamName(paramName);
        insertState.setLastParamVal(todayTotal);
        insertState.setCurrentTotal(todayTotal);
        insertState.setResetCount(0L);
        insertState.setDirtyFlag(0L);
        insertState.setLastCollectTime(collectTime);
        insertState.setUpdateTime(now);
        rtDailyProdStateMapper.insertRtDailyProdState(insertState);
        return;
    }

    // 为什么这样做：OLD 设备上报的是“今天当前总量”，
    // 这里必须直接覆盖 RT 结果，不能按差值累加，否则人工修正会把产量越改越大。
    state.setLastParamVal(todayTotal);
    state.setCurrentTotal(todayTotal);
    state.setLastCollectTime(collectTime);
    state.setUpdateTime(now);
    rtDailyProdStateMapper.updateRtDailyProdState(state);
}
```

### Base Service 路由建议

```java
// 文件：BaseDeviceParamValServiceImpl.java

private static final String DAILY_OUTPUT_PARAM_NAME = "机台状态-实际产出数量";
private static final String PDA_DAILY_TOTAL_PARAM_NAME = "生产计数-当前日期生产总数";

private void syncRtStateIfNeeded(BaseDeviceParamVal entity, int rows) {
    if (rows <= 0 || !deviceParamTableRouter.isOldDevice(entity.getDeviceCode())) {
        return;
    }

    try {
        BigDecimal newVal = new BigDecimal(entity.getParamValue());

        if (PDA_DAILY_TOTAL_PARAM_NAME.equals(entity.getParamName())) {
            // 为什么这样做：OLD 设备源数据是“今日累计值”，
            // RT 要保存的是当天权威总量，因此采用覆盖模式。
            rtDailyProdStateService.overwriteTodayProduction(
                    entity.getDeviceCode(),
                    DAILY_OUTPUT_PARAM_NAME,
                    newVal,
                    entity.getCollectTime()
            );
            return;
        }

        if (DAILY_OUTPUT_PARAM_NAME.equals(entity.getParamName())) {
            // 保留历史兼容能力：如果旧链路仍写累计计数器参数，则继续按旧规则处理。
            rtDailyProdStateService.incrementProduction(
                    entity.getDeviceCode(),
                    entity.getParamName(),
                    newVal,
                    entity.getCollectTime()
            );
        }
    } catch (NumberFormatException ex) {
        log.warn("OLD设备产量值不是合法数字，跳过RT同步 | deviceCode={}, paramValue={}",
                entity.getDeviceCode(), entity.getParamValue());
    }
}
```

## 6.3 修改 Board1 状态 SQL

### 文件位置

`zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/BaseDeviceParamValMapper.xml`

### 修改目标

1. 旧设备：当天最新三色灯
2. 新设备：近两小时最新三色灯

### 建议 SQL 结构

```xml
<!-- 文件：zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/BaseDeviceParamValMapper.xml -->

<select id="selectDeviceStatusStatistics" parameterType="java.util.Map" resultType="java.util.Map">
    WITH merged_param AS (
        <trim suffixOverrides="UNION ALL">
            SELECT device_code, param_name, collect_time, record_time
            FROM BASE_DEVICE_PARAM_VAL
            WHERE device_code LIKE 'OLD-%'
              AND param_name IN ('机台状态-三色灯机器运行', '机台状态-三色灯机器暂停', '机台状态-三色灯机器待机', '机台状态-三色灯机器报警')
              AND UPPER(param_value) = 'TRUE'
              AND collect_time >= TRUNC(SYSDATE)
              AND collect_time < TRUNC(SYSDATE) + 1
            UNION ALL
            <if test="tableSuffixes != null and tableSuffixes.size() > 0">
                <foreach item="suffix" collection="tableSuffixes" separator=" UNION ALL ">
                    SELECT device_code, param_name, collect_time, record_time
                    FROM BASE_DEVICE_PARAM_VAL_${suffix}
                    WHERE device_code NOT LIKE 'OLD-%'
                      AND param_name IN ('机台状态-三色灯机器运行', '机台状态-三色灯机器暂停', '机台状态-三色灯机器待机', '机台状态-三色灯机器报警')
                      AND UPPER(param_value) = 'TRUE'
                      AND collect_time >= SYSDATE - (2/24)
                      AND collect_time < SYSDATE
                </foreach>
            </if>
        </trim>
    )
    ...
</select>
```

### 行间注释要求

```xml
<!-- 为什么这样做：OLD 设备三色灯由 PDA 事件驱动写入，不是高频自动采集；
     如果也强制限制近两小时，会把当天早些时候录过状态的设备误判成未开机。 -->
```

同样的窗口规则要同步应用到：

1. `selectDeviceStatusStatistics`
2. `selectDeviceStatusList`

## 6.4 修改 Board4 设备总产量 SQL

### 文件位置

`zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml`

### 修改目标

将 `selectDeviceProductionAnalysis` 从“今日 RT”改为“历史总产量”。

### 建议 SQL

```xml
<!-- 文件：zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml -->

<select id="selectDeviceProductionAnalysis" resultType="com.aucma.report.domain.vo.Board4DeviceProductionVo">
    SELECT d.DEVICE_NAME AS deviceName,
           NVL(h.history_total, 0) + NVL(r.current_total, 0) AS production
    FROM BASE_DEVICELEDGER d
    LEFT JOIN (
        SELECT device_code,
               SUM(daily_prod) AS history_total
        FROM DEVICE_DAILY_PRODUCTION
        WHERE param_name = '机台状态-实际产出数量'
        GROUP BY device_code
    ) h
      ON d.device_code = h.device_code
    LEFT JOIN RT_DAILY_PROD_STATE r
      ON d.device_code = r.device_code
     AND r.prod_date = TRUNC(SYSDATE)
     AND r.param_name = '机台状态-实际产出数量'
    WHERE d.IS_FLAG = 1
    ORDER BY production DESC, d.device_code
</select>
```

### 行间注释要求

```xml
<!-- 为什么这样做：页面真实语义是“设备总产量”，
     因此必须使用“历史 DAY 汇总 + 今日 RT”两段式结果，而不是只看今天 RT。 -->
```

## 6.5 修改 Board5 TOP5 SQL

### 文件位置

`zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board5Mapper.xml`

### 修改目标

1. `selectProductionTop5` 改为直接查 RT
2. `selectMaxProduction` 改为直接查 RT

### 建议 SQL

```xml
<!-- 文件：zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board5Mapper.xml -->

<select id="selectProductionTop5" resultType="com.aucma.report.domain.vo.Board5ProductionTopVo">
    SELECT * FROM (
        SELECT d.DEVICE_NAME AS deviceName,
               NVL(r.current_total, 0) AS production,
               0 AS percent
        FROM BASE_DEVICELEDGER d
        LEFT JOIN RT_DAILY_PROD_STATE r
          ON d.DEVICE_CODE = r.DEVICE_CODE
         AND r.prod_date = TRUNC(SYSDATE)
         AND r.param_name = '机台状态-实际产出数量'
        WHERE d.IS_FLAG = 1
        ORDER BY production DESC, d.device_code
    )
    WHERE ROWNUM <= 5
</select>

<select id="selectMaxProduction" resultType="java.lang.Long">
    SELECT NVL(MAX(current_total), 1)
    FROM RT_DAILY_PROD_STATE
    WHERE prod_date = TRUNC(SYSDATE)
      AND param_name = '机台状态-实际产出数量'
</select>
```

### 行间注释要求

```xml
<!-- 为什么这样做：TOP5 要的是“今日产量排名”，RT 已经是新旧设备统一后的当天实时累计结果，
     再回源表分开查只会把统计口径重新打散。 -->
```

---

## 7. 代码文件位置汇总

### 必改文件

1. `zs_aucma-mes-back/aucma-dms/src/main/java/com/aucma/dms/controller/DmsMobileController.java`
2. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/IRtDailyProdStateService.java`
3. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/RtDailyProdStateServiceImpl.java`
4. `zs_aucma-mes-back/aucma-base/src/main/java/com/aucma/base/service/impl/BaseDeviceParamValServiceImpl.java`
5. `zs_aucma-mes-back/aucma-base/src/main/resources/mapper/base/BaseDeviceParamValMapper.xml`
6. `zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board4Mapper.xml`
7. `zs_aucma-mes-back/aucma-report/src/main/resources/mapper/report/Board5Mapper.xml`

### 需同步确认但本次可不改的文件

1. `zs_aucma-mes-ui/src/views/board/board1/index.vue`
2. `zs_aucma-mes-ui/src/views/board/board4/index.vue`
3. `zs_aucma-mes-ui/src/views/board/board5/index.vue`
4. `zs_aucma-mes-back/aucma-base/src/main/resources/board4_end_to_end_full_implementation_guide.md`
5. `zs_aucma-mes-back/aucma-base/src/main/resources/梳理.md`

说明：

前端接口名本次可保持不变，主要修正后端返回语义；但旧文档中“设备总产量=今日增量”的描述必须后续同步修正，避免再次误导实现。

---

## 8. 验证清单

### 8.1 OLD 设备 PDA 录入验证

1. 09:00 录入 OLD-01 当日产量 `100`
   - 源表写入参数名应为 `生产计数-当前日期生产总数`
   - RT 中 OLD-01 当天 `current_total=100`
2. 11:00 再录入 `120`
   - RT 应直接变为 `120`
3. 11:30 修正为 `118`
   - RT 应直接变为 `118`
   - 不允许变成 `238`

### 8.2 Board1 状态验证

1. OLD 设备上午 9 点录入“暂停”，下午 3 点未再录入
   - `board1` 仍应显示“停机/暂停”
2. NEW 设备超过两小时无采集
   - `board1` 应按无有效状态数据处理

### 8.3 Board4 历史总产量验证

1. `DEVICE_DAILY_PRODUCTION` 中某设备历史累计 `3000`
2. `RT_DAILY_PROD_STATE` 当天 `200`
3. 页面“设备总产量”应显示 `3200`

### 8.4 Board5 TOP5 验证

1. 取 RT 当天数据做排序
2. TOP5 与 `board4.dayTotal` 的设备明细求和口径应一致

---

## 9. 最终结论

本次业务梳理后的关键结论只有三条：

1. OLD 设备 PDA 录入值是“今日累计值”，因此源表应写 `生产计数-当前日期生产总数`，RT 更新必须走**覆盖模式**。
2. NEW 设备自动采集值是“累计计数器当前值”，因此继续通过月分表触发器按 **delta-sum** 更新 RT。
3. 所有大屏统计最终都应尽量读取 RT/DAY 事实层，而不是重新回源表拼业务逻辑。

只要这三条不被打破，`board1`、`board4`、`board5` 的状态、今日产量、历史总产量三套口径就能统一起来。
