Skip to content

软删除机制

7878字约26分钟

2025-07-15

介绍一下 Gorm 的软删除技术,讲解一下与 Mybatis 的一个区别,和传统 MySQL 的差异。

🧾 GORM 软删除详解

在数据库开发中,软删除(Soft Delete) 是一种逻辑上“删除”数据的方式,而不是物理上从表中移除记录。GORM 原生支持软删除功能,通过 gorm.DeletedAt​ 字段实现。


🔍 什么是软删除?

软删除的本质是在数据表中添加一个字段(如 deleted_at TIMESTAMP​),当执行删除操作时,并不是真正地将记录从数据库中删除,而是设置该字段为当前时间戳。后续查询时自动过滤掉这些标记为已删除的记录。

软删除常用于需要保留历史数据、审计日志或防止误删的场景。


💡 GORM 中的软删除机制

1. 定义软删除字段

在结构体中加入 DeletedAt​ 字段:

type User struct {
    ID        uint
    Name      string
    DeletedAt gorm.DeletedAt `gorm:"index"` // 启用软删除
}

GORM 使用 *time.Time​ 或 gorm.DeletedAt​ 类型来识别是否启用软删除功能。

2. 删除操作

使用标准的 Delete()​ 方法即可进行软删除:

db.Delete(&user)
// 实际 SQL: UPDATE users SET deleted_at = NOW() WHERE id = ?

3. 查询忽略软删除记录

默认情况下,GORM 的 Find​, First​ 等方法会自动忽略 deleted_at IS NOT NULL​ 的记录。

4. 查询软删除记录

如果需要查看被软删除的数据,可以使用 .Unscoped()​:

var user User
db.Unscoped().Where("id = ?", 1).First(&user)

5. 恢复软删除记录

同样使用 .Unscoped()​ 修改状态:

db.Unscoped().Model(&user).Update("deleted_at", nil)

⚖️ GORM 软删除 vs MyBatis 软删除

特性GORMMyBatis
内置支持✅ 原生支持❌ 需手动实现
语法简洁程度高(结构体+标签)低(需手写 SQL 判断 is_deleted​)
自动过滤逻辑删除✅ 默认过滤❌ 需手动加条件
恢复逻辑删除简单(更新 DeletedAt 为 nil)手动修改字段值
性能影响小(索引优化 + 单一字段)可能高(依赖字段判断)

📌 结论:

  • GORM 对软删除是封装完整的 ORM 层解决方案。
  • MyBatis 更灵活但需要开发者自行维护逻辑删除字段和查询条件。

🔄 GORM 软删除与传统 MySQL 物理删除对比

维度软删除(GORM)物理删除(MySQL DELETE)
数据是否丢失❌ 不丢失✅ 数据永久丢失
可恢复性✅ 可以通过设置 deleted_at = NULL​ 恢复❌ 很难恢复(除非有备份/binlog)
查询效率可接受(建立索引后)更快
存储占用有冗余数据不占空间
适合场景日志、审计、用户注销等临时数据清理、垃圾回收等

🛠 示例:软删除代码片段(GORM)

