From aa90b10f06ef32e707c4652ace24de567e5ddd2a Mon Sep 17 00:00:00 2001 From: laoboli <1293528695@qq.com> Date: Tue, 28 Apr 2026 18:58:24 +0800 Subject: [PATCH] feat: role. --- controllers/login.go | 49 ++++++++++++++--- main.go | 1 + middleware/jwt_for_user.go | 3 ++ models/user.go | 105 ++++++++++++++++++++++++++++++++++--- util/jwt.go | 17 ++++-- 5 files changed, 155 insertions(+), 20 deletions(-) diff --git a/controllers/login.go b/controllers/login.go index 17df234..cfcfb81 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -15,8 +15,13 @@ type LoginRequest struct { } type RegisterRequest struct { - Username string `json:"username" form:"username"` - Password string `json:"password" form:"password"` + Username string `json:"username" form:"username"` + 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 { @@ -41,8 +46,13 @@ func Register(c *gin.Context) { // 创建新用户 user := models.User{ - Username: req.Username, - Password: req.Password, // BeforeCreate钩子会自动加密 + Username: req.Username, + Email: req.Email, + Phone: req.Phone, + Password: req.Password, // BeforeCreate钩子会自动加密 + Role: req.Role, + FlavorType: req.FlavorType, + Regions: buildUserRegionBindings(req.RegionIDs), } if result := config.DB.Create(&user); result.Error != nil { @@ -51,7 +61,7 @@ func Register(c *gin.Context) { } // 生成Token - token, err := util.GenerateToken(user.ID, user.Username) + token, err := util.GenerateToken(&user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return @@ -73,7 +83,7 @@ func Login(c *gin.Context) { // 查找用户 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 { 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"}) return } + if !user.IsActive { + c.JSON(http.StatusForbidden, gin.H{"error": "User is disabled"}) + return + } // 生成JWT Token - token, err := util.GenerateToken(user.ID, user.Username) + token, err := util.GenerateToken(&user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return @@ -108,10 +122,29 @@ func GetProfile(c *gin.Context) { } 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"}) return } 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 +} diff --git a/main.go b/main.go index 09d6da9..58409cb 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ func main() { &models.StepStrideFreq{}, &models.RegressionResult{}, &models.User{}, + &models.UserRegionBinding{}, &models.MqttHeartRateRecord{}, &models.MqttStepCountRecord{}, &models.MqttGatewayStatusRecord{}, diff --git a/middleware/jwt_for_user.go b/middleware/jwt_for_user.go index 0b06f19..b8cc67b 100644 --- a/middleware/jwt_for_user.go +++ b/middleware/jwt_for_user.go @@ -36,6 +36,9 @@ func JWTAuth() gin.HandlerFunc { // 将用户信息存入上下文 c.Set("userID", claims.UserID) c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("flavorType", claims.FlavorType) + c.Set("regionIDs", claims.RegionIDs) c.Next() } diff --git a/models/user.go b/models/user.go index cfbab23..459884d 100644 --- a/models/user.go +++ b/models/user.go @@ -3,16 +3,48 @@ package models import ( "golang.org/x/crypto/bcrypt" "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 { - ID uint `gorm:"primaryKey" json:"id"` - Username string `gorm:"uniqueIndex;not null" json:"username"` - Email *string `gorm:"uniqueIndex;" json:"email"` - Phone *string `gorm:"uniqueIndex;" json:"phone"` - Password string `gorm:"not null" json:"-"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"uniqueIndex;not null" json:"username"` + Email *string `gorm:"uniqueIndex;" json:"email"` + Phone *string `gorm:"uniqueIndex;" json:"phone"` + Password string `gorm:"not null" json:"-"` + Role UserRole `gorm:"type:varchar(32);not null;default:'viewer';index" json:"role"` + 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 密码加密 @@ -31,10 +63,69 @@ func (u *User) CheckPassword(password string) bool { 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 创建前钩子 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 != "" { return u.HashPassword(u.Password) } 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 +} diff --git a/util/jwt.go b/util/jwt.go index fd8571f..dbde8ff 100644 --- a/util/jwt.go +++ b/util/jwt.go @@ -2,6 +2,7 @@ package util import ( "errors" + "hr_receiver/models" "time" "github.com/golang-jwt/jwt/v5" @@ -9,19 +10,25 @@ import ( var ApiSecret = "your-super-secret-key" // 预共享密钥 type Claims struct { - UserID uint `json:"user_id"` - Username string `json:"username"` + UserID uint `json:"user_id"` + Username string `json:"username"` + Role models.UserRole `json:"role"` + FlavorType models.UserFlavorType `json:"flavorType"` + RegionIDs []uint32 `json:"regionIds"` jwt.RegisteredClaims } // 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(1 * time.Second) // Token有效期24小时 claims := &Claims{ - UserID: userID, - Username: username, + UserID: user.ID, + Username: user.Username, + Role: user.Role, + FlavorType: user.FlavorType, + RegionIDs: user.RegionIDs(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()),