Create & Init Project...
This commit is contained in:
23
app/service/main/reply-feed/BUILD
Normal file
23
app/service/main/reply-feed/BUILD
Normal file
@ -0,0 +1,23 @@
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//app/service/main/reply-feed/api:all-srcs",
|
||||
"//app/service/main/reply-feed/cmd:all-srcs",
|
||||
"//app/service/main/reply-feed/conf:all-srcs",
|
||||
"//app/service/main/reply-feed/dao:all-srcs",
|
||||
"//app/service/main/reply-feed/model:all-srcs",
|
||||
"//app/service/main/reply-feed/server/grpc:all-srcs",
|
||||
"//app/service/main/reply-feed/server/http:all-srcs",
|
||||
"//app/service/main/reply-feed/service:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
60
app/service/main/reply-feed/CHANGELOG.md
Normal file
60
app/service/main/reply-feed/CHANGELOG.md
Normal file
@ -0,0 +1,60 @@
|
||||
# v1.1.9
|
||||
1. 增加uv展示
|
||||
|
||||
# v1.1.8
|
||||
1. 去掉归一化
|
||||
|
||||
# v1.1.7
|
||||
1. grpc dir
|
||||
|
||||
# v1.1.6
|
||||
1. hour隔天排序
|
||||
|
||||
# v1.1.5
|
||||
1. 修复hour排序
|
||||
|
||||
# v1.1.4
|
||||
1. 新增oid映射
|
||||
|
||||
# v1.1.3
|
||||
1. 新增两个算法, order by like和线上默认逻辑
|
||||
2. statistics数据不存在时补零
|
||||
|
||||
# v1.1.2
|
||||
1. 新增mid映射
|
||||
|
||||
# v1.1.1
|
||||
1. 数据展示接口,增加小时
|
||||
|
||||
# v1.1.0
|
||||
1. 新增数据展示接口
|
||||
|
||||
# v1.0.9
|
||||
1. fix total_view
|
||||
|
||||
# v1.0.8
|
||||
1. fix mid=0
|
||||
|
||||
# v1.0.7
|
||||
1. add pn count
|
||||
|
||||
# v1.0.6
|
||||
1. res add count
|
||||
|
||||
# v1.0.5
|
||||
1. fix view count
|
||||
|
||||
# v1.0.4
|
||||
1. grpc clinet
|
||||
|
||||
# v1.0.3
|
||||
1. grpc
|
||||
|
||||
# v1.0.2
|
||||
1. 增加white list功能,增加管理后台HTTP接口
|
||||
|
||||
# v1.0.1
|
||||
1. 修复init slots的bug
|
||||
|
||||
# v1.0.0
|
||||
1. init
|
11
app/service/main/reply-feed/CONTRIBUTORS.md
Normal file
11
app/service/main/reply-feed/CONTRIBUTORS.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Owner
|
||||
caoguoliang
|
||||
yangjiankun
|
||||
|
||||
# Author
|
||||
yangjiankun
|
||||
caoguoliang
|
||||
|
||||
# Reviewer
|
||||
caoguoliang
|
||||
yangjiankun
|
14
app/service/main/reply-feed/OWNERS
Normal file
14
app/service/main/reply-feed/OWNERS
Normal file
@ -0,0 +1,14 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- caoguoliang
|
||||
- yangjiankun
|
||||
labels:
|
||||
- main
|
||||
- service
|
||||
- service/main/reply-feed
|
||||
options:
|
||||
no_parent_owners: true
|
||||
reviewers:
|
||||
- caoguoliang
|
||||
- yangjiankun
|
12
app/service/main/reply-feed/README.md
Normal file
12
app/service/main/reply-feed/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# reply-feed-service
|
||||
|
||||
# 项目简介
|
||||
1.
|
||||
|
||||
# 编译环境
|
||||
|
||||
|
||||
# 依赖包
|
||||
|
||||
|
||||
# 编译执行
|
54
app/service/main/reply-feed/api/BUILD
Normal file
54
app/service/main/reply-feed/api/BUILD
Normal file
@ -0,0 +1,54 @@
|
||||
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 = "v1_proto",
|
||||
srcs = ["api.proto"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_proto_library(
|
||||
name = "v1_go_proto",
|
||||
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
|
||||
importpath = "go-common/app/service/main/reply-feed/api",
|
||||
proto = ":v1_proto",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["client.go"],
|
||||
embed = [":v1_go_proto"],
|
||||
importpath = "go-common/app/service/main/reply-feed/api",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//library/net/rpc/warden:go_default_library",
|
||||
"@com_github_gogo_protobuf//proto:go_default_library",
|
||||
"@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"],
|
||||
)
|
1132
app/service/main/reply-feed/api/api.pb.go
Normal file
1132
app/service/main/reply-feed/api/api.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
33
app/service/main/reply-feed/api/api.proto
Normal file
33
app/service/main/reply-feed/api/api.proto
Normal file
@ -0,0 +1,33 @@
|
||||
syntax = "proto3";
|
||||
package community.reply.feed.v1;
|
||||
|
||||
option go_package = "v1";
|
||||
|
||||
service ReplyFeed {
|
||||
rpc HotReply (HotReplyReq) returns (HotReplyRes);
|
||||
rpc Reply (ReplyReq) returns (ReplyRes);
|
||||
}
|
||||
|
||||
message HotReplyReq {
|
||||
int64 mid = 1;
|
||||
int64 oid = 2;
|
||||
int32 tp = 3;
|
||||
int32 pn = 4;
|
||||
int32 ps = 5;
|
||||
}
|
||||
|
||||
message HotReplyRes {
|
||||
string name = 1;
|
||||
repeated int64 rpIDs = 2[packed=true];
|
||||
int32 count = 3;
|
||||
}
|
||||
|
||||
message ReplyReq {
|
||||
int64 mid = 1;
|
||||
int32 pn = 2;
|
||||
int32 ps = 3;
|
||||
}
|
||||
|
||||
message ReplyRes {
|
||||
string name = 1;
|
||||
}
|
22
app/service/main/reply-feed/api/client.go
Normal file
22
app/service/main/reply-feed/api/client.go
Normal file
@ -0,0 +1,22 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"go-common/library/net/rpc/warden"
|
||||
)
|
||||
|
||||
// AppID unique app id for service discovery
|
||||
const AppID = "community.reply.feed"
|
||||
|
||||
// NewClient new identify grpc client
|
||||
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (ReplyFeedClient, error) {
|
||||
client := warden.NewClient(cfg, opts...)
|
||||
conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewReplyFeedClient(conn), nil
|
||||
}
|
45
app/service/main/reply-feed/cmd/BUILD
Normal file
45
app/service/main/reply-feed/cmd/BUILD
Normal file
@ -0,0 +1,45 @@
|
||||
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"],
|
||||
data = ["test.toml"],
|
||||
importpath = "go-common/app/service/main/reply-feed/cmd",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/conf:go_default_library",
|
||||
"//app/service/main/reply-feed/server/grpc:go_default_library",
|
||||
"//app/service/main/reply-feed/server/http:go_default_library",
|
||||
"//app/service/main/reply-feed/service:go_default_library",
|
||||
"//library/ecode/tip:go_default_library",
|
||||
"//library/log:go_default_library",
|
||||
"//library/net/trace: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"],
|
||||
)
|
47
app/service/main/reply-feed/cmd/main.go
Normal file
47
app/service/main/reply-feed/cmd/main.go
Normal file
@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go-common/app/service/main/reply-feed/conf"
|
||||
"go-common/app/service/main/reply-feed/server/grpc"
|
||||
"go-common/app/service/main/reply-feed/server/http"
|
||||
"go-common/app/service/main/reply-feed/service"
|
||||
ecode "go-common/library/ecode/tip"
|
||||
"go-common/library/log"
|
||||
"go-common/library/net/trace"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if err := conf.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Init(conf.Conf.Log)
|
||||
defer log.Close()
|
||||
log.Info("reply-feed service start")
|
||||
trace.Init(conf.Conf.Tracer)
|
||||
defer trace.Close()
|
||||
ecode.Init(conf.Conf.Ecode)
|
||||
svc := service.New(conf.Conf)
|
||||
http.Init(conf.Conf, svc)
|
||||
grpc.New(conf.Conf.GRPC, 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:
|
||||
svc.Close()
|
||||
log.Info("reply-feed service exit")
|
||||
return
|
||||
case syscall.SIGHUP:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
66
app/service/main/reply-feed/cmd/test.toml
Normal file
66
app/service/main/reply-feed/cmd/test.toml
Normal file
@ -0,0 +1,66 @@
|
||||
[midMapping]
|
||||
"1234"=1
|
||||
|
||||
[mysql]
|
||||
addr = "172.22.34.101:3306"
|
||||
dsn = "test_3306:UJPZaGKjpb2ylFx3HNhmLuwOYft4MCAi@tcp(172.22.34.101:3306)/bilibili_reply?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8"
|
||||
active = 20
|
||||
idle = 2
|
||||
idleTimeout ="4h"
|
||||
queryTimeout = "500ms"
|
||||
execTimeout = "500ms"
|
||||
tranTimeout = "500ms"
|
||||
[mysql.breaker]
|
||||
window = "3s"
|
||||
sleep = "100ms"
|
||||
bucket = 10
|
||||
ratio = 0.5
|
||||
request = 100
|
||||
|
||||
[log]
|
||||
stdout=true
|
||||
|
||||
[redis]
|
||||
name = "reply-feed-service"
|
||||
proto = "tcp"
|
||||
addr = "172.18.33.60:6889"
|
||||
idle = 10
|
||||
active = 10
|
||||
dialTimeout = "1s"
|
||||
readTimeout = "1s"
|
||||
writeTimeout = "1s"
|
||||
idleTimeout = "10s"
|
||||
expire = "1m"
|
||||
|
||||
[redisExpire]
|
||||
redisReplyZSetExpire = "12h"
|
||||
|
||||
[memcache]
|
||||
name = "reply-feed-service"
|
||||
proto = "tcp"
|
||||
addr = "172.18.33.61:11213"
|
||||
active = 50
|
||||
idle = 10
|
||||
dialTimeout = "1s"
|
||||
readTimeout = "1s"
|
||||
writeTimeout = "1s"
|
||||
idleTimeout = "10s"
|
||||
expire = "24h"
|
||||
|
||||
[databus]
|
||||
[databus.event]
|
||||
key = "170e302355453683"
|
||||
secret = "3d0e8db7bed0503949e545a469789279"
|
||||
group = "ReplyFeed-MainCommunity-P"
|
||||
topic = "ReplyFeed-T"
|
||||
action ="pub"
|
||||
name = "reply-feed/event"
|
||||
proto = "tcp"
|
||||
addr = "172.18.33.50:6205"
|
||||
idle = 2
|
||||
active = 5
|
||||
dialTimeout = "1s"
|
||||
readTimeout = "1s"
|
||||
writeTimeout = "1s"
|
||||
idleTimeout = "10s"
|
||||
expire = "1h"
|
42
app/service/main/reply-feed/conf/BUILD
Normal file
42
app/service/main/reply-feed/conf/BUILD
Normal file
@ -0,0 +1,42 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["conf.go"],
|
||||
importpath = "go-common/app/service/main/reply-feed/conf",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//library/cache/redis:go_default_library",
|
||||
"//library/conf:go_default_library",
|
||||
"//library/database/sql:go_default_library",
|
||||
"//library/ecode/tip:go_default_library",
|
||||
"//library/log:go_default_library",
|
||||
"//library/net/http/blademaster:go_default_library",
|
||||
"//library/net/http/blademaster/middleware/verify:go_default_library",
|
||||
"//library/net/rpc/warden:go_default_library",
|
||||
"//library/net/trace:go_default_library",
|
||||
"//library/queue/databus:go_default_library",
|
||||
"//library/time:go_default_library",
|
||||
"//vendor/github.com/BurntSushi/toml: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"],
|
||||
)
|
104
app/service/main/reply-feed/conf/conf.go
Normal file
104
app/service/main/reply-feed/conf/conf.go
Normal file
@ -0,0 +1,104 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
|
||||
"go-common/library/cache/redis"
|
||||
"go-common/library/conf"
|
||||
"go-common/library/database/sql"
|
||||
ecode "go-common/library/ecode/tip"
|
||||
"go-common/library/log"
|
||||
"go-common/library/net/http/blademaster"
|
||||
"go-common/library/net/http/blademaster/middleware/verify"
|
||||
"go-common/library/net/rpc/warden"
|
||||
"go-common/library/net/trace"
|
||||
"go-common/library/queue/databus"
|
||||
"go-common/library/time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var (
|
||||
confPath string
|
||||
client *conf.Client
|
||||
// Conf config
|
||||
Conf = &Config{}
|
||||
)
|
||||
|
||||
// Config .
|
||||
type Config struct {
|
||||
Log *log.Config
|
||||
BM *blademaster.ServerConfig
|
||||
GRPC *warden.ServerConfig
|
||||
Verify *verify.Config
|
||||
Tracer *trace.Config
|
||||
Redis *redis.Config
|
||||
RedisExpire *RedisExpire
|
||||
MySQL *sql.Config
|
||||
Ecode *ecode.Config
|
||||
Databus *Databus
|
||||
MidMapping map[string]int
|
||||
OidWhiteList map[string]int
|
||||
}
|
||||
|
||||
// RedisExpire RedisExpire
|
||||
type RedisExpire struct {
|
||||
RedisReplyZSetExpire time.Duration
|
||||
}
|
||||
|
||||
// Databus databus
|
||||
type Databus struct {
|
||||
Event *databus.Config
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&confPath, "conf", "", "default config path")
|
||||
}
|
||||
|
||||
// Init init conf
|
||||
func Init() error {
|
||||
if confPath != "" {
|
||||
return local()
|
||||
}
|
||||
return remote()
|
||||
}
|
||||
|
||||
func local() (err error) {
|
||||
_, err = toml.DecodeFile(confPath, &Conf)
|
||||
return
|
||||
}
|
||||
|
||||
func remote() (err error) {
|
||||
if client, err = conf.New(); err != nil {
|
||||
return
|
||||
}
|
||||
if err = load(); err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for range client.Event() {
|
||||
log.Info("config reload")
|
||||
if load() != nil {
|
||||
log.Error("config reload error (%v)", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func load() (err error) {
|
||||
var (
|
||||
s string
|
||||
ok bool
|
||||
tmpConf *Config
|
||||
)
|
||||
if s, ok = client.Toml2(); !ok {
|
||||
return errors.New("load config center error")
|
||||
}
|
||||
if _, err = toml.Decode(s, &tmpConf); err != nil {
|
||||
return errors.New("could not decode config")
|
||||
}
|
||||
*Conf = *tmpConf
|
||||
return
|
||||
}
|
57
app/service/main/reply-feed/dao/BUILD
Normal file
57
app/service/main/reply-feed/dao/BUILD
Normal file
@ -0,0 +1,57 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"dao.go",
|
||||
"db.go",
|
||||
"redis.go",
|
||||
],
|
||||
importpath = "go-common/app/service/main/reply-feed/dao",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/conf:go_default_library",
|
||||
"//app/service/main/reply-feed/model:go_default_library",
|
||||
"//library/cache/redis:go_default_library",
|
||||
"//library/database/sql:go_default_library",
|
||||
"//library/log: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 = [
|
||||
"dao_test.go",
|
||||
"db_test.go",
|
||||
"redis_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/conf:go_default_library",
|
||||
"//app/service/main/reply-feed/model:go_default_library",
|
||||
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
|
||||
],
|
||||
)
|
44
app/service/main/reply-feed/dao/dao.go
Normal file
44
app/service/main/reply-feed/dao/dao.go
Normal file
@ -0,0 +1,44 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-common/app/service/main/reply-feed/conf"
|
||||
"go-common/library/cache/redis"
|
||||
xsql "go-common/library/database/sql"
|
||||
)
|
||||
|
||||
// Dao dao
|
||||
type Dao struct {
|
||||
c *conf.Config
|
||||
|
||||
redis *redis.Pool
|
||||
redisReplyZSetExpire int
|
||||
db *xsql.DB
|
||||
}
|
||||
|
||||
// New init mysql db
|
||||
func New(c *conf.Config) (dao *Dao) {
|
||||
dao = &Dao{
|
||||
c: c,
|
||||
redis: redis.NewPool(c.Redis),
|
||||
redisReplyZSetExpire: int(time.Duration(c.RedisExpire.RedisReplyZSetExpire) / time.Second),
|
||||
db: xsql.NewMySQL(c.MySQL),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Close close the resource.
|
||||
func (d *Dao) Close() {
|
||||
d.redis.Close()
|
||||
d.db.Close()
|
||||
}
|
||||
|
||||
// Ping dao ping
|
||||
func (d *Dao) Ping(c context.Context) error {
|
||||
if err := d.PingRedis(c); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.db.Ping(c)
|
||||
}
|
35
app/service/main/reply-feed/dao/dao_test.go
Normal file
35
app/service/main/reply-feed/dao/dao_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"go-common/app/service/main/reply-feed/conf"
|
||||
)
|
||||
|
||||
var (
|
||||
d *Dao
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("DEPLOY_ENV") != "" {
|
||||
flag.Set("app_id", "main.community.reply-feed-service")
|
||||
flag.Set("conf_token", "b54e90375c747f48f477939fe272eefe")
|
||||
flag.Set("tree_id", "71072")
|
||||
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", "../cmd/test.toml")
|
||||
}
|
||||
flag.Parse()
|
||||
if err := conf.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d = New(conf.Conf)
|
||||
os.Exit(m.Run())
|
||||
}
|
242
app/service/main/reply-feed/dao/db.go
Normal file
242
app/service/main/reply-feed/dao/db.go
Normal file
@ -0,0 +1,242 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
"go-common/library/log"
|
||||
"go-common/library/xstr"
|
||||
)
|
||||
|
||||
const (
|
||||
_getSlotsStat = "SELECT name,slot,state FROM reply_abtest_strategy"
|
||||
_getSlotsStatManager = "SELECT name,slot,algorithm,weight,state FROM reply_abtest_strategy"
|
||||
_setSlot = "UPDATE reply_abtest_strategy SET name=?,algorithm=?,weight=?,state=? WHERE slot IN (%s)"
|
||||
_setWeight = "UPDATE reply_abtest_strategy SET weight=? WHERE name=?"
|
||||
_modifyState = "UPDATE reply_abtest_strategy SET state=? WHERE name=?"
|
||||
_getSlotsStatByName = "SELECT slot,algorithm,weight FROM reply_abtest_strategy WHERE name=?"
|
||||
|
||||
_getStatisticsDate = "SELECT name,date,hour,view,total_view,hot_view,hot_click,hot_like,hot_hate,hot_child,hot_report,total_like,total_hate,total_report,total_root,total_child,hot_like_uv,hot_hate_uv,hot_report_uv,hot_child_uv,total_like_uv,total_hate_uv,total_report_uv,total_child_uv,total_root_uv" +
|
||||
" FROM reply_abtest_statistics WHERE date>=? AND date<=? AND name!='default'"
|
||||
|
||||
_upsertLog = "INSERT INTO reply_abtest_statistics (name,date,hour,view,hot_click,hot_view,total_view) VALUES(?,?,?,?,?,?,?)" +
|
||||
" ON DUPLICATE KEY UPDATE view=view+?,hot_click=hot_click+?,hot_view=hot_view+?,total_view=total_view+?"
|
||||
)
|
||||
|
||||
var (
|
||||
_countIdleSlot = fmt.Sprintf("SELECT COUNT(*) FROM reply_abtest_strategy WHERE state=1 AND name='%s'", model.DefaultSlotName)
|
||||
_getIdelSlots = fmt.Sprintf("SELECT slot FROM reply_abtest_strategy WHERE state=1 AND name='%s' ORDER BY slot ASC LIMIT ?", model.DefaultSlotName)
|
||||
)
|
||||
|
||||
/*
|
||||
SlotsStat
|
||||
*/
|
||||
|
||||
// SlotsMapping get slot name stat from database.
|
||||
func (d *Dao) SlotsMapping(ctx context.Context) (slotsMap map[string]*model.SlotsMapping, err error) {
|
||||
slotsMap = make(map[string]*model.SlotsMapping)
|
||||
rows, err := d.db.Query(ctx, _getSlotsStat)
|
||||
if err != nil {
|
||||
log.Error("db.Query(%s) args(%s) error(%v)", _getSlotsStat, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
slot int
|
||||
state int
|
||||
)
|
||||
if err = rows.Scan(&name, &slot, &state); err != nil {
|
||||
log.Error("rows.Scan error(%v)", err)
|
||||
return
|
||||
}
|
||||
mapping, ok := slotsMap[name]
|
||||
if ok {
|
||||
mapping.Slots = append(mapping.Slots, slot)
|
||||
} else {
|
||||
mapping = &model.SlotsMapping{
|
||||
Name: name,
|
||||
Slots: []int{slot},
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
slotsMap[name] = mapping
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Error("rows.Err() error(%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SlotsStatManager get slots stat from database, used by manager.
|
||||
func (d *Dao) SlotsStatManager(ctx context.Context) (s []*model.SlotsStat, err error) {
|
||||
slotsMap := make(map[string]*model.SlotsStat)
|
||||
rows, err := d.db.Query(ctx, _getSlotsStatManager)
|
||||
if err != nil {
|
||||
log.Error("db.Query(%s) error(%v)", _getSlotsStatManager, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, algorithm, weight string
|
||||
slot, state int
|
||||
)
|
||||
if err = rows.Scan(&name, &slot, &algorithm, &weight, &state); err != nil {
|
||||
log.Error("rows.Scan error(%v)", err)
|
||||
return
|
||||
}
|
||||
if mapping, ok := slotsMap[name]; ok {
|
||||
mapping.Slots = append(mapping.Slots, slot)
|
||||
} else {
|
||||
slotsMap[name] = &model.SlotsStat{
|
||||
Name: name,
|
||||
Slots: []int{slot},
|
||||
Algorithm: algorithm,
|
||||
Weight: weight,
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Error("rows.Err() error(%v)", err)
|
||||
return
|
||||
}
|
||||
for _, stat := range slotsMap {
|
||||
s = append(s, stat)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CountIdleSlot count idle slot which name="default" and state=1
|
||||
func (d *Dao) CountIdleSlot(ctx context.Context) (count int, err error) {
|
||||
if err = d.db.QueryRow(ctx, _countIdleSlot).Scan(&count); err != nil {
|
||||
log.Error("db.QueryRow() error(%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IdleSlots get idle slots
|
||||
func (d *Dao) IdleSlots(ctx context.Context, count int) (slots []int64, err error) {
|
||||
rows, err := d.db.Query(ctx, _getIdelSlots, count)
|
||||
if err != nil {
|
||||
log.Error("db.Query(%s) args(%d) error(%v)", _getIdelSlots, count, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var slot int64
|
||||
if err = rows.Scan(&slot); err != nil {
|
||||
log.Error("rows.Scan() error(%v)", err)
|
||||
return
|
||||
}
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
// 槽位不够新创建实验组
|
||||
if len(slots) < count {
|
||||
slots = nil
|
||||
err = errors.New("out of slot")
|
||||
return
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Error("rows.Err() error(%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ModifyState ModifyState
|
||||
func (d *Dao) ModifyState(ctx context.Context, name string, state int) (err error) {
|
||||
if _, err = d.db.Exec(ctx, _modifyState, state, name); err != nil {
|
||||
log.Error("db.Exec(%s) args(%d, %s) error(%v)", _modifyState, state, name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateSlotsStat UpdateSlotStat and set state inactive.
|
||||
func (d *Dao) UpdateSlotsStat(ctx context.Context, name, algorithm, weight string, slots []int64, state int) (err error) {
|
||||
if _, err = d.db.Exec(ctx, fmt.Sprintf(_setSlot, xstr.JoinInts(slots)), name, algorithm, weight, state); err != nil {
|
||||
log.Error("db.Exec() error(%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SlotsStatByName get slots stat by name.
|
||||
func (d *Dao) SlotsStatByName(ctx context.Context, name string) (slots []int64, algorithm, weight string, err error) {
|
||||
rows, err := d.db.Query(ctx, _getSlotsStatByName, name)
|
||||
if err != nil {
|
||||
log.Error("db.Query(%s) args(%s) error(%v)", _getSlotsStatByName, name, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
slot int64
|
||||
)
|
||||
if err = rows.Scan(&slot, &algorithm, &weight); err != nil {
|
||||
log.Error("rows.Scan() error(%v)", err)
|
||||
return
|
||||
}
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Error("rows.Err() error(%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateWeight update a test set weight by name and algorithm.
|
||||
func (d *Dao) UpdateWeight(ctx context.Context, name string, weight interface{}) (err error) {
|
||||
b, err := json.Marshal(weight)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = d.db.Exec(ctx, _setWeight, string(b), name); err != nil {
|
||||
log.Error("db.Exec(%s), error(%v)", _setWeight, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Statistics
|
||||
*/
|
||||
|
||||
// UpsertStatistics insert or update statistics from database, if err != nil, retry
|
||||
func (d *Dao) UpsertStatistics(ctx context.Context, name string, date int, hour int, s *model.StatisticsStat) (err error) {
|
||||
if _, err = d.db.Exec(ctx, _upsertLog,
|
||||
name, date, hour,
|
||||
s.View, s.HotClick, s.HotView, s.TotalView,
|
||||
s.View, s.HotClick, s.HotView, s.TotalView); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StatisticsByDate StatisticsByDate
|
||||
func (d *Dao) StatisticsByDate(ctx context.Context, begin, end int64) (stats model.StatisticsStats, err error) {
|
||||
rows, err := d.db.Query(ctx, _getStatisticsDate, begin, end)
|
||||
if err != nil {
|
||||
log.Error("db.Query(%s) args(%d, %d) error(%v)", _getStatisticsDate, begin, end, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var s = new(model.StatisticsStat)
|
||||
err = rows.Scan(&s.Name, &s.Date, &s.Hour, &s.View, &s.TotalView, &s.HotView, &s.HotClick, &s.HotLike, &s.HotHate, &s.HotChildReply,
|
||||
&s.HotReport, &s.TotalLike, &s.TotalHate, &s.TotalReport, &s.TotalRootReply, &s.TotalChildReply,
|
||||
&s.HotLikeUV, &s.HotHateUV, &s.HotReportUV, &s.HotChildUV, &s.TotalLikeUV, &s.TotalHateUV, &s.TotalReportUV, &s.TotalChildUV, &s.TotalRootUV)
|
||||
if err != nil {
|
||||
log.Error("rows.Scan() error(%v)", err)
|
||||
return
|
||||
}
|
||||
stats = append(stats, s)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Error("rows.Err() error(%v)", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
158
app/service/main/reply-feed/dao/db_test.go
Normal file
158
app/service/main/reply-feed/dao/db_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDaoSlotsMapping(t *testing.T) {
|
||||
convey.Convey("SlotsMapping", t, func(ctx convey.C) {
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
slotsMap, err := d.SlotsMapping(context.Background())
|
||||
ctx.Convey("Then err should be nil.slotsMap should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(slotsMap, convey.ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoSlotsStatManager(t *testing.T) {
|
||||
convey.Convey("SlotsStatManager", t, func(ctx convey.C) {
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
s, err := d.SlotsStatManager(context.Background())
|
||||
ctx.Convey("Then err should be nil.s should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(s, convey.ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoIdleSlot(t *testing.T) {
|
||||
convey.Convey("IdleSlot", t, func(ctx convey.C) {
|
||||
var (
|
||||
count = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
slots, err := d.IdleSlots(context.Background(), count)
|
||||
ctx.Convey("Then err should be nil.slots should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(slots, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoCountIdleSlot(t *testing.T) {
|
||||
convey.Convey("CountIdleSlot", t, func(ctx convey.C) {
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
count, err := d.CountIdleSlot(context.Background())
|
||||
ctx.Convey("Then err should be nil. count should greater than 0.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(count, convey.ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoModifyState(t *testing.T) {
|
||||
convey.Convey("ModifyState", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
state = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
err := d.ModifyState(context.Background(), name, state)
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoUpdateSlotsStat(t *testing.T) {
|
||||
convey.Convey("UpdateSlotsStat", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
algorithm = ""
|
||||
weight = ""
|
||||
slots = []int64{-1}
|
||||
state = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
err := d.UpdateSlotsStat(context.Background(), name, algorithm, weight, slots, state)
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlotsStatByName(t *testing.T) {
|
||||
convey.Convey("SlotsStatByName", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
slots, algorithm, weight, err := d.SlotsStatByName(context.Background(), name)
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(slots, convey.ShouldBeNil)
|
||||
ctx.So(algorithm, convey.ShouldEqual, "")
|
||||
ctx.So(weight, convey.ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
func TestDaoUpdateWeight(t *testing.T) {
|
||||
convey.Convey("UpdateWeight", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
weight = interface{}(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
err := d.UpdateWeight(context.Background(), name, weight)
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoUpsertStatistics(t *testing.T) {
|
||||
convey.Convey("UpsertStatistics", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
date = int(0)
|
||||
hour = int(0)
|
||||
s = &model.StatisticsStat{}
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
err := d.UpsertStatistics(context.Background(), name, date, hour, s)
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoStatisticsByDate(t *testing.T) {
|
||||
convey.Convey("StatisticsByDate", t, func(ctx convey.C) {
|
||||
var (
|
||||
begin = int64(0)
|
||||
end = int64(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
stats, err := d.StatisticsByDate(context.Background(), begin, end)
|
||||
ctx.Convey("Then err should be nil.stats should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(stats, convey.ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
68
app/service/main/reply-feed/dao/redis.go
Normal file
68
app/service/main/reply-feed/dao/redis.go
Normal file
@ -0,0 +1,68 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-common/library/cache/redis"
|
||||
"go-common/library/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// r_<实验组名>_<oid>_<type>
|
||||
// 用redis ZSet存储热门评论列表,score为热评分数,member为rpID
|
||||
_replyZSetFormat = "r_%s_%d_%d"
|
||||
)
|
||||
|
||||
func keyReplyZSet(name string, oid int64, tp int) string {
|
||||
return fmt.Sprintf(_replyZSetFormat, name, oid, tp)
|
||||
}
|
||||
|
||||
// PingRedis redis health check.
|
||||
func (d *Dao) PingRedis(ctx context.Context) (err error) {
|
||||
conn := d.redis.Get(ctx)
|
||||
defer conn.Close()
|
||||
_, err = conn.Do("SET", "ping", "pong")
|
||||
return
|
||||
}
|
||||
|
||||
// ExpireReplyZSetRds expire reply list.
|
||||
func (d *Dao) ExpireReplyZSetRds(ctx context.Context, name string, oid int64, tp int) (ok bool, err error) {
|
||||
conn := d.redis.Get(ctx)
|
||||
defer conn.Close()
|
||||
key := keyReplyZSet(name, oid, tp)
|
||||
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.redisReplyZSetExpire)); err != nil {
|
||||
if err == redis.ErrNil {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
log.Error("redis EXPIRE key(%s) error(%v)", key, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReplyZSetRds get reply list from redis sorted set.
|
||||
func (d *Dao) ReplyZSetRds(ctx context.Context, name string, oid int64, tp, start, end int) (rpIDs []int64, err error) {
|
||||
conn := d.redis.Get(ctx)
|
||||
defer conn.Close()
|
||||
key := keyReplyZSet(name, oid, tp)
|
||||
values, err := redis.Values(conn.Do("ZREVRANGE", key, start, end))
|
||||
if err != nil {
|
||||
log.Error("redis ZREVRANGE(%s, %d, %d) error(%v)", key, start, end, err)
|
||||
return
|
||||
}
|
||||
if err = redis.ScanSlice(values, &rpIDs); err != nil {
|
||||
log.Error("redis ScanSlice(%v) error(%v)", values, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CountReplyZSetRds count reply num.
|
||||
func (d *Dao) CountReplyZSetRds(ctx context.Context, name string, oid int64, tp int) (count int, err error) {
|
||||
conn := d.redis.Get(ctx)
|
||||
defer conn.Close()
|
||||
key := keyReplyZSet(name, oid, tp)
|
||||
if count, err = redis.Int(conn.Do("ZCARD", key)); err != nil {
|
||||
log.Error("conn.Do(ZCARD, %s) error(%v)", key, err)
|
||||
}
|
||||
return
|
||||
}
|
88
app/service/main/reply-feed/dao/redis_test.go
Normal file
88
app/service/main/reply-feed/dao/redis_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDaokeyReplyZSet(t *testing.T) {
|
||||
convey.Convey("keyReplyZSet", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
oid = int64(0)
|
||||
tp = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
p1 := keyReplyZSet(name, oid, tp)
|
||||
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(p1, convey.ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoPingRedis(t *testing.T) {
|
||||
convey.Convey("PingRedis", t, func(ctx convey.C) {
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
err := d.PingRedis(context.Background())
|
||||
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoExpireReplyZSetRds(t *testing.T) {
|
||||
convey.Convey("ExpireReplyZSetRds", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
oid = int64(0)
|
||||
tp = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
ok, err := d.ExpireReplyZSetRds(context.Background(), name, oid, tp)
|
||||
ctx.Convey("Then err should be nil.ok should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(ok, convey.ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoGetReplyZSetRds(t *testing.T) {
|
||||
convey.Convey("ReplyZSetRds", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
oid = int64(0)
|
||||
tp = int(0)
|
||||
start = int(0)
|
||||
end = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
rpIDs, err := d.ReplyZSetRds(context.Background(), name, oid, tp, start, end)
|
||||
ctx.Convey("Then err should be nil.rpIDs should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(rpIDs, convey.ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaoCountReplyZSetRds(t *testing.T) {
|
||||
convey.Convey("CountReplyZSetRds", t, func(ctx convey.C) {
|
||||
var (
|
||||
name = ""
|
||||
oid = int64(-1)
|
||||
tp = int(0)
|
||||
)
|
||||
ctx.Convey("When everything goes positive", func(ctx convey.C) {
|
||||
count, err := d.CountReplyZSetRds(context.Background(), name, oid, tp)
|
||||
ctx.Convey("Then err should be nil.rpIDs should not be nil.", func(ctx convey.C) {
|
||||
ctx.So(err, convey.ShouldBeNil)
|
||||
ctx.So(count, convey.ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
29
app/service/main/reply-feed/model/BUILD
Normal file
29
app/service/main/reply-feed/model/BUILD
Normal file
@ -0,0 +1,29 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["model.go"],
|
||||
importpath = "go-common/app/service/main/reply-feed/model",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//library/ecode: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"],
|
||||
)
|
236
app/service/main/reply-feed/model/model.go
Normal file
236
app/service/main/reply-feed/model/model.go
Normal file
@ -0,0 +1,236 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"go-common/library/ecode"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// const var
|
||||
const (
|
||||
WilsonLHRRAlgorithm = "wilsonLHRR"
|
||||
WilsonLHRRFluidAlgorithm = "wilsonLHRRFluid"
|
||||
OriginAlgorithm = "origin"
|
||||
LikeDescAlgorithm = "likeDesc"
|
||||
|
||||
StateInactive = int(0)
|
||||
StateActive = int(1)
|
||||
|
||||
SlotsNum = 100
|
||||
|
||||
DefaultSlotName = "default"
|
||||
DefaultAlgorithm = "default"
|
||||
DefaultWeight = ""
|
||||
)
|
||||
|
||||
// EventMsg EventMsg
|
||||
type EventMsg struct {
|
||||
Action string `json:"action"`
|
||||
Oid int64 `json:"oid"`
|
||||
Tp int `json:"tp"`
|
||||
}
|
||||
|
||||
// SlotsMapping slot name mapping
|
||||
type SlotsMapping struct {
|
||||
Name string
|
||||
Slots []int
|
||||
State int
|
||||
}
|
||||
|
||||
// SlotsStat slots stat
|
||||
type SlotsStat struct {
|
||||
Name string
|
||||
Slots []int
|
||||
Algorithm string
|
||||
Weight string
|
||||
State int
|
||||
}
|
||||
|
||||
// StatisticsStats StatisticsStats
|
||||
type StatisticsStats []*StatisticsStat
|
||||
|
||||
// GroupByName group statistics by name
|
||||
func (s StatisticsStats) GroupByName() (res map[string]StatisticsStats) {
|
||||
res = make(map[string]StatisticsStats)
|
||||
for _, stat := range s {
|
||||
if _, ok := res[stat.Name]; ok {
|
||||
res[stat.Name] = append(res[stat.Name], stat)
|
||||
} else {
|
||||
var tmp []*StatisticsStat
|
||||
tmp = append(tmp, stat)
|
||||
res[stat.Name] = tmp
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StatisticsStat 实验组或者对照组的各项统计
|
||||
type StatisticsStat struct {
|
||||
// 流量所属槽位 0~99
|
||||
Slot int
|
||||
// 所属实验组名
|
||||
Name string
|
||||
State int
|
||||
Date int
|
||||
Hour int
|
||||
HotLike int64
|
||||
HotHate int64
|
||||
HotReport int64
|
||||
HotChildReply int64
|
||||
// 整个评论区
|
||||
TotalLike int64
|
||||
TotalHate int64
|
||||
TotalReport int64
|
||||
TotalChildReply int64
|
||||
TotalRootReply int64
|
||||
// 用户点开评论区次数
|
||||
View uint32
|
||||
// 评论列表接口调用次数
|
||||
TotalView uint32
|
||||
// 热门评论接口调用次数
|
||||
HotView uint32
|
||||
// 更多热门评论点击次数
|
||||
HotClick uint32
|
||||
// 用户在评论首页看到的热门评论被点赞点踩评论以及举报的次数
|
||||
|
||||
// UV的统计数据
|
||||
HotLikeUV int64
|
||||
HotHateUV int64
|
||||
HotReportUV int64
|
||||
HotChildUV int64
|
||||
|
||||
TotalLikeUV int64
|
||||
TotalHateUV int64
|
||||
TotalReportUV int64
|
||||
TotalChildUV int64
|
||||
TotalRootUV int64
|
||||
}
|
||||
|
||||
// Merge Merge
|
||||
func (stat1 *StatisticsStat) Merge(stat2 *StatisticsStat) (stat3 *StatisticsStat) {
|
||||
stat3 = new(StatisticsStat)
|
||||
stat3.View = stat1.View + stat2.View
|
||||
stat3.HotView = stat1.HotView + stat2.HotView
|
||||
stat3.HotClick = stat1.HotClick + stat2.HotClick
|
||||
stat3.TotalView = stat1.TotalView + stat2.TotalView
|
||||
return
|
||||
}
|
||||
|
||||
// DivideByPercent ...
|
||||
func (stat1 *StatisticsStat) DivideByPercent(percent int64) (stat2 *StatisticsStat) {
|
||||
stat2 = new(StatisticsStat)
|
||||
if percent <= 0 {
|
||||
return
|
||||
}
|
||||
stat2.Name = stat1.Name
|
||||
stat2.Date = stat1.Date
|
||||
stat2.Hour = stat1.Hour
|
||||
stat2.View = stat1.View / uint32(percent)
|
||||
stat2.HotView = stat1.HotView / uint32(percent)
|
||||
stat2.HotClick = stat1.HotClick / uint32(percent)
|
||||
stat2.TotalView = stat1.TotalView / uint32(percent)
|
||||
stat2.HotLike = stat1.HotLike / percent
|
||||
stat2.HotHate = stat1.HotHate / percent
|
||||
stat2.HotChildReply = stat1.HotChildReply / percent
|
||||
stat2.HotReport = stat1.HotReport / percent
|
||||
stat2.TotalLike = stat1.TotalLike / percent
|
||||
stat2.TotalHate = stat1.TotalHate / percent
|
||||
stat2.TotalReport = stat1.TotalReport / percent
|
||||
stat2.TotalRootReply = stat1.TotalRootReply / percent
|
||||
stat2.TotalChildReply = stat1.TotalChildReply / percent
|
||||
return
|
||||
}
|
||||
|
||||
// MergeByDate MergeByDate
|
||||
func (stat1 *StatisticsStat) MergeByDate(stat2 *StatisticsStat) (stat3 *StatisticsStat) {
|
||||
stat3 = new(StatisticsStat)
|
||||
stat3.Name = stat1.Name
|
||||
stat3.Date = stat1.Date
|
||||
stat3.View = stat1.View + stat2.View
|
||||
stat3.HotView = stat1.HotView + stat2.HotView
|
||||
stat3.HotClick = stat1.HotClick + stat2.HotClick
|
||||
stat3.TotalView = stat1.TotalView + stat2.TotalView
|
||||
stat3.HotLike = stat1.HotLike + stat2.HotLike
|
||||
stat3.HotHate = stat1.HotHate + stat2.HotHate
|
||||
stat3.HotChildReply = stat1.HotChildReply + stat2.HotChildReply
|
||||
stat3.HotReport = stat1.HotReport + stat2.HotReport
|
||||
stat3.TotalLike = stat1.TotalLike + stat2.TotalLike
|
||||
stat3.TotalHate = stat1.TotalHate + stat2.TotalHate
|
||||
stat3.TotalReport = stat1.TotalReport + stat2.TotalReport
|
||||
stat3.TotalRootReply = stat1.TotalRootReply + stat2.TotalRootReply
|
||||
stat3.TotalChildReply = stat1.TotalChildReply + stat2.TotalChildReply
|
||||
return
|
||||
}
|
||||
|
||||
// WilsonLHRRWeight wilson score interval weight
|
||||
type WilsonLHRRWeight struct {
|
||||
Like float64 `json:"like"`
|
||||
Hate float64 `json:"hate"`
|
||||
Reply float64 `json:"reply"`
|
||||
Report float64 `json:"report"`
|
||||
}
|
||||
|
||||
// Validate Validate
|
||||
func (weight WilsonLHRRWeight) Validate() (err error) {
|
||||
if weight.Report*weight.Reply*weight.Hate*weight.Like <= 0 {
|
||||
err = ecode.RequestErr
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WilsonLHRRFluidWeight WilsonLHRRFluidWeight
|
||||
type WilsonLHRRFluidWeight struct {
|
||||
Like float64 `json:"like"`
|
||||
Hate float64 `json:"hate"`
|
||||
Reply float64 `json:"reply"`
|
||||
Report float64 `json:"report"`
|
||||
Slope float64 `json:"slope"`
|
||||
}
|
||||
|
||||
// Validate Validate
|
||||
func (weight WilsonLHRRFluidWeight) Validate() (err error) {
|
||||
if weight.Report*weight.Reply*weight.Hate*weight.Like*weight.Slope <= 0 {
|
||||
err = ecode.RequestErr
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SSReq ss req
|
||||
type SSReq struct {
|
||||
DateFrom int64 `form:"date_from" validate:"required"`
|
||||
DateEnd int64 `form:"date_end" validate:"required"`
|
||||
Hour bool `form:"hour"`
|
||||
}
|
||||
|
||||
// SSHourRes ss res
|
||||
type SSHourRes struct {
|
||||
Legend []string `json:"legend"`
|
||||
XAxis []string `json:"x_axis"`
|
||||
Series map[string][]*StatisticsStat `json:"series"`
|
||||
}
|
||||
|
||||
// Sort ...
|
||||
func (s *SSHourRes) Sort() {
|
||||
sort.Strings(s.Legend)
|
||||
sort.Strings(s.XAxis)
|
||||
for _, v := range s.Series {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].Date*100+v[i].Hour < v[j].Date*100+v[j].Hour })
|
||||
}
|
||||
}
|
||||
|
||||
// SSDateRes ss res
|
||||
type SSDateRes struct {
|
||||
Legend []string `json:"legend"`
|
||||
XAxis []int `json:"x_axis"`
|
||||
Series map[string][]*StatisticsStat `json:"series"`
|
||||
}
|
||||
|
||||
// Sort ...
|
||||
func (s *SSDateRes) Sort() {
|
||||
sort.Strings(s.Legend)
|
||||
sort.Ints(s.XAxis)
|
||||
for _, v := range s.Series {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].Date < v[j].Date })
|
||||
}
|
||||
}
|
33
app/service/main/reply-feed/server/grpc/BUILD
Normal file
33
app/service/main/reply-feed/server/grpc/BUILD
Normal file
@ -0,0 +1,33 @@
|
||||
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/main/reply-feed/server/grpc",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/api:go_default_library",
|
||||
"//app/service/main/reply-feed/service: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"],
|
||||
)
|
19
app/service/main/reply-feed/server/grpc/server.go
Normal file
19
app/service/main/reply-feed/server/grpc/server.go
Normal file
@ -0,0 +1,19 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
pb "go-common/app/service/main/reply-feed/api"
|
||||
"go-common/app/service/main/reply-feed/service"
|
||||
"go-common/library/net/rpc/warden"
|
||||
)
|
||||
|
||||
// New grpc server
|
||||
func New(cfg *warden.ServerConfig, srv *service.Service) (wsvr *warden.Server) {
|
||||
var err error
|
||||
wsvr = warden.NewServer(cfg)
|
||||
pb.RegisterReplyFeedServer(wsvr.Server(), srv)
|
||||
wsvr, err = wsvr.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
37
app/service/main/reply-feed/server/http/BUILD
Normal file
37
app/service/main/reply-feed/server/http/BUILD
Normal file
@ -0,0 +1,37 @@
|
||||
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/main/reply-feed/server/http",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/conf:go_default_library",
|
||||
"//app/service/main/reply-feed/model:go_default_library",
|
||||
"//app/service/main/reply-feed/service:go_default_library",
|
||||
"//library/ecode:go_default_library",
|
||||
"//library/log:go_default_library",
|
||||
"//library/net/http/blademaster: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"],
|
||||
)
|
253
app/service/main/reply-feed/server/http/http.go
Normal file
253
app/service/main/reply-feed/server/http/http.go
Normal file
@ -0,0 +1,253 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-common/app/service/main/reply-feed/conf"
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
"go-common/app/service/main/reply-feed/service"
|
||||
"go-common/library/ecode"
|
||||
"go-common/library/log"
|
||||
bm "go-common/library/net/http/blademaster"
|
||||
"go-common/library/net/http/blademaster/middleware/verify"
|
||||
)
|
||||
|
||||
var (
|
||||
svc *service.Service
|
||||
vfy *verify.Verify
|
||||
)
|
||||
|
||||
// Init init
|
||||
func Init(c *conf.Config, s *service.Service) {
|
||||
svc = s
|
||||
vfy = verify.New(c.Verify)
|
||||
engine := bm.DefaultServer(c.BM)
|
||||
router(engine)
|
||||
if err := engine.Start(); err != nil {
|
||||
log.Error("engine.Start error(%v)", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func router(e *bm.Engine) {
|
||||
e.Ping(ping)
|
||||
e.Register(register)
|
||||
g := e.Group("/x/reply-feed")
|
||||
{
|
||||
g.POST("/strategy/new", newEGroup)
|
||||
g.POST("/strategy/edit", editEgroup)
|
||||
g.POST("/strategy/state", modifyState)
|
||||
g.POST("/strategy/resize", resizeSlots)
|
||||
g.POST("/strategy/reset", resetEgroup)
|
||||
g.GET("/strategy/list", listEGroup)
|
||||
|
||||
g.GET("/statistics/list", statistics)
|
||||
}
|
||||
}
|
||||
|
||||
func ping(c *bm.Context) {
|
||||
if err := svc.Ping(c); err != nil {
|
||||
log.Error("ping error(%v)", err)
|
||||
c.AbortWithStatus(http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func register(c *bm.Context) {
|
||||
c.JSON(map[string]interface{}{}, nil)
|
||||
}
|
||||
|
||||
func validate(algorithm string, weight string) (err error) {
|
||||
switch algorithm {
|
||||
case model.WilsonLHRRAlgorithm:
|
||||
w := &model.WilsonLHRRWeight{}
|
||||
if err = json.Unmarshal([]byte(weight), &w); err != nil {
|
||||
return
|
||||
}
|
||||
return w.Validate()
|
||||
case model.WilsonLHRRFluidAlgorithm:
|
||||
w := &model.WilsonLHRRFluidWeight{}
|
||||
if err = json.Unmarshal([]byte(weight), &w); err != nil {
|
||||
return
|
||||
}
|
||||
return w.Validate()
|
||||
case model.OriginAlgorithm, model.LikeDescAlgorithm:
|
||||
return
|
||||
default:
|
||||
log.Error("unknown algorithm accepted (%s)", algorithm)
|
||||
err = ecode.RequestErr
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func listEGroup(c *bm.Context) {
|
||||
stats, err := svc.SlotStatsManager(c)
|
||||
if err != nil {
|
||||
c.JSON(nil, err)
|
||||
return
|
||||
}
|
||||
c.JSON(stats, nil)
|
||||
}
|
||||
|
||||
func newEGroup(c *bm.Context) {
|
||||
v := new(struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
Percent int `form:"percent" validate:"required"`
|
||||
Algorithm string `form:"algorithm" validate:"required"`
|
||||
Weight string `form:"weight" validate:"required"`
|
||||
})
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if err = c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
if err = validate(v.Algorithm, v.Weight); err != nil {
|
||||
c.JSON(nil, ecode.RequestErr)
|
||||
return
|
||||
}
|
||||
c.JSON(nil, svc.NewEGroup(c, v.Name, v.Algorithm, v.Weight, v.Percent))
|
||||
}
|
||||
|
||||
func editEgroup(c *bm.Context) {
|
||||
v := new(struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
Algorithm string `form:"algorithm" validate:"required"`
|
||||
Weight string `form:"weight" validate:"required"`
|
||||
Slots []int64 `form:"slots,split" validate:"required"`
|
||||
})
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if err = c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
if err = validate(v.Algorithm, v.Weight); err != nil {
|
||||
c.JSON(nil, ecode.RequestErr)
|
||||
return
|
||||
}
|
||||
c.JSON(nil, svc.EditSlotsStat(c, v.Name, v.Algorithm, v.Weight, v.Slots))
|
||||
}
|
||||
|
||||
func modifyState(c *bm.Context) {
|
||||
v := new(struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
State int `form:"state"`
|
||||
})
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if err = c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
c.JSON(nil, svc.ModifyState(c, v.Name, v.State))
|
||||
}
|
||||
|
||||
func resizeSlots(c *bm.Context) {
|
||||
v := new(struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
Percent int `form:"percent" validate:"required"`
|
||||
})
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if err = c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
c.JSON(nil, svc.ResizeSlots(c, v.Name, v.Percent))
|
||||
}
|
||||
|
||||
func resetEgroup(c *bm.Context) {
|
||||
v := new(struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
})
|
||||
var (
|
||||
err error
|
||||
)
|
||||
if err = c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
c.JSON(nil, svc.ResetEGroup(c, v.Name))
|
||||
}
|
||||
|
||||
func statistics(c *bm.Context) {
|
||||
v := new(model.SSReq)
|
||||
if err := c.Bind(v); err != nil {
|
||||
return
|
||||
}
|
||||
if v.Hour {
|
||||
res := new(model.SSHourRes)
|
||||
data, err := svc.StatisticsByHour(c, v)
|
||||
if err != nil {
|
||||
c.JSON(nil, err)
|
||||
return
|
||||
}
|
||||
xAxisMap := make(map[string]struct{})
|
||||
res.Series = make(map[string][]*model.StatisticsStat)
|
||||
for legend, m := range data {
|
||||
res.Legend = append(res.Legend, legend)
|
||||
for xAxis := range m {
|
||||
xAxisMap[xAxis] = struct{}{}
|
||||
}
|
||||
}
|
||||
for k := range xAxisMap {
|
||||
res.XAxis = append(res.XAxis, k)
|
||||
}
|
||||
for _, legend := range res.Legend {
|
||||
if hourStatistics, ok := data[legend]; ok {
|
||||
for _, hour := range res.XAxis {
|
||||
if stat, exists := hourStatistics[hour]; exists {
|
||||
res.Series[legend] = append(res.Series[legend], stat)
|
||||
} else {
|
||||
t := strings.Split(hour, "-")
|
||||
if len(t) < 2 {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
date, _ := strconv.Atoi(t[0])
|
||||
hour, _ := strconv.Atoi(t[1])
|
||||
res.Series[legend] = append(res.Series[legend], &model.StatisticsStat{Date: date, Hour: hour})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.Sort()
|
||||
c.JSON(res, nil)
|
||||
return
|
||||
}
|
||||
res := new(model.SSDateRes)
|
||||
data, err := svc.StatisticsByDate(c, v)
|
||||
if err != nil {
|
||||
c.JSON(nil, err)
|
||||
return
|
||||
}
|
||||
xAxisMap := make(map[int]struct{})
|
||||
res.Series = make(map[string][]*model.StatisticsStat)
|
||||
for legend, m := range data {
|
||||
res.Legend = append(res.Legend, legend)
|
||||
for xAxis := range m {
|
||||
xAxisMap[xAxis] = struct{}{}
|
||||
}
|
||||
}
|
||||
for k := range xAxisMap {
|
||||
res.XAxis = append(res.XAxis, k)
|
||||
}
|
||||
for _, legend := range res.Legend {
|
||||
if dateStatistics, ok := data[legend]; ok {
|
||||
for _, date := range res.XAxis {
|
||||
if stat, exists := dateStatistics[date]; exists {
|
||||
res.Series[legend] = append(res.Series[legend], stat)
|
||||
} else {
|
||||
res.Series[legend] = append(res.Series[legend], &model.StatisticsStat{Date: date})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.Sort()
|
||||
c.JSON(res, nil)
|
||||
}
|
41
app/service/main/reply-feed/service/BUILD
Normal file
41
app/service/main/reply-feed/service/BUILD
Normal file
@ -0,0 +1,41 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admin.go",
|
||||
"reply.go",
|
||||
"service.go",
|
||||
],
|
||||
importpath = "go-common/app/service/main/reply-feed/service",
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//app/service/main/reply-feed/api:go_default_library",
|
||||
"//app/service/main/reply-feed/conf:go_default_library",
|
||||
"//app/service/main/reply-feed/dao:go_default_library",
|
||||
"//app/service/main/reply-feed/model:go_default_library",
|
||||
"//library/log:go_default_library",
|
||||
"//library/net/netutil:go_default_library",
|
||||
"//vendor/github.com/robfig/cron: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"],
|
||||
)
|
144
app/service/main/reply-feed/service/admin.go
Normal file
144
app/service/main/reply-feed/service/admin.go
Normal file
@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
"go-common/library/log"
|
||||
)
|
||||
|
||||
/*
|
||||
SlotsMapping
|
||||
*/
|
||||
|
||||
// SlotStatsManager SlotStatsManager
|
||||
func (s *Service) SlotStatsManager(ctx context.Context) ([]*model.SlotsStat, error) {
|
||||
return s.dao.SlotsStatManager(ctx)
|
||||
}
|
||||
|
||||
// NewEGroup new experimental group.
|
||||
func (s *Service) NewEGroup(ctx context.Context, name, algorithm, weight string, percent int) (err error) {
|
||||
var (
|
||||
usedSlots []int64
|
||||
slots []int64
|
||||
)
|
||||
if usedSlots, _, _, err = s.dao.SlotsStatByName(ctx, name); err != nil {
|
||||
return
|
||||
}
|
||||
if len(usedSlots) > 0 {
|
||||
return errors.New("duplicate name")
|
||||
}
|
||||
if slots, err = s.dao.IdleSlots(ctx, percent); err != nil {
|
||||
return
|
||||
}
|
||||
return s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, slots, model.StateInactive)
|
||||
}
|
||||
|
||||
// ResizeSlots resize slots
|
||||
func (s *Service) ResizeSlots(ctx context.Context, name string, percent int) (err error) {
|
||||
var (
|
||||
count int
|
||||
slots []int64
|
||||
idelSlots []int64
|
||||
algorithm, weight string
|
||||
)
|
||||
if slots, algorithm, weight, err = s.dao.SlotsStatByName(ctx, name); err != nil {
|
||||
return
|
||||
}
|
||||
if percent > len(slots) {
|
||||
if count, err = s.dao.CountIdleSlot(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if percent > count {
|
||||
err = errors.New("out of slot")
|
||||
return
|
||||
}
|
||||
if idelSlots, err = s.dao.IdleSlots(ctx, percent-len(slots)); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, idelSlots, model.StateActive); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err = s.dao.UpdateSlotsStat(ctx, model.DefaultSlotName, model.DefaultAlgorithm, model.DefaultWeight, slots[percent:], model.StateActive); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EditSlotsStat edit a test set weight.
|
||||
func (s *Service) EditSlotsStat(ctx context.Context, name, algorithm, weight string, slots []int64) (err error) {
|
||||
if err = s.dao.UpdateSlotsStat(ctx, name, algorithm, weight, slots, model.StateInactive); err != nil {
|
||||
log.Error("Edit SlotsMapping Failed, Error (%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ModifyState modify test set state, activate or inactivate by name.
|
||||
func (s *Service) ModifyState(ctx context.Context, name string, state int) (err error) {
|
||||
return s.dao.ModifyState(ctx, name, state)
|
||||
}
|
||||
|
||||
// ResetEGroup ResetEGroup
|
||||
func (s *Service) ResetEGroup(ctx context.Context, name string) (err error) {
|
||||
var slots []int64
|
||||
if slots, _, _, err = s.dao.SlotsStatByName(ctx, name); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.dao.UpdateSlotsStat(ctx, model.DefaultSlotName, model.DefaultAlgorithm, model.DefaultWeight, slots, model.StateActive); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TODO(Statistics)
|
||||
Statistics
|
||||
*/
|
||||
|
||||
// StatisticsByDate GetStatisticsByDate
|
||||
func (s *Service) StatisticsByDate(ctx context.Context, req *model.SSReq) (res map[string]map[int]*model.StatisticsStat, err error) {
|
||||
res = make(map[string]map[int]*model.StatisticsStat)
|
||||
stats, err := s.dao.StatisticsByDate(ctx, req.DateFrom, req.DateEnd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
groupedStatistics := stats.GroupByName()
|
||||
for name, ss := range groupedStatistics {
|
||||
dateStatistics := make(map[int]*model.StatisticsStat)
|
||||
for _, s := range ss {
|
||||
if _, ok := dateStatistics[s.Date]; ok {
|
||||
dateStatistics[s.Date] = dateStatistics[s.Date].MergeByDate(s)
|
||||
} else {
|
||||
dateStatistics[s.Date] = s
|
||||
}
|
||||
}
|
||||
res[name] = dateStatistics
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StatisticsByHour GetStatisticsByHour
|
||||
func (s *Service) StatisticsByHour(ctx context.Context, req *model.SSReq) (res map[string]map[string]*model.StatisticsStat, err error) {
|
||||
res = make(map[string]map[string]*model.StatisticsStat)
|
||||
stats, err := s.dao.StatisticsByDate(ctx, req.DateFrom, req.DateEnd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
groupedStatistics := stats.GroupByName()
|
||||
for name, ss := range groupedStatistics {
|
||||
hourStatistics := make(map[string]*model.StatisticsStat)
|
||||
for _, s := range ss {
|
||||
if s.Hour < 10 {
|
||||
hourStatistics[fmt.Sprintf("%d-0%d", s.Date, s.Hour)] = s
|
||||
} else {
|
||||
hourStatistics[fmt.Sprintf("%d-%d", s.Date, s.Hour)] = s
|
||||
}
|
||||
}
|
||||
res[name] = hourStatistics
|
||||
}
|
||||
return
|
||||
}
|
106
app/service/main/reply-feed/service/reply.go
Normal file
106
app/service/main/reply-feed/service/reply.go
Normal file
@ -0,0 +1,106 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"go-common/app/service/main/reply-feed/api"
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
)
|
||||
|
||||
func (s *Service) name(mid int64) (string, bool) {
|
||||
if mid == 0 {
|
||||
return model.DefaultSlotName, false
|
||||
}
|
||||
slot, ok := s.midMapping[mid]
|
||||
s.statisticsLock.RLock()
|
||||
defer s.statisticsLock.RUnlock()
|
||||
if ok && slot >= 0 && slot < model.SlotsNum {
|
||||
return s.statisticsStats[slot].Name, true
|
||||
}
|
||||
stat := s.statisticsStats[mid%model.SlotsNum]
|
||||
if stat.Name == model.DefaultSlotName || stat.State == model.StateInactive {
|
||||
return model.DefaultSlotName, false
|
||||
}
|
||||
return stat.Name, true
|
||||
}
|
||||
|
||||
func (s *Service) incrHot(req *v1.HotReplyReq) {
|
||||
if req.Mid == 0 {
|
||||
return
|
||||
}
|
||||
if req.Pn == 1 {
|
||||
if req.Ps == 20 {
|
||||
// 用户点击更多热评
|
||||
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].HotClick, 1)
|
||||
} else {
|
||||
// 点开评论区自带的5条热门评论
|
||||
return
|
||||
}
|
||||
}
|
||||
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].HotView, 1)
|
||||
}
|
||||
|
||||
func (s *Service) incrTotalView(req *v1.ReplyReq) {
|
||||
if req.Mid == 0 {
|
||||
return
|
||||
}
|
||||
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].TotalView, 1)
|
||||
}
|
||||
|
||||
func (s *Service) incrView(req *v1.ReplyReq) {
|
||||
if req.Mid == 0 {
|
||||
return
|
||||
}
|
||||
atomic.AddUint32(&s.statisticsStats[req.Mid%model.SlotsNum].View, 1)
|
||||
}
|
||||
|
||||
// HotReply return hot reply
|
||||
func (s *Service) HotReply(ctx context.Context, req *v1.HotReplyReq) (res *v1.HotReplyRes, err error) {
|
||||
var (
|
||||
start = (req.Pn - 1) * req.Ps
|
||||
end = start + req.Ps - 1
|
||||
ok bool
|
||||
count int
|
||||
)
|
||||
res = new(v1.HotReplyRes)
|
||||
// increment hot view and hot click count
|
||||
s.incrHot(req)
|
||||
if tp, exists := s.oidWhiteList[req.Oid]; exists && int32(tp) == req.Tp {
|
||||
res.Name = model.DefaultSlotName
|
||||
return
|
||||
}
|
||||
res.Name, ok = s.name(req.Mid)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ok, err = s.dao.ExpireReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp)); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
if res.RpIDs, err = s.dao.ReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp), int(start), int(end)); err != nil {
|
||||
return
|
||||
}
|
||||
if count, err = s.dao.CountReplyZSetRds(ctx, res.Name, req.Oid, int(req.Tp)); err != nil {
|
||||
return
|
||||
}
|
||||
res.Count = int32(count)
|
||||
} else {
|
||||
// s.eventProducer.Send(ctx, strconv.FormatInt(req.Oid, 10), &model.EventMsg{Action: "re_idx", Oid: req.Oid, Tp: int(req.Tp)})
|
||||
res.Name = model.DefaultSlotName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Reply do increment reply view count.
|
||||
func (s *Service) Reply(ctx context.Context, req *v1.ReplyReq) (res *v1.ReplyRes, err error) {
|
||||
res = new(v1.ReplyRes)
|
||||
// 用户点开评论区的次数
|
||||
if req.Pn == 1 {
|
||||
s.incrView(req)
|
||||
}
|
||||
// 用户在评论区总浏览次数
|
||||
s.incrTotalView(req)
|
||||
res.Name, _ = s.name(req.Mid)
|
||||
return
|
||||
}
|
171
app/service/main/reply-feed/service/service.go
Normal file
171
app/service/main/reply-feed/service/service.go
Normal file
@ -0,0 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-common/app/service/main/reply-feed/conf"
|
||||
"go-common/app/service/main/reply-feed/dao"
|
||||
"go-common/app/service/main/reply-feed/model"
|
||||
"go-common/library/log"
|
||||
"go-common/library/net/netutil"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
// Service struct
|
||||
type Service struct {
|
||||
c *conf.Config
|
||||
dao *dao.Dao
|
||||
// backoff
|
||||
bc netutil.BackoffConfig
|
||||
cron *cron.Cron
|
||||
statisticsStats []model.StatisticsStat
|
||||
statisticsLock sync.RWMutex
|
||||
// eventProducer *databus.Databus
|
||||
midMapping map[int64]int
|
||||
oidWhiteList map[int64]int
|
||||
}
|
||||
|
||||
// New init
|
||||
func New(c *conf.Config) (s *Service) {
|
||||
s = &Service{
|
||||
c: c,
|
||||
dao: dao.New(c),
|
||||
bc: netutil.BackoffConfig{
|
||||
MaxDelay: 1 * time.Second,
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Factor: 1.6,
|
||||
Jitter: 0.2,
|
||||
},
|
||||
cron: cron.New(),
|
||||
statisticsStats: make([]model.StatisticsStat, model.SlotsNum),
|
||||
// eventProducer: databus.New(c.Databus.Event),
|
||||
midMapping: make(map[int64]int),
|
||||
oidWhiteList: make(map[int64]int),
|
||||
}
|
||||
// toml不支持int为key
|
||||
for k, v := range s.c.MidMapping {
|
||||
mid, err := strconv.ParseInt(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.midMapping[mid] = v
|
||||
}
|
||||
for k, v := range s.c.OidWhiteList {
|
||||
oid, err := strconv.ParseInt(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.oidWhiteList[oid] = v
|
||||
}
|
||||
// 初始化各个实验组所占流量槽位
|
||||
var err error
|
||||
if err = s.loadSlots(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go s.loadproc()
|
||||
// 每整小时执行一次将统计数据写入DB
|
||||
s.cron.AddFunc("@hourly", func() {
|
||||
s.persistStatistics()
|
||||
})
|
||||
s.cron.Start()
|
||||
return s
|
||||
}
|
||||
|
||||
// namePercent ...
|
||||
// func (s *Service) namePercent() map[string]int64 {
|
||||
// p := make(map[string]int64)
|
||||
// s.statisticsLock.RLock()
|
||||
// for _, slot := range s.statisticsStats {
|
||||
// if _, ok := p[slot.Name]; ok {
|
||||
// p[slot.Name]++
|
||||
// } else {
|
||||
// p[slot.Name] = 1
|
||||
// }
|
||||
// }
|
||||
// s.statisticsLock.RUnlock()
|
||||
// return p
|
||||
// }
|
||||
|
||||
func (s *Service) loadproc() {
|
||||
for {
|
||||
time.Sleep(time.Minute)
|
||||
s.loadSlots()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) loadSlots() (err error) {
|
||||
slotsMapping, err := s.dao.SlotsMapping(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.statisticsLock.Lock()
|
||||
for _, mapping := range slotsMapping {
|
||||
for _, slot := range mapping.Slots {
|
||||
s.statisticsStats[slot].Name = mapping.Name
|
||||
s.statisticsStats[slot].Slot = slot
|
||||
s.statisticsStats[slot].State = mapping.State
|
||||
}
|
||||
}
|
||||
s.statisticsLock.Unlock()
|
||||
log.Warn("statistics stat (%v)", s.statisticsStats)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) persistStatistics() {
|
||||
ctx := context.Background()
|
||||
statisticsMap := make(map[string]*model.StatisticsStat)
|
||||
s.statisticsLock.RLock()
|
||||
for _, stat := range s.statisticsStats {
|
||||
s, ok := statisticsMap[stat.Name]
|
||||
if ok {
|
||||
statisticsMap[stat.Name] = s.Merge(&stat)
|
||||
} else {
|
||||
statisticsMap[stat.Name] = &stat
|
||||
}
|
||||
}
|
||||
s.statisticsLock.RUnlock()
|
||||
now := time.Now()
|
||||
year, month, day := now.Date()
|
||||
date := year*10000 + int(month)*100 + day
|
||||
hour := now.Hour()
|
||||
for name, stat := range statisticsMap {
|
||||
err := s.dao.UpsertStatistics(ctx, name, date, hour, stat)
|
||||
var (
|
||||
retryTimes = 0
|
||||
maxRetryTimes = 5
|
||||
)
|
||||
for err != nil && retryTimes < maxRetryTimes {
|
||||
time.Sleep(s.bc.Backoff(retryTimes))
|
||||
err = s.dao.UpsertStatistics(ctx, name, date, hour, stat)
|
||||
retryTimes++
|
||||
}
|
||||
if retryTimes >= maxRetryTimes {
|
||||
log.Error("upsert statistics error retry reached max retry times.")
|
||||
}
|
||||
}
|
||||
for i := range s.statisticsStats {
|
||||
reset(&s.statisticsStats[i])
|
||||
}
|
||||
}
|
||||
|
||||
func reset(stat *model.StatisticsStat) {
|
||||
stat.HotView = 0
|
||||
stat.View = 0
|
||||
stat.HotClick = 0
|
||||
stat.TotalView = 0
|
||||
}
|
||||
|
||||
// Ping Service
|
||||
func (s *Service) Ping(ctx context.Context) (err error) {
|
||||
return s.dao.Ping(ctx)
|
||||
}
|
||||
|
||||
// Close Service
|
||||
func (s *Service) Close() {
|
||||
s.persistStatistics()
|
||||
s.dao.Close()
|
||||
}
|
Reference in New Issue
Block a user