合并 main 分支并融合订单列表最小化与 SQL 治理
- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义 - 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试) - 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知 - 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
@@ -134,6 +134,10 @@ func (h *Handler) Refresh(c *gin.Context) {
|
||||
}
|
||||
tokens, err := h.service.RefreshToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrDependencyUnavailable) {
|
||||
response.ServiceUnavailable(c, "用户认证服务暂时不可用")
|
||||
return
|
||||
}
|
||||
response.Unauthorized(c, "刷新令牌无效或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,14 +32,19 @@ func (r *UserRepository) FindActiveForToken(ctx context.Context, id uint64, toke
|
||||
if r == nil || r.db == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
user, err := r.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
var user model.User
|
||||
if err := r.db.WithContext(ctx).
|
||||
Select("id, phone, status, token_version").
|
||||
First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Status != "active" || user.TokenVersion <= 0 || user.TokenVersion != tokenVersion {
|
||||
if user.Status != "active" {
|
||||
return nil, ErrUserDisabled
|
||||
}
|
||||
return user, nil
|
||||
if user.TokenVersion != tokenVersion {
|
||||
return nil, ErrTokenVersionMismatch
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateProfile(ctx context.Context, id uint64, nickname string, avatarURL string) (*model.User, error) {
|
||||
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
ErrPasswordTooWeak = errors.New("password too weak")
|
||||
ErrLoginLocked = errors.New("login locked")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrTokenVersionMismatch = errors.New("user token version mismatch")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -180,7 +181,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (
|
||||
return LoginResult{}, ErrUserDisabled
|
||||
}
|
||||
|
||||
tokens, err := s.issueTokenPair(user)
|
||||
tokens, err := s.generateUserTokenPair(user)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
@@ -190,7 +191,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (
|
||||
}
|
||||
|
||||
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (TokenPair, error) {
|
||||
if s.users == nil {
|
||||
if s.jwt == nil || s.users == nil {
|
||||
return TokenPair{}, ErrDependencyUnavailable
|
||||
}
|
||||
claims, err := s.jwt.ParseSubject(refreshToken, tokenTypeRefresh, "user")
|
||||
@@ -201,15 +202,18 @@ func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (TokenP
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
}
|
||||
return s.issueTokenPair(user)
|
||||
return s.generateUserTokenPair(user)
|
||||
}
|
||||
|
||||
func (s *Service) issueTokenPair(user *model.User) (TokenPair, error) {
|
||||
func (s *Service) generateUserTokenPair(user *model.User) (TokenPair, error) {
|
||||
if s.jwt == nil {
|
||||
return TokenPair{}, ErrDependencyUnavailable
|
||||
}
|
||||
return s.jwt.GenerateSubjectPairWithVersion(user.ID, user.Phone, "user", user.TokenVersion)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, userID uint64) error {
|
||||
if s.users == nil {
|
||||
if s.jwt == nil || s.users == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.users.RevokeTokens(ctx, userID)
|
||||
@@ -282,7 +286,7 @@ func (s *Service) LoginWithPassword(ctx context.Context, phone, password, client
|
||||
|
||||
_ = clearLoginFailure(ctx, s.redis, phone, clientIP)
|
||||
|
||||
tokens, err := s.issueTokenPair(user)
|
||||
tokens, err := s.generateUserTokenPair(user)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
@@ -387,7 +391,7 @@ func (s *Service) RegisterWithPassword(ctx context.Context, phone, code, passwor
|
||||
return LoginResult{}, ErrUserDisabled
|
||||
}
|
||||
|
||||
tokens, err := s.issueTokenPair(user)
|
||||
tokens, err := s.generateUserTokenPair(user)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestUserTokenVersionRevokesAccessAndRefreshTokens(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败:%v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.User{}); err != nil {
|
||||
t.Fatalf("数据库迁移失败:%v", err)
|
||||
}
|
||||
|
||||
user := model.User{Phone: "13900000001", Status: "active", TokenVersion: 1}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatalf("创建用户失败:%v", err)
|
||||
}
|
||||
repo := NewUserRepository(db)
|
||||
manager := NewJWTManager("test-secret")
|
||||
pair, err := manager.GenerateSubjectPairWithVersion(user.ID, user.Phone, "user", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("生成令牌失败:%v", err)
|
||||
}
|
||||
|
||||
_, err = repo.FindActiveForToken(t.Context(), user.ID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("有效令牌校验失败:%v", err)
|
||||
}
|
||||
|
||||
if err := db.Model(&model.User{}).Where("id = ?", user.ID).Updates(map[string]any{
|
||||
"status": "frozen",
|
||||
"token_version": 1,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("冻结用户失败:%v", err)
|
||||
}
|
||||
|
||||
_, err = repo.FindActiveForToken(t.Context(), user.ID, 0)
|
||||
if !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("冻结用户校验错误 = %v, want %v", err, ErrUserDisabled)
|
||||
}
|
||||
|
||||
service := NewService(repo, nil, manager, nil, nil)
|
||||
_, err = service.RefreshToken(t.Context(), pair.RefreshToken)
|
||||
if !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("冻结用户刷新错误 = %v, want %v", err, ErrUserDisabled)
|
||||
}
|
||||
|
||||
// 解冻后版本仍然不同,冻结前签发的 token 不能恢复使用。
|
||||
if err := db.Model(&model.User{}).Where("id = ?", user.ID).Updates(map[string]any{
|
||||
"status": "active",
|
||||
"token_version": 2,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("解冻用户失败:%v", err)
|
||||
}
|
||||
_, err = repo.FindActiveForToken(t.Context(), user.ID, 0)
|
||||
if !errors.Is(err, ErrTokenVersionMismatch) {
|
||||
t.Fatalf("旧令牌版本校验错误 = %v, want %v", err, ErrTokenVersionMismatch)
|
||||
}
|
||||
_, err = repo.FindActiveForToken(t.Context(), user.ID, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("新令牌版本校验失败:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user