切换批次和文件的 ID 类型为 UUID,更新相关逻辑和文档

This commit is contained in:
2026-01-14 13:30:50 +08:00
parent 5160ae78cc
commit 1ffa16cf48
11 changed files with 66 additions and 59 deletions

View File

@@ -291,8 +291,8 @@ const docTemplate = `{
"summary": "获取批次详情", "summary": "获取批次详情",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -344,8 +344,8 @@ const docTemplate = `{
"summary": "修改批次信息", "summary": "修改批次信息",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -403,8 +403,8 @@ const docTemplate = `{
"summary": "删除批次", "summary": "删除批次",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -710,8 +710,8 @@ const docTemplate = `{
"summary": "下载单个文件", "summary": "下载单个文件",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "文件 ID", "description": "文件 ID (UUID)",
"name": "file_id", "name": "file_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -883,7 +883,7 @@ const docTemplate = `{
} }
}, },
"id": { "id": {
"type": "integer" "type": "string"
}, },
"max_downloads": { "max_downloads": {
"type": "integer" "type": "integer"
@@ -911,13 +911,13 @@ const docTemplate = `{
"type": "object", "type": "object",
"properties": { "properties": {
"batch_id": { "batch_id": {
"type": "integer" "type": "string"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "string"
}, },
"mime_type": { "mime_type": {
"type": "string" "type": "string"
@@ -983,7 +983,7 @@ const docTemplate = `{
"type": "object", "type": "object",
"properties": { "properties": {
"batch_id": { "batch_id": {
"type": "integer" "type": "string"
}, },
"expire_at": { "expire_at": {
"type": "string" "type": "string"

View File

@@ -284,8 +284,8 @@
"summary": "获取批次详情", "summary": "获取批次详情",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -337,8 +337,8 @@
"summary": "修改批次信息", "summary": "修改批次信息",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -396,8 +396,8 @@
"summary": "删除批次", "summary": "删除批次",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "批次 ID", "description": "批次 ID (UUID)",
"name": "batch_id", "name": "batch_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -703,8 +703,8 @@
"summary": "下载单个文件", "summary": "下载单个文件",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"description": "文件 ID", "description": "文件 ID (UUID)",
"name": "file_id", "name": "file_id",
"in": "path", "in": "path",
"required": true "required": true
@@ -876,7 +876,7 @@
} }
}, },
"id": { "id": {
"type": "integer" "type": "string"
}, },
"max_downloads": { "max_downloads": {
"type": "integer" "type": "integer"
@@ -904,13 +904,13 @@
"type": "object", "type": "object",
"properties": { "properties": {
"batch_id": { "batch_id": {
"type": "integer" "type": "string"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "string"
}, },
"mime_type": { "mime_type": {
"type": "string" "type": "string"
@@ -976,7 +976,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"batch_id": { "batch_id": {
"type": "integer" "type": "string"
}, },
"expire_at": { "expire_at": {
"type": "string" "type": "string"

View File

@@ -94,7 +94,7 @@ definitions:
$ref: '#/definitions/model.FileItem' $ref: '#/definitions/model.FileItem'
type: array type: array
id: id:
type: integer type: string
max_downloads: max_downloads:
type: integer type: integer
pickup_code: pickup_code:
@@ -113,11 +113,11 @@ definitions:
model.FileItem: model.FileItem:
properties: properties:
batch_id: batch_id:
type: integer type: string
created_at: created_at:
type: string type: string
id: id:
type: integer type: string
mime_type: mime_type:
type: string type: string
original_name: original_name:
@@ -161,7 +161,7 @@ definitions:
public.UploadResponse: public.UploadResponse:
properties: properties:
batch_id: batch_id:
type: integer type: string
expire_at: expire_at:
type: string type: string
pickup_code: pickup_code:
@@ -353,11 +353,11 @@ paths:
delete: delete:
description: 标记批次为已删除,并物理删除关联的存储文件 description: 标记批次为已删除,并物理删除关联的存储文件
parameters: parameters:
- description: 批次 ID - description: 批次 ID (UUID)
in: path in: path
name: batch_id name: batch_id
required: true required: true
type: integer type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -377,11 +377,11 @@ paths:
get: get:
description: 根据批次 ID 获取批次信息及关联的文件列表 description: 根据批次 ID 获取批次信息及关联的文件列表
parameters: parameters:
- description: 批次 ID - description: 批次 ID (UUID)
in: path in: path
name: batch_id name: batch_id
required: true required: true
type: integer type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -408,11 +408,11 @@ paths:
- application/json - application/json
description: 允许修改备注、过期策略、最大下载次数、状态等 description: 允许修改备注、过期策略、最大下载次数、状态等
parameters: parameters:
- description: 批次 ID - description: 批次 ID (UUID)
in: path in: path
name: batch_id name: batch_id
required: true required: true
type: integer type: string
- description: 修改内容 - description: 修改内容
in: body in: body
name: request name: request
@@ -611,11 +611,11 @@ paths:
get: get:
description: 根据文件 ID 下载单个文件 description: 根据文件 ID 下载单个文件
parameters: parameters:
- description: 文件 ID - description: 文件 ID (UUID)
in: path in: path
name: file_id name: file_id
required: true required: true
type: integer type: string
produces: produces:
- application/octet-stream - application/octet-stream
responses: responses:

View File

@@ -93,7 +93,7 @@ func (h *BatchHandler) ListBatches(c *gin.Context) {
// @Description 根据批次 ID 获取批次信息及关联的文件列表 // @Description 根据批次 ID 获取批次信息及关联的文件列表
// @Tags Admin // @Tags Admin
// @Security AdminAuth // @Security AdminAuth
// @Param batch_id path int true "批次 ID" // @Param batch_id path string true "批次 ID (UUID)"
// @Produce json // @Produce json
// @Success 200 {object} model.Response{data=model.FileBatch} // @Success 200 {object} model.Response{data=model.FileBatch}
// @Failure 404 {object} model.Response // @Failure 404 {object} model.Response
@@ -101,7 +101,7 @@ func (h *BatchHandler) ListBatches(c *gin.Context) {
func (h *BatchHandler) GetBatch(c *gin.Context) { func (h *BatchHandler) GetBatch(c *gin.Context) {
id := c.Param("batch_id") id := c.Param("batch_id")
var batch model.FileBatch var batch model.FileBatch
if err := bootstrap.DB.Preload("FileItems").First(&batch, id).Error; err != nil { if err := bootstrap.DB.Preload("FileItems").First(&batch, "id = ?", id).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found")) c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found"))
return return
} }
@@ -115,7 +115,7 @@ func (h *BatchHandler) GetBatch(c *gin.Context) {
// @Security AdminAuth // @Security AdminAuth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param batch_id path int true "批次 ID" // @Param batch_id path string true "批次 ID (UUID)"
// @Param request body UpdateBatchRequest true "修改内容" // @Param request body UpdateBatchRequest true "修改内容"
// @Success 200 {object} model.Response{data=model.FileBatch} // @Success 200 {object} model.Response{data=model.FileBatch}
// @Failure 400 {object} model.Response // @Failure 400 {object} model.Response
@@ -123,7 +123,7 @@ func (h *BatchHandler) GetBatch(c *gin.Context) {
func (h *BatchHandler) UpdateBatch(c *gin.Context) { func (h *BatchHandler) UpdateBatch(c *gin.Context) {
id := c.Param("batch_id") id := c.Param("batch_id")
var batch model.FileBatch var batch model.FileBatch
if err := bootstrap.DB.First(&batch, id).Error; err != nil { if err := bootstrap.DB.First(&batch, "id = ?", id).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found")) c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found"))
return return
} }
@@ -155,16 +155,15 @@ func (h *BatchHandler) UpdateBatch(c *gin.Context) {
// @Description 标记批次为已删除,并物理删除关联的存储文件 // @Description 标记批次为已删除,并物理删除关联的存储文件
// @Tags Admin // @Tags Admin
// @Security AdminAuth // @Security AdminAuth
// @Param batch_id path int true "批次 ID" // @Param batch_id path string true "批次 ID (UUID)"
// @Produce json // @Produce json
// @Success 200 {object} model.Response // @Success 200 {object} model.Response
// @Failure 500 {object} model.Response // @Failure 500 {object} model.Response
// @Router /admin/batches/{batch_id} [delete] // @Router /admin/batches/{batch_id} [delete]
func (h *BatchHandler) DeleteBatch(c *gin.Context) { func (h *BatchHandler) DeleteBatch(c *gin.Context) {
idStr := c.Param("batch_id") id := c.Param("batch_id")
id, _ := strconv.ParseUint(idStr, 10, 32)
if err := h.batchService.DeleteBatch(c.Request.Context(), uint(id)); err != nil { if err := h.batchService.DeleteBatch(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, err.Error())) c.JSON(http.StatusInternalServerError, model.ErrorResponse(model.CodeInternalError, err.Error()))
return return
} }

View File

@@ -103,6 +103,11 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
return return
} }
if batch.Type == "text" {
h.batchService.IncrementDownloadCount(batch.ID)
batch.DownloadCount++
}
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{ c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
Remark: batch.Remark, Remark: batch.Remark,
ExpireAt: batch.ExpireAt, ExpireAt: batch.ExpireAt,
@@ -119,24 +124,23 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
// @Summary 下载单个文件 // @Summary 下载单个文件
// @Description 根据文件 ID 下载单个文件 // @Description 根据文件 ID 下载单个文件
// @Tags Public // @Tags Public
// @Param file_id path int true "文件 ID" // @Param file_id path string true "文件 ID (UUID)"
// @Produce application/octet-stream // @Produce application/octet-stream
// @Success 200 {file} file // @Success 200 {file} file
// @Failure 404 {object} model.Response // @Failure 404 {object} model.Response
// @Failure 410 {object} model.Response // @Failure 410 {object} model.Response
// @Router /api/files/{file_id}/download [get] // @Router /api/files/{file_id}/download [get]
func (h *PickupHandler) DownloadFile(c *gin.Context) { func (h *PickupHandler) DownloadFile(c *gin.Context) {
fileIDStr := c.Param("file_id") fileID := c.Param("file_id")
fileID, _ := strconv.ParseUint(fileIDStr, 10, 32)
var item model.FileItem var item model.FileItem
if err := bootstrap.DB.First(&item, fileID).Error; err != nil { if err := bootstrap.DB.First(&item, "id = ?", fileID).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found")) c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found"))
return return
} }
var batch model.FileBatch var batch model.FileBatch
if err := bootstrap.DB.First(&batch, item.BatchID).Error; err != nil { if err := bootstrap.DB.First(&batch, "id = ?", item.BatchID).Error; err != nil {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found")) c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "batch not found"))
return return
} }

View File

@@ -24,7 +24,7 @@ func NewUploadHandler() *UploadHandler {
type UploadResponse struct { type UploadResponse struct {
PickupCode string `json:"pickup_code"` PickupCode string `json:"pickup_code"`
ExpireAt *time.Time `json:"expire_at"` ExpireAt *time.Time `json:"expire_at"`
BatchID uint `json:"batch_id"` BatchID string `json:"batch_id"`
} }
// Upload 上传文件并生成取件码 // Upload 上传文件并生成取件码

View File

@@ -7,7 +7,7 @@ import (
) )
type FileBatch struct { type FileBatch struct {
ID uint `gorm:"primaryKey" json:"id"` ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
PickupCode string `gorm:"uniqueIndex;not null" json:"pickup_code"` PickupCode string `gorm:"uniqueIndex;not null" json:"pickup_code"`
Remark string `json:"remark"` Remark string `json:"remark"`
ExpireType string `json:"expire_type"` // time / download / permanent ExpireType string `json:"expire_type"` // time / download / permanent

View File

@@ -5,8 +5,8 @@ import (
) )
type FileItem struct { type FileItem struct {
ID uint `gorm:"primaryKey" json:"id"` ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
BatchID uint `gorm:"index;not null" json:"batch_id"` BatchID string `gorm:"index;not null;type:varchar(36)" json:"batch_id"`
OriginalName string `json:"original_name"` OriginalName string `json:"original_name"`
StoragePath string `json:"storage_path"` StoragePath string `json:"storage_path"`
Size int64 `json:"size"` Size int64 `json:"size"`

View File

@@ -57,9 +57,9 @@ func (s *BatchService) MarkAsExpired(batch *model.FileBatch) error {
return s.db.Model(batch).Update("status", "expired").Error return s.db.Model(batch).Update("status", "expired").Error
} }
func (s *BatchService) DeleteBatch(ctx context.Context, batchID uint) error { func (s *BatchService) DeleteBatch(ctx context.Context, batchID string) error {
var batch model.FileBatch var batch model.FileBatch
if err := s.db.Preload("FileItems").First(&batch, batchID).Error; err != nil { if err := s.db.Preload("FileItems").First(&batch, "id = ?", batchID).Error; err != nil {
return err return err
} }
@@ -80,7 +80,7 @@ func (s *BatchService) DeleteBatch(ctx context.Context, batchID uint) error {
}) })
} }
func (s *BatchService) IncrementDownloadCount(batchID uint) error { func (s *BatchService) IncrementDownloadCount(batchID string) error {
return s.db.Model(&model.FileBatch{}).Where("id = ?", batchID). return s.db.Model(&model.FileBatch{}).Where("id = ?", batchID).
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
} }

View File

@@ -34,6 +34,7 @@ func (s *UploadService) CreateBatch(ctx context.Context, files []*multipart.File
// 2. 准备 Batch // 2. 准备 Batch
batch := &model.FileBatch{ batch := &model.FileBatch{
ID: uuid.New().String(),
PickupCode: pickupCode, PickupCode: pickupCode,
Remark: remark, Remark: remark,
ExpireType: expireType, ExpireType: expireType,
@@ -69,6 +70,7 @@ func (s *UploadService) CreateTextBatch(ctx context.Context, content string, rem
// 2. 准备 Batch // 2. 准备 Batch
batch := &model.FileBatch{ batch := &model.FileBatch{
ID: uuid.New().String(),
PickupCode: pickupCode, PickupCode: pickupCode,
Remark: remark, Remark: remark,
ExpireType: expireType, ExpireType: expireType,
@@ -101,7 +103,7 @@ func (s *UploadService) applyExpire(batch *model.FileBatch, expireType string, e
} }
} }
func (s *UploadService) processFile(ctx context.Context, tx *gorm.DB, batchID uint, fileHeader *multipart.FileHeader) (*model.FileItem, error) { func (s *UploadService) processFile(ctx context.Context, tx *gorm.DB, batchID string, fileHeader *multipart.FileHeader) (*model.FileItem, error) {
file, err := fileHeader.Open() file, err := fileHeader.Open()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -110,7 +112,8 @@ func (s *UploadService) processFile(ctx context.Context, tx *gorm.DB, batchID ui
// 生成唯一存储路径 // 生成唯一存储路径
ext := filepath.Ext(fileHeader.Filename) ext := filepath.Ext(fileHeader.Filename)
storagePath := fmt.Sprintf("%d/%s%s", batchID, uuid.New().String(), ext) fileID := uuid.New().String()
storagePath := fmt.Sprintf("%s/%s%s", batchID, fileID, ext)
// 保存到存储层 // 保存到存储层
if err := storage.GlobalStorage.Save(ctx, storagePath, file); err != nil { if err := storage.GlobalStorage.Save(ctx, storagePath, file); err != nil {
@@ -119,6 +122,7 @@ func (s *UploadService) processFile(ctx context.Context, tx *gorm.DB, batchID ui
// 创建数据库记录 // 创建数据库记录
item := &model.FileItem{ item := &model.FileItem{
ID: fileID,
BatchID: batchID, BatchID: batchID,
OriginalName: fileHeader.Filename, OriginalName: fileHeader.Filename,
StoragePath: storagePath, StoragePath: storagePath,

View File

@@ -53,7 +53,7 @@ func (c *Cleaner) Clean() {
bootstrap.DB.Unscoped().Where("status IN ? OR deleted_at IS NOT NULL", []string{"expired", "deleted"}).Find(&toDelete) bootstrap.DB.Unscoped().Where("status IN ? OR deleted_at IS NOT NULL", []string{"expired", "deleted"}).Find(&toDelete)
for _, batch := range toDelete { for _, batch := range toDelete {
fmt.Printf("Deep cleaning batch: %d\n", batch.ID) fmt.Printf("Deep cleaning batch: %s\n", batch.ID)
c.batchService.DeleteBatch(context.Background(), batch.ID) c.batchService.DeleteBatch(context.Background(), batch.ID)
} }
} }