using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FrameworkDAL.CustomCode.GOP; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace FrameworkBLL.GOP { public class GopTargetOperationBLL : IGopTargetOperationBLL { private readonly IGopTargetOperationDAL _Dal; private readonly IQueryExecutor _QueryExecutor; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly ILogger _Logger; public GopTargetOperationBLL( IGopTargetOperationDAL dal, IQueryExecutor queryExecutor, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, ILogger logger) { _Dal = dal; _QueryExecutor = queryExecutor; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _Logger = logger; } public async Task SaveTargetOperation( GopTargetOperationDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { await _Validation.NotEmpty(dto.OperationCode, nameof(dto.OperationCode)); await _Validation.NotEmpty(dto.OperationName, nameof(dto.OperationName)); await _Validation.NotEmpty(dto.BaseUrl, nameof(dto.BaseUrl)); await _Validation.NotEmpty(dto.RelativePath, nameof(dto.RelativePath)); bool isNew = dto.TargetOperationId == 0; var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { if (isNew) { var auto = await _AutoNumber.GetNumberAsync( 1, AUTONUMBERCONSTANT.GOPTARGETOPERATION, loginDTO); dto.TargetOperationId = auto.StartNumber; await _Dal.Save(dto, loginDTO, trans, ct); } else { await _Dal.Update(dto, loginDTO, trans, ct); } await _QueryExecutor.CommitAsync(trans); // Every read path here is CLIENT_LEVEL cached (GetGopTargetOperation by id, // GetGopTargetOperationList, GetGopTargetOperationListByEntityCode) -- without // this, a save is invisible to callers (including EntityViewer's own // GetGopTargetOperationListByEntityCode consumer) until the cache naturally // expires. Found live during Phase 3 FE verification (2026-08-14): a real save // succeeded and persisted correctly, but the re-fetched list didn't show it. await _KeyInvalidate.InvalidateByKey( dto.TargetOperationId, EntityConstant.GOPTARGETOPERATION, CacheKeyLevel.CLIENT_LEVEL, loginDTO); await _KeyInvalidate.InvalidateByKey( loginDTO.ClientId, EntityConstant.GOPTARGETOPERATION, CacheKeyLevel.CLIENT_LEVEL, loginDTO); if (!string.IsNullOrWhiteSpace(dto.EntityCode)) { await _KeyInvalidate.InvalidateByKey( dto.EntityCode, EntityConstant.GOPTARGETOPERATION, CacheKeyLevel.CLIENT_LEVEL, loginDTO); } _Logger.LogInformation( "GopTargetOperation '{Code}' (id={Id}) {Action} by user {UserId}", dto.OperationCode, dto.TargetOperationId, isNew ? "created" : "updated", loginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.TargetOperationId}" : $"{SuccessResponse.UpdateSuccessMessage} {dto.TargetOperationId}"; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } public async Task GetTargetOperation( int targetOperationId, LoginDTO loginDTO, CancellationToken ct = default) => await _Dal.GetById(targetOperationId, loginDTO, ct); public async Task> GetTargetOperationList( LoginDTO loginDTO, CancellationToken ct = default) => await _Dal.GetList(loginDTO, ct); public async Task> GetTargetOperationSelectList( LoginDTO loginDTO, CancellationToken ct = default) => await _Dal.GetSelectList(loginDTO, ct); public async Task> GetTargetOperationListByEntityCode( string entityCode, LoginDTO loginDTO, CancellationToken ct = default) { await _Validation.NotEmpty(entityCode, nameof(entityCode)); return await _Dal.GetListByEntityCode(entityCode, loginDTO, ct); } public async Task DeleteTargetOperation( int targetOperationId, LoginDTO loginDTO, CancellationToken ct = default) { if (targetOperationId <= 0) throw new ArgumentException("TargetOperationId is required."); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _Dal.Delete(targetOperationId, loginDTO, trans, ct); await _QueryExecutor.CommitAsync(trans); _Logger.LogInformation( "GopTargetOperation id={Id} soft-deleted by user {UserId}", targetOperationId, loginDTO.UserId); return SuccessResponse.DeleteSuccessMessage; } catch { await _QueryExecutor.RollbackAsync(trans); throw; } } } }