diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index c611c48..9d1d04c 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -4,3 +4,25 @@ Etcd: Hosts: - 127.0.0.1:2379 Key: tweeting.rpc + +Cache: + - Host: 127.0.0.1:7001 + type: cluster + - Host: 127.0.0.1:7002 + type: cluster + - Host: 127.0.0.1:7003 + type: cluster + - Host: 127.0.0.1:7004 + type: cluster + - Host: 127.0.0.1:7005 + type: cluster + - Host: 127.0.0.1:7006 + type: cluster + +Mongo: + Schema: mongodb + Host: 127.0.0.1 + User: "" + Password: "" + Port: "27017" + Database: digimon_tweeting \ No newline at end of file diff --git a/generate/database/mongodb/20240829054501_post.up.text b/generate/database/mongodb/20240829054501_post.up.text new file mode 100644 index 0000000..4a809a7 --- /dev/null +++ b/generate/database/mongodb/20240829054501_post.up.text @@ -0,0 +1,5 @@ +use digimon_tweeting; +db.post.createIndex({ "uid": 1, "create_time": 1 }); +db.post.createIndex({ "is_ad": 1, "create_time": 1 }); +db.post.createIndex({ "create_time": 1 }); + diff --git a/generate/database/mongodb/20240829055501_likes.up.text b/generate/database/mongodb/20240829055501_likes.up.text new file mode 100644 index 0000000..09628ff --- /dev/null +++ b/generate/database/mongodb/20240829055501_likes.up.text @@ -0,0 +1,5 @@ +db.post_likes.createIndex( + { "target_id": 1, "uid": 1, "type": 1 }, + { unique: true } +); +db.post_likes.createIndex({ "create_time": 1 }); \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index be6b914..e760ba9 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -15,12 +15,17 @@ message Pager { int64 index=3; // 當前頁碼 } +message Media{ + string type =1; + string url =2; +} + // 新增貼文的請求 message NewPostReq { - int64 user_id = 1; // 發佈貼文的用戶ID + string uid = 1; // 發佈貼文的用戶ID string content = 2; // 貼文內容 repeated string tags = 3; // 貼文相關標籤 - repeated string media_url = 4; // 這筆文章的所有 Media URL + repeated Media media = 4; // 這筆文章的所有 Media URL bool is_ad = 5; // 是否為廣告 } @@ -38,7 +43,7 @@ message DeletePostsReq { message UpdatePostReq { string post_id = 1; // 貼文ID repeated string tags = 2; // 新的標籤列表 - repeated string media_url = 3; // 這筆文章的所有 Media URL + repeated Media media = 3; // 這筆文章的所有 Media URL optional string content = 4; // 新的貼文內容 optional int64 like_count = 5; // 喜歡數量 optional int64 dislike_count = 6; // 不喜歡數量 @@ -46,10 +51,9 @@ message UpdatePostReq { // 查詢貼文的請求 message QueryPostsReq { - repeated int64 user_id = 1; // 可選:根據用戶ID篩選貼文 - repeated int64 id = 2; // 可選:根據貼文ID篩選貼文 - repeated string tags = 3; // 可選:根據標籤篩選貼文 - optional bool only_ads = 4; // 可選:是否只顯示廣告 + repeated string uid = 1; // 可選:根據用戶ID篩選貼文 + repeated string id = 2; // 可選:根據貼文ID篩選貼文 + optional int32 only_ads = 4; // 可選:是否只顯示廣告 0 不篩選 1 只顯示廣告 2 不顯示廣告 int32 page_index = 5; // 分頁的頁碼 int32 page_size = 6; // 每頁顯示的數量 } @@ -57,10 +61,10 @@ message QueryPostsReq { // 貼文詳情 message PostDetailItem { string post_id = 1; // 貼文ID - int64 user_id = 2; // 發佈用戶ID + string uid = 2; // 發佈用戶ID string content = 3; // 貼文內容 repeated string tags = 4; // 標籤 - repeated string media_url = 5; // 圖片URL + repeated Media media = 5; // 圖片URL bool is_ad = 6; // 是否為廣告 int64 created_at = 7; // 發佈時間 int64 update_at = 8; // 更新時間 @@ -77,14 +81,30 @@ message ListPostsResp { // 讚/不讚請求 message LikeReq { string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 user_id = 2; // 點讚的用戶ID + string uid = 2; // 點讚的用戶ID int64 like_type = 3; // 讚或爛的類型 } +message GetLikeStatusReq{ + string uid = 1; // 點讚的用戶ID + repeated string target_id = 2; // 目標ID(可以是貼文ID或評論ID) + int64 like_type = 3; // 讚或爛的類型 +} + +message GetLikeStatusItem{ + string target_id = 1; // 目標ID(可以是貼文ID或評論ID) + bool status = 2; // 是否有按過 +} + +message GetLikeStatusResp{ + repeated GetLikeStatusItem data = 1; // 目標ID(可以是貼文ID或評論ID) +} + + // 讚/不讚項目 message LikeItem { string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 user_id = 2; // 點讚的用戶ID + string uid = 2; // 點讚的用戶ID int64 like_type = 3; // 讚或爛的類型 } @@ -92,8 +112,8 @@ message LikeItem { message LikeListReq { string target_id = 1; // 目標ID(可以是貼文ID或評論ID) int64 like_type = 2; // 讚或爛的類型 - int32 page_index = 3; // 當前頁碼 - int32 page_size = 4; // 每頁顯示數量 + int64 page_index = 3; // 當前頁碼 + int64 page_size = 4; // 每頁顯示數量 } // 讚/不讚列表回應 @@ -110,7 +130,7 @@ message LikeCountReq { // 讚/不讚數量回應 message LikeCountResp { - string count = 1; // 總共按讚數量 + int64 count = 1; // 總共按讚數量 } // 評論貼文的請求 @@ -154,6 +174,18 @@ message UpdateCommentReq { string content = 2; // 更新後的評論內容 } +message PostReactionActionResp { + string PostID =1; // 貼文的 ID + int64 reaction_type = 2; // 用戶的反應類型,可能是讚或不讚 + bool is_increment = 3; // 表示是否增加(true 表示增加,false 表示減少) +} + +message IncDecLikeDislikeCountReq { + string PostID =1; // 貼文的 ID + int64 reaction_type = 2; // 用戶的反應類型,可能是讚或不讚 + bool is_increment = 3; // 表示是否增加(true 表示增加,false 表示減少) +} + // 定義貼文服務 service PostService { // NewPost 新增貼文 @@ -164,11 +196,13 @@ service PostService { rpc UpdatePost(UpdatePostReq) returns (OKResp); // ListPosts 查詢貼文 rpc ListPosts(QueryPostsReq) returns (ListPostsResp); + // IncDecLikeDislikeCount 增減數量 + rpc IncDecLikeDislikeCount(IncDecLikeDislikeCountReq) returns (OKResp); // Like 點讚/取消讚 貼文 - rpc Like(LikeReq) returns (OKResp); + rpc Like(LikeReq) returns (PostReactionActionResp); // GetLikeStatus 取得讚/不讚狀態 - rpc GetLikeStatus(LikeReq) returns (OKResp); + rpc GetLikeStatus(GetLikeStatusReq) returns (GetLikeStatusResp); // LikeList 取得讚/不讚列表 rpc LikeList(LikeListReq) returns (LikeListResp); // CountLike 取得讚/不讚數量 diff --git a/go.mod b/go.mod index 1be6bfb..44c507f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module app-cloudep-tweeting-service go 1.22.3 require ( + code.30cm.net/digimon/library-go/errs v1.2.4 + code.30cm.net/digimon/library-go/validator v1.0.0 github.com/zeromicro/go-zero v1.7.0 + go.mongodb.org/mongo-driver v1.16.1 google.golang.org/grpc v1.66.0 google.golang.org/protobuf v1.34.2 ) @@ -18,14 +21,19 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -33,11 +41,14 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect @@ -47,6 +58,10 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/redis/go-redis/v9 v9.6.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect @@ -65,8 +80,10 @@ require ( go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index c1f85b9..eecde82 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,21 @@ package config -import "github.com/zeromicro/go-zero/zrpc" +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/zrpc" +) type Config struct { zrpc.RpcServerConf + Mongo struct { + Schema string + User string + Password string + Host string + Port string + Database string + } + + // 快取 + Cache cache.CacheConf } diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..7329e83 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,24 @@ +package domain + +type AdType int32 + +func (a AdType) ToInt32() int32 { + return int32(a) +} + +const ( + AdTypeAll AdType = iota + AdTypeOnlyAd + AdTypeOnlyNotAd +) + +type LikeType int8 + +func (l LikeType) ToInt8() int8 { + return int8(l) +} + +const ( + LikeTypeLike LikeType = iota + 1 // 按揍 + LikeTypeDisLike +) diff --git a/internal/domain/errors.go b/internal/domain/errors.go new file mode 100644 index 0000000..52ca91d --- /dev/null +++ b/internal/domain/errors.go @@ -0,0 +1,39 @@ +package domain + +import ( + "fmt" + "strings" + + "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "github.com/zeromicro/go-zero/core/logx" +) + +type ErrorCode uint32 + +func (e ErrorCode) ToUint32() uint32 { + return uint32(e) +} + +const ( + _ = iota + PostMongoErrorCode ErrorCode = iota +) + +// PostMongoError ... +func PostMongoError(s ...string) *errs.LibError { + return errs.NewError(code.CloudEPTweeting, code.DBError, + PostMongoErrorCode.ToUint32(), + fmt.Sprintf("%s", strings.Join(s, " "))) +} + +// PostMongoErrorL logs error message and returns Err +func PostMongoErrorL(l logx.Logger, filed []logx.LogField, s ...string) *errs.LibError { + e := PostMongoError(s...) + if filed != nil || len(filed) >= 0 { + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + } + l.WithCallerSkip(1).Error(e.Error()) + + return e +} diff --git a/internal/domain/status.go b/internal/domain/status.go new file mode 100644 index 0000000..abe9593 --- /dev/null +++ b/internal/domain/status.go @@ -0,0 +1,12 @@ +package domain + +type TweetingStatus int8 + +func (t TweetingStatus) ToInt8() int8 { + return int8(t) +} + +const ( + TweetingStatusNotReviewedYet TweetingStatus = iota + 1 + TweetingStatusPass +) diff --git a/internal/logic/postservice/count_like_logic.go b/internal/logic/postservice/count_like_logic.go index 4fb4e67..2294a9c 100644 --- a/internal/logic/postservice/count_like_logic.go +++ b/internal/logic/postservice/count_like_logic.go @@ -1,10 +1,12 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/svc" "context" - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" + ers "code.30cm.net/digimon/library-go/errs" "github.com/zeromicro/go-zero/core/logx" ) @@ -23,9 +25,35 @@ func NewCountLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CountLi } } +type countLikeReq struct { + PostID string `json:"post_id" validate:"required"` // 貼文的 ID + ReactionType domain.LikeType `json:"reaction_type" validate:"required,oneof=1 2"` // 用戶的反應類型,可能是讚或不讚 +} + // CountLike 取得讚/不讚數量 func (l *CountLikeLogic) CountLike(in *tweeting.LikeCountReq) (*tweeting.LikeCountResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&countLikeReq{ + PostID: in.GetTargetId(), + ReactionType: domain.LikeType(in.GetLikeType()), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } - return &tweeting.LikeCountResp{}, nil + count, err := l.svcCtx.PostLikeModel.Count(l.ctx, in.GetTargetId(), domain.LikeType(in.GetLikeType())) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostLikeModel.Count"}, + {Key: "err", Value: err}, + }, + "failed to count like or dislike").Wrap(err) + return nil, e + } + + return &tweeting.LikeCountResp{ + Count: count, + }, nil } diff --git a/internal/logic/postservice/delete_post_logic.go b/internal/logic/postservice/delete_post_logic.go index 714e1f5..41f7ddc 100644 --- a/internal/logic/postservice/delete_post_logic.go +++ b/internal/logic/postservice/delete_post_logic.go @@ -1,6 +1,7 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/internal/domain" "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" @@ -25,7 +26,18 @@ func NewDeletePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete // DeletePost 刪除貼文 func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line + _, err := l.svcCtx.PostModel.DeleteMany(l.ctx, in.GetPostId()...) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.DeletePost"}, + {Key: "err", Value: err}, + }, + "failed to add del post").Wrap(err) + return nil, e + } return &tweeting.OKResp{}, nil } diff --git a/internal/logic/postservice/get_like_status_logic.go b/internal/logic/postservice/get_like_status_logic.go index ba92101..1cdea66 100644 --- a/internal/logic/postservice/get_like_status_logic.go +++ b/internal/logic/postservice/get_like_status_logic.go @@ -1,8 +1,12 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" @@ -23,9 +27,50 @@ func NewGetLikeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } } -// GetLikeStatus 取得讚/不讚狀態 -func (l *GetLikeStatusLogic) GetLikeStatus(in *tweeting.LikeReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line - - return &tweeting.OKResp{}, nil +// 這個人按讚的文章列表(輸入UID 以及文章id,返回這個人有沒有對這些文章按讚) +type getLikeStatusReq struct { + Targets []string `json:"targets" validate:"required"` + LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"` + UID string `json:"uid" validate:"required"` +} + +// GetLikeStatus 取得讚/不讚狀態 +func (l *GetLikeStatusLogic) GetLikeStatus(in *tweeting.GetLikeStatusReq) (*tweeting.GetLikeStatusResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&getLikeStatusReq{ + Targets: in.GetTargetId(), + LikeType: domain.LikeType(in.GetLikeType()), + UID: in.GetUid(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + list, err := l.svcCtx.PostLikeModel.FindUIDPostLikeStatus(l.ctx, &model.QueryUIDPostLikeStatusReq{ + Targets: in.GetTargetId(), + LikeType: domain.LikeType(in.GetLikeType()), + UID: in.GetUid(), + }) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.FindUIDPostLikeStatus"}, + {Key: "err", Value: err}, + }, + "failed to find uid post like status list").Wrap(err) + return nil, e + } + var result = make([]*tweeting.GetLikeStatusItem, 0, len(list)) + + for _, item := range list { + result = append(result, &tweeting.GetLikeStatusItem{ + TargetId: item.TargetID, + Status: item.LikeStatus, + }) + } + + return &tweeting.GetLikeStatusResp{ + Data: result, + }, nil } diff --git a/internal/logic/postservice/inc_dec_like_dislike_count_logic.go b/internal/logic/postservice/inc_dec_like_dislike_count_logic.go new file mode 100644 index 0000000..0190ece --- /dev/null +++ b/internal/logic/postservice/inc_dec_like_dislike_count_logic.go @@ -0,0 +1,65 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type IncDecLikeDislikeCountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewIncDecLikeDislikeCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *IncDecLikeDislikeCountLogic { + return &IncDecLikeDislikeCountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type postReactionAction struct { + PostID string `json:"post_id" validate:"required"` // 貼文的 ID + ReactionType domain.LikeType `json:"reaction_type" validate:"required,oneof=1 2"` // 用戶的反應類型,可能是讚或不讚 + IsIncrement bool `json:"is_increment" validate:"required"` // 表示是否增加(true 表示增加,false 表示減少) +} + +// IncDecLikeDislikeCount 增減數量 +func (l *IncDecLikeDislikeCountLogic) IncDecLikeDislikeCount(in *tweeting.IncDecLikeDislikeCountReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&postReactionAction{ + PostID: in.GetPostID(), + ReactionType: domain.LikeType(in.GetReactionType()), + IsIncrement: in.GetIsIncrement(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.PostModel.IncDecLikeDislikeCountLogic(l.ctx, &model.PostReactionAction{ + PostID: in.GetPostID(), + ReactionType: domain.LikeType(in.GetReactionType()), + IsIncrement: in.GetIsIncrement(), + }) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.IncDecLikeDislikeCountLogic"}, + {Key: "err", Value: err}, + }, + "failed to inc like or dislike").Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/postservice/like_list_logic.go b/internal/logic/postservice/like_list_logic.go index 6f4023b..db89d6e 100644 --- a/internal/logic/postservice/like_list_logic.go +++ b/internal/logic/postservice/like_list_logic.go @@ -1,8 +1,12 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" @@ -23,9 +27,60 @@ func NewLikeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeList } } +// 換句話說就是對這個文章按讚的人的列表 + +type likeListReq struct { + Target string `json:"target" validate:"required"` + LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"` + PageSize int64 `json:"page_size" validate:"required"` + PageIndex int64 `json:"page_index" validate:"required"` +} + // LikeList 取得讚/不讚列表 func (l *LikeListLogic) LikeList(in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&likeListReq{ + Target: in.TargetId, + LikeType: domain.LikeType(in.GetLikeType()), + PageSize: in.GetPageSize(), + PageIndex: in.GetPageIndex(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } - return &tweeting.LikeListResp{}, nil + result, total, err := l.svcCtx.PostLikeModel.FindLikeUsers(l.ctx, &model.QueryPostLikeReq{ + Target: in.GetTargetId(), + LikeType: domain.LikeType(in.GetLikeType()), + PageSize: in.GetPageSize(), + PageIndex: in.GetPageIndex(), + }) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.LikeDislike"}, + {Key: "err", Value: err}, + }, + "failed to like or dislike").Wrap(err) + return nil, e + } + + var list = make([]*tweeting.LikeItem, 0, len(result)) + for _, item := range result { + list = append(list, &tweeting.LikeItem{ + LikeType: int64(item.Type), + TargetId: item.TargetID, + Uid: item.UID, + }) + } + + return &tweeting.LikeListResp{ + List: list, + Page: &tweeting.Pager{ + Size: in.GetPageSize(), + Index: in.GetPageIndex(), + Total: total, + }, + }, nil } diff --git a/internal/logic/postservice/like_logic.go b/internal/logic/postservice/like_logic.go index 3965b38..d918171 100644 --- a/internal/logic/postservice/like_logic.go +++ b/internal/logic/postservice/like_logic.go @@ -1,8 +1,12 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" @@ -23,9 +27,43 @@ func NewLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeLogic { } } -// Like 點讚/取消讚 貼文 -func (l *LikeLogic) Like(in *tweeting.LikeReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line - - return &tweeting.OKResp{}, nil +type likeReq struct { + Target string `json:"target" validate:"required"` + UID string `json:"uid" validate:"required"` +} + +// Like 點讚/取消讚 貼文 +func (l *LikeLogic) Like(in *tweeting.LikeReq) (*tweeting.PostReactionActionResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&likeReq{ + Target: in.TargetId, + UID: in.GetUid(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + + likeResp, err := l.svcCtx.PostLikeModel.LikeDislike(l.ctx, &model.PostLikes{ + TargetID: in.GetTargetId(), + UID: in.GetUid(), + Type: int8(in.GetLikeType()), + }) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.LikeDislike"}, + {Key: "err", Value: err}, + }, + "failed to like or dislike").Wrap(err) + return nil, e + } + + // 將文章的數量增加或減少的功能,是業務邏輯,從外面再決定要不要丟MQ 後算,或是怎麼算 + + return &tweeting.PostReactionActionResp{ + PostID: likeResp.PostID, + ReactionType: int64(likeResp.ReactionType), + IsIncrement: likeResp.IsIncrement, + }, nil } diff --git a/internal/logic/postservice/list_posts_logic.go b/internal/logic/postservice/list_posts_logic.go index abba88d..90a7d84 100644 --- a/internal/logic/postservice/list_posts_logic.go +++ b/internal/logic/postservice/list_posts_logic.go @@ -1,10 +1,14 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + "app-cloudep-tweeting-service/internal/svc" "context" - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" + ers "code.30cm.net/digimon/library-go/errs" + "google.golang.org/protobuf/proto" "github.com/zeromicro/go-zero/core/logx" ) @@ -23,9 +27,84 @@ func NewListPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPos } } +type listReq struct { + OnlyAdds int32 `json:"only_adds" validate:"oneof=0 1 2 3"` + PageSize int64 `json:"page_size" validate:"required"` + PageIndex int64 `json:"page_index" validate:"required"` +} + // ListPosts 查詢貼文 func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPostsResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&listReq{ + PageSize: int64(in.GetPageSize()), + PageIndex: int64(in.GetPageIndex()), + OnlyAdds: in.GetOnlyAds(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } - return &tweeting.ListPostsResp{}, nil + query := &model.QueryPostModelReq{ + UID: in.GetUid(), + Id: in.GetId(), + PageSize: int64(in.GetPageSize()), + PageIndex: int64(in.GetPageIndex()), + } + + if in.OnlyAds != nil { + switch in.GetOnlyAds() { + case domain.AdTypeOnlyAd.ToInt32(): + query.OnlyAds = proto.Bool(true) + case domain.AdTypeOnlyNotAd.ToInt32(): + query.OnlyAds = proto.Bool(false) + default: + } + } + + find, count, err := l.svcCtx.PostModel.Find(l.ctx, query) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "query", Value: query}, + {Key: "func", Value: "PostModel.Find"}, + {Key: "err", Value: err}, + }, + "failed to add new post").Wrap(err) + return nil, e + } + + result := make([]*tweeting.PostDetailItem, 0, count) + + for _, item := range find { + media := make([]*tweeting.Media, 0, len(item.Media)) + for _, subItem := range item.Media { + media = append(media, &tweeting.Media{ + Type: subItem.Type, + Url: subItem.Links, + }) + } + + result = append(result, &tweeting.PostDetailItem{ + PostId: item.ID.Hex(), + Uid: item.UID, + Content: item.Content, + Tags: item.Tags, + Media: media, + IsAd: item.IsAd, + CreatedAt: item.CreateAt, + UpdateAt: item.UpdateAt, + LikeCount: int64(item.Like), + DislikeCount: int64(item.DisLike), + }) + } + + return &tweeting.ListPostsResp{ + Posts: result, + Page: &tweeting.Pager{ + Total: count, + Index: int64(in.GetPageIndex()), + Size: int64(in.GetPageSize()), + }, + }, nil } diff --git a/internal/logic/postservice/new_post_logic.go b/internal/logic/postservice/new_post_logic.go index a755c05..9efb3fb 100644 --- a/internal/logic/postservice/new_post_logic.go +++ b/internal/logic/postservice/new_post_logic.go @@ -1,10 +1,13 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + "app-cloudep-tweeting-service/internal/svc" "context" - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" + ers "code.30cm.net/digimon/library-go/errs" "github.com/zeromicro/go-zero/core/logx" ) @@ -23,9 +26,64 @@ func NewNewPostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewPostLo } } +type newTweetingReq struct { + UID string `json:"uid" validate:"required"` + Content string `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內 + Tags []string `json:"tags"` + MediaUrl []string `json:"media_url"` + IsAd bool `json:"is_ad"` // default false +} + // NewPost 新增貼文 func (l *NewPostLogic) NewPost(in *tweeting.NewPostReq) (*tweeting.PostResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&newTweetingReq{ + UID: in.GetUid(), + Content: in.GetContent(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + // ============ prepare ============ + // 新增資料 + tweet := &model.Post{ + UID: in.GetUid(), + Content: in.GetContent(), + Status: domain.TweetingStatusNotReviewedYet.ToInt8(), + IsAd: in.IsAd, + } + if len(in.GetTags()) > 0 { + // 存在貼文內的不提供搜尋,純顯示用,只不過在原始的tag 發生變動的時候,並不會一起改變 + // 搜尋會貼文與Tag 的表會再另外一邊做關聯 + // 暫時業務邏輯上tag 只提供新增,不提供修改以及刪除,故目前版本可行 + tweet.Tags = in.GetTags() + } - return &tweeting.PostResp{}, nil + if len(in.Media) > 0 { + // 將 Media 存入 + var media []model.Media + for _, item := range in.GetMedia() { + media = append(media, model.Media{ + Links: item.Url, + Type: item.Type, + }) + } + tweet.Media = media + } + // ============ insert ============ + err := l.svcCtx.PostModel.Insert(l.ctx, tweet) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.Insert"}, + {Key: "err", Value: err}, + }, + "failed to add new post").Wrap(err) + return nil, e + } + + return &tweeting.PostResp{ + PostId: tweet.ID.Hex(), + }, nil } diff --git a/internal/logic/postservice/update_post_logic.go b/internal/logic/postservice/update_post_logic.go index 454844b..8e7b894 100644 --- a/internal/logic/postservice/update_post_logic.go +++ b/internal/logic/postservice/update_post_logic.go @@ -1,8 +1,13 @@ package postservicelogic import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" "context" + ers "code.30cm.net/digimon/library-go/errs" + "go.mongodb.org/mongo-driver/bson/primitive" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" @@ -23,9 +28,53 @@ func NewUpdatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update } } +type checkPostId struct { + PostID string `validate:"required"` + Content string `json:"content,omitempty" validate:"lte=500"` +} + // UpdatePost 更新貼文 func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&checkPostId{ + PostID: in.GetPostId(), + Content: in.GetContent(), + }); err != nil { + return nil, ers.InvalidFormat(err.Error()) + } + // 沒有就沒有,有就走全覆蓋 + update := model.Post{} + oid, err := primitive.ObjectIDFromHex(in.GetPostId()) + if err != nil { + return nil, ers.InvalidFormat("failed to get correct post id") + } + update.ID = oid + update.Tags = in.GetTags() + // 將 Media 存入 + var media []model.Media + for _, item := range in.GetMedia() { + media = append(media, model.Media{ + Links: item.Url, + Type: item.Type, + }) + } + update.Media = media + update.Content = in.GetContent() + update.Like = uint64(in.GetLikeCount()) + update.DisLike = uint64(in.GetDislikeCount()) + + _, err = l.svcCtx.PostModel.UpdateOptional(l.ctx, &update) + if err != nil { + e := domain.PostMongoErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.UpdateOptional"}, + {Key: "err", Value: err}, + }, + "failed to add new post").Wrap(err) + return nil, e + } return &tweeting.OKResp{}, nil } diff --git a/internal/model/mongo/comment_likes_model_gen.go b/internal/model/mongo/comment_likes_model_gen.go index cf5a194..790db69 100644 --- a/internal/model/mongo/comment_likes_model_gen.go +++ b/internal/model/mongo/comment_likes_model_gen.go @@ -12,9 +12,9 @@ import ( ) type comment_likesModel interface { - Insert(ctx context.Context, data *Comment_likes) error - FindOne(ctx context.Context, id string) (*Comment_likes, error) - Update(ctx context.Context, data *Comment_likes) (*mongo.UpdateResult, error) + Insert(ctx context.Context, data *CommentLikes) error + FindOne(ctx context.Context, id string) (*CommentLikes, error) + Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error) Delete(ctx context.Context, id string) (int64, error) } @@ -26,7 +26,7 @@ func newDefaultComment_likesModel(conn *mon.Model) *defaultComment_likesModel { return &defaultComment_likesModel{conn: conn} } -func (m *defaultComment_likesModel) Insert(ctx context.Context, data *Comment_likes) error { +func (m *defaultComment_likesModel) Insert(ctx context.Context, data *CommentLikes) error { if data.ID.IsZero() { data.ID = primitive.NewObjectID() data.CreateAt = time.Now() @@ -37,13 +37,13 @@ func (m *defaultComment_likesModel) Insert(ctx context.Context, data *Comment_li return err } -func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*Comment_likes, error) { +func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*CommentLikes, error) { oid, err := primitive.ObjectIDFromHex(id) if err != nil { return nil, ErrInvalidObjectId } - var data Comment_likes + var data CommentLikes err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid}) switch err { @@ -56,7 +56,7 @@ func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*Co } } -func (m *defaultComment_likesModel) Update(ctx context.Context, data *Comment_likes) (*mongo.UpdateResult, error) { +func (m *defaultComment_likesModel) Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error) { data.UpdateAt = time.Now() res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data}) diff --git a/internal/model/mongo/comment_likes_types.go b/internal/model/mongo/comment_likes_types.go index be19d28..8165180 100644 --- a/internal/model/mongo/comment_likes_types.go +++ b/internal/model/mongo/comment_likes_types.go @@ -6,7 +6,7 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) -type Comment_likes struct { +type CommentLikes struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` // TODO: Fill your own fields UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"` diff --git a/internal/model/mongo/post_likes_model.go b/internal/model/mongo/post_likes_model.go index 9d1a52a..f818972 100644 --- a/internal/model/mongo/post_likes_model.go +++ b/internal/model/mongo/post_likes_model.go @@ -1,6 +1,17 @@ package model -import "github.com/zeromicro/go-zero/core/stores/mon" +import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + "errors" + "time" + + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) var _ Post_likesModel = (*customPost_likesModel)(nil) @@ -9,6 +20,34 @@ type ( // and implement the added methods in customPost_likesModel. Post_likesModel interface { post_likesModel + LikeDislike(ctx context.Context, postLike *PostLikes) (*PostReactionAction, error) + FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error) + FindUIDPostLikeStatus(ctx context.Context, param *QueryUIDPostLikeStatusReq) ([]UIDPostLikeStatusResp, error) + Count(ctx context.Context, target string, likeType domain.LikeType) (int64, error) + } + + PostReactionAction struct { + PostID string // 貼文的 ID + ReactionType domain.LikeType // 用戶的反應類型,可能是讚或不讚 + IsIncrement bool // 表示是否增加(true 表示增加,false 表示減少) + } + + QueryPostLikeReq struct { + Target string `bson:"target"` + LikeType domain.LikeType `bson:"like_type"` + PageIndex int64 `bson:"page_index"` + PageSize int64 `bson:"page_size"` + } + + QueryUIDPostLikeStatusReq struct { + Targets []string `bson:"target"` + LikeType domain.LikeType `bson:"like_type"` + UID string + } + + UIDPostLikeStatusResp struct { + TargetID string `json:"target_id"` + LikeStatus bool `json:"like_status"` } customPost_likesModel struct { @@ -23,3 +62,134 @@ func NewPost_likesModel(url, db, collection string) Post_likesModel { defaultPost_likesModel: newDefaultPost_likesModel(conn), } } + +func (m *defaultPost_likesModel) LikeDislike(ctx context.Context, postLike *PostLikes) (*PostReactionAction, error) { + result := &PostReactionAction{ + PostID: postLike.TargetID, + ReactionType: domain.LikeType(postLike.Type), + } + + // 使用 target_id、uid、type 來查詢資料是否存在 + filter := bson.M{ + "target_id": postLike.TargetID, + "uid": postLike.UID, + "type": postLike.Type, + } + + // 查詢資料是否存在 + var existingPostLike PostLikes + err := m.conn.FindOne(ctx, &existingPostLike, filter) + + if err == nil { + // 資料存在,進行刪除操作 + result.IsIncrement = false + _, err = m.conn.DeleteOne(ctx, filter) + if err != nil { + return nil, err // 刪除失敗 + } + + return result, nil // 刪除成功 + } else if errors.Is(mongo.ErrNoDocuments, err) { + // 資料不存在,進行插入操作 + result.IsIncrement = true + postLike.ID = primitive.NewObjectID() // 設置新的 ObjectID + postLike.CreateAt = time.Now().UTC().UnixNano() + _, err = m.conn.InsertOne(ctx, postLike) + if err != nil { + return nil, err // 插入失敗 + } + + return result, nil // 插入成功 + } else { + // 其他錯誤 + return nil, err + } +} + +func (m *defaultPost_likesModel) FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error) { + // 建立篩選條件 + filter := bson.M{} + + // 如果指定了 Target 條件,將其添加到篩選條件中 + if param.Target != "" { + filter["target_id"] = param.Target + } + + // 如果指定了 LikeType 條件,將其添加到篩選條件中 + if param.LikeType != 0 { + filter["type"] = param.LikeType + } + + // 計算符合條件的文檔總數 + totalCount, err := m.conn.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + // 設置分頁和排序選項 + opts := options.Find() + opts.SetSort(bson.D{{"createAt", -1}}) // 按照創建時間倒序排序 + if param.PageSize > 0 { + opts.SetLimit(param.PageSize) + } + if param.PageIndex > 0 && param.PageSize > 0 { + opts.SetSkip((param.PageIndex - 1) * param.PageSize) + } + + // 查詢符合條件的文檔 + var results []*PostLikes + err = m.conn.Find(ctx, &results, filter, opts) + if err != nil { + return nil, 0, err + } + + // 返回結果集、總數和錯誤信息 + return results, totalCount, nil +} + +func (m *defaultPost_likesModel) FindUIDPostLikeStatus(ctx context.Context, param *QueryUIDPostLikeStatusReq) ([]UIDPostLikeStatusResp, error) { + // 初始化返回結果的切片 + var results []UIDPostLikeStatusResp + + // 建立篩選條件 + filter := bson.M{ + "uid": param.UID, // 篩選指定的 UID + "target_id": bson.M{"$in": param.Targets}, // 篩選多個目標 ID + "type": param.LikeType, // 篩選指定的 LikeType + } + + // 查詢符合條件的點讚記錄 + var postLikes []PostLikes + err := m.conn.Find(ctx, &postLikes, filter) + if err != nil { + return nil, err + } + + // 構建一個 map 來保存查詢到的點讚記錄 + targetLikeMap := make(map[string]bool) + for _, like := range postLikes { + targetLikeMap[like.TargetID] = true + } + + // 構建每個目標的點讚狀態 + for _, targetID := range param.Targets { + likeStatus := UIDPostLikeStatusResp{ + TargetID: targetID, + LikeStatus: targetLikeMap[targetID], // 如果 map 中有該 targetID,返回 true,否則返回 false + } + + // 如果該 targetID 沒有點讚記錄,默認 LikeStatus 為 false + if _, found := targetLikeMap[targetID]; !found { + likeStatus.LikeStatus = false + } + + results = append(results, likeStatus) + } + + return results, nil +} + +func (c customPost_likesModel) Count(ctx context.Context, target string, likeType domain.LikeType) (int64, error) { + // TODO implement me + panic("implement me") +} diff --git a/internal/model/mongo/post_likes_model_gen.go b/internal/model/mongo/post_likes_model_gen.go index e0499b7..c7c6920 100644 --- a/internal/model/mongo/post_likes_model_gen.go +++ b/internal/model/mongo/post_likes_model_gen.go @@ -12,9 +12,9 @@ import ( ) type post_likesModel interface { - Insert(ctx context.Context, data *Post_likes) error - FindOne(ctx context.Context, id string) (*Post_likes, error) - Update(ctx context.Context, data *Post_likes) (*mongo.UpdateResult, error) + Insert(ctx context.Context, data *PostLikes) error + FindOne(ctx context.Context, id string) (*PostLikes, error) + Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error) Delete(ctx context.Context, id string) (int64, error) } @@ -26,24 +26,23 @@ func newDefaultPost_likesModel(conn *mon.Model) *defaultPost_likesModel { return &defaultPost_likesModel{conn: conn} } -func (m *defaultPost_likesModel) Insert(ctx context.Context, data *Post_likes) error { +func (m *defaultPost_likesModel) Insert(ctx context.Context, data *PostLikes) error { if data.ID.IsZero() { data.ID = primitive.NewObjectID() - data.CreateAt = time.Now() - data.UpdateAt = time.Now() + data.CreateAt = time.Now().UTC().UnixNano() } _, err := m.conn.InsertOne(ctx, data) return err } -func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*Post_likes, error) { +func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*PostLikes, error) { oid, err := primitive.ObjectIDFromHex(id) if err != nil { return nil, ErrInvalidObjectId } - var data Post_likes + var data PostLikes err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid}) switch err { @@ -56,9 +55,7 @@ func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*Post_ } } -func (m *defaultPost_likesModel) Update(ctx context.Context, data *Post_likes) (*mongo.UpdateResult, error) { - data.UpdateAt = time.Now() - +func (m *defaultPost_likesModel) Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error) { res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data}) return res, err } diff --git a/internal/model/mongo/post_likes_types.go b/internal/model/mongo/post_likes_types.go index 5b8f595..fd1fa1a 100644 --- a/internal/model/mongo/post_likes_types.go +++ b/internal/model/mongo/post_likes_types.go @@ -1,14 +1,17 @@ package model import ( - "time" - "go.mongodb.org/mongo-driver/bson/primitive" ) -type Post_likes struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` - // TODO: Fill your own fields - UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"` - CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"` +type PostLikes struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + TargetID string `bson:"target_id" json:"target_id"` + UID string `bson:"uid" json:"uid"` + Type int8 `bson:"type" json:"type"` + CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"` +} + +func (p *PostLikes) CollectionName() string { + return "post_like" } diff --git a/internal/model/mongo/post_model.go b/internal/model/mongo/post_model.go index 096dda8..30dd425 100644 --- a/internal/model/mongo/post_model.go +++ b/internal/model/mongo/post_model.go @@ -1,8 +1,19 @@ package model import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/monc" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) var _ PostModel = (*customPostModel)(nil) @@ -12,11 +23,23 @@ type ( // and implement the added methods in customPostModel. PostModel interface { postModel + DeleteMany(ctx context.Context, id ...string) (int64, error) + UpdateOptional(ctx context.Context, data *Post) (*mongo.UpdateResult, error) + Find(ctx context.Context, param *QueryPostModelReq) ([]*Post, int64, error) + IncDecLikeDislikeCountLogic(ctx context.Context, param *PostReactionAction) error } customPostModel struct { *defaultPostModel } + + QueryPostModelReq struct { + UID []string + Id []string + OnlyAds *bool + PageSize int64 + PageIndex int64 + } ) // NewPostModel returns a model for the mongo. @@ -26,3 +49,185 @@ func NewPostModel(url, db, collection string, c cache.CacheConf) PostModel { defaultPostModel: newDefaultPostModel(conn), } } + +func (m *defaultPostModel) DeleteMany(ctx context.Context, id ...string) (int64, error) { + objectIDs := make([]primitive.ObjectID, 0, len(id)) + key := make([]string, 0, len(id)) + + // prepare + for _, item := range id { + oid, err := primitive.ObjectIDFromHex(item) + if err != nil { + logx.WithCallerSkip(1).WithFields( + logx.Field("func", "defaultPostModel.DeleteMany"), + logx.Field("id", item), + ).Error(err.Error()) + + continue + } + objectIDs = append(objectIDs, oid) + key = append(key, prefixPostCacheKey+item) + } + + // 檢查是否有有效的 ObjectIDs + if len(objectIDs) == 0 { + return 0, ErrNotFound + } + + // 刪除文檔 + res, err := m.conn.DeleteMany(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}) + if err != nil { + return 0, err + } + + err = m.conn.DelCache(ctx, key...) + if err != nil { + return 0, err + } + + return res, err +} + +func (m *defaultPostModel) UpdateOptional(ctx context.Context, data *Post) (*mongo.UpdateResult, error) { + update := bson.M{"$set": bson.M{}} + + if data.UID != "" { + update["$set"].(bson.M)["uid"] = data.UID + } + + if data.Content != "" { + update["$set"].(bson.M)["content"] = data.Content + } + + if data.Status != 0 { + update["$set"].(bson.M)["status"] = data.Status + } + + if data.IsAd { + update["$set"].(bson.M)["is_ad"] = data.IsAd + } + + if len(data.Tags) > 0 { + update["$set"].(bson.M)["tags"] = data.Tags + } + + if len(data.Media) > 0 { + update["$set"].(bson.M)["media_url"] = data.Media + } + + if data.Like != 0 { + update["$set"].(bson.M)["like"] = data.Like + } + + if data.DisLike != 0 { + update["$set"].(bson.M)["dislike"] = data.DisLike + } + + // UpdateAt 是每次都需要更新的,不用檢查 + update["$set"].(bson.M)["updateAt"] = time.Now().UTC().UnixNano() + fmt.Println("update map", update) + + key := prefixPostCacheKey + data.ID.Hex() + res, err := m.conn.UpdateOne(ctx, key, bson.M{"_id": data.ID}, update) + + return res, err +} + +// Find 貼文列表 +func (m *defaultPostModel) Find(ctx context.Context, param *QueryPostModelReq) ([]*Post, int64, error) { + filter := bson.M{} + + // 添加 UID 過濾條件 + if len(param.UID) > 0 { + filter["uid"] = bson.M{"$in": param.UID} + } + + // 添加 ID 過濾條件 + if len(param.Id) > 0 { + var ids []primitive.ObjectID + for _, item := range param.Id { + oid, err := primitive.ObjectIDFromHex(item) + if err != nil { + // log + continue + } + ids = append(ids, oid) + } + + filter["_id"] = bson.M{"$in": ids} + } + + // 添加 OnlyAds 過濾條件 + if param.OnlyAds != nil { + // true 是廣告 false 不是廣告 , 沒寫就是不過率 + filter["is_ad"] = *param.OnlyAds + } + + // 分頁處理 + opts := options.Find() + opts.SetSort(bson.D{{"create_time", -1}}) + if param.PageSize > 0 { + opts.SetLimit(param.PageSize) + } + if param.PageIndex > 0 { + opts.SetSkip((param.PageIndex - 1) * param.PageSize) + } + + // 計算總數(不考慮分頁) + totalCount, err := m.conn.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + result := make([]*Post, 0, param.PageSize) + // 執行查詢 + err = m.conn.Find(ctx, &result, filter, opts) + if err != nil { + return nil, 0, err + } + + switch { + case err == nil: + return result, totalCount, nil + case errors.Is(err, monc.ErrNotFound): + return nil, 0, ErrNotFound + default: + return nil, 0, err + } +} + +func (c *customPostModel) IncDecLikeDislikeCountLogic(ctx context.Context, param *PostReactionAction) error { + // 建立篩選條件,找到要更新的貼文 + filter := bson.M{"_id": param.PostID} + + // 初始化更新操作 + update := bson.M{} + + // 根據 ReactionType 和 IsIncrement 決定更新邏輯 + if param.ReactionType == domain.LikeTypeLike { + if param.IsIncrement { + // 增加 like 計數 + update = bson.M{"$inc": bson.M{"like": 1}} + } else { + // 減少 like 計數 + update = bson.M{"$inc": bson.M{"like": -1}} + } + } else if param.ReactionType == domain.LikeTypeDisLike { + if param.IsIncrement { + // 增加 dislike 計數 + update = bson.M{"$inc": bson.M{"dislike": 1}} + } else { + // 減少 dislike 計數 + update = bson.M{"$inc": bson.M{"dislike": -1}} + } + } + + // 執行更新操作 + key := prefixPostCacheKey + param.PostID + _, err := c.conn.UpdateOne(ctx, key, filter, update) + if err != nil { + return err // 返回錯誤信息 + } + + return nil // 成功返回 nil +} diff --git a/internal/model/mongo/post_model_gen.go b/internal/model/mongo/post_model_gen.go index c7aa965..386d9c7 100644 --- a/internal/model/mongo/post_model_gen.go +++ b/internal/model/mongo/post_model_gen.go @@ -31,8 +31,8 @@ func newDefaultPostModel(conn *monc.Model) *defaultPostModel { func (m *defaultPostModel) Insert(ctx context.Context, data *Post) error { if data.ID.IsZero() { data.ID = primitive.NewObjectID() - data.CreateAt = time.Now() - data.UpdateAt = time.Now() + data.CreateAt = time.Now().UTC().UnixNano() + data.UpdateAt = time.Now().UTC().UnixNano() } key := prefixPostCacheKey + data.ID.Hex() @@ -60,7 +60,7 @@ func (m *defaultPostModel) FindOne(ctx context.Context, id string) (*Post, error } func (m *defaultPostModel) Update(ctx context.Context, data *Post) (*mongo.UpdateResult, error) { - data.UpdateAt = time.Now() + data.UpdateAt = time.Now().UTC().UnixNano() key := prefixPostCacheKey + data.ID.Hex() res, err := m.conn.UpdateOne(ctx, key, bson.M{"_id": data.ID}, bson.M{"$set": data}) return res, err diff --git a/internal/model/mongo/post_types.go b/internal/model/mongo/post_types.go index 1a5802f..3021f99 100644 --- a/internal/model/mongo/post_types.go +++ b/internal/model/mongo/post_types.go @@ -1,14 +1,33 @@ package model import ( - "time" - "go.mongodb.org/mongo-driver/bson/primitive" ) +// TODO Tag 這裡在效能與正確性之間做取捨 +// 存在貼文內的不提供搜尋,純顯示用,只不過在原始的tag 發生變動的時候,並不會一起改變 +// 搜尋會貼文與Tag 的表會再另外一邊做關聯 +// 暫時業務邏輯上tag 只提供新增,不提供修改以及刪除,故目前版本可行 + type Post struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` - // TODO: Fill your own fields - UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"` - CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"` + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + UID string `bson:"uid" json:"uid"` + Content string `bson:"content" json:"content"` + Status int8 `bson:"status" json:"status"` // 1. 等待審核中 , 2 審核通過,預設為 1 + IsAd bool `bson:"is_ad" json:"is_ad"` // 此則貼文是否為廣告貼文 + Tags []string `bson:"tags" json:"tags"` // 本則貼文的標籤,不提供搜尋,僅提供顯示(存名字,ID 建立之後就不提供修改與刪除) + Media []Media `bson:"media_url" json:"media_url"` // 網址 + Like uint64 `bson:"like" json:"like"` // 讚 + DisLike uint64 `bson:"dislike" json:"dislike"` // 不讚 + UpdateAt int64 `bson:"updateAt,omitempty" json:"updateAt,omitempty"` + CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"` +} + +type Media struct { + Type string // media type jpeg, m3u8 之類的 + Links string // 連結的網址 +} + +func (p *Post) CollectionName() string { + return "post" } diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index 8e4eab8..bb41396 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/commentservice" + commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index b5fdaad..61341db 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/postservice" + postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" "app-cloudep-tweeting-service/internal/svc" ) @@ -46,14 +46,20 @@ func (s *PostServiceServer) ListPosts(ctx context.Context, in *tweeting.QueryPos return l.ListPosts(in) } +// IncDecLikeDislikeCount 增減數量 +func (s *PostServiceServer) IncDecLikeDislikeCount(ctx context.Context, in *tweeting.IncDecLikeDislikeCountReq) (*tweeting.OKResp, error) { + l := postservicelogic.NewIncDecLikeDislikeCountLogic(ctx, s.svcCtx) + return l.IncDecLikeDislikeCount(in) +} + // Like 點讚/取消讚 貼文 -func (s *PostServiceServer) Like(ctx context.Context, in *tweeting.LikeReq) (*tweeting.OKResp, error) { +func (s *PostServiceServer) Like(ctx context.Context, in *tweeting.LikeReq) (*tweeting.PostReactionActionResp, error) { l := postservicelogic.NewLikeLogic(ctx, s.svcCtx) return l.Like(in) } // GetLikeStatus 取得讚/不讚狀態 -func (s *PostServiceServer) GetLikeStatus(ctx context.Context, in *tweeting.LikeReq) (*tweeting.OKResp, error) { +func (s *PostServiceServer) GetLikeStatus(ctx context.Context, in *tweeting.GetLikeStatusReq) (*tweeting.GetLikeStatusResp, error) { l := postservicelogic.NewGetLikeStatusLogic(ctx, s.svcCtx) return l.GetLikeStatus(in) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 37ac177..c2c7471 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,13 +1,36 @@ package svc -import "app-cloudep-tweeting-service/internal/config" +import ( + "app-cloudep-tweeting-service/internal/config" + model "app-cloudep-tweeting-service/internal/model/mongo" + "fmt" + + vi "code.30cm.net/digimon/library-go/validator" +) type ServiceContext struct { - Config config.Config + Config config.Config + Validate vi.Validate + + PostModel model.PostModel + PostLikeModel model.Post_likesModel } func NewServiceContext(c config.Config) *ServiceContext { + baseMongo := MustMongoConnectUrl(c) + postCollection := model.Post{} + postLikeCollection := model.PostLikes{} + return &ServiceContext{ - Config: c, + Config: c, + Validate: vi.MustValidator(), + PostModel: model.NewPostModel(baseMongo, c.Mongo.Database, postCollection.CollectionName(), c.Cache), + PostLikeModel: model.NewPost_likesModel(baseMongo, c.Mongo.Database, postLikeCollection.CollectionName()), } } + +func MustMongoConnectUrl(c config.Config) string { + return fmt.Sprintf( + "%s://%s:%s", c.Mongo.Schema, + c.Mongo.Host, c.Mongo.Port) +}