ข้ามไปที่เนื้อหา

S2 — Proactive Justice Operation System (PJOS) — Target Schema (Phase 2)

TARGET DESIGN — ไม่ใช่ schema ปัจจุบัน

ออกแบบใหม่ (greenfield) เพื่อ implement requirement จาก SRS S2 v2.0.0 (TOR 7.12.1–7.12.2) · ระบบนี้ยังไม่มี DB จริง — de-facto contract คือ mockup repos/pjos/fe/src/data.jsx + screens-*.jsx (การแปลง mockup → schema สรุปใน Mockup → Target mapping) · DDL เต็ม: schema.sql · ERD · Data Dictionary · 🛠️ Backend Implementation

ทำไมต้องออกแบบใหม่ (greenfield)

S2 เป็นระบบใหม่ ไม่มี legacy ให้ migrate — mockup pjos/fe (React/Vite) เก็บข้อมูลเป็น mock array ใน data.jsx (CASES/mkCase, OFFICERS, UNITS, SERVICE_CENTER_QUEUE, FIELD_RESULTS, PLAN_STEPS, RIGHTS_AREAS …) + ค่าฝังใน screens-*.jsx เท่านั้น pjos/be (NestJS) ยังเป็น placeholder ว่าง งานนี้คือแปลง mock contract นั้นเป็น clean target schema ระดับ production:

  • normalize root field → ราย/junctionmkCase เก็บ gender/age/vulnerableGroup ที่ราก (คิดแบบเหยื่อคนเดียว) แต่ SRS เป็น Multi-victim (REQ-003) + รายงาน "จำนวนราย แยกเพศ/อายุ" (REQ-010) → ย้ายมิติประชากรไป CaseVictim ; victims[]/assignees[] → ตาราง CaseVictim/CaseAssignment
  • เพิ่ม FK + audit + workflow ที่ mockup ไม่มี — FK referential integrity ครบทุก relation, audit cols (CreatedAt/CreatedBy/UpdatedAt/UpdatedBy), workflow state (intake→dedup→case→assign→plan→approve→implement→refer→terminate)
  • trigger gate + แผน versionedCaseTrigger (ต้องการ/ไม่ต้องการ 4 ด้าน) แยกจาก AssistancePlan (เก็บทุก Version เพื่อเทียบย้อนหลัง + loop-back)
  • generic ApprovalAction — 1 ตารางพิจารณาแบบลำดับชั้น ใช้ร่วม 3 flow (รายงานรอบ1 / รอบ2 / แผน) — ดู D-PJO-1 (ตาราง deviation ด้านล่าง)
  • polymorphic Attachment — 1 ตารางแนบไฟล์ใช้ร่วมทุก entity (รูปภาพหลักฐาน ≥50MB)
  • thin-mirror authAppUser/RoleAssignment/Role เป็นเงา SSO ของ S9 Web Portal ไม่เก็บรหัสผ่าน (ดู [OPEN-AUTH])

S2 PK = int IDENTITY (เหมือน S1/S3, ต่างจาก design_p2 D7)

S8/S9 ใช้ uniqueidentifier เพื่อ carry over GUID จาก legacy · S2 เป็น greenfield ไม่มี GUID เดิม จึงใช้ int IDENTITY(1,1) เป็น PK ทุกตาราง (log ปริมาณสูง = bigint IDENTITY) · CreatedBy/UpdatedBy = int FK → PJOS.AppUser

Schema ที่ ship จริง (ตั้งแต่ Plan 1, 2026-07/08) เบี่ยงจาก target design นี้ 2 จุด

