Compare commits

..

16 Commits

Author SHA1 Message Date
王性驊 4f3d9c52c8 Merge pull request 'fix scope' (#4) from fix/scope into main
Reviewed-on: #4
2024-09-03 11:55:58 +00:00
daniel.w c1d9dc0e47 fix scope 2024-09-03 19:55:27 +08:00
王性驊 7cbee88aec Merge pull request 'feature/fanout' (#3) from feature/fanout into main
Reviewed-on: #3
2024-09-03 11:45:03 +00:00
daniel.w a72c013982 add timeline test 2024-09-03 19:44:07 +08:00
daniel.w 184586cdff add social network test 2024-09-03 19:20:10 +08:00
daniel.w 647e645d61 fix test 2024-09-03 17:52:44 +08:00
daniel.w ba437e8352 fix golint 2024-09-03 17:47:34 +08:00
daniel.w e352f006b0 fix social network server 2024-09-03 17:11:12 +08:00
daniel.w 350bbf086c add social_network repo 2024-09-03 09:17:50 +08:00
daniel.w 5599f88787 add user to nerwork 2024-09-02 21:43:03 +08:00
daniel.w f135327805 fix format 2024-09-01 22:47:24 +08:00
daniel.w 1a770b5b41 fix format 2024-09-01 21:50:17 +08:00
daniel.w d82f5c3337 add timeline service 2024-09-01 21:49:28 +08:00
daniel.w c9a0926495 add timeline service 2024-08-31 08:59:08 +08:00
daniel.w 68cf992930 add mongo index 2024-08-30 15:12:53 +08:00
王性驊 5d22b9c575 feature/post_v2 (#2)
Co-authored-by: daniel.w <daniel.w@intteam.net>
Reviewed-on: #2
2024-08-30 07:08:43 +00:00
101 changed files with 6374 additions and 1480 deletions

View File

@ -117,6 +117,14 @@ issues:
- gocognit
- contextcheck
exclude-dirs:
- internal/model
exclude-files:
- .*_test.go
linters-settings:
gci:
sections:

View File

@ -18,6 +18,7 @@ test: # 進行測試
fmt: # 格式優化
$(GOFMT) -w $(GOFILES)
goimports -w ./
golangci-lint run
.PHONY: gen-rpc
gen-rpc: # 建立 rpc code
@ -49,9 +50,23 @@ build-docker:
gen-mongo-model: # 建立 rpc 資料庫
# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是
goctl model mongo -c no -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -c no -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
@echo "Generate mongo model files successfully"
.PHONY: mock-gen
mock-gen: # 建立 mock 資料
mockgen -source=./internal/model/mongo/post_model_gen.go -destination=./internal/mock/model/post_model_gen.go -package=mock
mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock
mockgen -source=./internal/model/mongo/comment_model_gen.go -destination=./internal/mock/model/comment_model_gen.go -package=mock
mockgen -source=./internal/model/mongo/comment_model.go -destination=./internal/mock/model/comment_model.go -package=mock
mockgen -source=./internal/domain/repository/social_network.go -destination=./internal/mock/repository/social_network.go -package=mock
mockgen -source=./internal/domain/repository/timeline.go -destination=./internal/mock/repository/timeline.go -package=mock
@echo "Generate mock files successfully"
.PHONY: migrate-database
migrate-database:
migrate -source file://generate/database/migrations/mongodb -database 'mongodb://127.0.0.1:27017/digimon_tweeting' up

View File

@ -5,20 +5,6 @@ Etcd:
- 127.0.0.1:2379
Key: tweeting.rpc
Cache:
- Host: 127.0.0.1:7001
type: cluster
- Host: 127.0.0.1:7002
type: cluster
- Host: 127.0.0.1:7003
type: cluster
- Host: 127.0.0.1:7004
type: cluster
- Host: 127.0.0.1:7005
type: cluster
- Host: 127.0.0.1:7006
type: cluster
Mongo:
Schema: mongodb
Host: 127.0.0.1
@ -26,3 +12,20 @@ Mongo:
Password: ""
Port: "27017"
Database: digimon_tweeting
TimelineSetting:
Expire: 86400
MaxLength: 1000
RedisCluster:
Host: 127.0.0.1:7001
Type: cluster
Neo4J:
URI: bolt://localhost:7687
Username: neo4j
Password: yyyytttt
MaxConnectionPoolSize: 20
MaxConnectionLifetime: 200s
ConnectionTimeout : 200s
LogLevel : debug

View File

@ -0,0 +1,9 @@
use digimon_tweeting;
db.post.createIndex({ "uid": 1});
db.post.createIndex({ "status": 1});
db.post.createIndex({ "is_ad": 1});
db.post.createIndex({ "createAt": 1 });
db.post.createIndex({ "uid": 1,"status": 1, "createAt": 1 });
db.post.createIndex({ "uid": 1, "createAt": 1 });
// TODO 看是否有要刪除過多的索引,要在測試一下

View File

@ -1,5 +0,0 @@
use digimon_tweeting;
db.post.createIndex({ "uid": 1, "create_time": 1 });
db.post.createIndex({ "is_ad": 1, "create_time": 1 });
db.post.createIndex({ "create_time": 1 });

View File

@ -0,0 +1,2 @@
use digimon_tweeting;
db.comment.createIndex({ "post_id": 1,"createAt":1});

View File

@ -1,5 +0,0 @@
db.post_likes.createIndex(
{ "target_id": 1, "uid": 1, "type": 1 },
{ unique: true }
);
db.post_likes.createIndex({ "create_time": 1 });

View File

@ -0,0 +1,5 @@
// 企業版才能用,社群版只能用預設的
CREATE DATABASE relation;
// 創建 User 節點 UID 是唯一鍵
CREATE CONSTRAINT FOR (u:User) REQUIRE u.uid IS UNIQUE

View File

@ -2,26 +2,25 @@ syntax = "proto3";
package tweeting;
option go_package = "./tweeting";
//
// ========== ===========
message OKResp {}
//
message NoneReq {}
//
message Pager {
message Pager
{
int64 total = 1; //
int64 size = 2; //
int64 index = 3; //
}
message Media{
string type =1;
string url =2;
}
// ========== ===========
//
message NewPostReq {
// ------ NewPost --------
message NewPostReq
{
string uid = 1; // ID
string content = 2; //
repeated string tags = 3; //
@ -29,18 +28,30 @@ message NewPostReq {
bool is_ad = 5; //
}
message Media
{
string type = 1;
string url = 2;
}
//
message PostResp {
message PostResp
{
string post_id = 1; // ID
}
// ------ DeletePost ------
//
message DeletePostsReq {
message DeletePostsReq
{
repeated string post_id = 1; // ID
}
// ------ UpdatePost ------
//
message UpdatePostReq {
message UpdatePostReq
{
string post_id = 1; // ID
repeated string tags = 2; //
repeated Media media = 3; // Media URL
@ -49,17 +60,20 @@ message UpdatePostReq {
optional int64 dislike_count = 6; //
}
// ------ListPosts ------
//
message QueryPostsReq {
message QueryPostsReq
{
repeated string uid = 1; // ID篩選貼文
repeated string id = 2; // ID篩選貼文
optional int32 only_ads = 4; // 0 1 2
int32 page_index = 5; //
int32 page_size = 6; //
repeated string post_id = 2; // ID篩選貼文
optional int32 only_ads = 3; // 0 1 2
int32 page_index = 4; //
int32 page_size = 5; //
}
//
message PostDetailItem {
message PostDetailItem
{
string post_id = 1; // ID
string uid = 2; // ID
string content = 3; //
@ -73,84 +87,61 @@ message PostDetailItem {
}
//
message ListPostsResp {
message ListPostsResp
{
repeated PostDetailItem posts = 1; //
Pager page = 2;
}
// /
message LikeReq {
string target_id = 1; // IDID或評論ID
string uid = 2; // ID
int64 like_type = 3; //
message ModifyLikeDislikeCountReq
{
string post_id = 1; // ID
int64 reaction_type = 2; //
bool is_increment = 3; // true false
int64 count = 4; //
}
message GetLikeStatusReq{
string uid = 1; // ID
repeated string target_id = 2; // IDID或評論ID
int64 like_type = 3; //
// ========== () ==========
service PostService
{
// CreatePost
rpc CreatePost(NewPostReq) returns (PostResp);
// DeletePost
rpc DeletePost(DeletePostsReq) returns (OKResp);
// UpdatePost
rpc UpdatePost(UpdatePostReq) returns (OKResp);
// ListPosts
rpc ListPosts(QueryPostsReq) returns (ListPostsResp);
}
message GetLikeStatusItem{
string target_id = 1; // IDID或評論ID
bool status = 2; //
}
// =================================================================================================
message GetLikeStatusResp{
repeated GetLikeStatusItem data = 1; // IDID或評論ID
}
// /
message LikeItem {
string target_id = 1; // IDID或評論ID
string uid = 2; // ID
int64 like_type = 3; //
}
// /
message LikeListReq {
string target_id = 1; // IDID或評論ID
int64 like_type = 2; //
int64 page_index = 3; //
int64 page_size = 4; //
}
// /
message LikeListResp {
repeated LikeItem list = 1; // /
Pager page = 2;
}
// /
message LikeCountReq {
string target_id = 1; // IDID或評論ID
int64 like_type = 2; //
}
// /
message LikeCountResp {
int64 count = 1; //
}
//
message CommentPostReq {
// ------------ ------------
message CommentPostReq
{
string post_id = 1; // ID
int64 user_id = 2; // ID
string uid = 2; // ID
string content = 3; //
}
//
message GetCommentsReq {
message CommentPostResp
{
string comment_id = 1; // ID
}
// ------------ ------------
message GetCommentsReq
{
string post_id = 1; // ID
int32 page_index = 2; //
int32 page_size = 3; //
}
//
message CommentDetail {
message CommentDetail
{
string comment_id = 1; // ID
int64 user_id = 2; // ID
string uid = 2; // ID
string content = 3; //
int64 created_at = 4; //
int64 like_count = 5; //
@ -158,74 +149,150 @@ message CommentDetail {
}
//
message GetCommentsResp {
message GetCommentsResp
{
repeated CommentDetail comments = 1; //
Pager page = 2;
}
//
message DeleteCommentReq {
string comment_id = 1; // ID
// ------------ ------------
message DeleteCommentReq
{
repeated string comment_id = 1; // ID
}
//
message UpdateCommentReq {
message UpdateCommentReq
{
string comment_id = 1; // ID
string content = 2; //
}
message PostReactionActionResp {
string PostID =1; // ID
int64 reaction_type = 2; //
bool is_increment = 3; // true false
}
message IncDecLikeDislikeCountReq {
string PostID =1; // ID
int64 reaction_type = 2; //
bool is_increment = 3; // true false
}
//
service PostService {
// NewPost
rpc NewPost(NewPostReq) returns(PostResp);
// DeletePost
rpc DeletePost(DeletePostsReq) returns (OKResp);
// UpdatePost
rpc UpdatePost(UpdatePostReq) returns (OKResp);
// ListPosts
rpc ListPosts(QueryPostsReq) returns (ListPostsResp);
// IncDecLikeDislikeCount
rpc IncDecLikeDislikeCount(IncDecLikeDislikeCountReq) returns (OKResp);
// Like /
rpc Like(LikeReq) returns (PostReactionActionResp);
// GetLikeStatus /
rpc GetLikeStatus(GetLikeStatusReq) returns (GetLikeStatusResp);
// LikeList /
rpc LikeList(LikeListReq) returns (LikeListResp);
// CountLike /
rpc CountLike(LikeCountReq) returns (LikeCountResp);
optional int64 like_count = 3; //
optional int64 dislike_count = 4; //
}
//
service CommentService {
service CommentService
{
// NewComment
rpc NewComment(CommentPostReq) returns (OKResp);
rpc NewComment(CommentPostReq) returns (CommentPostResp);
// GetComments
rpc GetComments(GetCommentsReq) returns (GetCommentsResp);
// DeleteComment
rpc DeleteComment(DeleteCommentReq) returns (OKResp);
// UpdateComment
rpc UpdateComment(UpdateCommentReq) returns (OKResp);
// LikeComment /
rpc LikeComment(LikeReq) returns (OKResp);
// GetLikeStatus /
rpc GetLikeStatus(LikeReq) returns (OKResp);
// LikeList /
rpc LikeList(LikeListReq) returns (LikeListResp);
// CountLike /
rpc CountLike(LikeCountReq) returns (LikeCountResp);
}
// ========== 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);
}

42
go.mod
View File

@ -5,25 +5,44 @@ go 1.22.3
require (
code.30cm.net/digimon/library-go/errs v1.2.4
code.30cm.net/digimon/library-go/validator v1.0.0
github.com/alicebob/miniredis/v2 v2.33.0
github.com/neo4j/neo4j-go-driver/v5 v5.24.0
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.33.0
github.com/zeromicro/go-zero v1.7.0
go.mongodb.org/mongo-driver v1.16.1
go.mongodb.org/mongo-driver v1.16.0
go.uber.org/mock v0.4.0
google.golang.org/grpc v1.66.0
google.golang.org/protobuf v1.34.2
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
@ -43,28 +62,49 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/redis/go-redis/v9 v9.6.1 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect

View File

@ -1,12 +1,15 @@
package config
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"time"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
zrpc.RpcServerConf
Mongo struct {
Schema string
User string
@ -16,6 +19,22 @@ type Config struct {
Database string
}
// 快取
Cache cache.CacheConf
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
}
}

View File

@ -12,13 +12,6 @@ const (
AdTypeOnlyNotAd
)
type LikeType int8
func (l LikeType) ToInt8() int8 {
return int8(l)
}
const (
LikeTypeLike LikeType = iota + 1 // 按揍
LikeTypeDisLike
LastOfTimelineFlag = "NoMoreData"
)

View File

@ -1,10 +1,9 @@
package domain
import (
"fmt"
"strings"
"code.30cm.net/digimon/library-go/errs"
ers "code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"github.com/zeromicro/go-zero/core/logx"
)
@ -15,25 +14,49 @@ func (e ErrorCode) ToUint32() uint32 {
return uint32(e)
}
// Error Code 統一這邊改
const (
_ = iota
PostMongoErrorCode ErrorCode = iota
CreatePostError
DelPostError
UpdatePostError
ListPostError
)
// PostMongoError ...
func PostMongoError(s ...string) *errs.LibError {
return errs.NewError(code.CloudEPTweeting, code.DBError,
PostMongoErrorCode.ToUint32(),
fmt.Sprintf("%s", strings.Join(s, " ")))
const (
CommentFoundErrorCode ErrorCode = iota + 10
CommentInsertErrorCode
CommentDeleteErrorCode
CommentUpdateErrorCode
CommentListErrorCode
)
const (
AddTimeLineErrorCode ErrorCode = iota + 20
FetchTimeLineErrorCode
ClearNoMoreDataErrorCode
HasNoMoreDataErrorCode
SetNoMoreDataErrorCode
)
const (
MarkRelationErrorCode ErrorCode = iota + 30
GetFollowerErrorCode
GetFollowerCountErrorCode
GetFolloweeErrorCode
GetFolloweeCountErrorCode
RemoveRelationErrorCode
)
func CommentError(ec ErrorCode, s ...string) *ers.LibError {
return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), strings.Join(s, " "))
}
// PostMongoErrorL logs error message and returns Err
func PostMongoErrorL(l logx.Logger, filed []logx.LogField, s ...string) *errs.LibError {
e := PostMongoError(s...)
if filed != nil || len(filed) >= 0 {
func CommentErrorL(ec ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError {
e := CommentError(ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
}
l.WithCallerSkip(1).Error(e.Error())
return e
}

19
internal/domain/redis.go Normal file
View File

@ -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"
)

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}()
}
})
}
}

View File

@ -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
}
}
}

View File

@ -1,31 +0,0 @@
package commentservicelogic
import (
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type CountLikeLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewCountLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CountLikeLogic {
return &CountLikeLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// CountLike 取得讚/不讚評論數量
func (l *CountLikeLogic) CountLike(in *tweeting.LikeCountReq) (*tweeting.LikeCountResp, error) {
// todo: add your logic here and delete this line
return &tweeting.LikeCountResp{}, nil
}

View File

@ -1,6 +1,7 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
@ -25,7 +26,20 @@ func NewDeleteCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Del
// DeleteComment 刪除評論
func (l *DeleteCommentLogic) DeleteComment(in *tweeting.DeleteCommentReq) (*tweeting.OKResp, error) {
// todo: add your logic here and delete this line
_, err := l.svcCtx.CommentModel.DeleteMany(l.ctx, in.GetCommentId()...)
if err != nil {
e := domain.CommentErrorL(
domain.CommentDeleteErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "CommentModel.DeleteMany"},
{Key: "err", Value: err},
},
"failed to del comment").Wrap(err)
return nil, e
}
return &tweeting.OKResp{}, nil
}

View File

@ -0,0 +1,85 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
)
func TestDeleteComment(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockCommentModel := mockmodel.NewMockCommentModel(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
CommentModel: mockCommentModel,
}
// 測試數據
commentReq := &tweeting.DeleteCommentReq{
CommentId: []string{"12345", "67890"},
}
// 測試數據集
tests := []struct {
name string
input *tweeting.DeleteCommentReq
prepare func()
expectErr bool
}{
{
name: "成功刪除評論",
input: commentReq,
prepare: func() {
// 模擬 DeleteMany 成功
mockCommentModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(2), nil).Times(1)
},
expectErr: false,
},
{
name: "刪除評論失敗",
input: commentReq,
prepare: func() {
// 模擬 DeleteMany 失敗
mockCommentModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(0), errors.New("delete failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 DeleteCommentLogic
logic := DeleteCommentLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 DeleteComment
resp, err := logic.DeleteComment(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
assert.Nil(t, resp)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
}
})
}
}

View File

@ -1,8 +1,12 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
@ -23,9 +27,78 @@ func NewGetCommentsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCo
}
}
// GetComments 查詢評論
func (l *GetCommentsLogic) GetComments(in *tweeting.GetCommentsReq) (*tweeting.GetCommentsResp, error) {
// todo: add your logic here and delete this line
return &tweeting.GetCommentsResp{}, nil
// 只列出要驗證的資料
type listReq struct {
PostID string `json:"post_id" validate:"required"`
PageSize int64 `json:"page_size" validate:"required"`
PageIndex int64 `json:"page_index" validate:"required"`
}
// 將單個 Post 轉換為 PostDetailItem
func convertToCommentDetailItem(item *model.Comment) *tweeting.CommentDetail {
return &tweeting.CommentDetail{
CommentId: item.ID.Hex(),
Uid: item.UID,
Content: item.Content,
CreatedAt: item.CreateAt,
LikeCount: item.LikeCount,
DislikeCount: item.DisLikeCount,
}
}
// GetComments 查詢評論
// 目前應該是沒有需求是要看 uid 在哪裡留過言,如果未來業務邏輯有再新增
func (l *GetCommentsLogic) GetComments(in *tweeting.GetCommentsReq) (*tweeting.GetCommentsResp, error) {
// 將 PageSize 和 PageIndex 提前轉換為 int64
pageSize := int64(in.GetPageSize())
pageIndex := int64(in.GetPageIndex())
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&listReq{
PageSize: pageSize,
PageIndex: pageIndex,
PostID: in.GetPostId(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// 構建查詢條件
query := &model.QueryCommentModelReq{
PostID: in.GetPostId(),
PageSize: pageSize,
PageIndex: pageIndex,
}
// 執行查詢
find, count, err := l.svcCtx.CommentModel.Find(l.ctx, query)
if err != nil {
e := domain.CommentErrorL(
domain.CommentListErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "query", Value: query},
{Key: "func", Value: "CommentModel.Find"},
{Key: "err", Value: err},
},
"failed to find comment").Wrap(err)
return nil, e
}
// 將查詢結果轉換為 API 回應格式
result := make([]*tweeting.CommentDetail, 0, count)
for _, item := range find {
result = append(result, convertToCommentDetailItem(item))
}
// 返回結果
return &tweeting.GetCommentsResp{
Comments: result,
Page: &tweeting.Pager{
Total: count,
Index: pageIndex,
Size: pageSize,
},
}, nil
}

View File

@ -0,0 +1,120 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
)
func TestGetComments(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockCommentModel := mockmodel.NewMockCommentModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
CommentModel: mockCommentModel,
Validate: mockValidate,
}
// 測試數據
getCommentsReq := &tweeting.GetCommentsReq{
PostId: "12345",
PageSize: 10,
PageIndex: 1,
}
mockComments := []*model.Comment{
{
ID: primitive.NewObjectID(),
PostID: "12345",
UID: "54321",
Content: "This is a comment",
LikeCount: 10,
DisLikeCount: 2,
},
{
ID: primitive.NewObjectID(),
PostID: "12345",
UID: "67890",
Content: "This is another comment",
LikeCount: 5,
DisLikeCount: 1,
},
}
// 測試數據集
tests := []struct {
name string
input *tweeting.GetCommentsReq
prepare func()
expectErr bool
}{
{
name: "成功查詢評論",
input: getCommentsReq,
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockCommentModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(mockComments, int64(len(mockComments)), nil).Times(1)
},
expectErr: false,
},
{
name: "查詢評論失敗",
input: getCommentsReq,
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockCommentModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("find failed")).Times(1)
},
expectErr: true,
},
{
name: "驗證失敗",
input: getCommentsReq,
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 GetCommentsLogic
logic := GetCommentsLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 GetComments
resp, err := logic.GetComments(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
assert.Nil(t, resp)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, resp.Comments, len(mockComments))
}
})
}
}

View File

@ -1,31 +0,0 @@
package commentservicelogic
import (
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLikeStatusLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetLikeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLikeStatusLogic {
return &GetLikeStatusLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// GetLikeStatus 取得讚/不讚評論狀態
func (l *GetLikeStatusLogic) GetLikeStatus(in *tweeting.LikeReq) (*tweeting.OKResp, error) {
// todo: add your logic here and delete this line
return &tweeting.OKResp{}, nil
}

View File

@ -1,31 +0,0 @@
package commentservicelogic
import (
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type LikeCommentLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLikeCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeCommentLogic {
return &LikeCommentLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// LikeComment 點讚/取消讚 評論
func (l *LikeCommentLogic) LikeComment(in *tweeting.LikeReq) (*tweeting.OKResp, error) {
// todo: add your logic here and delete this line
return &tweeting.OKResp{}, nil
}

View File

@ -1,31 +0,0 @@
package commentservicelogic
import (
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type LikeListLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLikeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeListLogic {
return &LikeListLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// LikeList 取得讚/不讚評論列表
func (l *LikeListLogic) LikeList(in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) {
// todo: add your logic here and delete this line
return &tweeting.LikeListResp{}, nil
}

View File

@ -1,10 +1,14 @@
package commentservicelogic
import (
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
ers "code.30cm.net/digimon/library-go/errs"
"github.com/zeromicro/go-zero/core/logx"
)
@ -23,9 +27,70 @@ func NewNewCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewCom
}
}
// NewComment 發表評論
func (l *NewCommentLogic) NewComment(in *tweeting.CommentPostReq) (*tweeting.OKResp, error) {
// todo: add your logic here and delete this line
return &tweeting.OKResp{}, nil
// 輸入的定義 -> 檢查用
type newCommentReq struct {
UID string `json:"uid" validate:"required"`
Content string `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內
PostID string `json:"post_id" validate:"required"`
}
// NewComment 發表評論
func (l *NewCommentLogic) NewComment(in *tweeting.CommentPostReq) (*tweeting.CommentPostResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&newCommentReq{
UID: in.GetUid(),
Content: in.GetContent(),
PostID: in.GetPostId(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// 檢查是否有這個文章
_, err := l.svcCtx.PostModel.FindOne(l.ctx, in.GetPostId())
if err != nil {
if errors.Is(model.ErrNotFound, err) {
// 錯誤代碼 05-031-00
return nil, ers.ResourceNotFound("failed to find post: ", in.GetPostId())
}
// 錯誤代碼 05-021-10
e := domain.CommentErrorL(
domain.CommentFoundErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.FindOne"},
{Key: "err", Value: err},
},
"failed to find post:", in.GetPostId()).Wrap(err)
return nil, e
}
data := &model.Comment{
PostID: in.GetPostId(),
UID: in.GetUid(),
Content: in.GetContent(),
LikeCount: 0,
DisLikeCount: 0,
}
err = l.svcCtx.CommentModel.Insert(l.ctx, data)
if err != nil {
// 錯誤代碼 05-021-11
e := domain.CommentErrorL(
domain.CommentInsertErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "CommentModel.Insert"},
{Key: "err", Value: err},
},
"failed to insert comment:", in.GetPostId()).Wrap(err)
return nil, e
}
return &tweeting.CommentPostResp{
CommentId: data.ID.Hex(),
}, nil
}

View File

@ -0,0 +1,124 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
)
func TestNewComment(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockPostModel := mockmodel.NewMockPostModel(ctrl)
mockCommentModel := mockmodel.NewMockCommentModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
PostModel: mockPostModel,
CommentModel: mockCommentModel,
Validate: mockValidate,
}
// 測試數據
postID := primitive.NewObjectID().Hex()
commentReq := &tweeting.CommentPostReq{
Uid: "12345",
Content: "This is a comment",
PostId: postID,
}
// 測試數據集
tests := []struct {
name string
input *tweeting.CommentPostReq
prepare func()
expectErr bool
}{
{
name: "成功發表評論",
input: commentReq,
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 FindOne 成功找到文章
mockPostModel.EXPECT().FindOne(gomock.Any(), postID).Return(&model.Post{}, nil).Times(1)
// 模擬 Insert 成功
mockCommentModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
},
expectErr: false,
},
{
name: "驗證失敗",
input: commentReq,
prepare: func() {
// 模擬 Validate 失敗
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1)
},
expectErr: true,
},
{
name: "文章不存在",
input: commentReq,
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 FindOne 找不到文章
mockPostModel.EXPECT().FindOne(gomock.Any(), postID).Return(nil, model.ErrNotFound).Times(1)
},
expectErr: true,
},
{
name: "插入評論失敗",
input: commentReq,
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 FindOne 成功找到文章
mockPostModel.EXPECT().FindOne(gomock.Any(), postID).Return(&model.Post{}, nil).Times(1)
// 模擬 Insert 失敗
mockCommentModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 NewCommentLogic
logic := NewCommentLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 NewComment
resp, err := logic.NewComment(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
assert.Nil(t, resp)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.CommentId)
}
})
}
}

View File

@ -1,10 +1,14 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
ers "code.30cm.net/digimon/library-go/errs"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/zeromicro/go-zero/core/logx"
)
@ -23,9 +27,57 @@ func NewUpdateCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upd
}
}
type checkCommentID struct {
CommentID string `validate:"required"`
Content string `json:"content,omitempty" validate:"lte=500"`
}
// UpdateComment 更新評論
func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*tweeting.OKResp, error) {
// todo: add your logic here and delete this line
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&checkCommentID{
CommentID: in.GetCommentId(),
Content: in.GetContent(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// 沒有就沒有,有就走全覆蓋
update := model.Comment{}
oid, err := primitive.ObjectIDFromHex(in.GetCommentId())
if err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat("failed to get correct comment id")
}
update.ID = oid
update.Content = in.GetContent()
// 因為 0 也有意義,所以如果是真的沒帶進來,用 -1 帶進去表示不作動
if in.LikeCount == nil {
update.LikeCount = -1
} else {
update.LikeCount = in.GetLikeCount()
}
if in.DislikeCount == nil {
update.DisLikeCount = -1
} else {
update.DisLikeCount = in.GetDislikeCount()
}
_, err = l.svcCtx.CommentModel.UpdateOptional(l.ctx, &update)
if err != nil {
// 錯誤代碼 05-021-13
e := domain.CommentErrorL(
domain.CommentUpdateErrorCode,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "CommentModel.UpdateOptional"},
{Key: "err", Value: err},
},
"failed to update comment:", in.CommentId).Wrap(err)
return nil, e
}
return &tweeting.OKResp{}, nil
}

View File

@ -0,0 +1,124 @@
package commentservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/proto"
)
func TestUpdateComment(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockCommentModel := mockmodel.NewMockCommentModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
CommentModel: mockCommentModel,
Validate: mockValidate,
}
// 測試數據
commentID := primitive.NewObjectID().Hex()
updateCommentReq := &tweeting.UpdateCommentReq{
CommentId: commentID,
Content: "Updated content",
LikeCount: proto.Int64(5),
DislikeCount: proto.Int64(2),
}
// 測試數據集
tests := []struct {
name string
input *tweeting.UpdateCommentReq
prepare func()
expectErr bool
}{
{
name: "成功更新評論",
input: updateCommentReq,
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 UpdateOptional 成功
mockCommentModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(&mongo.UpdateResult{
ModifiedCount: 1,
}, nil).Times(1)
},
expectErr: false,
},
{
name: "驗證失敗",
input: updateCommentReq,
prepare: func() {
// 模擬 Validate 失敗
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1)
},
expectErr: true,
},
{
name: "更新評論失敗",
input: updateCommentReq,
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 UpdateOptional 失敗
mockCommentModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(
&mongo.UpdateResult{
ModifiedCount: 0,
}, errors.New("update failed")).Times(1)
},
expectErr: true,
},
{
name: "無效的評論ID",
input: &tweeting.UpdateCommentReq{
CommentId: "invalid_id",
Content: "Updated content",
},
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 UpdateCommentLogic
logic := UpdateCommentLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 UpdateComment
resp, err := logic.UpdateComment(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
assert.Nil(t, resp)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
}
})
}
}

View File

@ -1,59 +0,0 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
"app-cloudep-tweeting-service/internal/svc"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"github.com/zeromicro/go-zero/core/logx"
)
type CountLikeLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewCountLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CountLikeLogic {
return &CountLikeLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
type countLikeReq struct {
PostID string `json:"post_id" validate:"required"` // 貼文的 ID
ReactionType domain.LikeType `json:"reaction_type" validate:"required,oneof=1 2"` // 用戶的反應類型,可能是讚或不讚
}
// CountLike 取得讚/不讚數量
func (l *CountLikeLogic) CountLike(in *tweeting.LikeCountReq) (*tweeting.LikeCountResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&countLikeReq{
PostID: in.GetTargetId(),
ReactionType: domain.LikeType(in.GetLikeType()),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
count, err := l.svcCtx.PostLikeModel.Count(l.ctx, in.GetTargetId(), domain.LikeType(in.GetLikeType()))
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostLikeModel.Count"},
{Key: "err", Value: err},
},
"failed to count like or dislike").Wrap(err)
return nil, e
}
return &tweeting.LikeCountResp{
Count: count,
}, nil
}

View File

@ -1,46 +1,49 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"github.com/zeromicro/go-zero/core/logx"
)
type NewPostLogic struct {
type CreatePostLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewNewPostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewPostLogic {
return &NewPostLogic{
func NewCreatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePostLogic {
return &CreatePostLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// TODO 要調查一下內容如果存 html 是否有需要Encode
// 輸入的定義 -> 檢查用
type newTweetingReq struct {
UID string `json:"uid" validate:"required"`
Content string `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內
Tags []string `json:"tags"`
MediaUrl []string `json:"media_url"`
MediaURL []string `json:"media_url"`
IsAd bool `json:"is_ad"` // default false
}
// NewPost 新增貼文
func (l *NewPostLogic) NewPost(in *tweeting.NewPostReq) (*tweeting.PostResp, error) {
// CreatePost 新增貼文
func (l *CreatePostLogic) CreatePost(in *tweeting.NewPostReq) (*tweeting.PostResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&newTweetingReq{
UID: in.GetUid(),
Content: in.GetContent(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// ============ prepare ============
@ -67,12 +70,14 @@ func (l *NewPostLogic) NewPost(in *tweeting.NewPostReq) (*tweeting.PostResp, err
Type: item.Type,
})
}
tweet.Media = media
tweet.MediaURL = media
}
// ============ insert ============
err := l.svcCtx.PostModel.Insert(l.ctx, tweet)
if err != nil {
e := domain.PostMongoErrorL(
// 錯誤代碼 05-021-02
e := domain.CommentErrorL(
domain.CreatePostError,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
@ -80,6 +85,7 @@ func (l *NewPostLogic) NewPost(in *tweeting.NewPostReq) (*tweeting.PostResp, err
{Key: "err", Value: err},
},
"failed to add new post").Wrap(err)
return nil, e
}

View File

@ -0,0 +1,107 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func TestCreatePost(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockPostModel := mockmodel.NewMockPostModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
PostModel: mockPostModel,
Validate: mockValidate,
}
// 測試數據集
tests := []struct {
name string
input *tweeting.NewPostReq
prepare func()
expectErr bool
}{
{
name: "成功創建貼文",
input: &tweeting.NewPostReq{
Uid: "12345",
Content: "Test content",
IsAd: false,
Tags: []string{"tag1", "tag2"},
Media: []*tweeting.Media{
{
Url: "http://example.com/image.png",
Type: "image",
},
},
},
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockPostModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1)
},
expectErr: false,
},
{
name: "驗證失敗",
input: &tweeting.NewPostReq{
Uid: "",
Content: "",
},
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1)
},
expectErr: true,
},
{
name: "插入貼文失敗",
input: &tweeting.NewPostReq{
Uid: "12345",
Content: "Test content",
IsAd: false,
},
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockPostModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 CreatePostLogic
logic := CreatePostLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 CreatePost
_, err := logic.CreatePost(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -1,11 +1,10 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
"context"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
"app-cloudep-tweeting-service/internal/svc"
"context"
"github.com/zeromicro/go-zero/core/logx"
)
@ -28,14 +27,17 @@ func NewDeletePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete
func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKResp, error) {
_, err := l.svcCtx.PostModel.DeleteMany(l.ctx, in.GetPostId()...)
if err != nil {
e := domain.PostMongoErrorL(
// 錯誤代碼 05-021-03
e := domain.CommentErrorL(
domain.DelPostError,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.DeletePost"},
{Key: "func", Value: "PostModel.DeleteMany"},
{Key: "err", Value: err},
},
"failed to add del post").Wrap(err)
"failed to del post").Wrap(err)
return nil, e
}

View File

@ -0,0 +1,81 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func TestDeletePost(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockPostModel := mockmodel.NewMockPostModel(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
PostModel: mockPostModel,
}
// 測試數據集
tests := []struct {
name string
input *tweeting.DeletePostsReq
prepare func()
expectErr bool
}{
{
name: "成功刪除貼文",
input: &tweeting.DeletePostsReq{
PostId: []string{"12345", "67890"},
},
prepare: func() {
// 模擬 DeleteMany 成功
mockPostModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(2), nil).Times(1)
},
expectErr: false,
},
{
name: "刪除貼文失敗",
input: &tweeting.DeletePostsReq{
PostId: []string{"12345", "67890"},
},
prepare: func() {
// 模擬 DeleteMany 失敗
mockPostModel.EXPECT().DeleteMany(gomock.Any(), "12345", "67890").Return(int64(0), errors.New("delete failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 DeletePostLogic
logic := DeletePostLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 DeletePost
_, err := logic.DeletePost(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -1,76 +0,0 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLikeStatusLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetLikeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLikeStatusLogic {
return &GetLikeStatusLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// 這個人按讚的文章列表輸入UID 以及文章id返回這個人有沒有對這些文章按讚
type getLikeStatusReq struct {
Targets []string `json:"targets" validate:"required"`
LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"`
UID string `json:"uid" validate:"required"`
}
// GetLikeStatus 取得讚/不讚狀態
func (l *GetLikeStatusLogic) GetLikeStatus(in *tweeting.GetLikeStatusReq) (*tweeting.GetLikeStatusResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&getLikeStatusReq{
Targets: in.GetTargetId(),
LikeType: domain.LikeType(in.GetLikeType()),
UID: in.GetUid(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
list, err := l.svcCtx.PostLikeModel.FindUIDPostLikeStatus(l.ctx, &model.QueryUIDPostLikeStatusReq{
Targets: in.GetTargetId(),
LikeType: domain.LikeType(in.GetLikeType()),
UID: in.GetUid(),
})
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.FindUIDPostLikeStatus"},
{Key: "err", Value: err},
},
"failed to find uid post like status list").Wrap(err)
return nil, e
}
var result = make([]*tweeting.GetLikeStatusItem, 0, len(list))
for _, item := range list {
result = append(result, &tweeting.GetLikeStatusItem{
TargetId: item.TargetID,
Status: item.LikeStatus,
})
}
return &tweeting.GetLikeStatusResp{
Data: result,
}, nil
}

View File

@ -1,65 +0,0 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type IncDecLikeDislikeCountLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewIncDecLikeDislikeCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *IncDecLikeDislikeCountLogic {
return &IncDecLikeDislikeCountLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
type postReactionAction struct {
PostID string `json:"post_id" validate:"required"` // 貼文的 ID
ReactionType domain.LikeType `json:"reaction_type" validate:"required,oneof=1 2"` // 用戶的反應類型,可能是讚或不讚
IsIncrement bool `json:"is_increment" validate:"required"` // 表示是否增加true 表示增加false 表示減少)
}
// IncDecLikeDislikeCount 增減數量
func (l *IncDecLikeDislikeCountLogic) IncDecLikeDislikeCount(in *tweeting.IncDecLikeDislikeCountReq) (*tweeting.OKResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&postReactionAction{
PostID: in.GetPostID(),
ReactionType: domain.LikeType(in.GetReactionType()),
IsIncrement: in.GetIsIncrement(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
err := l.svcCtx.PostModel.IncDecLikeDislikeCountLogic(l.ctx, &model.PostReactionAction{
PostID: in.GetPostID(),
ReactionType: domain.LikeType(in.GetReactionType()),
IsIncrement: in.GetIsIncrement(),
})
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.IncDecLikeDislikeCountLogic"},
{Key: "err", Value: err},
},
"failed to inc like or dislike").Wrap(err)
return nil, e
}
return &tweeting.OKResp{}, nil
}

View File

@ -1,86 +0,0 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type LikeListLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLikeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeListLogic {
return &LikeListLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// 換句話說就是對這個文章按讚的人的列表
type likeListReq struct {
Target string `json:"target" validate:"required"`
LikeType domain.LikeType `json:"like_type" validate:"required,oneof=1 2"`
PageSize int64 `json:"page_size" validate:"required"`
PageIndex int64 `json:"page_index" validate:"required"`
}
// LikeList 取得讚/不讚列表
func (l *LikeListLogic) LikeList(in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&likeListReq{
Target: in.TargetId,
LikeType: domain.LikeType(in.GetLikeType()),
PageSize: in.GetPageSize(),
PageIndex: in.GetPageIndex(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
result, total, err := l.svcCtx.PostLikeModel.FindLikeUsers(l.ctx, &model.QueryPostLikeReq{
Target: in.GetTargetId(),
LikeType: domain.LikeType(in.GetLikeType()),
PageSize: in.GetPageSize(),
PageIndex: in.GetPageIndex(),
})
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.LikeDislike"},
{Key: "err", Value: err},
},
"failed to like or dislike").Wrap(err)
return nil, e
}
var list = make([]*tweeting.LikeItem, 0, len(result))
for _, item := range result {
list = append(list, &tweeting.LikeItem{
LikeType: int64(item.Type),
TargetId: item.TargetID,
Uid: item.UID,
})
}
return &tweeting.LikeListResp{
List: list,
Page: &tweeting.Pager{
Size: in.GetPageSize(),
Index: in.GetPageIndex(),
Total: total,
},
}, nil
}

View File

@ -1,69 +0,0 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type LikeLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LikeLogic {
return &LikeLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
type likeReq struct {
Target string `json:"target" validate:"required"`
UID string `json:"uid" validate:"required"`
}
// Like 點讚/取消讚 貼文
func (l *LikeLogic) Like(in *tweeting.LikeReq) (*tweeting.PostReactionActionResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&likeReq{
Target: in.TargetId,
UID: in.GetUid(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
likeResp, err := l.svcCtx.PostLikeModel.LikeDislike(l.ctx, &model.PostLikes{
TargetID: in.GetTargetId(),
UID: in.GetUid(),
Type: int8(in.GetLikeType()),
})
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.LikeDislike"},
{Key: "err", Value: err},
},
"failed to like or dislike").Wrap(err)
return nil, e
}
// 將文章的數量增加或減少的功能是業務邏輯從外面再決定要不要丟MQ 後算,或是怎麼算
return &tweeting.PostReactionActionResp{
PostID: likeResp.PostID,
ReactionType: int64(likeResp.ReactionType),
IsIncrement: likeResp.IsIncrement,
}, nil
}

View File

@ -1,15 +1,16 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/domain"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
ers "code.30cm.net/digimon/library-go/errs"
"google.golang.org/protobuf/proto"
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
"app-cloudep-tweeting-service/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
@ -27,65 +28,24 @@ func NewListPostsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPos
}
}
// 只列出要驗證的資料
type listReq struct {
OnlyAdds int32 `json:"only_adds" validate:"oneof=0 1 2 3"`
PageSize int64 `json:"page_size" validate:"required"`
PageIndex int64 `json:"page_index" validate:"required"`
}
// ListPosts 查詢貼文
func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPostsResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&listReq{
PageSize: int64(in.GetPageSize()),
PageIndex: int64(in.GetPageIndex()),
OnlyAdds: in.GetOnlyAds(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
query := &model.QueryPostModelReq{
UID: in.GetUid(),
Id: in.GetId(),
PageSize: int64(in.GetPageSize()),
PageIndex: int64(in.GetPageIndex()),
}
if in.OnlyAds != nil {
switch in.GetOnlyAds() {
case domain.AdTypeOnlyAd.ToInt32():
query.OnlyAds = proto.Bool(true)
case domain.AdTypeOnlyNotAd.ToInt32():
query.OnlyAds = proto.Bool(false)
default:
}
}
find, count, err := l.svcCtx.PostModel.Find(l.ctx, query)
if err != nil {
e := domain.PostMongoErrorL(
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "query", Value: query},
{Key: "func", Value: "PostModel.Find"},
{Key: "err", Value: err},
},
"failed to add new post").Wrap(err)
return nil, e
}
result := make([]*tweeting.PostDetailItem, 0, count)
for _, item := range find {
media := make([]*tweeting.Media, 0, len(item.Media))
for _, subItem := range item.Media {
// 將單個 Post 轉換為 PostDetailItem
func convertToPostDetailItem(item *model.Post) *tweeting.PostDetailItem {
media := make([]*tweeting.Media, 0, len(item.MediaURL))
for _, subItem := range item.MediaURL {
media = append(media, &tweeting.Media{
Type: subItem.Type,
Url: subItem.Links,
})
}
result = append(result, &tweeting.PostDetailItem{
return &tweeting.PostDetailItem{
PostId: item.ID.Hex(),
Uid: item.UID,
Content: item.Content,
@ -94,17 +54,71 @@ func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPo
IsAd: item.IsAd,
CreatedAt: item.CreateAt,
UpdateAt: item.UpdateAt,
LikeCount: int64(item.Like),
DislikeCount: int64(item.DisLike),
})
LikeCount: item.Like,
DislikeCount: item.DisLike,
}
}
// ListPosts 查詢貼文 -> 主流程
func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPostsResp, error) {
// 將 PageSize 和 PageIndex 提前轉換為 int64
pageSize := int64(in.GetPageSize())
pageIndex := int64(in.GetPageIndex())
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&listReq{
PageSize: pageSize,
PageIndex: pageIndex,
OnlyAdds: in.GetOnlyAds(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// 構建查詢條件
query := &model.QueryPostModelReq{
UID: in.GetUid(),
Id: in.GetPostId(),
PageSize: pageSize,
PageIndex: pageIndex,
}
// 處理 OnlyAds 條件
if in.OnlyAds != nil {
onlyAds := in.GetOnlyAds()
query.OnlyAds = proto.Bool(onlyAds == domain.AdTypeOnlyAd.ToInt32())
}
// 執行查詢
find, count, err := l.svcCtx.PostModel.Find(l.ctx, query)
if err != nil {
// 錯誤代碼 05-021-05
e := domain.CommentErrorL(
domain.ListPostError,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "query", Value: query},
{Key: "func", Value: "PostModel.Find"},
{Key: "err", Value: err},
},
"failed to find posts").Wrap(err)
return nil, e
}
// 將查詢結果轉換為 API 回應格式
result := make([]*tweeting.PostDetailItem, 0, count)
for _, item := range find {
result = append(result, convertToPostDetailItem(item))
}
// 返回結果
return &tweeting.ListPostsResp{
Posts: result,
Page: &tweeting.Pager{
Total: count,
Index: int64(in.GetPageIndex()),
Size: int64(in.GetPageSize()),
Index: pageIndex,
Size: pageSize,
},
}, nil
}

View File

@ -0,0 +1,164 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
model "app-cloudep-tweeting-service/internal/model/mongo"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"testing"
"time"
"google.golang.org/protobuf/proto"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
)
func TestConvertToPostDetailItem(t *testing.T) {
// 構建測試數據
postID := primitive.NewObjectID()
post := &model.Post{
ID: postID,
UID: "12345",
Content: "Test Content",
Tags: []string{"tag1", "tag2"},
MediaURL: []model.Media{
{Type: "image", Links: "http://example.com/image.png"},
{Type: "video", Links: "http://example.com/video.mp4"},
},
IsAd: true,
CreateAt: time.Now().Unix(),
UpdateAt: time.Now().Unix(),
Like: 10,
DisLike: 2,
}
// 執行轉換
result := convertToPostDetailItem(post)
// 驗證結果
assert.Equal(t, postID.Hex(), result.PostId)
assert.Equal(t, "12345", result.Uid)
assert.Equal(t, "Test Content", result.Content)
assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
assert.Equal(t, true, result.IsAd)
assert.Equal(t, int64(10), result.LikeCount)
assert.Equal(t, int64(2), result.DislikeCount)
// 驗證 Media 的轉換
assert.Len(t, result.Media, 2)
assert.Equal(t, "image", result.Media[0].Type)
assert.Equal(t, "http://example.com/image.png", result.Media[0].Url)
assert.Equal(t, "video", result.Media[1].Type)
assert.Equal(t, "http://example.com/video.mp4", result.Media[1].Url)
}
func TestListPosts(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockPostModel := mockmodel.NewMockPostModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
PostModel: mockPostModel,
Validate: mockValidate,
}
// 構建測試數據
queryReq := &tweeting.QueryPostsReq{
PageSize: 10,
PageIndex: 1,
OnlyAds: proto.Int32(1),
}
mockPosts := []*model.Post{
{
ID: primitive.NewObjectID(),
UID: "12345",
Content: "Test Content 1",
Tags: []string{"tag1", "tag2"},
MediaURL: []model.Media{
{Type: "image", Links: "http://example.com/image1.png"},
},
IsAd: false,
CreateAt: time.Now().Unix(),
UpdateAt: time.Now().Unix(),
Like: 5,
DisLike: 1,
},
{
ID: primitive.NewObjectID(),
UID: "67890",
Content: "Test Content 2",
Tags: []string{"tag3", "tag4"},
MediaURL: []model.Media{
{Type: "video", Links: "http://example.com/video1.mp4"},
},
IsAd: true,
CreateAt: time.Now().Unix(),
UpdateAt: time.Now().Unix(),
Like: 3,
DisLike: 0,
},
}
// 測試數據集
tests := []struct {
name string
input *tweeting.QueryPostsReq
prepare func()
expectErr bool
}{
{
name: "成功查詢貼文",
input: queryReq,
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockPostModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(mockPosts, int64(len(mockPosts)), nil).Times(1)
},
expectErr: false,
},
{
name: "查詢貼文失敗",
input: queryReq,
prepare: func() {
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
mockPostModel.EXPECT().Find(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("find failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 ListPostsLogic
logic := ListPostsLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 ListPosts
resp, err := logic.ListPosts(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, resp.Posts, len(mockPosts))
}
})
}
}

View File

@ -28,7 +28,7 @@ func NewUpdatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update
}
}
type checkPostId struct {
type checkPostID struct {
PostID string `validate:"required"`
Content string `json:"content,omitempty" validate:"lte=500"`
}
@ -36,43 +36,59 @@ type checkPostId struct {
// UpdatePost 更新貼文
func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) {
// 驗證資料
if err := l.svcCtx.Validate.ValidateAll(&checkPostId{
if err := l.svcCtx.Validate.ValidateAll(&checkPostID{
PostID: in.GetPostId(),
Content: in.GetContent(),
}); err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat(err.Error())
}
// 沒有就沒有,有就走全覆蓋
update := model.Post{}
oid, err := primitive.ObjectIDFromHex(in.GetPostId())
if err != nil {
// 錯誤代碼 05-011-00
return nil, ers.InvalidFormat("failed to get correct post id")
}
update.ID = oid
update.Tags = in.GetTags()
// 將 Media 存入
var media []model.Media
media := make([]model.Media, 0, len(in.GetMedia()))
for _, item := range in.GetMedia() {
media = append(media, model.Media{
Links: item.Url,
Type: item.Type,
})
}
update.Media = media
update.MediaURL = media
update.Content = in.GetContent()
update.Like = uint64(in.GetLikeCount())
update.DisLike = uint64(in.GetDislikeCount())
// 因為 0 也有意義,所以如果是真的沒帶進來,用 -1 帶進去表示不作動
if in.LikeCount == nil {
update.Like = -1
} else {
update.Like = in.GetLikeCount()
}
if in.DislikeCount == nil {
update.DisLike = -1
} else {
update.DisLike = in.GetDislikeCount()
}
_, err = l.svcCtx.PostModel.UpdateOptional(l.ctx, &update)
if err != nil {
e := domain.PostMongoErrorL(
// 錯誤代碼 05-021-04
e := domain.CommentErrorL(
domain.UpdatePostError,
logx.WithContext(l.ctx),
[]logx.LogField{
{Key: "req", Value: in},
{Key: "func", Value: "PostModel.UpdateOptional"},
{Key: "err", Value: err},
},
"failed to add new post").Wrap(err)
"failed to update post", in.PostId).Wrap(err)
return nil, e
}

View File

@ -0,0 +1,118 @@
package postservicelogic
import (
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
mocklib "app-cloudep-tweeting-service/internal/mock/lib"
mockmodel "app-cloudep-tweeting-service/internal/mock/model"
"app-cloudep-tweeting-service/internal/svc"
"context"
"errors"
"go.mongodb.org/mongo-driver/mongo"
"google.golang.org/protobuf/proto"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"testing"
)
func TestUpdatePost(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 初始化 mock 依賴
mockPostModel := mockmodel.NewMockPostModel(ctrl)
mockValidate := mocklib.NewMockValidate(ctrl)
// 初始化服務上下文
svcCtx := &svc.ServiceContext{
PostModel: mockPostModel,
Validate: mockValidate,
}
// 測試數據集
tests := []struct {
name string
input *tweeting.UpdatePostReq
prepare func()
expectErr bool
}{
{
name: "成功更新貼文",
input: &tweeting.UpdatePostReq{
PostId: "66cfdc1d6f8fe7eac1e52523",
Content: proto.String("Updated content"),
Tags: []string{"tag1", "tag2"},
Media: []*tweeting.Media{
{
Url: "http://example.com/image.png",
Type: "image",
},
},
LikeCount: proto.Int64(10),
DislikeCount: proto.Int64(2),
},
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 UpdateOptional 成功
mockPostModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(&mongo.UpdateResult{
ModifiedCount: 1,
}, nil).Times(1)
},
expectErr: false,
},
{
name: "驗證失敗",
input: &tweeting.UpdatePostReq{
PostId: "",
},
prepare: func() {
// 模擬 Validate 失敗
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1)
},
expectErr: true,
},
{
name: "更新貼文失敗",
input: &tweeting.UpdatePostReq{
PostId: "66cfdc1d6f8fe7eac1e52523",
Content: proto.String("Updated content"),
},
prepare: func() {
// 模擬 Validate 成功
mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1)
// 模擬 UpdateOptional 失敗
mockPostModel.EXPECT().UpdateOptional(gomock.Any(), gomock.Any()).Return(&mongo.UpdateResult{
ModifiedCount: 0,
}, errors.New("update failed")).Times(1)
},
expectErr: true,
},
}
// 執行測試
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置測試環境
tt.prepare()
// 初始化 UpdatePostLogic
logic := UpdatePostLogic{
svcCtx: svcCtx,
ctx: context.TODO(),
}
// 執行 UpdatePost
_, err := logic.UpdatePost(tt.input)
// 驗證結果
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -0,0 +1,73 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./validate.go
//
// Generated by this command:
//
// mockgen -source=./validate.go -destination=../../mock/lib/validate.go -package=lib
//
// Package lib is a generated GoMock package.
package lib
import (
reflect "reflect"
required "code.30cm.net/digimon/library-go/validator"
gomock "go.uber.org/mock/gomock"
)
// MockValidate is a mock of Validate interface.
type MockValidate struct {
ctrl *gomock.Controller
recorder *MockValidateMockRecorder
}
// MockValidateMockRecorder is the mock recorder for MockValidate.
type MockValidateMockRecorder struct {
mock *MockValidate
}
// NewMockValidate creates a new mock instance.
func NewMockValidate(ctrl *gomock.Controller) *MockValidate {
mock := &MockValidate{ctrl: ctrl}
mock.recorder = &MockValidateMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockValidate) EXPECT() *MockValidateMockRecorder {
return m.recorder
}
// BindToValidator mocks base method.
func (m *MockValidate) BindToValidator(opts ...required.Option) error {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BindToValidator", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BindToValidator indicates an expected call of BindToValidator.
func (mr *MockValidateMockRecorder) BindToValidator(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindToValidator", reflect.TypeOf((*MockValidate)(nil).BindToValidator), opts...)
}
// ValidateAll mocks base method.
func (m *MockValidate) ValidateAll(obj any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ValidateAll", obj)
ret0, _ := ret[0].(error)
return ret0
}
// ValidateAll indicates an expected call of ValidateAll.
func (mr *MockValidateMockRecorder) ValidateAll(obj any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAll", reflect.TypeOf((*MockValidate)(nil).ValidateAll), obj)
}

View File

@ -0,0 +1,152 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/model/mongo/comment_model.go
//
// Generated by this command:
//
// mockgen -source=./internal/model/mongo/comment_model.go -destination=./internal/mock/model/comment_model.go -package=mock
//
// Package mock is a generated GoMock package.
package mock
import (
model "app-cloudep-tweeting-service/internal/model/mongo"
context "context"
reflect "reflect"
mongo "go.mongodb.org/mongo-driver/mongo"
gomock "go.uber.org/mock/gomock"
)
// MockCommentModel is a mock of CommentModel interface.
type MockCommentModel struct {
ctrl *gomock.Controller
recorder *MockCommentModelMockRecorder
}
// MockCommentModelMockRecorder is the mock recorder for MockCommentModel.
type MockCommentModelMockRecorder struct {
mock *MockCommentModel
}
// NewMockCommentModel creates a new mock instance.
func NewMockCommentModel(ctrl *gomock.Controller) *MockCommentModel {
mock := &MockCommentModel{ctrl: ctrl}
mock.recorder = &MockCommentModelMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCommentModel) EXPECT() *MockCommentModelMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockCommentModel) Delete(ctx context.Context, id string) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockCommentModelMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCommentModel)(nil).Delete), ctx, id)
}
// DeleteMany mocks base method.
func (m *MockCommentModel) DeleteMany(ctx context.Context, id ...string) (int64, error) {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range id {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteMany", varargs...)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteMany indicates an expected call of DeleteMany.
func (mr *MockCommentModelMockRecorder) DeleteMany(ctx any, id ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, id...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockCommentModel)(nil).DeleteMany), varargs...)
}
// Find mocks base method.
func (m *MockCommentModel) Find(ctx context.Context, param *model.QueryCommentModelReq) ([]*model.Comment, int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx, param)
ret0, _ := ret[0].([]*model.Comment)
ret1, _ := ret[1].(int64)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Find indicates an expected call of Find.
func (mr *MockCommentModelMockRecorder) Find(ctx, param any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockCommentModel)(nil).Find), ctx, param)
}
// FindOne mocks base method.
func (m *MockCommentModel) FindOne(ctx context.Context, id string) (*model.Comment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, id)
ret0, _ := ret[0].(*model.Comment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindOne indicates an expected call of FindOne.
func (mr *MockCommentModelMockRecorder) FindOne(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockCommentModel)(nil).FindOne), ctx, id)
}
// Insert mocks base method.
func (m *MockCommentModel) Insert(ctx context.Context, data *model.Comment) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, data)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockCommentModelMockRecorder) Insert(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockCommentModel)(nil).Insert), ctx, data)
}
// Update mocks base method.
func (m *MockCommentModel) Update(ctx context.Context, data *model.Comment) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockCommentModelMockRecorder) Update(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCommentModel)(nil).Update), ctx, data)
}
// UpdateOptional mocks base method.
func (m *MockCommentModel) UpdateOptional(ctx context.Context, data *model.Comment) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateOptional", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateOptional indicates an expected call of UpdateOptional.
func (mr *MockCommentModelMockRecorder) UpdateOptional(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOptional", reflect.TypeOf((*MockCommentModel)(nil).UpdateOptional), ctx, data)
}

View File

@ -0,0 +1,101 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/model/mongo/comment_model_gen.go
//
// Generated by this command:
//
// mockgen -source=./internal/model/mongo/comment_model_gen.go -destination=./internal/mock/model/comment_model_gen.go -package=mock
//
// Package mock is a generated GoMock package.
package mock
import (
model "app-cloudep-tweeting-service/internal/model/mongo"
context "context"
reflect "reflect"
mongo "go.mongodb.org/mongo-driver/mongo"
gomock "go.uber.org/mock/gomock"
)
// MockcommentModel is a mock of commentModel interface.
type MockcommentModel struct {
ctrl *gomock.Controller
recorder *MockcommentModelMockRecorder
}
// MockcommentModelMockRecorder is the mock recorder for MockcommentModel.
type MockcommentModelMockRecorder struct {
mock *MockcommentModel
}
// NewMockcommentModel creates a new mock instance.
func NewMockcommentModel(ctrl *gomock.Controller) *MockcommentModel {
mock := &MockcommentModel{ctrl: ctrl}
mock.recorder = &MockcommentModelMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockcommentModel) EXPECT() *MockcommentModelMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockcommentModel) Delete(ctx context.Context, id string) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockcommentModelMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockcommentModel)(nil).Delete), ctx, id)
}
// FindOne mocks base method.
func (m *MockcommentModel) FindOne(ctx context.Context, id string) (*model.Comment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, id)
ret0, _ := ret[0].(*model.Comment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindOne indicates an expected call of FindOne.
func (mr *MockcommentModelMockRecorder) FindOne(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockcommentModel)(nil).FindOne), ctx, id)
}
// Insert mocks base method.
func (m *MockcommentModel) Insert(ctx context.Context, data *model.Comment) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, data)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockcommentModelMockRecorder) Insert(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockcommentModel)(nil).Insert), ctx, data)
}
// Update mocks base method.
func (m *MockcommentModel) Update(ctx context.Context, data *model.Comment) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockcommentModelMockRecorder) Update(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockcommentModel)(nil).Update), ctx, data)
}

View File

@ -0,0 +1,152 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/model/mongo/post_model.go
//
// Generated by this command:
//
// mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock
//
// Package mock is a generated GoMock package.
package mock
import (
model "app-cloudep-tweeting-service/internal/model/mongo"
context "context"
reflect "reflect"
mongo "go.mongodb.org/mongo-driver/mongo"
gomock "go.uber.org/mock/gomock"
)
// MockPostModel is a mock of PostModel interface.
type MockPostModel struct {
ctrl *gomock.Controller
recorder *MockPostModelMockRecorder
}
// MockPostModelMockRecorder is the mock recorder for MockPostModel.
type MockPostModelMockRecorder struct {
mock *MockPostModel
}
// NewMockPostModel creates a new mock instance.
func NewMockPostModel(ctrl *gomock.Controller) *MockPostModel {
mock := &MockPostModel{ctrl: ctrl}
mock.recorder = &MockPostModelMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPostModel) EXPECT() *MockPostModelMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockPostModel) Delete(ctx context.Context, id string) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockPostModelMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPostModel)(nil).Delete), ctx, id)
}
// DeleteMany mocks base method.
func (m *MockPostModel) DeleteMany(ctx context.Context, id ...string) (int64, error) {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range id {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteMany", varargs...)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteMany indicates an expected call of DeleteMany.
func (mr *MockPostModelMockRecorder) DeleteMany(ctx any, id ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, id...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockPostModel)(nil).DeleteMany), varargs...)
}
// Find mocks base method.
func (m *MockPostModel) Find(ctx context.Context, param *model.QueryPostModelReq) ([]*model.Post, int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx, param)
ret0, _ := ret[0].([]*model.Post)
ret1, _ := ret[1].(int64)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Find indicates an expected call of Find.
func (mr *MockPostModelMockRecorder) Find(ctx, param any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockPostModel)(nil).Find), ctx, param)
}
// FindOne mocks base method.
func (m *MockPostModel) FindOne(ctx context.Context, id string) (*model.Post, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, id)
ret0, _ := ret[0].(*model.Post)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindOne indicates an expected call of FindOne.
func (mr *MockPostModelMockRecorder) FindOne(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockPostModel)(nil).FindOne), ctx, id)
}
// Insert mocks base method.
func (m *MockPostModel) Insert(ctx context.Context, data *model.Post) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, data)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockPostModelMockRecorder) Insert(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockPostModel)(nil).Insert), ctx, data)
}
// Update mocks base method.
func (m *MockPostModel) Update(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockPostModelMockRecorder) Update(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPostModel)(nil).Update), ctx, data)
}
// UpdateOptional mocks base method.
func (m *MockPostModel) UpdateOptional(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateOptional", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateOptional indicates an expected call of UpdateOptional.
func (mr *MockPostModelMockRecorder) UpdateOptional(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOptional", reflect.TypeOf((*MockPostModel)(nil).UpdateOptional), ctx, data)
}

View File

@ -0,0 +1,101 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/model/mongo/post_model_gen.go
//
// Generated by this command:
//
// mockgen -source=./internal/model/mongo/post_model_gen.go -destination=./internal/mock/model/post_model_gen.go -package=mock
//
// Package mock is a generated GoMock package.
package mock
import (
model "app-cloudep-tweeting-service/internal/model/mongo"
context "context"
reflect "reflect"
mongo "go.mongodb.org/mongo-driver/mongo"
gomock "go.uber.org/mock/gomock"
)
// MockpostModel is a mock of postModel interface.
type MockpostModel struct {
ctrl *gomock.Controller
recorder *MockpostModelMockRecorder
}
// MockpostModelMockRecorder is the mock recorder for MockpostModel.
type MockpostModelMockRecorder struct {
mock *MockpostModel
}
// NewMockpostModel creates a new mock instance.
func NewMockpostModel(ctrl *gomock.Controller) *MockpostModel {
mock := &MockpostModel{ctrl: ctrl}
mock.recorder = &MockpostModelMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockpostModel) EXPECT() *MockpostModelMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockpostModel) Delete(ctx context.Context, id string) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockpostModelMockRecorder) Delete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockpostModel)(nil).Delete), ctx, id)
}
// FindOne mocks base method.
func (m *MockpostModel) FindOne(ctx context.Context, id string) (*model.Post, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, id)
ret0, _ := ret[0].(*model.Post)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindOne indicates an expected call of FindOne.
func (mr *MockpostModelMockRecorder) FindOne(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockpostModel)(nil).FindOne), ctx, id)
}
// Insert mocks base method.
func (m *MockpostModel) Insert(ctx context.Context, data *model.Post) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, data)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockpostModelMockRecorder) Insert(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockpostModel)(nil).Insert), ctx, data)
}
// Update mocks base method.
func (m *MockpostModel) Update(ctx context.Context, data *model.Post) (*mongo.UpdateResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, data)
ret0, _ := ret[0].(*mongo.UpdateResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockpostModelMockRecorder) Update(ctx, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockpostModel)(nil).Update), ctx, data)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,25 +0,0 @@
package model
import "github.com/zeromicro/go-zero/core/stores/mon"
var _ Comment_likesModel = (*customComment_likesModel)(nil)
type (
// Comment_likesModel is an interface to be customized, add more methods here,
// and implement the added methods in customComment_likesModel.
Comment_likesModel interface {
comment_likesModel
}
customComment_likesModel struct {
*defaultComment_likesModel
}
)
// NewComment_likesModel returns a model for the mongo.
func NewComment_likesModel(url, db, collection string) Comment_likesModel {
conn := mon.MustNewModel(url, db, collection)
return &customComment_likesModel{
defaultComment_likesModel: newDefaultComment_likesModel(conn),
}
}

