import 'dart:async'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; import 'reservation_menu_repository.dart'; import 'reservation_recognition_gateway.dart'; import 'reservation_sub_screen_gateway.dart'; enum ReservationMenuQueryStatus { idle, recognizing, querying, success, empty, identifyError, networkError, deviceError, expired, } extension ReservationMenuQueryStatusX on ReservationMenuQueryStatus { bool get hasIdentity => this == ReservationMenuQueryStatus.success || this == ReservationMenuQueryStatus.empty; } /// 本人预约查询状态机,识别、网络、副屏和隐私计时都从这里统一收口。 class ReservationMenuQueryController extends GetxController { ReservationMenuQueryController({ ReservationMenuRepository? repository, ReservationRecognitionGateway? recognitionGateway, ReservationSubScreenGateway? subScreenGateway, bool Function()? networkAvailable, this.privacyDuration = const Duration(seconds: 60), this.timerTick = const Duration(seconds: 1), DateTime Function()? now, }) : _repository = repository ?? HardReservationMenuRepository(), _recognitionGateway = recognitionGateway ?? TerminalReservationRecognitionGateway(), _subScreenGateway = subScreenGateway ?? TerminalReservationSubScreenGateway(), _networkAvailable = networkAvailable ?? AppNetworkTool.isAutoNetwork, _now = now ?? DateTime.now; final ReservationMenuRepository _repository; final ReservationRecognitionGateway _recognitionGateway; final ReservationSubScreenGateway _subScreenGateway; final bool Function() _networkAvailable; final Duration privacyDuration; final Duration timerTick; final DateTime Function() _now; final method = ReservationRecognitionMethod.card.obs; final status = ReservationMenuQueryStatus.idle.obs; final result = Rxn(); final currentPage = 1.obs; final remainingSeconds = 0.obs; final errorMessage = ''.obs; Timer? _privacyTimer; DateTime? _expiresAt; int _requestGeneration = 0; int _revision = 0; String _sessionId = ''; bool _closed = false; bool _recognitionLocked = false; List get currentDays => result.value?.pageDays(currentPage.value) ?? const []; int get totalPages => result.value?.totalPages ?? 1; @override void onInit() { super.onInit(); unawaited(_initialize()); } Future _initialize() async { await _subScreenGateway.show(); await _beginSession(); if (_closed) return; await startRecognition(); } Future selectMethod(ReservationRecognitionMethod next) async { if (next == method.value && status.value == ReservationMenuQueryStatus.recognizing) { return; } _requestGeneration++; _stopPrivacyTimer(); result.value = null; currentPage.value = 1; errorMessage.value = ''; method.value = next; if (next == ReservationRecognitionMethod.card) { await _recognitionGateway.stopFace(); } else { await _recognitionGateway.stopCard(); } await startRecognition(); } Future startRecognition() async { if (_closed) return; final generation = ++_requestGeneration; _recognitionLocked = false; _stopPrivacyTimer(); result.value = null; currentPage.value = 1; errorMessage.value = ''; status.value = ReservationMenuQueryStatus.recognizing; // 人脸方式由统一会话服务先把副屏切到实时识别页;预约 loading 只在识别成功、 // 真正开始查询当前用户时发送,避免副屏在人脸画面前短暂显示查询状态。 if (method.value == ReservationRecognitionMethod.card) { await _syncSubScreen('loading'); } Future recognized(String value) async { if (_closed || generation != _requestGeneration || _recognitionLocked) { return; } // 同一硬件事件可能被串口或原生通道重复投递,只允许首个结果进入查询。 _recognitionLocked = true; await _recognitionGateway.stopAll(); await _queryIdentity(value, generation); } void failed(ReservationRecognitionFailure failure) { if (_closed || generation != _requestGeneration || _recognitionLocked) { return; } _recognitionLocked = true; if (failure.type == ReservationRecognitionFailureType.cancelled) { status.value = ReservationMenuQueryStatus.idle; errorMessage.value = ''; unawaited(_syncSubScreen('idle')); return; } errorMessage.value = failure.message; status.value = failure.type == ReservationRecognitionFailureType.device ? ReservationMenuQueryStatus.deviceError : ReservationMenuQueryStatus.identifyError; unawaited(_syncSubScreen('error')); } try { if (method.value == ReservationRecognitionMethod.card) { await _recognitionGateway.startCard( onRecognized: recognized, onFailure: failed, ); } else { await _recognitionGateway.startFace( onRecognized: recognized, onFailure: failed, ); } } catch (error, stack) { if (_closed || generation != _requestGeneration) return; logger.e('|预约查询|启动识别失败error:|$error|stack:|$stack|'); errorMessage.value = '${method.value.title}设备不可用,请检查设备后重试'; status.value = ReservationMenuQueryStatus.deviceError; await _syncSubScreen('error'); } } Future _queryIdentity(String identityValue, int generation) async { if (!_networkAvailable()) { errorMessage.value = '预约查询需要联网,请检查网络后重试'; status.value = ReservationMenuQueryStatus.networkError; await _syncSubScreen('error'); return; } status.value = ReservationMenuQueryStatus.querying; errorMessage.value = ''; await _syncSubScreen('loading'); try { final response = await _repository.query( method: method.value, identityValue: identityValue, ); // 新的刷卡或人脸事件已经开始时,旧请求即使成功也不能覆盖新身份。 if (_closed || generation != _requestGeneration) return; if (response == null) { throw const ReservationMenuRepositoryException('未获取到预约查询结果'); } result.value = ReservationMenuDisplayData.fromService(response); currentPage.value = 1; status.value = response.days.isEmpty ? ReservationMenuQueryStatus.empty : ReservationMenuQueryStatus.success; _startPrivacyTimer(); await _syncSubScreen( response.days.isEmpty ? 'empty' : 'success', includeResult: true, ); } on ReservationMenuIdentityException catch (error) { if (_closed || generation != _requestGeneration) return; errorMessage.value = error.message; status.value = ReservationMenuQueryStatus.identifyError; await _syncSubScreen('error'); } on ReservationMenuRepositoryException catch (error) { if (_closed || generation != _requestGeneration) return; errorMessage.value = error.message; status.value = ReservationMenuQueryStatus.deviceError; await _syncSubScreen('error'); } on DioException catch (error, stack) { if (_closed || generation != _requestGeneration) return; logger.e('|预约查询|网络请求失败error:|$error|stack:|$stack|'); errorMessage.value = '预约查询失败,请检查网络后重试'; status.value = ReservationMenuQueryStatus.networkError; await _syncSubScreen('error'); } catch (error, stack) { if (_closed || generation != _requestGeneration) return; logger.e('|预约查询|查询失败error:|$error|stack:|$stack|'); errorMessage.value = '暂时无法查询预约,请稍后重试'; status.value = ReservationMenuQueryStatus.networkError; await _syncSubScreen('error'); } } Future changePage(int page) async { if (result.value == null || page < 1 || page > totalPages) return; if (page == currentPage.value) return; currentPage.value = page; _startPrivacyTimer(); await _syncSubScreen('success', includeResult: true); } Future _beginSession() async { _sessionId = '${_now().microsecondsSinceEpoch}'; _revision = 0; await _syncSubScreen('idle'); } void _startPrivacyTimer() { _stopPrivacyTimer(); _expiresAt = _now().add(privacyDuration); remainingSeconds.value = privacyDuration.inSeconds; _privacyTimer = Timer.periodic(timerTick, (_) { if (_closed || _expiresAt == null) return; final milliseconds = _expiresAt!.difference(_now()).inMilliseconds; if (milliseconds <= 0) { unawaited(clearForPrivacy(expired: true)); return; } remainingSeconds.value = (milliseconds / 1000).ceil(); }); } void _stopPrivacyTimer() { _privacyTimer?.cancel(); _privacyTimer = null; _expiresAt = null; remainingSeconds.value = 0; } Future clearForPrivacy({bool expired = false}) async { _requestGeneration++; _stopPrivacyTimer(); await _recognitionGateway.stopAll(); result.value = null; currentPage.value = 1; errorMessage.value = ''; status.value = expired ? ReservationMenuQueryStatus.expired : ReservationMenuQueryStatus.idle; await _clearSubScreen(); } Future _syncSubScreen( String scenario, { bool includeResult = false, }) async { if (_closed || !_subScreenGateway.isAvailable) return; final display = result.value; final event = SubScreenReservationQueryEvent( sessionId: _sessionId, revision: ++_revision, scenario: scenario, maskedName: includeResult ? display?.maskedName : null, deptName: includeResult ? display?.deptName : null, merchantName: includeResult ? display?.merchantName : null, totalDayCount: includeResult ? display?.totalDayCount ?? 0 : 0, totalMealCount: includeResult ? display?.totalMealCount ?? 0 : 0, currentPage: currentPage.value, totalPages: includeResult ? totalPages : 1, days: includeResult ? currentDays.map((day) => day.toMap()).toList() : const [], expiresAt: _expiresAt?.toIso8601String(), ); try { await _subScreenGateway.update(event); } catch (error) { // 单屏或副屏断开不能影响主屏查询。 logger.w('|预约查询|副屏同步失败:$error'); } } Future _clearSubScreen() async { if (!_subScreenGateway.isAvailable) return; try { await _subScreenGateway.clear( sessionId: _sessionId, revision: ++_revision, ); } catch (error) { logger.w('|预约查询|副屏清屏失败:$error'); } } @override void onClose() { _closed = true; _requestGeneration++; _stopPrivacyTimer(); unawaited(_recognitionGateway.stopAll()); unawaited(_clearSubScreen()); super.onClose(); } }