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
@@ -0,0 +1,90 @@
import 'package:dio/dio.dart';
import '../../../../core/network/api_problem.dart';
import '../../../../data/network/api_client.dart';
import '../models/profile_settings.dart';
class ProfileApi {
const ProfileApi({required ApiClient apiClient}) : _apiClient = apiClient;
final ApiClient _apiClient;
Future<ProfileSettingsV1> getProfile() async {
final json = await _apiClient.getJson('/api/v1/users/me/profile');
return _toSettings(json);
}
Future<ProfileSettingsV1> updateProfile(ProfileSettingsV1 next) async {
final payload = <String, dynamic>{
'display_name': next.displayName,
'bio': next.bio,
if (next.avatarPath != null && next.avatarPath!.isNotEmpty)
'avatar_path': next.avatarPath,
};
final json = await _apiClient.rawDio.patch<Map<String, dynamic>>(
'/api/v1/users/me/profile',
data: payload,
);
final data = json.data;
if (data is! Map<String, dynamic>) {
throw ApiProblem(
status: 502,
title: 'Invalid profile payload',
detail: 'Expected profile response object',
);
}
return _toSettings(data);
}
Future<ProfileSettingsV1> uploadAvatar(String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
final response = await _apiClient.rawDio.post<Map<String, dynamic>>(
'/api/v1/users/me/avatar',
data: formData,
);
final data = response.data;
if (data is! Map<String, dynamic>) {
throw ApiProblem(
status: 502,
title: 'Invalid profile payload',
detail: 'Expected profile response object',
);
}
return _toSettings(data);
}
ProfileSettingsV1 _toSettings(Map<String, dynamic> json) {
final settingsRaw = json['settings'];
final preferencesRaw = settingsRaw is Map<String, dynamic>
? settingsRaw['preferences']
: null;
final preferences = preferencesRaw is Map<String, dynamic>
? PreferenceSettings(
interfaceLanguage:
(preferencesRaw['interface_language'] as String?) ?? 'zh-CN',
aiLanguage: (preferencesRaw['ai_language'] as String?) ?? 'zh-CN',
timezone:
(preferencesRaw['timezone'] as String?) ?? 'Asia/Shanghai',
country: (preferencesRaw['country'] as String?) ?? 'CN',
)
: const PreferenceSettings();
return ProfileSettingsV1(
displayName: (json['display_name'] as String?) ?? '',
bio: (json['bio'] as String?) ?? '',
avatarPath: json['avatar_path'] as String?,
avatarUrl: json['avatar_url'] as String?,
preferences: preferences,
privacy: settingsRaw is Map<String, dynamic>
? (settingsRaw['privacy'] as Map<String, dynamic>? ??
const <String, dynamic>{})
: const <String, dynamic>{},
notification: settingsRaw is Map<String, dynamic>
? (settingsRaw['notification'] as Map<String, dynamic>? ??
const <String, dynamic>{})
: const <String, dynamic>{},
);
}
}
@@ -40,24 +40,40 @@ class PreferenceSettings {
class ProfileSettingsV1 {
const ProfileSettingsV1({
this.version = 1,
this.displayName = '',
this.bio = '',
this.avatarPath,
this.avatarUrl,
this.preferences = const PreferenceSettings(),
this.privacy = const <String, Object?>{},
this.notification = const <String, Object?>{},
});
final int version;
final String displayName;
final String bio;
final String? avatarPath;
final String? avatarUrl;
final PreferenceSettings preferences;
final Map<String, Object?> privacy;
final Map<String, Object?> notification;
ProfileSettingsV1 copyWith({
int? version,
String? displayName,
String? bio,
String? avatarPath,
String? avatarUrl,
PreferenceSettings? preferences,
Map<String, Object?>? privacy,
Map<String, Object?>? notification,
}) {
return ProfileSettingsV1(
version: version ?? this.version,
displayName: displayName ?? this.displayName,
bio: bio ?? this.bio,
avatarPath: avatarPath ?? this.avatarPath,
avatarUrl: avatarUrl ?? this.avatarUrl,
preferences: preferences ?? this.preferences,
privacy: privacy ?? this.privacy,
notification: notification ?? this.notification,