type Product struct {
    ID        uint
    Name      string
    Price     float64
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

// 创建产品
db.Create(&Product{Name: "iPhone", Price: 999})

// 软删除产品
var product Product
db.First(&product, 1)
db.Delete(&product)

// 查看所有产品(含软删除)
var products []Product
db.Unscoped().Find(&products) // 返回包括 deleted_at != null 的数据

// 恢复软删除
db.Unscoped().Model(&product).Update("deleted_at", nil)

📌 总结表格

对比维度GORM 软删除MyBatis 软删除MySQL 物理删除
是否原生支持✅ 是❌ 否✅ 是
实现复杂度极低
数据可恢复性✅ 可恢复✅(取决于实现)❌ 几乎不可逆
查询效率中等(带过滤条件)中等(手写条件)
存储开销有冗余有冗余
场景适用审计、用户管理、日志系统等自定义需求强的项目无需保留数据的场景

✅ 推荐实践

  • 在涉及重要业务数据的模型中启用软删除(如用户、订单、文章等);
  • 结合 .Unscoped()​ 方法实现恢复、统计等功能;
  • 使用钩子函数(如 BeforeDelete​)进行权限检查或日志记录;
  • deleted_at​ 添加索引以提高查询性能;
  • 定期清理长期未使用的软删除数据(归档或物理删除)。

如果你还想了解如何结合 Gin 框架实现 RESTful API 的软删除接口、或者 GORM 的全局钩子做统一软删除处理策略,我也可以为你扩展讲解 👇

结合 Gin 框架实现 RESTful API 的软删除接口、或者 GORM 的全局钩子做统一软删除处理策略, Gorm-SQLite

好的!我们将结合 GORM + SQLite + Gin 框架,实现一个完整的 RESTful API 接口系统,支持:

  • 用户注册、登录(带权限区分)
  • 用户列表获取
  • 软删除功能(逻辑删除)
  • 使用 GORM 的钩子函数统一处理软删除逻辑和日志记录

🧱 技术栈

工具/库版本/用途说明
Go1.20+
GORMORM 操作数据库
SQLite轻量级数据库用于本地测试
Gin构建 RESTful API
bcrypt加密用户密码
gorm.DeletedAt实现软删除

📁 目录结构(简化)

.
├── main.go
├── go.mod
├── models/
│   └── user.go
├── handlers/
│   └── user.go
└── middlewares/
    └── auth.go

🔧 第一步:初始化项目 & 安装依赖

go mod init your_project_name
go get -u gorm.io/gorm gorm.io/driver/sqlite github.com/gin-gonic/gin golang.org/x/crypto/bcrypt

🗂️ models/user.go

定义用户模型与软删除字段。

package models

import (
    "gorm.io/gorm"
)

type Role string

const (
    RoleUser   Role = "user"
    RoleAdmin  Role = "admin"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Username  string         `json:"username" gorm:"unique"`
    Password  string         `json:"-"`
    Role      Role           `json:"role"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 启用软删除
}

🪝 钩子函数:models/hooks.go

我们可以添加一个全局钩子,在删除操作前做权限检查或日志记录。

package models

import (
    "fmt"
    "gorm.io/gorm"
)

func RegisterHooks(db *gorm.DB) {
    db.Callback().Delete().Before("gorm:delete").Register("before_delete_log", func(tx *gorm.DB) {
        fmt.Println("准备执行删除操作...")
    })

    db.Callback().Delete().After("gorm:delete").Register("after_delete_log", func(tx *gorm.DB) {
        fmt.Println("删除操作已完成。")
    })
}

📡 handlers/user.go

编写 Gin 处理接口。

package handlers

import (
    "net/http"
    "your_project_name/models"
    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

var db *gorm.DB

func SetDB(database *gorm.DB) {
    db = database
}

// 注册用户
func Register(c *gin.Context) {
    var user models.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    user.Password = string(hashedPassword)
    user.Role = models.RoleUser

    if err := db.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "注册成功"})
}

// 登录接口
func Login(c *gin.Context) {
    var input struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var user models.User
    if err := db.Where("username = ?", input.Username).First(&user).Error; err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }

    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "登录成功", "role": user.Role})
}

// 获取所有用户(排除已软删除)
func GetUsers(c *gin.Context) {
    var users []models.User
    db.Find(&users)
    c.JSON(http.StatusOK, users)
}

// 删除用户(软删除)
func DeleteUser(c *gin.Context) {
    id := c.Param("id")
    if err := db.Delete(&models.User{}, id).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

// 查看被软删除的用户
func GetDeletedUsers(c *gin.Context) {
    var users []models.User
    db.Unscoped().Where("deleted_at IS NOT NULL", "").Find(&users)
    c.JSON(http.StatusOK, users)
}

🧭 main.go

整合 Gin 和 GORM。

package main

import (
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "github.com/gin-gonic/gin"
    "your_project_name/handlers"
    "your_project_name/models"
)

func main() {
    // 初始化 SQLite 数据库
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败")
    }

    // 自动迁移
    db.AutoMigrate(&models.User{})

    // 注册钩子
    models.RegisterHooks(db)

    // 初始化 handlers DB
    handlers.SetDB(db)

    // 初始化 Gin
    r := gin.Default()

    // 路由组
    api := r.Group("/api")
    {
        api.POST("/register", handlers.Register)
        api.POST("/login", handlers.Login)

        api.GET("/users", handlers.GetUsers)
        api.DELETE("/users/:id", handlers.DeleteUser)
        api.GET("/deleted-users", handlers.GetDeletedUsers)
    }

    fmt.Println("服务启动于 http://localhost:8080")
    r.Run(":8080")
}

✅ 测试接口

使用 Postman 或 curl 进行测试:

注册用户

POST /api/register
{
  "username": "alice",
  "password": "123456"
}

登录

POST /api/login
{
  "username": "alice",
  "password": "123456"
}

获取用户列表(不包括软删除)

GET /api/users

获取被软删除的用户

GET /api/deleted-users

删除用户(软删除)

DELETE /api/users/1

🔄 补充:恢复软删除用户

你可以扩展接口实现恢复软删除功能:

func RestoreUser(c *gin.Context) {
    id := c.Param("id")
    db.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil)
    c.JSON(http.StatusOK, gin.H{"message": "恢复成功"})
}

然后添加路由:

api.PUT("/restore/users/:id", handlers.RestoreUser)

🎯 总结

功能是否支持说明
软删除使用 gorm.DeletedAt
钩子函数在删除前后输出日志
权限控制(示例中简单)✅(可扩展)可以基于 role 字段判断是否管理员
RESTful API使用 Gin 构建接口
用户管理 CRUD支持注册、查询、软删除、恢复
数据库引擎SQLite简单轻量,适合开发测试

🚀 下一步建议

  1. JWT Token 登录认证
  2. 中间件权限校验
  3. 使用 Redis 缓存用户信息
  4. 部署 MySQL 替换 SQLite
  5. 加入 Swagger 文档
  6. 单元测试覆盖

如果你希望我继续为你封装 JWT 认证模块、或者构建完整的 RBAC 权限系统(角色-权限关联表),也可以继续提问 👇

  1. JWT Token 登录认证
  2. 中间件权限校验
  3. MySQL 替换 SQLite
  4. 优化一下整个处理流程,稍微封装一下整个响应请求。

好的!我们将对当前项目进行 功能增强和结构优化,包括以下四个核心扩展:


✅ 目标

  1. 🧾 使用 JWT 实现登录认证
  2. 🔐 添加中间件实现权限控制(仅管理员可删除)
  3. 🛠 将数据库从 SQLite 替换为 MySQL
  4. 📦 封装统一的 API 响应格式

🗂️ 优化后的目录结构

.
├── main.go
├── go.mod
├── config/
│   └── db.go
├── models/
│   ├── user.go
│   └── hooks.go
├── handlers/
│   ├── auth.go
│   └── user.go
├── middlewares/
│   ├── auth.go
│   └── role.go
├── utils/
│   └── response.go
└── services/
    └── jwt.go

1️⃣ 配置数据库连接(MySQL)

config/db.go

package config

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func ConnectDB() *gorm.DB {
    dsn := "root:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败")
    }

    return db
}

⚠️ 替换 dsn​ 中的用户名、密码、数据库名为你自己的 MySQL 配置。


2️⃣ 用户模型与软删除字段

models/user.go

package models

import (
    "gorm.io/gorm"
)

type Role string

const (
    RoleUser  Role = "user"
    RoleAdmin Role = "admin"
)

type User struct {
    ID       uint           `json:"id" gorm:"primaryKey"`
    Username string         `json:"username" gorm:"unique"`
    Password string         `json:"-"`
    Role     Role           `json:"role"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

3️⃣ GORM 软删除钩子函数

models/hooks.go

package models

import (
    "fmt"
    "gorm.io/gorm"
)

func RegisterHooks(db *gorm.DB) {
    db.Callback().Delete().Before("gorm:delete").Register("before_delete_log", func(tx *gorm.DB) {
        fmt.Println("准备执行删除操作...")
    })

    db.Callback().Delete().After("gorm:delete").Register("after_delete_log", func(tx *gorm.DB) {
        fmt.Println("删除操作已完成。")
    })
}

4️⃣ JWT 登录认证服务

services/jwt.go

package services

import (
    "github.com/dgrijalva/jwt-go"
    "time"
)

var jwtKey = []byte("your-secret-key")

type Claims struct {
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.StandardClaims
}

func GenerateToken(username, role string) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)
    claims := &Claims{
        Username: username,
        Role:     role,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expirationTime.Unix(),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

5️⃣ 登录接口处理

handlers/auth.go

package handlers

import (
    "net/http"
    "your_project_name/models"
    "your_project_name/services"
    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

var db *gorm.DB

func SetDB(database *gorm.DB) {
    db = database
}

// 登录生成 Token
func Login(c *gin.Context) {
    var input struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }

    if err := c.ShouldBindJSON(&input); err != nil {
        utils.ErrorResponse(c, http.StatusBadRequest, err.Error())
        return
    }

    var user models.User
    if err := db.Where("username = ?", input.Username).First(&user).Error; err != nil {
        utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
        return
    }

    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
        utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
        return
    }

    token, _ := services.GenerateToken(user.Username, string(user.Role))
    utils.SuccessResponse(c, gin.H{"token": token})
}

6️⃣ 中间件:JWT 认证和角色校验

middlewares/auth.go

package middlewares

import (
    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
    "net/http"
    "strings"
    "your_project_name/services"
)

func AuthMiddleware(c *gin.Context) {
    authHeader := c.GetHeader("Authorization")
    if authHeader == "" {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "缺少授权头"})
        return
    }

    tokenString := strings.TrimPrefix(authHeader, "Bearer ")
    claims := &services.Claims{}

    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return services.JwtKey, nil
    })

    if err != nil || !token.Valid {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的 token"})
        return
    }

    c.Set("username", claims.Username)
    c.Set("role", claims.Role)
    c.Next()
}

