570 lines
22 KiB
Dart
570 lines
22 KiB
Dart
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';
|
|
import 'reservation_verification_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,
|
|
ReservationVerificationGateway? verificationGateway,
|
|
bool Function()? networkAvailable,
|
|
Future<ReservationQueryPagePreferences> Function()? preferencesLoader,
|
|
Future<void> Function(String)? speaker,
|
|
void Function(String)? successNotifier,
|
|
void Function(String)? errorNotifier,
|
|
this.privacyDuration = const Duration(seconds: 60),
|
|
this.timerTick = const Duration(seconds: 1),
|
|
DateTime Function()? now,
|
|
}) : _repository = repository ?? HardReservationMenuRepository(),
|
|
_recognitionGateway =
|
|
recognitionGateway ?? TerminalReservationRecognitionGateway(),
|
|
_subScreenGateway =
|
|
subScreenGateway ?? TerminalReservationSubScreenGateway(),
|
|
_verificationGateway =
|
|
verificationGateway ?? TerminalReservationVerificationGateway(),
|
|
_networkAvailable = networkAvailable ?? AppNetworkTool.isAutoNetwork,
|
|
_preferencesLoader = preferencesLoader ??
|
|
AppSettingTool.getReservationQueryPagePreferences,
|
|
_speaker = speaker ?? TtsManage.speak,
|
|
_successNotifier =
|
|
successNotifier ?? ((message) => Toast.success(msg: message)),
|
|
_errorNotifier =
|
|
errorNotifier ?? ((message) => Toast.error(msg: message)),
|
|
_now = now ?? DateTime.now;
|
|
|
|
final ReservationMenuRepository _repository;
|
|
final ReservationRecognitionGateway _recognitionGateway;
|
|
final ReservationSubScreenGateway _subScreenGateway;
|
|
final ReservationVerificationGateway _verificationGateway;
|
|
final bool Function() _networkAvailable;
|
|
final Future<ReservationQueryPagePreferences> Function() _preferencesLoader;
|
|
final Future<void> Function(String) _speaker;
|
|
final void Function(String) _successNotifier;
|
|
final void Function(String) _errorNotifier;
|
|
final Duration privacyDuration;
|
|
final Duration timerTick;
|
|
final DateTime Function() _now;
|
|
|
|
final method = ReservationRecognitionMethod.card.obs;
|
|
final methodOrder = <ReservationRecognitionMethod>[
|
|
...ReservationRecognitionMethod.values,
|
|
].obs;
|
|
|
|
/// 是否自动启动当前查询方式的硬件识别;页面生命周期内使用进入页面时读取的配置快照。
|
|
final autoRecognitionEnabled = false.obs;
|
|
|
|
/// 当前页面查询请求是否要求服务端脱敏用户姓名。
|
|
final nameMaskEnabled = true.obs;
|
|
|
|
final verificationEnabled = false.obs;
|
|
final verificationDateScope = ReservationVerificationDateScope.today.obs;
|
|
final status = ReservationMenuQueryStatus.idle.obs;
|
|
final result = Rxn<ReservationMenuDisplayData>();
|
|
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;
|
|
bool _verificationSubmitting = false;
|
|
String? _recognizedIdentityValue;
|
|
|
|
List<ReservationMenuDayData> get currentDays =>
|
|
result.value?.pageDays(currentPage.value) ?? const [];
|
|
|
|
int get totalPages => result.value?.totalPages ?? 1;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
unawaited(_initialize());
|
|
}
|
|
|
|
Future<void> _initialize() async {
|
|
await Future.wait([
|
|
_loadPagePreferences(),
|
|
_subScreenGateway.show(),
|
|
]);
|
|
await _beginSession();
|
|
}
|
|
|
|
/// 读取当前页面的一次性配置快照;读取失败时保留安全默认值继续提供手动查询。
|
|
Future<void> _loadPagePreferences() async {
|
|
try {
|
|
final preferences = await _preferencesLoader();
|
|
if (_closed) return;
|
|
methodOrder.assignAll(preferences.methodOrder);
|
|
method.value = preferences.defaultMethod;
|
|
autoRecognitionEnabled.value = preferences.autoRecognitionEnabled;
|
|
nameMaskEnabled.value = preferences.nameMaskEnabled;
|
|
verificationEnabled.value = preferences.verificationEnabled;
|
|
verificationDateScope.value = preferences.verificationDateScope;
|
|
} catch (error, stack) {
|
|
// 配置损坏或读取失败时继续使用安全默认值,不能让查询入口不可用。
|
|
logger.e('|预约查询|加载页面配置失败error:|$error|stack:|$stack|');
|
|
}
|
|
}
|
|
|
|
/// 切换本人识别方式,清理旧身份与硬件回调,并按配置决定是否立即启动新方式。
|
|
Future<void> selectMethod(ReservationRecognitionMethod next) async {
|
|
if (_closed || next == method.value) return;
|
|
final generation = ++_requestGeneration;
|
|
_stopPrivacyTimer();
|
|
result.value = null;
|
|
_recognizedIdentityValue = null;
|
|
currentPage.value = 1;
|
|
errorMessage.value = '';
|
|
method.value = next;
|
|
status.value = ReservationMenuQueryStatus.idle;
|
|
// generation 已先递增,即使旧硬件回调稍晚到达也不会进入查询流程。
|
|
// 自动模式直接交给新识别方式更新副屏,避免 idle 消息晚到并覆盖人脸实时画面。
|
|
if (!autoRecognitionEnabled.value) unawaited(_syncSubScreen('idle'));
|
|
await _recognitionGateway.stopAll();
|
|
if (_closed || generation != _requestGeneration) return;
|
|
if (autoRecognitionEnabled.value) await startRecognition();
|
|
}
|
|
|
|
/// 启动当前识别方式并建立一次防重复回调的查询代次;失败后停留在可人工恢复状态。
|
|
Future<void> startRecognition() async {
|
|
if (_closed) return;
|
|
final generation = ++_requestGeneration;
|
|
_recognitionLocked = false;
|
|
_stopPrivacyTimer();
|
|
result.value = null;
|
|
_recognizedIdentityValue = null;
|
|
currentPage.value = 1;
|
|
errorMessage.value = '';
|
|
status.value = ReservationMenuQueryStatus.recognizing;
|
|
// 人脸方式由统一会话服务先把副屏切到实时识别页;预约 loading 只在识别成功、
|
|
// 真正开始查询当前用户时发送,避免副屏在人脸画面前短暂显示查询状态。
|
|
if (method.value == ReservationRecognitionMethod.card) {
|
|
unawaited(_syncSubScreen('loading'));
|
|
}
|
|
|
|
Future<void> recognized(String value) async {
|
|
if (_closed || generation != _requestGeneration || _recognitionLocked) {
|
|
return;
|
|
}
|
|
// 同一硬件事件可能被串口或原生通道重复投递,只允许首个结果进入查询。
|
|
_recognitionLocked = true;
|
|
_recognizedIdentityValue = value;
|
|
await _recognitionGateway.stopAll();
|
|
await _queryIdentity(value, generation);
|
|
}
|
|
|
|
void failed(ReservationRecognitionFailure failure) {
|
|
if (_closed || generation != _requestGeneration || _recognitionLocked) {
|
|
return;
|
|
}
|
|
_recognitionLocked = true;
|
|
if (failure.type == ReservationRecognitionFailureType.cancelled) {
|
|
if (autoRecognitionEnabled.value) {
|
|
// 用户主动取消后暂停自动识别并提供恢复入口,禁止立即拉起造成无法退出的弹窗循环。
|
|
status.value = ReservationMenuQueryStatus.identifyError;
|
|
errorMessage.value = failure.message;
|
|
unawaited(_syncSubScreen('error'));
|
|
} else {
|
|
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<void> _queryIdentity(String identityValue, int generation) async {
|
|
if (!_networkAvailable()) {
|
|
errorMessage.value = '预约查询需要联网,请检查网络后重试';
|
|
status.value = ReservationMenuQueryStatus.networkError;
|
|
await _syncSubScreen('error');
|
|
return;
|
|
}
|
|
status.value = ReservationMenuQueryStatus.querying;
|
|
errorMessage.value = '';
|
|
// 副屏仅用于展示,插件异常或不回包不能挡在预约接口之前。
|
|
unawaited(_syncSubScreen('loading'));
|
|
try {
|
|
logger.i(
|
|
'|预约查询|开始请求预约菜单|method:${method.value.name}|'
|
|
'identity:${_maskedIdentity(identityValue)}|',
|
|
);
|
|
final response = await _repository.query(
|
|
method: method.value,
|
|
identityValue: identityValue,
|
|
maskName: nameMaskEnabled.value,
|
|
);
|
|
// 新的刷卡或人脸事件已经开始时,旧请求即使成功也不能覆盖新身份。
|
|
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;
|
|
logger.i(
|
|
'|预约查询|预约菜单请求完成|method:${method.value.name}|'
|
|
'dayCount:${response.days.length}|',
|
|
);
|
|
_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');
|
|
}
|
|
}
|
|
|
|
String _maskedIdentity(String value) {
|
|
if (value.length <= 2) return '**';
|
|
return '${value.substring(0, 1)}***${value.substring(value.length - 1)}';
|
|
}
|
|
|
|
Future<void> 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);
|
|
}
|
|
|
|
bool canVerifyDay(ReservationMenuDayData day) {
|
|
if (!verificationEnabled.value) return false;
|
|
return verificationDateScope.value ==
|
|
ReservationVerificationDateScope.all ||
|
|
day.isToday(_now());
|
|
}
|
|
|
|
/// 只核销用户在当前餐段弹框中选中的订单 ID,成功后重新查询并替换页面结果。
|
|
Future<bool> confirmVerification(
|
|
ReservationMenuDayData day,
|
|
ReservationMenuMealData meal,
|
|
int selectedOrderId,
|
|
) async {
|
|
if (_closed || _verificationSubmitting) return false;
|
|
final currentMeal = _findCurrentMeal(day, meal);
|
|
if (currentMeal == null || !currentMeal.reserved || !canVerifyDay(day)) {
|
|
_errorNotifier('当前餐段预约已失效,请重新查询');
|
|
return false;
|
|
}
|
|
if (currentMeal.orderIds.isEmpty) {
|
|
_errorNotifier('未获取到可核销的预约订单,请重新查询');
|
|
return false;
|
|
}
|
|
if (!currentMeal.orderIds.contains(selectedOrderId)) {
|
|
_errorNotifier('所选预约订单已失效,请重新查询');
|
|
return false;
|
|
}
|
|
if (!_networkAvailable()) {
|
|
_errorNotifier('核销需要联网,请检查网络后重试');
|
|
return false;
|
|
}
|
|
final identityValue = _recognizedIdentityValue;
|
|
if (identityValue == null || identityValue.isEmpty) {
|
|
_errorNotifier('识别信息已失效,请重新识别后核销');
|
|
return false;
|
|
}
|
|
final recognitionMethod = method.value;
|
|
|
|
_verificationSubmitting = true;
|
|
_stopPrivacyTimer();
|
|
try {
|
|
// selectedOrderId 只能来自当前点击餐段的订单选择器,避免误取其他餐段的 ID。
|
|
final canceled = await _verificationGateway.cancel(
|
|
orderId: selectedOrderId,
|
|
);
|
|
if (!canceled) throw StateError('订单 $selectedOrderId 核销接口未返回成功');
|
|
unawaited(_announceVerificationSuccess());
|
|
|
|
try {
|
|
await _refreshAfterVerification(
|
|
identityValue,
|
|
method: recognitionMethod,
|
|
);
|
|
_successNotifier('核销成功,页面已刷新');
|
|
_startPrivacyTimer();
|
|
final scenario = status.value == ReservationMenuQueryStatus.empty
|
|
? 'empty'
|
|
: 'success';
|
|
unawaited(_syncSubScreen(scenario, includeResult: true));
|
|
} catch (error, stack) {
|
|
// 核销已由服务端确认成功,后续刷新失败不能反向提示“核销失败”。
|
|
// 清掉旧结果可避免用户对同一笔订单再次点击,重新识别即可获取最新数据。
|
|
logger.w('|预约查询|核销成功但刷新失败error:|$error|stack:|$stack|');
|
|
_successNotifier('核销成功,请重新识别刷新结果');
|
|
_clearStaleResultAfterVerification();
|
|
}
|
|
return true;
|
|
} on DioException catch (error, stack) {
|
|
logger.e('|预约查询|核销网络请求失败error:|$error|stack:|$stack|');
|
|
_errorNotifier('核销失败,请检查网络后重试');
|
|
_startPrivacyTimer();
|
|
return false;
|
|
} catch (error, stack) {
|
|
logger.e('|预约查询|核销失败error:|$error|stack:|$stack|');
|
|
_errorNotifier('核销失败,请重新查询后重试');
|
|
_startPrivacyTimer();
|
|
return false;
|
|
} finally {
|
|
_verificationSubmitting = false;
|
|
}
|
|
}
|
|
|
|
/// 不信任弹框回传的餐段对象,重新从当前查询结果中定位餐段及其订单 ID。
|
|
/// 这样即使页面数据已经刷新或调用方传入了其他餐段的 ID,也不会发起错误核销。
|
|
ReservationMenuMealData? _findCurrentMeal(
|
|
ReservationMenuDayData selectedDay,
|
|
ReservationMenuMealData selectedMeal,
|
|
) {
|
|
final currentResult = result.value;
|
|
if (currentResult == null) return null;
|
|
|
|
ReservationMealSlot? selectedSlot;
|
|
for (final slot in ReservationMealSlot.values) {
|
|
if (slot.matches(selectedMeal)) {
|
|
selectedSlot = slot;
|
|
break;
|
|
}
|
|
}
|
|
if (selectedSlot == null) return null;
|
|
|
|
for (final day in currentResult.days) {
|
|
if (day.date != selectedDay.date) continue;
|
|
for (final meal in day.meals) {
|
|
if (selectedSlot.matches(meal)) return meal;
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<void> _announceVerificationSuccess() async {
|
|
try {
|
|
await _speaker('核销成功');
|
|
} catch (error, stack) {
|
|
// 语音属于成功后的辅助反馈,TTS 异常不能改变核销结果。
|
|
logger.w('|预约查询|核销成功语音播报失败error:|$error|stack:|$stack|');
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshAfterVerification(
|
|
String identityValue, {
|
|
required ReservationRecognitionMethod method,
|
|
}) async {
|
|
final response = await _repository.query(
|
|
method: method,
|
|
identityValue: identityValue,
|
|
maskName: nameMaskEnabled.value,
|
|
);
|
|
if (response == null) throw StateError('核销后未获取到最新预约结果');
|
|
result.value = ReservationMenuDisplayData.fromService(response);
|
|
status.value = response.days.isEmpty
|
|
? ReservationMenuQueryStatus.empty
|
|
: ReservationMenuQueryStatus.success;
|
|
if (currentPage.value > totalPages) currentPage.value = totalPages;
|
|
}
|
|
|
|
void _clearStaleResultAfterVerification() {
|
|
final generation = ++_requestGeneration;
|
|
_stopPrivacyTimer();
|
|
result.value = null;
|
|
_recognizedIdentityValue = null;
|
|
currentPage.value = 1;
|
|
errorMessage.value = '';
|
|
status.value = ReservationMenuQueryStatus.idle;
|
|
unawaited(_resumeAfterStaleResultClear(generation));
|
|
}
|
|
|
|
/// 核销后刷新失败时先清理主副屏,再按自动模式重新等待身份,避免页面停在无监听的空闲态。
|
|
Future<void> _resumeAfterStaleResultClear(int generation) async {
|
|
await _recognitionGateway.stopAll();
|
|
if (_closed || generation != _requestGeneration) return;
|
|
unawaited(_clearSubScreen());
|
|
if (!_closed && autoRecognitionEnabled.value) await startRecognition();
|
|
}
|
|
|
|
/// 建立本次页面会话,并按进入页面时加载的开关选择自动识别或手动等待态。
|
|
Future<void> _beginSession() async {
|
|
_sessionId = '${_now().microsecondsSinceEpoch}';
|
|
_revision = 0;
|
|
if (autoRecognitionEnabled.value) {
|
|
// 配置加载完成后直接复用手动入口,确保饭卡、人脸、防重入和异常处理只有一套状态机。
|
|
await startRecognition();
|
|
} else {
|
|
unawaited(_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<void> clearForPrivacy({bool expired = false}) async {
|
|
final generation = ++_requestGeneration;
|
|
_stopPrivacyTimer();
|
|
await _recognitionGateway.stopAll();
|
|
if (_closed || generation != _requestGeneration) return;
|
|
result.value = null;
|
|
_recognizedIdentityValue = null;
|
|
currentPage.value = 1;
|
|
errorMessage.value = '';
|
|
status.value = expired
|
|
? ReservationMenuQueryStatus.expired
|
|
: ReservationMenuQueryStatus.idle;
|
|
unawaited(_clearSubScreen());
|
|
if (_closed || generation != _requestGeneration) return;
|
|
// 自动模式只在隐私展示周期结束后重新布防,结果展示期间不会重复读取同一硬件回调。
|
|
if (autoRecognitionEnabled.value) await startRecognition();
|
|
}
|
|
|
|
Future<void> _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 : const [],
|
|
expiresAt: _expiresAt?.toIso8601String(),
|
|
);
|
|
try {
|
|
await _subScreenGateway.update(event);
|
|
} catch (error) {
|
|
// 单屏或副屏断开不能影响主屏查询。
|
|
logger.w('|预约查询|副屏同步失败:$error');
|
|
}
|
|
}
|
|
|
|
Future<void> _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();
|
|
}
|
|
}
|