270 lines
8.7 KiB
Dart
270 lines
8.7 KiB
Dart
// lib/services/market/market_service.dart
|
||
//
|
||
// سرویس برای endpoint بازار (market-service) در API gateway جدید:
|
||
// - GET /market (paginated) → لیست symbolها با دادهی OHLCV.
|
||
//
|
||
// دادهی خام Hlocvt را به مدل قدیمی NewStatisticModel (CategoryList/Content/
|
||
// Data) map میکنیم تا UI نبض صنعت بدون تغییر کار کند.
|
||
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:developer' as dev;
|
||
import 'dart:io' show SocketException;
|
||
|
||
// ignore: depend_on_referenced_packages
|
||
import 'package:http/http.dart' as http;
|
||
|
||
import 'package:didvan/config/auth_config.dart';
|
||
import 'package:didvan/models/new_statistic/new_statistics_model.dart';
|
||
import 'package:didvan/services/auth/auth_interceptor.dart';
|
||
import 'package:didvan/services/auth/token_storage.dart';
|
||
|
||
class MarketServiceResult {
|
||
final bool isSuccess;
|
||
final int? statusCode;
|
||
final List<CategoryList>? lists;
|
||
final String? errorMessage;
|
||
|
||
const MarketServiceResult({
|
||
required this.isSuccess,
|
||
this.statusCode,
|
||
this.lists,
|
||
this.errorMessage,
|
||
});
|
||
}
|
||
|
||
/// نتیجهی دریافت آیتمهای خام بازار (برای صفحهی «همه»).
|
||
class MarketItemsResult {
|
||
final bool isSuccess;
|
||
final int? statusCode;
|
||
final List<Map<String, dynamic>>? items;
|
||
final String? errorMessage;
|
||
|
||
const MarketItemsResult({
|
||
required this.isSuccess,
|
||
this.statusCode,
|
||
this.items,
|
||
this.errorMessage,
|
||
});
|
||
}
|
||
|
||
class MarketService {
|
||
MarketService._();
|
||
static final MarketService instance = MarketService._();
|
||
|
||
String get _base => AuthConfig.apiBaseUrl;
|
||
static const _timeout = Duration(seconds: 30);
|
||
|
||
/// دریافت دادهی بازار و map به CategoryList ها.
|
||
Future<MarketServiceResult> getMarketData({int limit = 50}) async {
|
||
await AuthInterceptor.ensureFreshToken();
|
||
try {
|
||
final uri = Uri.parse('$_base/market').replace(queryParameters: {
|
||
'limit': limit.toString(),
|
||
'page': '1',
|
||
});
|
||
final response = await _withRetry(
|
||
() => http.get(uri, headers: _headers()).timeout(_timeout),
|
||
);
|
||
|
||
dev.log('GET /market -> status=${response.statusCode}, '
|
||
'bodyLen=${response.body.length}', name: 'MarketService');
|
||
|
||
if (response.statusCode == 200) {
|
||
final body = json.decode(response.body);
|
||
final List items = body is Map && body['data'] is List
|
||
? body['data'] as List
|
||
: (body is List ? body : const []);
|
||
dev.log('GET /market -> parsed ${items.length} items',
|
||
name: 'MarketService');
|
||
|
||
final contents = <Content>[];
|
||
for (final raw in items) {
|
||
if (raw is! Map) continue;
|
||
final c = _mapToContent(Map<String, dynamic>.from(raw));
|
||
if (c != null) contents.add(c);
|
||
}
|
||
|
||
// همه را در یک گروه "بازارها" قرار میدهیم. (بکاند جدید
|
||
// دستهبندی ندارد؛ در صورت افزوده شدن، اینجا گروهبندی میشود.)
|
||
final lists = <CategoryList>[
|
||
CategoryList(category: 1, header: 'بازارها', contents: contents),
|
||
];
|
||
return MarketServiceResult(
|
||
isSuccess: true,
|
||
statusCode: 200,
|
||
lists: lists,
|
||
);
|
||
}
|
||
|
||
return MarketServiceResult(
|
||
isSuccess: false,
|
||
statusCode: response.statusCode,
|
||
errorMessage:
|
||
_extractError(response.body) ?? 'خطا در دریافت دادهی بازار.',
|
||
);
|
||
} catch (e) {
|
||
dev.log('getMarketData error: $e', name: 'MarketService');
|
||
return const MarketServiceResult(
|
||
isSuccess: false,
|
||
errorMessage: 'خطا در ارتباط با سرور.',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// دریافت آیتمهای خام بازار (List از Map). برای صفحهی «همه» که خودش
|
||
/// هر آیتم را به مدل GeneralItem map میکند.
|
||
Future<MarketItemsResult> getMarketItems({
|
||
int page = 1,
|
||
int limit = 100,
|
||
}) async {
|
||
await AuthInterceptor.ensureFreshToken();
|
||
try {
|
||
final uri = Uri.parse('$_base/market').replace(queryParameters: {
|
||
'limit': limit.toString(),
|
||
'page': page.toString(),
|
||
});
|
||
final response = await _withRetry(
|
||
() => http.get(uri, headers: _headers()).timeout(_timeout),
|
||
);
|
||
|
||
dev.log('GET /market (items) -> status=${response.statusCode}',
|
||
name: 'MarketService');
|
||
|
||
if (response.statusCode == 200) {
|
||
final body = json.decode(response.body);
|
||
final List items = body is Map && body['data'] is List
|
||
? body['data'] as List
|
||
: (body is List ? body : const []);
|
||
final mapped = items
|
||
.whereType<Map>()
|
||
.map((e) => Map<String, dynamic>.from(e))
|
||
.toList();
|
||
return MarketItemsResult(
|
||
isSuccess: true,
|
||
statusCode: 200,
|
||
items: mapped,
|
||
);
|
||
}
|
||
|
||
return MarketItemsResult(
|
||
isSuccess: false,
|
||
statusCode: response.statusCode,
|
||
errorMessage:
|
||
_extractError(response.body) ?? 'خطا در دریافت دادهی بازار.',
|
||
);
|
||
} catch (e) {
|
||
dev.log('getMarketItems error: $e', name: 'MarketService');
|
||
return const MarketItemsResult(
|
||
isSuccess: false,
|
||
errorMessage: 'خطا در ارتباط با سرور.',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// تبدیل یک Hlocvt به Content مدل قدیمی.
|
||
/// آخرین قیمت = close.last، درصد تغییر از دو close آخر محاسبه میشود.
|
||
Content? _mapToContent(Map<String, dynamic> json) {
|
||
try {
|
||
final closeRaw = json['close'];
|
||
final closes = (closeRaw is List)
|
||
? closeRaw.map((e) => (e as num).toDouble()).toList()
|
||
: <double>[];
|
||
|
||
String price = '-';
|
||
num changePercent = 0;
|
||
String direction = 'none';
|
||
|
||
if (closes.isNotEmpty) {
|
||
final last = closes.last;
|
||
price = _formatNumber(last);
|
||
if (closes.length >= 2) {
|
||
final prev = closes[closes.length - 2];
|
||
if (prev != 0) {
|
||
final change = ((last - prev) / prev) * 100;
|
||
changePercent = double.parse(change.toStringAsFixed(2));
|
||
direction = change > 0 ? 'high' : (change < 0 ? 'low' : 'none');
|
||
}
|
||
}
|
||
}
|
||
|
||
final symbolId = (json['symbolId'] ?? json['id'] ?? '').toString();
|
||
|
||
// آخرین 10 قیمت برای نمودار کوچک.
|
||
final chartPrices =
|
||
closes.length > 10 ? closes.sublist(closes.length - 10) : closes;
|
||
|
||
return Content(
|
||
id: symbolId.hashCode,
|
||
label: symbolId,
|
||
title: (json['title'] ?? symbolId).toString(),
|
||
marked: false,
|
||
data: Data(
|
||
p: price,
|
||
dp: changePercent,
|
||
dt: direction,
|
||
prices: chartPrices,
|
||
),
|
||
);
|
||
} catch (e) {
|
||
dev.log('mapToContent error: $e', name: 'MarketService');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
String _formatNumber(double v) {
|
||
// جداسازی سهرقمی ساده.
|
||
final s = v.toStringAsFixed(v.truncateToDouble() == v ? 0 : 2);
|
||
final parts = s.split('.');
|
||
final intPart = parts[0];
|
||
final buf = StringBuffer();
|
||
for (var i = 0; i < intPart.length; i++) {
|
||
if (i > 0 && (intPart.length - i) % 3 == 0) buf.write(',');
|
||
buf.write(intPart[i]);
|
||
}
|
||
return parts.length > 1 ? '${buf.toString()}.${parts[1]}' : buf.toString();
|
||
}
|
||
|
||
// ----------------- Helpers -----------------
|
||
|
||
Map<String, String> _headers() {
|
||
final headers = <String, String>{
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
};
|
||
final t = TokenStorage.accessToken;
|
||
if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t';
|
||
return headers;
|
||
}
|
||
|
||
Future<T> _withRetry<T>(Future<T> Function() body) async {
|
||
for (var attempt = 0; attempt < 3; attempt++) {
|
||
try {
|
||
return await body();
|
||
} on SocketException {
|
||
if (attempt == 2) rethrow;
|
||
await Future.delayed(Duration(milliseconds: 250 * (attempt + 1)));
|
||
} on http.ClientException {
|
||
if (attempt == 2) rethrow;
|
||
await Future.delayed(Duration(milliseconds: 250 * (attempt + 1)));
|
||
} on TimeoutException {
|
||
if (attempt == 2) rethrow;
|
||
}
|
||
}
|
||
throw StateError('retry loop exited without return');
|
||
}
|
||
|
||
String? _extractError(String body) {
|
||
if (body.isEmpty) return null;
|
||
try {
|
||
final data = json.decode(body);
|
||
if (data is Map) {
|
||
final msg = data['message'] ?? data['error'] ?? data['msg'];
|
||
if (msg is String && msg.isNotEmpty) return msg;
|
||
if (msg is List && msg.isNotEmpty) return msg.first.toString();
|
||
}
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
}
|