กระดานสนทนา (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
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
line_oa_id | INTEGER | NO | - | LINE OA เจ้าของกระดาน (unique — หนึ่ง OA มีหนึ่งกระดาน) |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
name | VARCHAR(255) | NO | 'Bulletin Board' | ชื่อกระดาน |
description | TEXT | YES | - | คำอธิบายกระดาน |
settings | JSONB | NO | '{}' | การตั้งค่าระดับกระดาน เช่น สิทธิ์การโพสต์ จำนวนรูปสูงสุด |
status | VARCHAR(20) | NO | 'active' | สถานะการใช้งาน |
created_by | INTEGER | YES | - | ผู้สร้างเรกคอร์ด |
updated_by | INTEGER | YES | - | ผู้แก้ไขล่าสุด |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่สร้าง |
updated_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่แก้ไขล่าสุด |
ตาราง bulletin.category
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
board_id | INTEGER | NO | - | FK ไปยัง bulletin.board(id) ลบแบบ CASCADE |
line_oa_id | INTEGER | NO | - | LINE OA ที่หมวดหมู่นี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
name | VARCHAR(100) | NO | - | ชื่อหมวดหมู่ |
color | VARCHAR(20) | YES | - | สีป้ายหมวดหมู่ |
sort_order | INTEGER | NO | 0 | ลำดับการแสดงผล |
access_mode | VARCHAR(20) | NO | 'inherit' | แกน "สิทธิ์การมองเห็น" — inherit คือใช้ตามกระดาน |
audience_ids | JSONB | NO | '[]' | รายการ audience ที่มองเห็นหมวดหมู่นี้ (ใช้คู่กับ access_mode) |
status | VARCHAR(20) | NO | 'active' | สถานะการใช้งาน |
created_by | INTEGER | YES | - | ผู้สร้างเรกคอร์ด |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่สร้าง |
updated_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่แก้ไขล่าสุด |
deleted_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ soft delete |
post_access | VARCHAR(20) | NO | 'member' | แกน "สิทธิ์การโพสต์" — member = ผู้ใช้ LINE โพสต์ได้, admin = โพสต์ได้จาก CMS เท่านั้น (migration 004) |
ตาราง bulletin.post
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
board_id | INTEGER | NO | - | FK ไปยัง bulletin.board(id) ลบแบบ CASCADE |
category_id | INTEGER | YES | - | FK ไปยัง bulletin.category(id) — ลบหมวดหมู่แล้วตั้งเป็น NULL |
line_oa_id | INTEGER | NO | - | LINE OA ที่กระทู้นี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
author_type | VARCHAR(20) | NO | - | ชนิดผู้เขียน: line_user หรือ admin |
author_line_user_id | VARCHAR(255) | YES | - | LINE user id ของผู้เขียน (ใช้เมื่อ author_type = 'line_user') |
author_user_id | INTEGER | YES | - | user id ฝั่ง CMS (ใช้เมื่อ author_type = 'admin') |
body | TEXT | NO | - | เนื้อหากระทู้ |
images | JSONB | NO | '[]' | รายการรูปแนบ |
status | VARCHAR(20) | NO | 'published' | สถานะกระทู้ (เผยแพร่/รออนุมัติ/ซ่อน) |
allow_comments | BOOLEAN | NO | true | เปิดให้แสดงความเห็นหรือไม่ |
is_pinned | BOOLEAN | NO | false | ปักหมุดกระทู้ไว้บนสุดหรือไม่ |
pinned_at | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ปักหมุด (ใช้เรียงลำดับกระทู้ที่ปักหมุด) |
comment_count | INTEGER | NO | 0 | จำนวนความเห็นที่เผยแพร่ — trigger คำนวณให้ ห้ามเขียนจากแอป |
reaction_count | INTEGER | NO | 0 | จำนวน reaction — trigger คำนวณให้ |
report_count | INTEGER | NO | 0 | จำนวนรายงานที่ยังเปิดอยู่ — trigger คำนวณให้ |
edited_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ผู้เขียนแก้ไขเนื้อหา |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่สร้าง |
updated_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่แก้ไขล่าสุด |
deleted_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ soft delete |
title | VARCHAR(200) | NO | - | หัวข้อกระทู้ — NOT NULL และไม่มี default โดยตั้งใจ (migration 005) |
last_activity_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | เวลาการตอบล่าสุด ใช้เรียงลำดับ forum — trigger คำนวณให้ (migration 005) |
ตาราง bulletin.comment
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
post_id | INTEGER | NO | - | FK ไปยัง bulletin.post(id) ลบแบบ CASCADE |
board_id | INTEGER | NO | - | กระดานต้นสังกัด (denormalize ไว้เพื่อคิว moderation ระดับกระดาน) |
line_oa_id | INTEGER | NO | - | LINE OA ที่ความเห็นนี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
author_type | VARCHAR(20) | NO | - | ชนิดผู้เขียน: line_user หรือ admin |
author_line_user_id | VARCHAR(255) | YES | - | LINE user id ของผู้เขียน |
author_user_id | INTEGER | YES | - | user id ฝั่ง CMS |
body | TEXT | NO | - | เนื้อหาความเห็น |
quote_comment_id | INTEGER | YES | - | FK ไปยัง bulletin.comment(id) — ความเห็นที่ถูกอ้างถึง (ลบแล้วตั้งเป็น NULL) |
status | VARCHAR(20) | NO | 'published' | สถานะความเห็น |
reaction_count | INTEGER | NO | 0 | จำนวน reaction — trigger คำนวณให้ |
report_count | INTEGER | NO | 0 | จำนวนรายงานที่ยังเปิดอยู่ — trigger คำนวณให้ |
edited_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ผู้เขียนแก้ไขเนื้อหา |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่สร้าง |
updated_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่แก้ไขล่าสุด |
deleted_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ soft delete |
images | JSONB | NO | '[]' | รูปแนบในความเห็น (migration 005) |
ตาราง bulletin.reaction
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
target_type | VARCHAR(10) | NO | - | ชนิดเป้าหมาย: post หรือ comment |
target_id | INTEGER | NO | - | id ของกระทู้หรือความเห็นที่ถูก react |
line_oa_id | INTEGER | NO | - | LINE OA ที่ reaction นี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
line_user_id | VARCHAR(255) | NO | - | LINE user id ของผู้ react |
emoji | VARCHAR(16) | NO | - | อีโมจิที่ใช้ react |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่ react |
ตาราง bulletin.report
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
target_type | VARCHAR(10) | NO | - | ชนิดเป้าหมาย: post หรือ comment |
target_id | INTEGER | NO | - | id ของกระทู้หรือความเห็นที่ถูกรายงาน |
board_id | INTEGER | NO | - | กระดานต้นสังกัด (ใช้กับคิว moderation) |
line_oa_id | INTEGER | NO | - | LINE OA ที่รายงานนี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
reporter_line_user_id | VARCHAR(255) | NO | - | LINE user id ของผู้รายงาน |
reason | VARCHAR(50) | NO | - | เหตุผลการรายงาน (ค่าจากรายการที่กำหนดไว้) |
detail | TEXT | YES | - | รายละเอียดเพิ่มเติมจากผู้รายงาน |
status | VARCHAR(20) | NO | 'open' | สถานะการจัดการรายงาน |
resolved_by | INTEGER | YES | - | ผู้ดูแลที่ปิดเรื่อง |
resolved_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ปิดเรื่อง |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่รายงาน |
ตาราง bulletin.block
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | SERIAL | NO | auto increment | Primary key |
board_id | INTEGER | NO | - | กระดานที่การระงับนี้มีผล |
line_oa_id | INTEGER | NO | - | LINE OA ที่การระงับนี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
line_user_id | VARCHAR(255) | NO | - | LINE user id ของผู้ถูกระงับ |
scope | VARCHAR(20) | NO | 'all' | ขอบเขตการระงับ เช่น all หรือ comment |
reason | TEXT | YES | - | เหตุผลการระงับ |
expires_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่การระงับหมดอายุ (NULL = ไม่มีกำหนด) |
created_by | INTEGER | YES | - | ผู้ดูแลที่สั่งระงับ |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_TIMESTAMP | วันเวลาที่สั่งระงับ |
revoked_date | TIMESTAMPTZ(3) | YES | - | วันเวลาที่ยกเลิกการระงับ |
revoked_by | INTEGER | YES | - | ผู้ดูแลที่ยกเลิกการระงับ |
ตาราง bulletin.audit_log
| Column | Type | Nullable | Default | คำอธิบาย |
|---|---|---|---|---|
id | BIGSERIAL | NO | auto increment | Primary key |
board_id | INTEGER | YES | - | กระดานที่เกี่ยวข้อง (NULL ได้สำหรับเหตุการณ์ที่ไม่ผูกกระดาน) |
line_oa_id | INTEGER | NO | - | LINE OA ที่เหตุการณ์นี้สังกัด |
organization_id | INTEGER | NO | - | องค์กรเจ้าของข้อมูล |
actor_type | VARCHAR(20) | NO | - | ชนิดผู้กระทำ เช่น admin / line_user / system |
actor_id | VARCHAR(255) | YES | - | รหัสผู้กระทำ |
actor_name | VARCHAR(255) | YES | - | ชื่อผู้กระทำ ณ เวลาที่บันทึก |
action | VARCHAR(50) | NO | - | การกระทำ เช่น ลบกระทู้ อนุมัติความเห็น ระงับผู้ใช้ |
target_type | VARCHAR(20) | NO | - | ชนิดของสิ่งที่ถูกกระทำ |
target_id | INTEGER | YES | - | id ของสิ่งที่ถูกกระทำ |
before | JSONB | YES | - | ข้อมูลก่อนเปลี่ยนแปลง |
after | JSONB | YES | - | ข้อมูลหลังเปลี่ยนแปลง |
ip | VARCHAR(45) | YES | - | IP address ของผู้กระทำ |
user_agent | TEXT | YES | - | User agent ของผู้กระทำ |
created_date | TIMESTAMPTZ(3) | NO | CURRENT_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 ตอบ 403BULLETIN_CATEGORY_ADMIN_ONLY) การซ่อนหมวดหมู่ในหน้าเขียนกระทู้เป็นเพียงความสะดวก - หมวดหมู่มีสองแกนสิทธิ์ที่แยกจากกัน: แกนมองเห็น (
access_mode+audience_ids) และ แกนโพสต์ (post_access) ทั้งสองแกนประกอบกัน ไม่มีแกนใดบ่งชี้อีกแกน — คู่ที่ใช้บ่อยคือaccess_mode = 'inherit'+post_access = 'admin'ซึ่งได้หมวด "ประกาศ" ที่ทุกคนอ่านได้ แต่เขียนได้จาก CMS เท่านั้น - Unique:
board.line_oa_idunique (หนึ่งกระดานต่อ 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_dateun-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 ของผู้ดูแล ไม่ใช่เนื้อหา