From f0816e0c93e9ead27b54f8c0e4d93434a8fbfd4f Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Fri, 30 Aug 2024 10:44:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=87=20post=20service=20=E5=AF=AB=E5=AE=8C?= =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 22 +- etc/tweeting.yaml | 14 ++ .../mongodb/20240829054501_post.up.js | 9 + generate/protobuf/tweeting.proto | 201 +++++------------- go.mod | 108 ++++++++++ internal/config/config.go | 15 ++ internal/domain/const.go | 13 ++ internal/domain/errors.go | 17 ++ internal/domain/status.go | 12 ++ .../logic/postservice/create_post_logic.go | 114 ++++++++++ .../postservice/create_post_logic_test.go | 107 ++++++++++ .../logic/postservice/delete_post_logic.go | 63 ++++++ .../postservice/delete_post_logic_test.go | 81 +++++++ .../logic/postservice/list_posts_logic.go | 139 ++++++++++++ .../postservice/list_posts_logic_test.go | 164 ++++++++++++++ .../logic/postservice/update_post_logic.go | 100 +++++++++ .../postservice/update_post_logic_test.go | 118 ++++++++++ internal/mock/lib/validate.go | 73 +++++++ internal/mock/model/post_model.go | 152 +++++++++++++ internal/mock/model/post_model_gen.go | 101 +++++++++ internal/model/mongo/comment_model.go | 25 +++ internal/model/mongo/comment_model_gen.go | 74 +++++++ internal/model/mongo/comment_types.go | 14 ++ internal/model/mongo/error.go | 12 ++ internal/model/mongo/post_model.go | 185 ++++++++++++++++ internal/model/mongo/post_model_gen.go | 74 +++++++ internal/model/mongo/post_types.go | 32 +++ .../server/postservice/post_service_server.go | 53 +++++ internal/svc/init_mongo.go | 20 ++ internal/svc/service_context.go | 24 +++ tweeting.go | 39 ++++ 31 files changed, 2022 insertions(+), 153 deletions(-) create mode 100644 etc/tweeting.yaml create mode 100644 generate/database/mongodb/20240829054501_post.up.js create mode 100644 go.mod create mode 100755 internal/config/config.go create mode 100644 internal/domain/const.go create mode 100644 internal/domain/errors.go create mode 100644 internal/domain/status.go create mode 100644 internal/logic/postservice/create_post_logic.go create mode 100644 internal/logic/postservice/create_post_logic_test.go create mode 100644 internal/logic/postservice/delete_post_logic.go create mode 100644 internal/logic/postservice/delete_post_logic_test.go create mode 100644 internal/logic/postservice/list_posts_logic.go create mode 100644 internal/logic/postservice/list_posts_logic_test.go create mode 100644 internal/logic/postservice/update_post_logic.go create mode 100644 internal/logic/postservice/update_post_logic_test.go create mode 100644 internal/mock/lib/validate.go create mode 100644 internal/mock/model/post_model.go create mode 100644 internal/mock/model/post_model_gen.go create mode 100644 internal/model/mongo/comment_model.go create mode 100644 internal/model/mongo/comment_model_gen.go create mode 100644 internal/model/mongo/comment_types.go create mode 100644 internal/model/mongo/error.go create mode 100644 internal/model/mongo/post_model.go create mode 100644 internal/model/mongo/post_model_gen.go create mode 100644 internal/model/mongo/post_types.go create mode 100644 internal/server/postservice/post_service_server.go create mode 100644 internal/svc/init_mongo.go create mode 100644 internal/svc/service_context.go create mode 100644 tweeting.go diff --git a/Makefile b/Makefile index 5a391e5..7872af5 100644 --- a/Makefile +++ b/Makefile @@ -49,9 +49,19 @@ build-docker: gen-mongo-model: # 建立 rpc 資料庫 # 只產生 Model 剩下的要自己撰寫,連欄位名稱也是 - goctl model mongo -c no -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) - goctl model mongo -c no -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) - goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) - goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) - goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) - @echo "Generate mongo model files successfully" \ No newline at end of file + goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) + goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) +# goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) +# goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) +# goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) + @echo "Generate mongo model files successfully" + +.PHONY: mock-gen +mock-gen: # 建立 mock 資料 + mockgen -source=./internal/model/mongo/post_model_gen.go -destination=./internal/mock/model/post_model_gen.go -package=mock + mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock + @echo "Generate mock files successfully" + +.PHONY: migrate-database +migrate-database: + migrate -source file://generate/database/migrations/mongodb -database 'mongodb://127.0.0.1:27017/digimon_tweeting' up diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml new file mode 100644 index 0000000..279f3cb --- /dev/null +++ b/etc/tweeting.yaml @@ -0,0 +1,14 @@ +Name: tweeting.rpc +ListenOn: 0.0.0.0:8080 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: tweeting.rpc + +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.js b/generate/database/mongodb/20240829054501_post.up.js new file mode 100644 index 0000000..d2dabbb --- /dev/null +++ b/generate/database/mongodb/20240829054501_post.up.js @@ -0,0 +1,9 @@ +use digimon_tweeting; +db.post.createIndex({ "uid": 1}); +db.post.createIndex({ "status": 1}); +db.post.createIndex({ "is_ad": 1}); +db.post.createIndex({ "createAt": 1 }); +db.post.createIndex({ "uid": 1,"status": 1, "createAt": 1 }); +db.post.createIndex({ "uid": 1, "createAt": 1 }); + +// TODO 看是否有要刪除過多的索引,要在測試一下 \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index be6b914..968a1aa 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -1,27 +1,34 @@ syntax = "proto3"; package tweeting; -option go_package="./tweeting"; +option go_package = "./tweeting"; -// 基本回應 -message OKResp {} +// ========== 基本回應 =========== +message OKResp {} // 空的請求 -message NoneReq {} +message NoneReq {} // 分頁信息 message Pager { - int64 total =1; // 總數量 - int64 size=2; // 每頁數量 - int64 index=3; // 當前頁碼 + int64 total = 1; // 總數量 + int64 size = 2; // 每頁數量 + int64 index = 3; // 當前頁碼 } -// 新增貼文的請求 +// ========== 貼文區 =========== + +// ------ NewPost 新增貼文-------- message NewPostReq { - int64 user_id = 1; // 發佈貼文的用戶ID - string content = 2; // 貼文內容 - repeated string tags = 3; // 貼文相關標籤 - repeated string media_url = 4; // 這筆文章的所有 Media URL - bool is_ad = 5; // 是否為廣告 + string uid = 1; // 發佈貼文的用戶ID + string content = 2; // 貼文內容 + repeated string tags = 3; // 貼文相關標籤 + repeated Media media = 4; // 這筆文章的所有 Media URL + bool is_ad = 5; // 是否為廣告 +} + +message Media { + string type = 1; + string url = 2; } // 貼文回應 @@ -29,169 +36,69 @@ message PostResp { string post_id = 1; // 創建成功的貼文ID } +// ------ DeletePost 刪除貼文 ------ + // 刪除貼文的請求 message DeletePostsReq { - repeated string post_id = 1; // 貼文ID + repeated string post_id = 1; // 貼文ID } +// ------ UpdatePost 更新貼文 ------ // 更新貼文的請求 message UpdatePostReq { - string post_id = 1; // 貼文ID - repeated string tags = 2; // 新的標籤列表 - repeated string media_url = 3; // 這筆文章的所有 Media URL - optional string content = 4; // 新的貼文內容 - optional int64 like_count = 5; // 喜歡數量 - optional int64 dislike_count = 6; // 不喜歡數量 + string post_id = 1; // 貼文ID + repeated string tags = 2; // 新的標籤列表 + repeated Media media = 3; // 這筆文章的所有 Media URL + optional string content = 4; // 新的貼文內容 + optional int64 like_count = 5; // 喜歡數量 + optional int64 dislike_count = 6; // 不喜歡數量 } +// ------ListPosts 查詢貼文 ------ // 查詢貼文的請求 message QueryPostsReq { - repeated int64 user_id = 1; // 可選:根據用戶ID篩選貼文 - repeated int64 id = 2; // 可選:根據貼文ID篩選貼文 - repeated string tags = 3; // 可選:根據標籤篩選貼文 - optional bool only_ads = 4; // 可選:是否只顯示廣告 - int32 page_index = 5; // 分頁的頁碼 - int32 page_size = 6; // 每頁顯示的數量 + repeated string uid = 1; // 可選:根據用戶ID篩選貼文 + repeated string post_id = 2; // 可選:根據貼文ID篩選貼文 + optional int32 only_ads = 3; // 可選:是否只顯示廣告 0 不篩選 1 只顯示廣告 2 不顯示廣告 + int32 page_index = 4; // 分頁的頁碼 + int32 page_size = 5; // 每頁顯示的數量 } // 貼文詳情 message PostDetailItem { - string post_id = 1; // 貼文ID - int64 user_id = 2; // 發佈用戶ID - string content = 3; // 貼文內容 - repeated string tags = 4; // 標籤 - repeated string media_url = 5; // 圖片URL - bool is_ad = 6; // 是否為廣告 - int64 created_at = 7; // 發佈時間 - int64 update_at = 8; // 更新時間 - int64 like_count = 9; // 讚數 - int64 dislike_count = 10; // 不喜歡數量 + string post_id = 1; // 貼文ID + string uid = 2; // 發佈用戶ID + string content = 3; // 貼文內容 + repeated string tags = 4; // 標籤 + repeated Media media = 5; // 圖片URL + bool is_ad = 6; // 是否為廣告 + int64 created_at = 7; // 發佈時間 + int64 update_at = 8; // 更新時間 + int64 like_count = 9; // 讚數 + int64 dislike_count = 10; // 不喜歡數量 } // 貼文列表回應 message ListPostsResp { repeated PostDetailItem posts = 1; // 貼文列表 - Pager page =2; -} - -// 讚/不讚請求 -message LikeReq { - string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 user_id = 2; // 點讚的用戶ID - int64 like_type = 3; // 讚或爛的類型 -} - -// 讚/不讚項目 -message LikeItem { - string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 user_id = 2; // 點讚的用戶ID - int64 like_type = 3; // 讚或爛的類型 -} - -// 讚/不讚列表請求 -message LikeListReq { - string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 like_type = 2; // 讚或爛的類型 - int32 page_index = 3; // 當前頁碼 - int32 page_size = 4; // 每頁顯示數量 -} - -// 讚/不讚列表回應 -message LikeListResp { - repeated LikeItem list = 1; // 讚/不讚列表 Pager page = 2; } -// 讚/不讚數量請求 -message LikeCountReq { - string target_id = 1; // 目標ID(可以是貼文ID或評論ID) - int64 like_type = 2; // 讚或爛的類型 +message ModifyLikeDislikeCountReq { + string post_id = 1; // 貼文的 ID + int64 reaction_type = 2; // 用戶的反應類型,可能是讚或不讚 + bool is_increment = 3; // 表示是否增加(true 表示增加,false 表示減少) + int64 count = 4; // 異動數量 } -// 讚/不讚數量回應 -message LikeCountResp { - string count = 1; // 總共按讚數量 -} - -// 評論貼文的請求 -message CommentPostReq { - string post_id = 1; // 貼文ID - int64 user_id = 2; // 評論者ID - string content = 3; // 評論內容 -} - -// 查詢評論的請求 -message GetCommentsReq { - string post_id = 1; // 貼文ID - int32 page_index = 2; // 分頁頁碼 - int32 page_size = 3; // 每頁顯示數量 -} - -// 評論詳情 -message CommentDetail { - string comment_id = 1; // 評論ID - int64 user_id = 2; // 評論者ID - string content = 3; // 評論內容 - int64 created_at = 4; // 創建時間 - int64 like_count = 5; // 讚數 - int64 dislike_count = 6; // 不喜歡數量 -} - -// 評論列表回應 -message GetCommentsResp { - repeated CommentDetail comments = 1; // 評論列表 - Pager page = 2; -} - -// 刪除評論請求 -message DeleteCommentReq { - string comment_id = 1; // 評論ID -} - -// 更新評論請求 -message UpdateCommentReq { - string comment_id = 1; // 評論ID - string content = 2; // 更新後的評論內容 -} - -// 定義貼文服務 +// ========== 定義貼文服務(最基本單位,不要把邏輯放進來,也考慮是否要做快取) ========== service PostService { - // NewPost 新增貼文 - rpc NewPost(NewPostReq) returns(PostResp); + // CreatePost 新增貼文 + rpc CreatePost(NewPostReq) returns (PostResp); // DeletePost 刪除貼文 rpc DeletePost(DeletePostsReq) returns (OKResp); // UpdatePost 更新貼文 rpc UpdatePost(UpdatePostReq) returns (OKResp); // ListPosts 查詢貼文 rpc ListPosts(QueryPostsReq) returns (ListPostsResp); - - // Like 點讚/取消讚 貼文 - rpc Like(LikeReq) returns (OKResp); - // GetLikeStatus 取得讚/不讚狀態 - rpc GetLikeStatus(LikeReq) returns (OKResp); - // LikeList 取得讚/不讚列表 - rpc LikeList(LikeListReq) returns (LikeListResp); - // CountLike 取得讚/不讚數量 - rpc CountLike(LikeCountReq) returns (LikeCountResp); } - -// 定義評論服務 -service CommentService { - // NewComment 發表評論 - rpc NewComment(CommentPostReq) returns (OKResp); - // GetComments 查詢評論 - rpc GetComments(GetCommentsReq) returns (GetCommentsResp); - // DeleteComment 刪除評論 - rpc DeleteComment(DeleteCommentReq) returns (OKResp); - // UpdateComment 更新評論 - rpc UpdateComment(UpdateCommentReq) returns (OKResp); - - // LikeComment 點讚/取消讚 評論 - rpc LikeComment(LikeReq) returns (OKResp); - // GetLikeStatus 取得讚/不讚評論狀態 - rpc GetLikeStatus(LikeReq) returns (OKResp); - // LikeList 取得讚/不讚評論列表 - rpc LikeList(LikeListReq) returns (LikeListResp); - // CountLike 取得讚/不讚評論數量 - rpc CountLike(LikeCountReq) returns (LikeCountResp); -} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea41a6a --- /dev/null +++ b/go.mod @@ -0,0 +1,108 @@ +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/stretchr/testify v1.9.0 + github.com/zeromicro/go-zero v1.7.0 + go.mongodb.org/mongo-driver v1.16.0 + go.uber.org/mock v0.4.0 + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + 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 + github.com/google/uuid v1.6.0 // indirect + 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 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + 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 + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + 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 + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.3 // indirect + k8s.io/apimachinery v0.29.4 // indirect + k8s.io/client-go v0.29.3 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100755 index 0000000..73cf966 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,15 @@ +package config + +import "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 + } +} diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..0e7211b --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,13 @@ +package domain + +type AdType int32 + +func (a AdType) ToInt32() int32 { + return int32(a) +} + +const ( + AdTypeAll AdType = iota + AdTypeOnlyAd + AdTypeOnlyNotAd +) diff --git a/internal/domain/errors.go b/internal/domain/errors.go new file mode 100644 index 0000000..6c63b9d --- /dev/null +++ b/internal/domain/errors.go @@ -0,0 +1,17 @@ +package domain + +type ErrorCode uint32 + +func (e ErrorCode) ToUint32() uint32 { + return uint32(e) +} + +// Error Code 統一這邊改 +const ( + _ = iota + PostMongoErrorCode ErrorCode = iota + CreatePostError + DelPostError + UpdatePostError + ListPostError +) 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/create_post_logic.go b/internal/logic/postservice/create_post_logic.go new file mode 100644 index 0000000..73023e7 --- /dev/null +++ b/internal/logic/postservice/create_post_logic.go @@ -0,0 +1,114 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + + "code.30cm.net/digimon/library-go/errs/code" + + "context" + "fmt" + "strings" + + "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" +) + +type CreatePostLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePostLogic { + return &CreatePostLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// TODO 要調查一下內容如果存 html 是否有需要Encode +// 輸入的定義 -> 檢查用 +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 +} + +// 定義 Error + +// CreatePostError 0502102 資料庫錯誤 +func CreatePostError(s ...string) *ers.LibError { + return ers.NewError(code.CloudEPTweeting, code.DBError, + domain.CreatePostError.ToUint32(), + fmt.Sprintf("%s", strings.Join(s, " "))) +} + +// CreatePostErrorL logs error message and returns Error +func CreatePostErrorL(l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError { + e := CreatePostError(s...) + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + + return e +} + +// CreatePost 新增貼文 +func (l *CreatePostLogic) CreatePost(in *tweeting.NewPostReq) (*tweeting.PostResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&newTweetingReq{ + UID: in.GetUid(), + Content: in.GetContent(), + }); err != nil { + // 錯誤代碼 05-011-00 + 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() + } + + 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.MediaURL = media + } + // ============ insert ============ + err := l.svcCtx.PostModel.Insert(l.ctx, tweet) + if err != nil { + e := CreatePostErrorL( + 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/create_post_logic_test.go b/internal/logic/postservice/create_post_logic_test.go new file mode 100644 index 0000000..d7deaf5 --- /dev/null +++ b/internal/logic/postservice/create_post_logic_test.go @@ -0,0 +1,107 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockmodel "app-cloudep-tweeting-service/internal/mock/model" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestCreatePost(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockPostModel := mockmodel.NewMockPostModel(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + PostModel: mockPostModel, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.NewPostReq + prepare func() + expectErr bool + }{ + { + name: "成功創建貼文", + input: &tweeting.NewPostReq{ + Uid: "12345", + Content: "Test content", + IsAd: false, + Tags: []string{"tag1", "tag2"}, + Media: []*tweeting.Media{ + { + Url: "http://example.com/image.png", + Type: "image", + }, + }, + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockPostModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "驗證失敗", + input: &tweeting.NewPostReq{ + Uid: "", + Content: "", + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "插入貼文失敗", + input: &tweeting.NewPostReq{ + Uid: "12345", + Content: "Test content", + IsAd: false, + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockPostModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert failed")).Times(1) + }, + expectErr: true, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 CreatePostLogic + logic := CreatePostLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 CreatePost + _, err := logic.CreatePost(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/logic/postservice/delete_post_logic.go b/internal/logic/postservice/delete_post_logic.go new file mode 100644 index 0000000..33a9e81 --- /dev/null +++ b/internal/logic/postservice/delete_post_logic.go @@ -0,0 +1,63 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + "fmt" + "strings" + + ers "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeletePostLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeletePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePostLogic { + return &DeletePostLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// DeletePostError 0502103 資料庫錯誤 +func DeletePostError(s ...string) *ers.LibError { + return ers.NewError(code.CloudEPTweeting, code.DBError, + domain.DelPostError.ToUint32(), + fmt.Sprintf("%s", strings.Join(s, " "))) +} + +// DeletePostErrorL logs error message and returns Error +func DeletePostErrorL(l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError { + e := DeletePostError(s...) + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + + return e +} + +// DeletePost 刪除貼文 +func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKResp, error) { + _, err := l.svcCtx.PostModel.DeleteMany(l.ctx, in.GetPostId()...) + if err != nil { + e := DeletePostErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.DeleteMany"}, + {Key: "err", Value: err}, + }, + "failed to del post").Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/postservice/delete_post_logic_test.go b/internal/logic/postservice/delete_post_logic_test.go new file mode 100644 index 0000000..adb2cbe --- /dev/null +++ b/internal/logic/postservice/delete_post_logic_test.go @@ -0,0 +1,81 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mockmodel "app-cloudep-tweeting-service/internal/mock/model" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestDeletePost(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockPostModel := mockmodel.NewMockPostModel(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + PostModel: mockPostModel, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DeletePostsReq + prepare func() + expectErr bool + }{ + { + name: "成功刪除貼文", + input: &tweeting.DeletePostsReq{ + PostId: []string{"12345", "67890"}, + }, + prepare: func() { + // 模擬 DeleteMany 成功 + mockPostModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(2), nil).Times(1) + }, + expectErr: false, + }, + { + name: "刪除貼文失敗", + input: &tweeting.DeletePostsReq{ + PostId: []string{"12345", "67890"}, + }, + prepare: func() { + // 模擬 DeleteMany 失敗 + mockPostModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(0), errors.New("delete failed")).Times(1) + }, + expectErr: true, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 DeletePostLogic + logic := DeletePostLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 DeletePost + _, err := logic.DeletePost(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/logic/postservice/list_posts_logic.go b/internal/logic/postservice/list_posts_logic.go new file mode 100644 index 0000000..a3e7f08 --- /dev/null +++ b/internal/logic/postservice/list_posts_logic.go @@ -0,0 +1,139 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + "context" + "fmt" + "strings" + + ers "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "google.golang.org/protobuf/proto" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListPostsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPostsLogic { + return &ListPostsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 只列出要驗證的資料 +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"` +} + +// ListPostError 0502105 資料庫錯誤 +func ListPostError(s ...string) *ers.LibError { + return ers.NewError(code.CloudEPTweeting, code.DBError, + domain.ListPostError.ToUint32(), + fmt.Sprintf("%s", strings.Join(s, " "))) +} + +// ListPostErrorL logs error message and returns Error +func ListPostErrorL(l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError { + e := ListPostError(s...) + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + + return e +} + +// 將單個 Post 轉換為 PostDetailItem +func convertToPostDetailItem(item *model.Post) *tweeting.PostDetailItem { + media := make([]*tweeting.Media, 0, len(item.MediaURL)) + for _, subItem := range item.MediaURL { + media = append(media, &tweeting.Media{ + Type: subItem.Type, + Url: subItem.Links, + }) + } + + return &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), + } +} + +// ListPosts 查詢貼文 -> 主流程 +func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPostsResp, error) { + // 將 PageSize 和 PageIndex 提前轉換為 int64 + pageSize := int64(in.GetPageSize()) + pageIndex := int64(in.GetPageIndex()) + + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&listReq{ + PageSize: pageSize, + PageIndex: pageIndex, + OnlyAdds: in.GetOnlyAds(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + // 構建查詢條件 + query := &model.QueryPostModelReq{ + UID: in.GetUid(), + Id: in.GetPostId(), + PageSize: pageSize, + PageIndex: pageIndex, + } + + // 處理 OnlyAds 條件 + if in.OnlyAds != nil { + onlyAds := in.GetOnlyAds() + query.OnlyAds = proto.Bool(onlyAds == domain.AdTypeOnlyAd.ToInt32()) + } + + // 執行查詢 + find, count, err := l.svcCtx.PostModel.Find(l.ctx, query) + if err != nil { + e := ListPostErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "query", Value: query}, + {Key: "func", Value: "PostModel.Find"}, + {Key: "err", Value: err}, + }, + "failed to find posts").Wrap(err) + return nil, e + } + + // 將查詢結果轉換為 API 回應格式 + result := make([]*tweeting.PostDetailItem, 0, count) + for _, item := range find { + result = append(result, convertToPostDetailItem(item)) + } + + // 返回結果 + return &tweeting.ListPostsResp{ + Posts: result, + Page: &tweeting.Pager{ + Total: count, + Index: pageIndex, + Size: pageSize, + }, + }, nil +} diff --git a/internal/logic/postservice/list_posts_logic_test.go b/internal/logic/postservice/list_posts_logic_test.go new file mode 100644 index 0000000..9e233df --- /dev/null +++ b/internal/logic/postservice/list_posts_logic_test.go @@ -0,0 +1,164 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockmodel "app-cloudep-tweeting-service/internal/mock/model" + model "app-cloudep-tweeting-service/internal/model/mongo" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + "time" + + "google.golang.org/protobuf/proto" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +func TestConvertToPostDetailItem(t *testing.T) { + // 構建測試數據 + postID := primitive.NewObjectID() + post := &model.Post{ + ID: postID, + UID: "12345", + Content: "Test Content", + Tags: []string{"tag1", "tag2"}, + MediaURL: []model.Media{ + {Type: "image", Links: "http://example.com/image.png"}, + {Type: "video", Links: "http://example.com/video.mp4"}, + }, + IsAd: true, + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + Like: 10, + DisLike: 2, + } + + // 執行轉換 + result := convertToPostDetailItem(post) + + // 驗證結果 + assert.Equal(t, postID.Hex(), result.PostId) + assert.Equal(t, "12345", result.Uid) + assert.Equal(t, "Test Content", result.Content) + assert.Equal(t, []string{"tag1", "tag2"}, result.Tags) + assert.Equal(t, true, result.IsAd) + assert.Equal(t, int64(10), result.LikeCount) + assert.Equal(t, int64(2), result.DislikeCount) + + // 驗證 Media 的轉換 + assert.Len(t, result.Media, 2) + assert.Equal(t, "image", result.Media[0].Type) + assert.Equal(t, "http://example.com/image.png", result.Media[0].Url) + assert.Equal(t, "video", result.Media[1].Type) + assert.Equal(t, "http://example.com/video.mp4", result.Media[1].Url) +} + +func TestListPosts(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockPostModel := mockmodel.NewMockPostModel(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + PostModel: mockPostModel, + Validate: mockValidate, + } + + // 構建測試數據 + queryReq := &tweeting.QueryPostsReq{ + PageSize: 10, + PageIndex: 1, + OnlyAds: proto.Int32(1), + } + + mockPosts := []*model.Post{ + { + ID: primitive.NewObjectID(), + UID: "12345", + Content: "Test Content 1", + Tags: []string{"tag1", "tag2"}, + MediaURL: []model.Media{ + {Type: "image", Links: "http://example.com/image1.png"}, + }, + IsAd: false, + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + Like: 5, + DisLike: 1, + }, + { + ID: primitive.NewObjectID(), + UID: "67890", + Content: "Test Content 2", + Tags: []string{"tag3", "tag4"}, + MediaURL: []model.Media{ + {Type: "video", Links: "http://example.com/video1.mp4"}, + }, + IsAd: true, + CreateAt: time.Now().Unix(), + UpdateAt: time.Now().Unix(), + Like: 3, + DisLike: 0, + }, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.QueryPostsReq + prepare func() + expectErr bool + }{ + { + name: "成功查詢貼文", + input: queryReq, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockPostModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(mockPosts, int64(len(mockPosts)), nil).Times(1) + }, + expectErr: false, + }, + { + name: "查詢貼文失敗", + input: queryReq, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockPostModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("find failed")).Times(1) + }, + expectErr: true, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 ListPostsLogic + logic := ListPostsLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 ListPosts + resp, err := logic.ListPosts(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Len(t, resp.Posts, len(mockPosts)) + } + }) + } +} diff --git a/internal/logic/postservice/update_post_logic.go b/internal/logic/postservice/update_post_logic.go new file mode 100644 index 0000000..acdb501 --- /dev/null +++ b/internal/logic/postservice/update_post_logic.go @@ -0,0 +1,100 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + model "app-cloudep-tweeting-service/internal/model/mongo" + "context" + "fmt" + "strings" + + ers "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" + "go.mongodb.org/mongo-driver/bson/primitive" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdatePostLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePostLogic { + return &UpdatePostLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type checkPostId struct { + PostID string `validate:"required"` + Content string `json:"content,omitempty" validate:"lte=500"` +} + +// UpdatePostError 0502104 資料庫錯誤 +func UpdatePostError(s ...string) *ers.LibError { + return ers.NewError(code.CloudEPTweeting, code.DBError, + domain.UpdatePostError.ToUint32(), + fmt.Sprintf("%s", strings.Join(s, " "))) +} + +// UpdatePostErrorL logs error message and returns Error +func UpdatePostErrorL(l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError { + e := UpdatePostError(s...) + l.WithCallerSkip(1).WithFields(filed...).Error(e.Error()) + + return e +} + +// UpdatePost 更新貼文 +func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&checkPostId{ + PostID: in.GetPostId(), + Content: in.GetContent(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + // 沒有就沒有,有就走全覆蓋 + update := model.Post{} + oid, err := primitive.ObjectIDFromHex(in.GetPostId()) + if err != nil { + // 錯誤代碼 05-011-00 + 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.MediaURL = 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 := UpdatePostErrorL( + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "PostModel.UpdateOptional"}, + {Key: "err", Value: err}, + }, + "failed to update post", in.PostId).Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/postservice/update_post_logic_test.go b/internal/logic/postservice/update_post_logic_test.go new file mode 100644 index 0000000..1858572 --- /dev/null +++ b/internal/logic/postservice/update_post_logic_test.go @@ -0,0 +1,118 @@ +package postservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockmodel "app-cloudep-tweeting-service/internal/mock/model" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + + "go.mongodb.org/mongo-driver/mongo" + "google.golang.org/protobuf/proto" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "testing" +) + +func TestUpdatePost(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockPostModel := mockmodel.NewMockPostModel(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + PostModel: mockPostModel, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.UpdatePostReq + prepare func() + expectErr bool + }{ + { + name: "成功更新貼文", + input: &tweeting.UpdatePostReq{ + PostId: "66cfdc1d6f8fe7eac1e52523", + Content: proto.String("Updated content"), + Tags: []string{"tag1", "tag2"}, + Media: []*tweeting.Media{ + { + Url: "http://example.com/image.png", + Type: "image", + }, + }, + LikeCount: proto.Int64(10), + DislikeCount: proto.Int64(2), + }, + prepare: func() { + // 模擬 Validate 成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 UpdateOptional 成功 + mockPostModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(&mongo.UpdateResult{ + ModifiedCount: 1, + }, nil).Times(1) + }, + expectErr: false, + }, + { + name: "驗證失敗", + input: &tweeting.UpdatePostReq{ + PostId: "", + }, + prepare: func() { + // 模擬 Validate 失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "更新貼文失敗", + input: &tweeting.UpdatePostReq{ + PostId: "66cfdc1d6f8fe7eac1e52523", + Content: proto.String("Updated content"), + }, + prepare: func() { + // 模擬 Validate 成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 UpdateOptional 失敗 + mockPostModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(&mongo.UpdateResult{ + ModifiedCount: 0, + }, errors.New("update failed")).Times(1) + }, + expectErr: true, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 UpdatePostLogic + logic := UpdatePostLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 UpdatePost + _, err := logic.UpdatePost(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/mock/lib/validate.go b/internal/mock/lib/validate.go new file mode 100644 index 0000000..dd852f8 --- /dev/null +++ b/internal/mock/lib/validate.go @@ -0,0 +1,73 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./validate.go +// +// Generated by this command: +// +// mockgen -source=./validate.go -destination=../../mock/lib/validate.go -package=lib +// + +// Package lib is a generated GoMock package. +package lib + +import ( + reflect "reflect" + + required "code.30cm.net/digimon/library-go/validator" + + gomock "go.uber.org/mock/gomock" +) + +// MockValidate is a mock of Validate interface. +type MockValidate struct { + ctrl *gomock.Controller + recorder *MockValidateMockRecorder +} + +// MockValidateMockRecorder is the mock recorder for MockValidate. +type MockValidateMockRecorder struct { + mock *MockValidate +} + +// NewMockValidate creates a new mock instance. +func NewMockValidate(ctrl *gomock.Controller) *MockValidate { + mock := &MockValidate{ctrl: ctrl} + mock.recorder = &MockValidateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockValidate) EXPECT() *MockValidateMockRecorder { + return m.recorder +} + +// BindToValidator mocks base method. +func (m *MockValidate) BindToValidator(opts ...required.Option) error { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BindToValidator", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// BindToValidator indicates an expected call of BindToValidator. +func (mr *MockValidateMockRecorder) BindToValidator(opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindToValidator", reflect.TypeOf((*MockValidate)(nil).BindToValidator), opts...) +} + +// ValidateAll mocks base method. +func (m *MockValidate) ValidateAll(obj any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateAll", obj) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateAll indicates an expected call of ValidateAll. +func (mr *MockValidateMockRecorder) ValidateAll(obj any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAll", reflect.TypeOf((*MockValidate)(nil).ValidateAll), obj) +} diff --git a/internal/mock/model/post_model.go b/internal/mock/model/post_model.go new file mode 100644 index 0000000..bf3fd1b --- /dev/null +++ b/internal/mock/model/post_model.go @@ -0,0 +1,152 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/model/mongo/post_model.go +// +// Generated by this command: +// +// mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + model "app-cloudep-tweeting-service/internal/model/mongo" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockPostModel is a mock of PostModel interface. +type MockPostModel struct { + ctrl *gomock.Controller + recorder *MockPostModelMockRecorder +} + +// MockPostModelMockRecorder is the mock recorder for MockPostModel. +type MockPostModelMockRecorder struct { + mock *MockPostModel +} + +// NewMockPostModel creates a new mock instance. +func NewMockPostModel(ctrl *gomock.Controller) *MockPostModel { + mock := &MockPostModel{ctrl: ctrl} + mock.recorder = &MockPostModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPostModel) EXPECT() *MockPostModelMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockPostModel) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockPostModelMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPostModel)(nil).Delete), ctx, id) +} + +// DeleteMany mocks base method. +func (m *MockPostModel) DeleteMany(ctx context.Context, id ...string) (int64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range id { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteMany", varargs...) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteMany indicates an expected call of DeleteMany. +func (mr *MockPostModelMockRecorder) DeleteMany(ctx any, id ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, id...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockPostModel)(nil).DeleteMany), varargs...) +} + +// Find mocks base method. +func (m *MockPostModel) Find(ctx context.Context, param *model.QueryPostModelReq) ([]*model.Post, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", ctx, param) + ret0, _ := ret[0].([]*model.Post) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Find indicates an expected call of Find. +func (mr *MockPostModelMockRecorder) Find(ctx, param any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockPostModel)(nil).Find), ctx, param) +} + +// FindOne mocks base method. +func (m *MockPostModel) FindOne(ctx context.Context, id string) (*model.Post, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockPostModelMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockPostModel)(nil).FindOne), ctx, id) +} + +// Insert mocks base method. +func (m *MockPostModel) Insert(ctx context.Context, data *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockPostModelMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockPostModel)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockPostModel) Update(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockPostModelMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPostModel)(nil).Update), ctx, data) +} + +// UpdateOptional mocks base method. +func (m *MockPostModel) UpdateOptional(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOptional", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateOptional indicates an expected call of UpdateOptional. +func (mr *MockPostModelMockRecorder) UpdateOptional(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOptional", reflect.TypeOf((*MockPostModel)(nil).UpdateOptional), ctx, data) +} diff --git a/internal/mock/model/post_model_gen.go b/internal/mock/model/post_model_gen.go new file mode 100644 index 0000000..4b22d6b --- /dev/null +++ b/internal/mock/model/post_model_gen.go @@ -0,0 +1,101 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/model/mongo/post_model_gen.go +// +// Generated by this command: +// +// mockgen -source=./internal/model/mongo/post_model_gen.go -destination=./internal/mock/model/post_model_gen.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + model "app-cloudep-tweeting-service/internal/model/mongo" + context "context" + reflect "reflect" + + mongo "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockpostModel is a mock of postModel interface. +type MockpostModel struct { + ctrl *gomock.Controller + recorder *MockpostModelMockRecorder +} + +// MockpostModelMockRecorder is the mock recorder for MockpostModel. +type MockpostModelMockRecorder struct { + mock *MockpostModel +} + +// NewMockpostModel creates a new mock instance. +func NewMockpostModel(ctrl *gomock.Controller) *MockpostModel { + mock := &MockpostModel{ctrl: ctrl} + mock.recorder = &MockpostModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockpostModel) EXPECT() *MockpostModelMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockpostModel) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockpostModelMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockpostModel)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockpostModel) FindOne(ctx context.Context, id string) (*model.Post, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*model.Post) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockpostModelMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockpostModel)(nil).FindOne), ctx, id) +} + +// Insert mocks base method. +func (m *MockpostModel) Insert(ctx context.Context, data *model.Post) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockpostModelMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockpostModel)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockpostModel) Update(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockpostModelMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockpostModel)(nil).Update), ctx, data) +} diff --git a/internal/model/mongo/comment_model.go b/internal/model/mongo/comment_model.go new file mode 100644 index 0000000..30fe357 --- /dev/null +++ b/internal/model/mongo/comment_model.go @@ -0,0 +1,25 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/mon" + +var _ CommentModel = (*customCommentModel)(nil) + +type ( + // CommentModel is an interface to be customized, add more methods here, + // and implement the added methods in customCommentModel. + CommentModel interface { + commentModel + } + + customCommentModel struct { + *defaultCommentModel + } +) + +// NewCommentModel returns a model for the mongo. +func NewCommentModel(url, db, collection string) CommentModel { + conn := mon.MustNewModel(url, db, collection) + return &customCommentModel{ + defaultCommentModel: newDefaultCommentModel(conn), + } +} diff --git a/internal/model/mongo/comment_model_gen.go b/internal/model/mongo/comment_model_gen.go new file mode 100644 index 0000000..6c5609b --- /dev/null +++ b/internal/model/mongo/comment_model_gen.go @@ -0,0 +1,74 @@ +// Code generated by goctl. DO NOT EDIT. +package model + +import ( + "context" + "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" +) + +type commentModel interface { + Insert(ctx context.Context, data *Comment) error + FindOne(ctx context.Context, id string) (*Comment, error) + Update(ctx context.Context, data *Comment) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) +} + +type defaultCommentModel struct { + conn *mon.Model +} + +func newDefaultCommentModel(conn *mon.Model) *defaultCommentModel { + return &defaultCommentModel{conn: conn} +} + +func (m *defaultCommentModel) Insert(ctx context.Context, data *Comment) error { + if data.ID.IsZero() { + data.ID = primitive.NewObjectID() + data.CreateAt = time.Now() + data.UpdateAt = time.Now() + } + + _, err := m.conn.InsertOne(ctx, data) + return err +} + +func (m *defaultCommentModel) FindOne(ctx context.Context, id string) (*Comment, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectId + } + + var data Comment + + err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid}) + switch err { + case nil: + return &data, nil + case mon.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultCommentModel) Update(ctx context.Context, data *Comment) (*mongo.UpdateResult, error) { + data.UpdateAt = time.Now() + + res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data}) + return res, err +} + +func (m *defaultCommentModel) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectId + } + + res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid}) + return res, err +} diff --git a/internal/model/mongo/comment_types.go b/internal/model/mongo/comment_types.go new file mode 100644 index 0000000..f82cabc --- /dev/null +++ b/internal/model/mongo/comment_types.go @@ -0,0 +1,14 @@ +package model + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Comment 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"` +} diff --git a/internal/model/mongo/error.go b/internal/model/mongo/error.go new file mode 100644 index 0000000..27d9244 --- /dev/null +++ b/internal/model/mongo/error.go @@ -0,0 +1,12 @@ +package model + +import ( + "errors" + + "github.com/zeromicro/go-zero/core/stores/mon" +) + +var ( + ErrNotFound = mon.ErrNotFound + ErrInvalidObjectId = errors.New("invalid objectId") +) diff --git a/internal/model/mongo/post_model.go b/internal/model/mongo/post_model.go new file mode 100644 index 0000000..9829de5 --- /dev/null +++ b/internal/model/mongo/post_model.go @@ -0,0 +1,185 @@ +package model + +import ( + "context" + "errors" + "time" + + "github.com/zeromicro/go-zero/core/stores/monc" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/zeromicro/go-zero/core/logx" + "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" +) + +var _ PostModel = (*customPostModel)(nil) + +type ( + // PostModel is an interface to be customized, add more methods here, + // 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) + } + + customPostModel struct { + *defaultPostModel + } + + QueryPostModelReq struct { + UID []string + Id []string + OnlyAds *bool + PageSize int64 + PageIndex int64 + } +) + +// NewPostModel returns a model for the mongo. +func NewPostModel(url, db, collection string) PostModel { + conn := mon.MustNewModel(url, db, collection) + return &customPostModel{ + defaultPostModel: newDefaultPostModel(conn), + } +} + +func (m *defaultPostModel) DeleteMany(ctx context.Context, id ...string) (int64, error) { + objectIDs := make([]primitive.ObjectID, 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) + } + + // 檢查是否有有效的 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 + } + + 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.MediaURL) > 0 { + update["$set"].(bson.M)["media_url"] = data.MediaURL + } + + 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() + res, err := m.conn.UpdateOne(ctx, 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 + } +} diff --git a/internal/model/mongo/post_model_gen.go b/internal/model/mongo/post_model_gen.go new file mode 100644 index 0000000..8948be5 --- /dev/null +++ b/internal/model/mongo/post_model_gen.go @@ -0,0 +1,74 @@ +// Code generated by goctl. DO NOT EDIT. +package model + +import ( + "context" + "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" +) + +type postModel interface { + Insert(ctx context.Context, data *Post) error + FindOne(ctx context.Context, id string) (*Post, error) + Update(ctx context.Context, data *Post) (*mongo.UpdateResult, error) + Delete(ctx context.Context, id string) (int64, error) +} + +type defaultPostModel struct { + conn *mon.Model +} + +func newDefaultPostModel(conn *mon.Model) *defaultPostModel { + return &defaultPostModel{conn: conn} +} + +func (m *defaultPostModel) Insert(ctx context.Context, data *Post) error { + if data.ID.IsZero() { + data.ID = primitive.NewObjectID() + data.CreateAt = time.Now().UTC().UnixNano() + data.UpdateAt = time.Now().UTC().UnixNano() + } + + _, err := m.conn.InsertOne(ctx, data) + return err +} + +func (m *defaultPostModel) FindOne(ctx context.Context, id string) (*Post, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectId + } + + var data Post + + err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid}) + switch err { + case nil: + return &data, nil + case mon.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +func (m *defaultPostModel) Update(ctx context.Context, data *Post) (*mongo.UpdateResult, error) { + data.UpdateAt = time.Now().UTC().UnixNano() + + res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data}) + return res, err +} + +func (m *defaultPostModel) Delete(ctx context.Context, id string) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, ErrInvalidObjectId + } + + res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid}) + return res, err +} diff --git a/internal/model/mongo/post_types.go b/internal/model/mongo/post_types.go new file mode 100644 index 0000000..3396ab9 --- /dev/null +++ b/internal/model/mongo/post_types.go @@ -0,0 +1,32 @@ +package model + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Post struct { + 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 建立之後就不提供修改與刪除) + MediaURL []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" +} + +// 照邏輯,應該需要建立的索引有 +// 單列索引:UID、Status、IsAd、CreateAt +// 複合索引:(UID + Status + IsAd)、(UID + CreateAt) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go new file mode 100644 index 0000000..27f442f --- /dev/null +++ b/internal/server/postservice/post_service_server.go @@ -0,0 +1,53 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: tweeting.proto + +package server + +import ( + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" + "app-cloudep-tweeting-service/internal/svc" +) + +type PostServiceServer struct { + svcCtx *svc.ServiceContext + tweeting.UnimplementedPostServiceServer +} + +func NewPostServiceServer(svcCtx *svc.ServiceContext) *PostServiceServer { + return &PostServiceServer{ + svcCtx: svcCtx, + } +} + +// CreatePost 新增貼文 +func (s *PostServiceServer) CreatePost(ctx context.Context, in *tweeting.NewPostReq) (*tweeting.PostResp, error) { + l := postservicelogic.NewCreatePostLogic(ctx, s.svcCtx) + return l.CreatePost(in) +} + +// DeletePost 刪除貼文 +func (s *PostServiceServer) DeletePost(ctx context.Context, in *tweeting.DeletePostsReq) (*tweeting.OKResp, error) { + l := postservicelogic.NewDeletePostLogic(ctx, s.svcCtx) + return l.DeletePost(in) +} + +// UpdatePost 更新貼文 +func (s *PostServiceServer) UpdatePost(ctx context.Context, in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { + l := postservicelogic.NewUpdatePostLogic(ctx, s.svcCtx) + return l.UpdatePost(in) +} + +// ListPosts 查詢貼文 +func (s *PostServiceServer) ListPosts(ctx context.Context, in *tweeting.QueryPostsReq) (*tweeting.ListPostsResp, error) { + l := postservicelogic.NewListPostsLogic(ctx, s.svcCtx) + return l.ListPosts(in) +} + +// ModifyLikeDislikeCount 調整讚或不讚數量 +func (s *PostServiceServer) ModifyLikeDislikeCount(ctx context.Context, in *tweeting.ModifyLikeDislikeCountReq) (*tweeting.OKResp, error) { + l := postservicelogic.NewModifyLikeDislikeCountLogic(ctx, s.svcCtx) + return l.ModifyLikeDislikeCount(in) +} diff --git a/internal/svc/init_mongo.go b/internal/svc/init_mongo.go new file mode 100644 index 0000000..980c67e --- /dev/null +++ b/internal/svc/init_mongo.go @@ -0,0 +1,20 @@ +package svc + +import ( + "app-cloudep-tweeting-service/internal/config" + model "app-cloudep-tweeting-service/internal/model/mongo" + "fmt" +) + +func mustMongoConnectUrl(c config.Config) string { + return fmt.Sprintf("%s://%s:%s", + c.Mongo.Schema, + c.Mongo.Host, + c.Mongo.Port, + ) +} + +func MustPostModel(c config.Config) model.PostModel { + postCollection := model.Post{} + return model.NewPostModel(mustMongoConnectUrl(c), c.Mongo.Database, postCollection.CollectionName()) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go new file mode 100644 index 0000000..ec86c98 --- /dev/null +++ b/internal/svc/service_context.go @@ -0,0 +1,24 @@ +package svc + +import ( + "app-cloudep-tweeting-service/internal/config" + model "app-cloudep-tweeting-service/internal/model/mongo" + + vi "code.30cm.net/digimon/library-go/validator" +) + +type ServiceContext struct { + Config config.Config + Validate vi.Validate + + PostModel model.PostModel +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Validate: vi.MustValidator(), + + PostModel: MustPostModel(c), + } +} diff --git a/tweeting.go b/tweeting.go new file mode 100644 index 0000000..c708b77 --- /dev/null +++ b/tweeting.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/config" + postserviceServer "app-cloudep-tweeting-service/internal/server/postservice" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/tweeting.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + tweeting.RegisterPostServiceServer(grpcServer, postserviceServer.NewPostServiceServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +}