支持动态更新管理员密码和下载次数逻辑完善,优化相关错误处理和配置更新流程

This commit is contained in:
2026-01-14 19:49:07 +08:00
parent 903d5b865e
commit e456d3a823
6 changed files with 75 additions and 19 deletions

3
go.mod
View File

@@ -14,6 +14,7 @@ require (
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
github.com/studio-b12/gowebdav v0.11.0 github.com/studio-b12/gowebdav v0.11.0
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/gin-swagger v1.6.1
@@ -45,6 +46,7 @@ require (
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
@@ -69,6 +71,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect

View File

@@ -29,11 +29,12 @@ type ListBatchesResponse struct {
} }
type UpdateBatchRequest struct { type UpdateBatchRequest struct {
Remark string `json:"remark"` Remark *string `json:"remark"`
ExpireType string `json:"expire_type"` ExpireType *string `json:"expire_type"`
ExpireAt *time.Time `json:"expire_at"` ExpireAt *time.Time `json:"expire_at"`
MaxDownloads int `json:"max_downloads"` MaxDownloads *int `json:"max_downloads"`
Status string `json:"status"` DownloadCount *int `json:"download_count"`
Status *string `json:"status"`
} }
// ListBatches 获取批次列表 // ListBatches 获取批次列表
@@ -136,16 +137,33 @@ func (h *BatchHandler) UpdateBatch(c *gin.Context) {
} }
updates := make(map[string]interface{}) updates := make(map[string]interface{})
updates["remark"] = input.Remark if input.Remark != nil {
updates["expire_type"] = input.ExpireType updates["remark"] = *input.Remark
}
if input.ExpireType != nil {
updates["expire_type"] = *input.ExpireType
}
if input.ExpireAt != nil {
updates["expire_at"] = input.ExpireAt updates["expire_at"] = input.ExpireAt
updates["max_downloads"] = input.MaxDownloads }
updates["status"] = input.Status if input.MaxDownloads != nil {
updates["max_downloads"] = *input.MaxDownloads
}
if input.DownloadCount != nil {
updates["download_count"] = *input.DownloadCount
}
if input.Status != nil {
updates["status"] = *input.Status
}
if len(updates) > 0 {
if err := bootstrap.DB.Model(&batch).Updates(updates).Error; err != nil { if err := bootstrap.DB.Model(&batch).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, err.Error())) c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, err.Error()))
return return
} }
// 重新从数据库读取,确保返回的是完整且最新的数据
bootstrap.DB.First(&batch, "id = ?", id)
}
c.JSON(http.StatusOK, model.SuccessResponse(batch)) c.JSON(http.StatusOK, model.SuccessResponse(batch))
} }

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
) )
type ConfigHandler struct{} type ConfigHandler struct{}
@@ -52,6 +53,16 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
newConfig.Database.Path = config.GlobalConfig.Database.Path newConfig.Database.Path = config.GlobalConfig.Database.Path
} }
// 如果传入了明文密码,则重新生成 hash
if newConfig.Security.AdminPassword != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(newConfig.Security.AdminPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, "Failed to hash password: "+err.Error()))
return
}
newConfig.Security.AdminPasswordHash = string(hash)
}
// 检查取件码长度是否变化 // 检查取件码长度是否变化
pickupCodeLengthChanged := newConfig.Security.PickupCodeLength != config.GlobalConfig.Security.PickupCodeLength && newConfig.Security.PickupCodeLength > 0 pickupCodeLengthChanged := newConfig.Security.PickupCodeLength != config.GlobalConfig.Security.PickupCodeLength && newConfig.Security.PickupCodeLength > 0

View File

@@ -68,7 +68,9 @@ func (h *PickupHandler) DownloadBatch(c *gin.Context) {
} }
// 增加下载次数 // 增加下载次数
h.batchService.IncrementDownloadCount(batch.ID) if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
fmt.Printf("[DownloadBatch] Failed to increment download count for batch %s: %v\n", batch.ID, err)
}
} }
type PickupHandler struct { type PickupHandler struct {
@@ -106,9 +108,12 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
} }
if batch.Type == "text" { if batch.Type == "text" {
h.batchService.IncrementDownloadCount(batch.ID) if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
fmt.Printf("[Pickup] Failed to increment download count for batch %s: %v\n", batch.ID, err)
} else {
batch.DownloadCount++ batch.DownloadCount++
} }
}
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{ c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
Remark: batch.Remark, Remark: batch.Remark,
@@ -163,11 +168,19 @@ func (h *PickupHandler) DownloadFile(c *gin.Context) {
defer reader.Close() defer reader.Close()
// 增加下载次数 // 增加下载次数
h.batchService.IncrementDownloadCount(batch.ID) if err := h.batchService.IncrementDownloadCount(batch.ID); err != nil {
// 记录错误但不中断下载过程
fmt.Printf("[Download] Failed to increment download count for batch %s: %v\n", batch.ID, err)
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", item.OriginalName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", item.OriginalName))
c.Header("Content-Type", item.MimeType) c.Header("Content-Type", item.MimeType)
c.Header("Content-Length", strconv.FormatInt(item.Size, 10)) c.Header("Content-Length", strconv.FormatInt(item.Size, 10))
// 如果是 HEAD 请求,只返回 Header
if c.Request.Method == http.MethodHead {
return
}
io.Copy(c.Writer, reader) io.Copy(c.Writer, reader)
} }

View File

@@ -24,6 +24,7 @@ type SiteConfig struct {
type SecurityConfig struct { type SecurityConfig struct {
AdminPasswordHash string `yaml:"admin_password_hash" json:"admin_password_hash"` // 管理员密码哈希 (bcrypt) AdminPasswordHash string `yaml:"admin_password_hash" json:"admin_password_hash"` // 管理员密码哈希 (bcrypt)
AdminPassword string `yaml:"-" json:"admin_password,omitempty"` // 管理员密码明文 (仅用于更新请求,不保存到文件)
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"` // 取件失败尝试限制 PickupFailLimit int `yaml:"pickup_fail_limit" json:"pickup_fail_limit"` // 取件失败尝试限制
JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` // JWT 签名密钥 JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` // JWT 签名密钥

View File

@@ -85,8 +85,18 @@ func (s *BatchService) DeleteBatch(ctx context.Context, batchID string) error {
} }
func (s *BatchService) IncrementDownloadCount(batchID string) error { func (s *BatchService) IncrementDownloadCount(batchID string) error {
return s.db.Model(&model.FileBatch{}).Where("id = ?", batchID). if batchID == "" {
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error return errors.New("batch id is empty")
}
result := s.db.Model(&model.FileBatch{}).Where("id = ?", batchID).
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("batch not found or already deleted")
}
return nil
} }
func (s *BatchService) GeneratePickupCode(length int) (string, error) { func (s *BatchService) GeneratePickupCode(length int) (string, error) {