package service import ( "context" "cms-api/internal/dao" "cms-api/internal/model" "fmt" "os" "path/filepath" "strings" "time" "github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/grand" ) type sAttachment struct{} func Attachment() *sAttachment { return &sAttachment{} } // Upload 上传文件 func (s *sAttachment) Upload(ctx context.Context, file *ghttp.UploadFile, uploadType string) (*model.Attachment, error) { // 检查文件大小 maxSize := int64(10 * 1024 * 1024) // 10MB if file.Size > maxSize { return nil, gerror.New("文件大小不能超过10MB") } // 检查文件类型 if !s.isAllowedFileType(file.Filename) { return nil, gerror.New("不支持的文件类型") } // 生成文件名和路径 fileName, filePath, fileUrl := s.generateFilePath(file.Filename, uploadType) // 确保目录存在 dir := filepath.Dir(filePath) if !gfile.Exists(dir) { if err := gfile.Mkdir(dir); err != nil { return nil, gerror.New("创建目录失败") } } // 保存文件 if _, err := file.Save(filePath, true); err != nil { return nil, gerror.New("保存文件失败: " + err.Error()) } // 获取上传者ID uploadedBy := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id")) if uploadedBy == 0 { uploadedBy = gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id")) } // 获取客户端IP uploadIp := g.RequestFromCtx(ctx).GetClientIp() // 创建附件记录 attachment := &model.Attachment{ OriginalName: file.Filename, FileName: fileName, FilePath: filePath, FileUrl: fileUrl, FileSize: file.Size, FileType: s.getFileType(file.Filename), MimeType: s.getMimeType(file.Filename), FileExt: s.getFileExt(file.Filename), StorageType: "local", UploadIp: uploadIp, UploadedBy: uploadedBy, UsageCount: 0, CreatedAt: gtime.Now(), UpdatedAt: gtime.Now(), } id, err := dao.Attachment.Create(ctx, attachment) if err != nil { // 如果数据库保存失败,删除已上传的文件 os.Remove(filePath) return nil, err } attachment.Id = int(id) return attachment, nil } // GetById 根据ID获取附件信息 func (s *sAttachment) GetById(ctx context.Context, id int) (*model.Attachment, error) { return dao.Attachment.GetById(ctx, id) } // List 获取附件列表 func (s *sAttachment) List(ctx context.Context, req *model.AttachmentListRequest) (*model.PageResponse, error) { var ( page = req.Page pageSize = req.PageSize ) if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } // 设置分页参数 req.Page = page req.PageSize = pageSize // 获取列表 attachments, total, err := dao.Attachment.List(ctx, req) if err != nil { return nil, err } return &model.PageResponse{ List: attachments, Total: total, Page: page, PageSize: pageSize, TotalPages: (total + pageSize - 1) / pageSize, }, nil } // Delete 删除附件 func (s *sAttachment) Delete(ctx context.Context, id int) error { // 检查附件是否存在 attachment, err := s.GetById(ctx, id) if err != nil { return err } if attachment == nil { return gerror.New("附件不存在") } // 删除物理文件 if gfile.Exists(attachment.FilePath) { if err := os.Remove(attachment.FilePath); err != nil { g.Log().Error(ctx, "删除物理文件失败:", err) } } // 删除数据库记录 return dao.Attachment.Delete(ctx, id) } // UpdateUsageCount 更新使用次数 func (s *sAttachment) UpdateUsageCount(ctx context.Context, id int) error { return dao.Attachment.IncrementUsageCount(ctx, id) } // GetStats 获取附件统计信息 func (s *sAttachment) GetStats(ctx context.Context) (map[string]interface{}, error) { // 获取总附件数 totalCount, err := dao.Attachment.GetCount(ctx) if err != nil { return nil, err } // 获取总文件大小 totalSize, err := dao.Attachment.GetTotalSize(ctx) if err != nil { return nil, err } // 获取各类型文件数量 imageCount, err := dao.Attachment.GetCountByType(ctx, "image") if err != nil { return nil, err } documentCount, err := dao.Attachment.GetCountByType(ctx, "document") if err != nil { return nil, err } videoCount, err := dao.Attachment.GetCountByType(ctx, "video") if err != nil { return nil, err } return map[string]interface{}{ "total_count": totalCount, "total_size": totalSize, "image_count": imageCount, "document_count": documentCount, "video_count": videoCount, }, nil } // isAllowedFileType 检查文件类型是否允许 func (s *sAttachment) isAllowedFileType(filename string) bool { allowedExts := []string{ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", // 图片 ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", // 文档 ".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv", // 视频 ".mp3", ".wav", ".flac", ".aac", // 音频 ".zip", ".rar", ".7z", ".tar", ".gz", // 压缩包 } ext := strings.ToLower(filepath.Ext(filename)) for _, allowedExt := range allowedExts { if ext == allowedExt { return true } } return false } // generateFilePath 生成文件路径 func (s *sAttachment) generateFilePath(originalName, uploadType string) (string, string, string) { // 获取文件扩展名 ext := filepath.Ext(originalName) // 生成唯一文件名 hash := gmd5.MustEncrypt(fmt.Sprintf("%s_%d_%s", originalName, time.Now().UnixNano(), grand.S(8))) fileName := hash + ext // 根据日期创建目录结构 now := time.Now() dateDir := fmt.Sprintf("%d/%02d/%02d", now.Year(), now.Month(), now.Day()) // 根据上传类型创建子目录 if uploadType == "" { uploadType = "general" } // 构建完整路径 relativePath := fmt.Sprintf("uploads/%s/%s/%s", uploadType, dateDir, fileName) fullPath := filepath.Join("storage", relativePath) fileUrl := "/" + strings.ReplaceAll(relativePath, "\\", "/") return fileName, fullPath, fileUrl } // getFileType 获取文件类型 func (s *sAttachment) getFileType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"} documentExts := []string{".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt"} videoExts := []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv"} audioExts := []string{".mp3", ".wav", ".flac", ".aac"} archiveExts := []string{".zip", ".rar", ".7z", ".tar", ".gz"} for _, imageExt := range imageExts { if ext == imageExt { return "image" } } for _, docExt := range documentExts { if ext == docExt { return "document" } } for _, videoExt := range videoExts { if ext == videoExt { return "video" } } for _, audioExt := range audioExts { if ext == audioExt { return "audio" } } for _, archiveExt := range archiveExts { if ext == archiveExt { return "archive" } } return "other" } // getMimeType 获取MIME类型 func (s *sAttachment) getMimeType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) mimeTypes := map[string]string{ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".bmp": "image/bmp", ".webp": "image/webp", ".pdf": "application/pdf", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".txt": "text/plain", ".mp4": "video/mp4", ".avi": "video/x-msvideo", ".mov": "video/quicktime", ".wmv": "video/x-ms-wmv", ".flv": "video/x-flv", ".mkv": "video/x-matroska", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".flac": "audio/flac", ".aac": "audio/aac", ".zip": "application/zip", ".rar": "application/x-rar-compressed", ".7z": "application/x-7z-compressed", ".tar": "application/x-tar", ".gz": "application/gzip", } if mimeType, exists := mimeTypes[ext]; exists { return mimeType } return "application/octet-stream" } // getFileExt 获取文件扩展名 func (s *sAttachment) getFileExt(filename string) string { ext := filepath.Ext(filename) if len(ext) > 0 { return ext[1:] // 去掉点号 } return "" } // BatchDelete 批量删除附件 func (s *sAttachment) BatchDelete(ctx context.Context, ids []int) error { for _, id := range ids { if err := s.Delete(ctx, id); err != nil { g.Log().Error(ctx, "批量删除附件失败, ID:", id, "错误:", err) // 继续删除其他文件,不中断整个过程 } } return nil } // GetByIds 根据ID列表获取附件 func (s *sAttachment) GetByIds(ctx context.Context, ids []int) ([]*model.Attachment, error) { return dao.Attachment.GetByIds(ctx, ids) }