View File

@ -1,74 +0,0 @@
// Code generated by goctl. DO NOT EDIT.
package model
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type comment_likesModel interface {
Insert(ctx context.Context, data *CommentLikes) error
FindOne(ctx context.Context, id string) (*CommentLikes, error)
Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error)
Delete(ctx context.Context, id string) (int64, error)
}
type defaultComment_likesModel struct {
conn *mon.Model
}
func newDefaultComment_likesModel(conn *mon.Model) *defaultComment_likesModel {
return &defaultComment_likesModel{conn: conn}
}
func (m *defaultComment_likesModel) Insert(ctx context.Context, data *CommentLikes) error {
if data.ID.IsZero() {
data.ID = primitive.NewObjectID()
data.CreateAt = time.Now()
data.UpdateAt = time.Now()
}
_, err := m.conn.InsertOne(ctx, data)
return err
}
func (m *defaultComment_likesModel) FindOne(ctx context.Context, id string) (*CommentLikes, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, ErrInvalidObjectId
}
var data CommentLikes
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultComment_likesModel) Update(ctx context.Context, data *CommentLikes) (*mongo.UpdateResult, error) {
data.UpdateAt = time.Now()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
func (m *defaultComment_likesModel) Delete(ctx context.Context, id string) (int64, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return 0, ErrInvalidObjectId
}
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}

