【A11】客户端登录流程笔记

一、决策树

复制代码
登录
    ↓
尝试连服务器
    ↓
┌──────────────────┬──────────────────┐
│ 服务器响应        │ 服务器离线        │
↓                  ↓
在线登录            查 local_users
(含 admin)        ↓
写 local_users  ┌──────────────┬──────────────┐
                │ 有记录        │ 无记录        │
                ↓              ↓
            验本地密码        admin + admin11?
                ↓              ↓
            ┌──────┬──────┐  ┌──────┬──────┐
            │ 对    │ 错    │  │ 是    │ 否    │
            ↓      ↓      ↓  ↓      ↓
        进主界面  拒绝   录入界面  拒绝
        按权限显示         (不算登录)

二、各分支说明

分支 条件 结果 写缓存
在线登录 服务器响应 进主界面,按权限显示 是
离线登录 服务器离线 + 本机有记录 + 密码对 进主界面,按权限显示 否
录入界面 服务器离线 + 本机无记录 + admin 进录入界面 否
拒绝 其他 报错 否

三、admin 的两条路径

路径 触发 算登录 写 local_users
在线登录 服务器响应 是 是
录入界面短接 服务器离线 + 本机无记录 否 否

关键:录入界面是一次性应急通道,不算登录,不写缓存。

四、local_users 表

sql 复制代码
CREATE TABLE local_users (
    user_id          INTEGER PRIMARY KEY,
    name             TEXT NOT NULL UNIQUE,   -- 姓名,登录标识
    password         TEXT NOT NULL,          -- 密码哈希,字段名不暴露逻辑
    permissions_json TEXT NOT NULL,          -- 权限集合
    last_online_at   INTEGER NOT NULL,       -- 最后一次在线登录时间
    token            TEXT,                   -- 上次的 JWT
    token_exp        INTEGER                 -- JWT 过期时间
);

五、权限级别

级别 含义
0 隐藏 / 不可见
1 只读查看
2 建议修改
3 完全控制(拥有人)

不区分角色,IT 就是对某些 PC 有级别 3 的用户。

六、界面分流

条件 界面
owned_pcs 非空 显示"我负责的 PC"+ 消息
owned_pcs 为空 只显示"我的 PC"

按权限分流,不按角色。

七、两个数据库

数据库 用途 驱动 生命周期
data_service.db 服务器库 rusqlite(录入时) 生成后拷给服务器
client_cache.db 客户端缓存 rusqlite 长期

客户端全程只用 rusqlite,不引入 sqlx。

八、核心代码骨架

登录

rust 复制代码
#[tauri::command]
pub async fn login(user: String, password: String) -> Result<LoginResult, String> {
    // 1. 先试服务器
    match api::login(&user, &password).await {
        Ok(resp) => {
            cache::save_local_user(&resp, &password).await.ok();
            return Ok(LoginResult::from_online(resp));
        }
        Err(e) if is_server_offline(&e) => {
            // 服务器离线,进入离线分支
        }
        Err(e) => return Err(e.to_string()),
    }

    // 2. 查本机记录
    if let Some(cached) = cache::find_local_user(&user).await? {
        if cache::verify_password(&password, &cached.password) {
            return Ok(LoginResult::offline(cached));
        }
        return Err("密码错误".into());
    }

    // 3. 无记录 + admin → 录入界面(不算登录)
    if user == "admin" && password == "admin11" {
        return Ok(LoginResult::init());
    }

    // 4. 其他 → 拒绝
    Err("连接服务器失败".into())
}

fn is_server_offline(e: &anyhow::Error) -> bool {
    e.downcast_ref::<reqwest::Error>()
        .map(|re| re.is_connect() || re.is_timeout())
        .unwrap_or(false)
}

缓存

rust 复制代码
pub async fn save_local_user(resp: &LoginResponse, password: &str) -> anyhow::Result<()> {
    let conn = open_cache().await?;
    let hash = bridge::auth::password::hash_password(password)?;
    let now = bridge::time::now_millis();

    conn.execute(
        "INSERT OR REPLACE INTO local_users \
         (user_id, name, password, permissions_json, last_online_at, token, token_exp) \
         VALUES (?, ?, ?, ?, ?, ?, ?)",
        params![
            resp.user_id,
            resp.name,
            hash,
            serde_json::to_string(&resp.permissions)?,
            now,
            resp.token,
            resp.token_exp,
        ],
    )?;
    Ok(())
}

