0e42f0e417
- 项目目录结构: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>
121 lines
3.2 KiB
Go
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
|
|
}
|