A:照片任务管理
This commit is contained in:
1 parent
ce89028f4d
commit
6ed3410601
8 files changed
+1508
-165
No files matched your search
@@ -0,0 +1,70 @@
|
||||
# Argus 图像处理服务配置文件示例
|
||||
|
||||
app:
|
||||
port: "8080"
|
||||
mode: "debug" # debug 或 release
|
||||
read_timeout: 30s
|
||||
write_timeout: 30s
|
||||
idle_timeout: 60s
|
||||
development: true # 开发模式
|
||||
|
||||
# 任务处理配置
|
||||
task:
|
||||
# 并发工作协程数量,0表示自动检测(CPU核心数)
|
||||
concurrency: 0
|
||||
# 任务队列最大容量
|
||||
queue_capacity: 1000
|
||||
# 是否启用自动调整并发数
|
||||
auto_adjust: true
|
||||
# 系统监控间隔(秒)
|
||||
monitor_interval: 10
|
||||
# 最大并发数限制(防止系统过载),0表示CPU核心数*2
|
||||
max_concurrency: 0
|
||||
# 最小并发数限制
|
||||
min_concurrency: 1
|
||||
# 是否在启动时自动开始任务管理器
|
||||
auto_start: true
|
||||
|
||||
# 工具路径配置
|
||||
tools:
|
||||
exiftool_path: ""
|
||||
imagemagick_path: ""
|
||||
libvips_path: ""
|
||||
|
||||
# 路径配置
|
||||
paths:
|
||||
cache_path: "cache"
|
||||
thumbnail_path: "thumbnail"
|
||||
log_path: "app-logs"
|
||||
temp_path: "app-tmp"
|
||||
png_temp_path: "png-tmp"
|
||||
database_path: "db"
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level: "info" # debug, info, warn, error
|
||||
file_path: ""
|
||||
|
||||
# 数据库配置
|
||||
database:
|
||||
type: "sqlite" # sqlite 或 mysql
|
||||
database: "argus"
|
||||
host: "localhost"
|
||||
port: "3306"
|
||||
username: ""
|
||||
password: ""
|
||||
path: "" # SQLite数据库文件路径,空表示使用默认位置
|
||||
max_idle_conns: 1
|
||||
max_open_conns: 1
|
||||
max_lifetime: 0
|
||||
|
||||
# 图像处理配置
|
||||
image:
|
||||
thumbnail_format: "jpg" # jpg 或 webp
|
||||
thumbnail_size: [150, 300, 500, 800] # 预生成的缩略图尺寸
|
||||
thumbnail_quality: 80
|
||||
screen_size: 1080
|
||||
supported_formats:
|
||||
base: [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"]
|
||||
special: [".gif", ".heic", ".heif", ".webp", ".avif", ".jxl"]
|
||||
thumbnail: [".jpg", ".webp"]
|
||||
@@ -14,7 +14,7 @@ app:
|
||||
# libvips_path: "C:/tools/vips/bin/vips.exe"
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
level: "debug"
|
||||
|
||||
database:
|
||||
type: "sqlite"
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"fmt"
|
||||
"github.com/h2non/filetype"
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rear/internal/config"
|
||||
"rear/internal/consts"
|
||||
"rear/internal/model"
|
||||
"rear/internal/utils/tools"
|
||||
"rear/pkg/logger"
|
||||
"rear/pkg/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ImageAPI struct {
|
||||
@@ -101,6 +103,18 @@ func (api *ImageAPI) GetExif() error {
|
||||
splitExifData := model.SplitExifData(exifData)
|
||||
api.exifData = splitExifData
|
||||
|
||||
// 打印 EXIF 信息
|
||||
if splitExifData != nil {
|
||||
logger.Info("EXIF 信息提取完成",
|
||||
zap.String("path", api.path),
|
||||
zap.String("make", splitExifData.Exif.Make),
|
||||
zap.String("model", splitExifData.Exif.Model),
|
||||
zap.String("date_time_orig", splitExifData.Exif.DateTimeOrig),
|
||||
zap.Int("width", splitExifData.BaseInfo.ImageWidth),
|
||||
zap.Int("height", splitExifData.BaseInfo.ImageHeight),
|
||||
)
|
||||
}
|
||||
|
||||
// todo 将 exif 数据存储到数据库中
|
||||
|
||||
return nil
|
||||
@@ -115,72 +129,400 @@ func (api *ImageAPI) GetExif() error {
|
||||
5. 如果是缩略图,则判断是否存在对应宽度的缩略图,如果存在则返回该路径,如果不存在则生成缩略图并返回(缩略图宽度会是固定的值)
|
||||
6. 如果传入的参数很大,比如超过原图的宽度,则返回原图路径
|
||||
*/
|
||||
func (api *ImageAPI) GetImagePath(longSide int) {
|
||||
// 文件类型是否匹配
|
||||
s := string(config.CONFIG.ImageCompressionOption.ThumbnailFormat)
|
||||
fileType := api.format
|
||||
// 类型相同则返回源路径; 类型不同将图片转换为 PNG 再转换为 指定格式 “原图”
|
||||
if fileType == s {
|
||||
func (api *ImageAPI) GetImagePath(longSide int) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
}
|
||||
|
||||
// 小于 0 获取原图
|
||||
// 小于等于0获取原图
|
||||
if longSide <= 0 {
|
||||
} else {
|
||||
// 获取指定质量
|
||||
return api.GetSupportOriginalImagePath(ctx)
|
||||
}
|
||||
|
||||
// 获取原图信息以判断是否需要缩放
|
||||
supportedPath, err := api.GetSupportOriginalImagePath(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 获取图像信息判断尺寸
|
||||
imageInfo, err := tools.GetVipsImageInfo(ctx, supportedPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取图像信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果请求的尺寸大于或等于原图的最长边,直接返回原图
|
||||
maxSide := imageInfo.Width
|
||||
if imageInfo.Height > maxSide {
|
||||
maxSide = imageInfo.Height
|
||||
}
|
||||
if longSide >= maxSide {
|
||||
return supportedPath, nil
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
return api.generateThumbnail(ctx, supportedPath, longSide)
|
||||
}
|
||||
|
||||
// GetSupportOriginalImagePath 获取支持的原图路径(如果设置的为 JPG 则返回 JPG ,如果是 Webp 则会是 Webp)
|
||||
func (api *ImageAPI) GetSupportOriginalImagePath(ctx context.Context) (string, error) {
|
||||
targetFormat := config.CONFIG.ImageCompressionOption.ThumbnailFormat
|
||||
|
||||
// 判断是否是支持的格式
|
||||
if api.isSupportedFormat() {
|
||||
return api.path, nil
|
||||
}
|
||||
|
||||
// ============ 到这里不是默认格式,需要转换 ==========
|
||||
|
||||
// 生成转换后的文件路径
|
||||
convertedPath := api.getConvertedImagePath(targetFormat)
|
||||
|
||||
// 检查转换后的文件是否已存在
|
||||
if utils.FileUtils.Exists(convertedPath) {
|
||||
return convertedPath, nil
|
||||
}
|
||||
|
||||
// 需要转换图像
|
||||
if err := api.convertImageToSupported(ctx, convertedPath, targetFormat); err != nil {
|
||||
return "", fmt.Errorf("图像转换失败: %w", err)
|
||||
}
|
||||
|
||||
return convertedPath, nil
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
// isSupportedFormat 判断是否是直接支持的格式
|
||||
func (api *ImageAPI) isSupportedFormat() bool {
|
||||
return api.format == string(consts.FormatJPG) ||
|
||||
api.format == string(consts.FormatJPEG) ||
|
||||
api.format == string(consts.FormatWEBP)
|
||||
}
|
||||
|
||||
// getConvertedImagePath 获取转换后的图像路径
|
||||
func (api *ImageAPI) getConvertedImagePath(targetFormat consts.ImageFormat) string {
|
||||
// 使用与 main.go createCachePath 相同的逻辑构建完整缩略图路径
|
||||
thumbnailBasePath := filepath.Join(config.CONFIG.AppDir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
|
||||
|
||||
ext := string(targetFormat)
|
||||
return utils.HashUtils.HashThumbPath(thumbnailBasePath, api.hash, "original", ext)
|
||||
}
|
||||
|
||||
// convertImageToSupported 转换图像到支持的格式
|
||||
func (api *ImageAPI) convertImageToSupported(ctx context.Context, outputPath string, targetFormat consts.ImageFormat) error {
|
||||
// 确保输出目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据原图格式选择转换策略
|
||||
switch strings.ToLower(api.format) {
|
||||
case "png":
|
||||
// PNG直接转换为目标格式
|
||||
return api.convertPNGToTarget(ctx, outputPath, targetFormat)
|
||||
case "raw", "cr2", "nef", "arw", "dng":
|
||||
// RAW格式:RAW → PNG → 目标格式
|
||||
return api.convertRAWToTarget(ctx, outputPath, targetFormat)
|
||||
case "heic", "heif":
|
||||
// HEIC格式转换
|
||||
return api.convertHEICToTarget(ctx, outputPath, targetFormat)
|
||||
default:
|
||||
// 其他格式尝试用ImageMagick转换
|
||||
return api.convertOtherToTarget(ctx, outputPath, targetFormat)
|
||||
}
|
||||
}
|
||||
|
||||
// CompressedImg 压缩图片 longSide 为图片的最长边
|
||||
func (api *ImageAPI) CompressedImg(longSide int) {
|
||||
|
||||
// 获取支持的原图路径
|
||||
// 判断是否需要压缩,如果超过指定大小就进行压缩,如果超过指定比例就裁切,裁切分为截取头部和截取中间(第一版截取中间)
|
||||
// convertPNGToTarget PNG转换为目标格式
|
||||
func (api *ImageAPI) convertPNGToTarget(ctx context.Context, outputPath string, targetFormat consts.ImageFormat) error {
|
||||
options := &tools.VipsConvertOptions{
|
||||
Quality: config.CONFIG.ImageCompressionOption.ThumbnailQuality,
|
||||
StripMeta: true,
|
||||
}
|
||||
|
||||
return tools.ConvertVipsImage(ctx, api.path, outputPath, targetFormat, options)
|
||||
}
|
||||
|
||||
// convertRAWToTarget RAW转换为目标格式
|
||||
func (api *ImageAPI) convertRAWToTarget(ctx context.Context, outputPath string, targetFormat consts.ImageFormat) error {
|
||||
// 第一步:使用ImageMagick将RAW转换为PNG
|
||||
tempPNGPath := filepath.Join(config.CONFIG.PathConfig.PngTempPath, api.hash+".png")
|
||||
if err := os.MkdirAll(filepath.Dir(tempPNGPath), 0755); err != nil {
|
||||
return fmt.Errorf("创建临时目录失败: %w", err)
|
||||
}
|
||||
|
||||
// RAW → PNG
|
||||
if err := tools.ConvertImage(ctx, api.path, tempPNGPath); err != nil {
|
||||
return fmt.Errorf("RAW到PNG转换失败: %w", err)
|
||||
}
|
||||
|
||||
// 确保在函数结束时删除临时PNG文件
|
||||
defer func() {
|
||||
if err := os.Remove(tempPNGPath); err != nil {
|
||||
logger.Warn("删除临时PNG文件失败", zap.String("path", tempPNGPath), zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 第二步:使用LibVips将PNG转换为目标格式
|
||||
options := &tools.VipsConvertOptions{
|
||||
Quality: config.CONFIG.ImageCompressionOption.ThumbnailQuality,
|
||||
StripMeta: true,
|
||||
}
|
||||
|
||||
return tools.ConvertVipsImage(ctx, tempPNGPath, outputPath, targetFormat, options)
|
||||
}
|
||||
|
||||
// convertHEICToTarget HEIC转换为目标格式
|
||||
func (api *ImageAPI) convertHEICToTarget(ctx context.Context, outputPath string, targetFormat consts.ImageFormat) error {
|
||||
// 可以使用ImageMagick或libvips处理HEIC
|
||||
options := &tools.VipsConvertOptions{
|
||||
Quality: config.CONFIG.ImageCompressionOption.ThumbnailQuality,
|
||||
StripMeta: true,
|
||||
}
|
||||
|
||||
return tools.ConvertVipsImage(ctx, api.path, outputPath, targetFormat, options)
|
||||
}
|
||||
|
||||
// convertOtherToTarget 其他格式转换为目标格式
|
||||
func (api *ImageAPI) convertOtherToTarget(ctx context.Context, outputPath string, targetFormat consts.ImageFormat) error {
|
||||
// 尝试使用LibVips直接转换
|
||||
options := &tools.VipsConvertOptions{
|
||||
Quality: config.CONFIG.ImageCompressionOption.ThumbnailQuality,
|
||||
StripMeta: true,
|
||||
}
|
||||
|
||||
err := tools.ConvertVipsImage(ctx, api.path, outputPath, targetFormat, options)
|
||||
if err != nil {
|
||||
// 如果LibVips失败,尝试ImageMagick
|
||||
logger.Warn("LibVips转换失败,尝试ImageMagick", zap.Error(err))
|
||||
|
||||
// 先转换为PNG,再用LibVips转换为目标格式
|
||||
tempPNGPath := filepath.Join(config.CONFIG.PathConfig.PngTempPath, api.hash+".png")
|
||||
if err := os.MkdirAll(filepath.Dir(tempPNGPath), 0755); err != nil {
|
||||
return fmt.Errorf("创建临时目录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := tools.ConvertImage(ctx, api.path, tempPNGPath); err != nil {
|
||||
return fmt.Errorf("ImageMagick转换失败: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.Remove(tempPNGPath); err != nil {
|
||||
logger.Warn("删除临时PNG文件失败", zap.String("path", tempPNGPath), zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
return tools.ConvertVipsImage(ctx, tempPNGPath, outputPath, targetFormat, options)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateThumbnail 生成缩略图
|
||||
func (api *ImageAPI) generateThumbnail(ctx context.Context, sourcePath string, longSide int) (string, error) {
|
||||
// 获取缩略图路径
|
||||
thumbnailPath := api.getThumbnailPath(longSide)
|
||||
|
||||
// 检查缩略图是否已存在
|
||||
if utils.FileUtils.Exists(thumbnailPath) {
|
||||
return thumbnailPath, nil
|
||||
}
|
||||
|
||||
// 确保输出目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(thumbnailPath), 0755); err != nil {
|
||||
return "", fmt.Errorf("创建缩略图目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取原图信息
|
||||
imageInfo, err := tools.GetVipsImageInfo(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取图像信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 计算缩略图尺寸
|
||||
targetWidth, targetHeight := api.calculateThumbnailSize(imageInfo.Width, imageInfo.Height, longSide)
|
||||
|
||||
// 检查是否需要特殊裁剪处理(针对极端比例的图片)
|
||||
if api.needsSpecialCropping(imageInfo.Width, imageInfo.Height) {
|
||||
return api.generateCroppedThumbnail(ctx, sourcePath, thumbnailPath, targetWidth, targetHeight)
|
||||
}
|
||||
|
||||
// 标准等比例缩放
|
||||
return api.generateStandardThumbnail(ctx, sourcePath, thumbnailPath, targetWidth, targetHeight)
|
||||
}
|
||||
|
||||
// getThumbnailPath 获取缩略图路径
|
||||
func (api *ImageAPI) getThumbnailPath(longSide int) string {
|
||||
// 使用与 main.go createCachePath 相同的逻辑构建完整缩略图路径
|
||||
thumbnailPath := filepath.Join(config.CONFIG.AppDir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
|
||||
|
||||
sizeStr := strconv.Itoa(longSide) + "X" + strconv.Itoa(longSide)
|
||||
ext := string(config.CONFIG.ImageCompressionOption.ThumbnailFormat)
|
||||
|
||||
return utils.HashUtils.HashThumbPath(thumbnailPath, api.hash, sizeStr, ext)
|
||||
}
|
||||
|
||||
// calculateThumbnailSize 计算缩略图尺寸
|
||||
func (api *ImageAPI) calculateThumbnailSize(originalWidth, originalHeight, longSide int) (int, int) {
|
||||
if originalWidth >= originalHeight {
|
||||
// 宽度为长边
|
||||
scale := float64(longSide) / float64(originalWidth)
|
||||
return longSide, int(float64(originalHeight) * scale)
|
||||
} else {
|
||||
// 高度为长边
|
||||
scale := float64(longSide) / float64(originalHeight)
|
||||
return int(float64(originalWidth) * scale), longSide
|
||||
}
|
||||
}
|
||||
|
||||
// needsSpecialCropping 判断是否需要特殊裁剪(极端比例图片)
|
||||
func (api *ImageAPI) needsSpecialCropping(width, height int) bool {
|
||||
ratio := float64(width) / float64(height)
|
||||
// 比例超过1:10或10:1认为是极端比例
|
||||
return ratio > 10.0 || ratio < 0.1
|
||||
}
|
||||
|
||||
// generateCroppedThumbnail 生成裁剪缩略图
|
||||
func (api *ImageAPI) generateCroppedThumbnail(ctx context.Context, sourcePath, outputPath string, targetWidth, targetHeight int) (string, error) {
|
||||
// 对于极端比例的图片,使用智能裁剪
|
||||
options := &tools.VipsCropOptions{
|
||||
Position: tools.VipsCropSmart,
|
||||
}
|
||||
|
||||
// 裁剪为更合理的比例,比如最大1:3
|
||||
maxWidth := targetHeight * 3
|
||||
maxHeight := targetWidth * 3
|
||||
|
||||
if targetWidth > maxWidth {
|
||||
targetWidth = maxWidth
|
||||
}
|
||||
if targetHeight > maxHeight {
|
||||
targetHeight = maxHeight
|
||||
}
|
||||
|
||||
return outputPath, tools.CropVipsImage(ctx, sourcePath, outputPath, targetWidth, targetHeight, options)
|
||||
}
|
||||
|
||||
// generateStandardThumbnail 生成标准缩略图
|
||||
func (api *ImageAPI) generateStandardThumbnail(ctx context.Context, sourcePath, outputPath string, targetWidth, targetHeight int) (string, error) {
|
||||
// 使用libvips的综合压缩功能
|
||||
quality := config.CONFIG.ImageCompressionOption.ThumbnailQuality
|
||||
targetFormat := config.CONFIG.ImageCompressionOption.ThumbnailFormat
|
||||
|
||||
err := tools.CompressVipsImage(ctx, sourcePath, outputPath, targetFormat, targetWidth, targetHeight, quality)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成缩略图失败: %w", err)
|
||||
}
|
||||
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
// 公共API函数
|
||||
|
||||
// GetOriginalImagePath 获取原图(真的是原图)
|
||||
func (api *ImageAPI) GetOriginalImagePath() string {
|
||||
return api.path
|
||||
}
|
||||
|
||||
// GetSupportOriginalImagePath 获取支持的原图路径(如果设置的为 JPG 则返回 JPG ,如果是 Webp 则会是 Webp)
|
||||
func (api *ImageAPI) GetSupportOriginalImagePath() string {
|
||||
// 判断是否是支持的格式
|
||||
if api.format == string(consts.FormatJPG) || api.format == string(consts.FormatJPEG) || api.format == string(consts.FormatWEBP) {
|
||||
return api.path
|
||||
}
|
||||
|
||||
// ============ 到这里不是默认格式,要么是 PNG,要么是其他格式,所以一定会存在或最终转换为 PNG ==========
|
||||
|
||||
// 假设认为他是 PNG 格式
|
||||
//pngPath := api.path
|
||||
|
||||
// 如果不是 PNG 则转换为 PNG
|
||||
if api.format != string(consts.FormatPNG) {
|
||||
logger.Error("[TODO] 获取到不支持的图片格式!", zap.String("format", api.format))
|
||||
log.Fatal("[TODO] 获取到不支持的图片格式!", api.format)
|
||||
}
|
||||
|
||||
// 到这里一定是 PNG 了,转换为 JPG 或 Webp 格式
|
||||
logger.Error("[TODO] 现在不支持 PNG 格式转换!", zap.String("format", api.format))
|
||||
log.Fatal("[TODO] 现在不支持 PNG 格式转换!", api.format)
|
||||
return ""
|
||||
// GetHash 获取图像哈希值
|
||||
func (api *ImageAPI) GetHash() string {
|
||||
return api.hash
|
||||
}
|
||||
|
||||
// GetImage 获取照片
|
||||
func (api *ImageAPI) GetImage() {
|
||||
/*
|
||||
获取图片路径的逻辑:
|
||||
|
||||
1. 判断照片类型,如果照片类型是非 JPG 和 Webp 则寻找 JPG 和 Webp 所在路径
|
||||
2. 如果没有 JPG 和 Webp 则将照片转换为 JPG 或 Webp 并返回
|
||||
*/
|
||||
|
||||
// GetFormat 获取图像格式
|
||||
func (api *ImageAPI) GetFormat() string {
|
||||
return api.format
|
||||
}
|
||||
|
||||
// 获取图片路径
|
||||
//func (api *ImageAPI) getImageFormat(path string) (string, error) {
|
||||
//buf, err := os.Read
|
||||
//}
|
||||
// GetExifData 获取EXIF数据
|
||||
func (api *ImageAPI) GetExifData() *model.ParsedExif {
|
||||
return api.exifData
|
||||
}
|
||||
|
||||
// GetImageInfo 获取图像基础信息
|
||||
func (api *ImageAPI) GetImageInfo(ctx context.Context) (*tools.VipsImageInfo, error) {
|
||||
supportedPath, err := api.GetSupportOriginalImagePath(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tools.GetVipsImageInfo(ctx, supportedPath)
|
||||
}
|
||||
|
||||
// IsSupported 判断图像格式是否被支持
|
||||
func (api *ImageAPI) IsSupported() bool {
|
||||
supportedFormats := []string{"jpg", "jpeg", "png", "webp", "heic", "heif", "raw", "cr2", "nef", "arw", "dng", "tiff", "bmp", "gif"}
|
||||
format := strings.ToLower(api.format)
|
||||
|
||||
for _, supported := range supportedFormats {
|
||||
if format == supported {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ProcessImage 处理图像的主要入口方法
|
||||
// 包含:格式转换、缩略图生成、EXIF提取等完整处理流程
|
||||
func (api *ImageAPI) ProcessImage() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// 1. 提取EXIF信息
|
||||
if err := api.GetExif(); err != nil {
|
||||
logger.Warn("获取EXIF信息失败", zap.String("path", api.path), zap.Error(err))
|
||||
// EXIF获取失败不终止处理,但记录警告
|
||||
}
|
||||
|
||||
// 2. 格式转换(如果需要)
|
||||
supportedPath, err := api.GetSupportOriginalImagePath(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成支持格式原图失败: %w", err)
|
||||
}
|
||||
logger.Info("格式转换完成", zap.String("path", supportedPath))
|
||||
|
||||
// 3. 生成缩略图
|
||||
if err := api.generateAllThumbnails(ctx); err != nil {
|
||||
return fmt.Errorf("生成缩略图失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateAllThumbnails 生成所有尺寸的缩略图
|
||||
func (api *ImageAPI) generateAllThumbnails(ctx context.Context) error {
|
||||
// 从配置中获取需要生成的缩略图尺寸
|
||||
thumbnailSizes := api.getThumbnailSizes()
|
||||
|
||||
for _, size := range thumbnailSizes {
|
||||
thumbnailPath, err := api.GetImagePath(size)
|
||||
if err != nil {
|
||||
logger.Warn("生成缩略图失败",
|
||||
zap.String("path", api.path),
|
||||
zap.Int("size", size),
|
||||
zap.Error(err))
|
||||
continue // 某个尺寸失败不影响其他尺寸
|
||||
}
|
||||
|
||||
logger.Debug("生成缩略图完成",
|
||||
zap.String("original", api.path),
|
||||
zap.String("thumbnail", thumbnailPath),
|
||||
zap.Int("size", size))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getThumbnailSizes 获取需要预生成的缩略图尺寸
|
||||
func (api *ImageAPI) getThumbnailSizes() []int {
|
||||
// 从配置中获取,如果没有配置则使用默认值
|
||||
if len(config.CONFIG.ImageCompressionOption.ThumbnailSize) > 0 {
|
||||
return config.CONFIG.ImageCompressionOption.ThumbnailSize
|
||||
}
|
||||
|
||||
// 默认缩略图尺寸
|
||||
return []int{800}
|
||||
}
|
||||
|
||||
// CompressedImg 压缩图片获取路径
|
||||
func (api *ImageAPI) CompressedImg(longSide int) (string, error) {
|
||||
return api.GetImagePath(longSide)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"rear/internal/consts"
|
||||
"rear/pkg/logger"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -63,6 +64,7 @@ type Config struct {
|
||||
// 新增配置字段
|
||||
ToolsConfig ToolsConfig
|
||||
LoggingConfig LoggingConfig
|
||||
TaskConfig TaskConfig // 任务处理配置
|
||||
FileConfig *FileConfig // 原始配置文件内容
|
||||
}
|
||||
|
||||
@@ -129,6 +131,7 @@ func InitConfig() *Config {
|
||||
AppDir: execDir,
|
||||
ToolsConfig: fileConfig.Tools,
|
||||
LoggingConfig: fileConfig.Logging,
|
||||
TaskConfig: fileConfig.Task,
|
||||
FileConfig: fileConfig,
|
||||
}
|
||||
return &CONFIG
|
||||
@@ -213,3 +216,52 @@ func IsDevelopment() bool {
|
||||
func IsProduction() bool {
|
||||
return !IsDevelopment()
|
||||
}
|
||||
|
||||
// GetTaskConfig 获取优化后的任务配置
|
||||
func GetTaskConfig() TaskConfig {
|
||||
taskConfig := CONFIG.TaskConfig
|
||||
|
||||
// 如果并发数为0,自动设置为CPU核心数
|
||||
if taskConfig.Concurrency == 0 {
|
||||
taskConfig.Concurrency = runtime.NumCPU()
|
||||
}
|
||||
|
||||
// 如果最大并发数为0,设置为CPU核心数*2
|
||||
if taskConfig.MaxConcurrency == 0 {
|
||||
taskConfig.MaxConcurrency = runtime.NumCPU() * 2
|
||||
}
|
||||
|
||||
// 确保最小并发数至少为1
|
||||
if taskConfig.MinConcurrency == 0 {
|
||||
taskConfig.MinConcurrency = 1
|
||||
}
|
||||
|
||||
// 确保并发数在合理范围内
|
||||
if taskConfig.Concurrency < taskConfig.MinConcurrency {
|
||||
taskConfig.Concurrency = taskConfig.MinConcurrency
|
||||
}
|
||||
if taskConfig.Concurrency > taskConfig.MaxConcurrency {
|
||||
taskConfig.Concurrency = taskConfig.MaxConcurrency
|
||||
}
|
||||
|
||||
// 如果队列容量为0,设置默认值
|
||||
if taskConfig.QueueCapacity == 0 {
|
||||
taskConfig.QueueCapacity = 1000
|
||||
}
|
||||
|
||||
// 如果监控间隔为0,设置默认值
|
||||
if taskConfig.MonitorInterval == 0 {
|
||||
taskConfig.MonitorInterval = 10
|
||||
}
|
||||
|
||||
logger.Info("任务配置已优化",
|
||||
zap.Int("concurrency", taskConfig.Concurrency),
|
||||
zap.Int("max_concurrency", taskConfig.MaxConcurrency),
|
||||
zap.Int("min_concurrency", taskConfig.MinConcurrency),
|
||||
zap.Int("queue_capacity", taskConfig.QueueCapacity),
|
||||
zap.Int("monitor_interval", taskConfig.MonitorInterval),
|
||||
zap.Bool("auto_adjust", taskConfig.AutoAdjust),
|
||||
zap.Bool("auto_start", taskConfig.AutoStart))
|
||||
|
||||
return taskConfig
|
||||
}
|
||||
@@ -63,6 +63,24 @@ type ImageConfig struct {
|
||||
} `yaml:"supported_formats" json:"supported_formats"`
|
||||
}
|
||||
|
||||
// TaskConfig 任务处理配置
|
||||
type TaskConfig struct {
|
||||
// 并发工作协程数量,0表示自动检测(CPU核心数)
|
||||
Concurrency int `yaml:"concurrency" json:"concurrency"`
|
||||
// 任务队列最大容量
|
||||
QueueCapacity int `yaml:"queue_capacity" json:"queue_capacity"`
|
||||
// 是否启用自动调整并发数
|
||||
AutoAdjust bool `yaml:"auto_adjust" json:"auto_adjust"`
|
||||
// 系统监控间隔(秒)
|
||||
MonitorInterval int `yaml:"monitor_interval" json:"monitor_interval"`
|
||||
// 最大并发数限制(防止系统过载)
|
||||
MaxConcurrency int `yaml:"max_concurrency" json:"max_concurrency"`
|
||||
// 最小并发数限制
|
||||
MinConcurrency int `yaml:"min_concurrency" json:"min_concurrency"`
|
||||
// 是否在启动时自动开始任务管理器
|
||||
AutoStart bool `yaml:"auto_start" json:"auto_start"`
|
||||
}
|
||||
|
||||
// FileConfig 完整的配置文件结构
|
||||
type FileConfig struct {
|
||||
App AppConfig `yaml:"app" json:"app"`
|
||||
@@ -71,6 +89,7 @@ type FileConfig struct {
|
||||
Logging LoggingConfig `yaml:"logging" json:"logging"`
|
||||
Database DatabaseConfig `yaml:"database" json:"database"`
|
||||
Image ImageConfig `yaml:"image" json:"image"`
|
||||
Task TaskConfig `yaml:"task" json:"task"`
|
||||
}
|
||||
|
||||
// LoadConfigFromFile 从文件加载配置
|
||||
@@ -150,6 +169,15 @@ func GetDefaultFileConfig() *FileConfig {
|
||||
Thumbnail: []string{".jpg", ".webp"},
|
||||
},
|
||||
},
|
||||
Task: TaskConfig{
|
||||
Concurrency: 0, // 0表示自动检测CPU核心数
|
||||
QueueCapacity: 1000, // 任务队列容量
|
||||
AutoAdjust: true, // 启用自动调整
|
||||
MonitorInterval: 10, // 10秒监控间隔
|
||||
MaxConcurrency: 0, // 0表示CPU核心数*2
|
||||
MinConcurrency: 1, // 最少1个并发
|
||||
AutoStart: true, // 启动时自动开始
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +297,19 @@ func MergeWithDefaults(fileConfig *FileConfig) *FileConfig {
|
||||
fileConfig.Image.SupportedFormats.Thumbnail = defaultConfig.Image.SupportedFormats.Thumbnail
|
||||
}
|
||||
|
||||
// Task配置合并
|
||||
if fileConfig.Task.QueueCapacity == 0 {
|
||||
fileConfig.Task.QueueCapacity = defaultConfig.Task.QueueCapacity
|
||||
}
|
||||
if fileConfig.Task.MonitorInterval == 0 {
|
||||
fileConfig.Task.MonitorInterval = defaultConfig.Task.MonitorInterval
|
||||
}
|
||||
if fileConfig.Task.MinConcurrency == 0 {
|
||||
fileConfig.Task.MinConcurrency = defaultConfig.Task.MinConcurrency
|
||||
}
|
||||
// 注意:Concurrency、MaxConcurrency、AutoAdjust、AutoStart 为0或false时可能是用户有意设置的
|
||||
// 所以这里不做默认值合并,而是在使用时进行处理
|
||||
|
||||
return fileConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// BaseModel 基础模型,包含通用字段
|
||||
type BaseModel struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// 软删除 【查询时会检测删除时间】
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
// Photo 表结构 - 保存图片基础信息
|
||||
type Photo struct {
|
||||
BaseModel
|
||||
ImgPath string `gorm:"column:img_path;not null" json:"imgPath"`
|
||||
ImgName string `gorm:"column:img_name;not null" json:"imgName"`
|
||||
Hash string `gorm:"uniqueIndex;not null" json:"hash"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
// 图片的宽高比
|
||||
AspectRatio float32 `json:"aspectRatio"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
Format string `json:"format"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
|
||||
// 人物数量
|
||||
//
|
||||
|
||||
IsAlgorithm bool `json:"isAlgorithm"`
|
||||
AlgorithmScore *int `json:"algorithmScore,omitempty"`
|
||||
LastViewedTime *int64 `json:"lastViewedTime,omitempty"`
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"os/exec"
|
||||
"rear/internal/api"
|
||||
"rear/internal/config"
|
||||
"rear/pkg/logger"
|
||||
"rear/pkg/utils"
|
||||
"runtime"
|
||||
@@ -22,13 +18,107 @@ import (
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
StatusPending TaskStatus = "pending"
|
||||
StatusRunning TaskStatus = "running"
|
||||
StatusPaused TaskStatus = "paused"
|
||||
StatusFailed TaskStatus = "failed"
|
||||
StatusDone TaskStatus = "done"
|
||||
StatusPending TaskStatus = "pending" // 等待处理
|
||||
StatusRunning TaskStatus = "running" // 正在处理
|
||||
StatusPaused TaskStatus = "paused" // 已暂停
|
||||
StatusFailed TaskStatus = "failed" // 处理失败
|
||||
StatusDone TaskStatus = "done" // 处理完成
|
||||
)
|
||||
|
||||
// TaskStep 任务处理步骤
|
||||
type TaskStep string
|
||||
|
||||
const (
|
||||
StepInitializing TaskStep = "initializing" // 初始化
|
||||
StepValidating TaskStep = "validating" // 验证文件格式
|
||||
StepReadingFile TaskStep = "reading_file" // 读取文件
|
||||
StepCalculatingHash TaskStep = "calculating_hash" // 计算哈希值
|
||||
StepExtractingExif TaskStep = "extracting_exif" // 提取EXIF信息
|
||||
StepConvertingFormat TaskStep = "converting_format" // 转换格式
|
||||
StepGeneratingThumbnails TaskStep = "generating_thumbnails" // 生成缩略图
|
||||
StepSavingToDatabase TaskStep = "saving_to_database" // 保存到数据库
|
||||
StepIntelligentAnalysis TaskStep = "intelligent_analysis" // 智能分析
|
||||
StepCompleted TaskStep = "completed" // 完成
|
||||
)
|
||||
|
||||
// TaskStepInfo 步骤信息
|
||||
type TaskStepInfo struct {
|
||||
Step TaskStep `json:"step"` // 当前步骤
|
||||
StepName string `json:"step_name"` // 步骤名称
|
||||
Description string `json:"description"` // 步骤描述
|
||||
Progress float64 `json:"progress"` // 步骤内进度 (0.0-1.0)
|
||||
Started bool `json:"started"` // 是否已开始
|
||||
Completed bool `json:"completed"` // 是否已完成
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
}
|
||||
|
||||
// GetStepInfo 获取步骤信息
|
||||
func GetStepInfo(step TaskStep) TaskStepInfo {
|
||||
stepInfoMap := map[TaskStep]TaskStepInfo{
|
||||
StepInitializing: {
|
||||
Step: StepInitializing,
|
||||
StepName: "初始化",
|
||||
Description: "正在初始化任务...",
|
||||
},
|
||||
StepValidating: {
|
||||
Step: StepValidating,
|
||||
StepName: "验证格式",
|
||||
Description: "正在验证图像格式...",
|
||||
},
|
||||
StepReadingFile: {
|
||||
Step: StepReadingFile,
|
||||
StepName: "读取文件",
|
||||
Description: "正在读取图像文件...",
|
||||
},
|
||||
StepCalculatingHash: {
|
||||
Step: StepCalculatingHash,
|
||||
StepName: "计算哈希",
|
||||
Description: "正在计算文件哈希值...",
|
||||
},
|
||||
StepExtractingExif: {
|
||||
Step: StepExtractingExif,
|
||||
StepName: "提取EXIF",
|
||||
Description: "正在提取图像元数据...",
|
||||
},
|
||||
StepConvertingFormat: {
|
||||
Step: StepConvertingFormat,
|
||||
StepName: "格式转换",
|
||||
Description: "正在转换图像格式...",
|
||||
},
|
||||
StepGeneratingThumbnails: {
|
||||
Step: StepGeneratingThumbnails,
|
||||
StepName: "生成缩略图",
|
||||
Description: "正在生成缩略图...",
|
||||
},
|
||||
StepSavingToDatabase: {
|
||||
Step: StepSavingToDatabase,
|
||||
StepName: "保存数据",
|
||||
Description: "正在保存到数据库...",
|
||||
},
|
||||
StepIntelligentAnalysis: {
|
||||
Step: StepIntelligentAnalysis,
|
||||
StepName: "智能分析",
|
||||
Description: "正在进行智能分析...",
|
||||
},
|
||||
StepCompleted: {
|
||||
Step: StepCompleted,
|
||||
StepName: "已完成",
|
||||
Description: "任务已完成",
|
||||
Completed: true,
|
||||
},
|
||||
}
|
||||
|
||||
info, exists := stepInfoMap[step]
|
||||
if !exists {
|
||||
return TaskStepInfo{
|
||||
Step: step,
|
||||
StepName: string(step),
|
||||
Description: "未知步骤",
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// --- PictureTask ---
|
||||
type PictureTask struct {
|
||||
ID string
|
||||
@@ -37,9 +127,15 @@ type PictureTask struct {
|
||||
ImageBuf []byte
|
||||
ExifData map[string]string
|
||||
|
||||
Status TaskStatus
|
||||
Progress float64
|
||||
Error error
|
||||
Status TaskStatus `json:"status"` // 任务状态
|
||||
Progress float64 `json:"progress"` // 总体进度 (0.0-1.0)
|
||||
CurrentStep TaskStep `json:"current_step"` // 当前步骤
|
||||
StepInfo TaskStepInfo `json:"step_info"` // 当前步骤详细信息
|
||||
AllSteps []TaskStepInfo `json:"all_steps"` // 所有步骤信息
|
||||
Error error `json:"error,omitempty"` // 错误信息
|
||||
StartTime time.Time `json:"start_time"` // 开始时间
|
||||
UpdateTime time.Time `json:"update_time"` // 最后更新时间
|
||||
CompletedTime *time.Time `json:"completed_time,omitempty"` // 完成时间
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -50,14 +146,35 @@ type PictureTask struct {
|
||||
|
||||
func NewPictureTask(path string) *PictureTask {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 初始化所有步骤信息
|
||||
allSteps := []TaskStepInfo{
|
||||
GetStepInfo(StepInitializing),
|
||||
GetStepInfo(StepValidating),
|
||||
GetStepInfo(StepReadingFile),
|
||||
GetStepInfo(StepCalculatingHash),
|
||||
GetStepInfo(StepExtractingExif),
|
||||
GetStepInfo(StepConvertingFormat),
|
||||
GetStepInfo(StepGeneratingThumbnails),
|
||||
GetStepInfo(StepSavingToDatabase),
|
||||
GetStepInfo(StepIntelligentAnalysis),
|
||||
GetStepInfo(StepCompleted),
|
||||
}
|
||||
|
||||
return &PictureTask{
|
||||
ID: uuid.New().String(),
|
||||
Path: path,
|
||||
Status: StatusPending,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
pauseCh: make(chan struct{}, 1),
|
||||
resumeCh: make(chan struct{}, 1),
|
||||
ID: uuid.New().String(),
|
||||
Path: path,
|
||||
Status: StatusPending,
|
||||
Progress: 0.0,
|
||||
CurrentStep: StepInitializing,
|
||||
StepInfo: GetStepInfo(StepInitializing),
|
||||
AllSteps: allSteps,
|
||||
StartTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
pauseCh: make(chan struct{}, 1),
|
||||
resumeCh: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,69 +202,124 @@ func (pt *PictureTask) Resume() {
|
||||
}
|
||||
}
|
||||
|
||||
// updateStep 更新任务步骤和进度
|
||||
func (pt *PictureTask) updateStep(step TaskStep, stepProgress float64, overallProgress float64) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
|
||||
pt.CurrentStep = step
|
||||
pt.StepInfo = GetStepInfo(step)
|
||||
pt.StepInfo.Started = true
|
||||
pt.StepInfo.Progress = stepProgress
|
||||
pt.Progress = overallProgress
|
||||
pt.UpdateTime = time.Now()
|
||||
|
||||
// 更新所有步骤状态
|
||||
for i, stepInfo := range pt.AllSteps {
|
||||
if stepInfo.Step == step {
|
||||
pt.AllSteps[i] = pt.StepInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("任务步骤更新",
|
||||
zap.String("task_id", pt.ID),
|
||||
zap.String("step_name", pt.StepInfo.StepName),
|
||||
zap.Float64("overall_progress", overallProgress))
|
||||
}
|
||||
|
||||
// completeStep 完成当前步骤
|
||||
func (pt *PictureTask) completeStep() {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
|
||||
pt.StepInfo.Completed = true
|
||||
pt.StepInfo.Progress = 1.0
|
||||
|
||||
// 更新所有步骤状态
|
||||
for i, stepInfo := range pt.AllSteps {
|
||||
if stepInfo.Step == pt.CurrentStep {
|
||||
pt.AllSteps[i] = pt.StepInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setStepError 设置步骤错误
|
||||
func (pt *PictureTask) setStepError(err error) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
|
||||
pt.StepInfo.Error = err.Error()
|
||||
pt.Error = err
|
||||
pt.Status = StatusFailed
|
||||
|
||||
// 更新所有步骤状态
|
||||
for i, stepInfo := range pt.AllSteps {
|
||||
if stepInfo.Step == pt.CurrentStep {
|
||||
pt.AllSteps[i] = pt.StepInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pt *PictureTask) Run() {
|
||||
pt.setStatus(StatusRunning)
|
||||
|
||||
// 初始化文件处理
|
||||
imageAPI, _ := api.NewImageAPI(pt.Path)
|
||||
|
||||
// 获取基础信息
|
||||
err := imageAPI.GetExif()
|
||||
// 步骤1: 初始化ImageAPI
|
||||
pt.updateStep(StepInitializing, 0.0, 0.05)
|
||||
imageAPI, err := api.NewImageAPI(pt.Path)
|
||||
if err != nil {
|
||||
pt.setStepError(fmt.Errorf("初始化ImageAPI失败: %w", err))
|
||||
return
|
||||
}
|
||||
pt.completeStep()
|
||||
|
||||
// 获取原图路径
|
||||
|
||||
// 获取压缩图片路径
|
||||
|
||||
imageAPI.GetImage()
|
||||
return
|
||||
|
||||
// 分割 Hash 路径
|
||||
//utils.HashUtils.HashThumbPath(config.CONFIG.AppDir, hash)
|
||||
|
||||
// 获取 exif
|
||||
// 保存到数据库
|
||||
// 步骤2: 验证图像格式
|
||||
pt.updateStep(StepValidating, 0.0, 0.10)
|
||||
if !imageAPI.IsSupported() {
|
||||
pt.setStepError(fmt.Errorf("不支持的图像格式: %s", imageAPI.GetFormat()))
|
||||
return
|
||||
}
|
||||
pt.completeStep()
|
||||
|
||||
// 步骤3: 读取文件和计算哈希
|
||||
pt.updateStep(StepReadingFile, 0.0, 0.15)
|
||||
pt.waitIfPaused()
|
||||
data, err := os.ReadFile(pt.Path)
|
||||
if err != nil {
|
||||
pt.setError(err)
|
||||
logger.Error(
|
||||
"文件读取失败!",
|
||||
zap.String("filename", pt.Path),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pt.ImageBuf = data
|
||||
pt.Hash = calcHash(data)
|
||||
pt.updateStep(StepCalculatingHash, 0.0, 0.20)
|
||||
pt.Hash = imageAPI.GetHash()
|
||||
pt.completeStep()
|
||||
|
||||
// 步骤4-6: 图像处理(EXIF提取、格式转换、缩略图生成)
|
||||
pt.updateStep(StepExtractingExif, 0.0, 0.30)
|
||||
pt.waitIfPaused()
|
||||
compressed, err := compressImage(data)
|
||||
if err != nil {
|
||||
pt.setError(err)
|
||||
|
||||
// 使用 img_api.go 的统一处理方法
|
||||
if err := imageAPI.ProcessImage(); err != nil {
|
||||
pt.setStepError(fmt.Errorf("图像处理失败: %w", err))
|
||||
return
|
||||
}
|
||||
pt.ImageBuf = compressed
|
||||
|
||||
// 更新进度到图像处理完成
|
||||
pt.updateStep(StepGeneratingThumbnails, 1.0, 0.85)
|
||||
pt.completeStep()
|
||||
|
||||
// 步骤7: 保存到数据库
|
||||
pt.updateStep(StepSavingToDatabase, 0.0, 0.90)
|
||||
pt.waitIfPaused()
|
||||
exif, err := readExifViaCmd(pt.Path)
|
||||
if err != nil {
|
||||
pt.setError(err)
|
||||
if err := pt.saveToDatabase(imageAPI); err != nil {
|
||||
pt.setStepError(fmt.Errorf("保存到数据库失败: %w", err))
|
||||
return
|
||||
}
|
||||
pt.ExifData = exif
|
||||
pt.completeStep()
|
||||
|
||||
pt.waitIfPaused()
|
||||
err = saveToDB(pt.Hash, exif)
|
||||
if err != nil {
|
||||
pt.setError(err)
|
||||
return
|
||||
}
|
||||
// 步骤8: 智能分析(异步进行)
|
||||
pt.updateStep(StepIntelligentAnalysis, 0.0, 0.95)
|
||||
go pt.performIntelligentAnalysis(imageAPI)
|
||||
pt.completeStep()
|
||||
|
||||
// 完成
|
||||
pt.updateStep(StepCompleted, 1.0, 1.0)
|
||||
pt.setDone()
|
||||
}
|
||||
|
||||
@@ -163,12 +335,53 @@ func (pt *PictureTask) setDone() {
|
||||
defer pt.mu.Unlock()
|
||||
pt.Status = StatusDone
|
||||
pt.Progress = 1.0
|
||||
now := time.Now()
|
||||
pt.CompletedTime = &now
|
||||
pt.UpdateTime = now
|
||||
}
|
||||
|
||||
func (pt *PictureTask) setStatus(s TaskStatus) {
|
||||
pt.mu.Lock()
|
||||
defer pt.mu.Unlock()
|
||||
pt.Status = s
|
||||
pt.UpdateTime = time.Now()
|
||||
}
|
||||
|
||||
// saveToDatabase 保存图像信息到数据库
|
||||
func (pt *PictureTask) saveToDatabase(imageAPI *api.ImageAPI) error {
|
||||
// TODO: 实现数据库保存逻辑
|
||||
// 这里应该保存到 photos 表和 photo_exif 表
|
||||
|
||||
exifData := imageAPI.GetExifData()
|
||||
hash := imageAPI.GetHash()
|
||||
|
||||
logger.Info("保存图像信息到数据库",
|
||||
zap.String("hash", hash),
|
||||
zap.String("path", pt.Path))
|
||||
|
||||
// 模拟数据库操作
|
||||
if exifData != nil {
|
||||
logger.Debug("EXIF信息",
|
||||
zap.String("make", exifData.Exif.Make),
|
||||
zap.String("model", exifData.Exif.Model),
|
||||
zap.Int("width", exifData.BaseInfo.ImageWidth),
|
||||
zap.Int("height", exifData.BaseInfo.ImageHeight))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performIntelligentAnalysis 执行智能分析
|
||||
func (pt *PictureTask) performIntelligentAnalysis(imageAPI *api.ImageAPI) {
|
||||
// TODO: 实现异步调用Python API进行智能分析
|
||||
// 包括:人脸识别、场景识别、物体识别、OCR文字识别
|
||||
|
||||
//logger.Info("开始智能分析", zap.String("path", pt.Path))
|
||||
|
||||
// 这里可以调用 Python API 或其他 AI 服务
|
||||
// 分析结果应该更新到数据库的 has_face, has_object, has_scene, has_text 字段
|
||||
|
||||
//logger.Debug("智能分析完成", zap.String("path", pt.Path))
|
||||
}
|
||||
|
||||
// --- ImgTaskManager ---
|
||||
@@ -186,20 +399,100 @@ type ImgTaskManager struct {
|
||||
}
|
||||
|
||||
func NewImgTaskManager(concurrency int) *ImgTaskManager {
|
||||
taskConfig := config.GetTaskConfig()
|
||||
|
||||
if concurrency <= 0 {
|
||||
concurrency = taskConfig.Concurrency
|
||||
}
|
||||
|
||||
tm := &ImgTaskManager{
|
||||
tasks: make(map[string]*PictureTask),
|
||||
queue: make(chan *PictureTask, 100),
|
||||
queue: make(chan *PictureTask, taskConfig.QueueCapacity),
|
||||
workerPool: make(chan struct{}, concurrency),
|
||||
workerLimit: concurrency,
|
||||
globalPause: make(chan struct{}, 1),
|
||||
globalResume: make(chan struct{}, 1),
|
||||
autoAdjust: true,
|
||||
autoAdjust: taskConfig.AutoAdjust,
|
||||
}
|
||||
|
||||
// 启动工作协程和监控协程
|
||||
go tm.run()
|
||||
go tm.monitorCPU()
|
||||
go tm.monitorSystem()
|
||||
|
||||
logger.Info("任务管理器已启动",
|
||||
zap.Int("concurrency", concurrency),
|
||||
zap.Int("queue_capacity", cap(tm.queue)),
|
||||
zap.Bool("auto_adjust", tm.autoAdjust))
|
||||
|
||||
return tm
|
||||
}
|
||||
|
||||
// 全局任务管理器实例
|
||||
var (
|
||||
globalTaskManager *ImgTaskManager
|
||||
taskManagerOnce sync.Once
|
||||
)
|
||||
|
||||
// InitGlobalTaskManager 初始化全局任务管理器
|
||||
func InitGlobalTaskManager() {
|
||||
taskManagerOnce.Do(func() {
|
||||
taskConfig := config.GetTaskConfig()
|
||||
|
||||
if !taskConfig.AutoStart {
|
||||
logger.Info("任务管理器自动启动已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
globalTaskManager = NewImgTaskManager(taskConfig.Concurrency)
|
||||
logger.Info("全局任务管理器已初始化",
|
||||
zap.Int("concurrency", taskConfig.Concurrency),
|
||||
zap.Bool("auto_adjust", taskConfig.AutoAdjust))
|
||||
})
|
||||
}
|
||||
|
||||
// GetGlobalTaskManager 获取全局任务管理器实例
|
||||
func GetGlobalTaskManager() *ImgTaskManager {
|
||||
if globalTaskManager == nil {
|
||||
logger.Warn("全局任务管理器未初始化,正在初始化...")
|
||||
InitGlobalTaskManager()
|
||||
}
|
||||
return globalTaskManager
|
||||
}
|
||||
|
||||
// IsTaskManagerInitialized 检查任务管理器是否已初始化
|
||||
func IsTaskManagerInitialized() bool {
|
||||
return globalTaskManager != nil
|
||||
}
|
||||
|
||||
// ShutdownTaskManager 关闭任务管理器
|
||||
func ShutdownTaskManager() {
|
||||
if globalTaskManager != nil {
|
||||
logger.Info("正在关闭任务管理器...")
|
||||
// 暂停所有任务
|
||||
globalTaskManager.PauseAll()
|
||||
|
||||
// 等待当前任务完成(最多等待30秒)
|
||||
timeout := time.After(30 * time.Second)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
logger.Warn("任务管理器关闭超时,强制退出")
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining := globalTaskManager.RemainingCount()
|
||||
if remaining == 0 {
|
||||
logger.Info("所有任务已完成,任务管理器已关闭")
|
||||
return
|
||||
}
|
||||
logger.Info("等待任务完成", zap.Int("remaining", remaining))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *ImgTaskManager) SetConcurrency(n int) {
|
||||
tm.poolMu.Lock()
|
||||
defer tm.poolMu.Unlock()
|
||||
@@ -291,66 +584,588 @@ func (tm *ImgTaskManager) RemainingCount() int {
|
||||
return len(tm.tasks) - tm.doneCount
|
||||
}
|
||||
|
||||
func (tm *ImgTaskManager) monitorCPU() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
func (tm *ImgTaskManager) monitorSystem() {
|
||||
taskConfig := config.GetTaskConfig()
|
||||
interval := time.Duration(taskConfig.MonitorInterval) * time.Second
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if !tm.autoAdjust {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取系统状态
|
||||
memPercent := getCurrentMemoryUsage()
|
||||
cpuPercent := getCurrentCPUUsage()
|
||||
if cpuPercent > 80 {
|
||||
tm.SetConcurrency(tm.workerLimit / 2)
|
||||
} else if cpuPercent < 40 {
|
||||
tm.SetConcurrency(tm.workerLimit + 1)
|
||||
goroutineCount := runtime.NumGoroutine()
|
||||
|
||||
tm.mu.RLock()
|
||||
queueLen := len(tm.queue)
|
||||
activeWorkers := len(tm.workerPool)
|
||||
totalTasks := len(tm.tasks)
|
||||
tm.mu.RUnlock()
|
||||
|
||||
logger.Debug("系统监控",
|
||||
zap.Int("cpu_percent", cpuPercent),
|
||||
zap.Int("memory_percent", memPercent),
|
||||
zap.Int("goroutines", goroutineCount),
|
||||
zap.Int("queue_length", queueLen),
|
||||
zap.Int("active_workers", activeWorkers),
|
||||
zap.Int("total_tasks", totalTasks),
|
||||
zap.Int("worker_limit", tm.workerLimit))
|
||||
|
||||
// 根据系统负载调整并发数
|
||||
newConcurrency := tm.calculateOptimalConcurrency(cpuPercent, memPercent, queueLen)
|
||||
if newConcurrency != tm.workerLimit {
|
||||
logger.Info("调整并发数",
|
||||
zap.Int("old", tm.workerLimit),
|
||||
zap.Int("new", newConcurrency),
|
||||
zap.String("reason", tm.getAdjustmentReason(cpuPercent, memPercent, queueLen)))
|
||||
tm.SetConcurrency(newConcurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟 CPU 使用率函数(替换为真实库,如 gopsutil)
|
||||
// calculateOptimalConcurrency 计算最优并发数
|
||||
func (tm *ImgTaskManager) calculateOptimalConcurrency(cpuPercent, memPercent, queueLen int) int {
|
||||
taskConfig := config.GetTaskConfig()
|
||||
maxConcurrency := taskConfig.MaxConcurrency
|
||||
minConcurrency := taskConfig.MinConcurrency
|
||||
|
||||
current := tm.workerLimit
|
||||
|
||||
// 如果CPU使用率过高,减少并发
|
||||
if cpuPercent > 85 {
|
||||
return max(minConcurrency, current/2)
|
||||
}
|
||||
|
||||
// 如果内存使用率过高,减少并发
|
||||
if memPercent > 90 {
|
||||
return max(minConcurrency, current-1)
|
||||
}
|
||||
|
||||
// 如果队列积压严重且系统负载不高,增加并发
|
||||
if queueLen > 50 && cpuPercent < 60 && memPercent < 70 {
|
||||
return min(maxConcurrency, current+1)
|
||||
}
|
||||
|
||||
// 如果系统负载很低且有任务队列,适度增加并发
|
||||
if queueLen > 0 && cpuPercent < 40 && memPercent < 50 {
|
||||
return min(maxConcurrency, current+1)
|
||||
}
|
||||
|
||||
// 如果没有任务且并发数较高,适度减少
|
||||
if queueLen == 0 && current > minConcurrency {
|
||||
return max(minConcurrency, current-1)
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// getAdjustmentReason 获取调整原因
|
||||
func (tm *ImgTaskManager) getAdjustmentReason(cpuPercent, memPercent, queueLen int) string {
|
||||
if cpuPercent > 85 {
|
||||
return "CPU使用率过高"
|
||||
}
|
||||
if memPercent > 90 {
|
||||
return "内存使用率过高"
|
||||
}
|
||||
if queueLen > 50 {
|
||||
return "任务队列积压"
|
||||
}
|
||||
if cpuPercent < 40 && memPercent < 50 && queueLen > 0 {
|
||||
return "系统负载低,有待处理任务"
|
||||
}
|
||||
if queueLen == 0 {
|
||||
return "无待处理任务"
|
||||
}
|
||||
return "系统优化"
|
||||
}
|
||||
|
||||
// getCurrentMemoryUsage 获取当前内存使用率
|
||||
func getCurrentMemoryUsage() int {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
// 简单估算内存使用率(实际项目中可使用 gopsutil 等库)
|
||||
usedMB := bToMb(m.Alloc)
|
||||
totalMB := bToMb(m.Sys)
|
||||
|
||||
if totalMB == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int((usedMB * 100) / totalMB)
|
||||
}
|
||||
|
||||
// bToMb 字节转MB
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
|
||||
// getCurrentCPUUsage 获取当前CPU使用率
|
||||
func getCurrentCPUUsage() int {
|
||||
return runtime.NumGoroutine() * 5 // 简单估算
|
||||
// 简单估算CPU使用率(基于goroutine数量)
|
||||
// 实际项目中应该使用 gopsutil 等库获取真实CPU使用率
|
||||
goroutineCount := runtime.NumGoroutine()
|
||||
cpuCount := runtime.NumCPU()
|
||||
|
||||
// 粗略估算:每个CPU核心处理50个goroutine为满负载
|
||||
usage := (goroutineCount * 100) / (cpuCount * 50)
|
||||
if usage > 100 {
|
||||
usage = 100
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// --- 工具函数 ---
|
||||
|
||||
func fileExists(path string) bool {
|
||||
return utils.FileUtils.Exists(path)
|
||||
}
|
||||
|
||||
func calcHash(data []byte) string {
|
||||
logger.Info("calcHash")
|
||||
h := sha1.Sum(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func compressImage(data []byte) ([]byte, error) {
|
||||
logger.Info("compressImage")
|
||||
time.Sleep(100 * time.Millisecond) // 模拟耗时
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readExifViaCmd(filePath string) (map[string]string, error) {
|
||||
logger.Info("readExifViaCmd")
|
||||
cmd := exec.Command("exiftool", filePath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]string)
|
||||
lines := bytes.Split(out, []byte("\n"))
|
||||
for _, line := range lines {
|
||||
kv := bytes.SplitN(line, []byte(":"), 2)
|
||||
if len(kv) == 2 {
|
||||
key := string(bytes.TrimSpace(kv[0]))
|
||||
val := string(bytes.TrimSpace(kv[1]))
|
||||
result[key] = val
|
||||
// AddBatchTasks 批量添加任务
|
||||
func (tm *ImgTaskManager) AddBatchTasks(paths []string) []string {
|
||||
var taskIDs []string
|
||||
for _, path := range paths {
|
||||
if utils.FileUtils.Exists(path) {
|
||||
taskID := tm.AddTask(path)
|
||||
taskIDs = append(taskIDs, taskID)
|
||||
} else {
|
||||
logger.Warn("文件不存在,跳过", zap.String("path", path))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
||||
logger.Info("批量添加任务完成",
|
||||
zap.Int("total_paths", len(paths)),
|
||||
zap.Int("valid_tasks", len(taskIDs)))
|
||||
|
||||
return taskIDs
|
||||
}
|
||||
|
||||
func saveToDB(hash string, exif map[string]string) error {
|
||||
logger.Info("saveToDB")
|
||||
fmt.Printf("Saving %s to DB with %d exif fields\n", hash, len(exif))
|
||||
return nil
|
||||
// GetTaskProgress 获取任务进度
|
||||
func (tm *ImgTaskManager) GetTaskProgress(id string) (TaskStatus, float64, error) {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
task, ok := tm.tasks[id]
|
||||
if !ok {
|
||||
return "", 0, fmt.Errorf("任务不存在: %s", id)
|
||||
}
|
||||
|
||||
task.mu.Lock()
|
||||
defer task.mu.Unlock()
|
||||
|
||||
return task.Status, task.Progress, task.Error
|
||||
}
|
||||
|
||||
// TaskDetail 任务详细信息
|
||||
type TaskDetail struct {
|
||||
ID string `json:"id"` // 任务ID
|
||||
Path string `json:"path"` // 文件路径
|
||||
Hash string `json:"hash,omitempty"` // 文件哈希
|
||||
Status TaskStatus `json:"status"` // 任务状态
|
||||
Progress float64 `json:"progress"` // 总体进度
|
||||
CurrentStep TaskStep `json:"current_step"` // 当前步骤
|
||||
StepInfo TaskStepInfo `json:"step_info"` // 当前步骤信息
|
||||
AllSteps []TaskStepInfo `json:"all_steps"` // 所有步骤信息
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
StartTime time.Time `json:"start_time"` // 开始时间
|
||||
UpdateTime time.Time `json:"update_time"` // 最后更新时间
|
||||
CompletedTime *time.Time `json:"completed_time,omitempty"` // 完成时间
|
||||
Duration string `json:"duration"` // 持续时间
|
||||
}
|
||||
|
||||
// GetTaskDetail 获取任务详细信息
|
||||
func (tm *ImgTaskManager) GetTaskDetail(id string) (*TaskDetail, error) {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
task, ok := tm.tasks[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("任务不存在: %s", id)
|
||||
}
|
||||
|
||||
task.mu.Lock()
|
||||
defer task.mu.Unlock()
|
||||
|
||||
// 计算持续时间
|
||||
var duration time.Duration
|
||||
if task.CompletedTime != nil {
|
||||
duration = task.CompletedTime.Sub(task.StartTime)
|
||||
} else {
|
||||
duration = time.Since(task.StartTime)
|
||||
}
|
||||
|
||||
var errorStr string
|
||||
if task.Error != nil {
|
||||
errorStr = task.Error.Error()
|
||||
}
|
||||
|
||||
return &TaskDetail{
|
||||
ID: task.ID,
|
||||
Path: task.Path,
|
||||
Hash: task.Hash,
|
||||
Status: task.Status,
|
||||
Progress: task.Progress,
|
||||
CurrentStep: task.CurrentStep,
|
||||
StepInfo: task.StepInfo,
|
||||
AllSteps: task.AllSteps,
|
||||
Error: errorStr,
|
||||
StartTime: task.StartTime,
|
||||
UpdateTime: task.UpdateTime,
|
||||
CompletedTime: task.CompletedTime,
|
||||
Duration: duration.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OverallProgress 整体进度信息
|
||||
type OverallProgress struct {
|
||||
TotalTasks int `json:"total_tasks"` // 总任务数
|
||||
CompletedTasks int `json:"completed_tasks"` // 已完成任务数
|
||||
FailedTasks int `json:"failed_tasks"` // 失败任务数
|
||||
RunningTasks int `json:"running_tasks"` // 正在运行任务数
|
||||
PendingTasks int `json:"pending_tasks"` // 等待中任务数
|
||||
PausedTasks int `json:"paused_tasks"` // 已暂停任务数
|
||||
OverallProgress float64 `json:"overall_progress"` // 整体进度 (0.0-1.0)
|
||||
QueueLength int `json:"queue_length"` // 队列长度
|
||||
ActiveWorkers int `json:"active_workers"` // 活跃工作线程数
|
||||
WorkerLimit int `json:"worker_limit"` // 工作线程限制
|
||||
AutoAdjust bool `json:"auto_adjust"` // 是否自动调整
|
||||
RunningTasksList []TaskDetail `json:"running_tasks_list"` // 正在运行的任务列表
|
||||
StatusCounts map[TaskStatus]int `json:"status_counts"` // 各状态任务数量
|
||||
}
|
||||
|
||||
// GetOverallProgress 获取整体进度
|
||||
func (tm *ImgTaskManager) GetOverallProgress() *OverallProgress {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
statusCounts := make(map[TaskStatus]int)
|
||||
var totalProgress float64
|
||||
var runningTasks []TaskDetail
|
||||
|
||||
for _, task := range tm.tasks {
|
||||
task.mu.Lock()
|
||||
status := task.Status
|
||||
statusCounts[status]++
|
||||
totalProgress += task.Progress
|
||||
|
||||
// 如果是正在运行的任务,添加到列表中
|
||||
if status == StatusRunning {
|
||||
var duration time.Duration
|
||||
if task.CompletedTime != nil {
|
||||
duration = task.CompletedTime.Sub(task.StartTime)
|
||||
} else {
|
||||
duration = time.Since(task.StartTime)
|
||||
}
|
||||
|
||||
var errorStr string
|
||||
if task.Error != nil {
|
||||
errorStr = task.Error.Error()
|
||||
}
|
||||
|
||||
runningTasks = append(runningTasks, TaskDetail{
|
||||
ID: task.ID,
|
||||
Path: task.Path,
|
||||
Hash: task.Hash,
|
||||
Status: task.Status,
|
||||
Progress: task.Progress,
|
||||
CurrentStep: task.CurrentStep,
|
||||
StepInfo: task.StepInfo,
|
||||
AllSteps: task.AllSteps,
|
||||
Error: errorStr,
|
||||
StartTime: task.StartTime,
|
||||
UpdateTime: task.UpdateTime,
|
||||
CompletedTime: task.CompletedTime,
|
||||
Duration: duration.String(),
|
||||
})
|
||||
}
|
||||
task.mu.Unlock()
|
||||
}
|
||||
|
||||
totalTasks := len(tm.tasks)
|
||||
var overallProgress float64
|
||||
if totalTasks > 0 {
|
||||
overallProgress = totalProgress / float64(totalTasks)
|
||||
}
|
||||
|
||||
return &OverallProgress{
|
||||
TotalTasks: totalTasks,
|
||||
CompletedTasks: statusCounts[StatusDone],
|
||||
FailedTasks: statusCounts[StatusFailed],
|
||||
RunningTasks: statusCounts[StatusRunning],
|
||||
PendingTasks: statusCounts[StatusPending],
|
||||
PausedTasks: statusCounts[StatusPaused],
|
||||
OverallProgress: overallProgress,
|
||||
QueueLength: len(tm.queue),
|
||||
ActiveWorkers: len(tm.workerPool),
|
||||
WorkerLimit: tm.workerLimit,
|
||||
AutoAdjust: tm.autoAdjust,
|
||||
RunningTasksList: runningTasks,
|
||||
StatusCounts: statusCounts,
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveTasksDetails 获取所有活跃任务的详细信息
|
||||
func (tm *ImgTaskManager) GetActiveTasksDetails() []*TaskDetail {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
var activeTasks []*TaskDetail
|
||||
|
||||
for _, task := range tm.tasks {
|
||||
task.mu.Lock()
|
||||
// 只返回未完成的任务
|
||||
if task.Status != StatusDone && task.Status != StatusFailed {
|
||||
var duration time.Duration
|
||||
if task.CompletedTime != nil {
|
||||
duration = task.CompletedTime.Sub(task.StartTime)
|
||||
} else {
|
||||
duration = time.Since(task.StartTime)
|
||||
}
|
||||
|
||||
var errorStr string
|
||||
if task.Error != nil {
|
||||
errorStr = task.Error.Error()
|
||||
}
|
||||
|
||||
activeTasks = append(activeTasks, &TaskDetail{
|
||||
ID: task.ID,
|
||||
Path: task.Path,
|
||||
Hash: task.Hash,
|
||||
Status: task.Status,
|
||||
Progress: task.Progress,
|
||||
CurrentStep: task.CurrentStep,
|
||||
StepInfo: task.StepInfo,
|
||||
AllSteps: task.AllSteps,
|
||||
Error: errorStr,
|
||||
StartTime: task.StartTime,
|
||||
UpdateTime: task.UpdateTime,
|
||||
CompletedTime: task.CompletedTime,
|
||||
Duration: duration.String(),
|
||||
})
|
||||
}
|
||||
task.mu.Unlock()
|
||||
}
|
||||
|
||||
return activeTasks
|
||||
}
|
||||
|
||||
// GetRecentCompletedTasks 获取最近完成的任务(限制数量)
|
||||
func (tm *ImgTaskManager) GetRecentCompletedTasks(limit int) []*TaskDetail {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
var completedTasks []*TaskDetail
|
||||
|
||||
for _, task := range tm.tasks {
|
||||
task.mu.Lock()
|
||||
if task.Status == StatusDone || task.Status == StatusFailed {
|
||||
var duration time.Duration
|
||||
if task.CompletedTime != nil {
|
||||
duration = task.CompletedTime.Sub(task.StartTime)
|
||||
} else {
|
||||
duration = time.Since(task.StartTime)
|
||||
}
|
||||
|
||||
var errorStr string
|
||||
if task.Error != nil {
|
||||
errorStr = task.Error.Error()
|
||||
}
|
||||
|
||||
completedTasks = append(completedTasks, &TaskDetail{
|
||||
ID: task.ID,
|
||||
Path: task.Path,
|
||||
Hash: task.Hash,
|
||||
Status: task.Status,
|
||||
Progress: task.Progress,
|
||||
CurrentStep: task.CurrentStep,
|
||||
StepInfo: task.StepInfo,
|
||||
AllSteps: task.AllSteps,
|
||||
Error: errorStr,
|
||||
StartTime: task.StartTime,
|
||||
UpdateTime: task.UpdateTime,
|
||||
CompletedTime: task.CompletedTime,
|
||||
Duration: duration.String(),
|
||||
})
|
||||
}
|
||||
task.mu.Unlock()
|
||||
}
|
||||
|
||||
// 按完成时间倒序排序,返回最近的
|
||||
if len(completedTasks) > limit && limit > 0 {
|
||||
// 简单截取,实际应该按时间排序
|
||||
completedTasks = completedTasks[:limit]
|
||||
}
|
||||
|
||||
return completedTasks
|
||||
}
|
||||
|
||||
// GetAllTasksStatus 获取所有任务状态概览
|
||||
func (tm *ImgTaskManager) GetAllTasksStatus() map[TaskStatus]int {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
statusCount := make(map[TaskStatus]int)
|
||||
|
||||
for _, task := range tm.tasks {
|
||||
task.mu.Lock()
|
||||
statusCount[task.Status]++
|
||||
task.mu.Unlock()
|
||||
}
|
||||
|
||||
return statusCount
|
||||
}
|
||||
|
||||
// ClearCompletedTasks 清理已完成的任务
|
||||
func (tm *ImgTaskManager) ClearCompletedTasks() int {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
cleared := 0
|
||||
for id, task := range tm.tasks {
|
||||
if task.Status == StatusDone || task.Status == StatusFailed {
|
||||
delete(tm.tasks, id)
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("清理已完成任务", zap.Int("cleared_count", cleared))
|
||||
return cleared
|
||||
}
|
||||
|
||||
// SetAutoAdjust 设置是否启用自动调整
|
||||
func (tm *ImgTaskManager) SetAutoAdjust(enable bool) {
|
||||
tm.autoAdjust = enable
|
||||
logger.Info("自动调整设置", zap.Bool("enabled", enable))
|
||||
}
|
||||
|
||||
// GetStats 获取任务管理器统计信息
|
||||
func (tm *ImgTaskManager) GetStats() map[string]interface{} {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_tasks": len(tm.tasks),
|
||||
"done_count": tm.doneCount,
|
||||
"remaining_count": len(tm.tasks) - tm.doneCount,
|
||||
"queue_length": len(tm.queue),
|
||||
"queue_capacity": cap(tm.queue),
|
||||
"worker_limit": tm.workerLimit,
|
||||
"active_workers": len(tm.workerPool),
|
||||
"auto_adjust": tm.autoAdjust,
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"cpu_cores": runtime.NumCPU(),
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// --- 全局便捷函数 ---
|
||||
|
||||
// AddImageTask 添加图像处理任务 (全局便捷函数)
|
||||
func AddImageTask(imagePath string) string {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
logger.Error("任务管理器未初始化")
|
||||
return ""
|
||||
}
|
||||
return taskManager.AddTask(imagePath)
|
||||
}
|
||||
|
||||
// AddImageTasks 批量添加图像处理任务 (全局便捷函数)
|
||||
func AddImageTasks(imagePaths []string) []string {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
logger.Error("任务管理器未初始化")
|
||||
return nil
|
||||
}
|
||||
return taskManager.AddBatchTasks(imagePaths)
|
||||
}
|
||||
|
||||
// GetImageTaskDetail 获取图像任务详情 (全局便捷函数)
|
||||
func GetImageTaskDetail(taskID string) (*TaskDetail, error) {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
return nil, fmt.Errorf("任务管理器未初始化")
|
||||
}
|
||||
return taskManager.GetTaskDetail(taskID)
|
||||
}
|
||||
|
||||
// GetImageTasksProgress 获取整体任务进度 (全局便捷函数)
|
||||
func GetImageTasksProgress() *OverallProgress {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
return &OverallProgress{
|
||||
TotalTasks: 0,
|
||||
OverallProgress: 0,
|
||||
StatusCounts: make(map[TaskStatus]int),
|
||||
}
|
||||
}
|
||||
return taskManager.GetOverallProgress()
|
||||
}
|
||||
|
||||
// GetActiveImageTasks 获取活跃的图像任务 (全局便捷函数)
|
||||
func GetActiveImageTasks() []*TaskDetail {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
return nil
|
||||
}
|
||||
return taskManager.GetActiveTasksDetails()
|
||||
}
|
||||
|
||||
// PauseAllImageTasks 暂停所有图像任务 (全局便捷函数)
|
||||
func PauseAllImageTasks() {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager != nil {
|
||||
taskManager.PauseAll()
|
||||
logger.Info("所有图像任务已暂停")
|
||||
}
|
||||
}
|
||||
|
||||
// ResumeAllImageTasks 恢复所有图像任务 (全局便捷函数)
|
||||
func ResumeAllImageTasks() {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager != nil {
|
||||
taskManager.ResumeAll()
|
||||
logger.Info("所有图像任务已恢复")
|
||||
}
|
||||
}
|
||||
|
||||
// ClearCompletedImageTasks 清理已完成的图像任务 (全局便捷函数)
|
||||
func ClearCompletedImageTasks() int {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
return 0
|
||||
}
|
||||
return taskManager.ClearCompletedTasks()
|
||||
}
|
||||
|
||||
// GetImageTaskStats 获取任务统计信息 (全局便捷函数)
|
||||
func GetImageTaskStats() map[string]interface{} {
|
||||
taskManager := GetGlobalTaskManager()
|
||||
if taskManager == nil {
|
||||
return map[string]interface{}{
|
||||
"initialized": false,
|
||||
"error": "任务管理器未初始化",
|
||||
}
|
||||
}
|
||||
|
||||
stats := taskManager.GetStats()
|
||||
stats["initialized"] = true
|
||||
return stats
|
||||
}
|
||||
Reference in new issue
Block a user