Skip to content

9.8 · ERD — Study, Fund, Notification

สาม domain ที่เหลือที่มี data model ซับซ้อนพอควร — study leave (ลาศึกษาต่อ), provident fund (กองทุนสงเคราะห์), notification (ระบบแจ้งเตือน)

เพื่อความกระชับ บทนี้จะ map ERD แต่ละ domain แบบย่อ + จุดสำคัญที่ต่างจาก pattern ทั่วไป


Study Leave — ลาศึกษาต่อ

Domain ที่ซับซ้อนที่สุดอันดับ 3 รองจาก WDHospital — เพราะมี relation มาก (8 children tables)

Tables

ตารางPKหน้าที่
StudyLeaveRequeststudyLeaveRequestId UUIDคำขอลาศึกษาหลัก
StudyLeaveDetailUUIDรายละเอียดช่วงเวลาการลา
StudyExtendUUIDขอขยายเวลาศึกษา
StudyEducationFeeUUIDค่าเล่าเรียนที่ขอเบิก
StudyEducationFeeDetailsUUIDรายละเอียดค่าเล่าเรียน (itemized)
StudyScholarshipUUIDทุนการศึกษาที่ได้รับ
StudyScholarshipDetailUUIDรายละเอียดทุน
StudyStrategyUUIDยุทธศาสตร์องค์กรที่ผูกกับคำขอ
StrategyUUIDmaster data ของยุทธศาสตร์
GoalsUUIDเป้าหมายที่ผูกกับคำขอ
OutputUUIDผลลัพธ์ที่คาดว่าจะได้
StudyLearnRecordUUIDประวัติการเรียน
StudyAttachmentUUIDเอกสารแนบ
StudyHistoryUUIDaudit trail
StudyFeeAttachmentsUUIDไฟล์แนบค่าเล่าเรียน

ERD — ASCII vertical flow

                    vtrc schema (db)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  StudyLeaveRequest                                           │
   │  ├── studyLeaveRequestId (PK, UUID)                          │
   │  ├── userId (UUID)                                           │
   │  ├── empCode / empName / position / empSalary (DECIMAL 12,2) │
   │  ├── empGroup / empLevel / departmentName / divisionName     │
   │  ├── organizationCode / organizationName                     │
   │  ├── studyLeaveNo / studyLeaveDocumentNo                     │
   │  ├── studyLeaveDocumentDate (DATE)                           │
   │  ├── studyLeaveResult (TEXT)                                 │
   │  ├── currentRevision (TEXT — version ของคำขอ)                │
   │  ├── studyLeaveStatus (TEXT — ไม่ใช่ ENUM!)                   │
   │  ├── studyLeaveRemark (TEXT)                                 │
   │  ├── isDeleted / isCancel (BOOLEAN)                          │
   │  ├── createdBy / updatedBy (UUID)                            │
   │  ├── requestAt (DATE) / requestBy (TEXT)                     │
   │  └── isAdmin (BOOLEAN)                                       │
   │      │                                                       │
   │      │ hasMany (8 children tables — ทั้งหมดใช้ FK เดียวกัน)    │
   │      ├──► StudyLeaveDetail   (รายละเอียดช่วงเวลา)             │
   │      ├──► StudyExtend        (ขยายเวลา)                      │
   │      ├──► StudyEducationFee  → StudyEducationFeeDetails       │
   │      ├──► StudyScholarship   → StudyScholarshipDetail         │
   │      ├──► StudyStrategy      ← Strategy (master)              │
   │      ├──► StudyLearnRecord   (ประวัติการเรียน)                │
   │      ├──► StudyAttachment    (เอกสารแนบ)                     │
   │      └──► StudyHistory       (audit trail)                    │
   │                                                              │
   └─────────────────────────────────────────────────────────────┘

