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,22 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/bbq/topic/api:all-srcs",
"//app/service/bbq/topic/cmd:all-srcs",
"//app/service/bbq/topic/internal/dao:all-srcs",
"//app/service/bbq/topic/internal/model:all-srcs",
"//app/service/bbq/topic/internal/server/grpc:all-srcs",
"//app/service/bbq/topic/internal/server/http:all-srcs",
"//app/service/bbq/topic/internal/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,41 @@
# v1.0.10
修复话题列表时没有去重置顶话题的bug
修复mysql的一个坑order by score会乱序无法limit offset
# v1.0.9
设置话题视频的置顶改成json
修复取消最后一个置顶的bug
把话题视频置顶设置改成json
自动生成cache有个坑会异步加缓存把这个改成同步
# v1.0.8
即使0也返回字段
# v1.0.7
cms话题列表提供hot_type
增加获取话题列表的接口
# v1.0.6
话题修改默认图片
# v1.0.5
增加设置话题置顶接口(直接替换)
话题增加封面图字段t
# v1.0.4
去除min_version客户端对于无法解析的schema会忽略
增加svid查找topic_id
置顶话题的检查,保证存在才会置顶
发现页新逻辑,只返回置顶
# v1.0.3
添加置顶信息
# v1.0.2
修复主从同步延时导致的获取topic_id失败问题
# v1.0.1
cms接口功能
# v1.0.0
1. 上线功能

View File

@@ -0,0 +1,6 @@
# Owner
luxiaowei
# Author
# Reviewer

View File

@@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- luxiaowei
labels:
- bbq
- service
- service/bbq/topic
options:
no_parent_owners: true

View File

@@ -0,0 +1,12 @@
# topic-service
## 项目简介
1.
## 编译环境
## 依赖包
## 编译执行

View File

@@ -0,0 +1,68 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
proto_library(
name = "api_proto",
srcs = ["api.proto"],
tags = ["automanaged"],
deps = [
"@com_google_protobuf//:empty_proto",
"@gogo_special_proto//github.com/gogo/protobuf/gogoproto",
],
)
go_proto_library(
name = "api_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_grpc"],
importpath = "go-common/app/service/bbq/topic/api",
proto = ":api_proto",
tags = ["automanaged"],
deps = [
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@io_bazel_rules_go//proto/wkt:empty_go_proto",
],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"common.go",
],
embed = [":api_go_proto"],
importpath = "go-common/app/service/bbq/topic/api",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/net/rpc/warden:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@io_bazel_rules_go//proto/wkt:empty_go_proto",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context: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"],
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
syntax = "proto3";
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
package bbq.service.topic.v1;
option go_package = "api";
option (gogoproto.goproto_getters_all) = false;
message ListExtensionReq {
repeated int64 svids = 1;
}
message ListExtensionReply {
repeated VideoExtension list = 1;
}
message UpdateVideoScoreReq {
int64 svid = 1;
double score = 2;
}
message UpdateVideoStateReq {
int64 svid = 1;
int32 state = 2;
}
message TopicVideosReq {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id"'];
string cursor_prev = 2[(gogoproto.moretags)='form:"cursor_prev"'];
string cursor_next = 3[(gogoproto.moretags)='form:"cursor_next"'];
}
message ListMultiTopicVideosReq {
repeated TopicVideosReq list = 1;
}
message VideoItem {
int64 svid = 1;
string cursor_value = 2;
int64 hot_type = 3;
}
message TopicDetail {
TopicInfo topic_info = 1;
repeated VideoItem list = 2;
bool has_more = 3[(gogoproto.jsontag) = "has_more"];
}
message ListDiscoveryTopicReq {
int32 page = 1[(gogoproto.moretags)='form:"page" validate:"required"'];
}
message ListDiscoveryTopicReply {
repeated TopicDetail list = 1;
bool has_more = 2;
}
message ListTopicsReq {
int32 page = 1[(gogoproto.moretags)='form:"page" validate:"required"'];
}
message ListTopicsReply {
bool has_more = 1[(gogoproto.jsontag) = "has_more"];
repeated TopicInfo list = 2;
}
message StickTopicReq {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id" validate:"required"'];
int64 op = 2[(gogoproto.moretags)='form:"op"'];// 0表示取消置顶1表示置顶
}
message StickTopicVideoReq {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id" validate:"required"'];
int64 svid = 2[(gogoproto.moretags)='form:"svid" validate:"required"'];
int64 op = 3[(gogoproto.moretags)='form:"op"'];// 0表示取消置顶1表示置顶
}
message SetStickTopicVideoReq {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id" validate:"required"'];
repeated int64 svids = 2[(gogoproto.moretags)='form:"svids"'];
}
// 所有查找走同一个接口
message ListCmsTopicsReq {
int32 page = 1[(gogoproto.moretags)='form:"page" validate:"required"'];
string name = 2[(gogoproto.moretags)='form:"name"'];
int64 topic_id = 3[(gogoproto.moretags)='form:"topic_id"'];
int32 state = 4[(gogoproto.moretags)='form:"state"'];
}
message ListCmsTopicsReply {
bool has_more = 1[(gogoproto.jsontag) = "has_more"];
repeated TopicInfo list = 2;
}
service Topic {
////////////////extension////////////////
rpc Register (VideoExtension) returns (.google.protobuf.Empty);
rpc ListExtension (ListExtensionReq) returns (ListExtensionReply);
//////////////////topic///////////////////
rpc UpdateVideoScore(UpdateVideoScoreReq) returns (.google.protobuf.Empty);
rpc UpdateVideoState(UpdateVideoStateReq) returns (.google.protobuf.Empty);
// 获取话题下的视频
rpc ListTopicVideos (TopicVideosReq) returns (TopicDetail);
// TODO: to deleted. 发现页分成先请求话题,再请求话题下视频
// 获取发现页下的话题
rpc ListDiscoveryTopics (ListDiscoveryTopicReq) returns (ListDiscoveryTopicReply);
// 推荐的话题,不会返回话题内的视频
rpc ListTopics (ListTopicsReq) returns (ListTopicsReply);
///// cms /////
rpc StickTopic(StickTopicReq) returns (.google.protobuf.Empty);
// 置顶、取消置顶话题下的视频需要cms先去判断该视频是否可以放在话题中
rpc StickTopicVideo(StickTopicVideoReq) returns (.google.protobuf.Empty);
rpc SetStickTopicVideo(SetStickTopicVideoReq) returns (.google.protobuf.Empty);
// 注意:该接口不支持复杂条件,当以下三种同时请求的时候只会按顺序选择一种进行返回:
// 话题name查找接口不区分隐藏返回数组但是暂时只会完全匹配
// 话题隐藏列表接口,按照时间反向排序
// 话题推荐列表接口,按照热度反向排序,其中第一页会包含置顶话题
rpc ListCmsTopics(ListCmsTopicsReq) returns (ListCmsTopicsReply);
// 修改话题简介topic_id必传传直接放在http请求里
rpc UpdateTopicDesc(TopicInfo) returns (.google.protobuf.Empty);
rpc UpdateTopicState(TopicInfo) returns (.google.protobuf.Empty);
// 话题详情页,含话题信息+视频信息
// 使用video-c的topic/detail接口php内部转换成cms逻辑
// 根据svid返回topic_id列表
rpc VideoTopic(VideoTopicReq) returns (VideoTopicReply);
}
message TopicVideoItem {
int64 topic_id = 1;
int64 svid = 2;
double score = 3;
int32 state = 4;
}
message VideoTopicReq {
int64 svid = 1[(gogoproto.moretags)='form:"svid"'];
}
message VideoTopicReply {
repeated TopicVideoItem list = 1;
}
message UpdateTopicStateReq {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id"'];
int32 op = 2[(gogoproto.moretags)='form:"op"'];// 0表示通过1表示下架
}
message TopicInfo {
int64 topic_id = 1[(gogoproto.moretags)='form:"topic_id"'];
string name = 2;
string desc = 3[(gogoproto.moretags)='form:"desc"'];
double score = 4;
int32 state = 5[(gogoproto.moretags)='form:"state"'];// 0表示通过1表示下架
int32 hot_type = 6[(gogoproto.jsontag)="hot_type"];
string cover_url = 7[(gogoproto.moretags)='form:"cover_url"'];
}
message TitleExtraItem {
int64 id = 1[(gogoproto.jsontag) = "id"];
int64 type = 2[(gogoproto.jsontag) = "type"];
string name = 3[(gogoproto.jsontag) = "name"];
int64 start = 4[(gogoproto.jsontag) = "start"];
int64 end = 5[(gogoproto.jsontag) = "end"];
string scheme = 6[(gogoproto.jsontag) = "scheme"];
}
// 用于传递给上游的extension通过序列化赋值给extension
message VideoExtension {
int64 svid = 1;
string extension = 2; // 序列化后的Extension会放在这里
}
// 结构化的extension真正的extension
message Extension {
repeated TitleExtraItem title_extra = 1;
}

