Initial project import
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:miaoji_zhang/shared/api/api_client.dart';
|
||||
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
|
||||
import 'package:miaoji_zhang/shared/services/local_export_service.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
|
||||
class AiCompanion {
|
||||
final String avatarKey;
|
||||
final String personaKey;
|
||||
final String? customName;
|
||||
final int roastLevel, stickerFrequency, proactiveLevel;
|
||||
|
||||
AiCompanion.fromJson(Map<String, dynamic> json)
|
||||
: avatarKey = json['avatarKey'] as String,
|
||||
personaKey = json['personaKey'] as String,
|
||||
customName = json['customName'] as String?,
|
||||
roastLevel = json['roastLevel'] as int,
|
||||
stickerFrequency = json['stickerFrequency'] as int,
|
||||
proactiveLevel = json['proactiveLevel'] as int;
|
||||
}
|
||||
|
||||
class UserProfile {
|
||||
final int userId;
|
||||
final String username;
|
||||
final String? nickname;
|
||||
final String appMode;
|
||||
final AiCompanion? aiCompanion;
|
||||
final bool onboardingDone;
|
||||
final bool aiEnabled;
|
||||
|
||||
UserProfile.fromJson(Map<String, dynamic> json)
|
||||
: userId = (json['userId'] as num).toInt(),
|
||||
username = json['username'] as String,
|
||||
nickname = json['nickname'] as String?,
|
||||
appMode = json['appMode'] as String,
|
||||
aiCompanion = json['aiCompanion'] == null
|
||||
? null
|
||||
: AiCompanion.fromJson(json['aiCompanion'] as Map<String, dynamic>),
|
||||
onboardingDone = json['onboardingDone'] as bool,
|
||||
aiEnabled =
|
||||
((json['permissions'] as Map<String, dynamic>?)?['ai'] as bool?) ??
|
||||
true;
|
||||
|
||||
const UserProfile.local({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.nickname,
|
||||
required this.appMode,
|
||||
required this.onboardingDone,
|
||||
this.aiEnabled = false,
|
||||
}) : aiCompanion = null;
|
||||
}
|
||||
|
||||
class AuthApi {
|
||||
static final _dio = ApiClient.instance.dio;
|
||||
|
||||
static Future<void> register(
|
||||
String username,
|
||||
String password, {
|
||||
required bool agreedToTerms,
|
||||
}) async {
|
||||
final response = await _dio.post(
|
||||
'/api/auth/register',
|
||||
data: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'agreedToTerms': agreedToTerms,
|
||||
},
|
||||
);
|
||||
await ApiClient.instance.saveToken(response.data['token'] as String);
|
||||
await me();
|
||||
}
|
||||
|
||||
static Future<bool> login(String username, String password) async {
|
||||
final response = await _dio.post(
|
||||
'/api/auth/login',
|
||||
data: {'username': username, 'password': password},
|
||||
);
|
||||
await ApiClient.instance.saveToken(response.data['token'] as String);
|
||||
return response.data['accountClosureCancelled'] as bool? ?? false;
|
||||
}
|
||||
|
||||
static Future<UserProfile> me() async {
|
||||
final session = SessionStore.instance;
|
||||
if (session.isGuest || (session.isAccount && session.shouldUseLocalOnly)) {
|
||||
return _localProfile();
|
||||
}
|
||||
try {
|
||||
final response = await _dio.get('/api/users/me');
|
||||
final profile = UserProfile.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
await _cacheProfile(profile);
|
||||
return profile;
|
||||
} catch (error) {
|
||||
if (session.isAccount &&
|
||||
(isConnectivityError(error) || session.needsReauth)) {
|
||||
return _localProfile();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<UserProfile> completeOnboarding({
|
||||
required String appMode,
|
||||
required String avatarKey,
|
||||
required String personaKey,
|
||||
String? customName,
|
||||
}) async {
|
||||
SessionStore.instance.requireOnline('完成账号初始化需要连接网络');
|
||||
final response = await _dio.post(
|
||||
'/api/users/me/onboarding',
|
||||
data: {
|
||||
'appMode': appMode,
|
||||
'avatarKey': avatarKey,
|
||||
'personaKey': personaKey,
|
||||
'customName': customName,
|
||||
},
|
||||
);
|
||||
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
|
||||
await _cacheProfile(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
static Future<UserProfile> switchMode(String appMode) async {
|
||||
SessionStore.instance.requireOnline('切换 AI 模式需要登录并连接网络');
|
||||
final response = await _dio.put(
|
||||
'/api/users/me/mode',
|
||||
data: {'appMode': appMode},
|
||||
);
|
||||
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
|
||||
await _cacheProfile(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
static Future<UserProfile> updateCompanion({
|
||||
String? avatarKey,
|
||||
String? personaKey,
|
||||
String? customName,
|
||||
int? roastLevel,
|
||||
int? stickerFrequency,
|
||||
int? proactiveLevel,
|
||||
}) async {
|
||||
SessionStore.instance.requireOnline('AI 伙伴设置需要登录并连接网络');
|
||||
final response = await _dio.put(
|
||||
'/api/users/me/companion',
|
||||
data: {
|
||||
'avatarKey': avatarKey,
|
||||
'personaKey': personaKey,
|
||||
'customName': customName,
|
||||
'roastLevel': roastLevel,
|
||||
'stickerFrequency': stickerFrequency,
|
||||
'proactiveLevel': proactiveLevel,
|
||||
},
|
||||
);
|
||||
final profile = UserProfile.fromJson(response.data as Map<String, dynamic>);
|
||||
await _cacheProfile(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
static Future<UserProfile> updateProfile(String nickname) async {
|
||||
final session = SessionStore.instance;
|
||||
final normalized = nickname.trim();
|
||||
if (session.isGuest) {
|
||||
await session.updateNickname(normalized);
|
||||
return _localProfile();
|
||||
}
|
||||
session.requireOnline('修改云端资料需要连接网络');
|
||||
final previous = session.nickname;
|
||||
await session.updateNickname(normalized);
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/api/users/me/profile',
|
||||
data: {'nickname': normalized},
|
||||
);
|
||||
final profile = UserProfile.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
await _cacheProfile(profile);
|
||||
return profile;
|
||||
} catch (_) {
|
||||
await session.updateNickname(previous);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> changePassword(
|
||||
String currentPassword,
|
||||
String newPassword,
|
||||
) async {
|
||||
SessionStore.instance.requireOnline('修改密码需要连接网络');
|
||||
final response = await _dio.put(
|
||||
'/api/users/me/password',
|
||||
data: {'currentPassword': currentPassword, 'newPassword': newPassword},
|
||||
);
|
||||
await ApiClient.instance.saveToken(response.data['token'] as String);
|
||||
}
|
||||
|
||||
static Future<List<int>> exportData() async {
|
||||
if (SessionStore.instance.shouldUseLocalOnly) {
|
||||
return LocalExportService.buildZip();
|
||||
}
|
||||
try {
|
||||
final response = await _dio.get<List<int>>(
|
||||
'/api/users/me/export',
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return response.data ?? const [];
|
||||
} catch (error) {
|
||||
if (isConnectivityError(error)) return LocalExportService.buildZip();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<DateTime> requestAccountClosure(
|
||||
String password,
|
||||
String confirmationText,
|
||||
) async {
|
||||
SessionStore.instance.requireOnline('注销账号需要连接网络');
|
||||
final response = await _dio.post(
|
||||
'/api/users/me/closure',
|
||||
data: {'password': password, 'confirmationText': confirmationText},
|
||||
);
|
||||
final scheduledAt = ShanghaiTime.parseCivil(
|
||||
response.data['scheduledAt'] as String,
|
||||
);
|
||||
await logout();
|
||||
return scheduledAt;
|
||||
}
|
||||
|
||||
static Future<void> deleteAccount(String password) async {
|
||||
SessionStore.instance.requireOnline('注销账号需要连接网络');
|
||||
await _dio.delete('/api/users/me', data: {'password': password});
|
||||
await logout();
|
||||
}
|
||||
|
||||
static Future<void> logout() async {
|
||||
CurrentLedgerStore.instance.clear();
|
||||
await ApiClient.instance.clearToken();
|
||||
await SessionStore.instance.clearActiveSession();
|
||||
}
|
||||
|
||||
static UserProfile _localProfile() {
|
||||
final session = SessionStore.instance;
|
||||
return UserProfile.local(
|
||||
userId: session.userId ?? 0,
|
||||
username: session.username ?? '游客',
|
||||
nickname: session.nickname,
|
||||
appMode: session.isGuest ? 'normal' : session.appMode,
|
||||
onboardingDone: session.isGuest || session.onboardingDone,
|
||||
aiEnabled: session.aiEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _cacheProfile(UserProfile profile) =>
|
||||
SessionStore.instance.activateAccount(
|
||||
userId: profile.userId,
|
||||
username: profile.username,
|
||||
nickname: profile.nickname,
|
||||
appMode: profile.appMode,
|
||||
onboardingDone: profile.onboardingDone,
|
||||
aiEnabled: profile.aiEnabled,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user