2026-01-22 15:14:27 +08:00
|
|
|
|
import 'dart:async';
|
2026-01-29 16:51:06 +08:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
2026-01-22 15:14:27 +08:00
|
|
|
|
import 'package:logging/logging.dart';
|
2026-01-29 16:51:06 +08:00
|
|
|
|
|
2026-01-22 15:14:27 +08:00
|
|
|
|
import 'mining_task_info.dart';
|
|
|
|
|
|
import '../utils/path_utils.dart';
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 挖矿任务持久化服务(基于 .log 文件,而非 SQLite)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 设计约定:
|
|
|
|
|
|
/// - 使用 `bin/mining_tasks.log` 记录当前(或最近)挖矿任务,一行一条 JSON 记录。
|
|
|
|
|
|
/// - 每次收到新的挖矿任务时,追加一条记录。
|
|
|
|
|
|
/// - 挖矿任务完成后,从 .log 中删除对应记录。
|
|
|
|
|
|
/// - 客户端启动时读取 .log:
|
|
|
|
|
|
/// - 如果存在任务且 `endTimestamp` 尚未过期,则恢复该任务;
|
|
|
|
|
|
/// - 如果已过期,则删除该记录,并不恢复。
|
2026-01-22 15:14:27 +08:00
|
|
|
|
class DatabaseService {
|
|
|
|
|
|
static final DatabaseService _instance = DatabaseService._internal();
|
|
|
|
|
|
factory DatabaseService() => _instance;
|
|
|
|
|
|
DatabaseService._internal();
|
|
|
|
|
|
|
|
|
|
|
|
final Logger _logger = Logger('DatabaseService');
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 日志文件路径:bin/mining_tasks.log
|
|
|
|
|
|
File get _logFile => File(PathUtils.binFile('mining_tasks.log'));
|
|
|
|
|
|
|
|
|
|
|
|
/// 初始化(确保 bin 目录存在)
|
2026-01-22 15:14:27 +08:00
|
|
|
|
Future<void> initialize() async {
|
|
|
|
|
|
try {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
// 确保 bin 目录存在
|
|
|
|
|
|
final binDir = Directory(PathUtils.binDir);
|
|
|
|
|
|
if (!await binDir.exists()) {
|
|
|
|
|
|
await binDir.create(recursive: true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 日志文件可懒创建,不强制在这里创建
|
|
|
|
|
|
_logger.info('任务日志初始化完成(使用文件存储,不再使用 SQLite)');
|
2026-01-22 15:14:27 +08:00
|
|
|
|
} catch (e) {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
_logger.severe('任务日志初始化失败: $e');
|
2026-01-22 15:14:27 +08:00
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 插入挖矿任务(在 .log 文件中追加一条记录)
|
2026-01-22 15:14:27 +08:00
|
|
|
|
Future<int> insertMiningTask(MiningTaskInfo task) async {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
await initialize();
|
2026-01-22 15:14:27 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
2026-01-29 16:51:06 +08:00
|
|
|
|
final record = <String, dynamic>{
|
|
|
|
|
|
'coin': task.coin,
|
|
|
|
|
|
'algo': task.algo,
|
|
|
|
|
|
'pool': task.pool,
|
|
|
|
|
|
'pool_url': task.poolUrl,
|
|
|
|
|
|
'wallet_address': task.walletAddress,
|
|
|
|
|
|
'worker_id': task.workerId,
|
|
|
|
|
|
'pool_user': task.poolUser,
|
|
|
|
|
|
'wallet_mining': task.walletMining,
|
|
|
|
|
|
'end_timestamp': task.endTimestamp,
|
|
|
|
|
|
'miner': task.miner,
|
|
|
|
|
|
'created_at': now,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
final jsonLine = jsonEncode(record);
|
|
|
|
|
|
await _logFile.writeAsString('$jsonLine\n', mode: FileMode.append, flush: true);
|
|
|
|
|
|
|
|
|
|
|
|
return 1; // 返回值目前未被使用,保持兼容即可
|
2026-01-22 15:14:27 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.severe('插入挖矿任务失败: $e');
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 挖矿任务完成后,从 .log 文件中删除该任务
|
|
|
|
|
|
Future<void> finishMiningTask(MiningTaskInfo task) async {
|
|
|
|
|
|
await initialize();
|
2026-01-22 15:14:27 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
if (!await _logFile.exists()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final lines = await _logFile.readAsLines();
|
|
|
|
|
|
if (lines.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
|
|
final List<String> keptLines = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (final line in lines) {
|
|
|
|
|
|
if (line.trim().isEmpty) continue;
|
|
|
|
|
|
try {
|
|
|
|
|
|
final Map<String, dynamic> data = jsonDecode(line) as Map<String, dynamic>;
|
|
|
|
|
|
final existing = _taskFromJson(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果与当前任务匹配,则跳过(即删除)
|
|
|
|
|
|
if (_isSameTask(existing, task)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
keptLines.add(line);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// 解析失败的行保留,避免误删
|
|
|
|
|
|
keptLines.add(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await _logFile.writeAsString(keptLines.join('\n') + (keptLines.isEmpty ? '' : '\n'));
|
2026-01-22 15:14:27 +08:00
|
|
|
|
} catch (e) {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
_logger.severe('完成挖矿任务(从日志中删除)失败: $e');
|
2026-01-22 15:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 加载未完成的挖矿任务
|
2026-01-29 16:51:06 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// - 读取 .log 中所有任务;
|
|
|
|
|
|
/// - 删除已过期(endTimestamp <= now)的任务;
|
|
|
|
|
|
/// - 返回最新的、尚未过期的任务(如果有)。
|
2026-01-22 15:14:27 +08:00
|
|
|
|
Future<MiningTaskInfo?> loadUnfinishedTask() async {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
await initialize();
|
2026-01-22 15:14:27 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
if (!await _logFile.exists()) {
|
2026-01-22 15:14:27 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
final lines = await _logFile.readAsLines();
|
|
|
|
|
|
if (lines.isEmpty) {
|
2026-01-22 15:14:27 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
|
|
|
|
final List<_StoredTask> validTasks = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (final line in lines) {
|
|
|
|
|
|
if (line.trim().isEmpty) continue;
|
|
|
|
|
|
try {
|
|
|
|
|
|
final Map<String, dynamic> data = jsonDecode(line) as Map<String, dynamic>;
|
|
|
|
|
|
final task = _taskFromJson(data);
|
|
|
|
|
|
final createdAt = (data['created_at'] as int?) ?? task.endTimestamp;
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤掉已过期任务
|
|
|
|
|
|
if (now >= task.endTimestamp) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validTasks.add(_StoredTask(task: task, createdAt: createdAt));
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.warning('解析任务日志行失败,已跳过: $e, 原始行: $line');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重新写回仅包含未过期的任务
|
|
|
|
|
|
if (validTasks.isEmpty) {
|
|
|
|
|
|
await _logFile.writeAsString('');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 按创建时间排序,取最新一条作为恢复任务
|
|
|
|
|
|
validTasks.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
|
|
|
|
final toKeep = validTasks;
|
|
|
|
|
|
final buffer = StringBuffer();
|
|
|
|
|
|
for (final t in toKeep) {
|
|
|
|
|
|
final record = _taskToJson(t.task, createdAt: t.createdAt);
|
|
|
|
|
|
buffer.writeln(jsonEncode(record));
|
|
|
|
|
|
}
|
|
|
|
|
|
await _logFile.writeAsString(buffer.toString());
|
|
|
|
|
|
|
|
|
|
|
|
return validTasks.first.task;
|
|
|
|
|
|
}
|
2026-01-22 15:14:27 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.severe('加载挖矿任务失败: $e');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 获取任务历史(目前基于 .log 仅保存“当前/最近”任务,这里返回空列表以保持接口兼容)
|
2026-01-22 15:14:27 +08:00
|
|
|
|
Future<List<Map<String, dynamic>>> getTaskHistory({int limit = 100}) async {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
// 如有需要,可以在未来扩展为持久化历史记录
|
|
|
|
|
|
return [];
|
2026-01-22 15:14:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 16:51:06 +08:00
|
|
|
|
/// 关闭(对文件存储无实际操作,保留接口以兼容旧代码)
|
2026-01-22 15:14:27 +08:00
|
|
|
|
Future<void> close() async {
|
2026-01-29 16:51:06 +08:00
|
|
|
|
// no-op
|
2026-01-22 15:14:27 +08:00
|
|
|
|
}
|
2026-01-29 16:51:06 +08:00
|
|
|
|
|
|
|
|
|
|
// === 内部工具方法 ===
|
|
|
|
|
|
|
|
|
|
|
|
MiningTaskInfo _taskFromJson(Map<String, dynamic> data) {
|
|
|
|
|
|
return MiningTaskInfo(
|
|
|
|
|
|
coin: data['coin'] as String,
|
|
|
|
|
|
algo: data['algo'] as String,
|
|
|
|
|
|
pool: (data['pool'] as String?) ?? '',
|
|
|
|
|
|
poolUrl: data['pool_url'] as String,
|
|
|
|
|
|
walletAddress: data['wallet_address'] as String,
|
|
|
|
|
|
workerId: data['worker_id'] as String,
|
|
|
|
|
|
poolUser: data['pool_user'] as String?,
|
|
|
|
|
|
walletMining: (data['wallet_mining'] as bool?) ??
|
|
|
|
|
|
((data['wallet_mining'] is int) ? (data['wallet_mining'] as int) == 1 : false),
|
|
|
|
|
|
endTimestamp: data['end_timestamp'] as int,
|
|
|
|
|
|
miner: data['miner'] as String,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> _taskToJson(MiningTaskInfo task, {required int createdAt}) {
|
|
|
|
|
|
return <String, dynamic>{
|
|
|
|
|
|
'coin': task.coin,
|
|
|
|
|
|
'algo': task.algo,
|
|
|
|
|
|
'pool': task.pool,
|
|
|
|
|
|
'pool_url': task.poolUrl,
|
|
|
|
|
|
'wallet_address': task.walletAddress,
|
|
|
|
|
|
'worker_id': task.workerId,
|
|
|
|
|
|
'pool_user': task.poolUser,
|
|
|
|
|
|
'wallet_mining': task.walletMining,
|
|
|
|
|
|
'end_timestamp': task.endTimestamp,
|
|
|
|
|
|
'miner': task.miner,
|
|
|
|
|
|
'created_at': createdAt,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool _isSameTask(MiningTaskInfo a, MiningTaskInfo b) {
|
|
|
|
|
|
return a.coin == b.coin &&
|
|
|
|
|
|
a.algo == b.algo &&
|
|
|
|
|
|
a.pool == b.pool &&
|
|
|
|
|
|
a.poolUrl == b.poolUrl &&
|
|
|
|
|
|
a.walletAddress == b.walletAddress &&
|
|
|
|
|
|
a.workerId == b.workerId &&
|
|
|
|
|
|
(a.poolUser ?? '') == (b.poolUser ?? '') &&
|
|
|
|
|
|
a.walletMining == b.walletMining &&
|
|
|
|
|
|
a.endTimestamp == b.endTimestamp &&
|
|
|
|
|
|
a.miner == b.miner;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _StoredTask {
|
|
|
|
|
|
final MiningTaskInfo task;
|
|
|
|
|
|
final int createdAt;
|
|
|
|
|
|
|
|
|
|
|
|
_StoredTask({required this.task, required this.createdAt});
|
2026-01-22 15:14:27 +08:00
|
|
|
|
}
|