## 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+ 文件)
242 lines
5.8 KiB
Go
242 lines
5.8 KiB
Go
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,
|
||
}
|
||
}
|