init: web_hgdjc_server
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
// Package srv_seopush 负责短剧新上架/下架时向搜索引擎推送页面地址。
|
||||
// 业务回调里即时推送一次,失败的任务由 web/skd/job/seopush 兜底重试。
|
||||
package srv_seopush
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/seopushmod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/seo/googleindex"
|
||||
"videoweb/pub/seo/h5path"
|
||||
seoindexnow "videoweb/pub/seo/indexnow"
|
||||
"videoweb/web/service/srv_cfg"
|
||||
"videoweb/web/webg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// channelFields 把两个推送通道的字段名抽象出来,共用同一套状态更新逻辑
|
||||
type channelFields struct {
|
||||
name string
|
||||
status string
|
||||
retry string
|
||||
errMsg string
|
||||
at string
|
||||
}
|
||||
|
||||
var googleChannel = channelFields{
|
||||
name: "google",
|
||||
status: seopushmod.FN.SeoPushTask.GoogleStatus,
|
||||
retry: seopushmod.FN.SeoPushTask.GoogleRetry,
|
||||
errMsg: seopushmod.FN.SeoPushTask.GoogleErr,
|
||||
at: seopushmod.FN.SeoPushTask.GoogleAt,
|
||||
}
|
||||
|
||||
var indexNowChannel = channelFields{
|
||||
name: "indexnow",
|
||||
status: seopushmod.FN.SeoPushTask.IndexNowStatus,
|
||||
retry: seopushmod.FN.SeoPushTask.IndexNowRetry,
|
||||
errMsg: seopushmod.FN.SeoPushTask.IndexNowErr,
|
||||
at: seopushmod.FN.SeoPushTask.IndexNowAt,
|
||||
}
|
||||
|
||||
// PlayletDetailURL 拼装短剧详情页完整地址,与 sitemap 一致:/detail/{slug}/,无 slug 回退 ID。
|
||||
func PlayletDetailURL(webAddr, slug string, playletId int64) string {
|
||||
if h5path.Segment(slug, playletId) == "" {
|
||||
return ""
|
||||
}
|
||||
return h5path.SitemapAbs(webAddr, h5path.Entity("/detail", slug, playletId))
|
||||
}
|
||||
|
||||
// VideoPlayURL 拼装视频播放页完整地址,路径与 H5 / sitemap 一致:/series-play/{slug}。
|
||||
func VideoPlayURL(webAddr, slug string, videoId int64) string {
|
||||
path := h5path.VideoPlay(slug, videoId)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return h5path.Abs(webAddr, path)
|
||||
}
|
||||
|
||||
// PageVisible 判断详情页当前是否对外可访问。
|
||||
func PageVisible(status playletmod.Status, onlineAt time.Time) bool {
|
||||
if status != playletmod.StatusUp {
|
||||
return false
|
||||
}
|
||||
if onlineAt.IsZero() {
|
||||
return true
|
||||
}
|
||||
return !onlineAt.After(time.Now())
|
||||
}
|
||||
|
||||
// SEOBaseURL 取站点对外地址
|
||||
func SEOBaseURL(ctx context.Context) string {
|
||||
sysCfg, serr := srv_cfg.GetSystemConfig(ctx)
|
||||
if serr != nil || sysCfg == nil {
|
||||
log.Warn("seopush: get system config failed", log.E(serr))
|
||||
return ""
|
||||
}
|
||||
base := sysCfg.SEOBaseURL()
|
||||
if base == "" {
|
||||
log.Warn("seopush: SEO base URL is empty, skip push")
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func canPushExternally() bool {
|
||||
return pubdef.Enabled() && webg.Conf.RunMod == "prod"
|
||||
}
|
||||
|
||||
// EnqueuePlayletUpdated 短剧新上架后登记并立即推送,仅在页面已可访问时生效
|
||||
func EnqueuePlayletUpdated(ctx context.Context, playletId int64,
|
||||
status playletmod.Status, onlineAt time.Time) {
|
||||
if !pubdef.Enabled() || !PageVisible(status, onlineAt) {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionUpdated, true)
|
||||
}
|
||||
|
||||
// RegisterPlayletUpdated 只登记不立即推送,供批量补推使用
|
||||
func RegisterPlayletUpdated(ctx context.Context, playletId int64,
|
||||
status playletmod.Status, onlineAt time.Time) {
|
||||
if !pubdef.Enabled() || !PageVisible(status, onlineAt) {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionUpdated, false)
|
||||
}
|
||||
|
||||
// EnqueuePlayletDeleted 短剧下架/删除后登记并立即推送
|
||||
func EnqueuePlayletDeleted(ctx context.Context, playletId int64) {
|
||||
if !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
enqueue(ctx, playletId, seopushmod.ActionDeleted, true)
|
||||
}
|
||||
|
||||
// VideoPageVisible 判断视频播放页当前是否对外可访问。
|
||||
func VideoPageVisible(status videoinformationmod.InformationSatus) bool {
|
||||
return status == videoinformationmod.InformationSatusVerifyOK
|
||||
}
|
||||
|
||||
// EnqueueVideoUpdated 视频上架后登记并立即推送。
|
||||
func EnqueueVideoUpdated(ctx context.Context, videoId int64, status videoinformationmod.InformationSatus) {
|
||||
if !pubdef.Enabled() || !VideoPageVisible(status) {
|
||||
return
|
||||
}
|
||||
enqueueVideo(ctx, videoId, seopushmod.ActionUpdated, true)
|
||||
}
|
||||
|
||||
// EnqueueVideoDeleted 视频下架/删除后登记并立即推送。
|
||||
func EnqueueVideoDeleted(ctx context.Context, videoId int64) {
|
||||
if !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
enqueueVideo(ctx, videoId, seopushmod.ActionDeleted, true)
|
||||
}
|
||||
|
||||
func enqueue(ctx context.Context, playletId int64, action string, pushNow bool) {
|
||||
webAddr := SEOBaseURL(ctx)
|
||||
if webAddr == "" {
|
||||
return
|
||||
}
|
||||
pageURL := PlayletDetailURL(webAddr, loadPlayletSlug(ctx, playletId), playletId)
|
||||
if pageURL == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
task := &seopushmod.SeoPushTask{
|
||||
Url: pageURL,
|
||||
VideoId: playletId,
|
||||
Action: action,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if action == seopushmod.ActionDeleted {
|
||||
_, _ = seopushmod.DelManySeoPushTask(ctx, bson.M{
|
||||
seopushmod.FN.SeoPushTask.Url: pageURL,
|
||||
seopushmod.FN.SeoPushTask.Action: seopushmod.ActionUpdated,
|
||||
})
|
||||
}
|
||||
if serr := seopushmod.UpsertPendingSeoPushTask(ctx, task); serr != nil {
|
||||
log.Warn("seopush: enqueue task failed", log.Any("url", pageURL), log.E(serr))
|
||||
return
|
||||
}
|
||||
if action == seopushmod.ActionUpdated {
|
||||
markPlayletQueued(ctx, playletId)
|
||||
}
|
||||
if !pushNow {
|
||||
return
|
||||
}
|
||||
|
||||
godef.Go(func() {
|
||||
PushTask(context.Background(), webAddr, task)
|
||||
})
|
||||
}
|
||||
|
||||
func enqueueVideo(ctx context.Context, videoId int64, action string, pushNow bool) {
|
||||
webAddr := SEOBaseURL(ctx)
|
||||
if webAddr == "" {
|
||||
return
|
||||
}
|
||||
pageURL := VideoPlayURL(webAddr, loadVideoSlug(ctx, videoId), videoId)
|
||||
if pageURL == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
task := &seopushmod.SeoPushTask{
|
||||
Url: pageURL,
|
||||
VideoId: videoId,
|
||||
Action: action,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if action == seopushmod.ActionDeleted {
|
||||
_, _ = seopushmod.DelManySeoPushTask(ctx, bson.M{
|
||||
seopushmod.FN.SeoPushTask.Url: pageURL,
|
||||
seopushmod.FN.SeoPushTask.Action: seopushmod.ActionUpdated,
|
||||
})
|
||||
}
|
||||
if serr := seopushmod.UpsertPendingSeoPushTask(ctx, task); serr != nil {
|
||||
log.Warn("seopush: enqueue video task failed", log.Any("url", pageURL), log.E(serr))
|
||||
return
|
||||
}
|
||||
if action == seopushmod.ActionUpdated {
|
||||
markVideoQueued(ctx, videoId)
|
||||
}
|
||||
if !pushNow {
|
||||
return
|
||||
}
|
||||
godef.Go(func() {
|
||||
PushTask(context.Background(), webAddr, task)
|
||||
})
|
||||
}
|
||||
|
||||
func loadPlayletSlug(ctx context.Context, playletId int64) string {
|
||||
if playletId <= 0 {
|
||||
return ""
|
||||
}
|
||||
item, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playletId},
|
||||
options.FindOne().SetProjection(bson.D{{Key: playletmod.FN.Playlet.Slug, Value: 1}}))
|
||||
if err != nil || item == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(item.Slug)
|
||||
}
|
||||
|
||||
func loadVideoSlug(ctx context.Context, videoId int64) string {
|
||||
if videoId <= 0 {
|
||||
return ""
|
||||
}
|
||||
item, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Id: videoId},
|
||||
options.FindOne().SetProjection(bson.D{{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1}}))
|
||||
if err != nil || item == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(item.Slug)
|
||||
}
|
||||
|
||||
func isObsoletePushURL(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(raw, "?") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(raw, "/lieyan/av/detail") {
|
||||
return true
|
||||
}
|
||||
// 旧推送路径 /play/{slug}。/series-play 是视频页,/detail/{slug}/ 是当前详情页。
|
||||
path := requestPath(raw)
|
||||
return path == "/play" || strings.HasPrefix(path, "/play/")
|
||||
}
|
||||
|
||||
func requestPath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if i := strings.IndexAny(raw, "?#"); i >= 0 {
|
||||
raw = raw[:i]
|
||||
}
|
||||
if i := strings.Index(raw, "://"); i >= 0 {
|
||||
raw = raw[i+3:]
|
||||
if j := strings.Index(raw, "/"); j >= 0 {
|
||||
return raw[j:]
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
if raw == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(raw, "/") {
|
||||
return "/" + raw
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func markPlayletQueued(ctx context.Context, playletId int64) {
|
||||
if serr := playletmod.UpdateOnePlaylet(ctx,
|
||||
bson.M{playletmod.FN.Playlet.Id: playletId},
|
||||
bson.M{"$set": bson.M{playletmod.FN.Playlet.IndexNow: int64(1)}}); serr != nil {
|
||||
log.Warn("seopush: mark playlet queued failed", log.Any("playletId", playletId), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
func markVideoQueued(ctx context.Context, videoId int64) {
|
||||
if serr := videoinformationmod.UpdateOneVideoInformation(ctx,
|
||||
bson.M{videoinformationmod.FN.VideoInformation.Id: videoId},
|
||||
bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.IndexNow: int64(1)}}); serr != nil {
|
||||
log.Warn("seopush: mark video queued failed", log.Any("videoId", videoId), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask 推送任务中尚未成功的通道
|
||||
func PushTask(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
|
||||
if task == nil || !pubdef.Enabled() {
|
||||
return
|
||||
}
|
||||
// 旧路径不再推送:查询参数、/play/{slug}、/lieyan/av/detail。
|
||||
if isObsoletePushURL(task.Url) {
|
||||
log.Warn("seopush: drop obsolete-url task", log.Any("url", task.Url), log.Any("action", task.Action))
|
||||
_, _ = seopushmod.DelManySeoPushTask(ctx, taskFilter(task))
|
||||
return
|
||||
}
|
||||
if !canPushExternally() {
|
||||
dryRun(ctx, task)
|
||||
return
|
||||
}
|
||||
if task.GoogleStatus != seopushmod.PushStatusSuccess {
|
||||
pushGoogle(ctx, task)
|
||||
}
|
||||
if task.IndexNowStatus != seopushmod.PushStatusSuccess {
|
||||
pushIndexNow(ctx, webAddr, task)
|
||||
}
|
||||
}
|
||||
|
||||
func dryRun(ctx context.Context, task *seopushmod.SeoPushTask) {
|
||||
log.Info("seopush: dry-run, external push skipped in non-prod",
|
||||
log.Any("url", task.Url), log.Any("action", task.Action))
|
||||
if task.GoogleStatus != seopushmod.PushStatusSuccess {
|
||||
markSuccess(ctx, task, googleChannel)
|
||||
}
|
||||
if task.IndexNowStatus != seopushmod.PushStatusSuccess {
|
||||
markSuccess(ctx, task, indexNowChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func pushGoogle(ctx context.Context, task *seopushmod.SeoPushTask) {
|
||||
notifyType := googleindex.TypeUpdated
|
||||
if task.Action == seopushmod.ActionDeleted {
|
||||
notifyType = googleindex.TypeDeleted
|
||||
}
|
||||
|
||||
resp, err := googleindex.Publish(ctx, task.Url, notifyType)
|
||||
if err == nil {
|
||||
markSuccess(ctx, task, googleChannel)
|
||||
log.Info("seopush: google pushed",
|
||||
log.Any("url", task.Url),
|
||||
log.Any("type", notifyType),
|
||||
log.Any("resp", resp))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, googleindex.ErrQuotaExceeded) {
|
||||
log.Warn("seopush: google daily quota exceeded, keep pending",
|
||||
log.Any("url", task.Url), log.Any("resp", err.Error()))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, googleindex.ErrNotEnabled) {
|
||||
log.Warn("seopush: google client not ready, keep pending", log.Any("url", task.Url))
|
||||
return
|
||||
}
|
||||
markFailed(ctx, task, googleChannel, err)
|
||||
}
|
||||
|
||||
func pushIndexNow(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
|
||||
if strings.TrimSpace(webAddr) == "" {
|
||||
return
|
||||
}
|
||||
config, err := seoindexnow.BuildConfig(webAddr)
|
||||
if err != nil {
|
||||
log.Warn("seopush: build indexnow config failed", log.Any("webAddr", webAddr), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
result := seoindexnow.NewBatchIndexNowSubmitter(config).SubmitBatch([]string{task.Url})
|
||||
if result.SuccessCount > 0 {
|
||||
markSuccess(ctx, task, indexNowChannel)
|
||||
log.Info("seopush: indexnow pushed",
|
||||
log.Any("url", task.Url),
|
||||
log.Any("statusCode", result.StatusCodes))
|
||||
return
|
||||
}
|
||||
markFailed(ctx, task, indexNowChannel, errors.New(strings.Join(result.Errors, "; ")))
|
||||
}
|
||||
|
||||
func markSuccess(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields) {
|
||||
now := time.Now()
|
||||
if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
|
||||
"$set": bson.M{
|
||||
ch.status: seopushmod.PushStatusSuccess,
|
||||
ch.errMsg: "",
|
||||
ch.at: now,
|
||||
seopushmod.FN.SeoPushTask.UpdatedAt: now,
|
||||
},
|
||||
}); serr != nil {
|
||||
log.Warn("seopush: mark success failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
func markFailed(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields, pushErr error) {
|
||||
now := time.Now()
|
||||
errText := ""
|
||||
if pushErr != nil {
|
||||
errText = pushErr.Error()
|
||||
}
|
||||
log.Warn("seopush: push failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.Any("err", errText))
|
||||
|
||||
if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
|
||||
"$set": bson.M{
|
||||
ch.errMsg: errText,
|
||||
ch.at: now,
|
||||
seopushmod.FN.SeoPushTask.UpdatedAt: now,
|
||||
},
|
||||
"$inc": bson.M{ch.retry: 1},
|
||||
}); serr != nil {
|
||||
log.Warn("seopush: mark failed failed",
|
||||
log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
|
||||
}
|
||||
}
|
||||
|
||||
func taskFilter(task *seopushmod.SeoPushTask) bson.M {
|
||||
return bson.M{
|
||||
seopushmod.FN.SeoPushTask.Url: task.Url,
|
||||
seopushmod.FN.SeoPushTask.Action: task.Action,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user