ff40ff9dd8
- 数据库:添加 has_purchased_starter_pack 字段到 register_bonus_claims - 后端:创建静态配置管理套餐信息,支持按国家/地区区分 - 后端:新增 GET /api/v1/points/packages API 返回可用套餐 - 后端:创建 utils/paths.py 统一路径管理 - 前端:动态获取套餐信息,移除硬编码 - 前端:添加 ProductCode 枚举约束,前后端类型安全 - 配置:Profile 默认国家改为 US(ISO 3166-1 alpha-2) - 文档:更新协议文档说明新 API 和字段
162 lines
5.5 KiB
Dart
162 lines
5.5 KiB
Dart
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> updateSettings(ProfileSettingsV1 settings) async {
|
|
final payload = <String, dynamic>{
|
|
'settings': {
|
|
'version': settings.version,
|
|
'preferences': {
|
|
'interface_language': settings.preferences.interfaceLanguage,
|
|
'ai_language': settings.preferences.aiLanguage,
|
|
'timezone': settings.preferences.timezone,
|
|
'country': settings.preferences.country,
|
|
},
|
|
'privacy': settings.privacy,
|
|
'notification': {
|
|
'allow_notifications': settings.notification.allowNotifications,
|
|
'allow_vibration': settings.notification.allowVibration,
|
|
},
|
|
'divination_tutorial': {
|
|
'divination_entry_shown':
|
|
settings.divinationTutorial.divinationEntryShown,
|
|
'auto_divination_shown':
|
|
settings.divinationTutorial.autoDivinationShown,
|
|
'manual_divination_shown':
|
|
settings.divinationTutorial.manualDivinationShown,
|
|
},
|
|
},
|
|
};
|
|
final json = await _apiClient.rawDio.patch<Map<String, dynamic>>(
|
|
'/api/v1/users/me/settings',
|
|
data: payload,
|
|
);
|
|
final data = json.data;
|
|
if (data is! Map<String, dynamic>) {
|
|
throw ApiProblem(
|
|
status: 502,
|
|
title: 'Invalid settings payload',
|
|
detail: 'Expected settings 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);
|
|
}
|
|
|
|
Future<void> deleteAccount() async {
|
|
await _apiClient.deleteNoContent('/api/v1/users/me');
|
|
}
|
|
|
|
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?) ?? 'US',
|
|
)
|
|
: const PreferenceSettings();
|
|
|
|
final notificationRaw = settingsRaw is Map<String, dynamic>
|
|
? settingsRaw['notification']
|
|
: null;
|
|
final notification = notificationRaw is Map<String, dynamic>
|
|
? NotificationSettings(
|
|
allowNotifications:
|
|
(notificationRaw['allow_notifications'] as bool? ?? true),
|
|
allowVibration:
|
|
(notificationRaw['allow_vibration'] as bool? ?? true),
|
|
)
|
|
: const NotificationSettings();
|
|
|
|
final divinationTutorialRaw = settingsRaw is Map<String, dynamic>
|
|
? settingsRaw['divination_tutorial']
|
|
: null;
|
|
final divinationTutorial = divinationTutorialRaw is Map<String, dynamic>
|
|
? DivinationTutorialSettings(
|
|
divinationEntryShown:
|
|
(divinationTutorialRaw['divination_entry_shown'] as bool?) ??
|
|
true,
|
|
autoDivinationShown:
|
|
(divinationTutorialRaw['auto_divination_shown'] as bool?) ??
|
|
true,
|
|
manualDivinationShown:
|
|
(divinationTutorialRaw['manual_divination_shown'] as bool?) ??
|
|
true,
|
|
)
|
|
: const DivinationTutorialSettings();
|
|
|
|
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: notification,
|
|
divinationTutorial: divinationTutorial,
|
|
);
|
|
}
|
|
}
|