middlewares/role.go

package middlewares

import "github.com/gin-gonic/gin"

func AdminOnly(c *gin.Context) {
    role, _ := c.Get("role")
    if role != "admin" {
        c.AbortWithStatusJSON(403, gin.H{"error": "无权限访问"})
        return
    }
    c.Next()
}

7️⃣ 统一响应封装

utils/response.go

package utils

import "github.com/gin-gonic/gin"

func SuccessResponse(c *gin.Context, data interface{}) {
    c.JSON(200, gin.H{"success": true, "data": data})
}

func ErrorResponse(c *gin.Context, code int, message string) {
    c.JSON(code, gin.H{"success": false, "message": message})
}

8️⃣ 优化用户处理逻辑

handlers/user.go

package handlers

import (
    "net/http"
    "your_project_name/models"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

var db *gorm.DB

func SetDB(database *gorm.DB) {
    db = database
}

func GetUsers(c *gin.Context) {
    var users []models.User
    db.Find(&users)
    utils.SuccessResponse(c, users)
}

func DeleteUser(c *gin.Context) {
    id := c.Param("id")
    db.Delete(&models.User{}, id)
    utils.SuccessResponse(c, gin.H{"message": "删除成功"})
}

func RestoreUser(c *gin.Context) {
    id := c.Param("id")
    db.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil)
    utils.SuccessResponse(c, gin.H{"message": "恢复成功"})
}

9️⃣ 启动主程序 main.go

package main

import (
    "your_project_name/config"
    "your_project_name/handlers"
    "your_project_name/middlewares"
    "your_project_name/models"
    "github.com/gin-gonic/gin"
)

func main() {
    db := config.ConnectDB()
    models.RegisterHooks(db)

    handlers.SetDB(db)

    r := gin.Default()

    api := r.Group("/api")
    {
        api.POST("/login", handlers.Login)

        // 受保护的路由
        protected := api.Group("")
        protected.Use(middlewares.AuthMiddleware)
        {
            protected.GET("/users", handlers.GetUsers)

            admin := protected.Group("")
            admin.Use(middlewares.AdminOnly)
            {
                admin.DELETE("/users/:id", handlers.DeleteUser)
                admin.PUT("/restore/users/:id", handlers.RestoreUser)
            }
        }
    }

    r.Run(":8080")
}

✅ 请求示例

登录获取 Token

POST /api/login
{
  "username": "alice",
  "password": "123456"
}

返回:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}

获取用户列表(所有角色都可访问)

GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

删除用户(仅限管理员)

DELETE /api/users/1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

🎯 总结

功能描述
数据库改为使用 MySQL
登录认证使用 JWT 实现
权限控制使用 Gin 中间件限制只有 admin 可删除
响应统一封装使用 utils.Response​ 返回统一格式
结构清晰模块化设计,易于维护和扩展

🚀 下一步建议

  • 🧪 添加单元测试(使用 Testify + GORM 测试模式)
  • 📄 加入 Swagger 接口文档支持
  • 📦 使用 Redis 缓存 Token 黑名单
  • 🧭 构建完整的 RBAC 系统(角色-权限关联)

如果你希望我继续帮你添加这些高级模块,欢迎随时告诉我 👇

实现[测试、缓存、RBAC]

  • 🧪 添加单元测试(使用 Testify + GORM 测试模式)
  • 📦 使用 Redis 缓存 Token 黑名单
  • 🧭 构建完整的 RBAC 系统(角色-权限关联)

