3、用户账户数据模型:用户信息数据结构、账户状态机设计、多用户数据隔离方案

好,咱们进入第三讲。这一讲我打算聊聊数据模型——说白了,就是用户账户在系统里到底长什么样,怎么存,怎么变,以及多个用户之间怎么做到「井水不犯河水」。

我在做车载系统的时候,发现很多团队一开始只关注功能,比如「能不能登录」「能不能切账号」。结果做到一半才发现,数据模型没设计好,后面改起来简直要命。嗯,咱们今天就把它一次性说清楚。

3.1 用户信息数据结构

先看最基础的部分——用户信息存什么?我个人习惯把用户数据分成三层:

  • 核心身份层:用户ID、账户类型(主账户/访客/子账户)、绑定的手机号或邮箱。这些是系统识别用户的唯一凭证,几乎不会变。
  • 个性化配置层:座椅位置、后视镜角度、驾驶模式偏好、音效设置、导航历史。这些是用户用得最多的数据,也是切换账户时最需要「带走」的东西。
  • 应用数据层:第三方App的登录态、媒体库收藏、通话记录。这部分数据通常由各个App自己管理,但系统需要提供统一的存储接口。

我建议用这样的数据结构来组织:

// 用户核心信息
data class UserProfile(
    val userId: String,           // 全局唯一ID
    val accountType: AccountType, // 主账户/访客/子账户
    val displayName: String,
    val avatarUri: String?,
    val boundPhone: String?,
    val boundEmail: String?
)

// 个性化配置
data class UserSettings(
    val userId: String,
    val seatPosition: SeatConfig,
    val drivingMode: DrivingMode,
    val audioSettings: AudioConfig,
    val navigationHistory: List<NavHistoryItem>
)

// 应用数据索引
data class AppDataIndex(
    val userId: String,
    val appId: String,
    val dataPath: String,   // 指向实际存储路径
    val lastModified: Long
)

你可能会问,为什么要把应用数据单独拎出来?我在项目里踩过坑——一开始把所有数据都塞到一个大JSON里,结果某个App升级后改了数据格式,整个用户配置都解析失败了。所以,核心数据和扩展数据一定要解耦

3.2 账户状态机设计

账户不是只有「登录」和「未登录」两种状态。你想想看,在车上,用户可能正在开车,突然想切账号;或者访客临时用一下,下车后系统要自动清理数据。这些场景都需要一个清晰的状态机来兜底。

我设计的账户状态机包含以下状态:

状态 说明 触发条件
未登录 系统启动后的默认状态,无用户数据加载 系统启动 / 用户登出
登录中 正在验证凭据,加载用户数据 用户输入账号密码 / 扫码登录
已登录 用户数据加载完成,系统处于可用状态 登录成功 / 切换账户完成
切换中 正在保存当前用户数据,准备加载新用户数据 用户发起切换账户操作
访客模式 临时使用,不保存个性化数据 用户选择访客登录 / 超时自动进入
锁定 因安全策略(如多次输错密码)暂时锁定 安全策略触发

状态转换的代码实现,我习惯用状态模式:

sealed class AccountState {
    object Unauthenticated : AccountState()
    data class LoggingIn(val credentials: Credentials) : AccountState()
    data class LoggedIn(val user: UserProfile) : AccountState()
    data class Switching(val fromUser: UserProfile, val toUserId: String) : AccountState()
    object GuestMode : AccountState()
    data class Locked(val until: Long) : AccountState()
}

// 状态机核心逻辑
class AccountStateMachine {
    private var currentState: AccountState = AccountState.Unauthenticated

    fun transition(event: AccountEvent): AccountState {
        return when (currentState) {
            is AccountState.Unauthenticated -> when (event) {
                is AccountEvent.Login -> {
                    // 开始登录流程
                    AccountState.LoggingIn(event.credentials)
                }
                is AccountEvent.EnterGuest -> {
                    AccountState.GuestMode
                }
                else -> currentState // 忽略无效事件
            }
            is AccountState.LoggedIn -> when (event) {
                is AccountEvent.SwitchAccount -> {
                    AccountState.Switching(
                        fromUser = (currentState as AccountState.LoggedIn).user,
                        toUserId = event.targetUserId
                    )
                }
                is AccountEvent.Logout -> {
                    // 保存当前用户数据
                    AccountState.Unauthenticated
                }
                else -> currentState
            }
            // 其他状态转换类似...
        }
    }
}
⚠️ 注意: 状态转换一定要考虑异常情况。我曾经遇到过切换账户时网络断了,结果系统卡在「切换中」状态,用户点哪都没反应。后来我加了一个超时机制——如果切换超过30秒没完成,自动回退到上一个用户状态。

3.3 多用户数据隔离方案

这是整个系统里最容易出问题的地方。多用户数据隔离,说白了就是「你的数据不能跑到我的账号里」。在Android Automotive上,我推荐用三种隔离级别:

  1. 文件系统隔离:每个用户拥有独立的存储目录,系统通过UserHandle来区分。Android系统本身支持多用户,每个用户有独立的/data/user/目录。
  2. 数据库隔离:每个用户使用独立的数据库文件,或者在同一数据库中通过userId字段做逻辑隔离。我个人更倾向于前者——物理隔离更安全,清理数据也方便。
  3. ContentProvider隔离:如果多个App需要共享数据,可以通过ContentProvider加权限控制。但要注意,不要直接暴露数据库,一定要通过接口层做校验。

具体实现上,我建议这样设计存储路径:

// 用户数据存储路径规划
/storage/emulated/{userId}/Android/data/com.example.carapp/
    ├── settings/          # 个性化配置
    │   ├── seat.json
    │   ├── audio.json
    │   └── driving.json
    ├── app_data/          # 应用数据
    │   ├── media_player/
    │   └── navigation/
    └── cache/             # 临时缓存,切换用户时可清理
💡 小技巧: 切换用户时,我建议采用「写时复制」策略——先保存当前用户数据,再加载新用户数据。如果保存失败,不要清空当前用户数据,否则用户回来发现配置丢了,体验会很差。

还有一个容易被忽略的点——共享数据怎么处理?比如车辆的总里程、保养记录,这些不属于任何个人用户,而是车辆本身的属性。我习惯把这些数据单独放在一个「系统级」存储区域,所有用户都能读,但只有管理员账户能写。

嗯,数据模型这部分就讲到这里。下一讲咱们聊聊账户的创建和登录流程,到时候我会分享一些关于「无感登录」和「离线认证」的实战经验。