Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/applistser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Application 福利应用页 SSR:拉取 /api/app/applist/list。
|
||||
func Application(c *gin.Context) {
|
||||
raw := applistser.FetchListOrEmpty(c, 0)
|
||||
vm := applistser.BuildPageVM(raw)
|
||||
data := gin.H{
|
||||
"title": "福利应用",
|
||||
"navActive": "application",
|
||||
"categories": vm.Categories,
|
||||
}
|
||||
if len(vm.Categories) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "application.html", data)
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"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"`
|
||||
}
|
||||
|
||||
type resetPasswordBody struct {
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type updateProfileBody struct {
|
||||
Avatar string `json:"avatar"`
|
||||
Introduction string `json:"introduction"`
|
||||
NickName string `json:"nickName"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
PrivacyMode *bool `json:"privacyMode"`
|
||||
}
|
||||
|
||||
// Register 代理上游注册,成功后写入登录 cookie。
|
||||
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 = strings.TrimSpace(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 + "@qmdj.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.Code != 200 || user == nil {
|
||||
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": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, "注册成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// 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 = strings.TrimSpace(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.Code != 200 || user == nil {
|
||||
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": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, "登录成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword 代理上游找回密码接口。
|
||||
func ResetPassword(c *gin.Context) {
|
||||
var body resetPasswordBody
|
||||
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)
|
||||
if body.AccountNo == "" || body.Email == "" || body.NewPassword == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "请填写账号、邮箱和新密码", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := authser.ResetPassword(c, authser.ResetPasswordReq{
|
||||
AccountNo: body.AccountNo,
|
||||
Email: body.Email,
|
||||
NewPassword: body.NewPassword,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("reset password upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "reset password failed", "tip": "重置密码失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
tip := firstNonEmpty(env.Tip, env.Msg)
|
||||
if env.Code == 200 {
|
||||
tip = firstNonEmpty(tip, "密码重置成功")
|
||||
} else {
|
||||
tip = firstNonEmpty(tip, "密码重置失败")
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
|
||||
}
|
||||
|
||||
// Guest 代理上游游客登录,成功后写入登录 cookie。
|
||||
// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent(由 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 = strings.TrimSpace(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.Code != 200 || user == nil {
|
||||
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": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "游客登录成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile 代理上游修改资料,成功后刷新 getInfo 并写 cookie。
|
||||
func UpdateProfile(c *gin.Context) {
|
||||
var body updateProfileBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.Avatar = strings.TrimSpace(body.Avatar)
|
||||
body.Introduction = strings.TrimSpace(body.Introduction)
|
||||
body.NickName = strings.TrimSpace(body.NickName)
|
||||
body.OldPassword = strings.TrimSpace(body.OldPassword)
|
||||
body.NewPassword = strings.TrimSpace(body.NewPassword)
|
||||
|
||||
req := authser.UpdateProfileReq{
|
||||
Avatar: body.Avatar,
|
||||
Introduction: body.Introduction,
|
||||
NickName: body.NickName,
|
||||
OldPassword: body.OldPassword,
|
||||
NewPassword: body.NewPassword,
|
||||
PrivacyMode: body.PrivacyMode,
|
||||
}
|
||||
if req.Avatar == "" && req.Introduction == "" && req.NickName == "" && req.OldPassword == "" && req.NewPassword == "" && req.PrivacyMode == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请至少修改一项资料",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (req.OldPassword == "") != (req.NewPassword == "") {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "修改密码需同时填写旧密码和新密码",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := authser.UpdateProfile(c, req)
|
||||
if err != nil {
|
||||
log.Error("update profile upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "update profile failed",
|
||||
"tip": "保存失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = authser.UpdateProfile(c, req)
|
||||
if err != nil {
|
||||
log.Error("update profile retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "update profile failed",
|
||||
"tip": "保存失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "保存失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 上游成功后拉最新资料写 cookie
|
||||
_, user, infoErr := authser.GetInfo(c)
|
||||
if infoErr != nil || user == nil {
|
||||
log.Error("update profile getInfo failed", log.E(infoErr))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, "保存成功"),
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
authser.SetAuthCookies(c, user)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, "保存成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadImage 代理上游图片上传(头像不加 watermark)。
|
||||
func UploadImage(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请选择图片文件",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
f, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "open file failed",
|
||||
"tip": "读取图片失败",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
resp, err := authser.UploadImage(c, fileHeader.Filename, f)
|
||||
if err != nil {
|
||||
log.Error("upload image upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "upload failed",
|
||||
"tip": "上传失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if resp == nil || resp.Code != 200 || resp.Data == nil || strings.TrimSpace(resp.Data.SourceNameFs) == "" {
|
||||
tip := "上传失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if resp != nil {
|
||||
code = resp.Code
|
||||
msg = resp.Msg
|
||||
tip = firstNonEmpty(resp.Tip, resp.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(resp.Msg, "success"),
|
||||
"tip": firstNonEmpty(resp.Tip, "上传成功"),
|
||||
"data": gin.H{
|
||||
"id": resp.Data.ID,
|
||||
"sourceNameFs": resp.Data.SourceNameFs,
|
||||
"url": resp.Data.SourceNameFs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserInfo 代理上游 GET /api/app/auth/getInfo。
|
||||
func GetUserInfo(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, user, err := authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("getInfo upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "getInfo failed",
|
||||
"tip": "获取用户信息失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, user, err = authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("getInfo retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "getInfo 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
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
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": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoginEntryURL 首页弹窗登录入口;redirectPath 为登录成功后回跳路径(须为站内相对路径)。
|
||||
func LoginEntryURL(redirectPath string) string {
|
||||
redirectPath = strings.TrimSpace(redirectPath)
|
||||
if i := strings.IndexByte(redirectPath, '?'); i >= 0 {
|
||||
redirectPath = redirectPath[:i]
|
||||
}
|
||||
if !isSafeRedirect(redirectPath) || redirectPath == "/" {
|
||||
return "/login/"
|
||||
}
|
||||
return "/login/return" + ensureRedirectTrailingSlash(redirectPath)
|
||||
}
|
||||
|
||||
// RegisterEntryURL 首页弹窗注册入口。
|
||||
func RegisterEntryURL() string {
|
||||
return "/register/"
|
||||
}
|
||||
|
||||
func ensureRedirectTrailingSlash(path string) string {
|
||||
if i := strings.IndexByte(path, '?'); i >= 0 {
|
||||
path = path[:i]
|
||||
}
|
||||
if path != "/" && !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// RedirectToLogin 未登录访问受保护页时,跳到首页并打开登录弹窗。
|
||||
func RedirectToLogin(c *gin.Context) {
|
||||
path := ""
|
||||
if c != nil && c.Request != nil && c.Request.URL != nil {
|
||||
path = c.Request.URL.RequestURI()
|
||||
}
|
||||
if path == "" || path == "/login" || strings.HasPrefix(path, "/login/") {
|
||||
path = "/"
|
||||
}
|
||||
c.Redirect(http.StatusFound, LoginEntryURL(path))
|
||||
}
|
||||
|
||||
func isSafeRedirect(path string) bool {
|
||||
if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(path, "\r\n") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CategoryDetail 分类详情页:按分类 slug 查询,slug 缺失时兼容数值 ID。
|
||||
func CategoryDetail(c *gin.Context) {
|
||||
identifier := strings.TrimSpace(c.Param("slug"))
|
||||
if identifier == "" {
|
||||
NotFound(c, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
q := playletser.ParseCategoryDetailQuery(identifier, routeOrQuery(c, "page", "page"))
|
||||
raw := playletser.FetchCategoryDetailOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
if emptyScopedPage(true, raw.TotalCount, len(raw.List)) {
|
||||
NotFound(c, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 接口返回了规范 slug 时优先使用 slug;没有 slug 才继续使用 ID。
|
||||
if slug := strings.TrimSpace(raw.CategorySlug); slug != "" {
|
||||
q.CategorySlug = slug
|
||||
q.CategoryID = raw.CategoryID
|
||||
} else if raw.CategoryID > 0 {
|
||||
q.CategoryID = raw.CategoryID
|
||||
}
|
||||
if redirectCanonical(c, playletser.CategoryDetailCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
|
||||
pageName := strings.TrimSpace(raw.CategoryName)
|
||||
if pageName == "" {
|
||||
pageName = strings.TrimSpace(raw.SeoInfo.Title)
|
||||
}
|
||||
if pageName == "" {
|
||||
pageName = identifier
|
||||
}
|
||||
description := strings.TrimSpace(raw.SeoInfo.Description)
|
||||
if description == "" {
|
||||
description = "汇集近期最新发布的短剧、漫剧、AI换脸、AI魔改等内容,持续更新不同题材与类型的新作品,方便快速发现站内最近上线和最新更新的精彩内容。"
|
||||
}
|
||||
title := playletser.CategoryDetailPageTitle(pageName, q.PageNum, raw.SeoInfo)
|
||||
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"pageKey": "category_detail",
|
||||
"pageName": pageName,
|
||||
"pageTitle": pageName,
|
||||
"categoryDescription": description,
|
||||
"sectionTitle": "最新上架作品",
|
||||
"pageNum": q.PageNum,
|
||||
"pagePath": playletser.CategoryDetailCanonicalPath(q),
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"list": raw.List,
|
||||
"totalCount": raw.TotalCount,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildCategoryDetailPagination(q, raw.TotalCount),
|
||||
}
|
||||
if raw.TotalCount <= 0 || len(raw.List) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "categoryDetail.html", data)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChapterDetail 代理上游 GET /api/app/playlet/chapter/detail(播放页 CSR 切集)。
|
||||
func ChapterDetail(c *gin.Context) {
|
||||
chapterID, _ := strconv.ParseInt(c.Query("chapterId"), 10, 64)
|
||||
playletID, _ := strconv.ParseInt(c.Query("playletId"), 10, 64)
|
||||
playletSlug := strings.TrimSpace(c.Query("playletSlug"))
|
||||
if chapterID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 chapterId",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := playletser.FetchChapterDetail(c, chapterID, playletID, playletSlug)
|
||||
if err != nil {
|
||||
log.Error("chapter detail upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter detail failed",
|
||||
"tip": "获取剧集失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if data != nil && useragent.TokenFromRequest(c.Request) != "" {
|
||||
authser.SetRechargeCookie(c, data.Balance)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"tip": "成功",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type chapterLikeBody struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// ChapterLikeSet 代理上游 POST /api/app/playlet/chapter/like/set。
|
||||
func ChapterLikeSet(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再点赞",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body chapterLikeBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 chapterId 或 isLike",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter like set upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter like set retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
type chapterCollectBody struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
IsCollect *bool `json:"isCollect"`
|
||||
}
|
||||
|
||||
// ChapterCollectSet 代理上游 POST /api/app/playlet/chapter/collect/set。
|
||||
func ChapterCollectSet(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再收藏",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body chapterCollectBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.IsCollect == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 chapterId 或 isCollect",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter collect set upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter collect failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
|
||||
ChapterID: body.ChapterID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter collect set retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter collect failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type chapterUnlockBody struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
}
|
||||
|
||||
// ChapterUnlock 代理上游 POST /api/app/playlet/chapter/unlock。
|
||||
func ChapterUnlock(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再解锁",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body chapterUnlockBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 || body.PlayletID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 chapterId 或 playletId",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter unlock upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter unlock failed",
|
||||
"tip": "解锁失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter unlock retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter unlock failed",
|
||||
"tip": "解锁失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "解锁失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetRechargeCookie(c, env.Data.Balance)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "解锁成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type chapterWatchReportBody struct {
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
PlayletID int64 `json:"playletId"`
|
||||
}
|
||||
|
||||
// ChapterWatchReport 代理上游 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 chapterWatchReportBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ChapterID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 chapterId",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter watch report upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter watch report failed",
|
||||
"tip": "上报失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
|
||||
ChapterID: body.ChapterID,
|
||||
PlayletID: body.PlayletID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("chapter watch report retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "chapter watch report failed",
|
||||
"tip": "上报失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "上报失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Collect 我的收藏 SSR:
|
||||
// 短剧 GET /api/app/playlet/collect/list;剧集 GET /api/app/playlet/chapter/collect/list。
|
||||
func Collect(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
q := playletser.ParseCollectListQuery(routeOrQuery(c, "page", "page"), routeOrQuery(c, "type", "type"))
|
||||
if redirectCanonical(c, playletser.CollectCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
data := gin.H{
|
||||
"title": "我的收藏",
|
||||
"description": "查看你在黄果短剧收藏的短剧与剧集,随时继续追更喜欢的内容。",
|
||||
"navActive": "userCenter",
|
||||
"collectType": q.Type,
|
||||
"pageNum": q.PageNum,
|
||||
"pageSize": q.PageSize,
|
||||
"totalCount": int64(0),
|
||||
"pagination": playletser.BuildCollectPagination(q, 0),
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
|
||||
if q.Type == playletser.CollectTypeChapter {
|
||||
raw, code, err := playletser.FetchChapterCollectList(c, q)
|
||||
if err != nil {
|
||||
aborted, retry := AbortSSRIfSessionExpired(c, code)
|
||||
if aborted {
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
raw, _, err = playletser.FetchChapterCollectList(c, q)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch playlet chapter collect list failed", log.E(err))
|
||||
raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
}
|
||||
if raw == nil {
|
||||
raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
data["chapterList"] = raw.List
|
||||
data["pageNum"] = raw.PageNum
|
||||
data["pageSize"] = raw.PageSize
|
||||
data["totalCount"] = raw.TotalCount
|
||||
data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
|
||||
} else {
|
||||
raw, code, err := playletser.FetchCollectList(c, q)
|
||||
if err != nil {
|
||||
aborted, retry := AbortSSRIfSessionExpired(c, code)
|
||||
if aborted {
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
raw, _, err = playletser.FetchCollectList(c, q)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch playlet collect list failed", log.E(err))
|
||||
raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
}
|
||||
if raw == nil {
|
||||
raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
data["list"] = raw.List
|
||||
data["pageNum"] = raw.PageNum
|
||||
data["pageSize"] = raw.PageSize
|
||||
data["totalCount"] = raw.TotalCount
|
||||
data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
|
||||
}
|
||||
|
||||
HTMLOK(c, "collect.html", data)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/service/commentser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CommentList 代理上游 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"))
|
||||
|
||||
q := commentser.ListQuery{
|
||||
ObjectID: objectID,
|
||||
ObjectType: objectType,
|
||||
PageNum: pageNum,
|
||||
PageSize: pageSize,
|
||||
SortOrder: sortOrder,
|
||||
}
|
||||
if q.ObjectID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 objectId",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := commentser.FetchList(c, q)
|
||||
if err != nil {
|
||||
log.Error("comment list upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "comment list failed",
|
||||
"tip": "获取评论失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"tip": "成功",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
type commentSendBody struct {
|
||||
ObjectID int64 `json:"objectId"`
|
||||
ObjectType int `json:"objectType"`
|
||||
Text string `json:"text"`
|
||||
ParentsID int64 `json:"parentsId"`
|
||||
ReplyCommentID int64 `json:"replyCommentId"`
|
||||
}
|
||||
|
||||
// CommentSend 代理上游 POST /api/app/comment/send。
|
||||
// parentsId=0 一级评论;parentsId>0 二级评论;replyCommentId 为实际被回复评论。
|
||||
func CommentSend(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再发表评论",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body commentSendBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.Text = strings.TrimSpace(body.Text)
|
||||
if body.ObjectID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 objectId",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ObjectType != commentser.ObjectTypeChapter {
|
||||
body.ObjectType = commentser.ObjectTypePlaylet
|
||||
}
|
||||
if body.Text == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请输入评论内容",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if utf8.RuneCountInString(body.Text) > 200 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "评论最多 200 字",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ParentsID < 0 {
|
||||
body.ParentsID = 0
|
||||
}
|
||||
if body.ReplyCommentID < 0 {
|
||||
body.ReplyCommentID = 0
|
||||
}
|
||||
|
||||
env, err := commentser.Send(c, commentser.CommentSendReq{
|
||||
ObjectID: body.ObjectID,
|
||||
ObjectType: body.ObjectType,
|
||||
Text: body.Text,
|
||||
ParentsID: body.ParentsID,
|
||||
ReplyCommentID: body.ReplyCommentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("comment send upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "comment send failed",
|
||||
"tip": "发表失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = commentser.Send(c, commentser.CommentSendReq{
|
||||
ObjectID: body.ObjectID,
|
||||
ObjectType: body.ObjectType,
|
||||
Text: body.Text,
|
||||
ParentsID: body.ParentsID,
|
||||
ReplyCommentID: body.ReplyCommentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("comment send retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "comment send failed",
|
||||
"tip": "发表失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "发表失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "发表成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
type commentLikeBody struct {
|
||||
CommentID int64 `json:"commentId"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// CommentLikeSet 代理上游 POST /api/app/comment/like/set。
|
||||
func CommentLikeSet(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再点赞",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body commentLikeBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.CommentID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 commentId 或 isLike",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{
|
||||
CommentID: body.CommentID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("comment like set upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "comment like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = commentser.LikeSet(c, commentser.CommentLikeSetReq{
|
||||
CommentID: body.CommentID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("comment like set retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "comment like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type buyPayBody struct {
|
||||
PackageID int64 `json:"packageId"`
|
||||
PayMode string `json:"payMode"`
|
||||
}
|
||||
|
||||
// attachWalletFromGetInfo 登录态拉取 getInfo,刷新 cookie,并写入模板金币余额。
|
||||
// 返回 true 表示注册账号会话失效,调用方应跳转登录。
|
||||
func attachWalletFromGetInfo(c *gin.Context, data gin.H) bool {
|
||||
if data == nil || useragent.TokenFromRequest(c.Request) == "" {
|
||||
return false
|
||||
}
|
||||
env, user, err := authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("wallet getInfo failed", log.E(err))
|
||||
return false
|
||||
}
|
||||
if env != nil && authser.IsSessionExpiredCode(env.Code) {
|
||||
retry, needLogin := authser.OnSessionExpired(c, env.Code)
|
||||
if needLogin {
|
||||
return true
|
||||
}
|
||||
if retry {
|
||||
env, user, err = authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("wallet getInfo retry failed", log.E(err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 || user == nil {
|
||||
return false
|
||||
}
|
||||
authser.SetAuthCookies(c, user)
|
||||
data["userId"] = authser.FormatUserID(user.UserID)
|
||||
data["recharge"] = user.Recharge
|
||||
data["rechargeText"] = authser.FormatRecharge(user.Recharge)
|
||||
data["vipType"] = user.VipType
|
||||
data["showCardId"] = user.ShowCardID
|
||||
data["showCardName"] = strings.TrimSpace(user.ShowCardName)
|
||||
active := authser.IsActiveVipUser(user.VipType, user.CurrentVipExpire)
|
||||
data["isVip"] = active
|
||||
if active {
|
||||
data["vipLabel"] = authser.VipDisplayLabel(user.ShowCardName, user.VipType)
|
||||
data["vipExpireText"] = authser.FormatVipExpireText(user.CurrentVipExpire)
|
||||
} else {
|
||||
delete(data, "vipLabel")
|
||||
delete(data, "vipExpireText")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Deposit 金币与会员充值页 SSR:套餐列表 + 金币流水明细。
|
||||
// Query tab=coin 时默认选中金币充值套餐。
|
||||
func Deposit(c *gin.Context) {
|
||||
// 先补游客会话,保证可拉流水。
|
||||
authser.EnsureGuestOnEnter(c)
|
||||
|
||||
preferTab := routeOrQuery(c, "tab", "tab")
|
||||
if hasQuery(c, "tab") {
|
||||
target := "/deposit"
|
||||
if preferTab != "" {
|
||||
target += "/" + preferTab
|
||||
}
|
||||
target += "/"
|
||||
if redirectCanonical(c, target) {
|
||||
return
|
||||
}
|
||||
}
|
||||
raw := rechargeser.FetchPackageListOrEmpty(c)
|
||||
vm := rechargeser.BuildDepositPageVM(raw, preferTab)
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
list, code, err := rechargeser.FetchCoinTransList(c, 1, 20)
|
||||
if err != nil {
|
||||
retry, needLogin := authser.OnSessionExpired(c, code)
|
||||
if needLogin {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
list, _, err = rechargeser.FetchCoinTransList(c, 1, 20)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch coin trans list failed", log.E(err))
|
||||
list = nil
|
||||
}
|
||||
}
|
||||
vm.AttachTxList(list)
|
||||
}
|
||||
data := gin.H{
|
||||
"title": "金币与会员",
|
||||
"vipList": vm.VipList,
|
||||
"coinList": vm.CoinList,
|
||||
"payMethods": vm.PayMethods,
|
||||
"txList": vm.TxList,
|
||||
"preferTab": preferTab,
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
if attachWalletFromGetInfo(c, data) {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
HTMLOK(c, "deposit.html", data)
|
||||
}
|
||||
|
||||
// CoinBuy 代理上游 POST /api/app/coin/buy。
|
||||
func CoinBuy(c *gin.Context) {
|
||||
handleBuyPay(c, "coin")
|
||||
}
|
||||
|
||||
// VipPay 代理上游 POST /api/app/vip/pay。
|
||||
func VipPay(c *gin.Context) {
|
||||
handleBuyPay(c, "vip")
|
||||
}
|
||||
|
||||
func handleBuyPay(c *gin.Context, kind string) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再支付",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body buyPayBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body.PayMode = rechargeser.NormalizePayMode(body.PayMode)
|
||||
if body.PackageID <= 0 || body.PayMode == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请选择套餐和支付方式",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
req := rechargeser.BuyPayReq{
|
||||
PackageID: body.PackageID,
|
||||
PayMode: body.PayMode,
|
||||
}
|
||||
|
||||
var (
|
||||
env *rechargeser.APIEnvelope[rechargeser.PayOrderResp]
|
||||
err error
|
||||
)
|
||||
switch kind {
|
||||
case "coin":
|
||||
env, err = rechargeser.BuyCoin(c, req)
|
||||
default:
|
||||
env, err = rechargeser.VipPay(c, req)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("buy/pay upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "pay failed",
|
||||
"tip": "发起支付失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
switch kind {
|
||||
case "coin":
|
||||
env, err = rechargeser.BuyCoin(c, req)
|
||||
default:
|
||||
env, err = rechargeser.VipPay(c, req)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("buy/pay retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "pay failed",
|
||||
"tip": "发起支付失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "发起支付失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
payURL := strings.TrimSpace(env.Data.PayURL)
|
||||
if payURL == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "empty payUrl",
|
||||
"tip": "未获取到支付链接,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "请完成支付"),
|
||||
"data": gin.H{
|
||||
"isOpenNewBrowser": env.Data.IsOpenNewBrowser,
|
||||
"payUrl": payURL,
|
||||
"payUrlMode": env.Data.PayURLMode,
|
||||
"tradeNo": env.Data.TradeNo,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Detail 详情页 SSR:GET /api/app/playlet/detail;seoInfo 直接注入模板。
|
||||
func Detail(c *gin.Context) {
|
||||
key := strings.TrimSpace(c.Param("slug"))
|
||||
if key == "" {
|
||||
NotFound(c, "未找到该短剧")
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchDetailOrNilByKey(c, key)
|
||||
if raw == nil {
|
||||
NotFound(c, "未找到该短剧")
|
||||
return
|
||||
}
|
||||
// 有 slug 使用 slug,否则保留 ID 路由。
|
||||
if redirectCanonical(c, playletser.DetailPath(raw.Slug, raw.ID)) {
|
||||
return
|
||||
}
|
||||
HTMLOK(c, "detail.html", gin.H{
|
||||
"title": raw.Title,
|
||||
"detail": raw,
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"episodeRanges": playletser.BuildEpisodeRanges(raw.PlayletChapterInfo, raw.TotalEpisode, 30),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/configser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var dialogTemplates = map[string]string{
|
||||
"download": "downloadAppDialog",
|
||||
"login": "loginDialog",
|
||||
"register": "registerDialog",
|
||||
"search": "searchDialog",
|
||||
}
|
||||
|
||||
// DialogFragment renders only the requested dialog body. Styles and behavior
|
||||
// are loaded by dialogLoader.js after the first user interaction.
|
||||
func DialogFragment(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
templateName, ok := dialogTemplates[name]
|
||||
if !ok {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{}
|
||||
if name == "download" {
|
||||
data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c)
|
||||
}
|
||||
|
||||
c.Header("Cache-Control", "private, max-age=300")
|
||||
c.HTML(http.StatusOK, templateName, data)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/service/feedbackser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type feedbackBody struct {
|
||||
Type int `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Contact string `json:"contact"`
|
||||
Screenshots []string `json:"screenshots"`
|
||||
}
|
||||
|
||||
type contactUsBody struct {
|
||||
Type int `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Email string `json:"email"`
|
||||
Attachments []string `json:"attachments"`
|
||||
}
|
||||
|
||||
// AddFeedback 代理上游 POST /api/app/feedback/add。
|
||||
func AddFeedback(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再提交反馈",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body feedbackBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"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": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请选择问题类型",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.Content == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "请填写问题描述",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if utf8.RuneCountInString(body.Content) > 500 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "问题描述最多 500 字",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
shots := make([]string, 0, len(body.Screenshots))
|
||||
for _, s := range body.Screenshots {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
shots = append(shots, s)
|
||||
}
|
||||
}
|
||||
if len(shots) > 10 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "最多上传 10 张图片",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := feedbackser.Add(c, feedbackser.FeedbackReq{
|
||||
Type: body.Type,
|
||||
Content: body.Content,
|
||||
Contact: body.Contact,
|
||||
Screenshots: shots,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("feedback add upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "feedback failed",
|
||||
"tip": "提交失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = feedbackser.Add(c, feedbackser.FeedbackReq{
|
||||
Type: body.Type,
|
||||
Content: body.Content,
|
||||
Contact: body.Contact,
|
||||
Screenshots: shots,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("feedback add retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "feedback failed",
|
||||
"tip": "提交失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "提交失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "提交成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
// AddContactUs 代理上游 POST /api/app/contactus/add。
|
||||
func AddContactUs(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录后再提交", "data": nil})
|
||||
return
|
||||
}
|
||||
var body contactUsBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1001, "msg": "参数错误", "tip": "参数错误", "data": nil})
|
||||
return
|
||||
}
|
||||
body.Title = strings.TrimSpace(body.Title)
|
||||
body.Content = strings.TrimSpace(body.Content)
|
||||
body.Email = strings.TrimSpace(body.Email)
|
||||
if body.Type < 1 || body.Type > 5 || body.Title == "" || body.Content == "" || body.Email == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "请完整填写问题类型、标题、内容和邮箱", "data": nil})
|
||||
return
|
||||
}
|
||||
attachments := make([]string, 0, len(body.Attachments))
|
||||
for _, attachment := range body.Attachments {
|
||||
if attachment = strings.TrimSpace(attachment); attachment != "" {
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
}
|
||||
req := feedbackser.ContactUsAddReq{
|
||||
Type: body.Type, Title: body.Title, Content: body.Content, Email: body.Email, Attachments: attachments,
|
||||
}
|
||||
env, err := feedbackser.AddContactUs(c, req)
|
||||
if err == nil && env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = feedbackser.AddContactUs(c, req)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("contact us upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "contact us failed", "tip": "提交失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
if env == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "contact us failed", "tip": "提交失败", "data": nil})
|
||||
return
|
||||
}
|
||||
defaultTip := "提交失败"
|
||||
if env.Code == 200 {
|
||||
defaultTip = "提交成功"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": env.Code,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, env.Msg, defaultTip),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// History 观看历史 SSR:GET /api/app/playlet/history/list。
|
||||
func History(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
q := playletser.ParseHistoryListQuery(routeOrQuery(c, "page", "page"))
|
||||
if redirectCanonical(c, playletser.HistoryCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
raw, code, err := playletser.FetchHistoryList(c, q)
|
||||
if err != nil {
|
||||
aborted, retry := AbortSSRIfSessionExpired(c, code)
|
||||
if aborted {
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
raw, _, err = playletser.FetchHistoryList(c, q)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("fetch playlet history list failed", log.E(err))
|
||||
raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
}
|
||||
if raw == nil {
|
||||
raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"title": "观看历史",
|
||||
"description": "查看黄果短剧观看历史,快速回到上次看到的剧集继续观看。",
|
||||
"navActive": "userCenter",
|
||||
"list": raw.List,
|
||||
"pageNum": raw.PageNum,
|
||||
"pageSize": raw.PageSize,
|
||||
"totalCount": raw.TotalCount,
|
||||
"pagination": playletser.BuildHistoryPagination(q, raw.TotalCount),
|
||||
"emptyTitle": "暂无观看历史",
|
||||
"emptyDesc": "开始播放短剧后,记录会出现在这里",
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
HTMLOK(c, "history.html", data)
|
||||
}
|
||||
|
||||
// HistoryClear 代理上游 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("playlet history clear upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "history clear failed",
|
||||
"tip": "清空失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.HistoryClear(c)
|
||||
if err != nil {
|
||||
log.Error("playlet history clear retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "history clear failed",
|
||||
"tip": "清空失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "清空失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "已清空观看历史"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/seoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HomeRoad 渲染黄果短剧最新地址与防失联入口页。
|
||||
func HomeRoad(c *gin.Context) {
|
||||
cfg := configser.FetchAppConfigVMOrEmpty(c)
|
||||
HTMLOK(c, "homeRoad.html", gin.H{
|
||||
"title": "回家的路 - 黄果短剧官方最新地址发布页",
|
||||
"pageKey": "home_road",
|
||||
"pageName": "回家的路",
|
||||
"pagePath": "/home-road/",
|
||||
"navActive": "homeRoad",
|
||||
"appConfig": cfg,
|
||||
"homeRoad": configser.BuildHomeRoadPageVM(cfg),
|
||||
"faqItems": []seoser.FAQItem{
|
||||
{Question: "回家的路页面有什么作用?", Answer: "集中展示最新地址、永久入口、备用入口和保存方式,方便访问异常时快速返回黄果短剧。"},
|
||||
{Question: "建议保存哪个入口?", Answer: "优先收藏当前推荐入口,并额外保存永久入口或备用入口。"},
|
||||
{Question: "入口打不开怎么办?", Answer: "先切换网络,再尝试永久入口或备用入口;仍无法访问时返回本页查看最新地址。"},
|
||||
{Question: "如何确认入口是否安全?", Answer: "只从本页、站内导航或黄果社区提示进入,不要从陌生弹窗、未知短链跳转。"},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HotLike 代理上游 GET /api/app/playlet/hot-like。
|
||||
func HotLike(c *gin.Context) {
|
||||
data, err := playletser.FetchHotLike(c)
|
||||
if err != nil {
|
||||
log.Error("playlet hot-like upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet hot-like failed",
|
||||
"tip": "获取热搜和猜你喜欢失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"tip": "成功",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Index 首页 SSR:拉取 /api/app/playlet/home,直接用接口字段渲染。
|
||||
func Index(c *gin.Context) {
|
||||
renderIndex(c, "")
|
||||
}
|
||||
|
||||
func AuthIndex(mode string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
renderIndex(c, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func renderIndex(c *gin.Context, authMode string) {
|
||||
authRedirect := c.Param("redirect")
|
||||
raw := playletser.FetchHomeOrEmpty(c)
|
||||
data := gin.H{
|
||||
"title": "黄果短剧",
|
||||
"navActive": "home",
|
||||
"homeResponse": raw,
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"announcements": raw.Announcements,
|
||||
"homeCategoryData": raw.HomeCategoryData,
|
||||
"homeRecommendThemes": raw.HomeRecommendThemes,
|
||||
"homeBannerData": playletser.ActiveHomeBanners(raw.HomeBannerData),
|
||||
"authMode": authMode,
|
||||
"authRedirect": authRedirect,
|
||||
}
|
||||
if authMode != "" {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "index.html", data)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List 短剧列表页 SSR:GET /api/app/playlet/operate/list,operateType 固定为 1。
|
||||
func List(c *gin.Context) {
|
||||
slug := strings.TrimSpace(c.Param("slug"))
|
||||
if slug == "ai-series" {
|
||||
legacy := playletser.ParseListQuery(
|
||||
routeOrQuery(c, "page", "page"),
|
||||
routeOrQuery(c, "sortOrder", "sortOrder"),
|
||||
c.Query("type"),
|
||||
)
|
||||
redirectCanonical(c, playletser.SeriesPathFromLegacyList(legacy.SortOrder, legacy.PageNum))
|
||||
return
|
||||
}
|
||||
filter := routeOrQuery(c, "sortOrder", "filterType")
|
||||
if filter == "" {
|
||||
filter = strings.TrimSpace(c.Query("sortOrder"))
|
||||
}
|
||||
q, ok := playletser.ParseShortPlayQuery(filter, routeOrQuery(c, "page", "page"))
|
||||
if !ok {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
if redirectCanonical(c, playletser.ShortPlayCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchOperateListOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
sectionTitle := playletser.ShortPlaySectionTitle(q.FilterType)
|
||||
data := gin.H{
|
||||
"title": "短剧",
|
||||
"description": "浏览黄果短剧全部短剧列表,按分类、标签筛选海量高清短剧,发现更多精彩剧集。",
|
||||
"pageKey": "list",
|
||||
"pageName": "AI成人短剧",
|
||||
"pagePath": playletser.ShortPlayCanonicalPath(q),
|
||||
"navActive": "list",
|
||||
"seoInfo": playletser.SeoInfo{},
|
||||
"seoSelections": []playletser.SEOSelection{{Dimension: "筛选", Value: strings.TrimSuffix(sectionTitle, "的AI成人短剧")}},
|
||||
"filters": playletser.BuildShortPlayFilterTabs(q),
|
||||
"sectionTitle": sectionTitle,
|
||||
"list": raw.List,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildShortPlayPagination(q, raw.TotalCount),
|
||||
}
|
||||
// /list/ai-short-play/original/ 入口页始终可索引;其余空结果仍 noindex。
|
||||
isOriginalEntry := q.FilterType == 3 && q.PageNum <= 1
|
||||
if !isOriginalEntry && (raw.TotalCount <= 0 || len(raw.List) == 0) {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "list.html", data)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// More 专题详情页 SSR:专题由路由 slug 标识;历史 /more 地址会跳转到 /topic canonical。
|
||||
func More(c *gin.Context) {
|
||||
q := playletser.ParseRecommendQuery(routeOrQuery(c, "page", "page"))
|
||||
if slug := strings.TrimSpace(c.Param("slug")); slug != "" {
|
||||
if id := playletser.ParseDetailID(slug); id > 0 {
|
||||
q.ThemeID = id
|
||||
} else {
|
||||
q.ThemeSlug = slug
|
||||
}
|
||||
}
|
||||
if path := c.Request.URL.Path; path == "/more" || strings.HasPrefix(path, "/more/") {
|
||||
redirectCanonical(c, playletser.RecommendPagePath(q))
|
||||
return
|
||||
}
|
||||
themes := playletser.FetchThemesOrEmpty(c, 2)
|
||||
themeName := ""
|
||||
for _, theme := range themes {
|
||||
matchesID := q.ThemeID > 0 && theme.ID == q.ThemeID
|
||||
matchesSlug := strings.TrimSpace(q.ThemeSlug) != "" && strings.EqualFold(strings.TrimSpace(theme.Slug), strings.TrimSpace(q.ThemeSlug))
|
||||
if !matchesID && !matchesSlug {
|
||||
continue
|
||||
}
|
||||
themeName = strings.TrimSpace(theme.Name)
|
||||
if q.ThemeID <= 0 {
|
||||
q.ThemeID = theme.ID
|
||||
}
|
||||
if strings.TrimSpace(theme.Slug) != "" {
|
||||
q.ThemeSlug = strings.TrimSpace(theme.Slug)
|
||||
}
|
||||
break
|
||||
}
|
||||
if redirectCanonical(c, playletser.RecommendPagePath(q)) {
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchRecommendListOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
hasIdentifier := q.ThemeID > 0 || strings.TrimSpace(q.ThemeSlug) != ""
|
||||
if emptyScopedPage(hasIdentifier, raw.TotalCount, len(raw.List)) {
|
||||
NotFound(c, "专题不存在")
|
||||
return
|
||||
}
|
||||
title := playletser.RecommendThemeTitle(themeName, raw.SeoInfo.Title, q.ThemeID)
|
||||
if queryTitle := strings.TrimSpace(c.Query("title")); queryTitle != "" {
|
||||
title = queryTitle
|
||||
}
|
||||
heading := title
|
||||
if !strings.HasSuffix(heading, "介绍") {
|
||||
heading += "介绍"
|
||||
}
|
||||
description := strings.TrimSpace(raw.SeoInfo.Description)
|
||||
if description == "" {
|
||||
description = "精品高分 AI 成人短剧专题,精选高人气、高评分 AI 原创短剧,每日持续更新。涵盖多种热门剧情与角色设定,为你带来高质量、高清流畅的观影体验。"
|
||||
}
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"pageKey": "topic",
|
||||
"pageName": title,
|
||||
"pageTitle": title,
|
||||
"topicHeading": heading,
|
||||
"topicDescription": description,
|
||||
"pagePath": playletser.RecommendCanonicalPath(q.ThemeID, q.ThemeSlug),
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"list": raw.List,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildRecommendPagination(q, raw.TotalCount),
|
||||
}
|
||||
if raw.TotalCount <= 0 || len(raw.List) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "more.html", data)
|
||||
}
|
||||
@@ -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,27 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// NotFound 统一 404 页(HTTP 404)。
|
||||
func NotFound(c *gin.Context, title string) {
|
||||
if title == "" {
|
||||
title = "页面未找到"
|
||||
}
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"path": c.Request.URL.RequestURI(),
|
||||
}
|
||||
setNoIndex(data, true)
|
||||
HTML(c, http.StatusNotFound, "404.html", data)
|
||||
}
|
||||
|
||||
// emptyScopedPage 判断带明确资源标识的列表详情是否不存在。
|
||||
// 聚合页允许暂时为空;标签、分类、专题等详情页成功返回空结果时应响应 404,
|
||||
// 避免用空模板和 HTTP 200 形成搜索引擎软 404。
|
||||
func emptyScopedPage(hasIdentifier bool, totalCount int64, itemCount int) bool {
|
||||
return hasIdentifier && totalCount <= 0 && itemCount == 0
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package web
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEmptyScopedPage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hasIdentifier bool
|
||||
totalCount int64
|
||||
itemCount int
|
||||
want bool
|
||||
}{
|
||||
{name: "missing detail", hasIdentifier: true, want: true},
|
||||
{name: "empty aggregate page", hasIdentifier: false, want: false},
|
||||
{name: "has total", hasIdentifier: true, totalCount: 1, want: false},
|
||||
{name: "has item", hasIdentifier: true, itemCount: 1, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := emptyScopedPage(tt.hasIdentifier, tt.totalCount, tt.itemCount); got != tt.want {
|
||||
t.Fatalf("emptyScopedPage(%v, %d, %d) = %v, want %v", tt.hasIdentifier, tt.totalCount, tt.itemCount, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Play 播放页 SSR:GET /api/app/playlet/chapter/detail;seoInfo 直接注入模板。
|
||||
func Play(c *gin.Context) {
|
||||
playletKey := strings.TrimSpace(c.Param("slug"))
|
||||
requestedChapterID := strings.TrimSpace(c.Param("chapterId"))
|
||||
if playletKey == "" {
|
||||
NotFound(c, "未找到可播放内容")
|
||||
return
|
||||
}
|
||||
if requestedChapterID != "" && playletser.ParseDetailID(requestedChapterID) <= 0 {
|
||||
NotFound(c, "未找到该剧集")
|
||||
return
|
||||
}
|
||||
playlet := playletser.FetchDetailOrNilByKey(c, playletKey)
|
||||
if playlet == nil {
|
||||
NotFound(c, "未找到可播放内容")
|
||||
return
|
||||
}
|
||||
q := playletser.ParsePlayQuery(playletKey, c.Param("chapterId"))
|
||||
q.PlayletID = playlet.ID
|
||||
q.PlayletSlug = playlet.Slug
|
||||
|
||||
q.ChapterID = playletser.ResolvePlayChapterID(playlet, q.ChapterID)
|
||||
if q.ChapterID <= 0 {
|
||||
NotFound(c, "未找到可播放内容")
|
||||
return
|
||||
}
|
||||
target := playletser.PlayPath(q.PlayletSlug, q.PlayletID)
|
||||
if q.ChapterID > 0 {
|
||||
target = playletser.ChapterPlayPath(q.PlayletSlug, q.PlayletID, q.ChapterID)
|
||||
}
|
||||
if redirectCanonical(c, target) {
|
||||
return
|
||||
}
|
||||
|
||||
chapter := playletser.FetchChapterDetailOrNil(c, q.ChapterID, q.PlayletID, q.PlayletSlug)
|
||||
if chapter == nil {
|
||||
NotFound(c, "未找到该剧集")
|
||||
return
|
||||
}
|
||||
|
||||
var episodeRanges []playletser.EpisodeRange
|
||||
var chapters []playletser.PlayletChapterData
|
||||
playletID := playlet.ID
|
||||
playletSlug := playlet.Slug
|
||||
chapters = playlet.PlayletChapterInfo
|
||||
episodeRanges = playletser.BuildEpisodeRanges(chapters, playlet.TotalEpisode, 30)
|
||||
|
||||
if chapter != nil && useragent.TokenFromRequest(c.Request) != "" {
|
||||
authser.SetRechargeCookie(c, chapter.Balance)
|
||||
}
|
||||
|
||||
seoInfo := playletser.ResolvePlayPageSEO(chapter, playlet)
|
||||
|
||||
data := gin.H{
|
||||
"title": playletser.ResolvePlayPageTitle(chapter, playlet),
|
||||
"chapter": chapter,
|
||||
"playlet": playlet,
|
||||
"playletID": playletID,
|
||||
"playletSlug": playletSlug,
|
||||
"seoInfo": seoInfo,
|
||||
"chapters": chapters,
|
||||
"episodeRanges": episodeRanges,
|
||||
}
|
||||
HTMLOK(c, "play.html", data)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChapterList 代理上游短剧选集列表接口。
|
||||
func ChapterList(c *gin.Context) {
|
||||
playletID, _ := strconv.ParseInt(c.Query("playletId"), 10, 64)
|
||||
playletSlug := strings.TrimSpace(c.Query("playletSlug"))
|
||||
if playletSlug == "" && playletID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "缺少 playletSlug 或 playletId", "data": nil})
|
||||
return
|
||||
}
|
||||
data, err := playletser.FetchChapterList(c, playletID, playletSlug)
|
||||
if err != nil {
|
||||
log.Error("chapter list upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "chapter list failed", "tip": "获取选集失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success", "tip": "成功", "data": data})
|
||||
}
|
||||
|
||||
// OperateList 代理上游运营位短剧列表接口。
|
||||
func OperateList(c *gin.Context) {
|
||||
q := playletser.OperateListQuery{
|
||||
OperateType: parsePositiveInt(c.Query("operateType")),
|
||||
FilterType: parsePositiveInt(c.Query("filterType")),
|
||||
PageNum: parsePositiveInt(c.Query("pageNum")),
|
||||
PageSize: parsePositiveInt(c.Query("pageSize")),
|
||||
}
|
||||
q.Normalize()
|
||||
data, err := playletser.FetchOperateList(c, q)
|
||||
if err != nil {
|
||||
log.Error("operate list upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "operate list failed", "tip": "获取运营短剧失败,请稍后重试", "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success", "tip": "成功", "data": data})
|
||||
}
|
||||
|
||||
func parsePositiveInt(raw string) int {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil || n <= 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type playletForwardHeatBody struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// PlayletForwardHeat 代理上游 POST /api/app/playlet/forward/heat。
|
||||
func PlayletForwardHeat(c *gin.Context) {
|
||||
var body playletForwardHeatBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.ForwardHeat(c, playletser.PlayletForwardHeatReq{ID: body.ID})
|
||||
if err != nil {
|
||||
log.Error("playlet forward heat upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet forward heat failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": env.Msg,
|
||||
"tip": firstNonEmpty(env.Tip, env.Msg, "success"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type playletLikeBody struct {
|
||||
ID int64 `json:"id"`
|
||||
IsLike *bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// PlayletLikeSet 代理上游 POST /api/app/playlet/like/set。
|
||||
func PlayletLikeSet(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再点赞",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body playletLikeBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsLike == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 id 或 isLike",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{
|
||||
ID: body.ID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("playlet like set upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.LikeSet(c, playletser.PlayletLikeSetReq{
|
||||
ID: body.ID,
|
||||
IsLike: *body.IsLike,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("playlet like set retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet like failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
|
||||
type playletCollectBody struct {
|
||||
ID int64 `json:"id"`
|
||||
IsCollect *bool `json:"isCollect"`
|
||||
}
|
||||
|
||||
// PlayletCollectSet 代理上游 POST /api/app/playlet/collect/set。
|
||||
func PlayletCollectSet(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 401,
|
||||
"msg": "unauthorized",
|
||||
"tip": "请先登录后再收藏",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body playletCollectBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "参数错误",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if body.ID <= 0 || body.IsCollect == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 1001,
|
||||
"msg": "参数错误",
|
||||
"tip": "缺少 id 或 isCollect",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{
|
||||
ID: body.ID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("playlet collect set upstream failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet collect failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if env != nil && TryJSONSessionRecover(c, env.Code) {
|
||||
env, err = playletser.CollectSet(c, playletser.PlayletCollectSetReq{
|
||||
ID: body.ID,
|
||||
IsCollect: *body.IsCollect,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("playlet collect set retry failed", log.E(err))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 500,
|
||||
"msg": "playlet collect failed",
|
||||
"tip": "操作失败,请稍后重试",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if env == nil || env.Code != 200 {
|
||||
tip := "操作失败"
|
||||
msg := ""
|
||||
code := 500
|
||||
if env != nil {
|
||||
code = env.Code
|
||||
msg = env.Msg
|
||||
tip = firstNonEmpty(env.Tip, env.Msg, tip)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": firstNonEmpty(env.Msg, "success"),
|
||||
"tip": firstNonEmpty(env.Tip, "成功"),
|
||||
"data": env.Data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Rank 排行榜页 SSR:GET /api/app/playlet/rank,列表用接口字段渲染。
|
||||
func Rank(c *gin.Context) {
|
||||
sortOrder := strings.TrimSpace(c.Param("sortOrder"))
|
||||
if sortOrder != "" {
|
||||
value, err := strconv.Atoi(sortOrder)
|
||||
if err != nil || value < 1 || value > 3 {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
if sortOrder == "" {
|
||||
sortOrder = strings.TrimSpace(c.Query("sortOrder"))
|
||||
}
|
||||
page := strings.TrimSpace(c.Param("page"))
|
||||
if page != "" {
|
||||
value, err := strconv.Atoi(page)
|
||||
if err != nil || value <= 0 {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
}
|
||||
if page == "" {
|
||||
page = strings.TrimSpace(c.Query("page"))
|
||||
}
|
||||
q := playletser.ParseRankQuery(page, sortOrder)
|
||||
if c.Query("sortOrder") != "" || c.Query("page") != "" {
|
||||
target := "/rank"
|
||||
if q.SortOrder != 1 {
|
||||
target += "/" + strconv.Itoa(q.SortOrder)
|
||||
}
|
||||
if q.PageNum > 1 {
|
||||
target += "/page/" + strconv.Itoa(q.PageNum)
|
||||
}
|
||||
target += "/"
|
||||
c.Redirect(http.StatusMovedPermanently, target)
|
||||
return
|
||||
}
|
||||
if redirectCanonical(c, playletser.RankCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchRankOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
tabs := playletser.BuildRankTabs(q)
|
||||
seoSelections := make([]playletser.SEOSelection, 0, 1)
|
||||
for _, tab := range tabs {
|
||||
if tab.Active {
|
||||
seoSelections = append(seoSelections, playletser.SEOSelection{Dimension: "排行榜类型", Value: tab.Label})
|
||||
break
|
||||
}
|
||||
}
|
||||
data := gin.H{
|
||||
"title": "排行榜",
|
||||
"description": "黄果短剧排行榜实时更新热播短剧,查看热门榜、飙升榜,不错过高口碑必看剧集。",
|
||||
"navActive": "rank",
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"seoSelections": seoSelections,
|
||||
"tabs": tabs,
|
||||
"list": raw.List,
|
||||
"rankOffset": playletser.RankOffset(q),
|
||||
"sortOrder": q.SortOrder,
|
||||
"isBinge": playletser.IsRankBinge(q.SortOrder),
|
||||
"pagination": playletser.BuildRankPagination(q, raw.TotalCount),
|
||||
}
|
||||
if raw.TotalCount <= 0 || len(raw.List) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "rank.html", data)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
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"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HTML 渲染页面并注入 footer / 导航 / 公共配置 / 登录态(以 cookie 为准)。
|
||||
func HTML(c *gin.Context, code int, name string, data gin.H) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
if _, ok := data["footer"]; !ok {
|
||||
footer := configser.FetchFooterOrEmpty(c)
|
||||
data["footer"] = footer
|
||||
}
|
||||
headerNav, ok := data["headerNav"].([]translationser.HeaderNavItemVM)
|
||||
if !ok {
|
||||
headerNav = translationser.FetchHeaderNavOrDefault(c)
|
||||
data["headerNav"] = headerNav
|
||||
}
|
||||
activeKey, _ := data["navActive"].(string)
|
||||
resolvedNavActive := translationser.ResolveNavigationActiveKey(activeKey, c.Request.URL.Path)
|
||||
headerNav = translationser.MarkHeaderNavActive(headerNav, c.Request.URL.Path)
|
||||
data["headerNav"] = headerNav
|
||||
data["resolvedNavActive"] = resolvedNavActive
|
||||
data["pageConsoleData"] = buildPageConsoleData(c.Request.URL.RequestURI(), code, name, data)
|
||||
seoInfo := pageSEOFromData(data["seoInfo"])
|
||||
seoChanged := false
|
||||
if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, c.Request.URL.Path); found {
|
||||
seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{
|
||||
Title: navSEO.Title,
|
||||
Description: navSEO.Description,
|
||||
Keywords: navSEO.Keyworld,
|
||||
}, seoInfo)
|
||||
seoChanged = true
|
||||
}
|
||||
if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 {
|
||||
seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections)
|
||||
seoChanged = true
|
||||
}
|
||||
if pageNum := paginationPage(data["pagination"]); pageNum > 1 {
|
||||
pageTitle, _ := data["title"].(string)
|
||||
pageTitle = playletser.PaginatedTitle(pageTitle, pageNum)
|
||||
if pageTitle != "" {
|
||||
data["title"] = pageTitle
|
||||
}
|
||||
seoInfo = playletser.PaginateSeoInfo(seoInfo, paginationDescription(data, pageTitle), pageNum)
|
||||
seoChanged = true
|
||||
}
|
||||
if seoChanged {
|
||||
data["seoInfo"] = seoInfo
|
||||
if seoInfo.Title != "" {
|
||||
data["title"] = seoInfo.Title
|
||||
}
|
||||
}
|
||||
if _, ok := data["appConfig"]; !ok {
|
||||
data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c)
|
||||
}
|
||||
cfg, ok := data["appConfig"].(configser.AppConfigVM)
|
||||
if !ok {
|
||||
cfg = configser.FetchAppConfigVMOrEmpty(c)
|
||||
}
|
||||
cfg = configser.ApplyTrackerFallback(cfg)
|
||||
data["appConfig"] = cfg
|
||||
siteURL := seoser.ResolveSiteURL(c, cfg)
|
||||
data["foreverUrlAddr"] = siteURL
|
||||
data["siteURL"] = siteURL
|
||||
attachPaginationLinks(data, siteURL)
|
||||
canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c))
|
||||
canonicalURL = seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data))
|
||||
data["canonicalURL"] = canonicalURL
|
||||
data["seoVerify"] = seoser.VerifyFromConfig()
|
||||
attachTrackerPage(c, code, data)
|
||||
noindex := pageNoIndex(data)
|
||||
if noindex {
|
||||
data["noindex"] = true
|
||||
}
|
||||
if code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(c.Request.URL.Path) {
|
||||
data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
|
||||
}
|
||||
|
||||
// 无登录 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。
|
||||
authser.EnsureGuestOnEnter(c)
|
||||
|
||||
loggedIn, userName, avatar, email := authser.AuthFromCookies(c)
|
||||
registered := authser.IsRegisteredFromCookies(c)
|
||||
data["isLoggedIn"] = loggedIn
|
||||
data["isRegistered"] = registered
|
||||
if _, ok := data["isVip"]; !ok {
|
||||
data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c)
|
||||
}
|
||||
isVip, _ := data["isVip"].(bool)
|
||||
data["isVip"] = isVip
|
||||
if loggedIn {
|
||||
if userName != "" {
|
||||
data["userName"] = userName
|
||||
}
|
||||
if 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)
|
||||
}
|
||||
}
|
||||
// 允许页面 handler 先注入最新钱包数据(如 getInfo),避免被 cookie 覆盖。
|
||||
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)
|
||||
}
|
||||
vipType := toInt(data["vipType"])
|
||||
data["vipType"] = vipType
|
||||
if _, ok := data["showCardName"]; !ok {
|
||||
data["showCardName"] = authser.ShowCardNameFromCookies(c)
|
||||
}
|
||||
showCardName, _ := data["showCardName"].(string)
|
||||
data["showCardName"] = strings.TrimSpace(showCardName)
|
||||
if _, ok := data["showCardId"]; !ok {
|
||||
data["showCardId"] = authser.ShowCardIDFromCookies(c)
|
||||
}
|
||||
if isVip {
|
||||
if _, ok := data["vipLabel"]; !ok {
|
||||
data["vipLabel"] = authser.VipDisplayLabel(showCardName, vipType)
|
||||
}
|
||||
if _, ok := data["vipExpireText"]; !ok {
|
||||
data["vipExpireText"] = authser.VipExpireTextFromCookies(c)
|
||||
}
|
||||
} else {
|
||||
delete(data, "vipLabel")
|
||||
delete(data, "vipExpireText")
|
||||
}
|
||||
} else {
|
||||
delete(data, "userName")
|
||||
delete(data, "avatar")
|
||||
delete(data, "email")
|
||||
delete(data, "userId")
|
||||
delete(data, "recharge")
|
||||
delete(data, "rechargeText")
|
||||
delete(data, "vipType")
|
||||
delete(data, "showCardId")
|
||||
delete(data, "showCardName")
|
||||
delete(data, "vipLabel")
|
||||
delete(data, "vipExpireText")
|
||||
}
|
||||
|
||||
c.HTML(code, name, data)
|
||||
}
|
||||
|
||||
// buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。
|
||||
// 快照在公共配置和 cookie 登录态注入前生成,避免把会话相关公共字段写入日志。
|
||||
func buildPageConsoleData(path string, code int, templateName string, data gin.H) gin.H {
|
||||
pageData := make(gin.H, len(data))
|
||||
for key, value := range data {
|
||||
if pageConsoleExcludedKey(key) {
|
||||
continue
|
||||
}
|
||||
pageData[key] = value
|
||||
}
|
||||
payload := gin.H{
|
||||
"path": path,
|
||||
"status": code,
|
||||
"template": templateName,
|
||||
"data": pageData,
|
||||
}
|
||||
if headerNav, ok := data["headerNav"]; ok {
|
||||
payload["headerNav"] = headerNav
|
||||
}
|
||||
if footer, ok := data["footer"]; ok {
|
||||
payload["footer"] = footer
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func pageConsoleExcludedKey(key string) bool {
|
||||
switch key {
|
||||
case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify",
|
||||
"isLoggedIn", "isRegistered", "userName", "avatar", "email", "userId":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func pageSEOFromData(value any) playletser.SeoInfo {
|
||||
switch seo := value.(type) {
|
||||
case playletser.SeoInfo:
|
||||
return seo
|
||||
case *playletser.SeoInfo:
|
||||
if seo != nil {
|
||||
return *seo
|
||||
}
|
||||
}
|
||||
return playletser.SeoInfo{}
|
||||
}
|
||||
|
||||
func paginationPage(value any) int {
|
||||
switch pagination := value.(type) {
|
||||
case playletser.PaginationVM:
|
||||
return pagination.Page
|
||||
case *playletser.PaginationVM:
|
||||
if pagination != nil {
|
||||
return pagination.Page
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func paginationDescription(data gin.H, pageTitle string) string {
|
||||
for _, key := range []string{"description", "categoryDescription", "tagDescription", "topicDescription"} {
|
||||
if value, _ := data[key].(string); strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
pageTitle = strings.TrimSpace(pageTitle)
|
||||
if pageTitle == "" {
|
||||
return "浏览本页内容与站内相关推荐。"
|
||||
}
|
||||
return "浏览" + pageTitle + "的本页内容与站内相关推荐。"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLOK 200 快捷方法。
|
||||
func HTMLOK(c *gin.Context, name string, data gin.H) {
|
||||
HTML(c, http.StatusOK, name, data)
|
||||
}
|
||||
|
||||
// setNoIndex 统一设置页面级索引策略,并让公共渲染层同步关闭 JSON-LD。
|
||||
func setNoIndex(data gin.H, follow bool) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
data["noindex"] = true
|
||||
if follow {
|
||||
data["robots"] = "noindex,follow"
|
||||
return
|
||||
}
|
||||
data["robots"] = "noindex,nofollow"
|
||||
}
|
||||
|
||||
func pageNoIndex(data gin.H) bool {
|
||||
if noindex, _ := data["noindex"].(bool); noindex {
|
||||
return true
|
||||
}
|
||||
robots, _ := data["robots"].(string)
|
||||
for _, directive := range strings.Split(strings.ToLower(robots), ",") {
|
||||
if strings.TrimSpace(directive) == "noindex" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func toInt(v any) int {
|
||||
return int(toInt64(v))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestBuildPageConsoleDataFiltersCommonSessionFields(t *testing.T) {
|
||||
payload := buildPageConsoleData("/detail/example", http.StatusOK, "detail.html", gin.H{
|
||||
"title": "示例短剧",
|
||||
"detail": gin.H{"id": 12, "slug": "example"},
|
||||
"appConfig": "public config",
|
||||
"headerNav": "navigation",
|
||||
"footer": "footer data",
|
||||
"isLoggedIn": true,
|
||||
"email": "viewer@example.com",
|
||||
"pageConsoleData": "old payload",
|
||||
})
|
||||
|
||||
if payload["path"] != "/detail/example" || payload["status"] != http.StatusOK || payload["template"] != "detail.html" {
|
||||
t.Fatalf("unexpected page console metadata: %#v", payload)
|
||||
}
|
||||
pageData, ok := payload["data"].(gin.H)
|
||||
if !ok {
|
||||
t.Fatalf("payload data type = %T, want gin.H", payload["data"])
|
||||
}
|
||||
if _, ok := pageData["detail"]; !ok {
|
||||
t.Fatalf("page response data was removed: %#v", pageData)
|
||||
}
|
||||
if payload["headerNav"] != "navigation" || payload["footer"] != "footer data" {
|
||||
t.Fatalf("common navigation/footer data missing: %#v", payload)
|
||||
}
|
||||
for _, key := range []string{"appConfig", "headerNav", "isLoggedIn", "email", "pageConsoleData"} {
|
||||
if _, ok := pageData[key]; ok {
|
||||
t.Fatalf("excluded key %q remains in console data: %#v", key, pageData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoIndexAndPageNoIndex(t *testing.T) {
|
||||
data := gin.H{}
|
||||
setNoIndex(data, false)
|
||||
if data["robots"] != "noindex,nofollow" || !pageNoIndex(data) {
|
||||
t.Fatalf("noindex policy was not applied: %#v", data)
|
||||
}
|
||||
|
||||
if pageNoIndex(gin.H{"robots": "index,follow"}) {
|
||||
t.Fatal("indexable robots value was treated as noindex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRichtextIndexPolicy(t *testing.T) {
|
||||
publicData := gin.H{}
|
||||
applyRichtextIndexPolicy(publicData, "关于我们")
|
||||
if pageNoIndex(publicData) {
|
||||
t.Fatal("about page should remain indexable")
|
||||
}
|
||||
|
||||
legalData := gin.H{}
|
||||
applyRichtextIndexPolicy(legalData, "隐私政策")
|
||||
if !pageNoIndex(legalData) || legalData["robots"] != "noindex,follow" {
|
||||
t.Fatalf("privacy page policy = %#v, want noindex,follow", legalData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/seoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Richtext 富文本页 SSR:GET /api/app/config/richtext/list?titles=
|
||||
func Richtext(c *gin.Context) {
|
||||
title := routeOrQuery(c, "title", "title")
|
||||
target := "/richtext"
|
||||
if title != "" {
|
||||
target += "/" + url.PathEscape(title)
|
||||
}
|
||||
target += "/"
|
||||
if redirectCanonical(c, target) {
|
||||
return
|
||||
}
|
||||
renderRichtext(c, title)
|
||||
}
|
||||
|
||||
func RichtextPage(title string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page := configser.FetchRichTextByTitle(c, title)
|
||||
if !page.Found {
|
||||
fallbackTemplates := map[string]string{
|
||||
"关于我们": "aboutUs.html",
|
||||
"用户协议": "userAgreement.html",
|
||||
"隐私政策": "privacyPolicy.html",
|
||||
"帮助中心": "helpCenter.html",
|
||||
}
|
||||
if fallbackTemplate := fallbackTemplates[title]; fallbackTemplate != "" {
|
||||
data := gin.H{"title": title}
|
||||
applyRichtextIndexPolicy(data, title)
|
||||
if title == "帮助中心" {
|
||||
data["faqItems"] = []seoser.FAQItem{
|
||||
{Question: "无法登录账号怎么办?", Answer: "请确认邮箱与密码是否正确;若忘记密码,可在登录页面使用“找回密码”。"},
|
||||
{Question: "观看记录没有同步?", Answer: "请确认已登录同一账号并保持网络连接,记录通常会自动同步。"},
|
||||
{Question: "视频无法播放或加载缓慢?", Answer: "建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。"},
|
||||
{Question: "如何管理收藏内容?", Answer: "进入个人中心的“追剧与收藏”页面,可查看或取消收藏。"},
|
||||
}
|
||||
}
|
||||
HTMLOK(c, fallbackTemplate, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
renderRichtextData(c, title, page)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRichtext(c *gin.Context, title string) {
|
||||
title = strings.TrimSpace(title)
|
||||
page := configser.FetchRichTextByTitle(c, title)
|
||||
renderRichtextData(c, title, page)
|
||||
}
|
||||
|
||||
func renderRichtextData(c *gin.Context, title string, page configser.RichTextPageVM) {
|
||||
if !page.Found {
|
||||
NotFound(c, "未找到该内容")
|
||||
return
|
||||
}
|
||||
|
||||
displayTitle := page.Title
|
||||
if displayTitle == "" {
|
||||
displayTitle = "内容详情"
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"title": displayTitle,
|
||||
"richtext": page,
|
||||
"richtextKey": title,
|
||||
}
|
||||
applyRichtextIndexPolicy(data, title)
|
||||
HTMLOK(c, "richtext.html", data)
|
||||
}
|
||||
|
||||
func applyRichtextIndexPolicy(data gin.H, title string) {
|
||||
switch strings.TrimSpace(title) {
|
||||
case "关于我们", "帮助中心":
|
||||
return
|
||||
default:
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func routeOrQuery(c *gin.Context, routeName, queryName string) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if value := strings.TrimSpace(c.Param(routeName)); value != "" {
|
||||
return value
|
||||
}
|
||||
return strings.TrimSpace(c.Query(queryName))
|
||||
}
|
||||
|
||||
func hasQuery(c *gin.Context, names ...string) bool {
|
||||
if c == nil || c.Request == nil || c.Request.URL == nil {
|
||||
return false
|
||||
}
|
||||
values := c.Request.URL.Query()
|
||||
for _, name := range names {
|
||||
if _, ok := values[name]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectCanonical(c *gin.Context, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
if c == nil || target == "" || c.Request == nil || c.Request.URL == nil {
|
||||
return false
|
||||
}
|
||||
if c.Request.URL.RequestURI() == target {
|
||||
return false
|
||||
}
|
||||
c.Redirect(http.StatusMovedPermanently, target)
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Search 搜索页 SSR:GET /api/app/playlet/search,列表直接用接口字段渲染。
|
||||
func Search(c *gin.Context) {
|
||||
q := playletser.ParseSearchQuery(routeOrQuery(c, "keyword", "q"), routeOrQuery(c, "page", "page"))
|
||||
if redirectCanonical(c, playletser.SearchCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
var raw *playletser.PlayletVideoResp
|
||||
if q.Keyword != "" {
|
||||
raw = playletser.FetchSearchOrEmpty(c, q)
|
||||
} else {
|
||||
raw = &playletser.PlayletVideoResp{PageNum: q.PageNum, PageSize: q.PageSize}
|
||||
}
|
||||
emptyTitle := "请输入关键词搜索"
|
||||
emptyDesc := "在上方输入剧名、题材或标签开始搜索"
|
||||
if q.Keyword != "" {
|
||||
emptyTitle = "未找到相关短剧"
|
||||
emptyDesc = "换个关键词试试,或返回首页看看热门内容"
|
||||
}
|
||||
pageTitle := "搜索短剧"
|
||||
description := "在黄果短剧搜索短剧名称、标签与关键词,快速找到想看的剧集。"
|
||||
if q.Keyword != "" {
|
||||
pageTitle = "搜索:" + q.Keyword
|
||||
description = "在黄果短剧搜索「" + q.Keyword + "」相关短剧结果,快速找到想看的剧集。"
|
||||
}
|
||||
data := gin.H{
|
||||
"title": pageTitle,
|
||||
"description": description,
|
||||
"query": q.Keyword,
|
||||
"list": raw.List,
|
||||
"totalCount": raw.TotalCount,
|
||||
"pagination": playletser.BuildSearchPagination(q, raw.TotalCount),
|
||||
"emptyTitle": emptyTitle,
|
||||
"emptyDesc": emptyDesc,
|
||||
}
|
||||
setNoIndex(data, true)
|
||||
HTMLOK(c, "search.html", data)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/seoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RobotsTXT 动态 robots.txt(含 Sitemap 声明)。
|
||||
func RobotsTXT(c *gin.Context) {
|
||||
cfg := configser.FetchAppConfigVMOrEmpty(c)
|
||||
foreverURL := seoser.ResolveSiteURL(c, cfg)
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.Header("Cache-Control", "public, max-age=3600")
|
||||
c.String(http.StatusOK, seoser.BuildRobotsTXT(foreverURL))
|
||||
}
|
||||
|
||||
// SEOVerifyFile 提供百度 HTML 验证文件和 IndexNow key.txt。
|
||||
func SEOVerifyFile(c *gin.Context) {
|
||||
codes := seoser.VerifyFromConfig()
|
||||
name := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
|
||||
switch {
|
||||
case codes.BaiduHTML != "" && name == codes.BaiduHTML:
|
||||
c.Header("Cache-Control", "public, max-age=86400")
|
||||
c.String(http.StatusOK, codes.Baidu)
|
||||
return
|
||||
case codes.IndexNowKey != "" && name == codes.IndexNowKey+".txt":
|
||||
c.Header("Cache-Control", "public, max-age=86400")
|
||||
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(codes.IndexNowKey))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Series AI 成人剧集 SSR:Swagger operate/list 固定 operateType=2,筛选映射 filterType=1/2/3。
|
||||
func Series(c *gin.Context) {
|
||||
q, ok := playletser.ParseSeriesQuery(c.Param("filter"), routeOrQuery(c, "page", "page"))
|
||||
if !ok {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
if redirectCanonical(c, playletser.SeriesCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchOperateListOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
|
||||
sectionTitle := playletser.SeriesSectionTitle(q.FilterType)
|
||||
data := gin.H{
|
||||
"title": "AI成人剧集",
|
||||
"description": "黄果短剧精选AI成人剧集,持续更新热门漫剧、独家原创剧集,支持免费在线观看。",
|
||||
"pageKey": "series",
|
||||
"pageName": "AI成人剧集",
|
||||
"pagePath": playletser.SeriesCanonicalPath(q),
|
||||
"navActive": "aiSeries",
|
||||
"filters": playletser.BuildSeriesFilterTabs(q),
|
||||
"sectionTitle": sectionTitle,
|
||||
"seoSelections": []playletser.SEOSelection{{Dimension: "筛选", Value: strings.TrimSuffix(sectionTitle, "的AI成人剧集")}},
|
||||
"list": raw.List,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildSeriesPagination(q, raw.TotalCount),
|
||||
}
|
||||
// 剧集入口页(/series/、/series/popular/、/series/original/)始终可索引;空结果仅对分页打 noindex。
|
||||
if q.PageNum > 1 && (raw.TotalCount <= 0 || len(raw.List) == 0) {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "series.html", data)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/authser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AbortSSRIfSessionExpired 处理 SSR 场景下的会话失效码。
|
||||
// aborted=true:已跳转登录,调用方应立刻 return;
|
||||
// retry=true:游客已重新登录,调用方应重试上游请求。
|
||||
func AbortSSRIfSessionExpired(c *gin.Context, code int) (aborted, retry bool) {
|
||||
if !authser.IsSessionExpiredCode(code) {
|
||||
return false, false
|
||||
}
|
||||
retry, needLogin := authser.OnSessionExpired(c, code)
|
||||
if needLogin {
|
||||
RedirectToLogin(c)
|
||||
return true, false
|
||||
}
|
||||
return false, retry
|
||||
}
|
||||
|
||||
// TryJSONSessionRecover JSON 接口遇到会话失效时:游客重登后返回 true(应重试);
|
||||
// 注册账号清 cookie 后返回 false,调用方继续把 code=1000 交给前端弹登录。
|
||||
func TryJSONSessionRecover(c *gin.Context, code int) bool {
|
||||
if !authser.IsSessionExpiredCode(code) {
|
||||
return false
|
||||
}
|
||||
retry, _ := authser.OnSessionExpired(c, code)
|
||||
return retry
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Setting 账号设置页:SSR 拉取 /api/app/auth/getInfo。
|
||||
func Setting(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
env, user, err := authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("setting getInfo failed", log.E(err))
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
if env != nil && authser.IsSessionExpiredCode(env.Code) {
|
||||
retry, needLogin := authser.OnSessionExpired(c, env.Code)
|
||||
if needLogin || !retry {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
env, user, err = authser.GetInfo(c)
|
||||
if err != nil || env == nil || env.Code != 200 || user == nil {
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
} else if env == nil || env.Code != 200 || user == nil {
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
vm := authser.BuildSettingVM(user)
|
||||
data := gin.H{
|
||||
"title": "账号设置",
|
||||
"setting": vm,
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
HTMLOK(c, "setting.html", data)
|
||||
}
|
||||
|
||||
// EditProfile 编辑资料页:SSR 拉取 getInfo,头像/昵称等回填。
|
||||
func EditProfile(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
env, user, err := authser.GetInfo(c)
|
||||
if err != nil {
|
||||
log.Error("edit-profile getInfo failed", log.E(err))
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
if env != nil && authser.IsSessionExpiredCode(env.Code) {
|
||||
retry, needLogin := authser.OnSessionExpired(c, env.Code)
|
||||
if needLogin || !retry {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
env, user, err = authser.GetInfo(c)
|
||||
if err != nil || env == nil || env.Code != 200 || user == nil {
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
} else if env == nil || env.Code != 200 || user == nil {
|
||||
authser.ClearAuthCookies(c)
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
authser.SetAuthCookies(c, user)
|
||||
vm := authser.BuildSettingVM(user)
|
||||
data := gin.H{
|
||||
"title": "编辑个人资料",
|
||||
"profile": vm,
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
HTMLOK(c, "editProfile.html", data)
|
||||
}
|
||||
|
||||
// EditPassword 修改密码页:需登录。
|
||||
func EditPassword(c *gin.Context) {
|
||||
if useragent.TokenFromRequest(c.Request) == "" {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
data := gin.H{
|
||||
"title": "修改登录密码",
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
HTMLOK(c, "editPassword.html", data)
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TagList(c *gin.Context) {
|
||||
q := playletser.ParseTagListQuery(routeOrQuery(c, "page", "page"))
|
||||
if slug := strings.TrimSpace(c.Param("slug")); slug != "" {
|
||||
if id := playletser.ParseDetailID(slug); id > 0 {
|
||||
q.TagID = id
|
||||
} else {
|
||||
q.TagSlug = slug
|
||||
}
|
||||
}
|
||||
if path := c.Request.URL.Path; path == "/tag-list" || strings.HasPrefix(path, "/tag-list/") {
|
||||
redirectCanonical(c, playletser.TagListCanonicalPath(q))
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchTagListOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
hasIdentifier := q.TagID > 0 || strings.TrimSpace(q.TagSlug) != ""
|
||||
if emptyScopedPage(hasIdentifier, raw.TotalCount, len(raw.List)) {
|
||||
NotFound(c, "标签不存在")
|
||||
return
|
||||
}
|
||||
if q.TagID > 0 {
|
||||
q.TagSlug = playletser.FindTagSlug(raw.List, q.TagID)
|
||||
}
|
||||
if redirectCanonical(c, playletser.TagListCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
|
||||
pageName := playletser.FindTagNameBySlug(raw.List, q.TagSlug)
|
||||
if pageName == "" {
|
||||
pageName = playletser.FindTagName(raw.List, q.TagID)
|
||||
}
|
||||
if pageName == "" {
|
||||
if name := strings.TrimSpace(raw.SeoInfo.Title); name != "" {
|
||||
pageName = name
|
||||
} else {
|
||||
pageName = "标签"
|
||||
}
|
||||
}
|
||||
description := strings.TrimSpace(raw.SeoInfo.Description)
|
||||
if description == "" {
|
||||
description = "精选" + pageName + "题材相关AI成人短剧与AI成人漫剧,持续更新" + pageName + "题材作品,方便快速浏览相关短剧。"
|
||||
}
|
||||
|
||||
title := playletser.TagListPageTitle(pageName, q.PageNum, raw.SeoInfo)
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"pageKey": "tag_list",
|
||||
"pageName": pageName,
|
||||
"pageTitle": pageName,
|
||||
"tagDescription": description,
|
||||
"pageNum": q.PageNum,
|
||||
"pagePath": playletser.TagListCanonicalPath(q),
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"list": raw.List,
|
||||
"totalCount": raw.TotalCount,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildTagListPagination(q, raw.TotalCount),
|
||||
}
|
||||
if raw.TotalCount <= 0 || len(raw.List) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "tagList.html", data)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ThemeIndex 专题列表页:专题数据来自 GET /api/app/theme/list?themeType=2。
|
||||
func ThemeIndex(c *gin.Context) {
|
||||
q := playletser.ParseThemeIndexQuery(c.Param("page"))
|
||||
if redirectCanonical(c, playletser.ThemeIndexCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
themes := playletser.FetchThemesOrEmpty(c, 2)
|
||||
list, totalCount := playletser.BuildThemeIndexPage(themes, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, totalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"title": "AI成人短剧专题推荐" + playletser.PageTitleSuffix(q.PageNum),
|
||||
"description": "专题集中展示黄果短剧各类精品专题合集,涵盖AI成人短剧、AI色情短剧、擦边短剧、AI换脸、AI魔改等热门主题。内容丰富多样,每日持续更新。",
|
||||
"pageKey": "theme_index",
|
||||
"pageName": "专题",
|
||||
"navActive": "topic",
|
||||
"pageNum": q.PageNum,
|
||||
"pagePath": playletser.ThemeIndexCanonicalPath(q),
|
||||
"list": list,
|
||||
"totalCount": totalCount,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildThemeIndexPagination(q, totalCount),
|
||||
}
|
||||
if totalCount <= 0 || len(list) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "themeIndex.html", data)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TodayUpdate 今日更新页 SSR:GET /api/app/playlet/today-update,列表用接口字段渲染。
|
||||
func TodayUpdate(c *gin.Context) {
|
||||
q := playletser.ParseTodayUpdateQuery(routeOrQuery(c, "page", "page"))
|
||||
if redirectCanonical(c, playletser.TodayUpdateCanonicalPath(q)) {
|
||||
return
|
||||
}
|
||||
raw := playletser.FetchTodayUpdateOrEmpty(c, q)
|
||||
if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
|
||||
NotFound(c, "页面不存在")
|
||||
return
|
||||
}
|
||||
data := gin.H{
|
||||
"title": "今日更新",
|
||||
"description": "黄果短剧今日更新:第一时间追更最新上线短剧与连载剧集,不错过每日精彩更新。",
|
||||
"navActive": "todayUpdate",
|
||||
"seoInfo": raw.SeoInfo,
|
||||
"list": raw.List,
|
||||
"itemListOffset": (q.PageNum - 1) * q.PageSize,
|
||||
"pagination": playletser.BuildTodayUpdatePagination(q, raw.TotalCount),
|
||||
}
|
||||
if raw.TotalCount <= 0 || len(raw.List) == 0 {
|
||||
setNoIndex(data, true)
|
||||
}
|
||||
HTMLOK(c, "todayUpdate.html", data)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// attachTrackerPage 按响应状态和路径注入 pageKey / pageName(handler 可显式覆盖)。
|
||||
// 所有 HTML 页面都必须得到明确页面信息,避免埋点落入 unknown / 未知页面。
|
||||
func attachTrackerPage(c *gin.Context, code int, data gin.H) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
key, name := trackerPageFromRequest(c.Request.URL.Path, code)
|
||||
if code >= http.StatusBadRequest {
|
||||
data["pageKey"] = key
|
||||
data["pageName"] = name
|
||||
return
|
||||
}
|
||||
existingKey, _ := data["pageKey"].(string)
|
||||
existingName, _ := data["pageName"].(string)
|
||||
existingKey = strings.TrimSpace(existingKey)
|
||||
existingName = strings.TrimSpace(existingName)
|
||||
|
||||
if existingKey == "" || existingKey == "unknown" {
|
||||
data["pageKey"] = key
|
||||
}
|
||||
if existingName == "" || existingName == "未知页面" {
|
||||
data["pageName"] = name
|
||||
}
|
||||
}
|
||||
|
||||
func trackerPageFromRequest(path string, code int) (pageKey, pageName string) {
|
||||
if code >= http.StatusInternalServerError {
|
||||
return "server_error", "服务异常页面"
|
||||
}
|
||||
if code >= http.StatusBadRequest {
|
||||
return "not_found", "404页面"
|
||||
}
|
||||
return trackerPageFromPath(path)
|
||||
}
|
||||
|
||||
func trackerPageFromPath(path string) (pageKey, pageName string) {
|
||||
p := strings.TrimSpace(path)
|
||||
if p == "" || p == "/" {
|
||||
return "home", "首页"
|
||||
}
|
||||
// 去掉尾部斜杠,便于精确匹配
|
||||
p = strings.TrimSuffix(p, "/")
|
||||
switch {
|
||||
case trackerPathHasPrefix(p, "/list"):
|
||||
return "list", "短剧列表"
|
||||
case trackerPathHasPrefix(p, "/series"):
|
||||
return "series", "AI成人剧集"
|
||||
case trackerPathHasPrefix(p, "/tag"), trackerPathHasPrefix(p, "/tag-list"):
|
||||
return "tag_list", "标签详情"
|
||||
case trackerPathHasPrefix(p, "/rank"):
|
||||
return "rank", "排行榜"
|
||||
case trackerPathHasPrefix(p, "/today-update"):
|
||||
return "today_update", "今日更新"
|
||||
case trackerPathHasPrefix(p, "/category"):
|
||||
return "category_detail", "分类详情"
|
||||
case trackerTopicIndexPath(p):
|
||||
return "theme_index", "专题列表"
|
||||
case trackerPathHasPrefix(p, "/topic"), trackerPathHasPrefix(p, "/more"):
|
||||
return "topic", "专题详情"
|
||||
case trackerPathHasPrefix(p, "/download-app"):
|
||||
return "download_app", "下载APP"
|
||||
case trackerPathHasPrefix(p, "/application"):
|
||||
return "application", "应用推荐"
|
||||
case trackerPathHasPrefix(p, "/home-road"):
|
||||
return "home_road", "回家的路"
|
||||
case trackerPathHasPrefix(p, "/detail"):
|
||||
return "detail", "短剧详情"
|
||||
case trackerPathHasPrefix(p, "/play"):
|
||||
return "play", "播放页"
|
||||
case trackerPathHasPrefix(p, "/login"):
|
||||
return "login", "登录"
|
||||
case trackerPathHasPrefix(p, "/register"):
|
||||
return "register", "注册"
|
||||
case trackerPathHasPrefix(p, "/user-center"):
|
||||
return "user_center", "个人中心"
|
||||
case trackerPathHasPrefix(p, "/deposit"):
|
||||
return "deposit", "充值中心"
|
||||
case trackerPathHasPrefix(p, "/collect"):
|
||||
return "collect", "我的收藏"
|
||||
case trackerPathHasPrefix(p, "/history"):
|
||||
return "history", "观看历史"
|
||||
case trackerPathHasPrefix(p, "/setting"):
|
||||
return "setting", "设置"
|
||||
case trackerPathHasPrefix(p, "/edit-profile"):
|
||||
return "edit_profile", "编辑资料"
|
||||
case trackerPathHasPrefix(p, "/edit-password"):
|
||||
return "edit_password", "修改密码"
|
||||
case trackerPathHasPrefix(p, "/search"):
|
||||
return "search", "搜索结果页"
|
||||
case trackerPathHasPrefix(p, "/about-us"):
|
||||
return "about_us", "关于我们"
|
||||
case trackerPathHasPrefix(p, "/user-agreement"):
|
||||
return "user_agreement", "用户协议"
|
||||
case trackerPathHasPrefix(p, "/privacy-policy"):
|
||||
return "privacy_policy", "隐私政策"
|
||||
case trackerPathHasPrefix(p, "/help-center"):
|
||||
return "help_center", "帮助中心"
|
||||
case trackerPathHasPrefix(p, "/richtext"):
|
||||
return "rich_text", "富文本页"
|
||||
case trackerPathHasPrefix(p, "/feedback"):
|
||||
return "feedback", "问题反馈"
|
||||
case trackerPathHasPrefix(p, "/nav"), trackerPathHasPrefix(p, "/dh"):
|
||||
return "nav", "官方导航"
|
||||
default:
|
||||
return "internal_other", "站内其他页面"
|
||||
}
|
||||
}
|
||||
|
||||
func trackerPathHasPrefix(path, base string) bool {
|
||||
return path == base || strings.HasPrefix(path, base+"/")
|
||||
}
|
||||
|
||||
func trackerTopicIndexPath(path string) bool {
|
||||
if path == "/topic" || path == "/more" {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(path, "/topic/page/") || strings.HasPrefix(path, "/more/page/")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestTrackerPageFromPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
key string
|
||||
name string
|
||||
}{
|
||||
{"/", "home", "首页"},
|
||||
{"/list/ai-short-play/2/page/3/", "list", "短剧列表"},
|
||||
{"/series/hot/page/2/", "series", "AI成人剧集"},
|
||||
{"/tag/family/page/2/", "tag_list", "标签详情"},
|
||||
{"/tag-list/family/", "tag_list", "标签详情"},
|
||||
{"/rank/2/page/3/", "rank", "排行榜"},
|
||||
{"/today-update/page/2/", "today_update", "今日更新"},
|
||||
{"/category/drama/page/2/", "category_detail", "分类详情"},
|
||||
{"/topic/page/2/", "theme_index", "专题列表"},
|
||||
{"/topic/sweet-love/page/2/", "topic", "专题详情"},
|
||||
{"/more/legacy-topic/", "topic", "专题详情"},
|
||||
{"/detail/my-playlet/", "detail", "短剧详情"},
|
||||
{"/play/my-playlet/42/", "play", "播放页"},
|
||||
{"/login/return/detail/foo/", "login", "登录"},
|
||||
{"/register/", "register", "注册"},
|
||||
{"/collect/chapter/page/2/", "collect", "我的收藏"},
|
||||
{"/search/测试/page/2/", "search", "搜索结果页"},
|
||||
{"/about-us/", "about_us", "关于我们"},
|
||||
{"/user-agreement/", "user_agreement", "用户协议"},
|
||||
{"/privacy-policy/", "privacy_policy", "隐私政策"},
|
||||
{"/help-center/", "help_center", "帮助中心"},
|
||||
{"/richtext/自定义内容/", "rich_text", "富文本页"},
|
||||
{"/dh/", "nav", "官方导航"},
|
||||
{"/unregistered-path/", "internal_other", "站内其他页面"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
key, name := trackerPageFromPath(tt.path)
|
||||
if key != tt.key || name != tt.name {
|
||||
t.Fatalf("trackerPageFromPath(%q) = (%q, %q), want (%q, %q)", tt.path, key, name, tt.key, tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackerPageFromRequestUsesResponseStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
status int
|
||||
key string
|
||||
name string
|
||||
}{
|
||||
{http.StatusNotFound, "not_found", "404页面"},
|
||||
{http.StatusInternalServerError, "server_error", "服务异常页面"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
key, name := trackerPageFromRequest("/missing/", tt.status)
|
||||
if key != tt.key || name != tt.name {
|
||||
t.Fatalf("trackerPageFromRequest(status=%d) = (%q, %q), want (%q, %q)", tt.status, key, name, tt.key, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachTrackerPagePreservesExplicitPage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/topic/custom/", nil)
|
||||
data := gin.H{"pageKey": "topic", "pageName": "自定义专题"}
|
||||
|
||||
attachTrackerPage(c, http.StatusOK, data)
|
||||
|
||||
if data["pageKey"] != "topic" || data["pageName"] != "自定义专题" {
|
||||
t.Fatalf("explicit tracker page was overwritten: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachTrackerPageReplacesUnknownValues(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/play/demo/1/", nil)
|
||||
data := gin.H{"pageKey": "unknown", "pageName": "未知页面"}
|
||||
|
||||
attachTrackerPage(c, http.StatusOK, data)
|
||||
|
||||
if data["pageKey"] != "play" || data["pageName"] != "播放页" {
|
||||
t.Fatalf("unknown tracker page was not replaced: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachTrackerPageMarksErrorResponse(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/topic/deleted/", nil)
|
||||
data := gin.H{"pageKey": "topic", "pageName": "已删除专题"}
|
||||
|
||||
attachTrackerPage(c, http.StatusNotFound, data)
|
||||
|
||||
if data["pageKey"] != "not_found" || data["pageName"] != "404页面" {
|
||||
t.Fatalf("error response retained business page mapping: %#v", data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserCenter 个人中心:登录态刷新 getInfo 金币余额,并拉取收藏数量。
|
||||
func UserCenter(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"title": "个人中心",
|
||||
"navActive": "userCenter",
|
||||
}
|
||||
setNoIndex(data, false)
|
||||
if attachWalletFromGetInfo(c, data) {
|
||||
RedirectToLogin(c)
|
||||
return
|
||||
}
|
||||
if useragent.TokenFromRequest(c.Request) != "" {
|
||||
raw, code, err := playletser.FetchCollectList(c, playletser.CollectListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 1,
|
||||
})
|
||||
if err != nil {
|
||||
aborted, retry := AbortSSRIfSessionExpired(c, code)
|
||||
if aborted {
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
raw, _, err = playletser.FetchCollectList(c, playletser.CollectListQuery{
|
||||
PageNum: 1,
|
||||
PageSize: 1,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("user-center collect count failed", log.E(err))
|
||||
raw = nil
|
||||
}
|
||||
}
|
||||
count := int64(0)
|
||||
if raw != nil {
|
||||
count = raw.TotalCount
|
||||
}
|
||||
if count < 0 {
|
||||
count = 0
|
||||
}
|
||||
data["collectCount"] = count
|
||||
data["collectCountText"] = strconv.FormatInt(count, 10) + "部"
|
||||
} else {
|
||||
data["collectCount"] = int64(0)
|
||||
data["collectCountText"] = "0部"
|
||||
}
|
||||
HTMLOK(c, "userCenter.html", data)
|
||||
}
|
||||
Reference in New Issue
Block a user