using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Localization; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.Guids; using Volo.Abp.Localization; using Volo.Abp.Timing; using Volo.Abp.Users; namespace Sozsoft.Mcp; /// /// Gelistirme talebinin Timesheets listesini MCP islemlerinden doldurur. Talep /// get_dev_request / start_dev_request ile acildiginda kullanicinin etkin talebi /// olur; ardindan yapilan her yazma islemi o talebe bir satir ekler. Satir modelin dikkatine /// birakilmaz: aciklama dil anahtarindan Turkce uretilir, sure son etkinlikten bu yana olculur. /// public interface IMcpDevRequestTimesheetRecorder { /// Talebi kullanicinin etkin talebi yapar ve olcumu baslatir. Task BeginAsync(Guid devRequestId); /// /// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi /// degilse) hicbir sey yapmaz. /// Task RecordAsync(string descriptionKey, params object[] args); } public class McpDevRequestTimesheetRecorder( IDistributedCache activityCache, IRepository devRequestRepository, IStringLocalizer localizer, ICurrentUser currentUser, IClock clock, IGuidGenerator guidGenerator, ILogger logger) : IMcpDevRequestTimesheetRecorder, ITransientDependency { /// Timesheet satirlarinin dili; talep ekrani Turkce kullanilir. private const string TimesheetCulture = "tr"; /// Talep ekranindaki tarih kutusunun okudugu bicim. private const string DateFormat = "yyyy-MM-dd"; /// /// Etkin talebin omru bir calisma oturumunu kapsar; suresi dolan kayit, gunler once acilmis /// bir talebe baska bir isin yazilmasini ve sahte uzun sureyi engeller. /// private static readonly TimeSpan ActivityLifetime = TimeSpan.FromHours(12); public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now); public async Task RecordAsync(string descriptionKey, params object[] args) { var activity = await activityCache.GetAsync(ActivityKey()); if (activity is null) return; var devRequest = await devRequestRepository.FindAsync(activity.RequestId); if (devRequest is null) return; JsonArray timesheets; try { timesheets = string.IsNullOrWhiteSpace(devRequest.Timesheets) ? [] : JsonNode.Parse(devRequest.Timesheets) as JsonArray ?? throw new JsonException(); } catch (JsonException) { // Yapilan is (deploy, tablo) zaten tamamlandi; bozuk bir timesheet onu geri almamali. logger.LogWarning("Timesheets of development request {RequestId} could not be read; entry skipped.", devRequest.Id); return; } string description; using (CultureHelper.Use(TimesheetCulture)) { description = localizer[descriptionKey, args]; } var now = clock.Now; var minutes = Math.Max(1, (int)Math.Round((now - activity.At).TotalMinutes)); timesheets.Add(new JsonObject { ["id"] = guidGenerator.Create().ToString(), ["date"] = now.ToString(DateFormat, CultureInfo.InvariantCulture), ["userName"] = currentUser.UserName ?? string.Empty, ["description"] = description, // Ekranin sure kutusu saati ondalik sayi olarak okur. ["duration"] = Math.Round(minutes / 60d, 4) }); devRequest.Timesheets = timesheets.ToJsonString(); await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await StampAsync(devRequest.Id, now); } private string ActivityKey() => currentUser.Id?.ToString() ?? string.Empty; private Task StampAsync(Guid devRequestId, DateTime at) => activityCache.SetAsync(ActivityKey(), new McpDevRequestActivityCacheItem { RequestId = devRequestId, At = at }, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ActivityLifetime }); }