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,21 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/interface/main/push-archive/cmd:all-srcs",
"//app/interface/main/push-archive/conf:all-srcs",
"//app/interface/main/push-archive/dao:all-srcs",
"//app/interface/main/push-archive/http:all-srcs",
"//app/interface/main/push-archive/model:all-srcs",
"//app/interface/main/push-archive/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,176 @@
# push-archive
### v1.6.0
1. 特殊关注和普通关注用不同的业务组推
### v1.5.0
1. using push grpc
### v1.4.3
1. 调整abtest策略叠加
### v1.4.2
1. prodSwitch
### v1.4.1
1. 优化abtest方式
### v1.4.0
1. abtest
### v1.3.11
1. hbase v2
2. 频率控制全部有配置控制
### v1.3.10
1. order配置可用分组和分组优先级order元素=分组key
2. active配置活跃过滤开关无值为关有值为开且为默认活跃时间
3. fangroup.hitby指定分组规则default/hbase, default=全部命中,hbase=走hbase表过滤
### v1.3.9
1. 移除up主维度半小时限制策略
2. 移除粉丝维度每个up主限制策略
3. 批量id范围去删除统计数据避免context超时
4. 基础库identify改成auth+verifyhttp.serverconfig改成bladermaster.serverconfig,bm改为newserver+start模式
### v1.3.8
1. xints迁移到model
### v1.3.7
1. 移除hbase的ping
### v1.3.6
1. 新推送接口必需提供uuid避免由于网络问题而重试产生的重复推送
### v1.3.5
1. 推送接口修改为/push-strategy/task/add
### v1.3.4
1. 迁移http到blademaster
### v1.3.3
1. 迁移push model至push-service
### v1.3.2
1. rpc双写推送开关数据到推送平台
### v1.3.1
1. 推送平台接口支持签名认证
### v1.3.0
1. 迁移至interface/main目录下
### v1.2.24
1. 使用account-service v7
### v1.2.23
1. 支持默认活跃时间过滤
### v1.2.22
1. fix 推送名单过滤前后变量共享导致的过滤无效问题
### v1.2.21
1. 过滤不在活跃时间内的粉丝推送
### v1.2.20
1. 特殊关注粉丝免限制条件: up同时在最近常看的列表中
2. 普通关注粉丝分组优先级策略
### v1.2.19
1. 特殊关注粉丝和普通关注粉丝的推送次数分开限制
### v1.2.18
1. 限制特殊关注的推送频率: cd时间内总推送次数 + cd时间内每个粉丝关注的每个up主的推送次数
### v1.2.17
1. pgc稿件屏蔽非特殊关注粉丝的推送
### v1.2.16
1. 统计数据redis缓存获取间隔为100ms, 降低获取 qps
### v1.2.15
1. 推送禁止时间分段设置
### v1.2.14
1. 推送开关由缓存查询改为db查询, antispam限制查询频率
### v1.2.13
1. 统计数据db del panic fix
### v1.2.12
1. 统计数据redis cache消耗间隔 1ms
### v1.2.11
1. 统计数据缓存到redisfix channel panic
### v1.2.10
1. 文案从统一共享,改成各组配置
### v1.2.9
1. 统计数据落库bilibili_push_archive.push_statistics
2. sms报警改为wechat报警
3. fangroup增加name=(hbasetable/special),用户prominfo统计
4. 推送接口的group参数截短,比如:ai:pushlist_offline_up截短为offline
### v1.2.8
1. 添加过审稿件统计、过审稿件的粉丝统计、特殊关注粉丝命中统计、特殊关注粉丝实际推送统计
### v1.2.7
1. 禁止推送时间内消费databus消息使得非禁止时间推送的都是非禁止时间内更新的稿件
### v1.2.6
1. 普关粉丝未设置推送开关时,默认推送,用户推送开关分批加载
### v1.2.5
1. 实验组hbase查询改为rowkey get形式移除之前的rowfilter形式经常超时
### v1.2.4
1. 实验组取值比例连续性移除
### v1.2.3
1. 实验组取值比例有起始值
### v1.2.2
1. 统计名字重复fix
### v1.2.1
1. 普通关注分3实验组
### v1.1.10
1. account rpc Info3 改成 info2
### v1.1.9
1. 修改稿件推送标题
### v1.1.8
1. 去除upper主白名单
### v1.1.7
1. 请求push接口加上版本限制
### v1.1.6
1. 修复 canal relation tag
### v1.1.5
1. up主过审推送频率限制存redis
2. 调账号RPC查up主名称
### v1.1.4
1. 修复稿件过审判断逻辑
### v1.1.3
1. 稿件过审后加up主推送白名单
2. 替换prom为go-common中对象
### v1.1.2
1. 控制databus消费速率
### v1.1.1
1. dao中加入hbase的close和ping
### v1.1.0
1. 稿件推送
### v1.0.0
1. 用户稿件推送设置的上传下载接口

View File

@@ -0,0 +1,12 @@
# Owner
zhapuyu
shencen
renwei
liweijia
wangzhe01
# Author
wangjian
# Reviewer
zhapuyu

View File

@@ -0,0 +1,18 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liweijia
- renwei
- shencen
- wangjian
- wangzhe01
- zhapuyu
labels:
- interface
- interface/main/push-archive
- main
options:
no_parent_owners: true
reviewers:
- wangjian
- zhapuyu

View File

@@ -0,0 +1,10 @@
#### push-archive
##### 项目简介
> 1.稿件动态推送
##### 编译环境
> 请只用golang v1.7.x以上版本编译执行。
##### 依赖包
> 1.公共包go-common

View File

@@ -0,0 +1,43 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["push-archive-test.toml"],
importpath = "go-common/app/interface/main/push-archive/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/http:go_default_library",
"//app/interface/main/push-archive/service:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,47 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/http"
"go-common/app/interface/main/push-archive/service"
"go-common/library/log"
"go-common/library/net/trace"
)
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("push-archive start")
svr := service.New(conf.Conf)
http.Init(conf.Conf, 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("push-archive get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
svr.Close()
log.Info("push-archive exit")
time.Sleep(time.Second)
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,283 @@
[log]
dir = "/data/log/push-archive/"
# [log.agent]
# family = "push-archive"
# taskID = "000057"
# proto = "unixgram"
# addr = "/var/run/lancer/collector.sock"
# chan = 10240
[HTTPClient]
dial = "1s"
timeout = "2s"
keepAlive = "60s"
key = "f265dcfa28272742"
secret = "437facc22dc8698b5544669bcc12348d"
[HTTPClient.breaker]
window ="1s"
sleep ="10ms"
bucket = 10
ratio = 0.5
request = 100
[wechat]
username="chenxi01"
token="GYQeuDWBbAsCNeGz"
secret="ZKpmgINTkianyMbMixyxcPQjMCSHCDrk"
[mysql]
addr = "172.16.0.148"
dsn = "test:test@tcp(172.16.33.205:3308)/bilibili_push_archive?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 10
idle = 5
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "1s"
[mysql.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[hbase]
master = ""
meta = ""
dialTimeout = "1s"
readTimeout = "2s"
readsTimeout = "5s"
writeTimeout = "2s"
writesTimeout = "5s"
[hbase.zookeeper]
root = ""
addrs = ["127.0.0.1:2181"]
timeout = "30s"
[fansHBase]
master = ""
meta = ""
dialTimeout = "1s"
readTimeout = "2s"
readsTimeout = "5s"
writeTimeout = "2s"
writesTimeout = "5s"
[fansHBase.zookeeper]
root = ""
addrs = ["172.18.33.131:2181","172.18.33.168:2181","172.18.33.169:2181"]
timeout = "30s"
[redis]
name = "push-archive"
proto = "tcp"
addr = "172.16.33.54:6379"
idle = 1000
active = 1000
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "30s"
[archiveSub]
key = "0QEO9F8JuuIxZzNDvklH"
secret="0QEO9F8JuuIxZzNDvklI"
group= "ArchiveNotify-Push-S"
topic= "ArchiveNotify-T"
action="sub"
name = "interface/push-archive"
proto = "tcp"
addr = "172.16.33.158:6205"
idle = 10
active = 100
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "60s"
[relationSub]
key = "0QEO9F8JuuIxZzNDvklH"
secret="0QEO9F8JuuIxZzNDvklI"
group= "Relation-Push-S"
topic= "Relation-T"
action="sub"
name = "interface/push-archive"
proto = "tcp"
addr = "172.16.33.158:6205"
idle = 10
active = 100
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "60s"
[accountRPC]
timeout = "200ms"
[pushRPC]
timeout = "800ms"
[anti]
on=true
second=4
n=1
hour=12
m=10
[anti.redis]
name = "push-archive"
proto = "tcp"
addr = "172.16.33.54:6379"
idle = 1000
active = 1000
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "30s"
[bm]
addr="0.0.0.0:7031"
maxListen=1000
timeout="1s"
readTimeout="1s"
writeTimeout="1s"
[push]
prodSwitch = true
addAPI = "http://api.bilibili.co/x/internal/push-strategy/task/add"
multiAPI = "http://api.bilibili.co/x/internal/push/task/add"
businessID = 2
businessToken = "ynt2nxa3uevlf4goejd99zelborhn07s"
businessSpecialID = 3
businessSpecialToken = ""
loadSettingsInterval = "5s"
[abtest]
hbaseBlacklistTable = "ai:pushlist_black"
hbaseBlacklistFamily = ["m"]
hbaseeWhitelistTable = "ai:pushlist_recover"
hbaseWhitelistFamily = ["m"]
testGroup = [0,1]
comparisonGroup = [2,3]
testMids = [91221505]
[arcPush]
upperLimitExpire = "30m"
pushStatisticsKeepDays = 3
pushStatisticsClearTime = "12:53:32"
order = ["1#ab_test_attention", "2#ab_test_special", "1#ab_comparison_attention", "2#ab_comparison_special", "1#attention", "2#special"]
activeTime = []
[[arcPush.forbidTimes]]
pushForbidStartTime = "00:00:00"
pushForbidEndTime = "08:00:00"
[[arcPush.forbidTimes]]
pushForbidStartTime = "23:00:00"
pushForbidEndTime = "23:59:59"
[[arcPush.proportions]]
proportionStartFrom = "10"
proportion = "0.30"
[[arcPush.fanGroup]]
name = "ai:pushlist_follow_recent"
desc = "最近关注up主的粉丝"
relationType = 1
hitby = "hbase"
limit = 1
perUpperLimit = 0
limitExpire = "1h"
hbaseTable = "ai:pushlist_follow_recent"
hbaseFamily = ["m"]
msgTemplateDesc = "你最近关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75363730305c75386664315c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ai:pushlist_play_recent"
desc = "最近常看up主的粉丝"
relationType = 1
hitby = "hbase"
limit = 1
perUpperLimit = 0
limitExpire = "1h"
hbaseTable = "ai:pushlist_play_recent"
hbaseFamily = ["m"]
msgTemplateDesc = "你最近常看的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75363730305c75386664315c75356533385c75373730625c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ai:pushlist_offline_up"
desc = "与up主亲密的粉丝"
relationType = 1
hitby = "hbase"
limit = 1
perUpperLimit = 0
limitExpire = "1h"
hbaseTable = "ai:pushlist_offline_up"
hbaseFamily = ["m"]
msgTemplateDesc = "你关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "attention"
desc = "普通关注粉丝"
relationType = 1
hitby = "default"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "special"
desc = "特别关注up主的粉丝"
relationType = 2
hitby = "default"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你特别关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75373237395c75353232625c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ab_test_attention"
desc = "abtest普通关注粉丝"
relationType = 1
hitby = "ab_test"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ab_test_special"
desc = "abtest特别关注up主的粉丝"
relationType = 2
hitby = "ab_test"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你特别关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75373237395c75353232625c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ab_comparison_attention"
desc = "abtest对照组 普通关注粉丝"
relationType = 1
hitby = "ab_comparison"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"
[[arcPush.fanGroup]]
name = "ab_comparison_special"
desc = "abtest对照组 特别关注up主的粉丝"
relationType = 2
hitby = "ab_comparison"
limit = 1
perUpperLimit = 0
limitExpire = "3h"
hbaseTable = ""
hbaseFamily = []
msgTemplateDesc = "你特别关注的%s刚发了视频\r\n%s"
msgTemplate = "225c75346636305c75373237395c75353232625c75353137335c75366365385c753736383425735c75353231615c75353364315c75346538365c75383963365c75393839315c725c6e257322"

View File

@@ -0,0 +1,45 @@
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/interface/main/push-archive/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/hbase.v2:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/antispam:go_default_library",
"//library/net/http/blademaster/middleware/auth: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,196 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
"go-common/library/log"
"go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/antispam"
"go-common/library/net/http/blademaster/middleware/auth"
"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"
xtime "go-common/library/time"
"go-common/library/database/hbase.v2"
"github.com/BurntSushi/toml"
)
// global var
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config config set
type Config struct {
Log *log.Config
HTTPClient *blademaster.ClientConfig
Tracer *trace.Config
Auth *auth.Config
Verify *verify.Config
// Wechat wechat config
Wechat *wechat
// HBase for fans
HBase *hbaseConf
// FansHBase for attention groups + active time
FansHBase *hbaseConf
Redis *redis.Config
// MySQL
MySQL *sql.Config
AccountRPC *rpc.ClientConfig
// ArchiveSub archive_result databus consumer
ArchiveSub *databus.Config
// RelationSub relation_xxx databus consumer
RelationSub *databus.Config
Push *push
// ArcPush archive push settings
ArcPush *arcPush
PushRPC *warden.ClientConfig
// Anti antispam
Anti *antispam.Config
Bm *blademaster.ServerConfig
Abtest *abtest
}
type abtest struct {
HbaseBlacklistTable string
HbaseBlacklistFamily []string
HbaseeWhitelistTable string
HbaseWhitelistFamily []string
TestGroup []int
ComparisonGroup []int
TestMids []int64
}
type hbaseConf struct {
hbase.Config
ReadTimeout xtime.Duration
ReadsTimeout xtime.Duration
WriteTimeout xtime.Duration
WritesTimeout xtime.Duration
}
/**
* 配置规则:
PushStatisticsKeepDays 推送数据保留天数
PushStatisticsClearTim 每日推送数据删除的时间点
Order 分组优先级,元素=类型#组名,优先级只针对同一类型下有效,没配置优先级的分组不可用
ActiveTime 默认活跃时间(24小时制),过滤粉丝是否在活跃时间段内;未配置则不过滤;若希望过滤活跃时间但不提供默认活跃时间,配置成[0]
ForbidTimes 固定免推送时间段组
Proportions 灰度策略粉丝尾号100内, 起始点+step
FanGroup 分组具体信息
*/
// arcPush 稿件更新的推送设置
type arcPush struct {
PushStatisticsKeepDays int
PushStatisticsClearTime string
Order []string
ActiveTime []int
ForbidTimes []ForbidTime
Proportions []Proportion
FanGroup []*fanGroup
UpperLimitExpire xtime.Duration
}
// ForbidTime 禁止时间范围
type ForbidTime struct {
PushForbidStartTime string
PushForbidEndTime string
}
// Proportion 灰度uid范围
type Proportion struct {
ProportionStartFrom string
Proportion string //必须是2位小数比如:1.00, 0.05
}
// fanGroup 关注up主的粉丝分组
type fanGroup struct {
Name string
Desc string
RelationType int
Hitby string //命中分组规则,default=全部命中hbase=hbase表过滤
Limit int
PerUpperLimit int
LimitExpire xtime.Duration
HBaseTable string
HBaseFamily []string
MsgTemplateDesc string
MsgTemplate string
}
type wechat struct {
UserName, Token, Secret string
}
type push struct {
ProdSwitch bool
AddAPI string
MultiAPI string
BusinessID int
BusinessToken string
BusinessSpecialID int
BusinessSpecialToken string
LoadSettingsInterval xtime.Duration
}
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,71 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"d_test.go",
"hbase_test.go",
"mysql_test.go",
"redis_test.go",
"statisitcs_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"Proportion.go",
"dao.go",
"fan_group.go",
"hbase.go",
"message.go",
"mysql.go",
"push.go",
"redis.go",
"statistics.go",
],
importpath = "go-common/app/interface/main/push-archive/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/hbase.v2:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/xstr:go_default_library",
"//vendor/github.com/tsuna/gohbase/hrpc: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,82 @@
package dao
import (
"strconv"
"strings"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
)
// Proportion 普通关注粉丝的灰度比例
type Proportion struct {
// 粉丝的后2位的最值
MinValue int
MaxValue int
}
// NewProportion new
func NewProportion(config []conf.Proportion) (ps []Proportion) {
var ppt float64
for _, g := range config {
valueStartFloat, err := strconv.ParseFloat(strings.TrimSpace(g.ProportionStartFrom), 64)
if err != nil {
log.Error("NewProportions config ArcPush.FanGroup.ProportionStartFrom strconv.ParseFloat(%s) error(%v)", g.ProportionStartFrom, err)
return
}
valueStartFrom := int(valueStartFloat)
// 比例验证
prop, err := strconv.ParseFloat(strings.TrimSpace(g.Proportion), 64)
if err != nil {
log.Error("NewProportions config ArcPush.FanGroup.Proportion(%s) strconv.ParseFloat err(%v)", g.Proportion, err)
return nil
}
if prop*100-float64(int(prop*100)) != 0 {
// 比例最多保留2位小数
log.Error("NewProportions config ArcPush.FanGroup.Proportion(%s) must keep at most 2 bits", g.Proportion)
return nil
}
ppt += prop
if prop <= 0 || prop > 1 || ppt > 1 || ppt <= 0 {
// 单个数在(0,1]区间,总和在(0, 1]区间
log.Error("NewProportions config ArcPush.FanGroup.Proportion(%s) must in (0, 1] and sum(%f) in (0, 1]", g.Proportion, ppt)
return nil
}
// 起始值和比例之和必须在0099之间
maxValue := int(100*prop-1) + valueStartFrom
if maxValue >= 100 {
log.Error("NewProportions config ArcPush.FanGroup.Proportion(%s)+ProportionStartFrom must in [0, 99]", g.Proportion)
return
}
p := Proportion{
MinValue: valueStartFrom,
MaxValue: maxValue,
}
ps = append(ps, p)
}
return
}
// FansByProportion 根据比例分配该关注类型的粉丝, 以全站所有用户作为分母
func (d *Dao) FansByProportion(upper int64, fans map[int64]int) (attentions []int64, specials []int64) {
for mid, relationType := range fans {
if relationType == model.RelationSpecial {
specials = append(specials, mid)
continue
}
if len(d.Proportions) == 0 {
attentions = append(attentions, mid)
continue
}
// mid最后2位是否在抽样区间内
last2Digits := int(mid % 100)
for _, g := range d.Proportions {
if last2Digits >= g.MinValue && last2Digits <= g.MaxValue {
attentions = append(attentions, mid)
break
}
}
}
return
}

View File

@@ -0,0 +1,83 @@
package dao
import (
"encoding/hex"
"flag"
"os"
"strconv"
"strings"
"testing"
"go-common/app/interface/main/push-archive/conf"
"github.com/smartystreets/goconvey/convey"
)
var d *Dao
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.archive.push-archive")
flag.Set("conf_token", "61c0d7d8527e8a4aad5b49826869e23c")
flag.Set("tree_id", "7615")
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/push-archive-test.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
os.Exit(m.Run())
}
func Test_msgTemplateEncode(t *testing.T) {
convey.Convey("msgTemplateDesc编码", t, func() {
for _, g := range d.FanGroups {
ascii := strconv.QuoteToASCII(g.MsgTemplateDesc)
msgtemp := hex.EncodeToString([]byte(ascii))
t.Logf("the group(%s) msgtemplate encoded(%v)\n", g.Name, msgtemp)
ascii = strconv.QuoteToASCII(g.MsgTemplate)
msgtemp2 := hex.EncodeToString([]byte(ascii))
convey.So(msgtemp, convey.ShouldEqual, msgtemp2)
}
})
}
func Test_msgTemplateDecode(t *testing.T) {
convey.Convey("msgTemplateDesc解码", t, func() {
for _, g := range d.FanGroups {
convey.So(g.MsgTemplate, convey.ShouldEqual, g.MsgTemplateDesc)
}
})
}
func Test_keyname(t *testing.T) {
convey.Convey("fangroup keyname", t, func() {
for gkey, g := range d.FanGroups {
convey.So(gkey, convey.ShouldEqual, fanGroupKey(g.RelationType, g.Name))
}
})
}
func Test_conf(t *testing.T) {
convey.Convey("配置结果", t, func() {
for gkey, g := range d.FanGroups {
convey.So(gkey, convey.ShouldEqual, fanGroupKey(g.RelationType, g.Name))
convey.So(len(strings.Split(g.MsgTemplateDesc, "\r\n")), convey.ShouldEqual, 2)
convey.So(g.MsgTemplate, convey.ShouldEqual, g.MsgTemplateDesc)
}
for i, g := range d.Proportions {
proportion, _ := strconv.ParseFloat(d.c.ArcPush.Proportions[i].Proportion, 64)
convey.So(g.MaxValue-g.MinValue+1, convey.ShouldEqual, proportion*100)
}
convey.So(len(d.GroupOrder), convey.ShouldEqual, len(d.c.ArcPush.Order))
})
}

