feat(post): add idempotency support for post creation with client request ID
All checks were successful
Build Backend / build (push) Successful in 3m42s
Build Backend / build-docker (push) Successful in 1m7s

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 "正在发布中,请稍候".
This commit is contained in:
lafay
2026-06-26 17:00:38 +08:00
parent 28cfcbf9a8
commit d8e56e60dc
8 changed files with 231 additions and 19 deletions

View File

@@ -66,6 +66,23 @@ func (s *voteService) CreateVotePost(ctx context.Context, userID string, req *dt
return nil, errors.New("投票选项最多10个")
}
// 幂等检查:同一 userID + clientRequestID 在 TTL 内只创建一次。
// 重复请求(多次点击、网络重试)会命中已回填的 postID直接返回首次创建的帖子。
if req.ClientRequestID != "" && s.cache != nil {
idemKey := cache.PostCreateIdempotentKey(userID, req.ClientRequestID)
result := cache.TryAcquireIdempotency(ctx, s.cache, idemKey, PostCreateIdempotentTTL)
if !result.Acquired {
// 命中已完成的请求:返回首次创建的帖子。
if result.ExistingValue != "" {
if existing, err := s.postRepo.GetByID(result.ExistingValue); err == nil && existing != nil {
return s.convertToPostResponse(existing, userID), nil
}
}
// 命中"进行中"占位:前一个请求还在处理,拒绝重复提交。
return nil, ErrDuplicatePostRequest
}
}
// 构建 vote segment
voteOptions := make([]dto.VoteOptionData, len(req.VoteOptions))
for i, opt := range req.VoteOptions {
@@ -103,9 +120,18 @@ func (s *voteService) CreateVotePost(ctx context.Context, userID string, req *dt
err := s.postRepo.Create(post, req.Images)
if err != nil {
// 创建失败:释放幂等占位,允许用户重试。
if req.ClientRequestID != "" && s.cache != nil {
cache.ReleaseIdempotency(ctx, s.cache, cache.PostCreateIdempotentKey(userID, req.ClientRequestID))
}
return nil, err
}
// 创建成功:回填幂等键为 postID使后续重复请求返回同一帖子。
if req.ClientRequestID != "" && s.cache != nil {
cache.CompleteIdempotency(ctx, s.cache, cache.PostCreateIdempotentKey(userID, req.ClientRequestID), post.ID, PostCreateIdempotentTTL)
}
// 异步审核
go s.reviewVotePostAsync(post.ID, userID, req.Title, content, req.Images)