View File

@ -1,14 +0,0 @@
package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type CommentLikes struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
// TODO: Fill your own fields
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
}

View File

@ -1,8 +1,14 @@
package model
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/monc"
"context"
"time"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
var _ CommentModel = (*customCommentModel)(nil)
@ -12,17 +18,84 @@ type (
// and implement the added methods in customCommentModel.
CommentModel interface {
commentModel
DeleteMany(ctx context.Context, id ...string) (int64, error)
UpdateOptional(ctx context.Context, data *Comment) (*mongo.UpdateResult, error)
Find(ctx context.Context, param *QueryCommentModelReq) ([]*Comment, int64, error)
}
customCommentModel struct {
*defaultCommentModel
}
QueryCommentModelReq struct {
PostID string
PageSize int64
PageIndex int64
}
)
func (xw customCommentModel) Find(ctx context.Context, param *QueryCommentModelReq) ([]*Comment, int64, error) {
// TODO implement me
panic("implement me")
}
// NewCommentModel returns a model for the mongo.
func NewCommentModel(url, db, collection string, c cache.CacheConf) CommentModel {
conn := monc.MustNewModel(url, db, collection, c)
func NewCommentModel(url, db, collection string) CommentModel {
conn := mon.MustNewModel(url, db, collection)
return &customCommentModel{
defaultCommentModel: newDefaultCommentModel(conn),
}
}
func (m *defaultCommentModel) DeleteMany(ctx context.Context, id ...string) (int64, error) {
objectIDs := make([]primitive.ObjectID, 0, len(id))
// prepare
for _, item := range id {
oid, err := primitive.ObjectIDFromHex(item)
if err != nil {
logx.WithCallerSkip(1).WithFields(
logx.Field("func", "defaultPostModel.DeleteMany"),
logx.Field("id", item),
).Error(err.Error())
continue
}
objectIDs = append(objectIDs, oid)
}
// 檢查是否有有效的 ObjectIDs
if len(objectIDs) == 0 {
return 0, ErrNotFound
}
// 刪除文檔
res, err := m.conn.DeleteMany(ctx, bson.M{"_id": bson.M{"$in": objectIDs}})
if err != nil {
return 0, err
}
return res, err
}
func (m *defaultCommentModel) UpdateOptional(ctx context.Context, data *Comment) (*mongo.UpdateResult, error) {
update := bson.M{"$set": bson.M{}}
if data.Content != "" {
update["$set"].(bson.M)["content"] = data.Content
}
if data.LikeCount != -1 {
update["$set"].(bson.M)["like_count"] = data.LikeCount
}
if data.DisLikeCount != -1 {
update["$set"].(bson.M)["dis_like_count"] = data.DisLikeCount
}
// UpdateAt 是每次都需要更新的,不用檢查
update["$set"].(bson.M)["updateAt"] = time.Now().UTC().UnixNano()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, update)
return res, err
}

