111
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
/// K线蜡烛数据模型
|
||||
class KlineCandle {
|
||||
final String coinCode;
|
||||
final String interval;
|
||||
final int openTime;
|
||||
final double openPrice;
|
||||
final double highPrice;
|
||||
final double lowPrice;
|
||||
final double closePrice;
|
||||
final double volume;
|
||||
final int closeTime;
|
||||
final bool isClosed;
|
||||
final int? timestamp;
|
||||
|
||||
const KlineCandle({
|
||||
required this.coinCode,
|
||||
required this.interval,
|
||||
required this.openTime,
|
||||
required this.openPrice,
|
||||
required this.highPrice,
|
||||
required this.lowPrice,
|
||||
required this.closePrice,
|
||||
required this.volume,
|
||||
required this.closeTime,
|
||||
this.isClosed = true,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory KlineCandle.fromJson(Map<String, dynamic> json) {
|
||||
return KlineCandle(
|
||||
coinCode: json['coinCode'] as String? ?? '',
|
||||
interval: json['interval'] as String? ?? '1h',
|
||||
openTime: json['openTime'] as int? ?? 0,
|
||||
openPrice: _toDouble(json['openPrice']),
|
||||
highPrice: _toDouble(json['highPrice']),
|
||||
lowPrice: _toDouble(json['lowPrice']),
|
||||
closePrice: _toDouble(json['closePrice']),
|
||||
volume: _toDouble(json['volume']),
|
||||
closeTime: json['closeTime'] as int? ?? 0,
|
||||
isClosed: json['isClosed'] as bool? ?? true,
|
||||
timestamp: json['timestamp'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is double) return v;
|
||||
if (v is int) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// 转换为 k_chart 库的 KLineEntity 格式
|
||||
Map<String, dynamic> toKLineEntityMap() {
|
||||
return {
|
||||
'open': openPrice,
|
||||
'high': highPrice,
|
||||
'low': lowPrice,
|
||||
'close': closePrice,
|
||||
'vol': volume,
|
||||
'amount': closePrice * volume,
|
||||
'time': openTime,
|
||||
'id': openTime,
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为 k_chart KLineEntity 对象
|
||||
dynamic toKLineEntity() {
|
||||
// k_chart KLineEntity.fromCustom 构造器
|
||||
return null; // placeholder, actual conversion in kline_page
|
||||
}
|
||||
|
||||
/// 从 REST API JSON 转换(历史K线)
|
||||
factory KlineCandle.fromHistoryJson(Map<String, dynamic> json) {
|
||||
return KlineCandle(
|
||||
coinCode: json['coinCode'] as String? ?? '',
|
||||
interval: json['interval'] as String? ?? '1h',
|
||||
openTime: json['openTime'] as int? ?? 0,
|
||||
openPrice: _toDouble(json['openPrice']),
|
||||
highPrice: _toDouble(json['highPrice']),
|
||||
lowPrice: _toDouble(json['lowPrice']),
|
||||
closePrice: _toDouble(json['closePrice']),
|
||||
volume: _toDouble(json['volume']),
|
||||
closeTime: json['closeTime'] as int? ?? 0,
|
||||
isClosed: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/kline_candle.dart';
|
||||
|
||||
/// K线 REST API 服务
|
||||
class KlineService {
|
||||
final DioClient _client;
|
||||
|
||||
KlineService(this._client);
|
||||
|
||||
/// 获取历史K线数据
|
||||
Future<ApiResponse<List<KlineCandle>>> fetchHistory({
|
||||
required String coinCode,
|
||||
required String interval,
|
||||
int limit = 200,
|
||||
int? before,
|
||||
}) async {
|
||||
final params = <String, dynamic>{
|
||||
'coinCode': coinCode,
|
||||
'interval': interval,
|
||||
'limit': limit,
|
||||
};
|
||||
if (before != null) params['before'] = before;
|
||||
|
||||
final response = await _client.get<Map<String, dynamic>>(
|
||||
ApiEndpoints.klineHistory,
|
||||
queryParameters: params,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final list = response.data!['list'] as List? ?? [];
|
||||
final candles = list
|
||||
.map((e) => KlineCandle.fromHistoryJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return ApiResponse.success(candles, response.message);
|
||||
}
|
||||
return ApiResponse.fail(response.message ?? '获取K线数据失败');
|
||||
}
|
||||
|
||||
/// 获取当前进行中的K线
|
||||
Future<ApiResponse<KlineCandle>> fetchCurrentCandle({
|
||||
required String coinCode,
|
||||
required String interval,
|
||||
}) async {
|
||||
final response = await _client.get<Map<String, dynamic>>(
|
||||
ApiEndpoints.klineCurrent,
|
||||
queryParameters: {'coinCode': coinCode, 'interval': interval},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(
|
||||
KlineCandle.fromJson(response.data!),
|
||||
response.message,
|
||||
);
|
||||
}
|
||||
return ApiResponse.fail(response.message ?? '获取当前K线失败');
|
||||
}
|
||||
|
||||
/// 获取支持的周期列表
|
||||
Future<ApiResponse<List<String>>> fetchIntervals() async {
|
||||
final response = await _client.get<Map<String, dynamic>>(
|
||||
ApiEndpoints.klineIntervals,
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final list = response.data!['list'] as List? ?? [];
|
||||
return ApiResponse.success(list.cast<String>(), response.message);
|
||||
}
|
||||
return ApiResponse.fail(response.message ?? '获取周期列表失败');
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:stomp_dart_client/stomp_dart_client.dart';
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../models/kline_candle.dart';
|
||||
|
||||
/// K线 WebSocket 服务(STOMP 协议)
|
||||
class KlineWebSocketService {
|
||||
StompClient? _stompClient;
|
||||
final Map<String, dynamic> _subscriptions = {};
|
||||
final Map<String, StreamController<KlineCandle>> _controllers = {};
|
||||
|
||||
bool _isConnected = false;
|
||||
bool _isConnecting = false;
|
||||
int _reconnectDelay = 2000; // 初始重连延迟
|
||||
static const int _maxReconnectDelay = 30000;
|
||||
|
||||
/// 订阅某个币种的K线数据
|
||||
Stream<KlineCandle> subscribe(String coinCode) {
|
||||
final key = coinCode.toUpperCase();
|
||||
if (!_controllers.containsKey(key)) {
|
||||
_controllers[key] = StreamController<KlineCandle>.broadcast();
|
||||
}
|
||||
_doSubscribe(key);
|
||||
return _controllers[key]!.stream;
|
||||
}
|
||||
|
||||
/// 取消订阅
|
||||
void unsubscribe(String coinCode) {
|
||||
final key = coinCode.toUpperCase();
|
||||
_subscriptions[key]?.call();
|
||||
_subscriptions.remove(key);
|
||||
}
|
||||
|
||||
/// 连接状态
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
/// 连接 WebSocket
|
||||
void connect() {
|
||||
if (_isConnecting || _isConnected) return;
|
||||
_isConnecting = true;
|
||||
|
||||
final wsUrl = ApiEndpoints.klineWs;
|
||||
|
||||
_stompClient = StompClient(
|
||||
config: StompConfig(
|
||||
url: wsUrl,
|
||||
onConnect: _onConnect,
|
||||
onDisconnect: _onDisconnect,
|
||||
onStompError: _onError,
|
||||
onWebSocketError: _onError,
|
||||
reconnectDelay: const Duration(milliseconds: 5000),
|
||||
heartbeatIncoming: const Duration(seconds: 20),
|
||||
heartbeatOutgoing: const Duration(seconds: 20),
|
||||
),
|
||||
);
|
||||
|
||||
_stompClient!.activate();
|
||||
}
|
||||
|
||||
/// 断开连接
|
||||
void disconnect() {
|
||||
_isConnecting = false;
|
||||
_stompClient?.deactivate();
|
||||
_stompClient = null;
|
||||
_isConnected = false;
|
||||
_subscriptions.clear();
|
||||
}
|
||||
|
||||
void _onConnect(StompFrame frame) {
|
||||
_isConnected = true;
|
||||
_isConnecting = false;
|
||||
_reconnectDelay = 2000; // 重置重连延迟
|
||||
|
||||
// 重新订阅所有已注册的币种
|
||||
for (final key in _controllers.keys) {
|
||||
_doSubscribe(key);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDisconnect(StompFrame? frame) {
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _onError(dynamic error) {
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
Future.delayed(Duration(milliseconds: _reconnectDelay), () {
|
||||
if (!_isConnected && !_isConnecting) {
|
||||
_reconnectDelay = (_reconnectDelay * 2).clamp(2000, _maxReconnectDelay);
|
||||
connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _doSubscribe(String coinCode) {
|
||||
if (_stompClient == null || !_isConnected) {
|
||||
connect(); // 触发连接,连接成功后会自动重新订阅
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免重复订阅
|
||||
if (_subscriptions.containsKey(coinCode)) return;
|
||||
|
||||
final dest = '/topic/kline/$coinCode';
|
||||
final sub = _stompClient!.subscribe(
|
||||
destination: dest,
|
||||
callback: (StompFrame frame) {
|
||||
if (frame.body != null) {
|
||||
try {
|
||||
final json = jsonDecode(frame.body!) as Map<String, dynamic>;
|
||||
final candle = KlineCandle.fromJson(json);
|
||||
_controllers[coinCode]?.add(candle);
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
);
|
||||
_subscriptions[coinCode] = sub;
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
disconnect();
|
||||
for (final controller in _controllers.values) {
|
||||
controller.close();
|
||||
}
|
||||
_controllers.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user