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 {
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 {
@@ -42,7 +47,12 @@ func Register(c *gin.Context) {
// 创建新用户
user := models.User{
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
}
+1
View File
@@ -27,6 +27,7 @@ func main() {
&models.StepStrideFreq{},
&models.RegressionResult{},
&models.User{},
&models.UserRegionBinding{},
&models.MqttHeartRateRecord{},
&models.MqttStepCountRecord{},
&models.MqttGatewayStatusRecord{},
+3
View File
@@ -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()
}
+93 -2
View File
@@ -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"`
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
}
+10 -3
View File
@@ -2,6 +2,7 @@ package util
import (
"errors"
"hr_receiver/models"
"time"
"github.com/golang-jwt/jwt/v5"
@@ -11,17 +12,23 @@ var ApiSecret = "your-super-secret-key" // 预共享密钥
type Claims struct {
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()),