diff --git a/cmd/server/main.go b/cmd/server/main.go index e5e71ed..436c213 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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(), diff --git a/internal/cache/share_code_cache.go b/internal/cache/share_code_cache.go index bfcde88..6013b85 100644 --- a/internal/cache/share_code_cache.go +++ b/internal/cache/share_code_cache.go @@ -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) diff --git a/internal/config/config.go b/internal/config/config.go index 7593275..774862f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/handler/share_code.go b/internal/handler/share_code.go index b8c39e9..01b8385 100644 --- a/internal/handler/share_code.go +++ b/internal/handler/share_code.go @@ -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)) diff --git a/internal/router/router.go b/internal/router/router.go index 0eb3c64..9699fa6 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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") { diff --git a/scripts/upload.sh b/scripts/upload.sh index 989f963..69dbc6e 100755 --- a/scripts/upload.sh +++ b/scripts/upload.sh @@ -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}/"