285 lines
8.7 KiB
Dart
285 lines
8.7 KiB
Dart
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/push_service.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 AuthLoginResult {
|
|
final bool accountClosureCancelled;
|
|
final UserProfile profile;
|
|
|
|
const AuthLoginResult({
|
|
required this.accountClosureCancelled,
|
|
required this.profile,
|
|
});
|
|
}
|
|
|
|
class AuthApi {
|
|
static final _dio = ApiClient.instance.dio;
|
|
|
|
static Future<UserProfile> 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);
|
|
return me(forceRemote: true);
|
|
}
|
|
|
|
static Future<AuthLoginResult> 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);
|
|
final profile = await me(forceRemote: true);
|
|
return AuthLoginResult(
|
|
accountClosureCancelled:
|
|
response.data['accountClosureCancelled'] as bool? ?? false,
|
|
profile: profile,
|
|
);
|
|
}
|
|
|
|
static Future<UserProfile> me({bool forceRemote = false}) async {
|
|
final session = SessionStore.instance;
|
|
if (!forceRemote &&
|
|
(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 PushService.instance.logout();
|
|
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,
|
|
);
|
|
}
|