View File

@@ -0,0 +1,20 @@
package api
import (
"context"
"go-common/library/net/rpc/warden"
"google.golang.org/grpc"
)
// AppID unique app id for service discovery
const AppID = "bbq.service.topic"
// NewClient new member grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (TopicClient, error) {
client := warden.NewClient(cfg, opts...)
conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
if err != nil {
return nil, err
}
return NewTopicClient(conn), nil
}

View File

@@ -0,0 +1,34 @@
package api
import (
"context"
"encoding/json"
"go-common/library/log"
)
// Transform2Interface 转换成interface
func Transform2Interface(ctx context.Context, data []byte) (inter interface{}, err error) {
err = json.Unmarshal(data, &inter)
if err != nil {
log.Errorw(ctx, "log", "transform to interface fail", "data", string(data))
return
}
return
}
// 话题的状态
const (
TopicStateAvailable = 0
TopicStateUnAvailable = 1
TopicVideoStateAvailable = 0
TopicVideoStateUnAvailable = 1
)
// 话题热门类型的enum用于TopicInfo->HotType字段
// 开始时使用了hot_type但其实就是表示特殊的话题状态
const (
TopicHotTypeHot = 1 // 热门
TopicHotTypeHistory = 2 // 历史,暂时只有客户端使用
TopicHotTypeStick = 4 // 置顶
)

View File

@@ -0,0 +1,43 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "go-common/app/service/bbq/topic/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/bbq/topic/internal/server/grpc:go_default_library",
"//app/service/bbq/topic/internal/server/http:go_default_library",
"//app/service/bbq/topic/internal/service:go_default_library",
"//library/conf/paladin:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log: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"],
)

View File

