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/service/main/push-strategy/cmd:all-srcs",
"//app/service/main/push-strategy/conf:all-srcs",
"//app/service/main/push-strategy/dao:all-srcs",
"//app/service/main/push-strategy/http:all-srcs",
"//app/service/main/push-strategy/model:all-srcs",
"//app/service/main/push-strategy/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,67 @@
## 推送策略
### v1.4.5
1. mid文件上传目录增加目录层级防止同一个目录下文件数过多
### v1.4.4
1. 稿件特殊关注不限制条数
### v1.4.3
1. cache size 可配
### v1.4.2
1. 加日志
### v1.4.1
1. 加cd日志
### v1.4.0
1. 各业务间加cd时间控制用户收消息的频率
### v1.3.0
1. 每日收到消息条数限制分APP
### v1.2.5
1. 规范dao ut
### v1.2.4
1. 支持图片推送字段
### v1.2.3
1. 修复判断用户业务开关的问题
### v1.2.2
1. use bm verify middleware
### v1.2.0
1. 推送服务切换到push-service
### v1.1.1
1. 修复免打扰时间判断问题
### v1.1.0
1. 新建任务时候mid异步写
### v1.0.7
1. 调整addTask接口uuid参数处理逻辑
### v1.0.6
1. 迁移model至push-service
### v1.0.5
1. 更改jobName生成规则
### v1.0.4
1. 业务方token参数放到header中
### v1.0.3
1. load任务间隔加sleep
### v1.0.2
1. 分批获取用户开关设置
### v1.0.1
1. 优化拆分任务逻辑,并发处理
### v1.0.0
1. 用户推送频率限制

View File

@@ -0,0 +1,9 @@
# Owner
renwei
zhapuyu
# Author
wangjian
# Reviewer
zhapuyu

View File

@@ -0,0 +1,15 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- renwei
- wangjian
- zhapuyu
labels:
- main
- service
- service/main/push-strategy
options:
no_parent_owners: true
reviewers:
- wangjian
- zhapuyu

View File

@@ -0,0 +1,10 @@
## push-strategy
### 项目简介
做一些推送策略,比如推送频率限制,推送逻辑等
### 编译环境
> 请只用golang v1.8.x以上版本编译执行。
### 依赖包
> 1.公共包go-common

View File

@@ -0,0 +1,44 @@
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-strategy-test.toml"],
importpath = "go-common/app/service/main/push-strategy/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push-strategy/http:go_default_library",
"//app/service/main/push-strategy/service:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,49 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/service/main/push-strategy/conf"
"go-common/app/service/main/push-strategy/http"
"go-common/app/service/main/push-strategy/service"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
trace.Init(conf.Conf.Tracer)
defer trace.Close()
log.Info("push-strategystart")
ecode.Init(conf.Conf.Ecode)
srv := service.New(conf.Conf)
http.Init(conf.Conf, srv)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("push-strategy get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
srv.Close()
time.Sleep(1 * time.Second)
log.Info("push-strategyexit")
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}

View File

@@ -0,0 +1,88 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/push-strategy.pid"
dir = "./"
family = "push-strategy"
[log]
dir = "/data/log/push-strategy/"
[httpClient]
key = "f265dcfa28272742"
secret = "437facc22dc8698b5544669bcc12348d"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
timer = 10
[httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[HTTPServer]
addr = "0.0.0.0:7561"
maxListen = 1000
timeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
[mysql]
addr = "172.16.33.205"
dsn = "test:test@tcp(172.16.33.205:3308)/bilibili_push?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
[redis]
name = "push-strategy"
proto = "tcp"
addr = "172.18.33.60:6968"
idle = 1000
active = 10000
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "30s"
limitDayExpire = "24h"
[memcache]
name = "push-strategy"
proto = "tcp"
addr = "172.18.33.60:11228"
idle = 1000
active = 1000
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "30s"
uuidExpire = "30m"
cdExpire = "1h"
[wechat]
token = "GYQeuDWBbAsCNeGz"
secret = "ZKpmgINTkianyMbMixyxcPQjMCSHCDrk"
username = "xxxxxxxxyyyyyyyyyy"
[bizID]
live = 1
archive = 2
[cfg]
loadTaskInteval = "1s"
loadBusinessInteval = "5m"
loadSettingsInteval = "1m"
nasPath = "/data/storage"
limitUserPerDay = 8
handleTaskGoroutines = 100
handleMidGoroutines = 5
cacheSize = 1024000

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 = ["conf.go"],
importpath = "go-common/app/service/main/push-strategy/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/rpc:go_default_library",
"//library/net/trace: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,119 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
xsql "go-common/library/database/sql"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
xhttp "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/rpc"
"go-common/library/net/trace"
xtime "go-common/library/time"
"github.com/BurntSushi/toml"
)
// Conf info.
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config struct.
type Config struct {
Ecode *ecode.Config
Verify *verify.Config
MySQL *xsql.Config
Log *log.Config
HTTPServer *xhttp.ServerConfig
HTTPClient *xhttp.ClientConfig
FilterRPC *rpc.ClientConfig
Tracer *trace.Config
Redis *rds
Memcache *mc
Wechat *wechat
Cfg *cfg
BizID *bizid
}
type rds struct {
*redis.Config
LimitDayExpire xtime.Duration
}
type mc struct {
*memcache.Config
UUIDExpire xtime.Duration
CDExpire xtime.Duration
}
type wechat struct {
Token string
Secret string
Username string
}
type bizid struct {
Live int
Archive int
}
type cfg struct {
LoadTaskInteval xtime.Duration
LoadBusinessInteval xtime.Duration
LoadSettingsInteval xtime.Duration
NASPath string
LimitUserPerDay int
HandleTaskGoroutines int
HandleMidGoroutines int
CacheSize int
}
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
}
err = load()
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,65 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"dao_test.go",
"memcache_test.go",
"mysql_test.go",
"redis_test.go",
"wechat_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
"//vendor/gopkg.in/h2non/gock.v1:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"dao.go",
"memcache.go",
"mysql.go",
"redis.go",
"wechat.go",
],
importpath = "go-common/app/service/main/push-strategy/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis: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",
],
)
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,85 @@
package dao
import (
"context"
"time"
"go-common/app/service/main/push-strategy/conf"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
xsql "go-common/library/database/sql"
xhttp "go-common/library/net/http/blademaster"
"go-common/library/stat/prom"
)
// Dao .
type Dao struct {
c *conf.Config
db *xsql.DB
redis *redis.Pool
mc *memcache.Pool
httpClient *xhttp.Client
appsStmt *xsql.Stmt
businessesStmt *xsql.Stmt
addTaskStmt *xsql.Stmt
taskStmt *xsql.Stmt
settingsByRangeStmt *xsql.Stmt
maxSettingIDStmt *xsql.Stmt
mcUUIDExpire int32
mcCDExpire int32
redisLimitDayExpire int32
}
// New new dao.
func New(c *conf.Config) (d *Dao) {
d = &Dao{
c: c,
db: xsql.NewMySQL(c.MySQL),
httpClient: xhttp.NewClient(c.HTTPClient),
redis: redis.NewPool(c.Redis.Config),
mc: memcache.NewPool(c.Memcache.Config),
mcUUIDExpire: int32(time.Duration(c.Memcache.UUIDExpire) / time.Second),
mcCDExpire: int32(time.Duration(c.Memcache.CDExpire) / time.Second),
redisLimitDayExpire: int32(time.Duration(c.Redis.LimitDayExpire) / time.Second),
}
d.appsStmt = d.db.Prepared(_appsSQL)
d.businessesStmt = d.db.Prepared(_businessesSQL)
d.addTaskStmt = d.db.Prepared(_addTaskSQL)
d.taskStmt = d.db.Prepared(_taskSQL)
d.settingsByRangeStmt = d.db.Prepared(_settingsByRangeSQL)
d.maxSettingIDStmt = d.db.Prepared(_maxSettingIDSQL)
return
}
// PromError prom error
func PromError(name string) {
prom.BusinessErrCount.Incr(name)
}
// PromInfo add prom info
func PromInfo(name string) {
prom.BusinessInfoCount.Incr(name)
}
// BeginTx begin transaction.
func (d *Dao) BeginTx(ctx context.Context) (*xsql.Tx, error) {
return d.db.Begin(ctx)
}
// Ping .
func (d *Dao) Ping(ctx context.Context) (err error) {
if err = d.pingRedis(ctx); err != nil {
return
}
if err = d.pingMC(ctx); err != nil {
return
}
return d.db.Ping(ctx)
}
// Close .
func (d *Dao) Close() {
d.mc.Close()
d.redis.Close()
d.db.Close()
}

