บอร์ดประกาศ — ข้อมูลบอร์ดและฟีดโพสต์
ภาพรวม
สอง endpoint สำหรับการอ่านบอร์ด ได้แก่ ข้อมูลระดับบอร์ด (settings, ตัวตนของ OA และรายการหมวดหมู่ที่ผู้เรียกมองเห็นได้) และฟีดโพสต์แบบ keyset pagination พร้อมแถบโพสต์ปักหมุด และสถานะรีแอ็กชันของผู้เรียกที่ decorate มาให้เรียบร้อยแล้ว
บอร์ดนี้ถูกออกแบบให้ ไม่ระบุชื่อผู้เขียน (pseudonymous) — PostView ไม่มีชื่อหรือรูปของผู้โพสต์อยู่เลย ตัวตนเดียวที่ส่งมาคือของ OA ซึ่งใช้ render โพสต์ที่เขียนจาก CMS ให้แสดงว่า "มาจากเพจ" และเนื่องจากเป็นค่าเดียวกันทุกโพสต์ จึงส่งมาครั้งเดียวพร้อมข้อมูลบอร์ด ไม่ซ้ำในทุกการ์ด
Business Flow
GET /api/bulletin/:hash
- เรียก
resolveAccessแล้วตรวจCanView()— ไม่ผ่านตอบ 403 - โหลดหมวดหมู่ของบอร์ด แล้ว กรองด้วย
CanSeeCategoryตอนส่งออก เพราะfindCategoriesSQLกรองเฉพาะ board, status และ deleted_date โดยไม่รู้เรื่องaccess_modeเลย หากไม่กรองในขั้นนี้ guest และ member ที่อยู่นอก audience จะอ่านชื่อและ audience id ของหมวดที่ถูกจำกัดไว้ได้ - ตอบกลับ
{boardId, settings, oaName, oaPictureUrl, categories}โดยoaPictureUrlresolve จากline_oa.coverเป็น public URL และเป็นสตริงว่างเมื่อไม่มีค่า ไม่ใช่ URL ที่ไม่สมบูรณ์ เพื่อให้ client fallback ด้วยการตรวจ falsy ธรรมดาได้
GET /api/bulletin/:hash/posts
Query parameter ที่รับ: category (int64 ไม่บังคับ), cursor (ค่า opaque ไม่บังคับ) และ limit (ค่าเริ่มต้น 20 โดย repository จำกัดเพดานไว้ที่ 50)
- เรียก
resolveAccessแล้วตรวจCanView() - ค่า
categoryที่ parse ไม่ได้ตอบ 400invalid categoryส่วน cursor ที่พังรูปจะถูกยอมรับว่าเป็น "เริ่มจากบนสุด" ไม่ใช่ 400 ซึ่งเป็นพฤติกรรมที่ตั้งใจไว้ - แถบปักหมุดจะแนบมาเฉพาะหน้าแรกที่ไม่ได้กรองหมวด คือเมื่อไม่มีทั้ง
categoryIDและcursorเพราะแถบนี้ไม่ได้ paginate หากแนบทุกหน้าจะเกิดข้อมูลซ้ำ โดยจำกัดไว้ที่ 5 โพสต์ผ่านค่าคงที่maxPinnedPostsระดับ package ซึ่งไม่ใช่ setting และ endpoint ปักหมุดฝั่ง CMS ปฏิเสธจำนวนที่เกินนี้อยู่แล้ว - ดึงฟีดหนึ่งหน้าด้วย
FindFeed(boardID, accessCtx, categoryID, cursor, limit)ซึ่ง SQL เข้ารหัสกฎการมองเห็นไว้ในตัว — โพสต์สถานะpublishedเห็นได้เมื่อผู้เรียกเห็นหมวดของมัน, โพสต์สถานะpendingเห็นได้เฉพาะเจ้าของ ส่วนhiddenและdeletedไม่มีใครเห็น - Cursor — หน้าที่เต็ม limit จะได้
nextCursorเป็นค่า base64 ของ"unixMilli:id"ของแถวสุดท้าย โดยอ้างอิงคอลัมน์last_activity_dateซึ่งเป็นคอลัมน์ที่ฟีด ORDER BY จริง หากใช้created_dateแทน หน้าที่ 2 จะทั้งซ้ำและข้ามข้อมูล cursor เป็นค่า opaque โดยเจตนา — client ที่ไม่ได้รับมาจะสร้างขึ้นเองไม่ได้ - Decorate รีแอ็กชันเป็น batch เดียว ครอบคลุมทั้งแถบปักหมุดและฟีด ผ่าน
ReactionStates(targetType, ids, lineOaID, lineUserID)แล้วจึงDecoratePostViewsผลคือทุกการ์ดถูกส่งออกมาพร้อมสถานะรีแอ็กชันของผู้เรียก โดยไม่มี query แยกต่อการ์ด - ตอบกลับ
{pinned, posts, nextCursor}โดยnextCursorมีเฉพาะเมื่อยังมีหน้าถัดไป
PostView และ CommentView
ทั้งสองเป็น projection ที่อ่าน author_line_user_id เพียงเพื่อคำนวณค่า isMine แล้วไม่คัดลอกค่านั้นออกไปที่ใดเลย ส่วน images ถูก decode จาก jsonb เป็น array ของ key
ไฟล์และฟังก์ชันหลัก
| Route | Handler |
|---|---|
GET /api/bulletin/:hash | internal/bulletin/handler.go → (*Handler).GetBoard |
GET /api/bulletin/:hash/posts | (*Handler).ListPosts |
internal/bulletin/repository.go—FindPinned,FindFeed,ReactionStates,FindCategories,categoryVisibilitySQL,filterByCategoryinternal/bulletin/view.go—PostView,NewPostView,NewPostViews,DecoratePostViews,CategoryView,visibleCategoryViews,decodeImages,ownedBy,publicURLOfinternal/bulletin/entity.go—Post,Category,ReactionState,FeedCursor,EncodeCursor,DecodeCursor,maxPinnedPosts- Response type ภายใน —
boardView,feedView
จุดเชื่อมต่อกับ Service อื่น
- ฐานข้อมูล — ตาราง
bulletin.board,bulletin.category,bulletin.post,bulletin.reactionและline_oa - Object storage —
internal/storagexใช้ resolveline_oa.coverเป็น public URL โดยรองรับค่า nil ซึ่งจะได้สตริงว่าง - Bulletin access control — เรียก
resolveAccess,CanViewและCanSeeCategory - App enabled guard — ควบคุมการเปิดปิดฟีเจอร์ระดับ platform
- client-web — ตรงกับฟีเจอร์
bulletin-board