feat: release v1.0.0

This commit is contained in:
2026-07-29 22:10:36 +08:00
parent ee9f50b859
commit c903b5a6b5
81 changed files with 2776 additions and 1044 deletions

View File

@@ -1,31 +1,219 @@
package cache
import (
"server/internal/db/sqlc"
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/maypok86/otter/v2"
"github.com/redis/go-redis/v9"
"server/internal/config"
)
type Caches struct {
SysUserApisCache *otter.Cache[int32, []db.GetSysUserApisRow]
rdb *redis.Client
keyPrefix string
}
func NewCaches() *Caches {
sysUserApisCache := otter.Must(&otter.Options[int32, []db.GetSysUserApisRow]{
MaximumSize: 1_000,
})
func NewCaches(rdb *redis.Client, cfg *config.Config) *Caches {
return &Caches{
SysUserApisCache: sysUserApisCache,
rdb: rdb,
keyPrefix: cfg.Redis.KeyPrefix,
}
}
// ClearSysUserCache 清理单个用户缓存
func (c *Caches) ClearSysUserCache(userID int32) {
c.SysUserApisCache.Invalidate(userID)
func (c *Caches) key(key string) string {
if c.keyPrefix == "" {
return key
}
return c.keyPrefix + key
}
// ClearAllSysUserCache 清理所有用户缓存
func (c *Caches) ClearAllSysUserCache() {
c.SysUserApisCache.InvalidateAll()
func (c *Caches) Set(ctx context.Context, key string, value any, expiration time.Duration) error {
return c.rdb.Set(ctx, c.key(key), value, expiration).Err()
}
func (c *Caches) Get(ctx context.Context, key string) (string, bool, error) {
val, err := c.rdb.Get(ctx, c.key(key)).Result()
if errors.Is(err, redis.Nil) {
return "", false, nil
}
if err != nil {
return "", false, err
}
return val, true, nil
}
func (c *Caches) SetJSON(ctx context.Context, key string, value any, expiration time.Duration) error {
b, err := json.Marshal(value)
if err != nil {
return err
}
return c.rdb.Set(ctx, c.key(key), b, expiration).Err()
}
func GetJSON[T any](ctx context.Context, c *Caches, key string) (*T, bool, error) {
val, err := c.rdb.Get(ctx, c.key(key)).Bytes()
if errors.Is(err, redis.Nil) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var result T
if err = json.Unmarshal(val, &result); err != nil {
return nil, false, err
}
return &result, true, nil
}
func GetOrSetJSON[T any](ctx context.Context, c *Caches, key string, expiration time.Duration, fetch func() (T, error)) (T, error) {
var zero T
cacheKey := c.key(key)
val, err := c.rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
var result T
if err = json.Unmarshal(val, &result); err == nil {
return result, nil
}
}
if !errors.Is(err, redis.Nil) {
return zero, err
}
result, err := fetch()
if err != nil {
return zero, err
}
b, err := json.Marshal(result)
if err != nil {
return zero, err
}
_ = c.rdb.Set(ctx, cacheKey, b, expiration).Err()
return result, nil
}
func (c *Caches) Del(ctx context.Context, keys ...string) error {
for i := range keys {
keys[i] = c.key(keys[i])
}
return c.rdb.Del(ctx, keys...).Err()
}
func (c *Caches) Exists(ctx context.Context, key string) (bool, error) {
n, err := c.rdb.Exists(ctx, c.key(key)).Result()
if err != nil {
return false, err
}
return n > 0, nil
}
// SAdd 向 Set 中添加成员
func (c *Caches) SAdd(ctx context.Context, key string, members ...any) error {
return c.rdb.SAdd(ctx, c.key(key), members...).Err()
}
// SMembers 获取 Set 中所有成员
func (c *Caches) SMembers(ctx context.Context, key string) ([]string, error) {
return c.rdb.SMembers(ctx, c.key(key)).Result()
}
// DelSetMembers 删除 Set 中记录的所有成员对应的缓存 key并删除 Set 本身
func (c *Caches) DelSetMembers(ctx context.Context, key string) error {
members, err := c.SMembers(ctx, key)
if err != nil {
return err
}
keys := append(members, key)
return c.Del(ctx, keys...)
}
// SRem 从 Set 中移除成员
func (c *Caches) SRem(ctx context.Context, key string, members ...any) error {
return c.rdb.SRem(ctx, c.key(key), members...).Err()
}
// SIsMember 判断 member 是否在 Set 中
func (c *Caches) SIsMember(ctx context.Context, key string, member any) (bool, error) {
return c.rdb.SIsMember(ctx, c.key(key), member).Result()
}
// MGet 批量获取多个 key 的值
func (c *Caches) MGet(ctx context.Context, keys ...string) (map[string]string, error) {
if len(keys) == 0 {
return map[string]string{}, nil
}
redisKeys := make([]string, len(keys))
for i, key := range keys {
redisKeys[i] = c.key(key)
}
values, err := c.rdb.MGet(ctx, redisKeys...).Result()
if err != nil {
return nil, err
}
result := make(map[string]string, len(keys))
for i, value := range values {
if value == nil {
continue
}
str, ok := value.(string)
if !ok {
continue
}
result[keys[i]] = str
}
return result, nil
}
// DelByPrefix 批量删除指定前缀的缓存键
func (c *Caches) DelByPrefix(ctx context.Context, prefix string) error {
prefix = c.key(prefix)
if !strings.HasSuffix(prefix, "*") {
prefix += "*"
}
var cursor uint64
var keys []string
var err error
for {
keys, cursor, err = c.rdb.Scan(ctx, cursor, prefix, 100).Result()
if err != nil {
return err
}
if len(keys) > 0 {
if err = c.rdb.Del(ctx, keys...).Err(); err != nil {
return err
}
}
if cursor == 0 {
break
}
}
return nil
}
func (c *Caches) Expire(ctx context.Context, key string, expiration time.Duration) error {
return c.rdb.Expire(ctx, c.keyPrefix+key, expiration).Err()
}

25
internal/pkg/cache/cachekey/key.go vendored Normal file
View File

@@ -0,0 +1,25 @@
package cachekey
import "fmt"
const (
UserApiPermissionsPattern = "user:api:permissions:*"
UserInfoPattern = "user:info:*"
)
func UserApiPermissions(id int32) string {
return fmt.Sprintf("user:api:permissions:%d", id)
}
func UserInfo(id int32) string {
return fmt.Sprintf("user:info:%d", id)
}
func AuthRefresh(hash string) string {
return fmt.Sprintf("auth:refresh:%s", hash)
}
// AuthRefreshUser 反向索引 用于定位refresh token来删除
func AuthRefreshUser(userID int32) string {
return fmt.Sprintf("auth:refresh:user:%d", userID)
}

View File

@@ -14,6 +14,8 @@ const (
SysPermissionsCodeKey = "sys_permissions_code_key"
SysApisMethodPathKey = "sys_apis_method_path_key"
CategoryCodeKey = "categories_code_key"
TagCodeKey = "tags_code_key"
SysMenusPathUniqueIdx = "sys_menus_path_unique_idx"
)
func MapRowsAffected(rows int64, err error, notFoundErr error) error {

View File

@@ -20,6 +20,7 @@ func New(httpCode int, msg string) *AppError {
var (
ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误")
ErrUserDisabled = New(http.StatusForbidden, "用户已被禁用")
ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效")
ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息")
ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效")
@@ -28,6 +29,7 @@ var (
ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant")
ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在")
ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在")
ErrTagNotFound = New(http.StatusNotFound, "标签数据不存在")
ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在")
ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在")
ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在")
@@ -40,9 +42,11 @@ var (
ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空")
ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误")
ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在")
ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码已存在")
ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug已存在")
ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码已存在")
ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码已存在")
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)已存在")
ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码不允许重复")
ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug不允许重复")
ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码不允许重复")
ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码不允许重复")
ErrTagCodeAlreadyExists = New(http.StatusBadRequest, "标签编码不允许重复")
ErrSysMenusPathUniqueIdx = New(http.StatusBadRequest, "菜单路径不允许重复")
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)不允许重复")
)