模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
57
suixinkan/DataStore/AppLocationStore.swift
Normal file
57
suixinkan/DataStore/AppLocationStore.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// AppLocationStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 管理当前账号的位置上报会话与提醒偏好。
|
||||
final class AppLocationStore {
|
||||
|
||||
private enum Key: String, CaseIterable {
|
||||
case onlineStatus = "key_online_status"
|
||||
case lastReportTime = "key_last_location_report_time"
|
||||
case reminderMinutes = "key_location_reminder_minutes"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private let session: AppSessionStore
|
||||
|
||||
/// 使用指定持久化容器和会话创建位置存储。
|
||||
init(defaults: UserDefaults, session: AppSessionStore) {
|
||||
self.defaults = defaults
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// 是否处于位置上报在线状态。
|
||||
var onlineStatus: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.onlineStatus)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.onlineStatus)) }
|
||||
}
|
||||
|
||||
/// 上次位置上报时间戳,单位为毫秒。
|
||||
var lastReportTime: Int64 {
|
||||
get { Int64(defaults.integer(forKey: scopedKey(.lastReportTime))) }
|
||||
set { defaults.set(Int(newValue), forKey: scopedKey(.lastReportTime)) }
|
||||
}
|
||||
|
||||
/// 位置超时提前提醒分钟数,0 表示不提醒。
|
||||
var reminderMinutes: Int {
|
||||
get { defaults.integer(forKey: scopedKey(.reminderMinutes)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.reminderMinutes)) }
|
||||
}
|
||||
|
||||
/// 清除上次位置上报时间。
|
||||
func clearLastReportTime() {
|
||||
defaults.removeObject(forKey: scopedKey(.lastReportTime))
|
||||
}
|
||||
|
||||
/// 清除当前账号的位置上报状态和偏好。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
}
|
||||
}
|
||||
38
suixinkan/DataStore/AppPaymentStore.swift
Normal file
38
suixinkan/DataStore/AppPaymentStore.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// AppPaymentStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 管理当前账号的收款功能偏好。
|
||||
final class AppPaymentStore {
|
||||
|
||||
private enum Key: String, CaseIterable {
|
||||
case isOpenReceiveVoice = "key_is_open_receive_voice"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private let session: AppSessionStore
|
||||
|
||||
/// 使用指定持久化容器和会话创建收款偏好存储。
|
||||
init(defaults: UserDefaults, session: AppSessionStore) {
|
||||
self.defaults = defaults
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// 收款到账是否开启语音播报。
|
||||
var isOpenReceiveVoice: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.isOpenReceiveVoice)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.isOpenReceiveVoice)) }
|
||||
}
|
||||
|
||||
/// 清除当前账号的收款偏好。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
}
|
||||
}
|
||||
91
suixinkan/DataStore/AppPermissionStore.swift
Normal file
91
suixinkan/DataStore/AppPermissionStore.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// AppPermissionStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 管理当前账号的角色权限、首页权限与可选景区缓存。
|
||||
final class AppPermissionStore {
|
||||
|
||||
private enum Key: String, CaseIterable {
|
||||
case legacyRoleId = "key_in_role_id"
|
||||
case rolePermissionList = "key_role_permission_list"
|
||||
case permissionItems = "key_in_permission"
|
||||
case roleScenicList = "key_current_role_scenic_list"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private let session: AppSessionStore
|
||||
|
||||
/// 使用指定持久化容器和会话创建权限存储。
|
||||
init(defaults: UserDefaults, session: AppSessionStore) {
|
||||
self.defaults = defaults
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// 旧版 role_id,用于权限匹配兜底。
|
||||
var legacyRoleId: Int {
|
||||
get { defaults.integer(forKey: scopedKey(.legacyRoleId)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.legacyRoleId)) }
|
||||
}
|
||||
|
||||
/// 保存完整 role-permission 列表。
|
||||
func saveRolePermissionList(_ list: [RolePermissionResponse]) {
|
||||
save(list, for: .rolePermissionList)
|
||||
}
|
||||
|
||||
/// 读取完整 role-permission 列表。
|
||||
func rolePermissionList() -> [RolePermissionResponse] {
|
||||
load([RolePermissionResponse].self, for: .rolePermissionList) ?? []
|
||||
}
|
||||
|
||||
/// 保存当前角色扁平权限。
|
||||
func savePermissionItems(_ items: [HomePermissionItem]) {
|
||||
save(items, for: .permissionItems)
|
||||
}
|
||||
|
||||
/// 读取当前角色扁平权限。
|
||||
func permissionItems() -> [HomePermissionItem] {
|
||||
load([HomePermissionItem].self, for: .permissionItems) ?? []
|
||||
}
|
||||
|
||||
/// 保存当前角色可选景区列表。
|
||||
func saveRoleScenicList(_ scenicList: [ScenicInfo]) {
|
||||
save(scenicList, for: .roleScenicList)
|
||||
}
|
||||
|
||||
/// 读取当前角色可选景区列表。
|
||||
func roleScenicList() -> [ScenicInfo] {
|
||||
load([ScenicInfo].self, for: .roleScenicList) ?? []
|
||||
}
|
||||
|
||||
/// 在 role-permission 列表中匹配当前账号角色。
|
||||
func matchRolePermissionItem(in list: [RolePermissionResponse]) -> RolePermissionResponse? {
|
||||
RolePermissionMatcher.match(
|
||||
in: list,
|
||||
roleCode: session.roleCode,
|
||||
roleName: session.roleName,
|
||||
legacyRoleId: legacyRoleId
|
||||
)
|
||||
}
|
||||
|
||||
/// 清除当前账号的全部权限缓存。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
}
|
||||
|
||||
private func save<Value: Encodable>(_ value: Value, for key: Key) {
|
||||
guard let data = try? JSONEncoder().encode(value) else { return }
|
||||
defaults.set(data, forKey: scopedKey(key))
|
||||
}
|
||||
|
||||
private func load<Value: Decodable>(_ type: Value.Type, for key: Key) -> Value? {
|
||||
guard let data = defaults.data(forKey: scopedKey(key)) else { return nil }
|
||||
return try? JSONDecoder().decode(type, from: data)
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
}
|
||||
}
|
||||
97
suixinkan/DataStore/AppScenicQueueStore.swift
Normal file
97
suixinkan/DataStore/AppScenicQueueStore.swift
Normal file
@ -0,0 +1,97 @@
|
||||
//
|
||||
// AppScenicQueueStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 管理当前账号的景区排队打卡点、播报和设置缓存。
|
||||
final class AppScenicQueueStore {
|
||||
|
||||
private enum Key: String, CaseIterable {
|
||||
case punchSpotId = "_scenic_queue_punch_spot_id"
|
||||
case punchSpotName = "_scenic_queue_punch_spot_name"
|
||||
case remark = "_scenic_queue_remark"
|
||||
case settingsSnapshot = "_scenic_queue_settings_snapshot"
|
||||
case useOfflineTTS = "_scenic_queue_offline_tts_v1"
|
||||
case presetVoices = "_scenic_queue_preset_voices_v1"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private let session: AppSessionStore
|
||||
|
||||
/// 使用指定持久化容器和会话创建景区排队存储。
|
||||
init(defaults: UserDefaults, session: AppSessionStore) {
|
||||
self.defaults = defaults
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// 已保存打卡点 ID。
|
||||
var punchSpotId: String {
|
||||
get { string(for: .punchSpotId) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.punchSpotId)) }
|
||||
}
|
||||
|
||||
/// 已保存打卡点名称。
|
||||
var punchSpotName: String {
|
||||
get { string(for: .punchSpotName) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.punchSpotName)) }
|
||||
}
|
||||
|
||||
/// 自定义播报文案。
|
||||
var remark: String {
|
||||
get { string(for: .remark) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.remark)) }
|
||||
}
|
||||
|
||||
/// 是否优先使用离线 TTS。
|
||||
var useOfflineTTS: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.useOfflineTTS)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.useOfflineTTS)) }
|
||||
}
|
||||
|
||||
/// 当前账号是否已经保存有效打卡点。
|
||||
func hasPunchSpotSaved() -> Bool {
|
||||
let id = punchSpotId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Int64(id).map { $0 > 0 } ?? false
|
||||
}
|
||||
|
||||
/// 保存排队设置快照。
|
||||
func saveSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot) {
|
||||
guard let data = try? JSONEncoder().encode(snapshot) else { return }
|
||||
defaults.set(data, forKey: scopedKey(.settingsSnapshot))
|
||||
}
|
||||
|
||||
/// 读取排队设置快照。
|
||||
func settingsSnapshot() -> ScenicQueueSettingsSnapshot? {
|
||||
guard let data = defaults.data(forKey: scopedKey(.settingsSnapshot)) else { return nil }
|
||||
return try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data)
|
||||
}
|
||||
|
||||
/// 保存排队预设播报文案,最多 5 条。
|
||||
func savePresetVoices(_ voices: [String]) {
|
||||
let normalized = voices
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.prefix(5)
|
||||
defaults.set(Array(normalized), forKey: scopedKey(.presetVoices))
|
||||
}
|
||||
|
||||
/// 读取排队预设播报文案。
|
||||
func presetVoices() -> [String] {
|
||||
defaults.stringArray(forKey: scopedKey(.presetVoices)) ?? []
|
||||
}
|
||||
|
||||
/// 清除当前账号的景区排队缓存。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
}
|
||||
|
||||
private func string(for key: Key) -> String {
|
||||
defaults.string(forKey: scopedKey(key)) ?? ""
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
}
|
||||
}
|
||||
233
suixinkan/DataStore/AppSessionStore.swift
Normal file
233
suixinkan/DataStore/AppSessionStore.swift
Normal file
@ -0,0 +1,233 @@
|
||||
//
|
||||
// AppSessionStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 应用账号类型,保留后端返回的未知类型以兼容后续扩展。
|
||||
enum AppAccountType: Equatable, Sendable {
|
||||
case scenicUser
|
||||
case storeUser
|
||||
case unknown(String)
|
||||
|
||||
/// 从持久化或后端原始值创建账号类型。
|
||||
init(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "scenic_user": self = .scenicUser
|
||||
case "store_user": self = .storeUser
|
||||
default: self = .unknown(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// 用于接口与持久化的原始字符串。
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .scenicUser: "scenic_user"
|
||||
case .storeUser: "store_user"
|
||||
case let .unknown(value): value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理登录凭证、用户资料、账号角色及当前业务上下文。
|
||||
final class AppSessionStore {
|
||||
|
||||
private enum Key: String {
|
||||
case token = "key_in_token"
|
||||
case lastLoginUsername = "key_last_login_username"
|
||||
case privacyAgreementAccepted = "key_privacy_agreement_accepted"
|
||||
case userId = "key_in_user_id"
|
||||
case userName = "key_in_user_name"
|
||||
case realName = "key_in_real_name"
|
||||
case avatar = "key_in_avatar"
|
||||
case phone = "key_in_phone"
|
||||
case accountType = "key_in_account_type"
|
||||
case accountDisplayName = "key_in_account_display_name"
|
||||
case roleCode = "key_in_role_code"
|
||||
case roleName = "key_in_role_name"
|
||||
case currentScenicId = "key_in_current_scenic_id"
|
||||
case currentScenicName = "key_in_current_scenic_name"
|
||||
case currentStoreId = "key_in_current_store_id"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
/// 使用指定 UserDefaults 创建会话存储。
|
||||
init(defaults: UserDefaults) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
/// 登录 Token。
|
||||
var token: String {
|
||||
get { string(for: .token) }
|
||||
set { defaults.set(newValue, forKey: Key.token.rawValue) }
|
||||
}
|
||||
|
||||
/// 上次登录手机号,用于登录页恢复。
|
||||
var lastLoginUsername: String? {
|
||||
get { defaults.string(forKey: Key.lastLoginUsername.rawValue) }
|
||||
set {
|
||||
if let newValue {
|
||||
defaults.set(newValue, forKey: Key.lastLoginUsername.rawValue)
|
||||
} else {
|
||||
defaults.removeObject(forKey: Key.lastLoginUsername.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否已同意隐私协议。
|
||||
var privacyAgreementAccepted: Bool {
|
||||
get { defaults.bool(forKey: Key.privacyAgreementAccepted.rawValue) }
|
||||
set { defaults.set(newValue, forKey: Key.privacyAgreementAccepted.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前业务用户 ID。
|
||||
var userId: String {
|
||||
get { string(for: .userId) }
|
||||
set { defaults.set(newValue, forKey: Key.userId.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前用户展示名称。
|
||||
var userName: String {
|
||||
get { string(for: .userName) }
|
||||
set { defaults.set(newValue, forKey: Key.userName.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前用户真实姓名。
|
||||
var realName: String {
|
||||
get { string(for: .realName) }
|
||||
set { defaults.set(newValue, forKey: Key.realName.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前用户头像地址。
|
||||
var avatar: String {
|
||||
get { string(for: .avatar) }
|
||||
set { defaults.set(newValue, forKey: Key.avatar.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前用户手机号。
|
||||
var phone: String {
|
||||
get { string(for: .phone) }
|
||||
set { defaults.set(newValue, forKey: Key.phone.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前账号业务类型。
|
||||
var accountType: AppAccountType {
|
||||
get { AppAccountType(rawValue: string(for: .accountType)) }
|
||||
set { defaults.set(newValue.rawValue, forKey: Key.accountType.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前账号展示名称。
|
||||
var accountDisplayName: String {
|
||||
get { string(for: .accountDisplayName) }
|
||||
set { defaults.set(newValue, forKey: Key.accountDisplayName.rawValue) }
|
||||
}
|
||||
|
||||
/// 后端返回的原始角色编码。
|
||||
var roleCode: String {
|
||||
get { string(for: .roleCode) }
|
||||
set { defaults.set(newValue, forKey: Key.roleCode.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前角色名称。
|
||||
var roleName: String {
|
||||
get { string(for: .roleName) }
|
||||
set { defaults.set(newValue, forKey: Key.roleName.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前景区 ID,0 表示未选择。
|
||||
var currentScenicId: Int {
|
||||
get { Int(string(for: .currentScenicId)) ?? 0 }
|
||||
set { defaults.set(newValue > 0 ? String(newValue) : "", forKey: Key.currentScenicId.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前景区名称。
|
||||
var currentScenicName: String {
|
||||
get { string(for: .currentScenicName) }
|
||||
set { defaults.set(newValue, forKey: Key.currentScenicName.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前店铺 ID,0 表示未选择。
|
||||
var currentStoreId: Int {
|
||||
get { defaults.integer(forKey: Key.currentStoreId.rawValue) }
|
||||
set { defaults.set(newValue, forKey: Key.currentStoreId.rawValue) }
|
||||
}
|
||||
|
||||
/// 当前账号缓存前缀,对齐 Android `getAccountCachePrefix`。
|
||||
var accountCachePrefix: String {
|
||||
let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let type = accountType.rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if uid.isEmpty { return "guest" }
|
||||
if type.isEmpty { return uid }
|
||||
return "\(uid)_\(type)"
|
||||
}
|
||||
|
||||
/// 当前解析后的业务角色。
|
||||
var currentAppRole: AppRoleCode? {
|
||||
AppRoleCode.fromCode(roleCode) ?? AppRoleCode.fromRoleName(roleName)
|
||||
}
|
||||
|
||||
/// 当前是否为摄影师角色。
|
||||
var isPhotographerRole: Bool {
|
||||
if let role = currentAppRole {
|
||||
return role.isPhotographer
|
||||
}
|
||||
return roleName.trimmingCharacters(in: .whitespacesAndNewlines) == "摄影师"
|
||||
}
|
||||
|
||||
/// Token 非空时表示已登录。
|
||||
var isLoggedIn: Bool {
|
||||
!token.isEmpty
|
||||
}
|
||||
|
||||
/// 保存登录 Token。
|
||||
func saveToken(_ token: String) {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
/// 从账号切换实体写入会话快照。
|
||||
func applyAccount(_ account: AccountSwitchAccount) {
|
||||
userId = String(account.businessUserId)
|
||||
accountType = AppAccountType(rawValue: account.accountType)
|
||||
accountDisplayName = account.title
|
||||
userName = account.subtitle
|
||||
phone = account.phone
|
||||
if !account.avatar.isEmpty {
|
||||
avatar = account.avatar
|
||||
}
|
||||
currentScenicName = account.scenicName
|
||||
currentScenicId = account.scenicId ?? 0
|
||||
currentStoreId = account.storeId ?? 0
|
||||
}
|
||||
|
||||
/// 从用户资料接口回写展示字段。
|
||||
func applyUserInfo(_ info: UserInfoResponse) {
|
||||
if !info.nickname.isEmpty { userName = info.nickname }
|
||||
if !info.realName.isEmpty { realName = info.realName }
|
||||
if !info.avatar.isEmpty { avatar = info.avatar }
|
||||
if !info.phone.isEmpty { phone = info.phone }
|
||||
if !info.roleName.isEmpty, roleName.isEmpty { roleName = info.roleName }
|
||||
}
|
||||
|
||||
/// 清除登录态及账号快照,保留隐私授权和上次登录用户名。
|
||||
func clearAuthenticatedSession() {
|
||||
token = ""
|
||||
userId = ""
|
||||
userName = ""
|
||||
realName = ""
|
||||
avatar = ""
|
||||
phone = ""
|
||||
accountType = .unknown("")
|
||||
accountDisplayName = ""
|
||||
roleCode = ""
|
||||
roleName = ""
|
||||
currentScenicId = 0
|
||||
currentScenicName = ""
|
||||
currentStoreId = 0
|
||||
}
|
||||
|
||||
private func string(for key: Key) -> String {
|
||||
defaults.string(forKey: key.rawValue) ?? ""
|
||||
}
|
||||
}
|
||||
@ -5,403 +5,43 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 本地会话存储,与 Android `AppStoreDataSource` 对齐。
|
||||
/// 应用本地存储统一入口,负责组装各功能模块并协调退出登录清理。
|
||||
final class AppStore {
|
||||
|
||||
/// 全局应用存储。
|
||||
static let shared = AppStore()
|
||||
|
||||
private enum Key {
|
||||
static let token = "key_in_token"
|
||||
static let lastLoginUsername = "key_last_login_username"
|
||||
static let privacyAgreementAccepted = "key_privacy_agreement_accepted"
|
||||
static let userId = "key_in_user_id"
|
||||
static let userName = "key_in_user_name"
|
||||
static let realName = "key_in_real_name"
|
||||
static let avatar = "key_in_avatar"
|
||||
static let phone = "key_in_phone"
|
||||
static let accountType = "key_in_account_type"
|
||||
static let accountDisplayName = "key_in_account_display_name"
|
||||
static let roleCode = "key_in_role_code"
|
||||
static let roleName = "key_in_role_name"
|
||||
static let currentScenicId = "key_in_current_scenic_id"
|
||||
static let currentScenicName = "key_in_current_scenic_name"
|
||||
static let currentStoreId = "key_in_current_store_id"
|
||||
static let legacyRoleId = "key_in_role_id"
|
||||
static let rolePermissionList = "key_role_permission_list"
|
||||
static let permissionItems = "key_in_permission"
|
||||
static let roleScenicList = "key_current_role_scenic_list"
|
||||
static let onlineStatus = "key_online_status"
|
||||
static let lastLocationReportTime = "key_last_location_report_time"
|
||||
static let locationReminderMinutes = "key_location_reminder_minutes"
|
||||
static let isOpenReceiveVoice = "key_is_open_receive_voice"
|
||||
static let scenicQueuePunchSpotId = "_scenic_queue_punch_spot_id"
|
||||
static let scenicQueuePunchSpotName = "_scenic_queue_punch_spot_name"
|
||||
static let scenicQueueRemark = "_scenic_queue_remark"
|
||||
static let scenicQueueSettingsSnapshot = "_scenic_queue_settings_snapshot"
|
||||
static let scenicQueueOfflineTTS = "_scenic_queue_offline_tts_v1"
|
||||
static let scenicQueuePresetVoices = "_scenic_queue_preset_voices_v1"
|
||||
}
|
||||
/// 登录会话与当前账号存储。
|
||||
let session: AppSessionStore
|
||||
|
||||
private let defaults: UserDefaults
|
||||
/// 角色权限与首页权限缓存。
|
||||
let permissions: AppPermissionStore
|
||||
|
||||
/// 位置上报会话存储。
|
||||
let location: AppLocationStore
|
||||
|
||||
/// 收款功能偏好存储。
|
||||
let payment: AppPaymentStore
|
||||
|
||||
/// 景区排队功能存储。
|
||||
let scenicQueue: AppScenicQueueStore
|
||||
|
||||
/// 使用指定 UserDefaults 创建模块化应用存储。
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
let session = AppSessionStore(defaults: defaults)
|
||||
self.session = session
|
||||
permissions = AppPermissionStore(defaults: defaults, session: session)
|
||||
location = AppLocationStore(defaults: defaults, session: session)
|
||||
payment = AppPaymentStore(defaults: defaults, session: session)
|
||||
scenicQueue = AppScenicQueueStore(defaults: defaults, session: session)
|
||||
}
|
||||
|
||||
/// 登录 Token。
|
||||
var token: String {
|
||||
get { defaults.string(forKey: Key.token) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.token) }
|
||||
}
|
||||
|
||||
/// 上次登录手机号,用于登录页恢复。
|
||||
var lastLoginUsername: String? {
|
||||
get { defaults.string(forKey: Key.lastLoginUsername) }
|
||||
set {
|
||||
if let newValue {
|
||||
defaults.set(newValue, forKey: Key.lastLoginUsername)
|
||||
} else {
|
||||
defaults.removeObject(forKey: Key.lastLoginUsername)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否已同意隐私协议。
|
||||
var privacyAgreementAccepted: Bool {
|
||||
get { defaults.bool(forKey: Key.privacyAgreementAccepted) }
|
||||
set { defaults.set(newValue, forKey: Key.privacyAgreementAccepted) }
|
||||
}
|
||||
|
||||
var userId: String {
|
||||
get { defaults.string(forKey: Key.userId) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.userId) }
|
||||
}
|
||||
|
||||
var userName: String {
|
||||
get { defaults.string(forKey: Key.userName) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.userName) }
|
||||
}
|
||||
|
||||
var realName: String {
|
||||
get { defaults.string(forKey: Key.realName) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.realName) }
|
||||
}
|
||||
|
||||
var avatar: String {
|
||||
get { defaults.string(forKey: Key.avatar) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.avatar) }
|
||||
}
|
||||
|
||||
var phone: String {
|
||||
get { defaults.string(forKey: Key.phone) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.phone) }
|
||||
}
|
||||
|
||||
var accountType: String {
|
||||
get { defaults.string(forKey: Key.accountType) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.accountType) }
|
||||
}
|
||||
|
||||
var accountDisplayName: String {
|
||||
get { defaults.string(forKey: Key.accountDisplayName) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.accountDisplayName) }
|
||||
}
|
||||
|
||||
var roleCode: String {
|
||||
get { defaults.string(forKey: Key.roleCode) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.roleCode) }
|
||||
}
|
||||
|
||||
var roleName: String {
|
||||
get { defaults.string(forKey: Key.roleName) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.roleName) }
|
||||
}
|
||||
|
||||
var currentScenicId: Int {
|
||||
get {
|
||||
let value = defaults.string(forKey: Key.currentScenicId) ?? ""
|
||||
return Int(value) ?? 0
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue > 0 ? String(newValue) : "", forKey: Key.currentScenicId)
|
||||
}
|
||||
}
|
||||
|
||||
var currentScenicName: String {
|
||||
get { defaults.string(forKey: Key.currentScenicName) ?? "" }
|
||||
set { defaults.set(newValue, forKey: Key.currentScenicName) }
|
||||
}
|
||||
|
||||
var currentStoreId: Int {
|
||||
get { defaults.integer(forKey: Key.currentStoreId) }
|
||||
set { defaults.set(newValue, forKey: Key.currentStoreId) }
|
||||
}
|
||||
|
||||
/// 旧版 role_id,用于权限匹配兜底。
|
||||
var legacyRoleId: Int {
|
||||
get { defaults.integer(forKey: accountScopedKey(Key.legacyRoleId)) }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.legacyRoleId)) }
|
||||
}
|
||||
|
||||
/// 是否在线(位置上报会话)。
|
||||
var onlineStatus: Bool {
|
||||
get { defaults.bool(forKey: accountScopedKey(Key.onlineStatus)) }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.onlineStatus)) }
|
||||
}
|
||||
|
||||
/// 上次位置上报时间戳(毫秒)。
|
||||
var lastLocationReportTime: Int64 {
|
||||
get { Int64(defaults.integer(forKey: accountScopedKey(Key.lastLocationReportTime))) }
|
||||
set { defaults.set(Int(newValue), forKey: accountScopedKey(Key.lastLocationReportTime)) }
|
||||
}
|
||||
|
||||
/// 位置超时提前提醒分钟数,0 表示不提醒。
|
||||
var locationReminderMinutes: Int {
|
||||
get { defaults.integer(forKey: accountScopedKey(Key.locationReminderMinutes)) }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.locationReminderMinutes)) }
|
||||
}
|
||||
|
||||
/// 收款到账是否开启语音播报,对齐 Android `getIsOpenReceiveVoice`。
|
||||
var isOpenReceiveVoice: Bool {
|
||||
get { defaults.bool(forKey: accountScopedKey(Key.isOpenReceiveVoice)) }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.isOpenReceiveVoice)) }
|
||||
}
|
||||
|
||||
/// 排队管理已保存打卡点 ID。
|
||||
var scenicQueuePunchSpotId: String {
|
||||
get { defaults.string(forKey: accountScopedKey(Key.scenicQueuePunchSpotId)) ?? "" }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueuePunchSpotId)) }
|
||||
}
|
||||
|
||||
/// 排队管理已保存打卡点名称。
|
||||
var scenicQueuePunchSpotName: String {
|
||||
get { defaults.string(forKey: accountScopedKey(Key.scenicQueuePunchSpotName)) ?? "" }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueuePunchSpotName)) }
|
||||
}
|
||||
|
||||
/// 排队管理自定义播报文案。
|
||||
var scenicQueueRemark: String {
|
||||
get { defaults.string(forKey: accountScopedKey(Key.scenicQueueRemark)) ?? "" }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueueRemark)) }
|
||||
}
|
||||
|
||||
/// 排队语音是否优先使用离线 TTS。
|
||||
var scenicQueueUseOfflineTTS: Bool {
|
||||
get { defaults.bool(forKey: accountScopedKey(Key.scenicQueueOfflineTTS)) }
|
||||
set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueueOfflineTTS)) }
|
||||
}
|
||||
|
||||
/// 当前账号缓存前缀,对齐 Android `getAccountCachePrefix`。
|
||||
var accountCachePrefix: String {
|
||||
let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let type = accountType.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if uid.isEmpty { return "guest" }
|
||||
if type.isEmpty { return uid }
|
||||
return "\(uid)_\(type)"
|
||||
}
|
||||
|
||||
/// 当前解析后的业务角色。
|
||||
var currentAppRole: AppRoleCode? {
|
||||
AppRoleCode.fromCode(roleCode) ?? AppRoleCode.fromRoleName(roleName)
|
||||
}
|
||||
|
||||
/// 当前是否为摄影师角色。
|
||||
var isPhotographerRole: Bool {
|
||||
if let role = currentAppRole {
|
||||
return role.isPhotographer
|
||||
}
|
||||
let name = roleName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return name == "摄影师"
|
||||
}
|
||||
|
||||
/// Token 非空视为已登录。
|
||||
var isLoggedIn: Bool {
|
||||
!token.isEmpty
|
||||
}
|
||||
|
||||
/// 保存登录 Token。
|
||||
func saveToken(_ token: String) {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
/// 从账号切换实体写入会话快照。
|
||||
func applyAccount(_ account: AccountSwitchAccount) {
|
||||
userId = String(account.businessUserId)
|
||||
accountType = account.accountType
|
||||
accountDisplayName = account.title
|
||||
userName = account.subtitle
|
||||
phone = account.phone
|
||||
if !account.avatar.isEmpty {
|
||||
avatar = account.avatar
|
||||
}
|
||||
currentScenicName = account.scenicName
|
||||
currentScenicId = account.scenicId ?? 0
|
||||
currentStoreId = account.storeId ?? 0
|
||||
}
|
||||
|
||||
/// 从用户资料接口回写展示字段。
|
||||
func applyUserInfo(_ info: UserInfoResponse) {
|
||||
if !info.nickname.isEmpty {
|
||||
userName = info.nickname
|
||||
}
|
||||
if !info.realName.isEmpty {
|
||||
realName = info.realName
|
||||
}
|
||||
if !info.avatar.isEmpty {
|
||||
avatar = info.avatar
|
||||
}
|
||||
if !info.phone.isEmpty {
|
||||
phone = info.phone
|
||||
}
|
||||
if !info.roleName.isEmpty, roleName.isEmpty {
|
||||
roleName = info.roleName
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除登录态与会话快照。
|
||||
/// 清除当前账号业务快照与登录态,保留隐私授权和上次登录用户名。
|
||||
func logout() {
|
||||
clearPermissionSnapshot()
|
||||
token = ""
|
||||
userId = ""
|
||||
userName = ""
|
||||
realName = ""
|
||||
avatar = ""
|
||||
phone = ""
|
||||
accountType = ""
|
||||
accountDisplayName = ""
|
||||
roleCode = ""
|
||||
roleName = ""
|
||||
currentScenicId = 0
|
||||
currentScenicName = ""
|
||||
currentStoreId = 0
|
||||
}
|
||||
|
||||
/// 保存完整 role-permission 列表。
|
||||
func saveRolePermissionList(_ list: [RolePermissionResponse]) {
|
||||
guard let data = try? JSONEncoder().encode(list) else { return }
|
||||
defaults.set(data, forKey: accountScopedKey(Key.rolePermissionList))
|
||||
}
|
||||
|
||||
/// 读取完整 role-permission 列表。
|
||||
func rolePermissionList() -> [RolePermissionResponse] {
|
||||
guard let data = defaults.data(forKey: accountScopedKey(Key.rolePermissionList)),
|
||||
let list = try? JSONDecoder().decode([RolePermissionResponse].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/// 保存当前角色扁平权限。
|
||||
func savePermissionItems(_ items: [HomePermissionItem]) {
|
||||
guard let data = try? JSONEncoder().encode(items) else { return }
|
||||
defaults.set(data, forKey: accountScopedKey(Key.permissionItems))
|
||||
}
|
||||
|
||||
/// 读取当前角色扁平权限。
|
||||
func permissionItems() -> [HomePermissionItem] {
|
||||
guard let data = defaults.data(forKey: accountScopedKey(Key.permissionItems)),
|
||||
let items = try? JSONDecoder().decode([HomePermissionItem].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
/// 保存当前角色可选景区列表。
|
||||
func saveRoleScenicList(_ scenicList: [ScenicInfo]) {
|
||||
guard let data = try? JSONEncoder().encode(scenicList) else { return }
|
||||
defaults.set(data, forKey: accountScopedKey(Key.roleScenicList))
|
||||
}
|
||||
|
||||
/// 读取当前角色可选景区列表。
|
||||
func roleScenicList() -> [ScenicInfo] {
|
||||
guard let data = defaults.data(forKey: accountScopedKey(Key.roleScenicList)),
|
||||
let list = try? JSONDecoder().decode([ScenicInfo].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/// 在 role-permission 列表中匹配当前账号角色。
|
||||
func matchRolePermissionItem(in list: [RolePermissionResponse]) -> RolePermissionResponse? {
|
||||
RolePermissionMatcher.match(
|
||||
in: list,
|
||||
roleCode: roleCode,
|
||||
roleName: roleName,
|
||||
legacyRoleId: legacyRoleId
|
||||
)
|
||||
}
|
||||
|
||||
/// 清除上次位置上报时间。
|
||||
func clearLastLocationReportTime() {
|
||||
defaults.removeObject(forKey: accountScopedKey(Key.lastLocationReportTime))
|
||||
}
|
||||
|
||||
/// 是否已经保存排队打卡点。
|
||||
func hasScenicQueuePunchSpotSaved() -> Bool {
|
||||
let id = scenicQueuePunchSpotId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Int64(id).map { $0 > 0 } ?? false
|
||||
}
|
||||
|
||||
/// 保存排队设置快照。
|
||||
func saveScenicQueueSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot) {
|
||||
guard let data = try? JSONEncoder().encode(snapshot) else { return }
|
||||
defaults.set(data, forKey: accountScopedKey(Key.scenicQueueSettingsSnapshot))
|
||||
}
|
||||
|
||||
/// 读取排队设置快照。
|
||||
func scenicQueueSettingsSnapshot() -> ScenicQueueSettingsSnapshot? {
|
||||
guard let data = defaults.data(forKey: accountScopedKey(Key.scenicQueueSettingsSnapshot)) else { return nil }
|
||||
return try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data)
|
||||
}
|
||||
|
||||
/// 保存排队预设播报文案,最多 5 条。
|
||||
func saveScenicQueuePresetVoices(_ voices: [String]) {
|
||||
let normalized = voices
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.prefix(5)
|
||||
defaults.set(Array(normalized), forKey: accountScopedKey(Key.scenicQueuePresetVoices))
|
||||
}
|
||||
|
||||
/// 读取排队预设播报文案。
|
||||
func scenicQueuePresetVoices() -> [String] {
|
||||
defaults.stringArray(forKey: accountScopedKey(Key.scenicQueuePresetVoices)) ?? []
|
||||
}
|
||||
|
||||
/// 清除排队管理本地缓存。
|
||||
func clearScenicQueueSnapshot() {
|
||||
[
|
||||
Key.scenicQueuePunchSpotId,
|
||||
Key.scenicQueuePunchSpotName,
|
||||
Key.scenicQueueRemark,
|
||||
Key.scenicQueueSettingsSnapshot,
|
||||
Key.scenicQueueOfflineTTS,
|
||||
Key.scenicQueuePresetVoices,
|
||||
].forEach { defaults.removeObject(forKey: accountScopedKey($0)) }
|
||||
}
|
||||
|
||||
/// 清除权限与位置会话快照。
|
||||
func clearPermissionSnapshot() {
|
||||
let prefix = accountScopedKey("")
|
||||
let keys = [
|
||||
Key.rolePermissionList,
|
||||
Key.permissionItems,
|
||||
Key.roleScenicList,
|
||||
Key.onlineStatus,
|
||||
Key.lastLocationReportTime,
|
||||
Key.locationReminderMinutes,
|
||||
Key.isOpenReceiveVoice,
|
||||
Key.scenicQueuePunchSpotId,
|
||||
Key.scenicQueuePunchSpotName,
|
||||
Key.scenicQueueRemark,
|
||||
Key.scenicQueueSettingsSnapshot,
|
||||
Key.scenicQueueOfflineTTS,
|
||||
Key.scenicQueuePresetVoices,
|
||||
]
|
||||
keys.forEach { defaults.removeObject(forKey: prefix + $0) }
|
||||
}
|
||||
|
||||
private func accountScopedKey(_ key: String) -> String {
|
||||
"\(accountCachePrefix)_\(key)"
|
||||
permissions.clear()
|
||||
location.clear()
|
||||
payment.clear()
|
||||
scenicQueue.clear()
|
||||
session.clearAuthenticatedSession()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user