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 := task.NewDevicePersistTask(rdb, deviceRepo, log)
devicePersistTask.Start(5 * time.Minute) devicePersistTask.Start(5 * time.Minute)
shareCodeCache := cache.NewShareCodeCache(rdb) shareCodeCache := cache.NewShareCodeCache(rdb, time.Duration(cfg.ShareCodeTTLMin)*time.Minute)
shareCodeRepo := repository.NewShareCodeRepository(db) shareCodeRepo := repository.NewShareCodeRepository(db)
shareCodePersistTask := task.NewShareCodePersistTask(shareCodeCache, shareCodeRepo, log) shareCodePersistTask := task.NewShareCodePersistTask(shareCodeCache, shareCodeRepo, log)
shareCodePersistTask.Start(5 * time.Minute) shareCodePersistTask.Start(5 * time.Minute)
@@ -105,7 +105,7 @@ func main() {
zap.String("region", cfg.S3.Region), 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{ srv := &http.Server{
Addr: cfg.Addr(), Addr: cfg.Addr(),
+8 -7
View File
@@ -51,7 +51,6 @@ const (
shareImportFlushLockPref = "share:import:flush:lock:" shareImportFlushLockPref = "share:import:flush:lock:"
shareMacIndexPrefix = "share:mac:" shareMacIndexPrefix = "share:mac:"
shareCodeLength = 5 shareCodeLength = 5
shareCodeTTL = 30 * time.Minute
shareMacIndexTTL = 1 * time.Hour shareMacIndexTTL = 1 * time.Hour
shareImportPendingTTL = 12 * time.Hour shareImportPendingTTL = 12 * time.Hour
shareCodeCharset = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" shareCodeCharset = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
@@ -102,18 +101,20 @@ type ShareCodeData struct {
type ShareCodeCache struct { type ShareCodeCache struct {
rdb *redis.Client rdb *redis.Client
codeTTL time.Duration
} }
func NewShareCodeCache(rdb *redis.Client) *ShareCodeCache { func NewShareCodeCache(rdb *redis.Client, codeTTL time.Duration) *ShareCodeCache {
return &ShareCodeCache{rdb: rdb} return &ShareCodeCache{rdb: rdb, codeTTL: codeTTL}
} }
func ShareCodeTTL() time.Duration { // ShareCodeTTL returns the configured share code TTL.
return shareCodeTTL func (c *ShareCodeCache) ShareCodeTTL() time.Duration {
return c.codeTTL
} }
func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqData []byte) (*ShareCodeData, error) { 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) eqJSON := string(eqData)
for i := 0; i < shareCodeMaxRetries; i++ { for i := 0; i < shareCodeMaxRetries; i++ {
@@ -125,7 +126,7 @@ func (c *ShareCodeCache) Create(ctx context.Context, macAddr, ipAddr string, eqD
key := shareCodeKey(code) key := shareCodeKey(code)
macIdxKey := shareMacIndexKey(macAddr) macIdxKey := shareMacIndexKey(macAddr)
ok, err := shareCreateScript.Run(ctx, c.rdb, []string{key, sharePendingSet, macIdxKey}, 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() ).Int()
if err != nil { if err != nil {
return nil, fmt.Errorf("create share code in redis: %w", err) return nil, fmt.Errorf("create share code in redis: %w", err)
+16
View File
@@ -16,6 +16,8 @@ type Config struct {
Equalize EqualizeConfig Equalize EqualizeConfig
S3 S3Config S3 S3Config
EnablePersistTask bool EnablePersistTask bool
ShareCodeMaxPerMac int
ShareCodeTTLMin int
} }
func Load() (*Config, error) { func Load() (*Config, error) {
@@ -57,6 +59,8 @@ func Load() (*Config, error) {
Equalize: eq, Equalize: eq,
S3: s3cfg, S3: s3cfg,
EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true", EnablePersistTask: getEnv("ENABLE_PERSIST_TASK", "false") == "true",
ShareCodeMaxPerMac: getEnvInt("SHARE_CODE_MAX_PER_MAC", 1),
ShareCodeTTLMin: getEnvInt("SHARE_CODE_TTL_MIN", 30),
}, nil }, nil
} }
@@ -70,3 +74,15 @@ func getEnv(key, fallback string) string {
} }
return fallback 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
}
+22 -2
View File
@@ -14,10 +14,11 @@ import (
type ShareCodeHandler struct { type ShareCodeHandler struct {
cache *cache.ShareCodeCache cache *cache.ShareCodeCache
log *zap.Logger log *zap.Logger
maxPerMac int
} }
func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger) *ShareCodeHandler { func NewShareCodeHandler(shareCache *cache.ShareCodeCache, log *zap.Logger, maxPerMac int) *ShareCodeHandler {
return &ShareCodeHandler{cache: shareCache, log: log} return &ShareCodeHandler{cache: shareCache, log: log, maxPerMac: maxPerMac}
} }
// ExportShareCode 导出分享码 // ExportShareCode 导出分享码
@@ -69,6 +70,25 @@ func (h *ShareCodeHandler) ExportShareCode(c *gin.Context) {
} }
ctx := c.Request.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)) data, err := h.cache.Create(ctx, mac, clientIP, []byte(eqDataRaw))
if err != nil { if err != nil {
h.log.Error("create share code failed", zap.Error(err)) h.log.Error("create share code failed", zap.Error(err))
+4 -3
View File
@@ -2,6 +2,7 @@ package router
import ( import (
"database/sql" "database/sql"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/luxsin/app-api/internal/cache" "github.com/luxsin/app-api/internal/cache"
@@ -18,7 +19,7 @@ import (
ginSwagger "github.com/swaggo/gin-swagger" 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 := gin.New()
r.Use(gin.Recovery()) r.Use(gin.Recovery())
r.Use(middleware.RequestID()) 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) otaRepo := repository.NewOTARepository(db)
curveRepo := repository.NewCurveRepository(db) curveRepo := repository.NewCurveRepository(db)
shareCodeCache := cache.NewShareCodeCache(rdb) shareCodeCache := cache.NewShareCodeCache(rdb, shareCodeTTL)
// Handler // Handler
health := handler.NewHealthHandler() 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) ota := handler.NewOTAHandler(otaRepo, log)
curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log) curve := handler.NewCurveHandler(curveRepo, curveCache, eqCfg, s3, log)
modelCSV := handler.NewModelCSVHandler(s3, log) modelCSV := handler.NewModelCSVHandler(s3, log)
shareCode := handler.NewShareCodeHandler(shareCodeCache, log) shareCode := handler.NewShareCodeHandler(shareCodeCache, log, shareCodeMaxPerMac)
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
{ {
+9 -6
View File
@@ -1,16 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
REMOTE_DIR="/data/project/app-api/" REMOTE_DIR="/data/project/app-api"
DEFAULT_PATHS=( DEFAULT_PATHS=(
./cmd/ ./cmd/
./internal/ ./internal/
./pkg ./pkg/
./docs/
./go.mod ./go.mod
./go.sum ./go.sum
./Dockerfile
./docker-compose.yml
) )
show_help() { 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 和 api2
upload.sh api1 同步默认路径到 api1 upload.sh api1 同步默认路径到 api1
upload.sh api2 ./internal 同步 internal/ 到 api2 upload.sh api2 ./internal 同步 internal/ 到 api2
upload.sh api1 Dockerfile 单独同步 Dockerfile 到 api1
upload.sh -n 虚拟执行,预览同步到 api1 和 api2 upload.sh -n 虚拟执行,预览同步到 api1 和 api2
upload.sh -n api1 ./internal 虚拟执行,预览同步到 api1 upload.sh -n api1 ./internal 虚拟执行,预览同步到 api1
EOF EOF
@@ -73,7 +75,8 @@ upload_to() {
local item rel remote_path local item rel remote_path
for item in "${paths[@]}"; do for item in "${paths[@]}"; do
rel="${item#./}" rel="${item#./}"
remote_path="${REMOTE_DIR}${rel}" rel="${rel%/}" # 剥离尾部斜杠,避免拼接时出现双斜杠
remote_path="${REMOTE_DIR}/${rel}"
if [[ -d "$item" ]]; then if [[ -d "$item" ]]; then
echo " 同步目录: $item -> ${remote_host}:${remote_path}/" echo " 同步目录: $item -> ${remote_host}:${remote_path}/"