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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user