feat:人脸质量上报、人脸支付对比凭证

This commit is contained in:
dzc committed 2026-09-10 17:49:07 +08:00
1 parent e15197edf4
commit dc5ef2bc9e
9 files changed
+341 -130

No files matched your search

@@ -792,6 +792,10 @@ abstract final class OrderEvidenceUploadTool {
!(task.androidOrderCode?.isNotEmpty ?? false);
/// 判断服务端拒绝是否属于哈希、格式或归属等确定不可恢复的错误。
///
/// 注意“订单不存在或不属于当前设备”不能列为永久失败:pj 离线人脸订单在下单时只
/// 绑定终端订单号,服务端订单要等联网同步后才存在,该拒绝必须按退避重试,由
/// 30 天本地保留期限兜底收敛。
static bool _isPermanentError(Object error) {
if (error is! DioException) return false;
final message = '${error.error ?? ''} ${error.message ?? ''}';
@@ -800,7 +804,6 @@ abstract final class OrderEvidenceUploadTool {
'哈希不一致',
'不受信任',
'地址不合法',
'不属于当前设备',
'用户不一致',
'样本不一致',
'图片大小与终端上报不一致',
+38 -10
View File
@@ -131,8 +131,10 @@ abstract class SyncFaceTool {
onProgress: onProgress,
);
}
await _reportPersistedFaceQualitySafely();
final summary = accumulator.toSummary();
final qualityReported = await _reportPersistedFaceQualitySafely();
final summary = accumulator.toSummary(
qualityReportNote: qualityReported ? null : '质量上报未完成,将在下次同步重试',
);
logger.i('|$_tag|同步完成|${summary.message}|');
return summary;
}
@@ -207,12 +209,16 @@ abstract class SyncFaceTool {
/// 使用保留的 V2 接口拉取非生产调试全租户快照。
///
/// 生产入口不会调用本方法;V2 不提供源更新时间,因此仅以图片地址判断图片版本。
/// 生产入口不会调用本方法;V2 以服务端 updateTime(缺失时回退 createTime)作为
/// 图片版本,保证调试同步写入的人脸同样可以参与质量上报和版本比对。
/// 服务端历史脏数据(空编号或重复编号)只跳过并记录计数,不阻断调试同步。
static Future<List<ServiceFaceSyncRecord>> _loadDebugAllSnapshot({
void Function(int processed, int? total)? onProgress,
}) async {
final records = <ServiceFaceSyncRecord>[];
final sampleIds = <String>{};
var invalidCount = 0;
var duplicateCount = 0;
for (var page = 1; page <= _maxSnapshotPages; page++) {
final data = await UserApi.getFaceListV2({
'current': page,
@@ -228,9 +234,13 @@ abstract class SyncFaceTool {
if (sampleId == null ||
userId == null ||
sampleId.isEmpty ||
userId.isEmpty ||
!sampleIds.add(sampleId)) {
throw const FaceSyncSnapshotException('调试全租户快照包含无效或重复人脸编号');
userId.isEmpty) {
invalidCount++;
continue;
}
if (!sampleIds.add(sampleId)) {
duplicateCount++;
continue;
}
records.add(ServiceFaceSyncRecord(
id: face.id ?? sampleId,
@@ -240,9 +250,17 @@ abstract class SyncFaceTool {
deptId: face.deptId ?? '',
deptName: face.deptName,
faceImg: face.faceImg,
sourceUpdatedAt: face.updateTime ?? face.createTime,
));
}
if (data.length < _debugPageSize) return records;
if (data.length < _debugPageSize) {
if (invalidCount > 0 || duplicateCount > 0) {
logger.w(
'|$_tag|调试快照跳过异常编号|invalid:$invalidCount|duplicate:$duplicateCount|',
);
}
return records;
}
onProgress?.call(records.length, null);
}
throw const FaceSyncSnapshotException('调试全租户快照分页超过安全上限');
@@ -306,6 +324,10 @@ abstract class SyncFaceTool {
}
/// 判断图片版本、特征或正式文件是否变化;完全一致时不下载也不写虹软。
///
/// 历史本地记录可能缺少图片版本时间(早期 V2 调试同步写入的 updateTime 为空);
/// 此时只要图片地址一致且本地特征与文件完好,就不再强制重新下载,由
/// [_metadataChanged] 的版本回填补齐,避免服务端图片损坏时记录永远无法修复。
static Future<bool> _imageChanged(
FaceEntity? existing,
ServiceFaceSyncRecord record,
@@ -313,6 +335,7 @@ abstract class SyncFaceTool {
if (existing == null) return true;
if (existing.imagePath != record.faceImg) return true;
if (record.sourceUpdatedAt != null &&
existing.updateTime != null &&
existing.updateTime != record.sourceUpdatedAt) {
return true;
}
@@ -321,7 +344,9 @@ abstract class SyncFaceTool {
return path == null || path.isEmpty || !await File(path).exists();
}
/// 判断姓名或部门元数据是否变化;此类变化不需要重新提取图片特征。
/// 判断姓名、部门元数据或图片版本时间是否变化;此类变化不需要重新提取图片特征。
///
/// 版本时间以服务端快照为准回填,用于修复缺少上报版本的本地历史记录。
static bool _metadataChanged(
FaceEntity existing,
ServiceFaceSyncRecord record,
@@ -329,7 +354,9 @@ abstract class SyncFaceTool {
existing.userId != record.userId ||
existing.userName != (record.username ?? '') ||
existing.deptId != record.deptId ||
existing.deptName != (record.deptName ?? '');
existing.deptName != (record.deptName ?? '') ||
(record.sourceUpdatedAt != null &&
existing.updateTime != record.sourceUpdatedAt);
/// 下载临时图片、提取特征并提交文件、SQLite 和虹软单样本增量。
///
@@ -852,7 +879,7 @@ class _FaceSyncAccumulator {
}
/// 生成可公开给任务日志和页面的不可变汇总。
FaceSyncSummary toSummary() => FaceSyncSummary(
FaceSyncSummary toSummary({String? qualityReportNote}) => FaceSyncSummary(
mode: mode,
scanned: scanned,
added: added,
@@ -861,5 +888,6 @@ class _FaceSyncAccumulator {
skipped: skipped,
failed: failed,
maskedFailedSampleIds: List.unmodifiable(maskedFailedSampleIds),
qualityReportNote: qualityReportNote,
);
}