Skip to content

Commit 4fcb44b

Browse files
committed
feat: 实现设备管理与会话管理功能
## 新增功能 ### 后端 - 新增 Redis 会话缓存 (app/core/redis.py) - 连接池管理,熔断机制 - cache_session, get_cached_session, delete_cached_session - 新增会话认证依赖 (app/api/deps.py) - Redis 缓存优先查询 - 防抖机制 (5分钟节流) - IP 严格模式支持 - 新增用户会话 API (app/api/user_sessions.py) - GET /users/me/sessions - 获取会话列表 - DELETE /users/me/sessions/{id} - 删除单个会话 - DELETE /users/me/sessions - 删除所有其他会话 - POST /users/me/sessions/refresh - 刷新会话 - 新增用户会话模型 (app/models/user_session.py) - 设备 ID、IP 地址追踪 - last_active_at, last_ip_address 字段 - 新增拼音工具 (app/services/pinyin_utils.py) - 登录接口增强 (app/api/users.py) - 设备 ID/名称收集 - 登录频率限制 - IP 设备数量限制 - Cookie-based 认证 ### 前端 - 新增设备管理页面 (DeviceManagement.tsx) - 会话列表展示 - 踢出单个/全部设备 - 会话刷新 - 新增设备 ID 工具 (lib/deviceId.ts) - API 客户端增强 (client.ts) - SessionInfo 类型定义 - sessionAPI (list/delete/deleteAll/refresh) - 布局增强 (Layout.tsx) - 设备管理入口 - 用户信息显示 ## 性能优化 - Redis 缓存优先策略 (0 DB 消耗目标) - 防抖前置:仅在必要时更新数据库 - 后台任务异步写库 ## 重构 - 表格组件重构 (DataTable.tsx) - 清单页面优化 (Inventory.tsx) - 已完成任务文档移至 docs/done/
1 parent 7e79dd5 commit 4fcb44b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3546
-1411
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ env/
2424
!.vscode/extensions.json
2525
.idea/
2626
.cursor/
27+
.history/
28+
.claude/
29+
.kilocode/
30+
.sonar/
31+
.docs/
2732
*.sw?
2833
*.swo
2934

.kilocode/mcp.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","D:\\Code"],"disabled":false,"alwaysAllow":["read_text_file","read_multiple_files","read_file"]}}}
1+
{
2+
"mcpServers": {}
3+
}

.kilocode/rules/GIT_STRATEGY.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Git 分支管理规范
22

33
不要更改此文档!
4+
不要删除任何未提交的文件除非我同意,危险命令:git clean
45
本项目采用 Git Flow 工作流,包含以下分支类型:
6+
57
## 注意事项
68

79
1. **禁止在 main 分支直接开发**
@@ -11,28 +13,27 @@
1113

1214
## 分支类型
1315

14-
| 分支 | 用途 | 生命周期 | 合并目标 |
15-
|-----|------|---------|---------|
16-
| `main` | 生产环境,包含稳定发布版本 | 长期 | - |
17-
| `develop` | 开发主分支,包含最新开发成果 | 长期 | `main` |
18-
| `feature/*` | 新功能开发 | 临时 | `develop` |
19-
| `release/*` | 发布前准备(bug修复、文档更新) | 临时 | `main` + `develop` |
20-
| `hotfix/*` | 生产环境紧急修复 | 临时 | `main` + `develop` |
16+
| 分支 | 用途 | 生命周期 | 合并目标 |
17+
| ------------- | ------------------------------- | -------- | ---------------------- |
18+
| `main` | 生产环境,包含稳定发布版本 | 长期 | - |
19+
| `develop` | 开发主分支,包含最新开发成果 | 长期 | `main` |
20+
| `feature/*` | 新功能开发 | 临时 | `develop` |
21+
| `release/*` | 发布前准备(bug修复、文档更新) | 临时 | `main` + `develop` |
22+
| `hotfix/*` | 生产环境紧急修复 | 临时 | `main` + `develop` |
2123

2224
## 提交规范
2325

24-
| 类型 | 说明 |
25-
|-----|------|
26-
| `feat` | 新功能 |
27-
| `fix` | Bug修复 |
28-
| `docs` | 文档更新 |
29-
| `refactor` | 代码重构 |
30-
| `perf` | 性能优化 |
31-
| `chore` | 构建/工具变动 |
26+
| 类型 | 说明 |
27+
| ------------ | ------------- |
28+
| `feat` | 新功能 |
29+
| `fix` | Bug修复 |
30+
| `docs` | 文档更新 |
31+
| `refactor` | 代码重构 |
32+
| `perf` | 性能优化 |
33+
| `chore` | 构建/工具变动 |
3234

