feat:人脸机器实时增量更新

This commit is contained in:
dzc committed 2026-08-17 13:39:18 +08:00
1 parent 8cfbbd768e
commit 3770173604
29 files changed
+1584 -359

No files matched your search

+147 -27
View File
@@ -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 等外部依赖暂不可用时使用的可诊断任务异常。