View File

@@ -0,0 +1,177 @@
package dao
import (
"context"
"fmt"
"os"
"time"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
xredis "go-common/library/cache/redis"
xsql "go-common/library/database/sql"
"go-common/library/log"
xhttp "go-common/library/net/http/blademaster"
"go-common/library/stat/prom"
"go-common/library/database/hbase.v2"
)
// Dao .
type Dao struct {
c *conf.Config
db *xsql.DB
redis *xredis.Pool
relationHBase *hbase.Client
relationHBaseReadTimeout time.Duration
relationHBaseWriteTimeout time.Duration
fanHBase *hbase.Client
fanHBaseReadTimeout time.Duration
httpClient *xhttp.Client
settingStmt *xsql.Stmt
setSettingStmt *xsql.Stmt
settingsMaxIDStmt *xsql.Stmt
setStatisticsStmt *xsql.Stmt
UpperLimitExpire int32
FanGroups map[string]*FanGroup
GroupOrder []string
Proportions []Proportion
ActiveDefaultTime map[int]int
PushBusinessID string
PushAuth string
}
var (
errorsCount = prom.BusinessErrCount
infosCount = prom.BusinessInfoCount
)
// New creates a push-service DAO instance.
func New(c *conf.Config) *Dao {
d := &Dao{
c: c,
db: xsql.NewMySQL(c.MySQL),
relationHBase: hbase.NewClient(&c.HBase.Config),
relationHBaseReadTimeout: time.Duration(c.HBase.ReadTimeout),
relationHBaseWriteTimeout: time.Duration(c.HBase.WriteTimeout),
fanHBase: hbase.NewClient(&c.FansHBase.Config),
fanHBaseReadTimeout: time.Duration(c.FansHBase.ReadTimeout),
redis: xredis.NewPool(c.Redis),
httpClient: xhttp.NewClient(c.HTTPClient),
UpperLimitExpire: int32(time.Duration(c.ArcPush.UpperLimitExpire) / time.Second),
FanGroups: NewFanGroups(c),
Proportions: NewProportion(c.ArcPush.Proportions),
}
d.settingStmt = d.db.Prepared(_settingSQL)
d.setSettingStmt = d.db.Prepared(_setSettingSQL)
d.settingsMaxIDStmt = d.db.Prepared(_settingsMaxIDSQL)
d.setStatisticsStmt = d.db.Prepared(_inStatisticsSQL)
for _, gp := range c.ArcPush.Order {
if _, exist := d.FanGroups[gp]; !exist {
log.Error("order config error, group %s not exist", gp)
fmt.Printf("order config error, group %s not exist\r\n\r\n", gp)
os.Exit(1)
}
}
d.GroupOrder = c.ArcPush.Order
// default active time
d.ActiveDefaultTime = map[int]int{}
for _, one := range c.ArcPush.ActiveTime {
d.ActiveDefaultTime[one] = 1
}
return d
}
// PromError prom error
func PromError(name string) {
errorsCount.Incr(name)
}
// PromInfo add prom info
func PromInfo(name string) {
infosCount.Incr(name)
}
// PromInfoAdd add prom info by value
func PromInfoAdd(name string, value int64) {
infosCount.Add(name, value)
}
// PromChanLen channel length
func PromChanLen(name string, length int64) {
infosCount.State(name, length)
}
// BeginTx begin transaction.
func (d *Dao) BeginTx(c context.Context) (*xsql.Tx, error) {
return d.db.Begin(c)
}
// Close dao.
func (d *Dao) Close() (err error) {
if err = d.relationHBase.Close(); err != nil {
log.Error("d.relationHBase.Close() error(%v)", err)
PromError("hbase:close")
}
if err = d.fanHBase.Close(); err != nil {
log.Error("d.fanHBase.Close() error(%v)", err)
PromError("fanHBase:close")
}
if err = d.redis.Close(); err != nil {
log.Error("d.redis.Close() error(%v)", err)
PromError("redis:close")
}
if err = d.db.Close(); err != nil {
log.Error("d.db.Close() error(%v)", err)
PromError("db:close")
}
return
}
// Ping check connection status.
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.db.Ping(c); err != nil {
PromError("mysql:Ping")
log.Error("d.db.Ping error(%v)", err)
return
}
if err = d.pingRedis(c); err != nil {
PromError("redis:Ping")
log.Error("d.redis.Ping error(%v)", err)
}
return
}
// Batch 批量处理
func Batch(list *[]int64, batchSize int, retry int, params *model.BatchParam, f func(fans *[]int64, params map[string]interface{}) error) {
if params == nil {
log.Warn("Batch params(%+v) nil", params)
return
}
for {
var (
mids []int64
err error
)
l := len(*list)
if l == 0 {
break
} else if l <= batchSize {
mids = (*list)[:l]
} else {
mids = (*list)[:batchSize]
l = batchSize
}
*list = (*list)[l:]
params.Handler(&params.Params, mids)
for i := 0; i < retry; i++ {
if err = f(&mids, params.Params); err == nil {
break
}
}
if err != nil {
log.Error("Batch error(%v), params(%+v)", err, params)
}
}
}

View File

@@ -0,0 +1,182 @@
package dao
import (
"bytes"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"time"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
)
// FanGroup 粉丝分组
type FanGroup struct {
// 组名
Name string
// 粉丝与up主的关注关系
RelationType int
Hitby string
// 限制条数
Limit int
PerUpperLimit int
LimitExpire int32
// 本组获取粉丝的hbase信息
HBaseTable string
HBaseFamily []string
MsgTemplateDesc string
MsgTemplate string
}
func fanGroupKey(relationType int, name string) string {
return fmt.Sprintf(`%d#%s`, relationType, name)
}
// NewFanGroups 实例化,验证配置, 若配置错误则panic
func NewFanGroups(config *conf.Config) (grp map[string]*FanGroup) {
grp = make(map[string]*FanGroup)
for _, g := range config.ArcPush.FanGroup {
if g.Name == "" {
log.Error("NewFanGroups config ArcPush.FanGroup.Name/hitby must not be empty")
break
}
// 粉丝和up主的关系配置验证
if g.RelationType != model.RelationAttention && g.RelationType != model.RelationSpecial {
log.Error("NewFanGroups config ArcPush.FanGroup.RelationType not exist(%d)", g.RelationType)
break
}
if g.Hitby != model.GroupDataTypeDefault && g.Hitby != model.GroupDataTypeHBase &&
g.Hitby != model.GroupDataTypeAbtest && g.Hitby != model.GroupDataTypeAbComparison {
log.Error("NewFanGroups config ArcPush.FanGroup.hitby(%s) must in [default,hbase]", g.Hitby)
break
}
key := fanGroupKey(g.RelationType, g.Name)
if _, ok := grp[key]; ok {
log.Error("NewFanGroups config ArcPush.FanGroup.relationtype(%d) and name(%s) must be unique", g.RelationType, g.Name)
break
}
// hbase配置
if g.HBaseTable != "" && len(g.HBaseFamily) == 0 {
log.Error("NewFanGroups config ArcPush.FanGroup.HbaseTable(%s) & HbaseFamily(%v) must exist togather", g.HBaseTable, g.HBaseFamily)
break
}
msgTemp, err := decodeMsgTemplate(g.Name, g.MsgTemplate)
if err != nil {
log.Error("NewFanGroups config ArcPush.FanGroup.MsgTemplate(%s) decodeMsgTemplate error(%v)", g.MsgTemplate, err)
break
}
if msgTemp != g.MsgTemplateDesc {
log.Error("NewFanGroups config ArcPush.FanGroup.MsgTemplate decodeMsgTemplate(%s) must equal to MsgTemplateDesc(%s)", msgTemp, g.MsgTemplateDesc)
break
}
if len(strings.SplitN(msgTemp, "\r\n", 2)) != 2 {
log.Error("NewFanGroups config ArcPush.FanGroup.MsgTemplate(%s) decodeMsgTemplate(%s) must contains `\r\n`", g.MsgTemplate, msgTemp)
break
}
grp[key] = &FanGroup{
Name: strings.TrimSpace(g.Name),
RelationType: g.RelationType,
Hitby: strings.TrimSpace(g.Hitby),
Limit: g.Limit,
PerUpperLimit: g.PerUpperLimit,
LimitExpire: int32(time.Duration(g.LimitExpire) / time.Second),
HBaseTable: strings.TrimSpace(g.HBaseTable),
HBaseFamily: g.HBaseFamily,
MsgTemplateDesc: g.MsgTemplateDesc,
MsgTemplate: msgTemp,
}
}
if len(grp) < len(config.ArcPush.FanGroup) {
fmt.Printf("NewFanGroups failed\r\n\r\n")
os.Exit(1)
}
return
}
// decodeMsgTemplate 将ascii格式的文案模版解码成中文格式---防止某些服务器不支持中文配置
func decodeMsgTemplate(groupName string, temp string) (decode string, err error) {
if temp == "" {
return
}
b, err := hex.DecodeString(temp)
if err != nil {
log.Error("DecodeMsgTemplate hex.DecodeString error(%v) groupName(%s), temp(%s)", err, groupName, temp)
return
}
buf := new(bytes.Buffer)
temp = string(b)
rows := strings.Split(temp[1:len(temp)-1], "\\r\\n")
lenRows := len(rows) - 1
for k, row := range rows {
parts := strings.Split(row, "%s")
lenParts := len(parts) - 1
for kp, str := range parts {
words := strings.Split(str, "\\u")
for _, w := range words {
if len(w) < 1 {
continue
}
wi, err := strconv.ParseInt(w, 16, 32)
if err != nil {
log.Error("DecodeMsgTemplate error(%v) groupName(%s), decode(%s), word(%s)", err, groupName, temp, w)
return "", err
}
buf.WriteString(fmt.Sprintf("%c", wi))
}
if kp >= lenParts {
continue
}
buf.WriteString("%s")
}
if k >= lenRows {
continue
}
buf.WriteString("\r\n")
}
decode = buf.String()
return
}
// FansByHBase hbase表中查询粉丝所关联的up主过滤up不在hbase结果中的粉丝
func (d *Dao) FansByHBase(upper int64, fanGroupKey string, fans *[]int64) (result []int64, excluded []int64) {
g := d.FanGroups[fanGroupKey]
// 不过滤
if len(g.HBaseTable) == 0 {
result = *fans
return
}
params := model.NewBatchParam(map[string]interface{}{
"base": upper,
"table": g.HBaseTable,
"family": g.HBaseFamily,
"result": &result,
"excluded": &excluded,
"handler": d.filterFanByUpper,
}, nil)
Batch(fans, 100, 1, params, d.FilterFans)
return
}
// FansByActiveTime 配置了默认活跃时间,则批量过滤粉丝是否在活跃时间段内,否则不推送;未配置则不过滤活跃时间;若希望没有默认活跃时间但希望过滤活跃时间,配置成[0]
func (d *Dao) FansByActiveTime(hour int, fans *[]int64) (result []int64, excluded []int64) {
// 未配置则不过滤活跃时间
if len(d.ActiveDefaultTime) <= 0 {
result = *fans
excluded = []int64{}
return
}
params := model.NewBatchParam(map[string]interface{}{
"base": hour,
"table": "dm_member_push_active_hour",
"family": []string{"p"},
"result": &result,
"excluded": &excluded,
"handler": d.filterFanByActive,
}, nil)
Batch(fans, 100, 1, params, d.FilterFans)
return
}

