254 lines
6.9 KiB
Dart
254 lines
6.9 KiB
Dart
// DualCheck API 封装
|
|
// 后端统一响应格式:{code:0, message, data}
|
|
// WebSocket 消息格式:{type, data, error}
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
/// 后端地址
|
|
const String _baseUrl = kIsWeb
|
|
? 'http://localhost:8080'
|
|
: 'http://127.0.0.1:8080';
|
|
|
|
/// WebSocket 地址
|
|
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);
|
|
},
|
|
));
|
|
}
|
|
|
|
final Dio _dio;
|
|
|
|
// ── Create ──
|
|
|
|
Future<CreateResp> createSession({required String nickname}) async {
|
|
final res = await _dio.post<Map<String, dynamic>>(
|
|
'/api/v1/dual-check/create',
|
|
data: {'nickname': nickname},
|
|
);
|
|
final d = res.data!['data'] as Map<String, dynamic>;
|
|
return CreateResp(
|
|
sessionId: d['session_id'] as String,
|
|
nickname: d['nickname'] as String,
|
|
ttl: d['ttl_seconds'] as int,
|
|
);
|
|
}
|
|
|
|
// ── Submit ──
|
|
|
|
Future<SubmitResp> submit({
|
|
required String sessionId,
|
|
required String nickname,
|
|
required String role, // "initiator" | "participant"
|
|
required String text,
|
|
}) async {
|
|
final res = await _dio.post<Map<String, dynamic>>(
|
|
'/api/v1/dual-check/submit',
|
|
data: {
|
|
'session_id': sessionId,
|
|
'nickname': nickname,
|
|
'role': role,
|
|
'text': text,
|
|
},
|
|
);
|
|
final d = res.data!['data'] as Map<String, dynamic>;
|
|
return SubmitResp(
|
|
submitted: d['submitted'] as bool,
|
|
initiatorReady: d['initiator_ready'] as bool,
|
|
match: d['match'] as bool?,
|
|
);
|
|
}
|
|
|
|
// ── CheckSession ──
|
|
|
|
Future<CheckResp> checkSession(String sessionId) async {
|
|
final res = await _dio.get<Map<String, dynamic>>(
|
|
'/api/v1/dual-check/check/$sessionId',
|
|
);
|
|
final d = res.data!['data'] as Map<String, dynamic>;
|
|
return CheckResp(
|
|
exists: d['exists'] as bool,
|
|
initiatorReady: d['initiator_ready'] as bool,
|
|
participantCount: d['participant_count'] as int,
|
|
ttlSeconds: d['ttl_seconds'] as int,
|
|
);
|
|
}
|
|
|
|
// ── GetSession 全量快照 ──
|
|
|
|
Future<SessionResp> getSession(String sessionId) async {
|
|
final res = await _dio.get<Map<String, dynamic>>(
|
|
'/api/v1/dual-check/session/$sessionId',
|
|
);
|
|
return parseSession(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},
|
|
);
|
|
return parseSession(res.data!['data'] as Map<String, dynamic>);
|
|
}
|
|
|
|
// ── EndSession 发起方主动结束会话 ──
|
|
|
|
Future<void> endSession(String sessionId) async {
|
|
await _dio.post('/api/v1/dual-check/session/$sessionId/end');
|
|
}
|
|
|
|
static SessionResp parseSession(Map<String, dynamic> d) {
|
|
final parts = (d['participants'] as List)
|
|
.map((p) => ParticipantResp.fromMap(p as Map<String, dynamic>))
|
|
.toList();
|
|
return SessionResp(
|
|
sessionId: d['session_id'] as String,
|
|
initiatorNickname: (d['initiator_nickname'] ?? '') as String,
|
|
initiatorReady: d['initiator_ready'] as bool,
|
|
allowResubmit: (d['allow_resubmit'] as bool?) ?? false,
|
|
participants: parts,
|
|
ttlSeconds: d['ttl_seconds'] as int,
|
|
);
|
|
}
|
|
|
|
// ── WS URL ──
|
|
|
|
String wsUrl({
|
|
required String sessionId,
|
|
required String role,
|
|
required String nickname,
|
|
}) {
|
|
return '$_wsBaseUrl/api/v1/dual-check/ws'
|
|
'?session_id=${Uri.encodeQueryComponent(sessionId)}'
|
|
'&role=$role'
|
|
'&nickname=${Uri.encodeQueryComponent(nickname)}';
|
|
}
|
|
}
|
|
|
|
// ── 数据模型 ──
|
|
|
|
class CreateResp {
|
|
final String sessionId;
|
|
final String nickname;
|
|
final int ttl;
|
|
CreateResp({required this.sessionId, required this.nickname, required this.ttl});
|
|
}
|
|
|
|
class SubmitResp {
|
|
final bool submitted;
|
|
final bool initiatorReady;
|
|
final bool? match;
|
|
SubmitResp({required this.submitted, required this.initiatorReady, this.match});
|
|
}
|
|
|
|
class CheckResp {
|
|
final bool exists;
|
|
final bool initiatorReady;
|
|
final int participantCount;
|
|
final int ttlSeconds;
|
|
CheckResp({
|
|
required this.exists,
|
|
required this.initiatorReady,
|
|
required this.participantCount,
|
|
required this.ttlSeconds,
|
|
});
|
|
}
|
|
|
|
class ParticipantResp {
|
|
final String nickname;
|
|
final bool joined;
|
|
final bool submitted;
|
|
final bool? match;
|
|
ParticipantResp({
|
|
required this.nickname,
|
|
required this.joined,
|
|
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?,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SessionResp {
|
|
final String sessionId;
|
|
final String initiatorNickname;
|
|
final bool initiatorReady;
|
|
final bool allowResubmit;
|
|
final List<ParticipantResp> participants;
|
|
final int ttlSeconds;
|
|
SessionResp({
|
|
required this.sessionId,
|
|
required this.initiatorNickname,
|
|
required this.initiatorReady,
|
|
required this.allowResubmit,
|
|
required this.participants,
|
|
required this.ttlSeconds,
|
|
});
|
|
}
|
|
|
|
// ── WS 消息类型 ──
|
|
|
|
class WSMessage {
|
|
final String type;
|
|
final Map<String, dynamic> data;
|
|
final String? 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?,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── 业务错误处理 ──
|
|
|
|
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 ?? '请求失败');
|
|
}
|