ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
343
lib/services/marquee_web_source.dart
Normal file
343
lib/services/marquee_web_source.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../models/marquee_message.dart';
|
||||
import '../utils/constants.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
/// Web 端滚动字幕数据源。
|
||||
///
|
||||
/// 专门负责从 Web 后台 `/theme/scrollingText/*` 接口请求、接收并解析滚动字幕数据。
|
||||
/// 支持通过参数配置请求地址(URL / path)与请求方式(GET / POST),
|
||||
/// 内置网络异常、超时、HTTP 错误、解析失败等错误处理,
|
||||
/// 将原始响应封装为结构化的 [MarqueeFetchResult]。
|
||||
class MarqueeWebSource {
|
||||
/// [baseUrl] 可为空;为空时使用 [AppConfig] 当前环境的默认地址。
|
||||
/// [dio] 可注入,便于测试或复用已挂载拦截器的实例。
|
||||
MarqueeWebSource({
|
||||
Dio? dio,
|
||||
String? baseUrl,
|
||||
this.timeout = AppConstants.apiTimeout,
|
||||
}) : _dio = dio ?? _createDio(baseUrl: baseUrl, timeout: timeout);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
/// 请求超时时间。
|
||||
final Duration timeout;
|
||||
|
||||
/// 默认列表接口路径。与 Web 后台滚动字幕管理页保持一致。
|
||||
static const String defaultPath = '/theme/scrollingText/list';
|
||||
|
||||
/// 默认请求方式。
|
||||
static const String defaultMethod = 'GET';
|
||||
|
||||
static Dio _createDio({String? baseUrl, required Duration timeout}) {
|
||||
return Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl ?? AppConfig.apiFullBaseUrl,
|
||||
connectTimeout: timeout,
|
||||
receiveTimeout: timeout,
|
||||
sendTimeout: timeout,
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8'},
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 从 Web 端获取滚动字幕列表。
|
||||
///
|
||||
/// [method] 支持 'GET' / 'POST' / 'PUT' 等;默认 'GET'。
|
||||
/// [path] 支持绝对 URL(以 http:// 或 https:// 开头)或相对路径。
|
||||
/// [queryParameters] 用于 GET / POST 的查询参数(如 page、limit、orgId)。
|
||||
/// [body] 用于 POST / PUT 的请求体。
|
||||
/// [storeId] 与 [keyword] 为业务常用过滤条件,会自动合并到 queryParameters。
|
||||
/// [page] / [limit] 默认 1 / 100。
|
||||
///
|
||||
/// 返回 [MarqueeFetchResult],内部包含解析后的 [MarqueeMessage] 列表、总数、错误码等。
|
||||
Future<MarqueeFetchResult> fetchMarquees({
|
||||
String method = defaultMethod,
|
||||
String path = defaultPath,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, dynamic>? body,
|
||||
String? storeId,
|
||||
String? keyword,
|
||||
int page = 1,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
final params = <String, dynamic>{
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
if (storeId != null && storeId.isNotEmpty) 'orgId': storeId,
|
||||
if (keyword != null && keyword.isNotEmpty) 'content': keyword,
|
||||
...?queryParameters,
|
||||
};
|
||||
|
||||
final options = Options(method: method.toUpperCase());
|
||||
|
||||
try {
|
||||
final upperMethod = method.toUpperCase();
|
||||
final Response<dynamic> response;
|
||||
|
||||
if (upperMethod == 'GET') {
|
||||
response = await _dio.get<dynamic>(
|
||||
path,
|
||||
queryParameters: params,
|
||||
options: options,
|
||||
);
|
||||
} else {
|
||||
response = await _dio.request<dynamic>(
|
||||
path,
|
||||
queryParameters: params,
|
||||
data: body,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
|
||||
return _parseResponse(response.data);
|
||||
} on DioException catch (e) {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: _formatDioError(e),
|
||||
errorCode: _errorCodeFromDio(e),
|
||||
);
|
||||
} on FormatException catch (e) {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: '数据解析失败:${e.message}',
|
||||
errorCode: MarqueeErrorCode.parseFailed,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: '请求异常:$e',
|
||||
errorCode: MarqueeErrorCode.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用单条数据请求封装(用于新增/编辑/删除后回显或详情查询)。
|
||||
Future<MarqueeFetchResult> fetchMarqueeDetail({
|
||||
String method = defaultMethod,
|
||||
required String path,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, dynamic>? body,
|
||||
}) async {
|
||||
return fetchMarquees(
|
||||
method: method,
|
||||
path: path,
|
||||
queryParameters: queryParameters,
|
||||
body: body,
|
||||
page: 1,
|
||||
limit: 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析 Web 端响应为结构化结果。
|
||||
///
|
||||
/// 兼容三种常见响应形态:
|
||||
/// 1. 后端信封 `{code:0, msg:'ok', data:{list:[...], totalCount:5}}`
|
||||
/// 2. 后端信封 `{code:0, msg:'ok', data:[...]}`
|
||||
/// 3. 原生数组 `[...]`
|
||||
/// 4. 解包后的分页对象 `{list:[...], totalCount:5}`
|
||||
MarqueeFetchResult _parseResponse(dynamic raw) {
|
||||
if (raw == null) {
|
||||
return MarqueeFetchResult.success(
|
||||
data: const <MarqueeMessage>[],
|
||||
total: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? envelope;
|
||||
dynamic payload;
|
||||
|
||||
if (raw is Map<String, dynamic>) {
|
||||
envelope = raw;
|
||||
payload = raw['data'] ?? raw;
|
||||
} else {
|
||||
payload = raw;
|
||||
}
|
||||
|
||||
// 处理业务错误码(信封中存在 code 且非成功)。
|
||||
if (envelope != null && envelope.containsKey('code')) {
|
||||
final code = envelope['code'];
|
||||
final success = code == 0 || code == 200 || code == '0' || code == '200';
|
||||
if (!success) {
|
||||
final msg = envelope['msg']?.toString() ??
|
||||
envelope['message']?.toString() ??
|
||||
'请求失败(业务码:$code)';
|
||||
return MarqueeFetchResult.failure(
|
||||
error: msg,
|
||||
errorCode: MarqueeErrorCode.businessError,
|
||||
businessCode: code?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析列表与总数。
|
||||
List<dynamic> listRaw;
|
||||
int? total;
|
||||
if (payload is Map<String, dynamic>) {
|
||||
if (payload['list'] is List) {
|
||||
listRaw = payload['list'] as List<dynamic>;
|
||||
final t = payload['totalCount'] ?? payload['total'];
|
||||
if (t is int) total = t;
|
||||
if (t is String) total = int.tryParse(t);
|
||||
} else if (payload['rows'] is List) {
|
||||
listRaw = payload['rows'] as List<dynamic>;
|
||||
final t = payload['totalCount'] ?? payload['total'];
|
||||
if (t is int) total = t;
|
||||
if (t is String) total = int.tryParse(t);
|
||||
} else {
|
||||
// 兜底:整个 data 就是单条对象。
|
||||
listRaw = [payload];
|
||||
}
|
||||
} else if (payload is List) {
|
||||
listRaw = payload;
|
||||
} else {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: '返回数据格式不正确',
|
||||
errorCode: MarqueeErrorCode.parseFailed,
|
||||
);
|
||||
}
|
||||
|
||||
final List<MarqueeMessage> data = [];
|
||||
for (final item in listRaw) {
|
||||
if (item is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
data.add(MarqueeMessage.fromJson(item));
|
||||
} on FormatException catch (e) {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: '单条字幕解析失败:${e.message}',
|
||||
errorCode: MarqueeErrorCode.parseFailed,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
return MarqueeFetchResult.failure(
|
||||
error: '单条字幕解析异常:$e',
|
||||
errorCode: MarqueeErrorCode.parseFailed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return MarqueeFetchResult.success(
|
||||
data: data,
|
||||
total: total ?? data.length,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDioError(DioException e) {
|
||||
final inner = e.error;
|
||||
if (inner is ApiException && inner.message.isNotEmpty) {
|
||||
return inner.message;
|
||||
}
|
||||
final response = e.response;
|
||||
if (response != null && response.statusCode != null) {
|
||||
return '服务器响应异常:HTTP ${response.statusCode}';
|
||||
}
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
return '连接超时,请检查网络';
|
||||
case DioExceptionType.connectionError:
|
||||
return '网络连接失败,请检查网络';
|
||||
case DioExceptionType.cancel:
|
||||
return '请求已取消';
|
||||
case DioExceptionType.badCertificate:
|
||||
return '服务器证书校验失败';
|
||||
case DioExceptionType.badResponse:
|
||||
return '服务器返回异常响应';
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
final text = e.message;
|
||||
if (text != null && text.isNotEmpty) return text;
|
||||
return '网络请求失败';
|
||||
}
|
||||
}
|
||||
|
||||
MarqueeErrorCode _errorCodeFromDio(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
return MarqueeErrorCode.timeout;
|
||||
case DioExceptionType.connectionError:
|
||||
return MarqueeErrorCode.networkError;
|
||||
case DioExceptionType.badCertificate:
|
||||
return MarqueeErrorCode.certificateError;
|
||||
case DioExceptionType.badResponse:
|
||||
return MarqueeErrorCode.httpError;
|
||||
case DioExceptionType.cancel:
|
||||
return MarqueeErrorCode.cancelled;
|
||||
default:
|
||||
return MarqueeErrorCode.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web 端滚动字幕请求结果。
|
||||
class MarqueeFetchResult {
|
||||
const MarqueeFetchResult({
|
||||
required this.data,
|
||||
this.total = 0,
|
||||
this.hasMore = false,
|
||||
this.error,
|
||||
this.errorCode,
|
||||
this.businessCode,
|
||||
});
|
||||
|
||||
factory MarqueeFetchResult.success({
|
||||
required List<MarqueeMessage> data,
|
||||
int total = 0,
|
||||
bool hasMore = false,
|
||||
}) {
|
||||
return MarqueeFetchResult(
|
||||
data: data,
|
||||
total: total,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
|
||||
factory MarqueeFetchResult.failure({
|
||||
required String error,
|
||||
MarqueeErrorCode errorCode = MarqueeErrorCode.unknown,
|
||||
String? businessCode,
|
||||
}) {
|
||||
return MarqueeFetchResult(
|
||||
data: const <MarqueeMessage>[],
|
||||
error: error,
|
||||
errorCode: errorCode,
|
||||
businessCode: businessCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析后的字幕列表。
|
||||
final List<MarqueeMessage> data;
|
||||
|
||||
/// 服务端总数(用于分页)。
|
||||
final int total;
|
||||
|
||||
/// 是否还有下一页。
|
||||
final bool hasMore;
|
||||
|
||||
/// 错误描述(成功时为 null)。
|
||||
final String? error;
|
||||
|
||||
/// 错误码(分类)。
|
||||
final MarqueeErrorCode? errorCode;
|
||||
|
||||
/// 后端业务错误码(非 HTTP 状态码)。
|
||||
final String? businessCode;
|
||||
|
||||
bool get isSuccess => error == null && errorCode == null;
|
||||
}
|
||||
|
||||
/// Web 端滚动字幕错误分类。
|
||||
enum MarqueeErrorCode {
|
||||
networkError,
|
||||
timeout,
|
||||
httpError,
|
||||
businessError,
|
||||
parseFailed,
|
||||
certificateError,
|
||||
cancelled,
|
||||
unknown,
|
||||
}
|
||||
Reference in New Issue
Block a user