View File

@@ -0,0 +1,313 @@
package dao
import (
"bytes"
"context"
"crypto/md5"
"encoding/binary"
"encoding/json"
"fmt"
"strconv"
"sync"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
"go-common/library/sync/errgroup"
"github.com/tsuna/gohbase/hrpc"
)
const _hbaseShard = 200
var (
hbaseTable = "ugc:PushArchive"
hbaseFamily = "relation"
hbaseFamilyB = []byte(hbaseFamily)
)
func _rowKey(upper, fans int64) string {
k := fmt.Sprintf("%d_%d", upper, fans%_hbaseShard)
key := fmt.Sprintf("%x", md5.Sum([]byte(k)))
return key
}
// Fans gets the upper's fans.
func (d *Dao) Fans(c context.Context, upper int64, isPGC bool) (res map[int64]int, err error) {
var mutex sync.Mutex
res = make(map[int64]int)
group := errgroup.Group{}
for i := 0; i < _hbaseShard; i++ {
shard := int64(i)
group.Go(func() (e error) {
key := _rowKey(upper, shard)
relations, e := d.fansByKey(context.TODO(), key)
if e != nil {
return
}
mutex.Lock()
for fans, tp := range relations {
// pgc稿件屏蔽非特殊关注粉丝
if isPGC && tp != model.RelationSpecial {
continue
}
res[fans] = tp
}
mutex.Unlock()
return
})
}
group.Wait()
return
}
// AddFans add upper's fans.
func (d *Dao) AddFans(c context.Context, upper, fans int64, tp int) (err error) {
key := _rowKey(upper, fans)
relations, err := d.fansByKey(c, key)
if err != nil {
return
}
relations[fans] = tp
err = d.saveRelation(c, key, upper, relations)
return
}
// DelFans del fans.
func (d *Dao) DelFans(c context.Context, upper, fans int64) (err error) {
key := _rowKey(upper, fans)
relations, err := d.fansByKey(c, key)
if err != nil {
return
}
delete(relations, fans)
err = d.saveRelation(c, key, upper, relations)
return
}
// DelSpecialAttention del special attention.
func (d *Dao) DelSpecialAttention(c context.Context, upper, fans int64) (err error) {
key := _rowKey(upper, fans)
relations, err := d.fansByKey(c, key)
if err != nil {
return
}
if relations[fans] != model.RelationSpecial {
return
}
relations[fans] = model.RelationAttention
err = d.saveRelation(c, key, upper, relations)
return
}
func (d *Dao) fansByKey(c context.Context, key string) (relations map[int64]int, err error) {
var (
result *hrpc.Result
ctx, cancel = context.WithTimeout(c, d.relationHBaseReadTimeout)
)
defer cancel()
relations = make(map[int64]int)
if result, err = d.relationHBase.Get(ctx, []byte(hbaseTable), []byte(key)); err != nil {
log.Error("d.relationHBase.Get error(%v) querytable(%v)", err, hbaseTable)
PromError("hbase:Get")
return
} else if result == nil {
return
}
for _, c := range result.Cells {
if c != nil && bytes.Equal(c.Family, hbaseFamilyB) {
if err = json.Unmarshal(c.Value, &relations); err != nil {
log.Error("json.Unmarshal() error(%v)", err)
return
}
break
}
}
return
}
func (d *Dao) saveRelation(c context.Context, key string, upper int64, relations map[int64]int) (err error) {
var (
column = strconv.FormatInt(upper, 10)
ctx, cancel = context.WithTimeout(c, d.relationHBaseWriteTimeout)
)
defer cancel()
value, err := json.Marshal(relations)
if err != nil {
return
}
values := map[string]map[string][]byte{hbaseFamily: {column: value}}
if _, err = d.relationHBase.PutStr(ctx, hbaseTable, key, values); err != nil {
log.Error("d.relationHBase.PutStr error(%v), table(%s), values(%+v)", err, hbaseTable, values)
PromError("hbase:Put")
}
return
}
// filterFanByUpper 根据fans在hbase存储的up主列表筛选出upper主在up主列表中的粉丝
func (d *Dao) filterFanByUpper(c context.Context, fan int64, up interface{}, table string, family []string) (included bool, err error) {
var (
res *hrpc.Result
key string
ctx, cancel = context.WithTimeout(c, d.fanHBaseReadTimeout)
)
defer cancel()
upper := up.(int64)
rowKeyMD := md5.Sum([]byte(strconv.FormatInt(fan, 10)))
key = fmt.Sprintf("%x", rowKeyMD)
if res, err = d.fanHBase.Get(ctx, []byte(table), []byte(key)); err != nil {
log.Error("d.fanHBase.Get error(%v) querytable(%v) key(%s), fan(%d), upper(%d)", err, table, key, fan, upper)
PromError("hbase:Get")
return
} else if res == nil {
return
}
for _, c := range res.Cells {
if c == nil || !existFamily(c.Family, family) {
continue
}
upID := int64(binary.BigEndian.Uint32(c.Value))
if upID != upper || upID <= 0 {
continue
}
included = true
log.Info("filter fan: included by hbase, fan(%d) upper(%d) table(%s)", fan, upper, table)
return
}
if !included {
log.Info("filter fan: excluded by hbase, fan(%d) upper(%d) table(%s)", fan, upper, table)
}
return
}
// FilterFans 批量筛选
func (d *Dao) FilterFans(fans *[]int64, params map[string]interface{}) (err error) {
base := params["base"]
table := params["table"].(string)
family := params["family"].([]string)
result := params["result"].(*[]int64)
excluded := params["excluded"].(*[]int64)
handler := params["handler"].(func(context.Context, int64, interface{}, string, []string) (bool, error))
mutex := sync.Mutex{}
group := errgroup.Group{}
l := len(*fans)
for i := 0; i < l; i++ {
shared := (*fans)[i]
group.Go(func() (e error) {
included, e := handler(context.TODO(), shared, base, table, family)
if e != nil {
log.Error("FilterFans error(%v) fan(%d) base(%d) table(%s) family(%v)", e, shared, base, table, family)
}
mutex.Lock()
if included {
*result = append(*result, shared)
} else {
*excluded = append(*excluded, shared)
}
mutex.Unlock()
return
})
}
group.Wait()
return
}
// existFamily 某个hbase列族是否存在于指定列族中
func existFamily(actual []byte, family []string) bool {
for _, f := range family {
if bytes.Equal(actual, []byte(f)) {
return true
}
}
return false
}
// filterFanByActive 根据用户的活跃时间段,过滤不在活跃期内更新的粉丝; 若无活跃列表,从默认活跃时间内过滤
func (d *Dao) filterFanByActive(ctx context.Context, fan int64, oneHour interface{}, table string, family []string) (included bool, err error) {
var (
b []byte
result *hrpc.Result
c, cancel = context.WithTimeout(ctx, d.fanHBaseReadTimeout)
activeHour int
)
defer cancel()
hour := oneHour.(int)
if _, included = d.ActiveDefaultTime[hour]; included {
return
}
rowKey := md5.Sum(strconv.AppendInt(b, fan, 10))
key := fmt.Sprintf("%x", rowKey)
if result, err = d.fanHBase.Get(c, []byte(table), []byte(key)); err != nil {
log.Error("filterFanByActive d.fanHBase.Get error(%v) table(%s) key(%s) fan(%d)", err, table, key, fan)
PromError("hbase:Get")
return
} else if result == nil {
return
}
included = false
for _, cell := range result.Cells {
if cell != nil && existFamily(cell.Family, family) {
activeHour, err = strconv.Atoi(string(cell.Value))
if err != nil {
log.Error("filterFanByActive strconv.Atoi error(%v) fan(%d) value(%s)", err, fan, string(cell.Value))
break
}
if activeHour == hour {
included = true
break
}
}
}
if !included {
log.Info("filter fanexcluded by active time from table, fan(%d)", fan)
}
return
}
// ExistsInBlacklist 按黑名单过滤用户
func (d *Dao) ExistsInBlacklist(ctx context.Context, upper int64, mids []int64) (exists, notExists []int64) {
var (
mutex sync.Mutex
group = errgroup.Group{}
)
for _, mid := range mids {
mid := mid
group.Go(func() error {
include, _ := d.filterFanByUpper(context.Background(), mid, upper, d.c.Abtest.HbaseBlacklistTable, d.c.Abtest.HbaseBlacklistFamily)
mutex.Lock()
if include {
exists = append(exists, mid)
} else {
notExists = append(notExists, mid)
}
mutex.Unlock()
return nil
})
}
group.Wait()
return
}
// ExistsInWhitelist 按白名单过滤用户
func (d *Dao) ExistsInWhitelist(ctx context.Context, upper int64, mids []int64) (exists, notExists []int64) {
var (
mutex sync.Mutex
group = errgroup.Group{}
)
for _, mid := range mids {
mid := mid
group.Go(func() error {
include, _ := d.filterFanByUpper(context.Background(), mid, upper, d.c.Abtest.HbaseeWhitelistTable, d.c.Abtest.HbaseWhitelistFamily)
mutex.Lock()
if include {
exists = append(exists, mid)
} else {
notExists = append(notExists, mid)
}
mutex.Unlock()
return nil
})
}
group.Wait()
return
}

View File

@@ -0,0 +1,119 @@
package dao
import (
"context"
"testing"
"go-common/app/interface/main/push-archive/model"
"github.com/smartystreets/goconvey/convey"
)
func Test_onekey(t *testing.T) {
var included bool
var err error
included, err = d.filterFanByUpper(context.TODO(), int64(12312313), int64(275152561), "ai:pushlist_follow_recent", []string{"m"})
convey.Convey("hbase过滤up主, 不存在", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(included, convey.ShouldEqual, false)
})
included, err = d.filterFanByUpper(context.TODO(), int64(27515303), int64(27515256), "ai:pushlist_follow_recent", []string{"m", "m1"})
convey.Convey("hbase过滤up主增加1个", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(included, convey.ShouldEqual, true)
})
included, err = d.filterFanByUpper(context.TODO(), int64(27515401), int64(27515256), "ai:pushlist_follow_recent", []string{"m"})
convey.Convey("hbase过滤up主增加1个", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(included, convey.ShouldEqual, true)
})
included, err = d.filterFanByUpper(context.TODO(), int64(27515300), int64(27515256), "ai:pushlist_follow_recent", []string{"m"})
convey.Convey("hbase过滤up主增加1个", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(included, convey.ShouldEqual, true)
})
}
func Test_keys(t *testing.T) {
var result, excluded []int64
params := map[string]interface{}{
"base": int64(27515256),
"table": "ai:pushlist_follow_recent",
"family": []string{"m"},
"result": &result,
"excluded": &excluded,
"handler": d.filterFanByUpper,
}
err := d.FilterFans(&[]int64{27515303, 27515401, 27515300, 12312313}, params)
convey.Convey("多协程过滤up主,3个符合1个排除", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(result), convey.ShouldEqual, 3)
convey.So(len(excluded), convey.ShouldEqual, 1)
})
}
func Test_batchfilter(t *testing.T) {
var result, excluded []int64
params := model.NewBatchParam(map[string]interface{}{
"base": int64(27515256),
"table": "ai:pushlist_follow_recent",
"family": []string{"m"},
"result": &result,
"excluded": &excluded,
"handler": d.filterFanByUpper,
}, nil)
Batch(&[]int64{27515303, 27515401, 27515300, 12312313}, 1, 2, params, d.FilterFans)
convey.Convey("批量过滤up主, ,3个符合1个排除", t, func() {
convey.So(len(result), convey.ShouldEqual, 3)
convey.So(len(excluded), convey.ShouldEqual, 1)
})
t.Logf("the result(%v), excluded(%v)", result, excluded)
}
func Test_addfans(t *testing.T) {
err := d.AddFans(context.TODO(), int64(275152561), int64(121212), model.RelationAttention)
convey.Convey("添加粉丝到up主", t, func() {
convey.So(err, convey.ShouldBeNil)
})
}
func Test_delfans(t *testing.T) {
err := d.DelFans(context.TODO(), int64(275152561), int64(121212))
convey.Convey("删除up主的粉丝", t, func() {
convey.So(err, convey.ShouldBeNil)
})
}
func Test_fansbyupper(t *testing.T) {
Test_addfans(t)
fans, err := d.Fans(context.TODO(), int64(275152561), false)
convey.Convey("up主增加一个粉丝后", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(fans), convey.ShouldEqual, 1)
})
fans, err = d.Fans(context.TODO(), int64(275152561), true)
convey.Convey("up主增加一个普通关注粉丝后, pgc稿件只有特殊关注粉丝", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(fans), convey.ShouldEqual, 0)
})
Test_delfans(t)
fans, err = d.Fans(context.TODO(), int64(275152561), false)
convey.Convey("up主删除一个粉丝后", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(fans), convey.ShouldEqual, 0)
})
}
func Test_fansbyactive(t *testing.T) {
// 18507659 + 37118721 + 88889069
fan := int64(88889069)
hour := 21
table := "dm_member_push_active_hour"
family := []string{"p"}
included, err := d.filterFanByActive(context.TODO(), fan, hour, table, family)
t.Logf("the included(%v) err(%v)", included, err)
}

View File

@@ -0,0 +1,81 @@
package dao
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"time"
"go-common/library/log"
)
// wechatResp 企业微信的响应
type wechatResp struct {
Msg string `json:"msg"`
Status int `json:"status"`
}
// WechatMessage 发送企业微信消息
func (d *Dao) WechatMessage(content string) (err error) {
params := map[string]string{
"content": content,
"timestamp": strconv.FormatInt(time.Now().Unix(), 10),
"title": "",
"token": d.c.Wechat.Token,
"type": "wechat",
"username": d.c.Wechat.UserName,
"url": "",
}
params["signature"] = d.signature(params, d.c.Wechat.Secret)
b, err := json.Marshal(params)
if err != nil {
log.Error("WechatMessage json.Marshal error(%v)", err)
return
}
req, err := http.NewRequest(http.MethodPost, "http://bap.bilibili.co/api/v1/message/add", bytes.NewReader(b))
if err != nil {
log.Error("WechatMessage NewRequest error(%v), params(%s)", err, string(b))
return
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
res := wechatResp{}
if err = d.httpClient.Do(context.TODO(), req, &res); err != nil {
log.Error("WechatMessage Do error(%v), params(%s)", err, string(b))
return
}
if res.Status != 0 {
err = fmt.Errorf("status(%d) msg(%s)", res.Status, res.Msg)
log.Error("WechatMessage response error(%v), params(%s)", err, string(b))
return
}
return
}
// signature 加密算法
func (d *Dao) signature(params map[string]string, secret string) string {
// content=xxx&timestamp=xxx格式
keys := []string{}
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
buf := bytes.Buffer{}
for _, k := range keys {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(url.QueryEscape(k) + "=")
buf.WriteString(url.QueryEscape(params[k]))
}
// 加密
h := md5.New()
io.WriteString(h, buf.String()+secret)
return fmt.Sprintf("%x", h.Sum(nil))
}

View File

@@ -0,0 +1,126 @@
package dao
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"go-common/library/xstr"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
"strconv"
)
const (
_settingSQL = `SELECT value FROM push_settings WHERE mid=? and dtime=0 limit 1`
_setSettingSQL = `INSERT INTO push_settings (mid,value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=?`
_settingsSQL = `SELECT mid,value FROM push_settings WHERE mid IN(%s) and dtime=0`
_settingsAllSQL = `SELECT mid,value FROM push_settings WHERE id > %s AND id <= %s`
_settingsMaxIDSQL = `SELECT MAX(id) AS mx FROM push_settings`
)
// Setting gets the setting.
func (d *Dao) Setting(c context.Context, mid int64) (st *model.Setting, err error) {
var v string
if err = d.settingStmt.QueryRow(c, mid).Scan(&v); err != nil {
if err == sql.ErrNoRows {
err = nil
return
}
log.Error("d.Setting(%d) error(%v)", mid, err)
PromError("db:获取用户配置")
return
}
st = new(model.Setting)
if err = json.Unmarshal([]byte(v), &st); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", v, err)
}
return
}
// SetSetting saves the setting.
func (d *Dao) SetSetting(c context.Context, mid int64, st *model.Setting) (err error) {
v, err := json.Marshal(st)
if err != nil {
log.Error("json.Marshal error(%v)", err)
return
}
if _, err = d.setSettingStmt.Exec(c, mid, v, v); err != nil {
log.Error("setSetting Exec mid(%d) error(%v)", mid, err)
PromError("db:保存用户设置")
}
return
}
// Settings gets the settings.
func (d *Dao) Settings(c context.Context, mids []int64) (res map[int64]*model.Setting, err error) {
res = make(map[int64]*model.Setting, len(mids))
rows, err := d.db.Query(c, fmt.Sprintf(_settingsSQL, xstr.JoinInts(mids)))
if err != nil {
log.Error("d.db.Query() error(%v)", err)
PromError("db:批量查询用户设置")
return
}
for rows.Next() {
var mid int64
var v string
if err = rows.Scan(&mid, &v); err != nil {
log.Error("rows.Scan() error(%v)", err)
PromError("db:批量查询用户设置")
return
}
st := new(model.Setting)
if err = json.Unmarshal([]byte(v), &st); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", v, err)
return
}
res[mid] = st
}
return
}
// SettingsAll gets all settings.
func (d *Dao) SettingsAll(c context.Context, startID int64, endID int64, res *map[int64]*model.Setting) (err error) {
start := strconv.FormatInt(startID, 10)
end := strconv.FormatInt(endID, 10)
rows, err := d.db.Query(c, fmt.Sprintf(_settingsAllSQL, start, end))
if err != nil {
log.Error("d.db.Query() error(%v)", err)
PromError("db:查询全部用户设置")
return
}
for rows.Next() {
var mid int64
var v string
if err = rows.Scan(&mid, &v); err != nil {
log.Error("rows.Scan() error(%v)", err)
PromError("db:查询用户设置")
return
}
st := new(model.Setting)
if err = json.Unmarshal([]byte(v), &st); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", v, err)
return
}
(*res)[mid] = st
}
return
}
//SettingsMaxID get settings' total number by max(id)
func (d *Dao) SettingsMaxID(c context.Context) (mx int64, err error) {
if err = d.settingsMaxIDStmt.QueryRow(c).Scan(&mx); err != nil {
if err == sql.ErrNoRows {
err = nil
return
}
log.Error("d.settingsMaxIDStmt.QueryRow.Scan error(%v)", err)
PromError("db:查询用户最大ID")
return
}
return
}

View File

@@ -0,0 +1,55 @@
package dao
import (
"context"
"encoding/json"
"testing"
"time"
"go-common/app/interface/main/push-archive/model"
"github.com/smartystreets/goconvey/convey"
)
func Test_mxID(t *testing.T) {
_, err := d.SettingsMaxID(context.TODO())
convey.Convey("获取最大的设置id", t, func() {
convey.So(err, convey.ShouldBeNil)
})
}
func Test_settingsall(t *testing.T) {
res := make(map[int64]*model.Setting)
start, end := int64(2), int64(3)
err := d.SettingsAll(context.TODO(), start, end, &res)
convey.Convey("batch search settings", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(res), convey.ShouldEqual, 0)
})
start, end = int64(0), int64(10)
err = d.SettingsAll(context.TODO(), start, end, &res)
convey.Convey("batch search settings", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(res), convey.ShouldBeGreaterThan, 0)
})
}
func Test_statistics(t *testing.T) {
fans := []int64{1, 2, 3, 4, 5}
b, err1 := json.Marshal(fans)
ps := model.PushStatistic{
Aid: int64(101),
Group: "ai:pushlist_follow_recent",
Type: model.StatisticsUnpush,
Mids: string(b),
MidsCounter: len(fans),
CTime: time.Now(),
}
rows, err := d.SetStatistics(context.TODO(), &ps)
convey.Convey("添加统计数据", t, func() {
convey.So(err1, convey.ShouldBeNil)
convey.So(err, convey.ShouldBeNil)
convey.So(rows, convey.ShouldEqual, 1)
})
}

View File

@@ -0,0 +1,83 @@
package dao
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
"go-common/library/xstr"
)
type _response struct {
Code int `json:"code"`
Data int `json:"data"`
}
// NoticeFans pushs the notification to fans.
func (d *Dao) NoticeFans(fans *[]int64, params map[string]interface{}) (err error) {
arc := params["archive"].(*model.Archive)
group := strings.TrimSpace(params["group"].(string))
msgTemplate := params["msgTemplate"].(string)
uuid := params["uuid"].(string)
relationType := params["relationType"].(int)
author := "UP主"
if arc.Author != "" {
author = fmt.Sprintf(`“%s”`, arc.Author)
}
// 普通关注和特殊关注用不同的业务组推
businessID := d.c.Push.BusinessID
businessToken := d.c.Push.BusinessToken
if relationType == model.RelationSpecial {
businessID = d.c.Push.BusinessSpecialID
businessToken = d.c.Push.BusinessSpecialToken
}
msg := fmt.Sprintf(msgTemplate, author, arc.Title)
sp := strings.SplitN(msg, "\r\n", 2)
buf := new(bytes.Buffer)
w := multipart.NewWriter(buf)
w.WriteField("group", group) // 实验组名,值为实验组数据表名
w.WriteField("app_id", "1") // 1表示哔哩哔哩动画
w.WriteField("business_id", strconv.Itoa(businessID))
w.WriteField("alert_title", sp[0])
w.WriteField("alert_body", sp[1])
w.WriteField("mids", xstr.JoinInts(*fans))
w.WriteField("link_type", "2") // 2代表视频稿件播放页
w.WriteField("link_value", strconv.FormatInt(arc.ID, 10))
w.WriteField("uuid", uuid)
// 1、v5.20.0 后客户端才接特殊关注 2、 iPad版本没更新不推
w.WriteField("builds", `{"2":{"Build":6500,"Condition":"gte"}, "3":{"Build":0,"Condition":"lt"}, "1":{"Build":519010,"Condition":"gte"}}`)
w.Close()
query := map[string]string{
"ts": strconv.FormatInt(time.Now().Unix(), 10),
"appkey": d.c.HTTPClient.Key,
}
query["sign"] = d.signature(query, d.c.HTTPClient.Secret)
url := fmt.Sprintf("%s?ts=%s&appkey=%s&sign=%s", d.c.Push.AddAPI, query["ts"], query["appkey"], query["sign"])
req, err := http.NewRequest(http.MethodPost, url, buf)
if err != nil {
log.Error("http.NewRequest(%s) error(%v) uuid(%s)", url, err, uuid)
PromError("http:NewRequest")
return
}
req.Header.Set("Content-Type", w.FormDataContentType())
req.Header.Set("Authorization", fmt.Sprintf("token=%s", businessToken))
res := &_response{}
if err = d.httpClient.Do(context.TODO(), req, &res); err != nil {
log.Error("httpClient.Do() error(%v)", err)
PromError("http:Do")
return
}
if res.Code != 0 || res.Data == 0 {
log.Error("push failed archive(%d) upper(%d) fans_total(%d) group(%s) response(%+v)", arc.ID, arc.Mid, len(*fans), group, res)
} else {
log.Info("push success archive(%d) upper(%d) fans_total(%d) group(%s) response(%+v)", arc.ID, arc.Mid, len(*fans), group, res)
}
return
}

View File

@@ -0,0 +1,162 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"go-common/app/interface/main/push-archive/model"
"go-common/library/cache/redis"
"go-common/library/log"
)
const (
_prefixUpperLimit = "pau_%d"
_prefixFanLimit = "paf_%d"
_statisticsKey = "statistics_push_archive"
_prefixPerUpperLimit = "perup_%d_%d"
)
func (d *Dao) do(c context.Context, command string, key string, args ...interface{}) (reply interface{}, err error) {
conn := d.redis.Get(c)
defer conn.Close()
values := []interface{}{key}
if len(args) > 0 {
values = append(values, args...)
}
reply, err = conn.Do(command, values...)
return
}
func upperLimitKey(mid int64) string {
return fmt.Sprintf(_prefixUpperLimit, mid)
}
// pingRedis ping redis.
func (d *Dao) pingRedis(c context.Context) (err error) {
if _, err = d.do(c, "SET", "PING", "PONG"); err != nil {
PromError("redis: ping remote")
log.Error("remote redis: conn.Do(SET,PING,PONG) error(%v)", err)
}
return
}
// ExistUpperLimitCache judge that whether upper push limit cache exists.
func (d *Dao) ExistUpperLimitCache(c context.Context, upper int64) (exist bool, err error) {
key := upperLimitKey(upper)
if exist, err = redis.Bool(d.do(c, "EXISTS", key)); err != nil {
PromError("redis:读取upper推送限制")
log.Error("ExistUpperLimitCache do(EXISTS, %s) error(%v)", key, err)
}
return
}
// AddUpperLimitCache sets upper push limit cache.
func (d *Dao) AddUpperLimitCache(c context.Context, upper int64) (err error) {
key := upperLimitKey(upper)
if _, err = d.do(c, "SETEX", key, d.UpperLimitExpire, ""); err != nil {
PromError("redis:添加upper推送限制")
log.Error("AddUpperLimitCache do(SETEX, %s) error(%v)", key, err)
}
return
}
//fanLimitKey 粉丝推送总次数限制key
func fanLimitKey(fan int64, relationType int) string {
key := fmt.Sprintf(_prefixFanLimit, fan)
if relationType != model.RelationSpecial {
key = fmt.Sprintf("%s_%d", key, relationType)
}
return key
}
//GetFanLimitCache 读取粉丝限制的当前值
func (d *Dao) GetFanLimitCache(c context.Context, fan int64, relationType int) (limit int, err error) {
key := fanLimitKey(fan, relationType)
if limit, err = redis.Int(d.do(c, "GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Error("GetFanLimitCache do(GET) error(%v)", err)
}
}
return
}
//AddFanLimitCache 添加粉丝限制的缓存
func (d *Dao) AddFanLimitCache(c context.Context, fan int64, relationType int, value int, expire int32) (err error) {
key := fanLimitKey(fan, relationType)
if _, err = d.do(c, "SETEX", key, expire, value); err != nil {
log.Error("AddFanLimitCache do(SETEX) error(%v)", err)
PromError("redis:添加fan推送限制")
}
return
}
//AddStatisticsCache 添加统计数据到redis
func (d *Dao) AddStatisticsCache(c context.Context, ps *model.PushStatistic) (err error) {
psByte, err := json.Marshal(*ps)
if err != nil {
log.Error("AddStatisticsCache json.Marshal error(%v), pushstatistic(%v)", err, ps)
return
}
key := _statisticsKey
if _, err = d.do(c, "LPUSH", key, string(psByte)); err != nil {
log.Error("AddStatisticsCache do(LPUSH, %s) error(%v) pushstatistic(%v)", key, err, ps)
PromError("redis:添加统计数据")
}
return
}
//GetStatisticsCache 读取一条统计数据
func (d *Dao) GetStatisticsCache(c context.Context) (ps *model.PushStatistic, err error) {
key := _statisticsKey
psStr, err := redis.String(d.do(c, "RPOP", key))
if err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Error("GetStatisticsCache do(RPOP, %s) error(%v)", key, err)
}
return
}
if err = json.Unmarshal([]byte(psStr), &ps); err != nil {
log.Error("GetStatisticsCache json.Unmarshal error(%v), ps(%s)", err, psStr)
return
}
return
}
//perUpperLimitKey 粉丝每个upper主的推送次数限制key
func perUpperLimitKey(fan int64, upper int64) string {
return fmt.Sprintf(_prefixPerUpperLimit, fan, upper)
}
//GetPerUpperLimitCache 粉丝每个upper主的已推送次数
func (d *Dao) GetPerUpperLimitCache(c context.Context, fan int64, upper int64) (limit int, err error) {
key := perUpperLimitKey(fan, upper)
if limit, err = redis.Int(d.do(c, "GET", key)); err != nil {
if err == redis.ErrNil {
err = nil
} else {
log.Error("GetPerUpperLimitCache do(GET, %s) error(%v)", key, err)
}
}
return
}
//AddPerUpperLimitCache 添加粉丝每个up主的推送次数
func (d *Dao) AddPerUpperLimitCache(c context.Context, fan int64, upper int64, value int, expire int32) (err error) {
key := perUpperLimitKey(fan, upper)
if _, err = d.do(c, "SETEX", key, expire, value); err != nil {
log.Error("AddPerUpperLimitCache do(SETEX, %s, %d, %d) error(%v)", key, expire, value, err)
PromError("redis:添加perupper推送限制")
}
return
}

View File

@@ -0,0 +1,84 @@
package dao
import (
"context"
"encoding/json"
"testing"
"time"
"go-common/app/interface/main/push-archive/model"
"github.com/smartystreets/goconvey/convey"
)
func Test_upperlimit(t *testing.T) {
upper := int64(998)
d.UpperLimitExpire = 1 // 1s
exist, err := d.ExistUpperLimitCache(context.TODO(), upper)
convey.Convey("upper主推送频率限制没存储过", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(exist, convey.ShouldEqual, false)
})
err = d.AddUpperLimitCache(context.TODO(), upper)
convey.Convey("upper主推送频率限制,添加推送1次再次获取已存在, 失效后不存在", t, func() {
convey.So(err, convey.ShouldBeNil)
exist, err = d.ExistUpperLimitCache(context.TODO(), upper)
convey.So(err, convey.ShouldBeNil)
convey.So(exist, convey.ShouldEqual, true)
time.Sleep(2 * time.Second)
exist, err = d.ExistUpperLimitCache(context.TODO(), upper)
convey.So(err, convey.ShouldBeNil)
convey.So(exist, convey.ShouldEqual, false)
})
}
func Test_statisticscache(t *testing.T) {
ps, err := d.GetStatisticsCache(context.TODO())
convey.Convey("从redis获取统计数据, 没有数据", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(ps, convey.ShouldBeNil)
})
per := int64(1000)
start := int64(1000000)
var mids []int64
for i := start; i < start+per; i++ {
mids = append(mids, i)
}
midscount := len(mids)
midsstr, _ := json.Marshal(mids)
ps = &model.PushStatistic{
Aid: int64(121321),
Group: "ai:pushlist_offline_up",
Type: 1,
Mids: string(midsstr),
MidsCounter: midscount,
CTime: time.Now(),
}
err = d.AddStatisticsCache(context.TODO(), ps)
convey.Convey("添加统计数据到redis", t, func() {
convey.So(err, convey.ShouldBeNil)
})
}
func Test_perupperlimit(t *testing.T) {
upper := int64(10)
fan := int64(20)
err := d.AddPerUpperLimitCache(context.TODO(), fan, upper, 1, 1)
convey.Convey("添加推送次数限制", t, func() {
convey.So(err, convey.ShouldEqual, nil)
})
total, err := d.GetPerUpperLimitCache(context.TODO(), fan, upper)
convey.Convey("获取推送次数限制, 失效后不存在", t, func() {
convey.So(err, convey.ShouldEqual, nil)
convey.So(total, convey.ShouldEqual, 1)
time.Sleep(time.Second * 2)
total, err = d.GetPerUpperLimitCache(context.TODO(), fan, upper)
convey.So(err, convey.ShouldEqual, nil)
convey.So(total, convey.ShouldEqual, 0)
})
}

