Files
mce/internal/session/redis_store.go
Victor_Jay 97adf54d6d fix: golangci-lint 零告警通过 (57→0) + gofumpt/goimports 全量格式化
## CI 修复 (P0/P1)

P0 — 编译阻塞:
  - interfaces.go: postUseCase ISP 接口移除不用的 Create/Update/Delete

P1 — 必须修复:
  - ST1000: 为 13 个包添加包注释 (common/config/model/service/...)
  - ST1005: redis_store.go 全部错误消息改为小写开头
  - errcheck (19处): defer Close()→闭包忽略, notifier.Create→_=, r.Run→检查error
  - errorlint (9处): switch-on-error→errors.Is 链, ==→errors.Is
  - ST1020/ST1022: 导出符号注释以符号名开头

P2 — 安全评审:
  - gosec (12处): G203/G301/G304/G306 添加 nolint 注释并附理由

P3 — 清理:
  - unused: 移除 hasUnicode/energyStore/current/energyAdminUseCase
  - gofumpt + goimports 全量格式化 (35+ 文件)
2026-06-22 02:27:35 +08:00

242 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package session
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
// Redis key 前缀前缀,避免与其他业务 key 冲突
const (
keyPrefix = "metalab:session:"
userKeyPrefix = "metalab:user_sessions:"
)
// RedisStore 基于 Redis 的会话存储(生产环境推荐)
// 利用 Redis TTL 自动过期,无须后台清理 goroutine
type RedisStore struct {
client *redis.Client
idleTimeout time.Duration // 不记住我:空闲超时
rememberTimeout time.Duration // 记住我:空闲超时
}
// NewRedisStore 创建 Redis 会话存储实例
func NewRedisStore(client *redis.Client, idleTimeout, rememberTimeout time.Duration) *RedisStore {
return &RedisStore{
client: client,
idleTimeout: idleTimeout,
rememberTimeout: rememberTimeout,
}
}
// userKey 返回用户会话索引的 Redis key
func (rs *RedisStore) userKey(uid uint) string {
return userKeyPrefix + fmt.Sprint(uid)
}
// sessionKeys 将 sid 列表转为完整的 Redis session key 列表
func (rs *RedisStore) sessionKeys(sids []string) []string {
keys := make([]string, len(sids))
for i, sid := range sids {
keys[i] = keyPrefix + sid
}
return keys
}
// Get 根据会话 ID 获取会话(过期返回 nil
func (rs *RedisStore) Get(id string) (*Session, error) {
ctx := context.Background()
data, err := rs.client.Get(ctx, keyPrefix+id).Bytes()
if errors.Is(err, redis.Nil) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("redis GET session: %w", err)
}
var s Session
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("redis 反序列化 session: %w", err)
}
return &s, nil
}
// Set 写入/更新会话并设置 TTL
func (rs *RedisStore) Set(s *Session) error {
ctx := context.Background()
data, err := json.Marshal(s)
if err != nil {
return fmt.Errorf("redis 序列化 session: %w", err)
}
ttl := rs.idleTimeout
if s.RememberMe {
ttl = rs.rememberTimeout
}
pipe := rs.client.Pipeline()
// 1. 写入会话数据
pipe.Set(ctx, keyPrefix+s.ID, data, ttl)
// 2. 将 sid 加入用户会话索引Redis SET
// 不设置 user_sessions 的 TTL——同一用户可能有不同 TTL 的会话,
// 设置单一 TTL 会导致长生命周期会话的索引被提前过期
pipe.SAdd(ctx, rs.userKey(s.UserID), s.ID)
_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("redis SET session: %w", err)
}
return nil
}
// Delete 删除单个会话
func (rs *RedisStore) Delete(id string) error {
ctx := context.Background()
// 先读取 session 获取 UserID再从集合中移除
data, err := rs.client.Get(ctx, keyPrefix+id).Bytes()
if errors.Is(err, redis.Nil) {
return nil
}
if err != nil {
return fmt.Errorf("redis GET session for delete: %w", err)
}
var s Session
if err := json.Unmarshal(data, &s); err == nil {
if sremErr := rs.client.SRem(ctx, rs.userKey(s.UserID), id).Err(); sremErr != nil {
log.Printf("[RedisStore] Delete: SRem failed sid=%s uid=%d err=%v", id[:16]+"...", s.UserID, sremErr)
}
}
return rs.client.Del(ctx, keyPrefix+id).Err()
}
// DeleteByUID 删除某用户的所有会话(改密/注销/强制下线)
func (rs *RedisStore) DeleteByUID(uid uint) error {
ctx := context.Background()
uidKey := rs.userKey(uid)
sids, err := rs.client.SMembers(ctx, uidKey).Result()
if err != nil {
return fmt.Errorf("redis SMEMBERS user_sessions: %w", err)
}
if len(sids) == 0 {
return nil
}
pipe := rs.client.Pipeline()
pipe.Del(ctx, rs.sessionKeys(sids)...)
pipe.Del(ctx, uidKey)
_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("redis DeleteByUID: %w", err)
}
log.Printf("[RedisStore] DeleteByUID uid=%d count=%d", uid, len(sids))
return nil
}
// DeleteByUIDExclude 删除某用户的所有会话,但保留指定的会话 ID
func (rs *RedisStore) DeleteByUIDExclude(uid uint, excludeSID string) error {
ctx := context.Background()
uidKey := rs.userKey(uid)
sids, err := rs.client.SMembers(ctx, uidKey).Result()
if err != nil {
return fmt.Errorf("redis SMEMBERS user_sessions: %w", err)
}
var toDelete []string
kept := 0
for _, sid := range sids {
if sid == excludeSID {
kept++
continue
}
toDelete = append(toDelete, sid)
}
if len(toDelete) == 0 {
return nil
}
pipe := rs.client.Pipeline()
pipe.Del(ctx, rs.sessionKeys(toDelete)...)
for _, sid := range toDelete {
pipe.SRem(ctx, uidKey, sid)
}
_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("redis DeleteByUIDExclude: %w", err)
}
log.Printf("[RedisStore] DeleteByUIDExclude uid=%d removed=%d kept=%d", uid, len(toDelete), kept)
return nil
}
// ListByUID 列出某用户的所有活跃会话
func (rs *RedisStore) ListByUID(uid uint) ([]*Session, error) {
ctx := context.Background()
uidKey := rs.userKey(uid)
sids, err := rs.client.SMembers(ctx, uidKey).Result()
if err != nil {
return nil, fmt.Errorf("redis SMEMBERS user_sessions: %w", err)
}
if len(sids) == 0 {
return nil, nil
}
results, err := rs.client.MGet(ctx, rs.sessionKeys(sids)...).Result()
if err != nil {
return nil, fmt.Errorf("redis MGET sessions: %w", err)
}
var sessions []*Session
for _, r := range results {
if r == nil {
continue
}
data, ok := r.(string)
if !ok {
continue
}
var s Session
if err := json.Unmarshal([]byte(data), &s); err != nil {
continue
}
sessions = append(sessions, &s)
}
return sessions, nil
}
// Cleanup Redis 自动 TTL 过期,无需手动清理
func (rs *RedisStore) Cleanup() int {
return 0
}
// StartCleanup Redis 模式无需后台清理 goroutine
func (rs *RedisStore) StartCleanup(interval time.Duration) {
// Redis TTL 自动过期,无需手动清理
}
// Metrics 返回 Redis 存储状态(活跃会话数统计代价高,返回 -1
func (rs *RedisStore) Metrics() StoreMetrics {
return StoreMetrics{
Type: "redis",
Healthy: true,
ActiveCount: -1,
}
}