เอกสารนี้ยังเป็นพิมพ์เขียวต้นทาง — คงไว้อ้างอิงเหตุผลเชิง requirement/normalization ข้างต้น แต่ implementation จริง (repos/pjos/backend/prisma/migrations/V001V007, ดู backend/README.md เป็น source of truth) ตัดสินใจต่างจากที่นี่ 2 จุด — ไม่ใช่การเขียนผิด แต่เป็นการตัดสินใจ ณ เวลา implement ที่ควรบันทึกไว้กันงง:

  1. Naming + PK type: snake_case + UNIQUEIDENTIFIER/NEWID() แทน PascalCase + int IDENTITY. ตารางจริงคือ PJOS.case, PJOS.case_result, PJOS.case_result_attachment ฯลฯ (ตัวพิมพ์เล็กทั้งหมด, snake_case column) ใช้ GUID เป็น PK ไม่ใช่ int IDENTITY ตามที่โน้ตด้านบนระบุไว้ — เหตุผลไม่ได้บันทึกละเอียดใน migration comment (เป็นทางเลือกของทีม implement ตอนเริ่ม Plan 1) แต่ สม่ำเสมอทั้ง schema ตั้งแต่ V001 จนถึง V007
  2. PJOS.Attachment แบบ polymorphic → ไม่ได้สร้าง. REQ-PJO-007 (V007) ใช้ตารางเฉพาะ case_result_attachment (FK ตรงไปยัง case_result.id) แทน เหตุผลบันทึกไว้ในตัว migration (V007_pjos_case_result.sql, comment ด้านบน CREATE TABLE): คู่ (OwnerEntityType, OwnerEntityId) แบบ polymorphic บังคับ FK ไม่ได้ — แถวกำพร้า/ชี้ entity ที่ไม่มีจริงเกิดได้โดย DB ไม่ห้าม ต้องคุมที่ app layer ทั้งหมด ขัดกับ ทุกตารางอื่นใน schema นี้ที่ให้ DB บังคับ invariant (filtered unique ของ victim/case_assignee, CHECK ของ agency.kind) และ ณ วันที่ V007 ship, REQ-PJO-008 (ส่งต่อ) ซึ่งเป็น entity ถัดไปที่ต้องการไฟล์แนบยังไม่ถูกสร้าง จึงไม่มี entity ตัวที่สองให้ "ใช้ร่วม" จริง ๆ วันนี้ — ถ้าวันหน้ามี ≥3 entity ที่ต้องการไฟล์แนบจริง ค่อยพิจารณาแยก ตารางกลางใหม่ (ที่ยังคง FK ต่อ entity ได้ผ่านตารางเชื่อม ไม่ใช่ polymorphic คู่ ownerType/ownerId แบบเดิม)

Stack & Architecture (จาก SRS §2.1, §5.3)

ส่วน เทคโนโลยี (ตาม SRS)
Backend Node.js / RESTful API (pjos.rlpd.go.th) — realize เป็น NestJS (ตาม pjos/be placeholder)
Frontend Responsive Web (Desktop/Tablet สำหรับเจ้าหน้าที่ภาคสนาม) + Rich Text Editor — mockup pjos/fe
Database MS SQL Server — schema ใหม่ PJOS
Auth ผ่าน S9 Web Portal — ThaiD/AD + SSO (ไม่เปิดให้ประชาชน — เจ้าหน้าที่กรมฯ เท่านั้น)
Integration (In) Service Center (รับ Line OA + ข่าวจากช่องทางต่าง ๆ) ผ่าน Web Service / REST
Integration (Out) Web Service ส่งต่อ S1 ที่ปรึกษากฎหมาย, S4 ไกล่เกลี่ย, S6 OCIPA + หน่วยงานภายนอก
Master Data Sync จาก OCIPA (ประเภทความช่วยเหลือ/สถานีตำรวจ) + Service Center
File Storage Object Storage (รูปภาพ/เอกสารหลักฐาน ≥ 50 MB/ไฟล์)
Observability NestJS app ใช้ไลบรารีกลาง @rlpdjs/observability (logger/trace/metrics/DB query log) → ELK [NFR-M02]

ความสัมพันธ์กับ repo rlpdjs

rlpdjs = แพ็กเกจ @rlpdjs/observability (ไลบรารี NestJS ที่ใช้ร่วมกันทุกระบบ S1…S9) — ไม่ใช่ backend ของ S2 → backend ของ S2 สร้างใหม่ใน pjos/be (NestJS) แล้ว install @rlpdjs/observability เป็น dependency ; Prisma ของ S2 ตั้ง provider = sqlserver ชี้ schema PJOS · ดู implementation.md

Role Hierarchy (SRS §3.1, 5 roles)