View File

@ -5,14 +5,12 @@ import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/monc"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
var prefixCommentCacheKey = "cache:comment:"
type commentModel interface {
Insert(ctx context.Context, data *Comment) error
FindOne(ctx context.Context, id string) (*Comment, error)
@ -21,22 +19,21 @@ type commentModel interface {
}
type defaultCommentModel struct {
conn *monc.Model
conn *mon.Model
}
func newDefaultCommentModel(conn *monc.Model) *defaultCommentModel {
func newDefaultCommentModel(conn *mon.Model) *defaultCommentModel {
return &defaultCommentModel{conn: conn}
}
func (m *defaultCommentModel) Insert(ctx context.Context, data *Comment) error {
if data.ID.IsZero() {
data.ID = primitive.NewObjectID()
data.CreateAt = time.Now()
data.UpdateAt = time.Now()
data.CreateAt = time.Now().UTC().UnixNano()
data.UpdateAt = time.Now().UTC().UnixNano()
}
key := prefixCommentCacheKey + data.ID.Hex()
_, err := m.conn.InsertOne(ctx, key, data)
_, err := m.conn.InsertOne(ctx, data)
return err
}
@ -47,12 +44,12 @@ func (m *defaultCommentModel) FindOne(ctx context.Context, id string) (*Comment,
}
var data Comment
key := prefixCommentCacheKey + id
err = m.conn.FindOne(ctx, key, &data, bson.M{"_id": oid})
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case monc.ErrNotFound:
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
@ -60,9 +57,9 @@ func (m *defaultCommentModel) FindOne(ctx context.Context, id string) (*Comment,
}
func (m *defaultCommentModel) Update(ctx context.Context, data *Comment) (*mongo.UpdateResult, error) {
data.UpdateAt = time.Now()
key := prefixCommentCacheKey + data.ID.Hex()
res, err := m.conn.UpdateOne(ctx, key, bson.M{"_id": data.ID}, bson.M{"$set": data})
data.UpdateAt = time.Now().UTC().UnixNano()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
@ -71,7 +68,7 @@ func (m *defaultCommentModel) Delete(ctx context.Context, id string) (int64, err
if err != nil {
return 0, ErrInvalidObjectId
}
key := prefixCommentCacheKey + id
res, err := m.conn.DeleteOne(ctx, key, bson.M{"_id": oid})
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}

View File

@ -1,14 +1,23 @@
package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Comment struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
// TODO: Fill your own fields
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
PostID string `bson:"post_id" json:"post_id"`
UID string `bson:"uid" json:"uid"` // 留下留言的人
Content string `bson:"content" json:"content"` // 留言內容
LikeCount int64 `bson:"like_count" json:"like_count"` // 喜歡這則留言的人
DisLikeCount int64 `bson:"dis_like_count" json:"dis_like_count"` // 不喜歡這則留言的人
UpdateAt int64 `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"`
}
func (c Comment) CollectionName() string {
return "comment"
}
// 照邏輯,應該需要建立的索引有
// 複合索引:(PostID + CreateAt)

View File

@ -1,195 +0,0 @@
package model
import (
"app-cloudep-tweeting-service/internal/domain"
"context"
"errors"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var _ Post_likesModel = (*customPost_likesModel)(nil)
type (
// Post_likesModel is an interface to be customized, add more methods here,
// and implement the added methods in customPost_likesModel.
Post_likesModel interface {
post_likesModel
LikeDislike(ctx context.Context, postLike *PostLikes) (*PostReactionAction, error)
FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error)
FindUIDPostLikeStatus(ctx context.Context, param *QueryUIDPostLikeStatusReq) ([]UIDPostLikeStatusResp, error)
Count(ctx context.Context, target string, likeType domain.LikeType) (int64, error)
}
PostReactionAction struct {
PostID string // 貼文的 ID
ReactionType domain.LikeType // 用戶的反應類型,可能是讚或不讚
IsIncrement bool // 表示是否增加true 表示增加false 表示減少)
}
QueryPostLikeReq struct {
Target string `bson:"target"`
LikeType domain.LikeType `bson:"like_type"`
PageIndex int64 `bson:"page_index"`
PageSize int64 `bson:"page_size"`
}
QueryUIDPostLikeStatusReq struct {
Targets []string `bson:"target"`
LikeType domain.LikeType `bson:"like_type"`
UID string
}
UIDPostLikeStatusResp struct {
TargetID string `json:"target_id"`
LikeStatus bool `json:"like_status"`
}
customPost_likesModel struct {
*defaultPost_likesModel
}
)
// NewPost_likesModel returns a model for the mongo.
func NewPost_likesModel(url, db, collection string) Post_likesModel {
conn := mon.MustNewModel(url, db, collection)
return &customPost_likesModel{
defaultPost_likesModel: newDefaultPost_likesModel(conn),
}
}
func (m *defaultPost_likesModel) LikeDislike(ctx context.Context, postLike *PostLikes) (*PostReactionAction, error) {
result := &PostReactionAction{
PostID: postLike.TargetID,
ReactionType: domain.LikeType(postLike.Type),
}
// 使用 target_id、uid、type 來查詢資料是否存在
filter := bson.M{
"target_id": postLike.TargetID,
"uid": postLike.UID,
"type": postLike.Type,
}
// 查詢資料是否存在
var existingPostLike PostLikes
err := m.conn.FindOne(ctx, &existingPostLike, filter)
if err == nil {
// 資料存在,進行刪除操作
result.IsIncrement = false
_, err = m.conn.DeleteOne(ctx, filter)
if err != nil {
return nil, err // 刪除失敗
}
return result, nil // 刪除成功
} else if errors.Is(mongo.ErrNoDocuments, err) {
// 資料不存在,進行插入操作
result.IsIncrement = true
postLike.ID = primitive.NewObjectID() // 設置新的 ObjectID
postLike.CreateAt = time.Now().UTC().UnixNano()
_, err = m.conn.InsertOne(ctx, postLike)
if err != nil {
return nil, err // 插入失敗
}
return result, nil // 插入成功
} else {
// 其他錯誤
return nil, err
}
}
func (m *defaultPost_likesModel) FindLikeUsers(ctx context.Context, param *QueryPostLikeReq) ([]*PostLikes, int64, error) {
// 建立篩選條件
filter := bson.M{}
// 如果指定了 Target 條件,將其添加到篩選條件中
if param.Target != "" {
filter["target_id"] = param.Target
}
// 如果指定了 LikeType 條件,將其添加到篩選條件中
if param.LikeType != 0 {
filter["type"] = param.LikeType
}
// 計算符合條件的文檔總數
totalCount, err := m.conn.CountDocuments(ctx, filter)
if err != nil {
return nil, 0, err
}
// 設置分頁和排序選項
opts := options.Find()
opts.SetSort(bson.D{{"createAt", -1}}) // 按照創建時間倒序排序
if param.PageSize > 0 {
opts.SetLimit(param.PageSize)
}
if param.PageIndex > 0 && param.PageSize > 0 {
opts.SetSkip((param.PageIndex - 1) * param.PageSize)
}
// 查詢符合條件的文檔
var results []*PostLikes
err = m.conn.Find(ctx, &results, filter, opts)
if err != nil {
return nil, 0, err
}
// 返回結果集、總數和錯誤信息
return results, totalCount, nil
}
func (m *defaultPost_likesModel) FindUIDPostLikeStatus(ctx context.Context, param *QueryUIDPostLikeStatusReq) ([]UIDPostLikeStatusResp, error) {
// 初始化返回結果的切片
var results []UIDPostLikeStatusResp
// 建立篩選條件
filter := bson.M{
"uid": param.UID, // 篩選指定的 UID
"target_id": bson.M{"$in": param.Targets}, // 篩選多個目標 ID
"type": param.LikeType, // 篩選指定的 LikeType
}
// 查詢符合條件的點讚記錄
var postLikes []PostLikes
err := m.conn.Find(ctx, &postLikes, filter)
if err != nil {
return nil, err
}
// 構建一個 map 來保存查詢到的點讚記錄
targetLikeMap := make(map[string]bool)
for _, like := range postLikes {
targetLikeMap[like.TargetID] = true
}
// 構建每個目標的點讚狀態
for _, targetID := range param.Targets {
likeStatus := UIDPostLikeStatusResp{
TargetID: targetID,
LikeStatus: targetLikeMap[targetID], // 如果 map 中有該 targetID返回 true否則返回 false
}
// 如果該 targetID 沒有點讚記錄,默認 LikeStatus 為 false
if _, found := targetLikeMap[targetID]; !found {
likeStatus.LikeStatus = false
}
results = append(results, likeStatus)
}
return results, nil
}
func (c customPost_likesModel) Count(ctx context.Context, target string, likeType domain.LikeType) (int64, error) {
// TODO implement me
panic("implement me")
}

View File

@ -1,71 +0,0 @@
// Code generated by goctl. DO NOT EDIT.
package model
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type post_likesModel interface {
Insert(ctx context.Context, data *PostLikes) error
FindOne(ctx context.Context, id string) (*PostLikes, error)
Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error)
Delete(ctx context.Context, id string) (int64, error)
}
type defaultPost_likesModel struct {
conn *mon.Model
}
func newDefaultPost_likesModel(conn *mon.Model) *defaultPost_likesModel {
return &defaultPost_likesModel{conn: conn}
}
func (m *defaultPost_likesModel) Insert(ctx context.Context, data *PostLikes) error {
if data.ID.IsZero() {
data.ID = primitive.NewObjectID()
data.CreateAt = time.Now().UTC().UnixNano()
}
_, err := m.conn.InsertOne(ctx, data)
return err
}
func (m *defaultPost_likesModel) FindOne(ctx context.Context, id string) (*PostLikes, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, ErrInvalidObjectId
}
var data PostLikes
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultPost_likesModel) Update(ctx context.Context, data *PostLikes) (*mongo.UpdateResult, error) {
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
func (m *defaultPost_likesModel) Delete(ctx context.Context, id string) (int64, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return 0, ErrInvalidObjectId
}
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}

