feat: 实现用户画像、占卜历史与后端用户管理模块
This commit is contained in:
+192
-5
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user