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 | หน้าที่ |
|---|---|---|
StudyLeaveRequest | studyLeaveRequestId UUID | คำขอลาศึกษาหลัก |
StudyLeaveDetail | UUID | รายละเอียดช่วงเวลาการลา |
StudyExtend | UUID | ขอขยายเวลาศึกษา |
StudyEducationFee | UUID | ค่าเล่าเรียนที่ขอเบิก |
StudyEducationFeeDetails | UUID | รายละเอียดค่าเล่าเรียน (itemized) |
StudyScholarship | UUID | ทุนการศึกษาที่ได้รับ |
StudyScholarshipDetail | UUID | รายละเอียดทุน |
StudyStrategy | UUID | ยุทธศาสตร์องค์กรที่ผูกกับคำขอ |
Strategy | UUID | master data ของยุทธศาสตร์ |
Goals | UUID | เป้าหมายที่ผูกกับคำขอ |
Output | UUID | ผลลัพธ์ที่คาดว่าจะได้ |
StudyLearnRecord | UUID | ประวัติการเรียน |
StudyAttachment | UUID | เอกสารแนบ |
StudyHistory | UUID | audit trail |
StudyFeeAttachments | UUID | ไฟล์แนบค่าเล่าเรียน |
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 ฝั่ง DBcurrentRevisionเป็น TEXT แต่เก็บเลข version (เช่น "1", "2") — ควรจะเป็น INTEGER- ทุก children ใช้
studyLeaveRequestIdเป็น FK → join ง่าย แต่ lazy load ทั้งหมดพร้อมกันทำให้ payload ใหญ่ - ไม่มี
Approverrelation — study leave ใช้ flow อนุมัติแบบอื่น (อยู่ที่ controller)
Provident Fund — กองทุนสงเคราะห์
Domain ที่จัดการการเปลี่ยนแปลงอัตราเงินสมทบ / ผู้รับผลประโยชน์ / คำขอเข้าร่วมกองทุน
Tables
| ตาราง | PK | หน้าที่ |
|---|---|---|
FundRequest | fundRequestId UUID | คำขอเปลี่ยนแปลงกองทุน |
FundApplication | UUID | คำขอเข้าร่วมกองทุน |
FundBenefitPerson | UUID | ผู้รับผลประโยชน์ |
FundDocument | UUID | เอกสารแนบ |
FundRateChange | UUID | เปลี่ยนอัตราเงินสมทบ |
FundRequestHistory | UUID | audit trail |
FundDocument | UUID | master 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 เดียวกับ MasterApproverrequestTypeENUM กำหนดว่าเป็นคำขอประเภทไหน — แต่ละ type ใช้ children tables ต่างกันFundRateChangeมี cron job อัปเดตทุกนาที (ดู Volume 3.8)statusENUM มีแค่ 4 ค่า — ไม่มีMOREINFOหรือREJECTแบบ domain อื่น
Notification — ระบบแจ้งเตือน
Domain ที่ fan-out ไปหลาย target (FCM push, in-app, public device)
Tables
| ตาราง | PK | หน้าที่ |
|---|---|---|
Notification | notificationId UUID | ข้อความหลัก |
NotificationRead | UUID | tracking ว่า user อ่านแล้ว |
NotificationResult | UUID | ผลการส่ง (success/fail per device) |
NotificationUserSetting | UUID | ตั้งค่า user (subscribe/unsubscribe) |
NotificationPublicDevice | UUID | device token สำหรับคนที่ยังไม่ login |
NotificationFail | UUID | log การส่ง fail |
NotificationRequest | UUID | request batch ส่ง (queue) |
QueueNotification | UUID | queue 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