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

356 lines
9.5 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"
"sort"
"strconv"
"strings"
"videoweb/app/service/upstream"
"videoweb/pub/common/log"
"github.com/gin-gonic/gin"
)
const (
listPath = "/api/app/playlet/list"
allListPath = "/api/app/playlet/all/list"
themePath = "/api/app/theme/list"
defaultListPageSize = 24
// all/list sortType:1最新 2最热 3收藏最多
allSortNewest = 1
allSortHeat = 2
allSortCollect = 3
// all/list updateStatus:0全部 1连载中 2已完结
allUpdateAll = 0
allUpdateOngoing = 1
allUpdateDone = 2
sortHeat = 1 // 热度优先(旧 playlet/list)
sortTodayUpdate = 2 // 今日更新(旧 playlet/list)
// 站点统一类型:1-非成人 2-成人(与 themeType 一致)。
contentTypeNonAdult = 1
contentTypeAdult = 2
// playlet/list 上游 type 与站点定义相反:1-成人 2-非成人。
upstreamListTypeAdult = 1
upstreamListTypeNonAdult = 2
)
// ListQuery 短剧列表查询参数(旧 playlet/list)。
type ListQuery struct {
PageNum int
PageSize int
SortOrder int
ThemeID int64
ThemeSlug string
Type int
}
// AllListQuery 对应 GET /api/app/playlet/all/list 查询参数。
type AllListQuery struct {
PageNum int
PageSize int
SortType int // 1最新 2最热 3收藏最多
ThemeID int64
ThemeSlug string
UpdateStatus int // 0全部 1连载中 2已完结
}
// Normalize 校正分页与排序默认值。
func (q *ListQuery) Normalize() {
if q.PageNum <= 0 {
q.PageNum = 1
}
if q.PageSize <= 0 {
q.PageSize = defaultListPageSize
}
if q.PageSize > 50 {
q.PageSize = 50
}
if q.SortOrder != sortTodayUpdate {
q.SortOrder = sortHeat
}
if q.ThemeID < 0 {
q.ThemeID = 0
}
q.Type = normalizeContentType(q.Type)
}
// Normalize 校正 all/list 查询参数。
func (q *AllListQuery) Normalize() {
if q.PageNum <= 0 {
q.PageNum = 1
}
if q.PageSize <= 0 {
q.PageSize = defaultListPageSize
}
if q.PageSize > 50 {
q.PageSize = 50
}
switch q.SortType {
case allSortHeat, allSortCollect:
default:
q.SortType = allSortNewest
}
switch q.UpdateStatus {
case allUpdateOngoing, allUpdateDone:
default:
q.UpdateStatus = allUpdateAll
}
if q.ThemeID < 0 {
q.ThemeID = 0
}
q.ThemeSlug = strings.TrimSpace(q.ThemeSlug)
}
// ParseAllListQuery 从路径参数解析 all/list 筛选。
// 路由:/list/ 、/list/:theme/ 、/list/:theme/:updateStatus/:sortType/ 、
//
// /list/:theme/:updateStatus/:sortType/page/:page/
func ParseAllListQuery(c *gin.Context) AllListQuery {
q := AllListQuery{PageSize: defaultListPageSize}
theme := strings.TrimSpace(c.Param("theme"))
if theme != "" {
if decoded, err := url.PathUnescape(theme); err == nil {
theme = strings.TrimSpace(decoded)
}
}
if theme != "" && !strings.EqualFold(theme, "all") {
if id, err := strconv.ParseInt(theme, 10, 64); err == nil && id > 0 {
q.ThemeID = id
} else {
q.ThemeSlug = theme
}
}
if n, err := strconv.Atoi(strings.TrimSpace(c.Param("updateStatus"))); err == nil {
q.UpdateStatus = n
}
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
}
// AllListURL 生成列表页筛选链接(纯路径,无 query)。
func AllListURL(themeSlug string, themeID int64, updateStatus, sortType, pageNum int) string {
q := AllListQuery{
PageNum: pageNum,
PageSize: defaultListPageSize,
SortType: sortType,
ThemeID: themeID,
ThemeSlug: themeSlug,
UpdateStatus: updateStatus,
}
q.Normalize()
return q.URL()
}
// URL 输出路径模式:/list/{theme}/{updateStatus}/{sortType}/[page/{n}/]
// theme 优先 slug,无 slug 时才用 id;都没有则为 all。
func (q AllListQuery) URL() string {
q.Normalize()
slug := strings.TrimSpace(q.ThemeSlug)
id := q.ThemeID
if slug != "" {
id = 0
}
segment := pathSegment(slug, id)
if segment == "" {
segment = "all"
}
defaults := q.UpdateStatus == allUpdateAll && q.SortType == allSortNewest
if defaults && q.PageNum <= 1 {
if segment == "all" {
return "/list"
}
return "/list/" + segment
}
path := "/list/" + segment + "/" + strconv.Itoa(q.UpdateStatus) + "/" + strconv.Itoa(q.SortType)
if q.PageNum > 1 {
path += "/page/" + strconv.Itoa(q.PageNum)
}
return path
}
// ResolveAllListTheme 用题材列表补全 slug / id:路径与跳转优先 slug。
func ResolveAllListTheme(q *AllListQuery, themes []PlayletThemeItem) {
if q == nil {
return
}
q.Normalize()
slug := strings.TrimSpace(q.ThemeSlug)
if slug != "" {
for _, t := range themes {
if strings.TrimSpace(t.Slug) == slug {
q.ThemeID = t.ID
q.ThemeSlug = strings.TrimSpace(t.Slug)
return
}
}
return
}
if q.ThemeID > 0 {
if found := FindThemeSlugByID(themes, q.ThemeID); found != "" {
q.ThemeSlug = found
}
}
}
// BuildAllListPagination 列表分页。
func BuildAllListPagination(q AllListQuery, totalCount int64) PaginationVM {
q.Normalize()
return BuildPagination(func(p int) string {
nq := q
nq.PageNum = p
return nq.URL()
}, q.PageNum, q.PageSize, totalCount)
}
// FetchAllList 拉取 GET /api/app/playlet/all/list。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_all_list
func FetchAllList(c *gin.Context, q AllListQuery) (*PlayletAllListResp, error) {
q.Normalize()
params := url.Values{}
params.Set("pageNum", strconv.Itoa(q.PageNum))
params.Set("pageSize", strconv.Itoa(q.PageSize))
params.Set("sortType", strconv.Itoa(q.SortType))
params.Set("updateStatus", strconv.Itoa(q.UpdateStatus))
if q.ThemeSlug != "" {
params.Set("themeSlug", q.ThemeSlug)
} else if q.ThemeID > 0 {
params.Set("themeId", strconv.FormatInt(q.ThemeID, 10))
}
var env APIEnvelope[PlayletAllListResp]
if err := upstream.GetPublicJSON(c, allListPath+"?"+params.Encode(), &env); err != nil {
return nil, err
}
if env.Code != 200 {
return nil, fmt.Errorf("playlet all list code=%d msg=%s", env.Code, env.Msg)
}
return &env.Data, nil
}
// FetchAllListOrEmpty 失败时返回空列表。
func FetchAllListOrEmpty(c *gin.Context, q AllListQuery) *PlayletAllListResp {
data, err := FetchAllList(c, q)
if err != nil {
log.Error("fetch playlet all list failed", log.E(err))
return &PlayletAllListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
if data == nil {
return &PlayletAllListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
data.SeoInfo.Normalize()
return data
}
// FetchList 分页查询已上架短剧。
func FetchList(c *gin.Context, q ListQuery) (*PlayletVideoResp, error) {
q.Normalize()
params := url.Values{}
params.Set("pageNum", strconv.Itoa(q.PageNum))
params.Set("pageSize", strconv.Itoa(q.PageSize))
params.Set("sortOrder", strconv.Itoa(q.SortOrder))
if strings.TrimSpace(q.ThemeSlug) != "" {
params.Set("themeSlug", strings.TrimSpace(q.ThemeSlug))
} else if q.ThemeID > 0 {
params.Set("themeId", strconv.FormatInt(q.ThemeID, 10))
} else {
params.Set("type", strconv.Itoa(listTypeForUpstream(q.Type)))
}
var env APIEnvelope[PlayletVideoResp]
if err := upstream.GetPublicJSON(c, listPath+"?"+params.Encode(), &env); err != nil {
return nil, err
}
if env.Code != 200 {
return nil, fmt.Errorf("playlet list code=%d msg=%s", env.Code, env.Msg)
}
return &env.Data, nil
}
// FetchListOrEmpty 失败时返回空列表。
func FetchListOrEmpty(c *gin.Context, q ListQuery) *PlayletVideoResp {
data, err := FetchList(c, q)
if err != nil {
log.Error("fetch playlet list failed", log.E(err))
return &PlayletVideoResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
if data == nil {
return &PlayletVideoResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
data.SeoInfo.Normalize()
return data
}
// FetchThemes 拉取题材列表。
func FetchThemes(c *gin.Context, contentType int) ([]PlayletThemeItem, error) {
contentType = normalizeContentType(contentType)
params := url.Values{}
params.Set("themeType", strconv.Itoa(contentType))
var env APIEnvelope[PlayletThemeListResponse]
if err := upstream.GetPublicJSON(c, themePath+"?"+params.Encode(), &env); err != nil {
return nil, err
}
if env.Code != 200 {
return nil, fmt.Errorf("theme list code=%d msg=%s", env.Code, env.Msg)
}
return env.Data.List, nil
}
// FetchThemesOrEmpty 失败时返回空题材。
func FetchThemesOrEmpty(c *gin.Context, contentType int) []PlayletThemeItem {
list, err := FetchThemes(c, contentType)
if err != nil {
log.Error("fetch theme list failed", log.E(err))
return nil
}
return ActiveThemes(list)
}
// ActiveThemes 仅保留上架题材,按 rank 降序(越大越靠前)。
func ActiveThemes(list []PlayletThemeItem) []PlayletThemeItem {
out := make([]PlayletThemeItem, 0, len(list))
for _, item := range list {
if !item.Status {
continue
}
item.Name = strings.TrimSpace(item.Name)
item.Slug = strings.TrimSpace(item.Slug)
if item.Name == "" || item.ID <= 0 {
continue
}
out = append(out, item)
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].Rank != out[j].Rank {
return out[i].Rank > out[j].Rank
}
return out[i].ID < out[j].ID
})
return out
}
func normalizeContentType(contentType int) int {
if contentType == contentTypeAdult {
return contentTypeAdult
}
return contentTypeNonAdult
}
// listTypeForUpstream 将站点统一类型转为 playlet/list 的 type(该接口 1=成人 2=非成人)。
func listTypeForUpstream(contentType int) int {
if normalizeContentType(contentType) == contentTypeAdult {
return upstreamListTypeAdult
}
return upstreamListTypeNonAdult
}