A:照片任务管理

This commit is contained in:
jimmy committed 2025-08-03 19:50:22 +08:00
1 parent ce89028f4d
commit 6ed3410601
8 files changed
+1508 -165

No files matched your search

+70
View File
@@ -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"]
+1 -1
View File
@@ -14,7 +14,7 @@ app:
# libvips_path: "C:/tools/vips/bin/vips.exe"
logging:
level: "info"
level: "debug"
database:
type: "sqlite"
+394 -52
View File
@@ -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)
}
+52
View File
@@ -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
}
+41
View File
@@ -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
}
+1 -1
View File
@@ -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"`
// 软删除 【查询时会检测删除时间】
+23
View File
@@ -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"`
}
+926 -111
View File
@@ -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
}