Files
Arkie-Library-Backend/internal/handlers/posts_public.go

218 lines
6.1 KiB
Go
Raw Normal View History

2026-05-25 16:45:33 +08:00
package handlers
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
func ListPosts(w http.ResponseWriter, r *http.Request) {
listPostsQuery(w, r, false)
}
func ListPostsRecommended(w http.ResponseWriter, r *http.Request) {
pool := poolFrom(r)
limit := postLimitDef(r, 12, 50)
rows, err := pool.Query(r.Context(), postSelectBase()+`
WHERE `+publicPostWhere+` AND p.is_recommended = TRUE
ORDER BY p.sort_order ASC, p.published_at DESC NULLS LAST, p.id DESC
LIMIT $1`, limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
items, err := collectPostRows(r, rows)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]any{"items": items})
}
func ListPostsLatest(w http.ResponseWriter, r *http.Request) {
pool := poolFrom(r)
limit := postLimitDef(r, 8, 50)
rows, err := pool.Query(r.Context(), postSelectBase()+`
WHERE `+publicPostWhere+`
ORDER BY p.published_at DESC NULLS LAST, p.id DESC
LIMIT $1`, limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
items, err := collectPostRows(r, rows)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]any{"items": items})
}
func SearchPosts(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
if q == "" {
writeJSON(w, PostListResponse{Items: []PostDTO{}})
return
}
listPostsQuery(w, r, true)
}
func GetPost(w http.ResponseWriter, r *http.Request) {
pool := poolFrom(r)
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "bad id", http.StatusBadRequest)
return
}
row := pool.QueryRow(r.Context(), postSelectBase()+`
WHERE p.id = $1 AND `+publicPostWhere, id)
dto, _, err := scanPostRow(r, row)
if err != nil {
if err == pgx.ErrNoRows {
http.NotFound(w, r)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
atts, _ := loadAttachmentsByPostIDs(r.Context(), pool, []uuid.UUID{id})
dto.Attachments = atts[id]
if dto.Attachments == nil {
dto.Attachments = []AttachmentDTO{}
}
2026-05-26 12:08:39 +08:00
tags, _ := loadPostTagNames(r.Context(), pool, id, requestLangCode(r))
2026-05-25 16:45:33 +08:00
dto.Tags = tags
writeJSON(w, dto)
}
func PostAttachmentDownload(w http.ResponseWriter, r *http.Request) {
pool := poolFrom(r)
postID, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "bad id", http.StatusBadRequest)
return
}
aid, err := uuid.Parse(chi.URLParam(r, "aid"))
if err != nil {
http.Error(w, "bad attachment id", http.StatusBadRequest)
return
}
cmd, err := pool.Exec(r.Context(), `
UPDATE posts SET download_count = download_count + 1, updated_at = NOW()
WHERE id = $1 AND status = 'published' AND is_public = TRUE
AND (published_at IS NULL OR published_at <= NOW())`, postID)
if err != nil || cmd.RowsAffected() == 0 {
http.NotFound(w, r)
return
}
writeJSON(w, map[string]any{"ok": true})
_ = aid
}
func listPostsQuery(w http.ResponseWriter, r *http.Request, searchMode bool) {
pool := poolFrom(r)
limit := postLimitDef(r, 20, 50)
cursor := strings.TrimSpace(r.URL.Query().Get("cursor"))
catSlug := strings.TrimSpace(r.URL.Query().Get("category"))
typ := strings.TrimSpace(r.URL.Query().Get("type"))
if typ == "" {
typ = "all"
}
langFilter := strings.TrimSpace(r.URL.Query().Get("language"))
base := postSelectBase() + ` WHERE ` + publicPostWhere
args := []any{}
cond := postTypeFilterSQL(typ, &args)
cond += postLanguageFilterSQL(langFilter, &args)
if catSlug != "" {
args = append(args, catSlug)
cond += fmt.Sprintf(` AND c.slug = $%d`, len(args))
}
if searchMode {
q := strings.TrimSpace(r.URL.Query().Get("q"))
if q != "" {
pat := "%" + q + "%"
args = append(args, pat)
n := len(args)
cond += fmt.Sprintf(` AND (
p.text_zh ILIKE $%d OR p.text_en ILIKE $%d OR p.text_ja ILIKE $%d OR p.text_ko ILIKE $%d OR
p.text_vi ILIKE $%d OR p.text_id ILIKE $%d OR p.text_ms ILIKE $%d OR
EXISTS (SELECT 1 FROM post_attachments pa WHERE pa.post_id = p.id AND pa.filename ILIKE $%d) OR
2026-05-26 12:08:39 +08:00
EXISTS (SELECT 1 FROM post_tags pt JOIN tags t ON t.id = pt.tag_id WHERE pt.post_id = p.id AND (
t.name ILIKE $%d OR t.name_en ILIKE $%d OR t.name_ja ILIKE $%d OR t.name_ko ILIKE $%d OR
t.name_vi ILIKE $%d OR t.name_id ILIKE $%d OR t.name_ms ILIKE $%d)))`,
n, n, n, n, n, n, n, n, n, n, n, n, n, n, n)
2026-05-25 16:45:33 +08:00
}
}
order := ` ORDER BY p.published_at DESC NULLS LAST, p.id DESC`
if cursor != "" {
pubT, cid, err := decodePostCursor(cursor)
if err != nil {
http.Error(w, "bad cursor", http.StatusBadRequest)
return
}
args = append(args, pubT, cid)
n := len(args)
cond += fmt.Sprintf(` AND (p.published_at, p.id) < ($%d::timestamptz, $%d::uuid)`, n-1, n)
}
args = append(args, limit+1)
limitArg := len(args)
q := base + cond + order + fmt.Sprintf(` LIMIT $%d`, limitArg)
rows, err := pool.Query(r.Context(), q, args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
items, err := collectPostRows(r, rows)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var nextCursor string
if len(items) > limit {
last := items[limit]
items = items[:limit]
if t, err := time.Parse(time.RFC3339, last.PublishedAt); err == nil {
uid, _ := uuid.Parse(last.ID)
nextCursor = encodePostCursor(t, uid)
}
}
writeJSON(w, PostListResponse{Items: items, NextCursor: nextCursor})
}
func collectPostRows(r *http.Request, rows pgx.Rows) ([]PostDTO, error) {
defer rows.Close()
var ids []uuid.UUID
list := make([]PostDTO, 0)
for rows.Next() {
dto, _, err := scanPostRow(r, rows)
if err != nil {
return nil, err
}
uid, _ := uuid.Parse(dto.ID)
ids = append(ids, uid)
dto.Attachments = []AttachmentDTO{}
list = append(list, dto)
}
if err := rows.Err(); err != nil {
return nil, err
}
atts, err := loadAttachmentsByPostIDs(r.Context(), poolFrom(r), ids)
if err != nil {
return nil, err
}
for i := range list {
uid, _ := uuid.Parse(list[i].ID)
if a, ok := atts[uid]; ok {
list[i].Attachments = a
}
}
return list, nil
}