feat: 设备身份改为「名称 + 指纹」——更换 IP 自动认领,分组/任务引用自动同步
背景:设备池原先拿 serial(IP)当身份。设备一换 IP(DHCP 重新分配)旧记录就成了连不上的
僵尸条目(表现为"断联·自动重连中"但设备并没关机),分组与 serial 模式的任务还吊着死地址。
2026-09-11 实际发生:.70 变成 .71、.72 消失,平台两个条目永远连不上。
实现
- **设备名称必填且唯一**:加入设备必须填名称;库层面用部分唯一索引兜底
(ux_device_name / ux_device_fingerprint,WHERE 非空 → 兼容历史空值),管理页可改名
- **设备指纹**(ro.serialno):网络设备添加/确认/扫描/采集型号时自动读取;
身份三层拆分——名称(人可读,稳定)、指纹(机器识别,稳定)、serial(当前地址,可变)
- **自动认领**:添加或确认设备时指纹命中池中已有记录 → 迁移原记录到新地址
(名称/型号/备注/启用状态/添加时间全保留),不新增条目
- **人工认领** `POST /api/devices/pool/relocate`:旧地址已断联、指纹没采过时的兜底——
人工指认"这条就是那台,现在在 X",迁移并同步引用
- **引用同步**:认领/迁址时把 device_group.serials 与 task_job.target.serial 的旧地址
换成新地址。⚠️ 必须同时改**内存**:分组/任务在 TaskManager 里另有内存副本且调度用内存对象,
只改库不重启不生效 → device_pool 迁址后回调 TaskManager.sync_device_serial
(装配层用 set_move_hook 注册;device_pool 不能反向 import task_manager,会循环依赖)
- **前端**:待连接池新增「识别」列(指纹命中时提示"≈ 名称(原 IP)",按钮变「认领为 X」);
设备池新增「名称/指纹」列与「改名/换地址」操作;断联设备表也加「换地址」(用户看到断联就在这里)
- 添加设备接口改用 adb_connect_light(单次短超时),避免不可达 IP 让请求卡 30s+;重名校验提前到 adb 之前
文档:API.md §6 重写(设备身份/认领/新接口)、DATA_MODEL.md(新列 + v5/v6 迁移 + 唯一索引)、
ARCHITECTURE.md §4.0(身份三层与引用同步的内存坑)、DEPLOY.md 排查表加"断联但没关机"条目
自测(全通过):名称必填/唯一/改名/重名拒绝(含库层面约束);指纹采集(真实读到 .71 的
gy7lskwkkvj7c6b6);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
This commit is contained in:
+18
-1
@@ -105,6 +105,7 @@
|
||||
| `enabled` | Boolean | `True` | 停用则不参与调度 |
|
||||
| `note` | Text | `""` | |
|
||||
| `created_at` | String(20) | `""` | |
|
||||
| `fingerprint` | String(120) | `""` | **设备指纹**(`ro.serialno`,迁移 v5 追加):同一台物理设备换 IP 后据此认领回原记录 |
|
||||
|
||||
### 2.7 `pending_device` — 待确认的发现设备
|
||||
|
||||
@@ -113,6 +114,7 @@
|
||||
| `serial` | String(120) | — | 主键 |
|
||||
| `source` | String(20) | `""` | `lan` / `tailscale` |
|
||||
| `first_seen` / `last_seen` | String(20) | `""` | 时间串 |
|
||||
| `fingerprint` | String(120) | `""` | 扫描时读取的设备指纹(迁移 v6 追加),用于提示"这是已有设备换了地址" |
|
||||
|
||||
> ⚠️ SQLAlchemy 模型的 `default=` 是 **Python 侧默认值**,SQLite 建表语句里没有 `DEFAULT` 子句;只有原生建表的表才有真正的 SQL DEFAULT。
|
||||
|
||||
@@ -152,6 +154,8 @@
|
||||
| 2 | 建 `device` 表(**无 `model` 列**) | 幂等(`IF NOT EXISTS`) |
|
||||
| 3 | `device` 增加 `model` | `ALTER TABLE`(同上兜底) |
|
||||
| 4 | 建 `pending_device` 表 | 幂等 |
|
||||
| 5 | `device` 增加 `fingerprint`(设备指纹) | `ALTER TABLE`(靠 duplicate column 兜底) |
|
||||
| 6 | `pending_device` 增加 `fingerprint` | 同上 |
|
||||
|
||||
执行器 `_migrate_schema()`:
|
||||
|
||||
@@ -161,7 +165,20 @@
|
||||
4. 每条迁移后 `INSERT OR REPLACE app_meta('schema_version', v)` 并提交
|
||||
5. 整体包 try:失败只记 error 日志,**不阻塞启动**
|
||||
|
||||
### 4.2 旧 JSON 迁移(一次性)
|
||||
### 4.2 唯一索引(设备身份)
|
||||
|
||||
`_migrate_schema()` 末尾会补建两个**部分唯一索引**(幂等,与版本号无关,老库升级时也会补):
|
||||
|
||||
| 索引 | 作用 |
|
||||
|------|------|
|
||||
| `ux_device_name` | 设备**名称唯一**(`WHERE name <> ''`,兼容历史空名) |
|
||||
| `ux_device_fingerprint` | **一台物理设备在池中只有一条记录**(`WHERE fingerprint <> ''`) |
|
||||
|
||||
> 用**部分索引**而不是表级约束:历史数据可能有空名称/空指纹,`<> ''` 让它们不参与唯一性判断。
|
||||
> 若历史数据里已有重复(建索引失败),只告警不回滚、不阻塞启动——约束从此刻起对新数据生效,
|
||||
> 老的重复行由管理页「改名」处理。
|
||||
|
||||
### 4.3 旧 JSON 迁移(一次性)
|
||||
|
||||
启动时若存在 `data/groups.json` / `data/jobs.json`:对应表为空则导入,随后把文件重命名为 `<name>.json.migrated` 归档。**库非空但 JSON 仍在 → 直接归档**,防止"用户删空数据后重启又复原"。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user