From 0cf0a41a60dd01d0eb420ab3be86004a40293ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Fri, 31 Jul 2026 15:19:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=A7=93=E5=90=8D=E5=B1=95=E7=A4=BA=E4=B8=8E=E8=B5=84=E6=96=99?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Auth/AuthSessionHelper.swift | 12 +++---- .../Features/Auth/Models/AuthModels.swift | 18 ++-------- .../Payment/Models/PaymentModels.swift | 6 ++-- .../PaymentCollectionDetailsViewModel.swift | 1 - .../Profile/ViewModels/ProfileViewModel.swift | 16 +++++++-- .../UI/Profile/ProfileViewController.swift | 11 +++--- suixinkanTests/AuthModelsTests.swift | 36 +++++++++++++++++++ ...ymentCollectionDetailsViewModelTests.swift | 16 +++++++-- suixinkanTests/PaymentModelsTests.swift | 7 ++-- suixinkanTests/ProfileViewModelTests.swift | 16 +++++++++ 10 files changed, 102 insertions(+), 37 deletions(-) diff --git a/suixinkan/Features/Auth/AuthSessionHelper.swift b/suixinkan/Features/Auth/AuthSessionHelper.swift index 7d28596..9ea6e3b 100644 --- a/suixinkan/Features/Auth/AuthSessionHelper.swift +++ b/suixinkan/Features/Auth/AuthSessionHelper.swift @@ -70,10 +70,10 @@ enum AuthSessionHelper { AppStore.shared.session.userId = String(user.businessUserId) AppStore.shared.session.accountType = .scenicUser AppStore.shared.session.accountDisplayName = firstNonEmpty( - user.scenicName, user.nickname, user.realName, user.username + user.scenicName, user.nickname, user.realName ) - AppStore.shared.session.userName = firstNonEmpty(user.nickname, user.username, user.realName) - AppStore.shared.session.realName = firstNonEmpty(user.realName, user.nickname, user.username) + AppStore.shared.session.userName = firstNonEmpty(user.nickname, user.realName) + AppStore.shared.session.realName = user.realName AppStore.shared.session.phone = user.phone AppStore.shared.session.currentScenicId = user.scenicId AppStore.shared.session.currentScenicName = user.scenicName @@ -90,10 +90,10 @@ enum AuthSessionHelper { AppStore.shared.session.userId = String(user.businessUserId) AppStore.shared.session.accountType = .storeUser AppStore.shared.session.accountDisplayName = firstNonEmpty( - user.storeName, user.scenicName, user.realName, user.userName, user.username + user.storeName, user.scenicName, user.realName ) - AppStore.shared.session.userName = firstNonEmpty(user.userName, user.username, user.realName) - AppStore.shared.session.realName = firstNonEmpty(user.realName, user.userName, user.username) + AppStore.shared.session.userName = user.realName + AppStore.shared.session.realName = user.realName AppStore.shared.session.phone = user.phone AppStore.shared.session.avatar = user.avatar AppStore.shared.session.currentScenicId = user.scenicId diff --git a/suixinkan/Features/Auth/Models/AuthModels.swift b/suixinkan/Features/Auth/Models/AuthModels.swift index 918a359..8341e16 100644 --- a/suixinkan/Features/Auth/Models/AuthModels.swift +++ b/suixinkan/Features/Auth/Models/AuthModels.swift @@ -196,7 +196,6 @@ struct V9ScenicUser: Decodable, Equatable { let userId: Int let scenicUserId: Int let ssUserId: Int - let username: String let realName: String let nickname: String let phone: String @@ -212,7 +211,7 @@ struct V9ScenicUser: Decodable, Equatable { } var displayName: String { - scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? username + scenicName.nonEmpty ?? nickname.nonEmpty ?? realName } func toAccountSwitchAccount() -> AccountSwitchAccount { @@ -238,8 +237,6 @@ struct V9ScenicUser: Decodable, Equatable { case userId = "user_id" case scenicUserId = "scenic_user_id" case ssUserId = "ss_user_id" - case username = "user_name" - case legacyUsername = "username" case realName = "real_name" case nickname case phone @@ -258,9 +255,6 @@ struct V9ScenicUser: Decodable, Equatable { userId = try container.decodeLossyInt(forKey: .userId) ?? 0 scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0 ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 0 - let preferredUsername = try container.decodeLossyString(forKey: .username) - let legacyUsername = try container.decodeLossyString(forKey: .legacyUsername) - username = preferredUsername.nonEmpty ?? legacyUsername realName = try container.decodeLossyString(forKey: .realName) nickname = try container.decodeLossyString(forKey: .nickname) phone = try container.decodeLossyString(forKey: .phone) @@ -281,8 +275,6 @@ struct V9StoreUser: Decodable, Equatable { let id: Int let userId: Int let storeUserId: Int - let username: String - let userName: String let realName: String let phone: String let avatar: String @@ -300,14 +292,14 @@ struct V9StoreUser: Decodable, Equatable { } var displayName: String { - storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? username + storeName.nonEmpty ?? scenicName.nonEmpty ?? realName } func toAccountSwitchAccount() -> AccountSwitchAccount { AccountSwitchAccount( accountType: accountType.nonEmpty ?? Self.accountTypeValue, businessUserId: businessUserId, - title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号", + title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? "门店账号", subtitle: joinAccountSubtitle(scenicName, roleName), phone: phone, realName: realName, @@ -325,8 +317,6 @@ struct V9StoreUser: Decodable, Equatable { case id case userId = "user_id" case storeUserId = "store_user_id" - case username - case userName = "user_name" case realName = "real_name" case phone case avatar @@ -346,8 +336,6 @@ struct V9StoreUser: Decodable, Equatable { id = try container.decodeLossyInt(forKey: .id) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0 storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 0 - username = try container.decodeLossyString(forKey: .username) - userName = try container.decodeLossyString(forKey: .userName) realName = try container.decodeLossyString(forKey: .realName) phone = try container.decodeLossyString(forKey: .phone) avatar = try container.decodeLossyString(forKey: .avatar) diff --git a/suixinkan/Features/Payment/Models/PaymentModels.swift b/suixinkan/Features/Payment/Models/PaymentModels.swift index 7234348..6fc2b1a 100644 --- a/suixinkan/Features/Payment/Models/PaymentModels.swift +++ b/suixinkan/Features/Payment/Models/PaymentModels.swift @@ -69,9 +69,9 @@ enum PayPageBrandingPolicy { /// 收款页当前账号信息的展示文本格式化规则。 enum PaymentAccountDisplayFormatter { - /// 解析当前登录摄影师名称,昵称为空时使用实名,仍为空时显示占位符。 - static func photographerName(userName: String, realName: String) -> String { - userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-" + /// 解析当前登录摄影师的真实姓名,空值显示占位符。 + static func photographerName(realName: String) -> String { + realName.trimmedNonEmpty ?? "-" } /// 去除空值与重复项后,合并当前账号的全部店铺名称。 diff --git a/suixinkan/Features/Payment/ViewModels/PaymentCollectionDetailsViewModel.swift b/suixinkan/Features/Payment/ViewModels/PaymentCollectionDetailsViewModel.swift index 439e265..26fec58 100644 --- a/suixinkan/Features/Payment/ViewModels/PaymentCollectionDetailsViewModel.swift +++ b/suixinkan/Features/Payment/ViewModels/PaymentCollectionDetailsViewModel.swift @@ -66,7 +66,6 @@ final class PaymentCollectionDetailsViewModel { var displayPhotographerName: String { PaymentAccountDisplayFormatter.photographerName( - userName: appStore.session.userName, realName: appStore.session.realName ) } diff --git a/suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift b/suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift index b44fcac..951f08f 100644 --- a/suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift +++ b/suixinkan/Features/Profile/ViewModels/ProfileViewModel.swift @@ -17,11 +17,11 @@ final class ProfileViewModel { var onStateChange: (() -> Void)? var displayNickname: String { - nonEmpty(userInfo?.nickname) ?? "未设置昵称" + nonEmpty(userInfo?.nickname) ?? AppStore.shared.session.userName.nonEmpty ?? "未设置昵称" } var displayRealName: String { - nonEmpty(userInfo?.realName) ?? "--" + nonEmpty(userInfo?.realName) ?? AppStore.shared.session.realName.nonEmpty ?? "--" } var displayPhone: String { @@ -37,6 +37,17 @@ final class ProfileViewModel { return uid.isEmpty ? "--" : uid } + /// 本地是否已有可供冷启动首屏展示的基本资料。 + var hasCachedBasicInfo: Bool { + [ + AppStore.shared.session.userName, + AppStore.shared.session.realName, + AppStore.shared.session.phone, + AppStore.shared.session.avatar, + AppStore.shared.session.userId, + ].contains { nonEmpty($0) != nil } + } + var accountDisplayName: String { let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines) return name.isEmpty ? "--" : name @@ -112,6 +123,7 @@ final class ProfileViewModel { if !info.roleName.isEmpty { AppStore.shared.session.roleName = info.roleName } + notifyStateChange() if showPhotographerFields { async let realName = api.realNameInfo() diff --git a/suixinkan/UI/Profile/ProfileViewController.swift b/suixinkan/UI/Profile/ProfileViewController.swift index 8c12209..af535dc 100644 --- a/suixinkan/UI/Profile/ProfileViewController.swift +++ b/suixinkan/UI/Profile/ProfileViewController.swift @@ -38,6 +38,11 @@ final class ProfileViewController: BaseViewController { title = "我的" } + override func viewDidLoad() { + super.viewDidLoad() + applyViewModel() + } + override func setupUI() { view.backgroundColor = UIColor(hex: 0xF7FAFF) @@ -104,12 +109,10 @@ final class ProfileViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - let showLoading = !hasLoadedProfileOnce + let showLoading = !hasLoadedProfileOnce && !viewModel.hasCachedBasicInfo Task { await reloadProfile(showGlobalLoading: showLoading) - if showLoading { - hasLoadedProfileOnce = true - } + hasLoadedProfileOnce = true } } diff --git a/suixinkanTests/AuthModelsTests.swift b/suixinkanTests/AuthModelsTests.swift index 1d9105d..8d0f840 100644 --- a/suixinkanTests/AuthModelsTests.swift +++ b/suixinkanTests/AuthModelsTests.swift @@ -62,6 +62,7 @@ final class AuthModelsTests: XCTestCase { XCTAssertEqual(response.token, "person-temp-token") XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201]) XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"]) + XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"]) } func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws { @@ -137,4 +138,39 @@ final class AuthModelsTests: XCTestCase { XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三") XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201)) } + + func testV9AccountModelsIgnoreBackendIdentifiersAndUseRealNameFallback() throws { + let json = """ + { + "token": "person-temp-token", + "scenic_users": [ + { + "account_type": "scenic_user", + "ss_user_id": 101, + "username": "backend_scenic_account", + "user_name": "backend_scenic_unique_id", + "real_name": "景区真实姓名" + } + ], + "store_users": [ + { + "account_type": "store_user", + "store_user_id": 201, + "username": "backend_store_account", + "user_name": "backend_store_unique_id", + "real_name": "门店真实姓名" + } + ] + } + """.data(using: .utf8)! + + let response = try JSONDecoder().decode(V9AuthResponse.self, from: json) + + XCTAssertEqual(response.scenicUsers.first?.realName, "景区真实姓名") + XCTAssertEqual(response.scenicUsers.first?.displayName, "景区真实姓名") + XCTAssertEqual(response.scenicUsers.first?.toAccountSwitchAccount().title, "景区真实姓名") + XCTAssertEqual(response.storeUsers.first?.realName, "门店真实姓名") + XCTAssertEqual(response.storeUsers.first?.displayName, "门店真实姓名") + XCTAssertEqual(response.storeUsers.first?.toAccountSwitchAccount().title, "门店真实姓名") + } } diff --git a/suixinkanTests/PaymentCollectionDetailsViewModelTests.swift b/suixinkanTests/PaymentCollectionDetailsViewModelTests.swift index 4bfa64b..1ee75c5 100644 --- a/suixinkanTests/PaymentCollectionDetailsViewModelTests.swift +++ b/suixinkanTests/PaymentCollectionDetailsViewModelTests.swift @@ -99,8 +99,9 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase { XCTAssertTrue(viewModel.isVoiceBroadcastOpen) } - func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() { - appStore.session.userName = "摄影师小王" + func testNalatiAccountInfoUsesPhotographerRealNameAndAllPermissionStores() { + appStore.session.userName = "摄影师昵称" + appStore.session.realName = "摄影师真实姓名" appStore.permissions.saveRolePermissionList([ RolePermissionResponse( role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"), @@ -113,10 +114,19 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase { let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) - XCTAssertEqual(viewModel.displayPhotographerName, "摄影师小王") + XCTAssertEqual(viewModel.displayPhotographerName, "摄影师真实姓名") XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店") } + func testPhotographerNameDoesNotFallBackToNicknameWhenRealNameMissing() { + appStore.session.userName = "摄影师昵称" + appStore.session.realName = " " + + let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) + + XCTAssertEqual(viewModel.displayPhotographerName, "-") + } + func testLoadPayCodeClearsQRWhenScenicMissing() async { appStore.session.currentScenicId = 0 let session = MockURLSession(responses: []) diff --git a/suixinkanTests/PaymentModelsTests.swift b/suixinkanTests/PaymentModelsTests.swift index c120bb9..60f54e7 100644 --- a/suixinkanTests/PaymentModelsTests.swift +++ b/suixinkanTests/PaymentModelsTests.swift @@ -55,11 +55,12 @@ final class PaymentModelsTests: XCTestCase { XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0)) } - func testPaymentAccountDisplayFormatterUsesCurrentPhotographerAndAllUniqueStores() { + func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() { XCTAssertEqual( - PaymentAccountDisplayFormatter.photographerName(userName: " 当前摄影师 ", realName: "实名"), - "当前摄影师" + PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "), + "摄影师真实姓名" ) + XCTAssertEqual(PaymentAccountDisplayFormatter.photographerName(realName: " "), "-") XCTAssertEqual( PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]), "一号店、二号店" diff --git a/suixinkanTests/ProfileViewModelTests.swift b/suixinkanTests/ProfileViewModelTests.swift index 0b3225c..bce3086 100644 --- a/suixinkanTests/ProfileViewModelTests.swift +++ b/suixinkanTests/ProfileViewModelTests.swift @@ -26,6 +26,22 @@ final class ProfileViewModelTests: XCTestCase { XCTAssertEqual(viewModel.displayNickname, "未设置昵称") } + func testColdStartBasicInfoUsesPersistedSessionCache() { + AppStore.shared.session.userName = "缓存昵称" + AppStore.shared.session.realName = "缓存真实姓名" + AppStore.shared.session.phone = "186****7230" + AppStore.shared.session.avatar = "https://cdn.example.com/cached-avatar.jpg" + + let viewModel = ProfileViewModel() + + XCTAssertTrue(viewModel.hasCachedBasicInfo) + XCTAssertEqual(viewModel.displayNickname, "缓存昵称") + XCTAssertEqual(viewModel.displayRealName, "缓存真实姓名") + XCTAssertEqual(viewModel.displayPhone, "186****7230") + XCTAssertEqual(viewModel.displayAvatarURL, "https://cdn.example.com/cached-avatar.jpg") + XCTAssertEqual(viewModel.displayUID, "profile-user") + } + func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() { AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue let viewModel = ProfileViewModel()