Files
nl_cms-api/internal/service/attachment.go

354 lines
8.9 KiB
Go
Raw Normal View History

2025-07-29 12:45:07 +08:00
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)
}