Skip to main content

กระดานสนทนา (Forum)

รายละเอียดคอลัมน์ของ schema bulletin ทั้งหมด (board/category/post/comment/reaction/report/block)

ภาพรวม

Schema bulletin มี 8 ตาราง — board (หนึ่งกระดานต่อหนึ่ง LINE OA) → category (หมวดหมู่) → post (กระทู้) → comment (ความเห็น) โดยมี reaction, report, block (ระงับผู้ใช้) และ audit_log ประกอบ schema นี้ apply แยกด้วย psql ไม่ผ่าน Prisma (migration 001-005) ตั้งแต่ migration 005 กระดานถูกออกแบบใหม่ให้เป็น forum (กระทู้มีหัวข้อ เรียงตามการตอบล่าสุด) ไม่ใช่ social feed

ตาราง bulletin.board

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
line_oa_idINTEGERNO-LINE OA เจ้าของกระดาน (unique — หนึ่ง OA มีหนึ่งกระดาน)
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
nameVARCHAR(255)NO'Bulletin Board'ชื่อกระดาน
descriptionTEXTYES-คำอธิบายกระดาน
settingsJSONBNO'{}'การตั้งค่าระดับกระดาน เช่น สิทธิ์การโพสต์ จำนวนรูปสูงสุด
statusVARCHAR(20)NO'active'สถานะการใช้งาน
created_byINTEGERYES-ผู้สร้างเรกคอร์ด
updated_byINTEGERYES-ผู้แก้ไขล่าสุด
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สร้าง
updated_dateTIMESTAMPTZ(3)YES-วันเวลาที่แก้ไขล่าสุด

ตาราง bulletin.category

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
board_idINTEGERNO-FK ไปยัง bulletin.board(id) ลบแบบ CASCADE
line_oa_idINTEGERNO-LINE OA ที่หมวดหมู่นี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
nameVARCHAR(100)NO-ชื่อหมวดหมู่
colorVARCHAR(20)YES-สีป้ายหมวดหมู่
sort_orderINTEGERNO0ลำดับการแสดงผล
access_modeVARCHAR(20)NO'inherit'แกน "สิทธิ์การมองเห็น" — inherit คือใช้ตามกระดาน
audience_idsJSONBNO'[]'รายการ audience ที่มองเห็นหมวดหมู่นี้ (ใช้คู่กับ access_mode)
statusVARCHAR(20)NO'active'สถานะการใช้งาน
created_byINTEGERYES-ผู้สร้างเรกคอร์ด
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สร้าง
updated_dateTIMESTAMPTZ(3)YES-วันเวลาที่แก้ไขล่าสุด
deleted_dateTIMESTAMPTZ(3)YES-วันเวลาที่ soft delete
post_accessVARCHAR(20)NO'member'แกน "สิทธิ์การโพสต์" — member = ผู้ใช้ LINE โพสต์ได้, admin = โพสต์ได้จาก CMS เท่านั้น (migration 004)

ตาราง bulletin.post

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
board_idINTEGERNO-FK ไปยัง bulletin.board(id) ลบแบบ CASCADE
category_idINTEGERYES-FK ไปยัง bulletin.category(id) — ลบหมวดหมู่แล้วตั้งเป็น NULL
line_oa_idINTEGERNO-LINE OA ที่กระทู้นี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
author_typeVARCHAR(20)NO-ชนิดผู้เขียน: line_user หรือ admin
author_line_user_idVARCHAR(255)YES-LINE user id ของผู้เขียน (ใช้เมื่อ author_type = 'line_user')
author_user_idINTEGERYES-user id ฝั่ง CMS (ใช้เมื่อ author_type = 'admin')
bodyTEXTNO-เนื้อหากระทู้
imagesJSONBNO'[]'รายการรูปแนบ
statusVARCHAR(20)NO'published'สถานะกระทู้ (เผยแพร่/รออนุมัติ/ซ่อน)
allow_commentsBOOLEANNOtrueเปิดให้แสดงความเห็นหรือไม่
is_pinnedBOOLEANNOfalseปักหมุดกระทู้ไว้บนสุดหรือไม่
pinned_atTIMESTAMPTZ(3)YES-วันเวลาที่ปักหมุด (ใช้เรียงลำดับกระทู้ที่ปักหมุด)
comment_countINTEGERNO0จำนวนความเห็นที่เผยแพร่ — trigger คำนวณให้ ห้ามเขียนจากแอป
reaction_countINTEGERNO0จำนวน reaction — trigger คำนวณให้
report_countINTEGERNO0จำนวนรายงานที่ยังเปิดอยู่ — trigger คำนวณให้
edited_dateTIMESTAMPTZ(3)YES-วันเวลาที่ผู้เขียนแก้ไขเนื้อหา
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สร้าง
updated_dateTIMESTAMPTZ(3)YES-วันเวลาที่แก้ไขล่าสุด
deleted_dateTIMESTAMPTZ(3)YES-วันเวลาที่ soft delete
titleVARCHAR(200)NO-หัวข้อกระทู้ — NOT NULL และไม่มี default โดยตั้งใจ (migration 005)
last_activity_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPเวลาการตอบล่าสุด ใช้เรียงลำดับ forum — trigger คำนวณให้ (migration 005)

