From fb726ca789b61548262b3929aedc9ab8759b290f Mon Sep 17 00:00:00 2001 From: kittapath <> Date: Sat, 11 Oct 2025 18:37:46 +0700 Subject: [PATCH] test --- Controllers/RecruitController.cs | 14 +- Requests/Recruits/RecruitDateRequest.cs | 7 + Services/RecruitService.cs | 278 ++++++++++++++---------- 3 files changed, 173 insertions(+), 126 deletions(-) create mode 100644 Requests/Recruits/RecruitDateRequest.cs diff --git a/Controllers/RecruitController.cs b/Controllers/RecruitController.cs index 728c5b4..5ea2895 100644 --- a/Controllers/RecruitController.cs +++ b/Controllers/RecruitController.cs @@ -2034,8 +2034,8 @@ namespace BMA.EHR.Recruit.Service.Controllers exam_order = x.recruit.RecruitImport != null && x.recruit.RecruitImport.Order != null ? x.recruit.RecruitImport.Order.ToString() : "", - score_year = x.recruit.RecruitImport != null && x.recruit.RecruitImport.Year != null - ? x.recruit.RecruitImport.Year.ToThaiYear().ToString() + score_year = x.recruit.RecruitImport != null && x.recruit.RecruitImport.Year != null + ? x.recruit.RecruitImport.Year.ToThaiYear().ToString() : "", }) .ToListAsync(); @@ -2074,7 +2074,7 @@ namespace BMA.EHR.Recruit.Service.Controllers } else { - header = new + header = new { count = _count, pass = 0, @@ -2083,7 +2083,7 @@ namespace BMA.EHR.Recruit.Service.Controllers other = 0 }; } - + // --------------------------- // 4️. ดึง period @@ -2611,15 +2611,15 @@ namespace BMA.EHR.Recruit.Service.Controllers /// เมื่อโอนคนแข่งขันไปบรรจุสำเร็จ /// ไม่ได้ Login เข้าระบบ /// เมื่อเกิดข้อผิดพลาดในการทำงาน - [HttpGet("placement/{examId:length(36)}")] + [HttpPost("placement/{examId:length(36)}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> UpdateAsyncRecruitToPlacement(Guid examId) + public async Task> UpdateAsyncRecruitToPlacement(Guid examId, [FromBody] RecruitDateRequest req) { try { - await _recruitService.UpdateAsyncRecruitToPlacement(examId); + await _recruitService.UpdateAsyncRecruitToPlacement(examId, req.AccountStartDate); return Success(); } catch (Exception ex) diff --git a/Requests/Recruits/RecruitDateRequest.cs b/Requests/Recruits/RecruitDateRequest.cs new file mode 100644 index 0000000..ab9d2ba --- /dev/null +++ b/Requests/Recruits/RecruitDateRequest.cs @@ -0,0 +1,7 @@ +namespace BMA.EHR.Recruit.Service.Requests.Recruits +{ + public class RecruitDateRequest + { + public DateTime AccountStartDate { get; set; } + } +} diff --git a/Services/RecruitService.cs b/Services/RecruitService.cs index ec16ef0..8fc146b 100644 --- a/Services/RecruitService.cs +++ b/Services/RecruitService.cs @@ -175,7 +175,7 @@ namespace BMA.EHR.Recruit.Service.Services await _minIOService.DeleteFileAsync(doc_id); } - public async Task UpdateAsyncRecruitToPlacement(Guid examId) + public async Task UpdateAsyncRecruitToPlacement(Guid examId, DateTime accountStartDate) { try { @@ -187,19 +187,26 @@ namespace BMA.EHR.Recruit.Service.Services var _placement = await _contextMetadata.Placements.AsQueryable() .FirstOrDefaultAsync(x => x.PlacementType.Name == "สอบแข่งขัน" && x.RefId == recruitImport.Id); - if (_placement != null) - throw new Exception("รอบการสอบนี้ได้ทำการบรรจุไปแล้ว"); + // if (_placement != null) + // throw new Exception("รอบการสอบนี้ได้ทำการบรรจุไปแล้ว"); + + // 🚀 Pre-load all lookup data once + var placementTypesCache = await _contextMetadata.PlacementTypes.ToListAsync(); + var provincesCache = await _contextOrg.province.ToListAsync(); + var districtsCache = await _contextOrg.district.ToListAsync(); + var subDistrictsCache = await _contextOrg.subDistrict.ToListAsync(); + var educationLevelsCache = await _contextOrg.educationLevel.ToListAsync(); var placement = new Placement { Name = recruitImport.Name, RefId = recruitImport.Id, - Round = recruitImport.Order == null ? "" : recruitImport.Order.ToString(), - Year = (int)(recruitImport.Year == null ? 0 : recruitImport.Year), + Round = recruitImport.Order.ToString() ?? "", + Year = recruitImport.Year, Number = await _context.Recruits.AsQueryable().Where(x => x.RecruitImport == recruitImport).CountAsync(), - PlacementType = await _contextMetadata.PlacementTypes.FirstOrDefaultAsync(x => x.Name.Trim().ToUpper().Contains("สอบแข่งขัน")) == null ? await _contextMetadata.PlacementTypes.FirstOrDefaultAsync() : await _contextMetadata.PlacementTypes.FirstOrDefaultAsync(x => x.Name.Trim().ToUpper().Contains("สอบแข่งขัน")), - StartDate = DateTime.Now, - EndDate = DateTime.Now.AddYears(2).AddDays(-1), + PlacementType = placementTypesCache.FirstOrDefault(x => x.Name.Trim().ToUpper().Contains("สอบแข่งขัน")) ?? placementTypesCache.First(), + StartDate = accountStartDate, + EndDate = accountStartDate.AddYears(2).AddDays(-1), CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", CreatedFullName = FullName ?? "", @@ -208,6 +215,8 @@ namespace BMA.EHR.Recruit.Service.Services LastUpdateFullName = FullName ?? "", }; await _contextMetadata.Placements.AddAsync(placement); + + // 🚀 Load all related data with single queries var candidates = await _context.Recruits.AsQueryable() .Include(x => x.Addresses) .Include(x => x.Certificates) @@ -215,47 +224,61 @@ namespace BMA.EHR.Recruit.Service.Services .Include(x => x.Occupations) .Where(x => x.RecruitImport == recruitImport) .ToListAsync(); + + var scoreImport = await _context.ScoreImports.AsQueryable() + .FirstOrDefaultAsync(x => x.RecruitImport == recruitImport); + + var recruitScores = await _context.RecruitScores.AsQueryable() + .Where(x => x.ScoreImport == scoreImport && x.ExamStatus == "ผ่าน") + .ToListAsync(); + + var recruitScoresDict = recruitScores.ToDictionary(x => x.ExamId, x => x); + + // 🚀 Prepare HTTP client once + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "")); + httpClient.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); + + // 🚀 Batch HTTP requests + var orgTasks = candidates.Select(async candidate => + { + var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; + try + { + var response = await httpClient.GetStringAsync(apiUrl); + return new { candidate.CitizenId, org = JsonConvert.DeserializeObject(response) }; + } + catch + { + return new { candidate.CitizenId, org = (dynamic?)null }; + } + }).ToList(); + + var orgResults = await Task.WhenAll(orgTasks); + var orgDict = orgResults.ToDictionary(x => x.CitizenId, x => x.org); + + // 🚀 Prepare batch inserts + var placementProfiles = new List(); + var placementEducations = new List(); + var placementCertificates = new List(); + foreach (var candidate in candidates) { - var IsOfficer = false; - dynamic org = null; - var apiUrl = $"{_configuration["API"]}/org/profile/citizenid/position/{candidate.CitizenId}"; - using (var client = new HttpClient()) - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Replace("Bearer ", "")); - client.DefaultRequestHeaders.Add("api_key", _configuration["API_KEY"]); - var _req = new HttpRequestMessage(HttpMethod.Get, apiUrl); - var _res = await client.SendAsync(_req); - var _result = await _res.Content.ReadAsStringAsync(); - - org = JsonConvert.DeserializeObject(_result); - - if (org == null || org.result == null) - { - IsOfficer = false; - } - else - { - IsOfficer = true; - } - } - var Address = candidate.Addresses.FirstOrDefault() == null ? null : $"{candidate.Addresses.FirstOrDefault().Address}"; - var Moo = candidate.Addresses.FirstOrDefault() == null ? null : $" หมู่ {candidate.Addresses.FirstOrDefault().Moo}"; - var Soi = candidate.Addresses.FirstOrDefault() == null ? null : $" ซอย {candidate.Addresses.FirstOrDefault().Soi}"; - var Road = candidate.Addresses.FirstOrDefault() == null ? null : $" ถนน {candidate.Addresses.FirstOrDefault().Road}"; - var Address1 = candidate.Addresses.FirstOrDefault() == null ? null : $"{candidate.Addresses.FirstOrDefault().Address1}"; - var Moo1 = candidate.Addresses.FirstOrDefault() == null ? null : $" หมู่ {candidate.Addresses.FirstOrDefault().Moo1}"; - var Soi1 = candidate.Addresses.FirstOrDefault() == null ? null : $" ซอย {candidate.Addresses.FirstOrDefault().Soi1}"; - var Road1 = candidate.Addresses.FirstOrDefault() == null ? null : $" ถนน {candidate.Addresses.FirstOrDefault().Road1}"; - var scoreImport = await _context.ScoreImports.AsQueryable() - .FirstOrDefaultAsync(x => x.RecruitImport == recruitImport); - var recruitScore = await _context.RecruitScores.AsQueryable() - .Where(x => x.ScoreImport == scoreImport) - .Where(x => x.ExamId == candidate.ExamId) - .Where(x => x.ExamStatus == "ผ่าน") - .FirstOrDefaultAsync(x => x.ExamId == candidate.ExamId && x.ScoreImport == scoreImport); - if (recruitScore == null) + if (!recruitScoresDict.TryGetValue(candidate.ExamId, out var recruitScore)) continue; + + var org = orgDict.GetValueOrDefault(candidate.CitizenId); + var isOfficer = org?.result != null; + + // 🚀 Cache repeated calculations + var firstAddress = candidate.Addresses?.FirstOrDefault(); + var firstEducation = candidate.Educations?.FirstOrDefault(); + var firstCertificate = candidate.Certificates?.FirstOrDefault(); + var firstOccupation = candidate.Occupations?.FirstOrDefault(); + + var registAddress = BuildAddress(firstAddress?.Address, firstAddress?.Moo, firstAddress?.Soi, firstAddress?.Road); + var currentAddress = BuildAddress(firstAddress?.Address1, firstAddress?.Moo1, firstAddress?.Soi1, firstAddress?.Road1); + var placementProfile = new PlacementProfile { Placement = placement, @@ -272,90 +295,88 @@ namespace BMA.EHR.Recruit.Service.Services DateOfBirth = candidate.DateOfBirth, Relationship = candidate.Marry, CitizenId = candidate.CitizenId, - CitizenProvinceId = _contextOrg.province.FirstOrDefault(x => x.name == candidate.CitizenCardIssuer)?.Id ?? null, + CitizenProvinceId = provincesCache.FirstOrDefault(x => x.name == candidate.CitizenCardIssuer)?.Id, CitizenDate = candidate.CitizenCardExpireDate, - Telephone = candidate?.Addresses?.FirstOrDefault()?.Telephone ?? null, - MobilePhone = candidate?.Addresses?.FirstOrDefault()?.Mobile ?? null, - RegistAddress = $"{Address}{Moo}{Soi}{Road}", - RegistProvinceId = _contextOrg.province.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Province ?? ""))?.Id ?? null, - RegistDistrictId = _contextOrg.district.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Amphur ?? ""))?.Id ?? null, - RegistSubDistrictId = _contextOrg.subDistrict.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.District ?? ""))?.Id ?? null, - RegistZipCode = candidate?.Addresses?.FirstOrDefault()?.ZipCode ?? null, + Telephone = firstAddress?.Telephone, + MobilePhone = firstAddress?.Mobile, + RegistAddress = registAddress, + RegistProvinceId = provincesCache.FirstOrDefault(x => x.name == firstAddress?.Province)?.Id, + RegistDistrictId = districtsCache.FirstOrDefault(x => x.name == firstAddress?.Amphur)?.Id, + RegistSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.District)?.Id, + RegistZipCode = firstAddress?.ZipCode, RegistSame = false, - CurrentAddress = $"{Address1}{Moo1}{Soi1}{Road1}", - CurrentProvinceId = _contextOrg.province.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Province1 ?? ""))?.Id ?? null, - CurrentDistrictId = _contextOrg.district.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.Amphur1 ?? ""))?.Id ?? null, - CurrentSubDistrictId = _contextOrg.subDistrict.FirstOrDefault(x => x.name == (candidate!.Addresses!.FirstOrDefault()!.District1 ?? ""))?.Id ?? null, - CurrentZipCode = candidate?.Addresses?.FirstOrDefault()?.ZipCode1 ?? null, - Marry = candidate?.Marry?.Contains("สมรส") ?? false, - + CurrentAddress = currentAddress, + CurrentProvinceId = provincesCache.FirstOrDefault(x => x.name == firstAddress?.Province1)?.Id, + CurrentDistrictId = districtsCache.FirstOrDefault(x => x.name == firstAddress?.Amphur1)?.Id, + CurrentSubDistrictId = subDistrictsCache.FirstOrDefault(x => x.name == firstAddress?.District1)?.Id, + CurrentZipCode = firstAddress?.ZipCode1, + Marry = candidate.Marry?.Contains("สมรส") ?? false, OccupationPositionType = "other", - OccupationTelephone = candidate?.Occupations?.FirstOrDefault()?.Telephone ?? null, - OccupationPosition = candidate?.Occupations?.FirstOrDefault()?.Position ?? null, - - PointTotalA = recruitScore == null ? null : Convert.ToDouble(recruitScore.FullA), - PointA = recruitScore == null ? null : Convert.ToDouble(recruitScore.SumA), - PointTotalB = recruitScore == null ? null : Convert.ToDouble(recruitScore.FullB), - PointB = recruitScore == null ? null : Convert.ToDouble(recruitScore.SumB), - PointTotalC = recruitScore == null ? null : Convert.ToDouble(recruitScore.FullC), - PointC = recruitScore == null ? null : Convert.ToDouble(recruitScore.SumC), - ExamNumber = recruitScore == null || int.TryParse(recruitScore.Number, out int n) == false ? null : Convert.ToInt32(recruitScore.Number), + OccupationTelephone = firstOccupation?.Telephone, + OccupationPosition = firstOccupation?.Position, + PointTotalA = Convert.ToDouble(recruitScore.FullA), + PointA = Convert.ToDouble(recruitScore.SumA), + PointTotalB = Convert.ToDouble(recruitScore.FullB), + PointB = Convert.ToDouble(recruitScore.SumB), + PointTotalC = Convert.ToDouble(recruitScore.FullC), + PointC = Convert.ToDouble(recruitScore.SumC), + ExamNumber = int.TryParse(recruitScore.Number, out int n) ? n : null, ExamRound = null, IsRelief = false, PlacementStatus = "UN-CONTAIN", - Pass = recruitScore == null ? null : recruitScore.ExamStatus, + Pass = recruitScore.ExamStatus, RemarkHorizontal = "โดยมีเงื่อนไขว่าต้องปฏิบัติงานให้กรุงเทพมหานครเป็นระยะเวลาไม่น้อยกว่า ๕ ปี นับแต่วันที่ได้รับการบรรจุและแต่งตั้ง โดยห้ามโอนไปหน่วยงานหรือส่วนราชการอื่น เว้นเเต่ลาออกจากราชการ", - Amount = org?.result?.amount ?? null, - PositionSalaryAmount = org?.result?.positionSalaryAmount ?? null, - MouthSalaryAmount = org?.result?.mouthSalaryAmount ?? null, + Amount = org?.result?.amount, + PositionSalaryAmount = org?.result?.positionSalaryAmount, + MouthSalaryAmount = org?.result?.mouthSalaryAmount, CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", CreatedFullName = FullName ?? "", LastUpdatedAt = DateTime.Now, LastUpdateUserId = UserId ?? "", LastUpdateFullName = FullName ?? "", - IsOfficer = IsOfficer, - profileId = org?.result?.profileId ?? null, - IsOld = org == null || org.result == null ? false : true, - AmountOld = org?.result?.amount ?? null, - nodeOld = org?.result?.node ?? null, - nodeIdOld = org?.result?.nodeId ?? null, - posmasterIdOld = org?.result?.posmasterId ?? null, - rootOld = org?.result?.root ?? null, - rootIdOld = org?.result?.rootId ?? null, - rootShortNameOld = org?.result?.rootShortName ?? null, - child1Old = org?.result?.child1 ?? null, - child1IdOld = org?.result?.child1Id ?? null, - child1ShortNameOld = org?.result?.child1ShortName ?? null, - child2Old = org?.result?.child2 ?? null, - child2IdOld = org?.result?.child2Id ?? null, - child2ShortNameOld = org?.result?.child2ShortName ?? null, - child3Old = org?.result?.child3 ?? null, - child3IdOld = org?.result?.child3Id ?? null, - child3ShortNameOld = org?.result?.child3ShortName ?? null, - child4Old = org?.result?.child4 ?? null, - child4IdOld = org?.result?.child4Id ?? null, - child4ShortNameOld = org?.result?.child4ShortName ?? null, - orgRevisionIdOld = org?.result?.orgRevisionId ?? null, - posMasterNoOld = org?.result?.posMasterNo ?? null, - positionNameOld = org?.result?.position ?? null, - posTypeIdOld = org?.result?.posTypeId ?? null, - posTypeNameOld = org?.result?.posTypeName ?? null, - posLevelIdOld = org?.result?.posLevelId ?? null, - posLevelNameOld = org?.result?.posLevelName ?? null, + IsOfficer = isOfficer, + profileId = org?.result?.profileId, + IsOld = org?.result != null, + AmountOld = org?.result?.amount, + nodeOld = org?.result?.node, + nodeIdOld = org?.result?.nodeId, + posmasterIdOld = org?.result?.posmasterId, + rootOld = org?.result?.root, + rootIdOld = org?.result?.rootId, + rootShortNameOld = org?.result?.rootShortName, + child1Old = org?.result?.child1, + child1IdOld = org?.result?.child1Id, + child1ShortNameOld = org?.result?.child1ShortName, + child2Old = org?.result?.child2, + child2IdOld = org?.result?.child2Id, + child2ShortNameOld = org?.result?.child2ShortName, + child3Old = org?.result?.child3, + child3IdOld = org?.result?.child3Id, + child3ShortNameOld = org?.result?.child3ShortName, + child4Old = org?.result?.child4, + child4IdOld = org?.result?.child4Id, + child4ShortNameOld = org?.result?.child4ShortName, + orgRevisionIdOld = org?.result?.orgRevisionId, + posMasterNoOld = org?.result?.posMasterNo, + positionNameOld = org?.result?.position, + posTypeIdOld = org?.result?.posTypeId, + posTypeNameOld = org?.result?.posTypeName, + posLevelIdOld = org?.result?.posLevelId, + posLevelNameOld = org?.result?.posLevelName, }; - await _contextMetadata.PlacementProfiles.AddAsync(placementProfile); + placementProfiles.Add(placementProfile); var placementEducation = new PlacementEducation { PlacementProfile = placementProfile, - EducationLevelId = _contextOrg.educationLevel.FirstOrDefault(x => x.name == (candidate!.Educations!.FirstOrDefault()!.HighDegree ?? ""))?.Id ?? null, - EducationLevelName = _contextOrg.educationLevel.FirstOrDefault(x => x.name == (candidate!.Educations!.FirstOrDefault()!.HighDegree ?? ""))?.name ?? null, - Field = candidate?.Educations?.FirstOrDefault()?.Major ?? null, - Gpa = candidate?.Educations?.FirstOrDefault()?.GPA!.ToString() ?? null, - Institute = candidate?.Educations?.FirstOrDefault()?.University ?? null, - Degree = candidate?.Educations?.FirstOrDefault()?.Degree ?? null, - FinishDate = candidate?.Educations?.FirstOrDefault()?.BachelorDate ?? null, + EducationLevelId = educationLevelsCache.FirstOrDefault(x => x.name == firstEducation?.HighDegree)?.Id, + EducationLevelName = educationLevelsCache.FirstOrDefault(x => x.name == firstEducation?.HighDegree)?.name, + Field = firstEducation?.Major, + Gpa = firstEducation?.GPA.ToString(), + Institute = firstEducation?.University, + Degree = firstEducation?.Degree, + FinishDate = firstEducation?.BachelorDate, IsDate = true, CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", @@ -364,15 +385,15 @@ namespace BMA.EHR.Recruit.Service.Services CreatedFullName = FullName ?? "", LastUpdateFullName = FullName ?? "", }; - await _contextMetadata.PlacementEducations.AddAsync(placementEducation); + placementEducations.Add(placementEducation); var placementCertificate = new PlacementCertificate { PlacementProfile = placementProfile, - CertificateNo = candidate?.Certificates?.FirstOrDefault()?.CertificateNo ?? null, - IssueDate = candidate?.Certificates?.FirstOrDefault()?.IssueDate ?? null, - ExpireDate = candidate?.Certificates?.FirstOrDefault()?.ExpiredDate ?? null, - CertificateType = candidate?.Certificates?.FirstOrDefault()?.Description ?? null, + CertificateNo = firstCertificate?.CertificateNo, + IssueDate = firstCertificate?.IssueDate, + ExpireDate = firstCertificate?.ExpiredDate, + CertificateType = firstCertificate?.Description, CreatedAt = DateTime.Now, CreatedUserId = UserId ?? "", LastUpdatedAt = DateTime.Now, @@ -380,15 +401,34 @@ namespace BMA.EHR.Recruit.Service.Services CreatedFullName = FullName ?? "", LastUpdateFullName = FullName ?? "", }; - await _contextMetadata.PlacementCertificates.AddAsync(placementCertificate); - await _contextMetadata.SaveChangesAsync(); + placementCertificates.Add(placementCertificate); } + + // 🚀 Batch insert all records + await _contextMetadata.PlacementProfiles.AddRangeAsync(placementProfiles); + await _contextMetadata.PlacementEducations.AddRangeAsync(placementEducations); + await _contextMetadata.PlacementCertificates.AddRangeAsync(placementCertificates); + + // 🚀 Single SaveChanges at the end + await _contextMetadata.SaveChangesAsync(); + + httpClient.Dispose(); } catch { throw; } } + + private string BuildAddress(string? address, string? moo, string? soi, string? road) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(address)) parts.Add(address); + if (!string.IsNullOrWhiteSpace(moo)) parts.Add($"หมู่ {moo}"); + if (!string.IsNullOrWhiteSpace(soi)) parts.Add($"ซอย {soi}"); + if (!string.IsNullOrWhiteSpace(road)) parts.Add($"ถนน {road}"); + return string.Join(" ", parts); + } public DateTime CheckDateTime(string Date, string Formate) { // ตอนนี้ทำไว้ให้รองรับแค่ "dd/MM/yyyy", "yyyy-MM-dd"