สิ่งที่น่าสนใจ

  • studyLeaveStatus เป็น TEXT ไม่ใช่ ENUM — ทำให้ status value ไม่ enforce ฝั่ง DB
  • currentRevision เป็น TEXT แต่เก็บเลข version (เช่น "1", "2") — ควรจะเป็น INTEGER
  • ทุก children ใช้ studyLeaveRequestId เป็น FK → join ง่าย แต่ lazy load ทั้งหมดพร้อมกันทำให้ payload ใหญ่
  • ไม่มี Approver relation — study leave ใช้ flow อนุมัติแบบอื่น (อยู่ที่ controller)

Provident Fund — กองทุนสงเคราะห์

Domain ที่จัดการการเปลี่ยนแปลงอัตราเงินสมทบ / ผู้รับผลประโยชน์ / คำขอเข้าร่วมกองทุน

Tables

ตารางPKหน้าที่
FundRequestfundRequestId UUIDคำขอเปลี่ยนแปลงกองทุน
FundApplicationUUIDคำขอเข้าร่วมกองทุน
FundBenefitPersonUUIDผู้รับผลประโยชน์
FundDocumentUUIDเอกสารแนบ
FundRateChangeUUIDเปลี่ยนอัตราเงินสมทบ
FundRequestHistoryUUIDaudit trail
FundDocumentUUIDmaster doc

ERD — ASCII vertical flow

                    vtrc schema (db)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  FundRequest                                                 │
   │  ├── fundRequestId (PK, UUID)                                │
   │  ├── userId (STRING — ผิด type! ควรเป็น UUID)                 │
   │  ├── empCode (STRING)                                        │
   │  ├── organizationName / divisionName                         │
   │  ├── requestType (ENUM 'BENEFIT' | 'RATE' | 'APPLICATION')   │
   │  ├── status (ENUM 'DRAFT' | 'PENDING' | 'SUCCESS' | 'CANCEL')│
   │  ├── fundRequestNo (STRING)                                  │
   │  ├── fundRequestOrder (INTEGER)                              │
   │  ├── remark (STRING 500)                                     │
   │  ├── syncDate / requestDate (DATE)                           │
   │  └── createdBy / updatedBy (STRING — ผิด type)                │
   │      │                                                       │
   │      │ hasMany                                                │
   │      ├──► FundRateChange     (เปลี่ยนอัตรา)                    │
   │      ├──► FundBenefitPerson  (ผู้รับผลประโยชน์)                 │
   │      └──► FundDocument       (เอกสารแนบ)                     │
   │                                                              │
   │  FundApplication (คำขอเข้าร่วม)                                │
   │  ├── fundApplicationId (PK, UUID)                            │
   │  └── ...                                                     │
   │                                                              │
   └─────────────────────────────────────────────────────────────┘

สิ่งที่น่าสนใจ

  • FundRequest.userId เป็น STRING (ควรเป็น UUID) — bug type เดียวกับ MasterApprover
  • requestType ENUM กำหนดว่าเป็นคำขอประเภทไหน — แต่ละ type ใช้ children tables ต่างกัน
  • FundRateChange มี cron job อัปเดตทุกนาที (ดู Volume 3.8)
  • status ENUM มีแค่ 4 ค่า — ไม่มี MOREINFO หรือ REJECT แบบ domain อื่น

Notification — ระบบแจ้งเตือน

Domain ที่ fan-out ไปหลาย target (FCM push, in-app, public device)

Tables

ตารางPKหน้าที่
NotificationnotificationId UUIDข้อความหลัก
NotificationReadUUIDtracking ว่า user อ่านแล้ว
NotificationResultUUIDผลการส่ง (success/fail per device)
NotificationUserSettingUUIDตั้งค่า user (subscribe/unsubscribe)
NotificationPublicDeviceUUIDdevice token สำหรับคนที่ยังไม่ login
NotificationFailUUIDlog การส่ง fail
NotificationRequestUUIDrequest batch ส่ง (queue)
QueueNotificationUUIDqueue item สำหรับ worker

