TriloopTem_App/docs/commercial-audit.md
2026-06-20 21:43:09 +08:00

559 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. 其余低优先级项