非常棒!你已经打下了坚实的基础,现在我们继续为这个项目添加 单元测试、Redis 缓存 Token 黑名单完整的 RBAC 权限系统(角色-权限关联) 。我们将依次讲解每个模块,并提供完整的示例代码。


✅ 第一部分:添加单元测试(使用 Testify + GORM 测试模式)

我们将使用以下工具:

  • testing​ 标准库
  • testify​ 的 require/assert​ 做断言
  • GORM 的内存数据库(SQLite 内存模式)做隔离测试

1. 安装依赖

go get github.com/stretchr/testify

2. 示例测试文件 handlers/user_test.go

package handlers

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

func setupTestDB() *gorm.DB {
    db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    db.AutoMigrate(&models.User{})
    return db
}

func TestGetUsers(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.Default()

    // 初始化测试 DB
    testDB := setupTestDB()
    SetDB(testDB)

    // 插入测试数据
    testDB.Create(&models.User{Username: "alice", Password: "123456", Role: models.RoleUser})
    testDB.Create(&models.User{Username: "bob", Password: "abcdef", Role: models.RoleAdmin})

    r.GET("/api/users", GetUsers)

    req, _ := http.NewRequest(http.MethodGet, "/api/users", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)

    var resp map[string]interface{}
    _ = json.Unmarshal(w.Body.Bytes(), &resp)

    data := resp["data"].([]interface{})
    assert.Len(t, data, 2)
}

3. 示例登录测试 handlers/auth_test.go

func TestLoginSuccess(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.Default()

    testDB := setupTestDB()
    SetDB(testDB)

    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
    testDB.Create(&models.User{
        Username: "alice",
        Password: string(hashedPassword),
        Role:     models.RoleUser,
    })

    r.POST("/api/login", Login)

    body := bytes.NewBufferString(`{"username":"alice","password":"123456"}`)
    req, _ := http.NewRequest(http.MethodPost, "/api/login", body)
    req.Header.Set("Content-Type", "application/json")

    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.Contains(t, w.Body.String(), "token")
}

✅ 第二部分:使用 Redis 缓存 Token 黑名单

当用户注销时,我们需要将 Token 加入黑名单,防止其继续被使用。


1. 安装 Redis 包

go get github.com/go-redis/redis/v8

2. 创建 Redis 工具包 utils/redis.go

package utils

import (
    "context"
    "time"

    "github.com/go-redis/redis/v8"
)

var RedisClient *redis.Client

func InitRedis() {
    RedisClient = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
}

func AddToBlacklist(token string, exp time.Duration) error {
    ctx := context.Background()
    return RedisClient.Set(ctx, "blacklist:"+token, "1", exp).Err()
}

func IsTokenBlacklisted(token string) bool {
    ctx := context.Background()
    val, _ := RedisClient.Get(ctx, "blacklist:"+token).Result()
    return val == "1"
}

3. 修改 JWT 登出接口 handlers/auth.go

// 新增登出接口
func Logout(c *gin.Context) {
    authHeader := c.GetHeader("Authorization")
    tokenString := strings.TrimPrefix(authHeader, "Bearer ")

    _, err := jwt.ParseWithClaims(tokenString, &services.Claims{}, func(token *jwt.Token) (interface{}, error) {
        return services.JwtKey, nil
    })

    if err == nil {
        utils.AddToBlacklist(tokenString, 24*time.Hour)
        utils.SuccessResponse(c, gin.H{"message": "已登出"})
        return
    }

    utils.ErrorResponse(c, http.StatusUnauthorized, "无效的 token")
}

4. 修改中间件检查是否在黑名单中 middlewares/auth.go

if utils.IsTokenBlacklisted(tokenString) {
    c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token 已失效,请重新登录"})
    return
}

✅ 第三部分:构建完整的 RBAC 系统(角色-权限关联)

我们将实现:

  • 角色表 (roles​)
  • 权限表 (permissions​)
  • 角色-权限关联表 (role_permissions​)
  • 用户-角色表 (user_roles​)
  • 接口控制基于权限名(如 can_delete_user​, can_edit_content​)

1. 数据模型定义

models/role.go

type Role struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    Name        string         `json:"name" gorm:"unique"`
    Permissions []Permission   `json:"permissions" gorm:"many2many:role_permissions;"`
    DeletedAt   gorm.DeletedAt `json:"-"`
}

models/permission.go

type Permission struct {
    ID   uint   `json:"id" gorm:"primaryKey"`
    Name string `json:"name" gorm:"unique"` // 如 can_delete_user, can_edit_post
}

2. 自动迁移与初始化权限

main.go

db.AutoMigrate(&models.User{}, &models.Role{}, &models.Permission{})

// 初始化默认权限
db.FirstOrCreate(&models.Permission{Name: "can_delete_user"}, models.Permission{Name: "can_delete_user"})
db.FirstOrCreate(&models.Permission{Name: "can_edit_post"}, models.Permission{Name: "can_edit_post"})

// 初始化角色
var adminRole models.Role
db.Where("name = ?", "admin").FirstOrCreate(&adminRole, models.Role{Name: "admin"})
db.Model(&adminRole).Association("Permissions").Append([]models.Permission{
    {Name: "can_delete_user"},
    {Name: "can_edit_post"},
})

3. 中间件:权限校验

middlewares/permission.go

func HasPermission(permissionName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        username, _ := c.Get("username")
        var user models.User
        db.Where("username = ?", username).Preload("Roles.Permissions").First(&user)

        for _, role := range user.Roles {
            for _, perm := range role.Permissions {
                if perm.Name == permissionName {
                    c.Next()
                    return
                }
            }
        }

        c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
    }
}

4. 控制器中使用权限控制

handlers/user.go

