using Microsoft.Data.SqlClient; using OrpaonVision.ConfigApp.Infrastructure.Persistence.Options; using OrpaonVision.Core.Annotation; using OrpaonVision.Core.Annotation.Contracts; using OrpaonVision.Core.Abstractions; using OrpaonVision.Core.Enums; using OrpaonVision.Core.Results; using OrpaonVision.Model.Annotation; namespace OrpaonVision.ConfigApp.Infrastructure.Persistence; /// /// 标注任务仓储(SQL Server 最小实现)。 /// public sealed class AnnotationTaskStore : IAnnotationTaskStore { private readonly PersistenceOptions _options; private readonly IAppLogger _logger; /// /// 构造函数。 /// public AnnotationTaskStore(PersistenceOptions options, IAppLogger logger) { _options = options; _logger = logger; } /// public Result SaveOrUpdate(AnnotationTaskDetailDto detail) { if (detail.CvatTaskId <= 0) { return Result.Fail("ANNOTATION_TASK_ID_INVALID", "CVAT 任务 ID 无效。", "必须大于 0。" ); } try { using var connection = CreateOpenConnection(); var model = new AnnotationTaskModel { TaskCode = $"CVAT-{detail.CvatTaskId}", TaskName = detail.TaskName, PlatformType = (int)AnnotationPlatformEnum.Cvat, ExternalTaskId = detail.CvatTaskId.ToString(), DatasetId = 0, LabelSetId = 0, Status = MapTaskStatus(detail.TaskStatus), ProgressPercent = null, SyncStatus = 2, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, CreatedBy = "system", UpdatedBy = "system" }; using var upsertCommand = connection.CreateCommand(); upsertCommand.CommandText = @" MERGE dbo.mdl_annotation_task AS target USING (SELECT @external_task_id AS external_task_id) AS source ON target.external_task_id = source.external_task_id WHEN MATCHED THEN UPDATE SET task_name = @task_name, status = @status, sync_status = @sync_status, updated_at = @updated_at, updated_by = @updated_by, cvat_project_id = @cvat_project_id, item_count = @item_count, task_status_text = @task_status_text, updated_at_utc = @updated_at_utc WHEN NOT MATCHED THEN INSERT ( task_code, task_name, platform_type, external_task_id, dataset_id, label_set_id, status, sync_status, created_at, created_by, updated_at, updated_by, cvat_project_id, item_count, task_status_text, updated_at_utc ) VALUES ( @task_code, @task_name, @platform_type, @external_task_id, @dataset_id, @label_set_id, @status, @sync_status, @created_at, @created_by, @updated_at, @updated_by, @cvat_project_id, @item_count, @task_status_text, @updated_at_utc );"; upsertCommand.Parameters.AddWithValue("@task_code", model.TaskCode); upsertCommand.Parameters.AddWithValue("@task_name", model.TaskName); upsertCommand.Parameters.AddWithValue("@platform_type", model.PlatformType); upsertCommand.Parameters.AddWithValue("@external_task_id", model.ExternalTaskId ?? (object)DBNull.Value); upsertCommand.Parameters.AddWithValue("@dataset_id", model.DatasetId); upsertCommand.Parameters.AddWithValue("@label_set_id", model.LabelSetId); upsertCommand.Parameters.AddWithValue("@status", model.Status); upsertCommand.Parameters.AddWithValue("@sync_status", model.SyncStatus); upsertCommand.Parameters.AddWithValue("@created_at", model.CreatedAt); upsertCommand.Parameters.AddWithValue("@created_by", model.CreatedBy ?? (object)DBNull.Value); upsertCommand.Parameters.AddWithValue("@updated_at", model.UpdatedAt ?? (object)DBNull.Value); upsertCommand.Parameters.AddWithValue("@updated_by", model.UpdatedBy ?? (object)DBNull.Value); upsertCommand.Parameters.AddWithValue("@cvat_project_id", detail.CvatProjectId ?? (object)DBNull.Value); upsertCommand.Parameters.AddWithValue("@item_count", detail.ItemCount); upsertCommand.Parameters.AddWithValue("@task_status_text", detail.TaskStatus); upsertCommand.Parameters.AddWithValue("@updated_at_utc", detail.UpdatedAtUtc ?? (object)DBNull.Value); upsertCommand.ExecuteNonQuery(); return Result.Success("ANNOTATION_TASK_STORE_OK", "标注任务快照已保存。" ); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError("保存标注任务快照失败。", ex, traceId); return Result.FromException(ex, "ANNOTATION_TASK_STORE_FAILED", "保存标注任务快照失败。", traceId); } } /// public Result GetByCvatTaskId(long cvatTaskId) { if (cvatTaskId <= 0) { return Result.Fail("ANNOTATION_TASK_ID_INVALID", "CVAT 任务 ID 无效。", "必须大于 0。" ); } try { using var connection = CreateOpenConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT TOP(1) external_task_id, task_name, cvat_project_id, task_status_text, item_count, updated_at_utc FROM dbo.mdl_annotation_task WHERE external_task_id = @external_task_id"; command.Parameters.AddWithValue("@external_task_id", cvatTaskId.ToString()); using var reader = command.ExecuteReader(); if (!reader.Read()) { return Result.Success(null, message: "未找到对应的任务快照。" ); } var detail = new AnnotationTaskDetailDto { Platform = AnnotationPlatformEnum.Cvat, CvatTaskId = long.TryParse(reader["external_task_id"]?.ToString(), out var taskId) ? taskId : cvatTaskId, TaskName = reader["task_name"]?.ToString() ?? string.Empty, CvatProjectId = reader["cvat_project_id"] is DBNull ? null : Convert.ToInt64(reader["cvat_project_id"]), TaskStatus = reader["task_status_text"]?.ToString() ?? string.Empty, ItemCount = reader["item_count"] is DBNull ? 0 : Convert.ToInt32(reader["item_count"]), UpdatedAtUtc = reader["updated_at_utc"] is DBNull ? null : Convert.ToDateTime(reader["updated_at_utc"]).ToUniversalTime() }; return Result.Success(detail, message: "读取任务快照成功。" ); } catch (Exception ex) { var traceId = Guid.NewGuid().ToString("N"); _logger.LogError("读取标注任务快照失败。", ex, traceId); var failure = Result.FromException(ex, "ANNOTATION_TASK_QUERY_FAILED", "读取标注任务快照失败。", traceId); return Result.FailWithTrace(failure.Code, failure.Message, failure.TraceId ?? traceId, [.. failure.Errors]); } } private SqlConnection CreateOpenConnection() { if (string.IsNullOrWhiteSpace(_options.ConnectionString)) { throw new InvalidOperationException("数据库连接字符串未配置。请设置 Persistence:ConnectionString。"); } var connection = new SqlConnection(_options.ConnectionString); connection.Open(); return connection; } private static int MapTaskStatus(string taskStatus) { return taskStatus.Trim().ToLowerInvariant() switch { "annotation" => 1, "in_progress" => 1, "completed" => 2, _ => 0 }; } }