From 673788b4b68bd2dae4eec492a36ae7840484f6fa Mon Sep 17 00:00:00 2001 From: hxuanyu <2252193204@qq.com> Date: Wed, 14 Jan 2026 17:03:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=94=AF=E6=8C=81=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=9B=B4=E6=96=B0=E5=8F=96=E4=BB=B6=E7=A0=81=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=EF=BC=8C=E8=B0=83=E6=95=B4=E5=AD=98=E9=87=8F=E5=8F=96?= =?UTF-8?q?=E4=BB=B6=E7=A0=81=E4=BB=A5=E9=80=82=E9=85=8D=E6=96=B0=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=E5=B9=B6=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=92=8C=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/config_specification.md | 2 +- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- internal/api/admin/config.go | 13 +++++ internal/config/config.go | 2 +- internal/service/batch_service.go | 86 ++++++++++++++++++++++++++++++ internal/service/upload_service.go | 27 ++-------- 8 files changed, 108 insertions(+), 28 deletions(-) diff --git a/docs/config_specification.md b/docs/config_specification.md index 182387d..289a705 100644 --- a/docs/config_specification.md +++ b/docs/config_specification.md @@ -17,7 +17,7 @@ | 字段名 | 类型 | 含义 | 示例 | | :--- | :--- | :--- | :--- | | `admin_password_hash` | string | 管理员密码的 bcrypt 哈希值 | `$2a$10$...` | -| `pickup_code_length` | int | 自动生成的取件码长度(已包含在公共配置接口中) | `6` | +| `pickup_code_length` | int | 自动生成的取件码长度。变更后系统将自动对存量取件码进行右侧补零或截取以适配新长度。 | `6` | | `pickup_fail_limit` | int | 单个 IP 对单个取件码尝试失败的最大次数,超过后将被临时封禁 | `5` | | `jwt_secret` | string | 用于签发管理端 JWT Token 的密钥,建议设置为复杂随机字符串 | `file-relay-secret` | diff --git a/docs/docs.go b/docs/docs.go index 363231b..b7b3f05 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1077,7 +1077,7 @@ const docTemplate = `{ "type": "string" }, "pickup_code_length": { - "description": "取件码长度", + "description": "取件码长度 (变更后将自动通过右侧补零或截取调整存量数据)", "type": "integer" }, "pickup_fail_limit": { diff --git a/docs/swagger.json b/docs/swagger.json index fd2ebb5..e18dcd5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1070,7 +1070,7 @@ "type": "string" }, "pickup_code_length": { - "description": "取件码长度", + "description": "取件码长度 (变更后将自动通过右侧补零或截取调整存量数据)", "type": "integer" }, "pickup_fail_limit": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e2739ac..6b0f353 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -113,7 +113,7 @@ definitions: description: JWT 签名密钥 type: string pickup_code_length: - description: 取件码长度 + description: 取件码长度 (变更后将自动通过右侧补零或截取调整存量数据) type: integer pickup_fail_limit: description: 取件失败尝试限制 diff --git a/internal/api/admin/config.go b/internal/api/admin/config.go index ffa521b..088327a 100644 --- a/internal/api/admin/config.go +++ b/internal/api/admin/config.go @@ -4,6 +4,7 @@ import ( "FileRelay/internal/bootstrap" "FileRelay/internal/config" "FileRelay/internal/model" + "FileRelay/internal/service" "net/http" "github.com/gin-gonic/gin" @@ -51,9 +52,21 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { newConfig.Database.Path = config.GlobalConfig.Database.Path } + // 检查取件码长度是否变化 + pickupCodeLengthChanged := newConfig.Security.PickupCodeLength != config.GlobalConfig.Security.PickupCodeLength && newConfig.Security.PickupCodeLength > 0 + // 更新内存配置 config.UpdateGlobalConfig(&newConfig) + // 如果长度变化,同步更新现有取件码 + if pickupCodeLengthChanged { + batchService := service.NewBatchService() + if err := batchService.UpdateAllPickupCodes(newConfig.Security.PickupCodeLength); err != nil { + c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "Failed to update existing pickup codes: "+err.Error())) + return + } + } + // 重新初始化存储(热更新业务逻辑) if err := bootstrap.ReloadStorage(); err != nil { c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "Failed to reload storage: "+err.Error())) diff --git a/internal/config/config.go b/internal/config/config.go index ca7a00a..636946a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,7 +24,7 @@ type SiteConfig struct { type SecurityConfig struct { AdminPasswordHash string `yaml:"admin_password_hash" json:"admin_password_hash"` // 管理员密码哈希 (bcrypt) - PickupCodeLength int `yaml:"pickup_code_length" json:"pickup_code_length"` // 取件码长度 + PickupCodeLength int `yaml:"pickup_code_length" json:"pickup_code_length"` // 取件码长度 (变更后将自动通过右侧补零或截取调整存量数据) PickupFailLimit int `yaml:"pickup_fail_limit" json:"pickup_fail_limit"` // 取件失败尝试限制 JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` // JWT 签名密钥 } diff --git a/internal/service/batch_service.go b/internal/service/batch_service.go index d7e2df2..256e360 100644 --- a/internal/service/batch_service.go +++ b/internal/service/batch_service.go @@ -6,8 +6,12 @@ import ( "FileRelay/internal/storage" "context" "errors" + "math/big" + "strings" "time" + "crypto/rand" + "gorm.io/gorm" ) @@ -84,3 +88,85 @@ func (s *BatchService) IncrementDownloadCount(batchID string) error { return s.db.Model(&model.FileBatch{}).Where("id = ?", batchID). UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error } + +func (s *BatchService) GeneratePickupCode(length int) (string, error) { + const charset = "0123456789" + b := make([]byte, length) + for i := range b { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + b[i] = charset[num.Int64()] + } + // 检查是否冲突 (排除已删除的,但包括活跃的和已过期的) + var count int64 + s.db.Model(&model.FileBatch{}).Where("pickup_code = ?", string(b)).Count(&count) + if count > 0 { + return s.GeneratePickupCode(length) // 递归生成 + } + return string(b), nil +} + +func (s *BatchService) UpdateAllPickupCodes(newLength int) error { + var batches []model.FileBatch + // 只更新未删除的记录,包括 active 和 expired + if err := s.db.Find(&batches).Error; err != nil { + return err + } + + return s.db.Transaction(func(tx *gorm.DB) error { + for _, batch := range batches { + oldCode := batch.PickupCode + if len(oldCode) == newLength { + continue + } + + var newCode string + if len(oldCode) < newLength { + // 右侧补零,方便用户输入原码后通过补 0 完成输入 + newCode = oldCode + strings.Repeat("0", newLength-len(oldCode)) + } else { + // 截取前 newLength 位,保留原码头部 + newCode = oldCode[:newLength] + } + + // 检查冲突 (在事务中检查) + var count int64 + tx.Model(&model.FileBatch{}).Where("pickup_code = ? AND id != ?", newCode, batch.ID).Count(&count) + if count > 0 { + // 如果冲突,生成一个新的随机码 + var err error + newCode, err = s.generateUniquePickupCodeInTx(tx, newLength) + if err != nil { + return err + } + } + + if err := tx.Model(&batch).Update("pickup_code", newCode).Error; err != nil { + return err + } + } + return nil + }) +} + +func (s *BatchService) generateUniquePickupCodeInTx(tx *gorm.DB, length int) (string, error) { + const charset = "0123456789" + for { + b := make([]byte, length) + for i := range b { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + b[i] = charset[num.Int64()] + } + + var count int64 + tx.Model(&model.FileBatch{}).Where("pickup_code = ?", string(b)).Count(&count) + if count == 0 { + return string(b), nil + } + } +} diff --git a/internal/service/upload_service.go b/internal/service/upload_service.go index 819726a..a0dd96c 100644 --- a/internal/service/upload_service.go +++ b/internal/service/upload_service.go @@ -6,9 +6,7 @@ import ( "FileRelay/internal/model" "FileRelay/internal/storage" "context" - "crypto/rand" "fmt" - "math/big" "mime/multipart" "path/filepath" "time" @@ -27,7 +25,8 @@ func NewUploadService() *UploadService { func (s *UploadService) CreateBatch(ctx context.Context, files []*multipart.FileHeader, remark string, expireType string, expireValue interface{}) (*model.FileBatch, error) { // 1. 生成取件码 - pickupCode, err := s.generatePickupCode(config.GlobalConfig.Security.PickupCodeLength) + batchService := NewBatchService() + pickupCode, err := batchService.GeneratePickupCode(config.GlobalConfig.Security.PickupCodeLength) if err != nil { return nil, err } @@ -63,7 +62,8 @@ func (s *UploadService) CreateBatch(ctx context.Context, files []*multipart.File func (s *UploadService) CreateTextBatch(ctx context.Context, content string, remark string, expireType string, expireValue interface{}) (*model.FileBatch, error) { // 1. 生成取件码 - pickupCode, err := s.generatePickupCode(config.GlobalConfig.Security.PickupCodeLength) + batchService := NewBatchService() + pickupCode, err := batchService.GeneratePickupCode(config.GlobalConfig.Security.PickupCodeLength) if err != nil { return nil, err } @@ -136,22 +136,3 @@ func (s *UploadService) processFile(ctx context.Context, tx *gorm.DB, batchID st return item, nil } - -func (s *UploadService) generatePickupCode(length int) (string, error) { - const charset = "0123456789" - b := make([]byte, length) - for i := range b { - num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - b[i] = charset[num.Int64()] - } - // 检查是否冲突 - var count int64 - s.db.Model(&model.FileBatch{}).Where("pickup_code = ? AND status = ?", string(b), "active").Count(&count) - if count > 0 { - return s.generatePickupCode(length) // 递归生成 - } - return string(b), nil -}