add timeline service
This commit is contained in:
parent
c9a0926495
commit
d82f5c3337
6
Makefile
6
Makefile
|
@ -51,9 +51,9 @@ gen-mongo-model: # 建立 rpc 資料庫
|
||||||
# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是
|
# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是
|
||||||
goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||||
goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||||
# goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||||
# goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||||
# goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||||
@echo "Generate mongo model files successfully"
|
@echo "Generate mongo model files successfully"
|
||||||
|
|
||||||
.PHONY: mock-gen
|
.PHONY: mock-gen
|
||||||
|
|
|
@ -12,3 +12,7 @@ Mongo:
|
||||||
Password: ""
|
Password: ""
|
||||||
Port: "27017"
|
Port: "27017"
|
||||||
Database: digimon_tweeting
|
Database: digimon_tweeting
|
||||||
|
|
||||||
|
TimelineSetting:
|
||||||
|
Expire: 86400
|
||||||
|
MaxLength: 1000
|
|
@ -201,8 +201,17 @@ message GetTimelineResp
|
||||||
message AddPostToTimelineReq
|
message AddPostToTimelineReq
|
||||||
{
|
{
|
||||||
string uid = 1; // key
|
string uid = 1; // key
|
||||||
repeated PostDetailItem posts = 2;
|
repeated PostTimelineItem posts = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 貼文更新
|
||||||
|
message PostTimelineItem
|
||||||
|
{
|
||||||
|
string post_id = 1; // 貼文ID
|
||||||
|
int64 created_at = 7; // 發佈時間 -> 排序使用
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// TimelineService 業務邏輯在外面組合
|
// TimelineService 業務邏輯在外面組合
|
||||||
service TimelineService
|
service TimelineService
|
||||||
{
|
{
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -5,6 +5,7 @@ go 1.22.3
|
||||||
require (
|
require (
|
||||||
code.30cm.net/digimon/library-go/errs v1.2.4
|
code.30cm.net/digimon/library-go/errs v1.2.4
|
||||||
code.30cm.net/digimon/library-go/validator v1.0.0
|
code.30cm.net/digimon/library-go/validator v1.0.0
|
||||||
|
github.com/alicebob/miniredis/v2 v2.33.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/zeromicro/go-zero v1.7.0
|
github.com/zeromicro/go-zero v1.7.0
|
||||||
go.mongodb.org/mongo-driver v1.16.0
|
go.mongodb.org/mongo-driver v1.16.0
|
||||||
|
@ -14,6 +15,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
@ -65,6 +67,7 @@ require (
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||||
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
|
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
||||||
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
|
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
|
||||||
|
|
|
@ -4,6 +4,7 @@ import "github.com/zeromicro/go-zero/zrpc"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
zrpc.RpcServerConf
|
zrpc.RpcServerConf
|
||||||
|
|
||||||
Mongo struct {
|
Mongo struct {
|
||||||
Schema string
|
Schema string
|
||||||
User string
|
User string
|
||||||
|
@ -12,4 +13,9 @@ type Config struct {
|
||||||
Port string
|
Port string
|
||||||
Database string
|
Database string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TimelineSetting struct {
|
||||||
|
Expire int64 // Second
|
||||||
|
MaxLength int64 // 暫存筆數
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,7 @@ const (
|
||||||
AdTypeOnlyAd
|
AdTypeOnlyAd
|
||||||
AdTypeOnlyNotAd
|
AdTypeOnlyNotAd
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LastOfTimelineFlag = "NoMoreData"
|
||||||
|
)
|
||||||
|
|
|
@ -33,6 +33,10 @@ const (
|
||||||
CommentListErrorCode
|
CommentListErrorCode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimeLineErrorCode ErrorCode = iota + 20
|
||||||
|
)
|
||||||
|
|
||||||
func CommentError(ec ErrorCode, s ...string) *ers.LibError {
|
func CommentError(ec ErrorCode, s ...string) *ers.LibError {
|
||||||
return ers.NewError(code.CloudEPTweeting, code.DBError,
|
return ers.NewError(code.CloudEPTweeting, code.DBError,
|
||||||
ec.ToUint32(),
|
ec.ToUint32(),
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type RedisKey string
|
||||||
|
|
||||||
|
func (key RedisKey) ToString() string {
|
||||||
|
return string(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key RedisKey) With(s ...string) RedisKey {
|
||||||
|
parts := append([]string{string(key)}, s...)
|
||||||
|
|
||||||
|
return RedisKey(strings.Join(parts, ":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimelineRedisKey RedisKey = "timeline"
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRedisKeyToString 測試 ToString 方法
|
||||||
|
func TestRedisKeyToString(t *testing.T) {
|
||||||
|
key := RedisKey("user:timeline")
|
||||||
|
expected := "user:timeline"
|
||||||
|
result := key.ToString()
|
||||||
|
|
||||||
|
assert.Equal(t, expected, result, "ToString should return the correct string representation of RedisKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisKeyWith 測試 With 方法
|
||||||
|
func TestRedisKeyWith(t *testing.T) {
|
||||||
|
key := RedisKey("user:timeline")
|
||||||
|
subKey := "12345"
|
||||||
|
expected := "user:timeline:12345"
|
||||||
|
result := key.With(subKey)
|
||||||
|
|
||||||
|
assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with the provided subKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisKeyWithMultiple 測試 With 方法與多個參數
|
||||||
|
func TestRedisKeyWithMultiple(t *testing.T) {
|
||||||
|
key := RedisKey("user:timeline")
|
||||||
|
subKeys := []string{"12345", "posts"}
|
||||||
|
expected := "user:timeline:12345:posts"
|
||||||
|
result := key.With(subKeys...)
|
||||||
|
|
||||||
|
assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with multiple provided subKeys")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRedisKeyWithEmpty 測試 With 方法與空參數
|
||||||
|
func TestRedisKeyWithEmpty(t *testing.T) {
|
||||||
|
key := RedisKey("user:timeline")
|
||||||
|
expected := "user:timeline"
|
||||||
|
result := key.With()
|
||||||
|
|
||||||
|
assert.Equal(t, RedisKey(expected), result, "With should return the original key when no subKeys are provided")
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
----------------------------------------
|
||||||
|
| | | | |
|
||||||
|
| data A| data B |NO Data | .... |
|
||||||
|
| | | | |
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
動態時報在發現這個 Queue 有 No Data 的 Flag 時
|
||||||
|
就不再去 Query 資料庫,防止被狂刷。
|
||||||
|
只是要注意在業務上何時要 加入/刪除 這個 Flag
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TimelineRepository 定義時間線的存儲接口,可以根據不同的排序策略實現。
|
||||||
|
type TimelineRepository interface {
|
||||||
|
// AddPost 將貼文添加到動態時報,並根據排序策略進行排序。
|
||||||
|
AddPost(ctx context.Context, req AddPostRequest) error
|
||||||
|
// FetchTimeline 獲取指定用戶的動態時報。
|
||||||
|
FetchTimeline(ctx context.Context, req FetchTimelineRequest) (FetchTimelineResponse, error)
|
||||||
|
// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。
|
||||||
|
SetNoMoreDataFlag(ctx context.Context, uid string) error
|
||||||
|
// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。
|
||||||
|
HasNoMoreData(ctx context.Context, uid string) (bool, error)
|
||||||
|
// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。
|
||||||
|
ClearNoMoreDataFlag(ctx context.Context, uid string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPostRequest 用於將貼文添加到時間線的請求結構體。
|
||||||
|
type AddPostRequest struct {
|
||||||
|
UID string
|
||||||
|
PostItems []TimelineItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimelineItem 表示時間線中的一個元素,排序依據取決於 Score。
|
||||||
|
type TimelineItem struct {
|
||||||
|
PostID string // 貼文ID
|
||||||
|
Score int64 // 排序使用的分數,根據具體實現可能代表時間、優先級等
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchTimelineRequest 用於獲取時間線的請求結構體。
|
||||||
|
type FetchTimelineRequest struct {
|
||||||
|
UID string
|
||||||
|
PageSize int64
|
||||||
|
PageIndex int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchTimelineResponse 表示獲取時間線的回應結構體。
|
||||||
|
type FetchTimelineResponse struct {
|
||||||
|
Items []TimelineItem
|
||||||
|
Page tweeting.Pager
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
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 MustGenerateUseCase(param TimelineRepositoryParam) repository.TimelineRepository {
|
||||||
|
return &TimelineRepository{
|
||||||
|
cfg: param.Config,
|
||||||
|
redis: param.Redis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPost 將貼文添加到時間線,並根據 Score 排序
|
||||||
|
func (t *TimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error {
|
||||||
|
key := domain.TimelineRedisKey.With(req.UID).ToString()
|
||||||
|
|
||||||
|
// 準備要插入的元素
|
||||||
|
zItems := make([]redis.Pair, len(req.PostItems))
|
||||||
|
for i, item := range req.PostItems {
|
||||||
|
zItems[i] = redis.Pair{
|
||||||
|
Score: item.Score,
|
||||||
|
Key: item.PostID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 將 ZSet 元素添加到 Redis
|
||||||
|
_, err := t.redis.ZaddsCtx(ctx, key, zItems...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查 ZSet 長度,並在超過 maxLength 時刪除多餘的元素
|
||||||
|
if t.cfg.TimelineSetting.MaxLength > 0 {
|
||||||
|
// 這裡從 0 到 - (maxLength+1) 代表超過限制的元素範圍
|
||||||
|
_, err := t.redis.ZremrangebyrankCtx(ctx, key, 0, -(t.cfg.TimelineSetting.MaxLength + 1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置過期時間
|
||||||
|
return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchTimeline 獲取指定用戶的動態時報
|
||||||
|
func (t *TimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) {
|
||||||
|
key := domain.TimelineRedisKey.With(req.UID).ToString()
|
||||||
|
|
||||||
|
start := (req.PageIndex - 1) * req.PageSize
|
||||||
|
end := start + req.PageSize - 1
|
||||||
|
|
||||||
|
// 從 Redis 中按分數由高到低獲取時間線元素
|
||||||
|
pair, err := t.redis.ZrevrangeWithScoresCtx(ctx, key, start, end)
|
||||||
|
if err != nil {
|
||||||
|
return repository.FetchTimelineResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 構建返回結果
|
||||||
|
items := make([]repository.TimelineItem, len(pair))
|
||||||
|
for i, z := range pair {
|
||||||
|
items[i] = repository.TimelineItem{
|
||||||
|
PostID: z.Key,
|
||||||
|
Score: z.Score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 計算總數量
|
||||||
|
total, err := t.redis.ZcardCtx(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return repository.FetchTimelineResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.FetchTimelineResponse{
|
||||||
|
Items: items,
|
||||||
|
Page: tweeting.Pager{
|
||||||
|
Total: int64(total),
|
||||||
|
Index: req.PageIndex,
|
||||||
|
Size: req.PageSize,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫
|
||||||
|
func (t *TimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error {
|
||||||
|
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||||
|
|
||||||
|
// 添加一個標誌到時間線的 ZSet
|
||||||
|
_, err := t.redis.ZaddsCtx(ctx, key, redis.Pair{
|
||||||
|
Score: time.Now().UTC().Unix(),
|
||||||
|
Key: domain.LastOfTimelineFlag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置過期時間
|
||||||
|
return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫
|
||||||
|
func (t *TimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) {
|
||||||
|
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||||
|
|
||||||
|
// 檢查 "NoMoreData" 標誌是否存在
|
||||||
|
score, err := t.redis.ZscoreCtx(ctx, key, domain.LastOfTimelineFlag)
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return false, nil // 標誌不存在
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err // 其他錯誤
|
||||||
|
}
|
||||||
|
|
||||||
|
return score != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌
|
||||||
|
func (t *TimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error {
|
||||||
|
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||||
|
// 移除 "NoMoreData" 標誌
|
||||||
|
_, err := t.redis.ZremCtx(ctx, key, domain.LastOfTimelineFlag)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,439 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"app-cloudep-tweeting-service/internal/config"
|
||||||
|
"app-cloudep-tweeting-service/internal/domain"
|
||||||
|
"app-cloudep-tweeting-service/internal/domain/repository"
|
||||||
|
"context"
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := MustGenerateUseCase(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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||||
"app-cloudep-tweeting-service/internal/logic/commentservice"
|
commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice"
|
||||||
"app-cloudep-tweeting-service/internal/svc"
|
"app-cloudep-tweeting-service/internal/svc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||||
"app-cloudep-tweeting-service/internal/logic/postservice"
|
postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice"
|
||||||
"app-cloudep-tweeting-service/internal/svc"
|
"app-cloudep-tweeting-service/internal/svc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||||
"app-cloudep-tweeting-service/internal/logic/timelineservice"
|
timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice"
|
||||||
"app-cloudep-tweeting-service/internal/svc"
|
"app-cloudep-tweeting-service/internal/svc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue