beta v0.2 dualcheck
This commit is contained in:
1 parent
c137299016
commit
1ec68bacf5
14 files changed
+1591
-359
No files matched your search
@@ -1,50 +1,40 @@
|
||||
// DualCheck API 封装
|
||||
// 后端统一响应格式:{code:0, message, data}
|
||||
// WebSocket 消息格式:{type, data, error}
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 后端地址
|
||||
const String _baseUrl = kIsWeb
|
||||
? 'http://localhost:8080'
|
||||
: 'http://127.0.0.1:8080';
|
||||
|
||||
/// WebSocket 地址
|
||||
// ── URL ──
|
||||
const String _baseUrl =
|
||||
kIsWeb ? 'http://localhost:8080' : 'http://127.0.0.1:8080';
|
||||
const String _wsBaseUrl =
|
||||
kIsWeb ? 'ws://localhost:8080' : 'ws://127.0.0.1:8080';
|
||||
|
||||
class DualCheckApi {
|
||||
DualCheckApi()
|
||||
: _dio = Dio(BaseOptions(
|
||||
baseUrl: _baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
)) {
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic> && data['code'] != 0) {
|
||||
handler.reject(DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: _BizError(data['code'] as int, data['message'] as String),
|
||||
));
|
||||
return;
|
||||
}
|
||||
handler.next(response);
|
||||
},
|
||||
));
|
||||
}
|
||||
DualCheckApi._();
|
||||
static final DualCheckApi instance = DualCheckApi._();
|
||||
|
||||
final Dio _dio;
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
baseUrl: _baseUrl,
|
||||
contentType: 'application/json',
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
sendTimeout: const Duration(seconds: 10),
|
||||
))
|
||||
..interceptors.add(_BizErrorInterceptor());
|
||||
|
||||
// ── Create ──
|
||||
|
||||
Future<CreateResp> createSession({required String nickname}) async {
|
||||
Future<CreateResp> createSession({
|
||||
required String nickname,
|
||||
required String browserId,
|
||||
}) async {
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/create',
|
||||
data: {'nickname': nickname},
|
||||
data: {
|
||||
'nickname': nickname,
|
||||
'browser_id': browserId,
|
||||
},
|
||||
);
|
||||
final d = res.data!['data'] as Map<String, dynamic>;
|
||||
return CreateResp(
|
||||
@@ -82,8 +72,9 @@ class DualCheckApi {
|
||||
// ── CheckSession ──
|
||||
|
||||
Future<CheckResp> checkSession(String sessionId) async {
|
||||
final res = await _dio.get<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/check/$sessionId',
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/check',
|
||||
data: {'session_id': sessionId},
|
||||
);
|
||||
final d = res.data!['data'] as Map<String, dynamic>;
|
||||
return CheckResp(
|
||||
@@ -97,18 +88,42 @@ class DualCheckApi {
|
||||
// ── GetSession 全量快照 ──
|
||||
|
||||
Future<SessionResp> getSession(String sessionId) async {
|
||||
final res = await _dio.get<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/session/$sessionId',
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/session',
|
||||
data: {'session_id': sessionId},
|
||||
);
|
||||
return parseSession(res.data!['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
// ── Join 参与者加入登记 ──
|
||||
|
||||
Future<Map<String, dynamic>> joinParticipant({
|
||||
required String sessionId,
|
||||
required String nickname,
|
||||
}) async {
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/join',
|
||||
data: {'session_id': sessionId, 'nickname': nickname},
|
||||
);
|
||||
return res.data!['data'] as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// ── SetResubmit 发起方切换"允许参与者重新提交" ──
|
||||
|
||||
Future<SessionResp> setResubmit(String sessionId, {required bool allow}) async {
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/session/$sessionId/resubmit',
|
||||
data: {'allow': allow},
|
||||
'/api/v1/dual-check/resubmit',
|
||||
data: {'session_id': sessionId, 'allow': allow},
|
||||
);
|
||||
return parseSession(res.data!['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
// ── SetIgnoreWs 发起方切换"忽略首尾空白" ──
|
||||
|
||||
Future<SessionResp> setIgnoreWs(String sessionId, {required bool ignore}) async {
|
||||
final res = await _dio.post<Map<String, dynamic>>(
|
||||
'/api/v1/dual-check/ignore-ws',
|
||||
data: {'session_id': sessionId, 'ignore': ignore},
|
||||
);
|
||||
return parseSession(res.data!['data'] as Map<String, dynamic>);
|
||||
}
|
||||
@@ -116,7 +131,7 @@ class DualCheckApi {
|
||||
// ── EndSession 发起方主动结束会话 ──
|
||||
|
||||
Future<void> endSession(String sessionId) async {
|
||||
await _dio.post('/api/v1/dual-check/session/$sessionId/end');
|
||||
await _dio.post('/api/v1/dual-check/end', data: {'session_id': sessionId});
|
||||
}
|
||||
|
||||
static SessionResp parseSession(Map<String, dynamic> d) {
|
||||
@@ -128,6 +143,7 @@ class DualCheckApi {
|
||||
initiatorNickname: (d['initiator_nickname'] ?? '') as String,
|
||||
initiatorReady: d['initiator_ready'] as bool,
|
||||
allowResubmit: (d['allow_resubmit'] as bool?) ?? false,
|
||||
ignoreWs: (d['ignore_ws'] as bool?) ?? false,
|
||||
participants: parts,
|
||||
ttlSeconds: d['ttl_seconds'] as int,
|
||||
);
|
||||
@@ -139,14 +155,57 @@ class DualCheckApi {
|
||||
required String sessionId,
|
||||
required String role,
|
||||
required String nickname,
|
||||
required String browserId,
|
||||
}) {
|
||||
return '$_wsBaseUrl/api/v1/dual-check/ws'
|
||||
'?session_id=${Uri.encodeQueryComponent(sessionId)}'
|
||||
'&role=$role'
|
||||
'&nickname=${Uri.encodeQueryComponent(nickname)}';
|
||||
'&nickname=${Uri.encodeQueryComponent(nickname)}'
|
||||
'&browser_id=${Uri.encodeQueryComponent(browserId)}';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dio 拦截器:把后端业务错误转成 DioException ──
|
||||
|
||||
class _BizErrorInterceptor extends Interceptor {
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final code = data['code'];
|
||||
if (code != null && code != 0) {
|
||||
final msg = data['message']?.toString() ?? '请求失败';
|
||||
handler.reject(DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: _BizError(code as int, msg),
|
||||
type: DioExceptionType.badResponse,
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
}
|
||||
|
||||
class _BizError {
|
||||
final int code;
|
||||
final String message;
|
||||
const _BizError(this.code, this.message);
|
||||
@override
|
||||
String toString() => 'BusinessError($code): $message';
|
||||
}
|
||||
|
||||
bool isBizError(DioException e) => e.error is _BizError;
|
||||
int? bizErrorCode(DioException e) {
|
||||
final err = e.error;
|
||||
return err is _BizError ? err.code : null;
|
||||
}
|
||||
String bizErrorMessage(DioException e) {
|
||||
final err = e.error;
|
||||
return err is _BizError ? err.message : (e.message ?? '请求失败');
|
||||
}
|
||||
|
||||
// ── 数据模型 ──
|
||||
|
||||
class CreateResp {
|
||||
@@ -187,14 +246,12 @@ class ParticipantResp {
|
||||
required this.submitted,
|
||||
this.match,
|
||||
});
|
||||
factory ParticipantResp.fromMap(Map<String, dynamic> m) {
|
||||
return ParticipantResp(
|
||||
nickname: m['nickname'] as String,
|
||||
joined: m['joined'] as bool? ?? true,
|
||||
submitted: m['submitted'] as bool,
|
||||
match: m['match'] as bool?,
|
||||
);
|
||||
}
|
||||
factory ParticipantResp.fromMap(Map<String, dynamic> m) => ParticipantResp(
|
||||
nickname: m['nickname'] as String,
|
||||
joined: (m['joined'] as bool?) ?? true,
|
||||
submitted: (m['submitted'] as bool?) ?? false,
|
||||
match: m['match'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
class SessionResp {
|
||||
@@ -202,6 +259,7 @@ class SessionResp {
|
||||
final String initiatorNickname;
|
||||
final bool initiatorReady;
|
||||
final bool allowResubmit;
|
||||
final bool ignoreWs;
|
||||
final List<ParticipantResp> participants;
|
||||
final int ttlSeconds;
|
||||
SessionResp({
|
||||
@@ -209,45 +267,49 @@ class SessionResp {
|
||||
required this.initiatorNickname,
|
||||
required this.initiatorReady,
|
||||
required this.allowResubmit,
|
||||
required this.ignoreWs,
|
||||
required this.participants,
|
||||
required this.ttlSeconds,
|
||||
});
|
||||
|
||||
SessionResp copyWith({
|
||||
String? sessionId,
|
||||
String? initiatorNickname,
|
||||
bool? initiatorReady,
|
||||
bool? allowResubmit,
|
||||
bool? ignoreWs,
|
||||
List<ParticipantResp>? participants,
|
||||
int? ttlSeconds,
|
||||
}) =>
|
||||
SessionResp(
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
initiatorNickname: initiatorNickname ?? this.initiatorNickname,
|
||||
initiatorReady: initiatorReady ?? this.initiatorReady,
|
||||
allowResubmit: allowResubmit ?? this.allowResubmit,
|
||||
ignoreWs: ignoreWs ?? this.ignoreWs,
|
||||
participants: participants ?? this.participants,
|
||||
ttlSeconds: ttlSeconds ?? this.ttlSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
// ── WS 消息类型 ──
|
||||
// ── WS 消息解析 ──
|
||||
|
||||
class WSMessage {
|
||||
final String type;
|
||||
final Map<String, dynamic> data;
|
||||
final String? error;
|
||||
final String error;
|
||||
WSMessage({required this.type, required this.data, this.error = ''});
|
||||
|
||||
WSMessage({required this.type, required this.data, this.error});
|
||||
|
||||
factory WSMessage.fromJson(Map<String, dynamic> json) {
|
||||
return WSMessage(
|
||||
type: json['type'] as String,
|
||||
data: (json['data'] as Map<String, dynamic>?) ?? const {},
|
||||
error: json['error'] as String?,
|
||||
);
|
||||
factory WSMessage.fromJson(String raw) {
|
||||
try {
|
||||
final m = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return WSMessage(
|
||||
type: m['type'] as String? ?? '',
|
||||
data: (m['data'] as Map<String, dynamic>?) ?? {},
|
||||
error: (m['error'] as String?) ?? '',
|
||||
);
|
||||
} catch (_) {
|
||||
return WSMessage(type: '', data: {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 业务错误处理 ──
|
||||
|
||||
class _BizError implements Exception {
|
||||
_BizError(this.code, this.message);
|
||||
final int code;
|
||||
final String message;
|
||||
@override
|
||||
String toString() => '[biz:$code] $message';
|
||||
}
|
||||
|
||||
bool isBizError(DioException e) => e.error is _BizError;
|
||||
int? bizErrorCode(DioException e) {
|
||||
final err = e.error;
|
||||
return err is _BizError ? err.code : null;
|
||||
}
|
||||
String bizErrorMessage(DioException e) {
|
||||
final err = e.error;
|
||||
return err is _BizError ? err.message : (e.message ?? '请求失败');
|
||||
}
|
||||
Reference in new issue
Block a user