feat: 设置页新增登录管理 TAB

- Session 结构体新增 IP、UserAgent、Remark 字段,登录/注册时记录设备信息
- Store 接口新增 ListByUID、DeleteByUIDExclude 方法,MemoryStore 完整实现
- SessionManager 新增 ListByUID、DestroyOtherByUID、UpdateRemark 方法
- 新增 UA 轻量解析器(session/ua.go),提取浏览器/OS/设备类型
- 新增 settings_api_sessions.go,提供列表/踢出/一键踢出/备注 4 个 API
- 控制器层声明 sessionManager 最小接口(ISP),SettingsController 注入依赖
- Auth 中间件注入 sid 到 gin context,支持识别当前会话
- 设置页左侧导航增加登录管理入口,tab 白名单新增 sessions
- 前端实现设备列表展示(浏览器/OS/设备图标/IP/时间)、当前设备标识、备注输入、踢出按钮、一键踢出
- settings.css 新增会话管理全套样式及响应式适配
This commit is contained in:
2026-05-30 23:58:37 +08:00
parent e3853071d6
commit d203447eb3
15 changed files with 884 additions and 13 deletions

97
internal/session/ua.go Normal file
View File

@ -0,0 +1,97 @@
package session
import "strings"
// DeviceInfo 从 User-Agent 解析出的设备摘要信息
type DeviceInfo struct {
Device string // 设备类型Desktop / Mobile / Tablet
OS string // 操作系统Windows 10 / macOS / Android 12 等
Browser string // 浏览器Chrome 120 / Firefox 121 / Safari 17 等
}
// ParseUA 从原始 User-Agent 字符串解析设备信息(轻量级,不引入第三方库)
func ParseUA(ua string) DeviceInfo {
info := DeviceInfo{Device: "Desktop"}
lower := strings.ToLower(ua)
// --- 操作系统 ---
switch {
case strings.Contains(lower, "windows nt 10"):
info.OS = "Windows 10/11"
case strings.Contains(lower, "windows nt 6.3"):
info.OS = "Windows 8.1"
case strings.Contains(lower, "windows nt 6.2"):
info.OS = "Windows 8"
case strings.Contains(lower, "windows nt 6.1"):
info.OS = "Windows 7"
case strings.Contains(lower, "windows"):
info.OS = "Windows"
case strings.Contains(lower, "mac os x"):
info.OS = "macOS"
if strings.Contains(lower, "iphone") {
info.OS = "iOS"
info.Device = "Mobile"
} else if strings.Contains(lower, "ipad") {
info.OS = "iPadOS"
info.Device = "Tablet"
}
case strings.Contains(lower, "android"):
info.OS = "Android"
info.Device = "Mobile"
if strings.Contains(lower, "tablet") || strings.Contains(lower, "ipad") {
info.Device = "Tablet"
}
case strings.Contains(lower, "iphone"):
info.OS = "iOS"
info.Device = "Mobile"
case strings.Contains(lower, "ipad"):
info.OS = "iPadOS"
info.Device = "Tablet"
case strings.Contains(lower, "linux"):
info.OS = "Linux"
case strings.Contains(lower, "cros"):
info.OS = "ChromeOS"
default:
info.OS = "Unknown"
}
// --- 浏览器 ---
switch {
case strings.Contains(lower, "edg/"):
info.Browser = "Edge"
info.Browser += extractVersion(ua, "Edg/")
case strings.Contains(lower, "chrome/") && !strings.Contains(lower, "edg/"):
info.Browser = "Chrome"
info.Browser += extractVersion(ua, "Chrome/")
case strings.Contains(lower, "firefox/"):
info.Browser = "Firefox"
info.Browser += extractVersion(ua, "Firefox/")
case strings.Contains(lower, "safari/") && !strings.Contains(lower, "chrome/"):
info.Browser = "Safari"
info.Browser += extractVersion(ua, "Version/")
default:
info.Browser = "Unknown"
}
return info
}
// extractVersion 从 UA 中提取主版本号
func extractVersion(ua, prefix string) string {
idx := strings.Index(ua, prefix)
if idx == -1 {
return ""
}
rest := ua[idx+len(prefix):]
dot := strings.Index(rest, ".")
if dot == -1 {
// 无点号,取到空格或结尾
sp := strings.Index(rest, " ")
if sp > 0 {
return " " + rest[:sp]
}
return ""
}
return " " + rest[:dot]
}