View File

@@ -0,0 +1,25 @@
package dao
import (
"context"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestDao_GetStatisticsIDRange(t *testing.T) {
Convey("GetStatisticsIDRange", t, func() {
deadline, _ := time.Parse("2006-01-02 15:04:05", "2018-05-01 00:00:00")
min, max, err := d.GetStatisticsIDRange(context.TODO(), deadline)
So(err, ShouldBeNil)
So(min, ShouldBeLessThanOrEqualTo, max)
})
}
func TestDao_DelStatisticsByID(t *testing.T) {
Convey("DelStatisticsByID", t, func() {
_, err := d.DelStatisticsByID(context.TODO(), 1, 10)
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,49 @@
package dao
import (
"context"
"time"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
)
const (
_inStatisticsSQL = "INSERT INTO `push_statistics` (`aid`, `group`, `type`, `mids`, `mids_counter`, `ctime`, `mtime`) VALUES(?,?,?,?,?,?,?);"
_statisticsIDRangeSQL = "SELECT coalesce(min(id), 0), coalesce(max(id) , 0) FROM `push_statistics` WHERE `ctime` < ?"
_delStatisticsByIDSQL = "DELETE FROM `push_statistics` WHERE `id` >=? AND `id`<=?;"
)
//SetStatistics 插入一条记录
func (d *Dao) SetStatistics(ctx context.Context, st *model.PushStatistic) (rows int64, err error) {
res, err := d.setStatisticsStmt.Exec(ctx, st.Aid, st.Group, st.Type, st.Mids, st.MidsCounter, st.CTime, time.Now())
if err != nil {
log.Error("SetStatistics() d.setStatisticsStmt.Exec error(%v), pushstatistic(%v)", err, st)
PromError("db:保存统计数据")
return
}
rows, err = res.RowsAffected()
return
}
//GetStatisticsIDRange get id range
func (d *Dao) GetStatisticsIDRange(ctx context.Context, deadline time.Time) (min int64, max int64, err error) {
if err = d.db.QueryRow(ctx, _statisticsIDRangeSQL, deadline).Scan(&min, &max); err != nil {
log.Error("GetStatisticsIDRange() error(%v), deadline(%v)", err, deadline)
PromError("db:查询统计数据")
}
return
}
//DelStatisticsByID delete by id range
func (d *Dao) DelStatisticsByID(ctx context.Context, min, max int64) (rows int64, err error) {
res, err := d.db.Exec(ctx, _delStatisticsByIDSQL, min, max)
if err != nil {
log.Error("DelStatistics() error(%v), min(%d) max(%d)", err, min, max)
PromError("db:删除统计数据")
return
}
rows, err = res.RowsAffected()
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",
"setting.go",
],
importpath = "go-common/app/interface/main/push-archive/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/model:go_default_library",
"//app/interface/main/push-archive/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/antispam:go_default_library",
"//library/net/http/blademaster/middleware/auth:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,66 @@
package http
import (
"net/http"
"strconv"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
"go-common/app/interface/main/push-archive/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/antispam"
"go-common/library/net/http/blademaster/middleware/auth"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
pushSrv *service.Service
authSrv *auth.Auth
veriSrv *verify.Verify
anti *antispam.Antispam
)
// Init init http.
func Init(c *conf.Config, srv *service.Service) {
pushSrv = srv
eng := bm.DefaultServer(c.Bm)
authSrv = auth.New(c.Auth)
veriSrv = verify.New(c.Verify)
anti = antispam.New(c.Anti)
addRoutes(eng)
if err := eng.Start(); err != nil {
log.Error("eng.Start error(%v)", err)
panic(err)
}
}
func addRoutes(e *bm.Engine) {
e.Ping(ping)
// TODO delete, for test
e.GET("/x/push-archive/test", func(ctx *bm.Context) {
aidStr := ctx.Request.Form.Get("aid")
aid, _ := strconv.ParseInt(aidStr, 10, 64)
midStr := ctx.Request.Form.Get("mid")
mid, _ := strconv.ParseInt(midStr, 10, 64)
pushSrv.Test(&model.Archive{
ID: aid,
Mid: mid,
})
}) // TODO delete, for test
set := e.Group("/x/push-archive/setting", veriSrv.Verify, authSrv.UserMobile)
{
set.GET("/get", anti.ServeHTTP, setting)
set.POST("/set", setSetting)
}
}
func ping(c *bm.Context) {
if err := pushSrv.Ping(c); err != nil {
log.Error("push-archive ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,35 @@
package http
import (
"go-common/app/interface/main/push-archive/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"strconv"
)
func getMID(c *bm.Context) (mid int64) {
midi, _ := c.Get("mid")
if midi != nil {
mid = midi.(int64)
}
return
}
func setting(c *bm.Context) {
mid := getMID(c)
c.JSON(pushSrv.Setting(c, mid))
}
func setSetting(c *bm.Context) {
mid := getMID(c)
tp, _ := strconv.Atoi(c.Request.Form.Get("type"))
if tp <= 0 {
log.Error("type(%d) is wrong", tp)
c.JSON(nil, ecode.RequestErr)
return
}
st := &model.Setting{Type: tp}
c.JSON(nil, pushSrv.SetSetting(c, mid, st))
}

View File

@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"batch_param.go",
"model.go",
"xints.go",
],
importpath = "go-common/app/interface/main/push-archive/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/archive/api:go_default_library",
"//app/service/main/relation/model:go_default_library",
"//library/log:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"batch_param_test.go",
"xints_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)

View File

@@ -0,0 +1,61 @@
package model
import (
"crypto/md5"
"encoding/hex"
"fmt"
"time"
"go-common/library/log"
"go-common/library/xstr"
)
// ParamHandler fn
type ParamHandler func(target *map[string]interface{}, args ...interface{})
// BatchParam str
type BatchParam struct {
Params map[string]interface{}
Handler ParamHandler
}
// NewBatchParam func
func NewBatchParam(p map[string]interface{}, h ParamHandler) *BatchParam {
if h == nil {
h = BaseParamHandler
}
return &BatchParam{
Params: p,
Handler: h,
}
}
var (
// BaseParamHandler fn
BaseParamHandler = func(target *map[string]interface{}, args ...interface{}) {}
// PushParamHandler fn
PushParamHandler = func(target *map[string]interface{}, args ...interface{}) {
var (
ok bool
arc *Archive
fans []int64
)
if target == nil || (*target)["archive"] == nil {
log.Warn("PushParamHandler target(%+v)/target[archive] nil, args(%+v)", target, args)
} else if arc, ok = (*target)["archive"].(*Archive); !ok || arc == nil {
log.Warn("PushParamHandler target[archive]=%+v parse failed/nil, args(%+v), target(%+v)", (*target)["archive"], args, target)
}
if arc == nil {
arc = &Archive{}
}
if len(args) != 1 || args[0] == nil {
log.Warn("PushParamHandler args(%+v) less than 1 or nil, target(%+v) archive(%+v)", args, target, arc)
} else if fans, ok = args[0].([]int64); !ok {
log.Warn("PushParamHandler args(%+v) parse failed, target(%+v) archive(%+v)", args, target, arc)
}
b := md5.Sum([]byte(fmt.Sprintf("%d%s", arc.ID, xstr.JoinInts(fans))))
(*target)["uuid"] = fmt.Sprintf("%s%d", hex.EncodeToString(b[:]), time.Now().UnixNano())
}
)

View File

@@ -0,0 +1,45 @@
package model
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestBatchParam_Format(t *testing.T) {
Convey("BatchParam_Format", t, func() {
f := func(p *BatchParam, t *testing.T, args ...interface{}) {
t.Logf("args(%+v)", args...)
p.Handler(&p.Params, args...)
t.Logf("params(%+v)\r\n", p)
}
var uuid interface{}
m := map[string]interface{}{
"h1": 10,
"archive": &Archive{ID: int64(12)},
}
p1 := NewBatchParam(m, BaseParamHandler)
f(p1, t)
_, exist := p1.Params["uuid"]
So(exist, ShouldEqual, false)
p2 := NewBatchParam(m, PushParamHandler)
f(p2, t)
_, exist = p2.Params["uuid"]
So(exist, ShouldEqual, true)
uuid = p2.Params["uuid"]
f(p2, t, []int{1, 2})
_, exist = p2.Params["uuid"]
So(exist, ShouldEqual, true)
So(p2.Params["uuid"], ShouldNotEqual, uuid)
f(p2, t, []int64{1, 2})
_, exist = p2.Params["uuid"]
So(exist, ShouldEqual, true)
var h ParamHandler
p2.Params["h1"] = 1
t.Logf("params(%+v), %+v, %v", p2.Params, h, h == nil)
})
}

View File

@@ -0,0 +1,135 @@
package model
import (
"encoding/json"
"time"
"go-common/app/service/main/archive/api"
relmdl "go-common/app/service/main/relation/model"
)
const (
// PushTypeUnknown 用户未上报推送设置
PushTypeUnknown = iota
// PushTypeForbid 禁止推送稿件更新通知
PushTypeForbid
// PushTypeSpecial 推送特别关注的upper的更新
PushTypeSpecial
// PushTypeAttention 推送关注的upper的更新
PushTypeAttention
)
const (
// RelationAttention 关注
RelationAttention = iota + 1
// RelationSpecial 特别关注
RelationSpecial
)
const (
// StatisticsUnpush 命中分组但未推送
StatisticsUnpush = iota
// StatisticsPush 命中分组且推送
StatisticsPush = 1
)
const (
// GroupDataTypeDefault 默认
GroupDataTypeDefault = "default"
// GroupDataTypeHBase AI脚本提供的hbase数据
GroupDataTypeHBase = "hbase"
// GroupDataTypeAbtest ab实验数据
GroupDataTypeAbtest = "ab_test"
// GroupDataTypeAbComparison ab对照数据
GroupDataTypeAbComparison = "ab_comparison"
)
const (
// AttrBitIsPGC pgc稿件的属性位
AttrBitIsPGC = 9
)
// Setting user push setting.
type Setting struct {
Type int `json:"type"`
}
// Message canal databus message.
type Message struct {
Action string `json:"action"`
Table string `json:"table"`
New json.RawMessage `json:"new"`
Old json.RawMessage `json:"old"`
}
// Relation user relation.
type Relation struct {
Mid int64 `json:"mid,omitempty"`
Fid int64 `json:"fid,omitempty"`
Attribute uint32 `json:"attribute"`
Status int `json:"status"`
MTime string `json:"mtime"`
CTime string `json:"ctime"`
}
// Following judge that whether has following relation.
func (r *Relation) Following() bool {
attr := relmdl.Following{Attribute: r.Attribute}
return attr.Following()
}
// RelationTagUser user relatino tag.
type RelationTagUser struct {
Mid int64 `json:"mid,omitempty"`
Fid int64 `json:"fid,omitempty"`
Tag string `json:"tag"`
MTime string `json:"mtime"`
CTime string `json:"ctime"`
}
// HasTag judge that whether has specified tag.
func (r *RelationTagUser) HasTag(tag int64) bool {
i := new(Ints)
i.Scan([]byte(r.Tag))
return i.Exist(tag)
}
// Archive model
type Archive struct {
ID int64 `json:"aid"`
Mid int64 `json:"mid"`
TypeID int16 `json:"typeid"`
HumanRank int `json:"humanrank"`
Duration int `json:"duration"`
Title string `json:"title"`
Cover string `json:"cover"`
Content string `json:"content"`
Tag string `json:"tag"`
Attribute int32 `json:"attribute"`
Copyright int8 `json:"copyright"`
AreaLimit int8 `json:"arealimit"`
State int `json:"state"`
Author string `json:"author"`
Access int `json:"access"`
Forward int `json:"forward"`
PubTime string `json:"pubtime"`
Round int8 `json:"round"`
CTime string `json:"ctime"`
MTime string `json:"mtime"`
}
// IsNormal judge that whether archive's state is normally.
func (a *Archive) IsNormal() bool {
arc := api.Arc{State: int32(a.State)}
return arc.IsNormal()
}
// PushStatistic 推送统计数据对象
type PushStatistic struct {
Aid int64 `json:"aid"`
Group string `json:"group"`
Type int `json:"type"`
Mids string `json:"mids"`
MidsCounter int `json:"mids_counter"`
CTime time.Time `json:"ctime"`
}

View File

@@ -0,0 +1,91 @@
package model
import (
"database/sql/driver"
"encoding/binary"
)
// Ints be used to MySql\Protobuf varbinary converting.
type Ints []int64
// MarshalTo marshal int64 slice to bytes,each int64 will occupy Fixed 8 bytes.
// if the argument data not supplied with the full size,it will return the actual written size
func (is Ints) MarshalTo(data []byte) (int, error) {
for i, n := range is {
start := i * 8
end := (i + 1) * 8
if len(data) < end {
return start, nil
}
bs := data[start:end]
binary.BigEndian.PutUint64(bs, uint64(n))
}
return 8 * len(is), nil
}
// Size return the total size it will occupy in bytes
func (is Ints) Size() int {
return len(is) * 8
}
// Unmarshal parse the data into int64 slice
func (is *Ints) Unmarshal(data []byte) error {
return is.Scan(data)
}
// Scan parse the data into int64 slice
func (is *Ints) Scan(src interface{}) (err error) {
switch sc := src.(type) {
case []byte:
var res []int64
for i := 0; i < len(sc) && i+8 <= len(sc); i += 8 {
ui := binary.BigEndian.Uint64(sc[i : i+8])
res = append(res, int64(ui))
}
*is = res
}
return
}
// Value marshal int64 slice to driver.Value,each int64 will occupy Fixed 8 bytes
func (is Ints) Value() (driver.Value, error) {
return is.Bytes(), nil
}
// Bytes marshal int64 slice to bytes,each int64 will occupy Fixed 8 bytes
func (is Ints) Bytes() []byte {
res := make([]byte, 0, 8*len(is))
for _, i := range is {
bs := make([]byte, 8)
binary.BigEndian.PutUint64(bs, uint64(i))
res = append(res, bs...)
}
return res
}
// Evict get rid of the sepcified num from the slice
func (is *Ints) Evict(e int64) (ok bool) {
res := make([]int64, len(*is)-1)
for _, v := range *is {
if v != e {
res = append(res, v)
} else {
ok = true
}
}
*is = res
return
}
// Exist judge the sepcified num is in the slice or not
func (is Ints) Exist(i int64) (e bool) {
for _, v := range is {
if v == i {
e = true
return
}
}
return
}

View File

@@ -0,0 +1,74 @@
package model
import (
"testing"
)
func TestMarshalAndUnmarshal(t *testing.T) {
var a = Ints{1, 2, 3}
data := make([]byte, a.Size())
n, err := a.MarshalTo(data)
if n != 24 {
t.Logf("marshal size must be 24")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Ints
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b[0] != 1 || b[1] != 2 || b[2] != 3 {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}
func TestUncompleteMarshal(t *testing.T) {
var a = Ints{1, 2, 3}
data := make([]byte, a.Size()-7)
n, err := a.MarshalTo(data)
if n != 16 {
t.Logf("marshal size must be 16")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Ints
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b[0] != 1 || b[1] != 2 {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}
func TestNilMarshal(t *testing.T) {
var a = Ints{1, 2, 3}
var data []byte
n, err := a.MarshalTo(data)
if n != 0 {
t.Logf("marshal size must be 0")
t.FailNow()
}
if err != nil {
t.Fatalf("err:%v", err)
}
var b Ints
err = b.Unmarshal(data)
if err != nil {
t.Fatalf("err:%v", err)
}
if b != nil {
t.Logf("unmarshal failed!b:%v", b)
t.FailNow()
}
}

View File

@@ -0,0 +1,73 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"archive_test.go",
"limit_test.go",
"service_test.go",
"setting_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/dao:go_default_library",
"//app/interface/main/push-archive/model:go_default_library",
"//app/service/main/push/api/grpc/v1:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"abtest.go",
"archive.go",
"limit.go",
"relation.go",
"service.go",
"setting.go",
"statistics.go",
],
importpath = "go-common/app/interface/main/push-archive/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/push-archive/conf:go_default_library",
"//app/interface/main/push-archive/dao:go_default_library",
"//app/interface/main/push-archive/model:go_default_library",
"//app/service/main/account/model:go_default_library",
"//app/service/main/account/rpc/client:go_default_library",
"//app/service/main/push/api/grpc/v1:go_default_library",
"//library/cache:go_default_library",
"//library/conf/env:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/queue/databus:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,51 @@
package service
import (
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
)
var (
// 存放实验组mid尾号
fansTestGroup = make(map[int]struct{})
// 存放对照组mid尾号
fansComparisonGroup = make(map[int]struct{})
// 指定mid放进测试组
fansTestMids = make(map[int64]struct{})
)
func (s *Service) mappingAbtest() {
for _, n := range s.c.Abtest.TestGroup {
fansTestGroup[n] = struct{}{}
}
for _, n := range s.c.Abtest.ComparisonGroup {
fansComparisonGroup[n] = struct{}{}
}
for _, n := range s.c.Abtest.TestMids {
fansTestMids[n] = struct{}{}
}
}
// 将所有粉丝通过abtest规则拆分成 (实验流量||对照组流量) && 其余流量
func (s *Service) fansByAbtest(group *dao.FanGroup, fans []int64) (result, others []int64) {
for _, fan := range fans {
n := int(fan % 10)
if group.Hitby == model.GroupDataTypeAbtest {
if _, ok := fansTestMids[fan]; ok {
result = append(result, fan)
continue
}
if _, ok := fansTestGroup[n]; ok {
result = append(result, fan)
continue
}
} else if group.Hitby == model.GroupDataTypeAbComparison {
if _, ok := fansComparisonGroup[n]; ok {
result = append(result, fan)
continue
}
}
others = append(others, fan)
}
return
}

View File

@@ -0,0 +1,333 @@
package service
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
accmdl "go-common/app/service/main/account/model"
"go-common/library/log"
)
const (
_insertAct = "insert"
_updateAct = "update"
_deleteAct = "delete"
_archiveTable = "archive"
_pushRetryTimes = 3
_pushPartSize = 500000 // 每次请求最多推50w mid
_hbaseBatch = 100
)
func (s *Service) consumeArchiveproc() {
defer s.wg.Done()
var (
err error
msgs = s.archiveSub.Messages()
)
for {
msg, ok := <-msgs
if !ok {
log.Warn("s.ArchiveSub has been closed.")
return
}
msg.Commit()
s.arcMo++
log.Info("consume archive key(%s) offset(%d) message(%s)", msg.Key, msg.Offset, msg.Value)
m := new(model.Message)
if err = json.Unmarshal(msg.Value, &m); err != nil {
log.Error("json.Unmarshal(%v) error(%v)", string(msg.Value), err)
continue
}
if m.Table != _archiveTable {
continue
}
n := new(model.Archive)
if err = json.Unmarshal(m.New, n); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", m.New, err)
continue
}
// 稿件过审
switch m.Action {
case _insertAct:
if !n.IsNormal() {
log.Info("archive (%d) upper(%d) is not normal when insert, no need to push", n.ID, n.Mid)
continue
}
case _updateAct:
o := new(model.Archive)
if err = json.Unmarshal(m.Old, o); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", m.Old, err)
continue
}
if !n.IsNormal() || o.State == n.State || o.PubTime == n.PubTime {
log.Info("archive (%d) upper(%d) is not normal when update, no need to push", n.ID, n.Mid)
continue
}
default:
continue
}
dao.PromInfo("archive_push")
// 在指定周期内up主只推送一次
if s.limit(n.Mid) {
log.Info("upper's push limit upper(%d) aid(%d)", n.Mid, n.ID)
continue
}
log.Info("noticeFans start, mid(%d) aid(%d)", n.Mid, n.ID)
err = s.noticeFans(n) // notice fans
log.Info("noticeFans end, mid(%d) aid(%d) error(%v)", n.Mid, n.ID, err)
}
}
// isForbidTime 当前时间是否被禁止推送消息
func (s *Service) isForbidTime() (forbid bool, err error) {
var forbidStart, forbidEnd time.Time
now := time.Now()
for _, f := range s.ForbidTimes {
forbidStart, err = s.getTodayTime(f.PushForbidStartTime)
if err != nil {
log.Error("isForbidTime getTodayTime(%s) error(%v)", f.PushForbidStartTime, err)
return
}
forbidEnd, err = s.getTodayTime(f.PushForbidEndTime)
if err != nil {
log.Error("isForbidTime getTodayTime(%s) error(%v)", f.PushForbidEndTime, err)
return
}
if now.After(forbidStart) && now.Before(forbidEnd) {
forbid = true
break
}
}
return
}
func (s *Service) noticeFans(arc *model.Archive) (err error) {
isPGC := s.isPGC(arc)
mids, counter, err := s.fans(arc.Mid, arc.ID, isPGC)
if err != nil {
return
}
log.Info("noticeFans get fans mid(%d) aid(%d) fans number(%d)", arc.Mid, arc.ID, counter)
arc.Author = s.name(arc.Mid)
for gKey, list := range mids {
g := s.dao.FanGroups[gKey]
params := model.NewBatchParam(map[string]interface{}{
"relationType": g.RelationType,
"archive": arc,
"group": s.getGroupParam(g),
"msgTemplate": g.MsgTemplate,
}, model.PushParamHandler)
dao.Batch(&list, _pushPartSize, _pushRetryTimes, params, s.dao.NoticeFans)
}
return
}
func (s *Service) fans(upper int64, aid int64, isPGC bool) (mids map[string][]int64, counter int, err error) {
mids = make(map[string][]int64)
// 从 HBase 获取upper粉丝数据
fans, err := s.dao.Fans(context.TODO(), upper, isPGC)
ln := len(fans)
if err != nil {
log.Error("fans from hbase upper(%d) error(%v)", upper, err)
return
}
log.Info("filter fans -- get all fans of upper(%d), fans num(%d)", upper, ln)
if ln == 0 {
return
}
dao.PromInfoAdd("archive_fans", int64(ln))
// 筛选粉丝,分配到对应的实验组
hitFans := s.group(upper, fans)
hour := time.Now().Hour()
for gKey, hits := range hitFans {
log.Info("before filter by active time, upper(%d) group(%s) fans(%d)", upper, gKey, len(hits))
if len(hits) <= 0 {
continue
}
g := s.dao.FanGroups[gKey]
var activeFans, excluded []int64
if g.RelationType == model.RelationAttention {
activeFans, excluded = s.dao.FansByActiveTime(hour, &hits)
noLimitFans := s.noPushLimitFans(upper, gKey, &activeFans)
for _, mid := range activeFans {
if !s.filterUserSetting(mid, g.RelationType) {
excluded = append(excluded, mid)
log.Info("filter fans: excluded by usersettings, fan(%d), relationtype(%d), upper(%d) table(%s)", mid, g.RelationType, upper, g.HBaseTable)
continue
}
if !s.pushLimit(mid, upper, g, &noLimitFans) {
excluded = append(excluded, mid)
continue
}
mids[gKey] = append(mids[gKey], mid)
dao.PromInfo(fmt.Sprintf("%s__send", g.Name))
log.Info("filter fans: included by pushlimit: upper(%d)'s fan(%d) group.name(%s)", upper, mid, g.Name)
}
} else {
mids[gKey] = hits
}
counter += len(mids[gKey])
log.Info("upper(%d) aid(%d) group(%s) fans(%d)", upper, aid, gKey, len(mids[gKey]))
// 统计数据落表
s.formStatisticsProc(aid, g.Name, mids[gKey], &excluded)
}
return
}
// group 粉丝分组: 分组优先级(优先级 & 分组的可用性)-->分组规则(默认default,hbase过滤走hbase)
func (s *Service) group(upper int64, fans map[int64]int) (list map[string][]int64) {
list = make(map[string][]int64)
// 将粉丝分为 特殊关注 和 普通关注 两类
attentions, specials := s.dao.FansByProportion(upper, fans)
log.Info("group count upper(%d) attentions(%d) specials(%d)", upper, len(attentions), len(specials))
// 禁止推送时间判断, 免打扰用户(针对普通关注)
isForbid, err := s.isForbidTime()
if err != nil {
log.Error("isForbidTime upper(%d) error(%v)", upper, err)
}
// 按顺序筛选出每个分组的数据
for _, gkey := range s.dao.GroupOrder {
g := s.dao.FanGroups[gkey]
if g.RelationType == model.RelationAttention && isForbid {
log.Info("forbid by time upper(%d) group(%+v)", upper, g)
continue
}
// 优先处理abtest的流量
if g.Hitby == model.GroupDataTypeAbtest {
switch g.RelationType {
case model.RelationAttention:
var pool, exists, ex []int64
pool, attentions = s.fansByAbtest(g, attentions)
for _, gk := range s.dao.GroupOrder {
gg := s.dao.FanGroups[gk]
if gg.RelationType != model.RelationAttention ||
gg.Hitby != model.GroupDataTypeHBase ||
gg.Name == "ai:pushlist_follow_recent" {
continue
}
ex, pool = s.dao.FansByHBase(upper, gk, &pool)
exists = append(exists, ex...)
}
list[gkey] = s.filterByBlacklist(upper, exists)
case model.RelationSpecial:
list[gkey], specials = s.fansByAbtest(g, specials)
}
continue
}
if g.Hitby == model.GroupDataTypeAbComparison {
// 对照组保持原来逻辑
switch g.RelationType {
case model.RelationAttention:
var pool, exists []int64
pool, attentions = s.fansByAbtest(g, attentions)
for _, gk := range s.dao.GroupOrder {
gg := s.dao.FanGroups[gk]
if gg.RelationType != model.RelationAttention || gg.Hitby != model.GroupDataTypeHBase {
continue
}
exists, pool = s.dao.FansByHBase(upper, gk, &pool)
list[gkey] = append(list[gkey], exists...)
}
case model.RelationSpecial:
list[gkey], specials = s.fansByAbtest(g, specials)
}
continue
}
switch {
case g.RelationType == model.RelationAttention && g.Hitby == model.GroupDataTypeDefault:
list[gkey] = attentions
attentions = []int64{}
case g.RelationType == model.RelationSpecial && g.Hitby == model.GroupDataTypeDefault:
list[gkey] = specials
specials = []int64{}
case g.RelationType == model.RelationAttention && g.Hitby == model.GroupDataTypeHBase:
list[gkey], attentions = s.dao.FansByHBase(upper, gkey, &attentions)
case g.RelationType == model.RelationSpecial && g.Hitby == model.GroupDataTypeHBase:
list[gkey], specials = s.dao.FansByHBase(upper, gkey, &specials)
default:
log.Error("group failed for grouporder(%s) & group.relationtype(%d) & hitby(%s)", gkey, g.RelationType, g.Hitby)
}
}
return
}
func (s *Service) filterByBlacklist(upper int64, fans []int64) (result []int64) {
for {
var mids []int64
l := len(fans)
if l == 0 {
return
}
if l <= _hbaseBatch {
mids = fans[:]
fans = []int64{}
} else {
mids = fans[:_hbaseBatch]
fans = fans[_hbaseBatch:]
}
exists, notExists := s.dao.ExistsInBlacklist(context.Background(), upper, mids)
result = append(result, notExists...) // 不在黑名单中的用户直接可推
exists, notExists = s.dao.ExistsInWhitelist(context.Background(), upper, exists)
result = append(result, exists...) // 存在黑名单中但是后来被恢复到白名单中的用户可推
for _, mid := range notExists {
// 只出现在黑名单中的用户不推
log.Warn("filter by blacklist, mid(%d)", mid)
}
}
}
// getGroupParam g.name被_分隔后的第1个/2个值比如ai:pushlist_offline_up的是offline, special的就是special
func (s *Service) getGroupParam(g *dao.FanGroup) string {
p := strings.Split(g.Name, "_")
if len(p) == 1 {
return p[0]
}
if g.RelationType == model.RelationSpecial {
return "s_" + p[1]
}
return p[1]
}
// filterUserSetting 普通关注up主的粉丝中推送开启配置为全部推送的人很少因此只要其开启了特殊关注/全部关注/没有设置的就发送;特殊关注等同
func (s *Service) filterUserSetting(mid int64, relationType int) bool {
if relationType == model.RelationSpecial && s.userSettings[mid] != nil && s.userSettings[mid].Type == model.PushTypeForbid {
return false
}
if relationType == model.RelationAttention && s.userSettings[mid] != nil && s.userSettings[mid].Type == model.PushTypeForbid {
return false
}
return true
}
func (s *Service) name(mid int64) (name string) {
arg := &accmdl.ArgMid{Mid: mid}
info, err := s.accRPC.Info3(context.TODO(), arg)
if err != nil {
dao.PromError("archive:获取作者信息")
log.Error("s.accRPC.Info3(%+v) error(%v)", arg, err)
return
}
name = info.Name
return
}
func (s *Service) isPGC(arc *model.Archive) bool {
return (arc.Attribute >> model.AttrBitIsPGC & 1) == 1
}
// Test for test
// TODO delete, for test
func (s *Service) Test(arc *model.Archive) {
if arc.ID == 0 || arc.Mid == 0 {
return
}
go s.noticeFans(arc)
} // TODO delete, for test

View File

@@ -0,0 +1,75 @@
package service
import (
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func Test_groupparam(t *testing.T) {
initd()
expect := map[string]string{
"1#ai:pushlist_follow_recent": "follow",
"1#ai:pushlist_play_recent": "play",
"1#ai:pushlist_offline_up": "offline",
"2#special": "special",
}
convey.Convey("推送的group参数", t, func() {
for k, g := range s.dao.FanGroups {
group := s.getGroupParam(g)
convey.So(group, convey.ShouldEqual, expect[k])
}
})
}
func Test_usersettingfilter(t *testing.T) {
initd()
mid := int64(11111111)
s.userSettings[mid] = &model.Setting{Type: model.PushTypeForbid}
allow := s.filterUserSetting(mid, model.RelationSpecial)
convey.Convey("usersettings filter关闭开关则排除", t, func() {
convey.So(allow, convey.ShouldEqual, false)
})
s.userSettings[mid] = nil
allow = s.filterUserSetting(mid, model.RelationSpecial)
convey.Convey("usersettings filter未设置开关则允许", t, func() {
convey.So(allow, convey.ShouldEqual, true)
})
s.userSettings[mid] = &model.Setting{Type: model.PushTypeAttention}
allow = s.filterUserSetting(mid, model.RelationSpecial)
convey.Convey("usersettings filter设置未所有关注则允许", t, func() {
convey.So(allow, convey.ShouldEqual, true)
})
}
func Test_ispgc(t *testing.T) {
arc := new(model.Archive)
convey.Convey("pgc稿件判断", t, func() {
arc.Attribute = int32(110336)
convey.So(s.isPGC(arc), convey.ShouldEqual, true)
arc.Attribute = int32(16512)
convey.So(s.isPGC(arc), convey.ShouldEqual, false)
})
}
func TestServicefansByAbtest(t *testing.T) {
initd()
group := &dao.FanGroup{
Hitby: "ab_test",
HBaseTable: "push_archive_ab_test",
HBaseFamily: []string{"cf"},
}
fans := []int64{1, 2, 3, 4, 5, 6}
convey.Convey("fansByAbtest", t, func() {
exists, notExists := s.fansByAbtest(group, fans)
t.Logf("exists(%v)", exists)
t.Logf("notExists(%v)", notExists)
})
}

View File

@@ -0,0 +1,138 @@
package service
import (
"context"
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
)
func (s *Service) pushLimit(fan int64, upper int64, g *dao.FanGroup, noLimitFans *map[int64]int) (allow bool) {
if _, ok := (*noLimitFans)[fan]; ok {
log.Info("included by pushlimit(%d) upper(%d) group.name(%s) without pushlimit)", fan, upper, g.Name)
allow = true
return
}
if !s.fanLimit(fan, g) {
log.Info("excluded by fanlimit(%d) upper(%d) group.name(%s)", fan, upper, g.Name)
return
}
if !s.perUpperLimit(fan, upper, g) {
log.Info("excluded by perupperlimit(%d) upper(%d) group.name(%s)", fan, upper, g.Name)
return
}
allow = true
return
}
//perUpperLimit 粉丝的在指定周期内的次数限制
func (s *Service) perUpperLimit(fan int64, upper int64, g *dao.FanGroup) (allow bool) {
limit := g.PerUpperLimit
//没有次数限制
if limit <= 0 {
allow = true
return
}
//有次数限制
var (
now int
err error
)
if now, err = s.dao.GetPerUpperLimitCache(context.TODO(), fan, upper); err != nil {
log.Error("s.dao.GetPerUpperLimitCache err(%v), fan(%d), upper(%d) group.name(%s)", err, fan, upper, g.Name)
return
}
now = now + 1
if limit < now {
return
}
if err = s.dao.AddPerUpperLimitCache(context.TODO(), fan, upper, now, g.LimitExpire); err != nil {
log.Error("s.dao.AddPerUpperLimitCache err(%v), fan(%d), upper(%d), value(%d) group.name(%s)", err, fan, upper, now, g.Name)
return
}
allow = true
return
}
//fanLimit 粉丝的在指定周期内的次数限制
func (s *Service) fanLimit(fan int64, g *dao.FanGroup) (allow bool) {
limit := g.Limit
//没有次数限制
if limit <= 0 {
allow = true
return
}
//有次数限制
var (
now int
err error
)
if now, err = s.dao.GetFanLimitCache(context.TODO(), fan, g.RelationType); err != nil {
log.Error("s.dao.GetFanLimitCache err(%v), fan(%d), group.name(%s)", err, fan, g.Name)
return
}
now = now + 1
if limit < now {
return
}
if err = s.dao.AddFanLimitCache(context.TODO(), fan, g.RelationType, now, g.LimitExpire); err != nil {
log.Error("s.dao.AddFanLimitCache err(%v), fan(%d), value(%d), group.name(%s)", err, fan, now, g.Name)
return
}
allow = true
return
}
// limit limits push frequency.
func (s *Service) limit(upper int64) (limit bool) {
if s.dao.UpperLimitExpire == 0 {
return
}
limit = true
exist, err := s.dao.ExistUpperLimitCache(context.TODO(), upper)
if err != nil {
log.Error("s.dao.ExistUpperLimitCache(%d) error(%v)", upper, err)
return
}
if exist {
return
}
if err = s.dao.AddUpperLimitCache(context.TODO(), upper); err != nil {
log.Error("s.dao.AddUpperLimitCache(%d) error(%v)", upper, err)
return
}
limit = false
return
}
func (s *Service) noPushLimitFans(upper int64, fanGroupKey string, fans *[]int64) (noLimitFans map[int64]int) {
noLimitFans = map[int64]int{}
g := s.dao.FanGroups[fanGroupKey]
// 没有频率限制,没有免限制范围的概念
if g.Limit <= 0 {
return
}
// 只有特殊关注,才有免限制范围
if g.RelationType != model.RelationSpecial {
return
}
// 没有hbase表没有免限制的概念
if len(g.HBaseTable) == 0 {
return
}
// abtest 不走免限制逻辑
if g.Hitby == model.GroupDataTypeAbtest || g.Hitby == model.GroupDataTypeAbComparison {
return
}
f := *fans
hit, _ := s.dao.FansByHBase(upper, fanGroupKey, &f)
for _, mid := range hit {
noLimitFans[mid] = 1
}
return
}

View File

@@ -0,0 +1,54 @@
package service
import (
"testing"
"time"
"go-common/app/interface/main/push-archive/dao"
"github.com/smartystreets/goconvey/convey"
)
func Test_limit(t *testing.T) {
initd2()
upper := int64(113)
convey.Convey("upper主次数限制", t, func() {
convey.So(s.limit(upper), convey.ShouldEqual, false)
convey.So(s.limit(upper), convey.ShouldEqual, true)
convey.So(s.limit(upper), convey.ShouldEqual, true)
time.Sleep(time.Second * 2)
convey.So(s.limit(upper), convey.ShouldEqual, false)
convey.So(s.limit(upper), convey.ShouldEqual, true)
})
fan := int64(121)
g := &dao.FanGroup{
Limit: 2,
PerUpperLimit: 0,
LimitExpire: 2,
}
noLimitFans := &map[int64]int{}
convey.Convey("粉丝推送次数限制", t, func() {
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, false)
time.Sleep(time.Second * 2)
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
})
g.PerUpperLimit = 1
fan = int64(333)
convey.Convey("粉丝推送upper主的次数限制", t, func() {
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, false)
time.Sleep(time.Second * 2)
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
})
convey.Convey("粉丝不受pushlimit限制", t, func() {
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, false)
(*noLimitFans)[fan] = 1
convey.So(s.pushLimit(fan, upper, g, noLimitFans), convey.ShouldEqual, true)
})
}

View File

@@ -0,0 +1,173 @@
package service
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"go-common/app/interface/main/push-archive/model"
"go-common/library/conf/env"
"go-common/library/log"
)
const (
_relationMidTable = "user_relation_mid_"
_relationTagUserTable = "user_relation_tag_user_"
_retry = 3
_relationStatusDeleted = 1 // 取消关注
_relationTagSpecial = int64(-10) // 特殊关注的tag
)
func (s *Service) consumeRelationproc() {
defer s.wg.Done()
var err error
for {
msg, ok := <-s.relationSub.Messages()
if !ok {
log.Warn("s.RelationSub has been closed.")
return
}
msg.Commit()
time.Sleep(time.Millisecond)
s.relMo++
log.Info("consume relation key(%s) offset(%d) message(%s)", msg.Key, msg.Offset, msg.Value)
m := new(model.Message)
if err = json.Unmarshal(msg.Value, &m); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", msg.Value, err)
continue
}
switch {
case strings.HasPrefix(m.Table, _relationMidTable):
err = s.relationMid(m.Action, m.New, m.Old)
case strings.HasPrefix(m.Table, _relationTagUserTable):
err = s.relationTagUser(m.Action, m.New, m.Old)
default:
continue
}
if err != nil {
log.Error("consumeRelationproc data(%s) error(%+v)", msg.Value, err)
if env.DeployEnv == env.DeployEnvProd {
s.dao.WechatMessage(fmt.Sprintf("push-archive sync relation fail error(%v)", err))
}
}
}
}
func (s *Service) addFans(upper, fans int64, tp int) (err error) {
for i := 0; i < _retry; i++ {
if err = s.dao.AddFans(context.TODO(), upper, fans, tp); err == nil {
break
}
log.Info("retry s.dao.AddFans(%d,%d,%d)", upper, fans, tp)
}
if err != nil {
log.Error("s.dao.AddFans(%d,%d,%d) error(%v)", upper, fans, tp, err)
}
return
}
func (s *Service) delFans(upper, fans int64) (err error) {
for i := 0; i < _retry; i++ {
if err = s.dao.DelFans(context.TODO(), upper, fans); err == nil {
break
}
log.Info("retry s.dao.DelFans(%d,%d)", upper, fans)
}
if err != nil {
log.Error("s.dao.DelFans(%d,%d) error(%v)", upper, fans, err)
}
return
}
func (s *Service) delSpecialAttention(upper, fans int64) (err error) {
for i := 0; i < _retry; i++ {
if err = s.dao.DelSpecialAttention(context.TODO(), upper, fans); err == nil {
break
}
log.Info("retry s.dao.DelSpecialAttention(%d,%d)", upper, fans)
}
if err != nil {
log.Error("s.dao.DelSpecialAttention(%d,%d) error(%v)", upper, fans, err)
}
return
}
// relationMid .
func (s *Service) relationMid(action string, nwMsg, oldMsg []byte) (err error) {
n := &model.Relation{}
if err = json.Unmarshal(nwMsg, n); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", nwMsg, err)
return
}
switch action {
case _insertAct:
if !n.Following() {
return
}
err = s.addFans(n.Fid, n.Mid, model.RelationAttention)
case _updateAct:
o := &model.Relation{}
if err = json.Unmarshal(oldMsg, o); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", oldMsg, err)
return
}
if n.Status == o.Status && n.Attribute == o.Attribute {
return
}
if n.Status == _relationStatusDeleted || !n.Following() {
err = s.delFans(n.Fid, n.Mid) // 删除粉丝关系
} else {
err = s.addFans(n.Fid, n.Mid, model.RelationAttention) // 增加、更新关注数据
}
}
if err != nil {
log.Error("s.relationMid(%s,%s) error(%v)", nwMsg, oldMsg, err)
}
return
}
// relationTagUser .
func (s *Service) relationTagUser(action string, nwMsg, oldMsg []byte) (err error) {
n := &model.RelationTagUser{}
if err = json.Unmarshal(nwMsg, n); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", nwMsg, err)
return
}
tagB, _ := base64.StdEncoding.DecodeString(n.Tag)
n.Tag = string(tagB)
switch action {
case _insertAct:
if !n.HasTag(_relationTagSpecial) {
return
}
err = s.addFans(n.Fid, n.Mid, model.RelationSpecial)
case _updateAct:
o := &model.RelationTagUser{}
if err = json.Unmarshal(oldMsg, o); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", oldMsg, err)
return
}
tagB, _ = base64.StdEncoding.DecodeString(o.Tag)
o.Tag = string(tagB)
nt := n.HasTag(_relationTagSpecial)
ot := o.HasTag(_relationTagSpecial)
if nt && !ot {
err = s.addFans(n.Fid, n.Mid, model.RelationSpecial)
} else if !nt && ot {
err = s.delSpecialAttention(n.Fid, n.Mid)
}
case _deleteAct:
err = s.delSpecialAttention(n.Fid, n.Mid)
}
if err != nil {
log.Error("s.relationTagUser(%s,%s) error(%v)", action, nwMsg, err)
}
return
}

