feat: 添加日志管理和敏感词过滤功能
- 新增日志管理模块:操作日志、登录日志、数据变更日志 - 新增敏感词过滤器 (sanitizer) - 新增日志异步写入管理器 - 新增日志定时清理服务 - 优化帖子相关服务和上传服务
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/s3"
|
||||
"github.com/disintegration/imaging"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
@@ -27,6 +29,13 @@ type UploadService struct {
|
||||
userService UserService
|
||||
}
|
||||
|
||||
// 预览图配置
|
||||
const (
|
||||
PreviewMaxWidth = 300 // 列表/网格模式最大宽度
|
||||
PreviewMaxHeight = 800 // 详情页最大高度
|
||||
PreviewQuality = 70 // WebP 质量
|
||||
)
|
||||
|
||||
// NewUploadService 创建上传服务
|
||||
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
|
||||
return &UploadService{
|
||||
@@ -35,25 +44,31 @@ func NewUploadService(s3Client *s3.Client, userService UserService) *UploadServi
|
||||
}
|
||||
}
|
||||
|
||||
// UploadImage 上传图片
|
||||
func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, error) {
|
||||
// UploadImage 上传图片(返回原图URL和预览图URL)
|
||||
func (s *UploadService) UploadImage(ctx context.Context, file *multipart.FileHeader) (string, string, string, error) {
|
||||
processedData, contentType, ext, err := prepareImageForUpload(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
// 压缩后再计算哈希,确保同一压缩结果映射同一对象名
|
||||
hash := sha256.Sum256(processedData)
|
||||
hashStr := fmt.Sprintf("%x", hash)
|
||||
|
||||
// 上传原图
|
||||
objectName := fmt.Sprintf("images/%s%s", hashStr, ext)
|
||||
|
||||
url, err := s.s3Client.UploadData(ctx, objectName, processedData, contentType)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||
return "", "", "", fmt.Errorf("failed to upload to S3: %w", err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
// 生成预览图
|
||||
previewURL, previewURLLarge, err := s.GeneratePreviewImages(ctx, processedData, hashStr)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview images: %v", err)
|
||||
}
|
||||
|
||||
return url, previewURL, previewURLLarge, nil
|
||||
}
|
||||
|
||||
// getExtFromContentType 根据Content-Type获取文件扩展名
|
||||
@@ -272,3 +287,73 @@ func normalizeImageContentType(contentType string) string {
|
||||
return contentType
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratePreviewImages 生成预览图
|
||||
func (s *UploadService) GeneratePreviewImages(ctx context.Context, originalData []byte, hash string) (previewURL, previewURLLarge string, err error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(originalData))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
originalWidth := bounds.Dx()
|
||||
originalHeight := bounds.Dy()
|
||||
|
||||
// 生成普通预览图(列表/网格模式)
|
||||
previewURL, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "preview", PreviewMaxWidth, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate preview: %v", err)
|
||||
}
|
||||
|
||||
// 生成大预览图(详情页)
|
||||
previewURLLarge, err = s.generatePreview(ctx, img, originalWidth, originalHeight, "large", PreviewMaxHeight, hash)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate large preview: %v", err)
|
||||
}
|
||||
|
||||
return previewURL, previewURLLarge, nil
|
||||
}
|
||||
|
||||
// generatePreview 生成单个预览图
|
||||
func (s *UploadService) generatePreview(ctx context.Context, img image.Image, originalWidth, originalHeight int, mode string, maxDim int, hash string) (string, error) {
|
||||
var targetWidth, targetHeight int
|
||||
|
||||
// 计算目标尺寸
|
||||
if originalWidth > originalHeight {
|
||||
// 宽图:按宽度缩放
|
||||
if originalWidth > maxDim {
|
||||
targetWidth = maxDim
|
||||
targetHeight = int(float64(originalHeight) * float64(maxDim) / float64(originalWidth))
|
||||
} else {
|
||||
return "", nil // 图片足够小,不需要生成预览图
|
||||
}
|
||||
} else {
|
||||
// 高图:按高度缩放
|
||||
if originalHeight > maxDim {
|
||||
targetHeight = maxDim
|
||||
targetWidth = int(float64(originalWidth) * float64(maxDim) / float64(originalHeight))
|
||||
} else {
|
||||
return "", nil // 图片足够小,不需要生成预览图
|
||||
}
|
||||
}
|
||||
|
||||
// 缩放图片(使用高质量 Lanczos 算法)
|
||||
scaledImg := imaging.Resize(img, targetWidth, targetHeight, imaging.Lanczos)
|
||||
|
||||
// 编码为 JPEG(WebP 在某些设备上可能不支持,使用 JPEG 保证兼容性)
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, scaledImg, &jpeg.Options{Quality: PreviewQuality}); err != nil {
|
||||
return "", fmt.Errorf("failed to encode jpeg: %w", err)
|
||||
}
|
||||
|
||||
// 构建对象名:thumbnails/{hash}_{mode}.jpg
|
||||
objectName := fmt.Sprintf("thumbnails/%s_%s.jpg", hash, mode)
|
||||
|
||||
// 上传到 S3
|
||||
url, err := s.s3Client.UploadData(ctx, objectName, buf.Bytes(), "image/jpeg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to upload preview: %w", err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user