@@ -0,0 +1,51 @@
package main
import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/service/bbq/topic/internal/server/grpc"
"go-common/app/service/bbq/topic/internal/server/http"
"go-common/app/service/bbq/topic/internal/service"
"go-common/library/conf/paladin"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
)
func main() {
flag.Parse()
if err := paladin.Init(); err != nil {
panic(err)
}
log.Init(nil) // debug flag: log.dir={path}
defer log.Close()
log.Info("topic-service start")
ecode.Init(nil)
svc := service.New()
grpcSrv := grpc.New(svc)
httpSrv := http.New(svc)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second)
grpcSrv.Shutdown(ctx)
httpSrv.Shutdown(ctx)
log.Info("topic-service exit")
svc.Close()
time.Sleep(time.Second)
cancel()
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,2 @@
# This is a TOML document. Boom

View File

@@ -0,0 +1,4 @@
[server]
addr = "0.0.0.0:9005"
timeout = "1s"

View File

@@ -0,0 +1,4 @@
[server]
addr = "0.0.0.0:8000"
timeout = "1s"

View File

@@ -0,0 +1,11 @@
[topic]
addr = "172.16.38.91:3306"
dsn = "root:123456@tcp(172.16.38.91:3306)/bbq?allowNativePasswords=true&timeout=800ms&readTimeout=1200ms&writeTimeout=800ms&parseTime=true&loc=Local&charset=utf8,utf8mb4"
readDSN = ["root:123456@tcp(172.16.38.91:3306)/bbq?allowNativePasswords=true&timeout=800ms&readTimeout=1200ms&writeTimeout=800ms&parseTime=true&loc=Local&charset=utf8,utf8mb4"]
active = 20
idle = 10
idleTimeout ="4h"
queryTimeout = "1000ms"
execTimeout = "2000ms"
tranTimeout = "4000ms"

View File

@@ -0,0 +1,13 @@
topicExpire = "10m"
[topic]
name = "topic"
proto = "tcp"
addr = "172.16.38.91:6379"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"

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

View File

@@ -0,0 +1,31 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"extension.go",
"model.go",
],
importpath = "go-common/app/service/bbq/topic/internal/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,11 @@
package model
// extension_type 会存在数据库中
const (
ExtensionTypeTitleExtra = 1
)
// title_extra type 针对title_extra结构而言
const (
TitleExtraTypeTopic = 1
)

View File

@@ -0,0 +1,38 @@
package model
// 一些常量
const (
MaxBatchLen = 10
MaxTopicNameLen = 64
MaxTopicDescLen = 256
MaxSvTopicNum = 15
MaxTopicVideoLen = 10
MaxTopicLen = 10
TopicVideoSize = 10
DiscoveryTopicVideoSize = 6
DiscoveryTopicSize = 3
CmsTopicSize = 10
MaxDiscoveryTopicPage = 300
MaxTopicVideoOffset = 1000
MaxStickTopicNum = 10
MaxStickTopicVideoNum = 6
)
// Topic状态
const (
TopicStateAvailable = 0
TopicStateUnavailable = 1
)
// redis key format
const (
RedisStickTopicKey = "stick:topic"
ReidsStickTopicVideoKey = "stick:topic:video:%d"
)
// CursorValue 发现页下/话题详情页下的cursor
type CursorValue struct {
// 注意这里的offset=db_offset+1
Offset int `json:"offset"` // 默认值为0从1开始parseCursor中设置
StickRank int `json:"stick_rank"` // 默认值为0从1开始
}

View File

@@ -0,0 +1,34 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["server.go"],
importpath = "go-common/app/service/bbq/topic/internal/server/grpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/bbq/topic/api:go_default_library",
"//app/service/bbq/topic/internal/service:go_default_library",
"//library/conf/paladin:go_default_library",
"//library/net/rpc/warden: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"],
)

View File

@@ -0,0 +1,27 @@
package grpc
import (
pb "go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/service"
"go-common/library/conf/paladin"
"go-common/library/net/rpc/warden"
)
// New new a grpc server.
func New(svc *service.Service) *warden.Server {
var rc struct {
Server *warden.ServerConfig
}
if err := paladin.Get("grpc.toml").UnmarshalTOML(&rc); err != nil {
if err != paladin.ErrNotExist {
panic(err)
}
}
ws := warden.NewServer(rc.Server)
pb.RegisterTopicServer(ws.Server(), svc)
ws, err := ws.Start()
if err != nil {
panic(err)
}
return ws
}

View File

@@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["http.go"],
importpath = "go-common/app/service/bbq/topic/internal/server/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/bbq/topic/api:go_default_library",
"//app/service/bbq/topic/internal/service:go_default_library",
"//library/conf/paladin:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/binding:go_default_library",
"//library/net/http/blademaster/middleware/verify: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"],
)

View File

