# 报表查询模块文档

## 一、功能概述

报表查询页面用于查看生产称重记录，展示每次配方执行后的实际称重结果与设定参数的对比。支持按日期范围、批号、称重系统三维筛选。

**数据来源**：`R_WEIGH` 表（现场 PC 执行配方并完成称重后写入）
**查询方式**：日期范围（最多 5 天）+ 批号下拉筛选 + 系统编号筛选
**展示形式**：表格（序号、系统、配方名称、设定车数、实际车数、设定重量、设定误差、实际重量、发送时间、批号）

> 与发送记录的区别：发送记录查询的是"下达了什么配方"（`R_TRANSFER`），报表查询的是"执行后称重结果如何"（`R_WEIGH`）。

## 二、功能架构

### 2.1 功能结构图

```
┌─────────────────────────────────────────────────────────────┐
│                       报表查询模块                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐    ┌──────────────┐                      │
│  │   小程序端    │    │   API接口    │                      │
│  │  (微信H5)    │    │  (PHP)       │                      │
│  └──────┬───────┘    └──────┬───────┘                      │
│         │                   │                               │
│         └───────────────────┘                               │
│                             │                               │
│                             ▼                               │
│                    ┌──────────────────┐                    │
│                    │   MySQL数据库    │                    │
│                    │   R_WEIGH表      │                    │
│                    └──────────────────┘                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 核心功能列表

| 功能 | 说明 |
|------|------|
| 日期范围筛选 | 开始日期 ~ 结束日期，最多 5 天 |
| 批号动态下拉 | 根据当前日期范围自动加载该时段内的所有批号 |
| 系统筛选 | 全部 / 1# / 2# / 3# 三套称重系统 |
| 数据表格 | 10 列横向滚动表格，展示完整称重记录 |

## 三、数据模型

### 3.1 数据库表结构 (R_WEIGH)

| 字段名 | 类型 | 说明 |
|--------|------|------|
| ID | bigint | 主键ID，自增 |
| LOT_ID | varchar | 批号（唯一标识一次配方执行） |
| BATCH | int | 实际已完成车数 |
| SYS_NO | int | 系统编号（1/2/3，对应三套称重系统） |
| RECIPE_NAME | varchar | 配方名称 |
| WEIGHT_SET | decimal | 设定重量(Kg) |
| TOLERANCE_SET | decimal | 设定误差(Kg) |
| WEIGHT | decimal | 实际称重重量(Kg) |
| SAVE_TIME | varchar(14) | 保存时间，格式 `yyyyMMddHHmmss` |
| BATCH_SET | int | 设定车数 |

> `R_WEIGH` 与 `R_TRANSFER` 通过 `LOT_ID` 关联，前者记录称重结果，后者记录发送指令。

### 3.2 字段映射与显示

| 字段 | 原始值 | 显示值 | 说明 |
|------|--------|--------|------|
| `SYS_NO` | 1 | `1#` | 系统编号后加 `#` 号 |
| `SAVE_TIME` | `20260506013358` | `2026-05-06 01:33:58` | 字符串分段格式化 |
| `WEIGHT_SET` / `TOLERANCE_SET` / `WEIGHT` | 3.4 | `3.4` | 保留 1 位小数 |
| `BATCH_SET` / `BATCH` | 5 | `5` | 直接显示整数 |

## 四、接口文档

### 4.1 获取批号列表（用于下拉框）

```
GET /api/exhibition/report.php?action=listLots&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
```

**请求参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| startDate | string | 否 | 开始日期，默认 5 天前 |
| endDate | string | 否 | 结束日期，默认今天 |

**响应示例**：
```json
{
  "code": 0,
  "message": "获取成功",
  "data": {
    "lots": ["202604280133456659", "202604280118498792"]
  }
}
```

**后端逻辑**：
1. 将日期转为 `Ymd000000` / `Ymd235959` 格式
2. 查询 `R_WEIGH` 中该时间范围内的所有 `LOT_ID`（去重）
3. 按 `LOT_ID` 降序排列，最多返回 200 条
4. 未传日期参数时，查询全表最近 200 个批号

> **注意**：早期版本使用 `ORDER BY SAVE_TIME DESC`，在 MySQL `ONLY_FULL_GROUP_BY` 模式下会报错（`SAVE_TIME` 不在 `SELECT DISTINCT` 列表中）。已修正为 `ORDER BY LOT_ID DESC`。

### 4.2 获取报表数据

```
GET /api/exhibition/report.php?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&sysNo=0&lotId=
```

**请求参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| startDate | string | 是 | 开始日期 `YYYY-MM-DD` |
| endDate | string | 是 | 结束日期 `YYYY-MM-DD` |
| sysNo | int | 否 | 系统编号（0=全部，1/2/3=具体系统） |
| lotId | string | 否 | 批号（空字符串=全部） |

**响应示例**：
```json
{
  "code": 0,
  "message": "获取成功",
  "data": {
    "list": [
      {
        "id": 4,
        "lotId": "202604280133456659",
        "recipeName": "配方A",
        "batchSet": 11,
        "batch": 1,
        "sysNo": 1,
        "weightSet": 4.4,
        "toleranceSet": 1.0,
        "weight": 3.4,
        "saveTime": "20260506013358"
      }
    ],
    "startDate": "2026-05-06",
    "endDate": "2026-05-06"
  }
}
```

**后端逻辑**：
1. 校验日期格式和范围（不超过 5 天）
2. 将日期转为 `Ymd000000` / `Ymd235959`
3. 基础条件：`SAVE_TIME >= ? AND SAVE_TIME <= ?`
4. 若 `sysNo > 0`，追加 `AND SYS_NO = ?`
5. 若 `lotId` 非空，追加 `AND LOT_ID = ?`
6. 按 `SAVE_TIME DESC` 排序返回

## 五、界面设计

### 5.1 布局结构

```
┌──────────────────────────────────────────┐
│ 2026/05/05  至  2026/05/06         🔍   │  ← 第一行：日期 + 查询按钮
├──────────────────────────────────────────┤
│ 批号: [下拉框      ▼]  系统: [下拉 ▼]    │  ← 第二行：批号下拉 + 系统下拉
├──────────────────────────────────────────┤
│ 序 系统 配方 设定 实际 设定 设定 实际 时间 批号 │  ← 表头（10列）
│ 1  1#   A    11   1   4.4  1.0  3.4  ...  ...  │  ← 数据行
│ ...                                      │
└──────────────────────────────────────────┘
```

### 5.2 为什么用两排筛选栏？

**第一版设计**：所有筛选条件挤在一排（日期 + 批号 + 系统 + 查询按钮）

**问题**：
- 移动端宽度有限（375px），5 个控件在一排严重拥挤
- 日期输入框被压缩到无法完整显示
- 批号下拉框和系统下拉框宽度不足，选项文字被截断

**最终版**：分成两排
- 第一排：开始日期 + "至" + 结束日期 + 查询按钮（SVG 放大镜）
- 第二排：`批号:` 标签 + 批号下拉框（`flex: 2`，占大部分宽度）+ `系统:` 标签 + 系统下拉框（`flex: 1; max-width: 90px`，较窄）

两排布局释放了一排 4 个控件的拥挤，批号下拉框有足够空间显示完整批号（如 `202604280133456659`）。

### 5.3 字段映射与显示设计

| 字段 | 显示方式 | 说明 |
|------|----------|------|
| 系统 | `sysNo + '#'` | 如 `1#`、`2#`、`3#` |
| 设定重量/误差/实际重量 | `toFixed(1)` | 保留 1 位小数 |
| 发送时间 | `YYYY-MM-DD HH:mm:ss` | 从 `YmdHis` 字符串分段解析 |
| 批号 | 完整显示 | 作为最后一列，宽度 90px |

### 5.4 列宽设计（移动端横向滚动）

报表有 10 列，比发送记录多 4 列，移动端必须横向滚动：

| 列 | 宽度 | 说明 |
|----|------|------|
| 序号 | 28px | 仅数字 |
| 系统 | 40px | `1#` 格式 |
| 配方名称 | 80px | 最长 8 个汉字 |
| 设定车数 | 50px | 仅两位数 |
| 实际车数 | 50px | 仅两位数 |
| 设定重量 | 55px | 含小数 |
| 设定误差 | 55px | 含小数 |
| 实际重量 | 55px | 含小数 |
| 发送时间 | 自适应 | 时间字符串 |
| 批号 | 90px | 14位数字部分可见 |

表格 `min-width: 640px`，强制超出屏幕宽度触发 `overflow-x: auto` 横向滚动。

## 六、开发说明

### 6.1 文件清单

| 文件路径 | 说明 |
|----------|------|
| `mobile/exhibition/report/index.html` | 报表查询页面（两排筛选 + 10列表格） |
| `api/exhibition/report.php` | 报表查询 API（`action=listLots` + 主查询） |

### 6.2 关键代码说明

#### 批号下拉框动态加载

```javascript
async function loadLotOptions() {
    // 1. 校验日期范围合法性
    const diffDays = (end - start) / 86400000;
    if (diffDays > 5 || diffDays < -5) return;

    const currentVal = lotSelect.value;
    lotSelect.innerHTML = '<option value="">全部</option>';

    // 2. 根据日期范围获取批号列表
    const res = await fetch('/api/exhibition/report.php?action=listLots&startDate=' 
        + encodeURIComponent(startVal) + '&endDate=' + encodeURIComponent(endVal));
    const result = await res.json();

    // 3. 填充下拉框，保留之前选中的值
    if (result.code === 0 && result.data && result.data.lots) {
        result.data.lots.forEach(function(lot) {
            const opt = document.createElement('option');
            opt.value = lot;
            opt.textContent = lot;
            lotSelect.appendChild(opt);
        });
        if (currentVal) {
            // 若之前选中的批号仍在新列表中则保留
            let found = false;
            for (let i = 0; i < lotSelect.options.length; i++) {
                if (lotSelect.options[i].value === currentVal) {
                    found = true; break;
                }
            }
            if (found) lotSelect.value = currentVal;
        }
    }
}
```

> **设计要点**：批号下拉框不是静态写死的，而是根据用户选择的日期范围动态从数据库拉取。切换日期后自动刷新批号列表，保证下拉框中的选项始终与当前日期范围内的数据对应。

#### 日期变化联动刷新批号

```javascript
function onDateChange() {
    const diffDays = (end - start) / 86400000;
    if (diffDays > 5 || diffDays < -5) {
        showToast('时间范围不能超过5天，请手动调整');
        return;
    }
    // 日期合法时自动刷新批号下拉框
    loadLotOptions();
}
```

#### 后端多条件动态查询

```php
$where = "SAVE_TIME >= ? AND SAVE_TIME <= ?";
$params = [$startTime, $endTime];

if ($sysNo > 0) {
    $where .= " AND SYS_NO = ?";
    $params[] = $sysNo;
}
if (!empty($lotId)) {
    $where .= " AND LOT_ID = ?";
    $params[] = $lotId;
}

$sql = "SELECT ... FROM R_WEIGH WHERE $where ORDER BY SAVE_TIME DESC";
```

## 七、报表查询设计流程与开发思路

### 7.1 需求分析与设计目标

**业务场景**：展会现场，管理人员需要查看某段时间内各套称重系统的实际生产结果，对比设定参数与实际称重数据。

**核心需求**：
1. 时间范围查询（最近几天内的记录）
2. 按批号精确筛选（某次配方执行的所有称重记录）
3. 按系统筛选（只看 1# 或 2# 系统的数据）
4. 数据对比：设定值 vs 实际值

**设计目标**：
1. 筛选条件清晰：日期 + 批号 + 系统，三维组合
2. 批号不盲填：下拉框只列出当前日期范围内存在的批号
3. 一屏可见：表格横向滚动，10 列数据完整展示
4. 数据精准：设定车数/实际车数、设定重量/实际重量对比一目了然

### 7.2 UI 设计决策

#### 为什么筛选条件分两排？

发送记录页面只有日期筛选，所有控件在一排刚好填满。报表增加了批号和系统两个下拉框，如果挤在一排：

| 布局方案 | 日期框宽 | 批号框宽 | 系统框宽 | 查询按钮 | 结果 |
|----------|----------|----------|----------|----------|------|
| 单排（5控件） | ~85px | ~70px | ~50px | 34px | 日期显示不全，批号截断 |
| 双排（上3下2） | ~130px | ~200px | ~80px | 34px | 所有控件宽度充足 |

两排布局的代价是筛选区高度增加约 40px，但换来了所有输入控件的可用性。

#### 为什么批号下拉框占 `flex: 2`，系统下拉框较窄？

- 批号值是 18 位数字（如 `202604280133456659`），需要最宽的空间
- 系统只有 3 个选项（1#/2#/3#），窄宽度即可完整显示
- 两者比例约 2:1，视觉上批号为主导筛选条件

#### 为什么表格 10 列用横向滚动而非折叠？

考虑过两种方案：

**方案 A：折叠/卡片式**
- 每行折叠为卡片，点击展开详情
- 优点：一屏可见多条记录
- 缺点：无法快速横向对比不同记录的同一字段（如对比两条记录的"实际重量"）

**方案 B：横向滚动表格**
- 10 列固定表头，左右滑动查看
- 优点：表头始终可见，数据对齐，便于对比
- 缺点：需要横向滑动

**决策**：报表的核心价值是"数据对比"（设定 vs 实际），横向滚动表格的数据对齐性远优于折叠卡片。用户更关心精确数值对比，而非一次看多少条。

### 7.3 交互设计决策

#### 批号下拉框的动态加载策略

**问题**：批号下拉框的选项从哪里来？

**方案演进**：
1. **静态方案**：页面加载时一次性拉取全部批号
   - 问题：`R_WEIGH` 数据量大时，下拉框选项过多（几百上千条），查找困难
2. **动态方案**：根据当前日期范围实时拉取
   - 优点：只显示当前日期范围内的批号，选项数量可控
   - 额外收益：切换日期后批号列表自动更新，始终与数据同步

**触发时机**：
- 页面初始化时（默认昨天~今天）
- 日期选择器 `change` 事件（日期变化且合法时）

**状态保持**：加载新批号列表前记录当前选中的值，加载后若该值仍在新列表中则自动恢复选中。避免用户切换日期后已选的批号被清空。

#### 日期变化时的校验策略

报表页面有两个日期校验层次：

| 场景 | 函数 | 行为 |
|------|------|------|
| 日期变化（onDateChange） | 检查范围 + 提示 | 超出 5 天提示 toast，不刷新批号 |
| 点击查询（validateDateRange） | 完整校验 + 阻断 | 空值/格式错误/倒序/超范围均阻断查询 |

**为什么 onDateChange 只做提示不阻断？**
- 日期选择器是用户逐步调整的，可能先选一个较远日期再调整另一个
- 如果每次变化都阻断，用户体验被打断
- 只在最终点击查询时才强制校验

### 7.4 数据库查询设计

#### 为什么 `listLots` 用 `SELECT DISTINCT LOT_ID`？

`R_WEIGH` 中一次配方执行（一个 `LOT_ID`）可能对应多条记录（多套系统、多车称重）。用户筛选批号时只需要知道"有哪些批号"，不需要重复。

#### `ONLY_FULL_GROUP_BY` 的坑

MySQL 8.0 默认开启 `ONLY_FULL_GROUP_BY`，这条 SQL 会报错：
```sql
SELECT DISTINCT LOT_ID FROM R_WEIGH 
WHERE SAVE_TIME >= ? AND SAVE_TIME <= ? 
ORDER BY SAVE_TIME DESC  -- ❌ SAVE_TIME 不在 SELECT 列表中
```

**修正方案**：将 `ORDER BY SAVE_TIME DESC` 改为 `ORDER BY LOT_ID DESC`。
- `LOT_ID` 的生成规则是 `YmdHis` + 4位随机数，天然按时间降序排列
- 用 `LOT_ID` 排序与按时间排序效果一致，且兼容 `ONLY_FULL_GROUP_BY`

#### 为什么主查询用 `SAVE_TIME >= ? AND SAVE_TIME <= ?` 字符串比较？

