using System.Threading.Channels; using BMA.EHR.Recruit.Core; using BMA.EHR.Recruit.Services; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using OfficeOpenXml; using System.Net.Http.Headers; using BMA.EHR.Recruit.Data; using EFCore.BulkExtensions; using BMA.EHR.Recruit.Extensions; using BMA.EHR.Recruit.Models.Recruits; using BMA.EHR.Recruit.Requests.Recruits; namespace BMA.EHR.Recruit.Services; public class ImportBackgroundService : BackgroundService { private readonly ImportJobQueue _queue; private readonly ImportJobTracker _tracker; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public ImportBackgroundService( ImportJobQueue queue, ImportJobTracker tracker, IServiceScopeFactory scopeFactory, ILogger logger) { _queue = queue; _tracker = tracker; _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("ImportBackgroundService started."); while (!stoppingToken.IsCancellationRequested) { var job = await _queue.DequeueAsync(stoppingToken); _ = Task.Run(async () => { using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var minioService = scope.ServiceProvider.GetRequiredService(); var recruitService = scope.ServiceProvider.GetRequiredService(); var webHostEnv = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); try { _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running); switch (job.JobType) { case ImportJobType.CandidateFile: await ProcessCandidateFileAsync(context, minioService, webHostEnv, job); break; case ImportJobType.CandidateFileById: await ProcessCandidateFileByIdAsync(context, minioService, recruitService, webHostEnv, job); break; case ImportJobType.ScoreFile: await ProcessScoreFileAsync(context, minioService, recruitService, job); break; case ImportJobType.ResultFile: await ProcessResultFileAsync(context, recruitService, job); break; } _tracker.UpdateStatus(job.JobId, ImportJobStatus.Completed, job.TotalCount); } catch (Exception ex) { logger.LogError(ex, "Import job {JobId} failed: {Message}", job.JobId, ex.Message); _tracker.UpdateStatus(job.JobId, ImportJobStatus.Failed, 0, ex.InnerException?.Message ?? ex.Message); // cleanup minio file on failure if (!string.IsNullOrEmpty(job.ImportDocId)) { try { await minioService.DeleteFileAsync(Guid.Parse(job.ImportDocId)); } catch { } } } finally { // cleanup temp file try { if (System.IO.File.Exists(job.ImportFile)) System.IO.File.Delete(job.ImportFile); } catch { } } }, stoppingToken); } } #region CandidateFile private async Task ProcessCandidateFileAsync(ApplicationDbContext _context, MinIOService _minioService, IWebHostEnvironment _webHostEnv, ImportJobInfo job) { var imported = await _context.RecruitImports.FindAsync(job.RecruitImportId); if (imported == null) throw new Exception("RecruitImport not found"); using var c_package = new ExcelPackage(new FileInfo(job.ImportFile)); for (int i = 0; i < c_package.Workbook.Worksheets.Count; i++) { var workSheet = c_package.Workbook.Worksheets[i]; var totalRows = workSheet.Dimension.Rows; var cols = workSheet.GetHeaderColumns(); int row = 2; int batchCount = 0; const int batchSize = 500; int totalProcessed = 0; var batchRecruits = new List(); var batchEducations = new List(); var batchOccupations = new List(); var batchAddresses = new List(); var batchPayments = new List(); var batchCertificates = new List(); while (row <= totalRows) { var cell1 = workSheet?.Cells[row, 1]?.GetValue(); if (cell1 == "" || cell1 == null) break; var r = new Models.Recruits.Recruit(); r.ExamId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ExamID)]?.GetValue(); r.CitizenId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PersonalID)]?.GetValue(); r.Prefix = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Prefix)]?.GetValue(); r.FirstName = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.FirstName)]?.GetValue(); r.LastName = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.LastName)]?.GetValue(); r.Gendor = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Gender)]?.GetValue(); r.National = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.National)]?.GetValue().IsNull(""); r.Race = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Race)]?.GetValue().IsNull(""); r.Religion = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Religion)]?.GetValue().IsNull(""); r.DateOfBirth = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.DateOfBirth)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")); r.Marry = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Marry)]?.GetValue(); r.Isspecial = "N"; r.CitizenCardIssuer = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PersonalCardIssue)]?.GetValue(); r.CitizenCardExpireDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PersonalCardExpireDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")); r.ApplyDate = (DateTime)workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ApplyDate)]?.GetValue(); r.PositionName = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PositionName)]?.GetValue().IsNull(""); r.PositionType = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PositionType)]?.GetValue().IsNull(""); r.PositionLevel = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PositionLevel)]?.GetValue().IsNull(""); // address var address = new RecruitAddress() { Address = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Address)]?.GetValue() ?? "", Moo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Moo)]?.GetValue() ?? "", Soi = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Soi)]?.GetValue() ?? "", Road = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Road)]?.GetValue() ?? "", District = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.District)]?.GetValue() ?? "", Amphur = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Amphur)]?.GetValue() ?? "", Province = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Province)]?.GetValue() ?? "", ZipCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ZipCode)]?.GetValue() ?? "", Telephone = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Telephone)]?.GetValue() ?? "", Mobile = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Mobile)]?.GetValue() ?? "", Address1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Address1)]?.GetValue() ?? "", Moo1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Moo1)]?.GetValue() ?? "", Soi1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Soi1)]?.GetValue() ?? "", Road1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Road1)]?.GetValue() ?? "", District1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.District1)]?.GetValue() ?? "", Amphur1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Amphur1)]?.GetValue() ?? "", Province1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Province1)]?.GetValue() ?? "", ZipCode1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ZipCode1)]?.GetValue() ?? "", }; // payment var payment = new RecruitPayment() { PaymentId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PaymentID)]?.GetValue() ?? "", CompanyCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CompanyCode)]?.GetValue() ?? "", TextFile = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.TextFile)]?.GetValue() ?? "", BankCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.BankCode)]?.GetValue() ?? "", AccountNumber = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.AccouontNumer)]?.GetValue() ?? "", TransDate = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.TransDate)]?.GetValue() ?? "", TransTime = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.TransTime)]?.GetValue() ?? "", CustomerName = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CustomerName)]?.GetValue() ?? "", RefNo1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.RefNo1)]?.GetValue() ?? "", TermBranch = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.TermBranch)]?.GetValue() ?? "", TellerId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.TellerID)]?.GetValue() ?? "", CreditDebit = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CreditDebit)]?.GetValue() ?? "", PaymentType = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Type)]?.GetValue(), ChequeNo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ChequeNo)]?.GetValue() ?? "", Amount = (decimal)workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Amount)]?.GetValue(), ChqueBankCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ChqBankCode)]?.GetValue() ?? "" }; // occupation var occupation = new RecruitOccupation() { Occupation = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Occupation)]?.GetValue() ?? "", Position = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Position)]?.GetValue() ?? "", Workplace = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Workplace)]?.GetValue() ?? "", Telephone = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.WorkplaceTelephone)]?.GetValue() ?? "", WorkAge = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.WorkAge)]?.GetValue() ?? "", }; // certificate var certificate = new RecruitCertificate() { CertificateNo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateNo)]?.GetValue() ?? "", Description = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateDesc)]?.GetValue() ?? "", IssueDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateIssueDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")), ExpiredDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateExpireDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")) }; var education = new RecruitEducation() { Degree = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Degree)]?.GetValue() ?? "", Major = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Major)]?.GetValue() ?? "", MajorGroupId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.MajorGroupID)]?.GetValue() ?? "", MajorGroupName = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.MajorGroupName)]?.GetValue() ?? "", University = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.University)]?.GetValue() ?? "", GPA = (double)workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.GPA)]?.GetValue(), Specialist = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.SpecialList)]?.GetValue() ?? "", HighDegree = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.HighDegree)]?.GetValue() ?? "", BachelorDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.BachelorDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")) }; r.Addresses.Add(address); r.Payments.Add(payment); r.Occupations.Add(occupation); r.Certificates.Add(certificate); r.Educations.Add(education); r.RecruitImport = imported; batchRecruits.Add(r); batchAddresses.Add(address); batchPayments.Add(payment); batchOccupations.Add(occupation); batchCertificates.Add(certificate); batchEducations.Add(education); row++; batchCount++; totalProcessed++; if (batchCount >= batchSize) { await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); for (int j = 0; j < batchRecruits.Count; j++) { batchAddresses[j].Recruit = batchRecruits[j]; batchPayments[j].Recruit = batchRecruits[j]; batchOccupations[j].Recruit = batchRecruits[j]; batchCertificates[j].Recruit = batchRecruits[j]; batchEducations[j].Recruit = batchRecruits[j]; } await _context.BulkInsertAsync(batchAddresses); await _context.BulkInsertAsync(batchPayments); await _context.BulkInsertAsync(batchOccupations); await _context.BulkInsertAsync(batchCertificates); await _context.BulkInsertAsync(batchEducations); batchRecruits.Clear(); batchAddresses.Clear(); batchPayments.Clear(); batchOccupations.Clear(); batchCertificates.Clear(); batchEducations.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } // Process remaining records if (batchRecruits.Count > 0) { await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); for (int j = 0; j < batchRecruits.Count; j++) { batchAddresses[j].Recruit = batchRecruits[j]; batchPayments[j].Recruit = batchRecruits[j]; batchOccupations[j].Recruit = batchRecruits[j]; batchCertificates[j].Recruit = batchRecruits[j]; batchEducations[j].Recruit = batchRecruits[j]; } await _context.BulkInsertAsync(batchAddresses); await _context.BulkInsertAsync(batchPayments); await _context.BulkInsertAsync(batchOccupations); await _context.BulkInsertAsync(batchCertificates); await _context.BulkInsertAsync(batchEducations); } } job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } #endregion #region CandidateFileById private async Task ProcessCandidateFileByIdAsync(ApplicationDbContext _context, MinIOService _minioService, RecruitService _recruitService, IWebHostEnvironment _webHostEnv, ImportJobInfo job) { var imported = await _context.RecruitImports.FindAsync(job.RecruitImportId); if (imported == null) throw new Exception("RecruitImport not found"); // Save import history using regular SaveChanges (small operation) imported.ImportHostories.Add(new RecruitImportHistory { Description = "นำเข้าข้อมูลผู้สมัครสอบแข่งขัน", CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", }); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); var importId = imported.Id; using var c_package = new ExcelPackage(new FileInfo(job.ImportFile)); for (int i = 0; i < c_package.Workbook.Worksheets.Count; i++) { var workSheet = c_package.Workbook.Worksheets[i]; var totalRows = workSheet.Dimension.Rows; int row = 2; int batchCount = 0; const int batchSize = 500; int totalProcessed = 0; var batchRecruits = new List(); var batchEducations = new List(); var batchOccupations = new List(); var batchAddresses = new List(); var batchPayments = new List(); while (row <= totalRows) { var cell1 = workSheet?.Cells[row, 1]?.GetValue(); if (cell1 == "" || cell1 == null) break; var r = new Models.Recruits.Recruit(); r.ExamId = workSheet?.Cells[row, 1]?.GetValue() ?? ""; r.PositionName = workSheet?.Cells[row, 3]?.GetValue() ?? ""; r.HddPosition = workSheet?.Cells[row, 4]?.GetValue() ?? ""; r.Prefix = workSheet?.Cells[row, 5]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 6]?.GetValue() ?? "" : workSheet?.Cells[row, 5]?.GetValue() ?? ""; r.FirstName = workSheet?.Cells[row, 7]?.GetValue() ?? ""; r.LastName = workSheet?.Cells[row, 8]?.GetValue() ?? ""; r.Gendor = workSheet?.Cells[row, 98]?.GetValue() ?? ""; r.National = workSheet?.Cells[row, 9]?.GetValue() ?? ""; r.Race = ""; r.Religion = workSheet?.Cells[row, 10]?.GetValue() ?? ""; r.DateOfBirth = !string.IsNullOrWhiteSpace(workSheet?.Cells[row, 11]?.GetValue()) ? _recruitService.CheckDateTime(workSheet?.Cells[row, 11]?.GetValue() ?? "", "dd/MM/yyyy") : null; r.CitizenId = workSheet?.Cells[row, 12]?.GetValue() ?? ""; r.typeTest = workSheet?.Cells[row, 13]?.GetValue() ?? ""; r.Marry = ""; r.Isspecial = "N"; r.CitizenCardExpireDate = null; r.ModifiedDate = null; r.ApplyDate = !string.IsNullOrWhiteSpace(workSheet?.Cells[row, 87]?.GetValue()) ? _recruitService.CheckDateTime(workSheet?.Cells[row, 87]?.GetValue() ?? "", "dd/MM/yyyy") : null; r.PositionType = ""; r.PositionLevel = ""; r.CreatedAt = DateTime.Now; r.CreatedUserId = job.UserId ?? ""; r.CreatedFullName = job.FullName ?? "System Administrator"; r.LastUpdatedAt = DateTime.Now; r.LastUpdateUserId = job.UserId ?? ""; r.LastUpdateFullName = job.FullName ?? "System Administrator"; // Store child entities in separate lists for bulk insert var education = new RecruitEducation() { Degree = workSheet?.Cells[row, 18]?.GetValue() ?? "", Major = workSheet?.Cells[row, 19]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 20]?.GetValue() ?? "" : workSheet?.Cells[row, 19]?.GetValue() ?? "", MajorGroupId = "", MajorGroupName = "", University = workSheet?.Cells[row, 21]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 22]?.GetValue() ?? "" : workSheet?.Cells[row, 21]?.GetValue() ?? "", GPA = (double)workSheet?.Cells[row, 26]?.GetValue(), Specialist = "", HighDegree = workSheet?.Cells[row, 27]?.GetValue() ?? "", BachelorDate = !string.IsNullOrWhiteSpace(workSheet?.Cells[row, 25]?.GetValue()) ? _recruitService.CheckDateTime(workSheet?.Cells[row, 25]?.GetValue() ?? "", "dd/MM/yyyy") : null, CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" }; var occupation = new RecruitOccupation() { Occupation = workSheet?.Cells[row, 33]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 34]?.GetValue() ?? "" : workSheet?.Cells[row, 33]?.GetValue() ?? "", Position = workSheet?.Cells[row, 37]?.GetValue() ?? "", Workplace = $"{(workSheet?.Cells[row, 36]?.GetValue() ?? "")} {(workSheet?.Cells[row, 35]?.GetValue() ?? "")}", Telephone = workSheet?.Cells[row, 9999]?.GetValue() ?? "", WorkAge = workSheet?.Cells[row, 9999]?.GetValue() ?? "", CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" }; var address = new RecruitAddress() { Address = $"{(workSheet?.Cells[row, 49]?.GetValue() ?? "")} {(workSheet?.Cells[row, 50]?.GetValue() ?? "")}", Moo = workSheet?.Cells[row, 51]?.GetValue() ?? "", Soi = workSheet?.Cells[row, 52]?.GetValue() ?? "", Road = workSheet?.Cells[row, 53]?.GetValue() ?? "", District = workSheet?.Cells[row, 54]?.GetValue() ?? "", Amphur = workSheet?.Cells[row, 55]?.GetValue() ?? "", Province = workSheet?.Cells[row, 56]?.GetValue() ?? "", ZipCode = (workSheet?.Cells[row, 57]?.GetValue() ?? "").Trim(), Telephone = workSheet?.Cells[row, 58]?.GetValue() ?? "", Mobile = "", Address1 = $"{(workSheet?.Cells[row, 61]?.GetValue() ?? "")} {(workSheet?.Cells[row, 62]?.GetValue() ?? "")}", Moo1 = workSheet?.Cells[row, 63]?.GetValue() ?? "", Soi1 = workSheet?.Cells[row, 64]?.GetValue() ?? "", Road1 = workSheet?.Cells[row, 65]?.GetValue() ?? "", District1 = workSheet?.Cells[row, 66]?.GetValue() ?? "", Amphur1 = workSheet?.Cells[row, 67]?.GetValue() ?? "", Province1 = workSheet?.Cells[row, 68]?.GetValue() ?? "", ZipCode1 = (workSheet?.Cells[row, 69]?.GetValue() ?? "").Trim(), CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" }; var payment = new RecruitPayment() { PaymentId = workSheet?.Cells[row, 104]?.GetValue() ?? "", CompanyCode = workSheet?.Cells[row, 105]?.GetValue() ?? "", TextFile = workSheet?.Cells[row, 106]?.GetValue() ?? "", BankCode = workSheet?.Cells[row, 107]?.GetValue() ?? "", AccountNumber = workSheet?.Cells[row, 108]?.GetValue() ?? "", TransDate = workSheet?.Cells[row, 109]?.GetValue() ?? "", TransTime = workSheet?.Cells[row, 110]?.GetValue() ?? "", CustomerName = workSheet?.Cells[row, 111]?.GetValue() ?? "", RefNo1 = workSheet?.Cells[row, 112]?.GetValue() ?? "", TermBranch = workSheet?.Cells[row, 113]?.GetValue() ?? "", TellerId = workSheet?.Cells[row, 114]?.GetValue() ?? "", CreditDebit = workSheet?.Cells[row, 115]?.GetValue() ?? "", PaymentType = workSheet?.Cells[row, 116]?.GetValue() ?? "", ChequeNo = workSheet?.Cells[row, 117]?.GetValue() ?? "", Amount = (decimal)workSheet?.Cells[row, 118]?.GetValue(), ChqueBankCode = workSheet?.Cells[row, 119]?.GetValue() ?? "", CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" }; r.Educations.Add(education); r.Occupations.Add(occupation); r.Addresses.Add(address); r.Payments.Add(payment); batchRecruits.Add(r); batchEducations.Add(education); batchOccupations.Add(occupation); batchAddresses.Add(address); batchPayments.Add(payment); row++; batchCount++; totalProcessed++; if (batchCount >= batchSize) { // BulkInsert Recruits first (with SetOutputIdentity to get generated Ids) await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); // Assign generated Recruit Id to child entities for (int j = 0; j < batchRecruits.Count; j++) { batchEducations[j].Recruit = batchRecruits[j]; batchOccupations[j].Recruit = batchRecruits[j]; batchAddresses[j].Recruit = batchRecruits[j]; batchPayments[j].Recruit = batchRecruits[j]; } // BulkInsert child entities (no identity output needed) await _context.BulkInsertAsync(batchEducations); await _context.BulkInsertAsync(batchOccupations); await _context.BulkInsertAsync(batchAddresses); await _context.BulkInsertAsync(batchPayments); // Clear all lists for next batch batchRecruits.Clear(); batchEducations.Clear(); batchOccupations.Clear(); batchAddresses.Clear(); batchPayments.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } // Process remaining records if (batchRecruits.Count > 0) { await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); for (int j = 0; j < batchRecruits.Count; j++) { batchEducations[j].Recruit = batchRecruits[j]; batchOccupations[j].Recruit = batchRecruits[j]; batchAddresses[j].Recruit = batchRecruits[j]; batchPayments[j].Recruit = batchRecruits[j]; } await _context.BulkInsertAsync(batchEducations); await _context.BulkInsertAsync(batchOccupations); await _context.BulkInsertAsync(batchAddresses); await _context.BulkInsertAsync(batchPayments); } } job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } #endregion #region ScoreFile private async Task ProcessScoreFileAsync(ApplicationDbContext _context, MinIOService _minioService, RecruitService _recruitService, ImportJobInfo job) { var rec_import = await _context.RecruitImports.AsQueryable() .Include(x => x.ScoreImport) .ThenInclude(x => x.Scores) .Include(x => x.ImportHostories) .FirstOrDefaultAsync(x => x.Id == job.RecruitImportId); if (rec_import == null) throw new Exception("RecruitImport not found"); if (rec_import.ScoreImport != null && rec_import.ScoreImport.Scores != null) { await _context.BulkDeleteAsync(rec_import.ScoreImport.Scores.ToList()); } rec_import.ImportHostories.Add(new RecruitImportHistory { Description = "นำเข้าข้อมูลผลคะแนนสอบ", CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", }); // get doc from minio var doc = await _minioService.UploadFileAsync(new DummyFormFile(job.ImportFile)); var imported = new ScoreImport { Year = rec_import.Year, ImportFile = doc, CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", Scores = new List() }; // Save ScoreImport parent first to get its Id rec_import.ScoreImport = imported; await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); // preload recruits (lightweight - only ExamId) var recruitsDict = await _context.Recruits .Where(x => x.RecruitImport.Id == rec_import.Id) .GroupBy(x => x.ExamId) .Where(g => g.Count() == 1) .Select(g => g.First()) .ToDictionaryAsync(x => x.ExamId, x => x); using var c_package = new ExcelPackage(new FileInfo(job.ImportFile)); for (int i = 0; i < c_package.Workbook.Worksheets.Count; i++) { var workSheet = c_package.Workbook.Worksheets[i]; var cols = workSheet.GetHeaderColumns(); int row = 8; int batchCount = 0; const int batchSize = 500; int totalProcessed = 0; var endRow = workSheet.Dimension.End.Row; var batchScores = new List(); while (row <= endRow) { var cell1 = workSheet?.Cells[row, 1]?.GetValue(); if (cell1 == "" || cell1 == null) break; var r = new RecruitScore(); r.ExamId = workSheet?.Cells[row, 2]?.GetValue(); if (!string.IsNullOrEmpty(r.ExamId) && recruitsDict.TryGetValue(r.ExamId, out var recruit)) { r.CitizenId = workSheet?.Cells[row, 3]?.GetValue()?.Trim(); r.FullA = 200; r.SumA = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 5]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 5].GetValue(), 2); r.PercentageA = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 6]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 6].GetValue(), 2); r.AStatus = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 7]?.GetValue()) ? "" : workSheet?.Cells[row, 7]?.GetValue(); r.SumAB = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 5]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 5].GetValue(), 2); r.ABStatus = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 7]?.GetValue()) ? "" : workSheet?.Cells[row, 7]?.GetValue(); r.FullC = 50; r.SumC = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 8]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 8].GetValue(), 2); r.FullD = 50; r.SumD = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 9]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 9].GetValue(), 2); r.SumCD = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 10]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 10].GetValue(), 2); r.PercentageC = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 11]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 11].GetValue(), 2); r.CStatus = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 12]?.GetValue()) ? "" : workSheet?.Cells[row, 12]?.GetValue(); r.FullScore = 300; r.TotalScore = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 13]?.GetValue()) ? 0.00 : Math.Round(workSheet.Cells[row, 13].GetValue(), 2); var examStatusCol7 = workSheet?.Cells[row, 7]?.GetValue()?.Trim(); var examStatusCol14 = workSheet?.Cells[row, 14]?.GetValue()?.Trim(); r.ExamStatus = examStatusCol7 == "ขาดสอบ" ? "ขส." : examStatusCol14 == "ได้" ? "ผ่าน" : examStatusCol14 == "ตก" ? "ไม่ผ่าน" : "-"; r.RemarkScore = string.IsNullOrWhiteSpace(workSheet?.Cells[row, 15]?.GetValue()) ? string.Empty : workSheet?.Cells[row, 15]?.GetValue(); var examAttr = workSheet?.Cells[row, 16]?.GetValue()?.Trim(); r.ExamAttribute = examAttr == "ผ่าน" ? "มีคุณสมบัติ" : examAttr == "ไม่ผ่าน" ? "ไม่มีคุณสมบัติ" : ""; r.Major = workSheet.Name; r.CreatedAt = DateTime.Now; r.CreatedUserId = job.UserId ?? ""; r.CreatedFullName = job.FullName ?? "System Administrator"; r.LastUpdatedAt = DateTime.Now; r.LastUpdateUserId = job.UserId ?? ""; r.LastUpdateFullName = job.FullName ?? "System Administrator"; r.ScoreImport = imported; batchScores.Add(r); } row++; batchCount++; totalProcessed++; if (batchCount >= batchSize) { await _context.BulkInsertAsync(batchScores); batchScores.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } // Process remaining records if (batchScores.Count > 0) { await _context.BulkInsertAsync(batchScores); } } job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } #endregion #region ResultFile private async Task ProcessResultFileAsync(ApplicationDbContext _context, RecruitService _recruitService, ImportJobInfo job) { var rec_import = await _context.RecruitImports.AsQueryable() .Include(x => x.ScoreImport) .ThenInclude(x => x.Scores) .Include(x => x.ImportHostories) .FirstOrDefaultAsync(x => x.Id == job.RecruitImportId); if (rec_import == null) throw new Exception("RecruitImport not found"); // update old scores if (rec_import.ScoreImport != null && rec_import.ScoreImport.Scores != null) { var oldScores = rec_import.ScoreImport.Scores .Where(x => !string.IsNullOrEmpty(x.Number)) .ToList(); if (oldScores.Count > 0) { foreach (var x in oldScores) { x.Number = string.Empty; x.RemarkExamOrder = string.Empty; } await _context.BulkUpdateAsync(oldScores); } } rec_import.ImportHostories.Add(new RecruitImportHistory { Description = "นำเข้าข้อมูลผลการสอบ", CreatedAt = DateTime.Now, CreatedUserId = job.UserId ?? "", CreatedFullName = job.FullName ?? "System Administrator", LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", }); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); // preload scores - re-query from DB to avoid tracking issues var scoreList = await _context.RecruitScores .Where(s => s.ScoreImport.RecruitImportId == rec_import.Id && !string.IsNullOrEmpty(s.ExamId)) .GroupBy(x => x.ExamId) .Where(g => g.Count() == 1) .Select(g => g.First()) .ToListAsync(); var score = scoreList.ToDictionary(s => s.ExamId!, s => s); // Read from saved file (ResultFile uses stream from Form, but we saved to disk) using var stream = System.IO.File.OpenRead(job.ImportFile); using var c_package = new ExcelPackage(stream); foreach (var workSheet in c_package.Workbook.Worksheets) { int row = 7; int batchCount = 0; const int batchSize = 500; var endRow = workSheet.Dimension.End.Row; var batchUpdates = new List(); while (row <= endRow) { var examId = workSheet?.Cells[row, 2]?.GetValue(); if (string.IsNullOrWhiteSpace(examId)) { row++; continue; } if (score.TryGetValue(examId, out var existingScore)) { existingScore.Number = workSheet?.Cells[row, 1]?.GetValue(); existingScore.RemarkExamOrder = workSheet?.Cells[row, 4]?.GetValue() ?? string.Empty; existingScore.LastUpdatedAt = DateTime.Now; existingScore.LastUpdateUserId = job.UserId ?? ""; existingScore.LastUpdateFullName = job.FullName ?? "System Administrator"; batchUpdates.Add(existingScore); batchCount++; } row++; if (batchCount >= batchSize) { await _context.BulkUpdateAsync(batchUpdates); batchUpdates.Clear(); batchCount = 0; } } // Process remaining records if (batchUpdates.Count > 0) { await _context.BulkUpdateAsync(batchUpdates); } } } #endregion #region Helpers private static int GetColumnIndex(string[] columns, string name, bool partial = false) { try { if (partial) return Array.FindIndex(columns, x => x.Contains(name)) + 1; else return Array.FindIndex(columns, x => x == name) + 1; } catch { return 0; } } #endregion } // Helper class to wrap a file path as IFormFile for MinIO upload internal class DummyFormFile : IFormFile { private readonly string _filePath; private readonly FileInfo _fileInfo; public DummyFormFile(string filePath) { _filePath = filePath; _fileInfo = new FileInfo(filePath); } public string ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; public string ContentDisposition => $"form-data; name=\"file\"; filename=\"{_fileInfo.Name}\""; public IHeaderDictionary Headers => new HeaderDictionary(); public long Length => _fileInfo.Length; public string Name => "file"; public string FileName => _fileInfo.Name; public void CopyTo(Stream target) { using var stream = _fileInfo.OpenRead(); stream.CopyTo(target); } public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) { using var stream = _fileInfo.OpenRead(); await stream.CopyToAsync(target, cancellationToken); } public Stream OpenReadStream() { return _fileInfo.OpenRead(); } }