feature/fanout #3
			
				
			
		
		
		
	|  | @ -117,6 +117,14 @@ issues: | |||
|         - gocognit | ||||
|         - contextcheck | ||||
| 
 | ||||
|   exclude-dirs: | ||||
|     - internal/model | ||||
| 
 | ||||
|   exclude-files: | ||||
|     - .*_test.go | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| linters-settings: | ||||
|   gci: | ||||
|     sections: | ||||
|  |  | |||
							
								
								
									
										9
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										9
									
								
								Makefile
								
								
								
								
							|  | @ -18,6 +18,7 @@ test: # 進行測試 | |||
| fmt: # 格式優化
 | ||||
| 	$(GOFMT) -w $(GOFILES) | ||||
| 	goimports -w  ./ | ||||
| 	golangci-lint run | ||||
| 
 | ||||
| .PHONY: gen-rpc | ||||
| gen-rpc: # 建立 rpc code
 | ||||
|  | @ -51,9 +52,9 @@ gen-mongo-model: # 建立 rpc 資料庫 | |||
| 	# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是 | ||||
| 	goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||
| 	goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||
| #	goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
 | ||||
| #	goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
 | ||||
| #	goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
 | ||||
| 	goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||
| 	goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||
| 	goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) | ||||
| 	@echo "Generate mongo model files successfully" | ||||
| 
 | ||||
| .PHONY: mock-gen | ||||
|  | @ -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/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/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" | ||||
| 
 | ||||
| .PHONY: migrate-database | ||||
|  |  | |||
|  | @ -12,3 +12,20 @@ Mongo: | |||
|   Password: "" | ||||
|   Port: "27017" | ||||
|   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; | ||||
| 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 更新評論 | ||||
|   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 ( | ||||
| 	code.30cm.net/digimon/library-go/errs v1.2.4 | ||||
| 	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/testcontainers/testcontainers-go v0.33.0 | ||||
| 	github.com/zeromicro/go-zero v1.7.0 | ||||
| 	go.mongodb.org/mongo-driver v1.16.0 | ||||
| 	go.uber.org/mock v0.4.0 | ||||
|  | @ -14,18 +17,32 @@ 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/cenkalti/backoff/v4 v4.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-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/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/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/go-logr/logr v1.4.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/jsonreference v0.20.2 // 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/klauspost/compress v1.17.8 // 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/mattn/go-colorable v0.1.13 // 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/reflect2 v1.0.2 // 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/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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect | ||||
| 	github.com/prometheus/client_golang v1.19.1 // indirect | ||||
| 	github.com/prometheus/client_model v0.5.0 // indirect | ||||
| 	github.com/prometheus/common v0.48.0 // indirect | ||||
| 	github.com/prometheus/procfs v0.12.0 // indirect | ||||
| 	github.com/redis/go-redis/v9 v9.6.1 // indirect | ||||
| 	github.com/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/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/scram v1.1.2 // indirect | ||||
| 	github.com/xdg-go/stringprep v1.0.4 // 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/client/pkg/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/exporters/jaeger v1.17.0 // indirect | ||||
| 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect | ||||
|  |  | |||
|  | @ -1,9 +1,15 @@ | |||
| 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 { | ||||
| 	zrpc.RpcServerConf | ||||
| 
 | ||||
| 	Mongo struct { | ||||
| 		Schema   string | ||||
| 		User     string | ||||
|  | @ -12,4 +18,23 @@ type Config struct { | |||
| 		Port     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 | ||||
| 	AdTypeOnlyNotAd | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	LastOfTimelineFlag = "NoMoreData" | ||||
| ) | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package domain | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	ers "code.30cm.net/digimon/library-go/errs" | ||||
|  | @ -33,10 +32,25 @@ const ( | |||
| 	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 { | ||||
| 	return ers.NewError(code.CloudEPTweeting, code.DBError, | ||||
| 		ec.ToUint32(), | ||||
| 		fmt.Sprintf("%s", strings.Join(s, " "))) | ||||
| 	return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), strings.Join(s, " ")) | ||||
| } | ||||
| 
 | ||||
| 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}, | ||||
| 			}, | ||||
| 			"failed to del comment").Wrap(err) | ||||
| 
 | ||||
| 		return nil, e | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -41,8 +41,8 @@ func convertToCommentDetailItem(item *model.Comment) *tweeting.CommentDetail { | |||
| 		Uid:          item.UID, | ||||
| 		Content:      item.Content, | ||||
| 		CreatedAt:    item.CreateAt, | ||||
| 		LikeCount:    int64(item.LikeCount), | ||||
| 		DislikeCount: int64(item.DisLikeCount), | ||||
| 		LikeCount:    item.LikeCount, | ||||
| 		DislikeCount: item.DisLikeCount, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -82,6 +82,7 @@ func (l *GetCommentsLogic) GetComments(in *tweeting.GetCommentsReq) (*tweeting.G | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to find comment").Wrap(err) | ||||
| 
 | ||||
| 		return nil, e | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,16 +27,16 @@ func NewUpdateCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upd | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| type checkCommentId struct { | ||||
| 	CommentId string `validate:"required"` | ||||
| type checkCommentID struct { | ||||
| 	CommentID string `validate:"required"` | ||||
| 	Content   string `json:"content,omitempty" validate:"lte=500"` | ||||
| } | ||||
| 
 | ||||
| // UpdateComment 更新評論
 | ||||
| func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*tweeting.OKResp, error) { | ||||
| 	// 驗證資料
 | ||||
| 	if err := l.svcCtx.Validate.ValidateAll(&checkCommentId{ | ||||
| 		CommentId: in.GetCommentId(), | ||||
| 	if err := l.svcCtx.Validate.ValidateAll(&checkCommentID{ | ||||
| 		CommentID: in.GetCommentId(), | ||||
| 		Content:   in.GetContent(), | ||||
| 	}); err != nil { | ||||
| 		// 錯誤代碼 05-011-00
 | ||||
|  | @ -75,6 +75,7 @@ func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*twee | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to update comment:", in.CommentId).Wrap(err) | ||||
| 
 | ||||
| 		return nil, e | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ type newTweetingReq struct { | |||
| 	UID      string   `json:"uid" validate:"required"` | ||||
| 	Content  string   `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內
 | ||||
| 	Tags     []string `json:"tags"` | ||||
| 	MediaUrl []string `json:"media_url"` | ||||
| 	MediaURL []string `json:"media_url"` | ||||
| 	IsAd     bool     `json:"is_ad"` // default false
 | ||||
| } | ||||
| 
 | ||||
|  | @ -85,6 +85,7 @@ func (l *CreatePostLogic) CreatePost(in *tweeting.NewPostReq) (*tweeting.PostRes | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to add new post").Wrap(err) | ||||
| 
 | ||||
| 		return nil, e | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKR | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to del post").Wrap(err) | ||||
| 
 | ||||
| 		return nil, e | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,8 +54,8 @@ func convertToPostDetailItem(item *model.Post) *tweeting.PostDetailItem { | |||
| 		IsAd:         item.IsAd, | ||||
| 		CreatedAt:    item.CreateAt, | ||||
| 		UpdateAt:     item.UpdateAt, | ||||
| 		LikeCount:    int64(item.Like), | ||||
| 		DislikeCount: int64(item.DisLike), | ||||
| 		LikeCount:    item.Like, | ||||
| 		DislikeCount: item.DisLike, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -102,6 +102,7 @@ func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPo | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to find posts").Wrap(err) | ||||
| 
 | ||||
| 		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"` | ||||
| 	Content string `json:"content,omitempty" validate:"lte=500"` | ||||
| } | ||||
|  | @ -36,7 +36,7 @@ type checkPostId struct { | |||
| // UpdatePost 更新貼文
 | ||||
| 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(), | ||||
| 		Content: in.GetContent(), | ||||
| 	}); err != nil { | ||||
|  | @ -53,7 +53,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe | |||
| 	update.ID = oid | ||||
| 	update.Tags = in.GetTags() | ||||
| 	// 將 Media 存入
 | ||||
| 	var media []model.Media | ||||
| 	media := make([]model.Media, 0, len(in.GetMedia())) | ||||
| 	for _, item := range in.GetMedia() { | ||||
| 		media = append(media, model.Media{ | ||||
| 			Links: item.Url, | ||||
|  | @ -88,6 +88,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe | |||
| 				{Key: "err", Value: err}, | ||||
| 			}, | ||||
| 			"failed to update post", in.PostId).Wrap(err) | ||||
| 
 | ||||
| 		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" | ||||
| ) | ||||
| 
 | ||||
| func mustMongoConnectUrl(c config.Config) string { | ||||
| func mustMongoConnectURL(c config.Config) string { | ||||
| 	return fmt.Sprintf("%s://%s:%s", | ||||
| 		c.Mongo.Schema, | ||||
| 		c.Mongo.Host, | ||||
|  | @ -18,10 +18,12 @@ func mustMongoConnectUrl(c config.Config) string { | |||
| 
 | ||||
| func MustPostModel(c config.Config) model.PostModel { | ||||
| 	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 { | ||||
| 	m := model.Comment{} | ||||
| 	return model.NewCommentModel(mustMongoConnectUrl(c), c.Mongo.Database, m.CollectionName()) | ||||
| 
 | ||||
| 	return model.NewCommentModel(mustMongoConnectURL(c), c.Mongo.Database, m.CollectionName()) | ||||
| } | ||||
|  |  | |||
|  | @ -2,7 +2,12 @@ package svc | |||
| 
 | ||||
| import ( | ||||
| 	"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" | ||||
| 	"app-cloudep-tweeting-service/internal/repository" | ||||
| 
 | ||||
| 	"github.com/zeromicro/go-zero/core/stores/redis" | ||||
| 
 | ||||
| 	vi "code.30cm.net/digimon/library-go/validator" | ||||
| ) | ||||
|  | @ -10,16 +15,39 @@ import ( | |||
| type ServiceContext struct { | ||||
| 	Config                  config.Config | ||||
| 	Validate                vi.Validate | ||||
| 
 | ||||
| 	PostModel               model.PostModel | ||||
| 	CommentModel            model.CommentModel | ||||
| 	TimelineRepo            domainRepo.TimelineRepository | ||||
| 	SocialNetworkRepository domainRepo.SocialNetworkRepository | ||||
| } | ||||
| 
 | ||||
| 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{ | ||||
| 		Config:       c, | ||||
| 		Validate:     vi.MustValidator(), | ||||
| 		PostModel:    MustPostModel(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 | ||||
| 
 | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"app-cloudep-tweeting-service/gen_result/pb/tweeting" | ||||
| 	"app-cloudep-tweeting-service/internal/config" | ||||
| 	commentserviceServer "app-cloudep-tweeting-service/internal/server/commentservice" | ||||
| 	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" | ||||
| 	"flag" | ||||
| 	"log" | ||||
| 
 | ||||
| 	"github.com/zeromicro/go-zero/core/conf" | ||||
| 	"github.com/zeromicro/go-zero/core/service" | ||||
|  | @ -27,6 +29,9 @@ func main() { | |||
| 
 | ||||
| 	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { | ||||
| 		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 { | ||||
| 			reflection.Register(grpcServer) | ||||
|  | @ -34,6 +39,6 @@ func main() { | |||
| 	}) | ||||
| 	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() | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue