# TriloopTem App 商业化审计报告

> 审计日期：2026-06-20
> 审计范围：代码质量、UX 健壮性、安全性、性能、数据完整性
> 总计问题：36 项（致命 4 / 高 13 / 中 14 / 低 5）

---

## 一、代码质量与架构

### 1.1 [致命] 生产代码中有大量调试日志

**文件及行号：**
- `src/services/TcpService.ts` — 第 38, 45, 51, 65, 68, 70, 75, 83, 125, 129 行（10 处）
- `src/services/DeviceService.ts` — 第 30, 111-114, 149, 153, 159 行（5 处）
- `src/protocol/parser.ts` — 第 58-61, 64, 71, 77 行（4 处）

**问题描述：**
总计约 20 处 `console.log` / `console.warn` 调用。其中 TcpService 第 51 和 68 行在每个 TCP data 事件上做 `Array.from(...).map(b => '0x' + b.toString(16))` 十六进制格式化，DeviceService 第 111-114 行在每帧数据到达时循环打印各通道的原始 ADC 值。在 250kHz 六通道连续采集时，这些日志每秒触发数百次，产生大量 GC 压力。

**修复方案：**
删除所有 `console.log` / `console.warn`，或用 `if (__DEV__)` 包裹。建议封装一个 `logger` 模块，在 release 构建中为 no-op。

---

### 1.2 [高] IP 地址和端口号缺少格式校验

**文件：** `src/components/modals/ConnectModal.tsx` 第 42-43 行

**问题描述：**
连接弹窗仅检查 `!host.trim()` 和 `isNaN(port)`，接受诸如 `999.999.999.999` 等无效 IP，端口号也没有范围限制（1-65535）。

**修复方案：**
在连接前用正则或 IP 解析函数校验地址格式，端口校验 `port >= 1 && port <= 65535`。

---

### 1.3 [高] 心跳写入失败被静默吞掉

**文件：** `src/services/TcpService.ts` 第 97-99 行

**问题描述：**
心跳定时器每 8 秒写 `'0'` 到 socket，外层 `try/catch` 捕获所有异常但不做任何处理。如果 socket 处于半断开状态，心跳持续失败但 `this.connected` 仍为 `true`，用户无法感知连接已断。

**修复方案：**
在 catch 中将 `this.connected` 设为 `false`，调用 `this.callbacks?.onError()`，触发重连流程。

---

### 1.4 [高] 数据库查询使用 `any` 类型

**文件：** `src/services/StorageService.ts` 第 247 行

**问题描述：**
`getAllAsync<any>` 绕过了 TypeScript 类型检查。如果后续数据库 schema 变更（列名改动），运行时会产生 `undefined` 值但编译阶段无法发现。

**修复方案：**
为每个查询定义明确的行类型接口，替换 `any`。

---

### 1.5 [中] TcpService 单例无防重复连接保护

**文件：** `src/services/TcpService.ts` 第 23-30 行

**问题描述：**
在已连接状态下调用 `connect()` 不会先断开旧连接。`createSocket()` 虽然销毁旧 socket，但旧的 reconnect 定时器可能仍在运行，回调引用也未清理。

**修复方案：**
在 `connect()` 开头调用 `this.disconnect()` 确保干净状态。

---

### 1.6 [中] 分帧传输状态使用模块级变量，断线后不重置

**文件：** `src/services/DeviceService.ts` 第 24-26 行

**问题描述：**
`splitChunks`、`splitActive`、`splitFrameNo` 是模块级 `let` 变量。如果分帧传输中途断线重连，这些状态不会重置，下次分帧传输会拼接到旧的脏数据上。

**修复方案：**
在 `onClose` 回调中重置分帧状态，或在 `deviceStartSplitFrame` 开始时清理。

---

### 1.7 [中] `clearTimeout` 对可能为 null 的值使用 `!` 断言

**文件：** `src/services/TcpService.ts` 第 43, 113, 137 行

**问题描述：**
`clearTimeout(this.reconnectTimer!)` 虽然在大多数 JS 引擎中对 null 不会崩溃，但 `!` 非空断言不正确，严格模式下可能隐藏问题。

**修复方案：**
改为 `if (this.reconnectTimer) clearTimeout(this.reconnectTimer)`。

---

### 1.8 [低] 导出函数缺少返回类型注解

**文件：** `src/services/DeviceService.ts` 第 28, 172-186 行

**问题描述：**
`handleIncomingFrame`、`deviceStartSplitFrame` 等公共 API 函数没有显式返回类型。

**修复方案：**
为所有 `export` 函数添加返回类型注解。

---

### 1.9 [低] filePrefix 默认值在模块加载时计算，跨天不更新

**文件：** `src/stores/deviceStore.ts` 第 29 行

**问题描述：**
`new Date().toISOString().slice(0,10).replace(/-/g,'')` 仅在模块首次加载时执行一次。如果 App 过了午夜仍在运行，文件前缀还是昨天的日期。

**修复方案：**
在创建新采集会话时动态生成日期前缀，或在每天零点时刷新。

---

## 二、UX 与健壮性

### 2.1 [致命] ACK 竞态条件——并发请求覆盖

**文件：** `src/services/DeviceService.ts` 第 147-170 行

**问题描述：**
`pendingAcks` 以 func code 为 key 存储 Promise resolver。如果用户快速连续点击（如双击"下发配置"按钮），第二次 `pendingAcks.set()` 会静默覆盖第一次的 resolver，第一个 Promise 永远不会 resolve，直到 5 秒超时才报错。

**修复方案：**
在覆盖前先 reject 已有的 pending promise，或使用队列/序列号机制。UI 层也应在 `busy` 状态下禁用按钮（已部分实现但需确认所有路径）。

---

### 2.2 [高] TCP 初始连接无超时

**文件：** `src/services/TcpService.ts` 第 32-91 行

**问题描述：**
`TcpSocket.createConnection` 没有连接超时设置。如果设备不可达，可能挂起 75+ 秒等待操作系统级 TCP 超时。期间 UI 显示"连接中"但无法取消。

**修复方案：**
添加 10 秒连接超时：如果在超时内未收到 `onConnect` 回调，销毁 socket 并调用 `onError`。可以在 `createSocket()` 中启动一个定时器，`onConnect` 时清除。

---

### 2.3 [高] clearHistory 只清内存不清数据库

**文件：** `src/stores/dataStore.ts` 第 62 行

**问题描述：**
`clearHistory` 仅清空内存中的 `history` 数组和 `currentFrame`，但 SQLite 中的帧数据和磁盘上的 bin 文件不受影响。用户以为"清除"了数据，实际上数据仍然存在。

**修复方案：**
要么连同数据库和文件一起删除，要么将按钮文案改为"清除显示"并向用户说明数据仍已保存。

---

### 2.4 [高] 分享功能不可用时无用户反馈

**文件：** `src/utils/export.ts` 第 94-99 行

**问题描述：**
`shareFile` 调用 `Sharing.isAvailableAsync()` 检测分享是否可用，如果不可用则静默返回。用户点击"导出"后看到 CSV 文件生成但什么也没发生。

**修复方案：**
在分享不可用时弹出 Alert 提示，或提供复制文件路径的选项。

---

### 2.5 [中] 操作前不检查连接状态

**文件：** `src/hooks/useDevice.ts` 第 35-78 行

**问题描述：**
`setup`、`startContinuous`、`startSingle` 在发送命令前不检查是否已连接。当未连接时，`sendAndWaitAck` 会立即 reject "TCP not connected"，但用户看到的提示是"配置超时"而非"未连接"。

**修复方案：**
在每个操作开始时检查 `tcpService.isConnected()`，未连接时显示明确的"请先连接设备"提示。

---

### 2.6 [中] wave.tsx 项目名加载无错误处理

**文件：** `app/(tabs)/wave.tsx` 第 396-400 行

**问题描述：**
动态 import + 异步链没有 `.catch()` 处理。如果 `listProjects()` 失败，Promise rejection 未被捕获。

