一、决策树
登录
↓
尝试连服务器
↓
┌──────────────────┬──────────────────┐
│ 服务器响应 │ 服务器离线 │
↓ ↓
在线登录 查 local_users
(含 admin) ↓
写 local_users ┌──────────────┬──────────────┐
│ 有记录 │ 无记录 │
↓ ↓
验本地密码 admin + admin11?
↓ ↓
┌──────┬──────┐ ┌──────┬──────┐
│ 对 │ 错 │ │ 是 │ 否 │
↓ ↓ ↓ ↓ ↓
进主界面 拒绝 录入界面 拒绝
按权限显示 (不算登录)
二、各分支说明
| 分支 |
条件 |
结果 |
写缓存 |
| 在线登录 |
服务器响应 |
进主界面,按权限显示 |
是 |
| 离线登录 |
服务器离线 + 本机有记录 + 密码对 |
进主界面,按权限显示 |
否 |
| 录入界面 |
服务器离线 + 本机无记录 + admin |
进录入界面 |
否 |
| 拒绝 |
其他 |
报错 |
否 |
三、admin 的两条路径
| 路径 |
触发 |
算登录 |
写 local_users |
| 在线登录 |
服务器响应 |
是 |
是 |
| 录入界面短接 |
服务器离线 + 本机无记录 |
否 |
否 |
关键:录入界面是一次性应急通道,不算登录,不写缓存。
四、local_users 表
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。
八、核心代码骨架
登录
#[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)
}
缓存
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 确认等)。
十一、极端情况:库损坏需重建
- 停服务器
- 删除
data_service.db
- 清空客户端
local_users(含 admin 记录)
- admin 离线登录 → 触发录入界面
- 重新导入
属于灾难恢复,非常规操作。
十二、核心原则
| 原则 |
说明 |
| 先在线,后离线 |
服务器响应优先,离线是兜底 |
| 本机记录是服务器正常的侧面证明 |
有记录 = 曾连过服务器 = 数据可信 |
| admin 是最后的兜底 |
只在"无记录 + 服务器离线"时触发录入 |
| 录入界面不留入口 |
一次性通道,数据跳过常规检查 |
| 权限交集 |
登录人权限 ∩ 机器拥有人权限 |
| 在线登录写缓存 |
每次在线登录更新 local_users |
| admin 短接不算登录 |
不写 local_users |
十三、待讨论