first commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// authTokenSAddScript 向用户 token 集合加入 refresh_token,仅在集合由空变为非空时设置 TTL,
|
||||
// 避免每次登录都刷新集合 TTL 导致旧 token 条目长期驻留。
|
||||
const authTokenSAddScript = `
|
||||
local added = redis.call('SADD', KEYS[1], ARGV[1])
|
||||
if added == 1 and redis.call('SCARD', KEYS[1]) == 1 then
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
||||
end
|
||||
return added
|
||||
`
|
||||
|
||||
// AuthTokenSet 登录成功后写入会话:
|
||||
// - access_token -> user_id
|
||||
// - refresh_token -> user_id
|
||||
// - user_id 的 token 集合加入 refresh_token(用于多端管理 / 登出)
|
||||
func AuthTokenSet(ctx context.Context, userID int64, accessToken, refreshToken string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthTokenSet")
|
||||
defer span.End()
|
||||
|
||||
accessKey := fmt.Sprintf(redismodel.KeyAuthAccess, accessToken)
|
||||
refreshKey := fmt.Sprintf(redismodel.KeyAuthRefresh, refreshToken)
|
||||
userTokensKey := fmt.Sprintf(redismodel.KeyAuthUserTokens, userID)
|
||||
|
||||
accessTTL := time.Duration(global.JWTSetting.AccessTokenExpire) * time.Second
|
||||
refreshTTL := time.Duration(global.JWTSetting.RefreshTokenExpire) * time.Second
|
||||
|
||||
pipe := global.RedisEngine.TxPipeline()
|
||||
pipe.Set(ctx, accessKey, userID, accessTTL)
|
||||
pipe.Set(ctx, refreshKey, userID, refreshTTL)
|
||||
pipe.Eval(ctx, authTokenSAddScript, []string{userTokensKey}, refreshToken, int(refreshTTL.Seconds()))
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthTokenDel 登出/失效:删除 access、refresh 映射,并从用户 token 集合移除 refresh_token
|
||||
func AuthTokenDel(ctx context.Context, userID int64, accessToken, refreshToken string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthTokenDel")
|
||||
defer span.End()
|
||||
|
||||
accessKey := fmt.Sprintf(redismodel.KeyAuthAccess, accessToken)
|
||||
refreshKey := fmt.Sprintf(redismodel.KeyAuthRefresh, refreshToken)
|
||||
userTokensKey := fmt.Sprintf(redismodel.KeyAuthUserTokens, userID)
|
||||
|
||||
pipe := global.RedisEngine.TxPipeline()
|
||||
pipe.Del(ctx, accessKey)
|
||||
pipe.Del(ctx, refreshKey)
|
||||
pipe.SRem(ctx, userTokensKey, refreshToken)
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthUserIDByAccess 通过 access token 查询 user_id,token 不存在或已过期返回 redis.Nil
|
||||
func AuthUserIDByAccess(ctx context.Context, accessToken string) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthUserIDByAccess")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyAuthAccess, accessToken)
|
||||
userID, err := global.RedisEngine.Get(ctx, key).Int64()
|
||||
if err != nil && err != redis.Nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return userID, err
|
||||
}
|
||||
|
||||
// AuthUserIDByRefresh 通过 refresh token 查询 user_id,token 不存在或已过期返回 redis.Nil
|
||||
func AuthUserIDByRefresh(ctx context.Context, refreshToken string) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthUserIDByRefresh")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyAuthRefresh, refreshToken)
|
||||
userID, err := global.RedisEngine.Get(ctx, key).Int64()
|
||||
if err != nil && err != redis.Nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return userID, err
|
||||
}
|
||||
|
||||
// sessionKeyTTL session_key 缓存时长(30 天,与微信 session_key 有效期对齐)。
|
||||
const sessionKeyTTL = 30 * 24 * time.Hour
|
||||
|
||||
// AuthSessionKeySet 写入用户的 session_key(登录时 code2session 返回,用于虚拟支付 signature 计算)。
|
||||
func AuthSessionKeySet(ctx context.Context, userID int64, sessionKey string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthSessionKeySet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyAuthSessionKey, userID)
|
||||
if err := global.RedisEngine.Set(ctx, key, sessionKey, sessionKeyTTL).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthSessionKeyGet 读取用户的 session_key,不存在返回 redis.Nil。
|
||||
func AuthSessionKeyGet(ctx context.Context, userID int64) (string, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthSessionKeyGet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyAuthSessionKey, userID)
|
||||
sessionKey, err := global.RedisEngine.Get(ctx, key).Result()
|
||||
if err != nil && err != redis.Nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return sessionKey, err
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
)
|
||||
|
||||
// viewCountTTL 访问计数 key 的过期时间,避免长期驻留导致内存无界增长。
|
||||
const viewCountTTL = 7 * 24 * time.Hour
|
||||
|
||||
// ClosetShareViewIncr 衣柜分享访问计数 +1,返回最新值
|
||||
func ClosetShareViewIncr(ctx context.Context, shareUUID string) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.ClosetShareViewIncr")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyClosetShareView, shareUUID)
|
||||
val, err := quotaDailyIncrScript.Run(ctx, global.RedisEngine, []string{key}, int(viewCountTTL.Seconds())).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// collectSetTTL 收藏集合 key 的过期时间,避免长期驻留导致与 MySQL 弱一致漂移。
|
||||
const collectSetTTL = 7 * 24 * time.Hour
|
||||
|
||||
// CollectAdd 用户收藏(collect_type + target_id 分桶,成员为 user_id)
|
||||
func CollectAdd(ctx context.Context, collectType int8, targetID, userID int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.CollectAdd")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyCollectSet, collectType, targetID)
|
||||
if err := global.RedisEngine.SAdd(ctx, key, userID).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
// 设置 TTL(best-effort)。
|
||||
_ = global.RedisEngine.Expire(ctx, key, collectSetTTL).Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CollectRemove 用户取消收藏
|
||||
func CollectRemove(ctx context.Context, collectType int8, targetID, userID int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.CollectRemove")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyCollectSet, collectType, targetID)
|
||||
if err := global.RedisEngine.SRem(ctx, key, userID).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CollectIsMember 判断用户是否已收藏;集合 key 不存在(缓存过期/冷启动)时返回 redis.Nil,
|
||||
// 由调用方回落 MySQL 以区分「未命中」与「确实未收藏」。
|
||||
func CollectIsMember(ctx context.Context, collectType int8, targetID, userID int64) (bool, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.CollectIsMember")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyCollectSet, collectType, targetID)
|
||||
exists, err := global.RedisEngine.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return false, err
|
||||
}
|
||||
if exists == 0 {
|
||||
return false, redis.Nil
|
||||
}
|
||||
ok, err := global.RedisEngine.SIsMember(ctx, key, userID).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return false, err
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// CollectCount 获取收藏总数
|
||||
func CollectCount(ctx context.Context, collectType int8, targetID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.CollectCount")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyCollectSet, collectType, targetID)
|
||||
cnt, err := global.RedisEngine.SCard(ctx, key).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return cnt, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// HoroscopeSet 缓存星座运势内容(zodiac + 日期维度,data 由上层负责序列化)
|
||||
func HoroscopeSet(ctx context.Context, zodiac string, statDate int64, data []byte, ttl time.Duration) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.HoroscopeSet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyHoroscope, zodiac, statDate)
|
||||
if err := global.RedisEngine.Set(ctx, key, data, ttl).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HoroscopeGet 读取星座运势缓存,未命中返回 redis.Nil
|
||||
func HoroscopeGet(ctx context.Context, zodiac string, statDate int64) ([]byte, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.HoroscopeGet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyHoroscope, zodiac, statDate)
|
||||
data, err := global.RedisEngine.Get(ctx, key).Bytes()
|
||||
if err != nil && err != redis.Nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// likeSetTTL 点赞集合 key 的过期时间,避免长期驻留导致与 MySQL 弱一致漂移。
|
||||
const likeSetTTL = 7 * 24 * time.Hour
|
||||
|
||||
// LikeAdd 用户点赞(like_type + target_id 分桶,成员为 user_id)
|
||||
func LikeAdd(ctx context.Context, likeType int8, targetID, userID int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.LikeAdd")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyLikeSet, likeType, targetID)
|
||||
if err := global.RedisEngine.SAdd(ctx, key, userID).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
// 设置 TTL(best-effort)。
|
||||
_ = global.RedisEngine.Expire(ctx, key, likeSetTTL).Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LikeRemove 用户取消点赞
|
||||
func LikeRemove(ctx context.Context, likeType int8, targetID, userID int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.LikeRemove")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyLikeSet, likeType, targetID)
|
||||
if err := global.RedisEngine.SRem(ctx, key, userID).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LikeIsMember 判断用户是否已点赞;集合 key 不存在(缓存过期/冷启动)时返回 redis.Nil,
|
||||
// 由调用方回落 MySQL 以区分「未命中」与「确实未点赞」。
|
||||
func LikeIsMember(ctx context.Context, likeType int8, targetID, userID int64) (bool, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.LikeIsMember")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyLikeSet, likeType, targetID)
|
||||
exists, err := global.RedisEngine.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return false, err
|
||||
}
|
||||
if exists == 0 {
|
||||
return false, redis.Nil
|
||||
}
|
||||
ok, err := global.RedisEngine.SIsMember(ctx, key, userID).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return false, err
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// LikeCount 获取点赞总数
|
||||
func LikeCount(ctx context.Context, likeType int8, targetID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.LikeCount")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyLikeSet, likeType, targetID)
|
||||
cnt, err := global.RedisEngine.SCard(ctx, key).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return cnt, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// unlockScript 仅当锁值匹配时才删除,防止误删其他持有者的锁
|
||||
var unlockScript = redis.NewScript(`
|
||||
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('DEL', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`)
|
||||
|
||||
// TryLock 尝试获取分布式锁(SET NX EX),获取成功返回 true。
|
||||
// value 建议传唯一标识(如 request_id / uuid),用于释放时校验持有者。
|
||||
func TryLock(ctx context.Context, key, value string, ttl time.Duration) (bool, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.TryLock")
|
||||
defer span.End()
|
||||
|
||||
ok, err := global.RedisEngine.SetNX(ctx, key, value, ttl).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return false, err
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// Unlock 释放分布式锁,仅当 value 匹配时才删除;锁不存在或不属于自己时静默返回。
|
||||
func Unlock(ctx context.Context, key, value string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.Unlock")
|
||||
defer span.End()
|
||||
|
||||
if err := unlockScript.Run(ctx, global.RedisEngine, []string{key}, value).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// notificationUnreadTTL 未读数 key 的过期时间,避免长期驻留与 MySQL 弱一致漂移。
|
||||
const notificationUnreadTTL = 7 * 24 * time.Hour
|
||||
|
||||
// notificationUnreadIncrScript 首次自增时设置过期时间,保证 INCR 与 EXPIRE 原子执行。
|
||||
var notificationUnreadIncrScript = redis.NewScript(`
|
||||
local v = redis.call('INCR', KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
return v
|
||||
`)
|
||||
|
||||
// notificationUnreadDecrScript 未读数 -1,保证结果不低于 0
|
||||
var notificationUnreadDecrScript = redis.NewScript(`
|
||||
local v = redis.call('GET', KEYS[1])
|
||||
if v and tonumber(v) > 0 then
|
||||
return redis.call('DECRBY', KEYS[1], 1)
|
||||
end
|
||||
return 0
|
||||
`)
|
||||
|
||||
// NotificationUnreadIncr 用户未读消息数 +1,返回最新值
|
||||
func NotificationUnreadIncr(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadIncr")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyNotificationUnread, userID)
|
||||
val, err := notificationUnreadIncrScript.Run(ctx, global.RedisEngine, []string{key}, int(notificationUnreadTTL.Seconds())).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// NotificationUnreadDecr 用户未读消息数 -1,返回扣减后的值(不低于 0)
|
||||
func NotificationUnreadDecr(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadDecr")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyNotificationUnread, userID)
|
||||
val, err := notificationUnreadDecrScript.Run(ctx, global.RedisEngine, []string{key}).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// NotificationUnreadGet 获取用户未读消息数,不存在时返回 0
|
||||
func NotificationUnreadGet(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadGet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyNotificationUnread, userID)
|
||||
v, err := global.RedisEngine.Get(ctx, key).Int64()
|
||||
if err == redis.Nil {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// NotificationUnreadSet 回填未读数缓存(DB 对账回源后使用),避免 key 过期导致后续扣减被吞。
|
||||
func NotificationUnreadSet(ctx context.Context, userID int64, cnt int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadSet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyNotificationUnread, userID)
|
||||
if err := global.RedisEngine.Set(ctx, key, cnt, notificationUnreadTTL).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
)
|
||||
|
||||
// OutfitViewIncr 公开穿搭浏览计数 +1,返回最新值
|
||||
func OutfitViewIncr(ctx context.Context, outfitID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.OutfitViewIncr")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyOutfitView, outfitID)
|
||||
val, err := quotaDailyIncrScript.Run(ctx, global.RedisEngine, []string{key}, int(viewCountTTL.Seconds())).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// quotaDailyTTL 每日计数 key 的过期时间(跨天,保证到期自然过期)
|
||||
const quotaDailyTTL = 48 * time.Hour
|
||||
|
||||
// quotaDailyIncrScript 首次自增时设置过期时间,保证 INCR 与 EXPIRE 原子执行
|
||||
var quotaDailyIncrScript = redis.NewScript(`
|
||||
local v = redis.call('INCR', KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
return v
|
||||
`)
|
||||
|
||||
// quotaDailyIncr 每日计数 +1
|
||||
func quotaDailyIncr(ctx context.Context, key string) (int64, error) {
|
||||
return quotaDailyIncrScript.Run(ctx, global.RedisEngine, []string{key}, int(quotaDailyTTL.Seconds())).Int64()
|
||||
}
|
||||
|
||||
// quotaDailyValue 读取每日计数,key 不存在时返回 0
|
||||
func quotaDailyValue(ctx context.Context, key string) (int64, error) {
|
||||
v, err := global.RedisEngine.Get(ctx, key).Int64()
|
||||
if err == redis.Nil {
|
||||
return 0, nil
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// QuotaDailyAIIncr 当日 AI 推荐次数 +1
|
||||
func QuotaDailyAIIncr(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyAIIncr")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyIncr(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyAI, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// QuotaDailyOutfitShareIncr 当日社区分享(搭配分享)次数 +1
|
||||
func QuotaDailyOutfitShareIncr(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyOutfitShareIncr")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyIncr(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyOutfitShare, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// QuotaDailyClosetShareIncr 当日衣柜分享次数 +1
|
||||
func QuotaDailyClosetShareIncr(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyClosetShareIncr")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyIncr(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyClosetShare, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// QuotaDailyAIValue 当日 AI 推荐次数
|
||||
func QuotaDailyAIValue(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyAIValue")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyValue(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyAI, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// QuotaDailyOutfitShareValue 当日社区分享(搭配分享)次数
|
||||
func QuotaDailyOutfitShareValue(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyOutfitShareValue")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyValue(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyOutfitShare, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// QuotaDailyClosetShareValue 当日衣柜分享次数
|
||||
func QuotaDailyClosetShareValue(ctx context.Context, userID int64, date int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.QuotaDailyClosetShareValue")
|
||||
defer span.End()
|
||||
|
||||
val, err := quotaDailyValue(ctx, fmt.Sprintf(redismodel.KeyQuotaDailyClosetShare, userID, date))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ---- 通用每日额度原子限流(带限额) ----
|
||||
|
||||
// dailyQuotaConsumeScript 带限额的原子自增:
|
||||
// KEYS[1]=当日计数 key;ARGV[1]=每日上限;ARGV[2]=消费数量;ARGV[3]=TTL 秒。
|
||||
// 当前计数 + n 超过上限返回 -1;否则 INCRBY 并在首次写入时设置 TTL,返回消费后的当日计数。
|
||||
var dailyQuotaConsumeScript = redis.NewScript(`
|
||||
local v = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
local limit = tonumber(ARGV[1])
|
||||
local n = tonumber(ARGV[2])
|
||||
if v + n > limit then
|
||||
return -1
|
||||
end
|
||||
local r = redis.call('INCRBY', KEYS[1], n)
|
||||
if v == 0 then
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[3])
|
||||
end
|
||||
return r
|
||||
`)
|
||||
|
||||
// dailyQuotaRefundScript 回退当日计数(DECRBY 且不小于 0)。
|
||||
// ARGV[1]=回退数量。
|
||||
var dailyQuotaRefundScript = redis.NewScript(`
|
||||
local v = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
local n = tonumber(ARGV[1])
|
||||
if v <= 0 then
|
||||
return 0
|
||||
end
|
||||
if n > v then
|
||||
n = v
|
||||
end
|
||||
return redis.call('DECRBY', KEYS[1], n)
|
||||
`)
|
||||
|
||||
// DailyQuotaConsume 原子消费当日额度。
|
||||
// 返回 (消费后计数, nil);额度不足返回 (-1, nil);Redis 异常返回 (0, err)。
|
||||
func DailyQuotaConsume(ctx context.Context, key string, limit int64, n int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.DailyQuotaConsume")
|
||||
defer span.End()
|
||||
|
||||
if limit <= 0 || n <= 0 {
|
||||
return -1, nil
|
||||
}
|
||||
val, err := dailyQuotaConsumeScript.Run(ctx, global.RedisEngine, []string{key}, limit, n, int(quotaDailyTTL.Seconds())).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// DailyQuotaRefund 回退当日计数(保证不小于 0)。失败仅记录,调用方按 best-effort 处理。
|
||||
func DailyQuotaRefund(ctx context.Context, key string, n int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.DailyQuotaRefund")
|
||||
defer span.End()
|
||||
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := dailyQuotaRefundScript.Run(ctx, global.RedisEngine, []string{key}, n).Int64(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DailyQuotaValue 读取当日计数,key 不存在时返回 0。
|
||||
func DailyQuotaValue(ctx context.Context, key string) (int64, error) {
|
||||
return quotaDailyValue(ctx, key)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// userInfoTTL 用户鉴权信息缓存默认过期时间
|
||||
const userInfoTTL = 24 * time.Hour
|
||||
|
||||
// UserInfoSet 缓存用户鉴权核心信息(含 Status,供中间件鉴权时快速读取)
|
||||
func UserInfoSet(ctx context.Context, user *mysqlmodel.User, ttl time.Duration) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.UserInfoSet")
|
||||
defer span.End()
|
||||
|
||||
if ttl <= 0 {
|
||||
ttl = userInfoTTL
|
||||
}
|
||||
data, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
key := fmt.Sprintf(redismodel.KeyUserInfo, user.ID)
|
||||
if err := global.RedisEngine.Set(ctx, key, data, ttl).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserInfoDel 删除用户缓存(用户资料变更/注销时失效缓存)
|
||||
func UserInfoDel(ctx context.Context, userID int64) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.UserInfoDel")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyUserInfo, userID)
|
||||
if err := global.RedisEngine.Del(ctx, key).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserInfoGet 读取缓存中的用户鉴权核心信息,未命中返回 redis.Nil
|
||||
func UserInfoGet(ctx context.Context, userID int64) (*mysqlmodel.User, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.UserInfoGet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyUserInfo, userID)
|
||||
data, err := global.RedisEngine.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
if err != redis.Nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var user mysqlmodel.User
|
||||
if err := json.Unmarshal(data, &user); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
Reference in New Issue
Block a user