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