รายการบทความและหมวดหมู่สาธารณะ
ภาพรวม
สอง endpoint ที่ให้หน้าเว็บดึงรายการบทความที่เผยแพร่แล้วของ OA หนึ่ง พร้อมตัวกรอง (หมวดหมู่ หมวดหมู่ย่อยแบบไล่ลงไปถึงลูกทั้งหมด คำค้น และช่วงวันที่เผยแพร่) รวมถึงดึงรายการหมวดหมู่และหมวดหมู่ย่อยสำหรับ filter bar
ทั้งสอง endpoint เป็น endpoint สาธารณะที่ไม่ต้องใช้ LIFF token จึงมี validation ที่เข้มงวดและมี rate limit 10 ครั้งต่อ 60 วินาที ต่อหนึ่ง IP
Business Flow
รายการบทความ — GET /api/public-content/contents
การตรวจสอบ query parameter
| Parameter | เงื่อนไข |
|---|---|
lineOaId | บังคับ เป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป |
categoryId / subcategoryId | ไม่บังคับ เป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป |
search | ไม่บังคับ ถูก sanitize ก่อน validate โดย trim แล้วลบอักขระพิเศษทิ้ง จากนั้นต้องผ่าน pattern ^[a-zA-Z0-9-\s\-_]*$ และยาวไม่เกิน 100 ตัวอักษร |
publishedDateFrom / publishedDateTo | ไม่บังคับ ต้องอยู่ในรูปแบบ ISO 8601 |
page | ไม่บังคับ ค่าระหว่าง 1–100 |
limit | ไม่บังคับ ค่าระหว่าง 1–50 |
Business rule
validateLineOa— แถวในline_oaต้อง active และไม่ถูก soft-delete มิฉะนั้นตอบ 404Invalid or inactive channelvalidateDateRange— เมื่อระบุมาทั้งสองด้าน ช่วงที่เกิน 365 วันตอบ 400Date range cannot exceed 365 daysและวันเริ่มต้นที่อยู่หลังวันสิ้นสุดตอบ 400publishedDateFrom must be before publishedDateToหากด้านใดด้านหนึ่ง parse ไม่ได้จะข้ามการตรวจไป- คำค้นที่สั้นกว่า 2 ตัวอักษรตอบ 400
Search term must be at least 2 characters - ค่าเริ่มต้นคือ
page=1และlimit=10โดยskip = (page-1)*limitและ หากskipเกิน 500 จะตอบ 400Page number exceeds maximum allowedเพื่อป้องกัน deep pagination - เมื่อระบุ
subcategoryIdระบบจะขยายเป็น หมวดหมู่ย่อยลูกหลานทั้งหมดที่ active ด้วย recursive CTE ผ่านFindActiveDescendantIDsแล้วกรองด้วยชุด id นั้น ผู้ใช้จึงเลือกหมวดย่อยระดับบนแล้วเห็นบทความของหมวดลูกด้วย - ดึงข้อมูลหนึ่งหน้าแล้ว hydrate เพิ่ม — โหลด
content_page_translationทุกภาษาใน batch เดียว ผ่านFindTranslationsByPageIDsและโหลดข้อมูลอ้างอิงหมวดหมู่/หมวดหมู่ย่อย (id,name,slug) โดย cache ต่อหนึ่งหน้า เพื่อไม่ให้เกิดปัญหา N+1 ข้อมูลอ้างอิงนี้อ่านแบบไม่กรอง active หรือ soft-delete เพื่อให้ผลลัพธ์ตรงกับพฤติกรรมเดิม และหมวดหมู่ที่หายไปจะไม่ปรากฏ key ในผลลัพธ์ - ตอบกลับ
{data, total, page, limit, totalPages}โดยtotalPagesคำนวณจากceil(total/limit)
รายการหมวดหมู่ — GET /api/public-content/contents/categories
lineOaIdถูกอ่านแบบ raw โดยไม่ผ่าน DTO ค่าที่ขาดหายหรือไม่ใช่ตัวเลขจึงกลายเป็น 0 และvalidateLineOaจะตอบ 404- คืนหมวดหมู่ที่ active ของ OA นั้นพร้อมหมวดหมู่ย่อยที่ active จัดกลุ่มไว้ใต้
category_idโดย repository เรียงลำดับตามsort_orderมาให้แล้ว - หมวดหมู่ที่ไม่มีหมวดหมู่ย่อยจะคืนค่าเป็น array ว่าง ไม่ใช่ null
ไฟล์และฟังก์ชันหลัก
| Route | Rate limit | Handler |
|---|---|---|
GET /api/public-content/contents | 10/60s | internal/publiccontent/handler.go → (*Handler).ListPublicContents |
GET /api/public-content/contents/categories | 10/60s | (*Handler).ListPublicCategories |
internal/publiccontent/register.go—Register(r, deps)ทำหน้าที่ mount ทั้ง 4 route ของโดเมนนี้internal/publiccontent/service.go—ListPublicContents,ListPublicCategories,validateLineOa,validateDateRange,buildListItems,mapToPublicListItem,parseJSDateinternal/publiccontent/handler.go—contentsDTO,byLinkDTO,numberTransform,searchTransform,isDateStringCheck- Repository —
internal/contentpage/repository.go(ListPublished,FindTranslationsByPageIDs),internal/contentcategory/repository.goและinternal/contentsubcategory/repository.go(FindActiveDescendantIDs)
จุดเชื่อมต่อกับ Service อื่น
- ฐานข้อมูล — ตาราง
content_page,content_page_translation,content_category,content_subcategoryและline_oa - Middleware —
middleware.RateLimit(10)ทำงานต่อ route และต่อ IP - ฟีเจอร์ที่เกี่ยวข้อง — ใช้ repository ร่วมกับ content link viewer และ content page viewer เนื่องจากทั้งสาม endpoint อยู่ใน package
publiccontentเดียวกัน - client-web — ตรงกับฟีเจอร์
content-link-listing(หน้ารายการพร้อมค้นหาและ infinite scroll) และcontent-page-viewerที่นำ metadata ของรายการไปใช้