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,25 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/main/thumbup/api:all-srcs",
"//app/service/main/thumbup/cmd:all-srcs",
"//app/service/main/thumbup/conf:all-srcs",
"//app/service/main/thumbup/dao:all-srcs",
"//app/service/main/thumbup/model:all-srcs",
"//app/service/main/thumbup/rpc/client:all-srcs",
"//app/service/main/thumbup/server/gorpc:all-srcs",
"//app/service/main/thumbup/server/grpc:all-srcs",
"//app/service/main/thumbup/server/http:all-srcs",
"//app/service/main/thumbup/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,149 @@
### thumbup-service
##### Version 1.20.4
> 1. 使用zrange
##### Version 1.20.3
> 1. 异步发消息
##### Version 1.20.2
> 1. 优化 has like接口 查询500条
##### Version 1.20.1
> 1. has like接口返回点赞时间
##### Version 1.20.0
> 1. use tidb
##### Version 1.19.1
> 1. fixed panic bug
##### Version 1.19.0
> 1. 增加grpc接口
##### Version 1.18.0
> 1. 支持切换 tidb 写
> 2. 增加正序查询压测接口
> 3. 增加单查压测接口
##### Version 1.17.0
> 1. 增加压测接口
##### Version 1.16.5
> 1. 恢复tidb
##### Version 1.16.3
> 1. 停掉tidb 双写
##### Version 1.16.2
> 1. 写流量迁移到mysql
##### Version 1.16.1
> 1. 增加读流量来源切换
##### Version 1.16.0
> 1. 拜年祭需求
##### Version 1.15.3
> 1. 写接口增加mysql降级
##### Version 1.15.2
> 1. 增加up主mid更新接口验证
##### Version 1.15.1
> 1. update_upmids接口改为只更新不增加
##### Version 1.15.0
> 1. 增加update_upmids接口
##### Version 1.14.0
> 1. tidb双写
##### Version 1.13.0
> 1. 从tidb获取数据
##### Version 1.12.1
> 1. item忽略mtime索引
##### Version 1.12.0
> 1. databus增加up_mid
##### Version 1.11.1
> 1. 修复pn ps 为0的问题
##### Version 1.11.0
> 1. 新增点赞返回计数信息的RPC接口
##### Version 1.10.0
> 1. 点赞时更新up mid
##### Version 1.9.0
> 1. 增加被点赞人字段
##### Version 1.8.3
> 1. add ut
##### Version 1.8.2
> 1. 使用新的rpc server
##### Version 1.8.1
> 1. 去除remote ip
##### Version 1.8.0
> 1. db主从分离
##### Version 1.7.0
> 1. use new verify
##### Version 1.6.0
> 1. 计数流增加mid字段
##### Version 1.5.2
> 1. 点赞人列表增加mid字段
##### Version 1.5.1
> 1. 修复回源率统计
##### Version 1.5.0
> 1. 支持修改点赞/踩的值
##### Version 1.4.4
> 1. 升级tools/cache
##### Version 1.4.3
> 1. add register
##### Version 1.4.2
> 1. use bm
##### Version 1.4.1
> 1. 修复单飞问题
##### Version 1.4.0
> 1.缓存使用缓存工具重构
> 2.去掉article缓存包的依赖
> 3.业务表增加用户点赞列表 减少不必要的查询
##### Version 1.3.2
> 1.点赞bug修复
##### Version 1.3.1
> 1.点赞计数聚合写入
##### Version 1.3.0
> 1.发送计数databus信息
##### Version 1.2.9
> 1.发送稿件databus信息
##### Version 1.2.8
> 1.重复点赞的时候报错
> 2.增加点赞总数接口
##### Version 1.2.7
> 1.修复类型转换错误
##### Version 1.2.5
> 1.支持跨业务批量查询点赞
##### Version 1.2.4
> 1.重复点赞的时候更新时间
##### Version 1.2.3
> 1.fix like check bug
##### Version 1.2.2
> 1.stats 接口优化 返回空数据
##### Version 1.2.1
> 1.stats 接口优化 && fix bug
##### Version 1.2.0
> 1.stats 接口优化空缓存 && 去掉json解析
##### Version 1.1.1
> 1.identity bugfix
##### Version 1.1.0
> 1.合并interface到service
> 2.是否点赞接口批量查询优化
##### Version 1.0.0
> 1.点赞项目初始化

View File

@@ -0,0 +1,10 @@
# Owner
liweijia
zhapuyu
renwei
# Author
wangxu01
# Reviewer
zhapuyu

View File

@@ -0,0 +1,16 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liweijia
- renwei
- wangxu01
- zhapuyu
labels:
- main
- service
- service/main/thumbup
options:
no_parent_owners: true
reviewers:
- wangxu01
- zhapuyu

View File

@@ -0,0 +1,13 @@
#### thumbup-service
##### 项目简介
> 1.点赞平台项目
##### 编译环境
> 请只用golang v1.7.x以上版本编译执行。
##### 依赖包
> 1.公共包go-common
##### 特别说明
> 1.model目录可能会被其他项目引用请谨慎请改并通知各方。

View File

@@ -0,0 +1,66 @@
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/main/thumbup/api",
proto = ":api_proto",
tags = ["automanaged"],
deps = [
"//library/time:go_default_library",
"@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"],
embed = [":api_go_proto"],
importpath = "go-common/app/service/main/thumbup/api",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/net/rpc/warden:go_default_library",
"//library/time: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,247 @@
syntax = "proto3";
package community.service.thumbup.v1;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
option go_package = "api";
option (gogoproto.goproto_getters_all) = false;
service Thumbup {
// 点赞接口
rpc Like(LikeReq) returns(LikeReply);
// 查询计数接口
rpc Stats(StatsReq) returns(StatsReply);
// 批量查询计数接口
rpc MultiStats(MultiStatsReq) returns(MultiStatsReply);
// 查询是否点赞接口
rpc HasLike(HasLikeReq) returns (HasLikeReply);
// 用户对业务的点赞列表
rpc UserLikes(UserLikesReq) returns(UserLikesReply);
// 对象的点赞人列表
rpc ItemLikes(ItemLikesReq) returns(ItemLikesReply);
// 修改计数的值
rpc UpdateCount(UpdateCountReq) returns(.google.protobuf.Empty);
// 查询原始计数 未修改的值
rpc RawStat(RawStatReq) returns(RawStatReply);
}
enum Action {
// The first value represents the default and must be == 0.
ACTION_UNSPECIFIED = 0;
ACTION_LIKE = 1;
ACTION_CANCEL_LIKE = 2;
ACTION_DISLIKE = 3;
ACTION_CANCEL_DISLIKE = 4;
}
enum State {
// The first value represents the default and must be == 0.
STATE_UNSPECIFIED = 0;
STATE_LIKE = 1;
STATE_DISLIKE = 2;
}
message LikeReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// mid
int64 mid = 2 [(gogoproto.moretags) = 'form:"mid" validate:"required,min=1"'];
// up 主mid
int64 up_mid = 3 [(gogoproto.moretags) = 'form:"up_mid"'];
// 来源id
int64 origin_id = 4 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 5 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
Action action = 6 [(gogoproto.moretags) = 'form:"action_id" validate:"required"'];
// ip
string ip = 7 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message StatState {
// 来源id
int64 origin_id = 1 [(gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 2 [(gogoproto.customname) = "MessageID"];
// 点赞数
int64 like_number = 3;
// 点踩数
int64 dislike_number = 4;
// 点赞状态
State like_state = 5;
}
message LikeReply {
// 来源id
int64 origin_id = 1 [(gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 2 [(gogoproto.customname) = "MessageID"];
// 点赞数
int64 like_number = 3;
// 点踩数
int64 dislike_number = 4;
}
message StatsReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// 来源id
int64 origin_id = 2 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
repeated int64 message_ids = 3 [(gogoproto.moretags) = 'form:"message_ids" validate:"required"'];
// mid 可选参数 不需要返回like_state不要填
int64 mid = 4 [(gogoproto.moretags) = 'form:"mid"'];
// ip
string ip = 5 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message StatsReply {
map<int64, StatState> stats = 1;
}
message HasLikeReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// 对象id
repeated int64 message_ids = 2 [(gogoproto.moretags) = 'form:"message_ids" validate:"required"'];
// mid
int64 mid = 3 [(gogoproto.moretags) = 'form:"mid" validate:"required"'];
// ip
string ip = 4 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message HasLikeReply {
map<int64, UserLikeState> states = 1;
}
message UserLikeState {
// mid
int64 mid = 1;
// 点赞时间
int64 time = 2[(gogoproto.casttype) = "go-common/library/time.Time"];
State state = 3;
}
message UserLikesReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// mid
int64 mid = 2 [(gogoproto.moretags) = 'form:"mid" validate:"required"'];
// pn
int64 pn = 3 [(gogoproto.moretags) = 'form:"pn" validate:"required,min=1"'];
// ps
int64 ps = 4 [(gogoproto.moretags) = 'form:"ps" validate:"required,min=1"'];
// ip
string ip = 5 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message ItemRecord {
// 对象id
int64 message_id = 1 [(gogoproto.customname) = "MessageID"];
// 点赞时间
int64 time = 2[(gogoproto.casttype) = "go-common/library/time.Time"];
}
message UserLikesReply {
int64 total = 1;
repeated ItemRecord items = 2;
}
message UserRecord {
// mid
int64 mid = 1;
// 点赞时间
int64 time = 2[(gogoproto.casttype) = "go-common/library/time.Time"];
}
message ItemLikesReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// 来源id
int64 origin_id = 2 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 3 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
// last_mid 上个mid 去重用
int64 last_mid = 4 [(gogoproto.moretags) = 'form:"last_mid" json:"last_mid"'];
// pn
int64 pn = 5 [(gogoproto.moretags) = 'form:"pn" validate:"required,min=1"'];
// ps
int64 ps = 6 [(gogoproto.moretags) = 'form:"ps" validate:"required,min=1"'];
// ip
string ip = 7 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message ItemLikesReply {
repeated UserRecord users = 1;
}
message UpdateCountReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// 来源id
int64 origin_id = 2 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 3 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
// 点赞数修改(增量)
int64 like_change = 4;
// 点踩数修改(增量)
int64 dislike_change = 5;
// 操作人
string operator = 6;
// ip
string ip = 7 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message RawStatReq {
// 业务
string business = 1 [(gogoproto.moretags) = 'form:"business" validate:"required"'];
// 来源id
int64 origin_id = 2 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 3 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
// ip
string ip = 4 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message RawStatReply {
// 来源id
int64 origin_id = 1 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 2 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
// 点赞数
int64 like_number = 3;
// 点踩数
int64 dislike_number = 4;
// 点赞数修改(增量)
int64 like_change = 5;
// 点踩数修改(增量)
int64 dislike_change = 6;
}
message MultiStatsReq {
message Record {
// 来源id
int64 origin_id = 1 [(gogoproto.moretags) = 'form:"origin_id"', (gogoproto.customname) = "OriginID"];
// 对象id
int64 message_id = 2 [(gogoproto.moretags) = 'form:"message_id" validate:"required"', (gogoproto.customname) = "MessageID"];
}
message Business {
repeated Record records = 1;
}
// mid
int64 mid = 1 [(gogoproto.moretags) = 'form:"mid" validate:"required"'];
// business and records
map<string, Business> business = 2;
// ip
string ip = 3 [(gogoproto.jsontag) = "ip", (gogoproto.customname) = "IP"];
}
message MultiStatsReply {
message Records {
map<int64, StatState> records = 1;
}
// business and records
map<string, Records> business = 1;
}

View File

@@ -0,0 +1,25 @@
package api
import (
"context"
"fmt"
"go-common/library/net/rpc/warden"
"google.golang.org/grpc"
)
// AppID .
const AppID = "community.service.thumbup"
// NewClient new grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (ThumbupClient, error) {
client := warden.NewClient(cfg, opts...)
cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID))
if err != nil {
return nil, err
}
return NewThumbupClient(cc), nil
}
//go:generate $GOPATH/src/go-common/app/tool/warden/protoc.sh

View File

@@ -0,0 +1,46 @@
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 = ["thumbup-test.toml"],
importpath = "go-common/app/service/main/thumbup/cmd",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/server/gorpc:go_default_library",
"//app/service/main/thumbup/server/grpc:go_default_library",
"//app/service/main/thumbup/server/http:go_default_library",
"//app/service/main/thumbup/service:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
"//library/queue/databus/report: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,60 @@
package main
import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/service/main/thumbup/conf"
rpc "go-common/app/service/main/thumbup/server/gorpc"
grpc "go-common/app/service/main/thumbup/server/grpc"
"go-common/app/service/main/thumbup/server/http"
"go-common/app/service/main/thumbup/service"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/trace"
"go-common/library/queue/databus/report"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
log.Init(conf.Conf.Log)
trace.Init(conf.Conf.Tracer)
defer trace.Close()
defer log.Close()
log.Info("thumbup start")
ecode.Init(conf.Conf.Ecode)
report.InitManager(nil)
// server init
svr := service.New(conf.Conf)
http.Init(conf.Conf, svr)
rpcSvr := rpc.New(conf.Conf, svr)
grpcSvr := grpc.New(conf.Conf.GRPC, svr)
// signal handler
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("thumbup get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
log.Info("thumbup exit")
rpcSvr.Close()
grpcSvr.Shutdown(context.TODO())
svr.Close()
time.Sleep(time.Second)
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}

View File

@@ -0,0 +1,6 @@
#!/bin/bash
command -v goconvey >/dev/null 2>&1 || { echo >&2 "required goconvey but it's not installed."; echo "Aborting."; echo "Please run commond: go get github.com/smartystreets/goconvey"; exit 1; }
cd ../
goconvey -excludedDirs "vendor,node_modules,rpc" -packages 1

View File

@@ -0,0 +1,121 @@
[rpcServer]
proto = "tcp"
addr = "0.0.0.0:7249"
[grpc]
timeout = "1s"
addr = "0.0.0.0:7245"
[bm]
addr = "0.0.0.0:7246"
timeout = "1s"
[redis]
name = "thumbup-service"
proto = "tcp"
addr = "172.18.33.60:6981"
idle = 10
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
StatsExpire = "6h"
UserLikesExpire = "1h"
ItemLikesExpire = "6h"
[tidb]
addr = "172.22.34.51"
dsn = "likes:wy5taQn9CBKy9z5elbZQjnwMZOgpzk0r@tcp(172.22.34.51:4000)/bilibili_likes?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 2
idleTimeout ="4h"
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "1s"
[tidb.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[memcache]
name = "thumbup-service"
proto = "tcp"
addr = "172.18.33.61:11230"
idle = 10
active = 10
dialTimeout = "2s"
readTimeout = "2s"
writeTimeout = "2s"
idleTimeout = "7h"
StatsExpire = "6h"
[statDatabus]
key = "9765cdac5894f2ba"
secret = "f4237d712c3ed1e7fab0137b81418b14"
group= "StatLike-MainWebSvr-P"
topic= "StatLike-T"
action="pub"
name = "thumbup-service/stat-pub"
proto = "tcp"
addr = "172.22.33.174:6205"
active = 5
idle = 1
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
[likeDatabus]
key = "9765cdac5894f2ba"
secret = "f4237d712c3ed1e7fab0137b81418b14"
group = "Thumbup-MainWebSvr-P"
topic = "Thumbup-T"
action = "pub"
buffer = 2048
name = "thumbup-pub"
proto = "tcp"
addr = "172.22.33.174:6205"
idle = 1
active = 1
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "1s"
[itemDatabus]
key = "9765cdac5894f2ba"
secret = "f4237d712c3ed1e7fab0137b81418b14"
group= "ThumbupItem-MainWebSvr-P"
topic= "ThumbupItem-T"
action="pub"
name = "thumbup-service/item-pub"
proto = "tcp"
addr = "172.22.33.174:6205"
active = 5
idle = 1
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
[userDatabus]
key = "9765cdac5894f2ba"
secret = "f4237d712c3ed1e7fab0137b81418b14"
group= "ThumbupUser-MainWebSvr-P"
topic= "ThumbupUser-T"
action="pub"
name = "thumbup-service/item-pub"
proto = "tcp"
addr = "172.22.33.174:6205"
active = 5
idle = 1
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"
[rate]
[rate.urls]
"/x/internal/thumbup/update_upmids" = {limit = 10000.0, burst = 10000}
[thumbup]

View File

@@ -0,0 +1,44 @@
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/thumbup/conf",
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/tidb: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/rate:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/rpc: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"],
)

View File

@@ -0,0 +1,138 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/tidb"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/rate"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/rpc"
"go-common/library/net/rpc/warden"
"go-common/library/net/trace"
"go-common/library/queue/databus"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
// global var
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config config set
type Config struct {
// elk
Log *log.Config
// BM
BM *bm.ServerConfig
// rpc server
RPCServer *rpc.ServerConfig
GRPC *warden.ServerConfig
// tracer
Tracer *trace.Config
// verify
Verify *verify.Config
Rate *rate.Config
// redis
Redis *Redis
// memcache
Memcache *Memcache
// Tidb
Tidb *tidb.Config
// ecode
Ecode *ecode.Config
StatDatabus *databus.Config
LikeDatabus *databus.Config
ItemDatabus *databus.Config
UserDatabus *databus.Config
StatMerge *StatMerge
// ThumbUp
ThumbUp ThumbUp
}
// StatMerge .
type StatMerge struct {
Business string
Target int64
Sources []int64
}
// Memcache config
type Memcache struct {
*memcache.Config
StatsExpire time.Duration
}
// Redis config
type Redis struct {
*redis.Config
StatsExpire time.Duration
UserLikesExpire time.Duration
ItemLikesExpire time.Duration
}
// ThumbUp thumb up config
type ThumbUp struct {
}
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
}

View File

@@ -0,0 +1,78 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"cache_test.go",
"dao.cache_test.go",
"dao_test.go",
"databus_test.go",
"item_likes_redis_test.go",
"memcached_test.go",
"redis_test.go",
"tidb_test.go",
"user_likes_redis_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"cache.go",
"dao.cache.go",
"dao.go",
"databus.go",
"item_likes_redis.go",
"memcached.go",
"redis.go",
"tidb.go",
"user_likes_redis.go",
],
importpath = "go-common/app/service/main/thumbup/dao",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/api:go_default_library",
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/tidb:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//library/time:go_default_library",
"//library/xstr:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/golang.org/x/sync/singleflight: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,31 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/main/thumbup/model"
)
//go:generate $GOPATH/src/go-common/app/tool/cache/gen
type _cache interface {
// 用户点赞列表
// cache: -singleflight=true -ignores=||start,end
userLikeList(c context.Context, mid int64, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error)
}
func (d *Dao) cacheSFuserLikeList(mid, businessID int64, state int8, start, end int) string {
return fmt.Sprintf("sf_u%v_%v_%v", mid, businessID, state)
}
// UserLikeList 用户点赞列表
func (d *Dao) UserLikeList(c context.Context, mid int64, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error) {
var ls []*model.ItemLikeRecord
ls, err = d.userLikeList(c, mid, businessID, state, start, end)
for _, x := range ls {
if x.MessageID != -1 {
res = append(res, x)
}
}
return
}

View File

@@ -0,0 +1,26 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoUserLikeList(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
state = int8(1)
start = int(0)
end = int(10)
)
convey.Convey("UserLikeList", t, func(ctx convey.C) {
_, err := d.UserLikeList(c, mid, businessID, state, start, end)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// ctx.So(res, convey.ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,58 @@
// 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: -singleflight=true -ignores=||start,end
userLikeList(c context.Context, mid int64, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error)
}
*/
package dao
import (
"context"
"go-common/app/service/main/thumbup/model"
"go-common/library/stat/prom"
"golang.org/x/sync/singleflight"
)
var _ _cache
var cacheSingleFlights = [1]*singleflight.Group{{}}
// userLikeList 用户点赞列表
func (d *Dao) userLikeList(c context.Context, id int64, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error) {
addCache := true
res, err = d.CacheUserLikeList(c, id, businessID, state, start, end)
if err != nil {
addCache = false
err = nil
}
if len(res) != 0 {
prom.CacheHit.Incr("userLikeList")
return
}
var rr interface{}
sf := d.cacheSFuserLikeList(id, businessID, state, start, end)
rr, err, _ = cacheSingleFlights[0].Do(sf, func() (r interface{}, e error) {
prom.CacheMiss.Incr("userLikeList")
r, e = d.RawUserLikeList(c, id, businessID, state, start, end)
return
})
res = rr.([]*model.ItemLikeRecord)
if err != nil {
return
}
miss := res
if !addCache {
return
}
d.cache.Do(c, func(c context.Context) {
d.AddCacheUserLikeList(c, id, miss, businessID, state)
})
return
}

View File

@@ -0,0 +1,26 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaouserLikeList(t *testing.T) {
var (
c = context.TODO()
id = int64(1)
businessID = int64(1)
state = int8(1)
start = int(1)
end = int(1)
)
convey.Convey("userLikeList", t, func(ctx convey.C) {
_, err := d.userLikeList(c, id, businessID, state, start, end)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// ctx.So(res, convey.ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,166 @@
package dao
import (
"context"
"time"
"go-common/app/service/main/thumbup/conf"
"go-common/app/service/main/thumbup/model"
"go-common/library/cache/memcache"
xredis "go-common/library/cache/redis"
"go-common/library/database/tidb"
"go-common/library/log"
"go-common/library/queue/databus"
"go-common/library/stat/prom"
"go-common/library/sync/pipeline/fanout"
)
// PromError prom error
func PromError(name string) {
prom.BusinessErrCount.Incr(name)
}
// Dao dao
type Dao struct {
// config
c *conf.Config
// db
tidb *tidb.DB
// memcache
mc *memcache.Pool
mcStatsExpire int32
//redis
redis *xredis.Pool
redisStatsExpire int64
redisUserLikesExpire int64
redisItemLikesExpire int64
// redisSortExpire int64
// stmt
businessesStmt *tidb.Stmts
likeStateStmt *tidb.Stmts
userLikeCountStmt *tidb.Stmts
itemLikeListStmt *tidb.Stmts
userLikeListStmt *tidb.Stmts
statsOriginStmt *tidb.Stmts
statStmt *tidb.Stmts
updateLikeStmt *tidb.Stmts
updateCountChangeStmt *tidb.Stmts
statDbus *databus.Databus
likeDbus *databus.Databus
itemDbus *databus.Databus
userDbus *databus.Databus
cache *fanout.Fanout
async *fanout.Fanout
tidbAsync *fanout.Fanout
BusinessMap map[string]*model.Business
BusinessIDMap map[int64]*model.Business
}
// New dao new
func New(c *conf.Config) (d *Dao) {
d = &Dao{
// config
c: c,
// mc
mc: memcache.NewPool(c.Memcache.Config),
mcStatsExpire: int32(time.Duration(c.Memcache.StatsExpire) / time.Second),
// redis
redis: xredis.NewPool(c.Redis.Config),
redisStatsExpire: int64(time.Duration(c.Redis.StatsExpire) / time.Second),
redisUserLikesExpire: int64(time.Duration(c.Redis.UserLikesExpire) / time.Second),
redisItemLikesExpire: int64(time.Duration(c.Redis.ItemLikesExpire) / time.Second),
// db
tidb: tidb.NewTiDB(c.Tidb),
statDbus: databus.New(c.StatDatabus),
likeDbus: databus.New(c.LikeDatabus),
itemDbus: databus.New(c.ItemDatabus),
userDbus: databus.New(c.UserDatabus),
cache: fanout.New("cache", fanout.Worker(1), fanout.Buffer(10240)),
async: fanout.New("async", fanout.Worker(2), fanout.Buffer(10240)),
tidbAsync: fanout.New("tidb-async", fanout.Worker(10), fanout.Buffer(10240)),
}
d.businessesStmt = d.tidb.Prepared(_tidbBusinessesSQL)
d.likeStateStmt = d.tidb.Prepared(_tidbLikeMidSQL)
d.userLikeCountStmt = d.tidb.Prepared(_tidbUserLikeCountSQL)
d.itemLikeListStmt = d.tidb.Prepared(_tidbItemLikeListSQL)
d.userLikeListStmt = d.tidb.Prepared(_tidbUserLikeListSQL)
d.statsOriginStmt = d.tidb.Prepared(_tidbStatsOriginSQL)
d.statStmt = d.tidb.Prepared(_tidbStatSQL)
d.updateLikeStmt = d.tidb.Prepared(_tidbUpdateLikeSQL)
d.updateCountChangeStmt = d.tidb.Prepared(_tidbupdateCountChange)
d.loadBusiness()
go d.loadBusinessproc()
return d
}
// Ping check connection success.
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.pingMC(c); err != nil {
PromError("mc:Ping")
log.Error("d.pingMC error(%v)", err)
return
}
if err = d.pingRedis(c); err != nil {
PromError("redis:Ping")
log.Error("d.pingRedis error(%v)", err)
return
}
return
}
// Close close resource.
func (d *Dao) Close() {
d.async.Close()
d.tidbAsync.Close()
d.tidb.Close()
d.mc.Close()
d.redis.Close()
}
// pingMc ping memcache
func (d *Dao) pingMC(c context.Context) (err error) {
conn := d.mc.Get(c)
defer conn.Close()
item := memcache.Item{Key: "ping", Value: []byte{1}, Expiration: 100}
err = conn.Set(&item)
return
}
// pingRedis ping redis.
func (d *Dao) pingRedis(c context.Context) (err error) {
conn := d.redis.Get(c)
if _, err = conn.Do("SET", "PING", "PONG"); err != nil {
PromError("redis: ping remote")
log.Error("remote redis: conn.Do(SET,PING,PONG) error(%v)", err)
}
conn.Close()
return
}
// LoadBusiness .
func (d *Dao) loadBusiness() {
var business []*model.Business
var err error
businessMap := make(map[string]*model.Business)
businessIDMap := make(map[int64]*model.Business)
for {
if business, err = d.Businesses(context.TODO()); err != nil {
time.Sleep(time.Second)
continue
}
for _, b := range business {
businessMap[b.Name] = b
businessIDMap[b.ID] = b
}
d.BusinessMap = businessMap
d.BusinessIDMap = businessIDMap
return
}
}
func (d *Dao) loadBusinessproc() {
for {
time.Sleep(time.Minute * 5)
d.loadBusiness()
}
}

View File

@@ -0,0 +1,39 @@
package dao
import (
"flag"
"os"
"testing"
"go-common/app/service/main/thumbup/conf"
)
var d *Dao
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.web-svr.thumbup-service")
flag.Set("conf_token", "VhnSEtd0oymsNQaDUYuEknoWu2mVOOVK")
flag.Set("tree_id", "7720")
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/thumbup-test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
m.Run()
os.Exit(0)
}
// INSERT INTO `bilibili_likes`.`counts_01`(`id`, `mtime`, `ctime`, `business_id`, `origin_id`, `message_id`, `likes_count`, `dislikes_count`, `likes_change`, `dislikes_change`) VALUES (98, '2018-06-13 14:51:11', '2018-06-13 14:51:11', 1, 99901, 8888, 1, 2, 3, 4);
// INSERT INTO `bilibili_likes`.`counts_01`(`id`, `mtime`, `ctime`, `business_id`, `origin_id`, `message_id`, `likes_count`, `dislikes_count`, `likes_change`, `dislikes_change`) VALUES (99, '2018-06-13 14:56:56', '2018-06-13 14:56:56', 1, 101, 8888, 0, 0, 0, 0);
// INSERT INTO `bilibili_likes`.`likes`(`id`, `mtime`, `ctime`, `business_id`, `origin_id`, `message_id`, `mid`, `type`) VALUES (0, '2018-11-01 18:03:28', '2018-11-01 18:03:28', 1, 1, 1, 1, 1);
// INSERT INTO `bilibili_likes`.`counts`(`id`, `mtime`, `ctime`, `business_id`, `origin_id`, `message_id`, `likes_count`, `dislikes_count`, `likes_change`, `dislikes_change`, `up_mid`) VALUES (0, '2018-11-03 12:16:25', '2018-11-03 12:16:25', 1, 1, 1, 1, 1, 0, 0, 0);

View File

@@ -0,0 +1,72 @@
package dao
import (
"context"
"strconv"
"time"
"go-common/app/service/main/thumbup/model"
"go-common/library/log"
)
// PubStatDatabus pub stat databus
func (d *Dao) PubStatDatabus(c context.Context, business string, mid int64, s *model.Stats, upMid int64) (err error) {
msg := &model.StatMsg{Type: business, ID: s.ID, Count: s.Likes, Timestamp: time.Now().Unix(), OriginID: s.OriginID, DislikeCount: s.Dislikes, Mid: mid, UpMid: upMid}
if err = d.statDbus.Send(c, strconv.FormatInt(s.ID, 10), msg); err != nil {
log.Error("d.databus.Send error(%v)", err)
PromError("databus:stat")
return
}
log.Info("s.PubStatDatabus (%+v)", msg)
return
}
// PubLikeDatabus .
func (d *Dao) PubLikeDatabus(c context.Context, p *model.LikeMsg) (err error) {
if err = d.likeDbus.Send(c, strconv.FormatInt(p.Mid, 10), p); err != nil {
log.Error("d.likeDbus.Send error(%v)", err)
PromError("databus:like")
return
}
log.Info("s.PubLikeDatabus success (%+v)", p)
return
}
// PubItemMsg .
func (d *Dao) PubItemMsg(c context.Context, business string, originID, messageID int64, state int8) (err error) {
msg := &model.ItemMsg{
State: state,
Business: business,
OriginID: originID,
MessageID: messageID,
}
if err = d.itemDbus.Send(c, strconv.FormatInt(messageID, 10), msg); err != nil {
log.Error("d.PubItemMsg.databus.Send error(%v)", err)
PromError("databus:item")
return
}
log.Info("s.PubItemMsg success (%+v)", msg)
return
}
// PubUserMsg .
func (d *Dao) PubUserMsg(c context.Context, business string, mid int64, state int8) (err error) {
msg := &model.UserMsg{
Mid: mid,
State: state,
Business: business,
}
if err = d.userDbus.Send(c, strconv.FormatInt(mid, 10), msg); err != nil {
log.Error("d.PubUserMsg.databus.Send error(%v)", err)
PromError("databus:user")
return
}
log.Info("s.PubUserMsg success (%+v)", msg)
return
}
// AddCacheUserLikeList .
func (d *Dao) AddCacheUserLikeList(c context.Context, mid int64, miss []*model.ItemLikeRecord, businessID int64, state int8) (err error) {
err = d.PubUserMsg(c, d.BusinessIDMap[businessID].Name, mid, state)
return
}

View File

@@ -0,0 +1,24 @@
package dao
import (
"context"
"go-common/app/service/main/thumbup/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoPubStatDatabus(t *testing.T) {
var (
c = context.TODO()
business = "archive"
mid = int64(1)
s = &model.Stats{}
)
convey.Convey("PubStatDatabus", t, func(ctx convey.C) {
err := d.PubStatDatabus(c, business, mid, s, 1)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,206 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/main/thumbup/model"
"go-common/library/cache/redis"
"go-common/library/log"
xtime "go-common/library/time"
)
func itemLikesKey(businessID, messageID int64, state int8) string {
return fmt.Sprintf("i2_m_%d_b_%d_%d", messageID, businessID, state)
}
// CacheItemLikeList .
func (d *Dao) CacheItemLikeList(c context.Context, messageID, businessID int64, state int8, start, end int) (res []*model.UserLikeRecord, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
items, err := redis.Values(conn.Do("ZREVRANGE", key, start, end, "withscores"))
if err != nil {
if err == redis.ErrNil {
err = nil
return
}
PromError("redis:CacheItemLikeList")
log.Errorv(c, log.KV("CacheItemLikeList", fmt.Sprintf("%+v", err)))
return
}
for len(items) > 0 {
var id, t int64
if items, err = redis.Scan(items, &id, &t); err != nil {
PromError("redis:CacheItemLikeList")
log.Errorv(c, log.KV("CacheItemLikeList", fmt.Sprintf("%+v", err)))
return
}
res = append(res, &model.UserLikeRecord{Mid: id, Time: xtime.Time(t)})
}
return
}
// AddCacheItemLikeList .
func (d *Dao) AddCacheItemLikeList(c context.Context, messageID int64, miss []*model.UserLikeRecord, businessID int64, state int8) (err error) {
if len(miss) == 0 {
return
}
limit := d.BusinessIDMap[businessID].MessageLikesLimit
var count int
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
if err = conn.Send("DEL", key); err != nil {
PromError("redis:项目点赞列表")
log.Errorv(c, log.KV("AddCacheItemLikeList", fmt.Sprintf("AddCacheItemLikeList conn.Send(DEL, %s) error(%+v)", key, err)))
return
}
count++
for _, item := range miss {
id := item.Mid
score := int64(item.Time)
if err = conn.Send("ZADD", key, "CH", score, id); err != nil {
PromError("redis:项目点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("AddCacheItemLikeList conn.Send(ZADD, %s, %d, %v) error(%v)", key, score, id, err)))
return
}
count++
}
if err = conn.Send("ZREMRANGEBYRANK", key, 0, -(limit + 1)); err != nil {
PromError("redis:项目点赞列表rm")
log.Errorv(c, log.KV("log", fmt.Sprintf("AddCacheItemLikeList conn.Send(ZREMRANGEBYRANK, %s, 0, %d) error(%v)", key, -(limit+1), err)))
return
}
count++
if err = conn.Send("EXPIRE", key, d.redisItemLikesExpire); err != nil {
PromError("redis:项目点赞列表过期")
log.Errorv(c, log.KV("log", fmt.Sprintf("AddCacheItemLikeList conn.Send(EXPIRE, %s, %d) error(%v)", key, d.redisItemLikesExpire, err)))
return
}
count++
if err = conn.Flush(); err != nil {
PromError("redis:项目点赞列表flush")
log.Errorv(c, log.KV("log", fmt.Sprintf("AddCacheItemLikeList conn.Flush error(%v)", err)))
return
}
for i := 0; i < count; i++ {
if _, err = conn.Receive(); err != nil {
PromError("redis:项目点赞列表receive")
log.Errorv(c, log.KV("log", fmt.Sprintf("AddCacheItemLikeList conn.Receive error(%v)", err)), log.KV("miss", miss))
return
}
}
return
}
// ExpireItemLikesCache .
func (d *Dao) ExpireItemLikesCache(c context.Context, messageID, businessID int64, state int8) (ok bool, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.redisItemLikesExpire)); err != nil {
PromError("redis:expire项目点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(EXPIRE, %s) error(%v)", key, err)))
}
return
}
// ItemLikeExists .
func (d *Dao) ItemLikeExists(c context.Context, messageID, businessID int64, mids []int64, state int8) (res map[int64]int64, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
res = make(map[int64]int64)
for _, name := range mids {
conn.Send("ZSCORE", key, name)
}
if err = conn.Flush(); err != nil {
PromError("redis:ItemLikeExists")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Flush() error(%v)", err)))
return
}
for _, name := range mids {
var s int64
if s, err = redis.Int64(conn.Receive()); err == nil {
res[name] = s
} else if err == redis.ErrNil {
err = nil
} else {
PromError("redis:ItemLikeExists")
log.Errorv(c, log.KV("log", fmt.Sprintf("ItemLikeExists conn.Receive() error(%v)", err)))
return
}
}
return
}
// AppendCacheItemLikeList .
func (d *Dao) AppendCacheItemLikeList(c context.Context, messageID int64, item *model.UserLikeRecord, businessID int64, state int8) (err error) {
if item == nil {
return
}
limit := d.BusinessIDMap[businessID].MessageLikesLimit
var count int
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
id := item.Mid
score := int64(item.Time)
if err = conn.Send("ZADD", key, "CH", score, id); err != nil {
PromError("redis:项目点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZADD, %s, %d, %v) error(%v)", key, score, id, err)))
return
}
count++
if err = conn.Send("ZREMRANGEBYRANK", key, 0, -(limit + 1)); err != nil {
PromError("redis:项目点赞列表rm")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZREMRANGEBYRANK, %s, 0, %d) error(%v)", key, -(limit+1), err)))
return
}
count++
if err = conn.Send("EXPIRE", key, d.redisItemLikesExpire); err != nil {
PromError("redis:项目点赞列表过期")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(EXPIRE, %s, %d) error(%v)", key, d.redisItemLikesExpire, err)))
return
}
count++
if err = conn.Flush(); err != nil {
PromError("redis:项目点赞列表flush")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Flush error(%v)", err)))
return
}
for i := 0; i < count; i++ {
if _, err = conn.Receive(); err != nil {
PromError("redis:项目点赞列表receive")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Receive error(%v)", err)))
return
}
}
return
}
// DelItemLikeCache .
func (d *Dao) DelItemLikeCache(c context.Context, messageID, businessID int64, mid int64, state int8) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, state)
if _, err = conn.Do("ZREM", key, mid); err != nil {
PromError("redis:zrem项目点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZREM, %s, %v) error(%v)", key, mid, err)))
}
return
}
// ItemLikesCountCache .
func (d *Dao) ItemLikesCountCache(c context.Context, businessID, messageID int64) (res int, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := itemLikesKey(businessID, messageID, model.StateLike)
res, err = redis.Int(conn.Do("ZCOUNT", key, "(0", "+inf"))
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("dao.ItemLikesCountCache(%d, %d) err:%v", businessID, messageID, err)))
PromError("redis:项目点赞总数")
}
return
}

