Compare commits

...

6 commits

Author SHA1 Message Date
afc58b767e Merge branch 'develop' into refactor/handler_org
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m13s
* develop:
  cronjob ส่งข้อมูลผู้เกษียณไปให้ระบบพ้นราชการ #2330
2026-05-05 18:12:18 +07:00
6c1e4a1e42 Optimize handler_org batch writes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 18:11:55 +07:00
harid
93d4857ea1 cronjob ส่งข้อมูลผู้เกษียณไปให้ระบบพ้นราชการ #2330
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m17s
2026-05-05 16:43:47 +07:00
750947f34f 1. เพิ่ม helper สำหรับ build clone rows จาก metadata ของ repository แล้ว pre-generate UUID ให้ parent และ child ล่วงหน้า
2. เปลี่ยน inner clone flow เป็น cloneEmployeeNodeBatch(...) ที่ทำงานเป็นชุด แทนการ save() parent แล้ว save() children ทีละรายการ
3. ใช้ insertInChunks(...) สำหรับ batch insert ของ parent rows และ EmployeePosition rows
4. ใช้ helper เดียวกันซ้ำทุกระดับของ tree (root, child1, child2, child3, child4) เพื่อลด code duplication และคง mapping ของ destination org ids ตาม logic เดิม
2026-05-05 16:38:54 +07:00
e7e4e2075b 1. รวม query_employeePosMaster กับ query_employeeTempPosMaster ให้ดึงแบบขนานด้วย Promise.all
2. ตัด full-table scan ของ ProfileEmployee ออก โดยเปลี่ยนจาก find({ select: ["id"] }) ทั้งตาราง มาเป็น query เฉพาะ current_holderId ที่อ้างถึงจริงในชุดข้อมูล publish
3. เก็บ normalization ของ _orgemployeePosMaster และ _orgemployeeTempPosMaster ไว้หลัง query ชุดเดียวกัน ทำให้ block นี้กระชับขึ้นและลด read cost ที่ไม่จำเป็น
2026-05-05 16:25:06 +07:00
b5c75379ff fixed error and not retry 2026-05-05 15:59:39 +07:00
5 changed files with 698 additions and 496 deletions

View file

