232 lines
7.8 KiB
Python
232 lines
7.8 KiB
Python
"""
|
||
工作手机SDK v3.0 - 设备管理服务(含设备指纹校验)
|
||
"""
|
||
|
||
import hashlib
|
||
from typing import Optional, List, TYPE_CHECKING
|
||
|
||
try:
|
||
from motor.motor_asyncio import AsyncIOMotorClient
|
||
except ImportError: # NAS 无 Mongo 依赖时降级
|
||
AsyncIOMotorClient = None # type: ignore[misc, assignment]
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
from config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class DeviceManager:
|
||
"""设备管理器"""
|
||
|
||
def __init__(self):
|
||
self.client: Optional[AsyncIOMotorClient] = None
|
||
self.db = None
|
||
|
||
async def init(self):
|
||
"""初始化数据库连接(MongoDB 不可用时降级为无 DB 模式)"""
|
||
if AsyncIOMotorClient is None:
|
||
logger.warning("motor 未安装,MongoDB 功能降级为无 DB 模式")
|
||
return
|
||
try:
|
||
self.client = AsyncIOMotorClient(
|
||
settings.MONGO_URI,
|
||
serverSelectionTimeoutMS=3000, # 3秒超时,不阻塞启动
|
||
)
|
||
self.db = self.client[settings.MONGO_DB]
|
||
|
||
# 尝试创建索引
|
||
try:
|
||
await self.db.devices.create_index("device_id", unique=True)
|
||
await self.db.commands.create_index("device_id")
|
||
await self.db.commands.create_index("created_at")
|
||
logger.info(f"MongoDB连接成功: {settings.MONGO_DB}")
|
||
except Exception as e:
|
||
logger.warning(f"MongoDB索引创建跳过(可能需要认证): {e}")
|
||
logger.info(f"MongoDB连接成功(无索引): {settings.MONGO_DB}")
|
||
except Exception as e:
|
||
logger.warning(f"MongoDB 连接失败(降级为无DB模式,ADB 直连仍可用): {e}")
|
||
self.client = None
|
||
self.db = None
|
||
|
||
async def close(self):
|
||
"""关闭连接"""
|
||
if self.client:
|
||
self.client.close()
|
||
|
||
# ========== 设备管理 ==========
|
||
|
||
async def register_device(self, device_data: dict) -> dict:
|
||
"""注册或更新设备(自动计算并校验指纹)"""
|
||
device_id = device_data.get("device_id")
|
||
|
||
fp_hash = self.compute_fingerprint(device_data)
|
||
device_data["fingerprint_hash"] = fp_hash
|
||
|
||
if self.db is None:
|
||
return {"device_id": device_id, "updated": False, "fingerprint": fp_hash}
|
||
|
||
device_data["updated_at"] = datetime.now()
|
||
|
||
result = await self.db.devices.update_one(
|
||
{"device_id": device_id},
|
||
{
|
||
"$set": device_data,
|
||
"$setOnInsert": {"created_at": datetime.now()}
|
||
},
|
||
upsert=True
|
||
)
|
||
|
||
collision = await self.check_fingerprint_collision(device_id, device_data)
|
||
if collision["collision"]:
|
||
logger.warning(
|
||
f"[防封] 注册设备 {device_id} 指纹碰撞: {collision['collided_with']}"
|
||
)
|
||
|
||
return {
|
||
"device_id": device_id,
|
||
"updated": result.modified_count > 0,
|
||
"fingerprint": fp_hash,
|
||
"fingerprint_collision": collision["collision"],
|
||
"collided_with": collision.get("collided_with", []),
|
||
}
|
||
|
||
async def get_device(self, device_id: str) -> Optional[dict]:
|
||
"""获取设备信息"""
|
||
if self.db is None:
|
||
return None
|
||
return await self.db.devices.find_one({"device_id": device_id}, {"_id": 0})
|
||
|
||
async def get_all_devices(self, skip: int = 0, limit: int = 100) -> List[dict]:
|
||
"""获取所有设备"""
|
||
if self.db is None:
|
||
return []
|
||
cursor = self.db.devices.find({}, {"_id": 0}).skip(skip).limit(limit)
|
||
return await cursor.to_list(length=limit)
|
||
|
||
async def update_device_status(self, device_id: str, status: str):
|
||
"""更新设备状态"""
|
||
if self.db is None:
|
||
return
|
||
await self.db.devices.update_one(
|
||
{"device_id": device_id},
|
||
{"$set": {"status": status, "updated_at": datetime.now()}}
|
||
)
|
||
|
||
async def update_heartbeat(self, device_id: str):
|
||
"""更新设备心跳时间(设备状态实时反馈到服务器/数据库)"""
|
||
if self.db is None:
|
||
return
|
||
now = datetime.now()
|
||
await self.db.devices.update_one(
|
||
{"device_id": device_id},
|
||
{"$set": {"last_heartbeat": now, "status": "online", "updated_at": now}},
|
||
upsert=True
|
||
)
|
||
|
||
# ========== 设备指纹校验(防封 AF11) ==========
|
||
|
||
@staticmethod
|
||
def compute_fingerprint(info: dict) -> str:
|
||
"""从设备上报信息计算指纹哈希(MD5)"""
|
||
keys = sorted([
|
||
"brand", "model", "manufacturer", "android_version",
|
||
"sdk_version", "serial", "imei", "mac", "bluetooth_mac",
|
||
"screen_width", "screen_height", "density",
|
||
"cpu_abi", "fingerprint", "android_id",
|
||
])
|
||
parts = []
|
||
for k in keys:
|
||
v = info.get(k, "")
|
||
if v:
|
||
parts.append(f"{k}={v}")
|
||
raw = "|".join(parts)
|
||
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
||
|
||
async def check_fingerprint_collision(self, device_id: str, info: dict) -> dict:
|
||
"""
|
||
检查设备指纹是否与其他设备碰撞。
|
||
返回 {"collision": bool, "collided_with": [device_ids...]}
|
||
"""
|
||
fp = self.compute_fingerprint(info)
|
||
|
||
if self.db is None:
|
||
return {"collision": False, "collided_with": [], "fingerprint": fp}
|
||
|
||
try:
|
||
await self.db.devices.update_one(
|
||
{"device_id": device_id},
|
||
{"$set": {"fingerprint_hash": fp, "fingerprint_info": info}},
|
||
upsert=True,
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"指纹写入失败: {e}")
|
||
|
||
try:
|
||
cursor = self.db.devices.find(
|
||
{"fingerprint_hash": fp, "device_id": {"$ne": device_id}},
|
||
{"device_id": 1, "_id": 0},
|
||
)
|
||
collisions = [doc["device_id"] async for doc in cursor]
|
||
except Exception as e:
|
||
logger.warning(f"指纹碰撞查询失败: {e}")
|
||
collisions = []
|
||
|
||
if collisions:
|
||
logger.warning(
|
||
f"[防封] 设备指纹碰撞! {device_id} 与 {collisions} 指纹相同 (hash={fp})"
|
||
)
|
||
|
||
return {
|
||
"collision": len(collisions) > 0,
|
||
"collided_with": collisions,
|
||
"fingerprint": fp,
|
||
}
|
||
|
||
# ========== 命令日志 ==========
|
||
|
||
async def log_command(
|
||
self,
|
||
device_id: str,
|
||
command_type: str,
|
||
params: dict,
|
||
result: dict
|
||
):
|
||
"""记录命令执行日志"""
|
||
if self.db is None:
|
||
return
|
||
try:
|
||
await self.db.commands.insert_one({
|
||
"device_id": device_id,
|
||
"command_type": command_type,
|
||
"params": params,
|
||
"result": result,
|
||
"created_at": datetime.now()
|
||
})
|
||
except Exception as e:
|
||
# Mongo 无权限/不可用时不阻断主流程
|
||
logger.warning(f"命令日志写入失败(已跳过): {e}")
|
||
|
||
async def get_command_logs(
|
||
self,
|
||
device_id: str,
|
||
limit: int = 50
|
||
) -> List[dict]:
|
||
"""获取命令日志"""
|
||
if self.db is None:
|
||
return []
|
||
try:
|
||
cursor = self.db.commands.find(
|
||
{"device_id": device_id},
|
||
{"_id": 0}
|
||
).sort("created_at", -1).limit(limit)
|
||
return await cursor.to_list(length=limit)
|
||
except Exception as e:
|
||
logger.warning(f"命令日志查询失败(已跳过): {e}")
|
||
return []
|
||
|
||
|
||
# 全局实例
|
||
device_manager = DeviceManager()
|