Files
dudu/ios/dudu/Shared/AccountStore.swift
T
wangjia b5ab92a57e
ci / server (push) Failing after 13s
ci / design-tokens (push) Failing after 11s
fix: 应用 xhigh 代码评审的跨端修复
来自 xhigh code review 的正确性/健壮性修复,覆盖全部五端:
- server:鉴权 fail-closed、计量交叉校验与配额扣穿处理、WS 网关并发与关闭顺序、
  billing 行锁、redis Lua 过期与设备槽刷新、config 解析
- desktop:会话 epoch 防串话、WS 重连与 401 处理、api 客户端复用、统一 usePoll 轮询
- android:握手时序、请求头封装、账户状态派生、按需重组
- ios:finalize 宽限、串行采集、错误文案服务端优先、删除死代码 CommitController

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:50:08 +08:00

152 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// App App Group UserDefaults
// server/pkg/protocol/dto.go
// token guest > 0 ok = 0 trial quota
// 14Bmock guest applyLogin token
// GET /v1/me applyMe 退 logout
// 14DWS usage applyUsage balanceSeconds / trialRemainingSeconds
import Combine
import Foundation
/// ok/trial/quota dto.go AccountOK/AccountTrial/AccountQuota
/// guest token
public enum AccountState: String, CaseIterable {
case ok, trial, guest, quota
}
public final class AccountStore: ObservableObject {
public static let appGroupID = "group.app.dudu.shared"
private enum Key {
static let token = "account.token" // tokenAuthTokenResponse.token
static let nickname = "account.nickname" // MeResponse.nickname_masked
static let balanceSeconds = "account.balance_seconds" // MeResponse.balance_seconds
static let trialRemaining = "account.trial_remaining" // trial_daily_limit - trial_used_today
static let trialDailyLimit = "account.trial_daily_limit"
static let kbFullAccess = "kb.full_access" // hasFullAccess 2
static let kbSeen = "kb.seen" // 4 ""
}
private let defaults: UserDefaults
@Published public private(set) var token: String?
@Published public private(set) var nicknameMasked: String?
@Published public private(set) var balanceSeconds: Int
@Published public private(set) var trialRemainingSeconds: Int
@Published public private(set) var trialDailyLimitSeconds: Int
@Published public private(set) var keyboardFullAccess: Bool
@Published public private(set) var keyboardSeen: Bool
public init() {
// App Group 退 standard
let d = UserDefaults(suiteName: Self.appGroupID) ?? .standard
defaults = d
token = d.string(forKey: Key.token)
nicknameMasked = d.string(forKey: Key.nickname)
balanceSeconds = d.integer(forKey: Key.balanceSeconds)
trialRemainingSeconds = d.integer(forKey: Key.trialRemaining)
trialDailyLimitSeconds = (d.object(forKey: Key.trialDailyLimit) as? Int) ?? DuduProtocol.trialDailySeconds
keyboardFullAccess = d.bool(forKey: Key.kbFullAccess)
keyboardSeen = d.bool(forKey: Key.kbSeen)
}
public var state: AccountState {
guard token != nil else { return .guest }
if balanceSeconds > 0 { return .ok }
if trialRemainingSeconds > 0 { return .trial }
return .quota
}
/// UI "X / 3 "
public var trialUsedSeconds: Int {
max(0, trialDailyLimitSeconds - trialRemainingSeconds)
}
/// App UserDefaults @Published
/// viewWillAppear App
public func reload() {
token = defaults.string(forKey: Key.token)
nicknameMasked = defaults.string(forKey: Key.nickname)
balanceSeconds = defaults.integer(forKey: Key.balanceSeconds)
trialRemainingSeconds = defaults.integer(forKey: Key.trialRemaining)
trialDailyLimitSeconds = (defaults.object(forKey: Key.trialDailyLimit) as? Int) ?? DuduProtocol.trialDailySeconds
keyboardFullAccess = defaults.bool(forKey: Key.kbFullAccess)
keyboardSeen = defaults.bool(forKey: Key.kbSeen)
}
// MARK: - 14B
/// tokenApp Group
public func applyLogin(token: String, nicknameMasked nickname: String) {
defaults.set(token, forKey: Key.token)
defaults.set(nickname, forKey: Key.nickname)
reload()
}
/// GET /v1/me / /
public func applyMe(_ me: MeResponse) {
defaults.set(me.nicknameMasked, forKey: Key.nickname)
defaults.set(Int(me.balanceSeconds), forKey: Key.balanceSeconds)
defaults.set(me.trialDailyLimit, forKey: Key.trialDailyLimit)
defaults.set(max(0, me.trialDailyLimit - me.trialUsedToday), forKey: Key.trialRemaining)
reload()
}
/// 14DWS usage /
/// 0ServerMsg decodeIfPresent
/// balance_seconds=0 0 nil 0
/// state balance=0 && trial=0 quota #16 omitempty 0
/// 便 omitempty 0 nil error
/// QUOTA_EXCEEDED GET /v1/me 0
public func applyUsage(balanceSeconds: Int64?, trialRemaining: Int?) {
if let b = balanceSeconds { defaults.set(Int(b), forKey: Key.balanceSeconds) }
if let t = trialRemaining { defaults.set(t, forKey: Key.trialRemaining) }
reload()
}
/// 退 token guest
public func logout() {
defaults.removeObject(forKey: Key.token)
defaults.removeObject(forKey: Key.nickname)
defaults.set(0, forKey: Key.balanceSeconds)
defaults.set(0, forKey: Key.trialRemaining)
reload()
}
/// viewWillAppear 访""
/// App 2 访/ 4
public func recordKeyboardActive(fullAccess: Bool) {
defaults.set(fullAccess, forKey: Key.kbFullAccess)
defaults.set(true, forKey: Key.kbSeen)
reload()
}
#if DEBUG
/// mock App DEBUG
public func setMock(state: AccountState) {
switch state {
case .ok:
defaults.set("mock-token", forKey: Key.token)
defaults.set("wang***", forKey: Key.nickname)
defaults.set(472 * 60, forKey: Key.balanceSeconds)
defaults.set(120, forKey: Key.trialRemaining)
case .trial:
defaults.set("mock-token", forKey: Key.token)
defaults.set("wang***", forKey: Key.nickname)
defaults.set(0, forKey: Key.balanceSeconds)
defaults.set(120, forKey: Key.trialRemaining)
case .guest:
defaults.removeObject(forKey: Key.token)
defaults.removeObject(forKey: Key.nickname)
defaults.set(0, forKey: Key.balanceSeconds)
defaults.set(0, forKey: Key.trialRemaining)
case .quota:
defaults.set("mock-token", forKey: Key.token)
defaults.set("wang***", forKey: Key.nickname)
defaults.set(0, forKey: Key.balanceSeconds)
defaults.set(0, forKey: Key.trialRemaining)
}
reload()
}
#endif
}