Skip to content

Commit 048aa95

Browse files
authored
feat: 实现同意投票功能 (#49)
1 parent 74cbc92 commit 048aa95

File tree

9 files changed

+227
-11
lines changed

9 files changed

+227
-11
lines changed

api/handler/proposal.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,23 @@ func DeleteProposal(c *gin.Context) {
8484
func GetProposalSuggestions(c *gin.Context) {
8585
// TODO: not implemented
8686
}
87+
88+
// ToggleProposal 翻转投票状态
89+
// @Summary 投票或取消投票
90+
// @Description 对提案进行投票或取消投票
91+
// @Tags proposal
92+
// @Produce json
93+
// @Param id path string true "提案ID"
94+
// @Success 200 {object} dto.ToggleProposalResp
95+
// @Router /api/proposal/{id} [post]
96+
func ToggleProposal(c *gin.Context) {
97+
var req dto.ToggleProposalReq
98+
var resp *dto.ToggleProposalResp
99+
var err error
100+
101+
req.TargetID = c.Param(consts.CtxProposalID)
102+
c.Set(consts.CtxUserID, token.GetUserID(c))
103+
104+
resp, err = provider.Get().ProposalService.ToggleProposal(c, &req)
105+
PostProcess(c, req, resp, err)
106+
}

api/router/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func SetupRoutes() *gin.Engine {
7070
proposalGroup.POST("/:id/update", handler.UpdateProposal)
7171
proposalGroup.POST("/:id/delete", handler.DeleteProposal)
7272
proposalGroup.POST("/suggest", handler.GetProposalSuggestions)
73+
proposalGroup.POST("/:id", handler.ToggleProposal)
7374
}
7475
return router
7576
}

application/dto/proposal.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ type ListProposalReq struct {
3131
*PageParam
3232
}
3333

34+
type ToggleProposalReq struct {
35+
TargetID string `json:"targetId"`
36+
}
37+
3438
// ListProposalResp 对应 /api/proposal/list 的响应体
3539
type ListProposalResp struct {
3640
*Resp
3741
Total int64 `json:"total"`
3842
Proposals []*ProposalVO `json:"proposals"`
3943
}
44+
45+
type ToggleProposalResp struct {
46+
Proposal bool `json:"proposal"`
47+
ProposalCnt int64 `json:"proposalCnt"`
48+
*Resp
49+
}

application/service/proposal.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import (
1919

2020
"github.com/Boyuan-IT-Club/Meowpick-Backend/application/assembler"
2121
"github.com/Boyuan-IT-Club/Meowpick-Backend/application/dto"
22+
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/cache"
2223
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/repo"
2324
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/consts"
2425
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/errno"
26+
2527
"github.com/Boyuan-IT-Club/go-kit/errorx"
2628
"github.com/Boyuan-IT-Club/go-kit/logs"
2729
"github.com/google/wire"
@@ -30,11 +32,13 @@ import (
3032
var _ IProposalService = (*ProposalService)(nil)
3133

3234
type IProposalService interface {
35+
ToggleProposal(ctx context.Context, req *dto.ToggleProposalReq) (resp *dto.ToggleProposalResp, err error)
3336
ListProposals(ctx context.Context, req *dto.ListProposalReq) (*dto.ListProposalResp, error)
3437
}
3538

3639
type ProposalService struct {
37-
ProposalRepo repo.IProposalRepo
40+
ProposalRepo *repo.ProposalRepo
41+
ProposalCache *cache.ProposalCache
3842
ProposalAssembler *assembler.ProposalAssembler
3943
}
4044

@@ -43,6 +47,44 @@ var ProposalServiceSet = wire.NewSet(
4347
wire.Bind(new(IProposalService), new(*ProposalService)),
4448
)
4549

50+
// ToggleProposal 切换投票状态
51+
func (s *ProposalService) ToggleProposal(ctx context.Context, req *dto.ToggleProposalReq) (resp *dto.ToggleProposalResp, err error) {
52+
53+
// 鉴权
54+
userId, ok := ctx.Value(consts.CtxUserID).(string)
55+
if !ok || userId == "" {
56+
return nil, errorx.New(errno.ErrUserNotLogin)
57+
}
58+
59+
// 投票或取消投票目标
60+
active, err := s.ProposalRepo.Toggle(ctx, userId, req.TargetID, consts.ProposalType)
61+
if err != nil {
62+
return nil, errorx.WrapByCode(err, errno.ErrProposalToggleFailed)
63+
}
64+
65+
// 设置缓存的投票状态
66+
if err = s.ProposalCache.SetStatusByUserIdAndTarget(ctx, userId, req.TargetID, active,
67+
consts.CacheProposalStatusTTL,
68+
); err != nil {
69+
logs.CtxWarnf(ctx, "[ProposalCache] [SetStatusByUserIdAndTarget] error: %v", err)
70+
}
71+
72+
// 获取新的总投票数
73+
proposalCount, err := s.ProposalRepo.CountProposalByTarget(ctx, req.TargetID, consts.ProposalType)
74+
if err != nil {
75+
logs.CtxWarnf(ctx, "[ProposalRepo] [CountByTarget] error: %v", err)
76+
return nil, errorx.WrapByCode(err, errno.ErrProposalCountFailed,
77+
errorx.KV("key", consts.ReqTargetID), errorx.KV("value", req.TargetID))
78+
}
79+
80+
// 构造响应并返回
81+
return &dto.ToggleProposalResp{
82+
Resp: dto.Success(),
83+
Proposal: active,
84+
ProposalCnt: proposalCount,
85+
}, nil
86+
}
87+
4688
// ListProposals 分页查询所有提案,用于投票列表或管理端审核
4789
func (s *ProposalService) ListProposals(ctx context.Context, req *dto.ListProposalReq) (*dto.ListProposalResp, error) {
4890
// 鉴权

infra/cache/porposal.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"strconv"
6+
"time"
7+
8+
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/config"
9+
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/consts"
10+
"github.com/zeromicro/go-zero/core/stores/redis"
11+
)
12+
13+
var _ IProposalCache = (*ProposalCache)(nil)
14+
15+
const (
16+
ProposalStatusCacheKey = consts.CacheProposalKeyPrefix + "status"
17+
)
18+
19+
type IProposalCache interface {
20+
GetStatusByUserIdAndTarget(ctx context.Context, userId, targetId string) (bool, bool, error)
21+
SetStatusByUserIdAndTarget(ctx context.Context, userId, targetId string, isVote bool, ttl time.Duration) error
22+
}
23+
24+
type ProposalCache struct {
25+
cache *redis.Redis
26+
}
27+
28+
func NewProposalCache(cfg *config.Config) *ProposalCache {
29+
cache := redis.MustNewRedis(*cfg.Redis)
30+
return &ProposalCache{cache: cache}
31+
}
32+
33+
// GetStatusByUserIdAndTarget 获取点赞状态缓存
34+
func (c *ProposalCache) GetStatusByUserIdAndTarget(ctx context.Context, userId, targetId string) (bool, bool, error) {
35+
key := ProposalStatusCacheKey + userId + ":" + targetId
36+
statusStr, err := c.cache.GetCtx(ctx, key)
37+
if err != nil {
38+
return false, false, nil
39+
}
40+
if statusStr == "" {
41+
return false, false, nil
42+
}
43+
isProposal, err := strconv.ParseBool(statusStr)
44+
if err != nil {
45+
_, _ = c.cache.DelCtx(ctx, key)
46+
return false, false, err
47+
}
48+
return isProposal, true, nil
49+
}
50+
51+
// SetStatusByUserIdAndTarget 设置投票状态缓存
52+
func (c *ProposalCache) SetStatusByUserIdAndTarget(ctx context.Context, userId, targetId string, isProposal bool, ttl time.Duration) error {
53+
key := ProposalStatusCacheKey + userId + ":" + targetId
54+
return c.cache.SetexCtx(ctx, key, strconv.FormatBool(isProposal), int(ttl.Seconds()))
55+
}

infra/repo/proposal.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ package repo
1616

1717
import (
1818
"context"
19+
"time"
1920

2021
"github.com/Boyuan-IT-Club/Meowpick-Backend/application/dto"
22+
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/cache"
2123
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/config"
2224
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/model"
2325
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/util/page"
2426
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/consts"
2527
"github.com/zeromicro/go-zero/core/stores/monc"
2628
"go.mongodb.org/mongo-driver/bson"
29+
"go.mongodb.org/mongo-driver/bson/primitive"
30+
"go.mongodb.org/mongo-driver/mongo"
31+
"go.mongodb.org/mongo-driver/mongo/options"
2732
)
2833

2934
var _ IProposalRepo = (*ProposalRepo)(nil)
@@ -34,10 +39,14 @@ const (
3439

3540
type IProposalRepo interface {
3641
FindMany(ctx context.Context, param *dto.PageParam) ([]*model.Proposal, int64, error)
42+
Toggle(ctx context.Context, userId, targetId string, targetType int32) (bool, error)
43+
IsProposal(ctx context.Context, userId, targetId string, targetType int32) (bool, error)
44+
CountProposalByTarget(ctx context.Context, targetId string, targetType int32) (int64, error)
3745
}
3846

3947
type ProposalRepo struct {
40-
conn *monc.Model
48+
conn *monc.Model
49+
cache *cache.ProposalCache
4150
}
4251

4352
func NewProposalRepo(cfg *config.Config) *ProposalRepo {
@@ -66,3 +75,54 @@ func (r *ProposalRepo) FindMany(ctx context.Context, param *dto.PageParam) ([]*m
6675

6776
return proposals, total, nil
6877
}
78+
79+
// Toggle 翻转投票状态
80+
func (r *ProposalRepo) Toggle(ctx context.Context, userId, targetId string, targetType int32) (bool, error) {
81+
now := time.Now()
82+
pipeline := mongo.Pipeline{
83+
{{"$set", bson.M{
84+
consts.ID: bson.M{"$ifNull": bson.A{"$" + consts.ID, primitive.NewObjectID().Hex()}},
85+
86+
consts.UserID: bson.M{"$ifNull": bson.A{"$" + consts.UserID, userId}},
87+
consts.TargetID: bson.M{"$ifNull": bson.A{"$" + consts.TargetID, targetId}},
88+
89+
consts.CreatedAt: bson.M{"$ifNull": bson.A{"$" + consts.CreatedAt, now}},
90+
consts.UpdatedAt: now,
91+
92+
consts.Active: bson.M{"$cond": bson.A{
93+
bson.M{"$not": bson.M{"$ifNull": bson.A{"$" + consts.ID, nil}}},
94+
true,
95+
bson.M{"$not": "$active"},
96+
}},
97+
}}},
98+
}
99+
var proposal struct {
100+
Active bool `bson:"active"`
101+
}
102+
103+
err := r.conn.FindOneAndUpdateNoCache(ctx,
104+
&proposal,
105+
bson.M{consts.UserID: userId, consts.TargetID: targetId},
106+
pipeline,
107+
options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After),
108+
)
109+
return proposal.Active, err
110+
}
111+
112+
// IsProposal 获取一个用户对一个目标的当前投票状态
113+
func (r *ProposalRepo) IsProposal(ctx context.Context, userId, targetId string, targetType int32) (bool, error) {
114+
cnt, err := r.conn.CountDocuments(ctx, bson.M{
115+
consts.UserID: userId,
116+
consts.TargetID: targetId,
117+
consts.Active: bson.M{"$ne": false},
118+
})
119+
return cnt > 0, err
120+
}
121+
122+
// CountProposalByTarget 获得目标的总投票数
123+
func (r *ProposalRepo) CountProposalByTarget(ctx context.Context, targetId string, targetType int32) (int64, error) {
124+
return r.conn.CountDocuments(ctx, bson.M{
125+
consts.TargetID: targetId,
126+
consts.Active: bson.M{"$ne": false},
127+
})
128+
}

provider/wire_gen.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

types/consts/consts.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,27 @@ const (
4747
CacheUserKeyPrefix = "meowpick:user:"
4848
CacheTeacherKeyPrefix = "meowpick:teacher:"
4949
CacheCourseKeyPrefix = "meowpick:course:"
50+
CacheProposalKeyPrefix = "meowpick:proposal:"
5051

51-
CacheCommentCountTTL = 12 * time.Hour
52-
CacheLikeStatusTTL = 10 * time.Minute
52+
CacheCommentCountTTL = 12 * time.Hour
53+
CacheLikeStatusTTL = 10 * time.Minute
54+
CacheProposalStatusTTL = 10 * time.Minute
5355
)
5456

5557
// 元素类别相关(如课程、评论、老师)
5658
const (
5759
CourseType int32 = 101 + iota
5860
CommentType
61+
ProposalType
5962
)
6063

6164
// 上下文相关
6265
const (
63-
CtxUserID = "userID"
64-
CtxToken = "token"
65-
CtxLikeID = "id"
66-
CtxCourseID = "courseId"
66+
CtxUserID = "userID"
67+
CtxToken = "token"
68+
CtxLikeID = "id"
69+
CtxCourseID = "courseId"
70+
CtxProposalID = "proposalId"
6771
)
6872

6973
// Request 相关

types/errno/proposal.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import "github.com/Boyuan-IT-Club/go-kit/errorx/code"
1919
// proposal: 106 000 000 ~ 106 999 999
2020

2121
const (
22-
ErrProposalFindFailed = 106000001
23-
ErrProposalCvtFailed = 106000002
22+
ErrProposalFindFailed = 106000001
23+
ErrProposalCvtFailed = 106000002
24+
ErrProposalToggleFailed = 106000003
25+
ErrProposalCountFailed = 106000004
26+
ErrProposalGetStatusFailed = 106000005
2427
)
2528

2629
func init() {
@@ -34,5 +37,19 @@ func init() {
3437
"failed to convert proposal from {src} to {dst}",
3538
code.WithAffectStability(false),
3639
)
40+
code.Register(
41+
ErrProposalToggleFailed,
42+
"failed to toggle vote status",
43+
code.WithAffectStability(false),
44+
)
45+
code.Register(
46+
ErrProposalCountFailed,
47+
"failed to get vote count by {key}: {value}",
48+
code.WithAffectStability(false),
49+
)
50+
code.Register(
51+
ErrProposalGetStatusFailed,
52+
"failed to get vote status by {key}: {value}",
53+
code.WithAffectStability(false),
54+
)
3755
}
38-

0 commit comments

Comments
 (0)