Files
web_huangguo_server/app/skd/job/sitemap/data.go
T
rootandClaude Opus 5.5 7bf8bf3030 sitemap 播放页直接写第一集地址,避免 /play/{slug}/ 301
取第一集的规则与 app 详情接口选集列表一致(集号、ID 升序;原创剧只算已加水印的剧集),
没有可播放剧集的短剧不再写入 sitemap。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:55:29 +08:00

401 lines
12 KiB
Go
Raw 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 sitemap
import (
"context"
"strconv"
"strings"
"sync"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/utils"
"videoweb/pub/models/playletcategorymod"
"videoweb/pub/models/playletchaptermod"
"videoweb/pub/models/playletmod"
"videoweb/pub/models/playlettagmod"
"videoweb/pub/models/playletthememod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type idWithTime struct {
Id int64
Slug string
LastMod time.Time
IsOriginal bool // 仅短剧使用:原创剧只算已加水印的剧集
}
type staticPageEntry struct {
path string
changeFreq string
priority float32
}
func buildStaticURLs(webAddr string, entries []staticPageEntry) []URL {
urls := make([]URL, 0, len(entries))
now := time.Now().Format(time.DateOnly)
for _, entry := range entries {
urls = append(urls, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entry.path),
LastMod: now,
ChangeFreq: entry.changeFreq,
Priority: entry.priority,
})
}
return urls
}
func entityPath(prefix, slug string, id int64) string {
if segment := utils.PathSegment(slug, id); segment != "" {
return strings.TrimSuffix(prefix, "/") + "/" + segment + "/"
}
if strings.HasSuffix(prefix, "/") {
return prefix
}
return prefix + "/"
}
func hgdjStaticPages() []staticPageEntry {
return []staticPageEntry{
{path: "/", changeFreq: "daily", priority: 1.0},
{path: "/list/ai-short-play/", changeFreq: "daily", priority: 0.9},
{path: "/list/ai-short-play/popular/", changeFreq: "daily", priority: 0.8},
{path: "/list/ai-short-play/original/", changeFreq: "daily", priority: 0.8},
{path: "/series/", changeFreq: "daily", priority: 0.9},
{path: "/series/popular/", changeFreq: "daily", priority: 0.8},
{path: "/series/original/", changeFreq: "daily", priority: 0.8},
{path: "/rank/", changeFreq: "daily", priority: 0.8},
{path: "/today-update/", changeFreq: "daily", priority: 0.8},
{path: "/topic/", changeFreq: "daily", priority: 0.8},
{path: "/application/", changeFreq: "weekly", priority: 0.6},
{path: "/home-road/", changeFreq: "weekly", priority: 0.6},
{path: "/download-app/", changeFreq: "monthly", priority: 0.5},
{path: "/nav/", changeFreq: "monthly", priority: 0.5},
{path: "/about-us/", changeFreq: "yearly", priority: 0.3},
{path: "/help-center/", changeFreq: "yearly", priority: 0.3},
}
}
// getURL 并发拉取短剧数据并写入 sitemap 分片,URL 规则对齐 jha-1043-hgdj-web。
func getURL(sg *SitemapGenerator) {
webAddr := strings.TrimSpace(sg.WebAddr)
if webAddr == "" {
return
}
ctx := context.Background()
var (
wg sync.WaitGroup
listed listedPlaylets
themeItems []idWithTime
tagItems []idWithTime
categoryItems []idWithTime
)
wg.Add(4)
go func() {
defer wg.Done()
listed = getPlayletList(ctx)
}()
go func() {
defer wg.Done()
themeItems = getThemeList(ctx)
}()
go func() {
defer wg.Done()
tagItems = getTagList(ctx)
}()
go func() {
defer wg.Done()
categoryItems = getCategoryList(ctx)
}()
wg.Wait()
// 专题/标签没有上架短剧时不写入,避免空列表页被收录。
// 分类页无绑定短剧时会回退展示全部上架短剧,因此启用分类都写入。
themeItems = keepWithListedPlaylets(themeItems, listed.themeIDs)
tagItems = keepWithListedPlaylets(tagItems, listed.tagIDs)
pageURLs := buildStaticURLs(webAddr, hgdjStaticPages())
for _, item := range themeItems {
pageURLs = append(pageURLs, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/topic", item.Slug, item.Id)),
LastMod: formatLastMod(item.LastMod),
ChangeFreq: "daily",
Priority: 0.8,
})
}
for _, item := range categoryItems {
pageURLs = append(pageURLs, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/category", item.Slug, item.Id)),
LastMod: formatLastMod(item.LastMod),
ChangeFreq: "daily",
Priority: 0.8,
})
}
for _, item := range tagItems {
pageURLs = append(pageURLs, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/tag", item.Slug, item.Id)),
LastMod: formatLastMod(item.LastMod),
ChangeFreq: "weekly",
Priority: 0.6,
})
}
if len(pageURLs) > 0 {
_ = sg.writeSitemapFile("sitemap-pages.xml", pageURLs)
}
if urls := buildPlayletPathURLs(webAddr, listed.items, "/detail", "weekly", 0.7); len(urls) > 0 {
_ = sg.writeSitemapFiles("sitemap-playletdetail", urls)
}
if urls := buildPlayletPlayURLs(webAddr, listed.items, firstPlayletChapterIDs(ctx, listed.items), "weekly", 0.6); len(urls) > 0 {
_ = sg.writeSitemapFiles("sitemap-playletplay", urls)
}
}
type listedPlaylets struct {
items []idWithTime
themeIDs map[int64]struct{}
tagIDs map[int64]struct{}
categoryIDs map[int64]struct{}
}
func keepWithListedPlaylets(items []idWithTime, listed map[int64]struct{}) []idWithTime {
if len(items) == 0 || len(listed) == 0 {
return nil
}
out := make([]idWithTime, 0, len(items))
for _, item := range items {
if _, ok := listed[item.Id]; ok {
out = append(out, item)
}
}
return out
}
func addListedIDs(dst map[int64]struct{}, ids []int64) {
for _, id := range ids {
if id > 0 {
dst[id] = struct{}{}
}
}
}
func buildPlayletPathURLs(webAddr string, items []idWithTime, prefix string, changeFreq string, priority float32) []URL {
urls := make([]URL, 0, len(items))
for _, item := range items {
lastMod := formatLastMod(item.LastMod)
if lastMod == "" {
lastMod = time.Now().Format(time.DateOnly)
}
urls = append(urls, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath(prefix, item.Slug, item.Id)),
LastMod: lastMod,
ChangeFreq: changeFreq,
Priority: priority,
})
}
return urls
}
// buildPlayletPlayURLs 播放页直接写第一集的规范地址 /play/{slug}/{chapterId}/,
// 避免 /play/{slug}/ 在 h5 再 301 一次;没有可播放剧集的短剧不写入(h5 会返回 404)。
func buildPlayletPlayURLs(webAddr string, items []idWithTime, firstChapter map[int64]int64, changeFreq string, priority float32) []URL {
urls := make([]URL, 0, len(items))
for _, item := range items {
chapterID := firstChapter[item.Id]
if chapterID <= 0 {
continue
}
lastMod := formatLastMod(item.LastMod)
if lastMod == "" {
lastMod = time.Now().Format(time.DateOnly)
}
urls = append(urls, URL{
Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/play", item.Slug, item.Id)+strconv.FormatInt(chapterID, 10)+"/"),
LastMod: lastMod,
ChangeFreq: changeFreq,
Priority: priority,
})
}
return urls
}
// firstPlayletChapterIDs 取每部短剧的第一集 ID,规则与 app 详情接口的选集列表一致
// (按集号、ID 升序;原创剧只算已加水印的剧集),h5 播放页跳转也取这一集。
func firstPlayletChapterIDs(ctx context.Context, items []idWithTime) map[int64]int64 {
result := make(map[int64]int64, len(items))
original := make(map[int64]bool, len(items))
ids := make([]int64, 0, len(items))
for _, item := range items {
if item.Id > 0 {
ids = append(ids, item.Id)
original[item.Id] = item.IsOriginal
}
}
const batch = 500
for start := 0; start < len(ids); start += batch {
end := start + batch
if end > len(ids) {
end = len(ids)
}
list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": ids[start:end]},
}, options.Find().
SetSort(bson.D{
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
}).
SetProjection(bson.M{
playletchaptermod.FN.PlayletChapter.Id: 1,
playletchaptermod.FN.PlayletChapter.PlayletId: 1,
playletchaptermod.FN.PlayletChapter.CurrentEpisode: 1,
playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl: 1,
}))
if err != nil {
log.Error("sitemap 查询短剧第一集失败", log.E(err))
continue
}
for _, ch := range list {
if ch == nil || ch.Id <= 0 || result[ch.PlayletId] > 0 {
continue
}
if original[ch.PlayletId] && ch.WatermarkVideoUrl == "" {
continue
}
result[ch.PlayletId] = ch.Id
}
}
return result
}
func formatLastMod(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.DateOnly)
}
func getPlayletList(ctx context.Context) listedPlaylets {
result := listedPlaylets{
items: make([]idWithTime, 0),
themeIDs: make(map[int64]struct{}),
tagIDs: make(map[int64]struct{}),
categoryIDs: make(map[int64]struct{}),
}
filter := bson.M{
playletmod.FN.Playlet.Status: playletmod.StatusUp,
}
pageSize := int64(1000)
for i := int64(0); i < 500; i++ {
skip := i * pageSize
list, _ := playletmod.FindManyPlaylet(ctx, filter, options.Find().
SetSort(bson.M{playletmod.FN.Playlet.Id: -1}).
SetSkip(skip).
SetLimit(pageSize).
SetProjection(bson.M{
playletmod.FN.Playlet.Id: 1,
playletmod.FN.Playlet.Slug: 1,
playletmod.FN.Playlet.UpdatedAt: 1,
playletmod.FN.Playlet.OnlineAt: 1,
playletmod.FN.Playlet.ThemeIds: 1,
playletmod.FN.Playlet.TagIds: 1,
playletmod.FN.Playlet.CategoryIds: 1,
playletmod.FN.Playlet.IsOriginal: 1,
}))
for _, info := range list {
if info == nil || info.Id <= 0 {
continue
}
lastMod := info.UpdatedAt
if lastMod.IsZero() {
lastMod = info.OnlineAt
}
result.items = append(result.items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: lastMod, IsOriginal: info.IsOriginal})
addListedIDs(result.themeIDs, info.ThemeIds)
addListedIDs(result.tagIDs, info.TagIds)
addListedIDs(result.categoryIDs, info.CategoryIds)
}
if len(list) < int(pageSize) {
break
}
}
return result
}
func getThemeList(ctx context.Context) []idWithTime {
filter := bson.M{
playletthememod.FN.PlayletTheme.Status: true,
}
pageSize := int64(1000)
items := make([]idWithTime, 0)
for i := int64(0); i < 100; i++ {
skip := i * pageSize
list, _ := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
SetSort(bson.M{playletthememod.FN.PlayletTheme.Rank: -1}).
SetSkip(skip).
SetLimit(pageSize))
for _, info := range list {
if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
continue
}
items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
}
if len(list) < int(pageSize) {
break
}
}
return items
}
func getTagList(ctx context.Context) []idWithTime {
filter := bson.M{
playlettagmod.FN.PlayletTag.Status: true,
}
pageSize := int64(1000)
items := make([]idWithTime, 0)
for i := int64(0); i < 100; i++ {
skip := i * pageSize
list, _ := playlettagmod.FindManyPlayletTag(ctx, filter, options.Find().
SetSort(bson.M{playlettagmod.FN.PlayletTag.Rank: -1}).
SetSkip(skip).
SetLimit(pageSize))
for _, info := range list {
if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
continue
}
items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
}
if len(list) < int(pageSize) {
break
}
}
return items
}
func getCategoryList(ctx context.Context) []idWithTime {
filter := bson.M{
playletcategorymod.FN.PlayletCategory.Status: true,
}
pageSize := int64(1000)
items := make([]idWithTime, 0)
for i := int64(0); i < 100; i++ {
skip := i * pageSize
list, _ := playletcategorymod.FindManyPlayletCategory(ctx, filter, options.Find().
SetSort(bson.M{playletcategorymod.FN.PlayletCategory.Rank: -1}).
SetSkip(skip).
SetLimit(pageSize))
for _, info := range list {
if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
continue
}
items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
}
if len(list) < int(pageSize) {
break
}
}
return items
}