308 lines
20 KiB
Markdown
308 lines
20 KiB
Markdown
# yack-mini 小程序 Code Review
|
||
|
||
> 评审范围:`yack-mini/miniprogram/**`(业务代码约 6.0k 行,不含第三方 `libs/mqtt-wx.min.js` 887 行)
|
||
> 评审日期:2026-09-27
|
||
> 结论:**架构分层清晰、BLE 状态机工程质量高**,主要债务集中在「页面层的脏边界」与「重构后未清理的死代码」。
|
||
|
||
## 一、总体评价
|
||
|
||
| 层 | 文件 | 评价 |
|
||
| --- | --- | --- |
|
||
| BLE 状态机 | `services/ble.js` (1213) | ⭐ 优秀。把微信 BLE 的三条硬约束(already connect / 并发建链 / 切设备作废)用 seq + 单 Promise 闸门建模,每条约束都有注释说明来源,超时兜底齐全。全项目最扎实的模块 |
|
||
| 采集网关 | `services/gateway.js` (872) | ⭐ 良好。串行队列 `_chain` + 状态快照订阅 + 收帧快照归属(测点/productId/reportSn)设计正确,注释解释了"为什么"而非"是什么" |
|
||
| MQTT 包装 | `services/mqtt-client.js` (491) | 良好。实例校验防幽灵客户端、建连去重、用户意图标记都到位 |
|
||
| 页面层 | `pages/**` | ⚠️ 一般。脏边界集中在此:参数无校验、高频 setData、过期注释 |
|
||
| 工具层 | `utils/**` | ⚠️ 一般。死函数多,且存在一处会损坏数据的编码缺陷 |
|
||
| 回归测试 | `tests/**` | 良好质量,但**覆盖缺口大**:只锁了"设备停用不连蓝牙"和"三个开关"两个点,主链路(收帧→上报→离线补传)零覆盖 |
|
||
|
||
---
|
||
|
||
## 二、P0 — 会导致功能错误或数据损坏,必修
|
||
|
||
> ✅ **以下 5 条已于 2026-09-27 全部修复**,回归用例见 `tests/p0-regressions.test.js`(45 断言,含反向验证:
|
||
> 回退成缺陷写法后 5 项转红)。改动文件:`pages/mine/mine.js`、`utils/storage.js`、`pages/device/scan/scan.js`、
|
||
> `utils/crypto.js`、`services/mqtt-client.js`。P1 及以下未动。
|
||
|
||
### P0-1 产品ID 用 `parseInt` 无整数校验,NaN 会写进上报 topic
|
||
|
||
- 位置:`pages/mine/mine.js:98`、`mine.js:103`
|
||
- 根因:`parseInt('12.5') === 12`(静默截断小数)、`parseInt('abc') === NaN`。`if (!productId)` 只挡空串,NaN 能一路穿到 `storage.setMqttConfig({ productId: NaN })`。后续 `gateway._resolveProductId()` 的判空是 `cfg.productId == null || cfg.productId === ''`,**NaN 两个都不满足**,于是构造出 `/NaN/{sn}/{测点}/measure/post` 这样的脏 topic 并持续上报。
|
||
- 修复:
|
||
|
||
```js
|
||
// mine.js handleMqttConnect()
|
||
const pid = Number(productId);
|
||
if (!Number.isInteger(pid) || pid <= 0) {
|
||
wx.showToast({ title: '产品ID必须是正整数', icon: 'none' });
|
||
return;
|
||
}
|
||
const creds = auth.getMqttCredentials(serialNumber, pid, productSecret);
|
||
...
|
||
storage.setMqttConfig({ serialNumber, productId: pid, productSecret });
|
||
```
|
||
|
||
### P0-2 设备数达上限时 `addDevice` 静默失败,UI 谎报"已连接"
|
||
|
||
- 位置:`utils/storage.js:132-139`(超限 `return`,无返回值)+ `pages/device/scan/scan.js:145-160`
|
||
- 根因:`addDevice` 成功路径没有 `return`,超限路径 `return undefined`。scan 页不检查返回值,继续执行 `activateDevice`(找不到该 SN,空操作)→ `getDevice` 返回 null → `gateway.start(deviceConfig)` 传入一个**根本没落盘**的设备 → `_startNow` 里 `_isEnabled()` 为 false → 不采集。用户看到 toast "已连接设备",实际既没保存也没连。
|
||
- 修复(两处):
|
||
|
||
```js
|
||
// storage.js
|
||
function addDevice(device) {
|
||
const devices = getDeviceList();
|
||
const index = devices.findIndex(d => d.serialNumber === device.serialNumber);
|
||
const encryptedDevice = encryptFields(device, ENCRYPT_FIELDS);
|
||
if (index >= 0) {
|
||
devices[index] = encryptedDevice;
|
||
} else {
|
||
if (devices.length >= LIMITS.MAX_DEVICE_COUNT) {
|
||
console.warn('[Storage] 设备数量已达上限:', LIMITS.MAX_DEVICE_COUNT);
|
||
return false; // ← 明确失败语义
|
||
}
|
||
devices.push(encryptedDevice);
|
||
}
|
||
saveDeviceList(devices);
|
||
return true;
|
||
}
|
||
|
||
// scan.js handleSelectDevice()
|
||
if (!storage.addDevice(deviceConfig)) {
|
||
wx.hideLoading();
|
||
wx.showToast({ title: `最多添加 ${50} 个设备`, icon: 'none' });
|
||
return;
|
||
}
|
||
storage.activateDevice(serialNumber);
|
||
```
|
||
|
||
### P0-3 Base64 编码用 `setUint8` 静默截断,非 ASCII 密钥加密后不可逆损坏
|
||
|
||
- 位置:`utils/crypto.js:19-24`
|
||
- 根因:`dataView.setUint8(i, str.charCodeAt(i))` 只写低 8 位。XOR 后码点可能 > 255(例如中文 `0x4E2D` XOR `0x66` = `0x4E4B` = 20043),被静默截断 → 解密得到乱码 → **产品密钥永久损坏且无报错**。密钥通常纯 ASCII 所以没暴露,但这是"输入一旦含非 Latin-1 字符就毁数据"的定时炸弹。
|
||
- 修复:
|
||
|
||
```js
|
||
function base64Encode(str) {
|
||
try {
|
||
const bytes = new Uint8Array(str.length);
|
||
for (let i = 0; i < str.length; i++) {
|
||
const code = str.charCodeAt(i);
|
||
if (code > 0xFF) {
|
||
console.error('[Crypto] 含非 Latin-1 字符,拒绝编码:', i);
|
||
return '';
|
||
}
|
||
bytes[i] = code;
|
||
}
|
||
return wx.arrayBufferToBase64(bytes.buffer);
|
||
} catch (e) {
|
||
console.error('[Crypto] Base64 编码失败:', e);
|
||
return '';
|
||
}
|
||
}
|
||
```
|
||
|
||
> 附带:`encrypt/decrypt` 异常时 **fail-open 返回原文**(`crypto.js:71`、`crypto.js:94`),会把明文当密文落盘。建议改为返回空串并让调用方拒绝写入。
|
||
|
||
### P0-4 切换 clientId 时订阅表被清空,`_resubscribe` 恢复不到任何东西
|
||
|
||
- 位置:`services/mqtt-client.js:177-181` vs `mqtt-client.js:409-417`
|
||
- 根因:`connect()` 在拆旧客户端时 `this.subscriptions.clear()`,而 `_resubscribe()` 只在 `client.on('connect')` 里执行 —— 执行时表已空。**任何一次换 clientId 重连都会永久丢失全部订阅**。当前业务只 publish 不 subscribe,所以未暴露;一旦接入下行指令就是线上事故。
|
||
- 修复:把"订阅表"的生命周期从"连接"解耦 —— 只在 `disconnect()`(用户意图)时清,换 clientId 时保留:
|
||
|
||
```js
|
||
// connect() 内
|
||
if (this.client) {
|
||
console.log('[MQTT] 终止旧客户端:', this.currentClientId, '->', newClientId);
|
||
this._teardownClient(this.client); // 保留 this.subscriptions,交由 _resubscribe 恢复
|
||
}
|
||
```
|
||
|
||
### P0-5 离线补传串行 `await publish`,publish 无超时 → 一次不结算就永久堵死
|
||
|
||
- 位置:`services/gateway.js:733-772`(`_flushOffline`)+ `services/mqtt-client.js:332-349`(`publish`)
|
||
- 根因:`_flushOffline` 用 `await mqttClient.publish(...)` 逐条串行发送,且被 `_flushingOffline` 布尔闸门保护。而 `publish` 的 resolve/reject **完全依赖底层回调**:QoS1 的回调要等 PUBACK 或重试用尽。若底层在 ws 已断、`_inflight` 条目回调路径异常时永不结算,`_flushingOffline` 将永远停在 `true`(`finally` 永远执行不到)→ **此后所有离线补传静默失效,直到重启小程序**。BLE 侧所有 GATT 操作都做了超时兜底,唯独 publish 没有,属于防御不一致。
|
||
- 修复:给 publish 加超时出口(与 BLE 的 `_callWithTimeout` 同一模式):
|
||
|
||
```js
|
||
// mqtt-client.js
|
||
publish(topic, message, options = { qos: 1 }) {
|
||
const timeout = options.publishTimeout != null ? options.publishTimeout : 15000;
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.client || !this.client.connected) {
|
||
reject(new Error('MQTT 未连接'));
|
||
return;
|
||
}
|
||
let settled = false;
|
||
const timer = setTimeout(() => {
|
||
if (settled) return;
|
||
settled = true;
|
||
reject(new Error('发布超时'));
|
||
}, timeout);
|
||
this.client.publish(topic, message, options, (err) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
if (err) { console.error('[MQTT] 发布失败:', err); reject(new Error('发布失败')); }
|
||
else { resolve(); }
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
> 另外 `_flushOffline` 建议加并发上限或分批,避免 1000 条缓存时逐条串行 await 拖很久。
|
||
|
||
---
|
||
|
||
## 三、P1 — 隐患 / 性能 / 一致性
|
||
|
||
### P1-1 通知风暴:每次 `_notify` 全量构造快照,页面再全量重建列表
|
||
|
||
- 位置:`services/gateway.js:820-829`(`getSnapshot` 里 `dataList.map(it => ({...it}))`,最多 200 条)+ `pages/device/detail/detail.js:190-196`(`slice().reverse().map()`,ASCII 模式下每条再跑一次 `hexToBytes`)
|
||
- 根因:收帧节流 100ms(`TIMEOUT.FLUSH_INTERVAL`),即每秒 10 次 × 200 条对象浅拷贝 × N 个订阅者,ASCII 视图下还要叠加 200 次 hex 解析。高频采集时主线程和 setData 都会明显吃紧。
|
||
- 建议:① `_notify` 合并到同一帧(`setTimeout(..., 16)` 去重,状态类更新仍可即时);② `getSnapshot` 增加 `dataListSince(id)` 增量接口,页面只 setData 新增部分;③ ASCII 转换结果缓存到 item 上(`item._ascii`),避免每次重算。
|
||
|
||
### P1-2 「我的」页在每次网关通知时同步读全量离线缓存
|
||
|
||
- 位置:`pages/mine/mine.js:165`(`checkStatus()` 内 `storage.getPendingData()`)
|
||
- 根因:`getPendingData()` 会做过期过滤 + 写回(同步 IO)。mine 页订阅了 gateway,收帧期间每 100ms 触发一次 → 每 100ms 一次全量读盘 + 可能的写盘。而 gateway 快照里**已经有 `offlineCount`**,这次读取完全多余。
|
||
- 修复:`const pendingCount = snap.offlineCount;`
|
||
|
||
### P1-3 `_upload` 无并发上限
|
||
|
||
- 位置:`services/gateway.js:596`
|
||
- 根因:自动模式下每批帧逐条 fire-and-forget 调 `_upload`,无在途数限制。MQTT 变慢时会堆积大量 QoS1 在途消息,触发底层的"在途消息过多"错误后又全部转离线缓存,形成雪崩。建议加在途计数器(如上限 20,超出直接进离线缓存)。
|
||
|
||
### P1-4 `_toOfflineItem` 可能返回 null 却被 push 进缓存数组
|
||
|
||
- 位置:`services/gateway.js:721-730` + `gateway.js:599`
|
||
- 根因:`device` 为 null 时返回 null,调用处未过滤 → 缓存里出现 null 元素 → `_flushOffline` 遍历到 `it.deviceId` 抛 TypeError,整批补传中断。改为 `const item = this._toOfflineItem(...); if (item) offline.push(item);`
|
||
|
||
### P1-5 环境配置硬编码,dev 还混搭了两个环境
|
||
|
||
- 位置:`env.config.js:7-20`
|
||
- 根因:`const ENV = 'development'` 是手改开关,发布时漏改就会把生产包指向 `http://192.168.110.117:8080`(内网 IP + 明文 HTTP)。更隐蔽的是 DEV 配置本身自相矛盾:`baseUrl` 是内网 IP,而 `mqttServer` 是公网 `47.121.180.229` —— 同一个"开发环境"里 API 和 MQTT 不在一处,真机联调极易误判"接口通了但数据上不去"。
|
||
- 建议:① 用 `wx.getAccountInfoSync().miniProgram.envVersion` 自动判定环境,彻底取消手改开关;② 生产环境必须 `https://` + `wxs://`(`project.config.json` 里 `urlCheck: true`,HTTP 域名真机必然被拦)。
|
||
|
||
### P1-6 `BLE_STATE.CONNECTED` 是永不出现的状态
|
||
|
||
- 位置:`services/ble.js:141`、`ble.js:1027`、`ble.js:1035`,`services/gateway.js:520`
|
||
- 根因:`_setState(BLE_STATE.CONNECTED)` 在整个项目里**从未被调用过**(状态机只经过 DISCONNECTED → CONNECTING → DISCOVERING → READY)。因此这 4 处的 `|| state === CONNECTED` 全是死分支,`isConnected()` 与 `isReady()` 是两份完全相同的实现。要么删掉 CONNECTED,要么在建链成功时显式置位(让"已连但未发现服务"有独立状态)。
|
||
|
||
### P1-7 超时配置双真相源
|
||
|
||
- 位置:`utils/constants.js:7-8,26` vs `services/ble.js:16-32`
|
||
- 根因:`TIMEOUT.BLE_SCAN` / `TIMEOUT.BLE_CONNECT` / `RETRY.BLE_RECONNECT_DELAY` **全项目无引用**;而 `ble.js` 的 `DEFAULT_CONFIG` 里重新硬编码了相同数值(10000 / 10000 / 重连基数 1000)。改一处不改另一处就会静默失效。
|
||
- 修复:让 `DEFAULT_CONFIG` 从 constants 取值:
|
||
|
||
```js
|
||
const { TIMEOUT, RETRY } = require('../utils/constants.js');
|
||
const DEFAULT_CONFIG = {
|
||
scanTimeout: TIMEOUT.BLE_SCAN,
|
||
connectTimeout: TIMEOUT.BLE_CONNECT,
|
||
reconnectBaseDelay: RETRY.BLE_RECONNECT_DELAY,
|
||
...
|
||
};
|
||
```
|
||
|
||
> 另外 `services/command-manager.js:6` 引入了 `LIMITS` 却从未使用,是无用 require。
|
||
|
||
### P1-8 回归测试只覆盖了两个点,主链路零覆盖
|
||
|
||
- 现状:`tests/gateway-disabled-device.test.js`(33 断言)+ `tests/detail-console-toggles.test.js`(31 断言),质量很高(含反向验证防假绿),但覆盖的是**开关语义**与**设备停用**两条边角。
|
||
- 缺口:完全没有覆盖主链路 —— 收帧 → 测点归属快照 → 自动上报 / 手动确认 / 离线缓存 → 补传。而 P0-2、P0-4、P0-5 全部出在这条链路上。建议按现有 `tests/helpers/fake-wx.js` 的可编程桩,补一套 `tests/gateway-measure-flow.test.js`(MQTT 桩可设 connected / publish hang / publish fail 三种态)。
|
||
|
||
### P1-9 `app.json` 未声明定位相关私有接口
|
||
|
||
- 位置:`app.json`
|
||
- 说明:Android 端 BLE 扫描依赖系统定位授权,且微信要求小程序在后台配置隐私协议并在 `app.json` 用 `requiredPrivateInfos` 声明所用接口。当前文件里没有相关声明,真机(尤其 Android)可能出现"扫描无任何结果且无报错"。建议核对微信后台隐私协议并补齐声明。
|
||
|
||
### P1-10 ⚡(性能类最优先)收帧链路上每 100ms 有 4~6 次同步读盘,且同一 key 被读两遍
|
||
|
||
- 位置:`services/gateway.js:572-577` → `measure-point.js:37,70` + `gateway.js:615,630`
|
||
- 根因:`_flush()` 每 100ms 执行一次,其中:
|
||
|
||
| 调用 | 位置 | 实际动作 |
|
||
| --- | --- | --- |
|
||
| `measurePoint.getCollectMode()` | `gateway.js:572` | `wx.getStorageSync` |
|
||
| `measurePoint.getMeasurePointId()` | `gateway.js:573` | `wx.getStorageSync` |
|
||
| `_resolveProductId()` | `gateway.js:576` → `615` | `getMqttConfig()` = `getStorageSync` + `decryptFields` |
|
||
| `_resolveSerialNumber()` | `gateway.js:577` → `630` | `getMqttConfig()` **再读一遍同一个 key** + 再解密一次 |
|
||
|
||
叠加 `mine.js:165` 的 `getPendingData()`(读盘 + 过期过滤 + 可能写回)与 `gateway.js:832` 的 `_refreshOfflineCount()`,收帧期间每秒约 40~60 次 `getStorageSync`。`wx.getStorageSync` 是**同步阻塞**调用,缓存量大(离线缓存上限 1000 条、设备上限 50 台)时单次可达毫秒级 —— 这正好卡在 BLE 高频收帧的关键路径上。
|
||
- 修复方向:上报上下文(productId / reportSn / 测点ID / 模式)本质是**会话级快照**,不该每次读盘。建议在 `_startNow` 时快照一次,`_flush` 直接读内存:
|
||
|
||
```js
|
||
// gateway.js
|
||
_refreshContext() {
|
||
const cfg = storage.getMqttConfig();
|
||
const dev = this._device;
|
||
this._ctx = {
|
||
productId: (cfg && cfg.productId != null && cfg.productId !== '')
|
||
? cfg.productId : (dev ? dev.productId : null),
|
||
reportSn: (cfg && cfg.serialNumber) ? cfg.serialNumber : (dev ? dev.serialNumber : null),
|
||
measurePointId: measurePoint.getMeasurePointId(),
|
||
mode: measurePoint.getCollectMode(),
|
||
};
|
||
}
|
||
// 失效时机:_startNow / resume / 「我的」页保存 MQTT 配置后 / 详情页改测点ID 后
|
||
```
|
||
|
||
> 注意:测点ID 与模式允许在采集过程中被用户修改(详情页),所以失效时机必须覆盖这两处,否则会退回"归属串点"的老问题。
|
||
|
||
### P1-11 测试桩能力缺口,恰好卡住主链路用例
|
||
|
||
- 位置:`tests/helpers/fake-wx.js:193-229`(`createFakeMqtt`)
|
||
- 根因:假 mqtt-client 的 `publish` **恒 `Promise.resolve()`**,无法模拟 hang / fail;且缺少 `allowAutoConnect()`(`app.js:101` 会调用它);`createWx` 里没有 `wx.request`。这直接导致 P1-8 建议补的主链路用例(含 P0-5 的"publish 永不结算"场景)现在写不出来 —— 桩要先行增强:
|
||
|
||
```js
|
||
// fake-wx.js: createFakeMqtt 增加
|
||
publishMode: 'ok', // 'ok' | 'hang' | 'fail'
|
||
publish: (topic, message) => {
|
||
rec.publishCalls.push({ topic, message });
|
||
if (rec.publishMode === 'hang') return new Promise(() => {}); // 永不结算
|
||
if (rec.publishMode === 'fail') return Promise.reject(new Error('发布失败'));
|
||
return Promise.resolve();
|
||
},
|
||
allowAutoConnect: () => { client.userDisconnected = false; },
|
||
```
|
||
|
||
---
|
||
|
||
## 四、P2 — 死代码与僵尸注释(建议一次性清理)
|
||
|
||
重构(product-device-measurePoint)后遗留较多,用户偏好"僵尸代码及时删"。
|
||
|
||
| 位置 | 内容 | 说明 |
|
||
| --- | --- | --- |
|
||
| `utils/auth.js:61,72,83,91,101` | `buildPropertyPostTopic` / `buildFunctionGetTopic` / `buildPropertyGetTopic` / `checkLoginStatus` / `getCurrentUserId` | 5 个导出函数全无调用方 |
|
||
| `utils/hex.js:111,137` | `crc16` / `verifyCrc16` | 无调用方(且 `verifyCrc16` 用 `===` 比大小写的 CRC,含小写输入会误判失败) |
|
||
| `utils/storage.js:45,81,159` | `removeToken` / `removeUserInfo` / `updateDevice` | 无调用方。⚠️ 其中 `updateDevice` **不加密敏感字段**(与 `addDevice`/`saveDeviceList` 不对称),一旦启用就把 productSecret 明文落盘 —— 要么删掉,要么补 `encryptFields` |
|
||
| `services/command-manager.js:112-170` | `getCommands` / `addCommand` / `updateCommand` / `removeCommand` / `incrementUseCount` / `clearHistory` | 6 个方法全无调用方;预设命令功能随 `pages/device/command` 页一起消亡 |
|
||
| `utils/command-storage.js:13-93,178` | `getCommands` / `saveCommands` / `addCommand` / `updateCommand` / `removeCommand` / `clearAll` | 同上,整层预设命令存储已死 |
|
||
| `services/command-manager.js:22` | `sendCommand(hex, method, device)` 的 `method` 参数 | 僵尸参数,永远是 `'BLE'`,函数体从不读它。建议删参数或改名为 `_`(更彻底:直接删) |
|
||
| `pages/index/index.wxml:4-11` | 注释掉的 `user-bar` 块 | 导致 `index.js:144 handleLogout` 成为死方法。要么恢复 UI,要么连方法一起删 |
|
||
| `pages/device/detail/detail.js:15-16` | 注释声称"「发送命令」页(pages/device/command)保留未删" | ❌ **该页在仓库里根本不存在**。过期注释比没注释更有害,必须更正 |
|
||
| `components/device-card/device-card.wxss:1-51` | `.swipe-wrap` / `.swipe-delete` / `.swipe-delete-text` / `.is-dragging` | 滑动删除交互已改成按钮删除,约 36 行样式残留 |
|
||
| `utils/crypto.js:9,31` + `constants.js:31` | `CRYPTO_KEY` 硬编码 | 注释自己承认"生产应从服务端动态获取"。客户端常量 key 的 XOR 只是防君子,**建议明确降级为"防明文落盘的混淆"并在注释里写清,不要给人安全错觉** |
|
||
|
||
---
|
||
|
||
## 五、P3 — 打磨项
|
||
|
||
1. `pages/device/detail/detail.js:444-451`:radio 取消用 `setData({collectMode: 0})` + `setTimeout(0)` 回拉,是绕过小程序受控组件限制的 hack。可行但脆弱,建议在注释里写明依赖的是"radio 内部选中态先于回调切换"这一未文档化行为。
|
||
2. `services/gateway.js`:`_flushOffline` 成功补传后不回写 `dataList` 里对应 item 的 `offline` 标记,UI 上"已缓存"标签会残留到下次清空。
|
||
3. `pages/device/detail/detail.js:492-499`:`handleFlushOffline` 用前后差算条数,若被 `_flushingOffline` 短路会显示"已上传 0 条",误导用户。建议用返回值条数而非快照差。
|
||
4. `pages/device/scan/scan.js:26`:`gateway.stop()` 的注释写"暂停正在跑的网关",实际是完整 teardown(phase → STOPPED)。命名与语义不符,建议改注释或提供真正的 `suspend`。
|
||
5. `services/ble.js:305`:`scanDevices` 的超时定时器无句柄,`stopScan()` 无法取消它 → 点"停止"后 10s 到点仍会回填扫描结果并覆盖 UI。建议保存 timer 并在 `stopScan` 里 clear,或给扫描加 seq 作废。
|
||
6. `pages/mine/mine.js:18`:`appVersion: '1.0.0'` 硬编码,与 `project.config.json` 无联动,发版必漏改。
|
||
7. `utils/request.js`:未设 `timeout`(默认 60s),登录流程弱网下会长时间 loading;401 分支只 reject 不做登录态收敛(与 `app.isLoggedIn()` 只看 userId 的设计一致,但两处口径应写在同一处注释里)。
|
||
8. `components/device-card/device-card.wxml:8`:启用但未连上显示"连接中",实际可能是"异常/未连接",语义偏乐观。
|
||
|
||
---
|
||
|
||
## 六、本次未验证项(需真机确认)
|
||
|
||
- 真机上 MQTT 底层 QoS1 在 ws 断连时的回调是否必然结算(决定 P0-5 是"防御性加固"还是"已发生的事故")。
|
||
- Android 端 BLE 扫描在缺少定位声明时的实际表现(P1-9)。
|
||
- 200 条 × 10 次/秒 setData 在中低端机上的实际帧率(P1-1)。
|