3335
格式:`类型: 简短描述`
3436

35-
3637
## 命名规范
3738

3839
```
@@ -46,6 +47,7 @@ hotfix/security-fix
4647
## 工作流程
4748

4849
### 功能开发 (Feature)
50+
4951
```bash
5052
# 1. 创建功能分支
5153
git checkout develop
@@ -62,6 +64,7 @@ git push origin develop
6264
```
6365

6466
### 发布准备 (Release)
67+
6568
```bash
6669
# 1. 创建 release 分支
6770
git checkout develop
@@ -82,6 +85,7 @@ git push origin develop
8285
```
8386

8487
### 紧急修复 (Hotfix)
88+
8589
```bash
8690
# 1. 创建 hotfix 分支
8791
git checkout main
@@ -99,5 +103,3 @@ git checkout develop
99103
git merge --no-ff hotfix/xxx
100104
git push origin develop
101105
```
102-
103-

.kilocode/rules/Rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
1. **Concurrency**: 初始化 SQLite 时必须启用 **WAL Mode**
99
2. **CAS Normalization**: 所有涉及 CAS 号的输入,必须在后端进行标准化清洗(去除空格、大写)。这是系统的防重基石。
1010
3. **Image Optimization**: 禁止将图片存入数据库 Blob。必须在后端使用 Pillow 压缩至 <100KB 并存入文件系统。
11-
4. **No Mobile Dependency**: 系统设计不依赖扫码枪或手机摄像头。所有流程闭环在 PC/平板 Web 端完成。
11+
4. **不要删除任何未提交的文件除非我同意,危险命令:git clean**
1212
5. **Git Commit**: 完成重大修改后,切换到Review模式在当前对话进行Code Review,给出报告与我进行讨论,讨论完成后更新`Readme.md`,更新`milestone.md`,之后执行git提交,注意分支和提交信息。
1313
6. **Chinese**: 前端使用中文展示(除英文名称等),后端保存用英文方便管理(除中文名称等),因此需要添加映射表
1414
7. **Debug**:我会将我自己发现的问题记录在 `BUGS.md`,在debug时请做好相关记录,你进行代码审查时发现的问题也要写入此文档

app/api/deps.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import hashlib
2+
from datetime import datetime, timezone
3+
from typing import Optional
4+
5+
from fastapi import BackgroundTasks, Depends, HTTPException, Request, status
6+
from sqlmodel import Session, select
7+
8+
from app.core.config import settings
9+
from app.core.redis import cache_session, delete_cached_session, get_cached_session
10+
from app.database import get_db, engine # 必须引入 engine 供后台任务使用
11+
from app.models.user import User
12+
from app.models.user_session import UserSession
13+
14+
def compute_token_hash(token: str) -> str:
15+
"""计算 Token 的 SHA-256 哈希值"""
16+
return hashlib.sha256(token.encode()).hexdigest()
17+
18+
def _update_activity_task(session_id: int, current_ip: str) -> None:
19+
"""
20+
后台同步更新任务:更新最后活跃时间与最后活动 IP。
21+
由 FastAPI 放入线程池运行,不会阻塞主事件循环。
22+
(防抖逻辑已前置到主流程,这里直接执行 DB 操作)
23+
"""
24+
with Session(engine) as db:
25+
session = db.get(UserSession, session_id)
26+
if session:
27+
session.last_active_at = datetime.now(timezone.utc)
28+
# 如果活动 IP 变了,记录最新的 IP
29+
if current_ip and session.last_ip_address != current_ip:
30+
session.last_ip_address = current_ip
31+
db.add(session)
32+
db.commit()
33+
34+
35+
def get_current_session(
36+
request: Request,
37+
background_tasks: BackgroundTasks,
38+
db: Session = Depends(get_db)
39+
) -> tuple[User, UserSession]:
40+
"""
41+
获取当前认证用户和会话
42+
"""
43+
credentials_exception = HTTPException(
44+
status_code=status.HTTP_401_UNAUTHORIZED,
45+
detail="Could not validate credentials",
46+
headers={"WWW-Authenticate": "Bearer"},
47+
)
48+
49+
# 1. 提取 Token
50+
token = request.cookies.get("access_token")
51+
if not token:
52+
auth_header = request.headers.get("Authorization")
53+
if auth_header and auth_header.startswith("Bearer "):
54+
token = auth_header[7:]
55+
56+
if not token:
57+
raise credentials_exception
58+
59+
# 2. 计算 Hash 并获取 IP
60+
token_hash = compute_token_hash(token)
61+
client_ip = request.client.host if request.client else "unknown"
62+
63+
# 3. Redis 缓存优先查询 (0 DB 消耗目标)
64+
cached_data = get_cached_session(token_hash)
65+
if cached_data:
66+
expires_at = datetime.fromisoformat(cached_data["expires_at"])
67+
if expires_at < datetime.now(timezone.utc):
68+
delete_cached_session(token_hash)
69+
raise HTTPException(
70+
status_code=status.HTTP_401_UNAUTHORIZED,
71+
detail="Session expired"
72+
)
73+
74+
# 严格 IP 绑定检查:对比的是首次登录的 ip_address
75+
if cached_data.get("ip_address") != client_ip and settings.session_strict_ip:
76+
raise HTTPException(
77+
status_code=status.HTTP_401_UNAUTHORIZED,
78+
detail="IP address changed"
79+
)
80+
81+
if not cached_data.get("is_active", True):
82+
delete_cached_session(token_hash)
83+
raise credentials_exception
84+
85+
# 从缓存恢复 User 和 Session
86+
user = User(
87+
id=cached_data["user_id"],
88+
username=cached_data["username"],
89+
is_active=cached_data.get("is_active", True)
90+
)
91+
92+
session = UserSession(
93+
id=cached_data.get("session_id"),
94+
user_id=user.id,
95+
device_id=cached_data.get("device_id"),
96+
device_name=cached_data.get("device_name"),
97+
ip_address=cached_data.get("ip_address"),
98+
last_ip_address=cached_data.get("last_ip_address", client_ip), # 优先从缓存取
99+
user_agent=cached_data.get("user_agent", ""),
100+
token_hash=token_hash,
101+
expires_at=expires_at,
102+
last_active_at=datetime.fromisoformat(cached_data["last_active_at"]) if cached_data.get("last_active_at") else None
103+
)
104+
105+
# --- 核心优化点:防抖前置 ---
106+
# 只有距离上次记录超过 5 分钟,或者 IP 发生了变动,才去更新 Redis 和 数据库
107+
needs_update = False
108+
now_utc = datetime.now(timezone.utc)
109+
110+
if session.last_active_at:
111+
if (now_utc - session.last_active_at).total_seconds() >= 300:
112+
needs_update = True
113+
else:
114+
needs_update = True
115+
116+
if session.last_ip_address != client_ip:
117+
needs_update = True
118+
119+
if needs_update:
120+
# 1. 触发后台写库任务(此时才触发,极大缓解并发写库问题)
121+
background_tasks.add_task(_update_activity_task, session.id, client_ip)
122+
123+
# 2. 刷新 Redis 缓存(不仅续期,也更新最后活跃时间和最后 IP)
124+
cached_data["last_active_at"] = now_utc.isoformat()
125+
cached_data["last_ip_address"] = client_ip
126+
cache_session(
127+
token_hash,
128+
cached_data,
129+
int((expires_at - now_utc).total_seconds())
130+
)
131+
132+
# 同步更新当前流程内存对象的值
133+
session.last_active_at = now_utc
134+
session.last_ip_address = client_ip
135+
136+
return user, session
137+
138+
# 4. 缓存未命中:从数据库查询
139+
session = db.exec(select(UserSession).where(UserSession.token_hash == token_hash)).first()
140+
141+
if not session:
142+
raise credentials_exception
143+
144+
if session.expires_at < datetime.now(timezone.utc):
145+
db.delete(session)
146+
db.commit()
147+
raise HTTPException(status_code=401, detail="Session expired")
148+
149+
if session.ip_address != client_ip and settings.session_strict_ip:
150+
raise HTTPException(status_code=401, detail="IP address changed")
151+
152+
user = db.get(User, session.user_id)
153+
if not user or not user.is_active:
154+
raise credentials_exception
155+
156+
# 缓存未命中时,说明刚登录或者缓存刚过期,直接在当前流程更新
157+
session.last_active_at = datetime.now(timezone.utc)
158+
session.last_ip_address = client_ip
159+
db.add(session)
160+
db.commit()
161+
db.refresh(session)
162+
163+
# 5. 回填 Redis 缓存(加入 last_ip_address)
164+
cache_session(
165+
token_hash,
166+
{
167+
"session_id": session.id,
168+
"user_id": user.id,
169+
"username": user.username,
170+
"is_active": user.is_active,
171+
"device_id": session.device_id,
172+
"device_name": session.device_name,
173+
"ip_address": session.ip_address,
174+
"last_ip_address": session.last_ip_address, # 缓存 last_ip
175+
"user_agent": session.user_agent,
176+
"expires_at": session.expires_at.isoformat(),
177+
"last_active_at": session.last_active_at.isoformat(),
178+
},
179+
int((session.expires_at - datetime.now(timezone.utc)).total_seconds())
180+
)
181+
182+
return user, session

0 commit comments

Comments
 (0)