feat: role.

This commit is contained in:
2026-04-28 18:58:24 +08:00
parent 9a95130488
commit aa90b10f06
5 changed files with 155 additions and 20 deletions
+37 -4
View File
@@ -17,6 +17,11 @@ type LoginRequest struct {
type RegisterRequest struct { type RegisterRequest struct {
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
Email *string `json:"email" form:"email"`
Phone *string `json:"phone" form:"phone"`
Role models.UserRole `json:"role" form:"role"`
FlavorType models.UserFlavorType `json:"flavorType" form:"flavorType"`
RegionIDs []uint32 `json:"regionIds" form:"regionIds"`
} }
type AuthResponse struct { type AuthResponse struct {
@@ -42,7 +47,12 @@ func Register(c *gin.Context) {
// 创建新用户 // 创建新用户
user := models.User{ user := models.User{
Username: req.Username, Username: req.Username,
Email: req.Email,
Phone: req.Phone,
Password: req.Password, // BeforeCreate钩子会自动加密 Password: req.Password, // BeforeCreate钩子会自动加密
Role: req.Role,
FlavorType: req.FlavorType,
Regions: buildUserRegionBindings(req.RegionIDs),
} }
if result := config.DB.Create(&user); result.Error != nil { if result := config.DB.Create(&user); result.Error != nil {
@@ -51,7 +61,7 @@ func Register(c *gin.Context) {
} }
// 生成Token // 生成Token
token, err := util.GenerateToken(user.ID, user.Username) token, err := util.GenerateToken(&user)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return return
@@ -73,7 +83,7 @@ func Login(c *gin.Context) {
// 查找用户 // 查找用户
var user models.User var user models.User
result := config.DB.Where("username = ?", req.Username).First(&user) result := config.DB.Preload("Regions").Where("username = ?", req.Username).First(&user)
if result.Error != nil { if result.Error != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
@@ -85,9 +95,13 @@ func Login(c *gin.Context) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return return
} }
if !user.IsActive {
c.JSON(http.StatusForbidden, gin.H{"error": "User is disabled"})
return
}
// 生成JWT Token // 生成JWT Token
token, err := util.GenerateToken(user.ID, user.Username) token, err := util.GenerateToken(&user)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return return
@@ -108,10 +122,29 @@ func GetProfile(c *gin.Context) {
} }
var user models.User var user models.User
if result := config.DB.First(&user, userID); result.Error != nil { if result := config.DB.Preload("Regions").First(&user, userID); result.Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return return
} }
c.JSON(http.StatusOK, user) c.JSON(http.StatusOK, user)
} }
func buildUserRegionBindings(regionIDs []uint32) []models.UserRegionBinding {
if len(regionIDs) == 0 {
return nil
}
seen := make(map[uint32]struct{}, len(regionIDs))
regions := make([]models.UserRegionBinding, 0, len(regionIDs))
for _, regionID := range regionIDs {
if regionID == 0 {
continue
}
if _, exists := seen[regionID]; exists {
continue
}
seen[regionID] = struct{}{}
regions = append(regions, models.UserRegionBinding{RegionID: regionID})
}
return regions
}
+1
View File
@@ -27,6 +27,7 @@ func main() {
&models.StepStrideFreq{}, &models.StepStrideFreq{},
&models.RegressionResult{}, &models.RegressionResult{},
&models.User{}, &models.User{},
&models.UserRegionBinding{},
&models.MqttHeartRateRecord{}, &models.MqttHeartRateRecord{},
&models.MqttStepCountRecord{}, &models.MqttStepCountRecord{},
&models.MqttGatewayStatusRecord{}, &models.MqttGatewayStatusRecord{},
+3
View File
@@ -36,6 +36,9 @@ func JWTAuth() gin.HandlerFunc {
// 将用户信息存入上下文 // 将用户信息存入上下文
c.Set("userID", claims.UserID) c.Set("userID", claims.UserID)
c.Set("username", claims.Username) c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("flavorType", claims.FlavorType)
c.Set("regionIDs", claims.RegionIDs)
c.Next() c.Next()
} }
+93 -2
View File
@@ -3,16 +3,48 @@ package models
import ( import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type UserRole string
const (
UserRoleSuperAdmin UserRole = "super_admin"
UserRoleRegionAdmin UserRole = "region_admin"
UserRoleOperator UserRole = "operator"
UserRoleViewer UserRole = "viewer"
)
type UserFlavorType string
const (
UserFlavorAll UserFlavorType = "all"
UserFlavorFull UserFlavorType = "full"
UserFlavorLight UserFlavorType = "light"
UserFlavorHeartRate UserFlavorType = "heartrate"
UserFlavorRun50 UserFlavorType = "run50"
)
type UserRegionBinding struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index;uniqueIndex:idx_user_region" json:"userId"`
RegionID uint32 `gorm:"not null;index;uniqueIndex:idx_user_region" json:"regionId"`
CreatedAt int64 `gorm:"not null" json:"created_at"`
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
}
type User struct { type User struct {
ID uint `gorm:"primaryKey" json:"id"` ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;not null" json:"username"` Username string `gorm:"uniqueIndex;not null" json:"username"`
Email *string `gorm:"uniqueIndex;" json:"email"` Email *string `gorm:"uniqueIndex;" json:"email"`
Phone *string `gorm:"uniqueIndex;" json:"phone"` Phone *string `gorm:"uniqueIndex;" json:"phone"`
Password string `gorm:"not null" json:"-"` Password string `gorm:"not null" json:"-"`
CreatedAt int64 `json:"created_at"` Role UserRole `gorm:"type:varchar(32);not null;default:'viewer';index" json:"role"`
UpdatedAt int64 `json:"updated_at"` FlavorType UserFlavorType `gorm:"type:varchar(32);not null;default:'all';index" json:"flavorType"`
IsActive bool `gorm:"not null;default:true;index" json:"isActive"`
Regions []UserRegionBinding `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"regions"`
CreatedAt int64 `gorm:"not null" json:"created_at"`
UpdatedAt int64 `gorm:"not null" json:"updated_at"`
} }
// HashPassword 密码加密 // HashPassword 密码加密
@@ -31,10 +63,69 @@ func (u *User) CheckPassword(password string) bool {
return err == nil return err == nil
} }
func (u *User) RegionIDs() []uint32 {
regionIDs := make([]uint32, 0, len(u.Regions))
for _, region := range u.Regions {
regionIDs = append(regionIDs, region.RegionID)
}
return regionIDs
}
func (u *User) HasRegionAccess(regionID uint32) bool {
if u.Role == UserRoleSuperAdmin {
return true
}
for _, region := range u.Regions {
if region.RegionID == regionID {
return true
}
}
return false
}
func (u *User) SupportsFlavor(flavor string) bool {
if u.FlavorType == UserFlavorAll {
return true
}
return string(u.FlavorType) == flavor
}
func (u *User) normalizeDefaults() {
if u.Role == "" {
u.Role = UserRoleViewer
}
if u.FlavorType == "" {
u.FlavorType = UserFlavorAll
}
}
// BeforeCreate 创建前钩子 // BeforeCreate 创建前钩子
func (u *User) BeforeCreate(tx *gorm.DB) (err error) { func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.normalizeDefaults()
now := time.Now().UnixMilli()
u.CreatedAt = now
u.UpdatedAt = now
u.IsActive = true
if u.Password != "" { if u.Password != "" {
return u.HashPassword(u.Password) return u.HashPassword(u.Password)
} }
return nil return nil
} }
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
u.normalizeDefaults()
u.UpdatedAt = time.Now().UnixMilli()
return nil
}
func (r *UserRegionBinding) BeforeCreate(tx *gorm.DB) (err error) {
now := time.Now().UnixMilli()
r.CreatedAt = now
r.UpdatedAt = now
return nil
}
func (r *UserRegionBinding) BeforeUpdate(tx *gorm.DB) (err error) {
r.UpdatedAt = time.Now().UnixMilli()
return nil
}
+10 -3
View File
@@ -2,6 +2,7 @@ package util
import ( import (
"errors" "errors"
"hr_receiver/models"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -11,17 +12,23 @@ var ApiSecret = "your-super-secret-key" // 预共享密钥
type Claims struct { type Claims struct {
UserID uint `json:"user_id"` UserID uint `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
Role models.UserRole `json:"role"`
FlavorType models.UserFlavorType `json:"flavorType"`
RegionIDs []uint32 `json:"regionIds"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
// GenerateToken 生成JWT Token // GenerateToken 生成JWT Token
func GenerateToken(userID uint, username string) (string, error) { func GenerateToken(user *models.User) (string, error) {
expirationTime := time.Now().Add(24 * 30 * time.Hour) // Token有效期24小时 expirationTime := time.Now().Add(24 * 30 * time.Hour) // Token有效期24小时
//expirationTime := time.Now().Add(1 * time.Second) // Token有效期24小时 //expirationTime := time.Now().Add(1 * time.Second) // Token有效期24小时
claims := &Claims{ claims := &Claims{
UserID: userID, UserID: user.ID,
Username: username, Username: user.Username,
Role: user.Role,
FlavorType: user.FlavorType,
RegionIDs: user.RegionIDs(),
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime), ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),