This commit is contained in:
谢宇宁
2026-09-29 15:30:33 +07:00
parent 0fbdba5ccf
commit 9abe0b1b76
181 changed files with 608 additions and 18501 deletions
-52
View File
@@ -1,52 +0,0 @@
package web
import (
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
const maxAccountPage = 10000
type accountPage struct {
Current int
Total int
Count int64
PrevURL string
NextURL string
}
func accountPageNum(c *gin.Context) (int, bool) {
if c.Request.URL.Query().Has("page") {
return 0, false
}
raw := strings.TrimSpace(c.Param("page"))
if raw == "" {
return 1, true
}
page, err := strconv.Atoi(raw)
if err != nil || page < 1 || page > maxAccountPage {
return 0, false
}
return page, true
}
func makeAccountPage(path string, page, size int, count int64) accountPage {
total := int((count + int64(size) - 1) / int64(size))
p := accountPage{Current: page, Total: total, Count: count}
link := func(n int) string {
path = strings.TrimRight(path, "/")
if n <= 1 {
return path
}
return path + "/page/" + strconv.Itoa(n)
}
if page > 1 {
p.PrevURL = link(page - 1)
}
if page < total {
p.NextURL = link(page + 1)
}
return p
}
-290
View File
@@ -1,290 +0,0 @@
package web
import (
"net/http"
"strings"
"videoweb/app/service/authser"
"videoweb/pub/common/log"
"github.com/gin-gonic/gin"
)
type registerBody struct {
AccountNo string `json:"accountNo"`
Email string `json:"email"`
Password string `json:"password"`
AgentInviteCode string `json:"agentInviteCode"`
DevID string `json:"devID"`
Channel string `json:"channel"`
}
type loginBody struct {
Account string `json:"account"`
Password string `json:"password"`
DevID string `json:"devID"`
Channel string `json:"channel"`
}
// Register 代理上游注册(POST /api/app/auth/register),成功后写入登录 cookie。
// 请求头带游客 Authorization 时升级该游客;邮箱缺省时用账号生成占位邮箱。
func Register(c *gin.Context) {
var body registerBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.AccountNo = strings.TrimSpace(body.AccountNo)
body.Email = strings.TrimSpace(body.Email)
body.AgentInviteCode = strings.TrimSpace(body.AgentInviteCode)
body.DevID = strings.TrimSpace(body.DevID)
body.Channel = authser.ResolveChannel(c, body.Channel)
if body.AccountNo == "" || body.Password == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请填写账号和密码",
"data": nil,
})
return
}
// 上游 registerReq 要求 email;前端无邮箱时用账号生成唯一占位邮箱。
if body.Email == "" {
body.Email = body.AccountNo + "@hgdj.local"
}
env, user, err := authser.Register(c, authser.RegisterReq{
AccountNo: body.AccountNo,
Email: body.Email,
Password: body.Password,
AgentInviteCode: body.AgentInviteCode,
DevID: body.DevID,
Channel: body.Channel,
})
if err != nil {
log.Error("register upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "register failed",
"tip": "注册失败,请稍后重试",
"data": nil,
})
return
}
if env == nil || env.Code != 200 || user == nil {
tip := "注册失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
if t := strings.TrimSpace(env.Tip); t != "" {
tip = t
} else if m := strings.TrimSpace(env.Msg); m != "" {
tip = m
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmptyAuth(env.Msg, "success"),
"tip": firstNonEmptyAuth(env.Tip, "注册成功"),
"data": user,
})
}
// Login 代理上游登录(POST /api/app/auth/login),成功后写入登录 cookie。
func Login(c *gin.Context) {
var body loginBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.Account = strings.TrimSpace(body.Account)
body.DevID = strings.TrimSpace(body.DevID)
body.Channel = authser.ResolveChannel(c, body.Channel)
if body.Account == "" || body.Password == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请填写账号和密码",
"data": nil,
})
return
}
env, user, err := authser.Login(c, authser.LoginReq{
Account: body.Account,
Password: body.Password,
DevID: body.DevID,
Channel: body.Channel,
})
if err != nil {
log.Error("login upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "login failed",
"tip": "登录失败,请稍后重试",
"data": nil,
})
return
}
if env == nil || env.Code != 200 || user == nil {
tip := "登录失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
if t := strings.TrimSpace(env.Tip); t != "" {
tip = t
} else if m := strings.TrimSpace(env.Msg); m != "" {
tip = m
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmptyAuth(env.Msg, "success"),
"tip": firstNonEmptyAuth(env.Tip, "登录成功"),
"data": user,
})
}
// Guest 代理上游游客登录,成功后写入登录 cookie。
// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent / X-Real-IP(由 BFF 组装)。
func Guest(c *gin.Context) {
var body struct {
DevID string `json:"devID"`
DeviceID string `json:"deviceId"`
Mac string `json:"mac"`
Channel string `json:"channel"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.DevID = strings.TrimSpace(body.DevID)
body.DeviceID = strings.TrimSpace(body.DeviceID)
body.Mac = strings.TrimSpace(body.Mac)
body.Channel = authser.ResolveChannel(c, body.Channel)
if body.DevID == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少设备标识",
"data": nil,
})
return
}
if body.DeviceID == "" {
body.DeviceID = body.DevID
}
env, user, err := authser.GuestLogin(c, authser.GuestLoginReq{
DevID: body.DevID,
DeviceID: body.DeviceID,
Mac: body.Mac,
Channel: body.Channel,
})
if err != nil {
log.Error("guest login upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "guest login failed",
"tip": "游客登录失败,请稍后重试",
"data": nil,
})
return
}
if env == nil || env.Code != 200 || user == nil {
tip := "游客登录失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
if t := strings.TrimSpace(env.Tip); t != "" {
tip = t
} else if m := strings.TrimSpace(env.Msg); m != "" {
tip = m
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmptyAuth(env.Msg, "success"),
"tip": firstNonEmptyAuth(env.Tip, "游客登录成功"),
"data": user,
})
}
// Logout 清除登录 cookie,并标记跳过紧接着的一次自动游客登录。
func Logout(c *gin.Context) {
authser.ClearAuthCookies(c)
authser.MarkGuestSkipAuto(c)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
"tip": "已退出登录",
"data": nil,
})
}
func firstNonEmptyAuth(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
-120
View File
@@ -1,120 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"github.com/gin-gonic/gin"
)
// ChapterLikeSet BFF:代理 POST /api/app/playlet/chapter/like/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_like_set
func ChapterLikeSet(c *gin.Context) {
var body struct {
ChapterID int64 `json:"chapterId"`
IsLike *bool `json:"isLike"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "chapterId/isLike required",
"tip": "缺少剧集 ID 或点赞状态",
"data": nil,
})
return
}
env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
ChapterID: body.ChapterID,
IsLike: *body.IsLike,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
// ChapterCollectSet BFF:代理 POST /api/app/playlet/chapter/collect/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_collect_set
func ChapterCollectSet(c *gin.Context) {
var body struct {
ChapterID int64 `json:"chapterId"`
IsCollect *bool `json:"isCollect"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.IsCollect == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "chapterId/isCollect required",
"tip": "缺少剧集 ID 或收藏状态",
"data": nil,
})
return
}
env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
ChapterID: body.ChapterID,
IsCollect: *body.IsCollect,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-64
View File
@@ -1,64 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"github.com/gin-gonic/gin"
)
// ChapterUnlock BFF:代理 POST /api/app/playlet/chapter/unlock。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_unlock
func ChapterUnlock(c *gin.Context) {
var body struct {
ChapterID int64 `json:"chapterId"`
PlayletID int64 `json:"playletId"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.PlayletID <= 0 {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "chapterId/playletId required",
"tip": "缺少剧集或短剧 ID",
"data": nil,
})
return
}
env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "解锁失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "解锁失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-75
View File
@@ -1,75 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// ChapterWatchReport BFF:代理 POST /api/app/playlet/chapter/watch/report。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_watch_report
func ChapterWatchReport(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录",
"data": nil,
})
return
}
var body struct {
ChapterID int64 `json:"chapterId"`
PlayletID int64 `json:"playletId"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "chapterId required",
"tip": "缺少剧集 ID",
"data": nil,
})
return
}
env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "上报失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "上报失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-112
View File
@@ -3,7 +3,6 @@ package web
import (
"net/http"
"strconv"
"strings"
"videoweb/app/service/commentser"
"github.com/gin-gonic/gin"
@@ -42,114 +41,3 @@ func CommentList(c *gin.Context) {
}
// CommentSend BFF:代理 POST /api/app/comment/send。
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_send
func CommentSend(c *gin.Context) {
var req commentser.CommentSendReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
env, err := commentser.Send(c, req)
if err != nil {
tip := "发布失败"
msg := err.Error()
switch {
case strings.Contains(msg, "empty text"):
tip = "请输入评论内容"
case strings.Contains(msg, "too long"):
tip = "评论不能超过 200 字"
case strings.Contains(msg, "invalid objectId"):
tip = "缺少评论对象"
}
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "发布失败",
"data": nil,
})
return
}
tip := env.Tip
if tip == "" {
tip = env.Msg
}
// 上游参数错误兜底提示(例如 objectType 不被支持)
if env.Code == 1001 && (tip == "" || tip == "参数错误") {
tip = "评论参数错误,请稍后重试"
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": tip,
"data": env.Data,
})
}
// CommentLikeSet BFF:代理 POST /api/app/comment/like/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_like_set
func CommentLikeSet(c *gin.Context) {
var body struct {
CommentID int64 `json:"commentId"`
IsLike *bool `json:"isLike"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.CommentID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "commentId/isLike required",
"tip": "缺少评论 ID 或点赞状态",
"data": nil,
})
return
}
env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{
CommentID: body.CommentID,
IsLike: *body.IsLike,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-338
View File
@@ -1,338 +0,0 @@
package web
import (
"fmt"
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/app/service/videoser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type favoriteWork struct {
ID int64
Title string
Genre string
DesktopMeta string
Episodes string
Badge string
Image string
Kind string
URL string
DramaID int64
Episode int
VideoID int64
TotalEpisode int
}
// Favorites 我的收藏:短剧 / 剧集(视频) / 章节收藏。
// 文档:
// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_collect_list
// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_collect_list
func Favorites(c *gin.Context) {
filter, page, validRoute := favoritesRoute(c)
if !validRoute {
NotFound(c)
return
}
data := gin.H{"title": "我的收藏 - 黄果短剧", "navActive": "favorites", "accountTab": "favorites"}
responses := gin.H{}
loadAccountSummary(c, data, responses)
status := 0
switch filter {
case "short", "chapter", "series":
case "following":
status = 1
case "finished":
status = 2
default:
filter = "all"
}
data["filter"] = filter
pageSize := 12
query := playletser.CollectListQuery{PageNum: page, PageSize: pageSize, WatchStatus: status}
if useragent.TokenFromRequest(c.Request) == "" {
responses["playlet/collect/list"] = gin.H{"error": "暂无登录会话"}
responses["video/collect/list"] = gin.H{"error": "暂无登录会话"}
data["favoritesResponse"] = responses["playlet/collect/list"]
data["playletCollectResponse"] = responses["playlet/collect/list"]
data["videoCollectResponse"] = responses["video/collect/list"]
data["favoritesDebug"] = responses
renderFavorites(c, data)
return
}
if filter == "chapter" {
list, code, err := playletser.FetchChapterCollectList(c, query)
if retry, _ := authser.OnSessionExpired(c, code); retry {
list, _, err = playletser.FetchChapterCollectList(c, query)
}
if err != nil {
log.Error("fetch chapter favorites page failed", log.E(err))
responses["playlet/chapter/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
data["favoritesUnavailable"] = true
} else if list != nil {
responses["playlet/chapter/collect/list"] = gin.H{"code": 200, "data": list}
data["paging"] = makeFavoritesPage(query.PageNum, query.PageSize, list.TotalCount, filter)
data["favorites"] = chapterFavoriteWorks(list.List)
} else {
responses["playlet/chapter/collect/list"] = gin.H{"code": code, "data": list}
}
data["favoritesResponse"] = responses["playlet/chapter/collect/list"]
data["playletCollectResponse"] = responses["playlet/chapter/collect/list"]
data["favoritesDebug"] = responses
renderFavorites(c, data)
return
}
if filter == "series" {
loadVideoFavorites(c, data, responses, page, pageSize)
data["favoritesResponse"] = responses["video/collect/list"]
data["playletCollectResponse"] = gin.H{}
data["videoCollectResponse"] = responses["video/collect/list"]
data["favoritesDebug"] = responses
renderFavorites(c, data)
return
}
var works []favoriteWork
var totalCount int64
playletFailed := false
videoFailed := false
if filter == "all" || filter == "short" || filter == "following" || filter == "finished" {
list, code, err := playletser.FetchCollectList(c, query)
if retry, _ := authser.OnSessionExpired(c, code); retry {
list, code, err = playletser.FetchCollectList(c, query)
}
if err == nil && list != nil {
responses["playlet/collect/list"] = gin.H{"code": 200, "data": list}
works = append(works, playletFavoriteWorks(list.List)...)
totalCount += list.TotalCount
} else if err != nil {
responses["playlet/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
log.Error("fetch favorites page failed", log.E(err))
playletFailed = true
} else {
responses["playlet/collect/list"] = gin.H{"code": code, "data": list}
}
}
if filter == "all" {
videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize}
videoList, videoCode, videoErr := videoser.FetchCollectList(c, videoQuery)
if retry, _ := authser.OnSessionExpired(c, videoCode); retry {
videoList, videoCode, videoErr = videoser.FetchCollectList(c, videoQuery)
}
if videoErr == nil && videoList != nil {
responses["video/collect/list"] = gin.H{"code": 200, "data": videoList}
works = append(works, videoFavoriteWorks(videoList.List)...)
totalCount += videoList.TotalCount
} else if videoErr != nil {
responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()}
log.Error("fetch video favorites page failed", log.E(videoErr))
videoFailed = true
} else {
responses["video/collect/list"] = gin.H{"code": videoCode, "data": videoList}
}
}
if playletFailed && (filter != "all" || videoFailed) {
data["favoritesUnavailable"] = true
}
if filter == "all" && playletFailed && videoFailed {
data["favoritesUnavailable"] = true
}
data["paging"] = makeFavoritesPage(page, pageSize, totalCount, filter)
data["favorites"] = works
data["favoritesResponse"] = responses["playlet/collect/list"]
data["playletCollectResponse"] = responses["playlet/collect/list"]
data["videoCollectResponse"] = responses["video/collect/list"]
data["favoritesDebug"] = responses
renderFavorites(c, data)
}
// renderFavorites 收藏为空时附带「大家都在看」后渲染。
func renderFavorites(c *gin.Context, data gin.H) {
attachFavoritesRecommended(c, data)
HTMLOK(c, "favorites.html", data)
}
// attachFavoritesRecommended 列表为空时拉取最热短剧(与搜索无结果一致)。
// 上游:GET /api/app/playlet/all/list sortType=2
func attachFavoritesRecommended(c *gin.Context, data gin.H) {
if works, ok := data["favorites"].([]favoriteWork); ok && len(works) > 0 {
return
}
watchingQ := playletser.AllListQuery{
PageNum: 1,
PageSize: 6,
SortType: 2, // 最热
UpdateStatus: 0,
}
watching := playletser.FetchAllListOrEmpty(c, watchingQ)
if watching == nil || len(watching.List) == 0 {
return
}
list := watching.List
if len(list) > 6 {
list = list[:6]
}
data["recommended"] = list
}
func loadVideoFavorites(c *gin.Context, data gin.H, responses gin.H, page, pageSize int) {
videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize}
list, code, err := videoser.FetchCollectList(c, videoQuery)
if retry, _ := authser.OnSessionExpired(c, code); retry {
list, code, err = videoser.FetchCollectList(c, videoQuery)
}
if err != nil {
log.Error("fetch video favorites page failed", log.E(err))
responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
data["favoritesUnavailable"] = true
return
}
if list == nil {
responses["video/collect/list"] = gin.H{"code": code, "data": list}
return
}
responses["video/collect/list"] = gin.H{"code": 200, "data": list}
data["paging"] = makeFavoritesPage(page, pageSize, list.TotalCount, "series")
data["favorites"] = videoFavoriteWorks(list.List)
}
func favoritesRoute(c *gin.Context) (filter string, page int, valid bool) {
if c.Request.URL.Query().Has("filter") {
return "", 0, false
}
filter = strings.TrimSpace(c.Param("filter"))
switch filter {
case "":
filter = "all"
case "short", "chapter", "series", "following", "finished":
default:
return "", 0, false
}
page, valid = accountPageNum(c)
if !valid {
return "", 0, false
}
return filter, page, true
}
func favoritesPagePath(filter string, page int) string {
path := "/favorites"
if filter != "" && filter != "all" {
path += "/" + filter
}
if page > 1 {
path += "/page/" + strconv.Itoa(page)
}
return path
}
func makeFavoritesPage(page, size int, count int64, filter string) accountPage {
total := int((count + int64(size) - 1) / int64(size))
pagination := accountPage{Current: page, Total: total, Count: count}
if page > 1 {
pagination.PrevURL = favoritesPagePath(filter, page-1)
}
if page < total {
pagination.NextURL = favoritesPagePath(filter, page+1)
}
return pagination
}
func playletFavoriteWorks(items []playletser.PlayletCollectData) []favoriteWork {
works := make([]favoriteWork, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.PlayName)
if title == "" {
title = strings.TrimSpace(item.Title)
}
if title == "" {
continue
}
work := favoriteWork{
ID: item.ID, Title: title, Genre: strings.Join(item.ThemeNames, " "),
Image: profileCover(item.Cover), Kind: "short",
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
DramaID: item.ID, Episode: item.LastEpisode, VideoID: item.LastChapterID,
TotalEpisode: item.TotalEpisode,
}
if work.Episode <= 0 {
work.Episode = 1
}
if item.TotalEpisode > 0 {
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
work.DesktopMeta = work.Episodes
}
if item.IsFeatured {
work.Badge = "爆剧"
}
works = append(works, work)
}
return works
}
func chapterFavoriteWorks(items []playletser.PlayletChapterCollectData) []favoriteWork {
works := make([]favoriteWork, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.PlayletTitle)
if title == "" {
continue
}
work := favoriteWork{
ID: item.ChapterID, Title: title, Image: profileCover(item.PlayletCover), Kind: "chapter",
URL: playletser.ChapterPlayPath(item.PlayletSlug, item.PlayletID, item.ChapterID), DesktopMeta: item.ChapterTitle,
DramaID: item.PlayletID, Episode: item.CurrentEpisode, VideoID: item.ChapterID,
TotalEpisode: item.TotalEpisode,
}
if work.Episode <= 0 {
work.Episode = 1
}
if item.CurrentEpisode > 0 {
work.Genre = fmt.Sprintf("第 %d 集", item.CurrentEpisode)
}
if item.TotalEpisode > 0 {
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
}
works = append(works, work)
}
return works
}
func videoFavoriteWorks(items []videoser.VideoCollectData) []favoriteWork {
works := make([]favoriteWork, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.Title)
if title == "" {
continue
}
work := favoriteWork{
ID: item.ID, Title: title, Genre: strings.Join(item.Keyworld, " "),
Image: profileCover(item.CoverImg), Kind: "series",
URL: videoser.VideoPlayURL(item.Slug, item.ID),
}
if item.VideoTime > 0 {
work.Episodes = formatVideoDuration(item.VideoTime)
work.DesktopMeta = work.Episodes
} else {
work.Episodes = "剧集"
work.DesktopMeta = "剧集"
}
works = append(works, work)
}
return works
}
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"net/http"
"path/filepath"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/feedbackser"
"github.com/gin-gonic/gin"
@@ -113,7 +112,7 @@ func UploadImage(c *gin.Context) {
if name == "" || name == "." || name == "/" {
name = "image.jpg"
}
resp, err := authser.UploadImage(c, name, f)
resp, err := feedbackser.UploadImage(c, name, f)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
-375
View File
@@ -1,375 +0,0 @@
package web
import (
"fmt"
"net/http"
"sort"
"strings"
"time"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/app/service/videoser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type historyEntry struct {
ID int64
Title string
Current int
Total int
EpisodeText string
Genre string
Time string
Image string
Progress string
Kind string
URL string
sortAt time.Time
}
type historyGroup struct {
Title string
Entries []historyEntry
}
// History 观看历史页:短剧 GET /api/app/playlet/history/list + 剧集 GET /api/app/video/history/list。
// 文档:
// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_history_list
// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_history_list
func History(c *gin.Context) {
page, validPage := accountPageNum(c)
if !validPage {
NotFound(c)
return
}
data := gin.H{"title": "观看历史 - 黄果短剧", "navActive": "history", "accountTab": "history"}
responses := gin.H{}
loadAccountSummary(c, data, responses)
pageSize := 20
var entries []historyEntry
var totalCount int64
if useragent.TokenFromRequest(c.Request) == "" {
responses["playlet/history/list"] = gin.H{"error": "暂无登录会话"}
responses["video/history/list"] = gin.H{"error": "暂无登录会话"}
} else {
playletQuery := playletser.HistoryListQuery{PageNum: page, PageSize: pageSize}
playletList, playletCode, playletErr := playletser.FetchHistoryList(c, playletQuery)
if retry, _ := authser.OnSessionExpired(c, playletCode); retry {
playletList, playletCode, playletErr = playletser.FetchHistoryList(c, playletQuery)
}
if playletErr == nil && playletList != nil {
responses["playlet/history/list"] = gin.H{"code": 200, "data": playletList}
entries = append(entries, playletHistoryEntries(playletList.List)...)
totalCount += playletList.TotalCount
} else if playletErr != nil {
responses["playlet/history/list"] = gin.H{"error": "加载失败", "msg": playletErr.Error()}
log.Error("fetch playlet history page failed", log.E(playletErr))
} else {
responses["playlet/history/list"] = gin.H{"code": playletCode, "data": playletList}
}
videoQuery := videoser.HistoryListQuery{PageNum: page, PageSize: pageSize}
videoList, videoCode, videoErr := videoser.FetchHistoryList(c, videoQuery)
if retry, _ := authser.OnSessionExpired(c, videoCode); retry {
videoList, videoCode, videoErr = videoser.FetchHistoryList(c, videoQuery)
}
if videoErr == nil && videoList != nil {
responses["video/history/list"] = gin.H{"code": 200, "data": videoList}
entries = append(entries, videoHistoryEntries(videoList.List)...)
totalCount += videoList.TotalCount
} else if videoErr != nil {
responses["video/history/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()}
log.Error("fetch video history page failed", log.E(videoErr))
} else {
responses["video/history/list"] = gin.H{"code": videoCode, "data": videoList}
}
if playletErr != nil && videoErr != nil {
data["historyUnavailable"] = true
}
}
data["paging"] = makeAccountPage("/history", page, pageSize, totalCount)
data["groups"] = groupHistoryEntries(entries)
data["historyResponse"] = responses["playlet/history/list"]
data["playletHistoryResponse"] = responses["playlet/history/list"]
data["videoHistoryResponse"] = responses["video/history/list"]
data["historyDebug"] = responses
HTMLOK(c, "history.html", data)
}
// HistoryClear BFF:代理 POST /api/app/playlet/history/clear。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_clear
func HistoryClear(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
env, err := playletser.HistoryClear(c)
if err != nil {
log.Error("clear playlet history failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil})
return
}
if env.Code != 200 {
tip := strings.TrimSpace(env.Tip)
if tip == "" {
tip = strings.TrimSpace(env.Msg)
}
if tip == "" {
tip = "清空失败,请稍后重试"
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
return
}
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
}
// VideoHistoryClear BFF:代理 POST /api/app/video/history/clear。
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_clear
func VideoHistoryClear(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
env, err := videoser.HistoryClear(c)
if err != nil {
log.Error("clear video history failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil})
return
}
if env.Code != 200 {
tip := strings.TrimSpace(env.Tip)
if tip == "" {
tip = strings.TrimSpace(env.Msg)
}
if tip == "" {
tip = "清空失败,请稍后重试"
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
return
}
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
}
// HistoryDelete BFF:代理 POST /api/app/playlet/history/delete。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_delete
func HistoryDelete(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
var body playletser.PlayletHistoryDeleteReq
if err := c.ShouldBindJSON(&body); err != nil || body.PlayletID <= 0 {
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "playletId required", "tip": "参数错误", "data": nil})
return
}
env, err := playletser.HistoryDelete(c, body)
if err != nil {
log.Error("delete playlet history failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil})
return
}
if env.Code != 200 {
tip := strings.TrimSpace(env.Tip)
if tip == "" {
tip = strings.TrimSpace(env.Msg)
}
if tip == "" {
tip = "删除失败,请稍后重试"
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
return
}
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
}
// VideoHistoryDelete BFF:代理 POST /api/app/video/history/delete。
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_delete
func VideoHistoryDelete(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
var body videoser.VideoHistoryDeleteReq
if err := c.ShouldBindJSON(&body); err != nil || body.VideoID <= 0 {
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "videoId required", "tip": "参数错误", "data": nil})
return
}
env, err := videoser.HistoryDelete(c, body)
if err != nil {
log.Error("delete video history failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil})
return
}
if env.Code != 200 {
tip := strings.TrimSpace(env.Tip)
if tip == "" {
tip = strings.TrimSpace(env.Msg)
}
if tip == "" {
tip = "删除失败,请稍后重试"
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
return
}
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data})
}
func playletHistoryEntries(items []playletser.PlayletHistoryData) []historyEntry {
out := make([]historyEntry, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.PlayName)
if title == "" {
title = strings.TrimSpace(item.Title)
}
if title == "" {
continue
}
progress := 0
if item.TotalEpisode > 0 {
progress = item.WatchedCount * 100 / item.TotalEpisode
}
if progress < 0 {
progress = 0
}
if progress > 100 {
progress = 100
}
out = append(out, historyEntry{
ID: item.ID,
Title: title,
Current: item.LastEpisode,
Total: item.TotalEpisode,
Genre: strings.Join(item.ThemeNames, " "),
Time: playletser.FormatHistoryListTime(item.HistoryAt),
Image: profileCover(item.Cover),
Progress: fmt.Sprintf("%d%%", progress),
Kind: "short",
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
sortAt: historySortAt(item.HistoryAt),
})
}
return out
}
func videoHistoryEntries(items []videoser.VideoHistoryData) []historyEntry {
out := make([]historyEntry, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.Title)
if title == "" {
continue
}
out = append(out, historyEntry{
ID: item.ID,
Title: title,
EpisodeText: videoHistoryEpisodeText(item),
Genre: strings.Join(item.Keyworld, " "),
Time: playletser.FormatHistoryListTime(item.HistoryAt),
Image: profileCover(item.CoverImg),
Progress: "",
Kind: "series",
URL: videoser.VideoPlayURL(item.Slug, item.ID),
sortAt: historySortAt(item.HistoryAt),
})
}
return out
}
func videoHistoryEpisodeText(item videoser.VideoHistoryData) string {
if item.VideoTime > 0 {
return formatVideoDuration(item.VideoTime)
}
return "剧集"
}
func formatVideoDuration(sec int) string {
if sec <= 0 {
return ""
}
h := sec / 3600
m := (sec % 3600) / 60
s := sec % 60
if h > 0 {
return fmt.Sprintf("%d:%02d:%02d", h, m, s)
}
return fmt.Sprintf("%d:%02d", m, s)
}
func historySortAt(raw string) time.Time {
t, ok := playletser.ParseHistoryTime(raw)
if !ok {
return time.Time{}
}
return t
}
func groupHistoryEntries(entries []historyEntry) []historyGroup {
if len(entries) == 0 {
return nil
}
sort.SliceStable(entries, func(i, j int) bool {
if entries[i].sortAt.Equal(entries[j].sortAt) {
return entries[i].ID > entries[j].ID
}
return entries[i].sortAt.After(entries[j].sortAt)
})
order := []string{"今天", "昨天", "更早"}
buckets := map[string][]historyEntry{
"今天": {},
"昨天": {},
"更早": {},
}
for _, entry := range entries {
bucket := historyBucketFromTime(entry.sortAt)
buckets[bucket] = append(buckets[bucket], entry)
}
groups := make([]historyGroup, 0, 3)
for _, title := range order {
if len(buckets[title]) == 0 {
continue
}
groups = append(groups, historyGroup{Title: title, Entries: buckets[title]})
}
return groups
}
func historyBucketFromTime(t time.Time) string {
if t.IsZero() {
return "更早"
}
now := time.Now()
startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
dayDiff := int(startToday.Sub(startThat).Hours() / 24)
switch {
case dayDiff <= 0:
return "今天"
case dayDiff == 1:
return "昨天"
default:
return "更早"
}
}
-8
View File
@@ -9,7 +9,6 @@ import (
// Index 首页 SSR:拉取 GET /api/app/playlet/home,按接口字段注入模板。
// 接口文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_home
// 继续观看:GET /api/app/playlet/history/list?pageNum=1&pageSize=6
// 热播榜:短剧 hot/rank/list;剧集 video/all/list sortType=最热
func Index(c *gin.Context) {
raw := playletser.FetchHomeOrEmpty(c)
@@ -17,12 +16,6 @@ func Index(c *gin.Context) {
if raw != nil && raw.SeoInfo.Title != "" {
title = raw.SeoInfo.Title
}
history := playletser.FetchHistoryListOrEmpty(c, playletser.HistoryListQuery{PageNum: 1, PageSize: 6})
continueWatch := history.List
if len(continueWatch) > 6 {
continueWatch = continueWatch[:6]
}
hotPlaylets := playletser.FetchHotRankListOrEmpty(c, playletser.HotRankQuery{
PageNum: 1,
PageSize: 10,
@@ -52,7 +45,6 @@ func Index(c *gin.Context) {
"homeConfigData": playletser.ActiveHomeConfigs(raw.HomeConfigData),
// GET /api/app/playlet/tag/options → list(已上架标签)
"tagOptions": playletser.FetchTagOptionsOrEmpty(c),
"continueWatch": continueWatch,
"hotRankPlaylets": hotPlaylets,
"hotRankVideos": hotVideos,
})
-11
View File
@@ -1,11 +0,0 @@
package web
import "github.com/gin-gonic/gin"
// LoginPage H5 登录页:渲染 template/pages/login.html。
// PC 端由页面内脚本回首页并打开 loginPop;H5 直接展示本页。
func LoginPage(c *gin.Context) {
HTMLOK(c, "login.html", gin.H{
"title": "登录 - 黄果短剧",
})
}
-83
View File
@@ -1,9 +1,7 @@
package web
import (
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/commentser"
"videoweb/app/service/playletser"
"videoweb/app/service/videoser"
@@ -92,13 +90,9 @@ func Play(c *gin.Context) {
SortOrder: commentser.SortHot,
})
unlockCost := int64(0)
balance := int64(0)
canWatch := false
episode := 0
if chapter != nil {
unlockCost = chapter.UnlockCost()
balance = chapter.Balance
canWatch = chapter.AllowWatch()
episode = chapter.CurrentEpisode
}
@@ -117,9 +111,6 @@ func Play(c *gin.Context) {
if episode <= 0 {
episode = 1
}
if unlockCost <= 0 {
unlockCost = int64(playletser.FirstUnlockCost(playlet.PlayletChapterInfo))
}
previewURL := strings.TrimSpace(playlet.Preview)
mainURL := ""
@@ -139,9 +130,6 @@ func Play(c *gin.Context) {
"mainURL": mainURL,
"previewURL": previewURL,
"hasPreview": previewURL != "",
"unlockCost": unlockCost,
"balance": balance,
"balanceText": authser.FormatRecharge(balance),
"nextChapterID": playletser.NextChapterID(playlet, chapterID),
"advitise": playlet.Advitise,
"related": playlet.RelatedRecommendData,
@@ -157,7 +145,6 @@ func Play(c *gin.Context) {
"commentSize": comments.PageSize,
"commentObjectID": commentObjectID,
"commentObjectType": commentObjectType,
"trackerDrama": dramaTrackingData(playlet, chapter, chapterID, episode),
"playResponse": gin.H{
"playlet": playlet,
"chapter": chapter,
@@ -174,76 +161,9 @@ func Play(c *gin.Context) {
},
}
data["canonicalPath"] = playletser.PlayPath(playlet.Slug, playlet.ID)
attachRechargeModal(c, data)
HTMLOK(c, "play.html", data)
}
func dramaTrackingData(playlet *playletser.PlayletInfoResp, chapter *playletser.PlayletChapterInfoResp, chapterID int64, episode int) gin.H {
name := strings.TrimSpace(playlet.PlayName)
if name == "" {
name = strings.TrimSpace(playlet.Title)
}
totalEpisode := playlet.TotalEpisode
if totalEpisode <= 0 {
totalEpisode = len(playlet.PlayletChapterInfo)
}
if totalEpisode <= 0 {
totalEpisode = 1
}
data := gin.H{
"MediaID": "",
"DramaID": playlet.ID,
"DramaTitle": name,
"Episode": max(1, episode),
"TotalEpisode": totalEpisode,
"VideoID": chapterID,
"DramaTypeID": "default",
"DramaTypeName": "默认分类",
"DramaTagKey": "default",
"DramaTagName": "默认标签",
"DramaDuration": 0,
"RecommendTraceID": "",
}
if chapter != nil {
if chapter.ID > 0 {
data["VideoID"] = chapter.ID
}
if chapter.MediaTime > 0 {
data["DramaDuration"] = chapter.MediaTime
}
}
for i, id := range playlet.ThemeIDs {
if id <= 0 || i >= len(playlet.ThemeNames) {
continue
}
name := strings.TrimSpace(playlet.ThemeNames[i])
if name == "" {
continue
}
data["DramaTypeID"] = strconv.FormatInt(id, 10)
data["DramaTypeName"] = name
break
}
tagIDs := make([]string, 0, len(playlet.TagIDs))
tagNames := make([]string, 0, len(playlet.TagIDs))
for i, id := range playlet.TagIDs {
if id <= 0 || i >= len(playlet.TagNames) {
continue
}
name := strings.TrimSpace(playlet.TagNames[i])
if name == "" {
continue
}
tagIDs = append(tagIDs, strconv.FormatInt(id, 10))
tagNames = append(tagNames, name)
}
if len(tagIDs) > 0 {
data["DramaTagKey"] = strings.Join(tagIDs, ",")
data["DramaTagName"] = strings.Join(tagNames, ",")
}
return data
}
// SeriesPlay 视频播放页:GET /api/app/video/detail。
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_detail
func SeriesPlay(c *gin.Context) {
@@ -369,8 +289,6 @@ func SeriesPlay(c *gin.Context) {
"list": comments.List,
"apis": []string{
"GET /api/app/comment/list",
"POST /api/app/comment/send",
"POST /api/app/comment/like/set",
},
},
"playDebug": gin.H{
@@ -383,6 +301,5 @@ func SeriesPlay(c *gin.Context) {
if strings.TrimSpace(c.Param("guessPage")) != "" {
data["canonicalPath"] = videoser.VideoPlayURL(video.Slug, video.ID)
}
attachRechargeModal(c, data)
HTMLOK(c, "seriesPlay.html", data)
}
-64
View File
@@ -1,64 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"github.com/gin-gonic/gin"
)
// PlayletLikeSet BFF:代理 POST /api/app/playlet/like/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_like_set
func PlayletLikeSet(c *gin.Context) {
var body struct {
ID int64 `json:"id"`
IsLike *bool `json:"isLike"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "id/isLike required",
"tip": "缺少短剧 ID 或点赞状态",
"data": nil,
})
return
}
env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{
ID: body.ID,
IsLike: *body.IsLike,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-64
View File
@@ -1,64 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"github.com/gin-gonic/gin"
)
// PlayletCollectSet BFF:代理 POST /api/app/playlet/collect/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_collect_set
func PlayletCollectSet(c *gin.Context) {
var body struct {
ID int64 `json:"id"`
IsCollect *bool `json:"isCollect"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsCollect == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "id/isCollect required",
"tip": "缺少短剧 ID 或收藏状态",
"data": nil,
})
return
}
env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{
ID: body.ID,
IsCollect: *body.IsCollect,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-229
View File
@@ -1,229 +0,0 @@
package web
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/app/service/usercenterser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type profileWork struct {
Title, Meta, Episodes, Badge, Image, Progress, Remaining, URL string
}
// Profile 从当前会话拉取用户信息、观看历史和收藏,不展示演示账号数据。
func Profile(c *gin.Context) {
data := gin.H{
"title": "个人中心 - 黄果短剧", "navActive": "profile", "accountTab": "profile",
}
responses := gin.H{}
loadAccountSummary(c, data, responses)
if useragent.TokenFromRequest(c.Request) != "" {
fetchProfileLists(c, data, responses)
}
if pretty, err := json.MarshalIndent(responses, "", " "); err == nil {
data["responseJSON"] = string(pretty)
}
HTMLOK(c, "profile.html", data)
}
func loadAccountSummary(c *gin.Context, data, responses gin.H) {
data["userName"] = "未登录"
data["balance"] = "—"
data["historyCount"] = "—"
data["favoriteCount"] = "—"
data["chasingCount"] = "—"
data["boughtChapterCount"] = "—"
user := authser.SyncUserOnEnter(c)
if user == nil {
if useragent.TokenFromRequest(c.Request) != "" {
responses["auth/getInfo"] = gin.H{"error": "加载失败"}
fetchProfileStats(c, data, responses)
return
}
responses["auth/getInfo"] = gin.H{"error": "暂无登录会话"}
return
}
applyProfileInfo(data, responses, user)
if useragent.TokenFromRequest(c.Request) != "" {
fetchProfileStats(c, data, responses)
}
}
func fetchProfileStats(c *gin.Context, data, responses gin.H) {
stats, code, err := usercenterser.Fetch(c)
if retry, _ := authser.OnSessionExpired(c, code); retry {
stats, code, err = usercenterser.Fetch(c)
}
if err != nil || stats == nil {
responses["user/center/data"] = gin.H{"error": "加载失败"}
if err != nil {
log.Error("fetch user center data failed", log.E(err))
}
return
}
responses["user/center/data"] = gin.H{"code": code, "data": stats}
data["historyCount"] = fmt.Sprint(stats.WatchCount)
data["favoriteCount"] = fmt.Sprint(stats.CollectCount)
data["chasingCount"] = fmt.Sprint(stats.ChasingCount)
data["boughtChapterCount"] = fmt.Sprint(stats.BoughtChapterCount)
}
func fetchProfileLists(c *gin.Context, data, responses gin.H) {
query := playletser.HistoryListQuery{PageNum: 1, PageSize: 6}
history, code, err := playletser.FetchHistoryList(c, query)
if retry, _ := authser.OnSessionExpired(c, code); retry {
history, _, err = playletser.FetchHistoryList(c, query)
}
if err == nil && history != nil {
responses["playlet/history/list"] = gin.H{"code": 200, "data": history}
data["history"] = profileHistoryWorks(history.List)
} else if err != nil {
responses["playlet/history/list"] = gin.H{"error": "加载失败"}
log.Error("fetch profile history failed", log.E(err))
data["historyUnavailable"] = true
}
collectQuery := playletser.CollectListQuery{PageNum: 1, PageSize: 6, WatchStatus: 0}
favorites, code, err := playletser.FetchCollectList(c, collectQuery)
if retry, _ := authser.OnSessionExpired(c, code); retry {
favorites, _, err = playletser.FetchCollectList(c, collectQuery)
}
if err == nil && favorites != nil {
responses["playlet/collect/list"] = gin.H{"code": 200, "data": favorites}
data["favorites"] = profileFavoriteWorks(favorites.List)
} else if err != nil {
responses["playlet/collect/list"] = gin.H{"error": "加载失败"}
log.Error("fetch profile favorites failed", log.E(err))
data["favoritesUnavailable"] = true
}
}
func applyProfileInfo(data, responses gin.H, user *authser.LoginResp) {
if user == nil {
return
}
// 登录响应可能包含 token;调试区域只展示非凭证字段。
rawUser, _ := json.Marshal(user)
var safeUser map[string]any
_ = json.Unmarshal(rawUser, &safeUser)
delete(safeUser, "token")
responses["auth/getInfo"] = gin.H{"code": 200, "data": safeUser}
name := strings.TrimSpace(user.NickName)
if name == "" {
name = strings.TrimSpace(user.AccountNo)
}
if name == "" {
name = "用户"
}
data["userName"] = name
data["setting"] = authser.BuildSettingVM(user)
data["userID"] = user.UserID
data["userId"] = fmt.Sprint(user.UserID)
data["balance"] = fmt.Sprint(user.Recharge)
data["recharge"] = user.Recharge
data["rechargeText"] = authser.FormatRecharge(user.Recharge)
if avatar := safeProfileImage(user.Avatar); avatar != "" {
data["avatar"] = avatar
}
}
func safeProfileImage(raw string) string {
raw = strings.TrimSpace(raw)
if strings.HasPrefix(raw, "/static/") {
return raw
}
u, err := url.Parse(raw)
if err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" {
return raw
}
return ""
}
func profileHistoryWorks(items []playletser.PlayletHistoryData) []profileWork {
works := make([]profileWork, 0, len(items))
for _, item := range items {
if len(works) == 6 {
break
}
name := strings.TrimSpace(item.Title)
if name == "" {
name = strings.TrimSpace(item.PlayName)
}
if name == "" {
continue
}
progress := 0
if item.TotalEpisode > 0 {
progress = item.WatchedCount * 100 / item.TotalEpisode
}
if progress < 0 {
progress = 0
}
if progress > 100 {
progress = 100
}
meta := "观看记录"
if item.LastEpisode > 0 {
meta = fmt.Sprintf("看至第 %d 集", item.LastEpisode)
}
remaining := ""
if item.UnwatchedCount > 0 {
remaining = fmt.Sprintf("剩 %d 集", item.UnwatchedCount)
}
works = append(works, profileWork{Title: name, Meta: meta, Image: profileCover(item.Cover), Progress: fmt.Sprintf("%d%%", progress), Remaining: remaining, URL: profilePlayURL(item.Slug, item.ID, item.LastEpisode)})
}
return works
}
func profileFavoriteWorks(items []playletser.PlayletCollectData) []profileWork {
works := make([]profileWork, 0, len(items))
for _, item := range items {
if len(works) == 6 {
break
}
name := strings.TrimSpace(item.Title)
if name == "" {
name = strings.TrimSpace(item.PlayName)
}
if name == "" {
continue
}
work := profileWork{Title: name, Meta: strings.Join(item.ThemeNames, " "), Image: profileCover(item.Cover), URL: profilePlayURL(item.Slug, item.ID, 1)}
if item.TotalEpisode > 0 {
work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode)
}
if item.IsFeatured {
work.Badge = "爆剧"
}
works = append(works, work)
}
return works
}
func profileCover(raw string) string {
if safe := safeProfileImage(raw); safe != "" {
return safe
}
return "/static/images/placeholder.png?v=2"
}
func profilePlayURL(slug string, id int64, episode int) string {
if slug == "" {
if id <= 0 {
return ""
}
slug = fmt.Sprint(id)
}
if episode < 1 {
episode = 1
}
return "/play/" + url.PathEscape(slug) + "/" + fmt.Sprint(episode)
}
-87
View File
@@ -1,87 +0,0 @@
package web
import (
"fmt"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type purchasedWork struct {
Title, Image, Info, Date, Progress, URL string
}
// PurchasedSeries 已购剧集:GET /api/app/playlet/bought/list。
// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_bought_list
func PurchasedSeries(c *gin.Context) {
page, validPage := accountPageNum(c)
if !validPage {
NotFound(c)
return
}
data := gin.H{"title": "已购剧集 - 黄果短剧", "navActive": "purchased", "accountTab": "purchased"}
responses := gin.H{}
loadAccountSummary(c, data, responses)
size := 20
if useragent.TokenFromRequest(c.Request) == "" {
responses["playlet/bought/list"] = gin.H{"error": "暂无登录会话"}
data["purchasedResponse"] = responses["playlet/bought/list"]
HTMLOK(c, "purchased.html", data)
return
}
list, code, err := playletser.FetchBoughtList(c, page, size)
if retry, _ := authser.OnSessionExpired(c, code); retry {
list, _, err = playletser.FetchBoughtList(c, page, size)
}
if err == nil && list != nil {
responses["playlet/bought/list"] = gin.H{"code": 200, "data": list}
data["paging"] = makeAccountPage("/purchased", page, size, list.TotalCount)
works := make([]purchasedWork, 0, len(list.List))
for _, item := range list.List {
title := strings.TrimSpace(item.PlayName)
if title == "" {
title = strings.TrimSpace(item.Title)
}
if title == "" {
continue
}
info := fmt.Sprintf("已购 %d 集", item.BoughtChapterCount)
if item.TotalEpisode > 0 {
info += fmt.Sprintf(" / 共 %d 集", item.TotalEpisode)
}
info += fmt.Sprintf(" · %d 金币", item.CostCoins)
progress := 0
if item.TotalEpisode > 0 {
progress = item.WatchedCount * 100 / item.TotalEpisode
}
if progress < 0 {
progress = 0
}
if progress > 100 {
progress = 100
}
works = append(works, purchasedWork{
Title: title, Image: profileCover(item.Cover), Info: info,
Date: playletser.FormatHistoryAt(item.BoughtAt), Progress: fmt.Sprintf("%d%%", progress),
URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID),
})
}
data["purchased"] = works
} else if err != nil {
responses["playlet/bought/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
log.Error("fetch bought playlets failed", log.E(err))
data["purchasedUnavailable"] = true
} else {
responses["playlet/bought/list"] = gin.H{"code": code, "data": list}
}
data["purchasedResponse"] = responses["playlet/bought/list"]
HTMLOK(c, "purchased.html", data)
}
-279
View File
@@ -1,279 +0,0 @@
package web
import (
"net/http"
"net/url"
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/rechargeser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
const rechargePageSize = 5
// Recharge 金币充值页:套餐 GET /api/app/recharge/package/list,购买 POST /api/app/coin/buy,流水 GET /api/app/coin/trans/list。
func Recharge(c *gin.Context) {
data := gin.H{
"title": "金币充值 - 黄果短剧", "pageHeading": "金币充值",
"backHref": "/profile", "helpHref": "/help",
}
responses := gin.H{}
loadAccountSummary(c, data, responses)
packages, err := rechargeser.FetchPackageList(c)
if err != nil {
log.Error("fetch recharge packages failed", log.E(err))
data["packageUnavailable"] = true
packages = &rechargeser.RechargePackageListResp{}
responses["recharge/package/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
} else {
responses["recharge/package/list"] = gin.H{"code": 200, "data": packages}
}
vm := rechargeser.BuildDepositPageVM(packages, "coin")
data["packages"] = vm.CoinList
data["paymentMethods"] = vm.PayMethods
data["rechargePackageResponse"] = packages
balance := authser.RechargeFromCookies(c)
if _, ok := data["recharge"]; ok {
balance = toInt64(data["recharge"])
}
data["balance"] = balance
data["balanceText"] = authser.FormatRecharge(balance)
page, _, err := fetchRechargeTransactions(c, 1, rechargePageSize, 0)
if err != nil {
log.Error("fetch recharge transactions failed", log.E(err))
data["transactionsUnavailable"] = true
page = &rechargeser.CoinTransPage{PageNum: 1, PageSize: rechargePageSize}
responses["coin/trans/list"] = gin.H{"error": "加载失败", "msg": err.Error()}
} else {
responses["coin/trans/list"] = gin.H{"code": 200, "data": page}
}
data["transactions"] = rechargeser.BuildTxListFromCoinTrans(page.List, 0)
data["transactionPage"] = page
data["transactionResponse"] = page
data["rechargeDebug"] = responses
HTMLOK(c, "recharge.html", data)
}
// RechargeCoinBuy BFF:代理 POST /api/app/coin/buy。
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/post_api_app_coin_buy
func RechargeCoinBuy(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
var body rechargeser.BuyPayReq
if err := c.ShouldBindJSON(&body); err != nil || body.PackageID <= 0 {
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "packageId required", "tip": "请选择充值套餐", "data": nil})
return
}
body.PayMode = rechargeser.NormalizePayMode(body.PayMode)
if body.PayMode == "" {
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "payMode required", "tip": "请选择支付方式", "data": nil})
return
}
if !validCoinPackagePayment(c, body.PackageID, body.PayMode) {
c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "invalid package/payMode", "tip": "套餐或支付方式不可用,请刷新后重试", "data": nil})
return
}
env, err := rechargeser.BuyCoin(c, body)
if err != nil {
log.Error("buy coin failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil})
return
}
if env != nil {
if retry, needLogin := authser.OnSessionExpired(c, env.Code); needLogin {
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": "登录已过期,请重新登录", "data": nil})
return
} else if retry {
env, err = rechargeser.BuyCoin(c, body)
if err != nil {
log.Error("buy coin retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil})
return
}
}
}
if env == nil || env.Code != 200 {
code, tip, msg := 500, "创建支付订单失败", "buy failed"
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmptyAuth(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{"code": code, "msg": msg, "tip": tip, "data": nil})
return
}
safeURL := safePaymentURL(env.Data.PayURL)
if safeURL == "" {
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "invalid payUrl", "tip": "支付地址无效,请稍后重试", "data": nil})
return
}
env.Data.PayURL = safeURL
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": env.Msg,
"tip": firstNonEmptyAuth(env.Tip, "订单创建成功"),
"data": env.Data,
})
}
// RechargeTransactions BFF:代理 GET /api/app/coin/trans/list。
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_list
// query:page / pageSize / type(0全部 1充值 2消费 3奖励)
func RechargeTransactions(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
page := queryInt(c, "page", 1)
pageSize := queryInt(c, "pageSize", rechargePageSize)
typ := queryIntAllowZero(c, "type", 0)
if typ < 0 || typ > 3 {
typ = 0
}
result, code, err := fetchRechargeTransactions(c, page, pageSize, typ)
if err != nil {
log.Error("fetch recharge transactions failed", log.E(err))
tip := "交易明细加载失败"
if code > 0 {
c.JSON(http.StatusOK, gin.H{"code": code, "msg": err.Error(), "tip": tip, "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": tip, "data": nil})
return
}
if result == nil {
result = &rechargeser.CoinTransPage{PageNum: page, PageSize: pageSize}
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "ok",
"tip": "",
"data": gin.H{
"list": rechargeser.BuildTxListFromCoinTrans(result.List, 0),
"rawList": result.List,
"pageNum": result.PageNum,
"pageSize": result.PageSize,
"totalCount": result.TotalCount,
},
})
}
// RechargeCoinTransExport BFF:代理 GET /api/app/coin/trans/export。
// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_export
// query:type(0全部 1充值 2消费 3奖励),返回 CSV。
func RechargeCoinTransExport(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil})
return
}
typ := queryIntAllowZero(c, "type", 0)
if typ < 0 || typ > 3 {
typ = 0
}
result, err := rechargeser.FetchCoinTransExport(c, typ)
if err != nil {
log.Error("export coin trans failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "导出失败,请稍后重试", "data": nil})
return
}
ct := strings.TrimSpace(result.ContentType)
if ct == "" || strings.Contains(strings.ToLower(ct), "json") {
ct = "text/csv; charset=utf-8"
}
disposition := strings.TrimSpace(result.ContentDisposition)
if disposition == "" {
disposition = `attachment; filename="coin-trans.csv"`
}
c.Header("Content-Type", ct)
c.Header("Content-Disposition", disposition)
c.Header("Cache-Control", "no-store")
c.Data(http.StatusOK, ct, result.Body)
}
func fetchRechargeTransactions(c *gin.Context, page, pageSize, typ int) (*rechargeser.CoinTransPage, int, error) {
result, code, err := rechargeser.FetchCoinTransPage(c, page, pageSize, typ)
if retry, _ := authser.OnSessionExpired(c, code); retry {
result, code, err = rechargeser.FetchCoinTransPage(c, page, pageSize, typ)
}
return result, code, err
}
// attachRechargeModal 注入充值弹窗数据:GET /api/app/recharge/package/list → coinList。
func attachRechargeModal(c *gin.Context, data gin.H) {
if data == nil {
return
}
packages := rechargeser.FetchPackageListOrEmpty(c)
vm := rechargeser.BuildDepositPageVM(packages, "coin")
data["rechargePackages"] = vm.CoinList
data["rechargePayMethods"] = vm.PayMethods
data["rechargePackageResponse"] = packages
if _, ok := data["unlockCost"]; !ok {
data["unlockCost"] = int64(0)
}
if _, ok := data["balance"]; !ok {
data["balance"] = authser.RechargeFromCookies(c)
}
if _, ok := data["balanceText"]; !ok {
data["balanceText"] = authser.FormatRecharge(toInt64(data["balance"]))
}
}
func validCoinPackagePayment(c *gin.Context, packageID int64, payMode string) bool {
data, err := rechargeser.FetchPackageList(c)
if err != nil || data == nil {
return false
}
for _, item := range data.CoinList {
if item.ID != packageID {
continue
}
for _, method := range item.PayMethods {
if rechargeser.NormalizePayMode(method.Type) == payMode {
return true
}
}
return false
}
return false
}
func safePaymentURL(raw string) string {
raw = strings.TrimSpace(raw)
u, err := url.Parse(raw)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return ""
}
return raw
}
func queryInt(c *gin.Context, name string, fallback int) int {
v, err := strconv.Atoi(strings.TrimSpace(c.Query(name)))
if err != nil || v <= 0 {
return fallback
}
return v
}
// queryIntAllowZero 允许 0(如 type=0 全部)。
func queryIntAllowZero(c *gin.Context, name string, fallback int) int {
raw := strings.TrimSpace(c.Query(name))
if raw == "" {
return fallback
}
v, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
return v
}
-11
View File
@@ -1,11 +0,0 @@
package web
import "github.com/gin-gonic/gin"
// RegisterPage H5 注册页:渲染 template/pages/register.html。
// PC 端由页面内脚本回首页并打开 registerPop;H5 直接展示本页。
func RegisterPage(c *gin.Context) {
HTMLOK(c, "register.html", gin.H{
"title": "注册账号 - 黄果短剧",
})
}
+1 -61
View File
@@ -4,7 +4,6 @@ import (
"net/http"
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/configser"
"videoweb/app/service/playletser"
"videoweb/app/service/seoser"
@@ -14,68 +13,12 @@ import (
"github.com/gin-gonic/gin"
)
// HTML 渲染页面并注入登录态(以 cookie 为准)。
// 无 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。
// HTML 渲染页面并注入公共导航、配置与 SEO 数据。
func HTML(c *gin.Context, code int, name string, data gin.H) {
if data == nil {
data = gin.H{}
}
// 所有服务端页面先完成渠道首次归因;已有会话也不会漏记落地页渠道。
if channel := authser.ResolveChannel(c, ""); channel != "" {
data["channel"] = channel
}
authser.SyncUserOnEnter(c)
loggedIn, userName, avatar, email := authser.AuthFromCookies(c)
registered := authser.IsRegisteredFromCookies(c)
data["isLoggedIn"] = loggedIn
data["isRegistered"] = registered
// 已登录(含游客)展示用户菜单;未登录才展示「登录」按钮。
if _, ok := data["guestHeader"]; !ok {
data["guestHeader"] = !loggedIn
}
if _, ok := data["isVip"]; !ok {
data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c)
}
isVip, _ := data["isVip"].(bool)
data["isVip"] = isVip
if loggedIn {
if _, provided := data["userName"]; !provided && userName != "" {
data["userName"] = userName
}
if _, provided := data["avatar"]; !provided && avatar != "" {
data["avatar"] = avatar
}
if email != "" {
data["email"] = email
}
if _, ok := data["userId"]; !ok {
if uid := authser.UserIDFromCookies(c); uid > 0 {
data["userId"] = authser.FormatUserID(uid)
}
}
if _, ok := data["recharge"]; !ok {
recharge := authser.RechargeFromCookies(c)
data["recharge"] = recharge
data["rechargeText"] = authser.FormatRecharge(recharge)
} else if _, ok := data["rechargeText"]; !ok {
data["rechargeText"] = authser.FormatRecharge(toInt64(data["recharge"]))
}
if _, ok := data["vipType"]; !ok {
data["vipType"] = authser.VipTypeFromCookies(c)
}
} else {
delete(data, "userName")
delete(data, "avatar")
delete(data, "email")
delete(data, "userId")
delete(data, "recharge")
delete(data, "rechargeText")
delete(data, "vipType")
}
// 导航分类:GET /api/app/translation/category/list → categoryList(包含分类 seoInfo)
if _, ok := data["categoryList"]; !ok {
data["categoryList"] = translationser.FetchHeaderCategoryListOrDefault(c)
@@ -119,9 +62,6 @@ func HTML(c *gin.Context, code int, name string, data gin.H) {
}
}
data["trackerAppId"] = configser.ResolveTrackerAppID()
attachTrackerPage(c.Request.URL.Path, code, data)
attachPageSEO(c, code, data)
c.HTML(code, name, data)
}
-85
View File
@@ -1,85 +0,0 @@
package web
import (
"net/http"
"strings"
"videoweb/app/service/authser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// Setting 展示账号设置,字段来自最新的用户信息接口。
func Setting(c *gin.Context) {
data := gin.H{"title": "账号设置 - 黄果短剧", "navActive": "setting", "accountTab": "setting"}
loadAccountSummary(c, data, gin.H{})
HTMLOK(c, "setting.html", data)
}
// UpdateProfile 代理账号资料修改。可选字段必须显式传入,避免覆盖未修改内容。
func UpdateProfile(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "tip": "请先登录"})
return
}
var body authser.UpdateProfileReq
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "参数错误"})
return
}
if body.NickName != nil {
v := strings.TrimSpace(*body.NickName)
if v == "" || len([]rune(v)) > 40 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "昵称须为 1 至 40 个字符"})
return
}
body.NickName = &v
}
if body.Introduction != nil {
v := strings.TrimSpace(*body.Introduction)
if len([]rune(v)) > 300 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "简介不能超过 300 个字符"})
return
}
body.Introduction = &v
}
if body.Avatar != nil {
v := strings.TrimSpace(*body.Avatar)
if safeProfileImage(v) == "" || strings.HasPrefix(v, "/static/") {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "头像地址无效"})
return
}
body.Avatar = &v
}
if (body.OldPassword == nil) != (body.NewPassword == nil) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "请输入旧密码和新密码"})
return
}
if body.OldPassword != nil && (*body.OldPassword == "" || *body.NewPassword == "") {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "密码不能为空"})
return
}
if body.NickName == nil && body.Introduction == nil && body.Avatar == nil && body.OldPassword == nil && body.PrivacyMode == nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "没有需要保存的修改"})
return
}
env, err := authser.UpdateProfile(c, body)
if err != nil {
log.Error("update profile failed", log.E(err))
c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"})
return
}
if env == nil || env.Code != 200 {
if env == nil {
c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"})
return
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "tip": firstNonEmptyAuth(env.Tip, env.Msg, "保存失败")})
return
}
if infoEnv, user, err := authser.GetInfo(c); err == nil && infoEnv != nil && infoEnv.Code == 200 && user != nil {
authser.SetAuthCookies(c, user)
}
c.JSON(http.StatusOK, gin.H{"code": 200, "tip": firstNonEmptyAuth(env.Tip, "保存成功")})
}
-80
View File
@@ -1,80 +0,0 @@
package web
import (
"net/http"
"strings"
)
// attachTrackerPage 为所有 HTML 页面补齐稳定的 pageKey/pageName,避免事件落入 unknown。
func attachTrackerPage(path string, code int, data map[string]any) {
if data == nil {
return
}
key, name := trackerPageFromRequest(path, code)
if current, _ := data["pageKey"].(string); strings.TrimSpace(current) == "" {
data["pageKey"] = key
}
if current, _ := data["pageName"].(string); strings.TrimSpace(current) == "" {
data["pageName"] = name
}
}
func trackerPageFromRequest(path string, code int) (string, string) {
if code >= http.StatusInternalServerError {
return "server_error", "服务异常页面"
}
if code >= http.StatusBadRequest {
return "not_found", "404页面"
}
return trackerPageFromPath(path)
}
func trackerPageFromPath(path string) (string, string) {
p := strings.TrimSuffix(strings.TrimSpace(path), "/")
if p == "" {
return "home", "首页"
}
rules := []struct {
prefix string
key string
name string
}{
{"/series-play", "series_play", "AI成人剧集播放页"},
{"/series", "series", "AI成人剧集"},
{"/play", "play", "短剧播放页"},
{"/detail", "detail", "短剧详情"},
{"/list", "list", "短剧列表"},
{"/recently-added", "recently_added", "最新短剧"},
{"/popular-recommend", "popular_recommend", "热门推荐"},
{"/rank", "rank", "排行榜"},
{"/topic-detail", "topic_detail", "专题详情"},
{"/topic", "topic", "专题列表"},
{"/tags", "tags", "标签详情"},
{"/more-series-list", "more_series_list", "成人剧集列表"},
{"/more-list", "more_list", "短剧扩展列表"},
{"/search-mobile", "search", "搜索结果页"},
{"/search", "search", "搜索结果页"},
{"/history", "history", "观看历史"},
{"/favorites", "favorites", "我的收藏"},
{"/purchased", "purchased", "已购剧集"},
{"/profile", "profile", "个人中心"},
{"/setting", "setting", "设置"},
{"/recharge", "recharge", "充值中心"},
{"/download-app", "download_app", "下载APP"},
{"/feedback", "feedback", "问题反馈"},
{"/help", "help", "帮助中心"},
{"/about", "about", "关于我们"},
{"/terms", "terms", "用户协议"},
{"/privacy", "privacy", "隐私政策"},
{"/2257", "compliance_2257", "2257合规声明"},
{"/dmca", "dmca", "DMCA版权声明"},
{"/login", "login", "登录"},
{"/register", "register", "注册"},
}
for _, rule := range rules {
if p == rule.prefix || strings.HasPrefix(p, rule.prefix+"/") {
return rule.key, rule.name
}
}
return "internal_other", "站内其他页面"
}
-64
View File
@@ -1,64 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/videoser"
"github.com/gin-gonic/gin"
)
// VideoCollectSet BFF:代理 POST /api/app/video/collect/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_collect_set
func VideoCollectSet(c *gin.Context) {
var body struct {
ID int64 `json:"id"`
IsCollect *bool `json:"isCollect"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsCollect == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "id/isCollect required",
"tip": "缺少视频 ID 或收藏状态",
"data": nil,
})
return
}
env, err := videoser.CollectSet(c, videoser.VideoCollectSetReq{
ID: body.ID,
IsCollect: *body.IsCollect,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}
-64
View File
@@ -1,64 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/videoser"
"github.com/gin-gonic/gin"
)
// VideoLikeSet BFF:代理 POST /api/app/video/like/set。
// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_like_set
func VideoLikeSet(c *gin.Context) {
var body struct {
ID int64 `json:"id"`
IsLike *bool `json:"isLike"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid body",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "id/isLike required",
"tip": "缺少视频 ID 或点赞状态",
"data": nil,
})
return
}
env, err := videoser.LikeSet(c, videoser.VideoLikeSetReq{
ID: body.ID,
IsLike: *body.IsLike,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": err.Error(),
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env == nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty response",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": env.Code,
"msg": env.Msg,
"tip": env.Tip,
"data": env.Data,
})
}