fix(sharecode): 支持分享码TTL配置并限制每MAC分享码数

- 配置增加 ShareCodeMaxPerMac 和 ShareCodeTTLMin 参数
- ShareCodeCache 支持自定义分享码有效期 TTL
- 生成分享码时使用配置的 TTL 替代固定30分钟
- 共享码处理添加限制,同一MAC有效分享码数量限制
- 路由层、处理器层和任务调度中增加TTL及限额参数传递
- 同步脚本默认同步路径调整,去除默认同步Docker文件,增加docs目录同步
- 修正上传脚本拼接远程路径时尾部斜杠问题
This commit is contained in:
eafonyang
2026-06-12 18:29:08 +08:00
parent 28fb798926
commit 598bbd4998
6 changed files with 82 additions and 41 deletions
+2 -2
View File
@@ -88,7 +88,7 @@ func main() {
devicePersistTask := task.NewDevicePersistTask(rdb, deviceRepo, log)
devicePersistTask.Start(5 * time.Minute)
shareCodeCache := cache.NewShareCodeCache(rdb)
shareCodeCache := cache.NewShareCodeCache(rdb, time.Duration(cfg.ShareCodeTTLMin)*time.Minute)
shareCodeRepo := repository.NewShareCodeRepository(db)
shareCodePersistTask := task.NewShareCodePersistTask(shareCodeCache, shareCodeRepo, log)
shareCodePersistTask.Start(5 * time.Minute)
@@ -105,7 +105,7 @@ func main() {
zap.String("region", cfg.S3.Region),
)
engine := router.New(log, db, searchClient, rdb, cfg.Equalize, s3Storage)
engine := router.New(log, db, searchClient, rdb, cfg.Equalize, s3Storage, cfg.ShareCodeMaxPerMac, time.Duration(cfg.ShareCodeTTLMin)*time.Minute)
srv := &http.Server{
Addr: cfg.Addr(),
+9 -8
View File
@@ -51,7 +51,6 @@ const (
shareImportFlushLockPref = "share:import:flush:lock:"
shareMacIndexPrefix = "share:mac:"
shareCodeLength = 5
shareCodeTTL = 30 * time.Minute
shareMacIndexTTL = 1 * time.Hour
shareImportPendingTTL = 12 * time.Hour
shareCodeCharset = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
@@ -101,19 +100,21 @@ type ShareCodeData struct {
}
type ShareCodeCache struct {
rdb *redis.Client
rdb *redis.Client
codeTTL time.Duration
}
func NewShareCodeCache(rdb *redis.Client) *ShareCodeCache {
return &ShareCodeCache{rdb: rdb}
func NewShareCodeCache(rdb *redis.Client, codeTTL time.Duration) *ShareCodeCache {
return &ShareCodeCache{rdb: rdb, codeTTL: codeTTL}
}
func ShareCodeTTL() time.Duration {
return shareCodeTTL
// ShareCodeTTL returns the configured share code TTL.
func (c *ShareCodeCache) ShareCodeTTL() time.Duration {
return c.codeTTL
}
func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqData []byte) (*ShareCodeData, error) {
expireAt := time.Now().Add(shareCodeTTL)
expireAt := time.Now().Add(c.codeTTL)
eqJSON := string(eqData)
for i := 0; i < shareCodeMaxRetries; i++ {
@@ -125,7 +126,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(shareCodeTTL.Seconds()), code, expireAt.Unix(), int(shareMacIndexTTL.Seconds()),
macAddr, ipAddr, eqJSON, expireAt.Format(time.RFC3339), int(c.codeTTL.Seconds()), code, expireAt.Unix(), int(shareMacIndexTTL.Seconds()),
).Int()
if err != nil {
return nil, fmt.Errorf("create share code in redis: %w", err)
+34 -18
View File
@@ -7,15 +7,17 @@ import (
)
type Config struct {
Env string
Host string
Port int
Database DatabaseConfig
Meilisearch MeilisearchConfig
Redis RedisConfig
Equalize EqualizeConfig
S3 S3Config
EnablePersistTask bool
Env string
Host string
Port int
Database DatabaseConfig
Meilisearch MeilisearchConfig
Redis RedisConfig
Equalize EqualizeConfig
S3 S3Config
EnablePersistTask bool
ShareCodeMaxPerMac int
ShareCodeTTLMin int
}
func Load() (*Config, error) {
@@ -48,15 +50,17 @@ func Load() (*Config, error) {
s3cfg := loadS3(env)
return &Config{
Env: env,
Host: getEnv("APP_HOST", "0.0.0.0"),
Port: port,
Database: db,
Meilisearch: ms,
Redis: rd,
Equalize: eq,
S3: s3cfg,
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
Env: env,
Host: getEnv("APP_HOST", "0.0.0.0"),
Port: port,
Database: db,
Meilisearch: ms,
Redis: rd,
Equalize: eq,
S3: s3cfg,
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
ShareCodeTTLMin: getEnvInt("SHARE_CODE_TTL_MIN", 30),
}, nil
}
@@ -70,3 +74,15 @@ func getEnv(key, fallback string) string {
}
return fallback
}
func getEnvInt(key string, fallback int) int {
v := os.Getenv(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}
+24 -4
View File
@@ -12,12 +12,13 @@ import (
)
type ShareCodeHandler struct {
cache *cache.ShareCodeCache
log *zap.Logger
cache *cache.ShareCodeCache
log *zap.Logger
maxPerMac int
}
func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger) *ShareCodeHandler {
return &ShareCodeHandler{cache: shareCache, log: log}
func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger, maxPerMac int) *ShareCodeHandler {
return &ShareCodeHandler{cache: shareCache, log: log, maxPerMac: maxPerMac}
}
// ExportShareCode 导出分享码
@@ -69,6 +70,25 @@ func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
}
ctx := c.Request.Context()
// 限制同一 MAC 在有效期内的分享码数量
existing, err := h.cache.ListByMac(ctx, mac)
if err != nil {
h.log.Error("check existing share codes by mac failed", zap.String("mac", mac), zap.Error(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "系统错误",
})
return
}
if len(existing) >= h.maxPerMac {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "已有未过期的分享码,请稍后再试",
})
return
}
data, err := h.cache.Create(ctx, mac, clientIP, []byte(eqDataRaw))
if err != nil {
h.log.Error("create share code failed", zap.Error(err))
+4 -3
View File
@@ -2,6 +2,7 @@ package router
import (
"database/sql"
"time"
"github.com/gin-gonic/gin"
"github.com/luxsin/app-api/internal/cache"
@@ -18,7 +19,7 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
)
func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Client, eqCfg config.EqualizeConfig, s3 *storage.S3Storage) *gin.Engine {
func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Client, eqCfg config.EqualizeConfig, s3 *storage.S3Storage, shareCodeMaxPerMac int, shareCodeTTL time.Duration) *gin.Engine {
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.RequestID())
@@ -36,7 +37,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
otaRepo := repository.NewOTARepository(db)
curveRepo := repository.NewCurveRepository(db)
shareCodeCache := cache.NewShareCodeCache(rdb)
shareCodeCache := cache.NewShareCodeCache(rdb, shareCodeTTL)
// Handler
health := handler.NewHealthHandler()
@@ -47,7 +48,7 @@ func New(log *zap.Logger, db *sql.DB, searchClient *search.Client, rdb *redis.Cl
ota := handler.NewOTAHandler(otaRepo, log)
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log)
modelCSV := handler.NewModelCSVHandler(s3, log)
shareCode := handler.NewShareCodeHandler(shareCodeCache, log)
shareCode := handler.NewShareCodeHandler(shareCodeCache, log, shareCodeMaxPerMac)
v1 := r.Group("/api/v1")
{
+9 -6
View File
@@ -1,16 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE_DIR="/data/project/app-api/"
REMOTE_DIR="/data/project/app-api"
DEFAULT_PATHS=(
./cmd/
./internal/
./pkg
./pkg/
./docs/
./go.mod
./go.sum
./Dockerfile
./docker-compose.yml
)
show_help() {
@@ -30,12 +29,15 @@ show_help() {
路径:
指定要同步的文件或目录,可传多个。
省略时使用默认路径: cmd/ internal/ pkg go.mod go.sum Dockerfile docker-compose.yml
省略时使用默认路径: cmd/ internal/ pkg/ docs/ go.mod go.sum
注意: Dockerfile 和 docker-compose.yml 未包含在默认路径中(如需同步请手动指定)
示例:
upload.sh 同步默认路径到 api1 和 api2
upload.sh api1 同步默认路径到 api1
upload.sh api2 ./internal 同步 internal/ 到 api2
upload.sh api1 Dockerfile 单独同步 Dockerfile 到 api1
upload.sh -n 虚拟执行,预览同步到 api1 和 api2
upload.sh -n api1 ./internal 虚拟执行,预览同步到 api1
EOF
@@ -73,7 +75,8 @@ upload_to() {
local item rel remote_path
for item in "${paths[@]}"; do
rel="${item#./}"
remote_path="${REMOTE_DIR}${rel}"
rel="${rel%/}" # 剥离尾部斜杠,避免拼接时出现双斜杠
remote_path="${REMOTE_DIR}/${rel}"
if [[ -d "$item" ]]; then
echo " 同步目录: $item -> ${remote_host}:${remote_path}/"