import 'package:permission_handler/permission_handler.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; /// 统一封装人脸 SDK 激活、状态查询、本地人脸库注册和删除操作。 /// /// 所有激活参数均从终端系统设置读取,调用方不得传入或记录密钥明文。 abstract class AppFaceTool { /// 人脸工具日志标签,不记录激活参数和人脸特征等敏感内容。 static const String _tag = "app 人脸工具"; /// 打开原生人脸识别页面;手动硬件模式或插件拒绝打开时返回 false。 static Future jumpFacePage() async { if (AppRuntimeTool.useManualHardwareInput) return false; final isJump = await FaceDiscernPlugs.jumpFaceDiscernPage(); if (!isJump) return false; return true; } /// 检查后端下发的人脸激活参数是否全部具有非空有效值。 static bool validateSysFaceParams(SysSetting sysSetting) { return missingSysFaceParams(sysSetting).isEmpty; } /// 返回缺失的人脸参数字段名,供 Load 页给出可排查但不泄露密钥的提示。 static List missingSysFaceParams(SysSetting sysSetting) { final missing = []; if ((sysSetting.faceAppId ?? '').trim().isEmpty) missing.add('APP_ID'); if ((sysSetting.faceSdkKey ?? '').trim().isEmpty) missing.add('SDK_KEY'); if ((sysSetting.faceActiveKey ?? '').trim().isEmpty) { missing.add('ACTIVE_KEY'); } return List.unmodifiable(missing); } /// 在 Floor 完成建库后激活虹软 SDK,并将本地数据库人脸完整重建到内存库。 /// /// 任意插件、权限或样本异常都转换为警告结果,调用方可以继续完成终端启动。 static Future prepareForStartup() async { if (AppRuntimeTool.useManualHardwareInput) { return const FaceStartupPreparationResult( status: FaceStartupPreparationStatus.skipped, message: '当前为手动硬件输入模式,已跳过真实人脸设备准备', total: 0, successCount: 0, failedCount: 0, failedSampleIds: [], missingParameters: [], ); } final setting = await AppSettingTool.getSysSetting(); final missing = missingSysFaceParams(setting); if (missing.isNotEmpty) { return FaceStartupPreparationResult( status: FaceStartupPreparationStatus.warning, message: '缺少 ${missing.join('、')},请先同步设备配置', total: 0, successCount: 0, failedCount: 0, failedSampleIds: const [], missingParameters: missing, ); } FaceActivationResult activation; try { activation = await activateSdk(); } catch (error, stack) { logger.w('|$_tag|启动阶段激活人脸插件异常error:|$error|stack:|$stack|'); return const FaceStartupPreparationResult( status: FaceStartupPreparationStatus.warning, message: '人脸插件暂时无法激活,可进入设置后重试', total: 0, successCount: 0, failedCount: 0, failedSampleIds: [], missingParameters: [], ); } if (!activation.success) { return FaceStartupPreparationResult( status: FaceStartupPreparationStatus.warning, message: activation.message, total: 0, successCount: 0, failedCount: 0, failedSampleIds: const [], missingParameters: const [], activationCode: activation.code, ); } try { final registration = await FaceDiscernPlugs.rebuildFaceLibrary(); final status = registration.success ? FaceStartupPreparationStatus.success : FaceStartupPreparationStatus.warning; return FaceStartupPreparationResult( status: status, message: registration.message, total: registration.total, successCount: registration.successCount, failedCount: registration.failedCount, failedSampleIds: List.unmodifiable(registration.failedSampleIds), missingParameters: const [], activationCode: activation.code, ); } catch (error, stack) { logger.w('|$_tag|启动阶段重建人脸库异常error:|$error|stack:|$stack|'); return FaceStartupPreparationResult( status: FaceStartupPreparationStatus.warning, message: '人脸插件已激活,但本地人脸库注册失败,可在设置中重新注册', total: 0, successCount: 0, failedCount: 0, failedSampleIds: const [], missingParameters: const [], activationCode: activation.code, ); } } /// 按需将本地人脸记录注册到 SDK 内存库。 /// /// [userId] 为空时处理完整人脸库,[forceRegister] 为 true 时忽略当前内存库数量并强制重建。 /// 返回值表示调用完成后内存库中是否至少存在一条可识别人脸。 static Future faceRegister({ String? userId, bool forceRegister = false, }) async { if (AppRuntimeTool.useManualHardwareInput) return true; var faceCount = await FaceDiscernPlugs.getFaceCount(); if (faceCount == 0 || forceRegister) { final result = await FaceDiscernPlugs.rebuildFaceLibrary( userId: userId, ); if (result.failedSampleIds.isNotEmpty) { logger.w('|$_tag|批量注册人脸失败数量:${result.failedSampleIds.length}'); } } faceCount = await FaceDiscernPlugs.getFaceCount(); if (faceCount == 0) return false; return true; } /// 将 SDK 识别结果解析为当前后端仍有效的人员和钱包信息。 /// /// 正常情况直接使用本地样本或 SDK 返回的人员编号查询;编号失效时,仅允许 /// 通过现有全量人脸同步刷新同一个 [FaceStream.sampleId] 的归属后重试。 /// 禁止按本地饭卡号替换为另一人员,避免旧人脸样本误绑定到卡片的新持有人。 static Future resolveRecognizedUser({ required FaceStream recognition, required SysType sysType, }) async { final app = Get.find(); final faceDao = app.dataBase.getFaceDao; final candidateUserIds = {}; final sampleId = recognition.sampleId?.toString(); if (sampleId != null && sampleId.isNotEmpty) { final localFace = await faceDao.findBySampleId(sampleId); _addCandidateUserId(candidateUserIds, localFace?.userId); } _addCandidateUserId(candidateUserIds, recognition.userId); if (!AppNetworkTool.isAutoNetwork()) { return _findCachedRecognizedUser(candidateUserIds); } final currentUser = await _findRemoteRecognizedUser( candidateUserIds, sysType, ); if (currentUser != null || sampleId == null || sampleId.isEmpty) { return currentUser; } // 识别会话已经由调用方结束,此处才允许进入统一人脸队列,避免并发操作引擎。 // 已知人员只回查该用户;没有可靠用户标识时才对账本部门,禁止恢复流程拉取全租户人脸。 logger.w('|$_tag|识别样本对应人员已失效,开始回查当前范围人脸并刷新样本归属|'); final scheduler = SyncCheckTool.instance; await scheduler.trigger( ScheduledTaskType.departmentFaceSync, faceUserIds: candidateUserIds.isEmpty ? null : candidateUserIds, triggerType: ScheduledTaskTriggerType.system, ); if (scheduler.runningTasks.contains(ScheduledTaskType.departmentFaceSync) || scheduler.pendingTasks.contains(ScheduledTaskType.departmentFaceSync)) { // 已有同步占用引擎时不读取旧归属继续支付;队列完成后由下一次识别使用新数据。 return null; } final refreshedFace = await faceDao.findBySampleId(sampleId); final refreshedUserId = refreshedFace?.userId.trim(); if (refreshedUserId == null || refreshedUserId.isEmpty || candidateUserIds.contains(refreshedUserId)) { return null; } candidateUserIds.add(refreshedUserId); return _findRemoteRecognizedUser({refreshedUserId}, sysType); } /// 将非空人员编号加入有序候选集合,保持本地样本优先于原生回包。 static void _addCandidateUserId(Set target, String? userId) { final value = userId?.trim(); if (value == null || value.isEmpty) return; target.add(value); } /// 离线时从本地人员缓存解析识别结果;没有缓存则返回空并交由业务提示同步。 static Future _findCachedRecognizedUser( Set candidateUserIds, ) async { for (final userId in candidateUserIds) { final user = await SyncUserTool.storageUserByUserId(userId); if (user != null) return SyncUserTool.parsePayUser(user: user); } return null; } /// 依次使用现有人员查询接口校验候选编号,并缓存首个仍有效的人员。 static Future _findRemoteRecognizedUser( Set candidateUserIds, SysType sysType, ) async { for (final userId in candidateUserIds) { final user = await HardApi.getUserInfoById({ 'id': userId, 'type': sysType.name, }); if (user == null || (user.userId ?? '').trim().isEmpty) continue; await SyncUserTool.storageUser(user: user); return user; } return null; } /// 查询当前设备的人脸 SDK 是否已经激活并可继续初始化。 static Future isActiveSdk() async { if (AppRuntimeTool.useManualHardwareInput) return true; final status = await getStatus(); return status?.activated == true; } /// 获取激活、引擎和人脸库一致性状态。 /// /// 参数不完整时返回未就绪状态;手动硬件输入模式不连接原生通道并返回 null。 static Future getStatus() async { final setting = await AppSettingTool.getSysSetting(); // 参数完整性来自本地已同步配置,不依赖人脸硬件或原生通道;虚拟机和手动输入模式也应明确展示“等待同步配置”。 if (!validateSysFaceParams(setting)) { return const FacePluginStatus( activated: false, activationCode: -1, parametersComplete: false, engineReady: false, engineCode: -1, databaseCount: 0, memoryCount: 0, registrationInProgress: false, libraryConsistent: false, ); } if (AppRuntimeTool.useManualHardwareInput) return null; return FaceDiscernPlugs.getFaceStatus( appId: setting.faceAppId ?? '', sdkKey: setting.faceSdkKey ?? '', activeKey: setting.faceActiveKey ?? '', ); } /// 激活参数只来自后端同步后的系统设置,页面不接收也不展示密钥明文。 static Future activateSdk() async { final setting = await AppSettingTool.getSysSetting(); if (!validateSysFaceParams(setting)) { return const FaceActivationResult( success: false, code: -1, message: '人脸插件参数不完整,请先同步设备配置', requiresPermission: false, canOpenSettings: false, ); } final permission = await Permission.phone.status; if (!permission.isGranted) { final requested = await Permission.phone.request(); if (!requested.isGranted) { return FaceActivationResult( success: false, code: -1, message: requested.isPermanentlyDenied ? '电话状态权限已被永久拒绝,请在系统设置中开启' : '激活需要电话状态权限,请授权后重试', requiresPermission: true, canOpenSettings: requested.isPermanentlyDenied, ); } } return FaceDiscernPlugs.activateFaceSdk( appId: setting.faceAppId!.trim(), sdkKey: setting.faceSdkKey!.trim(), activeKey: setting.faceActiveKey!.trim(), ); } /// 从 SDK 内存库删除指定样本;样本编号为空、SDK 未激活或插件失败时返回 false。 static Future removeFace({String? sampleId}) async { if (sampleId == null) return false; if (AppRuntimeTool.useManualHardwareInput) return true; final isActive = await AppFaceTool.isActiveSdk(); if (!isActive) return false; return await FaceDiscernPlugs.removeFace(sampleId: sampleId); } }