|
|
@ -35,6 +35,9 @@ class ProfileController extends GetxController { |
|
|
final _userportraitManager = Get.find<UserportraitManager>(); |
|
|
final _userportraitManager = Get.find<UserportraitManager>(); |
|
|
// 用于乐观更新的原始状态备份 |
|
|
// 用于乐观更新的原始状态备份 |
|
|
String _originalName = ''; |
|
|
String _originalName = ''; |
|
|
|
|
|
// 服务端已落库的昵称。默认名 User_xxxx 含敏感词,未改昵称时不要再提交, |
|
|
|
|
|
// 否则头像和性别会跟着被整单拒绝。 |
|
|
|
|
|
String _serverName = ''; |
|
|
String _originalAvatar = ''; |
|
|
String _originalAvatar = ''; |
|
|
String _originalLocalAvatar = ''; |
|
|
String _originalLocalAvatar = ''; |
|
|
|
|
|
|
|
|
@ -220,9 +223,13 @@ class ProfileController extends GetxController { |
|
|
// 创建FormData对象用于上传 |
|
|
// 创建FormData对象用于上传 |
|
|
dio.FormData formData = dio.FormData(); |
|
|
dio.FormData formData = dio.FormData(); |
|
|
|
|
|
|
|
|
// 添加基本用户信息 |
|
|
// 添加基本用户信息。昵称没变就不传:服务端对非空名字做敏感词校验, |
|
|
|
|
|
// 自动生成的 User_xxxx 一定含 "se",会把同一次的头像、性别一起拒绝。 |
|
|
formData.fields.add(MapEntry('language', currentLanguage.value)); |
|
|
formData.fields.add(MapEntry('language', currentLanguage.value)); |
|
|
|
|
|
if (userInfo.value.name.isNotEmpty && |
|
|
|
|
|
userInfo.value.name != _serverName) { |
|
|
formData.fields.add(MapEntry('name', userInfo.value.name)); |
|
|
formData.fields.add(MapEntry('name', userInfo.value.name)); |
|
|
|
|
|
} |
|
|
formData.fields.add(MapEntry('gender', userInfo.value.gender.toString())); |
|
|
formData.fields.add(MapEntry('gender', userInfo.value.gender.toString())); |
|
|
|
|
|
|
|
|
// 如果有头像,添加到FormData |
|
|
// 如果有头像,添加到FormData |
|
|
@ -272,8 +279,15 @@ class ProfileController extends GetxController { |
|
|
// 更新用户信息为服务器返回的最新数据 |
|
|
// 更新用户信息为服务器返回的最新数据 |
|
|
userInfo.update((u) { |
|
|
userInfo.update((u) { |
|
|
if (u != null) { |
|
|
if (u != null) { |
|
|
u.name = response['name'] ?? u.name; |
|
|
// 没改昵称时请求不带 name,服务端会把该字段回成空串,不能覆盖本地名字。 |
|
|
u.avatar = response['avatar'] ?? u.avatar; |
|
|
final returnedName = response['name']; |
|
|
|
|
|
if (returnedName is String && returnedName.isNotEmpty) { |
|
|
|
|
|
u.name = returnedName; |
|
|
|
|
|
} |
|
|
|
|
|
final returnedAvatar = response['avatar']; |
|
|
|
|
|
if (returnedAvatar is String && returnedAvatar.isNotEmpty) { |
|
|
|
|
|
u.avatar = returnedAvatar; |
|
|
|
|
|
} |
|
|
if (response['gender'] != null) { |
|
|
if (response['gender'] != null) { |
|
|
u.gender = response['gender'] as int? ?? u.gender; |
|
|
u.gender = response['gender'] as int? ?? u.gender; |
|
|
} |
|
|
} |
|
|
@ -292,8 +306,9 @@ class ProfileController extends GetxController { |
|
|
|
|
|
|
|
|
Logger().d('更新后的用户信息: ${userInfo.value.toJson()}'); |
|
|
Logger().d('更新后的用户信息: ${userInfo.value.toJson()}'); |
|
|
} else { |
|
|
} else { |
|
|
Logger().e('服务器响应为空或格式错误: $response'); |
|
|
// code != 0 时拦截器已经把服务端 msg 弹出来了,这里只回滚,不再叠一条「请重试」。 |
|
|
throw Exception('服务器响应异常'); |
|
|
Logger().e('服务器拒绝保存: $response'); |
|
|
|
|
|
throw const ProfileSaveRejected(); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger().e('同步到后端失败: $e'); |
|
|
Logger().e('同步到后端失败: $e'); |
|
|
@ -318,6 +333,7 @@ class ProfileController extends GetxController { |
|
|
final userJson = _storage.read(userKey); |
|
|
final userJson = _storage.read(userKey); |
|
|
if (userJson != null) { |
|
|
if (userJson != null) { |
|
|
userInfo.value = User.instance; |
|
|
userInfo.value = User.instance; |
|
|
|
|
|
_serverName = userInfo.value.name; |
|
|
Logger().d('从本地加载用户信息: $userJson'); |
|
|
Logger().d('从本地加载用户信息: $userJson'); |
|
|
|
|
|
|
|
|
// 重置头像准备状态 |
|
|
// 重置头像准备状态 |
|
|
@ -628,22 +644,34 @@ class ProfileController extends GetxController { |
|
|
|
|
|
|
|
|
// 更新用户名和头像(保存操作) |
|
|
// 更新用户名和头像(保存操作) |
|
|
Future<void> updateUserNameAndAvatar(String name) async { |
|
|
Future<void> updateUserNameAndAvatar(String name) async { |
|
|
if (name.trim().isEmpty) { |
|
|
final trimmed = name.trim(); |
|
|
|
|
|
if (trimmed.isEmpty) { |
|
|
Logger().e('无效名称: $name'); |
|
|
Logger().e('无效名称: $name'); |
|
|
Get.snackbar('error'.tr, 'usernameCannotBeEmpty'.tr); //用户名不能为空 |
|
|
Get.snackbar('error'.tr, 'usernameCannotBeEmpty'.tr); //用户名不能为空 |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 保存按钮拿到的是输入框文本。内联编辑没失焦时 userInfo 里还是旧昵称, |
|
|
|
|
|
// 必须以输入框为准,否则改名请求仍会带上过不了校验的默认名。 |
|
|
|
|
|
if (trimmed != userInfo.value.name) { |
|
|
|
|
|
userInfo.update((user) { |
|
|
|
|
|
if (user != null) { |
|
|
|
|
|
user.name = trimmed; |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
hasEdits.value = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 显示加载状态 |
|
|
// 显示加载状态 |
|
|
isUpdating.value = true; |
|
|
isUpdating.value = true; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
Logger().d('开始保存用户信息 - 名称: ${name.trim()}, 本地头像: ${localAvatar.value}'); |
|
|
Logger().d('开始保存用户信息 - 名称: $trimmed, 本地头像: ${localAvatar.value}'); |
|
|
|
|
|
|
|
|
// 同步到后端 |
|
|
// 同步到后端 |
|
|
await _syncWithBackend(); |
|
|
await _syncWithBackend(); |
|
|
|
|
|
|
|
|
// 同步成功后更新原始状态和本地存储 |
|
|
// 同步成功后更新原始状态和本地存储 |
|
|
|
|
|
_serverName = userInfo.value.name; |
|
|
_saveOriginalState(); |
|
|
_saveOriginalState(); |
|
|
await _syncLocalStorage(); |
|
|
await _syncLocalStorage(); |
|
|
|
|
|
|
|
|
@ -691,6 +719,9 @@ class ProfileController extends GetxController { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
} on ProfileSaveRejected { |
|
|
|
|
|
Logger().e('保存被服务端拒绝'); |
|
|
|
|
|
await _rollbackToOriginalState(); |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger().e('更新用户名和头像错误: $e'); |
|
|
Logger().e('更新用户名和头像错误: $e'); |
|
|
|
|
|
|
|
|
@ -795,3 +826,8 @@ class ProfileController extends GetxController { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 服务端以业务码拒绝了保存。拦截器已经弹出 msg,调用方只回滚、不再提示。 |
|
|
|
|
|
class ProfileSaveRejected implements Exception { |
|
|
|
|
|
const ProfileSaveRejected(); |
|
|
|
|
|
} |
|
|
|