Files
xianyan/lib/features/auth/services/qrcode_ws_service.dart
Developer 67f26ff166 feat: 多模块功能更新 - 文件传输/多语言/NFC/首页组件/进度美化等
- 文件传输: 设备发现、LAN发现服务优化
- NFC分享: provider和service增强
- 多语言: 16种语言翻译补全
- 首页: 句子详情面板、收藏页、离线页优化
- 我的: 成就、个人资料、签到、设置页面更新
- 新增: AR视图、进度美化页、Hive安全访问、鸿蒙兼容助手、共享组件
- iOS Widget: Intents扩展、XianyanWidget更新
- 鸿蒙: 6个卡片页面更新
- 其他: 路由注册、缓存配置、崩溃监控、TTS播放器等
2026-06-04 04:08:31 +08:00

194 lines
5.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言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';
}
}