@@ -0,0 +1,133 @@
package http
import (
"go-common/app/service/bbq/topic/api"
"go-common/library/ecode"
"go-common/library/net/http/blademaster/binding"
"net/http"
"go-common/app/service/bbq/topic/internal/service"
"go-common/library/conf/paladin"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
svc *service.Service
)
// New new a bm server.
func New(s *service.Service) (engine *bm.Engine) {
var (
hc struct {
Server *bm.ServerConfig
}
)
if err := paladin.Get("http.toml").UnmarshalTOML(&hc); err != nil {
if err != paladin.ErrNotExist {
panic(err)
}
}
svc = s
engine = bm.DefaultServer(hc.Server)
initRouter(engine, verify.New(nil))
if err := engine.Start(); err != nil {
panic(err)
}
return
}
func initRouter(e *bm.Engine, v *verify.Verify) {
e.Ping(ping)
e.Register(register)
g := e.Group("/bbq/topic")
{
g.GET("/start", howToStart)
g.POST("/update/state", updateTopicState)
g.POST("/update/desc", updateTopicDesc)
g.POST("/stick", stickTopic)
g.POST("/video/stick", stickTopicVideo)
g.POST("/video/set/stick", setStickTopicVideo)
g.GET("/cms/list", cmsTopicList)
g.GET("/video", topicVideo)
}
}
func ping(ctx *bm.Context) {
if err := svc.Ping(ctx); err != nil {
log.Error("ping error(%v)", err)
ctx.AbortWithStatus(http.StatusServiceUnavailable)
}
}
func register(c *bm.Context) {
c.JSON(map[string]interface{}{}, nil)
}
func howToStart(c *bm.Context) {
c.String(0, "golang")
}
func updateTopicState(c *bm.Context) {
arg := &api.TopicInfo{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.UpdateTopicState(c, arg))
}
func updateTopicDesc(c *bm.Context) {
arg := &api.TopicInfo{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.UpdateTopicDesc(c, arg))
}
func cmsTopicList(c *bm.Context) {
arg := &api.ListCmsTopicsReq{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.ListCmsTopics(c, arg))
}
func topicVideo(c *bm.Context) {
arg := &api.ListCmsTopicsReq{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.ListCmsTopics(c, arg))
}
func stickTopic(c *bm.Context) {
arg := &api.StickTopicReq{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.StickTopic(c, arg))
}
func stickTopicVideo(c *bm.Context) {
arg := &api.StickTopicVideoReq{}
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.StickTopicVideo(c, arg))
}
func setStickTopicVideo(c *bm.Context) {
arg := &api.SetStickTopicVideoReq{}
if err := c.BindWith(arg, binding.JSON); err != nil {
c.JSON(nil, ecode.ReqParamErr)
return
}
c.JSON(svc.SetStickTopicVideo(c, arg))
}

View File

@@ -0,0 +1,61 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"extension.go",
"service.go",
"topic.go",
],
importpath = "go-common/app/service/bbq/topic/internal/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/bbq/topic/api:go_default_library",
"//app/service/bbq/topic/internal/dao: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",
"//library/sync/errgroup.v2:go_default_library",
"@io_bazel_rules_go//proto/wkt:empty_go_proto",
],
)
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 = [
"extension_test.go",
"service_test.go",
"topic_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/log:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,65 @@
package service
import (
"context"
"encoding/json"
"github.com/golang/protobuf/ptypes/empty"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/log"
)
// Register 注册extension信息包括关联话题等
func (s *Service) Register(ctx context.Context, req *api.VideoExtension) (res *empty.Empty, err error) {
res = new(empty.Empty)
svid := req.Svid
// 0. check
if svid == 0 {
log.Warnw(ctx, "log", "svid is 0")
return
}
if len(req.Extension) == 0 {
return
}
var extension api.Extension
err = json.Unmarshal([]byte(req.Extension), &extension)
if err != nil {
log.Errorw(ctx, "log", "unmarshal extension fail", "req", req)
return
}
// 2. 获取新的title_extra
extension.TitleExtra, err = s.registerTopic(ctx, svid, extension.TitleExtra)
if err != nil {
log.Warnw(ctx, "log", "regist topic fail")
return
}
// 3. 存入extension中
num, err := s.dao.InsertExtension(ctx, svid, model.ExtensionTypeTitleExtra, &extension)
if err != nil {
log.Warnw(ctx, "log", "insert extension fail", "extension", extension, "svid", svid)
return
}
// 插入无效主要是因为已经存在所以这里默认为成功但是打error日志
if num == 0 {
log.Errorw(ctx, "log", "insert extension fail due to svid already exists", "svid", svid)
}
return
}
// ListExtension 获取视频的extension信息
func (s *Service) ListExtension(ctx context.Context, req *api.ListExtensionReq) (res *api.ListExtensionReply, err error) {
res = new(api.ListExtensionReply)
videoExtensions, err := s.dao.VideoExtension(ctx, req.Svids)
if err != nil {
log.Warnw(ctx, "log", "get video extension fail", "svids", req.Svids)
return
}
for _, extension := range videoExtensions {
res.List = append(res.List, extension)
}
return
}

View File

@@ -0,0 +1,44 @@
package service
import (
"context"
"go-common/app/service/bbq/topic/api"
"go-common/library/log"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceRegister(t *testing.T) {
convey.Convey("Register", t, func(convCtx convey.C) {
var (
ctx = context.Background()
//req = &api.VideoExtension{Svid: 1, Extension: "{\"title_extra\":[{\"end\":5,\"topicId\":0,\"type\":1,\"name\":\"#Test\",\"start\":0}]}"}
req = &api.VideoExtension{Svid: 1, Extension: "{\"title_extra\":[{\"name\":\"Test\",\"start\":0, \"end\":4},{\"name\":\"test_333333\",\"start\":10, \"end\":18}]}"}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.Register(ctx, req)
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 TestServiceListExtension(t *testing.T) {
convey.Convey("ListExtension", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListExtensionReq{Svids: []int64{1}}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.ListExtension(ctx, req)
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)
})
})
})
}

View File

@@ -0,0 +1,36 @@
package service
import (
"context"
"go-common/app/service/bbq/topic/internal/dao"
"go-common/library/conf/paladin"
)
// Service service.
type Service struct {
ac *paladin.Map
dao *dao.Dao
}
// New new a service and return.
func New() (s *Service) {
var ac = new(paladin.TOML)
if err := paladin.Watch("application.toml", ac); err != nil {
panic(err)
}
s = &Service{
ac: ac,
dao: dao.New(),
}
return s
}
// Ping ping the resource.
func (s *Service) Ping(ctx context.Context) (err error) {
return s.dao.Ping(ctx)
}
// Close close the resource.
func (s *Service) Close() {
s.dao.Close()
}

