- 文件传输: 设备发现、LAN发现服务优化 - NFC分享: provider和service增强 - 多语言: 16种语言翻译补全 - 首页: 句子详情面板、收藏页、离线页优化 - 我的: 成就、个人资料、签到、设置页面更新 - 新增: AR视图、进度美化页、Hive安全访问、鸿蒙兼容助手、共享组件 - iOS Widget: Intents扩展、XianyanWidget更新 - 鸿蒙: 6个卡片页面更新 - 其他: 路由注册、缓存配置、崩溃监控、TTS播放器等
194 lines
5.2 KiB
Dart
194 lines
5.2 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 二维码登录WebSocket推送服务
|
||
/// 创建时间: 2026-06-02
|
||
/// 更新时间: 2026-06-02
|
||
/// 作用: 通过WebSocket长连接接收二维码状态变更推送,替代HTTP轮询
|
||
/// 上次更新: 处理confirmed状态携带token的推送,收到终态后自动取消订阅
|
||
/// ============================================================
|
||
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||
import 'package:xianyan/core/utils/logger.dart';
|
||
|
||
typedef QrcodeStatusCallback = void Function(Map<String, dynamic> data);
|
||
|
||
class QrcodeWsService {
|
||
QrcodeWsService._();
|
||
|
||
static final QrcodeWsService instance = QrcodeWsService._();
|
||
|
||
WebSocketChannel? _channel;
|
||
bool _isConnected = false;
|
||
Timer? _heartbeatTimer;
|
||
Timer? _reconnectTimer;
|
||
int _reconnectAttempts = 0;
|
||
static const _maxReconnectAttempts = 5;
|
||
|
||
String? _subscribedCode;
|
||
QrcodeStatusCallback? _onStatusUpdate;
|
||
StreamSubscription<dynamic>? _subscription;
|
||
|
||
bool get isConnected => _isConnected;
|
||
|
||
/// 连接WebSocket服务器
|
||
Future<bool> connect() async {
|
||
if (_isConnected) return true;
|
||
|
||
final wsUrl = _resolveWsUrl();
|
||
if (wsUrl.isEmpty) {
|
||
Log.w('QrcodeWsService: WebSocket URL不可用');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
Log.i('QrcodeWsService: 连接 $wsUrl');
|
||
_channel = WebSocketChannel.connect(Uri.parse(wsUrl));
|
||
|
||
await _channel!.ready.timeout(
|
||
const Duration(seconds: 5),
|
||
onTimeout: () => throw TimeoutException('连接超时'),
|
||
);
|
||
|
||
_isConnected = true;
|
||
_reconnectAttempts = 0;
|
||
_startHeartbeat();
|
||
|
||
_subscription = _channel!.stream.listen(
|
||
(data) => _handleMessage(data),
|
||
onDone: () {
|
||
_isConnected = false;
|
||
Log.w('QrcodeWsService: 连接关闭');
|
||
_scheduleReconnect();
|
||
},
|
||
onError: (Object? e) {
|
||
_isConnected = false;
|
||
Log.e('QrcodeWsService: 连接错误 $e');
|
||
_scheduleReconnect();
|
||
},
|
||
);
|
||
|
||
Log.i('QrcodeWsService: 连接成功');
|
||
if (_subscribedCode != null) {
|
||
_sendSubscribe(_subscribedCode!);
|
||
}
|
||
return true;
|
||
} catch (e) {
|
||
Log.w('QrcodeWsService: 连接失败 $e');
|
||
_isConnected = false;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 订阅二维码状态变更
|
||
Future<void> subscribe(String code, QrcodeStatusCallback onStatus) async {
|
||
_subscribedCode = code;
|
||
_onStatusUpdate = onStatus;
|
||
|
||
if (!_isConnected) {
|
||
final ok = await connect();
|
||
if (!ok) {
|
||
Log.w('QrcodeWsService: WebSocket不可用,将使用HTTP轮询降级');
|
||
return;
|
||
}
|
||
}
|
||
|
||
_sendSubscribe(code);
|
||
}
|
||
|
||
/// 取消订阅
|
||
void unsubscribe() {
|
||
if (_isConnected && _subscribedCode != null) {
|
||
_send({'type': 'qrcode_unsubscribe', 'code': _subscribedCode});
|
||
}
|
||
_subscribedCode = null;
|
||
_onStatusUpdate = null;
|
||
}
|
||
|
||
/// 断开连接
|
||
void disconnect() {
|
||
_subscription?.cancel();
|
||
_subscription = null;
|
||
_heartbeatTimer?.cancel();
|
||
_reconnectTimer?.cancel();
|
||
_channel?.sink.close();
|
||
_channel = null;
|
||
_isConnected = false;
|
||
_subscribedCode = null;
|
||
_onStatusUpdate = null;
|
||
_reconnectAttempts = 0;
|
||
Log.i('QrcodeWsService: 已断开');
|
||
}
|
||
|
||
void _sendSubscribe(String code) {
|
||
_send({
|
||
'type': 'qrcode_subscribe',
|
||
'code': code,
|
||
});
|
||
Log.i('QrcodeWsService: 已订阅 $code');
|
||
}
|
||
|
||
void _send(Map<String, dynamic> data) {
|
||
if (!_isConnected || _channel == null) return;
|
||
try {
|
||
_channel!.sink.add(jsonEncode(data));
|
||
} catch (e) {
|
||
Log.w('QrcodeWsService: 发送失败 $e');
|
||
}
|
||
}
|
||
|
||
void _handleMessage(dynamic data) {
|
||
try {
|
||
final json = jsonDecode(data as String) as Map<String, dynamic>;
|
||
final type = json['type'] as String? ?? '';
|
||
|
||
if (type == 'qrcode_status_update') {
|
||
final status = json['status'] as String? ?? '';
|
||
Log.i('QrcodeWsService: 收到状态推送 $status');
|
||
_onStatusUpdate?.call(json);
|
||
if (status == 'confirmed' || status == 'expired' ||
|
||
status == 'cancelled') {
|
||
Log.i('QrcodeWsService: 终态 $status,自动取消订阅');
|
||
unsubscribe();
|
||
}
|
||
} else if (type == 'pong') {
|
||
// heartbeat ack
|
||
}
|
||
} catch (e) {
|
||
Log.w('QrcodeWsService: 消息解析失败 $e');
|
||
}
|
||
}
|
||
|
||
void _startHeartbeat() {
|
||
_heartbeatTimer?.cancel();
|
||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (_) {
|
||
_send({'type': 'ping'});
|
||
});
|
||
}
|
||
|
||
void _scheduleReconnect() {
|
||
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
||
Log.w('QrcodeWsService: 超过最大重连次数');
|
||
return;
|
||
}
|
||
if (_subscribedCode == null) return;
|
||
|
||
_reconnectAttempts++;
|
||
final delay = Duration(seconds: _reconnectAttempts * 2);
|
||
Log.i('QrcodeWsService: ${delay.inSeconds}秒后重连 (第$_reconnectAttempts次)');
|
||
|
||
_reconnectTimer?.cancel();
|
||
_reconnectTimer = Timer(delay, () async {
|
||
final ok = await connect();
|
||
if (ok && _subscribedCode != null) {
|
||
_sendSubscribe(_subscribedCode!);
|
||
}
|
||
});
|
||
}
|
||
|
||
String _resolveWsUrl() {
|
||
return 'wss://tools.wktyl.com:9443';
|
||
}
|
||
}
|