Role Code บทบาท สิทธิ์โดยสรุป
SYS_ADMIN ผู้ดูแลระบบ จัดการ Master Data, สิทธิ์ผู้ใช้, Configuration, Audit Log
EXECUTIVE ผู้บริหาร อนุมัติแผนขั้นสุดท้าย + เปลี่ยนผู้รับมอบหมาย, Dashboard ภาพรวม, เห็นเคสทั้งหมด
SUPERVISOR หัวหน้างาน ตรวจ/อนุมัติแผนขั้นต้น + Comment, มอบหมายงาน, อนุมัติรายงานรอบ 24 ชม.
OFFICER_PROACTIVE เจ้าหน้าที่เชิงรุก บันทึกคำร้อง วางแผน Case Management ดำเนินการตามแผน (เห็นเคสไม่ได้รับมอบหมายแบบ metadata-only)
OFFICER_PROVINCE สำนักงานยุติธรรมจังหวัด (สยจ.) ผู้ใช้รายจังหวัด — เห็นเฉพาะจังหวัดตน [REQ-PJO-009]

กำหนดสิทธิ์ที่ S9 Web Portal (SSO via JWT) — สอดคล้อง design_p2 D8

ยืนยันตัวตน + มอบสิทธิ์อยู่ที่ S9 Web Portal (ThaiD/AD + SSO realm rlpd) PJOS อ่านบทบาทจริงจาก JWT ขณะ runtime · AppUser/RoleAssignment/Role เป็น local mirror (auto-provision เมื่อ login ครั้งแรก) — ไม่มีคอลัมน์ password/lockout (lockout 5 ครั้ง/NFR-S07 ทำที่ SSO)

🔑 Requirement → Table Traceability (spine)

ทุก REQ-PJO-001…012 มีแถว — map ไปอย่างน้อย 1 ตาราง หรือ ระบุชัดว่าไม่มีผลต่อ schema

REQ TOR สาระสำคัญ ตารางที่รองรับ
REQ-PJO-001 7.12.1 รับคำร้องจาก Service Center 4 ช่องทาง + Dedup + แจ้งสิทธิภายใน 24 ชม. + consent IntakeRecord, Case, CaseRightsCheck, RightsChecklistItem, Channel
REQ-PJO-002 7.12.1 บันทึกรายงานผล 2 ช่วง SLA (24 ชม./15 วัน) + Approval ต่างกันแต่ละรอบ OutreachReport, ApprovalAction, ApprovalStep, Notification
REQ-PJO-003 7.12.2 นำเข้า/บันทึกคำขอ + Multi-victim + PDPA media consent + ประเภทช่วยเหลือ/สน. (Master) Case, CaseVictim, HelpType, PoliceStation, OffenseBase, VulnerableGroup
REQ-PJO-004 7.12.2 มอบหมาย Multi-assignee + เปลี่ยนผู้รับมอบหมาย (เฉพาะหัวหน้า/ผู้บริหาร) + Audit CaseAssignment, PermissionLog, OfficerProfile, AppUser
REQ-PJO-005 7.12.2 Trigger gate มาตรฐาน 4 ด้าน (ต้องการ/ไม่ต้องการ) + รายการย่อย admin CRUD + "ไม่มีแผน" CaseTrigger, RightsArea, RightsAreaItem, AssistancePlan, PlanItem
REQ-PJO-006 7.12.2 บันทึกผลพิจารณาแผน + Approval ลำดับชั้น (หัวหน้า→ผอ.) + Comment + loop-back + version AssistancePlan (versioned), ApprovalAction, ApprovalStep
REQ-PJO-007 7.12.2 บันทึกผลตามแผน (Rich Text) + แนบหลักฐานบังคับ + สถานะ/เหตุผลการยุติ + ผลภาคสนาม ImplementationResult, Attachment, ProgramTermination
REQ-PJO-008 7.12.2 ส่งต่อ S1/S4/S6 (Web Service) + ภายนอก (Manual) + เก็บ Reference + Audit Referral, Unit, Attachment
REQ-PJO-009 7.12.2 สืบค้น (ช่วงเวลา/ผู้รับผิดชอบ/จังหวัด) + สิทธิ์การมองเห็น (metadata-only) Case, CaseStatusHistory, SystemConfigquery + scope ที่ app layer
REQ-PJO-010 7.12.2 Dashboard ≥5 รายงาน (เรื่อง vs ราย, SLA 24 ชม./15 วัน, เพศ/อายุ/ฐานความผิด/สน.) aggregate queries บน Case/CaseVictim/OffenseBase ; export → AuditLog
REQ-PJO-011 7.12.2 ดาวน์โหลด/Export รายบุคคล + Dashboard (.xls/.xlsx/.pdf/.csv) (report engine ที่ app layer) ; export action → AuditLog
REQ-PJO-012 Cross Master Data + Workflow Governance + Audit Log + PDPA (5 บทบาท) ApprovalStep, SystemConfig, AuditLog, PermissionLog, lookup tables, Role/RoleAssignment
Auth §3.1 SSO via JWT (S9 Portal) + RBAC 5 บทบาท AppUser (mirror), RoleAssignment (mirror), Role (catalog)

