Initial commit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fish03
2026-09-23 14:43:16 +07:00
co-authored by Claude Opus 5.5
commit bef9f8b4ed
663 changed files with 81305 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
package commentser
import (
"fmt"
"videoweb/app/service/upstream"
"github.com/gin-gonic/gin"
)
const likeSetPath = "/api/app/comment/like/set"
// LikeSet 设置评论点赞/取消点赞。
func LikeSet(c *gin.Context, req CommentLikeSetReq) (*APIEnvelope[CommentLikeSetResp], error) {
if req.CommentID <= 0 {
return nil, fmt.Errorf("comment like invalid commentId")
}
var env APIEnvelope[CommentLikeSetResp]
if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil {
return nil, err
}
return &env, nil
}
+145
View File
@@ -0,0 +1,145 @@
package commentser
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
"videoweb/app/service/authser"
"videoweb/app/service/upstream"
"videoweb/pub/common/log"
"github.com/gin-gonic/gin"
)
const (
listPath = "/api/app/comment/list"
defaultCommentSize = 10
)
// ListQuery 评论列表查询参数。
type ListQuery struct {
ObjectID int64
ObjectType int
PageNum int
PageSize int
SortOrder int
}
// Normalize 校正分页与排序默认值。
func (q *ListQuery) Normalize() {
if q.PageNum <= 0 {
q.PageNum = 1
}
if q.PageSize <= 0 {
q.PageSize = defaultCommentSize
}
if q.PageSize > 50 {
q.PageSize = 50
}
if q.ObjectType != ObjectTypeChapter {
q.ObjectType = ObjectTypePlaylet
}
if q.SortOrder != SortLatest {
q.SortOrder = SortHot
}
}
// FetchList 分页查询评论。
func FetchList(c *gin.Context, q ListQuery) (*GetCommentListResp, error) {
q.Normalize()
if q.ObjectID <= 0 {
return nil, fmt.Errorf("comment list invalid objectId")
}
params := url.Values{}
params.Set("objectId", strconv.FormatInt(q.ObjectID, 10))
params.Set("objectType", strconv.Itoa(q.ObjectType))
params.Set("pageNum", strconv.Itoa(q.PageNum))
params.Set("pageSize", strconv.Itoa(q.PageSize))
params.Set("sortOrder", strconv.Itoa(q.SortOrder))
authser.EnsureGuestOnEnter(c)
requestPath := listPath + "?" + params.Encode()
env, err := fetchListOnce(c, requestPath)
if err != nil {
return nil, err
}
if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) && authser.RecoverContentSession(c, env.Code) {
env, err = fetchListOnce(c, requestPath)
if err != nil {
return nil, err
}
}
if env.Code != 200 {
return nil, fmt.Errorf("comment list code=%d msg=%s", env.Code, env.Msg)
}
return &env.Data, nil
}
func fetchListOnce(c *gin.Context, path string) (*APIEnvelope[GetCommentListResp], error) {
var env APIEnvelope[GetCommentListResp]
if err := upstream.GetJSON(c, path, &env); err != nil {
return nil, err
}
return &env, nil
}
// FetchListOrEmpty 失败时返回空列表。
func FetchListOrEmpty(c *gin.Context, q ListQuery) *GetCommentListResp {
data, err := FetchList(c, q)
if err != nil {
log.Error("fetch comment list failed", log.E(err))
return &GetCommentListResp{
ObjectID: q.ObjectID,
PageNum: q.PageNum,
PageSize: q.PageSize,
}
}
if data == nil {
return &GetCommentListResp{
ObjectID: q.ObjectID,
PageNum: q.PageNum,
PageSize: q.PageSize,
}
}
return data
}
// ParseSortOrder 解析站点评论排序(1 热门 / 2 最新)。
func ParseSortOrder(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil || n != SortLatest {
return SortHot
}
return SortLatest
}
// FormatCreatedAt 展示用时间格式化(非字段映射)。
func FormatCreatedAt(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
layouts := []string{
time.RFC3339Nano,
time.RFC3339,
"2006-01-02T15:04:05Z07:00",
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
if t, err := time.Parse(layout, raw); err == nil {
return t.Local().Format("2006/1/2 15:04")
}
}
return raw
}
// AvatarOrDefault 头像空时回退默认图。
func AvatarOrDefault(avatar string) string {
avatar = strings.TrimSpace(avatar)
if avatar == "" {
return "/static/images/avatar.png"
}
return avatar
}
+45
View File
@@ -0,0 +1,45 @@
package commentser
import (
"fmt"
"strings"
"unicode/utf8"
"videoweb/app/service/upstream"
"github.com/gin-gonic/gin"
)
const sendPath = "/api/app/comment/send"
// Send 发布评论(一级 / 二级)。
func Send(c *gin.Context, req CommentSendReq) (*APIEnvelope[any], error) {
req.Text = strings.TrimSpace(req.Text)
if req.ObjectID <= 0 {
return nil, fmt.Errorf("comment send invalid objectId")
}
if req.ObjectType != ObjectTypeChapter {
req.ObjectType = ObjectTypePlaylet
}
if req.Text == "" {
return nil, fmt.Errorf("comment send empty text")
}
if utf8.RuneCountInString(req.Text) > 200 {
return nil, fmt.Errorf("comment send text too long")
}
if req.ParentsID < 0 {
req.ParentsID = 0
}
if req.ReplyCommentID < 0 {
req.ReplyCommentID = 0
}
// 二级评论:未显式指定 replyCommentId 时默认回复父评论
if req.ParentsID > 0 && req.ReplyCommentID <= 0 {
req.ReplyCommentID = req.ParentsID
}
var env APIEnvelope[any]
if _, err := upstream.PostJSON(c, sendPath, req, &env); err != nil {
return nil, err
}
return &env, nil
}
+11
View File
@@ -0,0 +1,11 @@
package commentser
import "html/template"
// TemplateFuncs 评论展示辅助。
func TemplateFuncs() template.FuncMap {
return template.FuncMap{
"formatCommentTime": FormatCreatedAt,
"commentAvatar": AvatarOrDefault,
}
}
+71
View File
@@ -0,0 +1,71 @@
package commentser
// CommentSendReq 对应 swagger proto.CommentSendReq
// parentsId 为 0 发一级评论,大于 0 发二级评论;replyCommentId 为实际被回复的评论。
type CommentSendReq struct {
ObjectID int64 `json:"objectId"`
ObjectType int `json:"objectType"`
Text string `json:"text"`
ParentsID int64 `json:"parentsId"`
ReplyCommentID int64 `json:"replyCommentId"`
}
// CommentLikeSetReq 对应 swagger proto.CommentLikeSetReq
type CommentLikeSetReq struct {
CommentID int64 `json:"commentId"`
IsLike bool `json:"isLike"`
}
// CommentLikeSetResp 对应 swagger proto.CommentLikeSetResp
type CommentLikeSetResp struct {
CommentID int64 `json:"commentId"`
CountLike int64 `json:"countLike"`
IsLike int `json:"isLike"`
}
// CommentInfo 对应 swagger proto.CommentInfo
type CommentInfo struct {
ID int64 `json:"id"`
ObjectID int64 `json:"objectId"`
ObjectType int `json:"objectType"`
Text string `json:"text"`
UserID int64 `json:"userId"`
UserName string `json:"userName"`
UserAvatar string `json:"userAvatar"`
CountLike int64 `json:"countLike"`
CommentCount int64 `json:"commentCount"`
IsLike int `json:"isLike"`
CreatedAt string `json:"createdAt"`
Level int `json:"level,omitempty"`
ParentsID int64 `json:"parentsId,omitempty"`
ReplyCommentID int64 `json:"replyCommentId,omitempty"`
ReplyUserName string `json:"replyUserName,omitempty"`
Children []CommentInfo `json:"children,omitempty"`
}
// GetCommentListResp 对应 swagger proto.GetCommentListResp
type GetCommentListResp struct {
List []CommentInfo `json:"list"`
ObjectID int64 `json:"objectId"`
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
TotalCount int64 `json:"totalCount"`
}
// APIEnvelope 上游统一响应壳
type APIEnvelope[T any] struct {
Code int `json:"code"`
Msg string `json:"msg"`
Tip string `json:"tip"`
Data T `json:"data"`
}
const (
// ObjectTypePlaylet 短剧评论
ObjectTypePlaylet = 10
// ObjectTypeChapter 短剧剧集评论
ObjectTypeChapter = 11
SortHot = 1 // 热门
SortLatest = 2 // 最新
)