@ -19,6 +19,7 @@ import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgContro
import { DateSerializer } from "./interfaces/date-serializer";
import { initWebSocket } from "./services/webSocket";
import { RetirementService } from "./services/RetirementService";
async function main() {
await AppDataSource.initialize();
@ -114,6 +115,17 @@ async function main() {
}
});
// Cron job for posting retirement data to Exprofile - every day at 04:30:00 on the 1st of October
const cronTime_PostRetire = "0 30 4 1 10 *";
cron.schedule(cronTime_PostRetire, async () => {
try {
const retirementService = new RetirementService();
await retirementService.cronjobPostRetireToExprofile();
} catch (error) {
console.error("[Cronjob] Error executing cronjobPostRetireToExprofile:", error);
}
});
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
const server = app.listen(
APP_PORT,

View file

@ -104,6 +104,7 @@ import { PostRetireToExprofile } from "./ExRetirementController";
import { LeaveType } from "../entities/LeaveType";
import { KeycloakAttributeService } from "../services/KeycloakAttributeService";
import { reOrderCommandRecivesAndDelete } from "../services/CommandService";
import { RetirementService } from "../services/RetirementService";
@Route("api/v1/org/command")
@Tags("Command")
@Security("bearerAuth")
@ -1608,8 +1609,7 @@ export class CommandController extends Controller {
return new HttpSuccess();
}
// @Get("XXX")
async cronjobUpdateRetirementStatus(/*@Request() request: RequestWithUser*/) {
async cronjobUpdateRetirementStatus() {
const adminToken = (await getToken()) ?? "";
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
@ -1887,6 +1887,21 @@ export class CommandController extends Controller {
return new HttpSuccess();
}
/**
* API cronjobPostRetireToExprofile
* @summary (Exprofile)
*/
@Get("cronjob/cronjobPostRetireToExprofile")
async runCronjobPostRetireToExprofile() {
try {
const retirementService = new RetirementService();
const result = await retirementService.cronjobPostRetireToExprofile();
return new HttpSuccess(result);
} catch (error: any) {
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, error.message || "เกิดข้อผิดพลาด");
}
}
/**
* API tab4
*

View file

@ -237,16 +237,19 @@ export async function PostRetireToExprofile(
continue;
}
addLogSequence(request, {
action: "request",
status: "error",
description: "unconnected to exprofile api",
request: {
method: "POST",
url: API_URL_BANGKOK + "/importData",
response: JSON.stringify(error),
},
});
// เช็ค request ก่อนเรียก addLogSequence (สำหรับ cronjob ที่ส่ง null)
if (request) {
addLogSequence(request, {
action: "request",
status: "error",
description: "unconnected to exprofile api",
request: {
method: "POST",
url: API_URL_BANGKOK + "/importData",
response: JSON.stringify(error),
},
});
}
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
}

View file

@ -0,0 +1,133 @@
import { AppDataSource } from "../database/data-source";
import { Profile } from "../entities/Profile";
import { PostRetireToExprofile } from "../controllers/ExRetirementController";
import { Between, MoreThanOrEqual } from "typeorm";
const BATCH_SIZE = 100;
const CONCURRENT_PER_BATCH = 10; // ส่ง parallel ทีละ 10 คนในแต่ละ batch
export class RetirementService {
private profileRepository = AppDataSource.getRepository(Profile);
/**
* Cronjob (Exprofile)
* 04:30:00 1
*
* :
* - Query profiles leaveDate = 1 leaveType = "RETIRE"
* - Batch 100 records
* - Concurrent 10 (parallel) batch
* - fail log error
*/
async cronjobPostRetireToExprofile(): Promise<{
success: number;
failed: number;
failedProfiles: Array<{ id: string; name: string; error: string }>;
}> {
const result = {
success: 0,
failed: 0,
failedProfiles: [] as Array<{ id: string; name: string; error: string }>,
};
try {
// หาวันที่ 1 ตุลาคมของปีปัจจุบัน
const now = new Date();
const currentYear = now.getFullYear();
// สร้างวันที่ 1 ตุลาคมของปีปัจจุบัน (เวลา 00:00:00)
const startDate = new Date(currentYear, 9, 1, 0, 0, 0); // Month 9 = October (0-indexed)
const endDate = new Date(currentYear, 9, 1, 23, 59, 59);
// Query profiles ที่ leaveDate อยู่ในวันที่ 1 ตุลาคม และ leaveType = "RETIRE"
const profiles = await this.profileRepository.find({
where: [
{ leaveDate: Between(startDate, endDate), leaveType: "RETIRE" as any },
{ leaveDate: MoreThanOrEqual(startDate), leaveType: "RETIRE" as any },
],
relations: ["posLevel", "posType"],
});
// Filter เอาเฉพาะวันที่ 1 ตุลาคมเท่านั้น
const filteredProfiles = profiles.filter(p => {
if (!p.leaveDate) return false;
const leaveDate = new Date(p.leaveDate);
return (
leaveDate.getFullYear() === currentYear &&
leaveDate.getMonth() === 9 && // October
leaveDate.getDate() === 1
);
});
if (filteredProfiles.length === 0) {
return result;
}
// แบ่ง batch ทีละ 100 records
for (let i = 0; i < filteredProfiles.length; i += BATCH_SIZE) {
const batch = filteredProfiles.slice(i, i + BATCH_SIZE);
// แบ่งเป็น chunk เล็กๆ ทีละ CONCURRENT_PER_BATCH เพื่อส่ง parallel
for (let j = 0; j < batch.length; j += CONCURRENT_PER_BATCH) {
const chunk = batch.slice(j, j + CONCURRENT_PER_BATCH);
// ส่ง parallel ในแต่ละ chunk
await Promise.all(
chunk.map(async (profile) => {
try {
await this.postSingleProfileToExprofile(profile);
result.success++;
} catch (error: any) {
result.failed++;
const errorInfo = {
id: profile.id,
name: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
error: error.message || String(error),
};
result.failedProfiles.push(errorInfo);
}
})
);
}
}
} catch (error: any) {
throw error;
}
return result;
}
/**
* profile Exprofile
*/
private async postSingleProfileToExprofile(profile: Profile): Promise<void> {
if (!profile.leaveDate) {
return;
}
if (!profile.citizenId) {
return;
}
const retireYear = profile.leaveDate.getFullYear();
const retireDate = new Date(profile.leaveDate);
// ส่งไปยัง Exprofile
PostRetireToExprofile(
null,
profile.citizenId,
profile.prefix || "",
profile.firstName || "",
profile.lastName || "",
retireYear.toString(),
profile.position || "",
profile.posType?.posTypeName || "",
profile.posLevel?.posLevelName || "",
retireDate,
profile.org || "",
profile.leaveReason || "เกษียณอายุราชการ"
);
}
}

File diff suppressed because it is too large Load diff