ตาราง bulletin.comment

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
post_idINTEGERNO-FK ไปยัง bulletin.post(id) ลบแบบ CASCADE
board_idINTEGERNO-กระดานต้นสังกัด (denormalize ไว้เพื่อคิว moderation ระดับกระดาน)
line_oa_idINTEGERNO-LINE OA ที่ความเห็นนี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
author_typeVARCHAR(20)NO-ชนิดผู้เขียน: line_user หรือ admin
author_line_user_idVARCHAR(255)YES-LINE user id ของผู้เขียน
author_user_idINTEGERYES-user id ฝั่ง CMS
bodyTEXTNO-เนื้อหาความเห็น
quote_comment_idINTEGERYES-FK ไปยัง bulletin.comment(id) — ความเห็นที่ถูกอ้างถึง (ลบแล้วตั้งเป็น NULL)
statusVARCHAR(20)NO'published'สถานะความเห็น
reaction_countINTEGERNO0จำนวน reaction — trigger คำนวณให้
report_countINTEGERNO0จำนวนรายงานที่ยังเปิดอยู่ — trigger คำนวณให้
edited_dateTIMESTAMPTZ(3)YES-วันเวลาที่ผู้เขียนแก้ไขเนื้อหา
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สร้าง
updated_dateTIMESTAMPTZ(3)YES-วันเวลาที่แก้ไขล่าสุด
deleted_dateTIMESTAMPTZ(3)YES-วันเวลาที่ soft delete
imagesJSONBNO'[]'รูปแนบในความเห็น (migration 005)

ตาราง bulletin.reaction

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
target_typeVARCHAR(10)NO-ชนิดเป้าหมาย: post หรือ comment
target_idINTEGERNO-id ของกระทู้หรือความเห็นที่ถูก react
line_oa_idINTEGERNO-LINE OA ที่ reaction นี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
line_user_idVARCHAR(255)NO-LINE user id ของผู้ react
emojiVARCHAR(16)NO-อีโมจิที่ใช้ react
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่ react

ตาราง bulletin.report

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
target_typeVARCHAR(10)NO-ชนิดเป้าหมาย: post หรือ comment
target_idINTEGERNO-id ของกระทู้หรือความเห็นที่ถูกรายงาน
board_idINTEGERNO-กระดานต้นสังกัด (ใช้กับคิว moderation)
line_oa_idINTEGERNO-LINE OA ที่รายงานนี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
reporter_line_user_idVARCHAR(255)NO-LINE user id ของผู้รายงาน
reasonVARCHAR(50)NO-เหตุผลการรายงาน (ค่าจากรายการที่กำหนดไว้)
detailTEXTYES-รายละเอียดเพิ่มเติมจากผู้รายงาน
statusVARCHAR(20)NO'open'สถานะการจัดการรายงาน
resolved_byINTEGERYES-ผู้ดูแลที่ปิดเรื่อง
resolved_dateTIMESTAMPTZ(3)YES-วันเวลาที่ปิดเรื่อง
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่รายงาน

ตาราง bulletin.block

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
board_idINTEGERNO-กระดานที่การระงับนี้มีผล
line_oa_idINTEGERNO-LINE OA ที่การระงับนี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
line_user_idVARCHAR(255)NO-LINE user id ของผู้ถูกระงับ
scopeVARCHAR(20)NO'all'ขอบเขตการระงับ เช่น all หรือ comment
reasonTEXTYES-เหตุผลการระงับ
expires_dateTIMESTAMPTZ(3)YES-วันเวลาที่การระงับหมดอายุ (NULL = ไม่มีกำหนด)
created_byINTEGERYES-ผู้ดูแลที่สั่งระงับ
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สั่งระงับ
revoked_dateTIMESTAMPTZ(3)YES-วันเวลาที่ยกเลิกการระงับ
revoked_byINTEGERYES-ผู้ดูแลที่ยกเลิกการระงับ

ตาราง bulletin.audit_log