REQ ที่ไม่มีผลต่อ schema (จัดการที่ app/infra layer)

  • REQ-PJO-009 (scope การมองเห็น) — สยจ. เห็นเฉพาะจังหวัด, เจ้าหน้าที่เชิงรุกเห็นเคสไม่ได้รับมอบหมายแบบ metadata-only (ปกปิด PII) = logic ที่ query/interceptor layer จาก JWT ; schema แค่เก็บ ProvinceId + CaseAssignment
  • REQ-PJO-010/011 — chart rendering + export .xlsx/.pdf/.csv = report engine ที่ app layer ; schema เก็บได้แค่ export action ใน AuditLog
  • NFR ทั้งหมด — TLS 1.2+ (NFR-S01), AES-256 at-rest (NFR-S02), lockout (NFR-S07), CSP (NFR-S08) = infra/app layer ; ที่กระทบ schema คือ "PII ต้องเข้ารหัส at-rest" → คอลัมน์ CitizenId/ไฟล์แนบวางแผนเข้ารหัส (ดู data_dict.md)

Table Inventory (34 ตาราง, schema PJOS)

กลุ่ม ตาราง
Lookup (11) Province, Channel, HelpType (self-ref), OffenseBase, PoliceStation, VulnerableGroup, Unit, RightsArea, RightsAreaItem, RightsChecklistItem, Role
Identity/Auth — mirror (3) AppUser (SSO, no password), RoleAssignment, OfficerProfile
Intake (1) IntakeRecord (4 ช่องทาง + dedup)
Case core (4) Case, CaseVictim (multi-victim + demographics), CaseAssignment (multi-assignee), CaseRightsCheck
Outreach reports (1) OutreachReport (2 รอบ SLA)
Trigger / Plan / Approval (4) CaseTrigger, AssistancePlan (versioned), PlanItem, ApprovalAction (polymorphic)
Implementation / Referral / Termination (3) ImplementationResult, Referral, ProgramTermination
Cross-cutting (7) Attachment (polymorphic), Notification, CaseStatusHistory, ApprovalStep, SystemConfig, PermissionLog, AuditLog

Mockup → Target mapping (รองรับ frontend)

mockup data.jsx มี entity หลักจาก mkCase/CASES + constants — ตารางที่เหลือเป็น support/workflow/audit ที่เพิ่มเพื่อ integrity

