195 lines
5.3 KiB
Go
195 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"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
|
|
}
|
|
|
|
// RegisterInput 注册新门店所需参数
|
|
type RegisterInput struct {
|
|
ShopName string `json:"shop_name" binding:"required"`
|
|
Address string `json:"address" binding:"required"`
|
|
ManagerName string `json:"manager_name" binding:"required"`
|
|
Phone string `json:"phone" binding:"required"`
|
|
Description string `json:"description"`
|
|
Username string `json:"username" binding:"required"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
}
|
|
|
|
// RegisterResult 注册成功后返回的数据
|
|
type RegisterResult struct {
|
|
ShopCode string `json:"shop_code"`
|
|
ShopName string `json:"shop_name"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
// Register 自助注册新门店(公开接口,无需认证)
|
|
func (s *AuthService) Register(in RegisterInput) (*RegisterResult, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result RegisterResult
|
|
err = s.db.Transaction(func(tx *gorm.DB) error {
|
|
// 先用 UUID 占位创建门店,得到真实 ID
|
|
shop := model.Shop{
|
|
Name: in.ShopName,
|
|
Code: uuid.New().String(),
|
|
Address: in.Address,
|
|
Phone: in.Phone,
|
|
ManagerName: in.ManagerName,
|
|
Description: in.Description,
|
|
}
|
|
if err := tx.Create(&shop).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// 用自增 ID 生成正式门店编码
|
|
shop.Code = fmt.Sprintf("S%06d", shop.ID)
|
|
if err := tx.Model(&shop).Update("code", shop.Code).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// 创建管理员用户
|
|
user := model.User{
|
|
ShopID: shop.ID,
|
|
Username: in.Username,
|
|
PasswordHash: string(hash),
|
|
RealName: in.ManagerName,
|
|
Phone: in.Phone,
|
|
Role: "superadmin",
|
|
IsActive: true,
|
|
}
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
result = RegisterResult{
|
|
ShopCode: shop.Code,
|
|
ShopName: shop.Name,
|
|
Username: user.Username,
|
|
}
|
|
return nil
|
|
})
|
|
return &result, err
|
|
}
|
|
|
|
// 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
|
|
}
|