1
All checks were successful
Deploy API / deploy (push) Successful in 34s

This commit is contained in:
2026-05-26 12:08:39 +08:00
parent 09089e1335
commit b9834d9300
6 changed files with 238 additions and 16 deletions

View File

@@ -0,0 +1,78 @@
package handlers
import "strings"
// Preset post tag slugs for admin multi-select (简体中文 label in NameZh).
var presetPostTags = []struct {
Slug string
NameZh string
}{
{Slug: "official-recommended", NameZh: "官方推荐"},
{Slug: "newcomer", NameZh: "新人必看"},
{Slug: "week-featured", NameZh: "本周主推"},
{Slug: "shareable", NameZh: "可转发"},
{Slug: "downloadable", NameZh: "可下载"},
{Slug: "image", NameZh: "图片"},
{Slug: "video", NameZh: "视频"},
{Slug: "ppt", NameZh: "PPT"},
{Slug: "pdf", NameZh: "PDF"},
{Slug: "copy", NameZh: "文案"},
{Slug: "tutorial", NameZh: "教程"},
{Slug: "announcement", NameZh: "公告"},
{Slug: "event", NameZh: "活动"},
{Slug: "poster", NameZh: "海报"},
{Slug: "news", NameZh: "新闻"},
{Slug: "materials", NameZh: "物料"},
{Slug: "class", NameZh: "课堂"},
{Slug: "tweet", NameZh: "推文"},
}
var presetPostTagBySlug map[string]string
var presetPostTagByName map[string]string
func init() {
presetPostTagBySlug = make(map[string]string, len(presetPostTags))
presetPostTagByName = make(map[string]string, len(presetPostTags))
for _, t := range presetPostTags {
presetPostTagBySlug[t.Slug] = t.NameZh
presetPostTagByName[t.NameZh] = t.Slug
// Traditional variants from legacy data
switch t.Slug {
case "official-recommended":
presetPostTagByName["官方推薦"] = t.Slug
case "shareable":
presetPostTagByName["可轉發"] = t.Slug
case "downloadable":
presetPostTagByName["可下載"] = t.Slug
case "image":
presetPostTagByName["圖片"] = t.Slug
case "video":
presetPostTagByName["影片"] = t.Slug
case "event":
presetPostTagByName["活動"] = t.Slug
case "poster":
presetPostTagByName["海報"] = t.Slug
case "news":
presetPostTagByName["新聞"] = t.Slug
case "class":
presetPostTagByName["課堂"] = t.Slug
}
}
// Legacy slugs from 001_init.sql
presetPostTagBySlug["official"] = "官方推荐"
presetPostTagByName["official"] = "official-recommended"
}
func normalizePostTagSlug(raw string) (slug string, ok bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", false
}
if _, ok := presetPostTagBySlug[raw]; ok {
return raw, true
}
if slug, ok := presetPostTagByName[raw]; ok {
return slug, true
}
return "", false
}

View File

@@ -240,6 +240,11 @@ func validateAdminPost(ap *adminPost) error {
if ap.Status == "" {
ap.Status = "draft"
}
normalized, err := normalizeAdminPostTags(ap.Tags)
if err != nil {
return err
}
ap.Tags = normalized
return nil
}
@@ -284,40 +289,41 @@ func loadAttachmentsByPostIDs(ctx context.Context, pool *pgxpool.Pool, ids []uui
return out, rows.Err()
}
func loadPostTagNames(ctx context.Context, pool *pgxpool.Pool, postID uuid.UUID) ([]string, error) {
func loadPostTagNames(ctx context.Context, pool *pgxpool.Pool, postID uuid.UUID, lang string) ([]string, error) {
rows, err := pool.Query(ctx, `
SELECT t.name FROM post_tags pt JOIN tags t ON t.id = pt.tag_id WHERE pt.post_id = $1 ORDER BY t.name`, postID)
SELECT t.name, COALESCE(t.name_en,''), COALESCE(t.name_ja,''), COALESCE(t.name_ko,''),
COALESCE(t.name_vi,''), COALESCE(t.name_id,''), COALESCE(t.name_ms,''), t.slug
FROM post_tags pt JOIN tags t ON t.id = pt.tag_id
WHERE pt.post_id = $1 ORDER BY t.slug`, postID)
if err != nil {
return nil, err
}
defer rows.Close()
var names []string
for rows.Next() {
var n string
if err := rows.Scan(&n); err != nil {
var name, en, ja, ko, vi, id, ms, slug string
if err := rows.Scan(&name, &en, &ja, &ko, &vi, &id, &ms, &slug); err != nil {
return nil, err
}
names = append(names, n)
names = append(names, tagDisplayName(name, en, ja, ko, vi, id, ms, lang))
}
return names, rows.Err()
}
func replacePostTags(ctx context.Context, pool *pgxpool.Pool, postID uuid.UUID, tags []string) error {
func replacePostTags(ctx context.Context, pool *pgxpool.Pool, postID uuid.UUID, tagSlugs []string) error {
_, err := pool.Exec(ctx, `DELETE FROM post_tags WHERE post_id = $1`, postID)
if err != nil {
return err
}
for _, t := range tags {
t = strings.TrimSpace(t)
if t == "" {
for _, slug := range tagSlugs {
slug = strings.TrimSpace(slug)
if slug == "" {
continue
}
var tid int
slug := tagSlug(t)
err := pool.QueryRow(ctx, `INSERT INTO tags (name, slug) VALUES ($1, $2)
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name RETURNING id`, t, slug).Scan(&tid)
err := pool.QueryRow(ctx, `SELECT id FROM tags WHERE slug = $1`, slug).Scan(&tid)
if err != nil {
_ = pool.QueryRow(ctx, `SELECT id FROM tags WHERE slug = $1`, slug).Scan(&tid)
continue
}
_, _ = pool.Exec(ctx, `INSERT INTO post_tags (post_id, tag_id) VALUES ($1,$2) ON CONFLICT DO NOTHING`, postID, tid)
}

View File

@@ -85,7 +85,7 @@ func GetPost(w http.ResponseWriter, r *http.Request) {
if dto.Attachments == nil {
dto.Attachments = []AttachmentDTO{}
}
tags, _ := loadPostTagNames(r.Context(), pool, id)
tags, _ := loadPostTagNames(r.Context(), pool, id, requestLangCode(r))
dto.Tags = tags
writeJSON(w, dto)
}
@@ -143,8 +143,10 @@ func listPostsQuery(w http.ResponseWriter, r *http.Request, searchMode bool) {
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
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))`,
n, n, n, n, n, n, n, n, n)
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)
}
}
order := ` ORDER BY p.published_at DESC NULLS LAST, p.id DESC`

View File

@@ -0,0 +1,100 @@
package handlers
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func EnsureTagI18nSchema(ctx context.Context, pool *pgxpool.Pool) error {
_, err := pool.Exec(ctx, `
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_en TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_ja TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_ko TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_vi TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_id TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS name_ms TEXT NOT NULL DEFAULT '';
ALTER TABLE tags ADD COLUMN IF NOT EXISTS is_preset BOOLEAN NOT NULL DEFAULT FALSE`)
if err != nil {
return err
}
for _, t := range presetPostTags {
_, err := pool.Exec(ctx, `
INSERT INTO tags (name, slug, is_preset) VALUES ($1, $2, TRUE)
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, is_preset = TRUE`,
t.NameZh, t.Slug)
if err != nil {
return err
}
}
_, _ = pool.Exec(ctx, `UPDATE tags SET slug = 'official-recommended' WHERE slug = 'official'`)
return nil
}
func normalizeAdminPostTags(tags []string) ([]string, error) {
out := make([]string, 0, len(tags))
seen := map[string]bool{}
for _, raw := range tags {
slug, ok := normalizePostTagSlug(raw)
if !ok {
return nil, fmt.Errorf("unknown tag %q (use preset tags only)", strings.TrimSpace(raw))
}
if !seen[slug] {
seen[slug] = true
out = append(out, slug)
}
}
return out, nil
}
func tagDisplayName(name, en, ja, ko, vi, id, ms, lang string) string {
switch lang {
case "en":
if s := strings.TrimSpace(en); s != "" {
return s
}
case "ja":
if s := strings.TrimSpace(ja); s != "" {
return s
}
case "ko":
if s := strings.TrimSpace(ko); s != "" {
return s
}
case "vi":
if s := strings.TrimSpace(vi); s != "" {
return s
}
case "id":
if s := strings.TrimSpace(id); s != "" {
return s
}
case "ms":
if s := strings.TrimSpace(ms); s != "" {
return s
}
}
return strings.TrimSpace(name)
}
func loadPostTagSlugs(ctx context.Context, pool *pgxpool.Pool, postID uuid.UUID) ([]string, error) {
rows, err := pool.Query(ctx, `
SELECT t.slug FROM post_tags pt JOIN tags t ON t.id = pt.tag_id
WHERE pt.post_id = $1 ORDER BY t.slug`, postID)
if err != nil {
return nil, err
}
defer rows.Close()
var slugs []string
for rows.Next() {
var s string
if err := rows.Scan(&s); err != nil {
return nil, err
}
slugs = append(slugs, s)
}
return slugs, rows.Err()
}