feature/fanout #3
			
				
			
		
		
		
	|  | @ -117,6 +117,14 @@ issues: | ||||||
|         - gocognit |         - gocognit | ||||||
|         - contextcheck |         - contextcheck | ||||||
| 
 | 
 | ||||||
|  |   exclude-dirs: | ||||||
|  |     - internal/model | ||||||
|  | 
 | ||||||
|  |   exclude-files: | ||||||
|  |     - .*_test.go | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| linters-settings: | linters-settings: | ||||||
|   gci: |   gci: | ||||||
|     sections: |     sections: | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										9
									
								
								Makefile
								
								
								
								
							|  | @ -18,6 +18,7 @@ test: # 進行測試 | ||||||
| fmt: # 格式優化
 | fmt: # 格式優化
 | ||||||
| 	$(GOFMT) -w $(GOFILES) | 	$(GOFMT) -w $(GOFILES) | ||||||
| 	goimports -w  ./ | 	goimports -w  ./ | ||||||
|  | 	golangci-lint run | ||||||
| 
 | 
 | ||||||
| .PHONY: gen-rpc | .PHONY: gen-rpc | ||||||
| gen-rpc: # 建立 rpc code
 | gen-rpc: # 建立 rpc code
 | ||||||
|  | @ -51,9 +52,9 @@ gen-mongo-model: # 建立 rpc 資料庫 | ||||||
| 	# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是 | 	# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是 | ||||||
| 	goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | 	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 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 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 post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||||
| #	goctl model mongo -t comment_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" | 	@echo "Generate mongo model files successfully" | ||||||
| 
 | 
 | ||||||
| .PHONY: mock-gen | .PHONY: mock-gen | ||||||
|  | @ -62,6 +63,8 @@ mock-gen: # 建立 mock 資料 | ||||||
| 	mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock | 	mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock | ||||||
| 	mockgen -source=./internal/model/mongo/comment_model_gen.go -destination=./internal/mock/model/comment_model_gen.go -package=mock | 	mockgen -source=./internal/model/mongo/comment_model_gen.go -destination=./internal/mock/model/comment_model_gen.go -package=mock | ||||||
| 	mockgen -source=./internal/model/mongo/comment_model.go -destination=./internal/mock/model/comment_model.go -package=mock | 	mockgen -source=./internal/model/mongo/comment_model.go -destination=./internal/mock/model/comment_model.go -package=mock | ||||||
|  | 	mockgen -source=./internal/domain/repository/social_network.go -destination=./internal/mock/repository/social_network.go -package=mock | ||||||
|  | 	mockgen -source=./internal/domain/repository/timeline.go -destination=./internal/mock/repository/timeline.go -package=mock | ||||||
| 	@echo "Generate mock files successfully" | 	@echo "Generate mock files successfully" | ||||||
| 
 | 
 | ||||||
| .PHONY: migrate-database | .PHONY: migrate-database | ||||||
|  |  | ||||||
|  | @ -12,3 +12,20 @@ Mongo: | ||||||
|   Password: "" |   Password: "" | ||||||
|   Port: "27017" |   Port: "27017" | ||||||
|   Database: digimon_tweeting |   Database: digimon_tweeting | ||||||
|  | 
 | ||||||
|  | TimelineSetting: | ||||||
|  |   Expire: 86400 | ||||||
|  |   MaxLength: 1000 | ||||||
|  | 
 | ||||||
|  | RedisCluster: | ||||||
|  |   Host: 127.0.0.1:7001 | ||||||
|  |   Type: cluster | ||||||
|  | 
 | ||||||
|  | Neo4J: | ||||||
|  |   URI: bolt://localhost:7687 | ||||||
|  |   Username: neo4j | ||||||
|  |   Password: yyyytttt | ||||||
|  |   MaxConnectionPoolSize: 20 | ||||||
|  |   MaxConnectionLifetime: 200s | ||||||
|  |   ConnectionTimeout     : 200s | ||||||
|  |   LogLevel : debug | ||||||
|  | @ -1,5 +1,2 @@ | ||||||
| use digimon_tweeting; | use digimon_tweeting; | ||||||
| db.comment.createIndex({ "post_id": 1,"createAt":1}); | db.comment.createIndex({ "post_id": 1,"createAt":1}); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // TODO 看是否有要刪除過多的索引,要在測試一下 |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | // 企業版才能用,社群版只能用預設的 | ||||||
|  | CREATE DATABASE relation; | ||||||
|  | 
 | ||||||
|  | // 創建 User 節點 UID 是唯一鍵 | ||||||
|  | CREATE CONSTRAINT FOR (u:User) REQUIRE u.uid IS UNIQUE | ||||||
|  | @ -182,3 +182,117 @@ service CommentService | ||||||
|   // UpdateComment 更新評論 |   // UpdateComment 更新評論 | ||||||
|   rpc UpdateComment(UpdateCommentReq) returns (OKResp); |   rpc UpdateComment(UpdateCommentReq) returns (OKResp); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // ========== TimeLineService (個人動態時報) ========== | ||||||
|  | 
 | ||||||
