S1 — LCS — Backend Implementation Blueprint¶
TARGET DESIGN
พิมพ์เขียวสำหรับทีมพัฒนา backend ของ S1 ตาม schema schema.sql · ดู index.md (traceability + mockup→target)
สรุปการตัดสินใจสถาปัตยกรรม (ตอบ: "สร้าง backend ใหม่ หรือใช้ร่วมกับ rlpdjs?")¶
rlpdjs= ไลบรารี ไม่ใช่ backend — package@rlpdjs/observabilityเป็น shared NestJS library (logger / tracing / Prometheus / HTTP client / DB query log) Prisma stub ของมันเป็นpostgresqlเพื่อ generate type ของไลบรารีเท่านั้น README ระบุชัด "consumer apps define their own schema with real models."
แนวทางที่ถูกต้อง:
- สร้าง backend ใหม่ใน
repos/lcs/be(ปัจจุบันเป็น placeholder NestJS ว่าง) — นี่คือ S1 backend จริง (lcs-api.rlpd.go.th) - backend ตัวนี้
npm install @rlpdjs/observabilityเป็น dependency แล้วเรียกใช้ในAppModule(LoggerModule/PrometheusModule/HttpModule/DatabaseModule) — ได้ structured log → ELK ฟรี (NFR-M02) - อย่า เขียนโค้ดแอป S1 ไว้ใน repo
rlpdjs— มันเป็นไลบรารีกลางที่ทุกระบบ (S1…S9) ใช้ร่วม - Schema กับ backend คนละชั้น — schema นี้ (
design_p2/s1_lcs) เป็น contract ฐานข้อมูล ใช้ได้ไม่ว่า backend จะ deploy ที่ไหน
repos/
lcs/
fe/ ← React mockup (มีแล้ว) — contract ของ schema นี้
be/ ← NestJS S1 backend (สร้างใหม่ที่นี่)
└─ import @rlpdjs/observability
└─ Prisma (provider = sqlserver) → schema LCS
rlpdjs/ ← @rlpdjs/observability (ไลบรารีกลาง — ห้ามใส่ business logic ของ S1)
Prisma + MSSQL + observability (สำคัญ)¶
rlpdjs/prisma/schema.prismaมีprovider = "postgresql"แต่เป็น stub ของไลบรารีเอง — ไม่กระทบ consumer- S1 backend ตั้ง Prisma ของตัวเอง ที่
lcs/be/prisma/schema.prisma: - ส่ง
new PrismaClient()เข้าDatabaseModule.forRoot({ prisma })ของ@rlpdjs/observability→ ได้ query log อัตโนมัติ - ทางเลือก: ถ้าทีมถนัด TypeORM/raw มากกว่า Prisma ก็ได้ —
schema.sqlเป็น source of truth ; observability lib รองรับ Prisma เป็นหลัก (DB module) แต่ logger/http/metrics ใช้ได้กับทุก ORM
REQ → Endpoint → Service → ตาราง¶
| REQ | Endpoint (ตัวอย่าง REST) | Service / Logic | ตารางหลัก |
|---|---|---|---|
| 001 | POST /consultant-applications · POST /:id/review · POST /:id/approve |
สร้างคำขอ → ตรวจคุณสมบัติ → DIRECTOR อนุมัติ → สร้าง Consultant + ConsultantTerm (3 ปี) |
ConsultantApplication, Consultant, ConsultantSpecialty, ConsultantProvince, ConsultantTerm, Attachment, ApprovalStep |
| 002 | GET /me/consultant · GET /me/duty |
self-service: สถานะ + วาระ + ตารางเวร + ประวัติ (อ่านจาก JWT sub → Consultant.AppUserId) |
Consultant, ConsultantTerm, DutySlot, DutySession |
| 003 | POST /imports (multipart) · POST /imports/:id/commit |
parse .xls/.xlsx/.csv → validate ราย row → preview error → commit | ImportBatch, ImportError, Consultant/DutyRoster |
| 004 | POST /consultants/:id/revoke · job age75-auto-revoke |
เพิกถอน + เหตุผล + แนบไฟล์ ; cron ตรวจอายุ 75 → auto | ConsultantRevocation, Consultant, Notification, Attachment |
| 005 | POST /users/:id/roles · DELETE /users/:id/roles/:role |
grant/revoke (บังคับ Reason) → mirror SSO + เขียน PermissionLog |
RoleAssignment, PermissionLog, Role |
| 006 | POST /requests · POST /requests/:id/assign |
บันทึกคำขอจากช่องทาง + มอบหมาย (แสดงเฉพาะที่ปรึกษา ACTIVE + ว่างจาก DutySession/ConsultantAvailability) |
ConsultationRequest, RequestAssignment, Attachment |
| 007 | POST /requests/:id/result · POST /:id/qa |
บันทึกผล (versioned) → QA pass/fail → fail = เปิดแก้ + RequestStatusHistory + Notification |
ConsultationResult, QAReview, RequestStatusHistory, Notification |
| 008 | POST /requests/:id/escalate |
ส่ง Web Service → OCIPA (ผ่าน HttpModule ของ rlpdjs) → เก็บ ExternalRefId |
Escalation, ConsultationRequest |
| 009 | POST /me/availability · POST /rosters · POST /duty/:id/cancel |
แจ้งวันว่าง / อัปโหลดเวร / ยกเลิก + ผู้แทน (สถิติ) | ConsultantAvailability, DutyRoster, DutySlot, DutySession, DutyCancellation |
| 010 | POST /compensations/calculate · GET /compensations |
คำนวณ 8 ชม.=1,000 จาก DutySession (scope จังหวัดจาก JWT) → export → SENT_S5 |
CompensationCalculation, CompensationItem, DutySession |
| 011 | GET /dashboard/* · GET /reports/export |
aggregate query (Top 5 topic drill-down, channel, demographic) + export | (query) ConsultationRequest/Channel/ConsultationTopic ; export→AuditLog |
| 012 | GET /public/track/:referenceNo |
public (ไม่ต้อง login) — map Status→PublicStatus ผ่าน SystemConfig |
ConsultationRequest, RequestStatusHistory, SystemConfig |
| 013 | (middleware/interceptor) | NestJS interceptor เขียน AuditLog ทุก mutation (before/after JSON) |
AuditLog, PermissionLog |
Cross-cutting (NestJS pattern)¶
- AuthN: Keycloak JWT guard (ThaiD/AD federate) →
req.user.sub→ upsertAppUser(auto-provision) [OPEN-AUTH] - AuthZ: RBAC guard อ่าน role จาก JWT claim — ไม่เชื่อ
RoleAssignmentในการตัดสินใจ (เป็น mirror เท่านั้น) - Province scope: interceptor ผูก
ProvinceIdจาก JWT → จำกัด query (REQ-010: จังหวัดเห็นเฉพาะตน, ส่วนกลางเห็นหมด) - Audit: global interceptor →
AuditLog(REQ-013) ; การกระทำที่requiresReason(role.update/lawyer.revoke/qa.reject) บังคับ fieldreason - Attachment: upload → object storage (เข้ารหัส AES-256) → เก็บ metadata ใน
Attachment(polymorphic) [NFR-S02] - Validation: class-validator DTO ; เลขบัตร 13 หลัก, รูปแบบ ReferenceNo
- Notification: event-driven →
Notification(in-app) + อีเมล/LINE (ภายหลัง) - Observability: ทุก service ใช้
@rlpdjs/observability(LoggerService/PrometheusService) → trace + metric + structured JSON log → ELK (NFR-M02), Swagger/OpenAPI 3.0 (NFR-M03)
ลำดับงานพัฒนา (suggested)¶
- รัน
schema.sqlบน dev DB (schemaLCS) → generate Prisma models (prisma db pull) หรือเขียน models map@@schema("LCS") - Scaffold
lcs/beNestJS + wire@rlpdjs/observability+ Keycloak guard - Module ตามโดเมน:
consultant/(001-004),auth-admin/(005),request/(006-008,012),roster/(009),compensation/(010),report/(011),audit/(013) - seed lookup (Section 4 ของ schema.sql) → ต่อ frontend
lcs/feแทน mockdata.jsxทีละหน้าจอ - Integration: OCIPA Web Service (008), S5 handoff (010, [OPEN-S5]), ThaiD (auth)
ลำดับ frontend cutover
เริ่มจากหน้าจอที่ mock ตรง schema มากสุด — lawyer-db (Consultant), inbox/create-request (ConsultationRequest), qa-pending (QAReview) — แล้วค่อยทำ roster/compensation ที่ logic ซับซ้อนกว่า