View File

@@ -0,0 +1,37 @@
package service
import (
"flag"
"go-common/library/conf/paladin"
"go-common/library/log"
"os"
"testing"
)
var (
s *Service
)
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)
s = New()
os.Exit(m.Run())
}

View File

@@ -0,0 +1,446 @@
package service
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup.v2"
"strings"
)
// UpdateVideoScore 更新视频的score分
func (s *Service) UpdateVideoScore(ctx context.Context, req *api.UpdateVideoScoreReq) (res *empty.Empty, err error) {
res = new(empty.Empty)
err = s.dao.UpdateVideoScore(ctx, req.Svid, req.Score)
return
}
// UpdateVideoState 更新视频的状态databus消费会调用
func (s *Service) UpdateVideoState(ctx context.Context, req *api.UpdateVideoStateReq) (res *empty.Empty, err error) {
res = new(empty.Empty)
err = s.dao.UpdateVideoState(ctx, req.Svid, req.State)
return
}
// UpdateTopicDesc 更新话题信息,
func (s *Service) UpdateTopicDesc(ctx context.Context, in *api.TopicInfo) (res *empty.Empty, err error) {
res = new(empty.Empty)
// 0. check
if in.TopicId == 0 {
err = ecode.TopicIDErr
return
}
if strings.Count(in.Desc, "")-1 > model.MaxTopicDescLen {
err = ecode.TopicDescLenErr
log.Warnw(ctx, "log", "topic desc too long", "desc", in.Desc)
return
}
// 1. update
err = s.dao.UpdateTopic(ctx, in.TopicId, "desc", in.Desc)
if err != nil {
log.Warnw(ctx, "log", "update topic desc fail", "topic_id", in.TopicId)
}
return
}
// UpdateTopicState 更新话题信息,
func (s *Service) UpdateTopicState(ctx context.Context, in *api.TopicInfo) (res *empty.Empty, err error) {
res = new(empty.Empty)
// 0. check
if in.TopicId == 0 {
err = ecode.TopicIDErr
return
}
// 1. update
state := model.TopicStateAvailable
if in.State > 0 {
state = model.TopicStateUnavailable
}
err = s.dao.UpdateTopic(ctx, in.TopicId, "state", state)
if err != nil {
log.Warnw(ctx, "log", "update topic state fail", "topic_id", in.TopicId)
}
return
}
// VideoTopic 返回视频关联的所有话题信息cms使用当前只有topic_id
func (s *Service) VideoTopic(ctx context.Context, in *api.VideoTopicReq) (res *api.VideoTopicReply, err error) {
res = new(api.VideoTopicReply)
// 0. check
if in.Svid == 0 {
log.Errorw(ctx, "log", "param svid error")
err = ecode.ReqParamErr
return
}
// 1. 获取话题id列表
res.List, err = s.dao.GetVideoTopic(ctx, in.Svid)
if err != nil {
log.Warnw(ctx, "log", "get video topic fail")
return
}
return
}
// ListCmsTopics cms获取话题信息
func (s *Service) ListCmsTopics(ctx context.Context, in *api.ListCmsTopicsReq) (res *api.ListCmsTopicsReply, err error) {
res = new(api.ListCmsTopicsReply)
// 0. check
// 1. 查找
var topicIDs []int64
res.HasMore = false
stickTopic := make(map[int64]bool)
if len(in.Name) > 0 {
// 话题名搜索
var topics map[string]int64
if topics, err = s.dao.TopicID(ctx, []string{in.Name}); err != nil {
log.Warnw(ctx, "log", "get topic id fail", "name", in.Name)
return
}
topicID, exists := topics[in.Name]
if !exists {
log.Warnw(ctx, "log", "get topic id fail", "name", in.Name)
return
}
topicIDs = append(topicIDs, topicID)
} else if in.TopicId != 0 {
// topic_id搜索
topicIDs = append(topicIDs, in.TopicId)
} else if in.State == model.TopicStateUnavailable {
// 话题下架搜索
topicIDs, res.HasMore, err = s.dao.ListUnAvailableTopics(ctx, in.Page, model.CmsTopicSize)
if err != nil {
log.Warnw(ctx, "log", "get topic ids fail")
return
}
} else {
// 话题详情页搜索
topicIDs, res.HasMore, err = s.dao.ListRankTopics(ctx, in.Page, model.CmsTopicSize)
if err != nil {
log.Warnw(ctx, "log", "get topic ids fail")
return
}
}
// 2. 查找置顶数据
stickList, _ := s.dao.GetStickTopic(ctx)
for _, topicID := range stickList {
stickTopic[topicID] = true
}
// 3. 获取TopicInfo
topicInfos, err := s.dao.TopicInfo(ctx, topicIDs)
if err != nil {
log.Warnw(ctx, "log", "get topic info fail", "topic_ids", topicIDs)
return
}
for _, topicID := range topicIDs {
topicInfo, exists := topicInfos[topicID]
if !exists {
log.Errorw(ctx, "log", "get error topic id", "topic_id", topicID)
continue
}
if _, exists2 := stickTopic[topicID]; exists2 {
topicInfo.HotType = api.TopicHotTypeStick
}
res.List = append(res.List, topicInfo)
}
return
}
// ListDiscoveryTopics 用于发现页这里采用的是page的样式
// 这里其实可以考虑只返回话题列表由上游自行调用ListTopicVideo接口
func (s *Service) ListDiscoveryTopics(ctx context.Context, req *api.ListDiscoveryTopicReq) (res *api.ListDiscoveryTopicReply, err error) {
res = new(api.ListDiscoveryTopicReply)
page := req.Page
// 0. check
// 1. 获取话题列表
//老逻辑topicIDList, hasMore, err := s.dao.ListRankTopics(ctx, page, model.DiscoveryTopicSize)
// 新逻辑,只取置顶的话题
topicIDList, err := s.dao.GetStickTopic(ctx)
if err != nil {
log.Warnw(ctx, "log", "get recommend topic fail", "page", page)
return
}
res.HasMore = false // 永远为false只取置顶
if len(topicIDList) == 0 {
return
}
// 2. 获取话题信息
topicInfos, err := s.getAvailableTopicInfo(ctx, topicIDList)
if err != nil {
log.Warnw(ctx, "log", "get topic info fail", "topic_id", topicIDList)
return
}
// 3. 获取话题视频
topicDetails := make(map[int64]*api.TopicDetail, len(topicIDList))
topicDetailChan := make(chan *api.TopicDetail, len(topicIDList))
group := errgroup.WithCancel(ctx)
group.GOMAXPROCS(5)
var groupTopicVideo = func(topicID int64) {
group.Go(func(ctx context.Context) (err error) {
log.V(1).Infow(ctx, "log", "get one topic videos", "topic_id", topicID)
list, _, err := s.dao.ListTopicVideos(ctx, topicID, "", "", model.DiscoveryTopicVideoSize)
if err != nil {
log.Warnw(ctx, "log", "get topic videos fail", "topic_id", topicID)
return
}
topicDetailChan <- &api.TopicDetail{TopicInfo: &api.TopicInfo{TopicId: topicID}, List: list}
return
})
}
for _, topicID := range topicIDList {
groupTopicVideo(topicID)
}
err = group.Wait()
close(topicDetailChan)
if err != nil {
log.Warnw(ctx, "log", "group Go occurs error", "err", err)
return
}
for topicDetail := range topicDetailChan {
topicDetails[topicDetail.TopicInfo.TopicId] = topicDetail
}
// 4. 获取置顶topic_id
stickList, tmpErr := s.dao.GetStickTopic(ctx)
if tmpErr != nil {
log.Warnw(ctx, "log", "get stick topic fail")
}
stickMap := make(map[int64]bool)
for _, topicID := range stickList {
stickMap[topicID] = true
}
// 5. 组合回包
// 只有TopicInfo和video都存在才会返回给前级
for _, topicID := range topicIDList {
// 获取话题视频
topicDetail, exists := topicDetails[topicID]
if !exists {
log.Errorw(ctx, "log", "cannot find topic detail", "topic_id", topicID)
continue
}
if len(topicDetail.List) == 0 {
log.Warnw(ctx, "log", "video num is 0 in this topic", "topic", topicDetail)
continue
}
// 获取话题info
topicInfo, exists := topicInfos[topicID]
if !exists {
log.Errorw(ctx, "log", "cannot find topic info", "topic_id", topicID)
continue
}
// 设置置顶标志
if _, exists2 := stickMap[topicID]; exists2 {
topicInfo.HotType = api.TopicHotTypeStick
}
topicDetail.TopicInfo = topicInfo
res.List = append(res.List, topicDetail)
}
return
}
// ListTopics 话题列表
func (s *Service) ListTopics(ctx context.Context, req *api.ListTopicsReq) (res *api.ListTopicsReply, err error) {
res = new(api.ListTopicsReply)
page := &req.Page
// 0. check
// 1. 获取话题列表
// 新逻辑,只取置顶的话题
topicIDList, err := s.dao.GetStickTopic(ctx)
if err != nil {
log.Warnw(ctx, "log", "get recommend topic fail", "page", page)
return
}
res.HasMore = false // 永远为false只取置顶
if len(topicIDList) == 0 {
return
}
stickMap := make(map[int64]bool)
for _, topicID := range topicIDList {
stickMap[topicID] = true
}
// 2. 获取话题信息
topicInfos, err := s.getAvailableTopicInfo(ctx, topicIDList)
if err != nil {
log.Warnw(ctx, "log", "get topic info fail", "topic_id", topicIDList)
return
}
// 3. 组合回包
for _, topicID := range topicIDList {
// 获取话题info
topicInfo, exists := topicInfos[topicID]
if !exists {
log.Errorw(ctx, "log", "cannot find topic info", "topic_id", topicID)
continue
}
// 设置置顶标志
if _, exists2 := stickMap[topicID]; exists2 {
topicInfo.HotType = api.TopicHotTypeStick
}
res.List = append(res.List, topicInfo)
}
return
}
// ListTopicVideos 获取话题下的视频
func (s *Service) ListTopicVideos(ctx context.Context, req *api.TopicVideosReq) (res *api.TopicDetail, err error) {
res = new(api.TopicDetail)
// 0.check
if req.TopicId == 0 {
err = ecode.TopicIDErr
return
}
// 1. 获取话题信息
topicInfos, err := s.dao.TopicInfo(ctx, []int64{req.TopicId})
if err != nil {
log.Warnw(ctx, "log", "get topic info fail")
return
}
topicInfo, exists := topicInfos[req.TopicId]
if !exists {
err = ecode.TopicIDNotFound
return
}
res.TopicInfo = topicInfo
// 2. 获取话题内的视频
list, hasMore, err := s.dao.ListTopicVideos(ctx, req.TopicId, req.CursorPrev, req.CursorNext, model.TopicVideoSize)
if err != nil {
log.Warnw(ctx, "log", "get topic video list fail", "err", err, "req", req)
return
}
res.HasMore = hasMore
res.List = list
return
}
// StickTopic 话题的置顶、取消置顶操作
func (s *Service) StickTopic(ctx context.Context, in *api.StickTopicReq) (res *empty.Empty, err error) {
res = new(empty.Empty)
err = s.dao.StickTopic(ctx, in.TopicId, in.Op)
return
}
// StickTopicVideo 话题下视频的置顶、取消置顶操作
func (s *Service) StickTopicVideo(ctx context.Context, in *api.StickTopicVideoReq) (res *empty.Empty, err error) {
res = new(empty.Empty)
err = s.dao.StickTopicVideo(ctx, in.TopicId, in.Svid, in.Op)
return
}
// SetStickTopicVideo 替换话题下的置顶视频
func (s *Service) SetStickTopicVideo(ctx context.Context, in *api.SetStickTopicVideoReq) (res *empty.Empty, err error) {
res = new(empty.Empty)
err = s.dao.SetStickTopicVideo(ctx, in.TopicId, in.Svids)
if err != nil {
log.Warnw(ctx, "log", "set stick topic video fail")
return
}
return
}
func (s *Service) registerTopic(ctx context.Context, svid int64, list []*api.TitleExtraItem) (res []*api.TitleExtraItem, err error) {
// 0. 校验请求
if svid == 0 {
log.Warnw(ctx, "log", "svid=0")
return
}
if len(list) == 0 {
return
}
if len(list) > model.MaxSvTopicNum {
err = ecode.TopicTooManyInOneVideo
return
}
// 1 话题操作
// 1.0 插入新话题同时更新老话题这里dao层用on duplicate key就行
topics := make(map[string]*api.TopicInfo)
for _, item := range list {
topics[item.Name] = &api.TopicInfo{Name: item.Name}
}
// 插入同时获取话题ID
newTopics, err := s.dao.InsertTopics(ctx, topics)
if err != nil {
log.Warnw(ctx, "log", "insert topic fail")
return
}
// 2. 视频插入topic_video
var topicIDs []int64
for _, topicInfo := range newTopics {
topicIDs = append(topicIDs, topicInfo.TopicId)
}
num, err := s.dao.InsertTopicVideo(ctx, svid, topicIDs)
if err != nil {
log.Warnw(ctx, "log", "insert topic video fail")
return
}
// 打error日志但是这个情况是可以接受的保持幂等即可
if int(num) != len(topicIDs) {
log.Errorw(ctx, "log", "insert topic_video num not match", "rows_affected", num, "topic_num", len(topicIDs), "svid", svid)
}
// 3. 返回新的数组,结构补充完整
for _, item := range list {
if topicInfo, exists := newTopics[item.Name]; exists {
item.Scheme = fmt.Sprintf("qing://topic?topic_id=%d", topicInfo.TopicId)
} else {
log.Errorw(ctx, "log", "get topic id fail", "name", item.Name)
}
}
res = list
return
}
// 用于获取状态可见的话题,为的是和审核分开
func (s *Service) getAvailableTopicInfo(c context.Context, keys []int64) (res map[int64]*api.TopicInfo, err error) {
res, err = s.dao.TopicInfo(c, keys)
if err != nil {
log.Warnw(c, "log", "get topic info fail")
return
}
var toDeletedTopic []int64
for topicID, topicInfo := range res {
if topicInfo.State == api.TopicStateUnAvailable {
toDeletedTopic = append(toDeletedTopic, topicID)
}
}
for _, topicID := range toDeletedTopic {
log.Warnw(c, "log", "get one unavailable topic", "topic", res[topicID])
delete(res, topicID)
}
return
}

