feat:人脸机器实时增量更新
This commit is contained in:
1 parent
8cfbbd768e
commit
3770173604
29 files changed
+1584
-359
No files matched your search
@@ -52,6 +52,9 @@ class SyncCheckTool {
|
||||
/// 是否已经完成本轮启动,防止 AppControl 重复注册相同 Cron 任务。
|
||||
bool _started = false;
|
||||
|
||||
/// 当前进程最近一次补排部门人脸同步的本地日期,保证同一天最多自动补排一次。
|
||||
DateTime? _lastDepartmentFaceCatchUpDate;
|
||||
|
||||
/// 常用规则均使用 cron 0.6.2 可解析的标准字段、范围和步长,不包含 Quartz 专属的 L/W/#。
|
||||
static const List<ScheduledCronPreset> cronPresets = [
|
||||
ScheduledCronPreset(
|
||||
@@ -134,6 +137,10 @@ class SyncCheckTool {
|
||||
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',
|
||||
@@ -282,6 +289,12 @@ class SyncCheckTool {
|
||||
cron: '0 5 0 * * *',
|
||||
idleOnly: true,
|
||||
),
|
||||
ScheduledTaskConfig(
|
||||
type: ScheduledTaskType.departmentFaceSync,
|
||||
enabled: true,
|
||||
cron: '0 40 0 * * *',
|
||||
idleOnly: true,
|
||||
),
|
||||
ScheduledTaskConfig(
|
||||
type: ScheduledTaskType.classifySync,
|
||||
enabled: false,
|
||||
@@ -324,8 +337,40 @@ class SyncCheckTool {
|
||||
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())
|
||||
@@ -488,6 +533,9 @@ class SyncCheckTool {
|
||||
ScheduledTaskType type, {
|
||||
bool currentLogOnly = false,
|
||||
bool scheduled = false,
|
||||
bool catchUp = false,
|
||||
Set<String>? faceUserIds,
|
||||
bool debugAllFaces = false,
|
||||
ScheduledTaskTriggerType triggerType = ScheduledTaskTriggerType.manual,
|
||||
}) async {
|
||||
final config =
|
||||
@@ -495,8 +543,12 @@ class SyncCheckTool {
|
||||
final request = _PendingTaskRequest(
|
||||
currentLogOnly: currentLogOnly,
|
||||
requireSafeWindow: !scheduled || config.idleOnly,
|
||||
requireFullIdle: 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);
|
||||
@@ -577,7 +629,7 @@ class SyncCheckTool {
|
||||
try {
|
||||
final result = await _runAction(
|
||||
type,
|
||||
currentLogOnly: request.currentLogOnly,
|
||||
request: request,
|
||||
);
|
||||
records[type] = ScheduledTaskRecord(
|
||||
lastRunAt: startedAt,
|
||||
@@ -609,9 +661,11 @@ class SyncCheckTool {
|
||||
if (error is TerminalLogUploadException) {
|
||||
terminalLogSummary.value = error.summary;
|
||||
}
|
||||
final message = error is TerminalLogUploadException
|
||||
? error.summary.label
|
||||
: error.toString();
|
||||
final message = switch (error) {
|
||||
TerminalLogUploadException() => error.summary.label,
|
||||
FaceSyncPartialFailureException() => error.summary.message,
|
||||
_ => error.toString(),
|
||||
};
|
||||
records[type] = ScheduledTaskRecord(
|
||||
lastRunAt: startedAt,
|
||||
lastSuccessAt: records[type]?.lastSuccessAt,
|
||||
@@ -632,18 +686,26 @@ class SyncCheckTool {
|
||||
nextErrorType: error.runtimeType.toString(),
|
||||
nextErrorMessage: _safeError(error),
|
||||
nextErrorStack: _safeStack(stack),
|
||||
scanned: error is TerminalLogUploadException
|
||||
? error.summary.scanned
|
||||
: null,
|
||||
skipped: error is TerminalLogUploadException
|
||||
? error.summary.skipped
|
||||
: null,
|
||||
succeeded: error is TerminalLogUploadException
|
||||
? error.summary.succeeded
|
||||
: null,
|
||||
failed: error is TerminalLogUploadException
|
||||
? error.summary.failed
|
||||
: null,
|
||||
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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -698,7 +760,7 @@ class SyncCheckTool {
|
||||
/// 将稳定任务类型分派到既有 MQTT、HTTP、插件或文件业务入口并返回统一结果。
|
||||
Future<_ScheduledTaskActionResult> _runAction(
|
||||
ScheduledTaskType type, {
|
||||
required bool currentLogOnly,
|
||||
required _PendingTaskRequest request,
|
||||
}) async {
|
||||
switch (type) {
|
||||
case ScheduledTaskType.mqReconnect:
|
||||
@@ -742,6 +804,33 @@ class SyncCheckTool {
|
||||
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('分类同步完成');
|
||||
@@ -753,7 +842,7 @@ class SyncCheckTool {
|
||||
return const _ScheduledTaskActionResult.success('离线订单同步完成');
|
||||
case ScheduledTaskType.terminalLogUpload:
|
||||
final summary = await TerminalLogUploadTool.upload(
|
||||
currentOnly: currentLogOnly,
|
||||
currentOnly: request.currentLogOnly,
|
||||
);
|
||||
terminalLogSummary.value = summary;
|
||||
return _ScheduledTaskActionResult.success(
|
||||
@@ -775,6 +864,7 @@ class SyncCheckTool {
|
||||
bool _isHeavy(ScheduledTaskType type) {
|
||||
return const {
|
||||
ScheduledTaskType.configSync,
|
||||
ScheduledTaskType.departmentFaceSync,
|
||||
ScheduledTaskType.classifySync,
|
||||
ScheduledTaskType.productSync,
|
||||
ScheduledTaskType.offlineOrderSync,
|
||||
@@ -855,6 +945,11 @@ class SyncCheckTool {
|
||||
sourceType: ScheduledTaskSourceType.http,
|
||||
sourceName: 'HTTP 配置接口',
|
||||
),
|
||||
ScheduledTaskType.departmentFaceSync => const _ScheduledTaskDescriptor(
|
||||
title: '本部门人脸同步',
|
||||
sourceType: ScheduledTaskSourceType.http,
|
||||
sourceName: 'HTTP 人脸快照与虹软插件',
|
||||
),
|
||||
ScheduledTaskType.classifySync => const _ScheduledTaskDescriptor(
|
||||
title: '分类同步',
|
||||
sourceType: ScheduledTaskSourceType.http,
|
||||
@@ -932,6 +1027,9 @@ class _PendingTaskRequest {
|
||||
required this.requireSafeWindow,
|
||||
required this.requireFullIdle,
|
||||
required this.triggerType,
|
||||
required this.faceUserIds,
|
||||
required this.fullFaceSnapshot,
|
||||
required this.debugAllFaces,
|
||||
});
|
||||
|
||||
/// 是否只上传 Logger 当前写入的日志文件;为假时扫描全部日志。
|
||||
@@ -946,15 +1044,37 @@ class _PendingTaskRequest {
|
||||
/// 最终写入任务执行日志的触发来源。
|
||||
final ScheduledTaskTriggerType triggerType;
|
||||
|
||||
/// FACE 用户增量回查集合;部门完整快照时为空。
|
||||
final Set<String>? faceUserIds;
|
||||
|
||||
/// 当前请求是否为部门或调试完整快照,用于合并时让全量覆盖用户增量。
|
||||
final bool fullFaceSnapshot;
|
||||
|
||||
/// 是否执行仅非生产可触发的全租户调试快照。
|
||||
final bool debugAllFaces;
|
||||
|
||||
/// 全量日志请求优先于“仅当前文件”;任一手动请求到达后只需等待安全窗口。
|
||||
_PendingTaskRequest merge(_PendingTaskRequest other) => _PendingTaskRequest(
|
||||
currentLogOnly: currentLogOnly && other.currentLogOnly,
|
||||
requireSafeWindow: requireSafeWindow || other.requireSafeWindow,
|
||||
requireFullIdle: requireFullIdle && other.requireFullIdle,
|
||||
triggerType: triggerType == ScheduledTaskTriggerType.cron
|
||||
? other.triggerType
|
||||
: triggerType,
|
||||
);
|
||||
_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 等外部依赖暂不可用时使用的可诊断任务异常。
|
||||
|
||||
Reference in new issue
Block a user