Files
biz_xfj_cloud/lib/common/services/face_recognition_session_service.dart
T
dzc 541923150e feat:订单列表增加预约时间
feat:优化食堂菜品预约统计导出
feat:增加订单识别证据图片功能
2026-08-23 18:49:55 +08:00

474 lines
16 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
class FaceRecognitionSessionHandle {
const FaceRecognitionSessionHandle({
required this.sessionId,
required this.scene,
required this.events,
required this.result,
});
final String sessionId;
final FaceRecognitionScene scene;
final Stream<FaceRecognitionSessionEvent> events;
final Future<FaceStream> result;
}
/// 主屏唯一人脸会话入口,统一处理权限、原生状态、双屏动作和结果幂等。
///
/// Camera 与虹软热保持由 Android 负责;Dart 结束会话只关闭当前业务和 UI,
/// 不会让连续排队的下一位顾客重新打开摄像头。
class FaceRecognitionSessionService extends GetxService {
static const Duration _viceStateTimeout = Duration(seconds: 2);
final cameraOpen = false.obs;
final cameraStatus = FaceRecognitionSessionStatus.cold.obs;
StreamSubscription<FaceRecognitionSessionEvent>? _cameraStateSubscription;
StreamSubscription<FaceRecognitionSessionEvent>? _nativeStateSubscription;
StreamSubscription<FaceStream?>? _faceSubscription;
StreamController<FaceRecognitionSessionEvent>? _eventController;
Completer<FaceStream>? _resultCompleter;
String? _activeSessionId;
FaceRecognitionScene? _activeScene;
int _revision = 0;
int _nativeRevision = 0;
bool _resultLocked = false;
bool _nativeStarted = false;
bool _startFailureEmitted = false;
int? _payableAmount;
int? _totalAmount;
int? _discountAmount;
SubScreenAction? _lastViceAction;
String? _lastViceMessage;
bool get isActive => _activeSessionId != null;
String? get activeSessionId => _activeSessionId;
@override
void onInit() {
super.onInit();
// 顶栏需要跨业务 Session 展示 Camera 热状态,因此常驻订阅不能跟随弹框关闭。
_cameraStateSubscription = FaceDiscernPlugs.sessionStream.listen(
_trackCameraState,
onError: (_) => cameraOpen.value = false,
);
unawaited(_refreshCameraState());
}
Future<void> _refreshCameraState() async {
try {
_trackCameraState(await FaceDiscernPlugs.getRecognitionState());
} catch (error) {
logger.w('|人脸识别|读取摄像头状态失败:$error');
}
}
void _trackCameraState(FaceRecognitionSessionEvent event) {
cameraOpen.value = event.warm;
cameraStatus.value = event.status;
}
bool owns(String? sessionId) =>
sessionId != null && sessionId == _activeSessionId;
Future<FaceRecognitionSessionHandle> start({
required FaceRecognitionScene scene,
int? payableAmount,
int? totalAmount,
int? discountAmount,
}) async {
if (_activeSessionId != null) {
throw const FaceRecognitionFailure(
code: 'SESSION_BUSY',
message: '已有进行中的人脸识别,请勿重复操作',
);
}
final cameraPermission = await Permission.camera.request();
if (!cameraPermission.isGranted) {
throw FaceRecognitionFailure(
code: 'CAMERA_PERMISSION_DENIED',
message: cameraPermission.isPermanentlyDenied
? '相机权限已被永久拒绝,请在系统设置中开启'
: '人脸识别需要相机权限,请授权后重试',
);
}
final sessionId = DateTime.now().microsecondsSinceEpoch.toString();
logger.i('|人脸识别|发起会话sessionId:$sessionId|scene:${scene.name}|');
_activeSessionId = sessionId;
_activeScene = scene;
_revision = 0;
_nativeRevision = 0;
_resultLocked = false;
_nativeStarted = false;
_startFailureEmitted = false;
_payableAmount = payableAmount;
_totalAmount = totalAmount;
_discountAmount = discountAmount;
_lastViceAction = null;
_lastViceMessage = null;
_eventController =
StreamController<FaceRecognitionSessionEvent>.broadcast();
_resultCompleter = Completer<FaceStream>();
_nativeStateSubscription = FaceDiscernPlugs.sessionStream.listen(
_handleNativeState,
onError: _handleNativeError,
);
_faceSubscription = FaceDiscernPlugs.faceStream.listen(_handleFaceResult);
final handle = FaceRecognitionSessionHandle(
sessionId: sessionId,
scene: scene,
events: _eventController!.stream,
result: _resultCompleter!.future,
);
unawaited(_startNative(
sessionId: sessionId,
scene: scene,
payableAmount: payableAmount,
totalAmount: totalAmount,
discountAmount: discountAmount,
));
return handle;
}
Future<void> _startNative({
required String sessionId,
required FaceRecognitionScene scene,
int? payableAmount,
int? totalAmount,
int? discountAmount,
}) async {
try {
_startFailureEmitted = false;
// 副屏仅承担顾客提示,发送异常或插件不回包都不能阻断原生 Camera 主链路。
unawaited(_sendViceState(
SubScreenAction.faceRecognitionStart,
message: '正在启动摄像头',
payableAmount: payableAmount,
totalAmount: totalAmount,
discountAmount: discountAmount,
));
if (_activeSessionId != sessionId) return;
logger.i('|人脸识别|开始原生调用startRecognition|sessionId:$sessionId|');
final initial = await FaceDiscernPlugs.startRecognition(
sessionId: sessionId,
scene: scene,
);
logger.i(
'|人脸识别|原生调用startRecognition完成|sessionId:$sessionId|'
'status:${initial.status.name}|revision:${initial.revision}|',
);
if (_activeSessionId == sessionId) {
_nativeStarted = true;
if (initial.revision > _nativeRevision) {
_nativeRevision = initial.revision;
_eventController?.add(initial);
}
}
} on PlatformException catch (error) {
if (_activeSessionId != sessionId) return;
_nativeStarted = true;
_startFailureEmitted = true;
logger.e(
'|人脸识别|原生调用startRecognition失败|sessionId:$sessionId|'
'code:${error.code}|message:${error.message}|',
);
unawaited(_sendViceState(
SubScreenAction.faceRecognitionFailed,
message: error.message ?? '无法启动人脸识别,请检查设备后重试',
));
_emitLocalFailure(FaceRecognitionFailure(
code: error.code,
message: error.message ?? '无法启动人脸识别,请检查设备后重试',
));
} catch (error, stack) {
if (_activeSessionId != sessionId) return;
_nativeStarted = true;
_startFailureEmitted = true;
logger.e(
'|人脸识别|启动原生会话异常|sessionId:$sessionId|'
'error:$error|stack:$stack|',
);
_emitLocalFailure(const FaceRecognitionFailure(
code: 'RECOGNITION_START_FAILED',
message: '无法启动人脸识别,请检查设备后重试',
));
}
}
Future<void> retry() async {
final sessionId = _activeSessionId;
if (sessionId == null) return;
_resultLocked = false;
_startFailureEmitted = false;
if (_nativeStarted) {
await FaceDiscernPlugs.retryRecognition(sessionId);
return;
}
await _startNative(
sessionId: sessionId,
scene: _activeScene!,
payableAmount: _payableAmount,
totalAmount: _totalAmount,
discountAmount: _discountAmount,
);
}
/// 取消当前业务人脸会话,并在释放原生资源前先让副屏退出人脸页。
Future<void> cancel({String message = '已取消人脸识别'}) async {
final sessionId = _activeSessionId;
if (sessionId == null) return;
// 主屏弹框会在本方法返回后立即关闭,必须先串行投递取消状态;
// 否则后续的预约查询消息可能先到,副屏仍会停留在人脸识别页。
await _sendViceState(
SubScreenAction.faceRecognitionCancel,
message: message,
);
logger.i('|人脸识别|开始原生调用cancelRecognition|sessionId:$sessionId|');
await FaceDiscernPlugs.cancelRecognition(sessionId, message: message);
if (_resultCompleter?.isCompleted == false) {
_resultCompleter!.completeError(
FaceRecognitionFailure(code: 'CANCELLED', message: message),
);
}
await _disposeSession();
}
Future<void> finish() async {
final sessionId = _activeSessionId;
if (sessionId == null) return;
logger.i('|人脸识别|开始原生调用finishRecognition|sessionId:$sessionId|');
await FaceDiscernPlugs.finishRecognition(sessionId);
await _disposeSession();
}
Future<void> suspend() async {
final sessionId = _activeSessionId;
if (sessionId == null) return;
unawaited(_sendViceState(
SubScreenAction.faceRecognitionCancel,
message: '应用已进入后台,人脸识别已结束',
));
logger.i('|人脸识别|开始原生调用suspendRecognition|sessionId:$sessionId|');
await FaceDiscernPlugs.suspendRecognition(
reason: '应用已进入后台,人脸识别已结束',
);
if (_resultCompleter?.isCompleted == false) {
_resultCompleter!.completeError(const FaceRecognitionFailure(
code: 'CANCELLED',
message: '应用已进入后台,人脸识别已结束',
));
}
await _disposeSession();
}
void _handleNativeState(FaceRecognitionSessionEvent event) {
final sessionId = _activeSessionId;
if (sessionId == null ||
event.sessionId != sessionId ||
event.revision <= _nativeRevision) {
return;
}
_nativeRevision = event.revision;
// MethodChannel 失败会先于 EventChannel 中已排队的 initializing 事件返回。
// 本地已经进入失败态后,必须丢弃这些旧进度,避免副屏从“需要重试”倒退到“识别中”。
if (_startFailureEmitted &&
(event.status == FaceRecognitionSessionStatus.initializing ||
event.status == FaceRecognitionSessionStatus.detecting)) {
logger.w(
'|人脸识别|忽略启动失败后迟到的原生进度|sessionId:$sessionId|'
'status:${event.status.name}|revision:${event.revision}|',
);
return;
}
_eventController?.add(event);
switch (event.status) {
case FaceRecognitionSessionStatus.initializing:
case FaceRecognitionSessionStatus.detecting:
unawaited(_sendViceState(
SubScreenAction.faceRecognitionDetecting,
message: event.message.isEmpty ? '请正对摄像头' : event.message,
));
break;
case FaceRecognitionSessionStatus.success:
unawaited(_sendViceState(
SubScreenAction.faceRecognitionSuccess,
message: '识别成功,正在读取信息',
));
break;
case FaceRecognitionSessionStatus.failed:
unawaited(_sendViceState(
SubScreenAction.faceRecognitionFailed,
message: event.message.isEmpty ? '人脸识别失败,请重试' : event.message,
));
final code = event.errorCode;
if (code == 'LOW_MEMORY' || code == 'SESSION_SUSPENDED') {
_completeWithFailure(FaceRecognitionFailure(
code: code!,
message: event.message.isEmpty ? '人脸识别已结束,请重新发起' : event.message,
));
}
break;
case FaceRecognitionSessionStatus.cancelled:
unawaited(_sendViceState(
SubScreenAction.faceRecognitionCancel,
message: event.message.isEmpty ? '已取消人脸识别' : event.message,
));
_completeWithFailure(FaceRecognitionFailure(
code: 'CANCELLED',
message: event.message.isEmpty ? '已取消人脸识别' : event.message,
));
break;
case FaceRecognitionSessionStatus.cold:
case FaceRecognitionSessionStatus.warmIdle:
break;
}
}
void _handleNativeError(Object error) {
const failure = FaceRecognitionFailure(
code: 'SESSION_STREAM_ERROR',
message: '人脸识别服务连接异常,请重试',
);
_emitLocalFailure(failure);
_completeWithFailure(failure);
}
void _emitLocalFailure(FaceRecognitionFailure failure) {
final sessionId = _activeSessionId;
final scene = _activeScene;
if (sessionId == null || scene == null) return;
_eventController?.add(FaceRecognitionSessionEvent(
sessionId: sessionId,
scene: scene,
status: FaceRecognitionSessionStatus.failed,
revision: ++_revision,
width: 1280,
height: 720,
rotation: 0,
mirror: true,
warm: false,
livenessEnabled: false,
keepMaxFace: false,
similarityThreshold: 0.8,
message: failure.message,
faceBoxes: const [],
errorCode: failure.code,
));
}
void _handleFaceResult(FaceStream? event) {
final activeSessionId = _activeSessionId;
if (event == null || activeSessionId == null || _resultLocked) return;
// EventChannel 可能在会话切换后仍投递上一帧;空会话或旧会话结果都不能绑定到下一笔订单。
if (event.sessionId != activeSessionId) return;
final userId = event.userId?.trim();
if (userId == null || userId.isEmpty) return;
_resultLocked = true;
_resultCompleter?.complete(event);
}
void _completeWithFailure(FaceRecognitionFailure failure) {
if (_resultCompleter?.isCompleted == false) {
_resultCompleter!.completeError(failure);
}
}
Future<void> _sendViceState(
SubScreenAction action, {
required String message,
int? payableAmount,
int? totalAmount,
int? discountAmount,
}) async {
final sessionId = _activeSessionId;
final scene = _activeScene;
if (sessionId == null || scene == null || !AppSubScreenTool.isAvailable) {
return;
}
if (_lastViceAction == action && _lastViceMessage == message) return;
final revision = ++_revision;
// 版本在发出前即固定,避免插件不回包期间重复状态持续堆积 MethodChannel 请求。
_lastViceAction = action;
_lastViceMessage = message;
logger.d(
'|人脸识别|发送副屏状态|sessionId:$sessionId|action:${action.code}|'
'revision:$revision|',
);
try {
final delivered = await AppRuntimeTool.sendViceMessage(
action.code,
params: {
'sessionId': sessionId,
'revision': revision,
'scene': scene.name,
'message': message,
if (payableAmount != null) 'payableAmount': payableAmount,
if (totalAmount != null) 'totalAmount': totalAmount,
if (discountAmount != null) 'discountAmount': discountAmount,
},
).timeout(_viceStateTimeout);
if (!delivered) {
logger.w(
'|人脸识别|副屏状态未投递,已按尽力通知处理|'
'sessionId:$sessionId|action:${action.code}|revision:$revision|',
);
return;
}
logger.d(
'|人脸识别|副屏状态发送完成|sessionId:$sessionId|'
'action:${action.code}|revision:$revision|',
);
} on TimeoutException {
logger.w(
'|人脸识别|副屏状态发送超时,已按尽力通知处理|'
'sessionId:$sessionId|action:${action.code}|revision:$revision|'
'timeout:${_viceStateTimeout.inMilliseconds}ms|',
);
} catch (error) {
logger.w(
'|人脸识别|副屏状态同步失败|sessionId:$sessionId|'
'action:${action.code}|revision:$revision|error:$error|',
);
}
}
Future<void> _disposeSession() async {
await _nativeStateSubscription?.cancel();
await _faceSubscription?.cancel();
_nativeStateSubscription = null;
_faceSubscription = null;
await _eventController?.close();
_eventController = null;
_resultCompleter = null;
_activeSessionId = null;
_activeScene = null;
_revision = 0;
_nativeRevision = 0;
_resultLocked = false;
_nativeStarted = false;
_startFailureEmitted = false;
_payableAmount = null;
_totalAmount = null;
_discountAmount = null;
_lastViceAction = null;
_lastViceMessage = null;
}
@override
void onClose() {
unawaited(_cameraStateSubscription?.cancel());
_cameraStateSubscription = null;
unawaited(suspend());
super.onClose();
}
}