protected.POST("/delete/:id", middlewares.HasPermission("can_delete_user"), handlers.DeleteUser)

🎯 总结功能扩展

模块功能描述
单元测试使用 testify + SQLite in-memory 进行隔离测试
Redis Token 黑名单登出后 Token 不再可用
RBAC 系统支持角色-权限多对多,细粒度权限控制

🚀 下一步建议

  • 📄 添加 Swagger 文档支持,使用 swaggo/swag
  • 🧪 添加集成测试,测试整个 API 链路
  • 🔐 引入 HTTPS + TLS 配置
  • 📦 构建 Docker 容器部署
  • 📊 添加 Prometheus 监控指标

如果你希望我帮你实现其中任意一个模块(比如 Swagger 接口文档),欢迎随时告诉我👇 我会继续为你扩展完整代码和配置说明。


🔍 代码解析:

db.Where("username = ?", username).Preload("Roles.Permissions").First(&user)

分解说明:

部分含义
dbGORM 的数据库连接对象,代表当前与数据库的会话。
.Where("username = ?", username)添加查询条件,查找用户名等于传入的 username​ 的用户记录。
.Preload("Roles.Permissions")预加载用户拥有的角色以及这些角色所拥有的权限。这一步是为了避免后续访问时发生 N+1 查询问题。
.First(&user)查找第一条匹配结果并赋值给变量 user​。

🧱 假设模型结构如下(简化版):

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    Password string
    Roles    []Role `gorm:"many2many:user_roles;"`
}

type Role struct {
    ID          uint         `gorm:"primaryKey"`
    Name        string       `gorm:"unique"`
    Permissions []Permission `gorm:"many2many:role_permissions;"`
}

type Permission struct {
    ID   uint   `gorm:"primaryKey"`
    Name string `gorm:"unique"` // 如 "can_delete_user", "can_edit_post"
}

💡 举例说明:

假设数据库中有以下数据:

用户表 (users​)

idusername
1alice

角色表 (roles​)

idname
1admin

权限表 (permissions​)

idname
1can_delete_user
2can_edit_content

角色-权限关联表 (role_permissions​)

role_idpermission_id
11
12

用户-角色关联表 (user_roles​)

user_idrole_id
11

✅ 执行效果:

当执行:

var user User
db.Where("username = ?", "alice").Preload("Roles.Permissions").First(&user)
  • GORM 会:

    1. 先查出 alice​ 这个用户;
    2. 然后根据其关联的 Roles​ 表(即 admin​ 角色);
    3. 再查出该角色的所有 Permissions​(即 can_delete_user​, can_edit_content​);

最终,你可以在代码中直接访问用户的权限:

for _, role := range user.Roles {
    for _, perm := range role.Permissions {
        fmt.Println(perm.Name) // 输出:can_delete_user, can_edit_content
    }
}

⚠️ 注意事项:

  • 如果没有 .Preload("Roles.Permissions")​,在访问 user.Roles​ 或 user.Roles.Permissions​ 时会发生懒加载(Lazy Loading),导致额外的 SQL 查询,影响性能。
  • 使用 .Preload()​ 可以一次性将所需的数据全部加载进来,提高效率,适用于 RBAC、菜单树等嵌套结构场景。

🛠 使用场景示例(结合中间件做权限判断)

func HasPermission(permissionName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        username, _ := c.Get("username")
        var user User
        db.Where("username = ?", username).
            Preload("Roles.Permissions").
            First(&user)

        hasPerm := false
        for _, role := range user.Roles {
            for _, perm := range role.Permissions {
                if perm.Name == permissionName {
                    hasPerm = true
                    break
                }
            }
        }

        if !hasPerm {
            c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
            return
        }

        c.Next()
    }
}

然后你可以这样使用它:

protected.DELETE("/users/:id", middlewares.HasPermission("can_delete_user"), handlers.DeleteUser)

📌 总结:

内容说明
.Where(...)查询指定用户名的用户
.Preload("Roles.Permissions")一次性加载角色和权限,避免 N+1 查询
.First(&user)获取第一个匹配的用户记录
用途实现 RBAC 权限验证中的权限校验逻辑

如果你还想了解如何用 Swagger 自动生成接口文档、或者用 Redis 缓存权限信息提升性能,我也可以继续为你讲解 👇


缓存权限信息,可以做些什么?配合 RBAC

RBAC(基于角色的访问控制) 系统中,缓存权限信息可以显著提升性能和用户体验。特别是在高并发场景下,频繁地从数据库查询用户的权限可能会导致延迟、资源浪费甚至成为瓶颈。


🧠 缓存权限信息可以做什么?

将用户或角色的权限信息缓存到内存或 Redis 中,主要有以下作用:

功能/优势说明
❌ 减少数据库查询用户每次请求时不再需要查 DB 获取权限,提高响应速度
⚡ 提升性能尤其适用于高频接口调用、权限校验密集的系统
🔁 实现快速权限刷新机制当权限变更时主动更新缓存,而不是等待过期
🔄 支持 RBAC 权限热更新修改权限后无需重启服务即可生效
🧱 降低数据库压力减轻数据库负担,提高整体系统稳定性

💡 使用场景举例

  • ✅ 用户登录后,将其所有权限缓存到 Redis 或内存中;
  • 🔐 每次接口请求时,直接从缓存获取权限进行校验;
  • 🔄 管理员修改某个角色的权限后,自动清理该角色的缓存;
  • 🕒 设置合理的缓存过期时间(如 5 分钟),防止数据陈旧。

🛠️ 如何实现:配合 RBAC 系统缓存权限

我们以使用 Redis 缓存为例,介绍如何实现一个高效的权限缓存系统。


1. 安装依赖

go get github.com/go-redis/redis/v8

2. 初始化 Redis 客户端(utils/redis.go​)

package utils

import (
    "context"
    "github.com/go-redis/redis/v8"
)

