SurePhone ERD — Domain 0: Shared Foundations (ฐานกลาง)
ตาราง/partials ที่ทุก domain ใช้ร่วม — นิยามที่นี่ที่เดียว เวลา compile/export ให้ concat
00-shared+ domain ที่ต้องการ
DBML
//// ───────── Shared audit partials ─────────
TablePartial audit_c {
created_by varchar [not null, default: 'SYSTEM', note: 'ID ผู้สร้าง หรือ SYSTEM']
created_at timestamptz [not null, default: `CURRENT_TIMESTAMP`]
Note: 'Metadata ตรวจสอบผู้สร้าง + เวลา — ใช้กับตารางที่ insert-only / ไม่แก้ไข'
}
TablePartial audit_cu {
created_by varchar [not null, default: 'SYSTEM']
updated_by varchar [not null, default: 'SYSTEM']
created_at timestamptz [not null, default: `CURRENT_TIMESTAMP`]
updated_at timestamptz [not null, default: `CURRENT_TIMESTAMP`]
Note: 'Metadata ตรวจสอบผู้สร้าง/ผู้แก้ไข + เวลา — ใช้กับตารางที่แก้ไขได้'
}
//// ───────── sys.r2_file (catalog ไฟล์กลาง บน Cloudflare R2) ─────────
Table sys.r2_file {
id uuid [pk, default: `gen_random_uuid()`]
object_key varchar(1024) [not null, unique, note: 'path/key ของไฟล์ใน R2 (bucket เดียว = app config) — สร้าง signed URL ตอนเข้าถึง']
is_public bool [not null, default: false, note: 'true = ไฟล์สาธารณะ (โลโก้/QR/แบนเนอร์) เข้าถึงตรงได้; false = ส่วนบุคคล ต้องผ่าน signed URL']
mime_type varchar(255)
file_size_bytes bigint
original_filename varchar(255) [note: 'ชื่อไฟล์เดิมตอน upload']
~audit_c
indexes {
is_public [name: 'idx_r2_file_public']
}
Note: '''
Catalog ไฟล์ทุกไฟล์บน R2 (bucket เดียว) — single source of truth ของ metadata ไฟล์
เก็บ object_key เท่านั้น ไม่เก็บ public URL ถาวร เพราะไฟล์ส่วนบุคคลต้องเข้าถึงผ่าน signed URL
ตารางอื่นอ้างไฟล์ผ่านคอลัมน์ xxx_file_id (FK → sys.r2_file.id)
หลายไฟล์ต่อ 1 record (รูปเครื่อง/หลักฐาน/วิดีโอ) ใช้ child table ที่มี file_id แทน column เดียว
created_by (จาก audit_c) = ผู้ upload
'''
}Mermaid ER
erDiagram r2_file { uuid id PK varchar object_key UK bool is_public varchar mime_type bigint file_size_bytes varchar original_filename varchar created_by timestamptz created_at }
หลักการใช้งาน
- ทุกคอลัมน์ที่เคยเป็น
xxx_img_path varchar→ เปลี่ยนเป็นxxx_file_id uuid(FK →sys.r2_file.id) - ไฟล์สาธารณะ (โลโก้/QR/แบนเนอร์) set
is_public = trueตอน upload · ไฟล์ส่วนบุคคลfalse - แนบหลายไฟล์ → child table เช่น
contract.contract_documents (id, contract_id, file_id, doc_type) - ลบไฟล์จริงบน R2 → ใช้ deletion-queue table (จะนิยามใน sys/Domain 8 แบบ phonerefun) trigger ตอน record เปลี่ยน/ลบ
object_key คืออะไร
R2 (เหมือน S3) เป็น object storage แบบ flat key→bytes ไม่มีโฟลเดอร์จริง
object_key= “กุญแจ/เส้นทาง” ที่ระบุ object ภายใน bucket — เหมือน path ของไฟล์ เช่นbusiness/abc123/logo/9f2e.pngbucket+object_key= ชี้ไฟล์ได้ unique (เรามี bucket เดียว เลยเหลือแค่ object_key)- ไม่ใช่ URL — URL เกิดจาก key มาประกอบทีหลัง: public =
<base>/<object_key>, private = presigned URL จาก object_key (มีลายเซ็น + วันหมดอายุ) - เราตั้ง key เป็น path-like เพื่อจัดระเบียบ + ใส่ uuid กันชื่อชน เช่น
customer/{customer_id}/id_card/{uuid}.jpg - เก็บ
object_keyใน DB (เสถียร ไม่หมดอายุ) แทนการเก็บ URL (ซึ่งเปลี่ยน/หมดอายุได้)
การทำงาน (Flow)
sequenceDiagram autonumber actor U as ผู้ใช้/Client participant A as Backend (App) participant R as Cloudflare R2 (1 bucket) participant D as DB (sys.r2_file + owner) note over U,D: 1) Upload U->>A: อัปโหลดไฟล์ (bytes) A->>R: PUT object ที่ object_key R-->>A: ok A->>D: INSERT r2_file (object_key, is_public, mime...) → file_id A->>D: UPDATE owner SET xxx_file_id = file_id A-->>U: สำเร็จ note over U,D: 2) Read — public (โลโก้/QR) U->>R: GET public URL (จาก object_key) ตรงๆ R-->>U: ไฟล์ note over U,D: 3) Read — private (บัตร/สลิป/วิดีโอ) U->>A: ขอดูไฟล์ (file_id) A->>D: SELECT object_key, is_public + เช็คสิทธิ์ A->>R: presign(object_key, หมดอายุ 10 นาที) R-->>A: signed URL (ชั่วคราว) A-->>U: signed URL — ไม่เก็บลง DB U->>R: GET signed URL → ได้ไฟล์ note over A,R: 4) Cleanup — เปลี่ยน/ลบไฟล์ → enqueue job → worker เรียก R2 delete → ลบแถว r2_file
ตัวอย่าง — โลโก้บ้าน (public):
r2_file:{ id: "f1a2…", object_key: "business/abc/logo/9x.png", is_public: true, mime_type: "image/png" }businesses:{ id: "abc", name: "บ้าน A", logo_file_id: "f1a2…" }- แสดง: is_public=true → ต่อ URL จาก object_key เสิร์ฟตรงๆ
บัตรประชาชน (private): is_public=false → แอดมินเปิดดู ระบบเช็คสิทธิ์ → presign ลิงก์อายุ 10 นาที → หมดเวลาเปิดไม่ได้