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

This commit is contained in:
2026-05-26 11:12:22 +08:00
parent f8792f8db8
commit 09089e1335
3 changed files with 110 additions and 9 deletions

View File

@@ -23,6 +23,19 @@ type postLocalePayload struct {
Text string `json:"text"`
}
// requestLangCode is the normalized locale from ?lang= or Accept-Language (zh, en, ja, …).
func requestLangCode(r *http.Request) string {
raw := strings.TrimSpace(r.URL.Query().Get("lang"))
if raw == "" {
raw = r.Header.Get("Accept-Language")
}
raw = strings.TrimSpace(strings.Split(raw, ",")[0])
if raw == "" {
return "zh"
}
return translateNormalizePostLang(raw)
}
func (t postTextI18n) pick(r *http.Request) string {
return pickLangField(r, t.TextZh, t.TextEn, t.TextJa, t.TextKo, t.TextVi, t.TextId, t.TextMs)
}

View File

@@ -1,13 +1,18 @@
package handlers
import "strings"
import (
"regexp"
"strings"
)
// Valid post_type values (Browse filter chips + admin selector).
// Valid post_type values (Browse filter chips; inferred on admin save).
var validPostTypes = map[string]bool{
"image": true, "video": true, "music": true, "ppt": true, "pdf": true,
"link": true, "text": true, "archive": true,
}
var postTextURLPattern = regexp.MustCompile(`https?://`)
func normalizePostType(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
switch s {
@@ -33,3 +38,84 @@ func normalizePostType(raw string) string {
}
return "text"
}
// inferPostTypeFromContent picks post_type from attachments and text (admin no longer sends postType).
func inferPostTypeFromContent(text string, attachments []attachmentInput) string {
hasText := strings.TrimSpace(text) != ""
if len(attachments) == 0 {
if hasText && postTextURLPattern.MatchString(text) {
return "link"
}
return "text"
}
var image, video, music, ppt, pdf, archive bool
for _, a := range attachments {
img, vid, mus, p, pd, ar := attachmentPostTypeSignals(a)
image = image || img
video = video || vid
music = music || mus
ppt = ppt || p
pdf = pdf || pd
archive = archive || ar
}
// Specific file types first; image beats video when both exist (e.g. multi-photo posts).
switch {
case ppt:
return "ppt"
case pdf && !image && !video:
return "pdf"
case archive && !image && !video && !music && !ppt && !pdf:
return "archive"
case music && !image && !video:
return "music"
case image:
return "image"
case video:
return "video"
case pdf:
return "pdf"
case archive:
return "archive"
case music:
return "music"
}
if hasText && postTextURLPattern.MatchString(text) {
return "link"
}
return "text"
}
func attachmentPostTypeSignals(a attachmentInput) (image, video, music, ppt, pdf, archive bool) {
kind := strings.ToLower(strings.TrimSpace(a.Kind))
mime := strings.ToLower(strings.TrimSpace(a.Mime))
fn := strings.ToLower(strings.TrimSpace(a.Filename))
if kind == "" && mime != "" {
kind = classifyAttachmentKind(mime, fn)
}
if kind == "video" || strings.HasPrefix(mime, "video/") {
video = true
}
if kind == "image" || strings.HasPrefix(mime, "image/") {
image = true
}
if strings.HasPrefix(mime, "audio/") ||
strings.HasSuffix(fn, ".mp3") || strings.HasSuffix(fn, ".wav") ||
strings.HasSuffix(fn, ".m4a") || strings.HasSuffix(fn, ".flac") || strings.HasSuffix(fn, ".aac") {
music = true
}
if strings.Contains(mime, "pdf") || strings.HasSuffix(fn, ".pdf") {
pdf = true
}
if strings.Contains(mime, "presentation") ||
strings.HasSuffix(fn, ".ppt") || strings.HasSuffix(fn, ".pptx") {
ppt = true
}
if strings.HasSuffix(fn, ".zip") || strings.HasSuffix(fn, ".rar") ||
strings.HasSuffix(fn, ".7z") || strings.Contains(mime, "zip") {
archive = true
}
return
}

View File

@@ -31,7 +31,8 @@ type PostDTO struct {
PostType string `json:"postType"`
CategoryID int `json:"categoryId,omitempty"`
CategorySlug string `json:"categorySlug,omitempty"`
Language string `json:"language"`
Language string `json:"language"` // UI locale from ?lang= (matches text selection)
SourceLanguage string `json:"sourceLanguage,omitempty"` // DB source metadata (admin input language)
Text string `json:"text,omitempty"`
Localizations map[string]postLocalePayload `json:"localizations"`
Attachments []AttachmentDTO `json:"attachments"`
@@ -212,9 +213,9 @@ func translateNormalizePostLang(lang string) string {
}
func validateAdminPost(ap *adminPost) error {
ap.PostType = normalizePostType(ap.PostType)
if ap.PostType == "" || !validPostTypes[ap.PostType] {
return fmt.Errorf("postType required (image, video, music, ppt, pdf, link, text, archive)")
ap.PostType = inferPostTypeFromContent(ap.Text, ap.Attachments)
if !validPostTypes[ap.PostType] {
ap.PostType = "text"
}
if len(ap.Attachments) > maxPostAttachments {
return fmt.Errorf("too many attachments (max %d)", maxPostAttachments)
@@ -389,11 +390,11 @@ func scanPostRow(r *http.Request, row pgx.Row) (PostDTO, postTextI18n, error) {
var id uuid.UUID
var catID *int
var slug *string
var lang, postType string
var sourceLang, postType string
var pub, updated, created *time.Time
err := row.Scan(
&id, &texts.TextZh, &texts.TextEn, &texts.TextJa, &texts.TextKo, &texts.TextVi, &texts.TextId, &texts.TextMs,
&lang, &postType, &catID, &slug, &dto.IsRecommended, &pub, &updated, &created,
&sourceLang, &postType, &catID, &slug, &dto.IsRecommended, &pub, &updated, &created,
)
if err != nil {
return dto, texts, err
@@ -406,7 +407,8 @@ func scanPostRow(r *http.Request, row pgx.Row) (PostDTO, postTextI18n, error) {
if slug != nil {
dto.CategorySlug = *slug
}
dto.Language = lang
dto.SourceLanguage = sourceLang
dto.Language = requestLangCode(r)
dto.Text = texts.pick(r)
dto.Localizations = texts.toLocalizations()
if pub != nil {