var RedisClient *redis.Client
var Ctx = context.Background()

func InitRedis() {
    RedisClient = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis 地址
        Password: "",               // 密码(无则留空)
        DB:       0,                // 默认数据库
    })
}

3. 缓存用户权限逻辑(services/permission_cache.go​)

package services

import (
    "context"
    "encoding/json"
    "fmt"
    "time"
    "your_project_name/models"
    "your_project_name/utils"
)

// 缓存 Key 格式:permissions:<username>
const permissionCacheKey = "permissions:%s"

// 缓存权限到 Redis
func CacheUserPermissions(username string, perms []string) error {
    key := fmt.Sprintf(permissionCacheKey, username)
    data, _ := json.Marshal(perms)
    return utils.RedisClient.Set(utils.Ctx, key, data, 5*time.Minute).Err()
}

// 从 Redis 获取权限
func GetUserPermissionsFromCache(username string) ([]string, bool) {
    key := fmt.Sprintf(permissionCacheKey, username)
    val, err := utils.RedisClient.Get(utils.Ctx, key).Result()
    if err != nil {
        return nil, false
    }

    var perms []string
    _ = json.Unmarshal([]byte(val), &perms)
    return perms, true
}

// 清除用户的权限缓存
func ClearUserPermissionCache(username string) {
    key := fmt.Sprintf(permissionCacheKey, username)
    utils.RedisClient.Del(utils.Ctx, key)
}

4. 登录时缓存权限(handlers/auth.go​)

// 用户登录成功后,缓存权限
var user models.User
db.Where("username = ?", input.Username).
    Preload("Roles.Permissions").
    First(&user)

// 提取权限名列表
var permissions []string
for _, role := range user.Roles {
    for _, perm := range role.Permissions {
        permissions = append(permissions, perm.Name)
    }
}

// 缓存权限
services.CacheUserPermissions(user.Username, permissions)

5. 中间件读取缓存权限(middlewares/permission.go​)

func HasPermission(requiredPerm string) gin.HandlerFunc {
    return func(c *gin.Context) {
        username, _ := c.Get("username")
        perms, ok := services.GetUserPermissionsFromCache(username.(string))

        if !ok {
            c.AbortWithStatusJSON(401, gin.H{"error": "权限缓存未命中,请重新登录"})
            return
        }

        hasPerm := false
        for _, p := range perms {
            if p == requiredPerm {
                hasPerm = true
                break
            }
        }

        if !hasPerm {
            c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
            return
        }

        c.Next()
    }
}

6. 角色权限变更时清除缓存(handlers/role.go​)

func UpdateRolePermissions(c *gin.Context) {
    roleName := c.Param("name")

    // ... 更新角色权限逻辑 ...

    // 假设我们知道哪些用户属于这个角色,批量清除他们的权限缓存
    var users []models.User
    db.Where("role = ?", roleName).Find(&users)
    for _, user := range users {
        services.ClearUserPermissionCache(user.Username)
    }

    c.JSON(200, gin.H{"message": "权限已更新,缓存已清除"})
}

🎯 总结:缓存权限 + RBAC 的优势

优点描述
✅ 高性能不再每次都去数据库查询权限
✅ 可控性可设定缓存时间、主动刷新
✅ 高可用即使数据库暂时不可用,仍可继续验证缓存中的权限
✅ 易扩展后续可接入分布式缓存(如 Redis 集群)、支持更多权限维度

🚀 下一步建议

你可以进一步优化权限缓存系统:

功能说明
🧪 编写单元测试验证缓存是否生效使用 testify​ 测试缓存命中与失效
🧩 添加权限标签树结构支持按模块(如 user: 、post: )做通配符匹配
📊 接入 Prometheus 监控缓存命中率统计 hit / miss 次数
📦 支持多级缓存(内存 + Redis)本地 LRU + Redis 两级缓存架构
📈 用布隆过滤器减少无效查询防止大量不存在用户的无效缓存查询

如果你希望我继续为你添加这些高级功能(比如:多级缓存、通配符权限匹配、Prometheus 监控等),欢迎随时告诉我 👇 我会继续为你编写完整代码和配置!

📦 支持多级缓存(内存 + Redis)本地 LRU + Redis 两级缓存架构
📈 用布隆过滤器减少无效查询防止大量不存在用户的无效缓存查询

非常好!我们来一起实现 多级缓存架构(内存 LRU + Redis)使用布隆过滤器减少无效查询。这是现代高性能权限系统中常见的做法,既能保证访问速度,又能降低后端压力。


🧩 一、支持多级缓存:内存 LRU + Redis

🎯 目标:

  • 使用本地内存缓存(LRU)做一级缓存,提升访问速度;
  • 使用 Redis 做二级缓存,防止服务重启数据丢失;
  • 查询时优先查内存 → 查 Redis → 回退数据库;
  • 更新时同步清理本地缓存并更新 Redis。

📦 安装依赖:

go get github.com/hashicorp/golang-lru/v2

🔧 缓存结构设计

我们将构建一个 PermissionCache​ 结构体,封装以下逻辑:

层级类型特点
一级缓存LRU 内存缓存快速响应、低延迟
二级缓存Redis持久化、分布式共享
最终回退数据库确保最终一致性

📁 文件结构(新增)

utils/
├── redis.go
├── lru_cache.go
services/
└── permission_cache.go

📄 1. 实现 LRU 本地缓存(utils/lru_cache.go​)

package utils

import (
    "sync"
    "github.com/hashicorp/golang-lru/v2"
)

const LocalCacheSize = 1024 // 可调整大小

var (
    localCache *lru.Cache[string, []string]
    mu         sync.RWMutex
)

func InitLocalCache() {
    var err error
    localCache, err = lru.NewWithEvict[string, []string](LocalCacheSize, func(key string, value []string) {
        // 可选的淘汰回调(如写入日志或后台持久化)
    })
    if err != nil {
        panic("无法初始化本地 LRU 缓存")
    }
}

