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
};
}
}