View File

@@ -0,0 +1,177 @@
package service
import (
"context"
"sync"
"time"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
accrpc "go-common/app/service/main/account/rpc/client"
pb "go-common/app/service/main/push/api/grpc/v1"
pushrpc "go-common/app/service/main/push/api/grpc/v1"
"go-common/library/cache"
"go-common/library/conf/env"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/queue/databus"
)
// Service push service.
type Service struct {
c *conf.Config
dao *dao.Dao
cache *cache.Cache
accRPC *accrpc.Service3
wg sync.WaitGroup
archiveSub *databus.Databus
relationSub *databus.Databus
userSettings map[int64]*model.Setting
arcMo, relMo int64
CloseCh chan bool
ForbidTimes []conf.ForbidTime
settingCh chan *pb.SetSettingRequest
pushRPC pushrpc.PushClient
}
// New creates a push service instance.
func New(c *conf.Config) *Service {
s := &Service{
c: c,
dao: dao.New(c),
cache: cache.New(1, 102400),
accRPC: accrpc.New3(c.AccountRPC),
archiveSub: databus.New(c.ArchiveSub),
relationSub: databus.New(c.RelationSub),
userSettings: make(map[int64]*model.Setting),
CloseCh: make(chan bool),
ForbidTimes: c.ArcPush.ForbidTimes,
settingCh: make(chan *pb.SetSettingRequest, 3072),
}
var err error
if s.pushRPC, err = pushrpc.NewClient(c.PushRPC); err != nil {
panic(err)
}
s.mappingAbtest()
go s.loadUserSettingsproc()
time.Sleep(2 * time.Second) // consumeArchive will notice upper's fans, it depends on user's setting
s.wg.Add(1)
go s.loadUserSettingsproc()
if c.Push.ProdSwitch {
s.wg.Add(1)
go s.consumeRelationproc()
s.wg.Add(1)
go s.consumeArchiveproc()
go s.monitorConsume()
}
go s.clearStatisticsProc()
s.wg.Add(1)
go s.saveStatisticsProc()
go s.setSettingProc()
return s
}
// loadUserSettingsproc 若是用户没有设置推送开关默认会被推送消息因此若是新设置load出错/无值,则不替换旧设置
func (s *Service) loadUserSettingsproc() {
defer s.wg.Done()
var (
ps int64 = 30000
start int64
end int64
mxID int64
err error
res map[int64]*model.Setting
)
for {
select {
case _, ok := <-s.CloseCh:
if !ok {
log.Info("CloseCh is closed, close the loadUserSettingsproc")
return
}
default:
}
start = 0
end = 0
err = nil
res = make(map[int64]*model.Setting)
mxID, err = s.dao.SettingsMaxID(context.TODO())
if err != nil || mxID == 0 {
time.Sleep(10 * time.Millisecond)
continue
}
for {
start = end
end += ps
err = s.dao.SettingsAll(context.TODO(), start, end, &res)
if err != nil {
break
}
if end >= mxID {
break
}
}
if err != nil || len(res) == 0 {
time.Sleep(10 * time.Millisecond)
continue
}
s.userSettings = res
time.Sleep(time.Duration(s.c.Push.LoadSettingsInterval))
}
}
func (s *Service) monitorConsume() {
if env.DeployEnv != env.DeployEnvProd {
return
}
var (
arc int64 // archive result count
rel int64 // relation count
)
for {
time.Sleep(10 * time.Minute)
if s.arcMo-arc == 0 {
msg := "databus: push-archive archiveResult did not consume within ten minute"
s.dao.WechatMessage(msg)
log.Warn(msg)
}
arc = s.arcMo
if s.relMo-rel == 0 {
msg := "databus: push-archive relation did not consume within ten minute"
s.dao.WechatMessage(msg)
log.Warn(msg)
}
rel = s.relMo
}
}
// Close closes service.
func (s *Service) Close() {
s.archiveSub.Close()
s.relationSub.Close()
close(s.CloseCh)
s.wg.Wait()
s.dao.Close()
}
// Ping checks service.
func (s *Service) Ping(c *bm.Context) (err error) {
err = s.dao.Ping(c)
return
}
// getTodayTime 获取当日的某个时间点的时间
func (s *Service) getTodayTime(tm string) (todayTime time.Time, err error) {
now := time.Now()
today := now.Format("2006-01-02")
todayTime, err = time.ParseInLocation("2006-01-02 15:04:05", today+" "+tm, time.Local)
if err != nil {
log.Error("clearStatisticsProc time.ParseInLocation error(%v)", err)
}
return
}