View File

@@ -0,0 +1,39 @@
package dao
import (
"flag"
"os"
"path/filepath"
"testing"
"go-common/app/service/main/push-strategy/conf"
"gopkg.in/h2non/gock.v1"
)
var d *Dao
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.web-svr.push-strategy")
flag.Set("conf_token", "a626fc404c86f14654f0a74d80fc1da3")
flag.Set("tree_id", "26092")
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 {
dir, _ := filepath.Abs("../cmd/push-strategy-test.toml")
flag.Set("conf", dir)
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
d.httpClient.SetTransport(gock.DefaultTransport)
m.Run()
os.Exit(0)
}

View File

@@ -0,0 +1,115 @@
package dao
import (
"context"
"fmt"
"go-common/library/cache/memcache"
"go-common/library/log"
)
const (
_prefixUUID = "uuid_%d_%s"
_prefixCD = "cd_%d_%d"
)
func uuidKey(biz int64, uuid string) string {
return fmt.Sprintf(_prefixUUID, biz, uuid)
}
func cdKey(app, mid int64) string {
return fmt.Sprintf(_prefixCD, app, mid)
}
// pingMc ping memcache
func (d *Dao) pingMC(c context.Context) (err error) {
conn := d.mc.Get(c)
defer conn.Close()
item := memcache.Item{Key: "ping", Value: []byte{1}, Expiration: d.mcUUIDExpire}
err = conn.Set(&item)
return
}
// ExistsUUIDCache gets uuid from cache.
func (d *Dao) ExistsUUIDCache(c context.Context, biz int64, uuid string) (exist bool, err error) {
var (
conn = d.mc.Get(c)
key = uuidKey(biz, uuid)
)
defer conn.Close()
if _, err = conn.Get(key); err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
PromError("mc:get uuid")
log.Error("ExistsUUIDCache() conn.Get(%s) error(%v)", key, err)
return
}
exist = true
return
}
// AddUUIDCache adds uuid cache.
func (d *Dao) AddUUIDCache(c context.Context, biz int64, uuid string) (err error) {
var (
conn = d.mc.Get(c)
key = uuidKey(biz, uuid)
item = &memcache.Item{Key: key, Value: []byte{}, Expiration: d.mcUUIDExpire}
)
defer conn.Close()
if err = conn.Set(item); err != nil {
PromError("mc:add uuid")
log.Error("AddUUIDCache() conn.Set(%+v) error(%v)", item, err)
}
return
}
// DelUUIDCache delete uuid cache.
func (d *Dao) DelUUIDCache(c context.Context, biz int64, uuid string) (err error) {
var (
conn = d.mc.Get(c)
key = uuidKey(biz, uuid)
)
defer conn.Close()
if err = conn.Delete(key); err != nil {
PromError("mc:del uuid")
log.Error("DelUUIDCache(%s) error(%v)", key, err)
}
return
}
// ExistsCDCache gets cd from cache.
func (d *Dao) ExistsCDCache(ctx context.Context, app, mid int64) (exist bool, err error) {
var (
conn = d.mc.Get(ctx)
key = cdKey(app, mid)
)
defer conn.Close()
if _, err = conn.Get(key); err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
PromError("mc:get cd")
log.Error("ExistsCDCache() conn.Get(%s) error(%v)", key, err)
return
}
exist = true
return
}
// AddCDCache adds cd cache.
func (d *Dao) AddCDCache(ctx context.Context, app, mid int64) (err error) {
var (
conn = d.mc.Get(ctx)
key = cdKey(app, mid)
item = &memcache.Item{Key: key, Value: []byte{}, Expiration: d.mcCDExpire}
)
defer conn.Close()
if err = conn.Set(item); err != nil {
PromError("mc:add cd")
log.Error("AddCDCache() conn.Set(%+v) error(%v)", item, err)
}
return
}

View File