|  | message GetTimelineReq | ||||||
|  | { | ||||||
|  |   string uid = 1;      // 用户ID | ||||||
|  |   int64 pageIndex = 2; // 頁碼 | ||||||
|  |   int64 pageSize = 3;  // 每一頁大小 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FetchTimelineResponse | ||||||
|  | { | ||||||
|  |   repeated FetchTimelineItem posts = 1; // 貼文列表 | ||||||
|  |   Pager page = 2;                       // 分頁訊息 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FetchTimelineItem | ||||||
|  | { | ||||||
|  |   string post_id = 1; | ||||||
|  |   int64 score = 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message AddPostToTimelineReq | ||||||
|  | { | ||||||
|  |   string uid = 1; // key | ||||||
|  |   repeated PostTimelineItem posts = 3; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 貼文更新 | ||||||
|  | message PostTimelineItem | ||||||
|  | { | ||||||
|  |   string post_id = 1;   // 貼文ID | ||||||
|  |   int64 created_at = 7; // 發佈時間 -> 排序使用 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message DoNoMoreDataReq | ||||||
|  | { | ||||||
|  |   string uid = 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message HasNoMoreDataResp | ||||||
|  | { | ||||||
|  |   bool status = 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TimelineService 業務邏輯在外面組合 | ||||||
|  | service TimelineService | ||||||
|  | { | ||||||
|  |   // AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷 | ||||||
|  |   // 誰要砍誰不砍,處理排序,上限是 1000 筆(超過1000 請他回資料庫拿) | ||||||
|  |   // 只存活躍用戶的,不活躍不浪費快取的空間 | ||||||
|  |   rpc AddPost(AddPostToTimelineReq) returns (OKResp); | ||||||
|  |   // FetchTimeline 取得這個人的動態時報 | ||||||
|  |   rpc FetchTimeline(GetTimelineReq) returns (FetchTimelineResponse); | ||||||
|  |   // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。 | ||||||
|  |   rpc SetNoMoreDataFlag(DoNoMoreDataReq) returns (OKResp); | ||||||
|  |   // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。 | ||||||
|  |   rpc HasNoMoreData(DoNoMoreDataReq) returns (HasNoMoreDataResp); | ||||||
|  |   // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 | ||||||
|  |   rpc ClearNoMoreDataFlag(DoNoMoreDataReq) returns (OKResp); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ========== Social Network (關係網路) ========== | ||||||
|  | 
 | ||||||
|  | message AddUserToNetworkReq | ||||||
|  | { | ||||||
|  |   string uid = 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message DoFollowerRelationReq | ||||||
|  | { | ||||||
|  |   string follower_uid = 1; | ||||||
|  |   string followee_uid = 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FollowReq | ||||||
|  | { | ||||||
|  |   string uid = 1; | ||||||
|  |   int64 page_size = 2; | ||||||
|  |   int64 page_index = 3; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FollowResp | ||||||
|  | { | ||||||
|  |   repeated string uid = 1; | ||||||
|  |   Pager page = 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FollowCountReq | ||||||
|  | { | ||||||
|  |   string uid = 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | message FollowCountResp | ||||||
|  | { | ||||||
|  |   string uid = 1; | ||||||
|  |   int64 total = 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | service SocialNetworkService | ||||||
|  | { | ||||||
|  |   // MarkFollowRelation 關注 | ||||||
|  |   rpc MarkFollowRelation(DoFollowerRelationReq) returns (OKResp); | ||||||
|  |   // RemoveFollowRelation 取消關注 | ||||||
|  |   rpc RemoveFollowRelation(DoFollowerRelationReq) returns (OKResp); | ||||||
|  |   // GetFollower 取得跟隨者名單 | ||||||
|  |   rpc GetFollower(FollowReq) returns (FollowResp); | ||||||
|  |   // GetFollowee 取得我跟隨的名單 | ||||||
|  |   rpc GetFollowee(FollowReq) returns (FollowResp); | ||||||
|  |   // GetFollowerCount 取得跟隨者數量 | ||||||
|  |   rpc GetFollowerCount(FollowCountReq) returns (FollowCountResp); | ||||||
|  |   // GetFolloweeCount 取得我跟隨的數量 | ||||||
|  |   rpc GetFolloweeCount(FollowCountReq) returns (FollowCountResp); | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										37
									
								
								go.mod
								
								
								
								
							|  | @ -5,7 +5,10 @@ go 1.22.3 | ||||||
| require ( | require ( | ||||||
| 	code.30cm.net/digimon/library-go/errs v1.2.4 | 	code.30cm.net/digimon/library-go/errs v1.2.4 | ||||||
| 	code.30cm.net/digimon/library-go/validator v1.0.0 | 	code.30cm.net/digimon/library-go/validator v1.0.0 | ||||||
|  | 	github.com/alicebob/miniredis/v2 v2.33.0 | ||||||
|  | 	github.com/neo4j/neo4j-go-driver/v5 v5.24.0 | ||||||
| 	github.com/stretchr/testify v1.9.0 | 	github.com/stretchr/testify v1.9.0 | ||||||
|  | 	github.com/testcontainers/testcontainers-go v0.33.0 | ||||||
| 	github.com/zeromicro/go-zero v1.7.0 | 	github.com/zeromicro/go-zero v1.7.0 | ||||||
| 	go.mongodb.org/mongo-driver v1.16.0 | 	go.mongodb.org/mongo-driver v1.16.0 | ||||||
| 	go.uber.org/mock v0.4.0 | 	go.uber.org/mock v0.4.0 | ||||||
|  | @ -14,18 +17,32 @@ require ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
|  | 	dario.cat/mergo v1.0.0 // indirect | ||||||
|  | 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect | ||||||
|  | 	github.com/Microsoft/go-winio v0.6.2 // indirect | ||||||
|  | 	github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect | ||||||
| 	github.com/beorn7/perks v1.0.1 // indirect | 	github.com/beorn7/perks v1.0.1 // indirect | ||||||
| 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect | 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||||||
|  | 	github.com/containerd/containerd v1.7.18 // indirect | ||||||
|  | 	github.com/containerd/log v0.1.0 // indirect | ||||||
|  | 	github.com/containerd/platforms v0.2.1 // indirect | ||||||
| 	github.com/coreos/go-semver v0.3.1 // indirect | 	github.com/coreos/go-semver v0.3.1 // indirect | ||||||
| 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect | 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect | ||||||
|  | 	github.com/cpuguy83/dockercfg v0.3.1 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||||
|  | 	github.com/distribution/reference v0.6.0 // indirect | ||||||
|  | 	github.com/docker/docker v27.1.1+incompatible // indirect | ||||||
|  | 	github.com/docker/go-connections v0.5.0 // indirect | ||||||
|  | 	github.com/docker/go-units v0.5.0 // indirect | ||||||
| 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect | 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect | ||||||
| 	github.com/fatih/color v1.17.0 // indirect | 	github.com/fatih/color v1.17.0 // indirect | ||||||
|  | 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect | 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect | ||||||
| 	github.com/go-logr/logr v1.4.2 // indirect | 	github.com/go-logr/logr v1.4.2 // indirect | ||||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
|  | 	github.com/go-ole/go-ole v1.2.6 // indirect | ||||||
| 	github.com/go-openapi/jsonpointer v0.19.6 // indirect | 	github.com/go-openapi/jsonpointer v0.19.6 // indirect | ||||||
| 	github.com/go-openapi/jsonreference v0.20.2 // indirect | 	github.com/go-openapi/jsonreference v0.20.2 // indirect | ||||||
| 	github.com/go-openapi/swag v0.22.4 // indirect | 	github.com/go-openapi/swag v0.22.4 // indirect | ||||||
|  | @ -45,29 +62,49 @@ require ( | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
| 	github.com/klauspost/compress v1.17.8 // indirect | 	github.com/klauspost/compress v1.17.8 // indirect | ||||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | 	github.com/leodido/go-urn v1.4.0 // indirect | ||||||
|  | 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect | ||||||
|  | 	github.com/magiconair/properties v1.8.7 // indirect | ||||||
| 	github.com/mailru/easyjson v0.7.7 // indirect | 	github.com/mailru/easyjson v0.7.7 // indirect | ||||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
|  | 	github.com/moby/docker-image-spec v1.3.1 // indirect | ||||||
|  | 	github.com/moby/patternmatcher v0.6.0 // indirect | ||||||
|  | 	github.com/moby/sys/sequential v0.5.0 // indirect | ||||||
|  | 	github.com/moby/sys/user v0.1.0 // indirect | ||||||
|  | 	github.com/moby/term v0.5.0 // indirect | ||||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
| 	github.com/montanaflynn/stats v0.7.1 // indirect | 	github.com/montanaflynn/stats v0.7.1 // indirect | ||||||
|  | 	github.com/morikuni/aec v1.0.0 // indirect | ||||||
| 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||||||
|  | 	github.com/opencontainers/go-digest v1.0.0 // indirect | ||||||
|  | 	github.com/opencontainers/image-spec v1.1.0 // indirect | ||||||
| 	github.com/openzipkin/zipkin-go v0.4.3 // indirect | 	github.com/openzipkin/zipkin-go v0.4.3 // indirect | ||||||
| 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect | 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect | ||||||
|  | 	github.com/pkg/errors v0.9.1 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
|  | 	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect | ||||||
| 	github.com/prometheus/client_golang v1.19.1 // indirect | 	github.com/prometheus/client_golang v1.19.1 // indirect | ||||||
| 	github.com/prometheus/client_model v0.5.0 // indirect | 	github.com/prometheus/client_model v0.5.0 // indirect | ||||||
| 	github.com/prometheus/common v0.48.0 // indirect | 	github.com/prometheus/common v0.48.0 // indirect | ||||||
| 	github.com/prometheus/procfs v0.12.0 // indirect | 	github.com/prometheus/procfs v0.12.0 // indirect | ||||||
| 	github.com/redis/go-redis/v9 v9.6.1 // indirect | 	github.com/redis/go-redis/v9 v9.6.1 // indirect | ||||||
|  | 	github.com/shirou/gopsutil/v3 v3.23.12 // indirect | ||||||
|  | 	github.com/shoenig/go-m1cpu v0.1.6 // indirect | ||||||
|  | 	github.com/sirupsen/logrus v1.9.3 // indirect | ||||||
| 	github.com/spaolacci/murmur3 v1.1.0 // indirect | 	github.com/spaolacci/murmur3 v1.1.0 // indirect | ||||||
|  | 	github.com/tklauser/go-sysconf v0.3.12 // indirect | ||||||
|  | 	github.com/tklauser/numcpus v0.6.1 // indirect | ||||||
| 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect | 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect | ||||||
| 	github.com/xdg-go/scram v1.1.2 // indirect | 	github.com/xdg-go/scram v1.1.2 // indirect | ||||||
| 	github.com/xdg-go/stringprep v1.0.4 // indirect | 	github.com/xdg-go/stringprep v1.0.4 // indirect | ||||||
| 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | ||||||
|  | 	github.com/yuin/gopher-lua v1.1.1 // indirect | ||||||
|  | 	github.com/yusufpapurcu/wmi v1.2.3 // indirect | ||||||
| 	go.etcd.io/etcd/api/v3 v3.5.15 // 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/pkg/v3 v3.5.15 // indirect | ||||||
| 	go.etcd.io/etcd/client/v3 v3.5.15 // indirect | 	go.etcd.io/etcd/client/v3 v3.5.15 // indirect | ||||||
|  | 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect | ||||||
| 	go.opentelemetry.io/otel v1.24.0 // indirect | 	go.opentelemetry.io/otel v1.24.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/exporters/jaeger v1.17.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 v1.24.0 // indirect | ||||||
|  |  | ||||||
|  | @ -1,9 +1,15 @@ | ||||||
| package config | package config | ||||||
| 
 | 
 | ||||||
| import "github.com/zeromicro/go-zero/zrpc" | import ( | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/zeromicro/go-zero/core/stores/redis" | ||||||
|  | 	"github.com/zeromicro/go-zero/zrpc" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| type Config struct { | type Config struct { | ||||||
| 	zrpc.RpcServerConf | 	zrpc.RpcServerConf | ||||||
|  | 
 | ||||||
| 	Mongo struct { | 	Mongo struct { | ||||||
| 		Schema   string | 		Schema   string | ||||||
| 		User     string | 		User     string | ||||||
|  | @ -12,4 +18,23 @@ type Config struct { | ||||||
| 		Port     string | 		Port     string | ||||||
| 		Database string | 		Database string | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	TimelineSetting struct { | ||||||
|  | 		Expire    int64 // Second
 | ||||||
|  | 		MaxLength int64 // 暫存筆數
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Redis Cluster
 | ||||||
|  | 	RedisCluster redis.RedisConf | ||||||
|  | 
 | ||||||
|  | 	// 圖形資料庫
 | ||||||
|  | 	Neo4J struct { | ||||||
|  | 		URI                   string | ||||||
|  | 		Username              string | ||||||
|  | 		Password              string | ||||||
|  | 		MaxConnectionPoolSize int | ||||||
|  | 		MaxConnectionLifetime time.Duration | ||||||
|  | 		ConnectionTimeout     time.Duration | ||||||
|  | 		LogLevel              string | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,3 +11,7 @@ const ( | ||||||
| 	AdTypeOnlyAd | 	AdTypeOnlyAd | ||||||
| 	AdTypeOnlyNotAd | 	AdTypeOnlyNotAd | ||||||
| ) | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	LastOfTimelineFlag = "NoMoreData" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package domain | package domain | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	ers "code.30cm.net/digimon/library-go/errs" | 	ers "code.30cm.net/digimon/library-go/errs" | ||||||
|  | @ -33,10 +32,25 @@ const ( | ||||||
| 	CommentListErrorCode | 	CommentListErrorCode | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	AddTimeLineErrorCode ErrorCode = iota + 20 | ||||||
|  | 	FetchTimeLineErrorCode | ||||||
|  | 	ClearNoMoreDataErrorCode | ||||||
|  | 	HasNoMoreDataErrorCode | ||||||
|  | 	SetNoMoreDataErrorCode | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	MarkRelationErrorCode ErrorCode = iota + 30 | ||||||
|  | 	GetFollowerErrorCode | ||||||
|  | 	GetFollowerCountErrorCode | ||||||
|  | 	GetFolloweeErrorCode | ||||||
|  | 	GetFolloweeCountErrorCode | ||||||
|  | 	RemoveRelationErrorCode | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func CommentError(ec ErrorCode, s ...string) *ers.LibError { | func CommentError(ec ErrorCode, s ...string) *ers.LibError { | ||||||
| 	return ers.NewError(code.CloudEPTweeting, code.DBError, | 	return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), strings.Join(s, " ")) | ||||||
| 		ec.ToUint32(), |  | ||||||
| 		fmt.Sprintf("%s", strings.Join(s, " "))) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func CommentErrorL(ec ErrorCode, | func CommentErrorL(ec ErrorCode, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | package domain | ||||||
|  | 
 | ||||||
|  | import "strings" | ||||||
|  | 
 | ||||||
|  | type RedisKey string | ||||||
|  | 
 | ||||||
|  | func (key RedisKey) ToString() string { | ||||||
|  | 	return string(key) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (key RedisKey) With(s ...string) RedisKey { | ||||||
|  | 	parts := append([]string{string(key)}, s...) | ||||||
|  | 
 | ||||||
|  | 	return RedisKey(strings.Join(parts, ":")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	TimelineRedisKey RedisKey = "timeline" | ||||||
|  | ) | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | package domain | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TestRedisKeyToString 測試 ToString 方法
 | ||||||
|  | func TestRedisKeyToString(t *testing.T) { | ||||||
|  | 	key := RedisKey("user:timeline") | ||||||
|  | 	expected := "user:timeline" | ||||||
|  | 	result := key.ToString() | ||||||
|  | 
 | ||||||
|  | 	assert.Equal(t, expected, result, "ToString should return the correct string representation of RedisKey") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestRedisKeyWith 測試 With 方法
 | ||||||
|  | func TestRedisKeyWith(t *testing.T) { | ||||||
|  | 	key := RedisKey("user:timeline") | ||||||
|  | 	subKey := "12345" | ||||||
|  | 	expected := "user:timeline:12345" | ||||||
|  | 	result := key.With(subKey) | ||||||
|  | 
 | ||||||
|  | 	assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with the provided subKey") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestRedisKeyWithMultiple 測試 With 方法與多個參數
 | ||||||
|  | func TestRedisKeyWithMultiple(t *testing.T) { | ||||||
|  | 	key := RedisKey("user:timeline") | ||||||
|  | 	subKeys := []string{"12345", "posts"} | ||||||
|  | 	expected := "user:timeline:12345:posts" | ||||||
|  | 	result := key.With(subKeys...) | ||||||
|  | 
 | ||||||
|  | 	assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with multiple provided subKeys") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestRedisKeyWithEmpty 測試 With 方法與空參數
 | ||||||
|  | func TestRedisKeyWithEmpty(t *testing.T) { | ||||||
|  | 	key := RedisKey("user:timeline") | ||||||
|  | 	expected := "user:timeline" | ||||||
|  | 	result := key.With() | ||||||
|  | 
 | ||||||
|  | 	assert.Equal(t, RedisKey(expected), result, "With should return the original key when no subKeys are provided") | ||||||
|  | } | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | package repository | ||||||
|  | 
 | ||||||
|  | import "context" | ||||||
|  | 
 | ||||||
|  | type SocialNetworkRepository interface { | ||||||
|  | 	CreateUserNode(ctx context.Context, uid string) error | ||||||
|  | 	MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error | ||||||
|  | 	RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error | ||||||
|  | 	GetFollower(ctx context.Context, req FollowReq) (FollowResp, error) | ||||||
|  | 	GetFollowee(ctx context.Context, req FollowReq) (FollowResp, error) | ||||||
|  | 	GetFollowerCount(ctx context.Context, uid string) (int64, error) | ||||||
|  | 	GetFolloweeCount(ctx context.Context, uid string) (int64, error) | ||||||
|  | 	GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) | ||||||
|  | 	GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type FollowReq struct { | ||||||
|  | 	UID       string | ||||||
|  | 	PageSize  int64 | ||||||
|  | 	PageIndex int64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type FollowResp struct { | ||||||
|  | 	UIDs  []string | ||||||
|  | 	Total int64 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | package repository | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 	---------------------------------------- | ||||||
|  | 	|       |        |        |         | | ||||||
|  | 	| data A| data B |NO Data |  ....   | | ||||||
|  | 	|       |        |        |         | | ||||||
|  | 	---------------------------------------- | ||||||
|  | 
 | ||||||
|  | 	動態時報在發現這個 Queue 有 No Data 的 Flag  時 | ||||||
|  | 	就不再去 Query 資料庫,防止被狂刷。 | ||||||
|  | 	只是要注意在業務上何時要 加入/刪除 這個 Flag | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | // TimelineRepository 定義時間線的存儲接口,可以根據不同的排序策略實現。
 | ||||||
|  | type TimelineRepository interface { | ||||||
|  | 	// AddPost 將貼文添加到動態時報,並根據排序策略進行排序。
 | ||||||
|  | 	AddPost(ctx context.Context, req AddPostRequest) error | ||||||
|  | 	// FetchTimeline 獲取指定用戶的動態時報。
 | ||||||
|  | 	FetchTimeline(ctx context.Context, req FetchTimelineRequest) (FetchTimelineResponse, error) | ||||||
|  | 	// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。
 | ||||||
|  | 	SetNoMoreDataFlag(ctx context.Context, uid string) error | ||||||
|  | 	// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。
 | ||||||
|  | 	HasNoMoreData(ctx context.Context, uid string) (bool, error) | ||||||
|  | 	// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。
 | ||||||
|  | 	ClearNoMoreDataFlag(ctx context.Context, uid string) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPostRequest 用於將貼文添加到時間線的請求結構體。
 | ||||||
|  | type AddPostRequest struct { | ||||||
|  | 	UID       string | ||||||
|  | 	PostItems []TimelineItem | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TimelineItem 表示時間線中的一個元素,排序依據取決於 Score。
 | ||||||
|  | type TimelineItem struct { | ||||||
|  | 	PostID string // 貼文ID
 | ||||||
|  | 	Score  int64  // 排序使用的分數,根據具體實現可能代表時間、優先級等
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimelineRequest 用於獲取時間線的請求結構體。
 | ||||||
|  | type FetchTimelineRequest struct { | ||||||
|  | 	UID       string | ||||||
|  | 	PageSize  int64 | ||||||
|  | 	PageIndex int64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimelineResponse 表示獲取時間線的回應結構體。
 | ||||||
|  | type FetchTimelineResponse struct { | ||||||
|  | 	Items []TimelineItem | ||||||
|  | 	Page  tweeting.Pager | ||||||
|  | } | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | package neo4j | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | // Config holds the configuration for Neo4j connection.
 | ||||||
|  | type Config struct { | ||||||
|  | 	URI                   string | ||||||
|  | 	Username              string | ||||||
|  | 	Password              string | ||||||
|  | 	MaxConnectionPoolSize int | ||||||
|  | 	MaxConnectionLifetime time.Duration | ||||||
|  | 	ConnectionTimeout     time.Duration | ||||||
|  | 	LogLevel              string | ||||||
|  | } | ||||||
|  | @ -0,0 +1,53 @@ | ||||||
|  | package neo4j | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/neo4j/neo4j-go-driver/v5/neo4j" | ||||||
|  | 	n4Cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewNeo4J initializes a Neo4jInit using the provided Config and options.
 | ||||||
|  | // If opts is not provided, it will initialize Neo4jInit with default configuration.
 | ||||||
|  | func NewNeo4J(conf *Config, opts ...Option) *Client { | ||||||
|  | 	driverConfig := &n4Cfg.Config{ | ||||||
|  | 		MaxConnectionLifetime:        conf.MaxConnectionLifetime, | ||||||
|  | 		MaxConnectionPoolSize:        conf.MaxConnectionPoolSize, | ||||||
|  | 		ConnectionAcquisitionTimeout: conf.ConnectionTimeout, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	neo4ji := &Client{ | ||||||
|  | 		neo4jConf: driverConfig, | ||||||
|  | 		serviceConf: Config{ | ||||||
|  | 			URI:      conf.URI, | ||||||
|  | 			Username: conf.Username, | ||||||
|  | 			Password: conf.Password, | ||||||
|  | 			LogLevel: conf.LogLevel, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, opt := range opts { | ||||||
|  | 		opt(neo4ji) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return neo4ji | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Conn initiates connection to the database and returns a Neo4j driver instance.
 | ||||||
|  | func (c *Client) Conn() (neo4j.DriverWithContext, error) { | ||||||
|  | 	auth := neo4j.BasicAuth(c.serviceConf.Username, c.serviceConf.Password, "") | ||||||
|  | 	driver, err := neo4j.NewDriverWithContext(c.serviceConf.URI, auth, func(_ *n4Cfg.Config) {}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("neo4j driver initialization error: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	// Verify the connection to Neo4j.
 | ||||||
|  | 	err = driver.VerifyConnectivity(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("neo4j connectivity verification error: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return driver, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,209 @@ | ||||||
|  | package neo4j | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/testcontainers/testcontainers-go" | ||||||
|  | 	"github.com/testcontainers/testcontainers-go/wait" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestNewNeo4J(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name     string | ||||||
|  | 		conf     *Config | ||||||
|  | 		expected *Config | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "valid configuration", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   "neo4j://localhost:7687", | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "password", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			expected: &Config{ | ||||||
|  | 				URI:      "neo4j://localhost:7687", | ||||||
|  | 				Username: "neo4j", | ||||||
|  | 				Password: "password", | ||||||
|  | 				LogLevel: "info", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "empty URI", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   "", | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "password", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			expected: &Config{ | ||||||
|  | 				URI:      "", | ||||||
|  | 				Username: "neo4j", | ||||||
|  | 				Password: "password", | ||||||
|  | 				LogLevel: "info", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "empty username and password", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   "neo4j://localhost:7687", | ||||||
|  | 				Username:              "", | ||||||
|  | 				Password:              "", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			expected: &Config{ | ||||||
|  | 				URI:      "neo4j://localhost:7687", | ||||||
|  | 				Username: "", | ||||||
|  | 				Password: "", | ||||||
|  | 				LogLevel: "info", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "custom log level", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   "neo4j://localhost:7687", | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "password", | ||||||
|  | 				LogLevel:              "debug", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			expected: &Config{ | ||||||
|  | 				URI:      "neo4j://localhost:7687", | ||||||
|  | 				Username: "neo4j", | ||||||
|  | 				Password: "password", | ||||||
|  | 				LogLevel: "debug", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			client := NewNeo4J(tt.conf) | ||||||
|  | 			assert.NotNil(t, client) | ||||||
|  | 			assert.Equal(t, tt.expected.URI, client.serviceConf.URI) | ||||||
|  | 			assert.Equal(t, tt.expected.Username, client.serviceConf.Username) | ||||||
|  | 			assert.Equal(t, tt.expected.Password, client.serviceConf.Password) | ||||||
|  | 			assert.Equal(t, tt.expected.LogLevel, client.serviceConf.LogLevel) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestConn(t *testing.T) { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	neo4jContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ | ||||||
|  | 		ContainerRequest: testcontainers.ContainerRequest{ | ||||||
|  | 			Image:        "neo4j:latest", | ||||||
|  | 			ExposedPorts: []string{"7687/tcp"}, | ||||||
|  | 			Env: map[string]string{ | ||||||
|  | 				"NEO4J_AUTH": "neo4j/yyyytttt", | ||||||
|  | 			}, | ||||||
|  | 			WaitingFor: wait.ForLog("Started"), | ||||||
|  | 		}, | ||||||
|  | 		Started: true, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer neo4jContainer.Terminate(ctx) | ||||||
|  | 
 | ||||||
|  | 	host, _ := neo4jContainer.Host(ctx) | ||||||
|  | 	port, _ := neo4jContainer.MappedPort(ctx, "7687") | ||||||
|  | 
 | ||||||
|  | 	uri := fmt.Sprintf("bolt://%s:%s", host, port.Port()) | ||||||
|  | 	t.Log("Neo4j running at:", uri) | ||||||
|  | 
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name       string | ||||||
|  | 		conf       *Config | ||||||
|  | 		shouldFail bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "successful connection", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   uri, | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "yyyytttt", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			shouldFail: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed connection due to invalid URI", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   uri, | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "wrongpassword", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			shouldFail: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed connection due to missing URI", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   "", | ||||||
|  | 				Username:              "neo4j", | ||||||
|  | 				Password:              "password", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			shouldFail: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "failed connection due to missing username and password", | ||||||
|  | 			conf: &Config{ | ||||||
|  | 				URI:                   uri, | ||||||
|  | 				Username:              "", | ||||||
|  | 				Password:              "", | ||||||
|  | 				LogLevel:              "info", | ||||||
|  | 				MaxConnectionLifetime: time.Minute * 5, | ||||||
|  | 				MaxConnectionPoolSize: 10, | ||||||
|  | 				ConnectionTimeout:     time.Second * 5, | ||||||
|  | 			}, | ||||||
|  | 			shouldFail: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			client := NewNeo4J(tt.conf) | ||||||
|  | 			driver, err := client.Conn() | ||||||
|  | 			if tt.shouldFail { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, driver) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.NotNil(t, driver) | ||||||
|  | 
 | ||||||
|  | 				// Close the driver after test
 | ||||||
|  | 				defer func() { | ||||||
|  | 					err := driver.Close(context.Background()) | ||||||
|  | 					assert.NoError(t, err) | ||||||
|  | 				}() | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | package neo4j | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	n4Cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" | ||||||
|  | 	"github.com/neo4j/neo4j-go-driver/v5/neo4j/log" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	defaultMaxConnectionLifetime = 5 * time.Minute | ||||||
|  | 	defaultMaxConnectionPoolSize = 25 | ||||||
|  | 	defaultConnectionTimeout     = 5 * time.Second | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Option configures Neo4jInit behaviour.
 | ||||||
|  | type Option func(*Client) | ||||||
|  | 
 | ||||||
|  | type Client struct { | ||||||
|  | 	neo4jConf   *n4Cfg.Config | ||||||
|  | 	serviceConf Config | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WithLogLevel sets the log level for the Neo4j driver.
 | ||||||
|  | func WithLogLevel(level string) Option { | ||||||
|  | 	return func(neo4ji *Client) { | ||||||
|  | 		var logger log.Logger | ||||||
|  | 
 | ||||||
|  | 		switch strings.ToLower(level) { | ||||||
|  | 		case "panic", "fatal", "error": | ||||||
|  | 			logger = log.ToConsole(log.ERROR) | ||||||
|  | 		case "warn", "warning": | ||||||
|  | 			logger = log.ToConsole(log.WARNING) | ||||||
|  | 		case "info", "debug", "trace": | ||||||
|  | 			logger = log.ToConsole(log.INFO) | ||||||
|  | 		default: | ||||||
|  | 			logger = log.ToConsole(log.ERROR) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		neo4ji.neo4jConf.Log = logger | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // WithPerformance configures the Neo4j driver for performance by setting connection pool size and lifetime.
 | ||||||
|  | func WithPerformance() Option { | ||||||
|  | 	return func(neo4ji *Client) { | ||||||
|  | 		if neo4ji.serviceConf.MaxConnectionPoolSize > 0 { | ||||||
|  | 			neo4ji.neo4jConf.MaxConnectionPoolSize = neo4ji.serviceConf.MaxConnectionPoolSize | ||||||
|  | 		} else { | ||||||
|  | 			neo4ji.neo4jConf.MaxConnectionPoolSize = defaultMaxConnectionPoolSize | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if neo4ji.serviceConf.MaxConnectionLifetime > 0 { | ||||||
|  | 			neo4ji.neo4jConf.MaxConnectionLifetime = neo4ji.serviceConf.MaxConnectionLifetime | ||||||
|  | 		} else { | ||||||
|  | 			neo4ji.neo4jConf.MaxConnectionLifetime = defaultMaxConnectionLifetime | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -37,6 +37,7 @@ func (l *DeleteCommentLogic) DeleteComment(in *tweeting.DeleteCommentReq) (*twee | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to del comment").Wrap(err) | 			"failed to del comment").Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,8 +41,8 @@ func convertToCommentDetailItem(item *model.Comment) *tweeting.CommentDetail { | ||||||
| 		Uid:          item.UID, | 		Uid:          item.UID, | ||||||
| 		Content:      item.Content, | 		Content:      item.Content, | ||||||
| 		CreatedAt:    item.CreateAt, | 		CreatedAt:    item.CreateAt, | ||||||
| 		LikeCount:    int64(item.LikeCount), | 		LikeCount:    item.LikeCount, | ||||||
| 		DislikeCount: int64(item.DisLikeCount), | 		DislikeCount: item.DisLikeCount, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -82,6 +82,7 @@ func (l *GetCommentsLogic) GetComments(in *tweeting.GetCommentsReq) (*tweeting.G | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to find comment").Wrap(err) | 			"failed to find comment").Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,16 +27,16 @@ func NewUpdateCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upd | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type checkCommentId struct { | type checkCommentID struct { | ||||||
| 	CommentId string `validate:"required"` | 	CommentID string `validate:"required"` | ||||||
| 	Content   string `json:"content,omitempty" validate:"lte=500"` | 	Content   string `json:"content,omitempty" validate:"lte=500"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UpdateComment 更新評論
 | // UpdateComment 更新評論
 | ||||||
| func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*tweeting.OKResp, error) { | func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*tweeting.OKResp, error) { | ||||||
| 	// 驗證資料
 | 	// 驗證資料
 | ||||||
| 	if err := l.svcCtx.Validate.ValidateAll(&checkCommentId{ | 	if err := l.svcCtx.Validate.ValidateAll(&checkCommentID{ | ||||||
| 		CommentId: in.GetCommentId(), | 		CommentID: in.GetCommentId(), | ||||||
| 		Content:   in.GetContent(), | 		Content:   in.GetContent(), | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		// 錯誤代碼 05-011-00
 | 		// 錯誤代碼 05-011-00
 | ||||||
|  | @ -75,6 +75,7 @@ func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*twee | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to update comment:", in.CommentId).Wrap(err) | 			"failed to update comment:", in.CommentId).Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ type newTweetingReq struct { | ||||||
| 	UID      string   `json:"uid" validate:"required"` | 	UID      string   `json:"uid" validate:"required"` | ||||||
| 	Content  string   `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內
 | 	Content  string   `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內
 | ||||||
| 	Tags     []string `json:"tags"` | 	Tags     []string `json:"tags"` | ||||||
| 	MediaUrl []string `json:"media_url"` | 	MediaURL []string `json:"media_url"` | ||||||
| 	IsAd     bool     `json:"is_ad"` // default false
 | 	IsAd     bool     `json:"is_ad"` // default false
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -85,6 +85,7 @@ func (l *CreatePostLogic) CreatePost(in *tweeting.NewPostReq) (*tweeting.PostRes | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to add new post").Wrap(err) | 			"failed to add new post").Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,6 +37,7 @@ func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKR | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to del post").Wrap(err) | 			"failed to del post").Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,8 +54,8 @@ func convertToPostDetailItem(item *model.Post) *tweeting.PostDetailItem { | ||||||
| 		IsAd:         item.IsAd, | 		IsAd:         item.IsAd, | ||||||
| 		CreatedAt:    item.CreateAt, | 		CreatedAt:    item.CreateAt, | ||||||
| 		UpdateAt:     item.UpdateAt, | 		UpdateAt:     item.UpdateAt, | ||||||
| 		LikeCount:    int64(item.Like), | 		LikeCount:    item.Like, | ||||||
| 		DislikeCount: int64(item.DisLike), | 		DislikeCount: item.DisLike, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -102,6 +102,7 @@ func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPo | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to find posts").Wrap(err) | 			"failed to find posts").Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ func NewUpdatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type checkPostId struct { | type checkPostID struct { | ||||||
| 	PostID  string `validate:"required"` | 	PostID  string `validate:"required"` | ||||||
| 	Content string `json:"content,omitempty" validate:"lte=500"` | 	Content string `json:"content,omitempty" validate:"lte=500"` | ||||||
| } | } | ||||||
|  | @ -36,7 +36,7 @@ type checkPostId struct { | ||||||
| // UpdatePost 更新貼文
 | // UpdatePost 更新貼文
 | ||||||
| func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { | func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { | ||||||
| 	// 驗證資料
 | 	// 驗證資料
 | ||||||
| 	if err := l.svcCtx.Validate.ValidateAll(&checkPostId{ | 	if err := l.svcCtx.Validate.ValidateAll(&checkPostID{ | ||||||
| 		PostID:  in.GetPostId(), | 		PostID:  in.GetPostId(), | ||||||
| 		Content: in.GetContent(), | 		Content: in.GetContent(), | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
|  | @ -53,7 +53,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe | ||||||
| 	update.ID = oid | 	update.ID = oid | ||||||
| 	update.Tags = in.GetTags() | 	update.Tags = in.GetTags() | ||||||
| 	// 將 Media 存入
 | 	// 將 Media 存入
 | ||||||
| 	var media []model.Media | 	media := make([]model.Media, 0, len(in.GetMedia())) | ||||||
| 	for _, item := range in.GetMedia() { | 	for _, item := range in.GetMedia() { | ||||||
| 		media = append(media, model.Media{ | 		media = append(media, model.Media{ | ||||||
| 			Links: item.Url, | 			Links: item.Url, | ||||||
|  | @ -88,6 +88,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe | ||||||
| 				{Key: "err", Value: err}, | 				{Key: "err", Value: err}, | ||||||
| 			}, | 			}, | ||||||
| 			"failed to update post", in.PostId).Wrap(err) | 			"failed to update post", in.PostId).Wrap(err) | ||||||
|  | 
 | ||||||
| 		return nil, e | 		return nil, e | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 GetFolloweeCountLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewGetFolloweeCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFolloweeCountLogic { | ||||||
|  | 	return &GetFolloweeCountLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFolloweeCount 取得我跟隨的數量
 | ||||||
|  | func (l *GetFolloweeCountLogic) GetFolloweeCount(in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&getFollowCountReq{ | ||||||
|  | 		UID: in.Uid, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	followeeCount, err := l.svcCtx.SocialNetworkRepository.GetFolloweeCount(l.ctx, in.GetUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-34
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.GetFolloweeCountErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.GetFolloweeCount"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to count follower").Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.FollowCountResp{ | ||||||
|  | 		Uid:   in.GetUid(), | ||||||
|  | 		Total: followeeCount, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,88 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestGetFolloweeCountLogic_GetFolloweeCount(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 测试数据集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.FollowCountReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "ok", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "12345", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFolloweeCount(gomock.Any(), "12345").Return(int64(10), nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "取得跟隨數量失敗", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "12345", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFolloweeCount(gomock.Any(), "12345").Return(int64(0), errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			logic := GetFolloweeCountLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			got, err := logic.GetFolloweeCount(tt.input) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.input.Uid, got.Uid) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,70 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"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 GetFolloweeLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewGetFolloweeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFolloweeLogic { | ||||||
|  | 	return &GetFolloweeLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowee 取得我跟隨的名單
 | ||||||
|  | func (l *GetFolloweeLogic) GetFollowee(in *tweeting.FollowReq) (*tweeting.FollowResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&getFollowReq{ | ||||||
|  | 		UID:       in.Uid, | ||||||
|  | 		PageSize:  in.PageSize, | ||||||
|  | 		PageIndex: in.PageIndex, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	followee, err := l.svcCtx.SocialNetworkRepository.GetFollowee(l.ctx, repository.FollowReq{ | ||||||
|  | 		UID:       in.GetUid(), | ||||||
|  | 		PageIndex: in.GetPageIndex(), | ||||||
|  | 		PageSize:  in.GetPageSize(), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-33
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.GetFolloweeErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.GetFollowee"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to get relation: ", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.FollowResp{ | ||||||
|  | 		Uid: followee.UIDs, | ||||||
|  | 		Page: &tweeting.Pager{ | ||||||
|  | 			Total: followee.Total, | ||||||
|  | 			Index: in.GetPageIndex(), | ||||||
|  | 			Size:  in.GetPageSize(), | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,130 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestGetFolloweeLogic_GetFollowee(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.FollowReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.FollowResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功獲取我跟隨的名單", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "12345", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollowee 返回正確的結果
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollowee(gomock.Any(), repository.FollowReq{ | ||||||
|  | 					UID:       "12345", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FollowResp{ | ||||||
|  | 					UIDs:  []string{"user1", "user2"}, | ||||||
|  | 					Total: 2, | ||||||
|  | 				}, nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp: &tweeting.FollowResp{ | ||||||
|  | 				Uid: []string{"user1", "user2"}, | ||||||
|  | 				Page: &tweeting.Pager{ | ||||||
|  | 					Total: 2, | ||||||
|  | 					Index: 1, | ||||||
|  | 					Size:  10, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "獲取我跟隨的名單失敗", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "12345", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollowee 返回錯誤
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollowee(gomock.Any(), repository.FollowReq{ | ||||||
|  | 					UID:       "12345", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FollowResp{}, errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 GetFolloweeLogic
 | ||||||
|  | 			logic := GetFolloweeLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 GetFollowee
 | ||||||
|  | 			got, err := logic.GetFollowee(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 GetFollowerCountLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewGetFollowerCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFollowerCountLogic { | ||||||
|  | 	return &GetFollowerCountLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type getFollowCountReq struct { | ||||||
|  | 	UID string `validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowerCount 取得跟隨者數量
 | ||||||
|  | func (l *GetFollowerCountLogic) GetFollowerCount(in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&getFollowCountReq{ | ||||||
|  | 		UID: in.Uid, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	followerCount, err := l.svcCtx.SocialNetworkRepository.GetFollowerCount(l.ctx, in.GetUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-32
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.GetFollowerCountErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.GetFollowerCount"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to count follower").Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.FollowCountResp{ | ||||||
|  | 		Uid:   in.GetUid(), | ||||||
|  | 		Total: followerCount, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,108 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestGetFollowerCountLogic_GetFollowerCount(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.FollowCountReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.FollowCountResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功獲取跟隨者數量", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "12345", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollowerCount 返回正確的結果
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollowerCount(gomock.Any(), "12345").Return(int64(10), nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp: &tweeting.FollowCountResp{ | ||||||
|  | 				Uid:   "12345", | ||||||
|  | 				Total: 10, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "獲取跟隨者數量失敗", | ||||||
|  | 			input: &tweeting.FollowCountReq{ | ||||||
|  | 				Uid: "12345", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollowerCount 返回錯誤
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollowerCount(gomock.Any(), "12345").Return(int64(0), errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 GetFollowerCountLogic
 | ||||||
|  | 			logic := GetFollowerCountLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 GetFollowerCount
 | ||||||
|  | 			got, err := logic.GetFollowerCount(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"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 GetFollowerLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewGetFollowerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFollowerLogic { | ||||||
|  | 	return &GetFollowerLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type getFollowReq struct { | ||||||
|  | 	UID       string `validate:"required"` | ||||||
|  | 	PageSize  int64  `validate:"required"` | ||||||
|  | 	PageIndex int64  `validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollower 取得跟隨者名單
 | ||||||
|  | func (l *GetFollowerLogic) GetFollower(in *tweeting.FollowReq) (*tweeting.FollowResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&getFollowReq{ | ||||||
|  | 		UID:       in.Uid, | ||||||
|  | 		PageSize:  in.PageSize, | ||||||
|  | 		PageIndex: in.PageIndex, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	follower, err := l.svcCtx.SocialNetworkRepository.GetFollower(l.ctx, repository.FollowReq{ | ||||||
|  | 		UID:       in.GetUid(), | ||||||
|  | 		PageIndex: in.GetPageIndex(), | ||||||
|  | 		PageSize:  in.GetPageSize(), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-31
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.GetFollowerErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.GetFollower"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to get relation: ", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.FollowResp{ | ||||||
|  | 		Uid: follower.UIDs, | ||||||
|  | 		Page: &tweeting.Pager{ | ||||||
|  | 			Total: follower.Total, | ||||||
|  | 			Index: in.GetPageIndex(), | ||||||
|  | 			Size:  in.GetPageSize(), | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,130 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestGetFollowerLogic_GetFollower(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.FollowReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.FollowResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功獲取跟隨者名單", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "12345", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollower 返回正確的結果
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollower(gomock.Any(), repository.FollowReq{ | ||||||
|  | 					UID:       "12345", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FollowResp{ | ||||||
|  | 					UIDs:  []string{"user1", "user2"}, | ||||||
|  | 					Total: 2, | ||||||
|  | 				}, nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp: &tweeting.FollowResp{ | ||||||
|  | 				Uid: []string{"user1", "user2"}, | ||||||
|  | 				Page: &tweeting.Pager{ | ||||||
|  | 					Total: 2, | ||||||
|  | 					Index: 1, | ||||||
|  | 					Size:  10, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "獲取跟隨者名單失敗", | ||||||
|  | 			input: &tweeting.FollowReq{ | ||||||
|  | 				Uid:       "12345", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬 GetFollower 返回錯誤
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().GetFollower(gomock.Any(), repository.FollowReq{ | ||||||
|  | 					UID:       "12345", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FollowResp{}, errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 GetFollowerLogic
 | ||||||
|  | 			logic := GetFollowerLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 GetFollower
 | ||||||
|  | 			got, err := logic.GetFollower(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 MarkFollowRelationLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewMarkFollowRelationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MarkFollowRelationLogic { | ||||||
|  | 	return &MarkFollowRelationLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type doFollowReq struct { | ||||||
|  | 	FollowerUID string `json:"follower_uid" validate:"required"` // 追隨者,跟隨你的人(別人關注你)
 | ||||||
|  | 	FolloweeUID string `json:"followee_uid" validate:"required"` // 追蹤者,你跟隨的人(你關注別)
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarkFollowRelation 關注
 | ||||||
|  | func (l *MarkFollowRelationLogic) MarkFollowRelation(in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&doFollowReq{ | ||||||
|  | 		FollowerUID: in.GetFollowerUid(), | ||||||
|  | 		FolloweeUID: in.GetFolloweeUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 這裡要幫建立關係, follower 追蹤 -> followee
 | ||||||
|  | 	err := l.svcCtx.SocialNetworkRepository.MarkFollowerRelation(l.ctx, in.GetFollowerUid(), in.GetFolloweeUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-30
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.MarkRelationErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.MarkFollowerRelationBetweenUsers"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to mark relation form -> to", in.GetFollowerUid(), in.GetFolloweeUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.OKResp{}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,108 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestMarkFollowRelationLogic_MarkFollowRelation(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.DoFollowerRelationReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.OKResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功建立關注關係", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "follower123", | ||||||
|  | 				FolloweeUid: "followee456", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功建立關注關係
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().MarkFollowerRelation(gomock.Any(), "follower123", "followee456").Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "", | ||||||
|  | 				FolloweeUid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "建立關注關係失敗", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "follower123", | ||||||
|  | 				FolloweeUid: "followee456", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬建立關注關係失敗
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().MarkFollowerRelation(gomock.Any(), "follower123", "followee456").Return(errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 MarkFollowRelationLogic
 | ||||||
|  | 			logic := MarkFollowRelationLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 MarkFollowRelation
 | ||||||
|  | 			got, err := logic.MarkFollowRelation(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 RemoveFollowRelationLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewRemoveFollowRelationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RemoveFollowRelationLogic { | ||||||
|  | 	return &RemoveFollowRelationLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RemoveFollowRelation 取消關注
 | ||||||
|  | func (l *RemoveFollowRelationLogic) RemoveFollowRelation(in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&doFollowReq{ | ||||||
|  | 		FollowerUID: in.GetFollowerUid(), | ||||||
|  | 		FolloweeUID: in.GetFolloweeUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 這裡要幫刪除關係, follower 追蹤 -> followee
 | ||||||
|  | 	err := l.svcCtx.SocialNetworkRepository.RemoveFollowerRelation(l.ctx, in.GetFollowerUid(), in.GetFolloweeUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-35
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.RemoveRelationErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "SocialNetworkRepository.RemoveFollowerRelation"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to remove relation form -> to", in.GetFollowerUid(), in.GetFolloweeUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.OKResp{}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,108 @@ | ||||||
|  | package socialnetworkservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestRemoveFollowRelationLogic_RemoveFollowRelation(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		SocialNetworkRepository: mockSocialNetworkRepository, | ||||||
|  | 		Validate:                mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.DoFollowerRelationReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.OKResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功取消關注", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "follower123", | ||||||
|  | 				FolloweeUid: "followee456", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功刪除關注關係
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().RemoveFollowerRelation(gomock.Any(), "follower123", "followee456").Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "", | ||||||
|  | 				FolloweeUid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "取消關注失敗", | ||||||
|  | 			input: &tweeting.DoFollowerRelationReq{ | ||||||
|  | 				FollowerUid: "follower123", | ||||||
|  | 				FolloweeUid: "followee456", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬刪除關注關係失敗
 | ||||||
|  | 				mockSocialNetworkRepository.EXPECT().RemoveFollowerRelation(gomock.Any(), "follower123", "followee456").Return(errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 RemoveFollowRelationLogic
 | ||||||
|  | 			logic := RemoveFollowRelationLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 RemoveFollowRelation
 | ||||||
|  | 			got, err := logic.RemoveFollowRelation(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,77 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"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 AddPostLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewAddPostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddPostLogic { | ||||||
|  | 	return &AddPostLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type addPostReq struct { | ||||||
|  | 	UID string `json:"uid" validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷
 | ||||||
|  | func (l *AddPostLogic) AddPost(in *tweeting.AddPostToTimelineReq) (*tweeting.OKResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&addPostReq{ | ||||||
|  | 		UID: in.GetUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(in.GetPosts()) == 0 { | ||||||
|  | 		// 沒資料,直接 OK
 | ||||||
|  | 		return &tweeting.OKResp{}, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	post := make([]repository.TimelineItem, 0, len(in.GetPosts())) | ||||||
|  | 	for _, item := range in.GetPosts() { | ||||||
|  | 		post = append(post, repository.TimelineItem{ | ||||||
|  | 			PostID: item.PostId, | ||||||
|  | 			Score:  item.CreatedAt, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := l.svcCtx.TimelineRepo.AddPost(l.ctx, repository.AddPostRequest{ | ||||||
|  | 		UID:       in.GetUid(), | ||||||
|  | 		PostItems: post, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-20
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.AddTimeLineErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "TimelineRepo.AddPost"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to insert timeline repo :", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.OKResp{}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,137 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestAddPostLogic_AddPost(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		TimelineRepo: mockTimelineRepo, | ||||||
|  | 		Validate:     mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.AddPostToTimelineReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.OKResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功加入貼文", | ||||||
|  | 			input: &tweeting.AddPostToTimelineReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 				Posts: []*tweeting.PostTimelineItem{ | ||||||
|  | 					{PostId: "post1", CreatedAt: 1627890123}, | ||||||
|  | 					{PostId: "post2", CreatedAt: 1627890124}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功加入貼文
 | ||||||
|  | 				mockTimelineRepo.EXPECT().AddPost(gomock.Any(), repository.AddPostRequest{ | ||||||
|  | 					UID: "user123", | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 1627890123}, | ||||||
|  | 						{PostID: "post2", Score: 1627890124}, | ||||||
|  | 					}, | ||||||
|  | 				}).Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.AddPostToTimelineReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "沒有貼文資料", | ||||||
|  | 			input: &tweeting.AddPostToTimelineReq{ | ||||||
|  | 				Uid:   "user123", | ||||||
|  | 				Posts: []*tweeting.PostTimelineItem{}, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "加入貼文失敗", | ||||||
|  | 			input: &tweeting.AddPostToTimelineReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 				Posts: []*tweeting.PostTimelineItem{ | ||||||
|  | 					{PostId: "post1", CreatedAt: 1627890123}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬加入貼文失敗
 | ||||||
|  | 				mockTimelineRepo.EXPECT().AddPost(gomock.Any(), repository.AddPostRequest{ | ||||||
|  | 					UID: "user123", | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 1627890123}, | ||||||
|  | 					}, | ||||||
|  | 				}).Return(errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 AddPostLogic
 | ||||||
|  | 			logic := AddPostLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 AddPost
 | ||||||
|  | 			got, err := logic.AddPost(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 ClearNoMoreDataFlagLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewClearNoMoreDataFlagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ClearNoMoreDataFlagLogic { | ||||||
|  | 	return &ClearNoMoreDataFlagLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type clearNoMoreDataFlagReq struct { | ||||||
|  | 	UID string `json:"uid" validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。
 | ||||||
|  | func (l *ClearNoMoreDataFlagLogic) ClearNoMoreDataFlag(in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&clearNoMoreDataFlagReq{ | ||||||
|  | 		UID: in.GetUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := l.svcCtx.TimelineRepo.ClearNoMoreDataFlag(l.ctx, in.GetUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-22
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.ClearNoMoreDataErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "TimelineRepo.ClearNoMoreDataFlag"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to clear no more data flag timeline repo :", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.OKResp{}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,105 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestClearNoMoreDataFlagLogic_ClearNoMoreDataFlag(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		TimelineRepo: mockTimelineRepo, | ||||||
|  | 		Validate:     mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.DoNoMoreDataReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.OKResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功清除 NoMoreData 標誌", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功清除 NoMoreData 標誌
 | ||||||
|  | 				mockTimelineRepo.EXPECT().ClearNoMoreDataFlag(gomock.Any(), "user123").Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "清除 NoMoreData 標誌失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬清除 NoMoreData 標誌失敗
 | ||||||
|  | 				mockTimelineRepo.EXPECT().ClearNoMoreDataFlag(gomock.Any(), "user123").Return(errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 ClearNoMoreDataFlagLogic
 | ||||||
|  | 			logic := ClearNoMoreDataFlagLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 ClearNoMoreDataFlag
 | ||||||
|  | 			got, err := logic.ClearNoMoreDataFlag(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,84 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"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 FetchTimelineLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewFetchTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FetchTimelineLogic { | ||||||
|  | 	return &FetchTimelineLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type fetchTimelineReq struct { | ||||||
|  | 	UID       string `json:"uid" validate:"required"` | ||||||
|  | 	PageSize  int64  `json:"page_size" validate:"required"` | ||||||
|  | 	PageIndex int64  `json:"page_index" validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimeline 取得這個人的動態時報
 | ||||||
|  | func (l *FetchTimelineLogic) FetchTimeline(in *tweeting.GetTimelineReq) (*tweeting.FetchTimelineResponse, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&fetchTimelineReq{ | ||||||
|  | 		UID:       in.GetUid(), | ||||||
|  | 		PageSize:  in.PageSize, | ||||||
|  | 		PageIndex: in.PageIndex, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resp, err := l.svcCtx.TimelineRepo.FetchTimeline(l.ctx, repository.FetchTimelineRequest{ | ||||||
|  | 		UID:       in.GetUid(), | ||||||
|  | 		PageIndex: in.GetPageIndex(), | ||||||
|  | 		PageSize:  in.GetPageSize(), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-21
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.FetchTimeLineErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "TimelineRepo.FetchTimeline"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to fetch timeline repo :", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	result := make([]*tweeting.FetchTimelineItem, 0, resp.Page.Size) | ||||||
|  | 	for _, item := range resp.Items { | ||||||
|  | 		result = append(result, &tweeting.FetchTimelineItem{ | ||||||
|  | 			PostId: item.PostID, | ||||||
|  | 			Score:  item.Score, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.FetchTimelineResponse{ | ||||||
|  | 		Posts: result, | ||||||
|  | 		Page: &tweeting.Pager{ | ||||||
|  | 			Total: resp.Page.Total, | ||||||
|  | 			Index: resp.Page.Index, | ||||||
|  | 			Size:  resp.Page.Size, | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,140 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestFetchTimelineLogic_FetchTimeline(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		TimelineRepo: mockTimelineRepo, | ||||||
|  | 		Validate:     mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.GetTimelineReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.FetchTimelineResponse | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功獲取動態時報", | ||||||
|  | 			input: &tweeting.GetTimelineReq{ | ||||||
|  | 				Uid:       "user123", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功獲取動態時報
 | ||||||
|  | 				mockTimelineRepo.EXPECT().FetchTimeline(gomock.Any(), repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       "user123", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FetchTimelineResponse{ | ||||||
|  | 					Items: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 100}, | ||||||
|  | 						{PostID: "post2", Score: 200}, | ||||||
|  | 					}, | ||||||
|  | 					Page: tweeting.Pager{ | ||||||
|  | 						Total: 2, | ||||||
|  | 						Size:  10, | ||||||
|  | 						Index: 1, | ||||||
|  | 					}, | ||||||
|  | 				}, nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp: &tweeting.FetchTimelineResponse{ | ||||||
|  | 				Posts: []*tweeting.FetchTimelineItem{ | ||||||
|  | 					{PostId: "post1", Score: 100}, | ||||||
|  | 					{PostId: "post2", Score: 200}, | ||||||
|  | 				}, | ||||||
|  | 				Page: &tweeting.Pager{ | ||||||
|  | 					Total: 2, | ||||||
|  | 					Index: 1, | ||||||
|  | 					Size:  10, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.GetTimelineReq{ | ||||||
|  | 				Uid:       "", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "獲取動態時報失敗", | ||||||
|  | 			input: &tweeting.GetTimelineReq{ | ||||||
|  | 				Uid:       "user123", | ||||||
|  | 				PageSize:  10, | ||||||
|  | 				PageIndex: 1, | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬獲取動態時報失敗
 | ||||||
|  | 				mockTimelineRepo.EXPECT().FetchTimeline(gomock.Any(), repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       "user123", | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}).Return(repository.FetchTimelineResponse{}, errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 FetchTimelineLogic
 | ||||||
|  | 			logic := FetchTimelineLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 FetchTimeline
 | ||||||
|  | 			got, err := logic.FetchTimeline(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,62 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 HasNoMoreDataLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewHasNoMoreDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasNoMoreDataLogic { | ||||||
|  | 	return &HasNoMoreDataLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type hasNoMoreDataReq struct { | ||||||
|  | 	UID string `json:"uid" validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。
 | ||||||
|  | func (l *HasNoMoreDataLogic) HasNoMoreData(in *tweeting.DoNoMoreDataReq) (*tweeting.HasNoMoreDataResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&hasNoMoreDataReq{ | ||||||
|  | 		UID: in.GetUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	res, err := l.svcCtx.TimelineRepo.HasNoMoreData(l.ctx, in.GetUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-23
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.HasNoMoreDataErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "TimelineRepo.HasNoMoreData"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to get no more data flag:", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.HasNoMoreDataResp{ | ||||||
|  | 		Status: res, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,107 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestHasNoMoreDataLogic_HasNoMoreData(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		TimelineRepo: mockTimelineRepo, | ||||||
|  | 		Validate:     mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.DoNoMoreDataReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.HasNoMoreDataResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功檢查時間線是否完整", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功檢查時間線狀態
 | ||||||
|  | 				mockTimelineRepo.EXPECT().HasNoMoreData(gomock.Any(), "user123").Return(true, nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp: &tweeting.HasNoMoreDataResp{ | ||||||
|  | 				Status: true, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "檢查時間線狀態失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬檢查時間線狀態失敗
 | ||||||
|  | 				mockTimelineRepo.EXPECT().HasNoMoreData(gomock.Any(), "user123").Return(false, errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 HasNoMoreDataLogic
 | ||||||
|  | 			logic := HasNoMoreDataLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 HasNoMoreData
 | ||||||
|  | 			got, err := logic.HasNoMoreData(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"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 SetNoMoreDataFlagLogic struct { | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	logx.Logger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewSetNoMoreDataFlagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetNoMoreDataFlagLogic { | ||||||
|  | 	return &SetNoMoreDataFlagLogic{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 		Logger: logx.WithContext(ctx), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type sasNoMoreDataReq struct { | ||||||
|  | 	UID string `json:"uid" validate:"required"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。
 | ||||||
|  | func (l *SetNoMoreDataFlagLogic) SetNoMoreDataFlag(in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { | ||||||
|  | 	// 驗證資料
 | ||||||
|  | 	if err := l.svcCtx.Validate.ValidateAll(&sasNoMoreDataReq{ | ||||||
|  | 		UID: in.GetUid(), | ||||||
|  | 	}); err != nil { | ||||||
|  | 		// 錯誤代碼 05-011-00
 | ||||||
|  | 		return nil, ers.InvalidFormat(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := l.svcCtx.TimelineRepo.SetNoMoreDataFlag(l.ctx, in.GetUid()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// 錯誤代碼 05-021-24
 | ||||||
|  | 		e := domain.CommentErrorL( | ||||||
|  | 			domain.SetNoMoreDataErrorCode, | ||||||
|  | 			logx.WithContext(l.ctx), | ||||||
|  | 			[]logx.LogField{ | ||||||
|  | 				{Key: "req", Value: in}, | ||||||
|  | 				{Key: "func", Value: "TimelineRepo.SetNoMoreDataErrorCode"}, | ||||||
|  | 				{Key: "err", Value: err}, | ||||||
|  | 			}, | ||||||
|  | 			"failed to set no more data flag:", in.GetUid()).Wrap(err) | ||||||
|  | 
 | ||||||
|  | 		return nil, e | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &tweeting.OKResp{}, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,105 @@ | ||||||
|  | package timelineservicelogic | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	mocklib "app-cloudep-tweeting-service/internal/mock/lib" | ||||||
|  | 	mockRepo "app-cloudep-tweeting-service/internal/mock/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestSetNoMoreDataFlagLogic_SetNoMoreDataFlag(t *testing.T) { | ||||||
|  | 	ctrl := gomock.NewController(t) | ||||||
|  | 	defer ctrl.Finish() | ||||||
|  | 
 | ||||||
|  | 	// 初始化 mock 依賴
 | ||||||
|  | 	mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) | ||||||
|  | 	mockValidate := mocklib.NewMockValidate(ctrl) | ||||||
|  | 
 | ||||||
|  | 	// 初始化服務上下文
 | ||||||
|  | 	svcCtx := &svc.ServiceContext{ | ||||||
|  | 		TimelineRepo: mockTimelineRepo, | ||||||
|  | 		Validate:     mockValidate, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 測試數據集
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		input     *tweeting.DoNoMoreDataReq | ||||||
|  | 		prepare   func() | ||||||
|  | 		expectErr bool | ||||||
|  | 		wantResp  *tweeting.OKResp | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "成功標記時間線已完整", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬成功標記時間線狀態
 | ||||||
|  | 				mockTimelineRepo.EXPECT().SetNoMoreDataFlag(gomock.Any(), "user123").Return(nil).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			wantResp:  &tweeting.OKResp{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "驗證失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證失敗
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "標記時間線狀態失敗", | ||||||
|  | 			input: &tweeting.DoNoMoreDataReq{ | ||||||
|  | 				Uid: "user123", | ||||||
|  | 			}, | ||||||
|  | 			prepare: func() { | ||||||
|  | 				// 模擬驗證通過
 | ||||||
|  | 				mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) | ||||||
|  | 				// 模擬標記時間線狀態失敗
 | ||||||
|  | 				mockTimelineRepo.EXPECT().SetNoMoreDataFlag(gomock.Any(), "user123").Return(errors.New("repository error")).Times(1) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 			wantResp:  nil, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 執行測試
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			// 設置測試環境
 | ||||||
|  | 			tt.prepare() | ||||||
|  | 
 | ||||||
|  | 			// 初始化 SetNoMoreDataFlagLogic
 | ||||||
|  | 			logic := SetNoMoreDataFlagLogic{ | ||||||
|  | 				svcCtx: svcCtx, | ||||||
|  | 				ctx:    context.TODO(), | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// 執行 SetNoMoreDataFlag
 | ||||||
|  | 			got, err := logic.SetNoMoreDataFlag(tt.input) | ||||||
|  | 
 | ||||||
|  | 			// 驗證結果
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 				assert.Nil(t, got) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.wantResp, got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,174 @@ | ||||||
|  | // Code generated by MockGen. DO NOT EDIT.
 | ||||||
|  | // Source: ./internal/domain/repository/social_network.go
 | ||||||
|  | //
 | ||||||
|  | // Generated by this command:
 | ||||||
|  | //
 | ||||||
|  | //	mockgen -source=./internal/domain/repository/social_network.go -destination=./internal/mock/repository/social_network.go -package=mock
 | ||||||
|  | //
 | ||||||
|  | 
 | ||||||
|  | // Package mock is a generated GoMock package.
 | ||||||
|  | package mock | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	repository "app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	context "context" | ||||||
|  | 	reflect "reflect" | ||||||
|  | 
 | ||||||
|  | 	gomock "go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MockSocialNetworkRepository is a mock of SocialNetworkRepository interface.
 | ||||||
|  | type MockSocialNetworkRepository struct { | ||||||
|  | 	ctrl     *gomock.Controller | ||||||
|  | 	recorder *MockSocialNetworkRepositoryMockRecorder | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MockSocialNetworkRepositoryMockRecorder is the mock recorder for MockSocialNetworkRepository.
 | ||||||
|  | type MockSocialNetworkRepositoryMockRecorder struct { | ||||||
|  | 	mock *MockSocialNetworkRepository | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewMockSocialNetworkRepository creates a new mock instance.
 | ||||||
|  | func NewMockSocialNetworkRepository(ctrl *gomock.Controller) *MockSocialNetworkRepository { | ||||||
|  | 	mock := &MockSocialNetworkRepository{ctrl: ctrl} | ||||||
|  | 	mock.recorder = &MockSocialNetworkRepositoryMockRecorder{mock} | ||||||
|  | 	return mock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // EXPECT returns an object that allows the caller to indicate expected use.
 | ||||||
|  | func (m *MockSocialNetworkRepository) EXPECT() *MockSocialNetworkRepositoryMockRecorder { | ||||||
|  | 	return m.recorder | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CreateUserNode mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "CreateUserNode", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CreateUserNode indicates an expected call of CreateUserNode.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) CreateUserNode(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserNode", reflect.TypeOf((*MockSocialNetworkRepository)(nil).CreateUserNode), ctx, uid) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetDegreeBetweenUsers mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetDegreeBetweenUsers", ctx, uid1, uid2) | ||||||
|  | 	ret0, _ := ret[0].(int64) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetDegreeBetweenUsers indicates an expected call of GetDegreeBetweenUsers.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetDegreeBetweenUsers(ctx, uid1, uid2 any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDegreeBetweenUsers", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetDegreeBetweenUsers), ctx, uid1, uid2) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowee mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetFollowee(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetFollowee", ctx, req) | ||||||
|  | 	ret0, _ := ret[0].(repository.FollowResp) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowee indicates an expected call of GetFollowee.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollowee(ctx, req any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowee", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollowee), ctx, req) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFolloweeCount mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetFolloweeCount", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(int64) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFolloweeCount indicates an expected call of GetFolloweeCount.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetFolloweeCount(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFolloweeCount", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFolloweeCount), ctx, uid) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollower mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetFollower(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetFollower", ctx, req) | ||||||
|  | 	ret0, _ := ret[0].(repository.FollowResp) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollower indicates an expected call of GetFollower.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollower(ctx, req any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollower", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollower), ctx, req) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowerCount mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetFollowerCount(ctx context.Context, uid string) (int64, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetFollowerCount", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(int64) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowerCount indicates an expected call of GetFollowerCount.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollowerCount(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowerCount", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollowerCount), ctx, uid) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUIDsWithinNDegrees mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetUIDsWithinNDegrees", ctx, uid, degrees, pageSize, pageIndex) | ||||||
|  | 	ret0, _ := ret[0].([]string) | ||||||
|  | 	ret1, _ := ret[1].(int64) | ||||||
|  | 	ret2, _ := ret[2].(error) | ||||||
|  | 	return ret0, ret1, ret2 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUIDsWithinNDegrees indicates an expected call of GetUIDsWithinNDegrees.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) GetUIDsWithinNDegrees(ctx, uid, degrees, pageSize, pageIndex any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUIDsWithinNDegrees", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetUIDsWithinNDegrees), ctx, uid, degrees, pageSize, pageIndex) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarkFollowerRelation mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "MarkFollowerRelation", ctx, fromUID, toUID) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarkFollowerRelation indicates an expected call of MarkFollowerRelation.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) MarkFollowerRelation(ctx, fromUID, toUID any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkFollowerRelation", reflect.TypeOf((*MockSocialNetworkRepository)(nil).MarkFollowerRelation), ctx, fromUID, toUID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RemoveFollowerRelation mocks base method.
 | ||||||
|  | func (m *MockSocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "RemoveFollowerRelation", ctx, fromUID, toUID) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RemoveFollowerRelation indicates an expected call of RemoveFollowerRelation.
 | ||||||
|  | func (mr *MockSocialNetworkRepositoryMockRecorder) RemoveFollowerRelation(ctx, fromUID, toUID any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFollowerRelation", reflect.TypeOf((*MockSocialNetworkRepository)(nil).RemoveFollowerRelation), ctx, fromUID, toUID) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,113 @@ | ||||||
|  | // Code generated by MockGen. DO NOT EDIT.
 | ||||||
|  | // Source: ./internal/domain/repository/timeline.go
 | ||||||
|  | //
 | ||||||
|  | // Generated by this command:
 | ||||||
|  | //
 | ||||||
|  | //	mockgen -source=./internal/domain/repository/timeline.go -destination=./internal/mock/repository/timeline.go -package=mock
 | ||||||
|  | //
 | ||||||
|  | 
 | ||||||
|  | // Package mock is a generated GoMock package.
 | ||||||
|  | package mock | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	repository "app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	context "context" | ||||||
|  | 	reflect "reflect" | ||||||
|  | 
 | ||||||
|  | 	gomock "go.uber.org/mock/gomock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MockTimelineRepository is a mock of TimelineRepository interface.
 | ||||||
|  | type MockTimelineRepository struct { | ||||||
|  | 	ctrl     *gomock.Controller | ||||||
|  | 	recorder *MockTimelineRepositoryMockRecorder | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MockTimelineRepositoryMockRecorder is the mock recorder for MockTimelineRepository.
 | ||||||
|  | type MockTimelineRepositoryMockRecorder struct { | ||||||
|  | 	mock *MockTimelineRepository | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewMockTimelineRepository creates a new mock instance.
 | ||||||
|  | func NewMockTimelineRepository(ctrl *gomock.Controller) *MockTimelineRepository { | ||||||
|  | 	mock := &MockTimelineRepository{ctrl: ctrl} | ||||||
|  | 	mock.recorder = &MockTimelineRepositoryMockRecorder{mock} | ||||||
|  | 	return mock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // EXPECT returns an object that allows the caller to indicate expected use.
 | ||||||
|  | func (m *MockTimelineRepository) EXPECT() *MockTimelineRepositoryMockRecorder { | ||||||
|  | 	return m.recorder | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPost mocks base method.
 | ||||||
|  | func (m *MockTimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "AddPost", ctx, req) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPost indicates an expected call of AddPost.
 | ||||||
|  | func (mr *MockTimelineRepositoryMockRecorder) AddPost(ctx, req any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPost", reflect.TypeOf((*MockTimelineRepository)(nil).AddPost), ctx, req) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClearNoMoreDataFlag mocks base method.
 | ||||||
|  | func (m *MockTimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "ClearNoMoreDataFlag", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClearNoMoreDataFlag indicates an expected call of ClearNoMoreDataFlag.
 | ||||||
|  | func (mr *MockTimelineRepositoryMockRecorder) ClearNoMoreDataFlag(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearNoMoreDataFlag", reflect.TypeOf((*MockTimelineRepository)(nil).ClearNoMoreDataFlag), ctx, uid) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimeline mocks base method.
 | ||||||
|  | func (m *MockTimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "FetchTimeline", ctx, req) | ||||||
|  | 	ret0, _ := ret[0].(repository.FetchTimelineResponse) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimeline indicates an expected call of FetchTimeline.
 | ||||||
|  | func (mr *MockTimelineRepositoryMockRecorder) FetchTimeline(ctx, req any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTimeline", reflect.TypeOf((*MockTimelineRepository)(nil).FetchTimeline), ctx, req) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasNoMoreData mocks base method.
 | ||||||
|  | func (m *MockTimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "HasNoMoreData", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(bool) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasNoMoreData indicates an expected call of HasNoMoreData.
 | ||||||
|  | func (mr *MockTimelineRepositoryMockRecorder) HasNoMoreData(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasNoMoreData", reflect.TypeOf((*MockTimelineRepository)(nil).HasNoMoreData), ctx, uid) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNoMoreDataFlag mocks base method.
 | ||||||
|  | func (m *MockTimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "SetNoMoreDataFlag", ctx, uid) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNoMoreDataFlag indicates an expected call of SetNoMoreDataFlag.
 | ||||||
|  | func (mr *MockTimelineRepositoryMockRecorder) SetNoMoreDataFlag(ctx, uid any) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNoMoreDataFlag", reflect.TypeOf((*MockTimelineRepository)(nil).SetNoMoreDataFlag), ctx, uid) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,439 @@ | ||||||
|  | package repository | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/config" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	client4J "app-cloudep-tweeting-service/internal/lib/neo4j" | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/zeromicro/go-zero/core/logx" | ||||||
|  | 
 | ||||||
|  | 	"github.com/neo4j/neo4j-go-driver/v5/neo4j" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SocialNetworkParam struct { | ||||||
|  | 	Config      config.Config | ||||||
|  | 	Neo4jClient *client4J.Client | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type SocialNetworkRepository struct { | ||||||
|  | 	cfg         config.Config | ||||||
|  | 	neo4jClient *client4J.Client | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func MustSocialNetworkRepository(param SocialNetworkParam) repository.SocialNetworkRepository { | ||||||
|  | 	return &SocialNetworkRepository{ | ||||||
|  | 		cfg:         param.Config, | ||||||
|  | 		neo4jClient: param.Neo4jClient, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid": uid, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeWrite, | ||||||
|  | 	}).Run(ctx, "CREATE (n:User {uid: $uid}) RETURN n", params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 處理結果
 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		_ = run.Record().AsMap() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"fromUID": fromUID, | ||||||
|  | 		"toUID":   toUID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 這是有向的關係  form -> to
 | ||||||
|  | 	query := ` | ||||||
|  | 		MERGE (from:User {uid: $fromUID}) | ||||||
|  | 		MERGE (to:User {uid: $toUID}) | ||||||
|  | 		MERGE (from)-[:FRIENDS_WITH]->(to) | ||||||
|  | 		RETURN from, to | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeWrite, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 處理結果
 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		_ = run.Record().AsMap() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) GetFollower(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid":   req.UID, | ||||||
|  | 		"skip":  (req.PageIndex - 1) * req.PageSize, | ||||||
|  | 		"limit": req.PageSize, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (follower:User)-[:FRIENDS_WITH]->(user:User {uid: $uid}) | ||||||
|  | 		RETURN follower.uid AS uid | ||||||
|  | 		SKIP $skip LIMIT $limit | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var uidList []string | ||||||
|  | 	for run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		uid, ok := record.Get("uid") | ||||||
|  | 		if ok { | ||||||
|  | 			if uidStr, ok := uid.(string); ok { | ||||||
|  | 				uidList = append(uidList, uidStr) | ||||||
|  | 			} else { | ||||||
|  | 				// TODO 可以印 log
 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	total, err := s.GetFollowerCount(ctx, req.UID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return repository.FollowResp{ | ||||||
|  | 		UIDs:  uidList, | ||||||
|  | 		Total: total, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) GetFollowee(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid":   req.UID, | ||||||
|  | 		"skip":  (req.PageIndex - 1) * req.PageSize, | ||||||
|  | 		"limit": req.PageSize, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (user:User {uid: $uid})-[:FRIENDS_WITH]->(followee:User) | ||||||
|  | 		RETURN followee.uid AS uid | ||||||
|  | 		SKIP $skip LIMIT $limit | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var uidList []string | ||||||
|  | 	for run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		uid, ok := record.Get("uid") | ||||||
|  | 		if ok { | ||||||
|  | 			if uidStr, ok := uid.(string); ok { | ||||||
|  | 				uidList = append(uidList, uidStr) | ||||||
|  | 			} else { | ||||||
|  | 				// 可以印 log
 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	total, err := s.GetFolloweeCount(ctx, req.UID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FollowResp{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return repository.FollowResp{ | ||||||
|  | 		UIDs:  uidList, | ||||||
|  | 		Total: total, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) GetFollowerCount(ctx context.Context, uid string) (int64, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid": uid, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (:User)-[:FRIENDS_WITH]->(user:User {uid: $uid}) | ||||||
|  | 		RETURN count(*) AS followerCount | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var count int64 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		if followerCount, ok := record.Get("followerCount"); ok { | ||||||
|  | 			if dc, ok := followerCount.(int64); ok { | ||||||
|  | 				count = dc | ||||||
|  | 			} else { | ||||||
|  | 				logx.Info("followerCount error") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return count, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid": uid, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (user:User {uid: $uid})-[:FRIENDS_WITH]->(:User) | ||||||
|  | 		RETURN count(*) AS followeeCount | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var count int64 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		if followeeCount, ok := record.Get("followeeCount"); ok { | ||||||
|  | 			if dc, ok := followeeCount.(int64); ok { | ||||||
|  | 				count = dc | ||||||
|  | 			} else { | ||||||
|  | 				logx.Info("followeeCount error") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return count, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"fromUID": fromUID, | ||||||
|  | 		"toUID":   toUID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (from:User {uid: $fromUID})-[r:FRIENDS_WITH]->(to:User {uid: $toUID}) | ||||||
|  | 		DELETE r | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	_, err = session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeWrite, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to remove follower relation: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetDegreeBetweenUsers 取得這兩個點之間的度數 (最短路徑長度)
 | ||||||
|  | func (s *SocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid1": uid1, | ||||||
|  | 		"uid2": uid2, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (user1:User {uid: $uid1}), (user2:User {uid: $uid2}) | ||||||
|  | 		MATCH p = shortestPath((user1)-[*]-(user2)) | ||||||
|  | 		RETURN length(p) AS degree | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, fmt.Errorf("failed to get degree between users: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var degree int64 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		if deg, ok := record.Get("degree"); ok { | ||||||
|  | 			if degreeValue, ok := deg.(int64); ok { | ||||||
|  | 				degree = degreeValue | ||||||
|  | 			} else { | ||||||
|  | 				logx.Info("degree error") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return degree, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUIDsWithinNDegrees 取得某個節點在 n 度內關係所有 UID
 | ||||||
|  | func (s *SocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid":     uid, | ||||||
|  | 		"degrees": degrees, | ||||||
|  | 		"skip":    (pageIndex - 1) * pageSize, | ||||||
|  | 		"limit":   pageSize, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 查詢結果帶分頁
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (user:User {uid: $uid})-[:FRIENDS_WITH*1..$degrees]-(related:User) | ||||||
|  | 		WITH DISTINCT related.uid AS uid | ||||||
|  | 		SKIP $skip LIMIT $limit | ||||||
|  | 		RETURN uid | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, fmt.Errorf("failed to get uids within %d degrees of user: %w", degrees, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var uidList []string | ||||||
|  | 	for run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		uid, ok := record.Get("uid") | ||||||
|  | 		if ok { | ||||||
|  | 			if uidStr, ok := uid.(string); ok { | ||||||
|  | 				uidList = append(uidList, uidStr) | ||||||
|  | 			} else { | ||||||
|  | 				// 可以印 log
 | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 計算總數
 | ||||||
|  | 	totalCount, err := s.getTotalUIDsWithinNDegrees(ctx, uid, degrees) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, 0, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return uidList, totalCount, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *SocialNetworkRepository) getTotalUIDsWithinNDegrees(ctx context.Context, uid string, degrees int64) (int64, error) { | ||||||
|  | 	//nolint:contextcheck
 | ||||||
|  | 	session, err := s.neo4jClient.Conn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	defer session.Close(ctx) | ||||||
|  | 
 | ||||||
|  | 	params := map[string]interface{}{ | ||||||
|  | 		"uid":     uid, | ||||||
|  | 		"degrees": degrees, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	query := ` | ||||||
|  | 		MATCH (user:User {uid: $uid})-[:FRIENDS_WITH*1..$degrees]-(related:User) | ||||||
|  | 		RETURN count(DISTINCT related.uid) AS totalCount | ||||||
|  | 	` | ||||||
|  | 
 | ||||||
|  | 	run, err := session.NewSession(ctx, neo4j.SessionConfig{ | ||||||
|  | 		AccessMode: neo4j.AccessModeRead, | ||||||
|  | 	}).Run(ctx, query, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, fmt.Errorf("failed to get total uids within %d degrees of user: %w", degrees, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var totalCount int64 | ||||||
|  | 	if run.Next(ctx) { | ||||||
|  | 		record := run.Record() | ||||||
|  | 		if count, ok := record.Get("totalCount"); ok { | ||||||
|  | 			if countV, ok := count.(int64); ok { | ||||||
|  | 				totalCount = countV | ||||||
|  | 			} else { | ||||||
|  | 				logx.Info("totalCount error") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return totalCount, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | package repository | ||||||
|  | @ -0,0 +1,144 @@ | ||||||
|  | package repository | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/config" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/zeromicro/go-zero/core/stores/redis" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // TODO 第一版本先使用 Redis 來做,後續如果有效能考量,在考慮使用其他方案
 | ||||||
|  | 
 | ||||||
|  | type TimelineRepositoryParam struct { | ||||||
|  | 	Config config.Config | ||||||
|  | 	Redis  redis.Redis | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type TimelineRepository struct { | ||||||
|  | 	cfg   config.Config | ||||||
|  | 	redis redis.Redis | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func MustGenerateRepository(param TimelineRepositoryParam) repository.TimelineRepository { | ||||||
|  | 	return &TimelineRepository{ | ||||||
|  | 		cfg:   param.Config, | ||||||
|  | 		redis: param.Redis, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPost 將貼文添加到時間線,並根據 Score 排序
 | ||||||
|  | func (t *TimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error { | ||||||
|  | 	key := domain.TimelineRedisKey.With(req.UID).ToString() | ||||||
|  | 
 | ||||||
|  | 	// 準備要插入的元素
 | ||||||
|  | 	zItems := make([]redis.Pair, len(req.PostItems)) | ||||||
|  | 	for i, item := range req.PostItems { | ||||||
|  | 		zItems[i] = redis.Pair{ | ||||||
|  | 			Score: item.Score, | ||||||
|  | 			Key:   item.PostID, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 將 ZSet 元素添加到 Redis
 | ||||||
|  | 	_, err := t.redis.ZaddsCtx(ctx, key, zItems...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 檢查 ZSet 長度,並在超過 maxLength 時刪除多餘的元素
 | ||||||
|  | 	if t.cfg.TimelineSetting.MaxLength > 0 { | ||||||
|  | 		// 這裡從 0 到 - (maxLength+1) 代表超過限制的元素範圍
 | ||||||
|  | 		_, err := t.redis.ZremrangebyrankCtx(ctx, key, 0, -(t.cfg.TimelineSetting.MaxLength + 1)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 設置過期時間
 | ||||||
|  | 	return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimeline 獲取指定用戶的動態時報
 | ||||||
|  | func (t *TimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) { | ||||||
|  | 	key := domain.TimelineRedisKey.With(req.UID).ToString() | ||||||
|  | 
 | ||||||
|  | 	start := (req.PageIndex - 1) * req.PageSize | ||||||
|  | 	end := start + req.PageSize - 1 | ||||||
|  | 
 | ||||||
|  | 	// 從 Redis 中按分數由高到低獲取時間線元素
 | ||||||
|  | 	pair, err := t.redis.ZrevrangeWithScoresCtx(ctx, key, start, end) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FetchTimelineResponse{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 構建返回結果
 | ||||||
|  | 	items := make([]repository.TimelineItem, len(pair)) | ||||||
|  | 	for i, z := range pair { | ||||||
|  | 		items[i] = repository.TimelineItem{ | ||||||
|  | 			PostID: z.Key, | ||||||
|  | 			Score:  z.Score, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 計算總數量
 | ||||||
|  | 	total, err := t.redis.ZcardCtx(ctx, key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return repository.FetchTimelineResponse{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return repository.FetchTimelineResponse{ | ||||||
|  | 		Items: items, | ||||||
|  | 		Page: tweeting.Pager{ | ||||||
|  | 			Total: int64(total), | ||||||
|  | 			Index: req.PageIndex, | ||||||
|  | 			Size:  req.PageSize, | ||||||
|  | 		}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫
 | ||||||
|  | func (t *TimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error { | ||||||
|  | 	key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 
 | ||||||
|  | 	// 添加一個標誌到時間線的 ZSet
 | ||||||
|  | 	_, err := t.redis.ZaddsCtx(ctx, key, redis.Pair{ | ||||||
|  | 		Score: time.Now().UTC().Unix(), | ||||||
|  | 		Key:   domain.LastOfTimelineFlag, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 設置過期時間
 | ||||||
|  | 	return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫
 | ||||||
|  | func (t *TimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) { | ||||||
|  | 	key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 
 | ||||||
|  | 	// 檢查 "NoMoreData" 標誌是否存在
 | ||||||
|  | 	score, err := t.redis.ZscoreCtx(ctx, key, domain.LastOfTimelineFlag) | ||||||
|  | 	if errors.Is(err, redis.Nil) { | ||||||
|  | 		return false, nil // 標誌不存在
 | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, err // 其他錯誤
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return score != 0, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌
 | ||||||
|  | func (t *TimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error { | ||||||
|  | 	key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 	// 移除 "NoMoreData" 標誌
 | ||||||
|  | 	_, err := t.redis.ZremCtx(ctx, key, domain.LastOfTimelineFlag) | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | @ -0,0 +1,474 @@ | ||||||
|  | package repository | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"app-cloudep-tweeting-service/internal/config" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"context" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/alicebob/miniredis/v2" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/zeromicro/go-zero/core/stores/redis" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewRepo() (*miniredis.Miniredis, repository.TimelineRepository, error) { | ||||||
|  | 	r1, err := miniredis.Run() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	newRedis, err := redis.NewRedis(redis.RedisConf{ | ||||||
|  | 		Host: r1.Addr(), | ||||||
|  | 		Type: redis.ClusterType, | ||||||
|  | 		Pass: "", | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		r1.Close() | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c := config.Config{ | ||||||
|  | 		TimelineSetting: struct { | ||||||
|  | 			Expire    int64 | ||||||
|  | 			MaxLength int64 | ||||||
|  | 		}{Expire: 86400, MaxLength: 1000}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	timelineRepo := MustGenerateRepository(TimelineRepositoryParam{ | ||||||
|  | 		Config: c, | ||||||
|  | 		Redis:  *newRedis, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return r1, timelineRepo, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAddPost(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		action    func(t *testing.T, repo repository.TimelineRepository) error | ||||||
|  | 		expectErr bool | ||||||
|  | 		validate  func(t *testing.T, r1 *miniredis.Miniredis) | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "success", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "OOOOOOKJ" | ||||||
|  | 
 | ||||||
|  | 				return repo.AddPost(ctx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 100}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			validate: func(t *testing.T, r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "OOOOOOKJ" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				score, err := r1.ZScore(key, "post1") | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, float64(100), score) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "timeout", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond) | ||||||
|  | 				defer cancel() | ||||||
|  | 				time.Sleep(2 * time.Millisecond) | ||||||
|  | 
 | ||||||
|  | 				uid := "OOOOOLK" | ||||||
|  | 				return repo.AddPost(timeoutCtx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post2", Score: 200}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Redis error on Zadd", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				r1, repo, err := NewRepo() | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				r1.Close() // 模拟 Redis 错误
 | ||||||
|  | 
 | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "OOOOOWE" | ||||||
|  | 				return repo.AddPost(ctx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post3", Score: 300}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "duplicate Key", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "OOOOODUP" | ||||||
|  | 
 | ||||||
|  | 				// 第一次插入
 | ||||||
|  | 				err := repo.AddPost(ctx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 100}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// 第二次插入,使用相同的 PostID 但不同的 Score
 | ||||||
|  | 				return repo.AddPost(ctx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 200}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			validate: func(t *testing.T, r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "OOOOODUP" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				score, err := r1.ZScore(key, "post1") | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, float64(200), score) // 應該是第二次插入的分數
 | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			r1, repo, err := NewRepo() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 			err = tt.action(t, repo) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				if tt.validate != nil { | ||||||
|  | 					tt.validate(t, r1) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestFetchTimeline(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		action    func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) | ||||||
|  | 		expectErr bool | ||||||
|  | 		validate  func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse) | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "FetchTimeline - success", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 
 | ||||||
|  | 				_ = repo.AddPost(ctx, repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post1", Score: 200}, | ||||||
|  | 						{PostID: "post2", Score: 100}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				return repo.FetchTimeline(ctx, repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       uid, | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			validate: func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse) { | ||||||
|  | 				assert.Equal(t, 2, len(resp.Items)) | ||||||
|  | 				assert.Equal(t, "post1", resp.Items[0].PostID) | ||||||
|  | 				assert.Equal(t, "post2", resp.Items[1].PostID) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "FetchTimeline - timeout", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond) | ||||||
|  | 				defer cancel() | ||||||
|  | 				time.Sleep(2 * time.Millisecond) | ||||||
|  | 
 | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.FetchTimeline(timeoutCtx, repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       uid, | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "FetchTimeline - Redis error on ZrangebyscoreWithScoresCtx", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { | ||||||
|  | 				r1, repo, err := NewRepo() | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 				uid := "user123" | ||||||
|  | 
 | ||||||
|  | 				_ = repo.AddPost(context.Background(), repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post3", Score: 300}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				r1.Close() | ||||||
|  | 
 | ||||||
|  | 				return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       uid, | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "FetchTimeline - Redis error on ZcardCtx", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { | ||||||
|  | 				r1, repo, err := NewRepo() | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 				uid := "user123" | ||||||
|  | 
 | ||||||
|  | 				_ = repo.AddPost(context.Background(), repository.AddPostRequest{ | ||||||
|  | 					UID: uid, | ||||||
|  | 					PostItems: []repository.TimelineItem{ | ||||||
|  | 						{PostID: "post4", Score: 400}, | ||||||
|  | 					}, | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				r1.Close() | ||||||
|  | 
 | ||||||
|  | 				return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{ | ||||||
|  | 					UID:       uid, | ||||||
|  | 					PageSize:  10, | ||||||
|  | 					PageIndex: 1, | ||||||
|  | 				}) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			r1, repo, err := NewRepo() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 			resp, err := tt.action(t, repo) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				if tt.validate != nil { | ||||||
|  | 					tt.validate(t, r1, &resp) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSetNoMoreDataFlag(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		action    func(t *testing.T, repo repository.TimelineRepository) error | ||||||
|  | 		expectErr bool | ||||||
|  | 		validate  func(t *testing.T, r1 *miniredis.Miniredis) | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "SetNoMoreDataFlag - success", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.SetNoMoreDataFlag(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			validate: func(t *testing.T, r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "user123" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				score, _ := r1.ZScore(key, domain.LastOfTimelineFlag) | ||||||
|  | 				assert.NotZero(t, score) | ||||||
|  | 
 | ||||||
|  | 				// 驗證是否設定過期時間
 | ||||||
|  | 				ttl := r1.TTL(key) | ||||||
|  | 				assert.Equal(t, time.Duration(86400)*time.Second, ttl) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "SetNoMoreDataFlag - Redis error on ZaddsCtx", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				r1, repo, err := NewRepo() | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				r1.Close() // 手動關閉,復現錯誤
 | ||||||
|  | 
 | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.SetNoMoreDataFlag(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "SetNoMoreDataFlag - Redis error on ExpireCtx", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				r1, repo, err := NewRepo() | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 
 | ||||||
|  | 				r1.Close() | ||||||
|  | 
 | ||||||
|  | 				return repo.SetNoMoreDataFlag(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			r1, repo, err := NewRepo() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 			err = tt.action(t, repo) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				if tt.validate != nil { | ||||||
|  | 					tt.validate(t, r1) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestHasNoMoreData(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		action    func(t *testing.T, repo repository.TimelineRepository) (bool, error) | ||||||
|  | 		expectErr bool | ||||||
|  | 		expected  bool | ||||||
|  | 		setup     func(r1 *miniredis.Miniredis) | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "HasNoMoreData - 標誌存在", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.HasNoMoreData(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			expected:  true, | ||||||
|  | 			setup: func(r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "user123" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				_, _ = r1.ZAdd(key, float64(time.Now().UTC().Unix()), domain.LastOfTimelineFlag) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "HasNoMoreData - 標誌不存在", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.HasNoMoreData(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			expected:  false, | ||||||
|  | 			setup:     func(r1 *miniredis.Miniredis) {}, // 不設置標誌
 | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			r1, repo, err := NewRepo() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 			tt.setup(r1) | ||||||
|  | 
 | ||||||
|  | 			result, err := tt.action(t, repo) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				assert.Equal(t, tt.expected, result) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestClearNoMoreDataFlag(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		action    func(t *testing.T, repo repository.TimelineRepository) error | ||||||
|  | 		expectErr bool | ||||||
|  | 		setup     func(r1 *miniredis.Miniredis) | ||||||
|  | 		validate  func(t *testing.T, r1 *miniredis.Miniredis) | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "ClearNoMoreDataFlag - 成功清除標誌", | ||||||
|  | 			action: func(t *testing.T, repo repository.TimelineRepository) error { | ||||||
|  | 				ctx := context.Background() | ||||||
|  | 				uid := "user123" | ||||||
|  | 				return repo.ClearNoMoreDataFlag(ctx, uid) | ||||||
|  | 			}, | ||||||
|  | 			expectErr: false, | ||||||
|  | 			setup: func(r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "user123" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				_, err := r1.ZAdd(key, 100, domain.LastOfTimelineFlag) // 設置標誌
 | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 			}, | ||||||
|  | 			validate: func(t *testing.T, r1 *miniredis.Miniredis) { | ||||||
|  | 				uid := "user123" | ||||||
|  | 				key := domain.TimelineRedisKey.With(uid).ToString() | ||||||
|  | 				_, err := r1.ZScore(key, domain.LastOfTimelineFlag) | ||||||
|  | 				assert.Error(t, err) // 標誌應該已被移除
 | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			r1, repo, err := NewRepo() | ||||||
|  | 			assert.NoError(t, err) | ||||||
|  | 			defer r1.Close() | ||||||
|  | 
 | ||||||
|  | 			tt.setup(r1) | ||||||
|  | 
 | ||||||
|  | 			err = tt.action(t, repo) | ||||||
|  | 
 | ||||||
|  | 			if tt.expectErr { | ||||||
|  | 				assert.Error(t, err) | ||||||
|  | 			} else { | ||||||
|  | 				assert.NoError(t, err) | ||||||
|  | 				tt.validate(t, r1) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | // Code generated by goctl. DO NOT EDIT.
 | ||||||
|  | // Source: tweeting.proto
 | ||||||
|  | 
 | ||||||
|  | package server | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
|  | 	socialnetworkservicelogic "app-cloudep-tweeting-service/internal/logic/socialnetworkservice" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SocialNetworkServiceServer struct { | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	tweeting.UnimplementedSocialNetworkServiceServer | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewSocialNetworkServiceServer(svcCtx *svc.ServiceContext) *SocialNetworkServiceServer { | ||||||
|  | 	return &SocialNetworkServiceServer{ | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // MarkFollowRelation 關注
 | ||||||
|  | func (s *SocialNetworkServiceServer) MarkFollowRelation(ctx context.Context, in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewMarkFollowRelationLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.MarkFollowRelation(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RemoveFollowRelation 取消關注
 | ||||||
|  | func (s *SocialNetworkServiceServer) RemoveFollowRelation(ctx context.Context, in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewRemoveFollowRelationLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.RemoveFollowRelation(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollower 取得跟隨者名單
 | ||||||
|  | func (s *SocialNetworkServiceServer) GetFollower(ctx context.Context, in *tweeting.FollowReq) (*tweeting.FollowResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewGetFollowerLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.GetFollower(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowee 取得我跟隨的名單
 | ||||||
|  | func (s *SocialNetworkServiceServer) GetFollowee(ctx context.Context, in *tweeting.FollowReq) (*tweeting.FollowResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewGetFolloweeLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.GetFollowee(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFollowerCount 取得跟隨者數量
 | ||||||
|  | func (s *SocialNetworkServiceServer) GetFollowerCount(ctx context.Context, in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewGetFollowerCountLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.GetFollowerCount(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetFolloweeCount 取得我跟隨的數量
 | ||||||
|  | func (s *SocialNetworkServiceServer) GetFolloweeCount(ctx context.Context, in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { | ||||||
|  | 	l := socialnetworkservicelogic.NewGetFolloweeCountLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.GetFolloweeCount(in) | ||||||
|  | } | ||||||
|  | @ -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" | ||||||
|  | 	timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type TimelineServiceServer struct { | ||||||
|  | 	svcCtx *svc.ServiceContext | ||||||
|  | 	tweeting.UnimplementedTimelineServiceServer | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewTimelineServiceServer(svcCtx *svc.ServiceContext) *TimelineServiceServer { | ||||||
|  | 	return &TimelineServiceServer{ | ||||||
|  | 		svcCtx: svcCtx, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷
 | ||||||
|  | func (s *TimelineServiceServer) AddPost(ctx context.Context, in *tweeting.AddPostToTimelineReq) (*tweeting.OKResp, error) { | ||||||
|  | 	l := timelineservicelogic.NewAddPostLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.AddPost(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FetchTimeline 取得這個人的動態時報
 | ||||||
|  | func (s *TimelineServiceServer) FetchTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.FetchTimelineResponse, error) { | ||||||
|  | 	l := timelineservicelogic.NewFetchTimelineLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.FetchTimeline(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。
 | ||||||
|  | func (s *TimelineServiceServer) SetNoMoreDataFlag(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { | ||||||
|  | 	l := timelineservicelogic.NewSetNoMoreDataFlagLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.SetNoMoreDataFlag(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。
 | ||||||
|  | func (s *TimelineServiceServer) HasNoMoreData(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.HasNoMoreDataResp, error) { | ||||||
|  | 	l := timelineservicelogic.NewHasNoMoreDataLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.HasNoMoreData(in) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。
 | ||||||
|  | func (s *TimelineServiceServer) ClearNoMoreDataFlag(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { | ||||||
|  | 	l := timelineservicelogic.NewClearNoMoreDataFlagLogic(ctx, s.svcCtx) | ||||||
|  | 	return l.ClearNoMoreDataFlag(in) | ||||||
|  | } | ||||||
|  | @ -6,7 +6,7 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func mustMongoConnectUrl(c config.Config) string { | func mustMongoConnectURL(c config.Config) string { | ||||||
| 	return fmt.Sprintf("%s://%s:%s", | 	return fmt.Sprintf("%s://%s:%s", | ||||||
| 		c.Mongo.Schema, | 		c.Mongo.Schema, | ||||||
| 		c.Mongo.Host, | 		c.Mongo.Host, | ||||||
|  | @ -18,10 +18,12 @@ func mustMongoConnectUrl(c config.Config) string { | ||||||
| 
 | 
 | ||||||
| func MustPostModel(c config.Config) model.PostModel { | func MustPostModel(c config.Config) model.PostModel { | ||||||
| 	postCollection := model.Post{} | 	postCollection := model.Post{} | ||||||
| 	return model.NewPostModel(mustMongoConnectUrl(c), c.Mongo.Database, postCollection.CollectionName()) | 
 | ||||||
|  | 	return model.NewPostModel(mustMongoConnectURL(c), c.Mongo.Database, postCollection.CollectionName()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func MustCommentModel(c config.Config) model.CommentModel { | func MustCommentModel(c config.Config) model.CommentModel { | ||||||
| 	m := model.Comment{} | 	m := model.Comment{} | ||||||
| 	return model.NewCommentModel(mustMongoConnectUrl(c), c.Mongo.Database, m.CollectionName()) | 
 | ||||||
|  | 	return model.NewCommentModel(mustMongoConnectURL(c), c.Mongo.Database, m.CollectionName()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,24 +2,52 @@ package svc | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"app-cloudep-tweeting-service/internal/config" | 	"app-cloudep-tweeting-service/internal/config" | ||||||
|  | 	domainRepo "app-cloudep-tweeting-service/internal/domain/repository" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/lib/neo4j" | ||||||
| 	model "app-cloudep-tweeting-service/internal/model/mongo" | 	model "app-cloudep-tweeting-service/internal/model/mongo" | ||||||
|  | 	"app-cloudep-tweeting-service/internal/repository" | ||||||
|  | 
 | ||||||
|  | 	"github.com/zeromicro/go-zero/core/stores/redis" | ||||||
| 
 | 
 | ||||||
| 	vi "code.30cm.net/digimon/library-go/validator" | 	vi "code.30cm.net/digimon/library-go/validator" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ServiceContext struct { | type ServiceContext struct { | ||||||
| 	Config   config.Config | 	Config                  config.Config | ||||||
| 	Validate vi.Validate | 	Validate                vi.Validate | ||||||
| 
 | 	PostModel               model.PostModel | ||||||
| 	PostModel    model.PostModel | 	CommentModel            model.CommentModel | ||||||
| 	CommentModel model.CommentModel | 	TimelineRepo            domainRepo.TimelineRepository | ||||||
|  | 	SocialNetworkRepository domainRepo.SocialNetworkRepository | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewServiceContext(c config.Config) *ServiceContext { | func NewServiceContext(c config.Config) *ServiceContext { | ||||||
|  | 	newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	neoClient := neo4j.NewNeo4J(&neo4j.Config{ | ||||||
|  | 		URI:                   c.Neo4J.URI, | ||||||
|  | 		Username:              c.Neo4J.Username, | ||||||
|  | 		Password:              c.Neo4J.Password, | ||||||
|  | 		MaxConnectionPoolSize: c.Neo4J.MaxConnectionPoolSize, | ||||||
|  | 		MaxConnectionLifetime: c.Neo4J.MaxConnectionLifetime, | ||||||
|  | 		ConnectionTimeout:     c.Neo4J.ConnectionTimeout, | ||||||
|  | 	}, neo4j.WithPerformance(), neo4j.WithLogLevel(c.Neo4J.LogLevel)) | ||||||
|  | 
 | ||||||
| 	return &ServiceContext{ | 	return &ServiceContext{ | ||||||
| 		Config:       c, | 		Config:       c, | ||||||
| 		Validate:     vi.MustValidator(), | 		Validate:     vi.MustValidator(), | ||||||
| 		PostModel:    MustPostModel(c), | 		PostModel:    MustPostModel(c), | ||||||
| 		CommentModel: MustCommentModel(c), | 		CommentModel: MustCommentModel(c), | ||||||
|  | 		TimelineRepo: repository.MustGenerateRepository(repository.TimelineRepositoryParam{ | ||||||
|  | 			Config: c, | ||||||
|  | 			Redis:  *newRedis, | ||||||
|  | 		}), | ||||||
|  | 		SocialNetworkRepository: repository.MustSocialNetworkRepository(repository.SocialNetworkParam{ | ||||||
|  | 			Config:      c, | ||||||
|  | 			Neo4jClient: neoClient, | ||||||
|  | 		}), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								tweeting.go
								
								
								
								
							
							
						
						
									
										13
									
								
								tweeting.go
								
								
								
								
							|  | @ -1,13 +1,15 @@ | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||||
| 	"app-cloudep-tweeting-service/internal/config" | 	"app-cloudep-tweeting-service/internal/config" | ||||||
|  | 	commentserviceServer "app-cloudep-tweeting-service/internal/server/commentservice" | ||||||
| 	postserviceServer "app-cloudep-tweeting-service/internal/server/postservice" | 	postserviceServer "app-cloudep-tweeting-service/internal/server/postservice" | ||||||
|  | 	socialnetworkserviceServer "app-cloudep-tweeting-service/internal/server/socialnetworkservice" | ||||||
|  | 	timelineserviceServer "app-cloudep-tweeting-service/internal/server/timelineservice" | ||||||
| 	"app-cloudep-tweeting-service/internal/svc" | 	"app-cloudep-tweeting-service/internal/svc" | ||||||
|  | 	"flag" | ||||||
|  | 	"log" | ||||||
| 
 | 
 | ||||||
| 	"github.com/zeromicro/go-zero/core/conf" | 	"github.com/zeromicro/go-zero/core/conf" | ||||||
| 	"github.com/zeromicro/go-zero/core/service" | 	"github.com/zeromicro/go-zero/core/service" | ||||||
|  | @ -27,6 +29,9 @@ func main() { | ||||||
| 
 | 
 | ||||||
| 	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { | 	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { | ||||||
| 		tweeting.RegisterPostServiceServer(grpcServer, postserviceServer.NewPostServiceServer(ctx)) | 		tweeting.RegisterPostServiceServer(grpcServer, postserviceServer.NewPostServiceServer(ctx)) | ||||||
|  | 		tweeting.RegisterCommentServiceServer(grpcServer, commentserviceServer.NewCommentServiceServer(ctx)) | ||||||
|  | 		tweeting.RegisterTimelineServiceServer(grpcServer, timelineserviceServer.NewTimelineServiceServer(ctx)) | ||||||
|  | 		tweeting.RegisterSocialNetworkServiceServer(grpcServer, socialnetworkserviceServer.NewSocialNetworkServiceServer(ctx)) | ||||||
| 
 | 
 | ||||||
| 		if c.Mode == service.DevMode || c.Mode == service.TestMode { | 		if c.Mode == service.DevMode || c.Mode == service.TestMode { | ||||||
| 			reflection.Register(grpcServer) | 			reflection.Register(grpcServer) | ||||||
|  | @ -34,6 +39,6 @@ func main() { | ||||||
| 	}) | 	}) | ||||||
| 	defer s.Stop() | 	defer s.Stop() | ||||||
| 
 | 
 | ||||||
| 	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) | 	log.Printf("Starting rpc server at %s...\n", c.ListenOn) | ||||||
| 	s.Start() | 	s.Start() | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue