feat: 新增管理员用户管理功能与账号封禁体系

此提交完成了全站管理员后台核心功能开发:
1.  新增AdminUserService与管理接口,支持用户列表查询、角色变更与封禁解封
2.  重构认证链路,统一账号封禁错误码与处理逻辑
3.  新增全局强制下线机制,账号被封禁时自动弹出告知弹窗并清理登录态
4.  重构管理员后台路由与布局,统一权限校验逻辑
5.  优化通知轮询逻辑,页面切回前台时立即刷新未读计数
6.  合并原公告、外观管理页到统一后台面板
This commit is contained in:
2026-09-15 02:52:26 +08:00
parent 24c6dea38c
commit 6da4309453
20 changed files with 2248 additions and 105 deletions

View File

@@ -126,7 +126,7 @@ func (s *AuthService) Login(username, password string) (string, string, *model.U
}
// 封禁判定放在密码比较之后,同样避免时序差异
if user.Banned {
return "", "", nil, errors.New("账号已被封禁")
return "", "", nil, ErrAccountBanned
}
accessToken, err := s.generateToken(&user)
@@ -184,13 +184,14 @@ func (s *AuthService) ValidateClaims(claims *UserClaims) (*model.User, error) {
if err := s.db.First(&user, claims.ID).Error; err != nil {
return nil, errors.New("用户不存在")
}
// 封禁优先于版本判定:封禁必然伴随 token_version 递增,
// 但前端需要明确知道"被封禁"而非笼统的登录失效
if user.Banned {
return nil, ErrAccountBanned
}
// token 版本不匹配 → 已被撤销(改密码/封禁/管理员操作)
if user.TokenVersion != claims.TokenVersion {
return nil, errors.New("token 已失效")
}
// 实时校验封禁状态(不依赖 JWT 中的缓存值)
if user.Banned {
return nil, errors.New("账号已被封禁")
return nil, ErrTokenRevoked
}
return &user, nil
}
@@ -218,6 +219,14 @@ func generateRandomToken() string {
return base64.URLEncoding.EncodeToString(b)
}
// 账号级登录态错误:前端据此区分"被封禁强制下线"与普通登录过期
var (
// ErrAccountBanned 账号已被封禁(登录/刷新/鉴权全链路统一返回,便于前端识别并告知用户)
ErrAccountBanned = errors.New("账号已被封禁")
// ErrTokenRevoked 凭据版本失配:改密/管理员强制下线等导致旧 JWT 立即作废
ErrTokenRevoked = errors.New("登录态已失效")
)
// refresh token 相关错误
var (
ErrRefreshInvalid = errors.New("refresh token 无效")
@@ -299,7 +308,7 @@ func (s *AuthService) loadActiveUser(tx *gorm.DB, userID uint) (*model.User, err
return nil, errors.New("用户不存在")
}
if user.Banned {
return nil, errors.New("账号已被封禁")
return nil, ErrAccountBanned
}
return &user, nil
}
@@ -311,6 +320,12 @@ func (s *AuthService) ValidateRefreshToken(token string) (*model.User, error) {
return nil, ErrRefreshInvalid
}
if rt.Revoked {
// 撤销常源于封禁/改密:补查封禁状态,让被封禁用户的刷新请求
// 得到可识别的原因(前端据此弹封禁提示,而非笼统的登录过期)
var owner model.User
if err := s.db.Select("banned").First(&owner, rt.UserID).Error; err == nil && owner.Banned {
return nil, ErrAccountBanned
}
return nil, errors.New("refresh token 已撤销")
}
if time.Now().After(rt.ExpiresAt) {