docs(webrtc_controller_flutter): 更新项目文档以反映重构后的架构

AGENTS.md 与 README.md 同步更新:根据实际代码结构重写目录树、技术栈、架构分层及编码规范,移除旧版内联示例并补充新的开发约定与代码生成命令。
This commit is contained in:
2026-08-03 16:12:44 +08:00
parent 1918e5738e
commit d5e66a1777
51 changed files with 4711 additions and 1458 deletions

View File

@@ -0,0 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../domain/auth_repository.dart';
import 'dio_auth_repository.dart';
/// 认证仓库单例(登录 / 刷新 / 绑定列表 / TURN 凭证)。
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return DioAuthRepository();
});

View File

@@ -0,0 +1,160 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../../../app/constants/app_constants.dart';
import '../../../core/network/api_exception.dart';
import '../../../core/storage/token_storage.dart';
import '../domain/auth_repository.dart';
/// 基于 Dio 的 [AuthRepository] 实现。
class DioAuthRepository implements AuthRepository {
DioAuthRepository({Dio? dio, TokenStorage? storage})
: _dio = dio ?? Dio(_baseOptions),
_storage = storage ?? const TokenStorage();
static final BaseOptions _baseOptions = BaseOptions(
baseUrl: kApiBase,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
);
final Dio _dio;
final TokenStorage _storage;
// 刷新单飞:避免并发触发多次刷新。
Future<Map<String, dynamic>>? _refreshing;
String? _accessToken;
@override
String? get accessToken => _accessToken;
@override
Future<void> restore() async {
_accessToken = await _storage.readAccess();
}
@override
Future<bool> hasRefreshToken() async =>
await _storage.readRefresh() != null;
@override
Future<String?> getUsername() => _storage.readUsername();
Future<void> _saveTokens({
required String accessToken,
String? refreshToken,
String? username,
}) async {
_accessToken = accessToken;
await _storage.writeAccess(accessToken);
if (refreshToken != null) {
await _storage.writeRefresh(refreshToken);
}
if (username != null) {
await _storage.writeUsername(username);
}
}
@override
Future<Map<String, dynamic>> login(String username, String password) async {
final data = await _post(
'/api/auth/login',
body: {'username': username, 'password': password},
);
await _saveTokens(
accessToken: data['accessToken'] as String,
refreshToken: data['refreshToken'] as String?,
username: username,
);
return data;
}
@override
Future<Map<String, dynamic>> register(String username, String password) =>
_post('/api/auth/register',
body: {'username': username, 'password': password});
@override
Future<Map<String, dynamic>> refresh() async {
final rt = await _storage.readRefresh();
if (rt == null) {
await clear();
throw const ApiException('NO_REFRESH_TOKEN', 401);
}
_refreshing ??= _doRefresh(rt).whenComplete(() => _refreshing = null);
return _refreshing!;
}
Future<Map<String, dynamic>> _doRefresh(String rt) async {
final data = await _post('/api/auth/refresh', body: {'refreshToken': rt});
await _saveTokens(
accessToken: data['accessToken'] as String,
refreshToken: data['refreshToken'] as String?,
);
return data;
}
@override
Future<Map<String, dynamic>> verify() => _get('/api/client/verify');
@override
Future<Map<String, dynamic>> bindings() => _get('/api/client/bindings');
@override
Future<Map<String, dynamic>?> turnCredentials() async {
try {
return await _get('/api/client/turn-credentials');
} on ApiException {
return null;
}
}
@override
Future<void> clear() async {
_accessToken = null;
await _storage.clear();
}
Future<Map<String, dynamic>> _post(
String path, {
required Map<String, dynamic> body,
}) async {
final res = await _dio.post<dynamic>(
path,
data: jsonEncode(body),
);
return _handle(res);
}
Future<Map<String, dynamic>> _get(String path) async {
final res = await _dio.get<dynamic>(
path,
options: Options(
headers: {
if (_accessToken != null) 'Authorization': 'Bearer $_accessToken',
},
),
);
return _handle(res);
}
Map<String, dynamic> _handle(Response<dynamic> res) {
final data = res.data;
final body = (data is Map)
? data.cast<String, dynamic>()
: <String, dynamic>{};
if (res.statusCode == 401) {
throw ApiException(body['code'] as String? ?? 'UNAUTHORIZED', 401);
}
if (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300) {
throw ApiException(
body['error'] as String? ?? 'HTTP ${res.statusCode}',
res.statusCode ?? -1,
);
}
return body;
}
}