`SAVE_TIME` 在数据库中是 `varchar(14)` 类型（如 `20260506013358`），不是 `DATETIME`。因此：
- 不能直接用 `BETWEEN` 配合日期类型
- 必须先把前端日期转为 `Ymd000000` / `Ymd235959` 字符串，再用字符串范围比较
- 这种方式利用了字符串的字典序与时间的自然序一致的特性

## 八、开发中的关键问题与解决方案

### 问题 1：`loadLotOptions` 函数缺失导致批号下拉框永远为空

**现象**：报表页面批号下拉框中只有"全部"一个选项，切换日期也无变化。

**根因**：前端代码中调用了 `loadLotOptions()`（页面初始化和日期变化时各一次），但该函数从未定义。JS 执行时抛出 `ReferenceError`，后续逻辑中断，下拉框从未被填充。

**解决**：补全 `loadLotOptions` 函数，实现日期校验 → API 请求 → 动态填充 → 状态保持的完整逻辑。

### 问题 2：MySQL `ONLY_FULL_GROUP_BY` 导致 `listLots` API 500 错误

**现象**：批号下拉框为空，API 返回 500，错误信息：`Expression #1 of ORDER BY clause is not in SELECT list`。

**根因**：`SELECT DISTINCT LOT_ID ... ORDER BY SAVE_TIME DESC` 中，`SAVE_TIME` 不在 `SELECT` 列表里，违反 `ONLY_FULL_GROUP_BY` 规则。

**解决**：将 `ORDER BY SAVE_TIME DESC` 改为 `ORDER BY LOT_ID DESC`。由于 `LOT_ID` 基于时间戳生成，排序效果等价。

### 问题 3：默认日期范围内数据库无数据，误以为查询功能失效

**现象**：页面默认加载昨天~今天的数据，但表格显示"该时间段暂无数据"，批号下拉框也只有"全部"。

**根因**：测试数据库中 `R_WEIGH` 的数据时间是 `2026-05-06`，而默认查询的是昨天~今天（`2026-04-28 ~ 2026-04-29`），时间范围不匹配。

**解决**：这不是代码 bug，是测试数据的时间分布问题。将日期选择器调整到 `2026-05-06` 即可看到数据和批号。生产环境下数据是实时写入的，默认昨天~今天正常可用。

### 问题 4：WeChat WebView 缓存导致 JS 修改不生效

**现象**：补全 `loadLotOptions` 函数后，微信预览中批号下拉框仍然为空。

**根因**：微信内置浏览器对 H5 页面有强缓存，旧的 JS 代码仍在运行。

**解决**：小程序 WebView URL 追加 `?v=${Date.now()}` 时间戳，或在微信开发者工具中清除缓存。

## 九、最佳实践总结

1. **动态下拉框**：根据主筛选条件（日期）联动刷新次级筛选条件（批号），避免选项过多且保证数据一致性
2. **状态保持**：刷新下拉框选项前记录当前选中值，刷新后自动恢复，不打扰用户已有选择
3. **SQL 兼容性**：使用 `SELECT DISTINCT` 时，`ORDER BY` 的列必须在 SELECT 列表中（`ONLY_FULL_GROUP_BY` 模式）
4. **字符串时间比较**：`varchar(14)` 格式的时间字段（`YmdHis`）可通过字符串范围查询，无需转为日期类型
5. **双排筛选布局**：当单排控件超过 4 个时，考虑分两排放置，保证每个输入框的最小可用宽度
6. **横向滚动表格**：数据对比类页面优先用横向滚动表格，保持表头固定和数据对齐，优于折叠卡片
7. **分层校验**：`onChange` 只做轻量提示（不打断用户），`onSubmit` 做完整校验（阻断非法请求）

## 十、更新记录

| 日期 | 版本 | 更新内容 |
|------|------|----------|
| 2026-05-06 | v1.0 | 报表查询页面定稿：两排筛选栏（日期+批号+系统）、10列横向滚动表格、批号动态下拉联动、修复 `ONLY_FULL_GROUP_BY` SQL 错误、补全 `loadLotOptions` 函数、日期分层校验策略 |
