1132 lines
41 KiB
Dart
1132 lines
41 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cron/cron.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:mqtt_client/mqtt_client.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
/// 应用进程内任务调度入口。
|
|
///
|
|
/// 收银终端常驻时 Cron 可提供秒级规则;Android 杀死进程后不会继续执行,启动后由本类重新注册。
|
|
class SyncCheckTool {
|
|
/// 调度器写入本地诊断日志时使用的稳定模块标签。
|
|
static const _tag = '终端任务调度';
|
|
|
|
/// 禁止外部创建第二套调度状态,确保 Cron、运行集合和待执行队列全局唯一。
|
|
SyncCheckTool._();
|
|
|
|
/// 应用级任务调度单例,由 AppControl 初始化并由设置页读取响应式状态。
|
|
static final SyncCheckTool instance = SyncCheckTool._();
|
|
|
|
/// 当前已加载的任务配置,以稳定任务类型为键并向设置页提供响应式更新。
|
|
final configs = <ScheduledTaskType, ScheduledTaskConfig>{}.obs;
|
|
|
|
/// 每个任务最近一次执行结果快照,以稳定任务类型为键。
|
|
final records = <ScheduledTaskType, ScheduledTaskRecord>{}.obs;
|
|
|
|
/// 正在实际执行业务动作的任务集合,用于禁止同任务并发并驱动运行状态 UI。
|
|
final runningTasks = <ScheduledTaskType>{}.obs;
|
|
|
|
/// 正在等待安全窗口或等待前一次同类任务结束的任务集合。
|
|
final pendingTasks = <ScheduledTaskType>{}.obs;
|
|
|
|
/// 最近一次终端日志上传统计,供设置页展示扫描、跳过、成功和失败数量。
|
|
final terminalLogSummary = const TerminalLogUploadSummary().obs;
|
|
|
|
/// 当前进程内 Cron 调度器;任务全部停止后置空并关闭其内部计时资源。
|
|
Cron? _cron;
|
|
|
|
/// 已注册到 Cron 的任务句柄,以任务类型为键支持单任务重建和取消。
|
|
final Map<ScheduledTaskType, ScheduledTask> _scheduledTasks = {};
|
|
|
|
/// 同任务合并后的待执行请求,记录触发来源和需要满足的空闲级别。
|
|
final Map<ScheduledTaskType, _PendingTaskRequest> _pendingRequests = {};
|
|
|
|
/// 每 5 秒检查待执行任务安全窗口的轮询计时器;队列清空后自动释放。
|
|
Timer? _idleTimer;
|
|
|
|
/// 正在进行的释放操作,避免生命周期重复回调并发关闭同一个 Cron 实例。
|
|
Future<void>? _disposeFuture;
|
|
|
|
/// 是否已经完成本轮启动,防止 AppControl 重复注册相同 Cron 任务。
|
|
bool _started = false;
|
|
|
|
/// 当前进程最近一次补排部门人脸同步的本地日期,保证同一天最多自动补排一次。
|
|
DateTime? _lastDepartmentFaceCatchUpDate;
|
|
|
|
/// 常用规则均使用 cron 0.6.2 可解析的标准字段、范围和步长,不包含 Quartz 专属的 L/W/#。
|
|
static const List<ScheduledCronPreset> cronPresets = [
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 30 秒',
|
|
expression: '*/30 * * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 1 分钟',
|
|
expression: '0 * * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 2 分钟',
|
|
expression: '0 */2 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 3 分钟',
|
|
expression: '0 */3 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 5 分钟',
|
|
expression: '0 */5 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 10 分钟',
|
|
expression: '0 */10 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 15 分钟',
|
|
expression: '0 */15 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 20 分钟',
|
|
expression: '0 */20 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 30 分钟',
|
|
expression: '0 */30 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 45 分钟',
|
|
expression: '0 */45 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每小时',
|
|
expression: '0 0 * * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 2 小时',
|
|
expression: '0 0 */2 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 3 小时',
|
|
expression: '0 0 */3 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 4 小时',
|
|
expression: '0 0 */4 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 6 小时',
|
|
expression: '0 0 */6 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 8 小时',
|
|
expression: '0 0 */8 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.interval,
|
|
label: '每 12 小时',
|
|
expression: '0 0 */12 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 00:00',
|
|
expression: '0 0 0 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 00:10',
|
|
expression: '0 10 0 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 00:30',
|
|
expression: '0 30 0 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 00:40',
|
|
expression: '0 40 0 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 01:00',
|
|
expression: '0 0 1 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 02:00',
|
|
expression: '0 0 2 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 03:00',
|
|
expression: '0 0 3 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 04:00',
|
|
expression: '0 0 4 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 06:00',
|
|
expression: '0 0 6 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 08:00',
|
|
expression: '0 0 8 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 09:00',
|
|
expression: '0 0 9 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 12:00',
|
|
expression: '0 0 12 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 18:00',
|
|
expression: '0 0 18 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 21:00',
|
|
expression: '0 0 21 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 22:00',
|
|
expression: '0 0 22 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.daily,
|
|
label: '每天 23:00',
|
|
expression: '0 0 23 * * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '工作日 08:00',
|
|
expression: '0 0 8 * * 1-5'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '工作日 18:00',
|
|
expression: '0 0 18 * * 1-5'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周一 00:00',
|
|
expression: '0 0 0 * * 1'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周一 08:00',
|
|
expression: '0 0 8 * * 1'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周三 00:00',
|
|
expression: '0 0 0 * * 3'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周五 18:00',
|
|
expression: '0 0 18 * * 5'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周六 00:00',
|
|
expression: '0 0 0 * * 6'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.weekly,
|
|
label: '每周日 00:00',
|
|
expression: '0 0 0 * * 7'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 1 日 00:00',
|
|
expression: '0 0 0 1 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 1 日 03:00',
|
|
expression: '0 0 3 1 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 5 日 00:00',
|
|
expression: '0 0 0 5 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 10 日 00:00',
|
|
expression: '0 0 0 10 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 15 日 00:00',
|
|
expression: '0 0 0 15 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 20 日 00:00',
|
|
expression: '0 0 0 20 * *'),
|
|
ScheduledCronPreset(
|
|
group: ScheduledCronPresetGroup.monthly,
|
|
label: '每月 25 日 00:00',
|
|
expression: '0 0 0 25 * *'),
|
|
];
|
|
|
|
/// 首次建表或后续新增任务时写入的默认配置,不覆盖设备已经调整过的规则。
|
|
static const List<ScheduledTaskConfig> defaultConfigs = [
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.mqReconnect,
|
|
enabled: true,
|
|
cron: '0 * * * * *',
|
|
idleOnly: false,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.printCheck,
|
|
enabled: true,
|
|
cron: '0 */10 * * * *',
|
|
idleOnly: false,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.broadcastCheck,
|
|
enabled: true,
|
|
cron: '0 */2 * * * *',
|
|
idleOnly: false,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.deviceStatus,
|
|
enabled: true,
|
|
cron: '0 * * * * *',
|
|
idleOnly: false,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.autoUpdate,
|
|
enabled: true,
|
|
cron: '0 30 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.configSync,
|
|
enabled: false,
|
|
cron: '0 5 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.departmentFaceSync,
|
|
enabled: true,
|
|
cron: '0 40 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.classifySync,
|
|
enabled: false,
|
|
cron: '0 15 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.productSync,
|
|
enabled: false,
|
|
cron: '0 25 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.offlineOrderSync,
|
|
enabled: false,
|
|
cron: '0 35 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.terminalLogUpload,
|
|
enabled: true,
|
|
cron: '0 0 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
ScheduledTaskConfig(
|
|
type: ScheduledTaskType.expiredLogCleanup,
|
|
enabled: true,
|
|
cron: '0 10 0 * * *',
|
|
idleOnly: true,
|
|
),
|
|
];
|
|
|
|
/// 初始化结构化配置并注册全部已启用任务;同一生命周期内重复调用直接返回。
|
|
Future<void> startAll() async {
|
|
if (_started) return;
|
|
await _disposeFuture;
|
|
_started = true;
|
|
_cron ??= Cron();
|
|
await _loadState();
|
|
for (final config in configs.values) {
|
|
await _rebuildTask(config);
|
|
}
|
|
queueMissedDepartmentFaceSync();
|
|
}
|
|
|
|
/// 在每天 00:40 之后检查当天是否成功完成部门人脸同步,并在首次启动或恢复时补排一次。
|
|
///
|
|
/// 补排只加入同一人脸任务队列并要求完整空闲,不会阻塞启动或打断支付、识别和业务弹框。
|
|
void queueMissedDepartmentFaceSync() {
|
|
if (!_started) return;
|
|
final config = configs[ScheduledTaskType.departmentFaceSync];
|
|
if (config == null || !config.enabled) return;
|
|
final now = DateTime.now();
|
|
final scheduledAt = DateTime(now.year, now.month, now.day, 0, 40);
|
|
if (now.isBefore(scheduledAt)) return;
|
|
final lastSuccess =
|
|
records[ScheduledTaskType.departmentFaceSync]?.lastSuccessAt;
|
|
if (_isSameLocalDate(lastSuccess, now) ||
|
|
_isSameLocalDate(_lastDepartmentFaceCatchUpDate, now)) {
|
|
return;
|
|
}
|
|
_lastDepartmentFaceCatchUpDate = now;
|
|
unawaited(trigger(
|
|
ScheduledTaskType.departmentFaceSync,
|
|
catchUp: true,
|
|
triggerType: ScheduledTaskTriggerType.system,
|
|
));
|
|
}
|
|
|
|
/// 判断两个可空时间是否属于终端本地时区的同一自然日。
|
|
static bool _isSameLocalDate(DateTime? left, DateTime right) =>
|
|
left != null &&
|
|
left.year == right.year &&
|
|
left.month == right.month &&
|
|
left.day == right.day;
|
|
|
|
/// 完成旧 sysJson 一次性迁移,再从 SQLite 加载任务配置、最近结果和上传统计。
|
|
Future<void> _loadState() async {
|
|
final legacyConfigs = (await AppSettingTool.getScheduledTaskConfigs())
|
|
.where((item) => isValidCron(item.cron))
|
|
.toList(growable: false);
|
|
final legacyRecords = await AppSettingTool.getScheduledTaskRecords();
|
|
final legacyUploadStates =
|
|
await AppSettingTool.getTerminalLogUploadStates();
|
|
await ScheduledTaskStorageTool.initialize(
|
|
defaults: defaultConfigs,
|
|
legacyConfigs: legacyConfigs,
|
|
legacyRecords: legacyRecords,
|
|
);
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final hardId = setting.serviceId;
|
|
final canImportUploadState = hardId != null && hardId.isNotEmpty;
|
|
if (canImportUploadState && legacyUploadStates.isNotEmpty) {
|
|
await FileUploadRecordTool.importLegacy(hardId, legacyUploadStates);
|
|
}
|
|
await AppSettingTool.clearLegacyScheduledTaskState(
|
|
includeUploadState: canImportUploadState,
|
|
);
|
|
final state = await ScheduledTaskStorageTool.loadState();
|
|
configs.assignAll(state.configs);
|
|
records.assignAll(state.records);
|
|
final latestUploadLog = await ScheduledTaskStorageTool.getLatestLog(
|
|
ScheduledTaskType.terminalLogUpload,
|
|
);
|
|
if (latestUploadLog != null) {
|
|
terminalLogSummary.value = TerminalLogUploadSummary(
|
|
scanned: latestUploadLog.scannedCount ?? 0,
|
|
skipped: latestUploadLog.skippedCount ?? 0,
|
|
succeeded: latestUploadLog.successCount ?? 0,
|
|
failed: latestUploadLog.failureCount ?? 0,
|
|
lastSuccessAt:
|
|
latestUploadLog.status == ScheduledTaskRunStatus.success.name
|
|
? latestUploadLog.finishedAt
|
|
: records[ScheduledTaskType.terminalLogUpload]?.lastSuccessAt,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 校验 cron 0.6.2 使用的六段表达式及每段数值范围,返回是否可以安全注册。
|
|
static bool isValidCron(String expression) {
|
|
final fields = expression.trim().split(RegExp(r'\s+'));
|
|
if (fields.length != 6) return false;
|
|
final ranges = const [
|
|
(min: 0, max: 59),
|
|
(min: 0, max: 59),
|
|
(min: 0, max: 23),
|
|
(min: 1, max: 31),
|
|
(min: 1, max: 12),
|
|
(min: 0, max: 7),
|
|
];
|
|
for (var index = 0; index < fields.length; index++) {
|
|
if (!_isValidCronField(
|
|
fields[index],
|
|
min: ranges[index].min,
|
|
max: ranges[index].max,
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
try {
|
|
Schedule.parse(expression);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// cron 0.6.2 会静默过滤越界数字,因此保存前额外校验每一段,避免 `99 * * * * *`
|
|
/// 这类永远不会触发的表达式被误判为有效。
|
|
static bool _isValidCronField(
|
|
String field, {
|
|
required int min,
|
|
required int max,
|
|
}) {
|
|
if (field.isEmpty) return false;
|
|
for (final segment in field.split(',')) {
|
|
final intervalParts = segment.split('/');
|
|
if (intervalParts.length > 2) return false;
|
|
final base = intervalParts.first;
|
|
if (intervalParts.length == 2) {
|
|
final interval = int.tryParse(intervalParts.last);
|
|
if (interval == null || interval <= 0) return false;
|
|
if (base != '*' && !base.contains('-')) return false;
|
|
}
|
|
if (base == '*') continue;
|
|
if (base.contains('-')) {
|
|
final range = base.split('-');
|
|
if (range.length != 2) return false;
|
|
final lower = int.tryParse(range.first);
|
|
final upper = int.tryParse(range.last);
|
|
if (lower == null ||
|
|
upper == null ||
|
|
lower < min ||
|
|
upper > max ||
|
|
lower > upper) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
final value = int.tryParse(base);
|
|
if (value == null || value < min || value > max) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// 保存单个任务配置并仅重建该任务;任一步失败时恢复旧配置和旧调度句柄。
|
|
Future<void> updateConfig(ScheduledTaskConfig config) async {
|
|
if (!isValidCron(config.cron)) {
|
|
throw const FormatException('请输入有效的 6 段 Cron 表达式');
|
|
}
|
|
final previous = configs[config.type];
|
|
configs[config.type] = config;
|
|
try {
|
|
await _rebuildTask(config);
|
|
await ScheduledTaskStorageTool.saveConfig(config);
|
|
} catch (error, stack) {
|
|
if (previous == null) {
|
|
configs.remove(config.type);
|
|
} else {
|
|
configs[config.type] = previous;
|
|
}
|
|
// 调度注册和本地配置必须一起回滚,避免界面显示旧值但后台仍按新规则运行。
|
|
try {
|
|
if (previous == null) {
|
|
await _scheduledTasks.remove(config.type)?.cancel();
|
|
} else {
|
|
await _rebuildTask(previous);
|
|
}
|
|
if (previous != null) {
|
|
await ScheduledTaskStorageTool.saveConfig(previous);
|
|
}
|
|
} catch (rollbackError, rollbackStack) {
|
|
logger.e(
|
|
'|$_tag|任务配置回滚失败type:${config.type.name}|error:$rollbackError|stack:$rollbackStack|',
|
|
);
|
|
}
|
|
logger.e(
|
|
'|$_tag|任务配置保存失败type:${config.type.name}|error:$error|stack:$stack|',
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// 取消任务旧句柄,并按当前开关和 Cron 表达式重新注册对应任务。
|
|
Future<void> _rebuildTask(ScheduledTaskConfig config) async {
|
|
await _scheduledTasks.remove(config.type)?.cancel();
|
|
if (!_started || !config.enabled) return;
|
|
_scheduledTasks[config.type] = _cron!.schedule(
|
|
Schedule.parse(config.cron),
|
|
() => trigger(config.type, scheduled: true),
|
|
);
|
|
}
|
|
|
|
/// 手动和 MQTT 触发不检查时间规则;同任务重复触发只保留一个合并后的待执行请求。
|
|
Future<void> trigger(
|
|
ScheduledTaskType type, {
|
|
bool currentLogOnly = false,
|
|
bool scheduled = false,
|
|
bool catchUp = false,
|
|
Set<String>? faceUserIds,
|
|
bool debugAllFaces = false,
|
|
ScheduledTaskTriggerType triggerType = ScheduledTaskTriggerType.manual,
|
|
}) async {
|
|
final config =
|
|
configs[type] ?? defaultConfigs.firstWhere((item) => item.type == type);
|
|
final request = _PendingTaskRequest(
|
|
currentLogOnly: currentLogOnly,
|
|
requireSafeWindow: !scheduled || config.idleOnly,
|
|
requireFullIdle: (scheduled || catchUp) && config.idleOnly,
|
|
triggerType: scheduled ? ScheduledTaskTriggerType.cron : triggerType,
|
|
faceUserIds: faceUserIds == null ? null : Set.unmodifiable(faceUserIds),
|
|
fullFaceSnapshot:
|
|
type == ScheduledTaskType.departmentFaceSync && faceUserIds == null,
|
|
debugAllFaces: debugAllFaces,
|
|
);
|
|
if (runningTasks.contains(type)) {
|
|
_enqueue(type, request);
|
|
return;
|
|
}
|
|
if (!_isReady(request)) {
|
|
_enqueue(type, request);
|
|
_ensureIdleTimer();
|
|
return;
|
|
}
|
|
await _execute(type, request: request);
|
|
}
|
|
|
|
/// 将任务加入等待集合;同任务已有请求时按更完整的处理范围合并。
|
|
void _enqueue(ScheduledTaskType type, _PendingTaskRequest request) {
|
|
final existing = _pendingRequests[type];
|
|
_pendingRequests[type] = existing?.merge(request) ?? request;
|
|
pendingTasks.add(type);
|
|
}
|
|
|
|
/// 判断请求当前是否满足无需等待、业务安全窗口或完整空闲窗口中的对应条件。
|
|
bool _isReady(_PendingTaskRequest request) {
|
|
if (!request.requireSafeWindow) return true;
|
|
return request.requireFullIdle
|
|
? SyncIdleTool.instance.isIdle
|
|
: SyncIdleTool.instance.isSafeWindow;
|
|
}
|
|
|
|
/// 从可观察集合和内部请求映射同时取出一个待执行任务,避免 UI 与队列状态分离。
|
|
_PendingTaskRequest? _takePending(ScheduledTaskType type) {
|
|
pendingTasks.remove(type);
|
|
return _pendingRequests.remove(type);
|
|
}
|
|
|
|
/// 在存在待执行任务时启动唯一轮询器,安全窗口满足后按队列快照依次执行。
|
|
void _ensureIdleTimer() {
|
|
_idleTimer ??= Timer.periodic(const Duration(seconds: 5), (_) async {
|
|
if (pendingTasks.isEmpty) {
|
|
_idleTimer?.cancel();
|
|
_idleTimer = null;
|
|
return;
|
|
}
|
|
final tasks = pendingTasks.toList(growable: false);
|
|
for (final type in tasks) {
|
|
if (runningTasks.contains(type)) continue;
|
|
final request = _pendingRequests[type];
|
|
if (request == null || !_isReady(request)) continue;
|
|
_takePending(type);
|
|
await _execute(type, request: request);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 执行一次任务并同步运行集合、最近结果及结构化日志,结束后处理合并请求。
|
|
Future<void> _execute(
|
|
ScheduledTaskType type, {
|
|
required _PendingTaskRequest request,
|
|
}) async {
|
|
runningTasks.add(type);
|
|
final startedAt = DateTime.now();
|
|
final descriptor = _taskDescriptor(type);
|
|
ScheduledTaskLogEntity? executionLog;
|
|
try {
|
|
executionLog = await ScheduledTaskStorageTool.startLog(
|
|
type: type,
|
|
triggerType: request.triggerType,
|
|
sourceType: descriptor.sourceType,
|
|
taskName: descriptor.title,
|
|
sourceName: descriptor.sourceName,
|
|
startedAt: startedAt,
|
|
);
|
|
} catch (error, stack) {
|
|
// 诊断日志写入失败不能阻断收银终端本身的同步任务。
|
|
logger.w('|$_tag|创建任务日志失败type:${type.name}|error:$error|stack:$stack|');
|
|
}
|
|
final heavy = _isHeavy(type);
|
|
if (heavy) SyncIdleTool.instance.beginHeavyTask();
|
|
try {
|
|
final result = await _runAction(
|
|
type,
|
|
request: request,
|
|
);
|
|
records[type] = ScheduledTaskRecord(
|
|
lastRunAt: startedAt,
|
|
lastSuccessAt: result.status == ScheduledTaskRunStatus.success
|
|
? DateTime.now()
|
|
: records[type]?.lastSuccessAt,
|
|
success: true,
|
|
message: result.message,
|
|
);
|
|
await _safeSaveRecord(
|
|
type,
|
|
records[type]!,
|
|
result.status,
|
|
);
|
|
if (executionLog != null) {
|
|
await _safeFinishLog(
|
|
executionLog.finish(
|
|
nextStatus: result.status.name,
|
|
nextMessage: result.message,
|
|
at: DateTime.now(),
|
|
scanned: result.summary?.scanned,
|
|
skipped: result.summary?.skipped,
|
|
succeeded: result.summary?.succeeded,
|
|
failed: result.summary?.failed,
|
|
),
|
|
);
|
|
}
|
|
} catch (error, stack) {
|
|
if (error is TerminalLogUploadException) {
|
|
terminalLogSummary.value = error.summary;
|
|
}
|
|
final message = switch (error) {
|
|
TerminalLogUploadException() => error.summary.label,
|
|
FaceSyncPartialFailureException() => error.summary.message,
|
|
_ => error.toString(),
|
|
};
|
|
records[type] = ScheduledTaskRecord(
|
|
lastRunAt: startedAt,
|
|
lastSuccessAt: records[type]?.lastSuccessAt,
|
|
success: false,
|
|
message: message,
|
|
);
|
|
await _safeSaveRecord(
|
|
type,
|
|
records[type]!,
|
|
ScheduledTaskRunStatus.failed,
|
|
);
|
|
if (executionLog != null) {
|
|
await _safeFinishLog(
|
|
executionLog.finish(
|
|
nextStatus: ScheduledTaskRunStatus.failed.name,
|
|
nextMessage: message,
|
|
at: DateTime.now(),
|
|
nextErrorType: error.runtimeType.toString(),
|
|
nextErrorMessage: _safeError(error),
|
|
nextErrorStack: _safeStack(stack),
|
|
scanned: switch (error) {
|
|
TerminalLogUploadException() => error.summary.scanned,
|
|
FaceSyncPartialFailureException() => error.summary.scanned,
|
|
_ => null,
|
|
},
|
|
skipped: switch (error) {
|
|
TerminalLogUploadException() => error.summary.skipped,
|
|
FaceSyncPartialFailureException() => error.summary.skipped,
|
|
_ => null,
|
|
},
|
|
succeeded: switch (error) {
|
|
TerminalLogUploadException() => error.summary.succeeded,
|
|
FaceSyncPartialFailureException() => error.summary.changed,
|
|
_ => null,
|
|
},
|
|
failed: switch (error) {
|
|
TerminalLogUploadException() => error.summary.failed,
|
|
FaceSyncPartialFailureException() => error.summary.failed,
|
|
_ => null,
|
|
},
|
|
),
|
|
);
|
|
}
|
|
if (error is ScheduledTaskDependencyException) {
|
|
logger.w('|$_tag|任务依赖暂不可用type:${type.name}|error:${error.message}|');
|
|
} else {
|
|
logger.e(
|
|
'|$_tag|任务执行失败type:${type.name}|error:$error|stack:$stack|',
|
|
);
|
|
}
|
|
} finally {
|
|
if (heavy) SyncIdleTool.instance.endHeavyTask();
|
|
runningTasks.remove(type);
|
|
final nextRequest = _pendingRequests[type];
|
|
if (nextRequest != null) {
|
|
if (_isReady(nextRequest)) {
|
|
_takePending(type);
|
|
unawaited(_execute(type, request: nextRequest));
|
|
} else {
|
|
_ensureIdleTimer();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 结构化诊断数据属于旁路能力,SQLite 暂时不可写时不能把已经成功的业务任务改判为失败。
|
|
Future<void> _safeSaveRecord(
|
|
ScheduledTaskType type,
|
|
ScheduledTaskRecord record,
|
|
ScheduledTaskRunStatus status,
|
|
) async {
|
|
try {
|
|
await ScheduledTaskStorageTool.saveRecord(type, record, status);
|
|
} catch (error, stack) {
|
|
logger.w(
|
|
'|$_tag|保存任务状态失败type:${type.name}|error:$error|stack:$stack|',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 完成日志和历史裁剪失败只写本地告警,原始 HTTP、MQTT 或插件结果保持不变。
|
|
Future<void> _safeFinishLog(ScheduledTaskLogEntity log) async {
|
|
try {
|
|
await ScheduledTaskStorageTool.finishLog(log);
|
|
} catch (error, stack) {
|
|
logger.w(
|
|
'|$_tag|完成任务日志失败type:${log.taskCode}|error:$error|stack:$stack|',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 将稳定任务类型分派到既有 MQTT、HTTP、插件或文件业务入口并返回统一结果。
|
|
Future<_ScheduledTaskActionResult> _runAction(
|
|
ScheduledTaskType type, {
|
|
required _PendingTaskRequest request,
|
|
}) async {
|
|
switch (type) {
|
|
case ScheduledTaskType.mqReconnect:
|
|
if (!AppNetworkTool.isAutoNetwork()) {
|
|
return const _ScheduledTaskActionResult.skipped('网络不可用,本次跳过');
|
|
}
|
|
if (MqManager.state == MqttConnectionState.connected) {
|
|
return const _ScheduledTaskActionResult.success('消息服务连接正常');
|
|
}
|
|
await AppMqTool.mqRefresh();
|
|
if (MqManager.state != MqttConnectionState.connected) {
|
|
throw ScheduledTaskDependencyException(
|
|
MqManager.connectionFailureReason ?? '消息服务暂未连接',
|
|
);
|
|
}
|
|
return const _ScheduledTaskActionResult.success('消息服务重连成功');
|
|
case ScheduledTaskType.printCheck:
|
|
if (!AppNetworkTool.isAutoNetwork()) {
|
|
return const _ScheduledTaskActionResult.skipped('网络不可用,本次跳过');
|
|
}
|
|
await SyncPrintTool.doCheckPrint();
|
|
await SyncPrintTool.doCheckSync();
|
|
return const _ScheduledTaskActionResult.success('打印任务检查完成');
|
|
case ScheduledTaskType.broadcastCheck:
|
|
if (!AppNetworkTool.isAutoNetwork()) {
|
|
return const _ScheduledTaskActionResult.skipped('网络不可用,本次跳过');
|
|
}
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
await SyncTakeFoodBroadcastTool.doCheckBroadcast(
|
|
setting.hourType ?? ProductHourType.TOTAL,
|
|
);
|
|
return const _ScheduledTaskActionResult.success('取餐广播检查完成');
|
|
case ScheduledTaskType.deviceStatus:
|
|
await AppDeviceStatusTool.refreshAll();
|
|
return const _ScheduledTaskActionResult.success('设备状态检查完成');
|
|
case ScheduledTaskType.autoUpdate:
|
|
final update = await TerminalAutoUpdateTool.run();
|
|
return update.skipped
|
|
? _ScheduledTaskActionResult.skipped(update.message)
|
|
: _ScheduledTaskActionResult.success(update.message);
|
|
case ScheduledTaskType.configSync:
|
|
await SyncConfigTool.autoSyncSysInfo();
|
|
return const _ScheduledTaskActionResult.success('设备配置同步完成');
|
|
case ScheduledTaskType.departmentFaceSync:
|
|
final mode = request.debugAllFaces
|
|
? FaceSyncMode.debugAllSnapshot
|
|
: request.faceUserIds == null
|
|
? FaceSyncMode.departmentSnapshot
|
|
: FaceSyncMode.userDelta;
|
|
final summary = await SyncFaceTool.sync(
|
|
mode: mode,
|
|
userIds: request.faceUserIds,
|
|
);
|
|
if (summary.changed > 0 &&
|
|
request.triggerType == ScheduledTaskTriggerType.mqtt &&
|
|
Get.isRegistered<AppControl>()) {
|
|
Get.find<AppControl>().mqUpdateEventBus.fire(MqtActionEnum.FACE);
|
|
}
|
|
if (summary.hasFailures) {
|
|
throw FaceSyncPartialFailureException(summary);
|
|
}
|
|
return _ScheduledTaskActionResult.success(
|
|
summary.message,
|
|
summary: TerminalLogUploadSummary(
|
|
scanned: summary.scanned,
|
|
skipped: summary.skipped,
|
|
succeeded: summary.changed,
|
|
failed: summary.failed,
|
|
),
|
|
);
|
|
case ScheduledTaskType.classifySync:
|
|
await SyncProductTool.syncClassify();
|
|
return const _ScheduledTaskActionResult.success('分类同步完成');
|
|
case ScheduledTaskType.productSync:
|
|
await SyncProductTool.syncProduct();
|
|
return const _ScheduledTaskActionResult.success('商品同步完成');
|
|
case ScheduledTaskType.offlineOrderSync:
|
|
await SyncOrderTool.syncOfflineOrder();
|
|
return const _ScheduledTaskActionResult.success('离线订单同步完成');
|
|
case ScheduledTaskType.terminalLogUpload:
|
|
final summary = await TerminalLogUploadTool.upload(
|
|
currentOnly: request.currentLogOnly,
|
|
);
|
|
terminalLogSummary.value = summary;
|
|
return _ScheduledTaskActionResult.success(
|
|
summary.label,
|
|
summary: summary,
|
|
);
|
|
case ScheduledTaskType.expiredLogCleanup:
|
|
final uploadSummary = await TerminalLogUploadTool.upload();
|
|
terminalLogSummary.value = uploadSummary;
|
|
final deleted = await TerminalLogUploadTool.cleanupExpiredLogs();
|
|
return _ScheduledTaskActionResult.success(
|
|
'上传检查完成,清理 $deleted 个过期日志',
|
|
summary: uploadSummary,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 返回任务是否会占用较多网络、磁盘或数据处理资源,需要进入重型任务互斥区。
|
|
bool _isHeavy(ScheduledTaskType type) {
|
|
return const {
|
|
ScheduledTaskType.configSync,
|
|
ScheduledTaskType.departmentFaceSync,
|
|
ScheduledTaskType.classifySync,
|
|
ScheduledTaskType.productSync,
|
|
ScheduledTaskType.offlineOrderSync,
|
|
ScheduledTaskType.autoUpdate,
|
|
ScheduledTaskType.terminalLogUpload,
|
|
ScheduledTaskType.expiredLogCleanup,
|
|
}.contains(type);
|
|
}
|
|
|
|
/// 返回任务设置页和执行日志共用的中文名称。
|
|
static String taskTitle(ScheduledTaskType type) =>
|
|
_taskDescriptor(type).title;
|
|
|
|
/// 返回任务执行日志使用的主要依赖类型。
|
|
static ScheduledTaskSourceType taskSourceType(ScheduledTaskType type) =>
|
|
_taskDescriptor(type).sourceType;
|
|
|
|
/// 返回任务执行日志使用的具体依赖名称。
|
|
static String taskSourceName(ScheduledTaskType type) =>
|
|
_taskDescriptor(type).sourceName;
|
|
|
|
/// 从 SQLite 查询指定任务最近日志,并可按依赖类型过滤。
|
|
Future<List<ScheduledTaskLogEntity>> loadLogs(
|
|
ScheduledTaskType type, {
|
|
ScheduledTaskSourceType? sourceType,
|
|
}) =>
|
|
ScheduledTaskStorageTool.getLogs(type, sourceType: sourceType);
|
|
|
|
/// 从 SQLite 分页查询指定任务日志,并返回筛选后的总数与校正页码。
|
|
Future<
|
|
({
|
|
List<ScheduledTaskLogEntity> records,
|
|
int total,
|
|
int currentPage,
|
|
})> loadLogPage(
|
|
ScheduledTaskType type, {
|
|
ScheduledTaskSourceType? sourceType,
|
|
required int currentPage,
|
|
required int pageSize,
|
|
}) =>
|
|
ScheduledTaskStorageTool.getLogPage(
|
|
type,
|
|
sourceType: sourceType,
|
|
currentPage: currentPage,
|
|
pageSize: pageSize,
|
|
);
|
|
|
|
/// 将任务类型映射为稳定展示名称和依赖分类,不把 UI 文案写入持久化主键。
|
|
static _ScheduledTaskDescriptor _taskDescriptor(ScheduledTaskType type) =>
|
|
switch (type) {
|
|
ScheduledTaskType.mqReconnect => const _ScheduledTaskDescriptor(
|
|
title: 'MQTT 重连',
|
|
sourceType: ScheduledTaskSourceType.mqtt,
|
|
sourceName: 'MQTT 消息服务',
|
|
),
|
|
ScheduledTaskType.printCheck => const _ScheduledTaskDescriptor(
|
|
title: '打印任务检查',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 接口与打印插件',
|
|
),
|
|
ScheduledTaskType.broadcastCheck => const _ScheduledTaskDescriptor(
|
|
title: '取餐广播检查',
|
|
sourceType: ScheduledTaskSourceType.local,
|
|
sourceName: '本地广播队列',
|
|
),
|
|
ScheduledTaskType.deviceStatus => const _ScheduledTaskDescriptor(
|
|
title: '设备状态检查',
|
|
sourceType: ScheduledTaskSourceType.plugin,
|
|
sourceName: '终端硬件插件',
|
|
),
|
|
ScheduledTaskType.autoUpdate => const _ScheduledTaskDescriptor(
|
|
title: 'APK 自动更新',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: '版本接口、私有下载目录与 Root 安装服务',
|
|
),
|
|
ScheduledTaskType.configSync => const _ScheduledTaskDescriptor(
|
|
title: '设备配置同步',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 配置接口',
|
|
),
|
|
ScheduledTaskType.departmentFaceSync => const _ScheduledTaskDescriptor(
|
|
title: '本部门人脸同步',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 人脸快照与虹软插件',
|
|
),
|
|
ScheduledTaskType.classifySync => const _ScheduledTaskDescriptor(
|
|
title: '分类同步',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 分类接口',
|
|
),
|
|
ScheduledTaskType.productSync => const _ScheduledTaskDescriptor(
|
|
title: '商品同步',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 商品接口',
|
|
),
|
|
ScheduledTaskType.offlineOrderSync => const _ScheduledTaskDescriptor(
|
|
title: '离线订单同步',
|
|
sourceType: ScheduledTaskSourceType.http,
|
|
sourceName: 'HTTP 订单接口',
|
|
),
|
|
ScheduledTaskType.terminalLogUpload => const _ScheduledTaskDescriptor(
|
|
title: '终端日志上传',
|
|
sourceType: ScheduledTaskSourceType.file,
|
|
sourceName: '本地文件与 HTTP 上传接口',
|
|
),
|
|
ScheduledTaskType.expiredLogCleanup => const _ScheduledTaskDescriptor(
|
|
title: '过期日志清理',
|
|
sourceType: ScheduledTaskSourceType.file,
|
|
sourceName: '本地日志文件',
|
|
),
|
|
};
|
|
|
|
/// 将异常摘要截断到 1000 个字符,避免单条任务日志异常膨胀。
|
|
static String _safeError(Object error) {
|
|
final text = error.toString();
|
|
return text.length <= 1000 ? text : '${text.substring(0, 1000)}...';
|
|
}
|
|
|
|
/// 将异常堆栈截断到 4000 个字符,在保留诊断价值的同时限制 SQLite 记录大小。
|
|
static String _safeStack(StackTrace stack) {
|
|
final text = stack.toString();
|
|
return text.length <= 4000 ? text : '${text.substring(0, 4000)}...';
|
|
}
|
|
|
|
/// 幂等释放调度资源;并发调用会等待同一个正在执行的释放操作。
|
|
Future<void> dispose() async {
|
|
final disposing = _disposeFuture;
|
|
if (disposing != null) return disposing;
|
|
final operation = _dispose();
|
|
_disposeFuture = operation;
|
|
try {
|
|
await operation;
|
|
} finally {
|
|
if (identical(_disposeFuture, operation)) _disposeFuture = null;
|
|
}
|
|
}
|
|
|
|
/// 取消轮询和全部 Cron 句柄、清空待执行状态并关闭 Cron 实例。
|
|
Future<void> _dispose() async {
|
|
_started = false;
|
|
_idleTimer?.cancel();
|
|
_idleTimer = null;
|
|
for (final task in _scheduledTasks.values) {
|
|
await task.cancel();
|
|
}
|
|
_scheduledTasks.clear();
|
|
_pendingRequests.clear();
|
|
pendingTasks.clear();
|
|
final cron = _cron;
|
|
_cron = null;
|
|
await cron?.close();
|
|
}
|
|
}
|
|
|
|
/// 尚未执行或同任务运行中再次触发时保存的合并请求。
|
|
class _PendingTaskRequest {
|
|
/// 创建一次待执行请求,明确日志范围、空闲要求和触发来源。
|
|
const _PendingTaskRequest({
|
|
required this.currentLogOnly,
|
|
required this.requireSafeWindow,
|
|
required this.requireFullIdle,
|
|
required this.triggerType,
|
|
required this.faceUserIds,
|
|
required this.fullFaceSnapshot,
|
|
required this.debugAllFaces,
|
|
});
|
|
|
|
/// 是否只上传 Logger 当前写入的日志文件;为假时扫描全部日志。
|
|
final bool currentLogOnly;
|
|
|
|
/// 是否必须等待支付、人脸、遮罩、弹窗和其他重型任务全部结束。
|
|
final bool requireSafeWindow;
|
|
|
|
/// 是否还需在安全窗口基础上满足连续 60 秒无人操作。
|
|
final bool requireFullIdle;
|
|
|
|
/// 最终写入任务执行日志的触发来源。
|
|
final ScheduledTaskTriggerType triggerType;
|
|
|
|
/// FACE 用户增量回查集合;部门完整快照时为空。
|
|
final Set<String>? faceUserIds;
|
|
|
|
/// 当前请求是否为部门或调试完整快照,用于合并时让全量覆盖用户增量。
|
|
final bool fullFaceSnapshot;
|
|
|
|
/// 是否执行仅非生产可触发的全租户调试快照。
|
|
final bool debugAllFaces;
|
|
|
|
/// 全量日志请求优先于“仅当前文件”;任一手动请求到达后只需等待安全窗口。
|
|
_PendingTaskRequest merge(_PendingTaskRequest other) {
|
|
final mergedFullFaceSnapshot = fullFaceSnapshot || other.fullFaceSnapshot;
|
|
final mergedUserIds = mergedFullFaceSnapshot
|
|
? null
|
|
: <String>{...?faceUserIds, ...?other.faceUserIds};
|
|
return _PendingTaskRequest(
|
|
currentLogOnly: currentLogOnly && other.currentLogOnly,
|
|
requireSafeWindow: requireSafeWindow || other.requireSafeWindow,
|
|
// Cron/补排全量一旦进入队列,不因随后到达的 MQ 增量而降低 60 秒空闲要求。
|
|
requireFullIdle: mergedFullFaceSnapshot
|
|
? requireFullIdle || other.requireFullIdle
|
|
: requireFullIdle && other.requireFullIdle,
|
|
triggerType: triggerType == ScheduledTaskTriggerType.cron
|
|
? other.triggerType
|
|
: triggerType,
|
|
faceUserIds:
|
|
mergedUserIds == null ? null : Set.unmodifiable(mergedUserIds),
|
|
fullFaceSnapshot: mergedFullFaceSnapshot,
|
|
debugAllFaces: debugAllFaces || other.debugAllFaces,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// MQTT 等外部依赖暂不可用时使用的可诊断任务异常。
|
|
class ScheduledTaskDependencyException implements Exception {
|
|
/// 使用面向终端人员的安全摘要创建依赖异常。
|
|
const ScheduledTaskDependencyException(this.message);
|
|
|
|
/// 不包含地址、凭证或堆栈的依赖失败摘要。
|
|
final String message;
|
|
|
|
/// 返回可直接写入任务最近结果的依赖失败摘要。
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
/// 单个业务动作完成后返回给统一调度收尾流程的结果。
|
|
class _ScheduledTaskActionResult {
|
|
/// 创建成功结果,可附带文件任务的上传统计。
|
|
const _ScheduledTaskActionResult.success(this.message, {this.summary})
|
|
: status = ScheduledTaskRunStatus.success;
|
|
|
|
/// 创建无需继续处理的跳过结果,不携带文件上传统计。
|
|
const _ScheduledTaskActionResult.skipped(this.message)
|
|
: status = ScheduledTaskRunStatus.skipped,
|
|
summary = null;
|
|
|
|
/// 设置页和任务日志使用的简短中文结果摘要。
|
|
final String message;
|
|
|
|
/// 本次动作的最终运行状态,仅包含成功或跳过。
|
|
final ScheduledTaskRunStatus status;
|
|
|
|
/// 日志上传或清理任务的文件统计,其他任务为空。
|
|
final TerminalLogUploadSummary? summary;
|
|
}
|
|
|
|
/// 任务展示名称及主要依赖来源的稳定描述。
|
|
class _ScheduledTaskDescriptor {
|
|
/// 创建一项任务元数据,仅用于展示和结构化日志分类。
|
|
const _ScheduledTaskDescriptor({
|
|
required this.title,
|
|
required this.sourceType,
|
|
required this.sourceName,
|
|
});
|
|
|
|
/// 面向终端人员的中文任务名称。
|
|
final String title;
|
|
|
|
/// MQTT、HTTP、插件、文件或本地逻辑中的主要依赖分类。
|
|
final ScheduledTaskSourceType sourceType;
|
|
|
|
/// 执行日志展示的具体依赖名称快照。
|
|
final String sourceName;
|
|
}
|