分享码有效期,支持表达式

This commit is contained in:
eafonyang
2026-06-15 20:02:43 +08:00
parent fc4ec2c030
commit b5560ca702
9 changed files with 130 additions and 9 deletions
+3 -4
View File
@@ -2,14 +2,14 @@
//
// # Redis 存储结构
//
// share:{code} Hash, TTL 30min — 分享码主数据
// share:{code} Hash, TTL = SHARE_CODE_TTL_MIN — 分享码主数据
// mac_addr 创建者 MAC 地址
// ip_addr 创建者 IP
// eq_data EQ 参数 JSON
// expire_at 过期时间 (RFC3339)
// persisted 是否已刷入 DB ("0"/"1")
//
// share:mac:{mac} ZSET, TTL 1h (兜底) — MAC 二级索引 (查询时主动清理过期成员)
// share:mac:{mac} ZSET, TTL = SHARE_CODE_TTL_MIN — MAC 二级索引 (查询时主动清理过期成员)
// member = share_code
// score = expire_at unix timestamp
//
@@ -51,7 +51,6 @@ const (
shareImportFlushLockPref = "share:import:flush:lock:"
shareMacIndexPrefix = "share:mac:"
shareCodeLength = 5
shareMacIndexTTL = 1 * time.Hour
shareImportPendingTTL = 12 * time.Hour
shareCodeCharset = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
shareCodeMaxRetries = 20
@@ -126,7 +125,7 @@ func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqD
key := shareCodeKey(code)
macIdxKey := shareMacIndexKey(macAddr)
ok, err := shareCreateScript.Run(ctx, c.rdb, []string{key, sharePendingSet, macIdxKey},
macAddr, ipAddr, eqJSON, expireAt.Format(time.RFC3339), int(c.codeTTL.Seconds()), code, expireAt.Unix(), int(shareMacIndexTTL.Seconds()),
macAddr, ipAddr, eqJSON, expireAt.Format(time.RFC3339), int(c.codeTTL.Seconds()), code, expireAt.Unix(), int(c.codeTTL.Seconds()),
).Int()
if err != nil {
return nil, fmt.Errorf("create share code in redis: %w", err)
+8 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"time"
)
type Config struct {
@@ -17,7 +18,7 @@ type Config struct {
S3 S3Config
EnablePersistTask bool
ShareCodeMaxPerMac int
ShareCodeTTLMin int
ShareCodeTTL time.Duration
}
func Load() (*Config, error) {
@@ -49,6 +50,11 @@ func Load() (*Config, error) {
eq := loadEqualize(env)
s3cfg := loadS3(env)
shareCodeTTL, err := parseShareCodeTTL(getEnv("SHARE_CODE_TTL_MIN", "30"))
if err != nil {
return nil, fmt.Errorf("invalid SHARE_CODE_TTL_MIN: %w", err)
}
return &Config{
Env: env,
Host: getEnv("APP_HOST", "0.0.0.0"),
@@ -60,7 +66,7 @@ func Load() (*Config, error) {
S3: s3cfg,
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
ShareCodeTTLMin: getEnvInt("SHARE_CODE_TTL_MIN", 30),
ShareCodeTTL: shareCodeTTL,
}, nil
}
+59
View File
@@ -0,0 +1,59 @@
package config
import (
"fmt"
"strconv"
"strings"
"time"
)
const shareCodeTTLMax = 365 * 24 * time.Hour
// parseShareCodeTTL parses SHARE_CODE_TTL_MIN values:
// - bare number (e.g. "30") = minutes (backward compatible)
// - with suffix: m (minutes), h (hours), d (days), e.g. "30m", "24h", "30d"
func parseShareCodeTTL(raw string) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 30 * time.Minute, nil
}
if n, err := strconv.Atoi(raw); err == nil {
if n <= 0 {
return 0, fmt.Errorf("must be positive")
}
d := time.Duration(n) * time.Minute
if d > shareCodeTTLMax {
return 0, fmt.Errorf("exceeds maximum of 365d")
}
return d, nil
}
if len(raw) < 2 {
return 0, fmt.Errorf("invalid format %q", raw)
}
unit := strings.ToLower(string(raw[len(raw)-1]))
numStr := raw[:len(raw)-1]
n, err := strconv.Atoi(numStr)
if err != nil || n <= 0 {
return 0, fmt.Errorf("invalid format %q", raw)
}
var d time.Duration
switch unit {
case "m":
d = time.Duration(n) * time.Minute
case "h":
d = time.Duration(n) * time.Hour
case "d":
d = time.Duration(n) * 24 * time.Hour
default:
return 0, fmt.Errorf("unknown unit %q (use m, h, or d)", unit)
}
if d > shareCodeTTLMax {
return 0, fmt.Errorf("exceeds maximum of 365d")
}
return d, nil
}
+47
View File
@@ -0,0 +1,47 @@
package config
import (
"testing"
"time"
)
func TestParseShareCodeTTL(t *testing.T) {
tests := []struct {
raw string
want time.Duration
wantErr bool
}{
{"", 30 * time.Minute, false},
{"30", 30 * time.Minute, false},
{"30m", 30 * time.Minute, false},
{"30M", 30 * time.Minute, false},
{"24h", 24 * time.Hour, false},
{"24H", 24 * time.Hour, false},
{"30d", 30 * 24 * time.Hour, false},
{"365d", 365 * 24 * time.Hour, false},
{"0", 0, true},
{"-1", 0, true},
{"0m", 0, true},
{"abc", 0, true},
{"30x", 0, true},
{"366d", 0, true},
}
for _, tt := range tests {
t.Run(tt.raw, func(t *testing.T) {
got, err := parseShareCodeTTL(tt.raw)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for %q", tt.raw)
}
return
}
if err != nil {
t.Fatalf("unexpected error for %q: %v", tt.raw, err)
}
if got != tt.want {
t.Fatalf("parseShareCodeTTL(%q) = %v, want %v", tt.raw, got, tt.want)
}
})
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger, maxP
// ExportShareCode 导出分享码
//
// @Summary 创建 EQ 分享码
// @Description 将用户的 EQ 数据生成一个 5 位分享码,有效期 30 分钟
// @Description 将用户的 EQ 数据生成一个 5 位分享码,有效期由 SHARE_CODE_TTL_MIN 配置决定(支持 30、30m、24h、30d 等格式)
// @Tags ShareCode
// @Accept json
// @Produce json