Skip to main content

บอร์ดประกาศ — ข้อมูลบอร์ดและฟีดโพสต์

ภาพรวม

สอง endpoint สำหรับการอ่านบอร์ด ได้แก่ ข้อมูลระดับบอร์ด (settings, ตัวตนของ OA และรายการหมวดหมู่ที่ผู้เรียกมองเห็นได้) และฟีดโพสต์แบบ keyset pagination พร้อมแถบโพสต์ปักหมุด และสถานะรีแอ็กชันของผู้เรียกที่ decorate มาให้เรียบร้อยแล้ว

บอร์ดนี้ถูกออกแบบให้ ไม่ระบุชื่อผู้เขียน (pseudonymous) — PostView ไม่มีชื่อหรือรูปของผู้โพสต์อยู่เลย ตัวตนเดียวที่ส่งมาคือของ OA ซึ่งใช้ render โพสต์ที่เขียนจาก CMS ให้แสดงว่า "มาจากเพจ" และเนื่องจากเป็นค่าเดียวกันทุกโพสต์ จึงส่งมาครั้งเดียวพร้อมข้อมูลบอร์ด ไม่ซ้ำในทุกการ์ด

Business Flow

GET /api/bulletin/:hash

  1. เรียก resolveAccess แล้วตรวจ CanView() — ไม่ผ่านตอบ 403
  2. โหลดหมวดหมู่ของบอร์ด แล้ว กรองด้วย CanSeeCategory ตอนส่งออก เพราะ findCategoriesSQL กรองเฉพาะ board, status และ deleted_date โดยไม่รู้เรื่อง access_mode เลย หากไม่กรองในขั้นนี้ guest และ member ที่อยู่นอก audience จะอ่านชื่อและ audience id ของหมวดที่ถูกจำกัดไว้ได้
  3. ตอบกลับ {boardId, settings, oaName, oaPictureUrl, categories} โดย oaPictureUrl resolve จาก line_oa.cover เป็น public URL และเป็นสตริงว่างเมื่อไม่มีค่า ไม่ใช่ URL ที่ไม่สมบูรณ์ เพื่อให้ client fallback ด้วยการตรวจ falsy ธรรมดาได้

GET /api/bulletin/:hash/posts

Query parameter ที่รับ: category (int64 ไม่บังคับ), cursor (ค่า opaque ไม่บังคับ) และ limit (ค่าเริ่มต้น 20 โดย repository จำกัดเพดานไว้ที่ 50)

  1. เรียก resolveAccess แล้วตรวจ CanView()
  2. ค่า category ที่ parse ไม่ได้ตอบ 400 invalid category ส่วน cursor ที่พังรูปจะถูกยอมรับว่าเป็น "เริ่มจากบนสุด" ไม่ใช่ 400 ซึ่งเป็นพฤติกรรมที่ตั้งใจไว้
  3. แถบปักหมุดจะแนบมาเฉพาะหน้าแรกที่ไม่ได้กรองหมวด คือเมื่อไม่มีทั้ง categoryID และ cursor เพราะแถบนี้ไม่ได้ paginate หากแนบทุกหน้าจะเกิดข้อมูลซ้ำ โดยจำกัดไว้ที่ 5 โพสต์ผ่านค่าคงที่ maxPinnedPosts ระดับ package ซึ่งไม่ใช่ setting และ endpoint ปักหมุดฝั่ง CMS ปฏิเสธจำนวนที่เกินนี้อยู่แล้ว
  4. ดึงฟีดหนึ่งหน้าด้วย FindFeed(boardID, accessCtx, categoryID, cursor, limit) ซึ่ง SQL เข้ารหัสกฎการมองเห็นไว้ในตัว — โพสต์สถานะ published เห็นได้เมื่อผู้เรียกเห็นหมวดของมัน, โพสต์สถานะ pending เห็นได้เฉพาะเจ้าของ ส่วน hidden และ deleted ไม่มีใครเห็น
  5. Cursor — หน้าที่เต็ม limit จะได้ nextCursor เป็นค่า base64 ของ "unixMilli:id" ของแถวสุดท้าย โดยอ้างอิงคอลัมน์ last_activity_date ซึ่งเป็นคอลัมน์ที่ฟีด ORDER BY จริง หากใช้ created_date แทน หน้าที่ 2 จะทั้งซ้ำและข้ามข้อมูล cursor เป็นค่า opaque โดยเจตนา — client ที่ไม่ได้รับมาจะสร้างขึ้นเองไม่ได้
  6. Decorate รีแอ็กชันเป็น batch เดียว ครอบคลุมทั้งแถบปักหมุดและฟีด ผ่าน ReactionStates(targetType, ids, lineOaID, lineUserID) แล้วจึง DecoratePostViews ผลคือทุกการ์ดถูกส่งออกมาพร้อมสถานะรีแอ็กชันของผู้เรียก โดยไม่มี query แยกต่อการ์ด
  7. ตอบกลับ {pinned, posts, nextCursor} โดย nextCursor มีเฉพาะเมื่อยังมีหน้าถัดไป

PostView และ CommentView

ทั้งสองเป็น projection ที่อ่าน author_line_user_id เพียงเพื่อคำนวณค่า isMine แล้วไม่คัดลอกค่านั้นออกไปที่ใดเลย ส่วน images ถูก decode จาก jsonb เป็น array ของ key

ไฟล์และฟังก์ชันหลัก

RouteHandler
GET /api/bulletin/:hashinternal/bulletin/handler.go(*Handler).GetBoard
GET /api/bulletin/:hash/posts(*Handler).ListPosts
  • internal/bulletin/repository.goFindPinned, FindFeed, ReactionStates, FindCategories, categoryVisibilitySQL, filterByCategory
  • internal/bulletin/view.goPostView, NewPostView, NewPostViews, DecoratePostViews, CategoryView, visibleCategoryViews, decodeImages, ownedBy, publicURLOf
  • internal/bulletin/entity.goPost, Category, ReactionState, FeedCursor, EncodeCursor, DecodeCursor, maxPinnedPosts
  • Response type ภายใน — boardView, feedView

จุดเชื่อมต่อกับ Service อื่น

  • ฐานข้อมูล — ตาราง bulletin.board, bulletin.category, bulletin.post, bulletin.reaction และ line_oa
  • Object storageinternal/storagex ใช้ resolve line_oa.cover เป็น public URL โดยรองรับค่า nil ซึ่งจะได้สตริงว่าง
  • Bulletin access control — เรียก resolveAccess, CanView และ CanSeeCategory
  • App enabled guard — ควบคุมการเปิดปิดฟีเจอร์ระดับ platform
  • client-web — ตรงกับฟีเจอร์ bulletin-board