@@ -0,0 +1,56 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
var (
biz = int64(1)
uuid = "uuid"
)
func TestDaopingMC(t *testing.T) {
convey.Convey("pingMC", t, func(ctx convey.C) {
err := d.pingMC(context.Background())
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func addUUIDCache() error {
return d.AddUUIDCache(context.Background(), biz, uuid)
}
func TestDaoExistsUUIDCache(t *testing.T) {
addUUIDCache()
convey.Convey("ExistsUUIDCache", t, func(ctx convey.C) {
exist, err := d.ExistsUUIDCache(context.Background(), biz, uuid)
ctx.Convey("Then err should be nil.exist should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(exist, convey.ShouldNotBeNil)
})
})
}
func TestDaoAddUUIDCache(t *testing.T) {
convey.Convey("AddUUIDCache", t, func(ctx convey.C) {
err := addUUIDCache()
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoDelUUIDCache(t *testing.T) {
addUUIDCache()
convey.Convey("DelUUIDCache", t, func(ctx convey.C) {
err := d.DelUUIDCache(context.Background(), biz, uuid)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}

View File

@@ -0,0 +1,178 @@
package dao
import (
"context"
"database/sql"
"encoding/json"
"strconv"
"time"
pushmdl "go-common/app/service/main/push/model"
xsql "go-common/library/database/sql"
"go-common/library/log"
)
const (
// task
_addTaskSQL = "INSERT INTO push_tasks (job,type,app_id,business_id,platform,title,summary,link_type,link_value,build,sound,vibration,pass_through,mid_file,progress,push_time,expire_time,status,`group`,image_url,extra) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
_taskSQL = "SELECT id,job,type,app_id,business_id,platform,title,summary,link_type,link_value,build,sound,vibration,pass_through,mid_file,progress,push_time,expire_time,status,`group`,image_url,extra FROM push_tasks WHERE id=?"
// app && business
_appsSQL = "SELECT id,name,push_limit_user FROM push_apps WHERE dtime=0"
_businessesSQL = "SELECT id,app_id,name,description,token,sound,vibration,receive_switch,push_switch,silent_time,push_limit_user,whitelist FROM push_business WHERE dtime=0"
// setting
_settingsByRangeSQL = "SELECT mid,value FROM push_user_settings WHERE id>? AND id<=? AND dtime=0"
_maxSettingIDSQL = "SELECT MAX(id) FROM push_user_settings"
)
// Apps get all app info
func (d *Dao) Apps(ctx context.Context) (res map[int64]*pushmdl.APP, err error) {
rows, err := d.appsStmt.Query(ctx)
if err != nil {
log.Error("d.appsStmt.Query() error(%v)", err)
PromError("mysql:查询应用")
return
}
defer rows.Close()
res = make(map[int64]*pushmdl.APP)
for rows.Next() {
app := new(pushmdl.APP)
if err = rows.Scan(&app.ID, &app.Name, &app.PushLimitUser); err != nil {
log.Error("d.Apps() Scan() error(%v)", err)
PromError("mysql:查询应用Scan")
return
}
res[app.ID] = app
}
err = rows.Err()
return
}
// Businesses gets all business info.
func (d *Dao) Businesses(ctx context.Context) (res map[int64]*pushmdl.Business, err error) {
rows, err := d.businessesStmt.Query(ctx)
if err != nil {
log.Error("d.businessesStmt.Query() error(%v)", err)
PromError("mysql:查询业务方")
return
}
defer rows.Close()
res = make(map[int64]*pushmdl.Business)
for rows.Next() {
var (
silentTime string
b = &pushmdl.Business{}
)
if err = rows.Scan(&b.ID, &b.APPID, &b.Name, &b.Desc, &b.Token,
&b.Sound, &b.Vibration, &b.ReceiveSwitch, &b.PushSwitch, &silentTime, &b.PushLimitUser, &b.Whitelist); err != nil {
PromError("mysql:查询业务方Scan")
log.Error("d.Business() Scan() error(%v)", err)
return
}
b.SilentTime = pushmdl.ParseSilentTime(silentTime)
res[b.ID] = b
}
err = rows.Err()
return
}
// AddTask adds task.
func (d *Dao) AddTask(ctx context.Context, t *pushmdl.Task) (id int64, err error) {
var (
res sql.Result
platform = pushmdl.JoinInts(t.Platform)
build, _ = json.Marshal(t.Build)
progress, _ = json.Marshal(t.Progress)
extra, _ = json.Marshal(t.Extra)
)
if res, err = d.addTaskStmt.Exec(ctx, t.Job, t.Type, t.APPID, t.BusinessID, platform, t.Title, t.Summary, t.LinkType, t.LinkValue,
build, t.Sound, t.Vibration, t.PassThrough, t.MidFile, progress, t.PushTime, t.ExpireTime, t.Status, t.Group, t.ImageURL, extra); err != nil {
log.Error("d.AddTask(%+v) error(%v)", t, err)
PromError("mysql:添加推送任务")
return
}
id, err = res.LastInsertId()
return
}
// Task loads task by id.
func (d *Dao) Task(ctx context.Context, id int64) (t *pushmdl.Task, err error) {
var (
platform string
build string
progress string
extra string
now = time.Now()
)
t = &pushmdl.Task{Progress: &pushmdl.Progress{}, Extra: &pushmdl.TaskExtra{}}
if err = d.taskStmt.QueryRow(ctx, id).Scan(&id, &t.Job, &t.Type, &t.APPID, &t.BusinessID, &platform, &t.Title, &t.Summary, &t.LinkType, &t.LinkValue, &build,
&t.Sound, &t.Vibration, &t.PassThrough, &t.MidFile, &progress, &t.PushTime, &t.ExpireTime, &t.Status, &t.Group, &t.ImageURL, &extra); err != nil {
if err == sql.ErrNoRows {
t = nil
err = nil
return
}
log.Error("d.taskStmt.QueryRow(%d) error(%v)", id, now, err)
PromError("mysql:按ID查询任务")
return
}
t.ID = strconv.FormatInt(id, 10)
t.Platform = pushmdl.SplitInts(platform)
t.Build = pushmdl.ParseBuild(build)
if progress != "" {
if err = json.Unmarshal([]byte(progress), t.Progress); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", progress, err)
PromError("mysql:unmarshal progress")
}
}
if extra != "" {
if err = json.Unmarshal([]byte(extra), t.Extra); err != nil {
log.Error("json.Unmarshal(%s) extra error(%v)", extra, err)
PromError("mysql:unmarshal extra")
}
}
return
}
// MaxSettingID gets max setting id in DB.
func (d *Dao) MaxSettingID(ctx context.Context) (id int64, err error) {
if err = d.maxSettingIDStmt.QueryRow(ctx).Scan(&id); err != nil {
if err == sql.ErrNoRows {
err = nil
return
}
log.Error("d.maxSettingIDStmt.QueryRow.Scan error(%v)", err)
PromError("db:max setting id")
}
return
}
// SettingsByRange gets user setting by range.
func (d *Dao) SettingsByRange(ctx context.Context, start, end int64) (res map[int64]map[int]int, err error) {
var rows *xsql.Rows
if rows, err = d.settingsByRangeStmt.Query(ctx, start, end); err != nil {
log.Error("d.settingsStmt.Query(%d,%d) error(%v)", start, end, err)
PromError("mysql:Settings")
return
}
defer rows.Close()
res = make(map[int64]map[int]int)
for rows.Next() {
var (
mid int64
v string
st = make(map[int]int)
)
if err = rows.Scan(&mid, &v); err != nil {
PromError("mysql:Settings scan")
log.Error("d.Settings() Scan() error(%v)", err)
return
}
if e := json.Unmarshal([]byte(v), &st); e != nil {
log.Error("d.Settings() json unmarshal(%s) error(%v)", v, st)
continue
}
res[mid] = st
}
err = rows.Err()
return
}

View File

@@ -0,0 +1,69 @@
package dao
import (
"context"
"testing"
pushmdl "go-common/app/service/main/push/model"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoBusinesses(t *testing.T) {
convey.Convey("Businesses", t, func(ctx convey.C) {
res, err := d.Businesses(context.Background())
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}
func addTask() (id int64, err error) {
t := &pushmdl.Task{APPID: 1}
return d.AddTask(context.Background(), t)
}
func TestDaoAddTask(t *testing.T) {
convey.Convey("AddTask", t, func(ctx convey.C) {
id, err := addTask()
ctx.Convey("Then err should be nil.id should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(id, convey.ShouldBeGreaterThan, 0)
})
})
}
func TestDaoTask(t *testing.T) {
id, _ := addTask()
convey.Convey("Task", t, func(ctx convey.C) {
no, err := d.Task(context.Background(), id)
ctx.Convey("Then err should be nil.no should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(no, convey.ShouldNotBeNil)
})
})
}
func TestDaoMaxSettingID(t *testing.T) {
convey.Convey("MaxSettingID", t, func(ctx convey.C) {
_, err := d.MaxSettingID(context.Background())
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaoSettingsByRange(t *testing.T) {
var (
start = int64(0)
end = int64(1000)
)
convey.Convey("SettingsByRange", t, func(ctx convey.C) {
res, err := d.SettingsByRange(context.Background(), start, end)
ctx.Convey("Then err should be nil.res should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(res, convey.ShouldNotBeNil)
})
})
}

View File

@@ -0,0 +1,155 @@
package dao
import (
"context"
"fmt"
"go-common/library/cache/redis"
"go-common/library/log"
)
const (
_prefixLimitDay = "ld_%d_%d_%s"
_prefixLimitBiz = "lb_%d_%d_%s_%d"
_prefixLimitNotLive = "lnl_%d_%s"
)
func limitDayKey(day string, app, mid int64) string {
return fmt.Sprintf(_prefixLimitDay, app, mid, day)
}
func limitBizKey(day string, app, mid, biz int64) string {
return fmt.Sprintf(_prefixLimitBiz, app, mid, day, biz)
}
func limitNotLiveKey(day string, mid int64) string {
return fmt.Sprintf(_prefixLimitNotLive, mid, day)
}
// pingRedis ping redis.
func (d *Dao) pingRedis(ctx context.Context) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
if _, err = conn.Do("SET", "PING", "PONG"); err != nil {
PromError("redis: ping remote")
log.Error("remote redis: conn.Do(SET,PING,PONG) error(%v)", err)
}
return
}
// LimitDayCache gets limit cache by day & mid.
// 测试用,业务用不着
func (d *Dao) LimitDayCache(ctx context.Context, day string, app, mid int64) (count int, err error) {
var (
key = limitDayKey(day, app, mid)
conn = d.redis.Get(ctx)
)
defer conn.Close()
if count, err = redis.Int(conn.Do("GET", key)); err != nil {
PromError("redis:LimitDayCache")
log.Error("LimitDayCache(%s,%d,%d) error(%v)", day, app, mid, err)
}
return
}
// IncrLimitDayCache increases and gets limit cache by day & mid.
func (d *Dao) IncrLimitDayCache(ctx context.Context, day string, app, mid int64) (count int, err error) {
var (
key = limitDayKey(day, app, mid)
conn = d.redis.Get(ctx)
)
defer conn.Close()
if err = conn.Send("INCR", key); err != nil {
PromError("redis:IncrLimitDayCache")
log.Error("IncrLimitDayCache(%s,%d,%d) error(%v)", day, app, mid, err)
return
}
if err = conn.Send("EXPIRE", key, d.redisLimitDayExpire); err != nil {
PromError("redis:IncrLimitDayCache:expire")
log.Error("IncrLimitDayCache(%s,%d,%d) expire error(%v)", day, app, mid, err)
return
}
if err = conn.Flush(); err != nil {
PromError("redis:IncrLimitDayCache:flush")
log.Error("IncrLimitDayCache(%s,%d,%d) flush error(%v)", day, app, mid, err)
return
}
if count, err = redis.Int(conn.Receive()); err != nil {
PromError("redis:IncrLimitDayCache:receive:incr")
log.Error("IncrLimitDayCache(%s,%d,%d) receive incr error(%+v)", day, app, mid, err)
return
}
if _, err = conn.Receive(); err != nil {
PromError("redis:IncrLimitDayCache:receive:expire")
log.Error("IncrLimitDayCache(%s,%d,%d) receive expire error(%+v)", day, app, mid, err)
}
return
}
// IncrLimitBizCache increases and gets limit cache by day & mid & bisiness.
func (d *Dao) IncrLimitBizCache(ctx context.Context, day string, app, mid, biz int64) (count int, err error) {
var (
key = limitBizKey(day, app, mid, biz)
conn = d.redis.Get(ctx)
)
defer conn.Close()
if err = conn.Send("INCR", key); err != nil {
PromError("redis:IncrLimitBizCache")
log.Error("IncrLimitBizCache(%s,%d,%d,%d) error(%v)", day, app, mid, biz, err)
return
}
if err = conn.Send("EXPIRE", key, d.redisLimitDayExpire); err != nil {
PromError("redis:IncrLimitBizCache:expire")
log.Error("IncrLimitBizCache(%s,%d,%d,%d) expire error(%v)", day, app, mid, biz, err)
return
}
if err = conn.Flush(); err != nil {
PromError("redis:IncrLimitBizCache:flush")
log.Error("IncrLimitBizCache(%s,%d,%d,%d) flush error(%v)", day, app, mid, biz, err)
return
}
if count, err = redis.Int(conn.Receive()); err != nil {
PromError("redis:IncrLimitBizCache:receive:incr")
log.Error("IncrLimitBizCache(%s,%d,%d,%d) receive incr error(%+v)", day, app, mid, biz, err)
return
}
if _, err = conn.Receive(); err != nil {
PromError("redis:IncrLimitBizCache:receive:expire")
log.Error("IncrLimitBizCache(%s,%d,%d,%d) receive expire error(%+v)", day, app, mid, biz, err)
}
return
}
// IncrLimitNotLiveCache increases and gets not live limit cache by day & mid.
func (d *Dao) IncrLimitNotLiveCache(ctx context.Context, day string, mid int64) (count int, err error) {
var (
key = limitNotLiveKey(day, mid)
conn = d.redis.Get(ctx)
)
defer conn.Close()
if err = conn.Send("INCR", key); err != nil {
PromError("redis:IncrLimitNotLiveCache")
log.Error("IncrLimitNotLiveCache(%s,%d) error(%v)", day, mid, err)
return
}
if err = conn.Send("EXPIRE", key, d.redisLimitDayExpire); err != nil {
PromError("redis:IncrLimitNotLiveCache:expire")
log.Error("IncrLimitNotLiveCache(%s,%d) expire error(%v)", day, mid, err)
return
}
if err = conn.Flush(); err != nil {
PromError("redis:IncrLimitNotLiveCache:flush")
log.Error("IncrLimitNotLiveCache(%s,%d) flush error(%v)", day, mid, err)
return
}
if count, err = redis.Int(conn.Receive()); err != nil {
PromError("redis:IncrLimitNotLiveCache:receive:incr")
log.Error("IncrLimitNotLiveCache(%s,%d) receive incr error(%+v)", day, mid, err)
return
}
if _, err = conn.Receive(); err != nil {
PromError("redis:IncrLimitNotLiveCache:receive:expire")
log.Error("IncrLimitNotLiveCache(%s,%d) receive expire error(%+v)", day, mid, err)
}
return
}

View File

@@ -0,0 +1,64 @@
package dao
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
var (
day = "20180808"
mid = int64(91221505)
app = int64(1)
)
func incrLimitDayCache() (int, error) {
return d.IncrLimitDayCache(context.Background(), day, app, mid)
}
func TestDaoLimitDayCache(t *testing.T) {
incrLimitDayCache()
convey.Convey("LimitDayCache", t, func(ctx convey.C) {
count, err := d.LimitDayCache(context.Background(), day, app, mid)
ctx.Convey("Then err should be nil.count should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldBeGreaterThan, 0)
})
})
}
func TestDaoIncrLimitDayCache(t *testing.T) {
convey.Convey("IncrLimitDayCache", t, func(ctx convey.C) {
count, err := incrLimitDayCache()
ctx.Convey("Then err should be nil.count should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldBeGreaterThan, 0)
})
})
}
func incrLimitNotLiveCache() (int, error) {
return d.IncrLimitNotLiveCache(context.Background(), day, mid)
}
func TestDaoIncrLimitBizCache(t *testing.T) {
incrLimitNotLiveCache()
convey.Convey("IncrLimitBizCache", t, func(ctx convey.C) {
count, err := d.IncrLimitBizCache(context.Background(), day, app, mid, biz)
ctx.Convey("Then err should be nil.count should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldBeGreaterThan, 0)
})
})
}
func TestDaoIncrLimitNotLiveCache(t *testing.T) {
convey.Convey("IncrLimitNotLiveCache", t, func(ctx convey.C) {
count, err := incrLimitNotLiveCache()
ctx.Convey("Then err should be nil.count should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(count, convey.ShouldBeGreaterThan, 0)
})
})
}

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"
)
type wechatResp struct {
Status int `json:"status"`
Msg string `json:"msg"`
}
const (
// http://info.bilibili.co/pages/viewpage.action?pageId=5406728
_url = "http://bap.bilibili.co/api/v1/message/add"
)
// SendWechat 发送企业微信消息
func (d *Dao) SendWechat(msg string) (err error) {
log.Error("SendWechat logged error(%s)", msg)
params := map[string]string{
"content": msg,
"timestamp": strconv.FormatInt(time.Now().Unix(), 10),
"token": d.c.Wechat.Token,
"type": "wechat",
"username": d.c.Wechat.Username,
"url": "",
}
params["signature"] = d.sign(params)
b, err := json.Marshal(params)
if err != nil {
log.Error("SendWechat json.Marshal error(%v)", err)
return
}
req, err := http.NewRequest(http.MethodPost, _url, bytes.NewReader(b))
if err != nil {
log.Error("SendWechat 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("SendWechat 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("SendWechat response error(%v), params(%s)", err, string(b))
}
return
}
func (d *Dao) sign(params map[string]string) string {
var 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()+d.c.Wechat.Secret)
return fmt.Sprintf("%x", h.Sum(nil))
}

View File

@@ -0,0 +1,27 @@
package dao
import (
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestDaoSendWechat(t *testing.T) {
msg := "push strategy test wechat message"
convey.Convey("SendWechat", t, func(ctx convey.C) {
err := d.SendWechat(msg)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
}
func TestDaosign(t *testing.T) {
params := map[string]string{"a": "b"}
convey.Convey("sign", t, func(ctx convey.C) {
p1 := d.sign(params)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeEmpty)
})
})
}

View File

@@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"http.go",
"task.go",
],
importpath = "go-common/app/service/main/push-strategy/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push-strategy/service:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/time:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,41 @@
package http
import (
"go-common/app/service/main/push-strategy/conf"
"go-common/app/service/main/push-strategy/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
idfSrv *verify.Verify
srv *service.Service
)
// Init .
func Init(c *conf.Config, svc *service.Service) {
srv = svc
idfSrv = verify.New(c.Verify)
engine := bm.DefaultServer(c.HTTPServer)
route(engine)
if err := engine.Start(); err != nil {
log.Error("engine.Start() error(%v)", err)
panic(err)
}
}
func route(e *bm.Engine) {
e.Ping(ping)
g := e.Group("/x/internal/push-strategy", idfSrv.Verify)
{
g.POST("/task/add", addTask)
}
}
func ping(ctx *bm.Context) {
if err := srv.Ping(ctx); err != nil {
ctx.Error = err
ctx.AbortWithStatus(503)
}
}

View File

@@ -0,0 +1,107 @@
package http
import (
"net/url"
"strconv"
"strings"
"time"
pushmdl "go-common/app/service/main/push/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
xtime "go-common/library/time"
)
func addTask(c *bm.Context) {
var (
id int64
err error
req = c.Request
auth = req.Header.Get("Authorization")
)
req.ParseMultipartForm(500 * 1024 * 1024) // 500M
appID, _ := strconv.ParseInt(req.FormValue("app_id"), 10, 64)
if appID < 1 {
log.Error("app_id is wrong: %s", req.FormValue("app_id"))
c.JSON(nil, ecode.RequestErr)
return
}
businessID, _ := strconv.ParseInt(req.FormValue("business_id"), 10, 64)
if businessID < 1 {
log.Error("business_id is wrong: %s", req.FormValue("business_id"))
c.JSON(nil, ecode.RequestErr)
return
}
platform := req.FormValue("platform")
am, err := url.ParseQuery(auth)
if err != nil {
log.Error("parse Authorization(%s) error(%v)", auth, err)
c.JSON(nil, ecode.RequestErr)
return
}
token := am.Get("token")
if token == "" {
log.Error("token is empty")
c.JSON(nil, ecode.RequestErr)
return
}
alertTitle := req.FormValue("alert_title")
alertBody := req.FormValue("alert_body")
if alertBody == "" {
log.Error("alert_body is empty")
c.JSON(nil, ecode.RequestErr)
return
}
mids := req.FormValue("mids")
if mids == "" {
log.Error("mids is empty")
c.JSON(nil, ecode.RequestErr)
return
}
linkType, _ := strconv.Atoi(req.FormValue("link_type"))
if linkType < 1 {
log.Error("link_type is wrong: %s", req.FormValue("link_type"))
c.JSON(nil, ecode.RequestErr)
return
}
linkValue := req.FormValue("link_value")
expireTime, _ := strconv.ParseInt(req.FormValue("expire_time"), 10, 64)
if expireTime == 0 {
expireTime = time.Now().Add(3 * 24 * time.Hour).Unix()
}
pushTime, _ := strconv.ParseInt(req.FormValue("push_time"), 10, 64)
if pushTime == 0 {
pushTime = time.Now().Unix()
}
passThrough, _ := strconv.Atoi(req.FormValue("pass_throught"))
builds := req.FormValue("builds")
group := req.FormValue("group")
uuid := req.FormValue("uuid")
imageURL := req.FormValue("image_url")
task := &pushmdl.Task{
Job: pushmdl.JobName(time.Now().UnixNano(), alertBody, linkValue, group),
Type: pushmdl.TaskTypeStrategyMid,
APPID: appID,
BusinessID: businessID,
Platform: pushmdl.SplitInts(platform),
Title: alertTitle,
Summary: alertBody,
LinkType: int8(linkType),
LinkValue: linkValue,
Build: pushmdl.ParseBuild(builds),
PassThrough: passThrough,
PushTime: xtime.Time(pushTime),
ExpireTime: xtime.Time(expireTime),
Status: pushmdl.TaskStatusPretreatmentPrepared,
Group: group,
ImageURL: imageURL,
Extra: &pushmdl.TaskExtra{Group: group},
}
log.Info("http add task(%d) uuid(%s) business(%d) link_value(%s) mids(%d)", task.Job, uuid, task.BusinessID, task.LinkValue, len(strings.Split(mids, ",")))
if id, err = srv.AddTask(c, uuid, token, task, mids); err != nil {
c.JSON(nil, err)
return
}
c.JSON(id, nil)
}

View File

@@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["model.go"],
importpath = "go-common/app/service/main/push-strategy/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//app/service/main/push/model: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,11 @@
package model
import (
pushmdl "go-common/app/service/main/push/model"
)
// MidChan .
type MidChan struct {
Task *pushmdl.Task
Data *string
}

View File

@@ -0,0 +1,57 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["service_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"service.go",
"task.go",
],
importpath = "go-common/app/service/main/push-strategy/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/filter/model/rpc:go_default_library",
"//app/service/main/filter/rpc/client:go_default_library",
"//app/service/main/push-strategy/conf:go_default_library",
"//app/service/main/push-strategy/dao:go_default_library",
"//app/service/main/push-strategy/model:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/sync/errgroup: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,160 @@
package service
import (
"context"
"sync"
"time"
filterrpc "go-common/app/service/main/filter/rpc/client"
"go-common/app/service/main/push-strategy/conf"
"go-common/app/service/main/push-strategy/dao"
"go-common/app/service/main/push-strategy/model"
pushmdl "go-common/app/service/main/push/model"
"go-common/library/cache"
"go-common/library/log"
)
// Service is service.
type Service struct {
c *conf.Config
dao *dao.Dao
wg sync.WaitGroup
cache *cache.Cache
apps map[int64]*pushmdl.APP
businesses map[int64]*pushmdl.Business
filterRPC *filterrpc.Service
closed bool
settings map[int64]map[int]int
midCh chan *model.MidChan
}
const (
_dbBatch = 50000
)
// New .
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
cache: cache.New(1, c.Cfg.CacheSize),
apps: make(map[int64]*pushmdl.APP),
businesses: make(map[int64]*pushmdl.Business),
filterRPC: filterrpc.New(c.FilterRPC),
settings: make(map[int64]map[int]int),
midCh: make(chan *model.MidChan, 5120),
}
s.loadApps()
s.loadBusiness()
s.loadUserSetting()
go s.loadAppsproc()
go s.loadBusinessproc()
go s.loadUserSettingproc()
for i := 0; i < s.c.Cfg.HandleMidGoroutines; i++ {
s.wg.Add(1)
go s.saveMidproc()
}
return s
}
func (s *Service) loadApps() (res map[int64]*pushmdl.APP, err error) {
if res, err = s.dao.Apps(context.Background()); err != nil {
return
}
if len(res) > 0 {
s.apps = res
}
return
}
func (s *Service) loadAppsproc() {
for {
if res, err := s.loadApps(); err != nil || len(res) == 0 {
log.Error("s.loadApps() no apps error(%v)", err)
time.Sleep(100 * time.Millisecond)
continue
}
time.Sleep(time.Duration(s.c.Cfg.LoadBusinessInteval))
}
}
func (s *Service) loadBusiness() (res map[int64]*pushmdl.Business, err error) {
if res, err = s.dao.Businesses(context.Background()); err != nil {
return
}
if len(res) > 0 {
s.businesses = res
}
return
}
func (s *Service) loadBusinessproc() {
for {
time.Sleep(time.Duration(s.c.Cfg.LoadBusinessInteval))
if res, err := s.loadBusiness(); err != nil || len(res) == 0 {
log.Error("s.loadBusiness() no business")
time.Sleep(time.Second)
}
}
}
func (s *Service) loadUserSettingproc() {
for {
time.Sleep(time.Duration(s.c.Cfg.LoadSettingsInteval))
if err := s.loadUserSetting(); err != nil {
log.Error("s.loadUserSetting() no settings")
time.Sleep(time.Second)
continue
}
}
}
func (s *Service) loadUserSetting() (err error) {
maxid, err := s.dao.MaxSettingID(context.Background())
if err != nil {
log.Error("s.dao.MaxSettingID() error(%v)", err)
return
}
log.Info("max setting id(%d)", maxid)
var (
ss map[int64]map[int]int
res = make(map[int64]map[int]int)
)
for i := int64(0); i <= maxid; i += _dbBatch {
for j := 0; j < _retry; j++ {
if ss, err = s.dao.SettingsByRange(context.Background(), i, i+_dbBatch); err == nil {
break
}
time.Sleep(10 * time.Millisecond)
}
if err != nil {
log.Error("s.dao.SEttingsByRange(%d,%d) error(%v)", i, i+_dbBatch, err)
return
}
if len(ss) == 0 {
continue
}
for mid, data := range ss {
res[mid] = data
}
}
if len(res) > 0 {
s.settings = res
}
log.Info("loadUserSetting count(%d)", len(res))
return
}
// Ping .
func (s *Service) Ping(c context.Context) (err error) {
err = s.dao.Ping(c)
return
}
// Close .
func (s *Service) Close() {
s.closed = true
close(s.midCh)
s.wg.Wait()
s.dao.Close()
}