ERD — ASCII vertical flow

                    vtrc schema (db)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  Notification                                                │
   │  ├── notificationId (PK, UUID)                               │
   │  ├── title (STRING) / body (STRING)                          │
   │  ├── userId (UUID — null ถ้า broadcast)                       │
   │  ├── notificationPublicDeviceId (UUID)                       │
   │  ├── notificationGroup (ENUM 'NEWS'|'POLICY'|'SCHEME'|...)   │
   │  ├── typeSend (ENUM 'ALL' | 'INDIVIDUAL')                    │
   │  ├── postId / docId / courseId (UUID — link ไป content)       │
   │  ├── isConfirm / isRead (INTEGER 0|1)                        │
   │  ├── appId (STRING)                                          │
   │  ├── redirect_path (STRING — ตัวพิมพ์เล็ก ผิด convention!)     │
   │  └── updateBy / updateByDivision                             │
   │      │                                                       │
   │      │ belongsTo (3 content types)                            │
   │      ├──► Post       (if postId)                              │
   │      ├──► Document   (if docId)                              │
   │      └──► Course     (if courseId)                            │
   │                                                              │
   │  NotificationRead      (one row per user per notif)          │
   │  ├── notificationReadId (PK)                                 │
   │  ├── notificationId (FK logical)                             │
   │  └── userId / readAt                                         │
   │                                                              │
   │  NotificationResult    (one row per device per notif)        │
   │  ├── notificationResultId (PK)                               │
   │  ├── notificationId                                          │
   │  ├── deviceToken / status (sent|failed)                      │
   │  └── error (TEXT)                                            │
   │                                                              │
   │  NotificationPublicDevice (anonymous FCM tokens)             │
   │  ├── notificationPublicDeviceId (PK)                         │
   │  └── deviceToken (STRING)                                    │
   │                                                              │
   │  NotificationUserSetting (user opt-in/out per group)         │
   │  ├── userId                                                  │
   │  ├── notificationGroup                                       │
   │  └── isEnabled (BOOLEAN)                                     │
   │                                                              │
   └─────────────────────────────────────────────────────────────┘

สิ่งที่น่าสนใจ

  • Notification เป็น "polymorphic content" — อาจจะ reference Post, Document, หรือ Course ก็ได้
  • ไม่มี DB constraint บังคับว่าต้องมีอย่างน้อยหนึ่งในสาม (postId, docId, courseId) → อาจจะเป็น notification ทั่วไปที่ไม่ link content
  • redirect_path ใช้ snake_case ผิด convention (ทุกที่อื่นใช้ camelCase) — เป็น exception เดียว
  • NotificationRead เป็น join table ระหว่าง Notification กับ User — แต่ใช้ logical FK ไม่ใช่ relation

Queue pattern — async send

การส่ง notification ไม่ได้ทำ inline — push ผ่าน queue:

1. Resolver สร้าง Notification row + NotificationRequest row
2. Worker (cron / inline async) อ่าน NotificationRequest
3. สร้าง QueueNotification row per device
4. Worker ส่ง FCM ผ่าน firebase-admin
5. เขียน NotificationResult (success|fail)
6. ถ้า fail → NotificationFail row

รายละเอียดอยู่ใน Volume 3.8 และ controller layer


เคล็ดลับตอนทำงานจริง

Study leave

  • ถ้า query แล้ว payload ใหญ่มาก → ใช้ include เฉพาะ children ที่จำเป็น อย่า lazy load ทั้งหมด
  • studyLeaveStatus เป็น TEXT → grep status value ใน controller เพื่อดูค่าที่เป็นไปได้

Fund

  • อย่า trust FundRequest.userId เป็น UUID — เป็น STRING ต้อง cast
  • FundRateChange อัปเดตทุกนาทีโดย cron → อย่า query แล้ว cache ไว้นาน

Notification

  • ถ้า user ไม่ได้รับ notif → เช็ก NotificationUserSetting.isEnabled ก่อน
  • ถ้า broadcast ไม่ถึง → เช็ก NotificationResult.status ของ device token นั้น
  • redirect_path พิมพ์ผิด case — grep ด้วย redirect_path ไม่ใช่ redirectPath

ไป 8.9 Identity across databases