first commit

This commit is contained in:
2026-09-11 01:47:20 +08:00
commit 303207d3cc
74 changed files with 8348 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
package redisdao
import (
"context"
"fmt"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-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 -> admin_id
// - refresh_token -> admin_id
// - admin_id 的 token 集合加入 refresh_token(用于多端管理 / 登出)
func AuthTokenSet(ctx context.Context, adminID 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)
adminTokensKey := fmt.Sprintf(redismodel.KeyAuthUserTokens, adminID)
accessTTL := time.Duration(global.JWTSetting.AccessTokenExpire) * time.Second
refreshTTL := time.Duration(global.JWTSetting.RefreshTokenExpire) * time.Second
pipe := global.RedisEngine.TxPipeline()
pipe.Set(ctx, accessKey, adminID, accessTTL)
pipe.Set(ctx, refreshKey, adminID, refreshTTL)
pipe.Eval(ctx, authTokenSAddScript, []string{adminTokensKey}, 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, adminID 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)
adminTokensKey := fmt.Sprintf(redismodel.KeyAuthUserTokens, adminID)
pipe := global.RedisEngine.TxPipeline()
pipe.Del(ctx, accessKey)
pipe.Del(ctx, refreshKey)
pipe.SRem(ctx, adminTokensKey, refreshToken)
if _, err := pipe.Exec(ctx); err != nil {
span.RecordError(err)
return err
}
return nil
}
// AuthAdminIDByAccess 通过 access token 查询 admin_id,token 不存在或已过期返回 redis.Nil
func AuthAdminIDByAccess(ctx context.Context, accessToken string) (int64, error) {
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthAdminIDByAccess")
defer span.End()
key := fmt.Sprintf(redismodel.KeyAuthAccess, accessToken)
adminID, err := global.RedisEngine.Get(ctx, key).Int64()
if err != nil && err != redis.Nil {
span.RecordError(err)
}
return adminID, err
}
// AuthAdminIDByRefresh 通过 refresh token 查询 admin_id,token 不存在或已过期返回 redis.Nil
func AuthAdminIDByRefresh(ctx context.Context, refreshToken string) (int64, error) {
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthAdminIDByRefresh")
defer span.End()
key := fmt.Sprintf(redismodel.KeyAuthRefresh, refreshToken)
adminID, err := global.RedisEngine.Get(ctx, key).Int64()
if err != nil && err != redis.Nil {
span.RecordError(err)
}
return adminID, err
}
// AuthAdminTokensDelAll 失效某管理员全部 token(禁用/删除账号时调用)。
func AuthAdminTokensDelAll(ctx context.Context, adminID int64) error {
ctx, span := global.Tracer.Start(ctx, "redisdao.AuthAdminTokensDelAll")
defer span.End()
adminTokensKey := fmt.Sprintf(redismodel.KeyAuthUserTokens, adminID)
members, err := global.RedisEngine.SMembers(ctx, adminTokensKey).Result()
if err != nil && err != redis.Nil {
span.RecordError(err)
return err
}
pipe := global.RedisEngine.TxPipeline()
for _, refreshToken := range members {
pipe.Del(ctx, fmt.Sprintf(redismodel.KeyAuthRefresh, refreshToken))
}
// access token 没有全集索引(TTL 短,最多 AccessTokenExpire 秒后自然过期),
// 由 JWTAuth 的 DB 状态回查兜底拦截。
pipe.Del(ctx, adminTokensKey)
if _, err := pipe.Exec(ctx); err != nil {
span.RecordError(err)
return err
}
return nil
}
+44
View File
@@ -0,0 +1,44 @@
package redisdao
import (
"context"
"fmt"
"time"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"github.com/redis/go-redis/v9"
)
// businessRedisPrefix 业务服务(zt-closet-backend)的 Redis key 前缀。
// 管理后台推送通知时,必须自增业务服务所读的同一未读计数 key,
// 保证小程序端未读角标实时一致。要求管理后台与业务服务共用同一 Redis 库(config.yaml 的 Redis.DB)。
const businessRedisPrefix = "ztcloset:"
// KeyNotificationUnread 业务服务的用户未读通知计数 key(格式保持一致)。
var KeyNotificationUnread = businessRedisPrefix + "notification:unread:%d"
// notificationUnreadTTL 未读数 key 的过期时间,与业务服务保持一致。
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
`)
// 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(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
}