【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 的具体结构
相关推荐
共享家952710 小时前
408 操作系统第一章
服务器·数据库·windows
Ramble_Naylor10 小时前
Rust深度解读:Send 与 Sync 的本质与线程安全边界
python·算法·rust
Sakura_j_h_11 小时前
GitHub Actions 打包 Windows exe:中文编码与产物上传
windows·ci/cd·打包发布·github·pyinstaller·github actions
Ramble_Naylor13 小时前
Rust深度解读:异步流(Async Streams)与生成器的处理艺术
开发语言·后端·rust
xixiaoyunya14 小时前
搭一个浏览器即用的二进制文件分发点:目录规范与权限边界实践
网络·windows
麻瓜code15 小时前
Spring AI 全模块 API 字典一览(1.1.0 版)
人工智能·windows·spring
zrc00700716 小时前
steam无法登录 steam错误 登录时出现错误。请稍后再试。解决办法 steam无法登录解决办法 湖南地区steam DNS问题解决办法
windows·经验分享·游戏·缓存·游戏程序·创业创新·玩游戏
BK贝壳17 小时前
布尔队列 BHA_Queue:入队出队均摊 O(1);附 BHA_Dict
windows
-星空下无敌17 小时前
Windows Docker K8s 安装部署Naocs ,SpringBoot 3.x 4.x 整合Naocs 版本兼容 配置中心、服务注册与发现
windows·docker·nacos·kubernetes·springboot·alibaba
燃烧的枫17 小时前
Windows 下 YOLO11 Pose 的 TensorRT C++ 部署
windows·yolo·tensorrt·1024程序员节·yolo11-pose