📄 2. 多级缓存封装(services/permission_cache.go​)

package services

import (
    "context"
    "encoding/json"
    "fmt"
    "time"
    "your_project_name/utils"
)

// Redis Key 格式
const permissionRedisKey = "permissions:%s"

// 获取用户权限(先查内存 → 再查 Redis → 最后查 DB)
func GetUserPermissions(username string) ([]string, bool) {
    // 先查本地 LRU 缓存
    if perms, ok := utils.LocalCache.Get(username); ok {
        return perms, true
    }

    // 再查 Redis
    val, err := utils.RedisClient.Get(utils.Ctx, fmt.Sprintf(permissionRedisKey, username)).Result()
    if err == nil {
        var perms []string
        _ = json.Unmarshal([]byte(val), &perms)
        utils.LocalCache.Add(username, perms) // 加入本地缓存
        return perms, true
    }

    return nil, false
}

// 设置权限(同时写入 Redis 和本地缓存)
func SetUserPermissions(username string, perms []string) error {
    data, _ := json.Marshal(perms)
    utils.LocalCache.Add(username, perms)

    return utils.RedisClient.Set(utils.Ctx, fmt.Sprintf(permissionRedisKey, username), data, 5*time.Minute).Err()
}

// 清除缓存(用于角色权限变更时)
func ClearUserPermissionCache(username string) {
    utils.LocalCache.Remove(username)
    utils.RedisClient.Del(utils.Ctx, fmt.Sprintf(permissionRedisKey, username))
}

✨ 中间件使用方式不变(仍调用 GetUserPermissions​)

func HasPermission(requiredPerm string) gin.HandlerFunc {
    return func(c *gin.Context) {
        username, _ := c.Get("username")
        perms, ok := services.GetUserPermissions(username.(string))

        if !ok {
            c.AbortWithStatusJSON(401, gin.H{"error": "未授权,请重新登录"})
            return
        }

        for _, p := range perms {
            if p == requiredPerm {
                c.Next()
                return
            }
        }

        c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
    }
}

🧺 二、使用布隆过滤器减少无效查询

🎯 目标:

避免大量不存在用户的请求频繁穿透到 Redis 或数据库,造成缓存击穿和资源浪费。


📦 安装 Bloom Filter:

go get github.com/tylertreat/BoomFilters

📁 文件结构扩展:

utils/
├── bloom_filter.go

📄 1. 初始化布隆过滤器(utils/bloom_filter.go​)

package utils

import (
    "github.com/tylertreat/BoomFilters"
)

var UserExistsFilter *bloomfilter.ScalableBloomFilter

func InitBloomFilter() {
    UserExistsFilter = bloomfilter.NewDefaultScalableBloomFilter(1000000, 0.01)
}

📄 2. 在用户注册、登录时加入布隆过滤器判断

用户注册时加入布隆过滤器:

// 注册成功后加入布隆过滤器
utils.UserExistsFilter.Add([]byte(username))

登录时提前拦截无效用户名:

if !utils.UserExistsFilter.Test([]byte(input.Username)) {
    utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
    return
}

🔄 同步布隆过滤器与真实数据库状态

你可以定期从数据库中加载所有用户名,重建布隆过滤器,确保其准确度:

func SyncBloomFilterFromDB(db *gorm.DB) {
    var usernames []string
    db.Model(&models.User{}).Pluck("username", &usernames)

    filter := bloomfilter.NewDefaultScalableBloomFilter(uint(len(usernames)*2), 0.01)
    for _, u := range usernames {
        filter.Add([]byte(u))
    }

    utils.UserExistsFilter = filter
}

你可以在项目启动时执行一次,或定时任务每小时更新一次。


🎯 总结:多级缓存 + 布隆过滤器的优势

功能优势
🧩 多级缓存提升性能、容灾能力强、服务重启不丢数据
⏱️ 本地 LRU 缓存极低延迟、高命中率
📡 Redis 分布式缓存支持集群部署、缓存共享
🧹 布隆过滤器防止缓存穿透、拒绝无效用户名查询
🔁 自动刷新机制权限变化后缓存自动失效,不影响业务逻辑

🚀 下一步建议

你可以继续优化这个系统:

方向描述
🧪 单元测试对 LRU、Redis、Bloom Filter 做 mock 测试
📊 Prometheus 监控统计命中率、穿透率、缓存时间等指标
🧠 通配符权限匹配post:*​, user:delete​,提高灵活性
🛡️ 限流熔断防止恶意攻击导致缓存过载
📦 多租户支持每个租户独立缓存空间、隔离权限体系

如果你希望我继续为你添加这些高级功能(例如 Prometheus 监控模块、通配符权限解析),欢迎随时告诉我 👇 我会继续为你完善整个权限缓存系统!


📊 一、使用 Prometheus 统计缓存命中率等指标

✅ 功能目标:

  • 记录缓存命中数(LRU / Redis)
  • 记录缓存穿透数(未命中且查 DB)
  • 暴露 /metrics​ 接口供 Prometheus 抓取

🔧 实现步骤:

1. 安装 Prometheus Go Client:

go get github.com/prometheus/client_golang

2. 创建监控指标文件 utils/metrics.go

package utils

import (
    "github.com/prometheus/client_golang/prometheus"
)

var (
    cacheHits = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "permission_cache_hits_total",
            Help: "Number of permission cache hits by layer (lru/redis)",
        },
        []string{"layer"},
    )

    cacheMisses = prometheus.NewCounter(
        prometheus.CounterOpts{
            Name: "permission_cache_misses_total",
            Help: "Total number of permission cache misses",
        },
    )

    cachePenetrations = prometheus.NewCounter(
        prometheus.CounterOpts{
            Name: "permission_cache_penetration_total",
            Help: "Number of cache penetration attempts (invalid user)",
        },
    )
)

