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