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