Files
jiu/backend/internal/service/auth.go
T
wangjia 0e42f0e417 init: 后端框架脚手架 (Go + Gin + GORM + MySQL)
- 项目目录结构:backend/ deploy/ schema/ migrations/
- 数据库 Schema:所有建表 SQL,含 hotel_id 多租户隔离
- Go 后端:config、model、handler、service、middleware、router
- 认证:账号密码登录 + JWT(Access + Refresh Token)
- 许可证:HMAC-SHA256 激活码生成 + 设备绑定验证
- 业务模块:商品、仓库、往来单位、入库、出库、库存、盘点
- 库存事务:入库/出库审核时原子更新库存 + 流水记录
- 数据导入:Excel/CSV 批量导入商品、往来单位
- Docker Compose:本地 MySQL + Adminer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 01:24:53 +08:00

121 lines
3.2 KiB
Go

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"` // 秒
}
// Login 账号密码登录
func (s *AuthService) Login(hotelCode, username, password string) (*TokenPair, *model.User, error) {
var hotel model.Hotel
if err := s.db.Where("code = ?", hotelCode).First(&hotel).Error; err != nil {
return nil, nil, ErrInvalidCredentials
}
var user model.User
if err := s.db.Where("hotel_id = ? AND username = ? AND deleted_at IS NULL", hotel.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, hotel.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.HotelID, claims.Role)
}
func (s *AuthService) issueTokens(userID, hotelID 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,
HotelID: hotelID,
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,
HotelID: hotelID,
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,
}, nil
}