View File

@@ -0,0 +1,303 @@
package service
import (
"context"
"encoding/json"
"fmt"
"go-common/app/service/bbq/topic/api"
"go-common/app/service/bbq/topic/internal/model"
"go-common/library/log"
"math/rand"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceUpdateVideoScore(t *testing.T) {
convey.Convey("UpdateVideoScore", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.UpdateVideoScoreReq{Svid: 1, Score: 0.33}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.UpdateVideoScore(ctx, req)
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 TestServiceUpdateVideoState(t *testing.T) {
convey.Convey("UpdateVideoState", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.UpdateVideoStateReq{Svid: 1, State: 0}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.UpdateVideoState(ctx, req)
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 TestServiceUpdateTopicDesc(t *testing.T) {
convey.Convey("UpdateTopicDesc", t, func(convCtx convey.C) {
var (
ctx = context.Background()
desc = string("ehahahatest")
req = &api.TopicInfo{TopicId: 1, Desc: desc}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
origin, _ := s.dao.TopicInfo(ctx, []int64{1})
originTopic := origin[1]
res, err := s.UpdateTopicDesc(ctx, req)
curr, _ := s.dao.TopicInfo(ctx, []int64{1})
currTopic := curr[1]
s.UpdateTopicDesc(ctx, originTopic)
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)
convCtx.So(currTopic.Desc, convey.ShouldEqual, desc)
})
})
})
}
func TestServiceUpdateTopicState(t *testing.T) {
convey.Convey("UpdateTopicState", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.TopicInfo{TopicId: 1, State: 1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.UpdateTopicState(ctx, req)
curr, _ := s.dao.TopicInfo(ctx, []int64{1})
currTopic := curr[1]
req.State = 0
s.UpdateTopicState(ctx, req)
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)
convCtx.So(currTopic.State, convey.ShouldEqual, 1)
})
})
})
}
func TestServiceListCmsTopics(t *testing.T) {
convey.Convey("ListCmsTopics", t, func(convCtx convey.C) {
convCtx.Convey("search name", func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListCmsTopicsReq{Page: 1, Name: "Test"}
)
res, err := s.ListCmsTopics(ctx, req)
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)
convCtx.So(res.HasMore, convey.ShouldBeFalse)
convCtx.So(res.List[0].TopicId, convey.ShouldEqual, 1)
})
})
convCtx.Convey("search topic id", func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListCmsTopicsReq{TopicId: 1, Page: 1}
)
res, err := s.ListCmsTopics(ctx, req)
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)
convCtx.So(res.HasMore, convey.ShouldBeFalse)
convCtx.So(res.List[0].TopicId, convey.ShouldEqual, 1)
})
})
convCtx.Convey("search state available", func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListCmsTopicsReq{State: model.TopicStateAvailable, Page: 1}
)
res, err := s.ListCmsTopics(ctx, req)
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)
convCtx.So(res.List[0].State, convey.ShouldEqual, model.TopicStateAvailable)
})
})
convCtx.Convey("search state unavailable", func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListCmsTopicsReq{State: model.TopicStateUnavailable, Page: 1}
)
res, err := s.ListCmsTopics(ctx, req)
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)
convCtx.So(res.List[0].State, convey.ShouldEqual, model.TopicStateUnavailable)
})
})
})
}
func TestServiceListDiscoveryTopics(t *testing.T) {
convey.Convey("ListDiscoveryTopics", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListDiscoveryTopicReq{Page: 1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.ListDiscoveryTopics(ctx, req)
data, _ := json.Marshal(res)
log.V(1).Infow(ctx, "res", string(data))
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 TestServiceListTopicVideos(t *testing.T) {
convey.Convey("ListTopicVideos", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.TopicVideosReq{TopicId: 1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.ListTopicVideos(ctx, req)
data, _ := json.Marshal(res)
log.V(1).Infow(ctx, "res", string(data))
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 TestServiceStickTopic(t *testing.T) {
// convey.Convey("StickTopic", t, func(convCtx convey.C) {
// var (
// ctx = context.Background()
// in = &api.StickTopicReq{}
// )
// convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
// res, err := s.StickTopic(ctx, in)
// 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 TestServiceStickTopicVideo(t *testing.T) {
// convey.Convey("StickTopicVideo", t, func(convCtx convey.C) {
// var (
// ctx = context.Background()
// in = &api.StickTopicVideoReq{}
// )
// convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
// res, err := s.StickTopicVideo(ctx, in)
// 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 TestServiceregisterTopic(t *testing.T) {
convey.Convey("registerTopic", t, func(convCtx convey.C) {
var (
ctx = context.Background()
svid = int64(rand.Int() % 1000000)
list = []*api.TitleExtraItem{{Name: "Test"}, {Name: fmt.Sprintf("test_%d", rand.Int()%10000000)}}
)
log.V(1).Infow(ctx, "list", list)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.registerTopic(ctx, svid, list)
log.V(1).Infow(ctx, "list", list, "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 TestServicegetAvailableTopicInfo(t *testing.T) {
convey.Convey("getAvailableTopicInfo", t, func(convCtx convey.C) {
var (
c = context.Background()
keys = []int64{2}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.getAvailableTopicInfo(c, keys)
convCtx.So(err, convey.ShouldBeNil)
convCtx.So(len(res), convey.ShouldEqual, 0)
})
})
}
func TestServiceVideoTopic(t *testing.T) {
convey.Convey("VideoTopic", t, func(convCtx convey.C) {
var (
ctx = context.Background()
in = &api.VideoTopicReq{Svid: 1547635456050324977}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.VideoTopic(ctx, in)
data, _ := json.Marshal(res)
log.V(1).Infow(ctx, "res", string(data))
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 TestServiceSetStickTopicVideo(t *testing.T) {
convey.Convey("SetStickTopicVideo", t, func(convCtx convey.C) {
var (
ctx = context.Background()
topicID = int64(1)
in = &api.SetStickTopicVideoReq{TopicId: topicID, Svids: []int64{2, 3, 4, 5}}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
originList, _ := s.dao.GetStickTopicVideo(ctx, topicID)
res, err := s.SetStickTopicVideo(ctx, in)
currentList, _ := s.dao.GetStickTopicVideo(ctx, topicID)
data, _ := json.Marshal(currentList)
log.V(1).Infow(ctx, "res", string(data))
s.SetStickTopicVideo(ctx, &api.SetStickTopicVideoReq{TopicId: topicID, Svids: originList})
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 TestServiceListTopics(t *testing.T) {
convey.Convey("ListTopics", t, func(convCtx convey.C) {
var (
ctx = context.Background()
req = &api.ListTopicsReq{Page: 1}
)
convCtx.Convey("When everything goes positive", func(convCtx convey.C) {
res, err := s.ListTopics(ctx, req)
data, _ := json.Marshal(res)
log.V(1).Infow(ctx, "res", string(data))
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)
})
})
})
}