func InitMetrics() {
    prometheus.MustRegister(cacheHits)
    prometheus.MustRegister(cacheMisses)
    prometheus.MustRegister(cachePenetrations)
}

func IncCacheHit(layer string) {
    cacheHits.WithLabelValues(layer).Inc()
}

func IncCacheMiss() {
    cacheMisses.Inc()
}

func IncPenetration() {
    cachePenetrations.Inc()
}

3. 在 main.go​ 中注册 Prometheus 路由:

import (
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// ...

r.GET("/metrics", gin.WrapH(promhttp.Handler()))

4. 修改权限缓存服务逻辑,加入监控统计(services/permission_cache.go​)

func GetUserPermissions(username string) ([]string, bool) {
    if perms, ok := utils.LocalCache.Get(username); ok {
        utils.IncCacheHit("lru")
        return perms, true
    }

    val, err := utils.RedisClient.Get(utils.Ctx, fmt.Sprintf(permissionRedisKey, username)).Result()
    if err == nil {
        var perms []string
        _ = json.Unmarshal([]byte(val), &perms)
        utils.LocalCache.Add(username, perms)
        utils.IncCacheHit("redis")
        return perms, true
    }

    utils.IncCacheMiss()
    return nil, false
}

func ClearUserPermissionCache(username string) {
    utils.LocalCache.Remove(username)
    utils.RedisClient.Del(utils.Ctx, fmt.Sprintf(permissionRedisKey, username))
}

5. 登录时拦截无效用户名并记录穿透次数:

if !utils.UserExistsFilter.Test([]byte(input.Username)) {
    utils.IncPenetration()
    utils.ErrorResponse(c, http.StatusUnauthorized, "用户名或密码错误")
    return
}

✅ 效果:访问 /metrics​ 可查看如下指标

# HELP permission_cache_hits_total Number of permission cache hits by layer (lru/redis)
# TYPE permission_cache_hits_total counter
permission_cache_hits_total{layer="lru"} 100
permission_cache_hits_total{layer="redis"} 50

# HELP permission_cache_misses_total Total number of permission cache misses
# TYPE permission_cache_misses_total counter
permission_cache_misses_total 10

# HELP permission_cache_penetration_total Number of cache penetration attempts (invalid user)
# TYPE permission_cache_penetration_total counter
permission_cache_penetration_total 3

🧠 二、通配符权限匹配(Wildcard Permission Match)

✅ 功能目标:

  • 支持类似 post:*​, user:delete​ 的权限结构
  • 提高权限表达能力,简化角色配置

🔧 实现代码:

文件:services/permission_matcher.go

package services

import (
    "strings"
)

// 匹配通配符权限,如 can(post:create, post:*) => true
func HasPermission(requiredPerm string, userPerms []string) bool {
    for _, p := range userPerms {
        if p == requiredPerm {
            return true
        }

        // 处理 wildcard,如 user:* 匹配 user:read, user:write
        if strings.HasSuffix(p, ":*") {
            prefix := p[:len(p)-1] // 去掉 *
            if strings.HasPrefix(requiredPerm, prefix) {
                return true
            }
        }
    }
    return false
}

✅ 示例用法:

if !services.HasPermission("post:edit", userPerms) {
    c.AbortWithStatusJSON(403, gin.H{"error": "无权限操作"})
    return
}

若用户拥有 post:*​ 权限,则可以访问 post:create​, post:edit​, post:delete​ 等所有子权限。


🛡️ 三、限流熔断机制(Rate Limiting + Circuit Breaker)

✅ 功能目标:

  • 防止恶意请求频繁穿透缓存和数据库
  • 设置单位时间内最大请求数
  • 使用滑动窗口算法实现平滑限流

🔧 实现方式:使用 gRPC​ 或 Gin​ 插件,这里我们使用 Gin + uber-go/ratelimit

1. 安装依赖:

go get go.uber.org/ratelimit

2. 创建中间件 middlewares/rate_limit.go

package middlewares

import (
    "time"
    "go.uber.org/ratelimit"
    "github.com/gin-gonic/gin"
)

// 创建一个每秒最多 100 次请求的限流器
var limiter = ratelimit.New(100, ratelimit.WithSlidingWindow(1*time.Second))

func RateLimit() gin.HandlerFunc {
    return func(c *gin.Context) {
        if limiter.Take() == zeroTime {
            c.AbortWithStatusJSON(429, gin.H{"error": "请求过于频繁,请稍后再试"})
            return
        }
        c.Next()
    }
}

var zeroTime time.Time

3. 在路由中启用限流:

r.Use(middlewares.RateLimit())

📦 四、多租户支持(可选高级功能)

如果你有多个客户/组织,可以为每个租户建立独立缓存空间:

const permissionRedisKey = "permissions:%s:%s" // tenantID:username

在中间件中读取租户 ID,并将权限缓存键加上前缀。这能保证不同租户之间权限隔离。


🎯 总结:新增功能一览表

功能描述
📊 Prometheus 监控记录缓存命中率、穿透率、缓存刷新时间等
🧠 通配符权限匹配post:*​ 匹配 post:create​、post:delete
🛡️ 限流熔断机制防止高频攻击、缓存击穿
📦 多租户权限隔离不同租户缓存和权限互不干扰

🚀 下一步建议

可以继续:

  • 📌 添加 JWT 刷新 Token 机制;
  • 📁 将权限模型从 GORM 移植到 Casbin(更强大的 RBAC 库);
  • 📋 添加审计日志(Audit Log),记录谁在什么时间执行了哪些权限操作;
  • 🧱 实现完整的多级权限树,支持前端动态菜单渲染;
  • 🧪 编写完整的单元测试套件验证缓存命中率、穿透检测等功能;