บอร์ดประกาศ - คอมเมนต์และ quote
ภาพรวม
ฟีเจอร์นี้ดูแลเธรดคอมเมนต์ใต้โพสต์ในบอร์ดประกาศ ครอบคลุมการอ่านคอมเมนต์ทีละหน้า (เรียงจากเก่าไปใหม่), การเขียนคอมเมนต์พร้อมแนบรูปและอ้างอิง (quote) คอมเมนต์อื่น และการลบคอมเมนต์ของตนเอง
จุดออกแบบที่เป็นหัวใจของฟีเจอร์คือ การ resolve quote ที่ฝั่ง server แบบ batch ไม่ปล่อยให้ client จับคู่คอมเมนต์ต้นทางเอง ซึ่งแก้ปัญหาทั้งด้านความถูกต้องของข้อมูลและด้านสิทธิ์การมองเห็นไปพร้อมกัน
Business Flow
อ่านรายการคอมเมนต์ — GET /api/bulletin/:hash/posts/:id/comments
- ตรวจสิทธิ์ผ่าน
resolveAccess→CanView()→visiblePostForCaller(postID)โพสต์ที่ผู้เรียก มองไม่เห็นจะตอบ 404 ไม่ใช่คืนรายการคอมเมนต์ว่าง - รองรับ cursor + limit (ค่าเริ่มต้น 20) เช่นเดียวกับฟีดหลัก หาก cursor ผิดรูปแบบระบบจะเริ่มจากต้นเธรด
ListCommentsคืนเฉพาะคอมเมนต์สถานะ published รวมกับคอมเมนต์สถานะpendingของผู้เรียกเอง (ใช้ UNION ALL ใน SQL) ส่วนคอมเมนต์hiddenและdeletedไม่มีใครเห็นResolveQuotesทำงาน 1 query ต่อหนึ่งหน้า- รวบรวม
quoteCommentIdของทั้งหน้าแล้ว dedupe ก่อน (คอมเมนต์ยอดนิยมมักถูก quote หลายครั้ง ในหน้าเดียวกัน) จากนั้นดึงด้วยid = ANY(...)ครั้งเดียวเพื่อกันปัญหา N+1 - เหตุผลที่ต้องทำฝั่ง server: client รุ่นเดิมจับคู่ quote กับคอมเมนต์ที่ตัวเองมีอยู่ในมือ ทำให้ quote ที่ชี้ไปยังคอมเมนต์ในหน้าก่อนหน้าไม่ถูก render และที่แย่กว่านั้นคือ render สำเนาเก่าของคอมเมนต์ ที่ถูกลบไปแล้ว
- เป้าหมายที่ถูก soft delete, ถูก moderator ซ่อน, อยู่คนละ tenant หรือหายไปแล้ว จะถูกยุบเป็น
สถานะเดียวกันทั้งหมด คือ
{deleted:true, snippet:""}เพื่อให้ client แสดงข้อความ "คอมเมนต์นี้ถูกลบ" ได้โดยไม่ต้องเดา - ใช้ predicate
commentVisibleToCallerตัวเดียวกับการ listing ดังนั้น quote จะไม่มีทางเปิดเผย คอมเมนต์ที่ผู้เรียกอ่านในเธรดไม่ได้ QuoteViewมีเพียง{id, authorType, isMine, snippet}ไม่มี field ใดที่พาauthor_line_user_idออกไปได้ (รักษา pseudonymity) และ snippet ถูกตัดที่ 120 rune
- รวบรวม
- Response เป็น object
{comments, nextCursor?}ไม่ใช่ array เปล่า เพราะ cursor เป็นค่า opaque ถ้าไม่ส่งกลับมา client จะไปหน้าถัดไปไม่ได้ คอมเมนต์เรียงแบบ ASC ดังนั้น cursor คือแถวสุดท้ายของหน้า
สร้างคอมเมนต์ — POST /api/bulletin/:hash/posts/:id/comments (rate limit 10/60s)
- ตรวจ
CanComment()ซึ่งต่างจากCanWrite()ตรงที่ผู้ที่ถูก block ใน scope ใดก็ตามจากสองสโคป จะคอมเมนต์ไม่ได้ visiblePostForCallerตอบ 404 ถ้าผู้เรียกมองไม่เห็นโพสต์ ข้อสำคัญคือต้องเป็น 404 ไม่ใช่เปิดเผยBULLETIN_COMMENTS_CLOSED- โพสต์ที่ปิดคอมเมนต์ตอบ 403
BULLETIN_COMMENTS_CLOSED - เนื้อหาที่ trim แล้วว่างตอบ 400
BULLETIN_EMPTY_BODYส่วนเนื้อหาที่ยาวเกินmax_comment_lengthตอบBULLETIN_BODY_TOO_LONG - จำนวนรูปเกิน
max_images_per_comment(ค่าเริ่มต้น 2 ซึ่งเป็น setting แยกจากฝั่งโพสต์ที่เริ่มต้นที่ 4 เพื่อให้บอร์ดที่ขยายขีดจำกัดด้านหนึ่งไม่ขยายอีกด้านโดยไม่ตั้งใจ) ตอบBULLETIN_IMAGE_LIMIT quoteCommentIdต้องมีอยู่จริง อยู่บนโพสต์เดียวกัน และมีสถานะ published มิฉะนั้นตอบ 400"invalid quote comment"โดยการ lookup เป็นแบบ tenant-scoped- สถานะเริ่มต้นขึ้นกับ setting
require_comment_approvalถ้าเปิดจะเป็นpendingถ้าไม่เปิดจะเป็นpublished - ทำงานใน transaction เดียว: insert คอมเมนต์ → commit รูปผ่าน comment committer (key รูปแบบ
bulletin/comment/{id}/{index}.{ext}ต้องทำหลัง insert เพราะ key ต้องใช้ comment id) →UpdateCommentImages→ เขียน auditcomment.create - คืน
CommentViewที่ resolve quote มาให้เรียบร้อยแล้ว พร้อม status 201 client จึงได้โครงสร้าง ข้อมูลเหมือนกับที่ได้จาก listing โดยไม่ต้องมี special case
ลบคอมเมนต์ — DELETE /api/bulletin/:hash/comments/:id
- ตรวจสิทธิ์ด้วย
CanComment()ไม่ใช่CanWrite()เพราะผู้ที่ถูก block ใน scopecommentควรถูก ล็อกออกจากทุก action ของคอมเมนต์ รวมถึงการลบคอมเมนต์ของตัวเองด้วย ownedCommentตอบ 404 เหมือนกันทุกกรณี ไม่ว่าจะเป็นคอมเมนต์ที่ไม่มีอยู่ อยู่คนละ OA หรือไม่ใช่ ของผู้เรียก- ทำ soft delete พร้อมเขียน audit
comment.deleteแล้วตอบ 204
ไฟล์และฟังก์ชันหลัก
| Route | Rate limit | Handler |
|---|---|---|
GET /api/bulletin/:hash/posts/:id/comments | — | (*Handler).ListComments |
POST /api/bulletin/:hash/posts/:id/comments | 10/60s | (*Handler).CreateComment |
DELETE /api/bulletin/:hash/comments/:id | — | (*Handler).DeleteComment |
internal/bulletin/service.go—ListComments,CreateComment,DeleteComment,ResolveQuotes,ownedComment,commentVisibleToCaller,visiblePostForCaller,postVisibleToCaller,categoryVisible,GetPostinternal/bulletin/repository.go—ListComments(predicate แบบ UNION ALL),InsertComment,FindCommentByID,FindCommentsByIDs(แบบ batch),SoftDeleteComment,UpdateCommentImagesinternal/bulletin/view.go—CommentView,QuoteView,NewCommentView,NewCommentViews,quoteSnippet,maxQuoteSnippetRunesinternal/bulletin/committer.go— committer instance สำหรับimageKindComment
จุดเชื่อมต่อกับ Service อื่น
- ตารางฐานข้อมูล
bulletin.comment,bulletin.post,bulletin.audit_log - Object storage ผ่าน comment committer รายละเอียดดูที่ bulletin-post-write
- bulletin-access-control เป็นแหล่งของ
CanCommentและค่า cap ต่างๆ ใน settings GET /api/bulletin/:hash/posts/:id(GetPost) เป็น endpoint คู่กันที่ใช้ predicate การมองเห็น ชุดเดียวกัน- ตรงกับ client-web feature:
bulletin-post-detail