Mockup (data.jsx / screens) Target (PJOS) หมายเหตุ
CASES/mkCase (id, serviceCenterRef, channel, province, helpType, summary, status, priority, consent, budgetEstimate, contactDueAt, reportDueAt) Case id→CaseNo UNIQUE ; channel/helpType→lookup FK ; status/consent/priority→CHECK
mkCase.victims[] (name, citizenMasked, phone, vulnerableGroup, mediaConsent, consentAt) + root gender/age CaseVictim array→ตาราง 1:N ; มิติประชากร (เพศ/อายุ/กลุ่มเปราะบาง) ย้ายมาที่ราย (SRS multi-victim + รายงาน "จำนวนราย")
mkCase.assignees[]/assignedTo (string ชื่อ) + OFFICERS (capacity) CaseAssignment (multi-assignee) + OfficerProfile ชื่อ→FK AppUser ; multi-assignee + ประวัติ (IsCurrent/UnassignedAt) ; capacity→OfficerProfile ; active/due derived
RIGHTS_CHECKLIST (6 ข้อ) + mkCase.rightsChecked[] RightsChecklistItem + CaseRightsCheck enum→lookup + junction ต่อเคส
SERVICE_CENTER_QUEUE + duplicateRisk/sourceConfidence IntakeRecord รายการรับเข้า 4 ช่องทางก่อนยืนยัน + dedup score (O-6) → ยืนยันแล้วสร้าง Case
mkCase.reportRounds (round1 24ชม./round2 15วัน + approver) OutreachReport + ApprovalAction RoundNo 1/2 ; ขั้นอนุมัติ→ApprovalAction (รอบ2 = 2 ขั้น)
RIGHTS_AREAS (4 ด้าน + subItems) + trigger ต้องการ/ไม่ต้องการ RightsArea + RightsAreaItem + CaseTrigger subItems = admin CRUD (CR-4) ; gate→CaseTrigger.IsWanted ; ไม่ต้องการครบ→Case.NoPlanFlag
PLAN_STEPS (dimension, owner, due, budget) + planVersion/approval AssistancePlan (versioned) + PlanItem + ApprovalAction แผนเก็บทุก Version (IsCurrent) ; loop-back ตีกลับ→Version ใหม่
FIELD_RESULTS (area, officer, result, files) ImplementationResult (+ Attachment) ผลภาคสนาม = ผลดำเนินการ (Rich Text + พื้นที่) ; หลักฐานบังคับ
Referral/Escalation (target S1/S4/S6, external) Referral internal system/unit vs external ; web service vs manual
TERMINATION_REASONS + สถานะ รอขอยุติ/ออกจากโปรแกรม ProgramTermination ขอยุติ→พิจารณา (ผอ.)→ออกจากโปรแกรม
UNITS (referral target) Unit หน่วยงานภายในกรมฯ (admin เพิ่ม/ลด)
CHANNELS/STATUSES/PROVINCES/HELP_TYPES/OFFENSE_BASES/POLICE_STATIONS constants Channel/Province/HelpType/OffenseBase/PoliceStation + CHECK enum ป้ายไทย → lookup/CHECK

Support tables ที่เพิ่มใหม่ (ไม่มีใน mockup): CaseStatusHistory (tracking/audit), ApprovalStep/SystemConfig (workflow/SLA config), Notification, Attachment (polymorphic), AppUser/RoleAssignment/Role (auth mirror), PermissionLog/AuditLog (PDPA/governance).

หลักการแปลง mockup → relational (ทำไม mock ขึ้น production ตรง ๆ ไม่ได้)

data.jsx เป็น in-memory mock — บอกแค่รูปร่างข้อมูลที่ UI ต้องใช้ ไม่ได้บอกวิธีจัดเก็บ การแปลงหลัก:

  • id string → surrogate PK + business codeS2-2569-00120/SC-IMPORT-001 (ปนความหมายธุรกิจกับ key) → PK int IDENTITY + คอลัมน์ business code UNIQUE (CaseNo/IntakeNo)
  • array / root field → ตาราง 1:Nvictims[]/assignees[]CaseVictim/CaseAssignment ; root gender/age (mock คิดแบบเหยื่อคนเดียว) → ย้ายไป CaseVictim เพราะ SRS = multi-victim + รายงานรายบุคคล
  • enum ป้ายไทย → CHECK/lookup'รอพิจารณาแผน'/'เร่งด่วน'/'ประสงค์รับความช่วยเหลือ' (เปราะ) → CHECK constraint + lookup table
  • string ชื่อ → FKassignedTo:'นางสาวพิชญา สีหราช'CaseAssignment.AssigneeUserId (FK จริง)
  • field snapshot → derivedOFFICERS.active/due (workload) คำนวณจาก CaseAssignment ไม่เก็บซ้ำ ; เก็บเฉพาะ Capacity

สิ่งที่ "ตัดออก" ไม่ยกมาเป็นตาราง: STATUSES palette (สี = presentation), NAV/ROLE_NAV (routing config — ได้จาก role ใน JWT), fmtThaiDate/fmtNum (utility ฝั่ง UI), caseScope/canSeeDetail/maskPII (logic การมองเห็น = interceptor ที่ app layer ใช้ ProvinceId+CaseAssignment)

ประเด็นออกแบบที่เป็น deviation จาก S1

