The moderation system now uses a "pass/review" strategy instead of "pass/review/block", treating hard violations the same as review (only routing to manual review rather than auto-rejecting). Add fallback-approve behavior in the hook system so slow moderation APIs never silently block normal content when they time out or get cancelled - timed-out moderation now approves with "ReviewedBy: system" instead of leaving the zero-value result that would reject content. Update post/comment/vote services and handlers to use the new unified behavior. Simplify moderation prompts to emphasize "宁可放过,不可误伤" (prefer false positives over false negatives).
217 lines
4.9 KiB
Go
217 lines
4.9 KiB
Go
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 {
|
||
// 幂等命中"进行中"占位:提示客户端稍候,避免重复提交
|
||
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})
|
||
}
|