Files
biz_xfj_cloud/lib/common/utils/app/app_face_tool.dart
T

318 lines
12 KiB
Dart

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<bool> 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<String> missingSysFaceParams(SysSetting sysSetting) {
final missing = <String>[];
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<FaceStartupPreparationResult> 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<bool> 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<ServiceUser?> resolveRecognizedUser({
required FaceStream recognition,
required SysType sysType,
}) async {
final app = Get.find<AppControl>();
final faceDao = app.dataBase.getFaceDao;
final candidateUserIds = <String>{};
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<String> target, String? userId) {
final value = userId?.trim();
if (value == null || value.isEmpty) return;
target.add(value);
}
/// 离线时从本地人员缓存解析识别结果;没有缓存则返回空并交由业务提示同步。
static Future<ServiceUser?> _findCachedRecognizedUser(
Set<String> 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<ServiceUser?> _findRemoteRecognizedUser(
Set<String> 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<bool> isActiveSdk() async {
if (AppRuntimeTool.useManualHardwareInput) return true;
final status = await getStatus();
return status?.activated == true;
}
/// 获取激活、引擎和人脸库一致性状态。
///
/// 参数不完整时返回未就绪状态;手动硬件输入模式不连接原生通道并返回 null。
static Future<FacePluginStatus?> 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<FaceActivationResult> 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<bool> 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);
}
}