435 lines
11 KiB
Go
435 lines
11 KiB
Go
package forumapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/LoveLosita/smartflow/backend/gateway/shared/respond"
|
|
contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclassforum"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
requestTimeout = 2 * time.Second
|
|
forumLikeRewardAmount = int64(1)
|
|
forumImportRewardAmount = int64(5)
|
|
rewardHintStatusActive = "rule_active"
|
|
)
|
|
|
|
type ForumClient interface {
|
|
ListPosts(ctx context.Context, actorUserID uint64, page int, pageSize int, sort string, keyword string, tag string) ([]contracts.ForumPostBrief, contracts.PageResult, error)
|
|
ListTags(ctx context.Context, actorUserID uint64, limit int) ([]contracts.ForumTagItem, error)
|
|
CreatePost(ctx context.Context, req contracts.CreateForumPostRequest) (*contracts.ForumPostBrief, error)
|
|
GetPost(ctx context.Context, actorUserID uint64, postID uint64) (*contracts.ForumPostDetail, error)
|
|
LikePost(ctx context.Context, actorUserID uint64, postID uint64) (contracts.ForumPostCounters, contracts.ForumPostViewerState, error)
|
|
UnlikePost(ctx context.Context, actorUserID uint64, postID uint64) (contracts.ForumPostCounters, contracts.ForumPostViewerState, error)
|
|
ListComments(ctx context.Context, actorUserID uint64, postID uint64, page int, pageSize int, sort string) ([]contracts.ForumCommentNode, contracts.PageResult, error)
|
|
CreateComment(ctx context.Context, req contracts.CreateForumCommentRequest) (*contracts.ForumCommentNode, error)
|
|
DeleteComment(ctx context.Context, actorUserID uint64, commentID uint64) (*contracts.DeleteForumCommentResult, error)
|
|
ImportPost(ctx context.Context, req contracts.ImportForumPostRequest) (*contracts.ImportForumPostResult, error)
|
|
}
|
|
|
|
type Handler struct {
|
|
client ForumClient
|
|
}
|
|
|
|
func NewHandler(client ForumClient) *Handler {
|
|
return &Handler{client: client}
|
|
}
|
|
|
|
type pageEnvelope[T any] struct {
|
|
Items []T `json:"items"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Total int `json:"total"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|
|
|
|
type interactionEnvelope struct {
|
|
PostID uint64 `json:"post_id"`
|
|
Liked bool `json:"liked"`
|
|
LikeCount int64 `json:"like_count"`
|
|
RewardHint *rewardHint `json:"reward_hint,omitempty"`
|
|
}
|
|
|
|
type rewardHint struct {
|
|
Receiver string `json:"receiver"`
|
|
Status string `json:"status"`
|
|
Amount int64 `json:"amount"`
|
|
}
|
|
|
|
type nextAction struct {
|
|
Type string `json:"type"`
|
|
TaskClassID uint64 `json:"task_class_id"`
|
|
}
|
|
|
|
type importEnvelope struct {
|
|
ImportID uint64 `json:"import_id"`
|
|
PostID uint64 `json:"post_id"`
|
|
NewTaskClassID uint64 `json:"new_task_class_id"`
|
|
TaskClassTitle string `json:"task_class_title"`
|
|
ImportCount int64 `json:"import_count"`
|
|
RewardHint rewardHint `json:"reward_hint"`
|
|
NextAction nextAction `json:"next_action"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
type deleteCommentEnvelope struct {
|
|
CommentID uint64 `json:"comment_id"`
|
|
Status string `json:"status"`
|
|
Content string `json:"content"`
|
|
DeletedAt *string `json:"deleted_at"`
|
|
}
|
|
|
|
type createPostBody struct {
|
|
TaskClassID uint64 `json:"task_class_id"`
|
|
Title string `json:"title"`
|
|
Summary string `json:"summary"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
type createCommentBody struct {
|
|
Content string `json:"content"`
|
|
ParentCommentID *uint64 `json:"parent_comment_id"`
|
|
}
|
|
|
|
type importPostBody struct {
|
|
TargetTitle string `json:"target_title"`
|
|
}
|
|
|
|
func (h *Handler) ListPosts(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
pageValue, ok := intQuery(c, "page")
|
|
if !ok {
|
|
return
|
|
}
|
|
pageSize, ok := intQuery(c, "page_size")
|
|
if !ok {
|
|
return
|
|
}
|
|
items, page, err := client.ListPosts(
|
|
ctx,
|
|
currentUserID(c),
|
|
pageValue,
|
|
pageSize,
|
|
c.Query("sort"),
|
|
c.Query("keyword"),
|
|
c.Query("tag"),
|
|
)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, newPageEnvelope(items, page)))
|
|
}
|
|
|
|
func (h *Handler) ListTags(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
limit, ok := intQuery(c, "limit")
|
|
if !ok {
|
|
return
|
|
}
|
|
items, err := client.ListTags(ctx, currentUserID(c), limit)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, gin.H{"items": items}))
|
|
}
|
|
|
|
func (h *Handler) CreatePost(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body createPostBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
post, err := client.CreatePost(ctx, contracts.CreateForumPostRequest{
|
|
ActorUserID: currentUserID(c),
|
|
TaskClassID: body.TaskClassID,
|
|
Title: body.Title,
|
|
Summary: body.Summary,
|
|
Tags: append([]string(nil), body.Tags...),
|
|
IdempotencyKey: strings.TrimSpace(c.GetHeader("X-Idempotency-Key")),
|
|
})
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, post))
|
|
}
|
|
|
|
func (h *Handler) GetPost(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
detail, err := client.GetPost(ctx, currentUserID(c), postID)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, detail))
|
|
}
|
|
|
|
func (h *Handler) LikePost(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
counters, state, err := client.LikePost(ctx, currentUserID(c), postID)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, interactionEnvelope{
|
|
PostID: postID,
|
|
Liked: state.Liked,
|
|
LikeCount: counters.LikeCount,
|
|
RewardHint: &rewardHint{
|
|
Receiver: "author",
|
|
Status: rewardHintStatusActive,
|
|
Amount: forumLikeRewardAmount,
|
|
},
|
|
}))
|
|
}
|
|
|
|
func (h *Handler) UnlikePost(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
counters, state, err := client.UnlikePost(ctx, currentUserID(c), postID)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, interactionEnvelope{
|
|
PostID: postID,
|
|
Liked: state.Liked,
|
|
LikeCount: counters.LikeCount,
|
|
}))
|
|
}
|
|
|
|
func (h *Handler) ListComments(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
pageValue, ok := intQuery(c, "page")
|
|
if !ok {
|
|
return
|
|
}
|
|
pageSize, ok := intQuery(c, "page_size")
|
|
if !ok {
|
|
return
|
|
}
|
|
items, page, err := client.ListComments(ctx, currentUserID(c), postID, pageValue, pageSize, c.Query("sort"))
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, newPageEnvelope(items, page)))
|
|
}
|
|
|
|
func (h *Handler) CreateComment(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body createCommentBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
comment, err := client.CreateComment(ctx, contracts.CreateForumCommentRequest{
|
|
ActorUserID: currentUserID(c),
|
|
PostID: postID,
|
|
Content: body.Content,
|
|
ParentCommentID: body.ParentCommentID,
|
|
IdempotencyKey: strings.TrimSpace(c.GetHeader("X-Idempotency-Key")),
|
|
})
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, comment))
|
|
}
|
|
|
|
func (h *Handler) DeleteComment(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
commentID, ok := uint64Param(c, "comment_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
result, err := client.DeleteComment(ctx, currentUserID(c), commentID)
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, deleteCommentEnvelope{
|
|
CommentID: result.CommentID,
|
|
Status: result.Status,
|
|
Content: result.Content,
|
|
DeletedAt: result.DeletedAt,
|
|
}))
|
|
}
|
|
|
|
func (h *Handler) ImportPost(c *gin.Context) {
|
|
client, ok := h.ready(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
postID, ok := uint64Param(c, "post_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body importPostBody
|
|
if err := c.ShouldBindJSON(&body); err != nil && !errors.Is(err, io.EOF) {
|
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
result, err := client.ImportPost(ctx, contracts.ImportForumPostRequest{
|
|
ActorUserID: currentUserID(c),
|
|
PostID: postID,
|
|
TargetTitle: body.TargetTitle,
|
|
IdempotencyKey: strings.TrimSpace(c.GetHeader("X-Idempotency-Key")),
|
|
})
|
|
if err != nil {
|
|
respond.DealWithError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, importEnvelope{
|
|
ImportID: result.ImportID,
|
|
PostID: result.PostID,
|
|
NewTaskClassID: result.NewTaskClassID,
|
|
TaskClassTitle: result.TaskClassTitle,
|
|
ImportCount: result.ImportCount,
|
|
RewardHint: rewardHint{
|
|
Receiver: "author",
|
|
Status: rewardHintStatusActive,
|
|
Amount: forumImportRewardAmount,
|
|
},
|
|
NextAction: nextAction{
|
|
Type: "open_task_class",
|
|
TaskClassID: result.NewTaskClassID,
|
|
},
|
|
CreatedAt: result.CreatedAt,
|
|
}))
|
|
}
|
|
|
|
func (h *Handler) ready(c *gin.Context) (ForumClient, bool) {
|
|
if h == nil || h.client == nil {
|
|
c.JSON(http.StatusInternalServerError, respond.InternalError(errors.New("计划广场 gateway client 未初始化")))
|
|
return nil, false
|
|
}
|
|
return h.client, true
|
|
}
|
|
|
|
func currentUserID(c *gin.Context) uint64 {
|
|
userID := c.GetInt("user_id")
|
|
if userID <= 0 {
|
|
return 0
|
|
}
|
|
return uint64(userID)
|
|
}
|
|
|
|
func newPageEnvelope[T any](items []T, page contracts.PageResult) pageEnvelope[T] {
|
|
return pageEnvelope[T]{
|
|
Items: items,
|
|
Page: page.Page,
|
|
PageSize: page.PageSize,
|
|
Total: page.Total,
|
|
HasMore: page.HasMore,
|
|
}
|
|
}
|
|
|
|
func intQuery(c *gin.Context, key string) (int, bool) {
|
|
raw := strings.TrimSpace(c.Query(key))
|
|
if raw == "" {
|
|
return 0, true
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
|
return 0, false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func uint64Param(c *gin.Context, key string) (uint64, bool) {
|
|
value, err := strconv.ParseUint(strings.TrimSpace(c.Param(key)), 10, 64)
|
|
if err != nil || value == 0 {
|
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
|
return 0, false
|
|
}
|
|
return value, true
|
|
}
|