fix: 修正账号姓名展示与资料缓存

This commit is contained in:
2026-07-31 15:19:22 +08:00
parent f863004c8d
commit 0cf0a41a60
10 changed files with 102 additions and 37 deletions

View File

@ -70,10 +70,10 @@ enum AuthSessionHelper {
AppStore.shared.session.userId = String(user.businessUserId) AppStore.shared.session.userId = String(user.businessUserId)
AppStore.shared.session.accountType = .scenicUser AppStore.shared.session.accountType = .scenicUser
AppStore.shared.session.accountDisplayName = firstNonEmpty( 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.userName = firstNonEmpty(user.nickname, user.realName)
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.nickname, user.username) AppStore.shared.session.realName = user.realName
AppStore.shared.session.phone = user.phone AppStore.shared.session.phone = user.phone
AppStore.shared.session.currentScenicId = user.scenicId AppStore.shared.session.currentScenicId = user.scenicId
AppStore.shared.session.currentScenicName = user.scenicName AppStore.shared.session.currentScenicName = user.scenicName
@ -90,10 +90,10 @@ enum AuthSessionHelper {
AppStore.shared.session.userId = String(user.businessUserId) AppStore.shared.session.userId = String(user.businessUserId)
AppStore.shared.session.accountType = .storeUser AppStore.shared.session.accountType = .storeUser
AppStore.shared.session.accountDisplayName = firstNonEmpty( 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.userName = user.realName
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.userName, user.username) AppStore.shared.session.realName = user.realName
AppStore.shared.session.phone = user.phone AppStore.shared.session.phone = user.phone
AppStore.shared.session.avatar = user.avatar AppStore.shared.session.avatar = user.avatar
AppStore.shared.session.currentScenicId = user.scenicId AppStore.shared.session.currentScenicId = user.scenicId

View File

@ -196,7 +196,6 @@ struct V9ScenicUser: Decodable, Equatable {
let userId: Int let userId: Int
let scenicUserId: Int let scenicUserId: Int
let ssUserId: Int let ssUserId: Int
let username: String
let realName: String let realName: String
let nickname: String let nickname: String
let phone: String let phone: String
@ -212,7 +211,7 @@ struct V9ScenicUser: Decodable, Equatable {
} }
var displayName: String { var displayName: String {
scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? username scenicName.nonEmpty ?? nickname.nonEmpty ?? realName
} }
func toAccountSwitchAccount() -> AccountSwitchAccount { func toAccountSwitchAccount() -> AccountSwitchAccount {
@ -238,8 +237,6 @@ struct V9ScenicUser: Decodable, Equatable {
case userId = "user_id" case userId = "user_id"
case scenicUserId = "scenic_user_id" case scenicUserId = "scenic_user_id"
case ssUserId = "ss_user_id" case ssUserId = "ss_user_id"
case username = "user_name"
case legacyUsername = "username"
case realName = "real_name" case realName = "real_name"
case nickname case nickname
case phone case phone
@ -258,9 +255,6 @@ struct V9ScenicUser: Decodable, Equatable {
userId = try container.decodeLossyInt(forKey: .userId) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0
scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0 scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0
ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 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) realName = try container.decodeLossyString(forKey: .realName)
nickname = try container.decodeLossyString(forKey: .nickname) nickname = try container.decodeLossyString(forKey: .nickname)
phone = try container.decodeLossyString(forKey: .phone) phone = try container.decodeLossyString(forKey: .phone)
@ -281,8 +275,6 @@ struct V9StoreUser: Decodable, Equatable {
let id: Int let id: Int
let userId: Int let userId: Int
let storeUserId: Int let storeUserId: Int
let username: String
let userName: String
let realName: String let realName: String
let phone: String let phone: String
let avatar: String let avatar: String
@ -300,14 +292,14 @@ struct V9StoreUser: Decodable, Equatable {
} }
var displayName: String { var displayName: String {
storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? username storeName.nonEmpty ?? scenicName.nonEmpty ?? realName
} }
func toAccountSwitchAccount() -> AccountSwitchAccount { func toAccountSwitchAccount() -> AccountSwitchAccount {
AccountSwitchAccount( AccountSwitchAccount(
accountType: accountType.nonEmpty ?? Self.accountTypeValue, accountType: accountType.nonEmpty ?? Self.accountTypeValue,
businessUserId: businessUserId, businessUserId: businessUserId,
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号", title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? "门店账号",
subtitle: joinAccountSubtitle(scenicName, roleName), subtitle: joinAccountSubtitle(scenicName, roleName),
phone: phone, phone: phone,
realName: realName, realName: realName,
@ -325,8 +317,6 @@ struct V9StoreUser: Decodable, Equatable {
case id case id
case userId = "user_id" case userId = "user_id"
case storeUserId = "store_user_id" case storeUserId = "store_user_id"
case username
case userName = "user_name"
case realName = "real_name" case realName = "real_name"
case phone case phone
case avatar case avatar
@ -346,8 +336,6 @@ struct V9StoreUser: Decodable, Equatable {
id = try container.decodeLossyInt(forKey: .id) ?? 0 id = try container.decodeLossyInt(forKey: .id) ?? 0
userId = try container.decodeLossyInt(forKey: .userId) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0
storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 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) realName = try container.decodeLossyString(forKey: .realName)
phone = try container.decodeLossyString(forKey: .phone) phone = try container.decodeLossyString(forKey: .phone)
avatar = try container.decodeLossyString(forKey: .avatar) avatar = try container.decodeLossyString(forKey: .avatar)

View File

@ -69,9 +69,9 @@ enum PayPageBrandingPolicy {
/// ///
enum PaymentAccountDisplayFormatter { enum PaymentAccountDisplayFormatter {
/// 使 ///
static func photographerName(userName: String, realName: String) -> String { static func photographerName(realName: String) -> String {
userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-" realName.trimmedNonEmpty ?? "-"
} }
/// ///

View File

@ -66,7 +66,6 @@ final class PaymentCollectionDetailsViewModel {
var displayPhotographerName: String { var displayPhotographerName: String {
PaymentAccountDisplayFormatter.photographerName( PaymentAccountDisplayFormatter.photographerName(
userName: appStore.session.userName,
realName: appStore.session.realName realName: appStore.session.realName
) )
} }

View File

@ -17,11 +17,11 @@ final class ProfileViewModel {
var onStateChange: (() -> Void)? var onStateChange: (() -> Void)?
var displayNickname: String { var displayNickname: String {
nonEmpty(userInfo?.nickname) ?? "未设置昵称" nonEmpty(userInfo?.nickname) ?? AppStore.shared.session.userName.nonEmpty ?? "未设置昵称"
} }
var displayRealName: String { var displayRealName: String {
nonEmpty(userInfo?.realName) ?? "--" nonEmpty(userInfo?.realName) ?? AppStore.shared.session.realName.nonEmpty ?? "--"
} }
var displayPhone: String { var displayPhone: String {
@ -37,6 +37,17 @@ final class ProfileViewModel {
return uid.isEmpty ? "--" : uid 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 { var accountDisplayName: String {
let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines) let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "--" : name return name.isEmpty ? "--" : name
@ -112,6 +123,7 @@ final class ProfileViewModel {
if !info.roleName.isEmpty { if !info.roleName.isEmpty {
AppStore.shared.session.roleName = info.roleName AppStore.shared.session.roleName = info.roleName
} }
notifyStateChange()
if showPhotographerFields { if showPhotographerFields {
async let realName = api.realNameInfo() async let realName = api.realNameInfo()

View File

@ -38,6 +38,11 @@ final class ProfileViewController: BaseViewController {
title = "我的" title = "我的"
} }
override func viewDidLoad() {
super.viewDidLoad()
applyViewModel()
}
override func setupUI() { override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF7FAFF) view.backgroundColor = UIColor(hex: 0xF7FAFF)
@ -104,12 +109,10 @@ final class ProfileViewController: BaseViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
let showLoading = !hasLoadedProfileOnce let showLoading = !hasLoadedProfileOnce && !viewModel.hasCachedBasicInfo
Task { Task {
await reloadProfile(showGlobalLoading: showLoading) await reloadProfile(showGlobalLoading: showLoading)
if showLoading { hasLoadedProfileOnce = true
hasLoadedProfileOnce = true
}
} }
} }

View File

@ -62,6 +62,7 @@ final class AuthModelsTests: XCTestCase {
XCTAssertEqual(response.token, "person-temp-token") XCTAssertEqual(response.token, "person-temp-token")
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201]) XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"]) XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"])
} }
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws { func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
@ -137,4 +138,39 @@ final class AuthModelsTests: XCTestCase {
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三") XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201)) 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, "门店真实姓名")
}
} }

View File

@ -99,8 +99,9 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
XCTAssertTrue(viewModel.isVoiceBroadcastOpen) XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
} }
func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() { func testNalatiAccountInfoUsesPhotographerRealNameAndAllPermissionStores() {
appStore.session.userName = "" appStore.session.userName = ""
appStore.session.realName = ""
appStore.permissions.saveRolePermissionList([ appStore.permissions.saveRolePermissionList([
RolePermissionResponse( RolePermissionResponse(
role: RoleInfo(id: 41, name: "", roleCode: "photographer"), role: RoleInfo(id: 41, name: "", roleCode: "photographer"),
@ -113,10 +114,19 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore) let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
XCTAssertEqual(viewModel.displayPhotographerName, "") XCTAssertEqual(viewModel.displayPhotographerName, "")
XCTAssertEqual(viewModel.displayStoreNames, "") XCTAssertEqual(viewModel.displayStoreNames, "")
} }
func testPhotographerNameDoesNotFallBackToNicknameWhenRealNameMissing() {
appStore.session.userName = ""
appStore.session.realName = " "
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
XCTAssertEqual(viewModel.displayPhotographerName, "-")
}
func testLoadPayCodeClearsQRWhenScenicMissing() async { func testLoadPayCodeClearsQRWhenScenicMissing() async {
appStore.session.currentScenicId = 0 appStore.session.currentScenicId = 0
let session = MockURLSession(responses: []) let session = MockURLSession(responses: [])

View File

@ -55,11 +55,12 @@ final class PaymentModelsTests: XCTestCase {
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0)) XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
} }
func testPaymentAccountDisplayFormatterUsesCurrentPhotographerAndAllUniqueStores() { func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() {
XCTAssertEqual( XCTAssertEqual(
PaymentAccountDisplayFormatter.photographerName(userName: " 当前摄影师 ", realName: "实名"), PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "),
"当前摄影师" "摄影师真实姓名"
) )
XCTAssertEqual(PaymentAccountDisplayFormatter.photographerName(realName: " "), "-")
XCTAssertEqual( XCTAssertEqual(
PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]), PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]),
"一号店、二号店" "一号店、二号店"

View File

@ -26,6 +26,22 @@ final class ProfileViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.displayNickname, "未设置昵称") 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() { func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
let viewModel = ProfileViewModel() let viewModel = ProfileViewModel()