View File

@ -1,17 +0,0 @@
package model
import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
type PostLikes struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
TargetID string `bson:"target_id" json:"target_id"`
UID string `bson:"uid" json:"uid"`
Type int8 `bson:"type" json:"type"`
CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"`
}
func (p *PostLikes) CollectionName() string {
return "post_like"
}

View File

@ -1,19 +1,18 @@
package model
import (
"app-cloudep-tweeting-service/internal/domain"
"context"
"errors"
"fmt"
"time"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/monc"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var _ PostModel = (*customPostModel)(nil)
@ -26,7 +25,6 @@ type (
DeleteMany(ctx context.Context, id ...string) (int64, error)
UpdateOptional(ctx context.Context, data *Post) (*mongo.UpdateResult, error)
Find(ctx context.Context, param *QueryPostModelReq) ([]*Post, int64, error)
IncDecLikeDislikeCountLogic(ctx context.Context, param *PostReactionAction) error
}
customPostModel struct {
@ -43,8 +41,8 @@ type (
)
// NewPostModel returns a model for the mongo.
func NewPostModel(url, db, collection string, c cache.CacheConf) PostModel {
conn := monc.MustNewModel(url, db, collection, c)
func NewPostModel(url, db, collection string) PostModel {
conn := mon.MustNewModel(url, db, collection)
return &customPostModel{
defaultPostModel: newDefaultPostModel(conn),
}
@ -52,7 +50,6 @@ func NewPostModel(url, db, collection string, c cache.CacheConf) PostModel {
func (m *defaultPostModel) DeleteMany(ctx context.Context, id ...string) (int64, error) {
objectIDs := make([]primitive.ObjectID, 0, len(id))
key := make([]string, 0, len(id))
// prepare
for _, item := range id {
@ -66,7 +63,6 @@ func (m *defaultPostModel) DeleteMany(ctx context.Context, id ...string) (int64,
continue
}
objectIDs = append(objectIDs, oid)
key = append(key, prefixPostCacheKey+item)
}
// 檢查是否有有效的 ObjectIDs
@ -80,11 +76,6 @@ func (m *defaultPostModel) DeleteMany(ctx context.Context, id ...string) (int64,
return 0, err
}
err = m.conn.DelCache(ctx, key...)
if err != nil {
return 0, err
}
return res, err
}
@ -111,24 +102,21 @@ func (m *defaultPostModel) UpdateOptional(ctx context.Context, data *Post) (*mon
update["$set"].(bson.M)["tags"] = data.Tags
}
if len(data.Media) > 0 {
update["$set"].(bson.M)["media_url"] = data.Media
if len(data.MediaURL) > 0 {
update["$set"].(bson.M)["media_url"] = data.MediaURL
}
if data.Like != 0 {
if data.Like != -1 {
update["$set"].(bson.M)["like"] = data.Like
}
if data.DisLike != 0 {
if data.DisLike != -1 {
update["$set"].(bson.M)["dislike"] = data.DisLike
}
// UpdateAt 是每次都需要更新的,不用檢查
update["$set"].(bson.M)["updateAt"] = time.Now().UTC().UnixNano()
fmt.Println("update map", update)
key := prefixPostCacheKey + data.ID.Hex()
res, err := m.conn.UpdateOne(ctx, key, bson.M{"_id": data.ID}, update)
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, update)
return res, err
}
@ -195,39 +183,3 @@ func (m *defaultPostModel) Find(ctx context.Context, param *QueryPostModelReq) (
return nil, 0, err
}
}
func (c *customPostModel) IncDecLikeDislikeCountLogic(ctx context.Context, param *PostReactionAction) error {
// 建立篩選條件,找到要更新的貼文
filter := bson.M{"_id": param.PostID}
// 初始化更新操作
update := bson.M{}
// 根據 ReactionType 和 IsIncrement 決定更新邏輯
if param.ReactionType == domain.LikeTypeLike {
if param.IsIncrement {
// 增加 like 計數
update = bson.M{"$inc": bson.M{"like": 1}}
} else {
// 減少 like 計數
update = bson.M{"$inc": bson.M{"like": -1}}
}
} else if param.ReactionType == domain.LikeTypeDisLike {
if param.IsIncrement {
// 增加 dislike 計數
update = bson.M{"$inc": bson.M{"dislike": 1}}
} else {
// 減少 dislike 計數
update = bson.M{"$inc": bson.M{"dislike": -1}}
}
}
// 執行更新操作
key := prefixPostCacheKey + param.PostID
_, err := c.conn.UpdateOne(ctx, key, filter, update)
if err != nil {
return err // 返回錯誤信息
}
return nil // 成功返回 nil
}

View File

@ -5,14 +5,12 @@ import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/monc"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
var prefixPostCacheKey = "cache:post:"
type postModel interface {
Insert(ctx context.Context, data *Post) error
FindOne(ctx context.Context, id string) (*Post, error)
@ -21,10 +19,10 @@ type postModel interface {
}
type defaultPostModel struct {
conn *monc.Model
conn *mon.Model
}
func newDefaultPostModel(conn *monc.Model) *defaultPostModel {
func newDefaultPostModel(conn *mon.Model) *defaultPostModel {
return &defaultPostModel{conn: conn}
}
@ -35,8 +33,7 @@ func (m *defaultPostModel) Insert(ctx context.Context, data *Post) error {
data.UpdateAt = time.Now().UTC().UnixNano()
}
key := prefixPostCacheKey + data.ID.Hex()
_, err := m.conn.InsertOne(ctx, key, data)
_, err := m.conn.InsertOne(ctx, data)
return err
}
@ -47,12 +44,12 @@ func (m *defaultPostModel) FindOne(ctx context.Context, id string) (*Post, error
}
var data Post
key := prefixPostCacheKey + id
err = m.conn.FindOne(ctx, key, &data, bson.M{"_id": oid})
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case monc.ErrNotFound:
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
@ -61,8 +58,8 @@ func (m *defaultPostModel) FindOne(ctx context.Context, id string) (*Post, error
func (m *defaultPostModel) Update(ctx context.Context, data *Post) (*mongo.UpdateResult, error) {
data.UpdateAt = time.Now().UTC().UnixNano()
key := prefixPostCacheKey + data.ID.Hex()
res, err := m.conn.UpdateOne(ctx, key, bson.M{"_id": data.ID}, bson.M{"$set": data})
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
@ -71,7 +68,7 @@ func (m *defaultPostModel) Delete(ctx context.Context, id string) (int64, error)
if err != nil {
return 0, ErrInvalidObjectId
}
key := prefixPostCacheKey + id
res, err := m.conn.DeleteOne(ctx, key, bson.M{"_id": oid})
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}

View File

@ -4,23 +4,18 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
// TODO Tag 這裡在效能與正確性之間做取捨
// 存在貼文內的不提供搜尋純顯示用只不過在原始的tag 發生變動的時候,並不會一起改變
// 搜尋會貼文與Tag 的表會再另外一邊做關聯
// 暫時業務邏輯上tag 只提供新增,不提供修改以及刪除,故目前版本可行
type Post struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
UID string `bson:"uid" json:"uid"`
Content string `bson:"content" json:"content"`
Status int8 `bson:"status" json:"status"` // 1. 等待審核中 , 2 審核通過,預設為 1
IsAd bool `bson:"is_ad" json:"is_ad"` // 此則貼文是否為廣告貼文
UID string `bson:"uid" json:"uid"` // 搜尋條件
Content string `bson:"content" json:"content"` // 內容
Status int8 `bson:"status" json:"status"` // 1. 等待審核中 , 2 審核通過,預設為 1 ->過濾條件
IsAd bool `bson:"is_ad" json:"is_ad"` // 此則貼文是否為廣告貼文 -> 過濾條件
Tags []string `bson:"tags" json:"tags"` // 本則貼文的標籤,不提供搜尋,僅提供顯示(存名字ID 建立之後就不提供修改與刪除)
Media []Media `bson:"media_url" json:"media_url"` // 網址
Like uint64 `bson:"like" json:"like"` // 讚
DisLike uint64 `bson:"dislike" json:"dislike"` // 不讚
MediaURL []Media `bson:"media_url" json:"media_url"` // 網址
Like int64 `bson:"like" json:"like"` // 讚數量
DisLike int64 `bson:"dislike" json:"dislike"` // 不讚數量
UpdateAt int64 `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"`
CreateAt int64 `bson:"createAt,omitempty" json:"createAt,omitempty"` // -> 排序條件
}
type Media struct {
@ -31,3 +26,7 @@ type Media struct {
func (p *Post) CollectionName() string {
return "post"
}
// 照邏輯,應該需要建立的索引有
// 單列索引UID、Status、IsAd、CreateAt
// 複合索引:(UID + Status + IsAd)、(UID + CreateAt)

View File

@ -1,25 +0,0 @@
package model
import "github.com/zeromicro/go-zero/core/stores/mon"
var _ TagsModel = (*customTagsModel)(nil)
type (
// TagsModel is an interface to be customized, add more methods here,
// and implement the added methods in customTagsModel.
TagsModel interface {
tagsModel
}
customTagsModel struct {
*defaultTagsModel
}
)
// NewTagsModel returns a model for the mongo.
func NewTagsModel(url, db, collection string) TagsModel {
conn := mon.MustNewModel(url, db, collection)
return &customTagsModel{
defaultTagsModel: newDefaultTagsModel(conn),
}
}

View File

@ -1,74 +0,0 @@
// Code generated by goctl. DO NOT EDIT.
package model
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type tagsModel interface {
Insert(ctx context.Context, data *Tags) error
FindOne(ctx context.Context, id string) (*Tags, error)
Update(ctx context.Context, data *Tags) (*mongo.UpdateResult, error)
Delete(ctx context.Context, id string) (int64, error)
}
type defaultTagsModel struct {
conn *mon.Model
}
func newDefaultTagsModel(conn *mon.Model) *defaultTagsModel {
return &defaultTagsModel{conn: conn}
}
func (m *defaultTagsModel) Insert(ctx context.Context, data *Tags) error {
if data.ID.IsZero() {
data.ID = primitive.NewObjectID()
data.CreateAt = time.Now()
data.UpdateAt = time.Now()
}
_, err := m.conn.InsertOne(ctx, data)
return err
}
func (m *defaultTagsModel) FindOne(ctx context.Context, id string) (*Tags, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, ErrInvalidObjectId
}
var data Tags
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultTagsModel) Update(ctx context.Context, data *Tags) (*mongo.UpdateResult, error) {
data.UpdateAt = time.Now()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
func (m *defaultTagsModel) Delete(ctx context.Context, id string) (int64, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return 0, ErrInvalidObjectId
}
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}

View File

@ -1,14 +0,0 @@
package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Tags struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
// TODO: Fill your own fields
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
}

View File

@ -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
}

View File

@ -0,0 +1 @@
package repository

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -23,7 +23,7 @@ func NewCommentServiceServer(svcCtx *svc.ServiceContext) *CommentServiceServer {
}
// NewComment 發表評論
func (s *CommentServiceServer) NewComment(ctx context.Context, in *tweeting.CommentPostReq) (*tweeting.OKResp, error) {
func (s *CommentServiceServer) NewComment(ctx context.Context, in *tweeting.CommentPostReq) (*tweeting.CommentPostResp, error) {
l := commentservicelogic.NewNewCommentLogic(ctx, s.svcCtx)
return l.NewComment(in)
}
@ -45,27 +45,3 @@ func (s *CommentServiceServer) UpdateComment(ctx context.Context, in *tweeting.U
l := commentservicelogic.NewUpdateCommentLogic(ctx, s.svcCtx)
return l.UpdateComment(in)
}
// LikeComment 點讚/取消讚 評論
func (s *CommentServiceServer) LikeComment(ctx context.Context, in *tweeting.LikeReq) (*tweeting.OKResp, error) {
l := commentservicelogic.NewLikeCommentLogic(ctx, s.svcCtx)
return l.LikeComment(in)
}
// GetLikeStatus 取得讚/不讚評論狀態
func (s *CommentServiceServer) GetLikeStatus(ctx context.Context, in *tweeting.LikeReq) (*tweeting.OKResp, error) {
l := commentservicelogic.NewGetLikeStatusLogic(ctx, s.svcCtx)
return l.GetLikeStatus(in)
}
// LikeList 取得讚/不讚評論列表
func (s *CommentServiceServer) LikeList(ctx context.Context, in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) {
l := commentservicelogic.NewLikeListLogic(ctx, s.svcCtx)
return l.LikeList(in)
}
// CountLike 取得讚/不讚評論數量
func (s *CommentServiceServer) CountLike(ctx context.Context, in *tweeting.LikeCountReq) (*tweeting.LikeCountResp, error) {
l := commentservicelogic.NewCountLikeLogic(ctx, s.svcCtx)
return l.CountLike(in)
}

View File

@ -22,10 +22,10 @@ func NewPostServiceServer(svcCtx *svc.ServiceContext) *PostServiceServer {
}
}
// NewPost 新增貼文
func (s *PostServiceServer) NewPost(ctx context.Context, in *tweeting.NewPostReq) (*tweeting.PostResp, error) {
l := postservicelogic.NewNewPostLogic(ctx, s.svcCtx)
return l.NewPost(in)
// CreatePost 新增貼文
func (s *PostServiceServer) CreatePost(ctx context.Context, in *tweeting.NewPostReq) (*tweeting.PostResp, error) {
l := postservicelogic.NewCreatePostLogic(ctx, s.svcCtx)
return l.CreatePost(in)
}
// DeletePost 刪除貼文
@ -45,33 +45,3 @@ func (s *PostServiceServer) ListPosts(ctx context.Context, in *tweeting.QueryPos
l := postservicelogic.NewListPostsLogic(ctx, s.svcCtx)
return l.ListPosts(in)
}
// IncDecLikeDislikeCount 增減數量
func (s *PostServiceServer) IncDecLikeDislikeCount(ctx context.Context, in *tweeting.IncDecLikeDislikeCountReq) (*tweeting.OKResp, error) {
l := postservicelogic.NewIncDecLikeDislikeCountLogic(ctx, s.svcCtx)
return l.IncDecLikeDislikeCount(in)
}
// Like 點讚/取消讚 貼文
func (s *PostServiceServer) Like(ctx context.Context, in *tweeting.LikeReq) (*tweeting.PostReactionActionResp, error) {
l := postservicelogic.NewLikeLogic(ctx, s.svcCtx)
return l.Like(in)
}
// GetLikeStatus 取得讚/不讚狀態
func (s *PostServiceServer) GetLikeStatus(ctx context.Context, in *tweeting.GetLikeStatusReq) (*tweeting.GetLikeStatusResp, error) {
l := postservicelogic.NewGetLikeStatusLogic(ctx, s.svcCtx)
return l.GetLikeStatus(in)
}
// LikeList 取得讚/不讚列表
func (s *PostServiceServer) LikeList(ctx context.Context, in *tweeting.LikeListReq) (*tweeting.LikeListResp, error) {
l := postservicelogic.NewLikeListLogic(ctx, s.svcCtx)
return l.LikeList(in)
}
// CountLike 取得讚/不讚數量
func (s *PostServiceServer) CountLike(ctx context.Context, in *tweeting.LikeCountReq) (*tweeting.LikeCountResp, error) {
l := postservicelogic.NewCountLikeLogic(ctx, s.svcCtx)
return l.CountLike(in)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,29 @@
package svc
import (
"app-cloudep-tweeting-service/internal/config"
model "app-cloudep-tweeting-service/internal/model/mongo"
"fmt"
)
func mustMongoConnectURL(c config.Config) string {
return fmt.Sprintf("%s://%s:%s",
c.Mongo.Schema,
c.Mongo.Host,
c.Mongo.Port,
)
}
// TODO 思考快取做在那邊
func MustPostModel(c config.Config) model.PostModel {
postCollection := model.Post{}
return model.NewPostModel(mustMongoConnectURL(c), c.Mongo.Database, postCollection.CollectionName())
}
func MustCommentModel(c config.Config) model.CommentModel {
m := model.Comment{}
return model.NewCommentModel(mustMongoConnectURL(c), c.Mongo.Database, m.CollectionName())
}

View File

@ -2,8 +2,15 @@ package svc
import (
"app-cloudep-tweeting-service/internal/config"
domainRepo "app-cloudep-tweeting-service/internal/domain/repository"
"app-cloudep-tweeting-service/internal/lib/neo4j"
model "app-cloudep-tweeting-service/internal/model/mongo"
"fmt"
"app-cloudep-tweeting-service/internal/repository"
ers "code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"github.com/zeromicro/go-zero/core/stores/redis"
vi "code.30cm.net/digimon/library-go/validator"
)
@ -11,26 +18,40 @@ import (
type ServiceContext struct {
Config config.Config
Validate vi.Validate
PostModel model.PostModel
PostLikeModel model.Post_likesModel
CommentModel model.CommentModel
TimelineRepo domainRepo.TimelineRepository
SocialNetworkRepository domainRepo.SocialNetworkRepository
}
func NewServiceContext(c config.Config) *ServiceContext {
baseMongo := MustMongoConnectUrl(c)
postCollection := model.Post{}
postLikeCollection := model.PostLikes{}
ers.Scope = code.CloudEPTweeting
newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster())
if err != nil {
panic(err)
}
neoClient := neo4j.NewNeo4J(&neo4j.Config{
URI: c.Neo4J.URI,
Username: c.Neo4J.Username,
Password: c.Neo4J.Password,
MaxConnectionPoolSize: c.Neo4J.MaxConnectionPoolSize,
MaxConnectionLifetime: c.Neo4J.MaxConnectionLifetime,
ConnectionTimeout: c.Neo4J.ConnectionTimeout,
}, neo4j.WithPerformance(), neo4j.WithLogLevel(c.Neo4J.LogLevel))
return &ServiceContext{
Config: c,
Validate: vi.MustValidator(),
PostModel: model.NewPostModel(baseMongo, c.Mongo.Database, postCollection.CollectionName(), c.Cache),
PostLikeModel: model.NewPost_likesModel(baseMongo, c.Mongo.Database, postLikeCollection.CollectionName()),
PostModel: MustPostModel(c),
CommentModel: MustCommentModel(c),
TimelineRepo: repository.MustGenerateRepository(repository.TimelineRepositoryParam{
Config: c,
Redis: *newRedis,
}),
SocialNetworkRepository: repository.MustSocialNetworkRepository(repository.SocialNetworkParam{
Config: c,
Neo4jClient: neoClient,
}),
}
}
func MustMongoConnectUrl(c config.Config) string {
return fmt.Sprintf(
"%s://%s:%s", c.Mongo.Schema,
c.Mongo.Host, c.Mongo.Port)
}

Some files were not shown because too many files have changed in this diff Show More