feat: 新增用户信息更新功能

- 在APIEndpoints中新增用户信息更新端点。
- 实现UpdateUserRequest和UpdateUserResponse结构体,支持用户信息更新请求和响应。
- 在APIService中添加updateUser方法,处理用户信息更新请求。
- 更新AppSettingFeature以支持头像和昵称的修改,整合用户信息更新逻辑。
- 在AppSettingView中实现头像选择和昵称编辑功能,提升用户体验。
This commit is contained in:
edwinQQQ
2025-07-24 16:38:27 +08:00
parent c072a7e73d
commit 343fd9e2df
7 changed files with 307 additions and 88 deletions

View File

@@ -24,6 +24,7 @@ enum APIEndpoint: String, CaseIterable {
case publishFeed = "/dynamic/square/publish" //
case getUserInfo = "/user/get" //
case getMyDynamic = "/dynamic/getMyDynamic"
case updateUser = "/user/v2/update" //
// Web
case userAgreement = "/modules/rule/protocol.html"

View File

@@ -787,7 +787,7 @@ extension UserInfoManager {
debugInfoSync("🔄 开始刷新当前用户信息")
debugInfoSync(" 当前UID: \(currentUid)")
if let userInfo = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) {
if let _ = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) {
debugInfoSync("✅ 用户信息刷新成功")
return true
} else {
@@ -832,7 +832,7 @@ extension UserInfoManager {
}
//
if let cachedUserInfo = await getUserInfo() {
if let _ = await getUserInfo() {
debugInfoSync("📱 APP启动使用现有用户信息缓存")
return true
}
@@ -843,3 +843,32 @@ extension UserInfoManager {
}
}
// MARK: -
struct UpdateUserRequest: APIRequestProtocol {
typealias Response = UpdateUserResponse
let avatar: String?
let nick: String?
let uid: Int
let ticket: String
var endpoint: String { APIEndpoint.updateUser.path }
var method: HTTPMethod { .POST }
// queryParameters
var queryParameters: [String: String]? {
var params: [String: String] = [
"uid": String(uid),
"ticket": ticket
]
if let avatar = avatar { params["avatar"] = avatar }
if let nick = nick { params["nick"] = nick }
return params
}
var bodyParameters: [String: Any]? { nil }
}
struct UpdateUserResponse: Codable, Equatable {
let code: Int
let message: String
let data: UserInfo?
}

View File

@@ -222,6 +222,11 @@ struct LiveAPIService: APIServiceProtocol, Sendable {
}
}
// MARK: -
func updateUser(request: UpdateUserRequest) async throws -> UpdateUserResponse {
try await self.request(request)
}
// MARK: - Private Helper Methods
/// URL

View File

@@ -581,7 +581,7 @@ extension LoginHelper {
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedEmail = DESEncrypt.encryptUseDES(email, key: encryptionKey) else {
await debugErrorSync("❌ 邮箱DES加密失败")
debugErrorSync("❌ 邮箱DES加密失败")
return nil
}

View File

@@ -14,6 +14,14 @@ struct AppSettingFeature {
// WebView
var showUserAgreement: Bool = false
var showPrivacyPolicy: Bool = false
// /
var isUploadingAvatar: Bool = false
var avatarUploadError: String? = nil
var isEditingNickname: Bool = false
var nicknameInput: String = ""
var isUpdatingUser: Bool = false
var updateUserError: String? = nil
}
enum Action: Equatable {
@@ -35,89 +43,177 @@ struct AppSettingFeature {
// WebView
case userAgreementDismissed
case privacyPolicyDismissed
// /
case avatarTapped
case avatarSelected(Data)
case avatarUploadResult(Result<String, APIError>)
case nicknameEditConfirmed(String)
case updateUser(Result<UpdateUserResponse, APIError>)
case nicknameInputChanged(String)
case nicknameEditAlert(Bool)
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
return .send(.loadUserInfo)
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
return .send(.loadUserInfo)
case .editNicknameTapped:
//
return .none
case .logoutTapped:
//
return .run { send in
await UserInfoManager.clearAllAuthenticationData()
// FeatureMainFeature
// .noneMainFeatureAppSettingFeature.Action.logoutTapped
}
case .editNicknameTapped:
//
return .none
case .logoutTapped:
//
return .run { send in
await UserInfoManager.clearAllAuthenticationData()
// FeatureMainFeature
// .noneMainFeatureAppSettingFeature.Action.logoutTapped
case .loadUserInfo:
state.isLoadingUserInfo = true
state.userInfoError = nil
return .run { send in
do {
if let userInfo = await UserInfoManager.getUserInfo() {
await send(.userInfoResponse(.success(userInfo)))
} else {
await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在"))))
}
} catch {
let apiError: APIError
if let error = error as? APIError {
apiError = error
} else {
apiError = APIError.custom(error.localizedDescription)
}
await send(.userInfoResponse(.failure(apiError)))
}
case .loadUserInfo:
state.isLoadingUserInfo = true
state.userInfoError = nil
}
case let .userInfoResponse(.success(userInfo)):
state.userInfo = userInfo
state.nickname = userInfo.nick ?? ""
state.avatarURL = userInfo.avatar
state.isLoadingUserInfo = false
return .none
case let .userInfoResponse(.failure(error)):
state.userInfoError = error.localizedDescription
state.isLoadingUserInfo = false
return .none
case .personalInfoPermissionsTapped:
state.showPrivacyPolicy = true
return .none
case .helpTapped:
state.showUserAgreement = true
return .none
case .clearCacheTapped:
//
return .none
case .checkUpdatesTapped:
//
return .none
case .aboutUsTapped:
//
return .none
case .userAgreementDismissed:
state.showUserAgreement = false
return .none
case .privacyPolicyDismissed:
state.showPrivacyPolicy = false
return .none
case .avatarTapped:
//
return .none
case let .avatarSelected(imageData):
state.isUploadingAvatar = true
state.avatarUploadError = nil
return .run { [avatarData = imageData] send in
guard let uiImage = UIImage(data: avatarData) else {
await send(.avatarUploadResult(.failure(APIError.custom("图片格式错误"))))
return
}
//
if let url = await COSManager.shared.uploadUIImage(uiImage, apiService: apiService) {
await send(.avatarUploadResult(.success(url)))
} else {
await send(.avatarUploadResult(.failure(APIError.custom("头像上传失败"))))
}
}
case let .avatarUploadResult(.success(url)):
state.isUploadingAvatar = false
// updateUser API avatar
state.isUpdatingUser = true
state.updateUserError = nil
guard let userInfo = state.userInfo else { return .none }
return .run { send in
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
let req = UpdateUserRequest(avatar: url, nick: nil, uid: userInfo.uid ?? 0, ticket: ticket)
do {
let resp: UpdateUserResponse = try await apiService.request(req)
await send(.updateUser(.success(resp)))
} catch {
let apiError = error as? APIError ?? APIError.custom(error.localizedDescription)
await send(.updateUser(.failure(apiError)))
}
}
case let .avatarUploadResult(.failure(error)):
state.isUploadingAvatar = false
state.avatarUploadError = error.localizedDescription
return .none
case .nicknameEditAlert(let show):
state.isEditingNickname = show
state.nicknameInput = state.nickname
return .none
case .nicknameInputChanged(let text):
state.nicknameInput = String(text.prefix(15))
return .none
case .nicknameEditConfirmed(let newNick):
guard let userInfo = state.userInfo else { return .none }
state.isUpdatingUser = true
state.updateUserError = nil
return .run { send in
let ticket = await UserInfoManager.getCurrentUserTicket() ?? ""
let req = UpdateUserRequest(avatar: nil, nick: newNick, uid: userInfo.uid ?? 0, ticket: ticket)
do {
let resp: UpdateUserResponse = try await apiService.request(req)
await send(.updateUser(.success(resp)))
} catch {
let apiError = error as? APIError ?? APIError.custom(error.localizedDescription)
await send(.updateUser(.failure(apiError)))
}
}
case let .updateUser(.success(resp)):
state.isUpdatingUser = false
// resp.data userinfo1
if let uid = state.userInfo?.uid {
return .run { send in
do {
if let userInfo = await UserInfoManager.getUserInfo() {
await send(.userInfoResponse(.success(userInfo)))
} else {
await send(.userInfoResponse(.failure(APIError.custom("用户信息不存在"))))
}
} catch {
let apiError: APIError
if let error = error as? APIError {
apiError = error
} else {
apiError = APIError.custom(error.localizedDescription)
}
await send(.userInfoResponse(.failure(apiError)))
try? await Task.sleep(nanoseconds: 1_000_000_000)
if let newUser = await UserInfoManager.fetchUserInfoFromServer(uid: String(uid), apiService: apiService) {
await send(.userInfoResponse(.success(newUser)))
} else {
await send(.userInfoResponse(.failure(APIError.custom("获取最新用户信息失败"))))
}
}
case let .userInfoResponse(.success(userInfo)):
state.userInfo = userInfo
state.nickname = userInfo.nick ?? ""
state.avatarURL = userInfo.avatar
state.isLoadingUserInfo = false
return .none
case let .userInfoResponse(.failure(error)):
state.userInfoError = error.localizedDescription
state.isLoadingUserInfo = false
return .none
case .personalInfoPermissionsTapped:
state.showPrivacyPolicy = true
return .none
case .helpTapped:
state.showUserAgreement = true
return .none
case .clearCacheTapped:
//
return .none
case .checkUpdatesTapped:
//
return .none
case .aboutUsTapped:
//
return .none
case .userAgreementDismissed:
state.showUserAgreement = false
return .none
case .privacyPolicyDismissed:
state.showPrivacyPolicy = false
return .none
}
state.isEditingNickname = false
return .none
case let .updateUser(.failure(error)):
state.isUpdatingUser = false
state.updateUserError = error.localizedDescription
return .none
}
}
}
}

View File

@@ -2,11 +2,13 @@ import Foundation
import ComposableArchitecture
import CasePaths
struct MainFeature: Reducer {
@Reducer
struct MainFeature {
enum Tab: Int, Equatable, CaseIterable {
case feed, other
}
@ObservableState
struct State: Equatable {
var selectedTab: Tab = .feed
var feedList: FeedListFeature.State = .init()
@@ -42,10 +44,10 @@ struct MainFeature: Reducer {
}
var body: some ReducerOf<Self> {
Scope(state: \.feedList, action: \.feedList) {
Scope(state: \ .feedList, action: \ .feedList) {
FeedListFeature()
}
Scope(state: \.me, action: \.me) {
Scope(state: \ .me, action: \ .me) {
MeFeature()
}
Reduce { state, action in
@@ -104,7 +106,7 @@ struct MainFeature: Reducer {
}
}
//
.ifLet(\ .appSettingState, action: \.appSettingAction) {
.ifLet(\ .appSettingState, action: \ .appSettingAction) {
AppSettingFeature()
}
}

View File

@@ -1,31 +1,51 @@
import SwiftUI
import ComposableArchitecture
import PhotosUI
struct AppSettingView: View {
let store: StoreOf<AppSettingFeature>
@State private var showPhotoPicker = false
@State private var selectedPhotoItem: PhotosPickerItem? = nil
@State private var showNicknameAlert = false
@State private var nicknameInput: String = ""
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
ZStack {
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
// VStack(spacing: 0) {
VStack(spacing: 0) {
//
VStack(spacing: 0) {
//
avatarSection(viewStore: viewStore)
//
nicknameSection(viewStore: viewStore)
//
settingsSection(viewStore: viewStore)
Spacer()
//
logoutButton(viewStore: viewStore)
}
// }
}
// Loading &
if viewStore.isUploadingAvatar || viewStore.isUpdatingUser {
Color.black.opacity(0.3).ignoresSafeArea()
ProgressView("正在上传/更新...")
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.foregroundColor(.white)
.scaleEffect(1.2)
}
if let error = viewStore.avatarUploadError ?? viewStore.updateUserError {
VStack {
Spacer()
Text(error)
.foregroundColor(.red)
.padding()
.background(Color.white.opacity(0.9))
.cornerRadius(10)
Spacer()
}
}
}
.navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit"))
.navigationBarTitleDisplayMode(.inline)
@@ -51,6 +71,35 @@ struct AppSettingView: View {
isPresented: privacyPolicyBinding(viewStore: viewStore),
url: APIConfiguration.webURL(for: .privacyPolicy)
)
//
.photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared())
.onChange(of: selectedPhotoItem) { newItem in
if let item = newItem {
loadAndProcessImage(item: item) { data in
if let data = data {
viewStore.send(.avatarSelected(data))
}
}
}
}
// Alert
.alert("修改昵称", isPresented: $showNicknameAlert, actions: {
TextField("请输入昵称", text: $nicknameInput)
.onChange(of: nicknameInput) { newValue in
if newValue.count > 15 {
nicknameInput = String(newValue.prefix(15))
}
}
Button("确定") {
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty && trimmed != viewStore.nickname {
viewStore.send(.nicknameEditConfirmed(trimmed))
}
}
Button("取消", role: .cancel) {}
}, message: {
Text("昵称最长15个字符")
})
}
}
@@ -60,7 +109,13 @@ struct AppSettingView: View {
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack(alignment: .bottomTrailing) {
avatarImageView(viewStore: viewStore)
.onTapGesture {
showPhotoPicker = true
}
cameraButton
.onTapGesture {
showPhotoPicker = true
}
}
.padding(.top, 24)
}
@@ -141,7 +196,8 @@ struct AppSettingView: View {
.padding(.horizontal, 32)
.padding(.vertical, 18)
.onTapGesture {
viewStore.send(.editNicknameTapped)
nicknameInput = viewStore.nickname
showNicknameAlert = true
}
Divider().background(Color.gray.opacity(0.3))
@@ -255,4 +311,34 @@ struct AppSettingView: View {
send: AppSettingFeature.Action.privacyPolicyDismissed
)
}
// MARK: -
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping (Data?) -> Void) {
item.loadTransferable(type: Data.self) { result in
guard let data = try? result.get(), let uiImage = UIImage(data: data) else {
completion(nil)
return
}
let square = cropToSquare(image: uiImage)
let resized = resizeImage(image: square, targetSize: CGSize(width: 180, height: 180))
let jpegData = resized.jpegData(compressionQuality: 0.8)
completion(jpegData)
}
}
}
// MARK: -
private func cropToSquare(image: UIImage) -> UIImage {
let size = min(image.size.width, image.size.height)
let x = (image.size.width - size) / 2
let y = (image.size.height - size) / 2
let cropRect = CGRect(x: x, y: y, width: size, height: size)
guard let cgImage = image.cgImage?.cropping(to: cropRect) else { return image }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
}