using System.Text.Json; using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// 统一写入 aidop_action_run_log。 public class AidopActionRunLogWriter : ITransient { private readonly ISqlSugarClient _db; public AidopActionRunLogWriter(ISqlSugarClient db) { _db = db; } /// /// 同租户同动作是否已有进行中的运行(用于排程单飞等互斥)。 /// public async Task HasRunningAsync(string actionCode, long tenantId) { var n = await _db.Ado.GetIntAsync( """ SELECT COUNT(*) FROM aidop_action_run_log WHERE tenant_id = @TenantId AND action_code = @ActionCode AND status = 'RUNNING' """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@ActionCode", actionCode)); return n > 0; } /// /// 取同租户同动作最近一次 SUCCESS 的 id(可作为当前有效 run_id)。 /// public async Task GetLatestSuccessIdAsync(string actionCode, long tenantId) { var id = await _db.Ado.GetLongAsync( """ SELECT id FROM aidop_action_run_log WHERE tenant_id = @TenantId AND action_code = @ActionCode AND status = 'SUCCESS' ORDER BY start_time DESC, id DESC LIMIT 1 """, new List { new("@TenantId", tenantId), new("@ActionCode", actionCode) }); return id > 0 ? id : null; } public async Task StartAsync( string actionCode, long tenantId, string bizType, long? bizId, string? bizNo) { var id = YitIdHelper.NextId(); var now = DateTime.Now; await _db.Ado.ExecuteCommandAsync( """ INSERT INTO aidop_action_run_log (id, tenant_id, action_code, biz_type, biz_id, biz_no, status, start_time, create_time) VALUES (@Id, @TenantId, @ActionCode, @BizType, @BizId, @BizNo, 'RUNNING', @Now, @Now) """, new SugarParameter("@Id", id), new SugarParameter("@TenantId", tenantId), new SugarParameter("@ActionCode", actionCode), new SugarParameter("@BizType", bizType), new SugarParameter("@BizId", bizId ?? (object)DBNull.Value), new SugarParameter("@BizNo", bizNo ?? string.Empty), new SugarParameter("@Now", now)); return id; } public async Task SuccessAsync(long id, string message, object? detail = null) => await FinishAsync(id, "SUCCESS", message, detail); public async Task FailedAsync(long id, string message, object? detail = null) => await FinishAsync(id, "FAILED", message, detail); /// 运行中更新进度(不改 status,不清 end_time)。 public async Task UpdateProgressAsync(long id, string message, object? detail = null) { await _db.Ado.ExecuteCommandAsync( """ UPDATE aidop_action_run_log SET message = @Message, detail_json = @Detail WHERE id = @Id AND status = 'RUNNING' """, new SugarParameter("@Message", Truncate(message, 1000)), new SugarParameter("@Detail", detail is null ? (object)DBNull.Value : JsonSerializer.Serialize(detail)), new SugarParameter("@Id", id)); } /// /// 将超时仍为 RUNNING 的记录标为 FAILED,避免进程崩溃后单飞永久堵死。 /// public async Task FailStaleRunningAsync(string actionCode, long tenantId, TimeSpan olderThan) { var cutoff = DateTime.Now - olderThan; await _db.Ado.ExecuteCommandAsync( """ UPDATE aidop_action_run_log SET status = 'FAILED', message = CONCAT(IFNULL(message, ''), ' [超时未完成,已自动关闭]'), end_time = NOW() WHERE tenant_id = @TenantId AND action_code = @ActionCode AND status = 'RUNNING' AND start_time < @Cutoff """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@ActionCode", actionCode), new SugarParameter("@Cutoff", cutoff)); } private async Task FinishAsync(long id, string status, string message, object? detail) { var now = DateTime.Now; await _db.Ado.ExecuteCommandAsync( """ UPDATE aidop_action_run_log SET status = @Status, message = @Message, detail_json = @Detail, end_time = @Now WHERE id = @Id """, new SugarParameter("@Status", status), new SugarParameter("@Message", Truncate(message, 1000)), new SugarParameter("@Detail", detail is null ? (object)DBNull.Value : JsonSerializer.Serialize(detail)), new SugarParameter("@Now", now), new SugarParameter("@Id", id)); } private static string Truncate(string? s, int max) => string.IsNullOrEmpty(s) || s.Length <= max ? s ?? string.Empty : s[..max]; }