package service import ( "errors" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/middleware" "github.com/wangjia/jiu/backend/internal/model" ) var ( ErrInvalidCredentials = errors.New("invalid username or password") ErrUserInactive = errors.New("user is disabled") ) type AuthService struct { db *gorm.DB } func NewAuthService(db *gorm.DB) *AuthService { return &AuthService{db: db} } type TokenPair struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` // 秒 ShopID uint64 `json:"shop_id"` } // Login 账号密码登录 func (s *AuthService) Login(shopCode, username, password string) (*TokenPair, *model.User, error) { var shop model.Shop if err := s.db.Where("code = ?", shopCode).First(&shop).Error; err != nil { return nil, nil, ErrInvalidCredentials } var user model.User if err := s.db.Where("shop_id = ? AND username = ? AND deleted_at IS NULL", shop.ID, username). First(&user).Error; err != nil { return nil, nil, ErrInvalidCredentials } if !user.IsActive { return nil, nil, ErrUserInactive } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return nil, nil, ErrInvalidCredentials } pair, err := s.issueTokens(user.ID, shop.ID, user.Role) if err != nil { return nil, nil, err } return pair, &user, nil } // HashPassword 生成 bcrypt 哈希 func HashPassword(plain string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) return string(b), err } // RefreshTokens 用 Refresh Token 换新 Token Pair func (s *AuthService) RefreshTokens(refreshToken string) (*TokenPair, error) { claims := &middleware.Claims{} token, err := jwt.ParseWithClaims(refreshToken, claims, func(t *jwt.Token) (interface{}, error) { return []byte(config.C.JWT.Secret), nil }) if err != nil || !token.Valid { return nil, errors.New("invalid refresh token") } return s.issueTokens(claims.UserID, claims.ShopID, claims.Role) } func (s *AuthService) issueTokens(userID, shopID uint64, role string) (*TokenPair, error) { cfg := config.C.JWT now := time.Now() accessExp := now.Add(time.Duration(cfg.AccessExpireMin) * time.Minute) accessClaims := middleware.Claims{ UserID: userID, ShopID: shopID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(accessExp), IssuedAt: jwt.NewNumericDate(now), }, } accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString([]byte(cfg.Secret)) if err != nil { return nil, err } refreshExp := now.Add(time.Duration(cfg.RefreshExpireH) * time.Hour) refreshClaims := middleware.Claims{ UserID: userID, ShopID: shopID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(refreshExp), IssuedAt: jwt.NewNumericDate(now), }, } refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(cfg.Secret)) if err != nil { return nil, err } return &TokenPair{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: cfg.AccessExpireMin * 60, ShopID: shopID, }, nil }