Files
谢宇宁andClaude Opus 5.5 0fbdba5ccf Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:47:47 +07:00

219 lines
5.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package playletser
import (
"fmt"
"net/url"
"strconv"
"strings"
"videoweb/app/service/upstream"
"videoweb/pub/common/log"
"github.com/gin-gonic/gin"
)
const (
topicDetailPath = "/api/app/playlet/topic/detail"
defaultTopicDetailPageSize = 24
// topic/detail sortType:1编辑排序 2最新 3最热
topicDetailSortEditor = 1
topicDetailSortNewest = 2
topicDetailSortHeat = 3
)
// TopicDetailQuery 对应 GET /api/app/playlet/topic/detail 查询参数。
// 上游必填 id;路径优先 slug,服务端解析后再请求。
type TopicDetailQuery struct {
ID int64
Slug string
PageNum int
PageSize int
SortType int // 1编辑排序 2最新 3最热
}
// Normalize 校正分页与排序默认值。
func (q *TopicDetailQuery) Normalize() {
if q.PageNum <= 0 {
q.PageNum = 1
}
if q.PageSize <= 0 {
q.PageSize = defaultTopicDetailPageSize
}
if q.PageSize > 50 {
q.PageSize = 50
}
switch q.SortType {
case topicDetailSortNewest, topicDetailSortHeat:
default:
q.SortType = topicDetailSortEditor
}
if q.ID < 0 {
q.ID = 0
}
q.Slug = strings.TrimSpace(q.Slug)
}
// ParseTopicDetailQuery 从路径参数解析专题详情筛选。
// 路由:/topic-detail/:slug/ 、/topic-detail/:slug/page/:page/ 、
//
// /topic-detail/:slug/sort/:sortType/ 、/topic-detail/:slug/sort/:sortType/page/:page/
func ParseTopicDetailQuery(c *gin.Context) TopicDetailQuery {
q := TopicDetailQuery{PageSize: defaultTopicDetailPageSize}
ident := strings.TrimSpace(c.Param("slug"))
if ident != "" {
if decoded, err := url.PathUnescape(ident); err == nil {
ident = strings.TrimSpace(decoded)
}
}
if id, err := strconv.ParseInt(ident, 10, 64); err == nil && id > 0 {
q.ID = id
} else {
q.Slug = ident
}
if n, err := strconv.Atoi(strings.TrimSpace(c.Param("sortType"))); err == nil {
q.SortType = n
}
if n, err := strconv.Atoi(strings.TrimSpace(c.Param("page"))); err == nil {
q.PageNum = n
}
q.Normalize()
return q
}
// TopicDetailURL 生成专题详情筛选链接(纯路径,无 query)。
func TopicDetailURL(slug string, id int64, sortType, pageNum int) string {
q := TopicDetailQuery{
ID: id,
Slug: slug,
PageNum: pageNum,
PageSize: defaultTopicDetailPageSize,
SortType: sortType,
}
q.Normalize()
return q.URL()
}
// URL 输出:/topic-detail/{slug|id}/[sort/{s}/][page/{n}/]
func (q TopicDetailQuery) URL() string {
q.Normalize()
segment := pathSegment(q.Slug, q.ID)
if segment == "" {
return "/topic"
}
base := "/topic-detail/" + segment
if q.SortType == topicDetailSortEditor {
if q.PageNum <= 1 {
return base
}
return base + "/page/" + strconv.Itoa(q.PageNum)
}
path := base + "/sort/" + strconv.Itoa(q.SortType)
if q.PageNum > 1 {
path += "/page/" + strconv.Itoa(q.PageNum)
}
return path
}
// BuildTopicDetailPagination 专题详情作品分页。
func BuildTopicDetailPagination(q TopicDetailQuery, totalCount int64) PaginationVM {
q.Normalize()
return BuildPagination(func(p int) string {
nq := q
nq.PageNum = p
return nq.URL()
}, q.PageNum, q.PageSize, totalCount)
}
// ResolveTopicDetailID 路径为 slug 时,用专题列表解析上游必填 id。
func ResolveTopicDetailID(c *gin.Context, q *TopicDetailQuery) {
if q == nil || q.ID > 0 {
return
}
q.Normalize()
slug := q.Slug
if slug == "" {
return
}
for page := 1; page <= 5; page++ {
raw := FetchTopicListOrEmpty(c, TopicListQuery{
PageNum: page,
PageSize: 50,
SortType: topicSortNewest,
})
for _, item := range raw.List {
if strings.EqualFold(strings.TrimSpace(item.Slug), slug) {
q.ID = item.ID
q.Slug = strings.TrimSpace(item.Slug)
return
}
}
if len(raw.List) == 0 || int64(page*50) >= raw.TotalCount {
return
}
}
}
// FetchTopicDetail 拉取 GET /api/app/playlet/topic/detail。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_topic_detail
func FetchTopicDetail(c *gin.Context, q TopicDetailQuery) (*PlayletTopicDetailResp, error) {
q.Normalize()
if q.ID <= 0 {
return nil, fmt.Errorf("playlet topic detail missing id")
}
params := url.Values{}
params.Set("id", strconv.FormatInt(q.ID, 10))
params.Set("pageNum", strconv.Itoa(q.PageNum))
params.Set("pageSize", strconv.Itoa(q.PageSize))
params.Set("sortType", strconv.Itoa(q.SortType))
var env APIEnvelope[PlayletTopicDetailResp]
if err := upstream.GetPublicJSON(c, topicDetailPath+"?"+params.Encode(), &env); err != nil {
return nil, err
}
if env.Code != 200 {
return nil, fmt.Errorf("playlet topic detail code=%d msg=%s", env.Code, env.Msg)
}
return &env.Data, nil
}
// FetchTopicDetailOrEmpty 失败时返回空详情。
func FetchTopicDetailOrEmpty(c *gin.Context, q TopicDetailQuery) *PlayletTopicDetailResp {
data, err := FetchTopicDetail(c, q)
if err != nil {
log.Error("fetch playlet topic detail failed", log.E(err))
return &PlayletTopicDetailResp{ID: q.ID, Slug: q.Slug, PageNum: q.PageNum, PageSize: q.PageSize}
}
if data == nil {
return &PlayletTopicDetailResp{ID: q.ID, Slug: q.Slug, PageNum: q.PageNum, PageSize: q.PageSize}
}
data.SeoInfo.Normalize()
return data
}
// TopicDetailCoverURLs 专题详情顶部封面,最多 3 张。
func TopicDetailCoverURLs(detail PlayletTopicDetailResp) []string {
return TopicCoverURLs(PlayletTopicPageItem{Covers: detail.Covers, List: detail.List})
}
// TopicDetailHeroCover 头图背景用第一张封面。
func TopicDetailHeroCover(detail PlayletTopicDetailResp) string {
covers := TopicDetailCoverURLs(detail)
if len(covers) == 0 {
return ""
}
return covers[0]
}
// TopicDetailSortLabel 排序文案。
func TopicDetailSortLabel(sortType int) string {
switch sortType {
case topicDetailSortNewest:
return "最新"
case topicDetailSortHeat:
return "最热"
default:
return "编辑排序"
}
}