**修复方案：**
添加 `.catch(() => setProjectName(null))`。

---

### 2.7 [中] CSV 导出只导出内存中的帧

**文件：** `app/(tabs)/records.tsx` 第 113 行

**问题描述：**
`exportCsv(history, sessionId)` 只导出当前内存中的帧数据（最多 `MAX_HISTORY = 500` 帧）。如果用户采集了 1000 帧，CSV 中只有最近 500 帧，且用户不知情。

**修复方案：**
从数据库导出全部帧，或在导出前提示用户当前仅包含内存中的 N 帧。

---

### 2.8 [低] 大项目导出无进度指示

**文件：** `src/services/StorageService.ts` 第 336-393 行

**问题描述：**
`exportProject` 对大项目可能耗时较长（读取所有 bin 文件），虽然提供了 `onProgress` 回调，但调用方需确保显示进度 UI。

**修复方案：**
在项目导出时显示进度条或加载动画。

---

## 三、安全性

### 3.1 [高] 原始数据被打印到日志

**文件：**
- `src/services/TcpService.ts` 第 51, 68, 125 行
- `src/services/DeviceService.ts` 第 111-114 行

**问题描述：**
原始 TCP 载荷的十六进制内容和各通道 ADC 数值被打印到 console。在 Android 上，`console.log` 输出可通过 `adb logcat` 被任何持有 `READ_LOGS` 权限的应用读取。对于地球物理勘探数据，这可能涉及商业敏感信息。

**修复方案：**
同 1.1，移除或用 `__DEV__` 门控。

---

### 3.2 [中] 项目名/文件名未做文件系统安全过滤

**文件：** `src/services/StorageService.ts` 第 104-112, 390 行