View File

@@ -0,0 +1,185 @@
package service
import (
"context"
"flag"
"fmt"
"path/filepath"
"testing"
"time"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
time2 "go-common/library/time"
"github.com/smartystreets/goconvey/convey"
)
var s *Service
func initd() {
dir, _ := filepath.Abs("../cmd/push-archive-test.toml")
flag.Set("conf", dir)
conf.Init()
s = New(conf.Conf)
}
//普通关注的3个实验组比列均为5%尾号分别为00~04, 05~09, 10~14
func initd2() {
dir, _ := filepath.Abs("../cmd/push-archive-test.toml")
flag.Set("conf", dir)
conf.Init()
conf.Conf.ArcPush.UpperLimitExpire = time2.Duration(2 * time.Second)
ps := []conf.Proportion{}
for i := 0; i < 5; i++ {
p := conf.Proportion{
Proportion: "0.05",
ProportionStartFrom: fmt.Sprintf("%d", i*5),
}
ps = append(ps, p)
}
conf.Conf.ArcPush.Proportions = ps
s = New(conf.Conf)
}
func Test_todaytime(t *testing.T) {
initd()
todayTime, err := s.getTodayTime("06:10:00")
convey.Convey("当日定时时间", t, func() {
convey.So(err, convey.ShouldBeNil)
})
t.Logf("todaytime(%s) unix(%d)", todayTime.Format("2006-01-02 15:04:05"), todayTime.Unix())
}
func Test_forbidTime(t *testing.T) {
initd()
t.Logf("the forbidtimes(%v)\n", s.ForbidTimes)
forbid, err := s.isForbidTime()
convey.Convey("禁止时间判断", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(forbid, convey.ShouldEqual, false)
})
}
func Test_deadline(t *testing.T) {
initd()
deadline, err := s.getDeadline()
convey.Convey("最近第3天的凌晨", t, func() {
convey.So(err, convey.ShouldBeNil)
})
t.Logf("the deadline(%s) unix(%d)", deadline.Format("2006-01-02 15:04:05"), deadline.Unix())
}
func Test_proportion(t *testing.T) {
initd2()
fans := map[int64]int{
100014: model.RelationAttention,
100015: model.RelationAttention,
100016: model.RelationAttention,
100017: model.RelationAttention,
100038: model.RelationAttention,
100029: model.RelationAttention,
100070: model.RelationAttention,
100071: model.RelationAttention,
100072: model.RelationAttention,
100073: model.RelationSpecial,
10034: model.RelationSpecial,
10038: model.RelationSpecial,
}
expected := map[int]int{
1: 4,
2: 3,
}
attentions, specials := s.dao.FansByProportion(121231, fans)
convey.Convey("根据比列过滤各组", t, func() {
convey.So(len(attentions), convey.ShouldEqual, expected[1])
convey.So(len(specials), convey.ShouldEqual, expected[2])
})
}
func Test_Group(t *testing.T) {
initd2()
upper := int64(27515256)
//upper主所有粉丝
fans, err := s.dao.Fans(context.TODO(), upper, false)
convey.Convey("获取所有粉丝", t, func() {
convey.So(err, convey.ShouldBeNil)
convey.So(len(fans), convey.ShouldEqual, 37)
})
attentions, specials := s.dao.FansByProportion(upper, fans)
t.Logf("attentions(%d), specials(%d)", len(attentions), len(specials))
convey.Convey("没有hitby则没有命中任何分组", t, func() {
for _, g := range s.dao.FanGroups {
g.Hitby = ""
}
list := s.group(upper, fans)
convey.So(len(list), convey.ShouldEqual, 0)
})
convey.Convey("hitby=default只命中2组", t, func() {
s.dao.GroupOrder = []string{"1#attention", "1#ai:pushlist_play_recent", "2#special"}
for _, g := range s.dao.FanGroups {
g.Hitby = model.GroupDataTypeDefault
}
list := s.group(upper, fans)
convey.So(len(list), convey.ShouldEqual, 3)
convey.So(len(list["1#attention"]), convey.ShouldEqual, len(attentions))
convey.So(len(list["1#ai:pushlist_play_recent"]), convey.ShouldEqual, 0)
convey.So(len(list["2#special"]), convey.ShouldEqual, len(specials))
})
convey.Convey("hitby=hbase根据表命中获取", t, func() {
s.dao.GroupOrder = []string{"1#ai:pushlist_play_recent", "1#ai:pushlist_offline_up", "1#ai:pushlist_follow_recent", "1#attention", "2#special"}
for _, g := range s.dao.FanGroups {
if g.RelationType == 1 {
g.Hitby = "hbase"
}
}
list := s.group(upper, fans)
t.Logf("list(%+v)", list)
})
//map[1#ai:pushlist_follow_recent:[]
// 2#special:[21231134 4235023 1232032 2089809 88889018]
// 1#ai:pushlist_play_recent:[27515303 27515311 27515317 27515300 27515401 27515306]
// 1#ai:pushlist_offline_up:[]]
expect := map[string]int{
"1#ai:pushlist_follow_recent": 1,
"1#ai:pushlist_play_recent": 5,
"1#ai:pushlist_offline_up": 1,
"2#special": 5,
}
convey.Convey("实验组粉丝优先级分组", t, func() {
hit := s.group(upper, fans)
t.Logf("the hits(%v)", hit)
for gkey, f := range hit {
convey.So(len(f), convey.ShouldEqual, expect[gkey])
}
})
}
func Test_manytimes(t *testing.T) {
initd2()
upper := int64(27515256)
mids1, len1, err1 := s.fans(upper, 1020, false)
time.Sleep(time.Second * 2)
mids2, len2, err2 := s.fans(upper, 1030, false)
convey.Convey(" 同一up主多次获取过滤后的粉丝第二次比第一次少", t, func() {
convey.So(err1, convey.ShouldBeNil)
convey.So(err2, convey.ShouldBeNil)
convey.So(len1, convey.ShouldBeGreaterThan, 0)
convey.So(len2, convey.ShouldBeGreaterThan, 0)
convey.So(len2, convey.ShouldBeLessThanOrEqualTo, len1)
})
t.Logf("the mids1(%v)\n the mids2(%v)\n", mids1, mids2)
}

