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") ErrLicenseLocked = errors.New("license locked, please renew or contact support") ) 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 } if err := s.checkLicenseNotLocked(shop.ID); err != nil { return nil, nil, err } 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 } createTrialLicense(tx, shop.ID) 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) checkLicenseNotLocked(shopID uint64) error { var lic model.License if err := s.db.Where("shop_id = ? AND is_active = 1", shopID). Order("id DESC").First(&lic).Error; err != nil { return nil // no license record → allow login } if middleware.CalcLicensePhase(lic.ExpiresAt) == middleware.PhaseLocked { return ErrLicenseLocked } return nil } func (s *AuthService) issueTokens(userID, shopID uint64, role string) (*TokenPair, error) { cfg := config.C.JWT now := time.Now() // Embed license expires_at in JWT so LicenseGuard can check phase without DB. var licExpAt *int64 var lic model.License if err := s.db.Where("shop_id = ? AND is_active = 1", shopID). Order("id DESC").First(&lic).Error; err == nil && lic.ExpiresAt != nil { ts := lic.ExpiresAt.Unix() licExpAt = &ts } accessExp := now.Add(time.Duration(cfg.AccessExpireMin) * time.Minute) accessClaims := middleware.Claims{ UserID: userID, ShopID: shopID, Role: role, LicenseExpiresAt: licExpAt, 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, LicenseExpiresAt: licExpAt, 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 }