pub async fn find_local_user(name: &str) -> anyhow::Result<Option<LocalUser>> {
    let conn = open_cache().await?;
    let mut stmt = conn.prepare(
        "SELECT user_id, name, password, permissions_json, last_online_at, token, token_exp \
         FROM local_users WHERE name = ?",
    )?;
    let row = stmt.query_row([name], |r| {
        Ok(LocalUser {
            user_id: r.get(0)?,
            name: r.get(1)?,
            password: r.get(2)?,
            permissions_json: r.get(3)?,
            last_online_at: r.get(4)?,
            token: r.get(5)?,
            token_exp: r.get(6)?,
        })
    }).optional()?;
    Ok(row)
}

pub fn verify_password(input: &str, stored: &str) -> bool {
    bridge::auth::password::verify_password(input, stored)
}

九、录入界面

触发条件

唯一 :服务器离线 + 本机无记录 + admin + admin11

功能

步骤 操作
① 建库(跑迁移)
② 导入用户 Excel
③ 导入设备 Excel
④ 编辑设备(补全半成品)
⑤ 校验最低条件
⑥ 完成 → 提示拷贝

特点

  • 无入口:只在特定条件下触发
  • 一次性:用完不再进
  • 跳过常规检查:数据直接落库
  • 幂等:库已存在时继续编辑,不清空

退出

  • "重新登录" → app.restart() → 回登录页

十、离线模式

缓存内容

表 内容
local_users 本机登录过的用户
cached_pcs 自己权限内的 PC
cached_messages 消息历史
pending_changes 离线暂存的修改

能力

操作 离线可用
查看缓存数据 是
修改数据 暂存,上线后提交
提交建议 暂存
查看新消息 否

上线同步

离线期间的修改写入 pending_changes,上线后逐条提交服务器,走常规流程(IT 确认等)。

十一、极端情况:库损坏需重建

  1. 停服务器
  2. 删除 data_service.db
  3. 清空客户端 local_users(含 admin 记录)
  4. admin 离线登录 → 触发录入界面
  5. 重新导入

属于灾难恢复,非常规操作。

十二、核心原则

原则 说明
先在线,后离线 服务器响应优先,离线是兜底
本机记录是服务器正常的侧面证明 有记录 = 曾连过服务器 = 数据可信
admin 是最后的兜底 只在"无记录 + 服务器离线"时触发录入
录入界面不留入口 一次性通道,数据跳过常规检查
权限交集 登录人权限 ∩ 机器拥有人权限
在线登录写缓存 每次在线登录更新 local_users
admin 短接不算登录 不写 local_users

十三、待讨论

  • permissions_json 的具体结构
相关推荐
clz13145211 小时前
Dify的Windows平台部署:从Docker安装到Dify运行
windows·docker·容器
蜗牛互联网1 小时前
Python Agent 工具调用:审批令牌、参数白名单和累计预算
java·开发语言·人工智能·windows·后端·python
william_yangshun3 小时前
WordCraft 观察:纯 Rust 重写文字处理器,把文档变成可编程对象
开发语言·后端·rust·文档自动化
geovindu3 小时前
rust: pool Pattern (续)
开发语言·后端·设计模式·rust·对象池简写模式
夏虫不可与之言冰3 小时前
使用Flash Download Tool给ESP32烧录固件
c语言·c++·windows·单片机·物联网·iot
滕州市燕猫虎计算机科技工作室个体工商户13 小时前
《Rust程序设计》学习笔记八:结构体
笔记·学习·rust·rust程序设计
jack1868916 小时前
Devin 智能软件工程师实战应用指南
windows
Hello_Pyhx19 小时前
WinDirStat 2.9.3:从目录扫描到可复查的占用报表
windows·mac·垃圾清理
jun_bai20 小时前
使用Aspose实现word文档合并,内容替换
windows·c#·word