Files
backend/internal/handler/vote_handler.go
lafay d8e56e60dc
All checks were successful
Build Backend / build (push) Successful in 3m42s
Build Backend / build-docker (push) Successful in 1m7s
feat(post): add idempotency support for post creation with client request ID
Add client_request_id field to post creation endpoints (CreateVotePost and CreatePostWithSegments).
When provided, the server uses Redis SetNX-based idempotency to prevent duplicate posts
from double-clicks or network retries within a 24-hour TTL. Duplicate requests return
the originally created post; concurrent in-flight requests return 429 "正在发布中,请稍候".
2026-06-26 17:00:38 +08:00

222 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"with_you/internal/dto"
"with_you/internal/pkg/response"
"with_you/internal/service"
)
// VoteHandler 投票处理器
type VoteHandler struct {
voteService service.VoteService
postService service.PostService
}
// NewVoteHandler 创建投票处理器
func NewVoteHandler(voteService service.VoteService, postService service.PostService) *VoteHandler {
return &VoteHandler{
voteService: voteService,
postService: postService,
}
}
// CreateVotePost 创建投票帖子
// POST /api/v1/posts/vote
func (h *VoteHandler) CreateVotePost(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "请先登录")
return
}
var req dto.CreateVotePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
post, err := h.voteService.CreateVotePost(c.Request.Context(), userID, &req)
if err != nil {
var moderationErr *service.PostModerationRejectedError
if errors.As(err, &moderationErr) {
response.BadRequest(c, moderationErr.UserMessage())
return
}
// 幂等命中"进行中"占位:提示客户端稍候,避免重复提交
if errors.Is(err, service.ErrDuplicatePostRequest) {
response.ErrorWithStatusCode(c, http.StatusTooManyRequests, 429, err.Error())
return
}
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, post)
}
// GetVoteResult 获取投票结果
// GET /api/v1/posts/:id/vote
func (h *VoteHandler) GetVoteResult(c *gin.Context) {
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "帖子ID不能为空")
return
}
// 验证帖子存在
_, err := h.postService.GetByID(c.Request.Context(), postID)
if err != nil {
response.NotFound(c, "帖子不存在")
return
}
// 获取当前用户ID可选登录
userID := c.GetString("user_id")
// 如果用户未登录返回不带has_voted的结果
if userID == "" {
options, err := h.voteService.GetVoteOptions(postID)
if err != nil {
response.InternalServerError(c, "获取投票选项失败")
return
}
// 计算总票数
totalVotes := 0
for _, option := range options {
totalVotes += option.VotesCount
}
result := &dto.VoteResultDTO{
Options: options,
TotalVotes: totalVotes,
HasVoted: false,
}
response.Success(c, result)
return
}
// 用户已登录,获取完整的投票结果
result, err := h.voteService.GetVoteResult(postID, userID)
if err != nil {
response.InternalServerError(c, "获取投票结果失败")
return
}
response.Success(c, result)
}
// Vote 投票
// POST /api/v1/posts/:id/vote
func (h *VoteHandler) Vote(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "请先登录")
return
}
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "帖子ID不能为空")
return
}
// 验证帖子存在
_, err := h.postService.GetByID(c.Request.Context(), postID)
if err != nil {
response.NotFound(c, "帖子不存在")
return
}
// 解析请求体
var req struct {
OptionID string `json:"option_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.voteService.Vote(c.Request.Context(), postID, userID, req.OptionID); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, gin.H{"success": true})
}
// Unvote 取消投票
// DELETE /api/v1/posts/:id/vote
func (h *VoteHandler) Unvote(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "请先登录")
return
}
postID := c.Param("id")
if postID == "" {
response.BadRequest(c, "帖子ID不能为空")
return
}
// 验证帖子存在
_, err := h.postService.GetByID(c.Request.Context(), postID)
if err != nil {
response.NotFound(c, "帖子不存在")
return
}
if err := h.voteService.Unvote(c.Request.Context(), postID, userID); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
response.Success(c, gin.H{"success": true})
}
// UpdateVoteOption 更新投票选项(仅作者)
// PUT /api/v1/vote-options/:id
func (h *VoteHandler) UpdateVoteOption(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "请先登录")
return
}
optionID := c.Param("id")
if optionID == "" {
response.BadRequest(c, "选项ID不能为空")
return
}
// 解析请求体
var req struct {
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 获取帖子ID从查询参数或请求体中获取
postID := c.Query("post_id")
if postID == "" {
response.BadRequest(c, "帖子ID不能为空")
return
}
if err := h.voteService.UpdateVoteOption(c.Request.Context(), postID, optionID, userID, req.Content); err != nil {
response.Error(c, http.StatusForbidden, err.Error())
return
}
response.Success(c, gin.H{"success": true})
}