View File

@@ -0,0 +1,68 @@
package service
import (
"flag"
"math"
"path/filepath"
"sync"
"testing"
"go-common/app/service/main/push-strategy/conf"
pushmdl "go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
var s *Service
func init() {
dir, _ := filepath.Abs("../cmd/push-strategy-test.toml")
flag.Set("conf", dir)
conf.Init()
s = New(conf.Conf)
}
func TestCheckMidBySetting(t *testing.T) {
var (
mutex sync.Mutex
biz = s.c.BizID.Archive
mid = int64(91221505)
)
Convey("checkMidBySetting", t, func() {
Convey("without user setting", func() {
mutex.Lock()
s.settings = make(map[int64]map[int]int)
res := s.checkMidBySetting(biz, mid)
So(res, ShouldBeTrue)
mutex.Unlock()
})
Convey("not in limited business list", func() {
mutex.Lock()
s.settings = map[int64]map[int]int{mid: make(map[int]int)}
res := s.checkMidBySetting(math.MaxInt32, mid)
So(res, ShouldBeTrue)
mutex.Unlock()
})
Convey("business no setting, should be pass", func() {
mutex.Lock()
s.settings = map[int64]map[int]int{mid: make(map[int]int)}
res := s.checkMidBySetting(biz, mid)
So(res, ShouldBeTrue)
mutex.Unlock()
})
Convey("switch on, should be pass", func() {
mutex.Lock()
s.settings = map[int64]map[int]int{mid: {pushmdl.UserSettingArchive: pushmdl.SwitchOn}}
res := s.checkMidBySetting(biz, mid)
So(res, ShouldBeTrue)
mutex.Unlock()
})
Convey("switch off, should not be pass", func() {
mutex.Lock()
s.settings = map[int64]map[int]int{mid: {pushmdl.UserSettingArchive: pushmdl.SwitchOff}}
res := s.checkMidBySetting(biz, mid)
So(res, ShouldBeFalse)
mutex.Unlock()
})
})
}

View File

@@ -0,0 +1,404 @@
package service
import (
"context"
"crypto/md5"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
filtermdl "go-common/app/service/main/filter/model/rpc"
"go-common/app/service/main/push-strategy/dao"
"go-common/app/service/main/push-strategy/model"
pushmdl "go-common/app/service/main/push/model"
"go-common/library/ecode"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
const (
_retry = 3
)
func (s *Service) saveMid(v *model.MidChan) (err error) {
select {
case s.midCh <- v:
default:
dao.PromError("mid chan full")
err = ecode.PushServiceBusyErr
s.cache.Save(func() { s.dao.SendWechat("mid chan full") })
}
log.Info("mid chan len(%d)", len(s.midCh))
return
}
func (s *Service) saveMidproc() {
defer s.wg.Done()
for {
v, ok := <-s.midCh
if !ok {
log.Info("saveMidproc() closed")
return
}
log.Info("saveMidproc consume job(%d)", v.Task.Job)
s.handleTask(v)
log.Info("saveMidproc done job(%d)", v.Task.Job)
}
}
func (s *Service) handleTask(mch *model.MidChan) (err error) {
var (
path string
mids []string
mlock sync.Mutex
group = errgroup.Group{}
lines = strings.Split(*mch.Data, ",")
)
n := len(lines) / s.c.Cfg.HandleTaskGoroutines
for i := 1; i <= s.c.Cfg.HandleTaskGoroutines; i++ {
end := n
if i == s.c.Cfg.HandleTaskGoroutines {
end = len(lines)
}
part := lines[:end]
lines = lines[end:]
group.Go(func() (e error) {
var list []string
for _, v := range part {
mid := strings.Trim(v, " \n\t\r")
valid := s.checkMid(mch.Task.APPID, mch.Task.BusinessID, mid)
if !valid {
dao.PromInfo("task:filtered mid")
continue
}
list = append(list, mid)
}
if len(list) > 0 {
mlock.Lock()
mids = append(mids, list...)
mlock.Unlock()
}
return nil
})
}
group.Wait()
if len(mids) == 0 {
log.Info("handleTask(%+v) no valid mid", mch.Task)
return
}
if path, err = s.saveMids(mids); err != nil {
log.Error("handleTask(%+v) saveMids(%d) error(%v)", mch.Task, len(mids), err)
s.cache.Save(func() { s.dao.SendWechat(fmt.Sprintf("handleTask(%d) saveMid error(%v)", mch.Task.Job, err)) })
return
}
mch.Task.MidFile = path
if err = s.saveTask(mch.Task); err != nil {
log.Error("handleTask(%+v) saveTask error(%v)", mch.Task, err)
s.cache.Save(func() { s.dao.SendWechat(fmt.Sprintf("handleTask(%d) saveTask error(%v)", mch.Task.Job, err)) })
}
return
}
func (s *Service) saveMids(mids []string) (path string, err error) {
name := strconv.FormatInt(time.Now().UnixNano(), 10) + mids[0]
data := []byte(strings.Join(mids, "\n"))
for i := 0; i < _retry; i++ {
if path, err = s.saveNASFile(name, data); err == nil {
break
}
dao.PromError("retry saveNASFile")
time.Sleep(100 * time.Millisecond)
}
if err != nil {
s.dao.SendWechat("saveMids error:" + err.Error())
}
return
}
func (s *Service) saveTask(t *pushmdl.Task) (err error) {
for i := 0; i < _retry; i++ {
if _, err = s.dao.AddTask(context.Background(), t); err == nil {
break
}
dao.PromError("retry saveTask")
time.Sleep(100 * time.Millisecond)
}
if err != nil {
s.dao.SendWechat("saveTask error:" + err.Error())
}
return
}
func (s *Service) checkMid(app, biz int64, midStr string) (valid bool) {
mid, _ := strconv.ParseInt(midStr, 10, 64)
if mid == 0 {
log.Warn("limited, mid(%s) parse error", midStr)
return
}
// 检查推送开关
if !s.checkMidBySetting(int(biz), mid) {
return
}
// 稿件特殊关注,不限制
if biz == 42 {
return true
}
// 检查CD时间
if !s.checkMidByCDTime(app, biz, mid) {
return
}
// 检查推送条数
if !s.checkMidByCount(app, biz, mid) {
return
}
// 添加CD时间标记
if err := s.cache.Save(func() { s.dao.AddCDCache(context.Background(), app, mid) }); err != nil {
log.Error("add cd cache app(%d) biz(%d) mid(%d) error(%v)", app, biz, mid, err)
}
log.Info("passed, app(%d) biz(%d) mid(%d)", app, biz, mid)
return true
}
// 判断用户自定义业务开关有没有关闭
// 如果关闭则返回 false, 没有开关设置信息则默认是开启的,返回 true
func (s *Service) checkMidBySetting(biz int, mid int64) bool {
st, ok := s.settings[mid]
if !ok || st == nil {
return true
}
if biz != s.c.BizID.Live && biz != s.c.BizID.Archive {
return true
}
var skey int
switch biz {
case s.c.BizID.Live:
skey = pushmdl.UserSettingLive
case s.c.BizID.Archive:
skey = pushmdl.UserSettingArchive
default:
return true
}
if i, ok := st[skey]; ok && i == pushmdl.SwitchOff {
log.Info("limited, mid(%d) switch off biz(%d)", mid, biz)
return false
}
return true
}
// 根据用户每日在不同业务中收到的消息数进行过滤
// 超过限制返回 false没有超过限制允许推送返回 true
func (s *Service) checkMidByCount(app, biz, mid int64) bool {
var (
err error
countDay int
countBiz int
countNotLive int
day = time.Now().Format("20060102")
)
// 用户在某个业务下的一天的计数
for i := 0; i < _retry; i++ {
if countBiz, err = s.dao.IncrLimitBizCache(context.Background(), day, app, mid, biz); err == nil {
break
}
time.Sleep(5 * time.Millisecond)
}
if err != nil {
log.Error("s.dao.IncrLimitBizCache(%s,%d,%d,%d) error(%v)", day, app, mid, biz, err)
return true
}
if countBiz > s.businesses[biz].PushLimitUser {
log.Info("limited, mid(%d) app(%d) business(%d) more than business limit, current(%d)", mid, app, biz, countBiz)
return false
}
// TODO 这是临时逻辑,看推送效果后期可能去掉
// !! 只有粉版APP做直播的特殊业务逻辑限制 !!
// 保证直播4条能推满,非直播业务的条数限制一下
if app == pushmdl.APPIDBBPhone && biz != int64(s.c.BizID.Live) {
for i := 0; i < _retry; i++ {
if countNotLive, err = s.dao.IncrLimitNotLiveCache(context.Background(), day, mid); err == nil {
break
}
time.Sleep(5 * time.Millisecond)
}
if err != nil {
log.Error("s.dao.IncrLimitNotLiveCache(%s,%d) error(%v)", day, mid, err)
return true
}
if countNotLive > s.apps[app].PushLimitUser-4 {
log.Info("limited, mid(%d) more than live remain", mid)
return false
}
}
// 每个用户每天的推送计数
for i := 0; i < _retry; i++ {
if countDay, err = s.dao.IncrLimitDayCache(context.Background(), day, app, mid); err == nil {
break
}
time.Sleep(5 * time.Millisecond)
}
if err != nil {
log.Error("s.dao.IncrLimitDayCache(%s,%d,%d) error(%v)", day, app, mid, err)
return true
}
if countDay > s.apps[app].PushLimitUser {
log.Warn("limited, mid(%d) app(%d) more than day limit, current(%d)", mid, app, countDay)
return false
}
return true
}
// 用户级别的消息数过滤
// 目的是控制用户在某一段时间内收到的消息数(消息频率)
func (s *Service) checkMidByCDTime(app, biz, mid int64) bool {
// 白名单,不做过滤
if s.businesses[biz].Whitelist == pushmdl.SwitchOn {
return true
}
exist, err := s.dao.ExistsCDCache(context.Background(), app, mid)
if err != nil {
log.Error("s.dao.ExistsCDCache(%d,%d) error(%v)", app, mid, err)
return true
}
// 在cd时间内
if exist {
log.Info("limited, app(%d) business(%d) mid(%d) in cd time", app, biz, mid)
return false
}
return true
}
// AddTask adds task.
func (s *Service) AddTask(c context.Context, uuid, token string, task *pushmdl.Task, mids string) (job int64, err error) {
if err = s.checkBusiness(task.BusinessID, token); err != nil {
log.Warn("checkBusiness task(%+v) error(%v)", task, err)
return
}
var exist bool
if exist, err = s.dao.ExistsUUIDCache(c, task.BusinessID, uuid); err == nil && exist {
log.Warn("AddTask(%d,%s,%s) uuid limited", task.BusinessID, task.Title, task.LinkValue)
err = ecode.PushUUIDErr
return
}
s.dao.AddUUIDCache(c, task.BusinessID, uuid)
// filter sensitive words in title & content and check uuid
group, errCtx := errgroup.WithContext(c)
group.Go(func() error {
if filtered, e := s.filter(errCtx, task.Title); e == nil && filtered != task.Title {
log.Error("AddTask(%s) title(%s) contains sensitive words(%s)", task.LinkValue, task.Title, filtered)
return ecode.PushSensitiveWordsErr
}
return nil
})
group.Go(func() error {
if filtered, e := s.filter(errCtx, task.Summary); e == nil && filtered != task.Summary {
log.Error("AddTask(%s) content(%s) contains sensitive words(%s)", task.LinkValue, task.Summary, filtered)
return ecode.PushSensitiveWordsErr
}
return nil
})
if err = group.Wait(); err != nil {
s.dao.DelUUIDCache(c, task.BusinessID, uuid)
return
}
b := s.businesses[task.BusinessID]
task.APPID = b.APPID
task.Sound = b.Sound
task.Vibration = b.Vibration
if err = s.saveMid(&model.MidChan{Task: task, Data: &mids}); err != nil {
return
}
dao.PromInfo("task:添加任务")
return task.Job, nil
}
func (s *Service) checkBusiness(id int64, token string) error {
b, ok := s.businesses[id]
if !ok {
log.Error("business is not exist. business(%d) token(%s)", id, token)
dao.PromError("service:业务方不存在")
return ecode.PushBizAuthErr
}
if token != b.Token {
log.Error("wrong token business(%d) token(%s) need(%s)", id, token, b.Token)
dao.PromError("service:业务方token错误")
return ecode.PushBizAuthErr
}
if b.PushSwitch == pushmdl.SwitchOff {
log.Error("business was forbidden. business(%d) token(%s)", id, token)
dao.PromError("service:业务方被禁止推送")
return ecode.PushBizForbiddenErr
}
// 校验免打扰时间
if inSilence(b.SilentTime) {
log.Warn("in silent time, forbidden. business(%d)", id)
return ecode.PushSilenceErr
}
return nil
}
func inSilence(st pushmdl.BusinessSilentTime) bool {
if st.BeginHour == st.EndHour && st.BeginMinute == st.EndMinute {
return false
}
now := time.Now()
stm := time.Date(now.Year(), now.Month(), now.Day(), st.BeginHour, st.BeginMinute, 0, 0, time.Local)
etm := time.Date(now.Year(), now.Month(), now.Day(), st.EndHour, st.EndMinute, 59, 999, time.Local)
if st.BeginHour > st.EndHour || (st.BeginHour == st.EndHour && st.BeginMinute > st.EndMinute) {
etm = time.Date(now.Year(), now.Month(), now.Day(), st.EndHour, st.EndMinute, 59, 999, time.Local).Add(24 * time.Hour)
}
if now.Unix() >= stm.Unix() && now.Unix() <= etm.Unix() {
return true
}
return false
}
// Filter filters sensitive words.
func (s *Service) filter(c context.Context, content string) (res string, err error) {
var (
filterRes *filtermdl.FilterRes
arg = filtermdl.ArgFilter{Area: "common", Message: content}
)
if filterRes, err = s.filterRPC.Filter(c, &arg); err != nil {
dao.PromError("push:过滤服务")
log.Error("s.filter(%s) error(%v)", content, err)
return
}
if filterRes.Level < 20 {
return content, nil
}
res = filterRes.Result
return
}
// saveNASFile writes data into NAS.
func (s *Service) saveNASFile(name string, data []byte) (path string, err error) {
name = fmt.Sprintf("%x", md5.Sum([]byte(name)))
dir := fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(s.c.Cfg.NASPath, "/"), time.Now().Format("20060102"), name[:2])
if _, err = os.Stat(dir); err != nil {
if !os.IsNotExist(err) {
log.Error("os.IsNotExist(%s) error(%v)", dir, err)
return
}
if err = os.MkdirAll(dir, 0777); err != nil {
log.Error("os.MkdirAll(%s) error(%v)", dir, err)
return
}
}
path = fmt.Sprintf("%s/%s", dir, name)
f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Error("s.saveNASFile(%s) OpenFile() error(%v)", path, err)
return
}
if _, err = f.Write(data); err != nil {
log.Error("s.saveNASFile(%s) f.Write() error(%v)", err)
return
}
if err = f.Close(); err != nil {
log.Error("s.saveNASFile(%s) f.Close() error(%v)", err)
}
return
}