354 lines
8.9 KiB
Go
354 lines
8.9 KiB
Go
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)
|
|
}
|