Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"common.go",
"dao.cache.go",
"dao.go",
"extension.go",
"topic.go",
"topic_video.go",
],
importpath = "go-common/app/service/bbq/topic/internal/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/bbq/topic/api:go_default_library",
"//app/service/bbq/topic/internal/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf/paladin:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/sync/errgroup.v2:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//library/time:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"common_test.go",
"dao.cache_test.go",
"dao_test.go",
"extension_test.go",
"topic_test.go",
"topic_video_test.go",
],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = [
"//app/service/bbq/topic/api:go_default_library",
"//app/service/bbq/topic/internal/model:go_default_library",
"//library/conf/paladin:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,75 @@
package dao
import (
"context"
"encoding/json"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/cache/redis"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/xstr"
)
/*各种情况下是否需要查询db前三列表示情景条件
NEXT offset rank 是否需要db操作
1 1
0 1 0 0
0 n>1 0 1
0 0 n>0 0
*/
func parseCursor(ctx context.Context, cursorPrev, cursorNext string) (cursor model.CursorValue, directionNext bool, err error) {
// 判断是向前还是向后查询
directionNext = true
cursorStr := cursorNext
if len(cursorNext) == 0 && len(cursorPrev) > 0 {
directionNext = false
cursorStr = cursorPrev
}
// 解析cursor中的cursor_id
if len(cursorStr) != 0 {
var cursorData = []byte(cursorStr)
err = json.Unmarshal(cursorData, &cursor)
if err != nil {
err = ecode.ReqParamErr
return
}
}
// 最后做一次校验保证cursor的值是对的
if (cursor.StickRank > 0 && cursor.Offset > 0) || (!directionNext && cursor.Offset == 0 && cursor.StickRank == 0) {
err = ecode.TopicReqParamErr
log.Errorw(ctx, "log", "cursor value error", "prev", cursorPrev, "next", cursorNext)
return
}
return
}
func (d *Dao) getRedisList(ctx context.Context, key string) (list []int64, err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
str, err := redis.String(conn.Do("GET", key))
if err == redis.ErrNil {
err = nil
return
}
if err != nil {
log.Errorw(ctx, "log", "get redis list fail", "key", key)
return
}
list, err = xstr.SplitInts(str)
if err != nil {
log.Errorw(ctx, "log", "split list_str fail", "key", key, "str", str)
return
}
return
}
func (d *Dao) setRedisList(ctx context.Context, key string, list []int64) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
if _, err = conn.Do("SET", key, xstr.JoinInts(list)); err != nil {
log.Errorw(ctx, "log", "set redis list fail", "key", key, "list", list)
return
}
return
}

View File