Tag ประเด็น เหตุผล
D-PJO-1 ApprovalAction แบบ polymorphic (แทน per-entity status + ApprovalStep ของ S1) PJOS มี 3 flow อนุมัติที่ต่างกัน — รายงานรอบ1 (หัวหน้างานขั้นเดียว), รอบ2 (หัวหน้างาน→ผู้บริหาร 2 ขั้น), พิจารณาแผน (หัวหน้างาน→ผอ. + loop-back + comment + version) · 1 ตาราง append-only เก็บทุกการตัดสินใจ (StepNo/Role/Action/Decision/Comment) ใช้ร่วมได้ทั้ง 3 ; ลำดับขั้นกำหนดที่ ApprovalStep (config ไม่ hard-code)
D-PJO-2 แผน versioned (AssistancePlan 1 แถว/Version) ApprovalAction.OwnerEntityId ชี้ AssistancePlanId ของ Version เฉพาะ → ตอบโจทย์ "v1 หัวหน้าเห็นชอบ, ผอ.ตีกลับ→REWORK, สร้าง v2" + เทียบ Version ย้อนหลังได้ (REQ-006) ; IsCurrent ชี้ Version ล่าสุด
D-PJO-3 OfficerProfile แยกจาก AppUser AppUser เป็น thin SSO mirror (ไม่มี password) — ข้อมูลปฏิบัติงาน (Capacity/Unit) ของเจ้าหน้าที่ผู้รับเคสจึงแยกออกมา ; workload (active/due) คำนวณ จาก CaseAssignment

ประเด็นที่ต้อง align กับลูกค้า (OPEN — จาก SRS §5.2 TBD Checklist)

Tag ประเด็น สถานะ / แนวทางในเอกสารนี้
[OPEN-AUTH] NFR-S07 ระบุ lockout 5 ครั้ง แต่ design_p2 D8 = auth รวมศูนย์ที่ S9 Portal/Keycloak เลือก thin-mirror ไม่มี password/lockout (เหมือน S1/S3) — lockout ทำที่ SSO · ถ้าลูกค้ายืนยันต้องเก็บใน PJOS เอง → เพิ่ม PasswordHash/FailedLoginCount/LockedUntil ใน AppUser
[OPEN-DB] SRS §5.3 ระบุ "ฐานข้อมูล PJOS" (pjos.rlpd.go.th) แต่ design_p2 D1 = Single DB PSDBPRDDB ตามแนว design_p2 — schema namespace PJOS ใน PSDBPRDDB · ถ้าลูกค้ายืนยัน DB แยกจริง → DDL เดิมใช้ได้ เพียงเปลี่ยน target DB
[O-5] REQ-008 — รายชื่อระบบปลายทาง Escalation (นอกจาก S1/S4/S6) + โปรโตคอล Referral.TargetSystem (varchar) + TargetType รองรับหลายปลายทาง ; รายชื่อ + spec [รอยืนยัน]
[O-6] REQ-008/001 — Deduplicate เคสซ้ำจากหลายช่องทาง (MSC + Service Center + Line) IntakeRecord.DuplicateScore/MatchedCaseId/Status='MERGED' รองรับ ; เกณฑ์/threshold dedup [รอยืนยัน]
[CR-4] REQ-005 — รายการย่อยใต้ 4 ด้าน (Default Master Data) RightsAreaItem (admin CRUD) รองรับ ; ชุดเริ่มต้น [รอยืนยัน]
[TBD-victim] REQ-003 — Limit จำนวนผู้เสียหายต่อเคส CaseVictim 1:N ไม่จำกัดใน schema ; limit (ถ้ามี) เก็บใน SystemConfig
[TBD-5] REQ-007 — รายการเหตุผล "ไม่เสร็จสิ้น/ดำเนินการไม่ได้" + เหตุยุติโปรแกรม (Master) ImplementationResult.CompletionReason + ProgramTermination.Reason (varchar code) ; ชุดเหตุผล Master [รอยืนยัน]
[O-9] REQ-012 — นโยบาย Masking PII (ใครเห็นเต็ม/เห็น Mask) Mask = app layer (จาก JWT scope) ; การเข้าถึง PII log ใน AuditLog (Action='VIEW_PII') ; map สิทธิ์ใน SystemConfig [รอยืนยัน]
[TBD-meta] REQ-009 — ระดับ Metadata ที่ผู้ไม่ได้รับมอบหมายเห็น SystemConfig (key = field ที่เปิดเผย) ; รายการ field [รอยืนยัน]