View File

@@ -0,0 +1,77 @@
package service
import (
"context"
"time"
"go-common/app/interface/main/push-archive/model"
pb "go-common/app/service/main/push/api/grpc/v1"
"go-common/library/log"
)
// Setting gets user's archive-result setting.
func (s *Service) Setting(c context.Context, mid int64) (st *model.Setting, err error) {
st, err = s.dao.Setting(c, mid)
if err != nil {
return
}
if st == nil {
st = &model.Setting{Type: model.PushTypeSpecial} // 如果用户还未上传配置,则默认为特殊关注
}
return
}
// SetSetting saves user's archive-result setting.
func (s *Service) SetSetting(c context.Context, mid int64, st *model.Setting) (err error) {
err = s.dao.SetSetting(c, mid, st)
if err == nil {
set := &pb.SetSettingRequest{
Mid: mid,
Type: 1,
Value: int32(st.Type),
}
s.settingCh <- set
}
return
}
func (s *Service) setSettingProc() (err error) {
defer func() {
if msg := recover(); msg != nil {
log.Error("setSettingProc got panic(%+v)", msg)
}
}()
for {
time.Sleep(time.Millisecond * 200)
set, open := <-s.settingCh
if !open {
log.Error("setSettingProc settingCh is closed")
return
}
// before send rpc, check db value with new value, if diff, then rpc is later than another update
dbSet, err := s.dao.Setting(context.TODO(), set.Mid)
if err != nil {
log.Error("setSettingProc s.dao.Setting error(%v) set(%+v)", err, set)
s.settingCh <- set
continue
}
if dbSet == nil || dbSet.Type != int(set.Value) {
log.Info("setSettingProc push setting value diff, db(%+v) rpc(%+v)", dbSet, set)
continue
}
// rpc中0-关闭1-开启
var tp int32
if set.Value == model.PushTypeSpecial || set.Value == model.PushTypeAttention {
tp = 1
}
set.Value = tp
if _, err := s.pushRPC.SetSetting(context.TODO(), set); err != nil {
log.Error("s.pushRPC.SetSetting error(%v) set(%+v)", err, set)
s.settingCh <- set
}
}
}

View File

@@ -0,0 +1,116 @@
package service
import (
"context"
ht "net/http"
"sync"
"testing"
"go-common/app/interface/main/push-archive/conf"
"go-common/app/interface/main/push-archive/model"
pb "go-common/app/service/main/push/api/grpc/v1"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"github.com/smartystreets/goconvey/convey"
)
func Test_getsetting(t *testing.T) {
initd()
mid := int64(11111111)
s.SetSetting(context.TODO(), mid, nil)
convey.Convey("获取默认的用户开关设置", t, func() {
st, err := s.Setting(context.TODO(), mid)
convey.So(err, convey.ShouldBeNil)
convey.So(st.Type, convey.ShouldEqual, model.PushTypeSpecial)
})
}
func Test_setsetting(t *testing.T) {
initd()
mid := int64(11111111)
convey.Convey("存储用户开关设置", t, func() {
err := s.SetSetting(context.TODO(), mid, &model.Setting{Type: model.PushTypeAttention})
convey.So(err, convey.ShouldBeNil)
})
}
func Test_ps(t *testing.T) {
initd()
url := "http://127.0.0.1:7031/x/push-archive/setting/get?access_key=848657e31639317257ab274741c6ec7d"
client := bm.NewClient(conf.Conf.HTTPClient)
type _response struct {
Code int `json:"code"`
Data model.Setting `json:"data"`
}
wg := sync.WaitGroup{}
all := 10
busyIn := 0
errIn := 0
normalIn := 0
mx := sync.Mutex{}
for i := 0; i < all; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
defer mx.Unlock()
req, err := ht.NewRequest("GET", url, nil)
if err != nil {
mx.Lock()
errIn++
return
}
r := &_response{}
err = client.Do(context.TODO(), req, r)
mx.Lock()
if err != nil {
errIn++
return
}
if r.Code == ecode.ServiceUnavailable.Code() {
busyIn++
return
}
normalIn++
}(i)
}
wg.Wait()
convey.Convey("并行访问推送开关,有频率限制", t, func() {
convey.So(errIn, convey.ShouldEqual, 0)
convey.So(busyIn, convey.ShouldBeGreaterThan, 0)
convey.So(errIn+busyIn+normalIn, convey.ShouldEqual, all)
})
t.Logf("the errin(%d), busyin(%d), normalin(%d)", errIn, busyIn, normalIn)
}
func Test_pushrpc(t *testing.T) {
initd()
convey.Convey("推送平台rpc设置推送开关", t, func() {
set := &pb.SetSettingRequest{
Mid: int64(111111111),
Type: 1,
Value: 3,
}
res, err := s.pushRPC.Setting(context.TODO(), &pb.SettingRequest{Mid: set.Mid})
convey.So(err, convey.ShouldBeNil)
t.Logf("setting(%+v)", res)
_, err = s.pushRPC.SetSetting(context.TODO(), set)
convey.So(err, convey.ShouldBeNil)
res, err = s.pushRPC.Setting(context.TODO(), &pb.SettingRequest{Mid: set.Mid})
convey.So(err, convey.ShouldBeNil)
t.Logf("setting(%+v)", res)
})
}

View File

@@ -0,0 +1,142 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"go-common/app/interface/main/push-archive/dao"
"go-common/app/interface/main/push-archive/model"
"go-common/library/log"
)
// 切割需要存储的统计数据
func (s *Service) formStatisticsProc(aid int64, group string, included []int64, excluded *[]int64) {
params := model.NewBatchParam(map[string]interface{}{
"aid": aid,
"group": group,
"type": model.StatisticsPush,
"createdTime": time.Now(),
}, nil)
dao.Batch(&included, 1000, 2, params, s.formPushStatistic)
params.Params["type"] = model.StatisticsUnpush
dao.Batch(excluded, 1000, 2, params, s.formPushStatistic)
}
// 组建统计对象
func (s *Service) formPushStatistic(fans *[]int64, params map[string]interface{}) (err error) {
ln := len(*fans)
b, err := json.Marshal(*fans)
if err != nil {
log.Error("formStatistic json.Marshal error(%v) fans(%v) params(%v)", err, fans, params)
return
}
ps := &model.PushStatistic{
Aid: params["aid"].(int64),
Group: params["group"].(string),
Type: params["type"].(int),
Mids: string(b),
MidsCounter: ln,
CTime: params["createdTime"].(time.Time),
}
if err = s.dao.AddStatisticsCache(context.TODO(), ps); err != nil {
log.Error("formPushStatistic s.dao.AddStatisticsCache error(%v), pushstatistic(%v)", err, ps)
return
}
return
}
// 每日定时清除推送的统计数据,只保留最近几天的数据
func (s *Service) clearStatisticsProc() {
for {
// 到指定时间
clearTime, err := s.getTodayTime(s.c.ArcPush.PushStatisticsClearTime)
if err != nil {
log.Error("clearStatisticsProc getTodayTime(%s) error(%v)", s.c.ArcPush.PushStatisticsClearTime, err)
continue
}
dur := clearTime.Unix() - time.Now().Unix()
if dur < 0 || dur > 60 {
time.Sleep(time.Second * 50)
continue
}
// 需要删除数据的最大时间
time.Sleep(time.Second * 60)
deadline, err := s.getDeadline()
if err != nil {
log.Error("clearStatisticsProc getDeadline error(%v)", err)
continue
}
log.Info("start to clear statistics before deadline(%s)", deadline.Format("2006-01-02 15:04:05"))
var min, max, mid int64
for i := 0; i < 3; i++ {
if min == 0 && max == 0 {
min, max, err = s.dao.GetStatisticsIDRange(context.TODO(), deadline)
mid = min
}
for err == nil && mid < max {
min = mid
mid = min + 5000
if mid > max {
mid = max
}
_, err = s.dao.DelStatisticsByID(context.TODO(), min, mid)
time.Sleep(time.Second)
}
if err == nil {
log.Info("success end clear statistics before deadline(%s)", deadline.Format("2006-01-02 15:04:05"))
break
}
}
if err != nil {
s.dao.WechatMessage(fmt.Sprintf("clearStatisticsProc: push-archive failed to clear expired(%s) push_statistics, error(%v)", deadline.Format("2006-01-02 15:04:05"), err))
}
}
}
// 获取需要删除数据的 最大时间
func (s *Service) getDeadline() (deadline time.Time, err error) {
dd := "00:00:00"
today, err := s.getTodayTime(dd)
if err != nil {
log.Error("clearStatisticsProc getTodayTime(%s) error(%v)", dd, err)
return
}
deadline = today.AddDate(0, 0, -1*s.c.ArcPush.PushStatisticsKeepDays+1)
return
}
// 统计数据落库
func (s *Service) saveStatisticsProc() {
defer s.wg.Done()
for {
select {
case _, ok := <-s.CloseCh:
if !ok {
log.Info("CloseCh is closed, close the saveStatisticsProc")
return
}
default:
}
ps, err := s.dao.GetStatisticsCache(context.TODO())
if err != nil {
log.Error("saveStatisticsProc s.dao.GetStatisticsCache error(%v)", err)
time.Sleep(time.Millisecond * 100)
continue
}
if ps == nil {
time.Sleep(time.Millisecond * 100)
continue
}
if _, err := s.dao.SetStatistics(context.TODO(), ps); err != nil {
log.Error("saveStatisticsProc s.dao.SetStatistics error(%v) pushstatistic(%v)", err, ps)
s.dao.AddStatisticsCache(context.TODO(), ps)
}
time.Sleep(time.Millisecond * 100)
}
}