// 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 && 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 createSession({required String nickname}) async { final res = await _dio.post>( '/api/v1/dual-check/create', data: {'nickname': nickname}, ); final d = res.data!['data'] as Map; return CreateResp( sessionId: d['session_id'] as String, nickname: d['nickname'] as String, ttl: d['ttl_seconds'] as int, ); } // ── Submit ── Future submit({ required String sessionId, required String nickname, required String role, // "initiator" | "participant" required String text, }) async { final res = await _dio.post>( '/api/v1/dual-check/submit', data: { 'session_id': sessionId, 'nickname': nickname, 'role': role, 'text': text, }, ); final d = res.data!['data'] as Map; return SubmitResp( submitted: d['submitted'] as bool, initiatorReady: d['initiator_ready'] as bool, match: d['match'] as bool?, ); } // ── CheckSession ── Future checkSession(String sessionId) async { final res = await _dio.get>( '/api/v1/dual-check/check/$sessionId', ); final d = res.data!['data'] as Map; 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 getSession(String sessionId) async { final res = await _dio.get>( '/api/v1/dual-check/session/$sessionId', ); return parseSession(res.data!['data'] as Map); } // ── SetResubmit 发起方切换"允许参与者重新提交" ── Future setResubmit(String sessionId, {required bool allow}) async { final res = await _dio.post>( '/api/v1/dual-check/session/$sessionId/resubmit', data: {'allow': allow}, ); return parseSession(res.data!['data'] as Map); } // ── EndSession 发起方主动结束会话 ── Future endSession(String sessionId) async { await _dio.post('/api/v1/dual-check/session/$sessionId/end'); } static SessionResp parseSession(Map d) { final parts = (d['participants'] as List) .map((p) => ParticipantResp.fromMap(p as Map)) .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 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 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 data; final String? error; WSMessage({required this.type, required this.data, this.error}); factory WSMessage.fromJson(Map json) { return WSMessage( type: json['type'] as String, data: (json['data'] as Map?) ?? 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 ?? '请求失败'); }