@@ -0,0 +1,75 @@
package dao
import (
"context"
"github.com/smartystreets/goconvey/convey"
"testing"
)
func TestDaoparseCursor(t *testing.T) {
convey.Convey("parseCursor", t, func(convCtx convey.C) {
var (
ctx = context.Background()
)
convCtx.Convey("common case", func(convCtx convey.C) {
cursorPrev := ""
cursorNext := ""
cursor, directionNext, err := parseCursor(ctx, cursorPrev, cursorNext)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(directionNext, convey.ShouldBeTrue)
convCtx.So(cursor.Offset, convey.ShouldEqual, 0)
convCtx.So(cursor.StickRank, convey.ShouldEqual, 0)
})
convCtx.Convey("error case", func(convCtx convey.C) {
cursorPrev := ""
cursorNext := "{\"stick_rank\":1,\"offset\":1}"
_, _, err := parseCursor(ctx, cursorPrev, cursorNext)
convCtx.So(err, convey.ShouldNotBeNil)
cursorPrev = "{\"stick_rank\":0,\"offset\":0}"
cursorNext = ""
_, _, err = parseCursor(ctx, cursorPrev, cursorNext)
convCtx.So(err, convey.ShouldNotBeNil)
cursorPrev = "{stick_rank\":0,\"offset\":0}"
cursorNext = ""
_, _, err = parseCursor(ctx, cursorPrev, cursorNext)
convCtx.So(err, convey.ShouldNotBeNil)
})
})
}
func TestDaogetRedisList(t *testing.T) {
convey.Convey("getRedisList", t, func(convCtx convey.C) {
var (
ctx = context.Background()
key = "stick:ttttt"
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
list, err := d.getRedisList(ctx, key)
convCtx.Convey("Then err should be nil.list should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(list, convey.ShouldBeNil)
})
})
})
}
func TestDaosetRedisList(t *testing.T) {
convey.Convey("setRedisList", t, func(convCtx convey.C) {
var (
ctx = context.Background()
key = "stick:topic"
list = []int64{}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
err := d.setRedisList(ctx, key, list)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,179 @@
// Code generated by $GOPATH/src/go-common/app/tool/cache/gen. DO NOT EDIT.
/*
Package dao is a generated cache proxy package.
It is generated from:
type _cache interface {
// cache: -sync=true -batch=10 -max_group=10 -batch_err=break -nullcache=&api.VideoExtension{Svid:-1} -check_null_code=$==nil||$.Svid==-1
VideoExtension(c context.Context, ids []int64) (map[int64]*api.VideoExtension, error)
// cache: -sync=true -batch=10 -max_group=10 -batch_err=break -nullcache=&api.TopicInfo{TopicId:-1} -check_null_code=$==nil||$.TopicId==-1
TopicInfo(c context.Context, ids []int64) (map[int64]*api.TopicInfo, error)
}
*/
package dao
import (
"context"
"sync"
"go-common/app/service/bbq/topic/api"
"go-common/library/stat/prom"
"go-common/library/sync/errgroup"
)
var _ _cache
// VideoExtension get data from cache if miss will call source method, then add to cache.
func (d *Dao) VideoExtension(c context.Context, keys []int64) (res map[int64]*api.VideoExtension, err error) {
if len(keys) == 0 {
return
}
addCache := true
if res, err = d.CacheVideoExtension(c, keys); err != nil {
addCache = false
res = nil
err = nil
}
var miss []int64
for _, key := range keys {
if (res == nil) || (res[key] == nil) {
miss = append(miss, key)
}
}
prom.CacheHit.Add("VideoExtension", int64(len(keys)-len(miss)))
for k, v := range res {
if v == nil || v.Svid == -1 {
delete(res, k)
}
}
missLen := len(miss)
if missLen == 0 {
return
}
missData := make(map[int64]*api.VideoExtension, missLen)
prom.CacheMiss.Add("VideoExtension", int64(missLen))
var mutex sync.Mutex
group, ctx := errgroup.WithContext(c)
if missLen > 10 {
group.GOMAXPROCS(10)
}
var run = func(ms []int64) {
group.Go(func() (err error) {
data, err := d.RawVideoExtension(ctx, ms)
mutex.Lock()
for k, v := range data {
missData[k] = v
}
mutex.Unlock()
return
})
}
var (
i int
n = missLen / 10
)
for i = 0; i < n; i++ {
run(miss[i*n : (i+1)*n])
}
if len(miss[i*n:]) > 0 {
run(miss[i*n:])
}
err = group.Wait()
if res == nil {
res = make(map[int64]*api.VideoExtension, len(keys))
}
for k, v := range missData {
res[k] = v
}
if err != nil {
return
}
for _, key := range miss {
if res[key] == nil {
missData[key] = &api.VideoExtension{Svid: -1}
}
}
if !addCache {
return
}
d.AddCacheVideoExtension(c, missData)
return
}
// TopicInfo get data from cache if miss will call source method, then add to cache.
func (d *Dao) TopicInfo(c context.Context, keys []int64) (res map[int64]*api.TopicInfo, err error) {
if len(keys) == 0 {
return
}
addCache := true
if res, err = d.CacheTopicInfo(c, keys); err != nil {
addCache = false
res = nil
err = nil
}
var miss []int64
for _, key := range keys {
if (res == nil) || (res[key] == nil) {
miss = append(miss, key)
}
}
prom.CacheHit.Add("TopicInfo", int64(len(keys)-len(miss)))
for k, v := range res {
if v == nil || v.TopicId == -1 {
delete(res, k)
}
}
missLen := len(miss)
if missLen == 0 {
return
}
missData := make(map[int64]*api.TopicInfo, missLen)
prom.CacheMiss.Add("TopicInfo", int64(missLen))
var mutex sync.Mutex
group, ctx := errgroup.WithContext(c)
if missLen > 10 {
group.GOMAXPROCS(10)
}
var run = func(ms []int64) {
group.Go(func() (err error) {
data, err := d.RawTopicInfo(ctx, ms)
mutex.Lock()
for k, v := range data {
missData[k] = v
}
mutex.Unlock()
return
})
}
var (
i int
n = missLen / 10
)
for i = 0; i < n; i++ {
run(miss[i*n : (i+1)*n])
}
if len(miss[i*n:]) > 0 {
run(miss[i*n:])
}
err = group.Wait()
if res == nil {
res = make(map[int64]*api.TopicInfo, len(keys))
}
for k, v := range missData {
res[k] = v
}
if err != nil {
return
}
for _, key := range miss {
if res[key] == nil {
missData[key] = &api.TopicInfo{TopicId: -1}
}
}
if !addCache {
return
}
d.AddCacheTopicInfo(c, missData)
return
}

View File

@@ -0,0 +1,42 @@
package dao
import (
"context"
"go-common/library/log"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoVideoExtension(t *testing.T) {
convey.Convey("VideoExtension", t, func(convCtx convey.C) {
var (
c = context.Background()
keys = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.VideoExtension(c, keys)
log.V(1).Infow(c, "res", res)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoTopicInfo(t *testing.T) {
convey.Convey("TopicInfo", t, func(convCtx convey.C) {
var (
c = context.Background()
keys = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.TopicInfo(c, keys)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,83 @@
package dao
import (
"context"
"go-common/app/service/bbq/topic/api"
"go-common/library/sync/pipeline/fanout"
"time"
"go-common/library/cache/redis"
"go-common/library/conf/paladin"
"go-common/library/database/sql"
"go-common/library/log"
xtime "go-common/library/time"
)
//go:generate $GOPATH/src/go-common/app/tool/cache/gen
type _cache interface {
// cache: -sync=true -batch=10 -max_group=10 -batch_err=break -nullcache=&api.VideoExtension{Svid:-1} -check_null_code=$==nil||$.Svid==-1
VideoExtension(c context.Context, ids []int64) (map[int64]*api.VideoExtension, error)
// cache: -sync=true -batch=10 -max_group=10 -batch_err=break -nullcache=&api.TopicInfo{TopicId:-1} -check_null_code=$==nil||$.TopicId==-1
TopicInfo(c context.Context, ids []int64) (map[int64]*api.TopicInfo, error)
}
// Dao dao.
type Dao struct {
cache *fanout.Fanout
db *sql.DB
redis *redis.Pool
topicExpire int32
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
// New new a dao and return.
func New() (dao *Dao) {
var (
dc struct {
Topic *sql.Config
}
rc struct {
Topic *redis.Config
TopicExpire xtime.Duration
}
)
checkErr(paladin.Get("mysql.toml").UnmarshalTOML(&dc))
checkErr(paladin.Get("redis.toml").UnmarshalTOML(&rc))
dao = &Dao{
cache: fanout.New("cache", fanout.Worker(1), fanout.Buffer(1024)),
// mysql
db: sql.NewMySQL(dc.Topic),
// redis
redis: redis.NewPool(rc.Topic),
topicExpire: int32(time.Duration(rc.TopicExpire) / time.Second),
}
return
}
// Close close the resource.
func (d *Dao) Close() {
d.redis.Close()
d.db.Close()
}
// Ping ping the resource.
func (d *Dao) Ping(ctx context.Context) (err error) {
if err = d.pingRedis(ctx); err != nil {
return
}
return d.db.Ping(ctx)
}
func (d *Dao) pingRedis(ctx context.Context) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
if _, err = conn.Do("SET", "ping", "pong"); err != nil {
log.Error("conn.Set(PING) error(%v)", err)
}
return
}

View File

@@ -0,0 +1,37 @@
package dao
import (
"flag"
"go-common/library/conf/paladin"
"go-common/library/log"
"os"
"testing"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "")
flag.Set("conf_token", "")
flag.Set("tree_id", "")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../../configs/")
flag.Set("log.v", "20")
}
flag.Parse()
if err := paladin.Init(); err != nil {
panic(err)
}
log.Init(nil)
d = New()
os.Exit(m.Run())
}

View File

@@ -0,0 +1,121 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/service/bbq/topic/api"
"go-common/library/cache/redis"
"go-common/library/log"
"go-common/library/xstr"
)
const (
_selectExtension = "select svid, content from extension where svid in (%s)"
_insertExtension = "insert ignore into extension (`svid`,`type`,`content`) values (?,?,?)"
)
const (
_videoExtensionKey = "ext:%d"
)
// RawVideoExtension 从mysql获取extension
func (d *Dao) RawVideoExtension(ctx context.Context, svids []int64) (res map[int64]*api.VideoExtension, err error) {
res = make(map[int64]*api.VideoExtension)
if len(svids) == 0 {
return
}
querySQL := fmt.Sprintf(_selectExtension, xstr.JoinInts(svids))
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get extension error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
var svid int64
var content string
for rows.Next() {
if err = rows.Scan(&svid, &content); err != nil {
log.Errorw(ctx, "log", "get extension from mysql fail", "sql", querySQL)
return
}
// 由于数据库中的数据和缓存中还不太一样因此这里需要对db读取的数据进行额外处理
var extension api.Extension
json.Unmarshal([]byte(content), &extension.TitleExtra)
// TODOcheck
log.V(10).Infow(ctx, "log", "unmarshal content", "result", extension)
data, _ := json.Marshal(&extension)
res[svid] = &api.VideoExtension{Svid: svid, Extension: string(data)}
}
log.V(1).Infow(ctx, "log", "get extension", "req", svids, "rsp_size", len(res))
return
}
// CacheVideoExtension 从缓存获取extension
func (d *Dao) CacheVideoExtension(ctx context.Context, svids []int64) (res map[int64]*api.VideoExtension, err error) {
res = make(map[int64]*api.VideoExtension)
conn := d.redis.Get(ctx)
defer conn.Close()
for _, svid := range svids {
conn.Send("GET", fmt.Sprintf(_videoExtensionKey, svid))
}
conn.Flush()
var data string
for _, svid := range svids {
if data, err = redis.String(conn.Receive()); err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Errorv(ctx, log.KV("event", "redis_get"), log.KV("svid", svid))
}
continue
}
extension := new(api.VideoExtension)
extension.Svid = svid
extension.Extension = data
res[extension.Svid] = extension
}
log.Infov(ctx, log.KV("event", "redis_get"), log.KV("row_num", len(res)))
return
}
// AddCacheVideoExtension 添加extension缓存
func (d *Dao) AddCacheVideoExtension(ctx context.Context, extensions map[int64]*api.VideoExtension) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
for svid, value := range extensions {
conn.Send("SET", fmt.Sprintf(_videoExtensionKey, svid), value.Extension, "EX", d.topicExpire)
}
conn.Flush()
for i := 0; i < len(extensions); i++ {
conn.Receive()
}
log.Infov(ctx, log.KV("event", "redis_set"), log.KV("row_num", len(extensions)))
return
}
// DelCacheVideoExtension 删除extension缓存
func (d *Dao) DelCacheVideoExtension(ctx context.Context, svid int64) {
var key = fmt.Sprintf(_videoExtensionKey, svid)
conn := d.redis.Get(ctx)
defer conn.Close()
conn.Do("DEL", key)
}
// InsertExtension 插入extension到db
func (d *Dao) InsertExtension(ctx context.Context, svid int64, extensionType int64, extension *api.Extension) (rowsAffected int64, err error) {
data, _ := json.Marshal(extension.TitleExtra)
res, err := d.db.Exec(ctx, _insertExtension, svid, extensionType, string(data))
if err != nil {
log.Errorw(ctx, "log", "insert extension db fail", "svid", svid, "extension_type", extensionType, "extension", extensionType)
return
}
rowsAffected, tmpErr := res.RowsAffected()
if tmpErr != nil {
log.Warnw(ctx, "log", "get rows affected fail")
}
d.DelCacheVideoExtension(ctx, svid)
return
}

View File

@@ -0,0 +1,93 @@
package dao
import (
"context"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/log"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoRawVideoExtension(t *testing.T) {
convey.Convey("RawVideoExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svids = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.RawVideoExtension(ctx, svids)
log.V(1).Infow(ctx, "res", res)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoCacheVideoExtension(t *testing.T) {
convey.Convey("CacheVideoExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svids = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.CacheVideoExtension(ctx, svids)
log.V(1).Infow(ctx, "res", res)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoAddCacheVideoExtension(t *testing.T) {
convey.Convey("AddCacheVideoExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
)
extensions := make(map[int64]*api.VideoExtension)
extensions[1] = &api.VideoExtension{Svid: 1, Extension: "{\"title_extra\":[{\"type\":1,\"name\":\"Test\",\"end\":4,\"schema\":\"qing://topic?topic_id=1\"}]}"}
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
err := d.AddCacheVideoExtension(ctx, extensions)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoDelCacheVideoExtension(t *testing.T) {
convey.Convey("DelCacheVideoExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
d.DelCacheVideoExtension(ctx, svid)
convCtx.Convey("No return values", func(convCtx convey.C) {
})
})
})
}
func TestDaoInsertExtension(t *testing.T) {
convey.Convey("InsertExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(1)
extensionType = int64(1)
extension = &api.Extension{TitleExtra: []*api.TitleExtraItem{{Name: "Test", Type: model.TitleExtraTypeTopic, Start: 0, End: 4, Scheme: "qing://topic?topic_id=1"}}}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
rowsAffected, err := d.InsertExtension(ctx, svid, extensionType, extension)
convCtx.Convey("Then err should be nil.rowsAffected should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(rowsAffected, convey.ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,409 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/cache/redis"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup.v2"
"go-common/library/xstr"
"strings"
)
const (
_selectTopic = "select `id`, `name`, `desc`, `state` from topic where id in (%s)"
_insertUpdateTopic = "insert into topic (`name`,`score`,`state`,`video_num`) values %s on duplicate key update `video_num`=`video_num`+1"
_selectTopicID = "select id, name from topic where name in (%s)"
_selectDiscoveryTopic = "select id from topic where state=0 %s order by score desc, id desc limit %d, %d"
_selectUnavailabelTopic = "select id from topic where state=1 limit %d,%d"
_updateTopicField = "update topic set `%s` = ? where `id` = ?"
)
const (
_topicKey = "topic:%d"
)
// RawTopicInfo 从mysql获取topic info
func (d *Dao) RawTopicInfo(ctx context.Context, topicIDs []int64) (res map[int64]*api.TopicInfo, err error) {
res = make(map[int64]*api.TopicInfo)
if len(topicIDs) == 0 {
return
}
querySQL := fmt.Sprintf(_selectTopic, xstr.JoinInts(topicIDs))
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get topic error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
for rows.Next() {
topicInfo := new(api.TopicInfo)
if err = rows.Scan(&topicInfo.TopicId, &topicInfo.Name, &topicInfo.Desc, &topicInfo.State); err != nil {
log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
return
}
topicInfo.CoverUrl = "http://i0.hdslb.com/bfs/bbq/video-image/userface/155886860_1547729941"
res[topicInfo.TopicId] = topicInfo
}
log.V(1).Infow(ctx, "log", "get topic", "req", topicIDs, "rsp_size", len(res))
return
}
// CacheTopicInfo 从缓存获取topic info
func (d *Dao) CacheTopicInfo(ctx context.Context, topicIDs []int64) (res map[int64]*api.TopicInfo, err error) {
res = make(map[int64]*api.TopicInfo)
keys := make([]string, 0, len(topicIDs))
keyMidMap := make(map[int64]bool, len(topicIDs))
for _, topicID := range topicIDs {
key := fmt.Sprintf(_topicKey, topicID)
if _, exist := keyMidMap[topicID]; !exist {
// duplicate mid
keyMidMap[topicID] = true
keys = append(keys, key)
}
}
conn := d.redis.Get(ctx)
defer conn.Close()
for _, key := range keys {
conn.Send("GET", key)
}
conn.Flush()
var data []byte
for i := 0; i < len(keys); i++ {
if data, err = redis.Bytes(conn.Receive()); err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Errorv(ctx, log.KV("event", "redis_get"), log.KV("key", keys[i]))
}
continue
}
topicInfo := new(api.TopicInfo)
json.Unmarshal(data, topicInfo)
res[topicInfo.TopicId] = topicInfo
}
log.Infov(ctx, log.KV("event", "redis_get"), log.KV("row_num", len(res)))
return
}
// AddCacheTopicInfo 添加topic info缓存
func (d *Dao) AddCacheTopicInfo(ctx context.Context, topicInfos map[int64]*api.TopicInfo) (err error) {
keyValueMap := make(map[string][]byte, len(topicInfos))
for topicID, topicInfo := range topicInfos {
key := fmt.Sprintf(_topicKey, topicID)
if _, exist := keyValueMap[key]; !exist {
data, _ := json.Marshal(topicInfo)
keyValueMap[key] = data
}
}
conn := d.redis.Get(ctx)
defer conn.Close()
for key, value := range keyValueMap {
conn.Send("SET", key, value, "EX", d.topicExpire)
}
conn.Flush()
for i := 0; i < len(keyValueMap); i++ {
conn.Receive()
}
log.Infov(ctx, log.KV("event", "redis_set"), log.KV("row_num", len(topicInfos)))
return
}
// DelCacheTopicInfo 删除topic info缓存
func (d *Dao) DelCacheTopicInfo(ctx context.Context, topicID int64) {
var key = fmt.Sprintf(_topicKey, topicID)
conn := d.redis.Get(ctx)
defer conn.Close()
conn.Do("DEL", key)
}
// InsertTopics 插入话题
func (d *Dao) InsertTopics(ctx context.Context, topics map[string]*api.TopicInfo) (newTopics map[string]*api.TopicInfo, err error) {
//func (d *Dao) InsertTopics(ctx context.Context, topics map[string]int64) (err error) {
newTopics = make(map[string]*api.TopicInfo)
// 0. check
if len(topics) == 0 {
return
}
if len(topics) > model.MaxBatchLen {
err = ecode.TopicNumTooManyErr
return
}
// 长度校验
for _, item := range topics {
if strings.Count(item.Name, "")-1 > model.MaxTopicNameLen {
err = ecode.TopicNameLenErr
log.Errorw(ctx, "log", "topic name len too long", "name", item.Name)
return
}
}
// 1. 插入更新
group := errgroup.WithCancel(ctx)
group.GOMAXPROCS(5)
var groupInsertTopic = func(topicInfo *api.TopicInfo) {
group.Go(func(ctx context.Context) (err error) {
topicID, err := d.insertTopic(ctx, topicInfo)
if err != nil {
log.Warnw(ctx, "log", "get topic videos fail", "topic_name", topicInfo.Name)
return
}
if topicID == 0 {
log.Errorw(ctx, "log", "get error topic_id", "name", topicInfo.Name)
err = ecode.TopicInsertErr
return
}
topicInfo.TopicId = topicID
return
})
}
for _, topic := range topics {
groupInsertTopic(topic)
}
err = group.Wait()
if err != nil {
log.Warnw(ctx, "log", "do group insert topic fail")
return
}
// 由于insert的时候会返回ID所以直接赋值返回
newTopics = topics
return
}
// insertTopic 插入话题
func (d *Dao) insertTopic(ctx context.Context, topicInfo *api.TopicInfo) (topicID int64, err error) {
//func (d *Dao) InsertTopics(ctx context.Context, topics map[string]int64) (err error) {
// 0. check
// 长度校验
if strings.Count(topicInfo.Name, "")-1 > model.MaxTopicNameLen {
err = ecode.TopicNameLenErr
log.Errorw(ctx, "log", "topic name len too long", "name", topicInfo.Name)
return
}
var str string
// 1. 插入更新
str += fmt.Sprintf("('%s',%f,%d,1)", topicInfo.Name, topicInfo.Score, topicInfo.State)
insertSQL := fmt.Sprintf(_insertUpdateTopic, str)
log.V(1).Infow(ctx, "sql", insertSQL)
res, err := d.db.Exec(ctx, insertSQL)
if err != nil {
log.Errorw(ctx, "log", "insert topic fail", "topic_name", topicInfo.Name)
return
}
topicID, err = res.LastInsertId()
if err != nil {
log.Errorw(ctx, "log", "insert topic fail", "topic_name", topicInfo.Name)
return
}
return
}
// UpdateTopic 更新话题,当前有简介和状态
// 这个函数把操作权其实已经交给上层了,设计上不是个好设计,但是在于避免重复代码
func (d *Dao) UpdateTopic(ctx context.Context, topicID int64, field string, value interface{}) (err error) {
if field != "desc" && field != "state" {
return ecode.ReqParamErr
}
querySQL := fmt.Sprintf(_updateTopicField, field)
_, err = d.db.Exec(ctx, querySQL, value, topicID)
if err != nil {
log.Errorw(ctx, "log", "update topic field fail", "field", field, "value", value, "topic_id", topicID)
return
}
d.DelCacheTopicInfo(ctx, topicID)
return
}
// TopicID 通过话题name获取话题ID
// 话题ID结果存在topics中
func (d *Dao) TopicID(ctx context.Context, names []string) (topics map[string]int64, err error) {
topics = make(map[string]int64)
if len(names) == 0 {
return
}
if len(names) > model.MaxBatchLen {
err = ecode.TopicNumTooManyErr
return
}
querySQL := fmt.Sprintf(_selectTopicID, "\""+strings.Join(names, "\",\"")+"\"")
log.V(1).Infow(ctx, "log", "select topic id", "sql", querySQL)
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get topic id error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
var topicID int64
var name string
for rows.Next() {
if err = rows.Scan(&topicID, &name); err != nil {
log.Errorw(ctx, "log", "scan topic id error", "err", err, "sql", querySQL)
return
}
topics[name] = topicID
}
log.V(1).Infow(ctx, "log", "get topic id", "req", names, "rsp", topics)
return
}
// ListUnAvailableTopics .
func (d *Dao) ListUnAvailableTopics(ctx context.Context, page int32, size int32) (list []int64, hasMore bool, err error) {
hasMore = true
// 0. check
if page < 1 {
err = ecode.TopicReqParamErr
return
}
if page > model.MaxDiscoveryTopicPage {
hasMore = false
return
}
// 2. get list
offset := (page - 1) * size
querySQL := fmt.Sprintf(_selectUnavailabelTopic, offset, size)
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get topic video error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
for rows.Next() {
var topicID int64
if err = rows.Scan(&topicID); err != nil {
log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
return
}
list = append(list, topicID)
}
// 3. 判断has_more
if len(list) < int(size) {
hasMore = false
}
return
}
// ListRankTopics 获取推荐的话题列表
// TODO: 把置顶逻辑移上去
func (d *Dao) ListRankTopics(ctx context.Context, page int32, size int32) (list []int64, hasMore bool, err error) {
hasMore = true
// 0. check
if page < 1 {
err = ecode.TopicReqParamErr
return
}
if page > model.MaxDiscoveryTopicPage {
hasMore = false
return
}
// 1. 获取置顶数据s
additionalConditionSQL := ""
stickList, err := d.GetStickTopic(ctx)
if err != nil {
log.Warnw(ctx, "log", "get stick topic fail")
} else if len(stickList) > 0 {
additionalConditionSQL = fmt.Sprintf("and id not in (%s)", xstr.JoinInts(stickList))
}
// 2. 若page=1则获取推荐
if page == 1 {
list = stickList
}
// 3. 根据page获取话题列表
offset := (page - 1) * size
querySQL := fmt.Sprintf(_selectDiscoveryTopic, additionalConditionSQL, offset, size)
log.Infow(ctx, "sql", querySQL, "page", page, "size", size)
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get topic video error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
for rows.Next() {
var topicID int64
if err = rows.Scan(&topicID); err != nil {
log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
return
}
list = append(list, topicID)
}
// 4. 判断has_more
if len(list) < int(size) {
hasMore = false
}
return
}
// GetStickTopic 获取置顶视频
// TODO: 这个方式是临时之计当qps增大时会导致热点的产生
func (d *Dao) GetStickTopic(ctx context.Context) (list []int64, err error) {
return d.getRedisList(ctx, model.RedisStickTopicKey)
}
func (d *Dao) setStickTopic(ctx context.Context, list []int64) (err error) {
return d.setRedisList(ctx, model.RedisStickTopicKey, list)
}
// StickTopic .
func (d *Dao) StickTopic(ctx context.Context, opTopicID, op int64) (err error) {
// 0. check
info, err := d.TopicInfo(ctx, []int64{opTopicID})
if err != nil {
log.Warnw(ctx, "log", "get topic info fail", "topic_id", opTopicID)
return
}
topicInfo, exists := info[opTopicID]
if !exists {
log.Errorw(ctx, "log", "stick topic fail due to error topic_id", "topic_id", opTopicID)
err = ecode.TopicIDNotFound
return
}
if topicInfo.State != api.TopicStateAvailable {
log.Errorw(ctx, "log", "topic state unavailable to do sticking", "state", topicInfo.State, "topic_id", opTopicID)
err = ecode.TopicStateErr
return
}
// 1. 获取stick topic
stickList, err := d.GetStickTopic(ctx)
if err != nil {
log.Warnw(ctx, "log", "get stick topic fail")
return
}
// 2. 操作stick topic
var newStickList []int64
if op != 0 {
newStickList = append(newStickList, opTopicID)
}
for _, stickTopicID := range stickList {
if stickTopicID != opTopicID {
newStickList = append(newStickList, stickTopicID)
}
}
if len(newStickList) > model.MaxStickTopicNum {
newStickList = newStickList[:model.MaxStickTopicNum]
}
// 3. 更新stick topic
err = d.setStickTopic(ctx, newStickList)
if err != nil {
log.Warnw(ctx, "update stick topic fail")
return
}
return
}

View File

@@ -0,0 +1,286 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/ecode"
"go-common/library/log"
"math/rand"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoRawTopicInfo(t *testing.T) {
convey.Convey("RawTopicInfo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicIDs = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.RawTopicInfo(ctx, topicIDs)
log.Infow(ctx, "topics", res)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoCacheTopicInfo(t *testing.T) {
convey.Convey("CacheTopicInfo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicIDs = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.CacheTopicInfo(ctx, topicIDs)
log.Infow(ctx, "res", res)
convCtx.Convey("Then err should be nil.res should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoAddCacheTopicInfo(t *testing.T) {
convey.Convey("AddCacheTopicInfo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicInfos map[int64]*api.TopicInfo
)
topicInfos = make(map[int64]*api.TopicInfo)
topicInfos[1] = &api.TopicInfo{TopicId: 1, Name: "Test", State: 0, Desc: "test for tester"}
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
err := d.AddCacheTopicInfo(ctx, topicInfos)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoDelCacheTopicInfo(t *testing.T) {
convey.Convey("DelCacheTopicInfo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
d.DelCacheTopicInfo(ctx, topicID)
res, _ := d.CacheTopicInfo(ctx, []int64{topicID})
topicInfo := res[topicID]
convCtx.Convey("No return values", func(convCtx convey.C) {
convCtx.So(topicInfo, convey.ShouldBeNil)
})
})
})
}
func TestDaoInsertTopics(t *testing.T) {
convey.Convey("InsertTopics", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topics map[string]*api.TopicInfo
)
topics = make(map[string]*api.TopicInfo)
//topicName := fmt.Sprintf("test_%d", rand.Int()%10000000)
topicName := "Test"
topics[topicName] = &api.TopicInfo{Name: topicName, Score: float64(rand.Int()%10000) / float64(10000)}
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
_, err := d.InsertTopics(ctx, topics)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
log.Infow(ctx, "log", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:"+topicName)
topicName = fmt.Sprintf("test_%d", rand.Int()%10000000)
topics[topicName] = &api.TopicInfo{Name: topicName, Score: float64(rand.Int()%10000) / float64(10000)}
topicName = fmt.Sprintf("test_%d", rand.Int()%10000000)
topics[topicName] = &api.TopicInfo{Name: topicName, Score: float64(rand.Int()%10000) / float64(10000)}
convCtx.Convey("multi insert", func(convCtx convey.C) {
_, err := d.InsertTopics(ctx, topics)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
longTopics := make(map[string]*api.TopicInfo)
longName := "test_toolonggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg"
longTopics[longName] = &api.TopicInfo{Name: longName}
convCtx.Convey("error case", func(convCtx convey.C) {
_, duplicateErr := d.InsertTopics(ctx, longTopics)
convCtx.Convey("Then err should not be nil.", func(convCtx convey.C) {
convCtx.So(duplicateErr, convey.ShouldAlmostEqual, ecode.TopicNameLenErr)
})
})
})
}
func TestDaoTopicID(t *testing.T) {
convey.Convey("TopicID", t, func(convCtx convey.C) {
var (
ctx = context.Background()
names = []string{"Test"}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
topics, err := d.TopicID(ctx, names)
log.Infow(ctx, "names", names, "topics", topics)
convCtx.Convey("Then err should be nil.topics should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(topics, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoUpdateTopic(t *testing.T) {
convey.Convey("TopicID", t, func(convCtx convey.C) {
convCtx.Convey("update desc", func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
field = "desc"
value = "update_desc"
)
origin, _ := d.TopicInfo(ctx, []int64{topicID})
originTopic := origin[topicID]
err := d.UpdateTopic(ctx, topicID, field, value)
curr, _ := d.TopicInfo(ctx, []int64{topicID})
currTopic := curr[topicID]
d.UpdateTopic(ctx, topicID, field, originTopic.Desc)
convCtx.Convey("Then err should be nil.topics should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(currTopic.Desc, convey.ShouldEqual, value)
})
})
convCtx.Convey("update state", func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
field = "state"
value = model.TopicStateUnavailable
)
err := d.UpdateTopic(ctx, topicID, field, value)
curr, _ := d.TopicInfo(ctx, []int64{topicID})
currTopic := curr[topicID]
d.UpdateTopic(ctx, topicID, field, model.TopicStateAvailable)
convCtx.Convey("Then err should be nil.topics should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(currTopic.State, convey.ShouldEqual, value)
})
})
})
}
func TestDaoListUnAvailableTopics(t *testing.T) {
convey.Convey("ListUnAvailableTopics", t, func(convCtx convey.C) {
var (
ctx = context.Background()
page = int32(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
list, hasMore, err := d.ListUnAvailableTopics(ctx, page, model.CmsTopicSize)
log.Infow(ctx, "list", list, "has_more", hasMore)
convCtx.Convey("Then err should be nil.list,hasMore should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(list, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoListRankTopics(t *testing.T) {
convey.Convey("ListRankTopics", t, func(convCtx convey.C) {
var (
ctx = context.Background()
page = int32(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
list, hasMore, err := d.ListRankTopics(ctx, page, model.DiscoveryTopicSize)
log.Infow(ctx, "list", list, "topics", hasMore)
convCtx.Convey("Then err should be nil.list,hasMore should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(hasMore, convey.ShouldBeTrue)
convCtx.So(list, convey.ShouldNotBeNil)
})
})
convCtx.Convey("stick test", func(convCtx convey.C) {
originStickList, _ := d.GetStickTopic(ctx)
d.setStickTopic(ctx, []int64{111111110, 111111111, 111111112})
list, hasMore, err := d.ListRankTopics(ctx, 1, model.DiscoveryTopicSize)
log.Infow(ctx, "list", list, "has_more", hasMore)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(list[0], convey.ShouldEqual, 111111110)
convCtx.So(list[1], convey.ShouldEqual, 111111111)
convCtx.So(list[2], convey.ShouldEqual, 111111112)
convCtx.So(len(list), convey.ShouldEqual, 3+model.DiscoveryTopicSize)
// 恢复原来的置顶话题
if len(originStickList) > 0 {
d.setStickTopic(ctx, originStickList)
}
})
})
}
func TestDaogetStickTopic(t *testing.T) {
convey.Convey("GetStickTopic", t, func(convCtx convey.C) {
var (
ctx = context.Background()
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
list, err := d.GetStickTopic(ctx)
convCtx.Convey("Then err should be nil.list should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(list, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoStickTopic(t *testing.T) {
convey.Convey("StickTopic", t, func(convCtx convey.C) {
var (
ctx = context.Background()
opTopicID = int64(1)
op = int64(1)
)
originStickList, _ := d.GetStickTopic(ctx)
convCtx.Convey("common stick operate", func(convCtx convey.C) {
err := d.StickTopic(ctx, opTopicID, op)
newStickList, _ := d.GetStickTopic(ctx)
log.V(1).Infow(ctx, "new_stick_list", newStickList)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(newStickList[0], convey.ShouldEqual, 1)
})
convCtx.Convey("common cancel stick operate", func(convCtx convey.C) {
err := d.StickTopic(ctx, opTopicID, 0)
newCancelStickList, _ := d.GetStickTopic(ctx)
log.V(1).Infow(ctx, "new_cancel_stick_list", newCancelStickList)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(newCancelStickList[0], convey.ShouldNotEqual, 1)
})
convCtx.Convey("stick num test", func(convCtx convey.C) {
for i := 1; i < model.MaxStickTopicNum+3; i++ {
d.StickTopic(ctx, int64(i), 1)
}
list, _ := d.GetStickTopic(ctx)
log.V(1).Infow(ctx, "list", list)
convCtx.So(len(list), convey.ShouldEqual, model.MaxStickTopicNum)
})
// 恢复原来的置顶话题
if len(originStickList) > 0 {
d.setStickTopic(ctx, originStickList)
}
})
}

View File

@@ -0,0 +1,276 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/xstr"
)
const (
_insertTopicVideo = "insert ignore into topic_video (`topic_id`,`svid`,`state`) values %s"
_updateScore = "update topic_video set score=? where svid=%d"
_updateState = "update topic_video set state=? where svid=%d"
_selectTopicVideo = "select svid from topic_video where topic_id=%d and state=0 %s order by score desc limit %d,%d"
_selectVideoTopic = "select topic_id, score, state from topic_video where svid=%d"
)
// InsertTopicVideo 插入topic video表
func (d *Dao) InsertTopicVideo(ctx context.Context, svid int64, topicIDs []int64) (rowsAffected int64, err error) {
var str string
for _, topicID := range topicIDs {
if len(str) != 0 {
str += ","
}
str += fmt.Sprintf("(%d,%d,%d)", topicID, svid, api.TopicVideoStateUnAvailable)
}
insertSQL := fmt.Sprintf(_insertTopicVideo, str)
res, err := d.db.Exec(ctx, insertSQL)
if err != nil {
log.Errorw(ctx, "log", "insert topic_video fail", "svid", svid, "topic_ids", topicIDs)
return
}
rowsAffected, tmpErr := res.RowsAffected()
if tmpErr != nil {
log.Errorw(ctx, "log", "get rows affected fail", "svid", svid, "topic_ids", topicIDs)
}
log.V(1).Infow(ctx, "log", "insert one video topics", "svid", svid, "topics", topicIDs)
return
}
// UpdateVideoScore 更新视频的score
// @param topicID: 携带的时候会修改指定的topicID的video的score否则会全部修改
func (d *Dao) UpdateVideoScore(ctx context.Context, svid int64, score float64) (err error) {
updateSQL := fmt.Sprintf(_updateScore, svid)
_, err = d.db.Exec(ctx, updateSQL, score)
if err != nil {
log.Errorw(ctx, "log", "update topic video score fail", "svid", svid, "score", score)
return
}
return
}
// UpdateVideoState 更新视频的state
func (d *Dao) UpdateVideoState(ctx context.Context, svid int64, state int32) (err error) {
updateSQL := fmt.Sprintf(_updateState, svid)
_, err = d.db.Exec(ctx, updateSQL, state)
if err != nil {
log.Errorw(ctx, "log", "update topic video score fail", "svid", svid, "state", state)
return
}
return
}
// ListTopicVideos 获取话题下排序的视频列表
// 按道理来说Dao层不应该有那么多的复杂逻辑的但是redis、db等操作在业务本身就是耦合在一起的因此移到dao层简化逻辑操作
// TODO: 这里把置顶的数据放在了redis里所以导致排序问题过于复杂待修正
func (d *Dao) ListTopicVideos(ctx context.Context, topicID int64, cursorPrev, cursorNext string, size int) (res []*api.VideoItem, hasMore bool, err error) {
hasMore = true
// 0. check
if topicID == 0 {
log.Errorw(ctx, "log", "topic_id=0")
return
}
// 0.1 获取cursor和direction
cursor, directionNext, err := parseCursor(ctx, cursorPrev, cursorNext)
if err != nil {
log.Warnw(ctx, "log", "parse cursor fail", "prev", cursorPrev, "next", cursorNext)
return
}
// 1. 获取置顶视频
stickSvid, err := d.GetStickTopicVideo(ctx, topicID)
if err != nil {
log.Warnw(ctx, "log", "get stick topic video fail")
// 获取置顶视频失败后,属于可失败事件,继续往下走
}
stickMap := make(map[int64]bool)
additionalConditionSQL := ""
if len(stickSvid) > 0 {
additionalConditionSQL = fmt.Sprintf("and svid not in (%s)", xstr.JoinInts(stickSvid))
for _, svid := range stickSvid {
stickMap[svid] = true
}
}
// 2. 查询db
var svids []int64
dbOffset := cursor.Offset
limit := size
var rows *sql.Rows
// 有两种情况才需要请求db1、directionNext2、directionPrev && stickRank==0
if directionNext || cursor.StickRank == 0 {
if !directionNext {
dbOffset = cursor.Offset - 1 - size
if dbOffset < 0 {
dbOffset = 0
limit = cursor.Offset - 1
}
}
querySQL := fmt.Sprintf(_selectTopicVideo, topicID, additionalConditionSQL, dbOffset, limit)
log.V(1).Infow(ctx, "log", "select topic video", "sql", querySQL)
rows, err = d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get topic video error", "err", err, "sql", querySQL)
return
}
defer rows.Close()
for rows.Next() {
var svid int64
if err = rows.Scan(&svid); err != nil {
log.Errorw(ctx, "log", "get topic from mysql fail", "sql", querySQL)
return
}
svids = append(svids, svid)
}
log.V(1).Infow(ctx, "log", "get topic video", "svid", svids)
}
// 3. 组装回包
if directionNext {
if dbOffset == 0 {
index := 0
if cursor.StickRank != 0 {
index = cursor.StickRank
}
for ; index < len(stickSvid); index++ {
data, _ := json.Marshal(model.CursorValue{StickRank: index + 1})
res = append(res, &api.VideoItem{Svid: stickSvid[index], CursorValue: string(data)})
}
}
for index, svid := range svids {
data, _ := json.Marshal(model.CursorValue{Offset: dbOffset + 1 + index})
res = append(res, &api.VideoItem{Svid: svid, CursorValue: string(data)})
}
// TODO为了避免db查询量过大这里做限制
if len(svids) != limit || dbOffset > model.MaxTopicVideoOffset {
hasMore = false
}
} else {
for index := len(svids) - 1; index >= 0; index-- {
data, _ := json.Marshal(model.CursorValue{Offset: dbOffset + 1 + index})
res = append(res, &api.VideoItem{Svid: svids[index], CursorValue: string(data)})
}
// 如果dbOffset==0我们会把stick的视频页附上
if dbOffset == 0 {
index := len(stickSvid) - 1
if cursor.StickRank != 0 {
index = cursor.StickRank - 2
}
for ; index >= 0; index-- {
data, _ := json.Marshal(model.CursorValue{StickRank: index + 1})
res = append(res, &api.VideoItem{Svid: stickSvid[index], CursorValue: string(data)})
}
hasMore = false
}
}
// 4. 添加hot_type结果
for _, videoItem := range res {
if _, exists := stickMap[videoItem.Svid]; exists {
videoItem.HotType = api.TopicHotTypeStick
}
}
return
}
// GetVideoTopic 获取视频的话题列表
func (d *Dao) GetVideoTopic(ctx context.Context, svid int64) (list []*api.TopicVideoItem, err error) {
querySQL := fmt.Sprintf(_selectVideoTopic, svid)
rows, err := d.db.Query(ctx, querySQL)
if err != nil {
log.Errorw(ctx, "log", "get video topic_id fail", "svid", svid)
return
}
defer rows.Close()
for rows.Next() {
item := new(api.TopicVideoItem)
item.Svid = svid
if err = rows.Scan(&item.TopicId, &item.Score, &item.State); err != nil {
log.Errorw(ctx, "log", "get topic video fail", "svid", svid)
return
}
list = append(list, item)
}
log.V(1).Infow(ctx, "log", "get video topic", "sql", querySQL)
return
}
// GetStickTopicVideo 获取置顶视频
func (d *Dao) GetStickTopicVideo(ctx context.Context, topicID int64) (list []int64, err error) {
return d.getRedisList(ctx, fmt.Sprintf(model.ReidsStickTopicVideoKey, topicID))
}
// SetStickTopicVideo 设置置顶视频
func (d *Dao) SetStickTopicVideo(ctx context.Context, topicID int64, list []int64) (err error) {
return d.setRedisList(ctx, fmt.Sprintf(model.ReidsStickTopicVideoKey, topicID), list)
}
// StickTopicVideo 操作置顶视频
func (d *Dao) StickTopicVideo(ctx context.Context, opTopicID, opSvid, op int64) (err error) {
// 0. check
topicVideoItems, err := d.GetVideoTopic(ctx, opSvid)
if err != nil {
log.Warnw(ctx, "log", "get svid topic topicVideoItems fail", "topic_id", opTopicID)
return
}
var topicVideoItem *api.TopicVideoItem
for _, item := range topicVideoItems {
if item.TopicId == opTopicID {
topicVideoItem = item
break
}
}
if topicVideoItem == nil {
log.Errorw(ctx, "log", "stick topic fail due to error topic_id", "topic_id", opTopicID)
err = ecode.TopicIDNotFound
return
}
if topicVideoItem.State != api.TopicVideoStateAvailable {
log.Errorw(ctx, "log", "topic video state unavailable to do sticking", "state", topicVideoItem.State, "topic_id", opTopicID)
err = ecode.TopicVideoStateErr
return
}
// 1. 获取stick topic video
stickList, err := d.GetStickTopicVideo(ctx, opTopicID)
if err != nil {
log.Warnw(ctx, "log", "get stick topic video fail")
return
}
// 2. 操作stick topic video
var newStickList []int64
if op != 0 {
newStickList = append(newStickList, opSvid)
}
for _, stickSvid := range stickList {
if stickSvid != opSvid {
newStickList = append(newStickList, stickSvid)
}
}
if len(newStickList) > model.MaxStickTopicVideoNum {
newStickList = newStickList[:model.MaxStickTopicVideoNum]
}
// 3. 更新stick topic video
err = d.SetStickTopicVideo(ctx, opTopicID, newStickList)
if err != nil {
log.Warnw(ctx, "update stick topic video fail")
return
}
return
}

View File

@@ -0,0 +1,256 @@
package dao
import (
"context"
"encoding/json"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/log"
"math/rand"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoInsertTopicVideo(t *testing.T) {
convey.Convey("InsertTopicVideo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = rand.Int63() % 1000000
topicIDs = []int64{1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
rowsAffected, err := d.InsertTopicVideo(ctx, svid, topicIDs)
convCtx.Convey("Then err should be nil.rowsAffected should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(rowsAffected, convey.ShouldEqual, 1)
})
})
})
}
func TestDaoUpdateVideoScore(t *testing.T) {
convey.Convey("UpdateVideoScore", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(1)
score = float64(1.0)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
err := d.UpdateVideoScore(ctx, svid, score)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoUpdateVideoState(t *testing.T) {
convey.Convey("UpdateVideoState", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(1)
state = int32(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
err := d.UpdateVideoState(ctx, svid, state)
convCtx.Convey("Then err should be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDaoListTopicVideos(t *testing.T) {
convey.Convey("ListTopicVideos", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
)
res, hasMore, err := d.ListTopicVideos(ctx, topicID, "", "", model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res)
convCtx.Convey("Then err should be nil.res,hasMore should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(hasMore, convey.ShouldNotBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
originStickList, _ := d.GetStickTopicVideo(ctx, topicID)
newStickList := []int64{1, 2, 3, 4, 5, 6}
d.SetStickTopicVideo(ctx, topicID, newStickList)
var data []byte
convCtx.Convey("cursor_in_rank && direction_next", func(convCtx convey.C) {
data, _ = json.Marshal(model.CursorValue{Offset: 0, StickRank: 2})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, "", string(data), model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
convCtx.So(res[0].Svid, convey.ShouldEqual, 3)
convCtx.So(res[1].Svid, convey.ShouldEqual, 4)
convCtx.So(res[2].Svid, convey.ShouldEqual, 5)
convCtx.So(res[3].Svid, convey.ShouldEqual, 6)
// 检验cursor值是否符合要求
var unmarshalCursor model.CursorValue
json.Unmarshal([]byte(res[3].CursorValue), &unmarshalCursor)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 6)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 0)
json.Unmarshal([]byte(res[4].CursorValue), &unmarshalCursor)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 0)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 1)
convCtx.So(len(res), convey.ShouldEqual, model.TopicVideoSize+4)
convCtx.So(hasMore, convey.ShouldBeTrue)
convCtx.So(res, convey.ShouldNotBeNil)
})
convCtx.Convey("cursor_in_rank && direction_prev", func(convCtx convey.C) {
data, _ = json.Marshal(model.CursorValue{Offset: 0, StickRank: 4})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, string(data), "", model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
convCtx.So(res[0].Svid, convey.ShouldEqual, 3)
convCtx.So(res[1].Svid, convey.ShouldEqual, 2)
convCtx.So(res[2].Svid, convey.ShouldEqual, 1)
convCtx.So(len(res), convey.ShouldEqual, 3)
convCtx.So(hasMore, convey.ShouldBeFalse)
// 边缘情况选择了rank=1的视频
data, _ = json.Marshal(model.CursorValue{Offset: 0, StickRank: 1})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, string(data), "", model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
convCtx.So(len(res), convey.ShouldEqual, 0)
convCtx.So(hasMore, convey.ShouldBeFalse)
})
convCtx.Convey("direction_prev", func(convCtx convey.C) {
data, _ = json.Marshal(model.CursorValue{Offset: 1, StickRank: 0})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, string(data), "", model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
convCtx.So(len(res), convey.ShouldEqual, len(newStickList))
convCtx.So(hasMore, convey.ShouldBeFalse)
data, _ = json.Marshal(model.CursorValue{Offset: 5, StickRank: 0})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, string(data), "", model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
// 检验cursor值是否符合要求
var unmarshalCursor model.CursorValue
json.Unmarshal([]byte(res[0].CursorValue), &unmarshalCursor)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 0)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 4)
json.Unmarshal([]byte(res[1].CursorValue), &unmarshalCursor)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 0)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 3)
})
convCtx.Convey("direction_next", func(convCtx convey.C) {
data, _ = json.Marshal(model.CursorValue{Offset: 1, StickRank: 0})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, "", string(data), model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
convCtx.So(len(res), convey.ShouldEqual, int(model.TopicVideoSize))
convCtx.So(hasMore, convey.ShouldBeTrue)
data, _ = json.Marshal(model.CursorValue{Offset: 5, StickRank: 0})
res, hasMore, err = d.ListTopicVideos(ctx, topicID, "", string(data), model.TopicVideoSize)
log.V(1).Infow(ctx, "res", res, "has_more", hasMore)
// 检验cursor值是否符合要求
var unmarshalCursor model.CursorValue
json.Unmarshal([]byte(res[0].CursorValue), &unmarshalCursor)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 0)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 6)
json.Unmarshal([]byte(res[1].CursorValue), &unmarshalCursor)
convCtx.So(unmarshalCursor.StickRank, convey.ShouldEqual, 0)
convCtx.So(unmarshalCursor.Offset, convey.ShouldEqual, 7)
})
// 恢复原来的置顶话题
if len(originStickList) > 0 {
d.SetStickTopicVideo(ctx, topicID, originStickList)
}
})
}
func TestDaogetStickTopicVideo(t *testing.T) {
convey.Convey("GetStickTopicVideo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
list, err := d.GetStickTopicVideo(ctx, topicID)
log.V(1).Infow(ctx, "list", list)
convCtx.Convey("Then err should be nil.list should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(list, convey.ShouldNotBeNil)
})
})
})
}
func TestDaoStickTopicVideo(t *testing.T) {
convey.Convey("StickTopicVideo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
opTopicID = int64(1)
opSvid = int64(1)
op = int64(1)
)
originStickList, _ := d.GetStickTopicVideo(ctx, opTopicID)
convCtx.Convey("common stick operate", func(convCtx convey.C) {
err := d.StickTopicVideo(ctx, opTopicID, opSvid, op)
newStickList, _ := d.GetStickTopicVideo(ctx, opTopicID)
log.V(1).Infow(ctx, "new_stick_list", newStickList)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(newStickList[0], convey.ShouldEqual, 1)
})
convCtx.Convey("common cancel stick operate", func(convCtx convey.C) {
err := d.StickTopicVideo(ctx, opTopicID, opSvid, 0)
newCancelStickList, _ := d.GetStickTopicVideo(ctx, opTopicID)
log.V(1).Infow(ctx, "new_cancel_stick_list", newCancelStickList)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(newCancelStickList[0], convey.ShouldNotEqual, 1)
})
convCtx.Convey("stick num test", func(convCtx convey.C) {
for i := 1; i < model.MaxStickTopicVideoNum+3; i++ {
d.StickTopicVideo(ctx, opTopicID, int64(i), 1)
}
list, _ := d.GetStickTopicVideo(ctx, opTopicID)
log.V(1).Infow(ctx, "list", list)
convCtx.So(len(list), convey.ShouldEqual, model.MaxStickTopicVideoNum)
})
// 恢复原来的置顶话题
if len(originStickList) > 0 {
d.SetStickTopicVideo(ctx, opTopicID, originStickList)
}
})
}
func TestDaoGetVideoTopic(t *testing.T) {
convey.Convey("GetVideoTopic", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(1547635456050324977)
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := d.GetVideoTopic(ctx, svid)
data, _ := json.Marshal(res)
log.V(1).Infow(ctx, "res", string(data))
convCtx.Convey("Then err should be nil.list should not be nil.", func(convCtx convey.C) {
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(res, convey.ShouldNotBeNil)
})
})
})
}