init
This commit is contained in:
@@ -0,0 +1,645 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
apperrors "goravel/app/errors"
|
||||
"goravel/app/http/helpers"
|
||||
"goravel/app/models"
|
||||
"goravel/app/utils"
|
||||
"goravel/app/utils/errorlog"
|
||||
)
|
||||
|
||||
type AttachmentService interface {
|
||||
// GetByID 根据ID获取附件
|
||||
GetByID(id uint) (*models.Attachment, error)
|
||||
// GetByIDs 根据ID列表获取附件
|
||||
GetByIDs(ids []uint) ([]models.Attachment, error)
|
||||
// GetList 获取附件列表
|
||||
GetList(filters AttachmentFilters, page, pageSize int) ([]models.Attachment, int64, error)
|
||||
// InitChunkUpload 初始化分片上传(不再使用服务端缓存)
|
||||
InitChunkUpload(filename string, totalSize int64, chunkSize int64, totalChunks int) (string, error)
|
||||
|
||||
// UploadChunk 上传分片(不再使用服务端缓存)
|
||||
UploadChunk(chunkID string, chunkIndex int, chunkData []byte) error
|
||||
|
||||
// MergeChunks 合并分片(不再使用服务端缓存,需要传入 totalChunks)
|
||||
MergeChunks(chunkID string, filename string, mimeType string, totalChunks int) (*models.Attachment, error)
|
||||
|
||||
// GetChunkProgress 获取分片上传进度(不再使用服务端缓存,需要传入 totalChunks)
|
||||
GetChunkProgress(chunkID string, totalChunks int) (map[string]any, error)
|
||||
|
||||
// UploadFile 普通文件上传(小文件)
|
||||
UploadFile(fileData []byte, filename string, mimeType string) (*models.Attachment, error)
|
||||
|
||||
// GetFileURL 获取文件访问URL
|
||||
GetFileURL(attachment *models.Attachment) string
|
||||
|
||||
// GetFileType 根据MIME类型判断文件类型
|
||||
GetFileType(mimeType string) string
|
||||
|
||||
// DeleteFile 删除文件
|
||||
DeleteFile(attachment *models.Attachment) error
|
||||
// UpdateDisplayName 更新显示名称
|
||||
UpdateDisplayName(id uint, displayName string) error
|
||||
}
|
||||
|
||||
// AttachmentFilters 附件查询过滤器
|
||||
type AttachmentFilters struct {
|
||||
AdminID string
|
||||
Filename string
|
||||
DisplayName string
|
||||
FileType string
|
||||
Extension string
|
||||
StartTime string
|
||||
EndTime string
|
||||
OrderBy string
|
||||
}
|
||||
|
||||
type AttachmentServiceImpl struct {
|
||||
ctx http.Context
|
||||
disk string
|
||||
systemLogService SystemLogService
|
||||
}
|
||||
|
||||
func NewAttachmentService(ctx http.Context) AttachmentService {
|
||||
// 从数据库读取文件存储配置
|
||||
// 优先使用 file_disk,如果没有则使用 storage_disk(向后兼容),最后使用默认值 local
|
||||
disk := utils.GetConfigValue("storage", "file_disk", "")
|
||||
if disk == "" {
|
||||
disk = utils.GetConfigValue("storage", "storage_disk", "")
|
||||
}
|
||||
if disk == "" {
|
||||
disk = "local"
|
||||
}
|
||||
|
||||
return &AttachmentServiceImpl{
|
||||
ctx: ctx,
|
||||
disk: disk,
|
||||
systemLogService: NewSystemLogService(),
|
||||
}
|
||||
}
|
||||
|
||||
// InitChunkUpload 初始化分片上传
|
||||
// 注意:分片信息由客户端缓存,服务端只生成 chunkID
|
||||
func (s *AttachmentServiceImpl) InitChunkUpload(filename string, totalSize int64, chunkSize int64, totalChunks int) (string, error) {
|
||||
// 检查存储驱动:大文件分片上传仅支持本地存储
|
||||
cloudStorageDrivers := []string{"s3", "oss", "cos", "minio", "qiniu"}
|
||||
for _, driver := range cloudStorageDrivers {
|
||||
if s.disk == driver {
|
||||
return "", apperrors.ErrChunkUploadOnlyLocalStorage.WithMessage(fmt.Sprintf("大文件分片上传仅支持本地存储,当前存储驱动为: %s", driver))
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一的分片ID
|
||||
hash := md5.Sum(fmt.Appendf(nil, "%s_%d_%d", filename, totalSize, time.Now().UnixNano()))
|
||||
chunkID := hex.EncodeToString(hash[:])
|
||||
|
||||
// 不再使用服务端缓存,分片信息由客户端管理
|
||||
return chunkID, nil
|
||||
}
|
||||
|
||||
// UploadChunk 上传分片
|
||||
// 注意:不再使用服务端缓存,直接保存分片文件
|
||||
func (s *AttachmentServiceImpl) UploadChunk(chunkID string, chunkIndex int, chunkData []byte) error {
|
||||
if chunkIndex < 0 {
|
||||
return apperrors.ErrInvalidChunkIndex
|
||||
}
|
||||
|
||||
// 保存分片到临时目录
|
||||
storage := facades.Storage().Disk(s.disk)
|
||||
chunkPath := fmt.Sprintf("chunks/%s/%d", chunkID, chunkIndex)
|
||||
|
||||
if err := storage.Put(chunkPath, string(chunkData)); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "保存分片失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"chunk_index": chunkIndex,
|
||||
"error": err.Error(),
|
||||
}, "保存分片失败: %w", err)
|
||||
}
|
||||
return apperrors.ErrSaveChunkFailed.WithError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergeChunks 合并分片
|
||||
// 注意:不再使用服务端缓存,通过检查实际文件系统来验证分片
|
||||
func (s *AttachmentServiceImpl) MergeChunks(chunkID string, filename string, mimeType string, totalChunks int) (*models.Attachment, error) {
|
||||
storage := facades.Storage().Disk(s.disk)
|
||||
|
||||
// 检查所有分片文件是否存在
|
||||
indices := make([]int, totalChunks)
|
||||
for i := range indices {
|
||||
chunkPath := fmt.Sprintf("chunks/%s/%d", chunkID, i)
|
||||
if !storage.Exists(chunkPath) {
|
||||
return nil, apperrors.ErrChunkNotFound.WithParams(map[string]any{
|
||||
"chunk_index": i,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 生成最终文件路径
|
||||
ext := filepath.Ext(filename)
|
||||
if ext == "" {
|
||||
exts, _ := mime.ExtensionsByType(mimeType)
|
||||
if len(exts) > 0 {
|
||||
ext = exts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%s_%d", filename, time.Now().UnixNano())))
|
||||
uniqueName := hex.EncodeToString(hash[:]) + ext
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
finalPath := fmt.Sprintf("attachments/%s/%s", datePath, uniqueName)
|
||||
|
||||
// 合并分片(流式写入,避免大文件内存占用过高)
|
||||
// 对于本地存储,直接使用文件系统操作以提高性能
|
||||
var fileSize int64
|
||||
var missingChunks []int // 记录缺失的分片索引
|
||||
|
||||
// 获取存储根目录
|
||||
storageRoot := facades.Config().GetString("filesystems.disks." + s.disk + ".root")
|
||||
if storageRoot == "" {
|
||||
storageRoot = "storage/app"
|
||||
}
|
||||
|
||||
// 构建目标文件的完整路径
|
||||
finalFullPath := filepath.Join(storageRoot, finalPath)
|
||||
|
||||
// 确保目标目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(finalFullPath), 0755); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "创建目标目录失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"directory": filepath.Dir(finalFullPath),
|
||||
"error": err.Error(),
|
||||
}, "创建目标目录失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrCreateDirectoryFailed.WithError(err)
|
||||
}
|
||||
|
||||
// 创建目标文件(流式写入)
|
||||
outFile, err := os.Create(finalFullPath)
|
||||
if err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "创建目标文件失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"file_path": finalFullPath,
|
||||
"error": err.Error(),
|
||||
}, "创建目标文件失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrCreateFileFailed.WithError(err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// 按顺序读取并写入每个分片
|
||||
for i := range make([]int, totalChunks) {
|
||||
chunkPath := fmt.Sprintf("chunks/%s/%d", chunkID, i)
|
||||
chunkFullPath := filepath.Join(storageRoot, chunkPath)
|
||||
|
||||
// 检查分片文件是否存在
|
||||
if _, err := os.Stat(chunkFullPath); os.IsNotExist(err) {
|
||||
missingChunks = append(missingChunks, i)
|
||||
continue
|
||||
}
|
||||
|
||||
// 打开分片文件
|
||||
chunkFile, err := os.Open(chunkFullPath)
|
||||
if err != nil {
|
||||
// 读取失败,记录到系统日志
|
||||
if s.ctx != nil {
|
||||
_ = s.systemLogService.RecordHTTP(s.ctx, "warning", "attachment", fmt.Sprintf("Failed to read chunk %d for chunkID %s", i, chunkID), map[string]any{
|
||||
"chunk_index": i,
|
||||
"chunk_id": chunkID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
missingChunks = append(missingChunks, i)
|
||||
continue
|
||||
}
|
||||
|
||||
// 流式复制分片内容到目标文件
|
||||
written, err := io.Copy(outFile, chunkFile)
|
||||
if err != nil {
|
||||
chunkFile.Close()
|
||||
// 如果写入失败,删除已创建的目标文件
|
||||
_ = os.Remove(finalFullPath)
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "写入分片失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"chunk_index": i,
|
||||
"file_path": finalFullPath,
|
||||
"error": err.Error(),
|
||||
}, "写入分片 %d 失败: %w", i, err)
|
||||
}
|
||||
return nil, apperrors.ErrWriteChunkFailed.WithError(err).WithParams(map[string]any{
|
||||
"chunk_index": i,
|
||||
})
|
||||
}
|
||||
fileSize += written
|
||||
chunkFile.Close()
|
||||
}
|
||||
|
||||
// 关闭目标文件
|
||||
if err := outFile.Close(); err != nil {
|
||||
_ = os.Remove(finalFullPath)
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "关闭目标文件失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"file_path": finalFullPath,
|
||||
"error": err.Error(),
|
||||
}, "关闭目标文件失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrCloseFileFailed.WithError(err)
|
||||
}
|
||||
|
||||
// 检查是否有缺失的分片
|
||||
if len(missingChunks) > 0 {
|
||||
_ = os.Remove(finalFullPath)
|
||||
return nil, apperrors.ErrChunkMissing.WithParams(map[string]any{
|
||||
"missing_chunks": missingChunks,
|
||||
"count": len(missingChunks),
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否有数据被合并
|
||||
if fileSize == 0 {
|
||||
_ = os.Remove(finalFullPath)
|
||||
return nil, apperrors.ErrNoChunkDataToMerge
|
||||
}
|
||||
|
||||
// 验证文件大小(从文件系统获取实际大小)
|
||||
if fileInfo, err := os.Stat(finalFullPath); err == nil {
|
||||
fileSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
// 创建附件记录
|
||||
adminID := uint(0)
|
||||
if s.ctx != nil {
|
||||
if id, err := helpers.GetAdminIDFromContext(s.ctx); err == nil {
|
||||
adminID = id
|
||||
}
|
||||
}
|
||||
|
||||
fileType := s.GetFileType(mimeType)
|
||||
attachment := &models.Attachment{
|
||||
AdminID: adminID,
|
||||
Disk: s.disk,
|
||||
Path: finalPath,
|
||||
Filename: filename,
|
||||
Extension: strings.TrimPrefix(ext, "."),
|
||||
MimeType: mimeType,
|
||||
Size: fileSize,
|
||||
Status: 1,
|
||||
FileType: fileType,
|
||||
ChunkID: chunkID,
|
||||
}
|
||||
|
||||
if err := facades.Orm().Query().Create(attachment); err != nil {
|
||||
// 如果创建记录失败,删除已上传的文件
|
||||
_ = storage.Delete(finalPath)
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "创建附件记录失败", map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"file_path": finalPath,
|
||||
"filename": filename,
|
||||
"error": err.Error(),
|
||||
}, "创建附件记录失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrCreateFailed.WithError(err)
|
||||
}
|
||||
|
||||
// 合并成功后才清理分片文件(确保数据已保存到数据库)
|
||||
// 清理所有分片文件(包括可能缺失的)
|
||||
cleanupSuccess := true
|
||||
cleanupCount := 0
|
||||
for i := range make([]int, totalChunks) {
|
||||
chunkPath := fmt.Sprintf("chunks/%s/%d", chunkID, i)
|
||||
if storage.Exists(chunkPath) {
|
||||
if err := storage.Delete(chunkPath); err != nil {
|
||||
// 记录删除失败到系统日志,但不影响整体流程
|
||||
if s.ctx != nil {
|
||||
_ = s.systemLogService.RecordHTTP(s.ctx, "warning", "attachment", fmt.Sprintf("Failed to delete chunk file %s", chunkPath), map[string]any{
|
||||
"chunk_path": chunkPath,
|
||||
"chunk_id": chunkID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
cleanupSuccess = false
|
||||
} else {
|
||||
cleanupCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cleanupSuccess && s.ctx != nil {
|
||||
// 记录部分分片删除失败的警告到系统日志
|
||||
_ = s.systemLogService.RecordHTTP(s.ctx, "warning", "attachment", fmt.Sprintf("Some chunk files failed to delete for chunkID %s", chunkID), map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"cleaned_count": cleanupCount,
|
||||
"total_chunks": totalChunks,
|
||||
})
|
||||
}
|
||||
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
// GetChunkProgress 获取分片上传进度
|
||||
// 注意:不再使用服务端缓存,通过检查实际文件系统来获取进度
|
||||
// 优化:如果分片数量很大,可以考虑限制返回的索引数量或使用并发检查
|
||||
func (s *AttachmentServiceImpl) GetChunkProgress(chunkID string, totalChunks int) (map[string]any, error) {
|
||||
storage := facades.Storage().Disk(s.disk)
|
||||
|
||||
uploadedCount := 0
|
||||
uploadedIndices := []int{} // 已上传的分片索引数组
|
||||
|
||||
// 优化:如果分片数量超过1000,只返回前100个已上传的索引,减少响应大小
|
||||
maxIndices := 100
|
||||
if totalChunks > 1000 {
|
||||
maxIndices = 100
|
||||
}
|
||||
|
||||
// 检查每个分片文件是否存在
|
||||
indices := make([]int, totalChunks)
|
||||
for i := range indices {
|
||||
chunkPath := fmt.Sprintf("chunks/%s/%d", chunkID, i)
|
||||
if storage.Exists(chunkPath) {
|
||||
uploadedCount++
|
||||
// 只记录前 maxIndices 个索引,减少响应大小
|
||||
if len(uploadedIndices) < maxIndices {
|
||||
uploadedIndices = append(uploadedIndices, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress := float64(uploadedCount) / float64(totalChunks) * 100
|
||||
|
||||
return map[string]any{
|
||||
"chunk_id": chunkID,
|
||||
"total_chunks": totalChunks,
|
||||
"uploaded_count": uploadedCount,
|
||||
"uploaded_chunks": uploadedIndices, // 返回已上传的分片索引数组(最多100个)
|
||||
"progress": progress,
|
||||
"completed": uploadedCount == totalChunks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadFile 普通文件上传(小文件)
|
||||
func (s *AttachmentServiceImpl) UploadFile(fileData []byte, filename string, mimeType string) (*models.Attachment, error) {
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// 生成文件路径
|
||||
ext := filepath.Ext(filename)
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%s_%d", filename, time.Now().UnixNano())))
|
||||
uniqueName := hex.EncodeToString(hash[:]) + ext
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
finalPath := fmt.Sprintf("attachments/%s/%s", datePath, uniqueName)
|
||||
|
||||
// 保存文件
|
||||
storage := facades.Storage().Disk(s.disk)
|
||||
if err := storage.Put(finalPath, string(fileData)); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "保存文件失败", map[string]any{
|
||||
"filename": filename,
|
||||
"file_path": finalPath,
|
||||
"error": err.Error(),
|
||||
}, "保存文件失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrSaveFileFailed.WithError(err)
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
fileSize := int64(len(fileData))
|
||||
if size, err := storage.Size(finalPath); err == nil {
|
||||
fileSize = size
|
||||
}
|
||||
|
||||
// 创建附件记录
|
||||
adminID := uint(0)
|
||||
if s.ctx != nil {
|
||||
if id, err := helpers.GetAdminIDFromContext(s.ctx); err == nil {
|
||||
adminID = id
|
||||
}
|
||||
}
|
||||
|
||||
fileType := s.GetFileType(mimeType)
|
||||
attachment := &models.Attachment{
|
||||
AdminID: adminID,
|
||||
Disk: s.disk,
|
||||
Path: finalPath,
|
||||
Filename: filename,
|
||||
Extension: strings.TrimPrefix(ext, "."),
|
||||
MimeType: mimeType,
|
||||
Size: fileSize,
|
||||
Status: 1,
|
||||
FileType: fileType,
|
||||
}
|
||||
|
||||
if err := facades.Orm().Query().Create(attachment); err != nil {
|
||||
_ = storage.Delete(finalPath)
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "创建附件记录失败", map[string]any{
|
||||
"filename": filename,
|
||||
"file_path": finalPath,
|
||||
"error": err.Error(),
|
||||
}, "创建附件记录失败: %w", err)
|
||||
}
|
||||
return nil, apperrors.ErrCreateFailed.WithError(err)
|
||||
}
|
||||
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件访问URL
|
||||
func (s *AttachmentServiceImpl) GetFileURL(attachment *models.Attachment) string {
|
||||
// 对于本地存储,返回下载接口URL
|
||||
if attachment.Disk == "local" || attachment.Disk == "public" {
|
||||
return fmt.Sprintf("/api/admin/attachments/%d/preview", attachment.ID)
|
||||
}
|
||||
|
||||
// 对于云存储,生成临时URL或直接URL
|
||||
storage := facades.Storage().Disk(attachment.Disk)
|
||||
|
||||
// 尝试生成临时URL(24小时有效)
|
||||
if url, err := storage.TemporaryUrl(attachment.Path, time.Now().Add(24*time.Hour)); err == nil {
|
||||
return url
|
||||
}
|
||||
|
||||
// 如果生成临时URL失败,尝试从配置获取基础URL
|
||||
var configURL string
|
||||
switch attachment.Disk {
|
||||
case "s3":
|
||||
configURL = utils.GetConfigValue("storage", "s3_url", "")
|
||||
case "oss":
|
||||
configURL = utils.GetConfigValue("storage", "oss_url", "")
|
||||
case "cos":
|
||||
configURL = utils.GetConfigValue("storage", "cos_url", "")
|
||||
case "minio":
|
||||
configURL = utils.GetConfigValue("storage", "minio_url", "")
|
||||
}
|
||||
|
||||
if configURL != "" {
|
||||
if !strings.HasSuffix(configURL, "/") {
|
||||
configURL += "/"
|
||||
}
|
||||
return configURL + attachment.Path
|
||||
}
|
||||
|
||||
// 默认返回下载接口URL
|
||||
return fmt.Sprintf("/api/admin/attachments/%d/preview", attachment.ID)
|
||||
}
|
||||
|
||||
// GetFileType 根据MIME类型判断文件类型
|
||||
func (s *AttachmentServiceImpl) GetFileType(mimeType string) string {
|
||||
if strings.HasPrefix(mimeType, "image/") {
|
||||
return "image"
|
||||
}
|
||||
if strings.HasPrefix(mimeType, "video/") {
|
||||
return "video"
|
||||
}
|
||||
if strings.HasPrefix(mimeType, "application/pdf") ||
|
||||
strings.HasPrefix(mimeType, "application/msword") ||
|
||||
strings.HasPrefix(mimeType, "application/vnd.openxmlformats-officedocument") ||
|
||||
strings.HasPrefix(mimeType, "application/vnd.ms-excel") ||
|
||||
strings.HasPrefix(mimeType, "text/") {
|
||||
return "document"
|
||||
}
|
||||
return "other"
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取附件
|
||||
func (s *AttachmentServiceImpl) GetByID(id uint) (*models.Attachment, error) {
|
||||
var attachment models.Attachment
|
||||
if err := facades.Orm().Query().Where("id", id).First(&attachment); err != nil {
|
||||
return nil, apperrors.ErrAttachmentNotFound.WithError(err)
|
||||
}
|
||||
return &attachment, nil
|
||||
}
|
||||
|
||||
// GetByIDs 根据ID列表获取附件
|
||||
func (s *AttachmentServiceImpl) GetByIDs(ids []uint) ([]models.Attachment, error) {
|
||||
if len(ids) == 0 {
|
||||
return []models.Attachment{}, nil
|
||||
}
|
||||
|
||||
idsAny := helpers.ConvertUintSliceToAny(ids)
|
||||
var attachments []models.Attachment
|
||||
if err := facades.Orm().Query().WhereIn("id", idsAny).Get(&attachments); err != nil {
|
||||
return nil, apperrors.ErrQueryFailed.WithError(err)
|
||||
}
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
// GetList 获取附件列表
|
||||
func (s *AttachmentServiceImpl) GetList(filters AttachmentFilters, page, pageSize int) ([]models.Attachment, int64, error) {
|
||||
query := facades.Orm().Query().Model(&models.Attachment{})
|
||||
|
||||
// 应用筛选条件
|
||||
if filters.AdminID != "" {
|
||||
query = query.Where("admin_id", filters.AdminID)
|
||||
}
|
||||
if filters.Filename != "" {
|
||||
query = query.Where("filename LIKE ?", "%"+filters.Filename+"%")
|
||||
}
|
||||
if filters.DisplayName != "" {
|
||||
query = query.Where("display_name LIKE ?", "%"+filters.DisplayName+"%")
|
||||
}
|
||||
if filters.FileType != "" {
|
||||
query = query.Where("file_type = ?", filters.FileType)
|
||||
}
|
||||
if filters.Extension != "" {
|
||||
query = query.Where("extension = ?", filters.Extension)
|
||||
}
|
||||
if filters.StartTime != "" {
|
||||
query = query.Where("created_at >= ?", filters.StartTime)
|
||||
}
|
||||
if filters.EndTime != "" {
|
||||
query = query.Where("created_at <= ?", filters.EndTime)
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
orderBy := filters.OrderBy
|
||||
if orderBy == "" {
|
||||
orderBy = "id:desc"
|
||||
}
|
||||
query = helpers.ApplySort(query, orderBy, "id:desc")
|
||||
|
||||
// 分页查询
|
||||
var attachments []models.Attachment
|
||||
var total int64
|
||||
if err := query.With("Admin").Paginate(page, pageSize, &attachments, &total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return attachments, total, nil
|
||||
}
|
||||
|
||||
// UpdateDisplayName 更新显示名称
|
||||
func (s *AttachmentServiceImpl) UpdateDisplayName(id uint, displayName string) error {
|
||||
var attachment models.Attachment
|
||||
if err := facades.Orm().Query().Where("id", id).First(&attachment); err != nil {
|
||||
return fmt.Errorf("附件不存在: %v", err)
|
||||
}
|
||||
|
||||
attachment.DisplayName = displayName
|
||||
if err := facades.Orm().Query().Save(&attachment); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "更新附件显示名称失败", map[string]any{
|
||||
"attachment_id": id,
|
||||
"display_name": displayName,
|
||||
"error": err.Error(),
|
||||
}, "更新附件显示名称失败: %v", err)
|
||||
}
|
||||
return apperrors.ErrUpdateFailed.WithError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func (s *AttachmentServiceImpl) DeleteFile(attachment *models.Attachment) error {
|
||||
// 删除文件
|
||||
if attachment.Path != "" && attachment.Disk != "" {
|
||||
storage := facades.Storage().Disk(attachment.Disk)
|
||||
if err := storage.Delete(attachment.Path); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "删除文件失败", map[string]any{
|
||||
"attachment_id": attachment.ID,
|
||||
"file_path": attachment.Path,
|
||||
"disk": attachment.Disk,
|
||||
"error": err.Error(),
|
||||
}, "删除文件失败: %w", err)
|
||||
}
|
||||
return apperrors.ErrDeleteFileFailed.WithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
if _, err := facades.Orm().Query().Delete(attachment); err != nil {
|
||||
if s.ctx != nil {
|
||||
errorlog.RecordHTTP(s.ctx, "attachment", "删除附件记录失败", map[string]any{
|
||||
"attachment_id": attachment.ID,
|
||||
"error": err.Error(),
|
||||
}, "删除附件记录失败: %w", err)
|
||||
}
|
||||
return apperrors.ErrDeleteFailed.WithError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user