refactor(apps): 重构数据层目录结构并新增启动预热编排器

This commit is contained in:
zl-q
2026-03-29 20:26:30 +08:00
parent 33340de8f9
commit 4db9a13bfe
108 changed files with 1653 additions and 1320 deletions
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/app/di/injection.dart';
import 'package:social_app/app/router/app_router.dart';
import 'package:social_app/app/router/app_routes.dart';
import 'package:social_app/core/network/i_api_client.dart';
import 'package:social_app/data/network/i_api_client.dart';
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
@@ -0,0 +1,103 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/app/services/app_prewarm_orchestrator.dart';
import 'package:social_app/data/network/i_api_client.dart';
import 'package:social_app/data/cache/cache_store.dart';
import 'package:social_app/features/calendar/data/repositories/calendar_repository.dart';
import 'package:social_app/features/messages/data/repositories/inbox_repository.dart';
import 'package:social_app/features/chat/data/repositories/chat_history_repository.dart';
class _FakeApiClient implements IApiClient {
@override
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Response<T>> get<T>(String path, {Options? options}) {
throw UnimplementedError();
}
@override
Future<Stream<String>> getSseLines(
String path, {
Map<String, String>? headers,
}) {
throw UnimplementedError();
}
@override
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Response<T>> post<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Response<T>> put<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
}
void main() {
late HybridCacheStore store;
late _FakeApiClient apiClient;
late CalendarRepository calendarRepository;
late InboxRepository inboxRepository;
late ChatHistoryRepository chatHistoryRepository;
setUp(() {
apiClient = _FakeApiClient();
store = HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
);
calendarRepository = CalendarRepository(apiClient: apiClient, store: store);
inboxRepository = InboxRepositoryImpl(apiClient: apiClient, store: store);
chatHistoryRepository = ChatHistoryRepository(
apiClient: apiClient,
store: store,
);
});
test('completes prewarm within budget', () async {
final orchestrator = AppPrewarmOrchestrator(
calendarRepository: calendarRepository,
inboxRepository: inboxRepository,
chatHistoryRepository: chatHistoryRepository,
bootBudget: const Duration(milliseconds: 100),
prewarmChatHistory: () async {},
prewarmCalendarToday: () async {},
prewarmUnreadInbox: () async {},
);
await orchestrator.ensureStartedFor('u1');
expect(orchestrator.status, AppPrewarmStatus.completed);
expect(orchestrator.isBootBlocking, isFalse);
});
test('times out when budget exceeded', () async {
final completer = Completer<void>();
final orchestrator = AppPrewarmOrchestrator(
calendarRepository: calendarRepository,
inboxRepository: inboxRepository,
chatHistoryRepository: chatHistoryRepository,
bootBudget: const Duration(milliseconds: 30),
prewarmChatHistory: () => completer.future,
prewarmCalendarToday: () => completer.future,
prewarmUnreadInbox: () => completer.future,
);
await orchestrator.ensureStartedFor('u1');
expect(orchestrator.status, AppPrewarmStatus.timedOut);
expect(orchestrator.isBootBlocking, isFalse);
completer.complete();
});
}
+1 -3
View File
@@ -1,9 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/data/cache/cache_policy.dart';
import 'package:social_app/data/cache/cached_repository.dart';
import 'package:social_app/data/cache/hybrid_cache_store.dart';
import 'package:social_app/data/cache/memory_cache_store.dart';
import 'package:social_app/data/cache/persistent_cache_store.dart';
import 'package:social_app/data/cache/cache_store.dart';
class _IntCachedRepository extends CachedRepository<int> {
int loadCount = 0;
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:social_app/data/cache/cache_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('HybridCacheStore', () {
test('falls back to persistent and backfills memory', () async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final prefs = await SharedPreferences.getInstance();
final persistent = PersistentCacheStore(prefs: prefs);
await persistent.write<String>('k', 'v');
final hybrid = HybridCacheStore(
memory: MemoryCacheStore(),
persistent: persistent,
);
final firstRead = await hybrid.read<String>('k');
expect(firstRead, 'v');
await persistent.remove('k');
final secondRead = await hybrid.read<String>('k');
expect(secondRead, 'v');
});
});
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:social_app/data/cache/cache_store.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('SharedPrefsCacheStore', () {
test('persists entries across instances', () async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final prefs = await SharedPreferences.getInstance();
final storeA = SharedPrefsCacheStore(prefs: prefs);
await storeA.write<String>('k', 'v');
final storeB = SharedPrefsCacheStore(prefs: prefs);
final value = await storeB.read<String>('k');
expect(value, 'v');
});
test('reads cache entry persisted payload', () async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final prefs = await SharedPreferences.getInstance();
final now = DateTime.utc(2026, 3, 29, 12, 0, 0);
final storeA = SharedPrefsCacheStore(prefs: prefs);
await storeA.write<CacheEntry<String>>(
'entry',
CacheEntry<String>(value: 'payload', fetchedAt: now),
);
final storeB = SharedPrefsCacheStore(prefs: prefs);
final entry = await storeB.read<CacheEntry<String>>('entry');
expect(entry, isNotNull);
expect(entry!.value, 'payload');
expect(entry.fetchedAt, now);
});
});
}
@@ -1,13 +1,14 @@
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/core/network/i_api_client.dart';
import 'package:social_app/data/repositories/calendar_event_repository.dart';
import 'package:social_app/data/repositories/friend_repository.dart';
import 'package:social_app/data/repositories/inbox_repository.dart';
import 'package:social_app/data/repositories/models/calendar_event.dart';
import 'package:social_app/data/repositories/models/friend_request.dart';
import 'package:social_app/data/repositories/models/inbox_message.dart';
import 'package:social_app/data/repositories/user_repository.dart';
import 'package:social_app/data/network/i_api_client.dart';
import 'package:social_app/data/cache/cache_store.dart';
import 'package:social_app/features/calendar/data/repositories/calendar_repository.dart';
import 'package:social_app/features/contacts/data/repositories/friend_repository.dart';
import 'package:social_app/features/messages/data/repositories/inbox_repository.dart';
import 'package:social_app/features/contacts/data/models/friend_request.dart';
import 'package:social_app/features/messages/data/models/inbox_message.dart';
import 'package:social_app/features/calendar/data/models/schedule_item_model.dart';
import 'package:social_app/features/contacts/data/repositories/user_repository.dart';
class _FakeApiClient implements IApiClient {
final Map<String, dynamic> _getResponses = <String, dynamic>{};
@@ -90,7 +91,13 @@ void main() {
},
]);
final repository = InboxRepositoryImpl(client);
final repository = InboxRepositoryImpl(
apiClient: client,
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
final result = await repository.getMessages(isRead: false);
expect(result.single.messageType, InboxMessageType.calendar);
@@ -109,7 +116,13 @@ void main() {
'created_at': '2026-03-27T08:00:00Z',
});
final repository = FriendRepositoryImpl(client);
final repository = FriendRepositoryImpl(
apiClient: client,
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
final request = await repository.getRequestById('f1');
expect(request.status, FriendRequestStatus.accepted);
@@ -129,7 +142,13 @@ void main() {
'created_at': '2026-03-27T08:00:00Z',
});
final repository = FriendRepositoryImpl(client);
final repository = FriendRepositoryImpl(
apiClient: client,
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
await expectLater(
repository.getRequestsByIds(['f1', 'missing']),
throwsStateError,
@@ -137,7 +156,7 @@ void main() {
},
);
test('CalendarEventRepository maps archived status', () async {
test('CalendarRepository maps archived status', () async {
final client = _FakeApiClient();
client.setGet('/api/v1/schedule-items/e1', {
'id': 'e1',
@@ -156,10 +175,16 @@ void main() {
'updated_at': '2026-03-27T09:00:00Z',
});
final repository = CalendarEventRepositoryImpl(client);
final repository = CalendarRepository(
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
apiClient: client,
);
final event = await repository.getById('e1');
expect(event.status, CalendarEventStatus.archived);
expect(event.status, ScheduleStatus.archived);
});
test('UserRepository returns shared user summary', () async {
@@ -0,0 +1,89 @@
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/data/network/i_api_client.dart';
import 'package:social_app/data/cache/cache_store.dart';
import 'package:social_app/features/chat/data/repositories/chat_history_repository.dart';
class _FakeApiClient implements IApiClient {
final Map<String, dynamic> _getResponses = <String, dynamic>{};
final Map<String, int> getCalls = <String, int>{};
void setGet(String path, dynamic data) => _getResponses[path] = data;
@override
Future<Response<T>> get<T>(String path, {Options? options}) async {
getCalls[path] = (getCalls[path] ?? 0) + 1;
if (!_getResponses.containsKey(path)) {
throw StateError('missing GET mock for $path');
}
return Response<T>(
requestOptions: RequestOptions(path: path),
data: _getResponses[path] as T,
);
}
@override
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Stream<String>> getSseLines(
String path, {
Map<String, String>? headers,
}) {
throw UnimplementedError();
}
@override
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Response<T>> post<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
@override
Future<Response<T>> put<T>(String path, {data, Options? options}) {
throw UnimplementedError();
}
}
void main() {
test('loads first-page history from cache on second read', () async {
final client = _FakeApiClient();
client.setGet('/api/v1/agent/history', {
'scope': 'history_day',
'threadId': 't1',
'day': '2026-03-29',
'hasMore': true,
'messages': [
{
'id': 'm1',
'seq': 1,
'role': 'assistant',
'content': 'hello',
'timestamp': '2026-03-29T08:00:00Z',
'attachments': [],
},
],
});
final repository = ChatHistoryRepository(
apiClient: client,
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
final first = await repository.loadHistory();
final second = await repository.loadHistory();
expect(first.threadId, 't1');
expect(second.messages.length, 1);
expect(client.getCalls['/api/v1/agent/history'], 1);
});
}
@@ -1,11 +1,9 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/data/cache/hybrid_cache_store.dart';
import 'package:social_app/data/cache/memory_cache_store.dart';
import 'package:social_app/data/models/user_profile.dart';
import 'package:social_app/data/cache/persistent_cache_store.dart';
import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart';
import 'package:social_app/data/cache/cache_store.dart';
import 'package:social_app/features/contacts/data/models/user_profile.dart';
import 'package:social_app/features/settings/data/repositories/user_profile_cache_repository.dart';
void main() {
group('UserProfileCacheRepository', () {