feat: 实现用户画像、占卜历史与后端用户管理模块

This commit is contained in:
ZL-Q
2026-04-06 01:28:10 +08:00
parent d87b2e1e3a
commit 8a18b3528b
77 changed files with 5850 additions and 2604 deletions
+192 -5
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../core/auth/session_store.dart';
import '../core/logging/logger.dart';
import '../data/network/api_client.dart';
import '../data/storage/local_kv_store.dart';
import '../features/auth/data/apis/auth_api.dart';
@@ -10,7 +11,9 @@ import '../features/auth/presentation/bloc/auth_bloc.dart';
import '../features/auth/presentation/bloc/auth_state.dart';
import '../features/auth/presentation/screens/login_screen.dart';
import '../features/divination/data/apis/divination_api.dart';
import '../features/divination/data/models/divination_result.dart';
import '../features/home/presentation/screens/home_screen.dart';
import '../features/settings/data/apis/profile_api.dart';
import '../features/settings/data/models/profile_settings.dart';
import '../l10n/app_localizations.dart';
import '../shared/widgets/app_loading_indicator.dart';
@@ -25,9 +28,11 @@ class EryaoApp extends StatefulWidget {
}
class _EryaoAppState extends State<EryaoApp> {
static final Logger _logger = getLogger('app.eryao_app');
final SessionStore _sessionStore = SessionStore(LocalKvStore());
late final AuthBloc _authBloc;
late final DivinationApi _divinationApi;
late final ProfileApi _profileApi;
Locale _locale = const Locale('zh');
ProfileSettingsV1 _profileSettings = ProfileSettingsV1.defaultsForLocale(
const Locale('zh'),
@@ -35,6 +40,11 @@ class _EryaoAppState extends State<EryaoApp> {
int _creditsBalance = 0;
bool _loadingCredits = false;
String? _loadedCreditsUserEmail;
bool _loadingHistory = false;
String? _loadedHistoryUserEmail;
List<DivinationResultData> _historyRecords = const <DivinationResultData>[];
bool _loadingProfile = false;
String? _loadedProfileUserEmail;
@override
void initState() {
@@ -48,6 +58,7 @@ class _EryaoAppState extends State<EryaoApp> {
);
final authApi = AuthApi(apiClient: apiClient);
_divinationApi = DivinationApi(apiClient: apiClient);
_profileApi = ProfileApi(apiClient: apiClient);
final authRepository = AuthRepositoryImpl(
authApi: authApi,
sessionStore: _sessionStore,
@@ -64,22 +75,192 @@ class _EryaoAppState extends State<EryaoApp> {
return;
}
_loadingCredits = true;
_refreshCredits(userEmail: userEmail).whenComplete(() {
_loadingCredits = false;
});
}
void _ensureHistoryLoaded(String userEmail) {
if (_loadingHistory) {
return;
}
if (_loadedHistoryUserEmail == userEmail) {
return;
}
_loadingHistory = true;
_divinationApi
.getPointsBalance()
.then((balance) {
.getHistoryRecords(userId: userEmail)
.then((records) {
if (!mounted) {
return;
}
setState(() {
_creditsBalance = balance.availableBalance;
_loadedCreditsUserEmail = userEmail;
_historyRecords = records;
_loadedHistoryUserEmail = userEmail;
});
})
.catchError((Object error, StackTrace stackTrace) {
_logger.warning(
message: 'Failed to load divination history',
extra: <String, dynamic>{
'error': error.toString(),
'stackTrace': stackTrace.toString(),
},
);
})
.whenComplete(() {
_loadingCredits = false;
_loadingHistory = false;
});
}
Future<void> _refreshCredits({required String userEmail}) async {
final balance = await _divinationApi.getPointsBalance();
if (!mounted) {
return;
}
setState(() {
_creditsBalance = balance.availableBalance;
_loadedCreditsUserEmail = userEmail;
});
}
Future<void> _handleDivinationCompleted(DivinationResultData result) async {
final user = _authBloc.state.user;
if (user == null) {
return;
}
final optimisticRecords = _mergeAndSortHistory(<DivinationResultData>[
result,
..._historyRecords,
]);
if (!mounted) {
return;
}
setState(() {
_historyRecords = optimisticRecords;
_loadedHistoryUserEmail = user.email;
});
try {
final records = await _divinationApi.getHistoryRecords(
userId: user.email,
);
if (!mounted) {
return;
}
setState(() {
_historyRecords = _mergeAndSortHistory(<DivinationResultData>[
...records,
...optimisticRecords,
]);
_loadedHistoryUserEmail = user.email;
});
} catch (error, stackTrace) {
_logger.warning(
message: 'Failed to refresh history after divination completion',
extra: <String, dynamic>{
'error': error.toString(),
'stackTrace': stackTrace.toString(),
},
);
}
try {
await _refreshCredits(userEmail: user.email);
} catch (error, stackTrace) {
_logger.warning(
message: 'Failed to refresh credits after divination completion',
extra: <String, dynamic>{
'error': error.toString(),
'stackTrace': stackTrace.toString(),
},
);
}
}
List<DivinationResultData> _mergeAndSortHistory(
List<DivinationResultData> input,
) {
final seen = <String>{};
final deduped = <DivinationResultData>[];
for (final item in input) {
final key = _historyKey(item);
if (seen.add(key)) {
deduped.add(item);
}
}
deduped.sort(
(a, b) => b.params.divinationTime.compareTo(a.params.divinationTime),
);
return deduped;
}
String _historyKey(DivinationResultData item) {
return [
item.params.question,
item.binaryCode,
item.changedBinaryCode,
item.guaName,
item.targetGuaName,
item.signType,
].join('|');
}
Future<void> _refreshProfile({required String userEmail}) async {
if (_loadingProfile) {
return;
}
if (_loadedProfileUserEmail == userEmail) {
return;
}
_loadingProfile = true;
try {
final profile = await _profileApi.getProfile();
if (!mounted) {
return;
}
setState(() {
_profileSettings = profile;
_loadedProfileUserEmail = userEmail;
});
} finally {
_loadingProfile = false;
}
}
Future<ProfileSettingsV1> _uploadAvatar(String filePath) async {
final updated = await _profileApi.uploadAvatar(filePath);
if (!mounted) {
return updated;
}
setState(() {
_profileSettings = updated;
});
return updated;
}
Future<void> _saveProfileSettings(ProfileSettingsV1 next) async {
try {
final saved = await _profileApi.updateProfile(next);
if (!mounted) {
return;
}
setState(() {
_profileSettings = saved;
});
} catch (error, stackTrace) {
_logger.error(
message: 'Failed to save profile settings via API',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}
@override
void dispose() {
_authBloc.dispose();
@@ -149,13 +330,19 @@ class _EryaoAppState extends State<EryaoApp> {
if (state.status == AuthStatus.authenticated && state.user != null) {
_ensureCreditsLoaded(state.user!.email);
_ensureHistoryLoaded(state.user!.email);
_refreshProfile(userEmail: state.user!.email);
return HomeScreen(
account: state.user!.email,
sessionStore: _sessionStore,
currentLocale: _locale,
profileSettings: _profileSettings,
historyRecords: _historyRecords,
coinBalance: _creditsBalance,
onLocaleChanged: _handleInterfaceLanguageChanged,
onProfileSettingsChanged: _saveProfileSettings,
onUploadAvatar: _uploadAvatar,
onDivinationCompleted: _handleDivinationCompleted,
onLogout: _authBloc.logout,
);
}