sozsoft-platform/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestTimesheetRecorder.cs

151 lines
5.9 KiB
C#
Raw Permalink Normal View History

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;
2026-09-29 14:18:47 +00:00
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Sozsoft.Mcp;
/// <summary>
/// Gelistirme talebinin Timesheets listesini MCP islemlerinden doldurur. Talep
/// <c>get_dev_request</c> / <c>start_dev_request</c> 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.
/// </summary>
public interface IMcpDevRequestTimesheetRecorder
{
/// <summary>Talebi kullanicinin etkin talebi yapar ve olcumu baslatir.</summary>
Task BeginAsync(Guid devRequestId);
/// <summary>Kullanicinin etkin talebi; talep acilmadiysa ya da suresi dolduysa bos.</summary>
Task<Guid?> GetActiveRequestIdAsync();
/// <summary>
/// Etkin talebe bir satir yazar; etkin talep yoksa (islem bir talep dongusunun parcasi
/// degilse) hicbir sey yapmaz.
/// </summary>
Task RecordAsync(string descriptionKey, params object[] args);
}
public class McpDevRequestTimesheetRecorder(
IDistributedCache<McpDevRequestActivityCacheItem> activityCache,
IRepository<DevRequest, Guid> devRequestRepository,
IStringLocalizer<PlatformResource> localizer,
ICurrentUser currentUser,
IClock clock,
IGuidGenerator guidGenerator,
2026-09-29 14:18:47 +00:00
IUnitOfWorkManager unitOfWorkManager,
ILogger<McpDevRequestTimesheetRecorder> logger) : IMcpDevRequestTimesheetRecorder, ITransientDependency
{
/// <summary>Timesheet satirlarinin dili; talep ekrani Turkce kullanilir.</summary>
private const string TimesheetCulture = "tr";
/// <summary>Talep ekranindaki tarih kutusunun okudugu bicim.</summary>
private const string DateFormat = "yyyy-MM-dd";
/// <summary>
/// Etkin talebin omru bir calisma oturumunu kapsar; suresi dolan kayit, gunler once acilmis
/// bir talebe baska bir isin yazilmasini ve sahte uzun sureyi engeller.
/// </summary>
private static readonly TimeSpan ActivityLifetime = TimeSpan.FromHours(12);
public Task BeginAsync(Guid devRequestId) => StampAsync(devRequestId, clock.Now);
public async Task<Guid?> GetActiveRequestIdAsync() => (await activityCache.GetAsync(ActivityKey()))?.RequestId;
public async Task RecordAsync(string descriptionKey, params object[] args)
{
var activity = await activityCache.GetAsync(ActivityKey());
if (activity is null)
return;
2026-09-29 14:18:47 +00:00
string description;
using (CultureHelper.Use(TimesheetCulture))
{
description = localizer[descriptionKey, args];
}
var now = clock.Now;
var currentUow = unitOfWorkManager.Current;
if (currentUow is null)
{
await WriteAsync(activity, description, now);
return;
}
// Satir yazma araclari talebi Dapper baglantisindaki transaction ile gunceller; o kilit
// UoW bitene kadar surer ve EF'in ayri baglantidan yaptigi guncelleme onu bekleyip zaman
// asimina duser. Timesheet bu yuzden asil is commit edildikten sonra kendi UoW'sinde yazilir.
currentUow.OnCompleted(async () =>
{
try
{
using var uow = unitOfWorkManager.Begin(requiresNew: true);
await WriteAsync(activity, description, now);
await uow.CompleteAsync();
}
catch (Exception ex)
{
// Is commit edildi; timesheet hatasi araci basarisiz gostermemeli.
logger.LogWarning(ex, "Timesheet entry for development request {RequestId} could not be written.", activity.RequestId);
}
});
}
private async Task WriteAsync(McpDevRequestActivityCacheItem activity, string description, DateTime now)
{
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;
}
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 });
}