**问题描述：**
`createProject` 使用 `name.trim()` 但不过滤文件系统特殊字符。`exportProject` 第 390 行直接将项目名拼入文件路径 `TEM_${proj.name}_${Date.now()}.tem`。包含 `/`、`\`、`..` 或空字节的项目名可能导致路径穿越或文件创建失败。

**修复方案：**
对项目名做白名单过滤（只允许字母、数字、中文、下划线、短横线），或在文件名中使用项目 ID 代替名称。

---

### 3.3 [中] GPS 坐标明文存储

**文件：** `src/services/StorageService.ts` 第 58-69 行

**问题描述：**
GPS 经纬度、海拔以明文存储在 SQLite 中，zustand 持久化文件也是明文 JSON。

**修复方案：**
评估是否需要静态加密。如果需要，可使用 `expo-crypto` 或 SQLCipher。对于大多数行业仪器 App 这不是硬性要求，但需在隐私政策中声明。

---

### 3.4 [低] filePrefix 输入未做字符过滤

**文件：** `src/components/ParamForm.tsx` 第 183 行

**问题描述：**
`filePrefix` 只限制了长度（15 字符）但未过滤特殊字符。该字段仅用于协议包构建（不进 SQL），风险较低。

**修复方案：**
添加正则校验，只允许字母、数字、下划线。

---

## 四、性能

### 4.1 [致命] 热路径上的重量级调试计算

**文件：**
- `src/services/TcpService.ts` 第 51, 68 行
- `src/services/DeviceService.ts` 第 111-114 行

**问题描述：**
每个 TCP data 事件都做 `Array.from(arr.slice(0, 20)).map(...)` 创建中间数组和字符串。每帧测量数据到达时循环各通道做 `Array.from()` + `.toFixed()`。连续采集时每秒触发数百次。

**修复方案：**
同 1.1，直接删除。

---

### 4.2 [高] Parser 缓冲区溢出时静默丢弃数据

**文件：** `src/protocol/parser.ts` 第 27-29 行

**问题描述：**
当 `this.len + incoming.length > capacity`（2MB）时，直接 `this.len = 0` 重置缓冲区，丢弃所有已缓存数据。无错误回调，无日志，用户无感知。在高吞吐量连续采集时可能导致数据丢失。

**修复方案：**
至少触发一个错误回调通知上层。考虑动态扩容或增大默认容量。记录溢出事件以便排查。

---

### 4.3 [高] 内存中历史帧可能导致 OOM

**文件：** `src/stores/dataStore.ts` 第 47 行

**问题描述：**
每个 `MeasurementFrame` 包含 `adcRaw: Int32Array[]` 和 `adcUV: Float64Array[]`。以 6 通道 2000 采样深度计算，每帧约 `6 × 2000 × (4 + 8) = 144 KB`。`MAX_HISTORY = 500` 时内存占用可达 **72 MB**。中低端手机可能 OOM 崩溃。

**修复方案：**
- 方案 A：将 `MAX_HISTORY` 降至 50
- 方案 B：内存中只存元数据和峰值摘要，完整波形按需从 bin 文件加载
- 方案 C：历史帧只保留 `adcUV`，不保留 `adcRaw`（节省 1/3 内存）

---

### 4.4 [中] 缩放/拖拽时每帧重建 Skia Path

**文件：** `src/components/WaveformChart.tsx` 第 204-222 行

**问题描述：**
`channelPaths` 的 `useMemo` 依赖 `toY`、`xMinMs`、`xMaxMs`，这些在每次手势更新时都会变化。每个手势帧都触发全量重新计算所有通道的 Skia Path（最多 512 点/通道 × 6 通道）。

**修复方案：**
在固定坐标系中构建 Path，用 Skia 矩阵变换实现平移缩放，避免逐帧重建 Path。

---

### 4.5 [中] 降采样使用简单抽取，可能遗漏尖峰

**文件：** `src/hooks/useWaveform.ts` 第 15-22 行

**问题描述：**
当前降采样每隔 N 个取一个点。对于测量仪器来说，这可能完全遗漏瞬态尖峰信号，导致显示的峰值与实际不符。

**修复方案：**
使用 min-max 降采样（每组取最大值和最小值各一个点）或 LTTB 算法，确保极值被保留。

---

### 4.6 [中] PeakRow 每次渲染都重新计算峰值

**文件：** `app/(tabs)/wave.tsx` 第 294-297 行

**问题描述：**
`ch.reduce((m, v) => Math.max(m, Math.abs(v)), 0)` 在每次组件渲染时对每个可见通道运行一次。对于大数组（2000+ 点）这是不必要的开销。

**修复方案：**
在帧接收时计算峰值并缓存到 `MeasurementFrame`，或用 `useMemo` 包裹。

---

### 4.7 [低] Base64 编码产生大量中间字符串

**文件：** `src/services/BinLoader.ts` 第 68-75 行

**问题描述：**
`u8ToBase64` 将二进制数据拆成 8192 字节块，逐块 `String.fromCharCode` 再 `btoa`。6 通道 2000 点的帧约 48KB 二进制 → 64KB base64 字符串。批量导出时内存抖动明显。

**修复方案：**
考虑使用 `expo-file-system` 的直接二进制写入能力，或流式处理。

---

## 五、数据完整性

### 5.1 [致命] importProject 中 gain 列写入了错误值

**文件：** `src/services/StorageService.ts` 第 480-484 行

**问题描述：**
SQL INSERT 的 `gain` 参数位置（第 10 个参数）使用了 `meta.ampRatio`（增益 code，如 3 代表 1×），而不是实际增益倍数（`AMP_GAIN[meta.ampRatio]`，即 1）。导致导入的数据在重新加载时，电压换算使用错误的增益值，波形幅度错误。

**修复方案：**
第 483 行改为 `AMP_GAIN[meta.ampRatio] ?? 1`。

---

### 5.2 [高] 数据库迁移无版本管理

**文件：** `src/services/StorageService.ts` 第 73-78 行

**问题描述：**
当前通过 `try { ALTER TABLE } catch {}` 添加 `project_id` 列。这种方式只能处理单次迁移，随着 App 迭代新增更多 schema 变更时无法可靠管理。没有版本号追踪，无法知道数据库处于什么状态。

**修复方案：**
实现基于 `PRAGMA user_version` 的迁移系统：
```typescript
const version = await db.getFirstAsync<{user_version:number}>('PRAGMA user_version');
if (version.user_version < 1) {
  await db.runAsync('ALTER TABLE sessions ADD COLUMN project_id TEXT');
  await db.runAsync('PRAGMA user_version = 1');
}
if (version.user_version < 2) {
  // 下一次迁移...
  await db.runAsync('PRAGMA user_version = 2');
}
```

---

### 5.3 [高] 删除项目不级联删除关联数据

**文件：** `src/services/StorageService.ts` 第 139-142 行

**问题描述：**
`deleteProject` 只删除 `projects` 表中的记录。关联的 sessions（`project_id` 被 SET NULL）、frames 记录和磁盘上的 bin 文件都成为孤儿数据，永远不会被清理。

**修复方案：**
删除项目时，先查出关联的 sessions，再删除对应的 frames 和 bin 文件，最后删除 sessions 和 project 记录。

---

### 5.4 [高] persistFrame 使用 INSERT OR REPLACE 可能静默覆盖数据

**文件：** `src/services/StorageService.ts` 第 224 行

**问题描述：**
如果由于 bug 或竞态条件，两帧具有相同的 `(frameId, sessionId)` 主键，第二帧会静默覆盖第一帧的数据，无任何警告。

**修复方案：**
改用 `INSERT OR IGNORE` 并记录警告日志，或在插入前检查唯一性。

---

### 5.5 [中] Session/Project ID 可能冲突

**文件：**
- `src/stores/dataStore.ts` 第 113-119 行
- `src/services/StorageService.ts` 第 95-101 行

**问题描述：**
`generateSessionId` 和 `generateProjectId` 使用秒级时间戳生成 ID。如果在同一秒内创建两个会话或项目，ID 会重复。`ensureSession` 使用 `INSERT OR IGNORE`，第二个会话会静默复用第一个。

**修复方案：**
添加随机后缀（如 4 位随机字母数字），或使用 UUID，或至少使用毫秒精度。

---

### 5.6 [中] CSV 导出未转义特殊字符

**文件：**
- `src/utils/export.ts` 第 28-47 行
- `src/services/StorageService.ts` 第 318-327 行

**问题描述：**
CSV 值直接用逗号拼接，未做引号包裹或转义。虽然数值字段一般不含逗号，但如果 UTC 字符串在某些 locale 下包含逗号，CSV 格式会错乱。

**修复方案：**
对所有字段做标准 CSV 转义：包含逗号、引号或换行的值用双引号包裹，值内的双引号用两个双引号转义。

---

### 5.7 [中] 帧计数器内存与数据库可能不同步

**文件：** `src/stores/dataStore.ts` 第 38-41 行

**问题描述：**
`nextFrameId` 在 zustand 内存中递增，`persistFrame` 通过 `void` 异步调用（fire-and-forget）。如果 `persistFrame` 失败（如磁盘满），内存中计数器继续递增但数据库缺少对应帧，间隙永远不会被检测到。

**修复方案：**
`await persistFrame` 或至少处理其 rejection（记录错误，通知用户，重试）。

---

### 5.8 [中] 导入项目时临时文件清理为 fire-and-forget

**文件：** `src/services/StorageService.ts` 第 430 行

**问题描述：**
`FileSystem.deleteAsync(tmpPath, ...).catch(() => {})` 静默忽略清理失败。多次失败的导入会在设备上累积临时文件。

**修复方案：**
跟踪临时文件并定期清理，或 await 删除操作并在失败时记录。

---

### 5.9 [低] parseAck 读取 reason 字符串时未检查 payload 长度

**文件：** `src/protocol/parser.ts` 第 189-195 行

**问题描述：**
reason 字符串从 payload offset 22 开始读取，但没有检查 `payload.length >= 22`。如果设备返回一个短 ACK 包，循环会读取到 `undefined`，`String.fromCharCode(undefined)` 得到乱码。

**修复方案：**
在读取前检查 `if (payload.length < reasonOffset) return { result, reason: '' }`。

---

## 修复状态追踪

### ✅ 已修复
| # | 问题 | 修复日期 |
|---|------|---------|
| 1.1 | 调试日志加 `__DEV__` 门控 | 2026-06-20 |
| 1.2 | IP/端口格式校验 | 2026-06-20 |
| 1.3 | 心跳失败触发重连 | 2026-06-20 |
| 1.5 | connect 防重复连接 | 2026-06-20 |
| 1.6 | 分帧状态断线重置 | 2026-06-20 |
| 1.9 | filePrefix 动态日期 | 2026-06-20 |
| 2.1 | ACK 竞态覆盖保护 | 2026-06-20 |
| 2.2 | TCP 连接 10s 超时 | 2026-06-20 |
| 2.3 | clearHistory 文案明确 | 2026-06-20 |
| 2.4 | 分享不可用提示 | 2026-06-20 |
| 2.5 | 操作前检查连接 | 2026-06-20 |
| 2.6 | 项目名加载加 catch | 2026-06-20 |
| 3.1 | 日志泄露（同 1.1） | 2026-06-20 |
| 3.2 | 项目名文件安全过滤 | 2026-06-20 |
| 4.1 | 热路径调试计算（同 1.1） | 2026-06-20 |
| 4.2 | Parser 溢出加 onOverflow 回调 | 2026-06-20 |
| 4.3 | MAX_HISTORY 降至 50 | 2026-06-20 |
| 4.5 | 降采样改 min-max 保留极值 | 2026-06-20 |
| 4.6 | PeakRow 已删除 | 2026-06-20 |
| 5.1 | importProject gain 用 AMP_GAIN | 2026-06-20 |
| 5.2 | 数据库迁移 PRAGMA user_version | 2026-06-20 |
| 5.3 | 删项目级联清理文件 | 2026-06-20 |
| 5.4 | INSERT OR REPLACE → IGNORE | 2026-06-20 |
| 5.5 | ID 加毫秒+随机后缀 | 2026-06-20 |
| 5.9 | parseAck 长度检查 | 2026-06-20 |

### ⏳ 暂不修（低风险或需大重构）
| # | 问题 | 原因 |
|---|------|------|
| 1.4 | 数据库查询 `any` 类型 | 纯代码规范 |
| 1.7 | clearTimeout `!` 断言 | 运行无影响 |
| 1.8 | 缺返回类型注解 | 纯代码规范 |
| 2.7 | CSV 只导内存帧 | 需重构导出 |
| 2.8 | 大项目导出无进度 | UX 优化级 |
| 3.3 | GPS 明文存储 | 行业仪器通常不要求 |
| 3.4 | filePrefix 字符过滤 | 仅用于协议包 |
| 4.4 | 缩放重建 Path | 性能优化级 |
| 4.7 | Base64 中间字符串 | 优化级 |
| 5.6 | CSV 未转义 | 纯数值低风险 |
| 5.7 | 帧计数不同步 | 需重构存储层 |
| 5.8 | 临时文件清理 | 低风险 |

### 原修复优先级建议（已完成）

~~### 第一批（发布前必须修复）~~
1. ~~移除所有调试日志（1.1 + 3.1 + 4.1）~~
2. ~~修复 importProject gain 字段（5.1）~~
3. ~~降低内存历史上限或改为按需加载（4.3）~~
4. ~~修复 ACK 竞态条件（2.1）~~

~~### 第二批（发布前应该修复）~~
5. ~~添加 TCP 连接超时（2.2）~~
6. ~~心跳失败处理（1.3）~~
7. 操作前检查连接状态（2.5）
8. 数据库迁移版本管理（5.2）
9. 删除项目级联清理（5.3）
10. IP/端口校验（1.2）
11. 项目名文件安全过滤（3.2）
12. 分享不可用提示（2.4）
13. Parser 缓冲区溢出通知（4.2）

### 第三批（后续迭代优化）
14. Skia 矩阵变换优化波形缩放（4.4）
15. Min-max 降采样算法（4.5）
16. Session ID 防冲突（5.5）
17. CSV 转义（5.6）
18. 其余低优先级项
