diff --git a/internal/session/manager.go b/internal/session/manager.go
index 4959a07..7667d42 100644
--- a/internal/session/manager.go
+++ b/internal/session/manager.go
@@ -2,6 +2,7 @@ package session
import (
"log"
+ "sync"
"time"
"metazone.cc/metalab/internal/config"
@@ -15,18 +16,46 @@ type userStore interface {
// Manager 会话管理器:创建、验证、销毁、续期
type Manager struct {
- store Store
- userRepo userStore
- cfg *config.Config
+ store Store
+ userRepo userStore
+ cfg *config.Config
+ mu sync.Mutex
+ recentLogin map[uint]*loginEntry // uid → 最近一次登录记录(防重复创建)
+}
+
+type loginEntry struct {
+ sid string
+ ip string
+ userAgent string
+ createdAt time.Time
}
// NewManager 构造函数
func NewManager(store Store, userRepo userStore, cfg *config.Config) *Manager {
- return &Manager{store: store, userRepo: userRepo, cfg: cfg}
+ return &Manager{
+ store: store,
+ userRepo: userRepo,
+ cfg: cfg,
+ recentLogin: make(map[uint]*loginEntry),
+ }
}
-// Create 创建会话并写入存储
+// Create 创建会话并写入存储(同用户+同 IP+同 UA 在 10 秒内重复调用时返回已有会话,防重复创建)
func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string) (string, error) {
+ m.mu.Lock()
+ // 幂等检查:同用户+同 IP+同 UA,10 秒内不重复创建
+ if entry, ok := m.recentLogin[user.ID]; ok {
+ if entry.ip == ip && entry.userAgent == userAgent && time.Since(entry.createdAt) < 10*time.Second {
+ // 验证旧会话仍然有效
+ if s, _ := m.store.Get(entry.sid); s != nil {
+ m.mu.Unlock()
+ log.Printf("[SessionManager] Create dedup uid=%d reuse sid=%s", user.ID, entry.sid[:16]+"...")
+ return entry.sid, nil
+ }
+ }
+ }
+ m.mu.Unlock()
+
sid, err := NewID()
if err != nil {
return "", err
@@ -47,6 +76,11 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
if err := m.store.Set(s); err != nil {
return "", err
}
+
+ m.mu.Lock()
+ m.recentLogin[user.ID] = &loginEntry{sid: sid, ip: ip, userAgent: userAgent, createdAt: now}
+ m.mu.Unlock()
+
log.Printf("[SessionManager] Created sid=%s uid=%d rememberMe=%v ip=%s", sid[:16]+"...", user.ID, rememberMe, ip)
return sid, nil
}
diff --git a/templates/MetaLab-2026/html/settings/index.html b/templates/MetaLab-2026/html/settings/index.html
index 7dcf627..4c7f9e1 100644
--- a/templates/MetaLab-2026/html/settings/index.html
+++ b/templates/MetaLab-2026/html/settings/index.html
@@ -125,7 +125,7 @@
最后活动 {{.LastAccess.Format "2006-01-02 15:04"}}
diff --git a/templates/MetaLab-2026/static/css/settings.css b/templates/MetaLab-2026/static/css/settings.css
index 748fc7c..7e5a215 100644
--- a/templates/MetaLab-2026/static/css/settings.css
+++ b/templates/MetaLab-2026/static/css/settings.css
@@ -690,15 +690,15 @@ body {
background: #fff;
}
-.session-remark-input[readonly] {
- background: transparent;
- border-color: transparent;
- cursor: default;
+.session-remark-input[data-current="true"] {
+ background: rgba(46, 204, 113, .03);
+ border-color: rgba(46, 204, 113, .2);
}
-.session-remark-input[readonly]:focus {
- box-shadow: none;
- border-color: transparent;
+.session-remark-input[data-current="true"]:focus {
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, .15);
+ background: #fff;
}
.session-action {
diff --git a/templates/MetaLab-2026/static/js/login.js b/templates/MetaLab-2026/static/js/login.js
index 28018ab..3976053 100644
--- a/templates/MetaLab-2026/static/js/login.js
+++ b/templates/MetaLab-2026/static/js/login.js
@@ -55,7 +55,14 @@
doLogin(email, password);
});
+ var loginInProgress = false;
+
function doLogin(email, password) {
+ if (loginInProgress) return;
+ loginInProgress = true;
+ submitBtn.disabled = true;
+ submitBtn.textContent = '登录中...';
+
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/auth/login', true);
xhr.setRequestHeader('Content-Type', 'application/json');
@@ -65,7 +72,9 @@
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
if (xhr.status === 200 && resp && resp.success && resp.action === 'confirm_restore') {
- // 注销账号登录 → 二次确认恢复
+ loginInProgress = false;
+ submitBtn.disabled = false;
+ submitBtn.textContent = '登录';
if (confirm(resp.message + '\n\n确认继续登录将中止注销流程。')) {
doConfirmRestore(email, password);
}
@@ -75,6 +84,9 @@
window.location.href = '/';
}, 800);
} else {
+ loginInProgress = false;
+ submitBtn.disabled = false;
+ submitBtn.textContent = '登录';
var msg = (resp && resp.message) ? resp.message : '登录失败,请稍后重试';
window.MetaLab.toast(toast, msg, true);
}
@@ -84,6 +96,11 @@
}
function doConfirmRestore(email, password) {
+ if (loginInProgress) return;
+ loginInProgress = true;
+ submitBtn.disabled = true;
+ submitBtn.textContent = '恢复中...';
+
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/auth/confirm-restore', true);
xhr.setRequestHeader('Content-Type', 'application/json');
@@ -98,6 +115,9 @@
window.location.href = '/';
}, 800);
} else {
+ loginInProgress = false;
+ submitBtn.disabled = false;
+ submitBtn.textContent = '登录';
var msg = (resp && resp.message) ? resp.message : '操作失败,请稍后重试';
window.MetaLab.toast(toast, msg, true);
}