@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// About renders the static About page.
|
||||
func About(c *gin.Context) {
|
||||
HTMLOK(c, "about.html", gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const maxAccountPage = 10000
|
||||
|
||||
type accountPage struct {
|
||||
Current int
|
||||
Total int
|
||||
Count int64
|
||||
PrevURL string
|
||||
NextURL string
|
||||
}
|
||||
|
||||
func accountPageNum(c *gin.Context) (int, bool) {
|
||||
if c.Request.URL.Query().Has("page") {
|
||||
return 0, false
|
||||
}
|
||||
raw := strings.TrimSpace(c.Param("page"))
|
||||
if raw == "" {
|
||||
return 1, true
|
||||
}
|
||||
page, err := strconv.Atoi(raw)
|
||||
if err != nil || page < 1 || page > maxAccountPage {
|
||||
return 0, false
|
||||
}
|
||||
return page, true
|
||||
}
|
||||
|
||||
func makeAccountPage(path string, page, size int, count int64) accountPage {
|
||||
total := int((count + int64(size) - 1) / int64(size))
|
||||
p := accountPage{Current: page, Total: total, Count: count}
|
||||
link := func(n int) string {
|
||||
path = strings.TrimRight(path, "/")
|
||||
if n <= 1 {
|
||||
return path
|
||||
}
|
||||
return path + "/page/" + strconv.Itoa(n)
|
||||
}
|
||||
if page > 1 {
|
||||
p.PrevURL = link(page - 1)
|
||||
}
|
||||
if page < total {
|
||||
p.NextURL = link(page + 1)
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type registerBody struct {
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
AgentInviteCode string `json:"agentInviteCode"`
|
||||
DevID string `json:"devID"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
type loginBody struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
DevID string `json:"devID"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// Register 代理上游注册(POST /api/app/auth/register),成功后写入登录 cookie。
|
||||
// 请求头带游客 Authorization 时升级该游客;邮箱缺省时用账号生成占位邮箱。
|
||||
func Register(c *gin.Context) {
|
||||
var body registerBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.AccountNo = strings.TrimSpace(body.AccountNo)
|
||||
body.Email = strings.TrimSpace(body.Email)
|
||||
body.AgentInviteCode = strings.TrimSpace(body.AgentInviteCode)
|
||||
body.DevID = strings.TrimSpace(body.DevID)
|
||||
body.Channel = authser.ResolveChannel(c, body.Channel)
|
||||
|
||||
if body.AccountNo == "" || body.Password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请填写账号和密码",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 上游 registerReq 要求 email;前端无邮箱时用账号生成唯一占位邮箱。
|
||||
if body.Email == "" {
|
||||
body.Email = body.AccountNo + "@hgdj.local"
|
||||
}
|
||||
|
||||
env, user, err := authser.Register(c, authser.RegisterReq{
|
||||
AccountNo: body.AccountNo,
|
||||
Email: body.Email,
|
||||
Password: body.Password,
|
||||
AgentInviteCode: body.AgentInviteCode,
|
||||
DevID: body.DevID,
|
||||
Channel: body.Channel,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("register upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "register failed",
|
||||
"tip": "注册失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if env == nil || env.Code != 200 || user == nil {
|
||||
tip := "注册失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
if t := strings.TrimSpace(env.Tip); t != "" {
|
||||
tip = t
|
||||
} else if m := strings.TrimSpace(env.Msg); m != "" {
|
||||
tip = m
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmptyAuth(env.Msg, "success"),
|
||||
"tip": firstNonEmptyAuth(env.Tip, "注册成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// Login 代理上游登录(POST /api/app/auth/login),成功后写入登录 cookie。
|
||||
func Login(c *gin.Context) {
|
||||
var body loginBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.Account = strings.TrimSpace(body.Account)
|
||||
body.DevID = strings.TrimSpace(body.DevID)
|
||||
body.Channel = authser.ResolveChannel(c, body.Channel)
|
||||
if body.Account == "" || body.Password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请填写账号和密码",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, user, err := authser.Login(c, authser.LoginReq{
|
||||
Account: body.Account,
|
||||
Password: body.Password,
|
||||
DevID: body.DevID,
|
||||
Channel: body.Channel,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("login upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "login failed",
|
||||
"tip": "登录失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if env == nil || env.Code != 200 || user == nil {
|
||||
tip := "登录失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
if t := strings.TrimSpace(env.Tip); t != "" {
|
||||
tip = t
|
||||
} else if m := strings.TrimSpace(env.Msg); m != "" {
|
||||
tip = m
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmptyAuth(env.Msg, "success"),
|
||||
"tip": firstNonEmptyAuth(env.Tip, "登录成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// Guest 代理上游游客登录,成功后写入登录 cookie。
|
||||
// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent / X-Real-IP(由 BFF 组装)。
|
||||
func Guest(c *gin.Context) {
|
||||
var body struct {
|
||||
DevID string `json:"devID"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Mac string `json:"mac"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.DevID = strings.TrimSpace(body.DevID)
|
||||
body.DeviceID = strings.TrimSpace(body.DeviceID)
|
||||
body.Mac = strings.TrimSpace(body.Mac)
|
||||
body.Channel = authser.ResolveChannel(c, body.Channel)
|
||||
if body.DevID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少设备标识",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.DeviceID == "" {
|
||||
body.DeviceID = body.DevID
|
||||
}
|
||||
|
||||
env, user, err := authser.GuestLogin(c, authser.GuestLoginReq{
|
||||
DevID: body.DevID,
|
||||
DeviceID: body.DeviceID,
|
||||
Mac: body.Mac,
|
||||
Channel: body.Channel,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("guest login upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "guest login failed",
|
||||
"tip": "游客登录失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if env == nil || env.Code != 200 || user == nil {
|
||||
tip := "游客登录失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
if t := strings.TrimSpace(env.Tip); t != "" {
|
||||
tip = t
|
||||
} else if m := strings.TrimSpace(env.Msg); m != "" {
|
||||
tip = m
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmptyAuth(env.Msg, "success"),
|
||||
"tip": firstNonEmptyAuth(env.Tip, "游客登录成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 清除登录 cookie,并标记跳过紧接着的一次自动游客登录。
|
||||
func Logout(c *gin.Context) {
|
||||
authser.ClearAuthCookies(c)
|
||||
authser.MarkGuestSkipAuto(c)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"tip": "已退出登录",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
func firstNonEmptyAuth(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChapterLikeSet BFF:代理 POST /api/app/playlet/chapter/like/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_like_set
|
||||
func ChapterLikeSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "chapterId/isLike required",
|
||||
"tip": "缺少剧集 ID 或点赞状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// ChapterCollectSet BFF:代理 POST /api/app/playlet/chapter/collect/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_collect_set
|
||||
func ChapterCollectSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsCollect *bool `json:"isCollect"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.IsCollect == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "chapterId/isCollect required",
|
||||
"tip": "缺少剧集 ID 或收藏状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChapterUnlock BFF:代理 POST /api/app/playlet/chapter/unlock。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_unlock
|
||||
func ChapterUnlock(c *gin.Context) {
|
||||
var body struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.PlayletID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "chapterId/playletId required",
|
||||
"tip": "缺少剧集或短剧 ID",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "解锁失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "解锁失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChapterWatchReport BFF:代理 POST /api/app/playlet/chapter/watch/report。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_watch_report
|
||||
func ChapterWatchReport(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "chapterId required",
|
||||
"tip": "缺少剧集 ID",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "上报失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "上报失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/commentser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CommentList BFF:代理 GET /api/app/comment/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/get_api_app_comment_list
|
||||
func CommentList(c *gin.Context) {
|
||||
objectID, _ := strconv.ParseInt(c.Query("objectId"), 10, 64)
|
||||
objectType, _ := strconv.Atoi(c.Query("objectType"))
|
||||
pageNum, _ := strconv.Atoi(c.Query("pageNum"))
|
||||
pageSize, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
sortOrder, _ := strconv.Atoi(c.Query("sortOrder"))
|
||||
if objectID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "objectId required",
|
||||
"tip": "缺少评论对象",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
data := commentser.FetchListOrEmpty(c, commentser.ListQuery{
|
||||
ObjectID: objectID,
|
||||
ObjectType: objectType,
|
||||
PageNum: pageNum,
|
||||
PageSize: pageSize,
|
||||
SortOrder: sortOrder,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"tip": "成功",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// CommentSend BFF:代理 POST /api/app/comment/send。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_send
|
||||
func CommentSend(c *gin.Context) {
|
||||
var req commentser.CommentSendReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
env, err := commentser.Send(c, req)
|
||||
if err != nil {
|
||||
tip := "发布失败"
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(msg, "empty text"):
|
||||
tip = "请输入评论内容"
|
||||
case strings.Contains(msg, "too long"):
|
||||
tip = "评论不能超过 200 字"
|
||||
case strings.Contains(msg, "invalid objectId"):
|
||||
tip = "缺少评论对象"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "发布失败",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
tip := env.Tip
|
||||
if tip == "" {
|
||||
tip = env.Msg
|
||||
}
|
||||
// 上游参数错误兜底提示(例如 objectType 不被支持)
|
||||
if env.Code == 1001 && (tip == "" || tip == "参数错误") {
|
||||
tip = "评论参数错误,请稍后重试"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// CommentLikeSet BFF:代理 POST /api/app/comment/like/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_like_set
|
||||
func CommentLikeSet(c *gin.Context) {
|
||||
var body struct {
|
||||
CommentID int64 `json:"commentId"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.CommentID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "commentId/isLike required",
|
||||
"tip": "缺少评论 ID 或点赞状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{
|
||||
CommentID: body.CommentID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Compliance2257 renders the static 18 U.S.C. 2257 compliance statement page.
|
||||
func Compliance2257(c *gin.Context) {
|
||||
HTMLOK(c, "2257.html", gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// DMCA renders the static DMCA copyright notice page.
|
||||
func DMCA(c *gin.Context) {
|
||||
HTMLOK(c, "dmca.html", gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/videoser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type favoriteWork struct {
|
||||
ID int64
|
||||
Title string
|
||||
Genre string
|
||||
DesktopMeta string
|
||||
Episodes string
|
||||
Badge string
|
||||
Image string
|
||||
Kind string
|
||||
URL string
|
||||
DramaID int64
|
||||
Episode int
|
||||
VideoID int64
|
||||
TotalEpisode int
|
||||
}
|
||||
|
||||
// Favorites 我的收藏:短剧 / 剧集(视频) / 章节收藏。
|
||||
// 文档:
|
||||
// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_collect_list
|
||||
// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_collect_list
|
||||
func Favorites(c *gin.Context) {
|
||||
filter, page, validRoute := favoritesRoute(c)
|
||||
if !validRoute {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{"title": "我的收藏 - 黄果短剧", "navActive": "favorites", "accountTab": "favorites"}
|
||||
responses := gin.H{}
|
||||
loadAccountSummary(c, data, responses)
|
||||
|
||||
status := 0
|
||||
switch filter {
|
||||
case "short", "chapter", "series":
|
||||
case "following":
|
||||
status = 1
|
||||
case "finished":
|
||||
status = 2
|
||||
default:
|
||||
filter = "all"
|
||||
}
|
||||
data["filter"] = filter
|
||||
|
||||
pageSize := 12
|
||||
query := playletser.CollectListQuery{PageNum: page, PageSize: pageSize, WatchStatus: status}
|
||||
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
responses["playlet/collect/list"] = gin.H{"error": "暂无登录会话"}
|
||||
responses["video/collect/list"] = gin.H{"error": "暂无登录会话"}
|
||||
data["favoritesResponse"] = responses["playlet/collect/list"]
|
||||
data["playletCollectResponse"] = responses["playlet/collect/list"]
|
||||
data["videoCollectResponse"] = responses["video/collect/list"]
|
||||
data["favoritesDebug"] = responses
|
||||
renderFavorites(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
if filter == "chapter" {
|
||||
list, code, err := playletser.FetchChapterCollectList(c, query)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
list, _, err = playletser.FetchChapterCollectList(c, query)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch chapter favorites page failed", log.E(err))
|
||||
responses["playlet/chapter/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
data["favoritesUnavailable"] = true
|
||||
} else if list != nil {
|
||||
responses["playlet/chapter/collect/list"] = gin.H{"code": 200, "data": list}
|
||||
data["paging"] = makeFavoritesPage(query.PageNum, query.PageSize, list.TotalCount, filter)
|
||||
data["favorites"] = chapterFavoriteWorks(list.List)
|
||||
} else {
|
||||
responses["playlet/chapter/collect/list"] = gin.H{"code": code, "data": list}
|
||||
}
|
||||
data["favoritesResponse"] = responses["playlet/chapter/collect/list"]
|
||||
data["playletCollectResponse"] = responses["playlet/chapter/collect/list"]
|
||||
data["favoritesDebug"] = responses
|
||||
renderFavorites(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
if filter == "series" {
|
||||
loadVideoFavorites(c, data, responses, page, pageSize)
|
||||
data["favoritesResponse"] = responses["video/collect/list"]
|
||||
data["playletCollectResponse"] = gin.H{}
|
||||
data["videoCollectResponse"] = responses["video/collect/list"]
|
||||
data["favoritesDebug"] = responses
|
||||
renderFavorites(c, data)
|
||||
return
|
||||
}
|
||||
|
||||
var works []favoriteWork
|
||||
var totalCount int64
|
||||
playletFailed := false
|
||||
videoFailed := false
|
||||
|
||||
if filter == "all" || filter == "short" || filter == "following" || filter == "finished" {
|
||||
list, code, err := playletser.FetchCollectList(c, query)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
list, code, err = playletser.FetchCollectList(c, query)
|
||||
}
|
||||
if err == nil && list != nil {
|
||||
responses["playlet/collect/list"] = gin.H{"code": 200, "data": list}
|
||||
works = append(works, playletFavoriteWorks(list.List)...)
|
||||
totalCount += list.TotalCount
|
||||
} else if err != nil {
|
||||
responses["playlet/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
log.Error("fetch favorites page failed", log.E(err))
|
||||
playletFailed = true
|
||||
} else {
|
||||
responses["playlet/collect/list"] = gin.H{"code": code, "data": list}
|
||||
}
|
||||
}
|
||||
|
||||
if filter == "all" {
|
||||
videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize}
|
||||
videoList, videoCode, videoErr := videoser.FetchCollectList(c, videoQuery)
|
||||
if retry, _ := authser.OnSessionExpired(c, videoCode); retry {
|
||||
videoList, videoCode, videoErr = videoser.FetchCollectList(c, videoQuery)
|
||||
}
|
||||
if videoErr == nil && videoList != nil {
|
||||
responses["video/collect/list"] = gin.H{"code": 200, "data": videoList}
|
||||
works = append(works, videoFavoriteWorks(videoList.List)...)
|
||||
totalCount += videoList.TotalCount
|
||||
} else if videoErr != nil {
|
||||
responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()}
|
||||
log.Error("fetch video favorites page failed", log.E(videoErr))
|
||||
videoFailed = true
|
||||
} else {
|
||||
responses["video/collect/list"] = gin.H{"code": videoCode, "data": videoList}
|
||||
}
|
||||
}
|
||||
|
||||
if playletFailed && (filter != "all" || videoFailed) {
|
||||
data["favoritesUnavailable"] = true
|
||||
}
|
||||
if filter == "all" && playletFailed && videoFailed {
|
||||
data["favoritesUnavailable"] = true
|
||||
}
|
||||
|
||||
data["paging"] = makeFavoritesPage(page, pageSize, totalCount, filter)
|
||||
data["favorites"] = works
|
||||
data["favoritesResponse"] = responses["playlet/collect/list"]
|
||||
data["playletCollectResponse"] = responses["playlet/collect/list"]
|
||||
data["videoCollectResponse"] = responses["video/collect/list"]
|
||||
data["favoritesDebug"] = responses
|
||||
renderFavorites(c, data)
|
||||
}
|
||||
|
||||
// renderFavorites 收藏为空时附带「大家都在看」后渲染。
|
||||
func renderFavorites(c *gin.Context, data gin.H) {
|
||||
attachFavoritesRecommended(c, data)
|
||||
HTMLOK(c, "favorites.html", data)
|
||||
}
|
||||
|
||||
// attachFavoritesRecommended 列表为空时拉取最热短剧(与搜索无结果一致)。
|
||||
// 上游:GET /api/app/playlet/all/list sortType=2
|
||||
func attachFavoritesRecommended(c *gin.Context, data gin.H) {
|
||||
if works, ok := data["favorites"].([]favoriteWork); ok && len(works) > 0 {
|
||||
return
|
||||
}
|
||||
watchingQ := playletser.AllListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 6,
|
||||
SortType: 2, // 最热
|
||||
UpdateStatus: 0,
|
||||
}
|
||||
watching := playletser.FetchAllListOrEmpty(c, watchingQ)
|
||||
if watching == nil || len(watching.List) == 0 {
|
||||
return
|
||||
}
|
||||
list := watching.List
|
||||
if len(list) > 6 {
|
||||
list = list[:6]
|
||||
}
|
||||
data["recommended"] = list
|
||||
}
|
||||
|
||||
func loadVideoFavorites(c *gin.Context, data gin.H, responses gin.H, page, pageSize int) {
|
||||
videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize}
|
||||
list, code, err := videoser.FetchCollectList(c, videoQuery)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
list, code, err = videoser.FetchCollectList(c, videoQuery)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch video favorites page failed", log.E(err))
|
||||
responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
data["favoritesUnavailable"] = true
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
responses["video/collect/list"] = gin.H{"code": code, "data": list}
|
||||
return
|
||||
}
|
||||
responses["video/collect/list"] = gin.H{"code": 200, "data": list}
|
||||
data["paging"] = makeFavoritesPage(page, pageSize, list.TotalCount, "series")
|
||||
data["favorites"] = videoFavoriteWorks(list.List)
|
||||
}
|
||||
|
||||
func favoritesRoute(c *gin.Context) (filter string, page int, valid bool) {
|
||||
if c.Request.URL.Query().Has("filter") {
|
||||
return "", 0, false
|
||||
}
|
||||
filter = strings.TrimSpace(c.Param("filter"))
|
||||
switch filter {
|
||||
case "":
|
||||
filter = "all"
|
||||
case "short", "chapter", "series", "following", "finished":
|
||||
default:
|
||||
return "", 0, false
|
||||
}
|
||||
|
||||
page, valid = accountPageNum(c)
|
||||
if !valid {
|
||||
return "", 0, false
|
||||
}
|
||||
return filter, page, true
|
||||
}
|
||||
|
||||
func favoritesPagePath(filter string, page int) string {
|
||||
path := "/favorites"
|
||||
if filter != "" && filter != "all" {
|
||||
path += "/" + filter
|
||||
}
|
||||
if page > 1 {
|
||||
path += "/page/" + strconv.Itoa(page)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func makeFavoritesPage(page, size int, count int64, filter string) accountPage {
|
||||
total := int((count + int64(size) - 1) / int64(size))
|
||||
pagination := accountPage{Current: page, Total: total, Count: count}
|
||||
if page > 1 {
|
||||
pagination.PrevURL = favoritesPagePath(filter, page-1)
|
||||
}
|
||||
if page < total {
|
||||
pagination.NextURL = favoritesPagePath(filter, page+1)
|
||||
}
|
||||
return pagination
|
||||
}
|
||||
|
||||
func playletFavoriteWorks(items []playletser.PlayletCollectData) []favoriteWork {
|
||||
works := make([]favoriteWork, 0, len(items))
|
||||
for _, item := range items {
|
||||
title := strings.TrimSpace(item.PlayName)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(item.Title)
|
||||
}
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
work := favoriteWork{
|
||||
ID: item.ID, Title: title, Genre: strings.Join(item.ThemeNames, " "),
|
||||
Image: profileCover(item.Cover), Kind: "short",
|
||||
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
|
||||
DramaID: item.ID, Episode: item.LastEpisode, VideoID: item.LastChapterID,
|
||||
TotalEpisode: item.TotalEpisode,
|
||||
}
|
||||
if work.Episode <= 0 {
|
||||
work.Episode = 1
|
||||
}
|
||||
if item.TotalEpisode > 0 {
|
||||
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
|
||||
work.DesktopMeta = work.Episodes
|
||||
}
|
||||
if item.IsFeatured {
|
||||
work.Badge = "爆剧"
|
||||
}
|
||||
works = append(works, work)
|
||||
}
|
||||
return works
|
||||
}
|
||||
|
||||
func chapterFavoriteWorks(items []playletser.PlayletChapterCollectData) []favoriteWork {
|
||||
works := make([]favoriteWork, 0, len(items))
|
||||
for _, item := range items {
|
||||
title := strings.TrimSpace(item.PlayletTitle)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
work := favoriteWork{
|
||||
ID: item.ChapterID, Title: title, Image: profileCover(item.PlayletCover), Kind: "chapter",
|
||||
URL: playletser.ChapterPlayPath(item.PlayletSlug, item.PlayletID, item.ChapterID), DesktopMeta: item.ChapterTitle,
|
||||
DramaID: item.PlayletID, Episode: item.CurrentEpisode, VideoID: item.ChapterID,
|
||||
TotalEpisode: item.TotalEpisode,
|
||||
}
|
||||
if work.Episode <= 0 {
|
||||
work.Episode = 1
|
||||
}
|
||||
if item.CurrentEpisode > 0 {
|
||||
work.Genre = fmt.Sprintf("第 %d 集", item.CurrentEpisode)
|
||||
}
|
||||
if item.TotalEpisode > 0 {
|
||||
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
|
||||
}
|
||||
works = append(works, work)
|
||||
}
|
||||
return works
|
||||
}
|
||||
|
||||
func videoFavoriteWorks(items []videoser.VideoCollectData) []favoriteWork {
|
||||
works := make([]favoriteWork, 0, len(items))
|
||||
for _, item := range items {
|
||||
title := strings.TrimSpace(item.Title)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
work := favoriteWork{
|
||||
ID: item.ID, Title: title, Genre: strings.Join(item.Keyworld, " "),
|
||||
Image: profileCover(item.CoverImg), Kind: "series",
|
||||
URL: videoser.VideoPlayURL(item.Slug, item.ID),
|
||||
}
|
||||
if item.VideoTime > 0 {
|
||||
work.Episodes = formatVideoDuration(item.VideoTime)
|
||||
work.DesktopMeta = work.Episodes
|
||||
} else {
|
||||
work.Episodes = "剧集"
|
||||
work.DesktopMeta = "剧集"
|
||||
}
|
||||
works = append(works, work)
|
||||
}
|
||||
return works
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/feedbackser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Feedback 意见反馈页:渲染 template/pages/feedback.html。
|
||||
func Feedback(c *gin.Context) {
|
||||
HTMLOK(c, "feedback.html", gin.H{
|
||||
"title": "意见反馈 - 黄果短剧",
|
||||
})
|
||||
}
|
||||
|
||||
// FeedbackAdd BFF:代理 POST /api/app/feedback/add。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/意见反馈相关接口/post_api_app_feedback_add
|
||||
func FeedbackAdd(c *gin.Context) {
|
||||
var body feedbackser.FeedbackReq
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
body.Content = strings.TrimSpace(body.Content)
|
||||
body.Contact = strings.TrimSpace(body.Contact)
|
||||
if body.Type < 1 || body.Type > 4 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid type",
|
||||
"tip": "请选择问题类型",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.Content == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "content required",
|
||||
"tip": "请填写问题描述",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if runes := []rune(body.Content); len(runes) > 500 {
|
||||
body.Content = string(runes[:500])
|
||||
}
|
||||
if len(body.Screenshots) > 3 {
|
||||
body.Screenshots = body.Screenshots[:3]
|
||||
}
|
||||
|
||||
env, err := feedbackser.Add(c, body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "提交失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "提交失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadImage BFF:代理 POST /api/app/upload/image。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/上传/post_api_app_upload_image
|
||||
func UploadImage(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil || fileHeader == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "file required",
|
||||
"tip": "请选择图片文件",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
f, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "读取图片失败",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
name := filepath.Base(fileHeader.Filename)
|
||||
if name == "" || name == "." || name == "/" {
|
||||
name = "image.jpg"
|
||||
}
|
||||
resp, err := authser.UploadImage(c, name, f)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "上传失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if resp == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "上传失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var data gin.H
|
||||
if resp.Data != nil {
|
||||
url := strings.TrimSpace(resp.Data.SourceNameFs)
|
||||
if url == "" {
|
||||
url = strings.TrimSpace(resp.Data.ID)
|
||||
}
|
||||
data = gin.H{
|
||||
"id": resp.Data.ID,
|
||||
"sourceNameFs": resp.Data.SourceNameFs,
|
||||
"url": url,
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": resp.Code,
|
||||
"msg": resp.Msg,
|
||||
"tip": resp.Tip,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GuessLikeList BFF:代理 GET /api/app/playlet/guess-like/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_guess_like_list
|
||||
func GuessLikeList(c *gin.Context) {
|
||||
playletID, _ := strconv.ParseInt(c.Query("playletId"), 10, 64)
|
||||
pageNum, _ := strconv.Atoi(c.Query("pageNum"))
|
||||
pageSize, _ := strconv.Atoi(c.Query("pageSize"))
|
||||
if playletID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "playletId required",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
data := playletser.FetchGuessLikeListOrEmpty(c, playletser.GuessLikeQuery{
|
||||
PlayletID: playletID,
|
||||
PageNum: pageNum,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/seoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Help renders the static Help page.
|
||||
func Help(c *gin.Context) {
|
||||
HTMLOK(c, "help.html", gin.H{
|
||||
"faqItems": []seoser.FAQItem{
|
||||
{Question: "黄果短剧网页版可以看全集吗?", Answer: "可以。免费集直接观看,付费集可用金币按集解锁,解锁后永久有效。"},
|
||||
{Question: "解锁过的剧集还需要再付费吗?", Answer: "不需要。已解锁的剧集可在登录后继续观看,无需重复付费。"},
|
||||
{Question: "如何充值金币?", Answer: "登录后进入个人中心,选择金币充值,即可查看可用的充值方式。"},
|
||||
{Question: "支持哪些支付方式?", Answer: "可用的支付方式会在充值页面列出,请以页面显示为准。"},
|
||||
{Question: "忘记密码怎么办?", Answer: "请通过意见反馈联系我们,提交账号信息后由客服协助处理。"},
|
||||
{Question: "内容多久更新一次?", Answer: "黄果短剧持续更新内容,欢迎关注首页的新剧和最近上新。"},
|
||||
{Question: "如何联系客服或洽谈合作?", Answer: "请发送邮件至 support@huangguo.com,我们会尽快回复。"},
|
||||
{Question: "如何注销账号?", Answer: "请通过意见反馈或客服邮箱提交注销申请。"},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/videoser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type historyEntry struct {
|
||||
ID int64
|
||||
Title string
|
||||
Current int
|
||||
Total int
|
||||
EpisodeText string
|
||||
Genre string
|
||||
Time string
|
||||
Image string
|
||||
Progress string
|
||||
Kind string
|
||||
URL string
|
||||
sortAt time.Time
|
||||
}
|
||||
|
||||
type historyGroup struct {
|
||||
Title string
|
||||
Entries []historyEntry
|
||||
}
|
||||
|
||||
// History 观看历史页:短剧 GET /api/app/playlet/history/list + 剧集 GET /api/app/video/history/list。
|
||||
// 文档:
|
||||
// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_history_list
|
||||
// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_history_list
|
||||
func History(c *gin.Context) {
|
||||
page, validPage := accountPageNum(c)
|
||||
if !validPage {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{"title": "观看历史 - 黄果短剧", "navActive": "history", "accountTab": "history"}
|
||||
responses := gin.H{}
|
||||
loadAccountSummary(c, data, responses)
|
||||
|
||||
pageSize := 20
|
||||
var entries []historyEntry
|
||||
var totalCount int64
|
||||
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
responses["playlet/history/list"] = gin.H{"error": "暂无登录会话"}
|
||||
responses["video/history/list"] = gin.H{"error": "暂无登录会话"}
|
||||
} else {
|
||||
playletQuery := playletser.HistoryListQuery{PageNum: page, PageSize: pageSize}
|
||||
playletList, playletCode, playletErr := playletser.FetchHistoryList(c, playletQuery)
|
||||
if retry, _ := authser.OnSessionExpired(c, playletCode); retry {
|
||||
playletList, playletCode, playletErr = playletser.FetchHistoryList(c, playletQuery)
|
||||
}
|
||||
if playletErr == nil && playletList != nil {
|
||||
responses["playlet/history/list"] = gin.H{"code": 200, "data": playletList}
|
||||
entries = append(entries, playletHistoryEntries(playletList.List)...)
|
||||
totalCount += playletList.TotalCount
|
||||
} else if playletErr != nil {
|
||||
responses["playlet/history/list"] = gin.H{"error": "加载失败", "msg": playletErr.Error()}
|
||||
log.Error("fetch playlet history page failed", log.E(playletErr))
|
||||
} else {
|
||||
responses["playlet/history/list"] = gin.H{"code": playletCode, "data": playletList}
|
||||
}
|
||||
|
||||
videoQuery := videoser.HistoryListQuery{PageNum: page, PageSize: pageSize}
|
||||
videoList, videoCode, videoErr := videoser.FetchHistoryList(c, videoQuery)
|
||||
if retry, _ := authser.OnSessionExpired(c, videoCode); retry {
|
||||
videoList, videoCode, videoErr = videoser.FetchHistoryList(c, videoQuery)
|
||||
}
|
||||
if videoErr == nil && videoList != nil {
|
||||
responses["video/history/list"] = gin.H{"code": 200, "data": videoList}
|
||||
entries = append(entries, videoHistoryEntries(videoList.List)...)
|
||||
totalCount += videoList.TotalCount
|
||||
} else if videoErr != nil {
|
||||
responses["video/history/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()}
|
||||
log.Error("fetch video history page failed", log.E(videoErr))
|
||||
} else {
|
||||
responses["video/history/list"] = gin.H{"code": videoCode, "data": videoList}
|
||||
}
|
||||
|
||||
if playletErr != nil && videoErr != nil {
|
||||
data["historyUnavailable"] = true
|
||||
}
|
||||
}
|
||||
|
||||
data["paging"] = makeAccountPage("/history", page, pageSize, totalCount)
|
||||
data["groups"] = groupHistoryEntries(entries)
|
||||
data["historyResponse"] = responses["playlet/history/list"]
|
||||
data["playletHistoryResponse"] = responses["playlet/history/list"]
|
||||
data["videoHistoryResponse"] = responses["video/history/list"]
|
||||
data["historyDebug"] = responses
|
||||
HTMLOK(c, "history.html", data)
|
||||
}
|
||||
|
||||
// HistoryClear BFF:代理 POST /api/app/playlet/history/clear。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_clear
|
||||
func HistoryClear(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
env, err := playletser.HistoryClear(c)
|
||||
if err != nil {
|
||||
log.Error("clear playlet history failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env.Code != 200 {
|
||||
tip := strings.TrimSpace(env.Tip)
|
||||
if tip == "" {
|
||||
tip = strings.TrimSpace(env.Msg)
|
||||
}
|
||||
if tip == "" {
|
||||
tip = "清空失败,请稍后重试"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
|
||||
}
|
||||
|
||||
// VideoHistoryClear BFF:代理 POST /api/app/video/history/clear。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_clear
|
||||
func VideoHistoryClear(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
env, err := videoser.HistoryClear(c)
|
||||
if err != nil {
|
||||
log.Error("clear video history failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env.Code != 200 {
|
||||
tip := strings.TrimSpace(env.Tip)
|
||||
if tip == "" {
|
||||
tip = strings.TrimSpace(env.Msg)
|
||||
}
|
||||
if tip == "" {
|
||||
tip = "清空失败,请稍后重试"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
|
||||
}
|
||||
|
||||
// HistoryDelete BFF:代理 POST /api/app/playlet/history/delete。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_delete
|
||||
func HistoryDelete(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
var body playletser.PlayletHistoryDeleteReq
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.PlayletID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "playletId required", "tip": "参数错误", "data": nil})
|
||||
return
|
||||
}
|
||||
env, err := playletser.HistoryDelete(c, body)
|
||||
if err != nil {
|
||||
log.Error("delete playlet history failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env.Code != 200 {
|
||||
tip := strings.TrimSpace(env.Tip)
|
||||
if tip == "" {
|
||||
tip = strings.TrimSpace(env.Msg)
|
||||
}
|
||||
if tip == "" {
|
||||
tip = "删除失败,请稍后重试"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
|
||||
}
|
||||
|
||||
// VideoHistoryDelete BFF:代理 POST /api/app/video/history/delete。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_delete
|
||||
func VideoHistoryDelete(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
var body videoser.VideoHistoryDeleteReq
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.VideoID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "videoId required", "tip": "参数错误", "data": nil})
|
||||
return
|
||||
}
|
||||
env, err := videoser.HistoryDelete(c, body)
|
||||
if err != nil {
|
||||
log.Error("delete video history failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env.Code != 200 {
|
||||
tip := strings.TrimSpace(env.Tip)
|
||||
if tip == "" {
|
||||
tip = strings.TrimSpace(env.Msg)
|
||||
}
|
||||
if tip == "" {
|
||||
tip = "删除失败,请稍后重试"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
|
||||
}
|
||||
|
||||
func playletHistoryEntries(items []playletser.PlayletHistoryData) []historyEntry {
|
||||
out := make([]historyEntry, 0, len(items))
|
||||
for _, item := range items {
|
||||
title := strings.TrimSpace(item.PlayName)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(item.Title)
|
||||
}
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
progress := 0
|
||||
if item.TotalEpisode > 0 {
|
||||
progress = item.WatchedCount * 100 / item.TotalEpisode
|
||||
}
|
||||
if progress < 0 {
|
||||
progress = 0
|
||||
}
|
||||
if progress > 100 {
|
||||
progress = 100
|
||||
}
|
||||
out = append(out, historyEntry{
|
||||
ID: item.ID,
|
||||
Title: title,
|
||||
Current: item.LastEpisode,
|
||||
Total: item.TotalEpisode,
|
||||
Genre: strings.Join(item.ThemeNames, " "),
|
||||
Time: playletser.FormatHistoryListTime(item.HistoryAt),
|
||||
Image: profileCover(item.Cover),
|
||||
Progress: fmt.Sprintf("%d%%", progress),
|
||||
Kind: "short",
|
||||
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
|
||||
sortAt: historySortAt(item.HistoryAt),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func videoHistoryEntries(items []videoser.VideoHistoryData) []historyEntry {
|
||||
out := make([]historyEntry, 0, len(items))
|
||||
for _, item := range items {
|
||||
title := strings.TrimSpace(item.Title)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, historyEntry{
|
||||
ID: item.ID,
|
||||
Title: title,
|
||||
EpisodeText: videoHistoryEpisodeText(item),
|
||||
Genre: strings.Join(item.Keyworld, " "),
|
||||
Time: playletser.FormatHistoryListTime(item.HistoryAt),
|
||||
Image: profileCover(item.CoverImg),
|
||||
Progress: "",
|
||||
Kind: "series",
|
||||
URL: videoser.VideoPlayURL(item.Slug, item.ID),
|
||||
sortAt: historySortAt(item.HistoryAt),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func videoHistoryEpisodeText(item videoser.VideoHistoryData) string {
|
||||
if item.VideoTime > 0 {
|
||||
return formatVideoDuration(item.VideoTime)
|
||||
}
|
||||
return "剧集"
|
||||
}
|
||||
|
||||
func formatVideoDuration(sec int) string {
|
||||
if sec <= 0 {
|
||||
return ""
|
||||
}
|
||||
h := sec / 3600
|
||||
m := (sec % 3600) / 60
|
||||
s := sec % 60
|
||||
if h > 0 {
|
||||
return fmt.Sprintf("%d:%02d:%02d", h, m, s)
|
||||
}
|
||||
return fmt.Sprintf("%d:%02d", m, s)
|
||||
}
|
||||
|
||||
func historySortAt(raw string) time.Time {
|
||||
t, ok := playletser.ParseHistoryTime(raw)
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func groupHistoryEntries(entries []historyEntry) []historyGroup {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.SliceStable(entries, func(i, j int) bool {
|
||||
if entries[i].sortAt.Equal(entries[j].sortAt) {
|
||||
return entries[i].ID > entries[j].ID
|
||||
}
|
||||
return entries[i].sortAt.After(entries[j].sortAt)
|
||||
})
|
||||
order := []string{"今天", "昨天", "更早"}
|
||||
buckets := map[string][]historyEntry{
|
||||
"今天": {},
|
||||
"昨天": {},
|
||||
"更早": {},
|
||||
}
|
||||
for _, entry := range entries {
|
||||
bucket := historyBucketFromTime(entry.sortAt)
|
||||
buckets[bucket] = append(buckets[bucket], entry)
|
||||
}
|
||||
groups := make([]historyGroup, 0, 3)
|
||||
for _, title := range order {
|
||||
if len(buckets[title]) == 0 {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, historyGroup{Title: title, Entries: buckets[title]})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func historyBucketFromTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "更早"
|
||||
}
|
||||
now := time.Now()
|
||||
startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
dayDiff := int(startToday.Sub(startThat).Hours() / 24)
|
||||
switch {
|
||||
case dayDiff <= 0:
|
||||
return "今天"
|
||||
case dayDiff == 1:
|
||||
return "昨天"
|
||||
default:
|
||||
return "更早"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Index 首页 SSR:拉取 GET /api/app/playlet/home,按接口字段注入模板。
|
||||
// 接口文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_home
|
||||
// 继续观看:GET /api/app/playlet/history/list?pageNum=1&pageSize=6
|
||||
// 热播榜:短剧 hot/rank/list;剧集 video/all/list sortType=最热
|
||||
func Index(c *gin.Context) {
|
||||
raw := playletser.FetchHomeOrEmpty(c)
|
||||
title := "黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
history := playletser.FetchHistoryListOrEmpty(c, playletser.HistoryListQuery{PageNum: 1, PageSize: 6})
|
||||
continueWatch := history.List
|
||||
if len(continueWatch) > 6 {
|
||||
continueWatch = continueWatch[:6]
|
||||
}
|
||||
|
||||
hotPlaylets := playletser.FetchHotRankListOrEmpty(c, playletser.HotRankQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 10,
|
||||
RankType: 1,
|
||||
}).List
|
||||
if len(hotPlaylets) > 10 {
|
||||
hotPlaylets = hotPlaylets[:10]
|
||||
}
|
||||
|
||||
hotVideos := videoser.FetchVideoAllListOrEmpty(c, videoser.SeriesListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 10,
|
||||
SortType: videoser.SortHeat,
|
||||
MediaType: videoser.MediaAll,
|
||||
}).List
|
||||
if len(hotVideos) > 10 {
|
||||
hotVideos = hotVideos[:10]
|
||||
}
|
||||
|
||||
HTMLOK(c, "index.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "home",
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"announcements": raw.Announcements,
|
||||
"advitise": raw.Advitise,
|
||||
"homeBannerData": playletser.ActiveHomeBanners(raw.HomeBannerData),
|
||||
"homeConfigData": playletser.ActiveHomeConfigs(raw.HomeConfigData),
|
||||
// GET /api/app/playlet/tag/options → list(已上架标签)
|
||||
"tagOptions": playletser.FetchTagOptionsOrEmpty(c),
|
||||
"continueWatch": continueWatch,
|
||||
"hotRankPlaylets": hotPlaylets,
|
||||
"hotRankVideos": hotVideos,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List 短剧列表页:题材 + GET /api/app/playlet/all/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_all_list
|
||||
func List(c *gin.Context) {
|
||||
q := playletser.ParseAllListQuery(c)
|
||||
themes := playletser.FetchThemesOrEmpty(c, 0)
|
||||
playletser.ResolveAllListTheme(&q, themes)
|
||||
themeResolved := listThemeResolved(q, themes)
|
||||
raw := playletser.FetchAllListOrEmpty(c, q)
|
||||
pageHeading, seoSelections := listSEOContext(q, themes)
|
||||
title := pageHeading + " - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
var advitise any
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
advitise = raw.Advitise
|
||||
}
|
||||
HTMLOK(c, "list.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "list",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": seoSelections,
|
||||
"pageHeading": pageHeading,
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"advitise": advitise,
|
||||
"themeList": themes,
|
||||
"themeSlug": q.ThemeSlug,
|
||||
"themeId": q.ThemeID,
|
||||
"updateStatus": q.UpdateStatus,
|
||||
"sortType": q.SortType,
|
||||
"seoEntityValid": themeResolved || total > 0,
|
||||
"pagination": playletser.BuildAllListPagination(q, total),
|
||||
"canonicalPath": q.URL(),
|
||||
})
|
||||
}
|
||||
|
||||
func listThemeResolved(q playletser.AllListQuery, themes []playletser.PlayletThemeItem) bool {
|
||||
if q.ThemeID <= 0 && q.ThemeSlug == "" {
|
||||
return true
|
||||
}
|
||||
for _, theme := range themes {
|
||||
if (q.ThemeID > 0 && theme.ID == q.ThemeID) || (q.ThemeSlug != "" && theme.Slug == q.ThemeSlug) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func listSEOContext(q playletser.AllListQuery, themes []playletser.PlayletThemeItem) (string, []playletser.SEOSelection) {
|
||||
heading := "全部短剧"
|
||||
selections := make([]playletser.SEOSelection, 0, 3)
|
||||
themeName := q.ThemeSlug
|
||||
for _, theme := range themes {
|
||||
if (q.ThemeID > 0 && theme.ID == q.ThemeID) || (q.ThemeSlug != "" && theme.Slug == q.ThemeSlug) {
|
||||
themeName = theme.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if themeName != "" {
|
||||
heading = themeName + "短剧"
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "题材", Value: themeName})
|
||||
}
|
||||
if q.UpdateStatus == 1 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "状态", Value: "连载中"})
|
||||
} else if q.UpdateStatus == 2 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "状态", Value: "已完结"})
|
||||
}
|
||||
if q.SortType == 2 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "排序", Value: "最热"})
|
||||
} else if q.SortType == 3 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "排序", Value: "收藏最多"})
|
||||
}
|
||||
return heading, selections
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// LoginPage H5 登录页:渲染 template/pages/login.html。
|
||||
// PC 端由页面内脚本回首页并打开 loginPop;H5 直接展示本页。
|
||||
func LoginPage(c *gin.Context) {
|
||||
HTMLOK(c, "login.html", gin.H{
|
||||
"title": "登录 - 黄果短剧",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MoreList 首页配置更多页:GET /api/app/playlet/home/config/detail/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_home_config_detail_list
|
||||
func MoreList(c *gin.Context) {
|
||||
q := playletser.ParseMoreListQuery(c)
|
||||
themes := playletser.FetchThemesOrEmpty(c, 0)
|
||||
playletser.ResolveMoreListTheme(&q, themes)
|
||||
raw := playletser.FetchHomeConfigDetailListOrEmpty(c, q)
|
||||
if raw != nil {
|
||||
playletser.ApplyHomeConfigDetailIdentity(&q, raw)
|
||||
}
|
||||
home := playletser.FetchHomeOrEmpty(c)
|
||||
homeConfigData := playletser.ActiveHomeConfigs(home.HomeConfigData)
|
||||
|
||||
name := "更多短剧"
|
||||
desc := ""
|
||||
entityName := ""
|
||||
var seo playletser.SeoInfo
|
||||
if raw != nil {
|
||||
if raw.Name != "" {
|
||||
name = raw.Name
|
||||
entityName = raw.Name
|
||||
}
|
||||
desc = raw.Desc
|
||||
seo = raw.SeoInfo
|
||||
}
|
||||
if config, ok := playletser.FindHomeConfigByIdentity(homeConfigData, q.HomeConfigID, q.HomeConfigSlug); ok {
|
||||
seo = playletser.MergeSeoInfo(config.SeoInfo, seo)
|
||||
if raw == nil || raw.Name == "" {
|
||||
if config.Name != "" {
|
||||
name = config.Name
|
||||
entityName = config.Name
|
||||
}
|
||||
}
|
||||
if desc == "" {
|
||||
desc = config.Desc
|
||||
}
|
||||
}
|
||||
title := name + " - 黄果短剧"
|
||||
if seo.Title != "" {
|
||||
title = seo.Title
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var advitise any
|
||||
entityValid := false
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
advitise = raw.Advitise
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
entityValid = playletser.MoreListEntityValid(q, entityName, total)
|
||||
}
|
||||
|
||||
HTMLOK(c, "moreList.html", gin.H{
|
||||
"title": title,
|
||||
"seoInfo": seo,
|
||||
"routeConfigSEO": true,
|
||||
"homeConfigData": homeConfigData,
|
||||
"seoSelections": moreListSEOSelections(q, themes),
|
||||
"navActive": "home",
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"configId": q.HomeConfigID,
|
||||
"configSlug": q.HomeConfigSlug,
|
||||
"configName": name,
|
||||
"configDesc": desc,
|
||||
"timeRange": q.TimeRange,
|
||||
"themeSlug": q.ThemeSlug,
|
||||
"themeId": q.ThemeID,
|
||||
"seoEntityValid": entityValid,
|
||||
"themeList": themes,
|
||||
"timeTabs": playletser.BuildMoreTimeTabs(q),
|
||||
"advitise": advitise,
|
||||
"pagination": playletser.BuildMoreListPagination(q, total),
|
||||
"canonicalPath": playletser.MoreListCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MoreSeriesList 首页影集配置更多页(type=2):GET /api/app/playlet/home/config/detail/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_home_config_detail_list
|
||||
func MoreSeriesList(c *gin.Context) {
|
||||
q := playletser.ParseMoreListQuery(c)
|
||||
themes := playletser.FetchThemesOrEmpty(c, 0)
|
||||
playletser.ResolveMoreListTheme(&q, themes)
|
||||
raw := playletser.FetchHomeConfigDetailListOrEmpty(c, q)
|
||||
|
||||
name := "更多视频"
|
||||
desc := ""
|
||||
if raw != nil {
|
||||
if raw.Name != "" {
|
||||
name = raw.Name
|
||||
}
|
||||
desc = raw.Desc
|
||||
}
|
||||
title := name + " - 黄果短剧"
|
||||
|
||||
list := []playletser.VideoItemResp(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var advitise any
|
||||
entityValid := false
|
||||
if raw != nil {
|
||||
list = raw.VideoList
|
||||
total = raw.TotalCount
|
||||
advitise = raw.Advitise
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
playletser.ApplyHomeConfigDetailIdentity(&q, raw)
|
||||
entityValid = playletser.MoreListEntityValid(q, raw.Name, total)
|
||||
}
|
||||
|
||||
HTMLOK(c, "moreSeriesList.html", gin.H{
|
||||
"title": title,
|
||||
"seoSelections": moreListSEOSelections(q, themes),
|
||||
"navActive": "home",
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"configId": q.HomeConfigID,
|
||||
"configSlug": q.HomeConfigSlug,
|
||||
"configName": name,
|
||||
"configDesc": desc,
|
||||
"timeRange": q.TimeRange,
|
||||
"themeSlug": q.ThemeSlug,
|
||||
"themeId": q.ThemeID,
|
||||
"seoEntityValid": entityValid,
|
||||
"themeList": themes,
|
||||
"timeTabs": playletser.BuildMoreSeriesTimeTabs(q),
|
||||
"advitise": advitise,
|
||||
"pagination": playletser.BuildMoreSeriesListPagination(q, total),
|
||||
"canonicalPath": playletser.MoreSeriesListCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Nav 官方导航页 SSR:直接使用 config/info 注入的 appConfig。
|
||||
func Nav(c *gin.Context) {
|
||||
HTMLOK(c, "nav.html", gin.H{
|
||||
"title": "黄果短剧 · 官方导航",
|
||||
"isNavPage": true,
|
||||
"pageKey": "nav",
|
||||
"pageName": "官方导航",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// NotFound 404 页面:渲染 template/pages/404.html。
|
||||
func NotFound(c *gin.Context) {
|
||||
HTML(c, http.StatusNotFound, "404.html", gin.H{
|
||||
"title": "页面不存在 - 黄果短剧",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "videoweb/app/service/playletser"
|
||||
|
||||
// mockPagination 为静态稿页生成分页数据;pageURL 暂统一为 #。
|
||||
func mockPagination(page, pageSize int, total int64) playletser.PaginationVM {
|
||||
return playletser.BuildPagination(func(int) string { return "#" }, page, pageSize, total)
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/commentser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Play 播放页:GET /api/app/playlet/detail + GET /api/app/playlet/chapter/detail。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_detail
|
||||
func Play(c *gin.Context) {
|
||||
if c.Request.URL.Query().Has("guessPage") {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
q := playletser.ParsePlayQuery(c.Param("slug"), c.Param("ep"))
|
||||
if strings.TrimSpace(c.Param("ep")) != "" && q.ChapterID <= 0 {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
playletKey := c.Param("slug")
|
||||
playlet := playletser.FetchDetailOrNilByKey(c, playletKey)
|
||||
if playlet == nil {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
chapterID := playletser.ResolvePlayChapterID(playlet, q.ChapterID)
|
||||
listedChapter := playletser.FindChapter(playlet, chapterID)
|
||||
if chapterID <= 0 || (q.ChapterID > 0 && listedChapter == nil) {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
chapter := playletser.FetchChapterDetailOrNil(c, chapterID, playlet.ID, playlet.Slug)
|
||||
if chapter == nil {
|
||||
if listedChapter != nil {
|
||||
chapter = playletser.ChapterFromList(*listedChapter)
|
||||
}
|
||||
}
|
||||
if chapter == nil {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
seo := playletser.ResolvePlayPageSEO(chapter, playlet)
|
||||
title := playletser.ResolvePlayPageTitle(chapter, playlet)
|
||||
if title == "" {
|
||||
title = "在线播放 - 黄果短剧"
|
||||
}
|
||||
|
||||
hot := playletser.FetchAllListOrEmpty(c, playletser.AllListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 8,
|
||||
SortType: 2, // 最热
|
||||
UpdateStatus: 0, // 全部
|
||||
})
|
||||
hotList := []playletser.CommonData(nil)
|
||||
if hot != nil {
|
||||
hotList = hot.List
|
||||
}
|
||||
|
||||
guessPage := playletser.ParseGuessLikePage(c)
|
||||
guessLike := playletser.FetchGuessLikeListOrEmpty(c, playletser.GuessLikeQuery{
|
||||
PlayletID: playlet.ID,
|
||||
PageNum: guessPage,
|
||||
PageSize: 6,
|
||||
})
|
||||
if len(guessLike.List) == 0 && guessPage > 1 {
|
||||
guessLike = playletser.FetchGuessLikeListOrEmpty(c, playletser.GuessLikeQuery{
|
||||
PlayletID: playlet.ID,
|
||||
PageNum: 1,
|
||||
PageSize: 6,
|
||||
})
|
||||
}
|
||||
|
||||
commentObjectID := playlet.ID
|
||||
commentObjectType := commentser.ObjectTypePlaylet
|
||||
if chapterID > 0 {
|
||||
commentObjectID = chapterID
|
||||
commentObjectType = commentser.ObjectTypeChapter
|
||||
}
|
||||
comments := commentser.FetchListOrEmpty(c, commentser.ListQuery{
|
||||
ObjectID: commentObjectID,
|
||||
ObjectType: commentObjectType,
|
||||
PageNum: 1,
|
||||
PageSize: 4,
|
||||
SortOrder: commentser.SortHot,
|
||||
})
|
||||
|
||||
unlockCost := int64(0)
|
||||
balance := int64(0)
|
||||
canWatch := false
|
||||
episode := 0
|
||||
if chapter != nil {
|
||||
unlockCost = chapter.UnlockCost()
|
||||
balance = chapter.Balance
|
||||
canWatch = chapter.AllowWatch()
|
||||
episode = chapter.CurrentEpisode
|
||||
}
|
||||
if episode <= 0 {
|
||||
for i, item := range playlet.PlayletChapterInfo {
|
||||
if item.ID != chapterID {
|
||||
continue
|
||||
}
|
||||
episode = item.CurrentEpisode
|
||||
if episode <= 0 {
|
||||
episode = i + 1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if episode <= 0 {
|
||||
episode = 1
|
||||
}
|
||||
if unlockCost <= 0 {
|
||||
unlockCost = int64(playletser.FirstUnlockCost(playlet.PlayletChapterInfo))
|
||||
}
|
||||
|
||||
previewURL := strings.TrimSpace(playlet.Preview)
|
||||
mainURL := ""
|
||||
if canWatch && chapter != nil {
|
||||
mainURL = strings.TrimSpace(chapter.VideoURL)
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"navActive": "list",
|
||||
"seoInfo": seo,
|
||||
"playlet": playlet,
|
||||
"chapter": chapter,
|
||||
"chapterID": chapterID,
|
||||
"episode": episode,
|
||||
"canWatch": canWatch,
|
||||
"mainURL": mainURL,
|
||||
"previewURL": previewURL,
|
||||
"hasPreview": previewURL != "",
|
||||
"unlockCost": unlockCost,
|
||||
"balance": balance,
|
||||
"balanceText": authser.FormatRecharge(balance),
|
||||
"nextChapterID": playletser.NextChapterID(playlet, chapterID),
|
||||
"advitise": playlet.Advitise,
|
||||
"related": playlet.RelatedRecommendData,
|
||||
"guessLike": guessLike.List,
|
||||
"guessPage": guessLike.PageNum,
|
||||
"guessPageSize": guessLike.PageSize,
|
||||
"guessTotal": guessLike.TotalCount,
|
||||
"hotList": hotList,
|
||||
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
|
||||
"comments": comments.List,
|
||||
"commentTotal": comments.TotalCount,
|
||||
"commentPage": comments.PageNum,
|
||||
"commentSize": comments.PageSize,
|
||||
"commentObjectID": commentObjectID,
|
||||
"commentObjectType": commentObjectType,
|
||||
"trackerDrama": dramaTrackingData(playlet, chapter, chapterID, episode),
|
||||
"playResponse": gin.H{
|
||||
"playlet": playlet,
|
||||
"chapter": chapter,
|
||||
"chapterID": chapterID,
|
||||
"canWatch": canWatch,
|
||||
"mainURL": mainURL,
|
||||
"previewURL": previewURL,
|
||||
"hasPreview": previewURL != "",
|
||||
"guessLike": guessLike,
|
||||
"hotList": hotList,
|
||||
"comments": comments,
|
||||
"commentObjectID": commentObjectID,
|
||||
"commentObjectType": commentObjectType,
|
||||
},
|
||||
}
|
||||
data["canonicalPath"] = playletser.PlayPath(playlet.Slug, playlet.ID)
|
||||
attachRechargeModal(c, data)
|
||||
HTMLOK(c, "play.html", data)
|
||||
}
|
||||
|
||||
func dramaTrackingData(playlet *playletser.PlayletInfoResp, chapter *playletser.PlayletChapterInfoResp, chapterID int64, episode int) gin.H {
|
||||
name := strings.TrimSpace(playlet.PlayName)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(playlet.Title)
|
||||
}
|
||||
totalEpisode := playlet.TotalEpisode
|
||||
if totalEpisode <= 0 {
|
||||
totalEpisode = len(playlet.PlayletChapterInfo)
|
||||
}
|
||||
if totalEpisode <= 0 {
|
||||
totalEpisode = 1
|
||||
}
|
||||
data := gin.H{
|
||||
"MediaID": "",
|
||||
"DramaID": playlet.ID,
|
||||
"DramaTitle": name,
|
||||
"Episode": max(1, episode),
|
||||
"TotalEpisode": totalEpisode,
|
||||
"VideoID": chapterID,
|
||||
"DramaTypeID": "default",
|
||||
"DramaTypeName": "默认分类",
|
||||
"DramaTagKey": "default",
|
||||
"DramaTagName": "默认标签",
|
||||
"DramaDuration": 0,
|
||||
"RecommendTraceID": "",
|
||||
}
|
||||
if chapter != nil {
|
||||
if chapter.ID > 0 {
|
||||
data["VideoID"] = chapter.ID
|
||||
}
|
||||
if chapter.MediaTime > 0 {
|
||||
data["DramaDuration"] = chapter.MediaTime
|
||||
}
|
||||
}
|
||||
for i, id := range playlet.ThemeIDs {
|
||||
if id <= 0 || i >= len(playlet.ThemeNames) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(playlet.ThemeNames[i])
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
data["DramaTypeID"] = strconv.FormatInt(id, 10)
|
||||
data["DramaTypeName"] = name
|
||||
break
|
||||
}
|
||||
tagIDs := make([]string, 0, len(playlet.TagIDs))
|
||||
tagNames := make([]string, 0, len(playlet.TagIDs))
|
||||
for i, id := range playlet.TagIDs {
|
||||
if id <= 0 || i >= len(playlet.TagNames) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(playlet.TagNames[i])
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
tagIDs = append(tagIDs, strconv.FormatInt(id, 10))
|
||||
tagNames = append(tagNames, name)
|
||||
}
|
||||
if len(tagIDs) > 0 {
|
||||
data["DramaTagKey"] = strings.Join(tagIDs, ",")
|
||||
data["DramaTagName"] = strings.Join(tagNames, ",")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// SeriesPlay 视频播放页:GET /api/app/video/detail。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_detail
|
||||
func SeriesPlay(c *gin.Context) {
|
||||
if c.Request.URL.Query().Has("guessPage") {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
key := c.Param("slug")
|
||||
video := videoser.FetchVideoDetailOrNilByKey(c, key)
|
||||
if video == nil {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
title := videoser.ResolveVideoPageTitle(video)
|
||||
if title == "" {
|
||||
title = "在线播放 - 黄果短剧"
|
||||
} else {
|
||||
title = title + " - 黄果短剧"
|
||||
}
|
||||
|
||||
cover := video.CoverImg
|
||||
if cover == "" {
|
||||
cover = video.Video.Cover
|
||||
}
|
||||
videoURL := strings.TrimSpace(video.Video.URL)
|
||||
canWatch := videoURL != ""
|
||||
|
||||
commentObjectID := video.ID
|
||||
// 视频评论:objectType=20(swagger:10-短剧 11-短剧剧集 20-视频)
|
||||
commentObjectType := commentser.ObjectTypeVideo
|
||||
comments := commentser.FetchListOrEmpty(c, commentser.ListQuery{
|
||||
ObjectID: commentObjectID,
|
||||
ObjectType: commentObjectType,
|
||||
PageNum: 1,
|
||||
PageSize: 4,
|
||||
SortOrder: commentser.SortHot,
|
||||
})
|
||||
if comments.PageNum <= 0 {
|
||||
comments.PageNum = 1
|
||||
}
|
||||
if comments.PageSize <= 0 {
|
||||
comments.PageSize = 4
|
||||
}
|
||||
commentTotal := comments.TotalCount
|
||||
if commentTotal <= 0 && video.CountComment > 0 {
|
||||
commentTotal = video.CountComment
|
||||
}
|
||||
|
||||
hot := playletser.FetchAllListOrEmpty(c, playletser.AllListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 8,
|
||||
SortType: 2, // 最热
|
||||
UpdateStatus: 0, // 全部
|
||||
})
|
||||
hotList := []playletser.CommonData(nil)
|
||||
if hot != nil {
|
||||
hotList = hot.List
|
||||
}
|
||||
|
||||
// 视频详情无 recommendVideos:猜你喜欢与 play.html 一致,走短剧同题材推荐。
|
||||
// 以热榜首部短剧为种子 playletId(排除该剧本身)。
|
||||
guessPlayletID := int64(0)
|
||||
if len(hotList) > 0 {
|
||||
guessPlayletID = hotList[0].ID
|
||||
}
|
||||
guessPage := playletser.ParseGuessLikePage(c)
|
||||
guessLike := playletser.FetchGuessLikeListOrEmpty(c, playletser.GuessLikeQuery{
|
||||
PlayletID: guessPlayletID,
|
||||
PageNum: guessPage,
|
||||
PageSize: 6,
|
||||
})
|
||||
if len(guessLike.List) == 0 && guessPage > 1 {
|
||||
guessLike = playletser.FetchGuessLikeListOrEmpty(c, playletser.GuessLikeQuery{
|
||||
PlayletID: guessPlayletID,
|
||||
PageNum: 1,
|
||||
PageSize: 6,
|
||||
})
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"navActive": "aiSeries",
|
||||
"seoInfo": video.SeoInfo,
|
||||
"video": video,
|
||||
"videoID": video.ID,
|
||||
"videoSlug": video.Slug,
|
||||
"cover": cover,
|
||||
"videoURL": videoURL,
|
||||
"canWatch": canWatch,
|
||||
"guessLike": guessLike.List,
|
||||
"guessPage": guessLike.PageNum,
|
||||
"guessPageSize": guessLike.PageSize,
|
||||
"guessTotal": guessLike.TotalCount,
|
||||
"guessPlayletID": guessPlayletID,
|
||||
"advitise": video.Advitise,
|
||||
"comments": comments.List,
|
||||
"commentTotal": commentTotal,
|
||||
"commentPage": comments.PageNum,
|
||||
"commentSize": comments.PageSize,
|
||||
"commentObjectID": commentObjectID,
|
||||
"commentObjectType": commentObjectType,
|
||||
"hotList": hotList,
|
||||
"hotMoreURL": playletser.AllListURL("all", 0, 0, 2, 1),
|
||||
"videoResponse": gin.H{
|
||||
"video": video,
|
||||
"videoURL": videoURL,
|
||||
"canWatch": canWatch,
|
||||
"comments": comments,
|
||||
"hotList": hotList,
|
||||
"guessLike": guessLike,
|
||||
"guessPlayletID": guessPlayletID,
|
||||
"commentObjectID": commentObjectID,
|
||||
"commentObjectType": commentObjectType,
|
||||
},
|
||||
"commentDebug": gin.H{
|
||||
"path": "/api/app/comment/list",
|
||||
"objectId": commentObjectID,
|
||||
"objectType": commentObjectType, // 20-视频
|
||||
"pageNum": comments.PageNum,
|
||||
"pageSize": comments.PageSize,
|
||||
"totalCount": commentTotal,
|
||||
"list": comments.List,
|
||||
"apis": []string{
|
||||
"GET /api/app/comment/list",
|
||||
"POST /api/app/comment/send",
|
||||
"POST /api/app/comment/like/set",
|
||||
},
|
||||
},
|
||||
"playDebug": gin.H{
|
||||
"canWatch": canWatch,
|
||||
"videoURL": videoURL,
|
||||
"cover": cover,
|
||||
"videoTime": video.Video.VideoTime,
|
||||
},
|
||||
}
|
||||
if strings.TrimSpace(c.Param("guessPage")) != "" {
|
||||
data["canonicalPath"] = videoser.VideoPlayURL(video.Slug, video.ID)
|
||||
}
|
||||
attachRechargeModal(c, data)
|
||||
HTMLOK(c, "seriesPlay.html", data)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PlayletLikeSet BFF:代理 POST /api/app/playlet/like/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_like_set
|
||||
func PlayletLikeSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ID int64 `json:"id"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "id/isLike required",
|
||||
"tip": "缺少短剧 ID 或点赞状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{
|
||||
ID: body.ID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PlayletCollectSet BFF:代理 POST /api/app/playlet/collect/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_collect_set
|
||||
func PlayletCollectSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ID int64 `json:"id"`
|
||||
IsCollect *bool `json:"isCollect"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsCollect == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "id/isCollect required",
|
||||
"tip": "缺少短剧 ID 或收藏状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{
|
||||
ID: body.ID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PopularRecommend 热播榜页:GET /api/app/playlet/hot/rank/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_hot_rank_list
|
||||
func PopularRecommend(c *gin.Context) {
|
||||
q := playletser.ParseHotRankQuery(c)
|
||||
raw := playletser.FetchHotRankListOrEmpty(c, q)
|
||||
tabTitle := playletser.HotRankTabTitle(q.RankType)
|
||||
title := tabTitle + " - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
|
||||
HTMLOK(c, "popularRecommend.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "popular",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": rankingSEOSelections(tabTitle),
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"rankType": q.RankType,
|
||||
"tabTitle": tabTitle,
|
||||
"rankTabs": playletser.BuildHotRankTabs(q),
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"pagination": playletser.BuildHotRankPagination(q, total),
|
||||
"canonicalPath": playletser.HotRankCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Privacy renders the static Privacy Policy page.
|
||||
func Privacy(c *gin.Context) {
|
||||
HTMLOK(c, "privacy.html", gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/usercenterser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type profileWork struct {
|
||||
Title, Meta, Episodes, Badge, Image, Progress, Remaining, URL string
|
||||
}
|
||||
|
||||
// Profile 从当前会话拉取用户信息、观看历史和收藏,不展示演示账号数据。
|
||||
func Profile(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"title": "个人中心 - 黄果短剧", "navActive": "profile", "accountTab": "profile",
|
||||
}
|
||||
responses := gin.H{}
|
||||
loadAccountSummary(c, data, responses)
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
fetchProfileLists(c, data, responses)
|
||||
}
|
||||
if pretty, err := json.MarshalIndent(responses, "", " "); err == nil {
|
||||
data["responseJSON"] = string(pretty)
|
||||
}
|
||||
HTMLOK(c, "profile.html", data)
|
||||
}
|
||||
|
||||
func loadAccountSummary(c *gin.Context, data, responses gin.H) {
|
||||
data["userName"] = "未登录"
|
||||
data["balance"] = "—"
|
||||
data["historyCount"] = "—"
|
||||
data["favoriteCount"] = "—"
|
||||
data["chasingCount"] = "—"
|
||||
data["boughtChapterCount"] = "—"
|
||||
user := authser.SyncUserOnEnter(c)
|
||||
if user == nil {
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
responses["auth/getInfo"] = gin.H{"error": "加载失败"}
|
||||
fetchProfileStats(c, data, responses)
|
||||
return
|
||||
}
|
||||
responses["auth/getInfo"] = gin.H{"error": "暂无登录会话"}
|
||||
return
|
||||
}
|
||||
applyProfileInfo(data, responses, user)
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
fetchProfileStats(c, data, responses)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProfileStats(c *gin.Context, data, responses gin.H) {
|
||||
stats, code, err := usercenterser.Fetch(c)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
stats, code, err = usercenterser.Fetch(c)
|
||||
}
|
||||
if err != nil || stats == nil {
|
||||
responses["user/center/data"] = gin.H{"error": "加载失败"}
|
||||
if err != nil {
|
||||
log.Error("fetch user center data failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
responses["user/center/data"] = gin.H{"code": code, "data": stats}
|
||||
data["historyCount"] = fmt.Sprint(stats.WatchCount)
|
||||
data["favoriteCount"] = fmt.Sprint(stats.CollectCount)
|
||||
data["chasingCount"] = fmt.Sprint(stats.ChasingCount)
|
||||
data["boughtChapterCount"] = fmt.Sprint(stats.BoughtChapterCount)
|
||||
}
|
||||
|
||||
func fetchProfileLists(c *gin.Context, data, responses gin.H) {
|
||||
query := playletser.HistoryListQuery{PageNum: 1, PageSize: 6}
|
||||
history, code, err := playletser.FetchHistoryList(c, query)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
history, _, err = playletser.FetchHistoryList(c, query)
|
||||
}
|
||||
if err == nil && history != nil {
|
||||
responses["playlet/history/list"] = gin.H{"code": 200, "data": history}
|
||||
data["history"] = profileHistoryWorks(history.List)
|
||||
} else if err != nil {
|
||||
responses["playlet/history/list"] = gin.H{"error": "加载失败"}
|
||||
log.Error("fetch profile history failed", log.E(err))
|
||||
data["historyUnavailable"] = true
|
||||
}
|
||||
|
||||
collectQuery := playletser.CollectListQuery{PageNum: 1, PageSize: 6, WatchStatus: 0}
|
||||
favorites, code, err := playletser.FetchCollectList(c, collectQuery)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
favorites, _, err = playletser.FetchCollectList(c, collectQuery)
|
||||
}
|
||||
if err == nil && favorites != nil {
|
||||
responses["playlet/collect/list"] = gin.H{"code": 200, "data": favorites}
|
||||
data["favorites"] = profileFavoriteWorks(favorites.List)
|
||||
} else if err != nil {
|
||||
responses["playlet/collect/list"] = gin.H{"error": "加载失败"}
|
||||
log.Error("fetch profile favorites failed", log.E(err))
|
||||
data["favoritesUnavailable"] = true
|
||||
}
|
||||
}
|
||||
|
||||
func applyProfileInfo(data, responses gin.H, user *authser.LoginResp) {
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
// 登录响应可能包含 token;调试区域只展示非凭证字段。
|
||||
rawUser, _ := json.Marshal(user)
|
||||
var safeUser map[string]any
|
||||
_ = json.Unmarshal(rawUser, &safeUser)
|
||||
delete(safeUser, "token")
|
||||
responses["auth/getInfo"] = gin.H{"code": 200, "data": safeUser}
|
||||
name := strings.TrimSpace(user.NickName)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(user.AccountNo)
|
||||
}
|
||||
if name == "" {
|
||||
name = "用户"
|
||||
}
|
||||
data["userName"] = name
|
||||
data["setting"] = authser.BuildSettingVM(user)
|
||||
data["userID"] = user.UserID
|
||||
data["userId"] = fmt.Sprint(user.UserID)
|
||||
data["balance"] = fmt.Sprint(user.Recharge)
|
||||
data["recharge"] = user.Recharge
|
||||
data["rechargeText"] = authser.FormatRecharge(user.Recharge)
|
||||
if avatar := safeProfileImage(user.Avatar); avatar != "" {
|
||||
data["avatar"] = avatar
|
||||
}
|
||||
}
|
||||
|
||||
func safeProfileImage(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if strings.HasPrefix(raw, "/static/") {
|
||||
return raw
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" {
|
||||
return raw
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func profileHistoryWorks(items []playletser.PlayletHistoryData) []profileWork {
|
||||
works := make([]profileWork, 0, len(items))
|
||||
for _, item := range items {
|
||||
if len(works) == 6 {
|
||||
break
|
||||
}
|
||||
name := strings.TrimSpace(item.Title)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(item.PlayName)
|
||||
}
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
progress := 0
|
||||
if item.TotalEpisode > 0 {
|
||||
progress = item.WatchedCount * 100 / item.TotalEpisode
|
||||
}
|
||||
if progress < 0 {
|
||||
progress = 0
|
||||
}
|
||||
if progress > 100 {
|
||||
progress = 100
|
||||
}
|
||||
meta := "观看记录"
|
||||
if item.LastEpisode > 0 {
|
||||
meta = fmt.Sprintf("看至第 %d 集", item.LastEpisode)
|
||||
}
|
||||
remaining := ""
|
||||
if item.UnwatchedCount > 0 {
|
||||
remaining = fmt.Sprintf("剩 %d 集", item.UnwatchedCount)
|
||||
}
|
||||
works = append(works, profileWork{Title: name, Meta: meta, Image: profileCover(item.Cover), Progress: fmt.Sprintf("%d%%", progress), Remaining: remaining, URL: profilePlayURL(item.Slug, item.ID, item.LastEpisode)})
|
||||
}
|
||||
return works
|
||||
}
|
||||
|
||||
func profileFavoriteWorks(items []playletser.PlayletCollectData) []profileWork {
|
||||
works := make([]profileWork, 0, len(items))
|
||||
for _, item := range items {
|
||||
if len(works) == 6 {
|
||||
break
|
||||
}
|
||||
name := strings.TrimSpace(item.Title)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(item.PlayName)
|
||||
}
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
work := profileWork{Title: name, Meta: strings.Join(item.ThemeNames, " "), Image: profileCover(item.Cover), URL: profilePlayURL(item.Slug, item.ID, 1)}
|
||||
if item.TotalEpisode > 0 {
|
||||
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
|
||||
}
|
||||
if item.IsFeatured {
|
||||
work.Badge = "爆剧"
|
||||
}
|
||||
works = append(works, work)
|
||||
}
|
||||
return works
|
||||
}
|
||||
|
||||
func profileCover(raw string) string {
|
||||
if safe := safeProfileImage(raw); safe != "" {
|
||||
return safe
|
||||
}
|
||||
return "/static/images/placeholder.png?v=2"
|
||||
}
|
||||
|
||||
func profilePlayURL(slug string, id int64, episode int) string {
|
||||
if slug == "" {
|
||||
if id <= 0 {
|
||||
return ""
|
||||
}
|
||||
slug = fmt.Sprint(id)
|
||||
}
|
||||
if episode < 1 {
|
||||
episode = 1
|
||||
}
|
||||
return "/play/" + url.PathEscape(slug) + "/" + fmt.Sprint(episode)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type purchasedWork struct {
|
||||
Title, Image, Info, Date, Progress, URL string
|
||||
}
|
||||
|
||||
// PurchasedSeries 已购剧集:GET /api/app/playlet/bought/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_bought_list
|
||||
func PurchasedSeries(c *gin.Context) {
|
||||
page, validPage := accountPageNum(c)
|
||||
if !validPage {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{"title": "已购剧集 - 黄果短剧", "navActive": "purchased", "accountTab": "purchased"}
|
||||
responses := gin.H{}
|
||||
loadAccountSummary(c, data, responses)
|
||||
|
||||
size := 20
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
responses["playlet/bought/list"] = gin.H{"error": "暂无登录会话"}
|
||||
data["purchasedResponse"] = responses["playlet/bought/list"]
|
||||
HTMLOK(c, "purchased.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
list, code, err := playletser.FetchBoughtList(c, page, size)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
list, _, err = playletser.FetchBoughtList(c, page, size)
|
||||
}
|
||||
if err == nil && list != nil {
|
||||
responses["playlet/bought/list"] = gin.H{"code": 200, "data": list}
|
||||
data["paging"] = makeAccountPage("/purchased", page, size, list.TotalCount)
|
||||
works := make([]purchasedWork, 0, len(list.List))
|
||||
for _, item := range list.List {
|
||||
title := strings.TrimSpace(item.PlayName)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(item.Title)
|
||||
}
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
info := fmt.Sprintf("已购 %d 集", item.BoughtChapterCount)
|
||||
if item.TotalEpisode > 0 {
|
||||
info += fmt.Sprintf(" / 共 %d 集", item.TotalEpisode)
|
||||
}
|
||||
info += fmt.Sprintf(" · %d 金币", item.CostCoins)
|
||||
progress := 0
|
||||
if item.TotalEpisode > 0 {
|
||||
progress = item.WatchedCount * 100 / item.TotalEpisode
|
||||
}
|
||||
if progress < 0 {
|
||||
progress = 0
|
||||
}
|
||||
if progress > 100 {
|
||||
progress = 100
|
||||
}
|
||||
works = append(works, purchasedWork{
|
||||
Title: title, Image: profileCover(item.Cover), Info: info,
|
||||
Date: playletser.FormatHistoryAt(item.BoughtAt), Progress: fmt.Sprintf("%d%%", progress),
|
||||
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
|
||||
})
|
||||
}
|
||||
data["purchased"] = works
|
||||
} else if err != nil {
|
||||
responses["playlet/bought/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
log.Error("fetch bought playlets failed", log.E(err))
|
||||
data["purchasedUnavailable"] = true
|
||||
} else {
|
||||
responses["playlet/bought/list"] = gin.H{"code": code, "data": list}
|
||||
}
|
||||
|
||||
data["purchasedResponse"] = responses["playlet/bought/list"]
|
||||
HTMLOK(c, "purchased.html", data)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Rank 排行榜页:GET /api/app/playlet/rank。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_rank
|
||||
func Rank(c *gin.Context) {
|
||||
q := playletser.ParseRankQuery(c)
|
||||
raw := playletser.FetchRankOrEmpty(c, q)
|
||||
tabTitle := playletser.RankTabTitle(q.SortOrder)
|
||||
title := tabTitle + " - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
|
||||
HTMLOK(c, "rank.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "rank",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": rankingSEOSelections(tabTitle),
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"sortOrder": q.SortOrder,
|
||||
"tabTitle": tabTitle,
|
||||
"rankTabs": playletser.BuildRankTabs(q),
|
||||
"rankOffset": playletser.RankOffset(q),
|
||||
"isBinge": playletser.IsRankBinge(q.SortOrder),
|
||||
"pagination": playletser.BuildRankPagination(q, total),
|
||||
"canonicalPath": playletser.RankCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RecentlyAdded 最近上新页:GET /api/app/playlet/today-update。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_today_update
|
||||
func RecentlyAdded(c *gin.Context) {
|
||||
q := playletser.ParseTodayUpdateQuery(c.Param("page"))
|
||||
raw := playletser.FetchTodayUpdateOrEmpty(c, q)
|
||||
title := "最近上新 - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
|
||||
HTMLOK(c, "recentlyAdded.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "todayUpdate",
|
||||
"seoInfo": seo,
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"pagination": playletser.BuildTodayUpdatePagination(q, total),
|
||||
"canonicalPath": playletser.TodayUpdateCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const rechargePageSize = 5
|
||||
|
||||
// Recharge 金币充值页:套餐 GET /api/app/recharge/package/list,购买 POST /api/app/coin/buy,流水 GET /api/app/coin/trans/list。
|
||||
func Recharge(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"title": "金币充值 - 黄果短剧", "pageHeading": "金币充值",
|
||||
"backHref": "/profile", "helpHref": "/help",
|
||||
}
|
||||
responses := gin.H{}
|
||||
loadAccountSummary(c, data, responses)
|
||||
|
||||
packages, err := rechargeser.FetchPackageList(c)
|
||||
if err != nil {
|
||||
log.Error("fetch recharge packages failed", log.E(err))
|
||||
data["packageUnavailable"] = true
|
||||
packages = &rechargeser.RechargePackageListResp{}
|
||||
responses["recharge/package/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
} else {
|
||||
responses["recharge/package/list"] = gin.H{"code": 200, "data": packages}
|
||||
}
|
||||
vm := rechargeser.BuildDepositPageVM(packages, "coin")
|
||||
data["packages"] = vm.CoinList
|
||||
data["paymentMethods"] = vm.PayMethods
|
||||
data["rechargePackageResponse"] = packages
|
||||
|
||||
balance := authser.RechargeFromCookies(c)
|
||||
if _, ok := data["recharge"]; ok {
|
||||
balance = toInt64(data["recharge"])
|
||||
}
|
||||
data["balance"] = balance
|
||||
data["balanceText"] = authser.FormatRecharge(balance)
|
||||
|
||||
page, _, err := fetchRechargeTransactions(c, 1, rechargePageSize, 0)
|
||||
if err != nil {
|
||||
log.Error("fetch recharge transactions failed", log.E(err))
|
||||
data["transactionsUnavailable"] = true
|
||||
page = &rechargeser.CoinTransPage{PageNum: 1, PageSize: rechargePageSize}
|
||||
responses["coin/trans/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
|
||||
} else {
|
||||
responses["coin/trans/list"] = gin.H{"code": 200, "data": page}
|
||||
}
|
||||
data["transactions"] = rechargeser.BuildTxListFromCoinTrans(page.List, 0)
|
||||
data["transactionPage"] = page
|
||||
data["transactionResponse"] = page
|
||||
data["rechargeDebug"] = responses
|
||||
HTMLOK(c, "recharge.html", data)
|
||||
}
|
||||
|
||||
// RechargeCoinBuy BFF:代理 POST /api/app/coin/buy。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/post_api_app_coin_buy
|
||||
func RechargeCoinBuy(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
var body rechargeser.BuyPayReq
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.PackageID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "packageId required", "tip": "请选择充值套餐", "data": nil})
|
||||
return
|
||||
}
|
||||
body.PayMode = rechargeser.NormalizePayMode(body.PayMode)
|
||||
if body.PayMode == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "payMode required", "tip": "请选择支付方式", "data": nil})
|
||||
return
|
||||
}
|
||||
if !validCoinPackagePayment(c, body.PackageID, body.PayMode) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "invalid package/payMode", "tip": "套餐或支付方式不可用,请刷新后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := rechargeser.BuyCoin(c, body)
|
||||
if err != nil {
|
||||
log.Error("buy coin failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env != nil {
|
||||
if retry, needLogin := authser.OnSessionExpired(c, env.Code); needLogin {
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": "登录已过期,请重新登录", "data": nil})
|
||||
return
|
||||
} else if retry {
|
||||
env, err = rechargeser.BuyCoin(c, body)
|
||||
if err != nil {
|
||||
log.Error("buy coin retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
code, tip, msg := 500, "创建支付订单失败", "buy failed"
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmptyAuth(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "msg": msg, "tip": tip, "data": nil})
|
||||
return
|
||||
}
|
||||
safeURL := safePaymentURL(env.Data.PayURL)
|
||||
if safeURL == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "invalid payUrl", "tip": "支付地址无效,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
env.Data.PayURL = safeURL
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmptyAuth(env.Tip, "订单创建成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// RechargeTransactions BFF:代理 GET /api/app/coin/trans/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_list
|
||||
// query:page / pageSize / type(0全部 1充值 2消费 3奖励)
|
||||
func RechargeTransactions(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
page := queryInt(c, "page", 1)
|
||||
pageSize := queryInt(c, "pageSize", rechargePageSize)
|
||||
typ := queryIntAllowZero(c, "type", 0)
|
||||
if typ < 0 || typ > 3 {
|
||||
typ = 0
|
||||
}
|
||||
result, code, err := fetchRechargeTransactions(c, page, pageSize, typ)
|
||||
if err != nil {
|
||||
log.Error("fetch recharge transactions failed", log.E(err))
|
||||
tip := "交易明细加载失败"
|
||||
if code > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "msg": err.Error(), "tip": tip, "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": tip, "data": nil})
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
result = &rechargeser.CoinTransPage{PageNum: page, PageSize: pageSize}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"tip": "",
|
||||
"data": gin.H{
|
||||
"list": rechargeser.BuildTxListFromCoinTrans(result.List, 0),
|
||||
"rawList": result.List,
|
||||
"pageNum": result.PageNum,
|
||||
"pageSize": result.PageSize,
|
||||
"totalCount": result.TotalCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RechargeCoinTransExport BFF:代理 GET /api/app/coin/trans/export。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_export
|
||||
// query:type(0全部 1充值 2消费 3奖励),返回 CSV。
|
||||
func RechargeCoinTransExport(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
|
||||
return
|
||||
}
|
||||
typ := queryIntAllowZero(c, "type", 0)
|
||||
if typ < 0 || typ > 3 {
|
||||
typ = 0
|
||||
}
|
||||
result, err := rechargeser.FetchCoinTransExport(c, typ)
|
||||
if err != nil {
|
||||
log.Error("export coin trans failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "导出失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
ct := strings.TrimSpace(result.ContentType)
|
||||
if ct == "" || strings.Contains(strings.ToLower(ct), "json") {
|
||||
ct = "text/csv; charset=utf-8"
|
||||
}
|
||||
disposition := strings.TrimSpace(result.ContentDisposition)
|
||||
if disposition == "" {
|
||||
disposition = `attachment; filename="coin-trans.csv"`
|
||||
}
|
||||
c.Header("Content-Type", ct)
|
||||
c.Header("Content-Disposition", disposition)
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Data(http.StatusOK, ct, result.Body)
|
||||
}
|
||||
|
||||
func fetchRechargeTransactions(c *gin.Context, page, pageSize, typ int) (*rechargeser.CoinTransPage, int, error) {
|
||||
result, code, err := rechargeser.FetchCoinTransPage(c, page, pageSize, typ)
|
||||
if retry, _ := authser.OnSessionExpired(c, code); retry {
|
||||
result, code, err = rechargeser.FetchCoinTransPage(c, page, pageSize, typ)
|
||||
}
|
||||
return result, code, err
|
||||
}
|
||||
|
||||
// attachRechargeModal 注入充值弹窗数据:GET /api/app/recharge/package/list → coinList。
|
||||
func attachRechargeModal(c *gin.Context, data gin.H) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
packages := rechargeser.FetchPackageListOrEmpty(c)
|
||||
vm := rechargeser.BuildDepositPageVM(packages, "coin")
|
||||
data["rechargePackages"] = vm.CoinList
|
||||
data["rechargePayMethods"] = vm.PayMethods
|
||||
data["rechargePackageResponse"] = packages
|
||||
if _, ok := data["unlockCost"]; !ok {
|
||||
data["unlockCost"] = int64(0)
|
||||
}
|
||||
if _, ok := data["balance"]; !ok {
|
||||
data["balance"] = authser.RechargeFromCookies(c)
|
||||
}
|
||||
if _, ok := data["balanceText"]; !ok {
|
||||
data["balanceText"] = authser.FormatRecharge(toInt64(data["balance"]))
|
||||
}
|
||||
}
|
||||
|
||||
func validCoinPackagePayment(c *gin.Context, packageID int64, payMode string) bool {
|
||||
data, err := rechargeser.FetchPackageList(c)
|
||||
if err != nil || data == nil {
|
||||
return false
|
||||
}
|
||||
for _, item := range data.CoinList {
|
||||
if item.ID != packageID {
|
||||
continue
|
||||
}
|
||||
for _, method := range item.PayMethods {
|
||||
if rechargeser.NormalizePayMode(method.Type) == payMode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func safePaymentURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return ""
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func queryInt(c *gin.Context, name string, fallback int) int {
|
||||
v, err := strconv.Atoi(strings.TrimSpace(c.Query(name)))
|
||||
if err != nil || v <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// queryIntAllowZero 允许 0(如 type=0 全部)。
|
||||
func queryIntAllowZero(c *gin.Context, name string, fallback int) int {
|
||||
raw := strings.TrimSpace(c.Query(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// RegisterPage H5 注册页:渲染 template/pages/register.html。
|
||||
// PC 端由页面内脚本回首页并打开 registerPop;H5 直接展示本页。
|
||||
func RegisterPage(c *gin.Context) {
|
||||
HTMLOK(c, "register.html", gin.H{
|
||||
"title": "注册账号 - 黄果短剧",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/seoser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HTML 渲染页面并注入登录态(以 cookie 为准)。
|
||||
// 无 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。
|
||||
func HTML(c *gin.Context, code int, name string, data gin.H) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
|
||||
// 所有服务端页面先完成渠道首次归因;已有会话也不会漏记落地页渠道。
|
||||
if channel := authser.ResolveChannel(c, ""); channel != "" {
|
||||
data["channel"] = channel
|
||||
}
|
||||
authser.SyncUserOnEnter(c)
|
||||
|
||||
loggedIn, userName, avatar, email := authser.AuthFromCookies(c)
|
||||
registered := authser.IsRegisteredFromCookies(c)
|
||||
data["isLoggedIn"] = loggedIn
|
||||
data["isRegistered"] = registered
|
||||
// 已登录(含游客)展示用户菜单;未登录才展示「登录」按钮。
|
||||
if _, ok := data["guestHeader"]; !ok {
|
||||
data["guestHeader"] = !loggedIn
|
||||
}
|
||||
if _, ok := data["isVip"]; !ok {
|
||||
data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c)
|
||||
}
|
||||
isVip, _ := data["isVip"].(bool)
|
||||
data["isVip"] = isVip
|
||||
|
||||
if loggedIn {
|
||||
if _, provided := data["userName"]; !provided && userName != "" {
|
||||
data["userName"] = userName
|
||||
}
|
||||
if _, provided := data["avatar"]; !provided && avatar != "" {
|
||||
data["avatar"] = avatar
|
||||
}
|
||||
if email != "" {
|
||||
data["email"] = email
|
||||
}
|
||||
if _, ok := data["userId"]; !ok {
|
||||
if uid := authser.UserIDFromCookies(c); uid > 0 {
|
||||
data["userId"] = authser.FormatUserID(uid)
|
||||
}
|
||||
}
|
||||
if _, ok := data["recharge"]; !ok {
|
||||
recharge := authser.RechargeFromCookies(c)
|
||||
data["recharge"] = recharge
|
||||
data["rechargeText"] = authser.FormatRecharge(recharge)
|
||||
} else if _, ok := data["rechargeText"]; !ok {
|
||||
data["rechargeText"] = authser.FormatRecharge(toInt64(data["recharge"]))
|
||||
}
|
||||
if _, ok := data["vipType"]; !ok {
|
||||
data["vipType"] = authser.VipTypeFromCookies(c)
|
||||
}
|
||||
} else {
|
||||
delete(data, "userName")
|
||||
delete(data, "avatar")
|
||||
delete(data, "email")
|
||||
delete(data, "userId")
|
||||
delete(data, "recharge")
|
||||
delete(data, "rechargeText")
|
||||
delete(data, "vipType")
|
||||
}
|
||||
|
||||
// 导航分类:GET /api/app/translation/category/list → categoryList(包含分类 seoInfo)
|
||||
if _, ok := data["categoryList"]; !ok {
|
||||
data["categoryList"] = translationser.FetchHeaderCategoryListOrDefault(c)
|
||||
}
|
||||
if _, ok := data["requestPath"]; !ok {
|
||||
data["requestPath"] = c.Request.URL.Path
|
||||
}
|
||||
// 抽屉题材:GET /api/app/theme/list
|
||||
if _, ok := data["themeList"]; !ok {
|
||||
data["themeList"] = playletser.FetchThemesOrEmpty(c, 0)
|
||||
}
|
||||
// 顶栏短剧下拉快捷入口:homeConfigData type=1
|
||||
if _, ok := data["homeConfigPlaylets"]; !ok {
|
||||
if configs, ok := data["homeConfigData"].([]playletser.HomeConfigData); ok {
|
||||
data["homeConfigPlaylets"] = playletser.HomeConfigsByType(configs, 1)
|
||||
} else {
|
||||
home := playletser.FetchHomeOrEmpty(c)
|
||||
data["homeConfigPlaylets"] = playletser.HomeConfigsByType(home.HomeConfigData, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// PC 搜索下拉热搜:GET /api/app/playlet/hot-like
|
||||
if _, ok := data["searchTrends"]; !ok {
|
||||
trends := hotSearchTrends(playletser.FetchHotLikeOrEmpty(c).HotSearch)
|
||||
if len(trends) > 8 {
|
||||
trends = trends[:8]
|
||||
}
|
||||
data["searchTrends"] = trends
|
||||
}
|
||||
|
||||
// 公共配置:GET /api/app/config/info → config / appConfig
|
||||
if _, ok := data["appConfig"]; !ok {
|
||||
vm := configser.FetchAppConfigVMOrEmpty(c)
|
||||
data["appConfig"] = vm
|
||||
if _, ok := data["config"]; !ok {
|
||||
data["config"] = vm.Config
|
||||
}
|
||||
} else if _, ok := data["config"]; !ok {
|
||||
if vm, ok := data["appConfig"].(configser.AppConfigVM); ok {
|
||||
data["config"] = vm.Config
|
||||
}
|
||||
}
|
||||
|
||||
data["trackerAppId"] = configser.ResolveTrackerAppID()
|
||||
attachTrackerPage(c.Request.URL.Path, code, data)
|
||||
|
||||
attachPageSEO(c, code, data)
|
||||
c.HTML(code, name, data)
|
||||
}
|
||||
|
||||
// HTMLOK 以 200 渲染页面。
|
||||
func HTMLOK(c *gin.Context, name string, data gin.H) {
|
||||
if pageRepresentsMissingResource(c, data) {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
HTML(c, http.StatusOK, name, data)
|
||||
}
|
||||
|
||||
func toInt64(v any) int64 {
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return n
|
||||
case int:
|
||||
return int64(n)
|
||||
case int32:
|
||||
return int64(n)
|
||||
case float64:
|
||||
return int64(n)
|
||||
case string:
|
||||
i, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// attachPageSEO 在一个入口统一生成页面级 SEO 信号,避免各模板各自维护后产生冲突。
|
||||
func attachPageSEO(c *gin.Context, code int, data gin.H) {
|
||||
if c == nil || c.Request == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := data["appConfig"].(configser.AppConfigVM)
|
||||
if !ok {
|
||||
cfg = configser.FetchAppConfigVMOrEmpty(c)
|
||||
data["appConfig"] = cfg
|
||||
}
|
||||
path := c.Request.URL.Path
|
||||
pageTitle, _ := data["title"].(string)
|
||||
pageTitle = strings.TrimSpace(pageTitle)
|
||||
|
||||
seoInfo := pageSEOFromData(data["seoInfo"])
|
||||
siteName := firstNonEmptySEO(seoInfo.WebSiteName, cfg.SeoInfo.WebSiteName, "黄果短剧")
|
||||
routeConfigSEO, _ := data["routeConfigSEO"].(bool)
|
||||
if !routeConfigSEO {
|
||||
seoInfo = replaceSitewideSEOWithRouteSEO(path, data, seoInfo, pageSEOFromData(cfg.SeoInfo), siteName)
|
||||
}
|
||||
navigationSEOApplied := false
|
||||
if categories, ok := data["categoryList"].([]translationser.TranslationCategory); ok {
|
||||
headerNav := translationser.BuildHeaderNav(&translationser.AppTranslationInfo{CategoryList: categories})
|
||||
activeKey, _ := data["navActive"].(string)
|
||||
if navSEO, found := translationser.FindHeaderNavSEOByPath(headerNav, path); found {
|
||||
// 与 jha-1040-cgdj-web 一致:导航分类页面以分类接口 TDK 为主,页面接口只补缺。
|
||||
seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{
|
||||
Title: navSEO.Title,
|
||||
Description: navSEO.Description,
|
||||
Keywords: navSEO.Keyworld,
|
||||
}, seoInfo)
|
||||
navigationSEOApplied = true
|
||||
} else if !routeConfigSEO {
|
||||
// 当前项目的详情/播放页会复用 navActive;这类页面保持内容 TDK 为主,导航只补缺。
|
||||
if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, path); found {
|
||||
seoInfo = playletser.MergeSeoInfo(seoInfo, playletser.SeoInfo{
|
||||
Title: navSEO.Title,
|
||||
Description: navSEO.Description,
|
||||
Keywords: navSEO.Keyworld,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 {
|
||||
seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections)
|
||||
}
|
||||
if seoInfo.Title == "" {
|
||||
seoInfo.Title = firstNonEmptySEO(pageTitle, defaultPageTitle(path, siteName))
|
||||
}
|
||||
if seoInfo.Description == "" {
|
||||
seoInfo.Description = defaultMetaDescription(path, seoInfo.Title, data, siteName)
|
||||
}
|
||||
if len(seoInfo.Keywords) == 0 {
|
||||
seoInfo.Keywords = defaultMetaKeywords(path, siteName)
|
||||
}
|
||||
if pageNum := paginationPage(data["pagination"]); pageNum > 1 {
|
||||
seoInfo = playletser.PaginateSeoInfo(seoInfo, seoInfo.Description, pageNum)
|
||||
}
|
||||
metaTitle := seoInfo.Title
|
||||
if !navigationSEOApplied {
|
||||
metaTitle = ensureBrandTitle(metaTitle, siteName)
|
||||
}
|
||||
data["title"] = metaTitle
|
||||
data["metaTitle"] = metaTitle
|
||||
data["metaDescription"] = seoInfo.Description
|
||||
data["metaKeywords"] = strings.Join(seoInfo.Keywords, ",")
|
||||
data["siteName"] = siteName
|
||||
data["seoInfo"] = seoInfo
|
||||
|
||||
siteURL := seoser.ResolveSiteURL(c, cfg)
|
||||
data["siteURL"] = siteURL
|
||||
data["foreverUrlAddr"] = siteURL
|
||||
data["seoVerify"] = seoser.VerifyFromConfig()
|
||||
data["ogType"] = pageOGType(path)
|
||||
data["ogImage"] = pageOGImage(siteURL, path, data)
|
||||
attachPaginationLinks(data, siteURL)
|
||||
|
||||
robots, _ := data["robots"].(string)
|
||||
robots = strings.TrimSpace(robots)
|
||||
if robots == "" {
|
||||
robots = defaultRobotsForPath(path, code)
|
||||
}
|
||||
if !robotsHasNoIndex(robots) && pageShouldNotIndex(c, data) {
|
||||
robots = "noindex,follow"
|
||||
}
|
||||
data["robots"] = robots
|
||||
noindex := robotsHasNoIndex(robots)
|
||||
if noindex {
|
||||
data["noindex"] = true
|
||||
c.Header("X-Robots-Tag", strings.ReplaceAll(robots, ",", ", "))
|
||||
} else {
|
||||
delete(data, "noindex")
|
||||
}
|
||||
|
||||
canonicalEligible := code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(path)
|
||||
if canonicalEligible {
|
||||
canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c))
|
||||
if canonicalPath, _ := data["canonicalPath"].(string); strings.HasPrefix(strings.TrimSpace(canonicalPath), "/") {
|
||||
canonicalURL = seoser.CanonicalWithoutQuery(seoser.AbsURL(siteURL, strings.TrimSpace(canonicalPath)))
|
||||
}
|
||||
canonicalURL = seoser.CanonicalWithoutQuery(seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data)))
|
||||
data["canonicalURL"] = canonicalURL
|
||||
data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
|
||||
} else {
|
||||
delete(data, "canonicalURL")
|
||||
delete(data, "jsonLD")
|
||||
}
|
||||
}
|
||||
|
||||
func pageShouldNotIndex(c *gin.Context, data gin.H) bool {
|
||||
if isNavPage, _ := data["isNavPage"].(bool); isNavPage {
|
||||
return true
|
||||
}
|
||||
if c != nil && c.Request != nil {
|
||||
path := c.Request.URL.Path
|
||||
if isEpisodePlayPath(path, c.Param("ep")) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "/series-play/") {
|
||||
if canWatch, ok := data["canWatch"].(bool); ok && !canWatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return pageRepresentsMissingResource(c, data)
|
||||
}
|
||||
|
||||
// pageRepresentsMissingResource 识别不存在的实体、非法分页与越界分页。
|
||||
// 这些请求必须返回真实 404,不能仅依赖 noindex 掩盖 200 软 404。
|
||||
func pageRepresentsMissingResource(c *gin.Context, data gin.H) bool {
|
||||
if valid, exists := data["seoEntityValid"].(bool); exists && !valid {
|
||||
return true
|
||||
}
|
||||
pageNum := int(toInt64(data["pageNum"]))
|
||||
if c != nil {
|
||||
if rawPage := strings.TrimSpace(c.Param("page")); rawPage != "" {
|
||||
requestedPage, err := strconv.Atoi(rawPage)
|
||||
// 分页二级路由只允许第 2 页及以后;第 1 页使用更短的规范地址。
|
||||
if err != nil || requestedPage <= 1 {
|
||||
return true
|
||||
}
|
||||
pageNum = requestedPage
|
||||
}
|
||||
}
|
||||
pageSize := int(toInt64(data["pageSize"]))
|
||||
totalCount, hasTotal := data["totalCount"]
|
||||
return hasTotal && playletser.PageOutOfRange(pageNum, pageSize, toInt64(totalCount))
|
||||
}
|
||||
|
||||
func pageSEOFromData(value any) playletser.SeoInfo {
|
||||
var seo playletser.SeoInfo
|
||||
switch value := value.(type) {
|
||||
case playletser.SeoInfo:
|
||||
seo = value
|
||||
case *playletser.SeoInfo:
|
||||
if value != nil {
|
||||
seo = *value
|
||||
}
|
||||
case videoser.SeoInfo:
|
||||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||||
case *videoser.SeoInfo:
|
||||
if value != nil {
|
||||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||||
}
|
||||
case configser.SeoInfo:
|
||||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||||
case *configser.SeoInfo:
|
||||
if value != nil {
|
||||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||||
}
|
||||
}
|
||||
seo.Normalize()
|
||||
return seo
|
||||
}
|
||||
|
||||
// replaceSitewideSEOWithRouteSEO 只替换接口误复用的全站首页 TDK。
|
||||
// 页面接口真正下发的独立 SEO 仍保持最高优先级。
|
||||
func replaceSitewideSEOWithRouteSEO(path string, data gin.H, seo, siteDefault playletser.SeoInfo, siteName string) playletser.SeoInfo {
|
||||
routeSEO, ok := routeSpecificSEO(path, data, siteName)
|
||||
if !ok {
|
||||
return seo
|
||||
}
|
||||
if sameNonEmptySEOValue(seo.Title, siteDefault.Title) {
|
||||
seo.Title = routeSEO.Title
|
||||
}
|
||||
if sameNonEmptySEOValue(seo.Description, siteDefault.Description) {
|
||||
seo.Description = routeSEO.Description
|
||||
}
|
||||
if sameSEOKeywords(seo.Keywords, siteDefault.Keywords) {
|
||||
seo.Keywords = append([]string(nil), routeSEO.Keywords...)
|
||||
}
|
||||
seo.Normalize()
|
||||
return seo
|
||||
}
|
||||
|
||||
func isEpisodePlayPath(path, epParam string) bool {
|
||||
if strings.TrimSpace(epParam) != "" {
|
||||
return true
|
||||
}
|
||||
segments := strings.Split(strings.Trim(path, "/"), "/")
|
||||
return len(segments) >= 3 && segments[0] == "play" && segments[1] != "" && segments[2] != ""
|
||||
}
|
||||
|
||||
func routeSpecificSEO(path string, data gin.H, siteName string) (playletser.SeoInfo, bool) {
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
title := ""
|
||||
switch {
|
||||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||||
title = firstNonEmptySEO(dataString(data, "pageHeading"), "全部短剧")
|
||||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||||
title = firstNonEmptySEO(dataString(data, "pageHeading"), "剧集与动画")
|
||||
case path == "/topic" || strings.HasPrefix(path, "/topic/"):
|
||||
title = "专题剧单"
|
||||
case strings.HasPrefix(path, "/topic-detail/"):
|
||||
if name := dataString(data, "name"); name != "" {
|
||||
title = name + "专题短剧"
|
||||
} else {
|
||||
title = "短剧专题"
|
||||
}
|
||||
case strings.HasPrefix(path, "/tags/"):
|
||||
if name := dataString(data, "tagName"); name != "" {
|
||||
title = name + "标签短剧"
|
||||
} else {
|
||||
title = "标签短剧"
|
||||
}
|
||||
case strings.HasPrefix(path, "/more-list/"):
|
||||
title = firstNonEmptySEO(dataString(data, "configName"), "精选短剧")
|
||||
case strings.HasPrefix(path, "/more-series-list/"):
|
||||
title = firstNonEmptySEO(dataString(data, "configName"), "精选剧集")
|
||||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||||
title = "短剧排行榜"
|
||||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||||
title = "热门短剧推荐"
|
||||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||||
title = "最近上新短剧"
|
||||
default:
|
||||
return playletser.SeoInfo{}, false
|
||||
}
|
||||
return playletser.SeoInfo{
|
||||
Title: title,
|
||||
Description: defaultMetaDescription(path, title, data, siteName),
|
||||
Keywords: defaultMetaKeywords(path, siteName),
|
||||
}, true
|
||||
}
|
||||
|
||||
func dataString(data gin.H, key string) string {
|
||||
value, _ := data[key].(string)
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func sameNonEmptySEOValue(value, generic string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
generic = strings.TrimSpace(generic)
|
||||
return generic != "" && strings.EqualFold(value, generic)
|
||||
}
|
||||
|
||||
func sameSEOKeywords(value, generic []string) bool {
|
||||
if len(generic) == 0 || len(value) != len(generic) {
|
||||
return false
|
||||
}
|
||||
for i := range value {
|
||||
if !strings.EqualFold(strings.TrimSpace(value[i]), strings.TrimSpace(generic[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func paginationPage(value any) int {
|
||||
switch value := value.(type) {
|
||||
case playletser.PaginationVM:
|
||||
return value.Page
|
||||
case *playletser.PaginationVM:
|
||||
if value != nil {
|
||||
return value.Page
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func attachPaginationLinks(data gin.H, siteURL string) {
|
||||
var pagination playletser.PaginationVM
|
||||
switch value := data["pagination"].(type) {
|
||||
case playletser.PaginationVM:
|
||||
pagination = value
|
||||
case *playletser.PaginationVM:
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
pagination = *value
|
||||
default:
|
||||
return
|
||||
}
|
||||
if !pagination.Visible {
|
||||
return
|
||||
}
|
||||
if pagination.HasPrev && strings.TrimSpace(pagination.PrevURL) != "" {
|
||||
data["paginationPrevURL"] = seoser.AbsURL(siteURL, pagination.PrevURL)
|
||||
}
|
||||
if pagination.HasNext && strings.TrimSpace(pagination.NextURL) != "" {
|
||||
data["paginationNextURL"] = seoser.AbsURL(siteURL, pagination.NextURL)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRobotsForPath(path string, code int) string {
|
||||
if code >= http.StatusBadRequest {
|
||||
return "noindex,follow"
|
||||
}
|
||||
if seoser.IsIndexablePath(path) {
|
||||
return "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"
|
||||
}
|
||||
if seoser.IsPrivatePath(path) {
|
||||
return "noindex,nofollow"
|
||||
}
|
||||
return "noindex,follow"
|
||||
}
|
||||
|
||||
func robotsHasNoIndex(robots string) bool {
|
||||
for _, directive := range strings.Split(strings.ToLower(robots), ",") {
|
||||
if strings.TrimSpace(directive) == "noindex" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func defaultPageTitle(path, siteName string) string {
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if siteName == "" {
|
||||
siteName = "黄果短剧"
|
||||
}
|
||||
switch {
|
||||
case path == "":
|
||||
return siteName + " - 热门短剧在线看"
|
||||
case path == "/about":
|
||||
return "关于我们"
|
||||
case path == "/nav" || path == "/dh":
|
||||
return siteName + " · 官方导航"
|
||||
case path == "/help":
|
||||
return "常见问题"
|
||||
case path == "/terms":
|
||||
return "用户条款"
|
||||
case path == "/privacy":
|
||||
return "隐私政策"
|
||||
case path == "/2257":
|
||||
return "2257 合规声明"
|
||||
case path == "/dmca":
|
||||
return "DMCA 版权声明"
|
||||
case path == "/search" || strings.HasPrefix(path, "/search/") || path == "/search-mobile":
|
||||
return "搜索"
|
||||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||||
return "全部短剧"
|
||||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||||
return "剧集与动画"
|
||||
case path == "/topic" || strings.HasPrefix(path, "/topic/"):
|
||||
return "专题剧单"
|
||||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||||
return "短剧排行榜"
|
||||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||||
return "最近上新"
|
||||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||||
return "热门推荐"
|
||||
case strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/"):
|
||||
return "在线播放"
|
||||
default:
|
||||
return siteName
|
||||
}
|
||||
}
|
||||
|
||||
func defaultMetaDescription(path, title string, data gin.H, siteName string) string {
|
||||
for _, key := range []string{"description", "desc", "heroDesc", "categoryDescription", "tagDescription", "topicDescription"} {
|
||||
if value, _ := data[key].(string); strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
if siteName == "" {
|
||||
siteName = "黄果短剧"
|
||||
}
|
||||
cleanTitle := strings.TrimSpace(strings.TrimSuffix(title, " - "+siteName))
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if isNavPage, _ := data["isNavPage"].(bool); isNavPage {
|
||||
return siteName + "官方导航页,提供最新访问线路、永久地址、APP 下载与官方联系方式。"
|
||||
}
|
||||
switch {
|
||||
case path == "":
|
||||
return siteName + "汇集热门短剧、最近上新、排行榜与精选专题推荐,支持高清在线播放。"
|
||||
case path == "/about":
|
||||
return "了解" + siteName + "的平台理念、内容特色与联系方式。"
|
||||
case path == "/nav" || path == "/dh":
|
||||
return siteName + "官方导航页,提供最新访问线路、永久地址、APP 下载与官方联系方式。"
|
||||
case path == "/help":
|
||||
return "查看" + siteName + "常见问题、播放帮助、账号使用与客服支持信息。"
|
||||
case path == "/terms":
|
||||
return "查看" + siteName + "用户服务条款及平台使用规则。"
|
||||
case path == "/privacy":
|
||||
return "查看" + siteName + "隐私政策与个人信息保护说明。"
|
||||
case path == "/2257":
|
||||
return "查看" + siteName + " 18 U.S.C. 第 2257 章合规声明、年龄限制与内容反馈处理原则。"
|
||||
case path == "/dmca":
|
||||
return "查看" + siteName + " DMCA 版权声明、投诉材料要求与处理流程说明。"
|
||||
case path == "/search" || strings.HasPrefix(path, "/search/") || path == "/search-mobile":
|
||||
return "在" + siteName + "搜索剧名、演员、标签与相关作品。"
|
||||
case strings.HasPrefix(path, "/play/"):
|
||||
return "在线观看《" + cleanTitle + "》,查看选集、剧情信息与相关推荐。"
|
||||
case strings.HasPrefix(path, "/series-play/"):
|
||||
return "在线观看《" + cleanTitle + "》,浏览视频信息与相关推荐。"
|
||||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||||
return "浏览" + siteName + "全部短剧,按题材、更新状态和热度筛选精彩作品。"
|
||||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||||
return "浏览" + siteName + "精选剧集与动画,按类型、专题和热度发现更多内容。"
|
||||
case path == "/topic" || strings.HasPrefix(path, "/topic/") || strings.HasPrefix(path, "/topic-detail/"):
|
||||
return "浏览" + siteName + "精选专题剧单,发现同题材热门作品与持续更新内容。"
|
||||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||||
return "查看" + siteName + "实时更新的短剧排行榜,浏览热播榜、新剧榜与追剧榜。"
|
||||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||||
return "浏览" + siteName + "热门推荐,发现近期高热度、广受欢迎的精选短剧。"
|
||||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||||
return "查看" + siteName + "最近上新的短剧与剧集,及时追更最新内容。"
|
||||
default:
|
||||
return cleanTitle + ",浏览" + siteName + "相关内容与推荐。"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultMetaKeywords(path, siteName string) []string {
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if siteName == "" {
|
||||
siteName = "黄果短剧"
|
||||
}
|
||||
switch {
|
||||
case path == "":
|
||||
return []string{siteName, "短剧", "在线短剧", "热门短剧"}
|
||||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||||
return []string{"短剧列表", "短剧分类", "在线短剧", siteName}
|
||||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||||
return []string{"剧集", "动画", "在线观看", siteName}
|
||||
case strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/"):
|
||||
return []string{"在线播放", "短剧播放", siteName}
|
||||
case path == "/topic" || strings.HasPrefix(path, "/topic/") || strings.HasPrefix(path, "/topic-detail/"):
|
||||
return []string{"短剧专题", "专题剧单", siteName}
|
||||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||||
return []string{"短剧排行榜", "热门短剧", siteName}
|
||||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||||
return []string{"热门短剧", "短剧推荐", siteName}
|
||||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||||
return []string{"最新短剧", "最近上新", siteName}
|
||||
case strings.HasPrefix(path, "/tags/"):
|
||||
return []string{"标签短剧", "短剧分类", siteName}
|
||||
case strings.HasPrefix(path, "/more-list/"):
|
||||
return []string{"精选短剧", "热门短剧", siteName}
|
||||
case strings.HasPrefix(path, "/more-series-list/"):
|
||||
return []string{"精选剧集", "剧集推荐", siteName}
|
||||
default:
|
||||
return []string{siteName, "在线短剧"}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureBrandTitle(title, siteName string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
siteName = strings.TrimSpace(siteName)
|
||||
if title == "" {
|
||||
return siteName
|
||||
}
|
||||
if siteName == "" || strings.Contains(title, siteName) || strings.Contains(title, "黄果短剧") {
|
||||
return title
|
||||
}
|
||||
return title + " - " + siteName
|
||||
}
|
||||
|
||||
func pageOGType(path string) string {
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
if strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/") {
|
||||
return "video.episode"
|
||||
}
|
||||
if path == "/terms" || path == "/privacy" || path == "/2257" || path == "/dmca" {
|
||||
return "article"
|
||||
}
|
||||
return "website"
|
||||
}
|
||||
|
||||
func pageOGImage(siteURL string, path string, data gin.H) string {
|
||||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||||
// 播放页分享小卡片统一用品牌 OG 图,避免微信等抓取剧集封面。
|
||||
if strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/") {
|
||||
return seoser.AbsURL(siteURL, "/static/og-default.png")
|
||||
}
|
||||
if playlet, ok := data["playlet"].(*playletser.PlayletInfoResp); ok && playlet != nil {
|
||||
if cover := firstNonEmptySEO(playlet.Cover, playlet.Banner); cover != "" {
|
||||
return seoser.AbsURL(siteURL, cover)
|
||||
}
|
||||
}
|
||||
if cover, _ := data["cover"].(string); strings.TrimSpace(cover) != "" {
|
||||
return seoser.AbsURL(siteURL, cover)
|
||||
}
|
||||
return seoser.AbsURL(siteURL, "/static/og-default.png")
|
||||
}
|
||||
|
||||
func firstNonEmptySEO(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestAttachPageSEOUsesPageTypeInsteadOfRequestDomain(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestURL string
|
||||
foreverURL string
|
||||
status int
|
||||
data gin.H
|
||||
wantRobots string
|
||||
wantRobotsHeader string
|
||||
wantCanonical string
|
||||
wantJSONLD bool
|
||||
}{
|
||||
{
|
||||
name: "public page on alternate domain remains indexable",
|
||||
requestURL: "https://alternate.example/",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1",
|
||||
wantCanonical: "https://primary.example/",
|
||||
wantJSONLD: true,
|
||||
},
|
||||
{
|
||||
name: "public page without permanent address remains indexable",
|
||||
requestURL: "https://alternate.example/about",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1",
|
||||
wantCanonical: "https://alternate.example/about",
|
||||
wantJSONLD: true,
|
||||
},
|
||||
{
|
||||
name: "search page is not indexable",
|
||||
requestURL: "https://alternate.example/search/short-drama",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
{
|
||||
name: "private page is neither indexed nor followed",
|
||||
requestURL: "https://alternate.example/profile",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "noindex,nofollow",
|
||||
wantRobotsHeader: "noindex, nofollow",
|
||||
},
|
||||
{
|
||||
name: "navigation page is not indexable",
|
||||
requestURL: "https://alternate.example/",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{"isNavPage": true},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
{
|
||||
name: "missing public entity is not indexable",
|
||||
requestURL: "https://alternate.example/tags/missing",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{"seoEntityValid": false},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
{
|
||||
name: "error page is not indexable",
|
||||
requestURL: "https://alternate.example/about",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusNotFound,
|
||||
data: gin.H{},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
{
|
||||
name: "canonical play page stays indexable",
|
||||
requestURL: "https://alternate.example/play/zhuazou",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1",
|
||||
wantCanonical: "https://primary.example/play/zhuazou",
|
||||
wantJSONLD: true,
|
||||
},
|
||||
{
|
||||
name: "play page with chapter still canonicalizes to two-segment path",
|
||||
requestURL: "https://alternate.example/play/zhuazou",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{
|
||||
"playlet": playletser.PlayletInfoResp{ID: 12, Slug: "zhuazou"},
|
||||
"chapter": playletser.PlayletChapterInfoResp{ID: 901, PlayletID: 12},
|
||||
},
|
||||
wantRobots: "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1",
|
||||
wantCanonical: "https://primary.example/play/zhuazou",
|
||||
wantJSONLD: true,
|
||||
},
|
||||
{
|
||||
name: "episode play page is not indexable",
|
||||
requestURL: "https://alternate.example/play/zhuazou/12",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
{
|
||||
name: "unwatchable series play page is not indexable",
|
||||
requestURL: "https://alternate.example/series-play/film-1",
|
||||
foreverURL: "https://primary.example",
|
||||
status: http.StatusOK,
|
||||
data: gin.H{"canWatch": false},
|
||||
wantRobots: "noindex,follow",
|
||||
wantRobotsHeader: "noindex, follow",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(response)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, tt.requestURL, nil)
|
||||
|
||||
data := gin.H{
|
||||
"appConfig": configser.AppConfigVM{
|
||||
ForeverURLAddr: tt.foreverURL,
|
||||
},
|
||||
}
|
||||
for key, value := range tt.data {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
attachPageSEO(ctx, tt.status, data)
|
||||
|
||||
if got := data["robots"]; got != tt.wantRobots {
|
||||
t.Fatalf("robots = %q, want %q", got, tt.wantRobots)
|
||||
}
|
||||
if got := response.Header().Get("X-Robots-Tag"); got != tt.wantRobotsHeader {
|
||||
t.Fatalf("X-Robots-Tag = %q, want %q", got, tt.wantRobotsHeader)
|
||||
}
|
||||
|
||||
canonicalValue, hasCanonical := data["canonicalURL"]
|
||||
if tt.wantCanonical == "" {
|
||||
if hasCanonical {
|
||||
t.Fatalf("canonicalURL must be absent, got %#v", canonicalValue)
|
||||
}
|
||||
} else {
|
||||
canonical, ok := canonicalValue.(string)
|
||||
if !hasCanonical || !ok || canonical != tt.wantCanonical {
|
||||
t.Fatalf("canonicalURL = %#v, want %q", canonicalValue, tt.wantCanonical)
|
||||
}
|
||||
}
|
||||
|
||||
jsonLD, hasJSONLD := data["jsonLD"]
|
||||
if hasJSONLD != tt.wantJSONLD {
|
||||
t.Fatalf("jsonLD presence = %v, want %v (value %#v)", hasJSONLD, tt.wantJSONLD, jsonLD)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type searchTrend struct {
|
||||
Rank int
|
||||
Title string
|
||||
Heat string
|
||||
Badge string
|
||||
}
|
||||
|
||||
// SearchMobile 移动端搜索入口。
|
||||
// 热搜 + 猜你喜欢:GET /api/app/playlet/hot-like
|
||||
// 联想:前端调用 GET /api/web/search/suggest → 上游 /api/app/playlet/search/suggest
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_hot_like
|
||||
func SearchMobile(c *gin.Context) {
|
||||
if hasLegacySearchQuery(c) {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchHotLikeOrEmpty(c)
|
||||
trends := hotSearchTrends(raw.HotSearch)
|
||||
recommended := raw.GuessLike
|
||||
if len(recommended) > 15 {
|
||||
recommended = recommended[:15]
|
||||
}
|
||||
|
||||
HTMLOK(c, "searchMobile.html", gin.H{
|
||||
"title": "搜索 - 黄果短剧",
|
||||
"query": searchQueryFromRequest(c),
|
||||
"trends": trends,
|
||||
"recommended": recommended,
|
||||
"hotLikeDebug": gin.H{
|
||||
"path": "/api/app/playlet/hot-like",
|
||||
"response": raw,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func hotSearchTrends(items []playletser.PlayletHotSearchItem) []searchTrend {
|
||||
out := make([]searchTrend, 0, len(items))
|
||||
for i, item := range items {
|
||||
keyword := strings.TrimSpace(item.Keyword)
|
||||
if keyword == "" {
|
||||
continue
|
||||
}
|
||||
rank := i + 1
|
||||
badge := ""
|
||||
switch rank {
|
||||
case 1:
|
||||
badge = "热"
|
||||
case 2:
|
||||
badge = "新"
|
||||
}
|
||||
out = append(out, searchTrend{
|
||||
Rank: rank,
|
||||
Title: keyword,
|
||||
Heat: formatSearchHeat(item.Count),
|
||||
Badge: badge,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func formatSearchHeat(n int64) string {
|
||||
if n <= 0 {
|
||||
return "0"
|
||||
}
|
||||
if n < 10000 {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
v := float64(n) / 10000
|
||||
if v >= 100 {
|
||||
return fmt.Sprintf("%.0f 万", v)
|
||||
}
|
||||
return fmt.Sprintf("%.1f 万", v)
|
||||
}
|
||||
|
||||
// Search 搜索结果页:GET /api/app/playlet/search。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_search
|
||||
// 无结果时「大家都在看」:GET /api/app/playlet/all/list(取 6 条最热)。
|
||||
func Search(c *gin.Context) {
|
||||
if hasLegacySearchQuery(c) {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
keyword := searchQueryFromRequest(c)
|
||||
pageStr := strings.TrimSpace(c.Param("page"))
|
||||
q := playletser.ParseSearchQuery(keyword, pageStr)
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
recommended := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var searchDebug any
|
||||
var watchingDebug any
|
||||
|
||||
if q.Keyword != "" {
|
||||
raw := playletser.FetchSearchOrEmpty(c, q)
|
||||
searchDebug = gin.H{
|
||||
"path": "/api/app/playlet/search",
|
||||
"keyword": q.Keyword,
|
||||
"pageNum": q.PageNum,
|
||||
"pageSize": q.PageSize,
|
||||
"response": raw,
|
||||
}
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
if strings.TrimSpace(raw.Keyword) != "" {
|
||||
keyword = raw.Keyword
|
||||
}
|
||||
}
|
||||
} else {
|
||||
searchDebug = gin.H{
|
||||
"path": "/api/app/playlet/search",
|
||||
"keyword": "",
|
||||
"note": "empty keyword, skip upstream",
|
||||
}
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
watchingQ := playletser.AllListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 6,
|
||||
SortType: 2, // 最热
|
||||
UpdateStatus: 0, // 全部
|
||||
}
|
||||
watching := playletser.FetchAllListOrEmpty(c, watchingQ)
|
||||
watchingDebug = gin.H{
|
||||
"path": "/api/app/playlet/all/list",
|
||||
"pageNum": watchingQ.PageNum,
|
||||
"pageSize": watchingQ.PageSize,
|
||||
"sortType": watchingQ.SortType,
|
||||
"response": watching,
|
||||
}
|
||||
if watching != nil {
|
||||
recommended = watching.List
|
||||
if len(recommended) > 6 {
|
||||
recommended = recommended[:6]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HTMLOK(c, "search.html", gin.H{
|
||||
"title": "搜索 - 黄果短剧",
|
||||
"query": keyword,
|
||||
"hasResults": len(list) > 0,
|
||||
"resultCount": total,
|
||||
"works": list,
|
||||
"recommended": recommended,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"pagination": playletser.BuildSearchPagination(q, total),
|
||||
"searchDebug": searchDebug,
|
||||
"watchingDebug": watchingDebug,
|
||||
})
|
||||
}
|
||||
|
||||
type searchSuggestFeature struct {
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
URL string `json:"url"`
|
||||
Meta string `json:"meta"`
|
||||
}
|
||||
|
||||
type searchSuggestItem struct {
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"`
|
||||
Query string `json:"query"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SearchSuggest PC 顶栏输入联想:GET /api/web/search/suggest?keyword=
|
||||
// 上游:GET /api/app/playlet/search/suggest → proto.PlayletSearchSuggestResp
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_search_suggest
|
||||
func SearchSuggest(c *gin.Context) {
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
if keyword == "" {
|
||||
keyword = strings.TrimSpace(c.Query("q"))
|
||||
}
|
||||
if keyword == "" {
|
||||
c.JSON(200, gin.H{"code": 200, "keyword": "", "feature": nil, "items": []searchSuggestItem{}})
|
||||
return
|
||||
}
|
||||
|
||||
raw := playletser.FetchSearchSuggestOrEmpty(c, keyword)
|
||||
if kw := strings.TrimSpace(raw.Keyword); kw != "" {
|
||||
keyword = kw
|
||||
}
|
||||
|
||||
var feature *searchSuggestFeature
|
||||
items := make([]searchSuggestItem, 0, len(raw.List)+1)
|
||||
for _, item := range raw.List {
|
||||
title := playletser.PlayletDisplayName(item.PlayName, item.Title)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
href := playletser.PlayPath(item.Slug, item.ID)
|
||||
if feature == nil {
|
||||
feature = &searchSuggestFeature{
|
||||
Title: title,
|
||||
Cover: strings.TrimSpace(item.Cover),
|
||||
URL: href,
|
||||
Meta: buildSearchSuggestMeta(item),
|
||||
}
|
||||
continue
|
||||
}
|
||||
items = append(items, searchSuggestItem{
|
||||
Title: title,
|
||||
Kind: "短剧 剧集",
|
||||
Query: title,
|
||||
URL: href,
|
||||
})
|
||||
}
|
||||
|
||||
items = append(items, searchSuggestItem{
|
||||
Title: keyword,
|
||||
Kind: "搜索",
|
||||
Query: keyword,
|
||||
URL: playletser.SearchResultsURL(keyword),
|
||||
})
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"code": 200,
|
||||
"keyword": keyword,
|
||||
"feature": feature,
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func buildSearchSuggestMeta(item playletser.CommonData) string {
|
||||
parts := make([]string, 0, 4)
|
||||
for _, name := range item.ThemeNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
}
|
||||
for _, name := range item.TagNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" && len(parts) < 2 {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
}
|
||||
if item.TotalEpisode > 0 {
|
||||
parts = append(parts, fmt.Sprintf("全 %d 集", item.TotalEpisode))
|
||||
} else if item.UpdateStatus == 1 {
|
||||
parts = append(parts, "连载中")
|
||||
}
|
||||
if anchor := strings.TrimSpace(item.Anchor); anchor != "" && anchor != "暂无" && anchor != "原创" {
|
||||
parts = append(parts, "主演 "+anchor)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func hasLegacySearchQuery(c *gin.Context) bool {
|
||||
query := c.Request.URL.Query()
|
||||
return query.Has("q") || query.Has("page")
|
||||
}
|
||||
|
||||
func searchQueryFromRequest(c *gin.Context) string {
|
||||
q := strings.TrimSpace(c.Param("q"))
|
||||
if q == "" {
|
||||
return ""
|
||||
}
|
||||
if decoded, err := url.PathUnescape(q); err == nil {
|
||||
return strings.TrimSpace(decoded)
|
||||
}
|
||||
return q
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/seoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RobotsTXT 输出可抓取规则及绝对 sitemap 地址。
|
||||
func RobotsTXT(c *gin.Context) {
|
||||
cfg := configser.FetchAppConfigVMOrEmpty(c)
|
||||
siteURL := seoser.ResolveSiteURL(c, cfg)
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(http.StatusOK, seoser.BuildRobotsTXT(siteURL))
|
||||
}
|
||||
|
||||
// LLMSTXT 输出简洁站点说明及核心页面的规范地址,供 AI 抓取工具发现内容。
|
||||
func LLMSTXT(c *gin.Context) {
|
||||
cfg := configser.FetchAppConfigVMOrEmpty(c)
|
||||
writeLLMSTXT(c, cfg)
|
||||
}
|
||||
|
||||
func writeLLMSTXT(c *gin.Context, cfg configser.AppConfigVM) {
|
||||
siteURL := seoser.ResolveSiteURL(c, cfg)
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(http.StatusOK, seoser.BuildLLMSTXT(siteURL, cfg))
|
||||
}
|
||||
|
||||
// SEOVerifyFile 输出 IndexNow 所有权验证文本。
|
||||
func SEOVerifyFile(c *gin.Context) {
|
||||
key := strings.TrimSuffix(strings.TrimPrefix(c.Request.URL.Path, "/"), ".txt")
|
||||
configured := seoser.VerifyFromConfig().IndexNowKey
|
||||
if key == "" || key != configured {
|
||||
NotFound(c)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(http.StatusOK, configured)
|
||||
}
|
||||
|
||||
// DownloadApp 将站内统一下载入口重定向到配置的官方地址,避免模板回退链接产生 404。
|
||||
func DownloadApp(c *gin.Context) {
|
||||
cfg := configser.FetchAppConfigVMOrEmpty(c)
|
||||
target := strings.TrimSpace(cfg.AppAddr)
|
||||
if target == "" {
|
||||
target = strings.TrimSpace(cfg.OfficialAppAddr)
|
||||
}
|
||||
if target == "" {
|
||||
target = "/"
|
||||
}
|
||||
c.Header("X-Robots-Tag", "noindex, nofollow")
|
||||
c.Redirect(http.StatusFound, target)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package web
|
||||
|
||||
import "videoweb/app/service/playletser"
|
||||
|
||||
func rankingSEOSelections(value string) []playletser.SEOSelection {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return []playletser.SEOSelection{{Dimension: "榜单", Value: value}}
|
||||
}
|
||||
|
||||
func moreListSEOSelections(q playletser.MoreListQuery, themes []playletser.PlayletThemeItem) []playletser.SEOSelection {
|
||||
selections := make([]playletser.SEOSelection, 0, 2)
|
||||
if q.TimeRange == 1 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "时间", Value: "本周"})
|
||||
} else if q.TimeRange == 2 {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "时间", Value: "本月"})
|
||||
}
|
||||
themeName := q.ThemeSlug
|
||||
for _, theme := range themes {
|
||||
if (q.ThemeID > 0 && theme.ID == q.ThemeID) || (q.ThemeSlug != "" && theme.Slug == q.ThemeSlug) {
|
||||
themeName = theme.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if themeName != "" {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "题材", Value: themeName})
|
||||
}
|
||||
return selections
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Series 剧集列表页:GET /api/app/video/all/list + topic/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_all_list
|
||||
func Series(c *gin.Context) {
|
||||
q := videoser.ParseSeriesListQuery(c)
|
||||
topics := videoser.FetchVideoTopicListOrEmpty(c, q.MediaType)
|
||||
topicResolved := seriesTopicResolved(q, topics)
|
||||
raw := videoser.FetchVideoAllListOrEmpty(c, q)
|
||||
pageHeading, seoSelections := seriesSEOContext(q, topics)
|
||||
|
||||
title := pageHeading + " - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
}
|
||||
|
||||
list := []videoser.VideoAllListItemResp(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo videoser.SeoInfo
|
||||
var advitise any
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
advitise = raw.Advitise
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
|
||||
HTMLOK(c, "series.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "series",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": seoSelections,
|
||||
"pageHeading": pageHeading,
|
||||
"mediaType": q.MediaType,
|
||||
"topicId": q.TopicID,
|
||||
"sortType": q.SortType,
|
||||
"seoEntityValid": topicResolved || total > 0,
|
||||
"topicList": topics,
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"advitise": advitise,
|
||||
"pagination": videoser.BuildSeriesListPagination(q, total),
|
||||
"canonicalPath": q.URL(),
|
||||
})
|
||||
}
|
||||
|
||||
func seriesTopicResolved(q videoser.SeriesListQuery, topics []videoser.VideoTopicInfo) bool {
|
||||
if q.TopicID <= 0 {
|
||||
return true
|
||||
}
|
||||
for _, topic := range topics {
|
||||
if topic.ID == q.TopicID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func seriesSEOContext(q videoser.SeriesListQuery, topics []videoser.VideoTopicInfo) (string, []playletser.SEOSelection) {
|
||||
heading := "剧集与动画"
|
||||
selections := make([]playletser.SEOSelection, 0, 3)
|
||||
if q.MediaType == videoser.MediaFilm || q.MediaType == videoser.MediaAnime {
|
||||
mediaName := videoser.MediaTypeLabel(q.MediaType)
|
||||
heading = mediaName
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "类型", Value: mediaName})
|
||||
}
|
||||
if q.TopicID > 0 {
|
||||
for _, topic := range topics {
|
||||
if topic.ID == q.TopicID {
|
||||
name := topic.DisplayName()
|
||||
if name != "" {
|
||||
heading = name + heading
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "题材", Value: name})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if q.SortType == videoser.SortHeat {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "排序", Value: "最热"})
|
||||
} else if q.SortType == videoser.SortCollect {
|
||||
selections = append(selections, playletser.SEOSelection{Dimension: "排序", Value: "收藏最多"})
|
||||
}
|
||||
return heading, selections
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Setting 展示账号设置,字段来自最新的用户信息接口。
|
||||
func Setting(c *gin.Context) {
|
||||
data := gin.H{"title": "账号设置 - 黄果短剧", "navActive": "setting", "accountTab": "setting"}
|
||||
loadAccountSummary(c, data, gin.H{})
|
||||
HTMLOK(c, "setting.html", data)
|
||||
}
|
||||
|
||||
// UpdateProfile 代理账号资料修改。可选字段必须显式传入,避免覆盖未修改内容。
|
||||
func UpdateProfile(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "tip": "请先登录"})
|
||||
return
|
||||
}
|
||||
var body authser.UpdateProfileReq
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "参数错误"})
|
||||
return
|
||||
}
|
||||
if body.NickName != nil {
|
||||
v := strings.TrimSpace(*body.NickName)
|
||||
if v == "" || len([]rune(v)) > 40 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "昵称须为 1 至 40 个字符"})
|
||||
return
|
||||
}
|
||||
body.NickName = &v
|
||||
}
|
||||
if body.Introduction != nil {
|
||||
v := strings.TrimSpace(*body.Introduction)
|
||||
if len([]rune(v)) > 300 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "简介不能超过 300 个字符"})
|
||||
return
|
||||
}
|
||||
body.Introduction = &v
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
v := strings.TrimSpace(*body.Avatar)
|
||||
if safeProfileImage(v) == "" || strings.HasPrefix(v, "/static/") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "头像地址无效"})
|
||||
return
|
||||
}
|
||||
body.Avatar = &v
|
||||
}
|
||||
if (body.OldPassword == nil) != (body.NewPassword == nil) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "请输入旧密码和新密码"})
|
||||
return
|
||||
}
|
||||
if body.OldPassword != nil && (*body.OldPassword == "" || *body.NewPassword == "") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "密码不能为空"})
|
||||
return
|
||||
}
|
||||
if body.NickName == nil && body.Introduction == nil && body.Avatar == nil && body.OldPassword == nil && body.PrivacyMode == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "没有需要保存的修改"})
|
||||
return
|
||||
}
|
||||
env, err := authser.UpdateProfile(c, body)
|
||||
if err != nil {
|
||||
log.Error("update profile failed", log.E(err))
|
||||
c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"})
|
||||
return
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
if env == nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "tip": firstNonEmptyAuth(env.Tip, env.Msg, "保存失败")})
|
||||
return
|
||||
}
|
||||
if infoEnv, user, err := authser.GetInfo(c); err == nil && infoEnv != nil && infoEnv.Code == 200 && user != nil {
|
||||
authser.SetAuthCookies(c, user)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "tip": firstNonEmptyAuth(env.Tip, "保存成功")})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Tags 标签作品列表页:GET /api/app/playlet/tag/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_tag_list
|
||||
func Tags(c *gin.Context) {
|
||||
q := playletser.ParseTagListQuery(c)
|
||||
tagOptions := playletser.FetchTagOptionsOrEmpty(c)
|
||||
tagName := playletser.ResolveTagListIdentity(&q, tagOptions)
|
||||
tagResolved := tagName != ""
|
||||
raw := playletser.FetchTagListOrEmpty(c, q)
|
||||
|
||||
if tagName == "" && raw != nil {
|
||||
if raw.SeoInfo.Title != "" {
|
||||
tagName = raw.SeoInfo.Title
|
||||
} else {
|
||||
tagName = playletser.FindTagNameBySlug(raw.List, q.TagSlug)
|
||||
if tagName == "" {
|
||||
tagName = playletser.FindTagName(raw.List, q.TagID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
title := "标签 - 黄果短剧"
|
||||
if raw != nil {
|
||||
title = playletser.TagListPageTitle(tagName, q.PageNum, raw.SeoInfo)
|
||||
} else if tagName != "" {
|
||||
title = playletser.TagListPageTitle(tagName, q.PageNum, playletser.SeoInfo{})
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
|
||||
desc := ""
|
||||
if seo.Description != "" {
|
||||
desc = seo.Description
|
||||
} else if total > 0 {
|
||||
desc = fmt.Sprintf("%d 部作品带有此标签", total)
|
||||
}
|
||||
seoSelections := make([]playletser.SEOSelection, 0, 2)
|
||||
if tagName != "" {
|
||||
seoSelections = append(seoSelections, playletser.SEOSelection{Dimension: "标签", Value: tagName})
|
||||
}
|
||||
if q.SortType == 2 {
|
||||
seoSelections = append(seoSelections, playletser.SEOSelection{Dimension: "排序", Value: "最热"})
|
||||
} else if q.SortType == 3 {
|
||||
seoSelections = append(seoSelections, playletser.SEOSelection{Dimension: "排序", Value: "收藏最多"})
|
||||
}
|
||||
|
||||
HTMLOK(c, "tags.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "list",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": seoSelections,
|
||||
"tagName": tagName,
|
||||
"tagSlug": q.TagSlug,
|
||||
"tagId": q.TagID,
|
||||
"sortType": q.SortType,
|
||||
"seoEntityValid": tagResolved || total > 0,
|
||||
"relatedTags": playletser.RelatedTagOptions(tagOptions, q.TagID, q.TagSlug),
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"listCount": fmt.Sprintf("共 %d 部", total),
|
||||
"heroDesc": desc,
|
||||
"pagination": playletser.BuildTagListPagination(q, total),
|
||||
"canonicalPath": playletser.TagListCanonicalPath(q),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Terms renders the static Terms of Service page.
|
||||
func Terms(c *gin.Context) {
|
||||
HTMLOK(c, "terms.html", gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Topic 专题剧单列表页:GET /api/app/playlet/topic/list。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_topic_list
|
||||
func Topic(c *gin.Context) {
|
||||
q := playletser.ParseTopicListQuery(c)
|
||||
raw := playletser.FetchTopicListOrEmpty(c, q)
|
||||
home := playletser.FetchHomeOrEmpty(c)
|
||||
homeConfigData := playletser.ActiveHomeConfigs(home.HomeConfigData)
|
||||
list := []playletser.PlayletTopicPageItem(nil)
|
||||
total := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
var seoSelections []playletser.SEOSelection
|
||||
if q.SortType == 2 {
|
||||
seoSelections = []playletser.SEOSelection{{Dimension: "排序", Value: "最热"}}
|
||||
}
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
total = raw.TotalCount
|
||||
seo = raw.SeoInfo
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
// 首页“专题”区块的配置 TDK 优先;专题列表接口 SEO 仅补齐配置未填写的字段。
|
||||
if configSEO, ok := playletser.FirstHomeConfigSEOByType(homeConfigData, 3); ok {
|
||||
seo = playletser.MergeSeoInfo(configSEO, seo)
|
||||
}
|
||||
title := "专题剧单 - 黄果短剧"
|
||||
if seo.Title != "" {
|
||||
title = seo.Title
|
||||
}
|
||||
HTMLOK(c, "topic.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "topic",
|
||||
"seoInfo": seo,
|
||||
"routeConfigSEO": true,
|
||||
"homeConfigData": homeConfigData,
|
||||
"seoSelections": seoSelections,
|
||||
"list": list,
|
||||
"totalCount": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"sortType": q.SortType,
|
||||
"pagination": playletser.BuildTopicListPagination(q, total),
|
||||
"canonicalPath": q.URL(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TopicDetail 专题详情页:GET /api/app/playlet/topic/detail。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_topic_detail
|
||||
func TopicDetail(c *gin.Context) {
|
||||
q := playletser.ParseTopicDetailQuery(c)
|
||||
playletser.ResolveTopicDetailID(c, &q)
|
||||
raw := playletser.FetchTopicDetailOrEmpty(c, q)
|
||||
if raw != nil {
|
||||
if raw.Slug != "" {
|
||||
q.Slug = raw.Slug
|
||||
}
|
||||
if raw.ID > 0 {
|
||||
q.ID = raw.ID
|
||||
}
|
||||
}
|
||||
|
||||
name := ""
|
||||
if raw != nil {
|
||||
name = raw.Name
|
||||
}
|
||||
title := "专题 - 黄果短剧"
|
||||
if raw != nil && raw.SeoInfo.Title != "" {
|
||||
title = raw.SeoInfo.Title
|
||||
} else if name != "" {
|
||||
title = name + " - 黄果短剧"
|
||||
}
|
||||
|
||||
list := []playletser.CommonData(nil)
|
||||
more := []playletser.PlayletTopicPageItem(nil)
|
||||
total := int64(0)
|
||||
playletCount := int64(0)
|
||||
pageNum, pageSize := q.PageNum, q.PageSize
|
||||
var seo playletser.SeoInfo
|
||||
desc := ""
|
||||
heroCover := ""
|
||||
covers := []string(nil)
|
||||
if raw != nil {
|
||||
list = raw.List
|
||||
more = raw.MoreTopics
|
||||
total = raw.TotalCount
|
||||
playletCount = raw.PlayletCount
|
||||
seo = raw.SeoInfo
|
||||
desc = raw.Desc
|
||||
heroCover = playletser.TopicDetailHeroCover(*raw)
|
||||
covers = playletser.TopicDetailCoverURLs(*raw)
|
||||
if raw.PageNum > 0 {
|
||||
pageNum = raw.PageNum
|
||||
}
|
||||
if raw.PageSize > 0 {
|
||||
pageSize = raw.PageSize
|
||||
}
|
||||
}
|
||||
countText := "持续更新"
|
||||
if playletCount > 0 {
|
||||
countText = fmt.Sprintf("%d 部 持续更新", playletCount)
|
||||
} else if total > 0 {
|
||||
countText = fmt.Sprintf("%d 部 持续更新", total)
|
||||
}
|
||||
var seoSelections []playletser.SEOSelection
|
||||
if q.SortType != 1 {
|
||||
seoSelections = []playletser.SEOSelection{{Dimension: "排序", Value: playletser.TopicDetailSortLabel(q.SortType)}}
|
||||
}
|
||||
|
||||
HTMLOK(c, "topicDetail.html", gin.H{
|
||||
"title": title,
|
||||
"navActive": "topic",
|
||||
"seoInfo": seo,
|
||||
"seoSelections": seoSelections,
|
||||
"topic": raw,
|
||||
"name": name,
|
||||
"desc": desc,
|
||||
"slug": q.Slug,
|
||||
"topicId": q.ID,
|
||||
"list": list,
|
||||
"moreTopics": more,
|
||||
"totalCount": total,
|
||||
"playletCount": playletCount,
|
||||
"countText": countText,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
"sortType": q.SortType,
|
||||
"sortLabel": playletser.TopicDetailSortLabel(q.SortType),
|
||||
"seoEntityValid": q.ID > 0 && (name != "" || total > 0),
|
||||
"heroCover": heroCover,
|
||||
"covers": covers,
|
||||
"pagination": playletser.BuildTopicDetailPagination(q, total),
|
||||
"canonicalPath": q.URL(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// attachTrackerPage 为所有 HTML 页面补齐稳定的 pageKey/pageName,避免事件落入 unknown。
|
||||
func attachTrackerPage(path string, code int, data map[string]any) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
key, name := trackerPageFromRequest(path, code)
|
||||
if current, _ := data["pageKey"].(string); strings.TrimSpace(current) == "" {
|
||||
data["pageKey"] = key
|
||||
}
|
||||
if current, _ := data["pageName"].(string); strings.TrimSpace(current) == "" {
|
||||
data["pageName"] = name
|
||||
}
|
||||
}
|
||||
|
||||
func trackerPageFromRequest(path string, code int) (string, string) {
|
||||
if code >= http.StatusInternalServerError {
|
||||
return "server_error", "服务异常页面"
|
||||
}
|
||||
if code >= http.StatusBadRequest {
|
||||
return "not_found", "404页面"
|
||||
}
|
||||
return trackerPageFromPath(path)
|
||||
}
|
||||
|
||||
func trackerPageFromPath(path string) (string, string) {
|
||||
p := strings.TrimSuffix(strings.TrimSpace(path), "/")
|
||||
if p == "" {
|
||||
return "home", "首页"
|
||||
}
|
||||
rules := []struct {
|
||||
prefix string
|
||||
key string
|
||||
name string
|
||||
}{
|
||||
{"/series-play", "series_play", "AI成人剧集播放页"},
|
||||
{"/series", "series", "AI成人剧集"},
|
||||
{"/play", "play", "短剧播放页"},
|
||||
{"/detail", "detail", "短剧详情"},
|
||||
{"/list", "list", "短剧列表"},
|
||||
{"/recently-added", "recently_added", "最新短剧"},
|
||||
{"/popular-recommend", "popular_recommend", "热门推荐"},
|
||||
{"/rank", "rank", "排行榜"},
|
||||
{"/topic-detail", "topic_detail", "专题详情"},
|
||||
{"/topic", "topic", "专题列表"},
|
||||
{"/tags", "tags", "标签详情"},
|
||||
{"/more-series-list", "more_series_list", "成人剧集列表"},
|
||||
{"/more-list", "more_list", "短剧扩展列表"},
|
||||
{"/search-mobile", "search", "搜索结果页"},
|
||||
{"/search", "search", "搜索结果页"},
|
||||
{"/history", "history", "观看历史"},
|
||||
{"/favorites", "favorites", "我的收藏"},
|
||||
{"/purchased", "purchased", "已购剧集"},
|
||||
{"/profile", "profile", "个人中心"},
|
||||
{"/setting", "setting", "设置"},
|
||||
{"/recharge", "recharge", "充值中心"},
|
||||
{"/download-app", "download_app", "下载APP"},
|
||||
{"/feedback", "feedback", "问题反馈"},
|
||||
{"/help", "help", "帮助中心"},
|
||||
{"/about", "about", "关于我们"},
|
||||
{"/terms", "terms", "用户协议"},
|
||||
{"/privacy", "privacy", "隐私政策"},
|
||||
{"/2257", "compliance_2257", "2257合规声明"},
|
||||
{"/dmca", "dmca", "DMCA版权声明"},
|
||||
{"/login", "login", "登录"},
|
||||
{"/register", "register", "注册"},
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if p == rule.prefix || strings.HasPrefix(p, rule.prefix+"/") {
|
||||
return rule.key, rule.name
|
||||
}
|
||||
}
|
||||
return "internal_other", "站内其他页面"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// VideoCollectSet BFF:代理 POST /api/app/video/collect/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_collect_set
|
||||
func VideoCollectSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ID int64 `json:"id"`
|
||||
IsCollect *bool `json:"isCollect"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsCollect == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "id/isCollect required",
|
||||
"tip": "缺少视频 ID 或收藏状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := videoser.CollectSet(c, videoser.VideoCollectSetReq{
|
||||
ID: body.ID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// VideoLikeSet BFF:代理 POST /api/app/video/like/set。
|
||||
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_like_set
|
||||
func VideoLikeSet(c *gin.Context) {
|
||||
var body struct {
|
||||
ID int64 `json:"id"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "invalid body",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "id/isLike required",
|
||||
"tip": "缺少视频 ID 或点赞状态",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := videoser.LikeSet(c, videoser.VideoLikeSetReq{
|
||||
ID: body.ID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty response",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": env.Tip,
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user