ColumnTypeNullableDefaultคำอธิบาย
idBIGSERIALNOauto incrementPrimary key
board_idINTEGERYES-กระดานที่เกี่ยวข้อง (NULL ได้สำหรับเหตุการณ์ที่ไม่ผูกกระดาน)
line_oa_idINTEGERNO-LINE OA ที่เหตุการณ์นี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
actor_typeVARCHAR(20)NO-ชนิดผู้กระทำ เช่น admin / line_user / system
actor_idVARCHAR(255)YES-รหัสผู้กระทำ
actor_nameVARCHAR(255)YES-ชื่อผู้กระทำ ณ เวลาที่บันทึก
actionVARCHAR(50)NO-การกระทำ เช่น ลบกระทู้ อนุมัติความเห็น ระงับผู้ใช้
target_typeVARCHAR(20)NO-ชนิดของสิ่งที่ถูกกระทำ
target_idINTEGERYES-id ของสิ่งที่ถูกกระทำ
beforeJSONBYES-ข้อมูลก่อนเปลี่ยนแปลง
afterJSONBYES-ข้อมูลหลังเปลี่ยนแปลง
ipVARCHAR(45)YES-IP address ของผู้กระทำ
user_agentTEXTYES-User agent ของผู้กระทำ
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่บันทึก

หมายเหตุ

  • Check constraint chk_post_author / chk_comment_author: บังคับแบบ XOR ว่าต้องมีตัวตน ผู้เขียนเพียงด้านเดียว — author_type = 'line_user' ต้องมี author_line_user_id และ author_user_id ต้องเป็น NULL, author_type = 'admin' ต้องมี author_user_id และ author_line_user_id ต้องเป็น NULL
  • Check constraint chk_category_post_access: post_access ต้องเป็น member หรือ admin เท่านั้น — เป็นด่านสุดท้าย ส่วนการบังคับจริงอยู่ฝั่ง server (client-api ตอบ 403 BULLETIN_CATEGORY_ADMIN_ONLY) การซ่อนหมวดหมู่ในหน้าเขียนกระทู้เป็นเพียงความสะดวก
  • หมวดหมู่มีสองแกนสิทธิ์ที่แยกจากกัน: แกนมองเห็น (access_mode + audience_ids) และ แกนโพสต์ (post_access) ทั้งสองแกนประกอบกัน ไม่มีแกนใดบ่งชี้อีกแกน — คู่ที่ใช้บ่อยคือ access_mode = 'inherit' + post_access = 'admin' ซึ่งได้หมวด "ประกาศ" ที่ทุกคนอ่านได้ แต่เขียนได้จาก CMS เท่านั้น
  • Unique: board.line_oa_id unique (หนึ่งกระดานต่อ OA); idx_bul_reaction_one บน (target_type, target_id, line_user_id) — หนึ่งคน react ได้ครั้งเดียวต่อเป้าหมาย; idx_bul_report_once บน (target_type, target_id, reporter_line_user_id); idx_bul_block_active บน (board_id, line_user_id) WHERE revoked_date IS NULL
  • ข้อควรระวังเรื่อง block: แถวที่หมดอายุแล้ว (expires_date ผ่านไป) ยังมี revoked_date IS NULL จึงยังกินที่ใน unique index ตัวนั้นอยู่ การสั่งระงับใหม่จึงต้อง revoke แถวเดิมของผู้ใช้คนนั้นภายใน transaction เดียวกัน (supersede) มิฉะนั้นการระงับซ้ำ หรือยกระดับจาก comment เป็น all จะชน unique violation
  • Trigger นับจำนวน (migration 003 และปรับใน 005): trg_bulletin_comment_count, trg_bulletin_reaction_count, trg_bulletin_report_count คำนวณ comment_count, reaction_count, report_count และ last_activity_date ใหม่ทุกครั้งที่ข้อมูลต้นทางเปลี่ยน โดยจับ pg_advisory_xact_lock (lock space 4201 = post, 4202 = comment) เพื่อกัน race — วิธี SELECT ... FOR UPDATE เคยทดสอบแล้วเกิด deadlock จำนวนมาก ส่วนการไม่ล็อกเลย ทำให้ตัวเลขผิดถาวร แอปพลิเคชันต้องไม่เขียนคอลัมน์เหล่านี้เอง
  • last_activity_date un-bump ได้: ค่าคำนวณจาก GREATEST(post.created_date, COALESCE(max(comment.created_date), post.created_date)) ดังนั้นการลบหรือซ่อนความเห็นสุดท้าย จะดึงกระทู้กลับลงไปตามเวลาที่โพสต์ ไม่ค้างอยู่บนสุด
  • Index ของ feed: idx_bul_post_feed (เรียงตาม created_date) ใช้กับ CMS และ idx_bul_post_activity (เรียงตาม last_activity_date) ใช้กับหน้า forum ฝั่ง LIFF ทั้งคู่มี id DESC ต่อท้ายเพื่อเป็น tiebreak ของ keyset cursor และมีเงื่อนไข WHERE deleted_date IS NULL
  • Migration 005 ล้างเนื้อหาเดิม: TRUNCATE ... RESTART IDENTITY บน reaction, report, comment, post เพื่อให้ post.title ลงเป็น NOT NULL โดยไม่ต้องมี default ค้างไว้ ส่วน board, category, block และ audit_log ไม่ถูกแตะ เพราะเป็น state ของผู้ดูแล ไม่ใช่เนื้อหา