198 lines
6.1 KiB
Go
198 lines
6.1 KiB
Go
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 baseline = tonumber(ARGV[4])
|
||
if baseline > v then v = baseline; redis.call('SET', KEYS[1], v); redis.call('EXPIRE', KEYS[1], ARGV[3]) end
|
||
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, baseline 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()), baseline).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)
|
||
}
|
||
|
||
// DailyQuotaDelete 删除每日计数缓存;MySQL 是权威账本,后续读取会从数据库恢复。
|
||
func DailyQuotaDelete(ctx context.Context, key string) error {
|
||
if key == "" || global.RedisEngine == nil {
|
||
return nil
|
||
}
|
||
return global.RedisEngine.Del(ctx, key).Err()
|
||
}
|