View File

@@ -0,0 +1,137 @@
package dao
import (
"context"
"go-common/app/service/main/thumbup/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoitemLikesKey(t *testing.T) {
var (
businessID = int64(1)
messageID = int64(1)
state = int8(1)
)
convey.Convey("itemLikesKey", t, func(ctx convey.C) {
p1 := itemLikesKey(businessID, messageID, state)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoCacheItemLikeList(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
businessID = int64(1)
state = int8(1)
start = int(1)
end = int(1)
)
convey.Convey("CacheItemLikeList", t, func(ctx convey.C) {
_, err := d.CacheItemLikeList(c, messageID, businessID, state, start, end)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddCacheItemLikeList(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
miss = []*model.UserLikeRecord{}
businessID = int64(1)
state = int8(1)
)
convey.Convey("AddCacheItemLikeList", t, func(ctx convey.C) {
err := d.AddCacheItemLikeList(c, messageID, miss, businessID, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoExpireItemLikesCache(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
businessID = int64(1)
state = int8(1)
)
convey.Convey("ExpireItemLikesCache", t, func(ctx convey.C) {
ok, err := d.ExpireItemLikesCache(c, messageID, businessID, state)
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 TestDaoItemLikeExists(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
businessID = int64(1)
mids = []int64{}
state = int8(1)
)
convey.Convey("ItemLikeExists", t, func(ctx convey.C) {
res, err := d.ItemLikeExists(c, messageID, businessID, mids, state)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAppendCacheItemLikeList(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
item = &model.UserLikeRecord{}
businessID = int64(1)
state = int8(1)
)
convey.Convey("AppendCacheItemLikeList", t, func(ctx convey.C) {
err := d.AppendCacheItemLikeList(c, messageID, item, businessID, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelItemLikeCache(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
businessID = int64(1)
mid = int64(1)
state = int8(1)
)
convey.Convey("DelItemLikeCache", t, func(ctx convey.C) {
err := d.DelItemLikeCache(c, messageID, businessID, mid, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoItemLikesCountCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
messageID = int64(1)
)
convey.Convey("ItemLikesCountCache", t, func(ctx convey.C) {
res, err := d.ItemLikesCountCache(c, businessID, messageID)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,147 @@
package dao
import (
"context"
"fmt"
"sync"
"go-common/app/service/main/thumbup/model"
"go-common/library/cache/memcache"
"go-common/library/log"
"go-common/library/xstr"
"go-common/library/sync/errgroup"
)
const (
_bulkSize = 100
)
func statsKey(businessID, messageID int64) string {
return fmt.Sprintf("m_%d_b_%d", messageID, businessID)
}
func recoverStatsValue(c context.Context, s string) (res *model.Stats) {
var (
vs []int64
err error
)
res = new(model.Stats)
if s == "" {
return
}
if vs, err = xstr.SplitInts(s); err != nil || len(vs) < 2 {
PromError("mc:stats解析")
log.Error("dao.recoverStatsValue(%s) err: %v", s, err)
return
}
res = &model.Stats{Likes: vs[0], Dislikes: vs[1]}
return
}
// AddStatsCache .
func (d *Dao) AddStatsCache(c context.Context, businessID int64, vs ...*model.Stats) (err error) {
if len(vs) == 0 {
return
}
conn := d.mc.Get(c)
defer conn.Close()
for _, v := range vs {
if v == nil {
continue
}
key := statsKey(businessID, v.ID)
bs := xstr.JoinInts([]int64{v.Likes, v.Dislikes})
item := memcache.Item{Key: key, Value: []byte(bs), Expiration: d.mcStatsExpire}
if err = conn.Set(&item); err != nil {
PromError("mc:增加计数缓存")
log.Error("conn.Set(%s) error(%v)", key, err)
return
}
}
return
}
// DelStatsCache del stats cache
func (d *Dao) DelStatsCache(c context.Context, businessID int64, messageID int64) (err error) {
conn := d.mc.Get(c)
defer conn.Close()
key := statsKey(businessID, messageID)
if err = conn.Delete(key); err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
PromError("mc:DelStatsCache")
log.Error("d.DelStatsCache(%s) error(%+v)", key, err)
}
return
}
// AddStatsCacheMap .
func (d *Dao) AddStatsCacheMap(c context.Context, businessID int64, stats map[int64]*model.Stats) (err error) {
var s []*model.Stats
for _, v := range stats {
s = append(s, v)
}
return d.AddStatsCache(c, businessID, s...)
}
// StatsCache .
func (d *Dao) StatsCache(c context.Context, businessID int64, messageIDs []int64) (cached map[int64]*model.Stats, missed []int64, err error) {
if len(messageIDs) == 0 {
return
}
cached = make(map[int64]*model.Stats, len(messageIDs))
allKeys := make([]string, 0, len(messageIDs))
midmap := make(map[string]int64, len(messageIDs))
for _, id := range messageIDs {
k := statsKey(businessID, id)
allKeys = append(allKeys, k)
midmap[k] = id
}
group, errCtx := errgroup.WithContext(c)
mutex := sync.Mutex{}
keysLen := len(allKeys)
for i := 0; i < keysLen; i += _bulkSize {
var keys []string
if (i + _bulkSize) > keysLen {
keys = allKeys[i:]
} else {
keys = allKeys[i : i+_bulkSize]
}
group.Go(func() (err error) {
conn := d.mc.Get(errCtx)
replys, err := conn.GetMulti(keys)
defer conn.Close()
if err != nil {
PromError("mc:获取计数缓存")
log.Error("conn.Gets(%v) error(%v)", keys, err)
err = nil
return
}
for _, reply := range replys {
var s string
if err = conn.Scan(reply, &s); err != nil {
PromError("获取计数缓存json解析")
log.Error("json.Unmarshal(%v) error(%v)", reply.Value, err)
err = nil
continue
}
stat := recoverStatsValue(c, s)
stat.ID = midmap[reply.Key]
mutex.Lock()
cached[midmap[reply.Key]] = stat
delete(midmap, reply.Key)
mutex.Unlock()
}
return
})
}
group.Wait()
missed = make([]int64, 0, len(midmap))
for _, aid := range midmap {
missed = append(missed, aid)
}
return
}

View File

@@ -0,0 +1,93 @@
package dao
import (
"context"
"go-common/app/service/main/thumbup/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaostatsKey(t *testing.T) {
var (
businessID = int64(1)
messageID = int64(1)
)
convey.Convey("statsKey", t, func(ctx convey.C) {
p1 := statsKey(businessID, messageID)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaorecoverStatsValue(t *testing.T) {
var (
c = context.TODO()
s = ""
)
convey.Convey("recoverStatsValue", t, func(ctx convey.C) {
res := recoverStatsValue(c, s)
ctx.Convey("Then res should not be nil.", func(ctx convey.C) {
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
vs = &model.Stats{}
)
convey.Convey("AddStatsCache", t, func(ctx convey.C) {
err := d.AddStatsCache(c, businessID, vs)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
messageID = int64(1)
)
convey.Convey("DelStatsCache", t, func(ctx convey.C) {
err := d.DelStatsCache(c, businessID, messageID)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoAddStatsCacheMap(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
stats map[int64]*model.Stats
)
convey.Convey("AddStatsCacheMap", t, func(ctx convey.C) {
err := d.AddStatsCacheMap(c, businessID, stats)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
messageIDs = []int64{1}
)
convey.Convey("StatsCache", t, func(ctx convey.C) {
cached, missed, err := d.StatsCache(c, businessID, messageIDs)
ctx.Convey("Then err should be nil.cached,missed should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(missed, convey.ShouldNotBeNil)
ctx.So(cached, convey.ShouldBeEmpty)
})
})
}

View File

@@ -0,0 +1,106 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/main/thumbup/model"
"go-common/library/cache/redis"
"go-common/library/log"
"go-common/library/xstr"
)
func hashStatsKey(businessID, originID int64) string {
return fmt.Sprintf("stats_o_%d_b_%d", originID, businessID)
}
// ExpireHashStatsCache .
func (d *Dao) ExpireHashStatsCache(c context.Context, businessID, originID int64) (ok bool, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := hashStatsKey(businessID, originID)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.redisStatsExpire)); err != nil {
PromError("redis:计数缓存设定过期")
log.Error("conn.Do(EXPIRE, %s, %d) error(%v)", key, d.redisStatsExpire, err)
}
return
}
// DelHashStatsCache del hash cache
func (d *Dao) DelHashStatsCache(c context.Context, businessID, originID int64) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := hashStatsKey(businessID, originID)
if _, err = conn.Do("del", key); err != nil {
PromError("redis:计数缓存删除")
log.Error("conn.Do(DEL, %s) error(%v)", key, err)
}
return
}
// HashStatsCache .
func (d *Dao) HashStatsCache(c context.Context, businessID, originID int64, messageIDs []int64) (res map[int64]*model.Stats, err error) {
if len(messageIDs) == 0 {
return
}
conn := d.redis.Get(c)
defer conn.Close()
key := hashStatsKey(businessID, originID)
var ss []string
var commonds []interface{}
commonds = append(commonds, key)
for _, m := range messageIDs {
commonds = append(commonds, m)
}
if ss, err = redis.Strings(conn.Do("HMGET", commonds...)); err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Error("conn.Do(HMGET, %s, %v) error(%v)", key, messageIDs, err)
PromError("redis:获取统计信息")
}
return
}
res = make(map[int64]*model.Stats)
for i, id := range messageIDs {
if ss[i] == "" {
continue
}
stat := &model.Stats{ID: id, OriginID: originID}
num, _ := xstr.SplitInts(ss[i])
if len(num) > 1 {
stat.Likes = num[0]
stat.Dislikes = num[1]
}
res[id] = stat
}
return
}
// AddHashStatsCache .
func (d *Dao) AddHashStatsCache(c context.Context, businessID, originID int64, stats ...*model.Stats) (err error) {
if len(stats) == 0 {
return
}
conn := d.redis.Get(c)
defer conn.Close()
key := hashStatsKey(businessID, originID)
var commonds = []interface{}{key}
for _, stat := range stats {
commonds = append(commonds, stat.ID, xstr.JoinInts([]int64{stat.Likes, stat.Dislikes}))
}
if _, err = conn.Do("HMSET", commonds...); err != nil {
PromError("redis:增加统计信息")
log.Error("conn.DO(HMSET, %s, %v) error(%v)", key, commonds, err)
}
return
}
// AddHashStatsCacheMap .
func (d *Dao) AddHashStatsCacheMap(c context.Context, businessID, originID int64, stats map[int64]*model.Stats) (err error) {
var s []*model.Stats
for _, v := range stats {
s = append(s, v)
}
return d.AddHashStatsCache(c, businessID, originID, s...)
}

View File

@@ -0,0 +1,98 @@
package dao
import (
"context"
"testing"
"go-common/app/service/main/thumbup/model"
"github.com/smartystreets/goconvey/convey"
)
func TestDaohashStatsKey(t *testing.T) {
var (
businessID = int64(1)
originID = int64(1)
)
convey.Convey("hashStatsKey", t, func(ctx convey.C) {
p1 := hashStatsKey(businessID, originID)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoExpireHashStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
)
convey.Convey("ExpireHashStatsCache", t, func(ctx convey.C) {
ok, err := d.ExpireHashStatsCache(c, businessID, originID)
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 TestDaoDelHashStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
)
convey.Convey("DelHashStatsCache", t, func(ctx convey.C) {
err := d.DelHashStatsCache(c, businessID, originID)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoHashStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
messageIDs = []int64{1}
)
convey.Convey("HashStatsCache", t, func(ctx convey.C) {
res, err := d.HashStatsCache(c, businessID, originID, messageIDs)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddHashStatsCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
stats = &model.Stats{}
)
convey.Convey("AddHashStatsCache", t, func(ctx convey.C) {
err := d.AddHashStatsCache(c, businessID, originID, stats)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoAddHashStatsCacheMap(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
stats map[int64]*model.Stats
)
convey.Convey("AddHashStatsCacheMap", t, func(ctx convey.C) {
err := d.AddHashStatsCacheMap(c, businessID, originID, stats)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,426 @@
package dao
import (
"context"
xsql "database/sql"
"fmt"
"sync"
"time"
"go-common/app/service/main/thumbup/model"
sql "go-common/library/database/tidb"
"go-common/library/log"
xtime "go-common/library/time"
"go-common/library/xstr"
"go-common/library/sync/errgroup"
)
const (
_tidbBulkSize = 50
_tidbBusinessesSQL = "SELECT id, name, message_list_type, user_likes_limit, message_likes_limit, enable_originid, user_list_type FROM business WHERE dtime = '0000-00-00 00:00:00'"
_tidbLikeMidSQL = "SELECT type FROM likes WHERE mid=? AND business_id = ? AND origin_id = ? AND message_id = ?"
_tidbUserLikeListSQL = "SELECT message_id, mtime FROM likes WHERE mid = ? AND business_id =? AND type = ? ORDER BY mtime desc LIMIT ? OFFSET ?"
_tidbUserHasLikeSQL = "SELECT message_id, mtime FROM likes WHERE mid = ? AND business_id =? AND type = ? AND message_id in (%s)"
_tidbItemLikeListSQL = "SELECT mid, mtime FROM likes WHERE business_id =? AND origin_id =? AND message_id =? AND type = ? ORDER BY mtime desc LIMIT ? OFFSET ?"
_tidbStatSQL = "SELECT message_id, likes_count, dislikes_count, origin_id, likes_change, dislikes_change FROM counts WHERE business_id = ? AND origin_id = ? AND message_id = ?"
_tidbStatsSQL = "SELECT message_id, likes_count, dislikes_count, likes_change, dislikes_change FROM counts WHERE business_id = ? AND origin_id = 0 AND message_id in (%s)"
_tidbStatsOriginSQL = "SELECT message_id, likes_count, dislikes_count, likes_change, dislikes_change FROM counts WHERE business_id = ? AND origin_id = ?"
_tidbUserLikeCountSQL = "SELECT COUNT(*) FROM likes WHERE business_id =? AND mid = ? AND type = ?"
_tidbUpdateLikeSQL = "INSERT INTO likes (business_id, origin_id, message_id, mid, type, mtime) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE type=?, mtime = ?"
_tidbUpdateCountsSQL = "INSERT INTO counts (business_id, origin_id, message_id, likes_count, dislikes_count, up_mid) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE "
_tidbupdateCountChange = "UPDATE counts SET likes_change = likes_change + ?, dislikes_change = dislikes_change + ? WHERE business_id = ? AND origin_id = ? AND message_id = ?"
_tidbItemHasLikeSQL = "SELECT mid, mtime FROM likes WHERE business_id = ? AND origin_id = ? AND message_id = ? AND type =? AND mid in (%s)"
)
// Businesses get business list
func (d *Dao) Businesses(c context.Context) (res []*model.Business, err error) {
var rows *sql.Rows
if rows, err = d.businessesStmt.Query(c); err != nil {
PromError("tidb:业务查询")
log.Error("tidb.businessesStmt.Query error(%v)", err)
return
}
defer rows.Close()
for rows.Next() {
b := &model.Business{}
if err = rows.Scan(&b.ID, &b.Name, &b.MessageListType, &b.UserLikesLimit, &b.MessageLikesLimit, &b.EnableOriginID, &b.UserListType); err != nil {
PromError("tidb:业务Scan")
log.Error("tidb.rows.Business.Scan error(%v)", err)
return
}
res = append(res, b)
}
err = rows.Err()
return
}
// LikeState get like state
func (d *Dao) LikeState(c context.Context, mid, businessID, originID, messageID int64) (res int8, err error) {
if err = d.likeStateStmt.QueryRow(c, mid, businessID, originID, messageID).Scan(&res); err != nil {
if err == sql.ErrNoRows {
err = nil
return
}
PromError("tidb:LikeByMid")
log.Error("tidbLikeByMid(%d,%d,%d,%d) error(%v)", mid, businessID, originID, messageID, err)
}
return
}
// UserLikeCount count
func (d *Dao) UserLikeCount(c context.Context, businessID, mid int64, typ int8) (res int, err error) {
if err = d.userLikeCountStmt.QueryRow(c, businessID, mid, typ).Scan(&res); err != nil {
PromError("tidb:UserLikeCount")
log.Error("tidbUserLikeCount(%d,%d,%d) error(%v)", businessID, mid, typ, err)
}
return
}
// RawItemLikeList item like list
func (d *Dao) RawItemLikeList(c context.Context, messageID, businessID, originID int64, state int8, start, end int) (res []*model.UserLikeRecord, err error) {
limit := end - start + 1
var rows *sql.Rows
if rows, err = d.itemLikeListStmt.Query(c, businessID, originID, messageID, state, limit, start); err != nil {
PromError("tidb:项目点赞列表")
log.Error("tidb.ItemLikeList.Query error(%v,%v,%v,%v,%v, %v)", businessID, originID, messageID, state, limit, err)
return
}
defer rows.Close()
for rows.Next() {
b := &model.UserLikeRecord{}
var t time.Time
if err = rows.Scan(&b.Mid, &t); err != nil {
PromError("tidb:业务Scan")
log.Error("tidb.rows.Business.Scan error(%v)", err)
return
}
b.Time = xtime.Time(t.Unix())
res = append(res, b)
}
err = rows.Err()
return
}
// RawUserLikeList .
func (d *Dao) RawUserLikeList(c context.Context, mid, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error) {
limit := end - start
var rows *sql.Rows
if rows, err = d.userLikeListStmt.Query(c, mid, businessID, state, limit, start); err != nil {
PromError("tidb:用户点赞列表")
log.Error("tidb.UserLikeList.Query error(%v,%v,%v,%v,%v)", mid, businessID, state, limit, err)
return
}
defer rows.Close()
for rows.Next() {
b := &model.ItemLikeRecord{}
var t time.Time
if err = rows.Scan(&b.MessageID, &t); err != nil {
PromError("tidb:用户Scan")
log.Error("tidb.rows.Business.Scan error(%v)", err)
return
}
b.Time = xtime.Time(t.Unix())
res = append(res, b)
}
err = rows.Err()
return
}
// MessageStats .
func (d *Dao) MessageStats(c context.Context, businessID int64, ids []int64) (res map[int64]*model.Stats, err error) {
res = make(map[int64]*model.Stats)
var (
group = &errgroup.Group{}
mutex = &sync.Mutex{}
)
keysLen := len(ids)
for i := 0; i < keysLen; i += _tidbBulkSize {
var keys []int64
if (i + _tidbBulkSize) > keysLen {
keys = ids[i:]
} else {
keys = ids[i : i+_tidbBulkSize]
}
group.Go(func() error {
statsSQL := fmt.Sprintf(_tidbStatsSQL, xstr.JoinInts(keys))
rows, e := d.tidb.Query(c, statsSQL, businessID)
if e != nil {
err = e
return nil
}
defer rows.Close()
for rows.Next() {
s := &model.Stats{}
var likeChange, dislikeChange int64
e = rows.Scan(&s.ID, &s.Likes, &s.Dislikes, &likeChange, &dislikeChange)
s.Likes += likeChange
s.Dislikes += dislikeChange
if e != nil {
err = e
continue
}
mutex.Lock()
res[s.ID] = s
mutex.Unlock()
}
return nil
})
}
group.Wait()
if err != nil {
PromError("tidb stats Scan")
log.Error("tidb.rows.Stats.Scan error(%v)", err)
}
if len(res) == 0 {
res = nil
}
return
}
// OriginStats .
func (d *Dao) OriginStats(c context.Context, businessID, originID int64) (res map[int64]*model.Stats, err error) {
var rows *sql.Rows
if rows, err = d.statsOriginStmt.Query(c, businessID, originID); err != nil {
PromError("tidb:计数表")
log.Error("db.Stats.Query error(%v,%v,%v)", businessID, originID, err)
return
}
defer rows.Close()
for rows.Next() {
b := &model.Stats{OriginID: originID}
var likeChange, dislikeChange int64
if err = rows.Scan(&b.ID, &b.Likes, &b.Dislikes, &likeChange, &dislikeChange); err != nil {
PromError("tidb:计数Scan")
log.Error("tidb.rows.Stats.Scan error(%v)", err)
return
}
b.Likes += likeChange
b.Dislikes += dislikeChange
if res == nil {
res = make(map[int64]*model.Stats)
}
res[b.ID] = b
}
err = rows.Err()
return
}
// Stat .
func (d *Dao) Stat(c context.Context, businessID, originID, messageID int64) (res model.Stats, err error) {
var likeChange, dislikeChange int64
if err = d.statStmt.QueryRow(c, businessID, originID, messageID).Scan(&res.ID, &res.Likes, &res.Dislikes, &res.OriginID, &likeChange, &dislikeChange); err != nil {
if err == sql.ErrNoRows {
err = nil
res.ID = messageID
res.OriginID = originID
return
}
PromError("tidb:查询计数表")
log.Error("tidb.Stat.Query error(%v,%v,%v,%v)", businessID, originID, messageID, err)
}
res.Likes += likeChange
res.Dislikes += dislikeChange
return
}
// RawStats stat changes
func (d *Dao) RawStats(c context.Context, businessID, originID, messageID int64) (res model.RawStats, err error) {
if err = d.statStmt.QueryRow(c, businessID, originID, messageID).Scan(&res.ID, &res.Likes, &res.Dislikes, &res.OriginID, &res.LikesChange, &res.DislikesChange); err != nil {
if err == sql.ErrNoRows {
err = nil
res.ID = messageID
res.OriginID = originID
return
}
PromError("tidb:查询change计数表")
log.Error("tidb.StatChanges.Query error(%v,%v,%v,%v)", businessID, originID, messageID, err)
}
return
}
// TxUpdateCounts update like info via transaction.
func (d *Dao) tidbTxUpdateCounts(c context.Context, tx *sql.Tx, businessID, originID, messageID int64, likesCount, dislikesCount, upMid int64) (err error) {
likeSQL := _tidbUpdateCountsSQL
if (likesCount == 0) && (dislikesCount == 0) {
return
}
if (likesCount != 0) && (dislikesCount == 0) {
if likesCount > 0 {
likeSQL += fmt.Sprintf("likes_count = likes_count + %v", likesCount)
} else {
likeSQL += fmt.Sprintf("likes_count = likes_count %v", likesCount)
}
} else if (likesCount == 0) && (dislikesCount != 0) {
if dislikesCount > 0 {
likeSQL += fmt.Sprintf("dislikes_count = dislikes_count + %v", dislikesCount)
} else {
likeSQL += fmt.Sprintf("dislikes_count = dislikes_count %v", dislikesCount)
}
} else {
if likesCount > 0 {
likeSQL += fmt.Sprintf("likes_count = likes_count + %v", likesCount)
} else {
likeSQL += fmt.Sprintf("likes_count = likes_count %v", likesCount)
}
if dislikesCount > 0 {
likeSQL += fmt.Sprintf(", dislikes_count = dislikes_count + %v", dislikesCount)
} else {
likeSQL += fmt.Sprintf(", dislikes_count = dislikes_count %v", dislikesCount)
}
}
if upMid > 0 {
likeSQL += fmt.Sprintf(", up_mid = %d", upMid)
}
if _, err = tx.Exec(likeSQL, businessID, originID, messageID, likesCount, dislikesCount, upMid); err != nil {
PromError("tidb:更新计数表")
log.Error("dao.tidbTxUpdateCounts tx.Exec(%s, %v,%v,%v) error(%v)", likeSQL, businessID, originID, messageID, err)
}
return
}
// tidbTxStat .
func (d *Dao) tidbTxStat(c context.Context, tx *sql.Tx, businessID, originID, messageID int64) (res model.Stats, err error) {
var likeChange, dislikeChange int64
if err = tx.Stmts(d.statStmt).QueryRow(c, businessID, originID, messageID).Scan(&res.ID, &res.Likes, &res.Dislikes, &res.OriginID, &likeChange, &dislikeChange); err != nil {
if err == sql.ErrNoRows {
err = nil
res.ID = messageID
res.OriginID = originID
return
}
PromError("tidb:查询计数表")
log.Error("tidb: Tx db.Stat.Query error(%v,%v,%v,%v)", businessID, originID, messageID, err)
}
res.Likes += likeChange
res.Dislikes += dislikeChange
return
}
// UpdateCount .
func (d *Dao) UpdateCount(c context.Context, businessID, originID, messageID int64, likeChange, dislikeChange int64) (err error) {
if _, err = d.updateCountChangeStmt.Exec(c, likeChange, dislikeChange, businessID, originID, messageID); err != nil {
PromError("tidb:更新change计数表")
log.Error("db.tidbUpdateCountChange.Exec error(%v,%v,%v,%v)", businessID, originID, messageID, err)
}
return
}
// tidbBeginTran begin transaction.
func (d *Dao) tidbBeginTran(c context.Context) (*sql.Tx, error) {
return d.tidb.Begin(c)
}
// UpdateCounts .
func (d *Dao) UpdateCounts(c context.Context, businessID, originID, messageID int64, likeCount, dislikeCount int64, upMid int64) (err error) {
var tx *sql.Tx
if tx, err = d.tidbBeginTran(c); err != nil {
log.Error("tx.BeginTran() error(%v)", err)
return
}
var stat model.Stats
if stat, err = d.tidbTxStat(c, tx, businessID, originID, messageID); err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("tx.Rollback() error(%v)", err1)
}
return
}
if stat.Likes+likeCount < 0 {
likeCount = -stat.Likes
}
if stat.Dislikes+dislikeCount < 0 {
dislikeCount = -stat.Dislikes
}
if err = d.tidbTxUpdateCounts(c, tx, businessID, originID, messageID, likeCount, dislikeCount, upMid); err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("tx.Rollback() error(%v)", err1)
}
return
}
if err = tx.Commit(); err != nil {
PromError("like:更新喜欢计数")
log.Error("tx.Commit() error(%v)", err)
return
}
return
}
// UpdateUpMids .
func (d *Dao) UpdateUpMids(c context.Context, businessID int64, data []*model.UpMidsReq) (rows int64, err error) {
var tx *sql.Tx
if tx, err = d.tidbBeginTran(c); err != nil {
log.Error("UpdateUpMids tx.BeginTran() error(%v)", err)
return
}
var res xsql.Result
for _, u := range data {
if res, err = tx.Exec("UPDATE counts SET up_mid = ? WHERE business_id = ? AND origin_id = ? AND message_id = ?", u.UpMid, businessID, u.OriginID, u.MessageID); err != nil {
if err1 := tx.Rollback(); err1 != nil {
log.Error("UpdateUpMids tx.Rollback() error(%v)", err1)
}
return
}
aff, _ := res.RowsAffected()
rows += aff
}
if err = tx.Commit(); err != nil {
PromError("like:更新UpMids")
log.Error("UpdateUpMids tx.Commit() error(%v)", err)
}
return
}
// ItemHasLike .
func (d *Dao) ItemHasLike(c context.Context, businessID int64, originID, messageID int64, mids []int64, typ int8) (res map[int64]int64, err error) {
var rows *sql.Rows
sqlStr := fmt.Sprintf(_tidbItemHasLikeSQL, xstr.JoinInts(mids))
if rows, err = d.tidb.Query(c, sqlStr, businessID, originID, messageID, typ); err != nil {
PromError("tidb:itemHasLike")
log.Error("tidb.ItemHasLike.Query asc error(%v,%v,%v,%v,%v)", businessID, originID, messageID, typ, err)
return
}
defer rows.Close()
for rows.Next() {
var t time.Time
var mid int64
if res == nil {
res = make(map[int64]int64)
}
if err = rows.Scan(&mid, &t); err != nil {
PromError("tidb:itemHasLikeScan")
log.Error("tidb.rows.itemHasLike.Scan error(%v)", err)
return
}
res[mid] = t.Unix()
}
err = rows.Err()
return
}
// UserHasLike .
func (d *Dao) UserHasLike(c context.Context, businessID, mid int64, messageIDs []int64, typ int8) (res []*model.ItemLikeRecord, err error) {
var rows *sql.Rows
sqlStr := fmt.Sprintf(_tidbUserHasLikeSQL, xstr.JoinInts(messageIDs))
if rows, err = d.tidb.Query(c, sqlStr, mid, businessID, typ); err != nil {
PromError("tidb:userHasLike")
log.Error("tidb.UserHasLike.Query asc error(%v,%v,%v,%v,%v)", mid, businessID, messageIDs, typ, err)
return
}
defer rows.Close()
for rows.Next() {
var t xtime.Time
var messageID int64
if err = rows.Scan(&messageID, &t); err != nil {
PromError("tidb:userHasLikeScan")
log.Error("tidb.rows.userHasLike.Scan error(%v)", err)
return
}
tmp := &model.ItemLikeRecord{
MessageID: messageID,
Time: t,
}
res = append(res, tmp)
}
err = rows.Err()
return
}

View File

@@ -0,0 +1,254 @@
package dao
import (
"context"
"fmt"
"testing"
"go-common/app/service/main/thumbup/model"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoBusinesses(t *testing.T) {
var (
c = context.TODO()
)
convey.Convey("Businesses", t, func(ctx convey.C) {
res, err := d.Businesses(c)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoLikeState(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
originID = int64(1)
messageID = int64(1)
)
convey.Convey("LikeState", t, func(ctx convey.C) {
res, err := d.LikeState(c, mid, businessID, originID, messageID)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoUserLikeCount(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
mid = int64(1)
typ = int8(1)
)
convey.Convey("UserLikeCount", t, func(ctx convey.C) {
res, err := d.UserLikeCount(c, businessID, mid, typ)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoRawItemLikeList(t *testing.T) {
var (
c = context.TODO()
messageID = int64(1)
businessID = int64(1)
originID = int64(1)
state = int8(1)
start = int(1)
end = int(1)
)
convey.Convey("RawItemLikeList", t, func(ctx convey.C) {
_, err := d.RawItemLikeList(c, messageID, businessID, originID, state, start, end)
ctx.Convey("Then err should be nil.res,all should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoRawUserLikeList(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
state = int8(1)
start = int(1)
end = int(1)
)
convey.Convey("RawUserLikeList", t, func(ctx convey.C) {
_, err := d.RawUserLikeList(c, mid, businessID, state, start, end)
ctx.Convey("Then err should be nil.res,all should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// ctx.So(all, convey.ShouldNotBeNil)
// ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoMessageStats(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
ids = []int64{1}
)
convey.Convey("MessageStats", t, func(ctx convey.C) {
res, err := d.MessageStats(c, businessID, ids)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldBeEmpty)
})
})
}
func TestDaoOriginStats(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
)
convey.Convey("OriginStats", t, func(ctx convey.C) {
res, err := d.OriginStats(c, businessID, originID)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoStat(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
messageID = int64(1)
)
convey.Convey("Stat", t, func(ctx convey.C) {
res, err := d.Stat(c, businessID, originID, messageID)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaotidbRawStats(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
messageID = int64(1)
)
convey.Convey("RawStats", t, func(ctx convey.C) {
res, err := d.RawStats(c, businessID, originID, messageID)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoUpdateCount(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
messageID = int64(1)
likeChange = int64(1)
dislikeChange = int64(1)
)
convey.Convey("UpdateCount", t, func(ctx convey.C) {
err := d.UpdateCount(c, businessID, originID, messageID, likeChange, dislikeChange)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
// INSERT INTO `bilibili_likes`.`counts`(`id`, `mtime`, `ctime`, `business_id`, `origin_id`, `message_id`, `likes_count`, `dislikes_count`, `likes_change`, `dislikes_change`, `up_mid`) VALUES (0, '2018-11-14 17:25:40', '2018-11-03 12:06:27', 3, 0, 10099865, 100, 0, 0, 0, 8167601);
func Test_updateCounts(t *testing.T) {
convey.Convey("get data", t, func() {
c := context.Background()
messageID := int64(10099865)
bid := int64(3)
oid := int64(0)
stat, err := d.Stat(c, bid, oid, messageID)
convey.So(err, convey.ShouldBeNil)
args := [][2]int64{
{1, 1},
{-1000, -1000},
{1, 0},
{0, 1},
{-1, 0},
{-1, -1},
{0, -1},
{0, 0},
{100, -10000},
{100, 20},
{-10, 20},
}
var l, dd int64
for _, x := range args {
l = x[0]
dd = x[1]
convey.Convey(fmt.Sprintf("like %v dislike %v", l, dd), func() {
err := d.UpdateCounts(c, bid, oid, messageID, l, dd, 0)
convey.So(err, convey.ShouldBeNil)
nstat, err := d.Stat(c, bid, oid, messageID)
convey.So(err, convey.ShouldBeNil)
likes := stat.Likes + l
if likes < 0 {
likes = 0
}
dislikes := stat.Dislikes + dd
if dislikes < 0 {
dislikes = 0
}
convey.So(nstat.Likes, convey.ShouldEqual, likes)
convey.So(nstat.Dislikes, convey.ShouldEqual, dislikes)
})
}
})
}
func TestDaoUpdateUpMids(t *testing.T) {
var (
c = context.TODO()
businessID = int64(2)
)
data := []*model.UpMidsReq{
{MessageID: 100, UpMid: 100},
{MessageID: 200, UpMid: 200},
{MessageID: 300, UpMid: 300},
}
convey.Convey("UpdateUpMids", t, func(ctx convey.C) {
_, err := d.UpdateUpMids(c, businessID, data)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoItemHasLike(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
originID = int64(1)
messageID = int64(1)
mids = []int64{1, 2, 3}
)
convey.Convey("ItemHasLike", t, func(ctx convey.C) {
res, err := d.ItemHasLike(c, businessID, originID, messageID, mids, model.StateLike)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeEmpty)
})
})
}

View File

@@ -0,0 +1,173 @@
package dao
import (
"context"
"fmt"
pb "go-common/app/service/main/thumbup/api"
"go-common/app/service/main/thumbup/model"
"go-common/library/cache/redis"
"go-common/library/log"
"go-common/library/stat/prom"
xtime "go-common/library/time"
pkgerr "github.com/pkg/errors"
)
func userLikesKey(businessID, mid int64, state int8) string {
return fmt.Sprintf("u_m_%d_b_%d_%d", mid, businessID, state)
}
// CacheUserLikeList .
func (d *Dao) CacheUserLikeList(c context.Context, mid, businessID int64, state int8, start, end int) (res []*model.ItemLikeRecord, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, state)
items, err := redis.Values(conn.Do("ZREVRANGE", key, start, end, "withscores"))
if err != nil {
if err == redis.ErrNil {
err = nil
return
}
err = pkgerr.Wrap(err, "get user like cache")
PromError("redis:CacheUserLikeList")
log.Errorv(c, log.KV("CacheUserLikeList", fmt.Sprintf("%+v", err)))
return
}
for len(items) > 0 {
var id, t int64
if items, err = redis.Scan(items, &id, &t); err != nil {
err = pkgerr.Wrap(err, "get user like cache")
PromError("redis:CacheUserLikeList")
log.Errorv(c, log.KV("CacheUserLikeList", fmt.Sprintf("%+v", err)))
return
}
res = append(res, &model.ItemLikeRecord{MessageID: id, Time: xtime.Time(t)})
}
return
}
// ExpireUserLikesCache .
func (d *Dao) ExpireUserLikesCache(c context.Context, mid, businessID int64, state int8) (ok bool, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, state)
if ok, err = redis.Bool(conn.Do("EXPIRE", key, d.redisUserLikesExpire)); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:expire用户点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(EXPIRE, %s) error(%v)", key, err)))
}
return
}
// UserLikeExists .
func (d *Dao) UserLikeExists(c context.Context, mid, businessID int64, messageIDs []int64, state int8) (res map[int64]*pb.UserLikeState, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, state)
res = make(map[int64]*pb.UserLikeState)
prom.CacheHit.Incr("userLikeList")
for _, name := range messageIDs {
conn.Send("ZSCORE", key, name)
}
if err = conn.Flush(); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:UserLikeExists")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Flush() error(%v)", err)))
return
}
for _, name := range messageIDs {
var ts int64
if ts, err = redis.Int64(conn.Receive()); err == nil {
res[name] = &pb.UserLikeState{
Mid: mid,
Time: xtime.Time(ts),
State: pb.State(state),
}
} else if err == redis.ErrNil {
err = nil
} else {
err = pkgerr.Wrap(err, "")
PromError("redis:UserLikeExists")
log.Errorv(c, log.KV("log", fmt.Sprintf("UserLikeExists conn.Receive() error(%v)", err)))
return
}
}
return
}
// AppendCacheUserLikeList .
func (d *Dao) AppendCacheUserLikeList(c context.Context, mid int64, item *model.ItemLikeRecord, businessID int64, state int8) (err error) {
if item == nil {
return
}
limit := d.BusinessIDMap[businessID].UserLikesLimit
var count int
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, state)
id := item.MessageID
score := int64(item.Time)
if err = conn.Send("ZADD", key, "CH", score, id); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:用户点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZADD, %s, %d, %v) error(%v)", key, score, id, err)))
return
}
count++
if err = conn.Send("ZREMRANGEBYRANK", key, 0, -(limit + 1)); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:用户点赞列表rm")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZREMRANGEBYRANK, %s, 0, %d) error(%v)", key, -(limit+1), err)))
return
}
count++
if err = conn.Send("EXPIRE", key, d.redisUserLikesExpire); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:用户点赞列表过期")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(EXPIRE, %s, %d) error(%v)", key, d.redisUserLikesExpire, err)))
return
}
count++
if err = conn.Flush(); err != nil {
PromError("redis:用户点赞列表flush")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Flush error(%v)", err)))
return
}
for i := 0; i < count; i++ {
if _, err = conn.Receive(); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:用户点赞列表receive")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Receive error(%v)", err)))
return
}
}
return
}
// DelUserLikeCache .
func (d *Dao) DelUserLikeCache(c context.Context, mid, businessID int64, messageID int64, state int8) (err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, state)
if _, err = conn.Do("ZREM", key, messageID); err != nil {
err = pkgerr.Wrap(err, "")
PromError("redis:zrem用户点赞列表")
log.Errorv(c, log.KV("log", fmt.Sprintf("conn.Send(ZREM, %s, %v) error(%v)", key, messageID, err)))
}
return
}
// UserLikesCountCache .
func (d *Dao) UserLikesCountCache(c context.Context, businessID, mid int64) (res int, err error) {
conn := d.redis.Get(c)
defer conn.Close()
key := userLikesKey(businessID, mid, model.StateLike)
res, err = redis.Int(conn.Do("ZCOUNT", key, "(0", "+inf"))
if err != nil {
err = pkgerr.Wrap(err, "")
log.Errorv(c, log.KV("log", fmt.Sprintf("dao.UserLikesCountCache(%d, %d) err:%v", businessID, mid, err)))
PromError("redis:用户点赞总数")
}
return
}

View File

@@ -0,0 +1,137 @@
package dao
import (
"context"
"go-common/app/service/main/thumbup/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaouserLikesKey(t *testing.T) {
var (
businessID = int64(1)
mid = int64(1)
state = int8(1)
)
convey.Convey("userLikesKey", t, func(ctx convey.C) {
p1 := userLikesKey(businessID, mid, state)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
}
func TestDaoCacheUserLikeList(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
state = int8(1)
start = int(1)
end = int(10)
)
convey.Convey("CacheUserLikeList", t, func(ctx convey.C) {
_, err := d.CacheUserLikeList(c, mid, businessID, state, start, end)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
// ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddCacheUserLikeList(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
miss = []*model.ItemLikeRecord{}
businessID = int64(1)
state = int8(1)
)
convey.Convey("AddCacheUserLikeList", t, func(ctx convey.C) {
err := d.AddCacheUserLikeList(c, mid, miss, businessID, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoExpireUserLikesCache(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
state = int8(1)
)
convey.Convey("ExpireUserLikesCache", t, func(ctx convey.C) {
ok, err := d.ExpireUserLikesCache(c, mid, businessID, state)
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 TestDaoUserLikeExists(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
messageIDs = []int64{}
state = int8(1)
)
convey.Convey("UserLikeExists", t, func(ctx convey.C) {
res, err := d.UserLikeExists(c, mid, businessID, messageIDs, state)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func TestDaoAppendCacheUserLikeList(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
item = &model.ItemLikeRecord{}
businessID = int64(1)
state = int8(1)
)
convey.Convey("AppendCacheUserLikeList", t, func(ctx convey.C) {
err := d.AppendCacheUserLikeList(c, mid, item, businessID, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelUserLikeCache(t *testing.T) {
var (
c = context.TODO()
mid = int64(1)
businessID = int64(1)
messageID = int64(1)
state = int8(1)
)
convey.Convey("DelUserLikeCache", t, func(ctx convey.C) {
err := d.DelUserLikeCache(c, mid, businessID, messageID, state)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoUserLikesCountCache(t *testing.T) {
var (
c = context.TODO()
businessID = int64(1)
mid = int64(1)
)
convey.Convey("UserLikesCountCache", t, func(ctx convey.C) {
res, err := d.UserLikesCountCache(c, businessID, mid)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.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 = [
"rpc.go",
"thumbup.go",
],
importpath = "go-common/app/service/main/thumbup/model",
tags = ["automanaged"],
deps = ["//library/time: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,74 @@
package model
// ArgLike .
type ArgLike struct {
Mid int64
UpMid int64
Business string
OriginID int64
MessageID int64
Type int8
RealIP string
}
// ArgHasLike .
type ArgHasLike struct {
Business string
MessageIDs []int64
Mid int64
RealIP string
}
// ArgStats .
type ArgStats struct {
Business string
OriginID int64
MessageIDs []int64
RealIP string
}
// ArgStatsWithLike .
type ArgStatsWithLike struct {
Mid int64
Business string
OriginID int64
MessageIDs []int64
RealIP string
}
// ArgUserLikes .
type ArgUserLikes struct {
Business string
Mid int64
Pn, Ps int
RealIP string
}
// ArgItemLikes .
type ArgItemLikes struct {
Business string
OriginID int64
MessageID int64
Mid int64
Pn, Ps int
RealIP string
}
// ArgUpdateCount .
type ArgUpdateCount struct {
Business string
OriginID int64
MessageID int64
LikeChange int64
DislikeChange int64
Operator string
RealIP string
}
// ArgRawStats .
type ArgRawStats struct {
Business string
OriginID int64
MessageID int64
RealIP string
}

View File

@@ -0,0 +1,170 @@
package model
import (
xtime "go-common/library/time"
"time"
)
// type and states
const (
TypeLike = 1
TypeCancelLike = 2
TypeDislike = 3
TypeCancelDislike = 4
// only used by internal
TypeLikeReverse = 5
TypeDislikeReverse = 6
StateBlank = 0
StateLike = 1
StateDislike = 2
ItemListLike = 1
ItemListDislike = 2
ItemListAll = 3
UserListLike = 1
UserListDislike = 2
UserListAll = 3
)
// Business .
type Business struct {
ID int64 `json:"id"`
Name string `json:"name"`
MessageListType uint8 `json:"message_list_type"`
UserListType uint8 `json:"user_list_type"`
UserLikesLimit int `json:"user_likes_limit"`
MessageLikesLimit int `json:"message_likes_limit"`
EnableOriginID int `json:"enable_origin_id"`
}
// EnableItemLikeList .
func (b *Business) EnableItemLikeList() bool {
return (b.MessageListType == ItemListLike) || (b.MessageListType == ItemListAll)
}
// EnableItemDislikeList .
func (b *Business) EnableItemDislikeList() bool {
return (b.MessageListType == ItemListDislike) || (b.MessageListType == ItemListAll)
}
// EnableUserLikeList .
func (b *Business) EnableUserLikeList() bool {
return (b.UserListType == UserListLike) || (b.UserListType == UserListAll)
}
// EnableUserDislikeList .
func (b *Business) EnableUserDislikeList() bool {
return (b.UserListType == UserListDislike) || (b.UserListType == UserListAll)
}
// Stats .
type Stats struct {
OriginID int64 `json:"origin_id"`
ID int64 `json:"id"`
Likes int64 `json:"likes"`
Dislikes int64 `json:"dislikes"`
}
// RawStats .
type RawStats struct {
Stats
LikesChange int64 `json:"likes_change"`
DislikesChange int64 `json:"dislikes_change"`
}
// StatsWithLike .
type StatsWithLike struct {
Stats
LikeState int8 `json:"like_state"`
}
// UserLikeRecord .
type UserLikeRecord struct {
Mid int64 `json:"mid"`
Time xtime.Time `json:"time"`
}
// ItemLikeRecord .
type ItemLikeRecord struct {
MessageID int64 `json:"message_id"`
Time xtime.Time `json:"time"`
}
// UserTotalLike .
type UserTotalLike struct {
Total int `json:"total"`
List []*ItemLikeRecord `json:"list"`
}
// MultiBusinessItem .
type MultiBusinessItem struct {
OriginID int64 `json:"origin_id"`
MessageID int64 `json:"message_id"`
}
// MultiBusiness .
type MultiBusiness struct {
Mid int64 `json:"mid"`
Businesses map[string][]*MultiBusinessItem `json:"businesses" validate:"required"`
}
// StatMsg databus stat message
type StatMsg struct {
Type string `json:"type"`
ID int64 `json:"id"`
Count int64 `json:"count"`
Timestamp int64 `json:"timestamp"`
OriginID int64 `json:"origin_id,omitempty"`
DislikeCount int64 `json:"dislike_count,omitempty"`
Mid int64 `json:"mid,omitempty"`
UpMid int64 `json:"up_mid,omitempty"`
}
// LikeMsg like message
type LikeMsg struct {
Type int8
Mid int64
UpMid int64
LikeTime time.Time
Business string
OriginID int64
MessageID int64
}
// ItemMsg .
type ItemMsg struct {
State int8 `json:"state"`
Business string `json:"business"`
OriginID int64 `json:"origin_id"`
MessageID int64 `json:"message_id"`
}
// UserMsg .
type UserMsg struct {
Mid int64 `json:"mid"`
State int8 `json:"state"`
Business string `json:"business"`
}
// LikeItem like item
type LikeItem struct {
BusinessID int64
OriginID int64
MessageID int64
}
// LikeCounts like counts
type LikeCounts struct {
Like int64
Dislike int64
UpMid int64 `json:"-"`
}
// UpMidsReq .
type UpMidsReq struct {
OriginID int64 `json:"origin_id" validate:"min=0"`
MessageID int64 `json:"message_id" validate:"min=1,required"`
UpMid int64 `json:"up_mid" validate:"required"`
}

View File

@@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"mock.go",
"thumbup.go",
],
importpath = "go-common/app/service/main/thumbup/rpc/client",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/model:go_default_library",
"//library/net/rpc:go_default_library",
"//vendor/github.com/golang/mock/gomock: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,152 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: thumbup.go
// Package client is a generated GoMock package.
package client
import (
context "context"
model "go-common/app/service/main/thumbup/model"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockThumbupRPC is a mock of ThumbupRPC interface
type MockThumbupRPC struct {
ctrl *gomock.Controller
recorder *MockThumbupRPCMockRecorder
}
// MockThumbupRPCMockRecorder is the mock recorder for MockThumbupRPC
type MockThumbupRPCMockRecorder struct {
mock *MockThumbupRPC
}
// NewMockThumbupRPC creates a new mock instance
func NewMockThumbupRPC(ctrl *gomock.Controller) *MockThumbupRPC {
mock := &MockThumbupRPC{ctrl: ctrl}
mock.recorder = &MockThumbupRPCMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockThumbupRPC) EXPECT() *MockThumbupRPCMockRecorder {
return m.recorder
}
// Like mocks base method
func (m *MockThumbupRPC) Like(c context.Context, arg *model.ArgLike) error {
ret := m.ctrl.Call(m, "Like", c, arg)
ret0, _ := ret[0].(error)
return ret0
}
// Like indicates an expected call of Like
func (mr *MockThumbupRPCMockRecorder) Like(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Like", reflect.TypeOf((*MockThumbupRPC)(nil).Like), c, arg)
}
// HasLike mocks base method
func (m *MockThumbupRPC) HasLike(c context.Context, arg *model.ArgHasLike) (map[int64]int8, error) {
ret := m.ctrl.Call(m, "HasLike", c, arg)
ret0, _ := ret[0].(map[int64]int8)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HasLike indicates an expected call of HasLike
func (mr *MockThumbupRPCMockRecorder) HasLike(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasLike", reflect.TypeOf((*MockThumbupRPC)(nil).HasLike), c, arg)
}
// Stats mocks base method
func (m *MockThumbupRPC) Stats(c context.Context, arg *model.ArgStats) (map[int64]*model.Stats, error) {
ret := m.ctrl.Call(m, "Stats", c, arg)
ret0, _ := ret[0].(map[int64]*model.Stats)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stats indicates an expected call of Stats
func (mr *MockThumbupRPCMockRecorder) Stats(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockThumbupRPC)(nil).Stats), c, arg)
}
// UserLikes mocks base method
func (m *MockThumbupRPC) UserLikes(c context.Context, arg *model.ArgUserLikes) ([]*model.ItemLikeRecord, error) {
ret := m.ctrl.Call(m, "UserLikes", c, arg)
ret0, _ := ret[0].([]*model.ItemLikeRecord)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UserLikes indicates an expected call of UserLikes
func (mr *MockThumbupRPCMockRecorder) UserLikes(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserLikes", reflect.TypeOf((*MockThumbupRPC)(nil).UserLikes), c, arg)
}
// UserDislikes mocks base method
func (m *MockThumbupRPC) UserDislikes(c context.Context, arg *model.ArgUserLikes) ([]*model.ItemLikeRecord, error) {
ret := m.ctrl.Call(m, "UserDislikes", c, arg)
ret0, _ := ret[0].([]*model.ItemLikeRecord)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UserDislikes indicates an expected call of UserDislikes
func (mr *MockThumbupRPCMockRecorder) UserDislikes(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDislikes", reflect.TypeOf((*MockThumbupRPC)(nil).UserDislikes), c, arg)
}
// ItemLikes mocks base method
func (m *MockThumbupRPC) ItemLikes(c context.Context, arg *model.ArgItemLikes) ([]*model.UserLikeRecord, error) {
ret := m.ctrl.Call(m, "ItemLikes", c, arg)
ret0, _ := ret[0].([]*model.UserLikeRecord)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ItemLikes indicates an expected call of ItemLikes
func (mr *MockThumbupRPCMockRecorder) ItemLikes(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ItemLikes", reflect.TypeOf((*MockThumbupRPC)(nil).ItemLikes), c, arg)
}
// ItemDislikes mocks base method
func (m *MockThumbupRPC) ItemDislikes(c context.Context, arg *model.ArgItemLikes) ([]*model.UserLikeRecord, error) {
ret := m.ctrl.Call(m, "ItemDislikes", c, arg)
ret0, _ := ret[0].([]*model.UserLikeRecord)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ItemDislikes indicates an expected call of ItemDislikes
func (mr *MockThumbupRPCMockRecorder) ItemDislikes(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ItemDislikes", reflect.TypeOf((*MockThumbupRPC)(nil).ItemDislikes), c, arg)
}
// StatsWithLike mocks base method
func (m *MockThumbupRPC) StatsWithLike(c context.Context, arg *model.ArgStatsWithLike) (map[int64]*model.StatsWithLike, error) {
ret := m.ctrl.Call(m, "StatsWithLike", c, arg)
ret0, _ := ret[0].(map[int64]*model.StatsWithLike)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StatsWithLike indicates an expected call of StatsWithLike
func (mr *MockThumbupRPCMockRecorder) StatsWithLike(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatsWithLike", reflect.TypeOf((*MockThumbupRPC)(nil).StatsWithLike), c, arg)
}
// UserTotalLike mocks base method
func (m *MockThumbupRPC) UserTotalLike(c context.Context, arg *model.ArgUserLikes) (*model.UserTotalLike, error) {
ret := m.ctrl.Call(m, "UserTotalLike", c, arg)
ret0, _ := ret[0].(*model.UserTotalLike)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UserTotalLike indicates an expected call of UserTotalLike
func (mr *MockThumbupRPCMockRecorder) UserTotalLike(c, arg interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserTotalLike", reflect.TypeOf((*MockThumbupRPC)(nil).UserTotalLike), c, arg)
}

View File

@@ -0,0 +1,131 @@
package client
import (
"context"
"go-common/app/service/main/thumbup/model"
"go-common/library/net/rpc"
)
const (
_like = "RPC.Like"
_likeWithStats = "RPC.LikeWithStats"
_hasLike = "RPC.HasLike"
_stats = "RPC.Stats"
_userLikes = "RPC.UserLikes"
_userDislikes = "RPC.UserDislikes"
_itemLikes = "RPC.ItemLikes"
_itemDislikes = "RPC.ItemDislikes"
_statsWithLike = "RPC.StatsWithLike"
_userTotalLike = "RPC.UserTotalLike"
_updateCount = "RPC.UpdateCount"
_rawStats = "RPC.RawStats"
)
const (
_appid = "community.service.thumbup"
)
var (
// _noArg = &struct{}{}
_noReply = &struct{}{}
)
//go:generate mockgen -source thumbup.go -destination mock.go -package client
// ThumbupRPC rpc interface
type ThumbupRPC interface {
Like(c context.Context, arg *model.ArgLike) (err error)
HasLike(c context.Context, arg *model.ArgHasLike) (res map[int64]int8, err error)
Stats(c context.Context, arg *model.ArgStats) (res map[int64]*model.Stats, err error)
UserLikes(c context.Context, arg *model.ArgUserLikes) (res []*model.ItemLikeRecord, err error)
UserDislikes(c context.Context, arg *model.ArgUserLikes) (res []*model.ItemLikeRecord, err error)
ItemLikes(c context.Context, arg *model.ArgItemLikes) (res []*model.UserLikeRecord, err error)
ItemDislikes(c context.Context, arg *model.ArgItemLikes) (res []*model.UserLikeRecord, err error)
StatsWithLike(c context.Context, arg *model.ArgStatsWithLike) (res map[int64]*model.StatsWithLike, err error)
UserTotalLike(c context.Context, arg *model.ArgUserLikes) (res *model.UserTotalLike, err error)
}
// Service struct info.
type Service struct {
client *rpc.Client2
}
// New new service instance and return.
func New(c *rpc.ClientConfig) (s *Service) {
s = &Service{}
s.client = rpc.NewDiscoveryCli(_appid, c)
return
}
// Like add like/dislike
func (s *Service) Like(c context.Context, arg *model.ArgLike) (err error) {
err = s.client.Call(c, _like, arg, _noReply)
return
}
// LikeWithStats add like/dislike and return the stats info
func (s *Service) LikeWithStats(ctx context.Context, arg *model.ArgLike) (res *model.Stats, err error) {
err = s.client.Call(ctx, _likeWithStats, arg, &res)
return
}
// HasLike query user has liked something
func (s *Service) HasLike(c context.Context, arg *model.ArgHasLike) (res map[int64]int8, err error) {
err = s.client.Call(c, _hasLike, arg, &res)
return
}
// Stats return stats message
func (s *Service) Stats(c context.Context, arg *model.ArgStats) (res map[int64]*model.Stats, err error) {
err = s.client.Call(c, _stats, arg, &res)
return
}
// UserLikes user likes list
func (s *Service) UserLikes(c context.Context, arg *model.ArgUserLikes) (res []*model.ItemLikeRecord, err error) {
err = s.client.Call(c, _userLikes, arg, &res)
return
}
// UserDislikes user dislikes list
func (s *Service) UserDislikes(c context.Context, arg *model.ArgUserLikes) (res []*model.ItemLikeRecord, err error) {
err = s.client.Call(c, _userDislikes, arg, &res)
return
}
// ItemLikes item likes list
func (s *Service) ItemLikes(c context.Context, arg *model.ArgItemLikes) (res []*model.UserLikeRecord, err error) {
err = s.client.Call(c, _itemLikes, arg, &res)
return
}
// ItemDislikes item dislikes list
func (s *Service) ItemDislikes(c context.Context, arg *model.ArgItemLikes) (res []*model.UserLikeRecord, err error) {
err = s.client.Call(c, _itemDislikes, arg, &res)
return
}
// StatsWithLike return stats and like state
func (s *Service) StatsWithLike(c context.Context, arg *model.ArgStatsWithLike) (res map[int64]*model.StatsWithLike, err error) {
err = s.client.Call(c, _statsWithLike, arg, &res)
return
}
// UserTotalLike user item list with total count
func (s *Service) UserTotalLike(c context.Context, arg *model.ArgUserLikes) (res *model.UserTotalLike, err error) {
err = s.client.Call(c, _userTotalLike, arg, &res)
return
}
// UpdateCount update count
func (s *Service) UpdateCount(c context.Context, arg *model.ArgUpdateCount) (err error) {
err = s.client.Call(c, _updateCount, arg, _noReply)
return
}
// RawStats get stat changes
func (s *Service) RawStats(c context.Context, arg *model.ArgRawStats) (res model.RawStats, err error) {
err = s.client.Call(c, _rawStats, arg, &res)
return
}

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 = ["rpc.go"],
importpath = "go-common/app/service/main/thumbup/server/gorpc",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/service:go_default_library",
"//library/net/rpc:go_default_library",
"//library/net/rpc/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"],
)

View File

@@ -0,0 +1,106 @@
package server
import (
"go-common/app/service/main/thumbup/conf"
"go-common/app/service/main/thumbup/model"
"go-common/app/service/main/thumbup/service"
"go-common/library/net/rpc"
"go-common/library/net/rpc/context"
)
// RPC .
type RPC struct {
s *service.Service
}
// New creates rpc server.
func New(c *conf.Config, s *service.Service) (svr *rpc.Server) {
r := &RPC{s: s}
svr = rpc.NewServer(c.RPCServer)
if err := svr.Register(r); err != nil {
panic(err)
}
return
}
// Ping checks connection success.
func (r *RPC) Ping(c context.Context, arg *struct{}, res *struct{}) (err error) {
return
}
// Auth check connection success.
func (r *RPC) Auth(c context.Context, arg *rpc.Auth, res *struct{}) (err error) {
return
}
// Like add like/dislike
func (r *RPC) Like(c context.Context, arg *model.ArgLike, res *struct{}) (err error) {
_, err = r.s.Like(c, arg.Business, arg.Mid, arg.OriginID, arg.MessageID, arg.Type, arg.UpMid)
return
}
// LikeWithStats add like/dislike and return the stats info
func (r *RPC) LikeWithStats(ctx context.Context, arg *model.ArgLike, res *model.Stats) (err error) {
*res, err = r.s.Like(ctx, arg.Business, arg.Mid, arg.OriginID, arg.MessageID, arg.Type, arg.UpMid)
return
}
// Stats return stats message
func (r *RPC) Stats(c context.Context, arg *model.ArgStats, res *map[int64]*model.Stats) (err error) {
*res, err = r.s.Stats(c, arg.Business, arg.OriginID, arg.MessageIDs)
return
}
// UserLikes user likes list
func (r *RPC) UserLikes(c context.Context, arg *model.ArgUserLikes, res *[]*model.ItemLikeRecord) (err error) {
*res, err = r.s.UserLikes(c, arg.Business, arg.Mid, arg.Pn, arg.Ps)
return
}
// UserDislikes user dislikes list
func (r *RPC) UserDislikes(c context.Context, arg *model.ArgUserLikes, res *[]*model.ItemLikeRecord) (err error) {
*res, err = r.s.UserDislikes(c, arg.Business, arg.Mid, arg.Pn, arg.Ps)
return
}
// ItemLikes item likes list
func (r *RPC) ItemLikes(c context.Context, arg *model.ArgItemLikes, res *[]*model.UserLikeRecord) (err error) {
*res, err = r.s.ItemLikes(c, arg.Business, arg.OriginID, arg.MessageID, arg.Pn, arg.Ps, arg.Mid)
return
}
// ItemDislikes item dislikes list
func (r *RPC) ItemDislikes(c context.Context, arg *model.ArgItemLikes, res *[]*model.UserLikeRecord) (err error) {
*res, err = r.s.ItemDislikes(c, arg.Business, arg.OriginID, arg.MessageID, arg.Pn, arg.Ps, arg.Mid)
return
}
// HasLike query user has liked something
func (r *RPC) HasLike(c context.Context, arg *model.ArgHasLike, res *map[int64]int8) (err error) {
*res, _, err = r.s.HasLike(c, arg.Business, arg.Mid, arg.MessageIDs)
return
}
// StatsWithLike return stats and like state
func (r *RPC) StatsWithLike(c context.Context, arg *model.ArgStatsWithLike, res *map[int64]*model.StatsWithLike) (err error) {
*res, err = r.s.StatsWithLike(c, arg.Business, arg.Mid, arg.OriginID, arg.MessageIDs)
return
}
// UserTotalLike user item list with total count
func (r *RPC) UserTotalLike(c context.Context, arg *model.ArgUserLikes, res **model.UserTotalLike) (err error) {
*res, err = r.s.UserTotalLike(c, arg.Business, arg.Mid, arg.Pn, arg.Ps)
return
}
// UpdateCount update count
func (r *RPC) UpdateCount(c context.Context, arg *model.ArgUpdateCount, res *struct{}) (err error) {
err = r.s.UpdateCount(c, arg.Business, arg.OriginID, arg.MessageID, arg.LikeChange, arg.DislikeChange, arg.RealIP, arg.Operator)
return
}
// RawStats get stat changes
func (r *RPC) RawStats(c context.Context, arg *model.ArgRawStats, res *model.RawStats) (err error) {
*res, err = r.s.RawStats(c, arg.Business, arg.OriginID, arg.MessageID)
return
}

View File

@@ -0,0 +1,35 @@
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/thumbup/server/grpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/thumbup/api:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/service:go_default_library",
"//library/net/rpc/warden: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"],
)

View File

@@ -0,0 +1,141 @@
package grpc
import (
"context"
pb "go-common/app/service/main/thumbup/api"
"go-common/app/service/main/thumbup/model"
"go-common/app/service/main/thumbup/service"
"go-common/library/net/rpc/warden"
"github.com/golang/protobuf/ptypes/empty"
)
// New Coin warden rpc server
func New(c *warden.ServerConfig, svr *service.Service) *warden.Server {
ws := warden.NewServer(c)
pb.RegisterThumbupServer(ws.Server(), &server{s: svr})
ws, err := ws.Start()
if err != nil {
panic(err)
}
return ws
}
type server struct {
s *service.Service
}
func (r server) Like(c context.Context, req *pb.LikeReq) (reply *pb.LikeReply, err error) {
res, err := r.s.Like(c, req.Business, req.Mid, req.OriginID, req.MessageID, int8(req.Action), req.UpMid)
reply = &pb.LikeReply{
OriginID: res.OriginID,
MessageID: res.ID,
LikeNumber: res.Likes,
DislikeNumber: res.Dislikes,
}
return
}
func (r server) Stats(c context.Context, req *pb.StatsReq) (reply *pb.StatsReply, err error) {
res, err := r.s.StatsWithLike(c, req.Business, req.Mid, req.OriginID, req.MessageIds)
reply = &pb.StatsReply{Stats: map[int64]*pb.StatState{}}
for name, item := range res {
reply.Stats[name] = &pb.StatState{
OriginID: item.OriginID,
MessageID: item.ID,
LikeNumber: item.Likes,
DislikeNumber: item.Dislikes,
LikeState: pb.State(item.LikeState),
}
}
return
}
func (r server) MultiStats(c context.Context, req *pb.MultiStatsReq) (reply *pb.MultiStatsReply, err error) {
arg := &model.MultiBusiness{
Mid: req.Mid,
Businesses: make(map[string][]*model.MultiBusinessItem),
}
for name, b := range req.Business {
for _, i := range b.Records {
arg.Businesses[name] = append(arg.Businesses[name], &model.MultiBusinessItem{
OriginID: i.OriginID,
MessageID: i.MessageID,
})
}
}
res, err := r.s.MultiStatsWithLike(c, arg)
reply = &pb.MultiStatsReply{}
if res != nil {
reply.Business = make(map[string]*pb.MultiStatsReply_Records)
for k, v := range res {
items := &pb.MultiStatsReply_Records{
Records: make(map[int64]*pb.StatState),
}
for id, state := range v {
items.Records[id] = &pb.StatState{
OriginID: state.OriginID,
MessageID: state.ID,
LikeNumber: state.Likes,
DislikeNumber: state.Dislikes,
LikeState: pb.State(state.LikeState),
}
}
reply.Business[k] = items
}
}
return
}
func (r server) HasLike(c context.Context, req *pb.HasLikeReq) (reply *pb.HasLikeReply, err error) {
_, res, err := r.s.HasLike(c, req.Business, req.Mid, req.MessageIds)
reply = &pb.HasLikeReply{States: res}
return
}
func (r server) UserLikes(c context.Context, req *pb.UserLikesReq) (reply *pb.UserLikesReply, err error) {
res, err := r.s.UserTotalLike(c, req.Business, req.Mid, int(req.Pn), int(req.Ps))
reply = &pb.UserLikesReply{}
if res != nil {
reply.Total = int64(res.Total)
for _, item := range res.List {
reply.Items = append(reply.Items, &pb.ItemRecord{
MessageID: item.MessageID,
Time: item.Time,
})
}
}
return
}
func (r server) ItemLikes(c context.Context, req *pb.ItemLikesReq) (reply *pb.ItemLikesReply, err error) {
res, err := r.s.ItemLikes(c, req.Business, req.OriginID, req.MessageID, int(req.Pn), int(req.Ps), req.LastMid)
reply = &pb.ItemLikesReply{}
for _, item := range res {
reply.Users = append(reply.Users, &pb.UserRecord{
Mid: item.Mid,
Time: item.Time,
})
}
return
}
func (r server) UpdateCount(c context.Context, req *pb.UpdateCountReq) (reply *empty.Empty, err error) {
reply = &empty.Empty{}
err = r.s.UpdateCount(c, req.Business, req.OriginID, req.MessageID, req.LikeChange, req.DislikeChange, req.IP, req.Operator)
return
}
func (r server) RawStat(c context.Context, req *pb.RawStatReq) (reply *pb.RawStatReply, err error) {
res, err := r.s.RawStats(c, req.Business, req.OriginID, req.MessageID)
reply = &pb.RawStatReply{
OriginID: res.OriginID,
MessageID: res.ID,
LikeNumber: res.Likes,
DislikeNumber: res.Dislikes,
LikeChange: res.LikesChange,
DislikeChange: res.DislikesChange,
}
return
}

View 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 = [
"http.go",
"thumbup.go",
],
importpath = "go-common/app/service/main/thumbup/server/http",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//app/service/main/thumbup/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/binding:go_default_library",
"//library/net/http/blademaster/middleware/rate:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/metadata: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,62 @@
package http
import (
"net/http"
"go-common/app/service/main/thumbup/conf"
"go-common/app/service/main/thumbup/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/rate"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
likeSrv *service.Service
verifySrv *verify.Verify
)
// Init init
func Init(c *conf.Config, s *service.Service) {
verifySrv = verify.New(c.Verify)
rateLimit := rate.New(c.Rate)
likeSrv = s
// init outer router
engineOuter := bm.DefaultServer(c.BM)
engineOuter.Use(rateLimit)
outerRouter(engineOuter)
if err := engineOuter.Start(); err != nil {
log.Error("engine.Start error(%v)", err)
panic(err)
}
}
// outerRouter init outer router
func outerRouter(r *bm.Engine) {
r.Ping(ping)
r.Register(register)
cr := r.Group("/x/internal/thumbup", verifySrv.Verify)
{
cr.GET("/stats", stats)
cr.POST("/multi_stats", multiStats)
cr.GET("/user_likes", userLikes)
cr.GET("/item_likes", itemLikes)
cr.POST("/like", like)
cr.GET("/has_like", hasLike)
cr.POST("/update_count", updateCount)
cr.GET("/raw_stats", rawStats)
cr.POST("/update_upmids", updateUpMids)
cr.POST("/item_has_like", itemHasLike)
}
}
func ping(c *bm.Context) {
if err := likeSrv.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)
}

View File

@@ -0,0 +1,182 @@
package http
import (
"go-common/app/service/main/thumbup/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/binding"
"go-common/library/net/metadata"
)
func like(c *bm.Context) {
var (
err error
)
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageID int64 `form:"message_id" validate:"min=1,required"`
Type int8 `form:"type" validate:"required"`
Mid int64 `form:"mid" validate:"min=1,required"`
UpMid int64 `form:"up_mid" validate:"omitempty,min=1"`
})
if err = c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(likeSrv.Like(c, v.Business, v.Mid, v.OriginID, v.MessageID, v.Type, v.UpMid))
}
func hasLike(c *bm.Context) {
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageIDs []int64 `form:"message_ids,split" validate:"required"`
Mid int64 `form:"mid" validate:"min=1,required"`
})
if err := c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
res, _, err := likeSrv.HasLike(c, v.Business, v.Mid, v.MessageIDs)
c.JSON(res, err)
}
func stats(c *bm.Context) {
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageIDs []int64 `form:"message_ids,split" validate:"required"`
Mid int64 `form:"mid"`
})
if err := c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if v.Mid > 0 {
c.JSON(likeSrv.StatsWithLike(c, v.Business, v.Mid, v.OriginID, v.MessageIDs))
return
}
c.JSON(likeSrv.Stats(c, v.Business, v.OriginID, v.MessageIDs))
}
func userLikes(c *bm.Context) {
var (
err error
data []*model.ItemLikeRecord
)
v := new(struct {
Business string `form:"business" validate:"required"`
Type string `form:"type" validate:"required"`
Mid int64 `form:"mid" validate:"min=1,required"`
Pn int `form:"pn" default:"1" validate:"omitempty,min=1"`
Ps int `form:"ps" default:"20" validate:"omitempty,min=1"`
})
if err = c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if v.Type == "like" {
data, err = likeSrv.UserLikes(c, v.Business, v.Mid, v.Pn, v.Ps)
} else {
data, err = likeSrv.UserDislikes(c, v.Business, v.Mid, v.Pn, v.Ps)
}
if data == nil {
data = make([]*model.ItemLikeRecord, 0)
}
c.JSON(data, err)
}
func itemLikes(c *bm.Context) {
var (
err error
data []*model.UserLikeRecord
)
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageID int64 `form:"message_id" validate:"min=1,required"`
Mid int64 `form:"mid" validate:"omitempty,min=1"`
Type string `form:"type" validate:"required"`
Pn int `form:"pn" default:"1" validate:"omitempty,min=1"`
Ps int `form:"ps" default:"20" validate:"omitempty,min=1"`
})
if err = c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
if v.Type == "like" {
data, err = likeSrv.ItemLikes(c, v.Business, v.OriginID, v.MessageID, v.Pn, v.Ps, v.Mid)
} else {
data, err = likeSrv.ItemDislikes(c, v.Business, v.OriginID, v.MessageID, v.Pn, v.Ps, v.Mid)
}
if data == nil {
data = make([]*model.UserLikeRecord, 0)
}
c.JSON(data, err)
}
func multiStats(c *bm.Context) {
v := new(model.MultiBusiness)
if err := c.BindWith(v, binding.JSON); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(likeSrv.MultiStatsWithLike(c, v))
}
func updateCount(c *bm.Context) {
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageID int64 `form:"message_id" validate:"min=1,required"`
LikeChange int64 `form:"like_change"`
DislikeChange int64 `form:"dislike_change"`
Operator string `form:"operator" validate:"required"`
})
if err := c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
ip := metadata.String(c, metadata.RemoteIP)
c.JSON(nil, likeSrv.UpdateCount(c, v.Business, v.OriginID, v.MessageID, v.LikeChange, v.DislikeChange, ip, v.Operator))
}
func rawStats(c *bm.Context) {
v := new(struct {
Business string `form:"business" validate:"required"`
OriginID int64 `form:"origin_id" validate:"min=0"`
MessageID int64 `form:"message_id" validate:"min=1,required"`
})
if err := c.Bind(v); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(likeSrv.RawStats(c, v.Business, v.OriginID, v.MessageID))
}
func updateUpMids(c *bm.Context) {
v := new(struct {
Business string `json:"business" validate:"required"`
Data []*model.UpMidsReq `json:"data" validate:"required,gte=1,lte=100"`
})
if err := c.BindWith(v, binding.JSON); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, likeSrv.UpdateUpMids(c, v.Business, v.Data))
}
func itemHasLike(c *bm.Context) {
v := new(struct {
Business string `json:"business" validate:"required"`
OriginID int64 `json:"origin_id" validate:"min=0"`
MessageID int64 `json:"message_id" validate:"min=1"`
Mids []int64 `json:"mids,split" validate:"required"`
})
if err := c.BindWith(v, binding.JSON); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(likeSrv.ItemHasLike(c, v.Business, v.OriginID, v.MessageID, v.Mids))
}

View File

@@ -0,0 +1,68 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"like_test.go",
"service_test.go",
"stats_test.go",
"user_likes_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//library/cache/redis:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"check.go",
"has_like.go",
"item_likes.go",
"like.go",
"service.go",
"stats.go",
"user_likes.go",
],
importpath = "go-common/app/service/main/thumbup/service",
tags = ["automanaged"],
deps = [
"//app/service/main/thumbup/api:go_default_library",
"//app/service/main/thumbup/conf:go_default_library",
"//app/service/main/thumbup/dao:go_default_library",
"//app/service/main/thumbup/model:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus/report:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/sync/pipeline/fanout:go_default_library",
"//library/time: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,72 @@
package service
import (
"go-common/app/service/main/thumbup/model"
"go-common/library/ecode"
)
// CheckBusiness check if exist the business id.
func (s *Service) CheckBusiness(business string) (id int64, err error) {
b := s.dao.BusinessMap[business]
if b == nil {
err = ecode.ThumbupBusinessBlankErr
return
}
id = b.ID
return
}
// CheckBusinessOrigin check origin id.
func (s *Service) CheckBusinessOrigin(business string, originID int64) (id int64, err error) {
b := s.dao.BusinessMap[business]
if b == nil {
err = ecode.ThumbupBusinessBlankErr
return
}
if (b.EnableOriginID == 1 && originID == 0) || (b.EnableOriginID == 0 && originID != 0) {
err = ecode.ThumbupOriginErr
return
}
id = b.ID
return
}
func (s *Service) checkItemLikeType(businessID int64, state int8) error {
if state == model.StateLike {
if s.dao.BusinessIDMap[businessID] == nil {
return ecode.ThumbupBusinessBlankErr
}
if !s.dao.BusinessIDMap[businessID].EnableItemLikeList() {
return ecode.ThumbupBusinessErr
}
return nil
}
// check dislikes
if s.dao.BusinessIDMap[businessID] == nil {
return ecode.ThumbupBusinessBlankErr
}
if !s.dao.BusinessIDMap[businessID].EnableItemDislikeList() {
return ecode.ThumbupBusinessErr
}
return nil
}
func (s *Service) checkUserLikeType(businessID int64, state int8) error {
if state == model.StateLike {
if s.dao.BusinessIDMap[businessID] == nil {
return ecode.ThumbupBusinessBlankErr
}
if !s.dao.BusinessIDMap[businessID].EnableUserLikeList() {
return ecode.ThumbupBusinessErr
}
return nil
}
// check dislikes
if s.dao.BusinessIDMap[businessID] == nil {
return ecode.ThumbupBusinessBlankErr
}
if !s.dao.BusinessIDMap[businessID].EnableUserDislikeList() {
return ecode.ThumbupBusinessErr
}
return nil
}

View File

@@ -0,0 +1,114 @@
package service
import (
"context"
"fmt"
pb "go-common/app/service/main/thumbup/api"
"go-common/app/service/main/thumbup/dao"
"go-common/app/service/main/thumbup/model"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
// 用户点赞列表数据量少的话 全取比较好 点赞数量多的话 同时一次性取的少的话 zscore比较好
var _rangeLimit = 20 //经验值
// HasLike .
func (s *Service) HasLike(c context.Context, business string, mid int64, messageIDs []int64) (res map[int64]int8, all map[int64]*pb.UserLikeState, err error) {
if len(messageIDs) == 0 {
return
}
var businessID int64
if businessID, err = s.CheckBusiness(business); err != nil {
return
}
if mid == 0 {
return
}
var (
likes = make(map[int64]*pb.UserLikeState)
dislikes = make(map[int64]*pb.UserLikeState)
messageIDMap = make(map[int64]bool)
)
for _, id := range messageIDs {
messageIDMap[id] = true
}
group := errgroup.Group{}
limit := s.dao.BusinessIDMap[businessID].UserLikesLimit
if s.dao.BusinessIDMap[businessID].EnableUserLikeList() {
group.Go(func() (err error) {
exist, _ := s.dao.ExpireUserLikesCache(c, mid, businessID, model.StateLike)
if exist && len(messageIDs) <= _rangeLimit {
likes, err = s.dao.UserLikeExists(c, mid, businessID, messageIDs, model.StateLike)
return
}
var items []*model.ItemLikeRecord
if items, err = s.dao.UserLikeList(c, mid, businessID, model.StateLike, 0, limit); err != nil {
return err
}
s.dbus.Do(c, func(c context.Context) {
s.dao.PubUserMsg(c, business, mid, model.StateLike)
})
for _, item := range items {
if messageIDMap[item.MessageID] {
likes[item.MessageID] = &pb.UserLikeState{
Mid: mid,
Time: item.Time,
State: pb.State_STATE_LIKE,
}
}
}
return
})
}
if s.dao.BusinessIDMap[businessID].EnableUserDislikeList() {
group.Go(func() (err error) {
exist, _ := s.dao.ExpireUserLikesCache(c, mid, businessID, model.StateDislike)
if exist && len(messageIDs) <= _rangeLimit {
dislikes, err = s.dao.UserLikeExists(c, mid, businessID, messageIDs, model.StateDislike)
return
}
var items []*model.ItemLikeRecord
if items, err = s.dao.UserLikeList(c, mid, businessID, model.StateDislike, 0, limit); err != nil {
return err
}
s.dbus.Do(c, func(c context.Context) {
s.dao.PubUserMsg(c, business, mid, model.StateDislike)
})
for _, item := range items {
if messageIDMap[item.MessageID] {
dislikes[item.MessageID] = &pb.UserLikeState{
Mid: mid,
Time: item.Time,
State: pb.State_STATE_DISLIKE,
}
}
}
return
})
}
err = group.Wait()
if err != nil {
log.Errorv(c, log.KV("log", fmt.Sprintf("cache.Exists(%v) err: %+v", messageIDs, err)), log.KV("mid", mid), log.KV("business", business))
dao.PromError("has_like:Exists")
}
res = make(map[int64]int8)
all = make(map[int64]*pb.UserLikeState)
for id, v := range likes {
if v != nil {
res[id] = model.StateLike
all[id] = v
}
}
for id, v := range dislikes {
if v != nil {
res[id] = model.StateDislike
all[id] = v
}
}
if len(messageIDs) == 1 {
log.Info("has_like business: %s mid: %v message_id: %v, resp: %+v, err: %v", business, mid, messageIDs[0], res, err)
}
return
}

View File

@@ -0,0 +1,104 @@
package service
import (
"context"
"go-common/app/service/main/thumbup/model"
"go-common/library/stat/prom"
xtime "go-common/library/time"
)
// ItemLikes .
func (s *Service) ItemLikes(c context.Context, business string, originID, messageID int64, pn, ps int, mid int64) (res []*model.UserLikeRecord, err error) {
return s.itemLikes(c, business, originID, messageID, pn, ps, model.StateLike, mid)
}
// ItemDislikes .
func (s *Service) ItemDislikes(c context.Context, business string, originID, messageID int64, pn, ps int, mid int64) (res []*model.UserLikeRecord, err error) {
return s.itemLikes(c, business, originID, messageID, pn, ps, model.StateDislike, mid)
}
func (s *Service) itemLikes(c context.Context, business string, originID, messageID int64, pn, ps int, state int8, mid int64) (res []*model.UserLikeRecord, err error) {
var businessID int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
if err = s.checkItemLikeType(businessID, state); err != nil {
return
}
var (
start = (pn - 1) * ps
end = start + ps - 1 // from cache, end-1
)
res, _ = s.dao.CacheItemLikeList(c, messageID, businessID, state, start, end)
if len(res) != 0 {
prom.CacheHit.Incr("itemLikeList")
} else {
s.dbus.Do(c, func(c context.Context) {
s.dao.PubItemMsg(c, business, originID, messageID, state)
})
prom.CacheMiss.Incr("itemLikeList")
res, err = s.dao.RawItemLikeList(c, messageID, businessID, originID, state, start, end)
if err != nil {
return
}
}
for i, r := range res {
if r.Mid == mid && len(res) > i+1 {
res = res[i+1:]
return
}
}
return
}
// UpdateUpMids .
func (s *Service) UpdateUpMids(c context.Context, business string, data []*model.UpMidsReq) (err error) {
var businessID int64
if businessID, err = s.CheckBusiness(business); err != nil {
return
}
for _, m := range data {
if _, err = s.CheckBusinessOrigin(business, m.OriginID); err != nil {
return
}
}
prom.BusinessInfoCount.Add("update-mids-raw-"+business, int64(len(data)))
rows, err := s.dao.UpdateUpMids(c, businessID, data)
prom.BusinessInfoCount.Add("update-mids-affect-"+business, rows)
return
}
// ItemHasLike item是否被mids点赞过 返回mid/点赞时间戳
func (s *Service) ItemHasLike(c context.Context, business string, originID, messageID int64, mids []int64) (res map[int64]*model.UserLikeRecord, err error) {
var businessID int64
var likes map[int64]int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
state := int8(model.StateLike)
if err = s.checkItemLikeType(businessID, state); err != nil {
return
}
var exist bool
if exist, _ = s.dao.ExpireItemLikesCache(c, messageID, businessID, state); exist {
if likes, err = s.dao.ItemLikeExists(c, messageID, businessID, mids, state); err != nil {
exist = false
err = nil
}
}
if !exist {
likes, err = s.dao.ItemHasLike(c, businessID, originID, messageID, mids, state)
s.dbus.Do(c, func(c context.Context) {
s.dao.PubItemMsg(c, business, originID, messageID, state)
})
}
res = make(map[int64]*model.UserLikeRecord)
for mid, t := range likes {
res[mid] = &model.UserLikeRecord{
Mid: mid,
Time: xtime.Time(t),
}
}
return
}

View File

@@ -0,0 +1,110 @@
package service
import (
"context"
"time"
"go-common/app/service/main/thumbup/dao"
"go-common/app/service/main/thumbup/model"
"go-common/library/ecode"
"go-common/library/log"
)
// Like like
func (s *Service) Like(c context.Context, business string, mid, originID, messageID int64, likeType int8, upMid int64) (stat model.Stats, err error) {
var businessID int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
if (likeType < 0) || (likeType > 4) {
err = ecode.RequestErr
return
}
oldState, err := s.dao.LikeState(c, mid, businessID, originID, messageID)
if err == nil {
if oldState == model.StateBlank {
switch likeType {
case model.TypeCancelLike:
err = ecode.ThumbupCancelLikeErr
case model.TypeCancelDislike:
err = ecode.ThumbupCancelDislikeErr
}
} else if oldState == model.StateLike {
switch likeType {
case model.TypeLike:
err = ecode.ThumbupDupLikeErr
case model.TypeCancelDislike:
err = ecode.ThumbupCancelDislikeErr
}
} else if oldState == model.StateDislike {
switch likeType {
case model.TypeCancelLike:
err = ecode.ThumbupCancelLikeErr
case model.TypeDislike:
err = ecode.ThumbupDupDislikeErr
}
} else {
dao.PromError("点赞表状态异常")
log.Error("service.Like(mid:%v bid:%v oid: %v, messageID: %v, state: %v)", mid, businessID, originID, messageID, oldState)
return
}
if err != nil {
return
}
}
err = s.dbus.Do(c, func(c context.Context) {
msg := &model.LikeMsg{UpMid: upMid, Mid: mid, Type: likeType, LikeTime: time.Now(), Business: business, OriginID: originID, MessageID: messageID}
s.dao.PubLikeDatabus(c, msg)
})
if err != nil {
return
}
// todo fix
if business == "reply" {
return
}
stats, err := s.Stats(c, business, originID, []int64{messageID})
if err == nil && stats != nil && stats[messageID] != nil {
stat = *stats[messageID]
}
stat = calculateCount(stat, likeType)
return
}
func calculateCount(stat model.Stats, typ int8) model.Stats {
var likesCount, dislikesCount int64
switch typ {
case model.TypeLike:
likesCount = 1
case model.TypeCancelLike:
likesCount = -1
case model.TypeDislike:
dislikesCount = 1
case model.TypeCancelDislike:
dislikesCount = -1
case model.TypeLikeReverse:
likesCount = -1
dislikesCount = 1
case model.TypeDislikeReverse:
likesCount = 1
dislikesCount = -1
}
stat.Likes += likesCount
stat.Dislikes += dislikesCount
return stat
}
func (s *Service) updateStatCache(c context.Context, businessID, originID int64, stat *model.Stats) (err error) {
if stat == nil {
return
}
var ok bool
if originID == 0 {
err = s.dao.AddStatsCache(c, businessID, stat)
} else {
if ok, err = s.dao.ExpireHashStatsCache(c, businessID, originID); ok {
err = s.dao.AddHashStatsCache(c, businessID, originID, stat)
}
}
return
}

View File

@@ -0,0 +1,68 @@
package service
import (
"fmt"
"go-common/app/service/main/thumbup/model"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_updateCounts(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
messageID := int64(1)
bid := int64(2)
oid := int64(0)
stat, err := s.dao.Stat(c, bid, oid, messageID)
So(err, ShouldBeNil)
args := [][2]int64{
{1, 1},
{-1000, -1000},
{1, 0},
{0, 1},
{-1, 0},
{-1, -1},
{0, -1},
{0, 0},
{100, -10000},
{100, 20},
{-10, 20},
}
var l, d int64
for _, x := range args {
l = x[0]
d = x[1]
Convey(fmt.Sprintf("like %v dislike %v", l, d), func() {
err := s.dao.UpdateCounts(c, bid, oid, messageID, l, d, 0)
So(err, ShouldBeNil)
nstat, err := s.dao.Stat(c, bid, oid, messageID)
So(err, ShouldBeNil)
likes := stat.Likes + l
if likes < 0 {
likes = 0
}
dislikes := stat.Dislikes + d
if dislikes < 0 {
dislikes = 0
}
So(nstat.Likes, ShouldEqual, likes)
So(nstat.Dislikes, ShouldEqual, dislikes)
})
}
}))
}
func Test_calculateCount(t *testing.T) {
Convey("get data", t, func() {
stat := model.Stats{Likes: 1, Dislikes: 1, ID: 1}
So(calculateCount(stat, model.TypeLike), ShouldResemble, model.Stats{Likes: 2, Dislikes: 1, ID: 1})
So(calculateCount(stat, model.TypeCancelLike), ShouldResemble, model.Stats{Likes: 0, Dislikes: 1, ID: 1})
So(calculateCount(stat, model.TypeDislike), ShouldResemble, model.Stats{Likes: 1, Dislikes: 2, ID: 1})
So(calculateCount(stat, model.TypeCancelDislike), ShouldResemble, model.Stats{Likes: 1, Dislikes: 0, ID: 1})
So(calculateCount(stat, model.TypeLikeReverse), ShouldResemble, model.Stats{Likes: 0, Dislikes: 2, ID: 1})
So(calculateCount(stat, model.TypeDislikeReverse), ShouldResemble, model.Stats{Likes: 2, Dislikes: 0, ID: 1})
stat = model.Stats{Likes: 0, Dislikes: 0, ID: 1}
So(calculateCount(stat, model.TypeCancelLike), ShouldResemble, model.Stats{Likes: -1, Dislikes: 0, ID: 1})
So(calculateCount(stat, model.TypeCancelDislike), ShouldResemble, model.Stats{Likes: 0, Dislikes: -1, ID: 1})
})
}

View File

@@ -0,0 +1,47 @@
package service
import (
"context"
"sync"
"go-common/app/service/main/thumbup/conf"
"go-common/app/service/main/thumbup/dao"
"go-common/library/sync/pipeline/fanout"
)
// Service service
type Service struct {
c *conf.Config
dao *dao.Dao
cache *fanout.Fanout
dbus *fanout.Fanout
waiter *sync.WaitGroup
close bool
}
// New new
func New(c *conf.Config) *Service {
s := &Service{
c: c,
dao: dao.New(c),
cache: fanout.New("cache", fanout.Buffer(10240)),
dbus: fanout.New("dbus"),
waiter: new(sync.WaitGroup),
}
return s
}
// Close close dao.
func (s *Service) Close() {
s.cache.Close()
s.dbus.Close()
s.dao.Close()
s.close = true
s.waiter.Wait()
}
// Ping check connection success.
func (s *Service) Ping(c context.Context) (err error) {
err = s.dao.Ping(c)
return
}

View File

@@ -0,0 +1,37 @@
package service
import (
"context"
"flag"
"path/filepath"
"time"
"go-common/app/service/main/thumbup/conf"
"go-common/library/cache/redis"
. "github.com/smartystreets/goconvey/convey"
)
var (
svr *Service
c = context.Background()
)
func CleanCache() {
pool := redis.NewPool(conf.Conf.Redis.Config)
pool.Get(c).Do("FLUSHDB")
}
func init() {
dir, _ := filepath.Abs("../cmd/thumbup-test.toml")
flag.Set("conf", dir)
conf.Init()
svr = New(conf.Conf)
time.Sleep(time.Second)
}
func WithService(f func(s *Service)) func() {
return func() {
Reset(func() { CleanCache() })
f(svr)
}
}

View File

@@ -0,0 +1,248 @@
package service
import (
"context"
"sync"
"time"
"go-common/app/service/main/thumbup/model"
"go-common/library/ecode"
"go-common/library/queue/databus/report"
"go-common/library/sync/errgroup"
)
// Stats .
func (s *Service) Stats(c context.Context, business string, originID int64, messageIDs []int64) (res map[int64]*model.Stats, err error) {
var businessID int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
if originID != 0 {
res, err = s.originStats(c, businessID, originID, messageIDs)
} else {
res, err = s.itemStats(c, businessID, originID, messageIDs)
}
if err != nil {
return
}
for _, id := range messageIDs {
if res[id] == nil {
res[id] = &model.Stats{ID: id, OriginID: originID}
}
}
return
}
func (s *Service) originStats(c context.Context, businessID, originID int64, messageIDs []int64) (res map[int64]*model.Stats, err error) {
var (
cache bool
addCache = true
)
if cache, err = s.dao.ExpireHashStatsCache(c, businessID, originID); err != nil {
addCache = false
}
if cache {
if res, err = s.dao.HashStatsCache(c, businessID, originID, messageIDs); err == nil {
return
}
addCache = false
}
var stats map[int64]*model.Stats
if stats, err = s.dao.OriginStats(c, businessID, originID); err != nil {
return
}
res = make(map[int64]*model.Stats)
for _, id := range messageIDs {
res[id] = stats[id]
}
if addCache {
s.cache.Do(c, func(c context.Context) {
s.dao.AddHashStatsCacheMap(c, businessID, originID, stats)
})
}
return
}
func (s *Service) itemStats(c context.Context, businessID, originID int64, messageIDs []int64) (res map[int64]*model.Stats, err error) {
var (
missed []int64
addCache = true
)
res, missed, err = s.dao.StatsCache(c, businessID, messageIDs)
if err == nil && len(missed) == 0 {
return
} else if err != nil {
res = nil
missed = messageIDs
addCache = false
}
var stats map[int64]*model.Stats
if stats, err = s.dao.MessageStats(c, businessID, missed); err != nil {
return
}
if res == nil {
res = make(map[int64]*model.Stats)
}
for id, stat := range stats {
res[id] = stat
}
if addCache {
s.cache.Do(c, func(c context.Context) {
s.dao.AddStatsCacheMap(c, businessID, stats)
})
}
return
}
// AddStatsCache .
func (s *Service) AddStatsCache(c context.Context, businessID, originID int64, stat *model.Stats) error {
if originID == 0 {
return s.dao.AddStatsCache(c, businessID, stat)
}
return s.dao.AddHashStatsCache(c, businessID, originID, stat)
}
// StatsWithLike .
func (s *Service) StatsWithLike(c context.Context, business string, mid, originID int64, messageIDs []int64) (res map[int64]*model.StatsWithLike, err error) {
if _, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
group, ctx := errgroup.WithContext(c)
var stats map[int64]*model.Stats
var likes map[int64]int8
group.Go(func() (err error) {
stats, err = s.Stats(ctx, business, originID, messageIDs)
return
})
group.Go(func() (err error) {
likes, _, err = s.HasLike(ctx, business, mid, messageIDs)
return
})
if err = group.Wait(); err != nil {
return
}
res = make(map[int64]*model.StatsWithLike)
for id, stat := range stats {
if stat == nil {
stat = &model.Stats{}
}
var likeState int8
if likes != nil {
likeState = likes[id]
}
res[id] = &model.StatsWithLike{Stats: *stat, LikeState: likeState}
}
return
}
// MultiStatsWithLike multi stats
func (s *Service) MultiStatsWithLike(c context.Context, arg *model.MultiBusiness) (res map[string]map[int64]*model.StatsWithLike, err error) {
group := errgroup.Group{}
mutex := sync.Mutex{}
res = make(map[string]map[int64]*model.StatsWithLike)
for businessName, business := range arg.Businesses {
oids := make(map[int64][]int64)
for _, b := range business {
oids[b.OriginID] = append(oids[b.OriginID], b.MessageID)
}
businessName := businessName
for oid, ids := range oids {
oid := oid
ids := ids
group.Go(func() (err error) {
r, err := s.StatsWithLike(c, businessName, arg.Mid, oid, ids)
if err != nil {
return
}
mutex.Lock()
if res[businessName] == nil {
res[businessName] = r
} else {
for k, v := range r {
res[businessName][k] = v
}
}
mutex.Unlock()
return
})
}
}
err = group.Wait()
if err != nil && len(res) > 0 {
err = nil
}
return
}
// UpdateCount update count
func (s *Service) UpdateCount(c context.Context, business string, originID, messageID int64, likeChange, dislikeChange int64, ip, operator string) (err error) {
var businessID int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
if operator == "" {
err = ecode.RequestErr
return
}
if likeChange == 0 && dislikeChange == 0 {
return
}
const thumbupType = 171
stats, err := s.Stats(c, business, originID, []int64{messageID})
if err != nil {
return
}
var likeCount, dislikeCount int64
if stats[messageID] != nil {
likeCount = stats[messageID].Likes
dislikeCount = stats[messageID].Dislikes
}
if likeCount+likeChange < 0 {
likeChange = -likeCount
}
if dislikeCount+dislikeChange < 0 {
dislikeChange = -dislikeCount
}
if err = s.dao.UpdateCount(c, businessID, originID, messageID, likeChange, dislikeChange); err != nil {
return
}
// add log
_ = report.Manager(&report.ManagerInfo{
Uname: operator,
Business: thumbupType,
Type: 0,
Ctime: time.Now(),
Index: []interface{}{business, originID, messageID},
Content: map[string]interface{}{
"ip": ip,
"like_change": likeChange,
"dislike_change": dislikeChange,
},
})
_ = s.cache.Do(c, func(c context.Context) {
// update cache not del
stats, err := s.Stats(c, business, originID, []int64{messageID})
if err != nil {
return
}
if stats == nil || stats[messageID] == nil {
return
}
stat := stats[messageID]
stat.Likes += likeChange
stat.Dislikes += dislikeChange
s.updateStatCache(c, businessID, originID, stat)
s.dao.PubStatDatabus(c, business, 0, stat, 0)
})
return
}
// RawStats get stat changes
func (s *Service) RawStats(c context.Context, business string, originID, messageID int64) (res model.RawStats, err error) {
var businessID int64
if businessID, err = s.CheckBusinessOrigin(business, originID); err != nil {
return
}
res, err = s.dao.RawStats(c, businessID, originID, messageID)
return
}

View File

@@ -0,0 +1,28 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Stats(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
_, err := s.Stats(c, "danmu", 1, []int64{1, 2})
So(err, ShouldBeNil)
}))
}
func Test_StatsWithLike(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
_, err := s.StatsWithLike(c, "danmu", 1, 1, []int64{1, 2})
So(err, ShouldBeNil)
}))
}
func Test_UpdateCount(t *testing.T) {
Convey("work", t, WithService(func(s *Service) {
err := s.UpdateCount(c, "danmu", 1, 3, 1, 1, "", "sam")
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,67 @@
package service
import (
"context"
"go-common/app/service/main/thumbup/model"
"go-common/library/sync/errgroup"
)
// UserLikes .
func (s *Service) UserLikes(c context.Context, business string, mid int64, pn, ps int) (res []*model.ItemLikeRecord, err error) {
return s.userLikes(c, business, mid, pn, ps, model.StateLike)
}
// UserDislikes user dislikes
func (s *Service) UserDislikes(c context.Context, business string, mid int64, pn, ps int) (res []*model.ItemLikeRecord, err error) {
return s.userLikes(c, business, mid, pn, ps, model.StateDislike)
}
func (s *Service) userLikes(c context.Context, business string, mid int64, pn, ps int, state int8) (res []*model.ItemLikeRecord, err error) {
var businessID int64
if businessID, err = s.CheckBusiness(business); err != nil {
return
}
if err = s.checkUserLikeType(businessID, state); err != nil {
return
}
var (
start = (pn - 1) * ps
end = start + ps - 1 // from cache, end-1
)
res, err = s.dao.UserLikeList(c, mid, businessID, state, start, end)
return
}
// UserTotalLike user like list with total
func (s *Service) UserTotalLike(c context.Context, business string, mid int64, pn, ps int) (res *model.UserTotalLike, err error) {
group, ctx := errgroup.WithContext(c)
res = &model.UserTotalLike{}
group.Go(func() (err error) {
res.List, err = s.UserLikes(ctx, business, mid, pn, ps)
return
})
group.Go(func() (err error) {
res.Total, err = s.userTotal(ctx, business, mid)
return
})
if err = group.Wait(); err != nil {
res = nil
return
}
return
}
// userTotal user total like
func (s *Service) userTotal(c context.Context, business string, mid int64) (res int, err error) {
var businessID int64
if businessID, err = s.CheckBusiness(business); err != nil {
return
}
if exist, _ := s.dao.ExpireUserLikesCache(c, mid, businessID, model.StateLike); exist {
res, err = s.dao.UserLikesCountCache(c, businessID, mid)
return
}
res, err = s.dao.UserLikeCount(c, businessID, mid, model.StateLike)
return
}

View File

@@ -0,0 +1,29 @@
package service
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_UserLikes(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
res, err := s.UserLikes(c, "danmu", 1, 1, 2)
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
}))
}
func Test_UserTotalLike(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
_, err := s.UserTotalLike(c, "danmu", 1, 1, 2)
So(err, ShouldBeNil)
}))
}
func Test_UserDislikes(t *testing.T) {
Convey("get data", t, WithService(func(s *Service) {
_, err := s.UserDislikes(c, "danmu", 1, 1, 2)
So(err, ShouldBeNil)
}))
}