Files
xianyan/lib/features/auth/services/qrcode_ws_service.dart
Developer ae1df22732 feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
2026-06-02 04:50:32 +08:00

194 lines
5.2 KiB
Dart
Raw 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? _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: (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';
}
}