feat:设备日志模块
feat:定时任务 feat:设置页面用路由重构
This commit is contained in:
1 parent
1033f403b1
commit
2efa7a3c19
60 files changed
+6560
-643
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `file_upload_record` 本机文件上传状态的数据访问接口。
|
||||
@dao
|
||||
abstract class FileUploadRecordDao {
|
||||
/// 保存文件状态并返回主键;相同主键时替换原记录以保留同一路径的最新状态。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<int> save(FileUploadRecordEntity entity);
|
||||
|
||||
/// 按主键更新已经存在的文件上传状态。
|
||||
@update
|
||||
Future<void> edit(FileUploadRecordEntity entity);
|
||||
|
||||
/// 查询指定设备与业务类型的全部文件状态,并按最近更新时间倒序返回。
|
||||
@Query('''
|
||||
SELECT * FROM file_upload_record
|
||||
WHERE hardId = :hardId AND bizType = :bizType
|
||||
ORDER BY updateTime DESC
|
||||
''')
|
||||
Future<List<FileUploadRecordEntity>> getAll(
|
||||
String hardId,
|
||||
String bizType,
|
||||
);
|
||||
|
||||
/// 按设备、业务类型和相对路径查询唯一状态记录;不存在时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM file_upload_record
|
||||
WHERE hardId = :hardId
|
||||
AND bizType = :bizType
|
||||
AND relativePath = :relativePath
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<FileUploadRecordEntity?> getByPath(
|
||||
String hardId,
|
||||
String bizType,
|
||||
String relativePath,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `scheduled_task` 配置和最近执行快照的数据访问接口。
|
||||
@dao
|
||||
abstract class ScheduledTaskDao {
|
||||
/// 按 taskCode 保存单条任务;主键冲突时以新配置和执行快照整体替换旧记录。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> save(ScheduledTaskEntity entity);
|
||||
|
||||
/// 批量补齐默认任务;每条记录均按 taskCode 使用替换冲突策略。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> batchSave(List<ScheduledTaskEntity> entities);
|
||||
|
||||
/// 按实体主键更新一条已经存在的任务记录。
|
||||
@update
|
||||
Future<void> edit(ScheduledTaskEntity entity);
|
||||
|
||||
/// 查询全部任务并按稳定 taskCode 升序返回,便于启动时构建确定性配置映射。
|
||||
@Query('SELECT * FROM scheduled_task ORDER BY taskCode')
|
||||
Future<List<ScheduledTaskEntity>> getAll();
|
||||
|
||||
/// 按稳定 taskCode 查询单条任务;不存在时返回空。
|
||||
@Query('SELECT * FROM scheduled_task WHERE taskCode = :taskCode LIMIT 1')
|
||||
Future<ScheduledTaskEntity?> getByCode(String taskCode);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `scheduled_task_log` 结构化执行日志的数据访问接口。
|
||||
@dao
|
||||
abstract class ScheduledTaskLogDao {
|
||||
/// 新增任务开始日志并返回 SQLite 自增主键。
|
||||
@insert
|
||||
Future<int> save(ScheduledTaskLogEntity entity);
|
||||
|
||||
/// 按日志主键写回结束状态、耗时、错误和统计字段。
|
||||
@update
|
||||
Future<void> edit(ScheduledTaskLogEntity entity);
|
||||
|
||||
/// 按任务编码查询最近日志,可选依赖类型过滤,并按主键倒序限制返回数量。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
ORDER BY id DESC
|
||||
LIMIT :limit
|
||||
''')
|
||||
Future<List<ScheduledTaskLogEntity>> getByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int limit,
|
||||
);
|
||||
|
||||
/// 按任务编码和可选来源分页查询日志,使用主键倒序保证翻页期间最新记录始终位于前页。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
ORDER BY id DESC
|
||||
LIMIT :pageSize OFFSET :offset
|
||||
''')
|
||||
Future<List<ScheduledTaskLogEntity>> getPageByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int pageSize,
|
||||
int offset,
|
||||
);
|
||||
|
||||
/// 统计指定任务和可选来源下的日志总数,为分页组件提供准确的总记录数。
|
||||
@Query('''
|
||||
SELECT COUNT(0) FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
''')
|
||||
Future<int?> countByTask(String taskCode, String sourceType);
|
||||
|
||||
/// 查询指定任务最近一条日志;从未执行时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<ScheduledTaskLogEntity?> getLatest(String taskCode);
|
||||
|
||||
/// 终端长期常驻,执行日志只保留最近 2000 条,避免数据库无限增长。
|
||||
@Query('''
|
||||
DELETE FROM scheduled_task_log
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM scheduled_task_log ORDER BY id DESC LIMIT 2000
|
||||
)
|
||||
''')
|
||||
Future<void> trimHistory();
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:sqflite/sqflite.dart' as sqflite;
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
/// Floor 代码生成器使用的全部业务实体类型,新增实体时必须同步数据库版本和连续迁移。
|
||||
const entities = <Type>[
|
||||
BroadcastEntity,
|
||||
OrderEntity,
|
||||
@@ -23,8 +24,12 @@ const entities = <Type>[
|
||||
UserEntity,
|
||||
FaceEntity,
|
||||
SettingEntity,
|
||||
ScheduledTaskEntity,
|
||||
ScheduledTaskLogEntity,
|
||||
FileUploadRecordEntity,
|
||||
];
|
||||
|
||||
/// Floor 在实体字段与 SQLite 基础类型之间使用的全局转换器集合。
|
||||
const typeConverters = <Type>[
|
||||
DateTimeConverter,
|
||||
DateTimeEmptyConverter,
|
||||
@@ -41,23 +46,59 @@ const typeConverters = <Type>[
|
||||
BoolConverter,
|
||||
];
|
||||
|
||||
@Database(version: 7, entities: entities)
|
||||
/// 收银终端共享 SQLite 数据库入口,统一暴露各业务 DAO 和带迁移的建库方法。
|
||||
@Database(version: 8, entities: entities)
|
||||
@TypeConverters(typeConverters)
|
||||
abstract class AppDatabase extends FloorDatabase {
|
||||
/// 取餐广播队列 DAO。
|
||||
BroadcastDao get getBroadcastDao;
|
||||
|
||||
/// 本地订单及离线订单 DAO。
|
||||
OrderDao get getOrderDao;
|
||||
|
||||
/// 挂单数据 DAO。
|
||||
OrderHoldBillDao get getOrderHoldBillDao;
|
||||
|
||||
/// 食堂商品 DAO。
|
||||
CanteenSpuDao get getCanteenSpuDao;
|
||||
|
||||
/// 超市 SKU DAO。
|
||||
SupermarketSkuDao get getSupermarketSkuDao;
|
||||
|
||||
/// 超市 SPU DAO。
|
||||
SupermarketSpuDao get getSupermarketSpuDao;
|
||||
|
||||
/// 超市商品条码 DAO。
|
||||
SupermarketCodeDao get getSupermarketCodeDao;
|
||||
|
||||
/// 店铺卡配置 DAO。
|
||||
ShopCardDao get getShopCardDao;
|
||||
|
||||
/// 商品分类 DAO。
|
||||
ClassifyDao get getClassifyDao;
|
||||
|
||||
/// 打印历史记录 DAO。
|
||||
PrintHistoryDao get getPrintHistoryDao;
|
||||
|
||||
/// 本地用户数据 DAO。
|
||||
UserDao get getUserDao;
|
||||
|
||||
/// 本地人脸数据 DAO。
|
||||
FaceDao get getFaceDao;
|
||||
|
||||
/// 终端设置 DAO。
|
||||
SettingDao get getSettingDao;
|
||||
|
||||
/// 定时任务配置及最近执行快照 DAO。
|
||||
ScheduledTaskDao get getScheduledTaskDao;
|
||||
|
||||
/// 定时任务结构化执行日志 DAO。
|
||||
ScheduledTaskLogDao get getScheduledTaskLogDao;
|
||||
|
||||
/// 文件上传状态与幂等快照 DAO。
|
||||
FileUploadRecordDao get getFileUploadRecordDao;
|
||||
|
||||
/// 使用统一外部存储路径打开数据库并按版本顺序执行全部 Floor Migration。
|
||||
static Future<AppDatabase> getDataBase() async {
|
||||
final dbPath = await AppFileTool.getDatabasePath();
|
||||
final name = "$dbPath/${Constants.databaseName}";
|
||||
|
||||
@@ -98,13 +98,19 @@ class _$AppDatabase extends AppDatabase {
|
||||
|
||||
SettingDao? _getSettingDaoInstance;
|
||||
|
||||
ScheduledTaskDao? _getScheduledTaskDaoInstance;
|
||||
|
||||
ScheduledTaskLogDao? _getScheduledTaskLogDaoInstance;
|
||||
|
||||
FileUploadRecordDao? _getFileUploadRecordDaoInstance;
|
||||
|
||||
Future<sqflite.Database> open(
|
||||
String path,
|
||||
List<Migration> migrations, [
|
||||
Callback? callback,
|
||||
]) async {
|
||||
final databaseOptions = sqflite.OpenDatabaseOptions(
|
||||
version: 7,
|
||||
version: 8,
|
||||
onConfigure: (database) async {
|
||||
await database.execute('PRAGMA foreign_keys = ON');
|
||||
await callback?.onConfigure?.call(database);
|
||||
@@ -145,6 +151,20 @@ class _$AppDatabase extends AppDatabase {
|
||||
'CREATE TABLE IF NOT EXISTS `face` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userId` TEXT NOT NULL, `sampleId` TEXT NOT NULL, `userName` TEXT NOT NULL, `deptId` TEXT, `deptName` TEXT, `imagePath` TEXT NOT NULL, `path` TEXT, `faceModel` TEXT, `createTime` TEXT, `updateTime` TEXT)');
|
||||
await database.execute(
|
||||
'CREATE TABLE IF NOT EXISTS `setting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userJson` TEXT NOT NULL, `sysJson` TEXT NOT NULL, `canteenExtraPriceConfigJson` TEXT)');
|
||||
await database.execute(
|
||||
'CREATE TABLE IF NOT EXISTS `scheduled_task` (`taskCode` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `cronExpression` TEXT NOT NULL, `idleOnly` INTEGER NOT NULL, `lastRunAt` TEXT, `lastSuccessAt` TEXT, `lastStatus` TEXT NOT NULL, `lastMessage` TEXT NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, PRIMARY KEY (`taskCode`))');
|
||||
await database.execute(
|
||||
'CREATE TABLE IF NOT EXISTS `scheduled_task_log` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `taskCode` TEXT NOT NULL, `taskName` TEXT NOT NULL, `triggerType` TEXT NOT NULL, `sourceType` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `status` TEXT NOT NULL, `message` TEXT NOT NULL, `errorType` TEXT, `errorMessage` TEXT, `errorStack` TEXT, `startedAt` TEXT NOT NULL, `finishedAt` TEXT, `durationMs` INTEGER NOT NULL, `scannedCount` INTEGER, `skippedCount` INTEGER, `successCount` INTEGER, `failureCount` INTEGER)');
|
||||
await database.execute(
|
||||
'CREATE TABLE IF NOT EXISTS `file_upload_record` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `hardId` TEXT NOT NULL, `bizType` TEXT NOT NULL, `relativePath` TEXT NOT NULL, `originalName` TEXT NOT NULL, `sourceSize` INTEGER NOT NULL, `sourceModifiedTime` TEXT NOT NULL, `sanitizedHash` TEXT NOT NULL, `sanitizedSize` INTEGER NOT NULL, `status` TEXT NOT NULL, `serverFileId` TEXT, `attemptCount` INTEGER NOT NULL, `lastAttemptAt` TEXT, `uploadedAt` TEXT, `lastError` TEXT, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL)');
|
||||
await database.execute(
|
||||
'CREATE INDEX `index_scheduled_task_log_taskCode_startedAt` ON `scheduled_task_log` (`taskCode`, `startedAt`)');
|
||||
await database.execute(
|
||||
'CREATE INDEX `index_scheduled_task_log_sourceType_status_startedAt` ON `scheduled_task_log` (`sourceType`, `status`, `startedAt`)');
|
||||
await database.execute(
|
||||
'CREATE UNIQUE INDEX `index_file_upload_record_hardId_bizType_relativePath` ON `file_upload_record` (`hardId`, `bizType`, `relativePath`)');
|
||||
await database.execute(
|
||||
'CREATE INDEX `index_file_upload_record_bizType_status_updateTime` ON `file_upload_record` (`bizType`, `status`, `updateTime`)');
|
||||
|
||||
await callback?.onCreate?.call(database, version);
|
||||
},
|
||||
@@ -223,6 +243,24 @@ class _$AppDatabase extends AppDatabase {
|
||||
SettingDao get getSettingDao {
|
||||
return _getSettingDaoInstance ??= _$SettingDao(database, changeListener);
|
||||
}
|
||||
|
||||
@override
|
||||
ScheduledTaskDao get getScheduledTaskDao {
|
||||
return _getScheduledTaskDaoInstance ??=
|
||||
_$ScheduledTaskDao(database, changeListener);
|
||||
}
|
||||
|
||||
@override
|
||||
ScheduledTaskLogDao get getScheduledTaskLogDao {
|
||||
return _getScheduledTaskLogDaoInstance ??=
|
||||
_$ScheduledTaskLogDao(database, changeListener);
|
||||
}
|
||||
|
||||
@override
|
||||
FileUploadRecordDao get getFileUploadRecordDao {
|
||||
return _getFileUploadRecordDaoInstance ??=
|
||||
_$FileUploadRecordDao(database, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
class _$BroadcastDao extends BroadcastDao {
|
||||
@@ -1981,6 +2019,346 @@ class _$SettingDao extends SettingDao {
|
||||
}
|
||||
}
|
||||
|
||||
class _$ScheduledTaskDao extends ScheduledTaskDao {
|
||||
_$ScheduledTaskDao(
|
||||
this.database,
|
||||
this.changeListener,
|
||||
) : _queryAdapter = QueryAdapter(database),
|
||||
_scheduledTaskEntityInsertionAdapter = InsertionAdapter(
|
||||
database,
|
||||
'scheduled_task',
|
||||
(ScheduledTaskEntity item) => <String, Object?>{
|
||||
'taskCode': item.taskCode,
|
||||
'enabled': _boolConverter.encode(item.enabled),
|
||||
'cronExpression': item.cronExpression,
|
||||
'idleOnly': _boolConverter.encode(item.idleOnly),
|
||||
'lastRunAt': _dateTimeEmptyConverter.encode(item.lastRunAt),
|
||||
'lastSuccessAt':
|
||||
_dateTimeEmptyConverter.encode(item.lastSuccessAt),
|
||||
'lastStatus': item.lastStatus,
|
||||
'lastMessage': item.lastMessage,
|
||||
'createTime': _dateTimeConverter.encode(item.createTime),
|
||||
'updateTime': _dateTimeConverter.encode(item.updateTime)
|
||||
}),
|
||||
_scheduledTaskEntityUpdateAdapter = UpdateAdapter(
|
||||
database,
|
||||
'scheduled_task',
|
||||
['taskCode'],
|
||||
(ScheduledTaskEntity item) => <String, Object?>{
|
||||
'taskCode': item.taskCode,
|
||||
'enabled': _boolConverter.encode(item.enabled),
|
||||
'cronExpression': item.cronExpression,
|
||||
'idleOnly': _boolConverter.encode(item.idleOnly),
|
||||
'lastRunAt': _dateTimeEmptyConverter.encode(item.lastRunAt),
|
||||
'lastSuccessAt':
|
||||
_dateTimeEmptyConverter.encode(item.lastSuccessAt),
|
||||
'lastStatus': item.lastStatus,
|
||||
'lastMessage': item.lastMessage,
|
||||
'createTime': _dateTimeConverter.encode(item.createTime),
|
||||
'updateTime': _dateTimeConverter.encode(item.updateTime)
|
||||
});
|
||||
|
||||
final sqflite.DatabaseExecutor database;
|
||||
|
||||
final StreamController<String> changeListener;
|
||||
|
||||
final QueryAdapter _queryAdapter;
|
||||
|
||||
final InsertionAdapter<ScheduledTaskEntity>
|
||||
_scheduledTaskEntityInsertionAdapter;
|
||||
|
||||
final UpdateAdapter<ScheduledTaskEntity> _scheduledTaskEntityUpdateAdapter;
|
||||
|
||||
@override
|
||||
Future<List<ScheduledTaskEntity>> getAll() async {
|
||||
return _queryAdapter.queryList(
|
||||
'SELECT * FROM scheduled_task ORDER BY taskCode',
|
||||
mapper: (Map<String, Object?> row) => ScheduledTaskEntity(
|
||||
taskCode: row['taskCode'] as String,
|
||||
enabled: _boolConverter.decode(row['enabled'] as int),
|
||||
cronExpression: row['cronExpression'] as String,
|
||||
idleOnly: _boolConverter.decode(row['idleOnly'] as int),
|
||||
lastRunAt:
|
||||
_dateTimeEmptyConverter.decode(row['lastRunAt'] as String),
|
||||
lastSuccessAt:
|
||||
_dateTimeEmptyConverter.decode(row['lastSuccessAt'] as String),
|
||||
lastStatus: row['lastStatus'] as String,
|
||||
lastMessage: row['lastMessage'] as String,
|
||||
createTime: _dateTimeConverter.decode(row['createTime'] as String),
|
||||
updateTime:
|
||||
_dateTimeConverter.decode(row['updateTime'] as String)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ScheduledTaskEntity?> getByCode(String taskCode) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT * FROM scheduled_task WHERE taskCode = ?1 LIMIT 1',
|
||||
mapper: (Map<String, Object?> row) => ScheduledTaskEntity(
|
||||
taskCode: row['taskCode'] as String,
|
||||
enabled: _boolConverter.decode(row['enabled'] as int),
|
||||
cronExpression: row['cronExpression'] as String,
|
||||
idleOnly: _boolConverter.decode(row['idleOnly'] as int),
|
||||
lastRunAt:
|
||||
_dateTimeEmptyConverter.decode(row['lastRunAt'] as String),
|
||||
lastSuccessAt:
|
||||
_dateTimeEmptyConverter.decode(row['lastSuccessAt'] as String),
|
||||
lastStatus: row['lastStatus'] as String,
|
||||
lastMessage: row['lastMessage'] as String,
|
||||
createTime: _dateTimeConverter.decode(row['createTime'] as String),
|
||||
updateTime: _dateTimeConverter.decode(row['updateTime'] as String)),
|
||||
arguments: [taskCode]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ScheduledTaskEntity entity) async {
|
||||
await _scheduledTaskEntityInsertionAdapter.insert(
|
||||
entity, OnConflictStrategy.replace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> batchSave(List<ScheduledTaskEntity> entities) async {
|
||||
await _scheduledTaskEntityInsertionAdapter.insertList(
|
||||
entities, OnConflictStrategy.replace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> edit(ScheduledTaskEntity entity) async {
|
||||
await _scheduledTaskEntityUpdateAdapter.update(
|
||||
entity, OnConflictStrategy.abort);
|
||||
}
|
||||
}
|
||||
|
||||
class _$ScheduledTaskLogDao extends ScheduledTaskLogDao {
|
||||
_$ScheduledTaskLogDao(
|
||||
this.database,
|
||||
this.changeListener,
|
||||
) : _queryAdapter = QueryAdapter(database),
|
||||
_scheduledTaskLogEntityInsertionAdapter = InsertionAdapter(
|
||||
database,
|
||||
'scheduled_task_log',
|
||||
(ScheduledTaskLogEntity item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'taskCode': item.taskCode,
|
||||
'taskName': item.taskName,
|
||||
'triggerType': item.triggerType,
|
||||
'sourceType': item.sourceType,
|
||||
'sourceName': item.sourceName,
|
||||
'status': item.status,
|
||||
'message': item.message,
|
||||
'errorType': item.errorType,
|
||||
'errorMessage': item.errorMessage,
|
||||
'errorStack': item.errorStack,
|
||||
'startedAt': _dateTimeConverter.encode(item.startedAt),
|
||||
'finishedAt': _dateTimeEmptyConverter.encode(item.finishedAt),
|
||||
'durationMs': item.durationMs,
|
||||
'scannedCount': item.scannedCount,
|
||||
'skippedCount': item.skippedCount,
|
||||
'successCount': item.successCount,
|
||||
'failureCount': item.failureCount
|
||||
}),
|
||||
_scheduledTaskLogEntityUpdateAdapter = UpdateAdapter(
|
||||
database,
|
||||
'scheduled_task_log',
|
||||
['id'],
|
||||
(ScheduledTaskLogEntity item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'taskCode': item.taskCode,
|
||||
'taskName': item.taskName,
|
||||
'triggerType': item.triggerType,
|
||||
'sourceType': item.sourceType,
|
||||
'sourceName': item.sourceName,
|
||||
'status': item.status,
|
||||
'message': item.message,
|
||||
'errorType': item.errorType,
|
||||
'errorMessage': item.errorMessage,
|
||||
'errorStack': item.errorStack,
|
||||
'startedAt': _dateTimeConverter.encode(item.startedAt),
|
||||
'finishedAt': _dateTimeEmptyConverter.encode(item.finishedAt),
|
||||
'durationMs': item.durationMs,
|
||||
'scannedCount': item.scannedCount,
|
||||
'skippedCount': item.skippedCount,
|
||||
'successCount': item.successCount,
|
||||
'failureCount': item.failureCount
|
||||
});
|
||||
|
||||
final sqflite.DatabaseExecutor database;
|
||||
|
||||
final StreamController<String> changeListener;
|
||||
|
||||
final QueryAdapter _queryAdapter;
|
||||
|
||||
final InsertionAdapter<ScheduledTaskLogEntity>
|
||||
_scheduledTaskLogEntityInsertionAdapter;
|
||||
|
||||
final UpdateAdapter<ScheduledTaskLogEntity>
|
||||
_scheduledTaskLogEntityUpdateAdapter;
|
||||
|
||||
@override
|
||||
Future<List<ScheduledTaskLogEntity>> getByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int limit,
|
||||
) async {
|
||||
return _queryAdapter.queryList(
|
||||
'SELECT * FROM scheduled_task_log WHERE taskCode = ?1 AND (?2 = \'\' OR sourceType = ?2) ORDER BY id DESC LIMIT ?3',
|
||||
mapper: (Map<String, Object?> row) => ScheduledTaskLogEntity(id: row['id'] as int?, taskCode: row['taskCode'] as String, taskName: row['taskName'] as String, triggerType: row['triggerType'] as String, sourceType: row['sourceType'] as String, sourceName: row['sourceName'] as String, status: row['status'] as String, message: row['message'] as String, errorType: row['errorType'] as String?, errorMessage: row['errorMessage'] as String?, errorStack: row['errorStack'] as String?, startedAt: _dateTimeConverter.decode(row['startedAt'] as String), finishedAt: _dateTimeEmptyConverter.decode(row['finishedAt'] as String), durationMs: row['durationMs'] as int, scannedCount: row['scannedCount'] as int?, skippedCount: row['skippedCount'] as int?, successCount: row['successCount'] as int?, failureCount: row['failureCount'] as int?),
|
||||
arguments: [taskCode, sourceType, limit]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ScheduledTaskLogEntity>> getPageByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int pageSize,
|
||||
int offset,
|
||||
) async {
|
||||
return _queryAdapter.queryList(
|
||||
'SELECT * FROM scheduled_task_log WHERE taskCode = ?1 AND (?2 = \'\' OR sourceType = ?2) ORDER BY id DESC LIMIT ?3 OFFSET ?4',
|
||||
mapper: (Map<String, Object?> row) => ScheduledTaskLogEntity(id: row['id'] as int?, taskCode: row['taskCode'] as String, taskName: row['taskName'] as String, triggerType: row['triggerType'] as String, sourceType: row['sourceType'] as String, sourceName: row['sourceName'] as String, status: row['status'] as String, message: row['message'] as String, errorType: row['errorType'] as String?, errorMessage: row['errorMessage'] as String?, errorStack: row['errorStack'] as String?, startedAt: _dateTimeConverter.decode(row['startedAt'] as String), finishedAt: _dateTimeEmptyConverter.decode(row['finishedAt'] as String), durationMs: row['durationMs'] as int, scannedCount: row['scannedCount'] as int?, skippedCount: row['skippedCount'] as int?, successCount: row['successCount'] as int?, failureCount: row['failureCount'] as int?),
|
||||
arguments: [taskCode, sourceType, pageSize, offset]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> countByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT COUNT(0) FROM scheduled_task_log WHERE taskCode = ?1 AND (?2 = \'\' OR sourceType = ?2)',
|
||||
mapper: (Map<String, Object?> row) => row.values.first as int,
|
||||
arguments: [taskCode, sourceType]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ScheduledTaskLogEntity?> getLatest(String taskCode) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT * FROM scheduled_task_log WHERE taskCode = ?1 ORDER BY id DESC LIMIT 1',
|
||||
mapper: (Map<String, Object?> row) => ScheduledTaskLogEntity(id: row['id'] as int?, taskCode: row['taskCode'] as String, taskName: row['taskName'] as String, triggerType: row['triggerType'] as String, sourceType: row['sourceType'] as String, sourceName: row['sourceName'] as String, status: row['status'] as String, message: row['message'] as String, errorType: row['errorType'] as String?, errorMessage: row['errorMessage'] as String?, errorStack: row['errorStack'] as String?, startedAt: _dateTimeConverter.decode(row['startedAt'] as String), finishedAt: _dateTimeEmptyConverter.decode(row['finishedAt'] as String), durationMs: row['durationMs'] as int, scannedCount: row['scannedCount'] as int?, skippedCount: row['skippedCount'] as int?, successCount: row['successCount'] as int?, failureCount: row['failureCount'] as int?),
|
||||
arguments: [taskCode]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> trimHistory() async {
|
||||
await _queryAdapter.queryNoReturn(
|
||||
'DELETE FROM scheduled_task_log WHERE id NOT IN ( SELECT id FROM scheduled_task_log ORDER BY id DESC LIMIT 2000 )');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> save(ScheduledTaskLogEntity entity) {
|
||||
return _scheduledTaskLogEntityInsertionAdapter.insertAndReturnId(
|
||||
entity, OnConflictStrategy.abort);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> edit(ScheduledTaskLogEntity entity) async {
|
||||
await _scheduledTaskLogEntityUpdateAdapter.update(
|
||||
entity, OnConflictStrategy.abort);
|
||||
}
|
||||
}
|
||||
|
||||
class _$FileUploadRecordDao extends FileUploadRecordDao {
|
||||
_$FileUploadRecordDao(
|
||||
this.database,
|
||||
this.changeListener,
|
||||
) : _queryAdapter = QueryAdapter(database),
|
||||
_fileUploadRecordEntityInsertionAdapter = InsertionAdapter(
|
||||
database,
|
||||
'file_upload_record',
|
||||
(FileUploadRecordEntity item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'hardId': item.hardId,
|
||||
'bizType': item.bizType,
|
||||
'relativePath': item.relativePath,
|
||||
'originalName': item.originalName,
|
||||
'sourceSize': item.sourceSize,
|
||||
'sourceModifiedTime':
|
||||
_dateTimeConverter.encode(item.sourceModifiedTime),
|
||||
'sanitizedHash': item.sanitizedHash,
|
||||
'sanitizedSize': item.sanitizedSize,
|
||||
'status': item.status,
|
||||
'serverFileId': item.serverFileId,
|
||||
'attemptCount': item.attemptCount,
|
||||
'lastAttemptAt':
|
||||
_dateTimeEmptyConverter.encode(item.lastAttemptAt),
|
||||
'uploadedAt': _dateTimeEmptyConverter.encode(item.uploadedAt),
|
||||
'lastError': item.lastError,
|
||||
'createTime': _dateTimeConverter.encode(item.createTime),
|
||||
'updateTime': _dateTimeConverter.encode(item.updateTime)
|
||||
}),
|
||||
_fileUploadRecordEntityUpdateAdapter = UpdateAdapter(
|
||||
database,
|
||||
'file_upload_record',
|
||||
['id'],
|
||||
(FileUploadRecordEntity item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'hardId': item.hardId,
|
||||
'bizType': item.bizType,
|
||||
'relativePath': item.relativePath,
|
||||
'originalName': item.originalName,
|
||||
'sourceSize': item.sourceSize,
|
||||
'sourceModifiedTime':
|
||||
_dateTimeConverter.encode(item.sourceModifiedTime),
|
||||
'sanitizedHash': item.sanitizedHash,
|
||||
'sanitizedSize': item.sanitizedSize,
|
||||
'status': item.status,
|
||||
'serverFileId': item.serverFileId,
|
||||
'attemptCount': item.attemptCount,
|
||||
'lastAttemptAt':
|
||||
_dateTimeEmptyConverter.encode(item.lastAttemptAt),
|
||||
'uploadedAt': _dateTimeEmptyConverter.encode(item.uploadedAt),
|
||||
'lastError': item.lastError,
|
||||
'createTime': _dateTimeConverter.encode(item.createTime),
|
||||
'updateTime': _dateTimeConverter.encode(item.updateTime)
|
||||
});
|
||||
|
||||
final sqflite.DatabaseExecutor database;
|
||||
|
||||
final StreamController<String> changeListener;
|
||||
|
||||
final QueryAdapter _queryAdapter;
|
||||
|
||||
final InsertionAdapter<FileUploadRecordEntity>
|
||||
_fileUploadRecordEntityInsertionAdapter;
|
||||
|
||||
final UpdateAdapter<FileUploadRecordEntity>
|
||||
_fileUploadRecordEntityUpdateAdapter;
|
||||
|
||||
@override
|
||||
Future<List<FileUploadRecordEntity>> getAll(
|
||||
String hardId,
|
||||
String bizType,
|
||||
) async {
|
||||
return _queryAdapter.queryList(
|
||||
'SELECT * FROM file_upload_record WHERE hardId = ?1 AND bizType = ?2 ORDER BY updateTime DESC',
|
||||
mapper: (Map<String, Object?> row) => FileUploadRecordEntity(id: row['id'] as int?, hardId: row['hardId'] as String, bizType: row['bizType'] as String, relativePath: row['relativePath'] as String, originalName: row['originalName'] as String, sourceSize: row['sourceSize'] as int, sourceModifiedTime: _dateTimeConverter.decode(row['sourceModifiedTime'] as String), sanitizedHash: row['sanitizedHash'] as String, sanitizedSize: row['sanitizedSize'] as int, status: row['status'] as String, serverFileId: row['serverFileId'] as String?, attemptCount: row['attemptCount'] as int, lastAttemptAt: _dateTimeEmptyConverter.decode(row['lastAttemptAt'] as String), uploadedAt: _dateTimeEmptyConverter.decode(row['uploadedAt'] as String), lastError: row['lastError'] as String?, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String)),
|
||||
arguments: [hardId, bizType]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FileUploadRecordEntity?> getByPath(
|
||||
String hardId,
|
||||
String bizType,
|
||||
String relativePath,
|
||||
) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT * FROM file_upload_record WHERE hardId = ?1 AND bizType = ?2 AND relativePath = ?3 LIMIT 1',
|
||||
mapper: (Map<String, Object?> row) => FileUploadRecordEntity(id: row['id'] as int?, hardId: row['hardId'] as String, bizType: row['bizType'] as String, relativePath: row['relativePath'] as String, originalName: row['originalName'] as String, sourceSize: row['sourceSize'] as int, sourceModifiedTime: _dateTimeConverter.decode(row['sourceModifiedTime'] as String), sanitizedHash: row['sanitizedHash'] as String, sanitizedSize: row['sanitizedSize'] as int, status: row['status'] as String, serverFileId: row['serverFileId'] as String?, attemptCount: row['attemptCount'] as int, lastAttemptAt: _dateTimeEmptyConverter.decode(row['lastAttemptAt'] as String), uploadedAt: _dateTimeEmptyConverter.decode(row['uploadedAt'] as String), lastError: row['lastError'] as String?, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String)),
|
||||
arguments: [hardId, bizType, relativePath]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> save(FileUploadRecordEntity entity) {
|
||||
return _fileUploadRecordEntityInsertionAdapter.insertAndReturnId(
|
||||
entity, OnConflictStrategy.replace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> edit(FileUploadRecordEntity entity) async {
|
||||
await _fileUploadRecordEntityUpdateAdapter.update(
|
||||
entity, OnConflictStrategy.abort);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore_for_file: unused_element
|
||||
final _dateTimeConverter = DateTimeConverter();
|
||||
final _dateTimeEmptyConverter = DateTimeEmptyConverter();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
/// 收银终端 SQLite 连续版本迁移清单,禁止跳过中间版本或在原生层重复执行结构 SQL。
|
||||
abstract class DatabaseMigration {
|
||||
/// 从版本1到版本2的迁移:添加productNamePy字段
|
||||
static Migration migration1To2 = Migration(1, 2, (database) async {
|
||||
@@ -183,6 +184,84 @@ abstract class DatabaseMigration {
|
||||
}
|
||||
});
|
||||
|
||||
/// 版本8将调度配置、执行日志和文件上传状态从 sysJson 收敛为可查询的结构化表。
|
||||
static Migration migration7To8 = Migration(7, 8, (database) async {
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS scheduled_task (
|
||||
taskCode TEXT NOT NULL PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL,
|
||||
cronExpression TEXT NOT NULL,
|
||||
idleOnly INTEGER NOT NULL,
|
||||
lastRunAt TEXT,
|
||||
lastSuccessAt TEXT,
|
||||
lastStatus TEXT NOT NULL,
|
||||
lastMessage TEXT NOT NULL,
|
||||
createTime TEXT NOT NULL,
|
||||
updateTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS scheduled_task_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
taskCode TEXT NOT NULL,
|
||||
taskName TEXT NOT NULL,
|
||||
triggerType TEXT NOT NULL,
|
||||
sourceType TEXT NOT NULL,
|
||||
sourceName TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
errorType TEXT,
|
||||
errorMessage TEXT,
|
||||
errorStack TEXT,
|
||||
startedAt TEXT NOT NULL,
|
||||
finishedAt TEXT,
|
||||
durationMs INTEGER NOT NULL,
|
||||
scannedCount INTEGER,
|
||||
skippedCount INTEGER,
|
||||
successCount INTEGER,
|
||||
failureCount INTEGER
|
||||
)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_scheduled_task_log_taskCode_startedAt '
|
||||
'ON scheduled_task_log(taskCode, startedAt)',
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_scheduled_task_log_sourceType_status_startedAt '
|
||||
'ON scheduled_task_log(sourceType, status, startedAt)',
|
||||
);
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS file_upload_record (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hardId TEXT NOT NULL,
|
||||
bizType TEXT NOT NULL,
|
||||
relativePath TEXT NOT NULL,
|
||||
originalName TEXT NOT NULL,
|
||||
sourceSize INTEGER NOT NULL,
|
||||
sourceModifiedTime TEXT NOT NULL,
|
||||
sanitizedHash TEXT NOT NULL,
|
||||
sanitizedSize INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
serverFileId TEXT,
|
||||
attemptCount INTEGER NOT NULL,
|
||||
lastAttemptAt TEXT,
|
||||
uploadedAt TEXT,
|
||||
lastError TEXT,
|
||||
createTime TEXT NOT NULL,
|
||||
updateTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS index_file_upload_record_hardId_bizType_relativePath '
|
||||
'ON file_upload_record(hardId, bizType, relativePath)',
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_file_upload_record_bizType_status_updateTime '
|
||||
'ON file_upload_record(bizType, status, updateTime)',
|
||||
);
|
||||
});
|
||||
|
||||
/// 按起始版本升序返回全部迁移,Floor 打开旧数据库时据此连续升级到当前版本。
|
||||
static List<Migration> getDatabaseMigrations() {
|
||||
return [
|
||||
migration1To2,
|
||||
@@ -191,6 +270,7 @@ abstract class DatabaseMigration {
|
||||
migration4To5,
|
||||
migration5To6,
|
||||
migration6To7,
|
||||
migration7To8,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'file_upload_record_entity.g.dart';
|
||||
|
||||
/// 本机文件上传记录。
|
||||
///
|
||||
/// 目前 [bizType] 使用 `terminal_log`,保留业务类型后可复用到数据库备份等文件任务。
|
||||
@JsonSerializable()
|
||||
@Entity(
|
||||
tableName: 'file_upload_record',
|
||||
indices: [
|
||||
Index(value: ['hardId', 'bizType', 'relativePath'], unique: true),
|
||||
Index(value: ['bizType', 'status', 'updateTime']),
|
||||
],
|
||||
)
|
||||
class FileUploadRecordEntity {
|
||||
/// SQLite 自增主键;首次保存前为空。
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
final int? id;
|
||||
|
||||
/// 终端绑定的设备 ID,用于隔离设备重新绑定前后的文件状态。
|
||||
final String hardId;
|
||||
|
||||
/// 文件业务类型;终端日志固定使用 `terminal_log`。
|
||||
final String bizType;
|
||||
|
||||
/// 文件在业务根目录下使用正斜杠的相对路径。
|
||||
final String relativePath;
|
||||
|
||||
/// 原始文件名,不包含目录。
|
||||
final String originalName;
|
||||
|
||||
/// 生成脱敏副本时读取的原文件快照大小,单位为字节。
|
||||
final int sourceSize;
|
||||
|
||||
/// 生成脱敏副本时原文件的最后修改时间。
|
||||
final DateTime sourceModifiedTime;
|
||||
|
||||
/// 脱敏后文件内容的 MD5 哈希;首次尝试尚未完成脱敏时可为空字符串。
|
||||
final String sanitizedHash;
|
||||
|
||||
/// 脱敏后文件大小,单位为字节;尚未完成脱敏时为零。
|
||||
final int sanitizedSize;
|
||||
|
||||
/// 最近一次上传尝试状态,保存 [FileUploadStatus.name]。
|
||||
final String status;
|
||||
|
||||
/// 后端 hard_terminal_log_file 记录 ID;预检判定无需上传且本机无旧记录时可为空。
|
||||
final String? serverFileId;
|
||||
|
||||
/// 对该路径发起过的上传尝试次数,预检前即递增。
|
||||
final int attemptCount;
|
||||
|
||||
/// 最近一次开始上传尝试的本地时间;从未尝试时为空。
|
||||
final DateTime? lastAttemptAt;
|
||||
|
||||
/// 最近一次被服务端确认成功或无需上传的本地时间。
|
||||
final DateTime? uploadedAt;
|
||||
|
||||
/// 最近一次失败的截断错误摘要;成功记录为空。
|
||||
final String? lastError;
|
||||
|
||||
/// 当前设备与业务类型下首次创建该路径记录的本地时间。
|
||||
final DateTime createTime;
|
||||
|
||||
/// 状态、快照或服务端 ID 最近一次更新的本地时间。
|
||||
final DateTime updateTime;
|
||||
|
||||
/// 创建一条文件上传状态记录。
|
||||
const FileUploadRecordEntity({
|
||||
this.id,
|
||||
required this.hardId,
|
||||
required this.bizType,
|
||||
required this.relativePath,
|
||||
required this.originalName,
|
||||
required this.sourceSize,
|
||||
required this.sourceModifiedTime,
|
||||
required this.sanitizedHash,
|
||||
required this.sanitizedSize,
|
||||
required this.status,
|
||||
this.serverFileId,
|
||||
this.attemptCount = 0,
|
||||
this.lastAttemptAt,
|
||||
this.uploadedAt,
|
||||
this.lastError,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复上传记录,仅供调试序列化使用。
|
||||
factory FileUploadRecordEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$FileUploadRecordEntityFromJson(json);
|
||||
|
||||
/// 将上传记录转换为 JSON,仅供调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$FileUploadRecordEntityToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_upload_record_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
FileUploadRecordEntity _$FileUploadRecordEntityFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
FileUploadRecordEntity(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
hardId: json['hardId'] as String,
|
||||
bizType: json['bizType'] as String,
|
||||
relativePath: json['relativePath'] as String,
|
||||
originalName: json['originalName'] as String,
|
||||
sourceSize: (json['sourceSize'] as num).toInt(),
|
||||
sourceModifiedTime: DateTime.parse(json['sourceModifiedTime'] as String),
|
||||
sanitizedHash: json['sanitizedHash'] as String,
|
||||
sanitizedSize: (json['sanitizedSize'] as num).toInt(),
|
||||
status: json['status'] as String,
|
||||
serverFileId: json['serverFileId'] as String?,
|
||||
attemptCount: (json['attemptCount'] as num?)?.toInt() ?? 0,
|
||||
lastAttemptAt: json['lastAttemptAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastAttemptAt'] as String),
|
||||
uploadedAt: json['uploadedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['uploadedAt'] as String),
|
||||
lastError: json['lastError'] as String?,
|
||||
createTime: DateTime.parse(json['createTime'] as String),
|
||||
updateTime: DateTime.parse(json['updateTime'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileUploadRecordEntityToJson(
|
||||
FileUploadRecordEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'hardId': instance.hardId,
|
||||
'bizType': instance.bizType,
|
||||
'relativePath': instance.relativePath,
|
||||
'originalName': instance.originalName,
|
||||
'sourceSize': instance.sourceSize,
|
||||
'sourceModifiedTime': instance.sourceModifiedTime.toIso8601String(),
|
||||
'sanitizedHash': instance.sanitizedHash,
|
||||
'sanitizedSize': instance.sanitizedSize,
|
||||
'status': instance.status,
|
||||
'serverFileId': instance.serverFileId,
|
||||
'attemptCount': instance.attemptCount,
|
||||
'lastAttemptAt': instance.lastAttemptAt?.toIso8601String(),
|
||||
'uploadedAt': instance.uploadedAt?.toIso8601String(),
|
||||
'lastError': instance.lastError,
|
||||
'createTime': instance.createTime.toIso8601String(),
|
||||
'updateTime': instance.updateTime.toIso8601String(),
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'scheduled_task_entity.g.dart';
|
||||
|
||||
/// 终端定时任务配置及最近一次执行快照。
|
||||
///
|
||||
/// [taskCode] 使用代码枚举名作为稳定主键,界面文案调整时不会产生重复任务。
|
||||
@JsonSerializable()
|
||||
@Entity(tableName: 'scheduled_task')
|
||||
class ScheduledTaskEntity {
|
||||
/// 稳定任务编码,取 [ScheduledTaskType.name],作为表主键且不能随展示文案变化。
|
||||
@PrimaryKey()
|
||||
final String taskCode;
|
||||
|
||||
/// 是否启用 Cron 自动调度;关闭时仍可通过设置页手动触发。
|
||||
final bool enabled;
|
||||
|
||||
/// cron 0.6.2 使用的六段表达式,字段顺序为秒、分、时、日、月、周。
|
||||
final String cronExpression;
|
||||
|
||||
/// 自动任务是否必须等待连续 60 秒无人操作的完整空闲窗口。
|
||||
final bool idleOnly;
|
||||
|
||||
/// 最近一次任务开始执行的本地时间;从未执行时为空。
|
||||
final DateTime? lastRunAt;
|
||||
|
||||
/// 最近一次成功完成的本地时间;尚无成功记录时为空。
|
||||
final DateTime? lastSuccessAt;
|
||||
|
||||
/// 最近一次运行状态,保存 [ScheduledTaskRunStatus.name]。
|
||||
final String lastStatus;
|
||||
|
||||
/// 最近一次运行结果的简短中文摘要。
|
||||
final String lastMessage;
|
||||
|
||||
/// 任务配置首次写入 SQLite 的本地时间。
|
||||
final DateTime createTime;
|
||||
|
||||
/// 配置或执行快照最近一次更新的本地时间。
|
||||
final DateTime updateTime;
|
||||
|
||||
/// 创建一条定时任务配置与最近执行快照。
|
||||
const ScheduledTaskEntity({
|
||||
required this.taskCode,
|
||||
required this.enabled,
|
||||
required this.cronExpression,
|
||||
required this.idleOnly,
|
||||
this.lastRunAt,
|
||||
this.lastSuccessAt,
|
||||
required this.lastStatus,
|
||||
required this.lastMessage,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复实体,仅供兼容迁移和调试序列化使用。
|
||||
factory ScheduledTaskEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScheduledTaskEntityFromJson(json);
|
||||
|
||||
/// 将实体转换为 JSON,仅供兼容迁移和调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$ScheduledTaskEntityToJson(this);
|
||||
|
||||
/// 保留主键与创建时间,生成包含配置或最近执行结果变更的新实体。
|
||||
ScheduledTaskEntity copyWith({
|
||||
bool? enabled,
|
||||
String? cronExpression,
|
||||
bool? idleOnly,
|
||||
DateTime? lastRunAt,
|
||||
DateTime? lastSuccessAt,
|
||||
String? lastStatus,
|
||||
String? lastMessage,
|
||||
DateTime? updateTime,
|
||||
}) =>
|
||||
ScheduledTaskEntity(
|
||||
taskCode: taskCode,
|
||||
enabled: enabled ?? this.enabled,
|
||||
cronExpression: cronExpression ?? this.cronExpression,
|
||||
idleOnly: idleOnly ?? this.idleOnly,
|
||||
lastRunAt: lastRunAt ?? this.lastRunAt,
|
||||
lastSuccessAt: lastSuccessAt ?? this.lastSuccessAt,
|
||||
lastStatus: lastStatus ?? this.lastStatus,
|
||||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
createTime: createTime,
|
||||
updateTime: updateTime ?? this.updateTime,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_task_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ScheduledTaskEntity _$ScheduledTaskEntityFromJson(Map<String, dynamic> json) =>
|
||||
ScheduledTaskEntity(
|
||||
taskCode: json['taskCode'] as String,
|
||||
enabled: json['enabled'] as bool,
|
||||
cronExpression: json['cronExpression'] as String,
|
||||
idleOnly: json['idleOnly'] as bool,
|
||||
lastRunAt: json['lastRunAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastRunAt'] as String),
|
||||
lastSuccessAt: json['lastSuccessAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastSuccessAt'] as String),
|
||||
lastStatus: json['lastStatus'] as String,
|
||||
lastMessage: json['lastMessage'] as String,
|
||||
createTime: DateTime.parse(json['createTime'] as String),
|
||||
updateTime: DateTime.parse(json['updateTime'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScheduledTaskEntityToJson(
|
||||
ScheduledTaskEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'taskCode': instance.taskCode,
|
||||
'enabled': instance.enabled,
|
||||
'cronExpression': instance.cronExpression,
|
||||
'idleOnly': instance.idleOnly,
|
||||
'lastRunAt': instance.lastRunAt?.toIso8601String(),
|
||||
'lastSuccessAt': instance.lastSuccessAt?.toIso8601String(),
|
||||
'lastStatus': instance.lastStatus,
|
||||
'lastMessage': instance.lastMessage,
|
||||
'createTime': instance.createTime.toIso8601String(),
|
||||
'updateTime': instance.updateTime.toIso8601String(),
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'scheduled_task_log_entity.g.dart';
|
||||
|
||||
/// 定时任务的结构化执行日志。
|
||||
///
|
||||
/// 来源、触发方式、状态和统计值使用独立列,日志页面无需解析拼接文本即可筛选。
|
||||
@JsonSerializable()
|
||||
@Entity(
|
||||
tableName: 'scheduled_task_log',
|
||||
indices: [
|
||||
Index(value: ['taskCode', 'startedAt']),
|
||||
Index(value: ['sourceType', 'status', 'startedAt']),
|
||||
],
|
||||
)
|
||||
class ScheduledTaskLogEntity {
|
||||
/// SQLite 自增主键;日志尚未插入数据库时为空。
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
final int? id;
|
||||
|
||||
/// 任务稳定编码,关联 scheduled_task.taskCode,但不建立外键以兼容任务版本演进。
|
||||
final String taskCode;
|
||||
|
||||
/// 执行发生时的任务展示名称快照。
|
||||
final String taskName;
|
||||
|
||||
/// 触发来源,保存 [ScheduledTaskTriggerType.name]。
|
||||
final String triggerType;
|
||||
|
||||
/// 主要依赖类型,保存 [ScheduledTaskSourceType.name]。
|
||||
final String sourceType;
|
||||
|
||||
/// 执行发生时的依赖名称快照,例如 MQTT 消息服务或 HTTP 商品接口。
|
||||
final String sourceName;
|
||||
|
||||
/// 本次执行状态,保存 [ScheduledTaskRunStatus.name]。
|
||||
final String status;
|
||||
|
||||
/// 本次执行结果的简短中文摘要。
|
||||
final String message;
|
||||
|
||||
/// 异常运行时的 Dart 异常类型;成功或跳过时为空。
|
||||
final String? errorType;
|
||||
|
||||
/// 截断并脱敏后的异常摘要;成功或跳过时为空。
|
||||
final String? errorMessage;
|
||||
|
||||
/// 截断后的异常堆栈,最多保存调度器允许的诊断长度。
|
||||
final String? errorStack;
|
||||
|
||||
/// 任务开始执行的本地时间。
|
||||
final DateTime startedAt;
|
||||
|
||||
/// 任务结束的本地时间;执行中日志为空。
|
||||
final DateTime? finishedAt;
|
||||
|
||||
/// 从开始到结束的耗时,单位为毫秒;执行中默认为零。
|
||||
final int durationMs;
|
||||
|
||||
/// 文件类任务扫描到的文件数量;不适用的任务为空。
|
||||
final int? scannedCount;
|
||||
|
||||
/// 文件类任务因未变化或服务端已存在而跳过的数量;不适用时为空。
|
||||
final int? skippedCount;
|
||||
|
||||
/// 文件类任务实际上传成功的数量;不适用时为空。
|
||||
final int? successCount;
|
||||
|
||||
/// 文件类任务处理失败的数量;不适用时为空。
|
||||
final int? failureCount;
|
||||
|
||||
/// 创建一条任务执行日志;开始记录尚未结束时不填写结束信息。
|
||||
const ScheduledTaskLogEntity({
|
||||
this.id,
|
||||
required this.taskCode,
|
||||
required this.taskName,
|
||||
required this.triggerType,
|
||||
required this.sourceType,
|
||||
required this.sourceName,
|
||||
required this.status,
|
||||
required this.message,
|
||||
this.errorType,
|
||||
this.errorMessage,
|
||||
this.errorStack,
|
||||
required this.startedAt,
|
||||
this.finishedAt,
|
||||
this.durationMs = 0,
|
||||
this.scannedCount,
|
||||
this.skippedCount,
|
||||
this.successCount,
|
||||
this.failureCount,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复任务日志,仅供调试序列化使用。
|
||||
factory ScheduledTaskLogEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScheduledTaskLogEntityFromJson(json);
|
||||
|
||||
/// 将任务日志转换为 JSON,仅供调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$ScheduledTaskLogEntityToJson(this);
|
||||
|
||||
/// 使用最终状态、错误和统计值生成同一条日志的完成快照。
|
||||
ScheduledTaskLogEntity finish({
|
||||
required String nextStatus,
|
||||
required String nextMessage,
|
||||
required DateTime at,
|
||||
String? nextErrorType,
|
||||
String? nextErrorMessage,
|
||||
String? nextErrorStack,
|
||||
int? scanned,
|
||||
int? skipped,
|
||||
int? succeeded,
|
||||
int? failed,
|
||||
}) =>
|
||||
ScheduledTaskLogEntity(
|
||||
id: id,
|
||||
taskCode: taskCode,
|
||||
taskName: taskName,
|
||||
triggerType: triggerType,
|
||||
sourceType: sourceType,
|
||||
sourceName: sourceName,
|
||||
status: nextStatus,
|
||||
message: nextMessage,
|
||||
errorType: nextErrorType,
|
||||
errorMessage: nextErrorMessage,
|
||||
errorStack: nextErrorStack,
|
||||
startedAt: startedAt,
|
||||
finishedAt: at,
|
||||
durationMs: at.difference(startedAt).inMilliseconds,
|
||||
scannedCount: scanned,
|
||||
skippedCount: skipped,
|
||||
successCount: succeeded,
|
||||
failureCount: failed,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_task_log_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ScheduledTaskLogEntity _$ScheduledTaskLogEntityFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
ScheduledTaskLogEntity(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
taskCode: json['taskCode'] as String,
|
||||
taskName: json['taskName'] as String,
|
||||
triggerType: json['triggerType'] as String,
|
||||
sourceType: json['sourceType'] as String,
|
||||
sourceName: json['sourceName'] as String,
|
||||
status: json['status'] as String,
|
||||
message: json['message'] as String,
|
||||
errorType: json['errorType'] as String?,
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
errorStack: json['errorStack'] as String?,
|
||||
startedAt: DateTime.parse(json['startedAt'] as String),
|
||||
finishedAt: json['finishedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['finishedAt'] as String),
|
||||
durationMs: (json['durationMs'] as num?)?.toInt() ?? 0,
|
||||
scannedCount: (json['scannedCount'] as num?)?.toInt(),
|
||||
skippedCount: (json['skippedCount'] as num?)?.toInt(),
|
||||
successCount: (json['successCount'] as num?)?.toInt(),
|
||||
failureCount: (json['failureCount'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScheduledTaskLogEntityToJson(
|
||||
ScheduledTaskLogEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'taskCode': instance.taskCode,
|
||||
'taskName': instance.taskName,
|
||||
'triggerType': instance.triggerType,
|
||||
'sourceType': instance.sourceType,
|
||||
'sourceName': instance.sourceName,
|
||||
'status': instance.status,
|
||||
'message': instance.message,
|
||||
'errorType': instance.errorType,
|
||||
'errorMessage': instance.errorMessage,
|
||||
'errorStack': instance.errorStack,
|
||||
'startedAt': instance.startedAt.toIso8601String(),
|
||||
'finishedAt': instance.finishedAt?.toIso8601String(),
|
||||
'durationMs': instance.durationMs,
|
||||
'scannedCount': instance.scannedCount,
|
||||
'skippedCount': instance.skippedCount,
|
||||
'successCount': instance.successCount,
|
||||
'failureCount': instance.failureCount,
|
||||
};
|
||||
Reference in new issue
Block a user