Skip to content

2way-batch — Core Pipeline

บทนี้อธิบาย lifecycle ของ cron ใน 2way-batch (branch: master) และโค้ด business ที่ยังอยู่แต่ไม่ได้ถูกเรียก

ดูแผนที่ module ที่ 01-repository-map และ persistence ที่ 03-persistence-integrations


Cron lifecycle ภาพรวม

NestJS process boot (main.ts)


ScheduleModule.forRoot()   (app.module.ts:43)


SurveyTaskService constructed
  │  inject EntityManager('main'), LineService, SurveysService, SurveysRepository
  │  survey.task.service.ts:24-30

  │  (side effect) SurveysService ctor → SurveysUtil.initializer
  │  surveys.service.ts:21-27

Every 10 seconds:
  @Cron(CronExpression.EVERY_10_SECONDS)
  handleCron()
       │  survey.task.service.ts:32-37
       ├─► fetchCreateInScope()     ← เรียกจริง (stub log only)
       ├─► // fetchCreateOutScope() ← comment ปิด
       └─► // fetchApproval()       ← comment ปิด
typescript
@Cron(CronExpression.EVERY_10_SECONDS)
handleCron() {
  this.fetchCreateInScope();
  // this.fetchCreateOutScope();
  // this.fetchApproval();
}

แหล่ง: survey.task.service.ts:32-37


Path ที่ live — fetchCreateInScope (stub)

typescript
private async fetchCreateInScope() {
  this.logger.log('fetchCreateInScope.....Test print out for monitor logging.');
}

แหล่ง: survey.task.service.ts:39-41

ไม่มี query DB, ไม่มี LINE push, ไม่มีอัปเดตสถานะ — เป็น monitor heartbeat ที่พิมพ์ log ทุก 10 วินาที

ผลกระทบ ops: log volume สูง (~8640 บรรทัด/วันต่อ instance ถ้าไม่ aggregate) โดยไม่มีงานธุรกิจ — PERF-2WAYBATCH-01

ชื่อ method สื่อว่าเคยตั้งใจทำ "create survey in-scope" notification (status short name ACS ใน SurveysConstant.SHORT_NAME_IN_SCOPE, surveys.constant.ts:5) แต่ implementation ถูกแทนด้วย stub


Path ที่ตาย — fetchApproval (logic ครบแต่ไม่ถูกเรียก)

ตั้งใจ: หาแบบสำรวจที่ HR admin อนุมัติแล้ว → push LINE หาผู้ถูกมอบหมาย → อัปเดต action เป็น assignee received

Query

  1. Subquery: MAX(h2.seq_no) จาก TContentFlowHistory join MContentFlow ที่ short_name = HAP (SHORT_NAME_HR_ADMIN_APPROVED) (survey.task.service.ts:44-50,69)
  2. Main query: distinct content id + topic + start/end date จาก TContent join type/assign/status/history (survey.task.service.ts:56-71)
  3. Filter t.name_en = 'survey' และ status code = HR approved action code (survey.task.service.ts:65-68)

Action code มาจาก SurveysUtil.getActionCodeByName(HR_ADMIN_APPROVED) ซึ่งอ่านจาก in-memory map ที่ initializer โหลดจากตาราง m_status (surveys.util.ts:25-42,125-128) — ถ้า map ยังว่างจะได้ runtime error เมื่อเปิด method นี้

Per-row side effects

for each survey row:
  findAllContentAssignBySurveyId(id)
    for each assignee:
      findLineIDAsEmpCode(empCode)
      line.pushMassage({ lineId, messages })
  surveysService.updateSurveys({
    surveyId, action: ASSIGNEE_RECEIVED,
    contentFlowHis: { empCode: '2wayadm', userName: 'Batch Job2', ... }
  })

แหล่ง: survey.task.service.ts:73-110

ข้อความ LINE template:

ทีม VTRC Support ขออนุญาตแจ้งเตือน การสร้างแบบสำรวจใหม่ในระบบ หัวข้อ $1 เลขงาน $2

แหล่ง: survey.task.service.ts:76 — แทน $1 = topicName, $2 = 'S' + id

จุดอ่อนในโค้ดที่ยังไม่รัน (แต่จะเป็นปัญหาถ้าเปิดกลับ)

ประเด็นรายละเอียดบรรทัด
data.map(async ...) ไม่ awaitfire-and-forget promises73
if (!userLine) return ใน for-ofออกจาก iteration ของ assignee เมื่อไม่มี LINE id81
this.line.pushMassage(dto) ไม่ awaitคืน Observable จาก HttpService — ไม่ subscribe ใน caller89
updateSurveys ไม่ awaitrace กับรอบ cron ถัดไป109
Hardcoded org metadataempCode: '2wayadm', org/divi/dept ชื่อ "ประมวลผล เพื่อส่ง line..."97-106

Path ที่ตาย — fetchCreateOutScope (logic ครบแต่ไม่ถูกเรียก)

ตั้งใจ: หาแบบสำรวจที่สร้างแบบ out-of-scope → แจ้ง HR admin ผ่าน LINE → อัปเดตเป็น HR admin received

Query

คล้าย fetchApproval แต่:

  • flow short_name = ACD (SHORT_NAME_OUT_SCOPE) (survey.task.service.ts:120,135)
  • status code = CREATE_CODE (S00) (survey.task.service.ts:134, surveys.constant.ts:8)

Side effects

  1. dao.findEmployeeCodeOfHrAdmin() → config key list.empCode.hrAdmin (surveys.constant.ts:10, ใช้ที่ survey.task.service.ts:138, repo surveys.repository.ts:587-589)
  2. แยก empCode ด้วย comma → findLineIDAsEmpCodeline.pushMassage ด้วย messages: item.topicName เท่านั้น (survey.task.service.ts:143-151) — ไม่มี template ประโยคเต็มแบบ fetchApproval
  3. updateSurveys action HR_ADMIN_RECEIVED, userName 'Batch Job1' (survey.task.service.ts:154-170)

ปัญหาเดียวกันเรื่อง map / ไม่ await ชั้นนอก — แต่ชั้น arrEmpCodeHrAdmin.map มี await this.line.pushMassage ภายใน callback (151) ซึ่งยังไม่ทำให้ outer map รอครบ


LINE push จาก batch — hardcode IP chain

เมื่อ (ถ้า) เรียก LineService.pushMassage:

typescript
const data = {
  replyToken: body.lineId,
  messages: body.messages,
};

return this.http
  .post('http://10.188.128.126:9340/2way-batch/api/v1/line', data)

แหล่ง: line.service.ts:20-26

SurveyTaskService
  │  LinePushMsgDto { lineId, messages }

LineService.pushMassage
  │  remap → { replyToken: lineId, messages }
  │  POST http://10.188.128.126:9340/2way-batch/api/v1/line

(?) process ที่ฟัง :9340 path /2way-batch/api/v1/line
  │  ตาม nginx: location /2way-batch/ → :9340
  │  แต่ AppModule ปัจจุบันไม่ mount LineController

คาดหวังต่อ: gateway ไป LINE Messaging API

ห้ามสรุป ว่าปลายทางคือ 2way-line-service โดยอัตโนมัติ — 2way-line-service ฟังพอร์ต 8200 ตาม .env.example และ path เป็น /api/v1/Line (ตัว L ใหญ่ใน controller) ไม่ใช่ /2way-batch/api/v1/line บน 9340

การคาดสถานะ HTTP success ใน batch เป็น 201 (line.service.ts:29) — ต้องตรงกับสิ่งที่ปลายทางคืนจริง

pushMassage คืน RxJS Observable จาก this.http.post(...).pipe(...) (line.service.ts:25-41) — caller ใน cron ไม่ได้ subscribe / ไม่ได้แปลงเป็น Promise → แม้ uncomment ก็อาจไม่ยิง request จริงถ้าไม่มี subscriber


updateSurveys ที่ cron จะเรียก (ถ้าเปิด)

SurveysService.updateSurveys (surveys.service.ts:222-291):

  1. ตรวจ action code ผ่าน SurveysUtil.isActionCodeExisting
  2. ถ้าเป็น assignee read → อัปเดต read_date บน t_content_assign
  3. ไม่ใช่ → ตรวจ flow step, กัน duplicate short_name, อัปเดตผ่าน dao.updateSurvey

Batch ส่ง action จาก ASSIGNEE_RECEIVED / HR_ADMIN_RECEIVED (survey.task.service.ts:95,156) — ต้องมี mapping ใน m_status ที่ initializer โหลดไว้แล้ว


HTTP surface ที่ live จริง

Surfaceสถานะ
AppControllerว่าง — ไม่มี route
Controllers ใน announces/employees/surveys/line/uploadไม่ mount (module comment)
Swagger UImount ที่ ${BASE_PATH}docs ถ้า process รัน
Global JwtAuthGuardactive แต่ไม่มี @Public controller live ให้เรียกธุรกิจ
Cronactive ทุก 10 วินาที

สรุป: attack surface ผ่าน HTTP สำหรับ business API ใกล้ศูนย์ในสถานะ wiring ปัจจุบัน — ความเสี่ยงหลักอยู่ที่ resource waste + ถ้ามีคน uncomment modules โดยไม่ review @Public()


เปรียบเทียบกับ cron ใน 2way-api

2way-api มี SurveyTaskModule / AnnounceTaskModule / ComplaintTaskModule ที่ wire ใน AppModule และ schedule เวลาอื่น (ดูเอกสาร 2way-api) — 2way-batch เป็น repo แยกที่ตั้งใจรัน survey batch แต่ปัจจุบันลดเหลือ stub

ห้ามสมมติว่าการเปิด fetchApproval ใน batch จะซ้ำหรือไม่ซ้ำกับ cron ใน 2way-api โดยไม่เทียบ status codes และ environment DB


ลำดับการเปิด logic กลับ (ถ้าทีมต้องการ)

  1. ยืนยันว่า 2way-api cron ไม่ได้ทำงานเดียวกันอยู่แล้วบน DB เดียวกัน
  2. แก้ LineService ให้ชี้ env URL ของ LINE gateway ที่ถูกต้อง (และ mount หรือเรียก 2way-line-service)
  3. แปลง pushMassage ให้ await ได้จริง (Promise/firstValueFrom) แล้วใส่ await ใน cron
  4. ใส่ await / กัน overlapping cron (mutex หรือ schedule ที่ห่างขึ้น)
  5. Uncomment ทีละ method ใน handleCron พร้อมลดความถี่จาก 10 วินาที
  6. ทดสอบบน UAT ด้วย survey ตัวอย่างหนึ่งรายการ

สิ่งที่อยู่นอกขอบเขตบทนี้