Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/main/push/api/gorpc:all-srcs",
"//app/service/main/push/api/grpc/v1:all-srcs",
"//app/service/main/push/cmd:all-srcs",
"//app/service/main/push/conf:all-srcs",
"//app/service/main/push/dao:all-srcs",
"//app/service/main/push/model:all-srcs",
"//app/service/main/push/server/gorpc:all-srcs",
"//app/service/main/push/server/grpc:all-srcs",
"//app/service/main/push/server/http:all-srcs",
"//app/service/main/push/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,70 @@
# push-service
### v2.2.4
1. waitgroup
### v2.2.3
1. FCM click action
### v2.2.0
1. 接 FCM
### v2.1.0
1. 极光推送同步返回无效token
### v2.0.0
1. using gRPC
### v1.7.2
1. 去掉progress中的platforms分平台计数字段
### v1.7.1
1. 更新任务状态改成同步
### v1.7.0
1. 添加设备品牌推送计数信息
### v1.6.0
1. 去除信鸽
### v1.5.2
1. Android H5协议中url加转义
### v1.5.1
1. 增加自定义协议内容的推送类型
### v1.5.0
1. 添加极光送达回执
### v1.4.1
1. 添加上传图片接口
### v1.4.0
1. 支持图片推送
### v1.3.0
1. 上报token时支持更新操作
### v1.2.3
1. 小米callback加barStatus
### v1.2.2
1. use bm verify middleware
### v1.2.1
1. 支持批量token缓存写入
### v1.2.0
1. 按build号生成scheme
### v1.1.2
1. fix updateTaskProgress
### v1.1.1
1. RPC服务发现使用discovery
### v1.1.0
1. add RPC.AddMidProgress
### v1.0.0
1. 使用 go-common/env

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["client.go"],
importpath = "go-common/app/service/main/push/api/gorpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push/model:go_default_library",
"//library/net/rpc:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,106 @@
package gorpc
import (
"context"
"go-common/app/service/main/push/model"
"go-common/library/net/rpc"
)
const (
_addReport = "RPC.AddReport"
_delInvalidReports = "RPC.DelInvalidReports"
_delReport = "RPC.DelReport"
_addCallback = "RPC.AddCallback"
_addReportCache = "RPC.AddReportCache"
_addUserReportCache = "RPC.AddUserReportCache"
_setting = "RPC.Setting"
_setSetting = "RPC.SetSetting"
_addMidProgress = "RPC.AddMidProgress"
_addTokenCache = "RPC.AddTokenCache"
_addTokensCache = "RPC.AddTokensCache"
)
var (
// _noArg = &struct{}{}
_noReply = &struct{}{}
_appid = "push.service"
)
// Service struct info.
type Service struct {
client *rpc.Client2
}
// New new service instance and return.
func New(c *rpc.ClientConfig) (s *Service) {
s = &Service{}
s.client = rpc.NewDiscoveryCli(_appid, c)
return
}
// AddReport adds report.
func (s *Service) AddReport(c context.Context, arg *model.ArgReport) (err error) {
err = s.client.Call(c, _addReport, arg, _noReply)
return
}
// DelInvalidReports deletes invalid reports.
func (s *Service) DelInvalidReports(c context.Context, arg *model.ArgDelInvalidReport) (err error) {
err = s.client.Call(c, _delInvalidReports, arg, _noReply)
return
}
// DelReport deletes report.
func (s *Service) DelReport(c context.Context, arg *model.ArgReport) (err error) {
err = s.client.Call(c, _delReport, arg, _noReply)
return
}
// AddCallback adds callback data.
func (s *Service) AddCallback(c context.Context, arg *model.ArgCallback) (err error) {
err = s.client.Call(c, _addCallback, arg, _noReply)
return
}
// AddReportCache adds report.
func (s *Service) AddReportCache(c context.Context, arg *model.ArgReport) (err error) {
err = s.client.Call(c, _addReportCache, arg, _noReply)
return
}
// AddUserReportCache adds user report cache.
func (s *Service) AddUserReportCache(c context.Context, arg *model.ArgUserReports) (err error) {
err = s.client.Call(c, _addUserReportCache, arg, _noReply)
return
}
// Setting gets user push switch setting.
func (s *Service) Setting(c context.Context, arg *model.ArgMid) (res map[int]int, err error) {
err = s.client.Call(c, _setting, arg, &res)
return
}
// SetSetting sets user push switch setting.
func (s *Service) SetSetting(c context.Context, arg *model.ArgSetting) (err error) {
err = s.client.Call(c, _setSetting, arg, _noReply)
return
}
// AddMidProgress adds mid count number to task's progress field
func (s *Service) AddMidProgress(c context.Context, arg *model.ArgMidProgress) (err error) {
err = s.client.Call(c, _addMidProgress, arg, _noReply)
return
}
// AddTokenCache add token cache
func (s *Service) AddTokenCache(ctx context.Context, arg *model.ArgReport) (err error) {
err = s.client.Call(ctx, _addTokenCache, arg, _noReply)
return
}
// AddTokensCache add tokens cache
func (s *Service) AddTokensCache(ctx context.Context, arg *model.ArgReports) (err error) {
err = s.client.Call(ctx, _addTokensCache, arg, _noReply)
return
}

View File

@@ -0,0 +1,66 @@
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
proto_library(
name = "v1_proto",
srcs = ["api.proto"],
tags = ["automanaged"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "v1_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_grpc"],
importpath = "go-common/app/service/main/push/api/grpc/v1",
proto = ":v1_proto",
tags = ["automanaged"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["client.go"],
embed = [":v1_go_proto"],
importpath = "go-common/app/service/main/push/api/grpc/v1",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/net/rpc/warden:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_net//context:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = ["//library/net/rpc/warden:go_default_library"],
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
syntax = "proto3";
package push.service.v1;
option go_package = "v1";
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
service Push {
rpc AddReport (AddReportRequest) returns (AddReportReply) {}
rpc DelReport (DelReportRequest) returns (DelReportReply) {}
rpc DelInvalidReports (DelInvalidReportsRequest) returns (DelInvalidReportsReply) {}
rpc AddReportCache (AddReportCacheRequest) returns (AddReportCacheReply) {}
rpc AddUserReportCache (AddUserReportCacheRequest) returns (AddUserReportCacheReply) {}
rpc AddTokenCache (AddTokenCacheRequest) returns (AddTokenCacheReply) {}
rpc AddTokensCache (AddTokensCacheRequest) returns (AddTokensCacheReply) {}
rpc AddCallback (AddCallbackRequest) returns (AddCallbackReply) {}
rpc AddMidProgress (AddMidProgressRequest) returns (AddMidProgressReply) {}
rpc Setting (SettingRequest) returns (SettingReply) {}
rpc SetSetting (SetSettingRequest) returns (SetSettingReply) {}
}
message ModelReport {
int64 id = 1 [(gogoproto.customname) = "ID"];
int32 app_id = 2 [(gogoproto.customname) = "APPID"];
int32 platform_id = 3 [(gogoproto.customname) = "PlatformID"];
int64 mid = 4;
string buvid = 5;
string device_token = 6;
int32 build = 7;
int32 time_zone = 8;
int32 notify_switch = 9;
string device_brand = 10;
string device_model = 11;
string os_version = 12 [(gogoproto.customname) = "OSVersion"];
string extra = 13;
}
message AddReportRequest {
ModelReport report = 1;
}
message AddReportReply {}
message DelReportRequest {
int32 app_id = 1 [(gogoproto.customname) = "APPID"];
int64 mid = 2;
string device_token = 3;
}
message DelReportReply {}
message DelInvalidReportsRequest {
int32 type = 1;
}
message DelInvalidReportsReply {}
message AddReportCacheRequest {
ModelReport report = 1;
}
message AddReportCacheReply {}
message AddUserReportCacheRequest {
int64 mid = 1;
repeated ModelReport reports = 2;
}
message AddUserReportCacheReply {}
message AddTokenCacheRequest {
ModelReport report = 1;
}
message AddTokenCacheReply {}
message AddTokensCacheRequest {
repeated ModelReport reports = 2;
}
message AddTokensCacheReply {}
message AddCallbackRequest {
string task = 1;
int64 app = 2 [(gogoproto.customname) = "APP"];
int32 platform = 3;
int64 mid = 4;
int32 pid = 5;
string token = 6;
string buvid = 7;
int32 click = 8;
CallbackExtra extra = 9;
}
message AddCallbackReply {}
message CallbackExtra {
int32 status = 1 [(gogoproto.moretags) = 'json:"st'];
int32 channel = 2 [(gogoproto.moretags) = 'json:"chan"'];
}
message AddMidProgressRequest {
string task = 1;
int64 mid_total = 2;
int64 mid_valid = 3;
}
message AddMidProgressReply {}
message SettingRequest {
int64 mid = 1;
}
message SettingReply {
map<int32, int32> settings = 1;
}
message SetSettingRequest {
int64 mid = 1;
int32 type = 2;
int32 value = 3;
}
message SetSettingReply {}

View File

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

View File

@@ -0,0 +1,57 @@
package v1
import (
"context"
"fmt"
"testing"
"go-common/library/net/rpc/warden"
)
var (
err error
rpccli PushClient
)
func init() {
rpccli, err = NewClient(&warden.ClientConfig{})
if err != nil {
fmt.Printf("new push grpc client error(%v)", err)
}
}
func Test_AddReport(t *testing.T) {
_, err := rpccli.AddReport(context.Background(), &AddReportRequest{Report: &ModelReport{
APPID: 1,
Mid: 91221505,
DeviceToken: "tototototototo",
NotifySwitch: 1,
}})
if err != nil {
t.Errorf("AddReport error(%v)", err)
}
}
func Test_AddTokenCache(t *testing.T) {
_, err := rpccli.AddTokenCache(context.Background(), &AddTokenCacheRequest{Report: &ModelReport{
APPID: 1,
Mid: 91221505,
DeviceToken: "tototototototo",
NotifySwitch: 1,
}})
if err != nil {
t.Errorf("AddTokenCache error(%v)", err)
}
}
func Test_AddTokensCache(t *testing.T) {
_, err := rpccli.AddTokensCache(context.Background(), &AddTokensCacheRequest{Reports: []*ModelReport{{
APPID: 1,
Mid: 91221505,
DeviceToken: "tototototototo",
NotifySwitch: 1,
}}})
if err != nil {
t.Errorf("AddTokensCache error(%v)", err)
}
}

View File

@@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["push-service-test.toml"],
importpath = "go-common/app/service/main/push/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push/conf:go_default_library",
"//app/service/main/push/server/gorpc:go_default_library",
"//app/service/main/push/server/grpc:go_default_library",
"//app/service/main/push/server/http:go_default_library",
"//app/service/main/push/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,55 @@
package main
import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/service/main/push/conf"
pushrpc "go-common/app/service/main/push/server/gorpc"
"go-common/app/service/main/push/server/grpc"
"go-common/app/service/main/push/server/http"
"go-common/app/service/main/push/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)
trace.Init(conf.Conf.Tracer)
defer trace.Close()
defer log.Close()
log.Info("push-service start")
ecode.Init(conf.Conf.Ecode)
svr := service.New(conf.Conf)
rpcSrv := pushrpc.New(conf.Conf, svr)
grpcSvr := grpc.New(conf.Conf.GRPC, svr)
http.Init(conf.Conf, svr)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("push-service get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
rpcSrv.Close()
grpcSvr.Shutdown(context.Background())
svr.Close()
log.Info("push-service exit")
time.Sleep(time.Second)
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,152 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/push-service.pid"
dir = "./"
family = "push-service"
[log]
dir = "/data/log/push-service/"
[rpcServer]
proto = "tcp"
addr = "0.0.0.0:7779"
[grpc]
timeout = "1s"
addr = "0.0.0.0:9000"
[HTTPServer]
addr = "0.0.0.0:7771"
maxListen = 1000
timeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
[HTTPClient]
dial = "50ms"
timeout = "200ms"
keepAlive = "60s"
key = "f265dcfa28272742"
secret = "437facc22dc8698b5544669bcc12348d"
[HTTPClient.breaker]
window ="1s"
sleep ="10ms"
bucket = 10
ratio = 0.5
request = 100
[apns]
poolSize = 200
proxy = 0 # 0表示不使用代理
proxySocket = "133.130.49.92:9091"
timeout = "10s"
[android]
poolSize = 50
timeout = "30s"
pushHuaweiPart = 30
miUseVip = 0
[mysql]
addr = "172.16.33.205"
dsn = "test:test@tcp(172.16.33.205:3308)/bilibili_push?timeout=1m&readTimeout=1m&writeTimeout=1m&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 10
idle = 5
queryTimeout = "1m"
execTimeout = "1m"
tranTimeout = "1m"
[mysql.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[redis]
name = "push-service"
proto = "tcp"
addr = "172.16.33.54:6379"
idle = 1000
active = 10000
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "30s"
tokenExpire = "72h"
laterExpire = "72h"
midsExpire = "72h"
[memcache]
name = "push-service"
proto = "tcp"
addr = "172.18.33.60:11228"
idle = 1000
active = 1000
dialTimeout = "10s"
readTimeout = "10s"
writeTimeout = "10s"
idleTimeout = "30s"
settingExpire = "720h"
reportExpire = "720h"
uuidExpire = "30m"
[reportPub]
key = "0QEO9F8JuuIxZzNDvklH"
secret="0QEO9F8JuuIxZzNDvklI"
group= "PushReport-Push-P"
topic= "PushReport-T"
action="pub"
name = "push-report-pub"
proto = "tcp"
addr = "172.16.33.158:6205"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "60s"
writeTimeout = "1s"
idleTimeout = "10s"
[callbackPub]
key = "dbe67e6a4c36f877"
secret="6e6eb74cc73f2c8bff02fb40ee57da59"
group= "PushCallback-MainCommonArch-P"
topic= "PushCallback-T"
action="pub"
name = "push-callback-pub"
proto = "tcp"
addr = "172.16.33.158:6205"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "60s"
writeTimeout = "1s"
idleTimeout = "10s"
[push]
pickUpTask = true
loadBusinessInteval = "5m"
loadTaskInteval = "1s"
updateTaskProgressInteval = "5s"
pushChanSizeAPNS = 10000
pushGoroutinesAPNS = 1000
pushChanSizeMi = 10000
pushGoroutinesMi = 100
pushChanSizeHuawei = 10000
pushGoroutinesHuawei = 100
pushChanSizeOppo = 10000
pushGoroutinesOppo = 100
pushChanSizeJpush = 10000
pushGoroutinesJpush = 100
pushChanSizeFCM = 10000
pushGoroutinesFCM = 100
passThrough = 1
retryTimes = 3
pushPartInterval = "1s"
pushPartChanSize = 10
pushPartSize = 1000
callbackSize = 3 # callback 聚合数
callbackChanLen = 1024000
callbackGoroutines = 10
upimgURL = "http://uat-api.bilibili.co/x/internal/upload/admin/image"
upimgMaxSize = 1048576 # 允许上传的最大图片 字节数
updateTaskProgressProc = 10

View File

@@ -0,0 +1,44 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/service/main/push/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/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,146 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"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"
"github.com/BurntSushi/toml"
)
// global var
var (
confPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config config set
type Config struct {
Env string
Ecode *ecode.Config
Log *log.Config
HTTPServer *bm.ServerConfig
HTTPClient *bm.ClientConfig
RPCServer *rpc.ServerConfig
GRPC *warden.ServerConfig
FilterRPC *rpc.ClientConfig
Tracer *trace.Config
Verify *verify.Config
App *bm.App
Redis *rds
Memcache *mc
MySQL *sql.Config
ReportPub *databus.Config
CallbackPub *databus.Config
Android *android
Apns *apns
Push *push
}
// mc config
type mc struct {
*memcache.Config
SettingExpire xtime.Duration
ReportExpire xtime.Duration
UUIDExpire xtime.Duration
}
type rds struct {
*redis.Config
TokenExpire xtime.Duration
LaterExpire xtime.Duration
MidsExpire xtime.Duration
}
type android struct {
PoolSize int
Timeout xtime.Duration
PushHuaweiPart int
MiUseVip int
}
type apns struct {
PoolSize int
Proxy int
ProxySocket string
Timeout xtime.Duration
Deadline xtime.Duration
}
type push struct {
PickUpTask bool
LoadBusinessInteval xtime.Duration
LoadTaskInteval xtime.Duration
UpdateTaskProgressInteval xtime.Duration
PushChanSizeAPNS, PushGoroutinesAPNS int
PushChanSizeMi, PushGoroutinesMi int
PushChanSizeHuawei, PushGoroutinesHuawei int
PushChanSizeOppo, PushGoroutinesOppo int
PushChanSizeJpush, PushGoroutinesJpush int
PushChanSizeFCM, PushGoroutinesFCM int
PassThrough int
RetryTimes int
PushPartInterval xtime.Duration
PushPartChanSize int
PushPartSize int
CallbackSize, CallbackChanLen int
UpimgURL string
UpimgMaxSize int64
UpdateTaskProgressProc 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,93 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"client_test.go",
"dao_test.go",
"databus_test.go",
"memcache_test.go",
"mysql_test.go",
"push_test.go",
"redis_test.go",
"report_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push/conf:go_default_library",
"//app/service/main/push/dao/oppo:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"dao.go",
"databus.go",
"mc.cache.go",
"memcache.go",
"mysql.go",
"mysql_report.go",
"mysql_setting.go",
"push.go",
"redis.go",
"report.go",
],
importpath = "go-common/app/service/main/push/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push/conf:go_default_library",
"//app/service/main/push/dao/apns2:go_default_library",
"//app/service/main/push/dao/fcm:go_default_library",
"//app/service/main/push/dao/huawei:go_default_library",
"//app/service/main/push/dao/jpush:go_default_library",
"//app/service/main/push/dao/mi:go_default_library",
"//app/service/main/push/dao/oppo:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf/env:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/queue/databus:go_default_library",
"//library/stat/prom:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/main/push/dao/apns2:all-srcs",
"//app/service/main/push/dao/fcm:all-srcs",
"//app/service/main/push/dao/huawei:all-srcs",
"//app/service/main/push/dao/jpush:all-srcs",
"//app/service/main/push/dao/mi:all-srcs",
"//app/service/main/push/dao/oppo:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"notification.go",
],
importpath = "go-common/app/service/main/push/dao/apns2",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/stat:go_default_library",
"//library/stat/prom:go_default_library",
"@org_golang_x_net//http2:go_default_library",
"@org_golang_x_net//proxy: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,263 @@
// Package apns2 is a go Apple Push Notification Service (APNs) provider that
// allows you to send remote notifications to your iOS, tvOS, and OS X
// apps, using the new APNs HTTP/2 network protocol.
package apns2
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strconv"
"time"
"go-common/library/log"
"go-common/library/stat"
"go-common/library/stat/prom"
"golang.org/x/net/http2"
"golang.org/x/net/proxy"
)
const (
// HostDevelopment dev host.
HostDevelopment = "https://api.development.push.apple.com"
// HostProduction pro host.
HostProduction = "https://api.push.apple.com"
// StatusCodeSuccess success.
StatusCodeSuccess = 200
// StatusCodeBadReq bad req.
StatusCodeBadReq = 400
// StatusCodeCerErr There was an error with the certificate.
StatusCodeCerErr = 403
// StatusCodeMethodErr The request used a bad :method value. Only POST requests are supported.
StatusCodeMethodErr = 405
// StatusCodeNotForTopic The device token is not form the topic.
StatusCodeNotForTopic = 400
// StatusCodeNoActive The device token is no longer active for the topic.
StatusCodeNoActive = 410
// StatusCodePayloadTooLarge The notification payload was too large.
StatusCodePayloadTooLarge = 413
// StatusCodeTooManyReq The server received too many requests for the same device token.
StatusCodeTooManyReq = 429
// StatusCodeServerErr Internal server error
StatusCodeServerErr = 500
// StatusCodeServerUnavailable The server is shutting down and unavailable.
StatusCodeServerUnavailable = 503
)
// DefaultHost is a mutable var for testing purposes
var DefaultHost = HostDevelopment
// Client represents a connection with the APNs
type Client struct {
HTTPClient *http.Client
Certificate tls.Certificate
Host string
BoundID string
Stats stat.Stat
}
// func init() {
// proxy.RegisterDialerType("http", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) {
// return &net.Dialer{}, nil
// })
// }
// NewClient returns a new Client with an underlying http.Client configured with
// the correct APNs HTTP/2 transport settings. It does not connect to the APNs
// until the first Notification is sent via the Push method.
//
// As per the Apple APNs Provider API, you should keep a handle on this client
// so that you can keep your connections with APNs open across multiple
// notifications; dont repeatedly open and close connections. APNs treats rapid
// connection and disconnection as a denial-of-service attack.
func NewClient(certificate tls.Certificate, timeout time.Duration) *Client {
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{certificate},
ClientAuth: tls.NoClientCert,
}
if len(certificate.Certificate) > 0 {
tlsConfig.BuildNameToCertificate()
}
transport := &http2.Transport{
TLSClientConfig: tlsConfig,
}
// transport := &http.Transport{
// TLSClientConfig: tlsConfig,
// Proxy: func(_ *http.Request) (*url.URL, error) {
// return url.Parse("http://10.28.10.11:80")
// },
// DialContext: (&net.Dialer{
// Timeout: 30 * time.Second,
// KeepAlive: 30 * time.Second,
// DualStack: true,
// }).DialContext,
// MaxIdleConns: 100,
// IdleConnTimeout: 90 * time.Second,
// TLSHandshakeTimeout: 10 * time.Second,
// ExpectContinueTimeout: 1 * time.Second,
// }
return &Client{
HTTPClient: &http.Client{Transport: transport, Timeout: timeout},
Certificate: certificate,
Host: DefaultHost,
Stats: prom.HTTPClient,
}
}
// NewClientWithProxy returns a new Client with sock5 proxy.
func NewClientWithProxy(certificate tls.Certificate, timeout time.Duration, proxyAddr string) *Client {
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{certificate},
ClientAuth: tls.NoClientCert,
}
if len(certificate.Certificate) > 0 {
tlsConfig.BuildNameToCertificate()
}
return &Client{
HTTPClient: &http.Client{Transport: proxyTransport(proxyAddr, tlsConfig, timeout), Timeout: timeout},
Certificate: certificate,
Host: DefaultHost,
Stats: prom.HTTPClient,
}
}
func proxyTransport(proxyAddr string, config *tls.Config, timeout time.Duration) *http2.Transport {
return &http2.Transport{
DialTLS: func(network, addr string, cfg *tls.Config) (nc net.Conn, err error) {
dialer := &net.Dialer{Timeout: timeout / 2}
var proxyDialer proxy.Dialer
if proxyDialer, err = proxy.SOCKS5("tcp", proxyAddr, nil, dialer); err != nil {
log.Error("proxy.SOCKS5(%s) error(%v)", proxyAddr, err)
return nil, err
}
// u, _ := url.Parse("http://10.28.10.11:80")
// proxyDialer, err = proxy.FromURL(u, dialer)
var conn net.Conn
if conn, err = proxyDialer.Dial(network, addr); err != nil {
log.Error("proxyDialer.Dial(%s,%s) error(%v)", network, addr, err)
if conn, err = dialer.Dial(network, addr); err != nil {
log.Error("dialer.Dial(%s,%s) error(%v)", network, addr, err)
return nil, err
}
}
tlsConn := tls.Client(conn, cfg)
if err = tlsConn.Handshake(); err != nil {
log.Error("tlsConn.Handshake() error(%v)", err)
return nil, err
}
if !cfg.InsecureSkipVerify {
if err = tlsConn.VerifyHostname(cfg.ServerName); err != nil {
log.Error("tlsConn.VerifyHostname(%s) error(%v)", cfg.ServerName, err)
return nil, err
}
}
state := tlsConn.ConnectionState()
if state.NegotiatedProtocol != http2.NextProtoTLS {
err = fmt.Errorf("http2: unexpected ALPN protocol(%s) expect(%s)", state.NegotiatedProtocol, http2.NextProtoTLS)
return nil, err
}
if !state.NegotiatedProtocolIsMutual {
err = errors.New("http2: could not negotiate protocol mutually")
return nil, err
}
return tlsConn, nil
},
TLSClientConfig: config,
}
}
// Development sets the Client to use the APNs development push endpoint.
func (c *Client) Development() *Client {
c.Host = HostDevelopment
return c
}
// Production sets the Client to use the APNs production push endpoint.
func (c *Client) Production() *Client {
c.Host = HostProduction
return c
}
// Push sends a Notification to the APNs gateway. If the underlying http.Client
// is not currently connected, this method will attempt to reconnect
// transparently before sending the notification.
func (c *Client) Push(deviceToken string, payload *Payload, overTime int64) (response *Response, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.Host, int64(time.Since(now)/time.Millisecond))
log.Info("apns stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.Host, "failed")
}
}()
}
var (
req *http.Request
res *http.Response
t = time.NewTimer(c.HTTPClient.Timeout)
errCh = make(chan error, 1)
url = fmt.Sprintf("%v/3/device/%v", c.Host, deviceToken)
)
if req, err = http.NewRequest("POST", url, bytes.NewBuffer(payload.Marshal())); err != nil {
log.Error("http.NewRequest(%s) error(%v)", url, err)
return
}
req.Header.Set("apns-topic", c.BoundID)
req.Header.Set("apns-expiration", strconv.FormatInt(overTime, 10))
req.Header.Set("apns-collapse-id", payload.TaskID)
go func() {
res, err = c.HTTPClient.Do(req)
errCh <- err
}()
select {
case <-t.C:
err = errors.New("http.Do timeout")
return
case err = <-errCh:
if err != nil {
log.Error("c.HTTPClient.Do() error(%v)", err)
return
}
}
defer res.Body.Close()
response = &Response{StatusCode: res.StatusCode, ApnsID: res.Header.Get("apns-id")}
var bs []byte
bs, err = ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}
// MockPush mock push.
func (c *Client) MockPush(deviceToken string, payload *Payload, overTime int64) (response *Response, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.Host, int64(time.Since(now)/time.Millisecond))
// log.Info("mock apns stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.Host, "apple push mock")
}
}()
}
time.Sleep(200 * time.Millisecond)
response = &Response{StatusCode: StatusCodeSuccess}
return
}

View File

@@ -0,0 +1,175 @@
package apns2
import (
"crypto/tls"
"encoding/json"
"fmt"
"testing"
"time"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
k = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3ZDI9NUPfMH0/6DgmeCH/Zl8g3pOlLcMY0p6m8lMe9EDF1vZ
w3uR05YzbQCmYCwWZLKDTq/0d3RMRow8AON1wMv9ynyk7Z1Pb9C+ByN4GCWsRHjN
BbEg8jgBYeOoM6PlpDc16D8uPKukRuaq/j1beOn1HD4vnS8KqUzeS6fYbaiPjC7h
7QZOnNE5NwV6TKPMu/+0aNPNgZDeFtNwcXiiRX6OPfbOOnVr0/6WGorYdGMPLxeZ
viqZ37z7rDhup/LaiDedO/tm09lmKUbFOHS+qbSYPXztDHhOwTm36E1k2mD5Lq1S
d5zHRrwzgPTvieFQ+giA9u8pn2wBNDASBDF1pQIDAQABAoIBAQDdNQtdXUa8IQ1R
FraHCuPa7p2gysCfu22TyC1HUh+ZUqEKdjqg78M1AzXOsyJozDuDR7LPId8qUCND
IAlcPbw3w7Jbsjwbu74ufbLrf58MRLiMGCthbmndSsseh2NMQ2snm7Onb0Tjb95w
pyW69VlZDAQasX9qKCg1xTf/QtFTEGbJpkfN7lYYjwwhLFck39SUH254cwmqV8Tj
AQ9Uj6dQhYvWseSGtsbqkyj0/sUwyzfaUWeNjnYUaVQWxAalnLYiUCzRLR0IMwvM
cmscwYWrWlPPd8ND4yGyHXqeQH2fVJqEx/aIDSpujNeOm2MH0RFRNBU53d/tJpj8
XuqE8Yj1AoGBAO5klAY8vScnC3RQ8x3tlJkM3jkD3FmqSJu7erId0T6KgJz0Xmgj
zHYwvq+V4t0Kmk1Tt5Okq3wI41uDZUJCBXrnNWhm0oDHnlw38TiptBrfqIjfyK5y
OmmP49DWHZagF9nY74zM2doCLPnI9G2i2mAcCjCMXKfeJhOhzkgjnGOTAoGBAO3u
Cy5N9a8yjQ3y0B3iNwWQQs94kfwbyuy4cvvBOlkheB0JTEunNfHVVl4zlwFDnrH1
UWg7ySqTM3iCd9OE1bErbavHmGeTC0FYkKw/k4fct7icU1shSuPwkyp3GRYI6qW8
e24k2U32gO/ANu6WZsYKKAcYjMObQc+/LkLaV3TnAoGAT5QJia964Pf6reBb17C4
OwL9p4CvbMsYI8xIn+6uK7dmSX6ViSPyG74X2VsqeOkSKx/4FvQQPn5lDuZkxeJu
G+HUhT5VpKF+LoCKKIUV1ya0BsTVI86Dyzs6LDtdcyuL6q+s/45eZpT1WIiJd5O2
XADgMeaZA3x3r3QC/TfN+7sCgYAUQvpGxjLO6aIjdvMMKHCBE8jsvBrKel9si0SX
ddwPLQ96gYkyxBmO75j8Sq5oWCbShs6Y7sZxzrlKYOntZFmCTe13/HZZE6eYt/8R
/BQHNN+cZAuhLhOfl6QgsKW9P6Mj3Aoy1gZ/YieWwyqqZLp50PGZsRiDq9wN4f0B
inB6LwKBgQDTeiZ7lAHNhZW8VOpD/K/403xQQinJr6vMiWFm/znwcbm/pteWZuwR
omzn56zhYiXkDKKKxFkVIwf80xSz4Rmgl0p2BndsXU8hOVY/NCnAQJ1uZIsNilcP
KsBKVxvOzopDKZP+C0IQcRaw5VN9WvqJ9ijUEwgm2ufQyz1oj7tREg==
-----END RSA PRIVATE KEY-----`)
c = []byte(`-----BEGIN CERTIFICATE-----
MIIGOTCCBSGgAwIBAgIIDMyy2gAN2P4wDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNV
BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
HhcNMTgwNzA5MTA0NzQ5WhcNMTkwODA4MTA0NzQ5WjCBqjEkMCIGCgmSJomT8ixk
AQEMFHR2LmRhbm1ha3UuYmlsaWFuaW1lMTIwMAYDVQQDDClBcHBsZSBQdXNoIFNl
cnZpY2VzOiB0di5kYW5tYWt1LmJpbGlhbmltZTETMBEGA1UECwwKNzQ2ODQ1R0M5
NjEsMCoGA1UECgwjU2hhbmdoYWkgQmlsaWJpbGkgQW5pbWF0aW9uIENvLixMdGQx
CzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3ZDI
9NUPfMH0/6DgmeCH/Zl8g3pOlLcMY0p6m8lMe9EDF1vZw3uR05YzbQCmYCwWZLKD
Tq/0d3RMRow8AON1wMv9ynyk7Z1Pb9C+ByN4GCWsRHjNBbEg8jgBYeOoM6PlpDc1
6D8uPKukRuaq/j1beOn1HD4vnS8KqUzeS6fYbaiPjC7h7QZOnNE5NwV6TKPMu/+0
aNPNgZDeFtNwcXiiRX6OPfbOOnVr0/6WGorYdGMPLxeZviqZ37z7rDhup/LaiDed
O/tm09lmKUbFOHS+qbSYPXztDHhOwTm36E1k2mD5Lq1Sd5zHRrwzgPTvieFQ+giA
9u8pn2wBNDASBDF1pQIDAQABo4ICczCCAm8wDAYDVR0TAQH/BAIwADAfBgNVHSME
GDAWgBSIJxcJqbYYYIvs67r2R1nFUlSjtzCCARwGA1UdIASCARMwggEPMIIBCwYJ
KoZIhvdjZAUBMIH9MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMg
Y2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0
aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25z
IG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHBy
YWN0aWNlIHN0YXRlbWVudHMuMDUGCCsGAQUFBwIBFilodHRwOi8vd3d3LmFwcGxl
LmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eTATBgNVHSUEDDAKBggrBgEFBQcDAjAw
BgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vY3JsLmFwcGxlLmNvbS93d2RyY2EuY3Js
MB0GA1UdDgQWBBRezirr8YHmrv5m5/7ZCr3VybbdLjAOBgNVHQ8BAf8EBAMCB4Aw
EAYKKoZIhvdjZAYDAQQCBQAwEAYKKoZIhvdjZAYDAgQCBQAwgYMGCiqGSIb3Y2QG
AwYEdTBzDBR0di5kYW5tYWt1LmJpbGlhbmltZTAFDANhcHAMGXR2LmRhbm1ha3Uu
YmlsaWFuaW1lLnZvaXAwBgwEdm9pcAwhdHYuZGFubWFrdS5iaWxpYW5pbWUuY29t
cGxpY2F0aW9uMA4MDGNvbXBsaWNhdGlvbjANBgkqhkiG9w0BAQsFAAOCAQEAQLGl
rzH6QG5WKmEbYw3741TTer1E2MlCr7JP9rmn3W+IWy+cX2IQv9vaeFZ3pi/1uMkC
kK6JQd7gUXLPcGwldu4m36OOdUfRLWPH7yvvyYazEo6sDKAUzI/cg14Yj/3Z7ig1
nL6pvXzPd0LjreKKDc08wfmV8gbALLWzjkIcanNdijWlMtfwgLWQunr2jZAK4kKN
GpGku6BCZPYzFLidPMfnXIgOarNbM0SFX+3UY2+fWS+oJsNpGvqWUEINFjVhWoZ4
62WxT8BT2sCbMNGqJGM3wTYe6gkT3E52azu1bNc18/+5/V/qCIjsZLWsX4yCywFE
JQR4w5UPQzMbq6Ybeg==
-----END CERTIFICATE-----`)
hdk = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3ZDI9NUPfMH0/6DgmeCH/Zl8g3pOlLcMY0p6m8lMe9EDF1vZ
w3uR05YzbQCmYCwWZLKDTq/0d3RMRow8AON1wMv9ynyk7Z1Pb9C+ByN4GCWsRHjN
BbEg8jgBYeOoM6PlpDc16D8uPKukRuaq/j1beOn1HD4vnS8KqUzeS6fYbaiPjC7h
7QZOnNE5NwV6TKPMu/+0aNPNgZDeFtNwcXiiRX6OPfbOOnVr0/6WGorYdGMPLxeZ
viqZ37z7rDhup/LaiDedO/tm09lmKUbFOHS+qbSYPXztDHhOwTm36E1k2mD5Lq1S
d5zHRrwzgPTvieFQ+giA9u8pn2wBNDASBDF1pQIDAQABAoIBAQDdNQtdXUa8IQ1R
FraHCuPa7p2gysCfu22TyC1HUh+ZUqEKdjqg78M1AzXOsyJozDuDR7LPId8qUCND
IAlcPbw3w7Jbsjwbu74ufbLrf58MRLiMGCthbmndSsseh2NMQ2snm7Onb0Tjb95w
pyW69VlZDAQasX9qKCg1xTf/QtFTEGbJpkfN7lYYjwwhLFck39SUH254cwmqV8Tj
AQ9Uj6dQhYvWseSGtsbqkyj0/sUwyzfaUWeNjnYUaVQWxAalnLYiUCzRLR0IMwvM
cmscwYWrWlPPd8ND4yGyHXqeQH2fVJqEx/aIDSpujNeOm2MH0RFRNBU53d/tJpj8
XuqE8Yj1AoGBAO5klAY8vScnC3RQ8x3tlJkM3jkD3FmqSJu7erId0T6KgJz0Xmgj
zHYwvq+V4t0Kmk1Tt5Okq3wI41uDZUJCBXrnNWhm0oDHnlw38TiptBrfqIjfyK5y
OmmP49DWHZagF9nY74zM2doCLPnI9G2i2mAcCjCMXKfeJhOhzkgjnGOTAoGBAO3u
Cy5N9a8yjQ3y0B3iNwWQQs94kfwbyuy4cvvBOlkheB0JTEunNfHVVl4zlwFDnrH1
UWg7ySqTM3iCd9OE1bErbavHmGeTC0FYkKw/k4fct7icU1shSuPwkyp3GRYI6qW8
e24k2U32gO/ANu6WZsYKKAcYjMObQc+/LkLaV3TnAoGAT5QJia964Pf6reBb17C4
OwL9p4CvbMsYI8xIn+6uK7dmSX6ViSPyG74X2VsqeOkSKx/4FvQQPn5lDuZkxeJu
G+HUhT5VpKF+LoCKKIUV1ya0BsTVI86Dyzs6LDtdcyuL6q+s/45eZpT1WIiJd5O2
XADgMeaZA3x3r3QC/TfN+7sCgYAUQvpGxjLO6aIjdvMMKHCBE8jsvBrKel9si0SX
ddwPLQ96gYkyxBmO75j8Sq5oWCbShs6Y7sZxzrlKYOntZFmCTe13/HZZE6eYt/8R
/BQHNN+cZAuhLhOfl6QgsKW9P6Mj3Aoy1gZ/YieWwyqqZLp50PGZsRiDq9wN4f0B
inB6LwKBgQDTeiZ7lAHNhZW8VOpD/K/403xQQinJr6vMiWFm/znwcbm/pteWZuwR
omzn56zhYiXkDKKKxFkVIwf80xSz4Rmgl0p2BndsXU8hOVY/NCnAQJ1uZIsNilcP
KsBKVxvOzopDKZP+C0IQcRaw5VN9WvqJ9ijUEwgm2ufQyz1oj7tREg==
-----END RSA PRIVATE KEY-----`)
hdc = []byte(`-----BEGIN CERTIFICATE-----
MIIGPjCCBSagAwIBAgIIRY80KDvYYtUwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNV
BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
HhcNMTgwNzA5MTA0OTUyWhcNMTkwODA4MTA0OTUyWjCBrDElMCMGCgmSJomT8ixk
AQEMFXR2LmRhbm1ha3UuYmlsaWJpbGloZDEzMDEGA1UEAwwqQXBwbGUgUHVzaCBT
ZXJ2aWNlczogdHYuZGFubWFrdS5iaWxpYmlsaWhkMRMwEQYDVQQLDAo3NDY4NDVH
Qzk2MSwwKgYDVQQKDCNTaGFuZ2hhaSBCaWxpYmlsaSBBbmltYXRpb24gQ28uLEx0
ZDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDd
kMj01Q98wfT/oOCZ4If9mXyDek6UtwxjSnqbyUx70QMXW9nDe5HTljNtAKZgLBZk
soNOr/R3dExGjDwA43XAy/3KfKTtnU9v0L4HI3gYJaxEeM0FsSDyOAFh46gzo+Wk
NzXoPy48q6RG5qr+PVt46fUcPi+dLwqpTN5Lp9htqI+MLuHtBk6c0Tk3BXpMo8y7
/7Ro082BkN4W03BxeKJFfo499s46dWvT/pYaith0Yw8vF5m+KpnfvPusOG6n8tqI
N507+2bT2WYpRsU4dL6ptJg9fO0MeE7BObfoTWTaYPkurVJ3nMdGvDOA9O+J4VD6
CID27ymfbAE0MBIEMXWlAgMBAAGjggJ2MIICcjAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBHAYDVR0gBIIBEzCCAQ8wggEL
BgkqhkiG92NkBQEwgf0wgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFuY2Ugb24gdGhp
cyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9m
IHRoZSB0aGVuIGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlv
bnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRpZmljYXRpb24g
cHJhY3RpY2Ugc3RhdGVtZW50cy4wNQYIKwYBBQUHAgEWKWh0dHA6Ly93d3cuYXBw
bGUuY29tL2NlcnRpZmljYXRlYXV0aG9yaXR5MBMGA1UdJQQMMAoGCCsGAQUFBwMC
MDAGA1UdHwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuYXBwbGUuY29tL3d3ZHJjYS5j
cmwwHQYDVR0OBBYEFF7OKuvxgeau/mbn/tkKvdXJtt0uMA4GA1UdDwEB/wQEAwIH
gDAQBgoqhkiG92NkBgMBBAIFADAQBgoqhkiG92NkBgMCBAIFADCBhgYKKoZIhvdj
ZAYDBgR4MHYMFXR2LmRhbm1ha3UuYmlsaWJpbGloZDAFDANhcHAMGnR2LmRhbm1h
a3UuYmlsaWJpbGloZC52b2lwMAYMBHZvaXAMInR2LmRhbm1ha3UuYmlsaWJpbGlo
ZC5jb21wbGljYXRpb24wDgwMY29tcGxpY2F0aW9uMA0GCSqGSIb3DQEBCwUAA4IB
AQAltymx4RoeuW2Grnk4Vb+RneVafET87kT2HpjKTwnWglcFzDM2g3jeEC/MfDtZ
28Y/qMBmz4ThJthNOHgdEyvTqTdZG4739HzLdxB79GsraGhpMIORw8UOetsmogId
FspzWxR/nysIdEo8bj6gbAOmANrQn1zNFrO5/c31GxY+AFRGl6n/aY7ObCstpIca
L6TDWiPJyLH2Ha0qeGgBch97Jk1XVa2m6Syl9a5VtL8jM8SBDpx+krVsxL4YhBot
Ko45/s6H9wqCIx06h28N3EB0VALGSxeFhwlc/uVQRNu0w3DPHTTGHhNOGbzn2QVs
V5Ies2V9gtxFI5xwR+/ERU2z
-----END CERTIFICATE-----`)
)
func TestClient(t *testing.T) {
// unuserd
_ = hdc
_ = hdk
Convey("test apns", t, func() {
cert, err := tls.X509KeyPair(c, k)
if err != nil {
panic(err)
}
apnsClient := NewClient(cert, 10*time.Second).Production()
aps := Aps{
Alert: Alert{
Title: "test",
Body: "test",
},
Badge: 0,
// Sound: "default", 不加sound没有提醒
MutableContent: 1,
}
var token string
// token = "140b5f62b3db93bc6a072645d3825c50efa5693f733690542fffa034252c7495"
scheme := model.Scheme(model.LinkTypeLive, "123", model.PlatformIPhone, 529000)
payload := &Payload{Aps: aps, URL: scheme, TaskID: "3c9e1eaca2afd373_search_1473317045", Token: token, Image: "https://pic.qiantucdn.com/58pic/12/38/18/13758PIC4GV.jpg"}
bs, _ := json.Marshal(payload)
fmt.Printf("payload(%s)", bs)
apnsClient.BoundID = "tv.danmaku.bilianime"
resp, err := apnsClient.Push(token, payload, time.Now().Unix())
So(err, ShouldBeNil)
fmt.Println("StatusCode:", resp.StatusCode, "ApnsID:", resp.ApnsID, "Reason:", resp.Reason)
})
}

View File

@@ -0,0 +1,81 @@
package apns2
import (
"encoding/json"
)
// NOTE these structs and "Table" refer to https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html
// Payload payload.
type Payload struct {
Aps Aps `json:"aps"`
URL string `json:"url"` // bilibili schedule
TaskID string `json:"task_id"`
Token string `json:"tid"`
Image string `json:"image_url,omitempty"`
}
// Marshal marshals payload.
func (p *Payload) Marshal() []byte {
payload, _ := json.Marshal(p)
return payload
}
// Aps Apple Push Service request meta.
type Aps struct {
// If this property is included, the system displays a standard alert or a banner, based on the users setting.
// You can specify a string or a dictionary as the value of alert.
// If you specify a string, it becomes the message text of an alert with two buttons: Close and View.
// If the user taps View, the app launches.
// If you specify a dictionary, refer to Table 5-2 for descriptions of the keys of this dictionary.
// The JSON \U notation is not supported. Put the actual UTF-8 character in the alert text instead.
Alert Alert `json:"alert,omitempty"`
// The number to display as the badge of the app icon.
// If this property is absent, the badge is not changed. To remove the badge, set the value of this property to 0.
Badge int `json:"badge,omitempty"`
// The name of a sound file in the app bundle or in the Library/Sounds folder of the apps data container.
// The sound in this file is played as an alert. If the sound file doesnt exist or default is specified
// as the value, the default alert sound is played. The audio must be in one of the audio data formats
// that are compatible with system sounds; see Preparing Custom Alert Sounds for details.
Sound string `json:"sound,omitempty"`
// Provide this key with a value of 1 to indicate that new content is available.
// Including this key and value means that when your app is launched in the background or resumed,
// application:didReceiveRemoteNotification:fetchCompletionHandler: is called.
ContentAvailable int `json:"content-available,omitempty"`
// Provide this key with a string value that represents the identifier property of the
// UIMutableUserNotificationCategory object you created to define custom actions.
// To learn more about using custom actions, see Registering Your Actionable Notification Types.
Category string `json:"category,omitempty"`
// MutableContent .
MutableContent int `json:"mutable-content,omitempty"`
}
// Alert alert message.
type Alert struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
// could support any more other field
}
// Response reponse message.
type Response struct {
ApnsID string
// Http status. (refer to Table 6-4)
StatusCode int
// The APNs error string indicating the reason for the notification failure (if
// any). The error code is specified as a string. For a list of possible
// values, see the Reason constants above.
// If the notification was accepted, this value will be "".
Reason string
// If the value of StatusCode is 410, this is the last time at which APNs
// confirmed that the device token was no longer valid for the topic.
// Timestamp time.Time
}

View File

@@ -0,0 +1,196 @@
package dao
import (
"context"
"crypto/tls"
"errors"
"time"
"go-common/app/service/main/push/dao/apns2"
"go-common/app/service/main/push/dao/fcm"
"go-common/app/service/main/push/dao/huawei"
"go-common/app/service/main/push/dao/jpush"
"go-common/app/service/main/push/dao/mi"
"go-common/app/service/main/push/dao/oppo"
"go-common/app/service/main/push/model"
"go-common/library/conf/env"
"go-common/library/log"
)
var errNoClinets = errors.New("no clients")
func (d *Dao) loadClients() {
var cnt int
for cnt < 3 {
auths, err := d.auths(context.Background())
if err != nil {
log.Error("d.auths() error(%v)", err)
time.Sleep(time.Second)
cnt++
continue
}
if len(auths) == 0 {
return
}
for _, a := range auths {
log.Info("new push client. app(%d) platform(%d)", a.APPID, a.PlatformID)
i := fmtRoundIndex(a.APPID, a.PlatformID)
d.clientsIndex[i] = new(uint32)
switch a.PlatformID {
case model.PlatformIPhone:
d.clientsIPhone[a.APPID] = d.newApnsClients(model.PlatformIPhone, a.Value, a.Key, a.BundleID)
d.clientsLen[i] = len(d.clientsIPhone[a.APPID])
case model.PlatformIPad:
d.clientsIPad[a.APPID] = d.newApnsClients(model.PlatformIPad, a.Value, a.Key, a.BundleID)
d.clientsLen[i] = len(d.clientsIPad[a.APPID])
case model.PlatformHuawei:
cs := d.newHuaweiClients(a.APPID, a.BundleID)
if len(cs) > 0 {
d.clientsHuawei[a.APPID] = cs
d.clientsLen[i] = len(d.clientsHuawei)
}
case model.PlatformOppo:
cs := d.newOppoClients(a.APPID, a.BundleID)
if len(cs) > 0 {
d.clientsOppo[a.APPID] = cs
d.clientsLen[i] = len(d.clientsOppo)
}
case model.PlatformXiaomi:
d.clientsMi[a.APPID] = d.newMiClients(a.Key, a.Value)
d.clientsLen[i] = len(d.clientsMi[a.APPID])
d.clientMiByMids[a.APPID] = d.newMiClientByMids(a.Key, a.Value)
case model.PlatformJpush:
d.clientsJpush[a.APPID] = d.newJpushClients(a.Key, a.Value)
d.clientsLen[i] = len(d.clientsJpush[a.APPID])
case model.PlatformFCM:
d.clientsFCM[a.APPID] = d.newFcmClients(a.Key)
d.clientsLen[i] = len(d.clientsFCM[a.APPID])
default:
log.Warn("unknown platform (%+v)", a)
}
}
return
}
}
func (d *Dao) newMiClients(pkg, auth string) (cs []*mi.Client) {
for i := 0; i < d.c.Android.PoolSize; i++ {
c := mi.NewClient(pkg, auth, time.Duration(d.c.Android.Timeout))
if env.DeployEnv == env.DeployEnvDev {
c.SetDevelopmentURL(mi.RegURL)
} else {
if d.c.Android.MiUseVip == model.SwitchOn {
c.SetVipURL(mi.RegURL)
} else {
c.SetProductionURL(mi.RegURL)
}
}
cs = append(cs, c)
}
return
}
func (d *Dao) newMiClientByMids(pkg, auth string) (c *mi.Client) {
c = mi.NewClient(pkg, auth, time.Duration(d.c.Android.Timeout))
if env.DeployEnv == env.DeployEnvDev {
c.SetDevelopmentURL(mi.AccountURL)
} else {
if d.c.Android.MiUseVip == model.SwitchOn {
c.SetVipURL(mi.RegURL)
} else {
c.SetProductionURL(mi.RegURL)
}
}
return
}
func (d *Dao) newApnsClients(platform int, cert, key, bundleID string) (res []*apns2.Client) {
var (
err error
certificate tls.Certificate
)
if certificate, err = tls.X509KeyPair([]byte(cert), []byte(key)); err != nil {
log.Error("tls.X509KeyPair(%s,%s) error(%v)", cert, key, err)
PromError("client:加载证书")
return
}
poolSize := d.c.Apns.PoolSize
if platform == model.PlatformIPad {
poolSize /= 5 // iPad量少只有iPhone的不到20%
}
for i := 0; i < poolSize; i++ {
var c *apns2.Client
if env.DeployEnv == env.DeployEnvDev {
if d.c.Apns.Proxy == model.SwitchOn {
c = apns2.NewClientWithProxy(certificate, time.Duration(d.c.Apns.Timeout), d.c.Apns.ProxySocket).Development()
} else {
c = apns2.NewClient(certificate, time.Duration(d.c.Apns.Timeout)).Development()
}
} else {
if d.c.Apns.Proxy == model.SwitchOn {
c = apns2.NewClientWithProxy(certificate, time.Duration(d.c.Apns.Timeout), d.c.Apns.ProxySocket).Production()
} else {
c = apns2.NewClient(certificate, time.Duration(d.c.Apns.Timeout)).Production()
}
}
c.BoundID = bundleID
res = append(res, c)
}
return
}
func (d *Dao) newHuaweiClients(appid int64, pkg string) (cs []*huawei.Client) {
retry := _retry
for retry > 0 {
if d.huaweiAuth[appid] != nil {
break
}
retry--
log.Info("retry huawei auth (%d)", retry)
time.Sleep(3 * time.Second)
}
if d.huaweiAuth[appid] == nil {
log.Error("no huawei auth app(%d)", appid)
return
}
for i := 0; i < d.c.Android.PoolSize; i++ {
c := huawei.NewClient(pkg, d.huaweiAuth[appid], time.Duration(d.c.Android.Timeout))
cs = append(cs, c)
}
return
}
func (d *Dao) newOppoClients(appid int64, activity string) (cs []*oppo.Client) {
retry := _retry
for retry > 0 {
if d.oppoAuth[appid] != nil {
break
}
retry--
log.Info("retry oppo auth (%d)", retry)
time.Sleep(3 * time.Second)
}
if d.oppoAuth[appid] == nil {
log.Error("no oppo auth app(%d)", appid)
return
}
for i := 0; i < d.c.Android.PoolSize; i++ {
c := oppo.NewClient(d.oppoAuth[appid], activity, time.Duration(d.c.Android.Timeout))
cs = append(cs, c)
}
return
}
func (d *Dao) newJpushClients(appKey, secret string) (cs []*jpush.Client) {
for i := 0; i < d.c.Android.PoolSize; i++ {
cs = append(cs, jpush.NewClient(appKey, secret, time.Duration(d.c.Android.Timeout)))
}
return
}
func (d *Dao) newFcmClients(key string) (cs []*fcm.Client) {
for i := 0; i < d.c.Android.PoolSize; i++ {
cs = append(cs, fcm.NewClient(key, time.Duration(d.c.Android.Timeout)))
}
return
}

View File

@@ -0,0 +1,27 @@
package dao
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestNewHuaweis(t *testing.T) {
Convey("test new huawei", t, WithDao(func(d *Dao) {
cs := d.newHuaweiClients(123, "")
So(len(cs), ShouldEqual, 0)
}))
}
func TestNewOppoClients(t *testing.T) {
Convey("test new huawei", t, WithDao(func(d *Dao) {
cs := d.newOppoClients(123, "")
So(len(cs), ShouldEqual, 0)
}))
}
func TestNewJpushClients(t *testing.T) {
Convey("test new huawei", t, WithDao(func(d *Dao) {
cs := d.newJpushClients("123", "")
So(len(cs), ShouldBeGreaterThan, 0)
}))
}

View File

@@ -0,0 +1,260 @@
package dao
import (
"context"
"time"
"go-common/app/service/main/push/conf"
"go-common/app/service/main/push/dao/apns2"
"go-common/app/service/main/push/dao/fcm"
"go-common/app/service/main/push/dao/huawei"
"go-common/app/service/main/push/dao/jpush"
"go-common/app/service/main/push/dao/mi"
"go-common/app/service/main/push/dao/oppo"
"go-common/app/service/main/push/model"
"go-common/library/cache/memcache"
xredis "go-common/library/cache/redis"
xsql "go-common/library/database/sql"
"go-common/library/log"
"go-common/library/queue/databus"
"go-common/library/stat/prom"
)
const (
_retry = 3
)
//go:generate $GOPATH/src/go-common/app/tool/cache/mc
type _mc interface {
//mc: -key=tokenKey -type=get
TokenCache(c context.Context, key string) (*model.Report, error)
//mc: -key=tokenKey -expire=d.mcReportExpire
AddTokenCache(c context.Context, key string, value *model.Report) error
//mc: -key=tokenKey -expire=d.mcReportExpire
AddTokensCache(c context.Context, values map[string]*model.Report) error
//mc: -key=tokenKey
DelTokenCache(c context.Context, key string) error
}
// Dao .
type Dao struct {
c *conf.Config
db *xsql.DB
mc *memcache.Pool
redis *xredis.Pool
reportPub *databus.Databus
callbackPub *databus.Databus
clientsIPhone map[int64][]*apns2.Client
clientsIPad map[int64][]*apns2.Client
clientsMi map[int64][]*mi.Client
clientMiByMids map[int64]*mi.Client
clientsHuawei map[int64][]*huawei.Client
clientsOppo map[int64][]*oppo.Client
clientsJpush map[int64][]*jpush.Client
clientsFCM map[int64][]*fcm.Client
clientsLen map[string]int
clientsIndex map[string]*uint32
huaweiAuth map[int64]*huawei.Access
oppoAuth map[int64]*oppo.Auth
addTaskStmt *xsql.Stmt
updateTaskStatusStmt *xsql.Stmt
updateTaskProgressStmt *xsql.Stmt
taskStmt *xsql.Stmt
businessesStmt *xsql.Stmt
settingStmt *xsql.Stmt
setSettingStmt *xsql.Stmt
authsStmt *xsql.Stmt
addReportStmt *xsql.Stmt
updateReportStmt *xsql.Stmt
reportStmt *xsql.Stmt
reportByIDStmt *xsql.Stmt
delReportStmt *xsql.Stmt
reportsByMidStmt *xsql.Stmt
lastReportIDStmt *xsql.Stmt
addCallbackStmt *xsql.Stmt
redisTokenExpire int32
redisLaterExpire int32
redisMidsExpire int32
mcReportExpire int32
mcSettingExpire int32
mcUUIDExpire int32
}
var (
errorsCount = prom.BusinessErrCount
infosCount = prom.BusinessInfoCount
missedCount = prom.CacheMiss
cachedCount = prom.CacheHit
)
// New creates a push-service DAO instance.
func New(c *conf.Config) *Dao {
d := &Dao{
c: c,
db: xsql.NewMySQL(c.MySQL),
mc: memcache.NewPool(c.Memcache.Config),
redis: xredis.NewPool(c.Redis.Config),
reportPub: databus.New(c.ReportPub),
callbackPub: databus.New(c.CallbackPub),
clientsIPhone: make(map[int64][]*apns2.Client),
clientsIPad: make(map[int64][]*apns2.Client),
clientsMi: make(map[int64][]*mi.Client),
clientMiByMids: make(map[int64]*mi.Client),
clientsHuawei: make(map[int64][]*huawei.Client),
clientsOppo: make(map[int64][]*oppo.Client),
clientsJpush: make(map[int64][]*jpush.Client),
clientsFCM: make(map[int64][]*fcm.Client),
clientsLen: make(map[string]int),
clientsIndex: make(map[string]*uint32),
huaweiAuth: make(map[int64]*huawei.Access),
oppoAuth: make(map[int64]*oppo.Auth),
redisTokenExpire: int32(time.Duration(c.Redis.TokenExpire) / time.Second),
redisLaterExpire: int32(time.Duration(c.Redis.LaterExpire) / time.Second),
redisMidsExpire: int32(time.Duration(c.Redis.MidsExpire) / time.Second),
mcReportExpire: int32(time.Duration(c.Memcache.ReportExpire) / time.Second),
mcSettingExpire: int32(time.Duration(c.Memcache.SettingExpire) / time.Second),
mcUUIDExpire: int32(time.Duration(c.Memcache.UUIDExpire) / time.Second),
}
d.addTaskStmt = d.db.Prepared(_addTaskSQL)
d.updateTaskStatusStmt = d.db.Prepared(_upadteTaskStatusSQL)
d.updateTaskProgressStmt = d.db.Prepared(_upadteTaskProgressSQL)
d.taskStmt = d.db.Prepared(_taskByIDSQL)
d.businessesStmt = d.db.Prepared(_businessesSQL)
d.settingStmt = d.db.Prepared(_settingSQL)
d.setSettingStmt = d.db.Prepared(_setSettingSQL)
d.authsStmt = d.db.Prepared(_authsSQL)
d.addReportStmt = d.db.Prepared(_addReportSQL)
d.updateReportStmt = d.db.Prepared(_updateReportSQL)
d.addCallbackStmt = d.db.Prepared(_addCallbackSQL)
d.reportStmt = d.db.Prepared(_reportSQL)
d.reportByIDStmt = d.db.Prepared(_reportByIDSQL)
d.delReportStmt = d.db.Prepared(_delReportSQL)
d.reportsByMidStmt = d.db.Prepared(_reportsByMidSQL)
d.lastReportIDStmt = d.db.Prepared(_lastReportIDSQL)
go d.refreshAuthproc()
time.Sleep(time.Second)
d.loadClients()
return d
}
func (d *Dao) refreshAuthproc() {
for {
auths, err := d.auths(context.Background())
if err != nil {
log.Error("d.auths() error(%v)", err)
time.Sleep(time.Second)
continue
}
for _, a := range auths {
d.refreshAuth(a)
}
time.Sleep(1 * time.Minute)
}
}
func (d *Dao) refreshAuth(a *model.Auth) {
i := fmtRoundIndex(a.APPID, a.PlatformID)
switch a.PlatformID {
case model.PlatformOppo:
if d.clientsLen[i] == 0 || d.oppoAuth[a.APPID] == nil || d.oppoAuth[a.APPID].IsExpired() {
auth, err := oppo.NewAuth(a.Key, a.Value)
if err != nil {
log.Error("new oppo auth failed, key(%s) secret(%s) error(%v)", a.Key, a.Value, err)
return
}
log.Info("oppo refresh auth app(%d) auth(%+v)", a.APPID, auth)
if d.oppoAuth[a.APPID] == nil {
d.oppoAuth[a.APPID] = new(oppo.Auth)
}
*d.oppoAuth[a.APPID] = *auth
if d.clientsLen[i] == 0 {
cs := d.newOppoClients(a.APPID, a.BundleID)
if len(cs) > 0 {
d.clientsOppo[a.APPID] = cs
d.clientsLen[i] = len(d.clientsOppo)
log.Info("oppo renew push clients app(%d)", a.APPID)
}
}
}
case model.PlatformHuawei:
if d.clientsLen[i] == 0 || d.huaweiAuth[a.APPID] == nil || d.huaweiAuth[a.APPID].IsExpired() {
ac, err := huawei.NewAccess(a.Key, a.Value)
if err != nil {
log.Error("new huawei access failed, id(%s) secret(%s) error(%v)", a.Key, a.Value, err)
return
}
log.Info("huawei refresh auth app(%d) auth(%+v)", a.APPID, ac)
if d.huaweiAuth[a.APPID] == nil {
d.huaweiAuth[a.APPID] = new(huawei.Access)
}
*d.huaweiAuth[a.APPID] = *ac
if d.clientsLen[i] == 0 {
cs := d.newHuaweiClients(a.APPID, a.BundleID)
if len(cs) > 0 {
d.clientsHuawei[a.APPID] = cs
d.clientsLen[i] = len(d.clientsHuawei)
log.Info("huawei renew push clients app(%d)", a.APPID)
}
}
}
}
}
// PromError prom error
func PromError(name string) {
errorsCount.Incr(name)
}
// PromInfo add prom info
func PromInfo(name string) {
infosCount.Incr(name)
}
// 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.db.Close(); err != nil {
log.Error("d.db.Close() error(%v)", err)
PromError("db:close")
}
if err = d.redis.Close(); err != nil {
log.Error("d.redis.Close() error(%v)", err)
PromError("redis:close")
}
if err = d.mc.Close(); err != nil {
log.Error("d.mc.Close() error(%v)", err)
PromError("mc:close")
}
return
}
// Ping check connection status.
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.pingRedis(c); err != nil {
PromError("redis:Ping")
log.Error("d.pingRedis error(%v)", err)
return
}
if err = d.pingMC(c); err != nil {
PromError("mc:Ping")
log.Error("d.pingMC error(%v)", err)
return
}
if err = d.db.Ping(c); err != nil {
PromError("mysql:Ping")
log.Error("d.db.Ping error(%v)", err)
}
return
}

View File

@@ -0,0 +1,85 @@
package dao
import (
"context"
"flag"
"os"
"path/filepath"
"testing"
"go-common/app/service/main/push/conf"
"go-common/app/service/main/push/model"
"go-common/library/cache/redis"
. "github.com/smartystreets/goconvey/convey"
)
var d *Dao
func init() {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.web-svr.push-service")
flag.Set("conf_token", "668329d872842a0079691e868e0fa12d")
flag.Set("tree_id", "35083")
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-service-test.toml")
flag.Set("conf", dir)
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
}
func WithDao(f func(d *Dao)) func() {
return func() {
Reset(func() { CleanCache() })
f(d)
}
}
func CleanCache() {
c := context.TODO()
redisPool := redis.NewPool(conf.Conf.Redis.Config)
redisPool.Get(c).Do("FLUSHDB")
}
func Test_MiInvalidTokens(t *testing.T) {
Convey("fetch mi invalid tokens", t, WithDao(func(d *Dao) {
// 用的时候打开,消息消费完了就没了
// err := d.DelInvalidMiReports(context.TODO())
// So(err, ShouldBeNil)
}))
}
func Test_buildAPNS(t *testing.T) {
Convey("build apns", t, func() {
info := &model.PushInfo{
TaskID: model.TempTaskID(),
APPID: 1,
Title: model.DefaultMessageTitle,
Summary: "bilibili",
LinkType: 8,
}
item := &model.PushItem{
Platform: 2,
Token: "sdfsdfewfsadfsdfsdf",
Mid: 888,
}
apns := buildAPNS(info, item)
t.Logf("apns(%+v)", apns)
})
}
func Test_RefreshAuth(t *testing.T) {
Convey("ping redis", t, WithDao(func(d *Dao) {
err := d.Ping(context.Background())
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,33 @@
package dao
import (
"context"
"strconv"
"go-common/app/service/main/push/model"
"go-common/library/log"
)
// PubReport add report to databus.
func (d *Dao) PubReport(c context.Context, info *model.Report) (err error) {
if err = d.reportPub.Send(c, info.Buvid, info); err != nil {
PromError("databus:发送上报的设备信息")
log.Error("d.reportPub.Send(%+v) error(%v)", info, err)
return
}
PromInfo("databus:发送上报的设备信息")
log.Info("PubReport(%+v) success.", info)
return
}
// PubCallback add push arrive/click callback to databus.
func (d *Dao) PubCallback(c context.Context, v []*model.Callback) (err error) {
if err = d.callbackPub.Send(c, strconv.Itoa(len(v)), v); err != nil {
PromError("databus:发送callback")
log.Error("d.callbackPub.Send(%+v) error(%v)", v, err)
return
}
PromInfo("databus:发送callback")
log.Info("PubCallback(%+v) success.", v)
return
}

View File

@@ -0,0 +1,24 @@
package dao
import (
"context"
"testing"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_PubReport(t *testing.T) {
Convey("Test_PubReport", t, WithDao(func(d *Dao) {
err := d.PubReport(context.Background(), &model.Report{})
So(err, ShouldBeNil)
}))
}
func Test_PubCallback(t *testing.T) {
Convey("Test_PubCallback", t, WithDao(func(d *Dao) {
err := d.PubCallback(context.Background(), []*model.Callback{&model.Callback{}})
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"model.go",
],
importpath = "go-common/app/service/main/push/dao/fcm",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,103 @@
package fcm
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
const (
// PriorityHigh used for high notification priority
PriorityHigh = "high"
// PriorityNormal used for normal notification priority
PriorityNormal = "normal"
// HeaderRetryAfter HTTP header constant
HeaderRetryAfter = "Retry-After"
// ErrorKey readable error caching
ErrorKey = "error"
// MethodPOST indicates http post method
MethodPOST = "POST"
// ServerURL push server url
ServerURL = "https://fcm.googleapis.com/fcm/send"
)
// retryableErrors whether the error is a retryable
var retryableErrors = map[string]bool{
"Unavailable": true,
"InternalServerError": true,
}
// Client stores client with api key to firebase
type Client struct {
APIKey string
HTTPClient *http.Client
}
// NewClient creates a new client
func NewClient(apiKey string, timeout time.Duration) *Client {
return &Client{
APIKey: apiKey,
HTTPClient: &http.Client{Timeout: timeout},
}
}
func (f *Client) authorization() string {
return fmt.Sprintf("key=%v", f.APIKey)
}
// Send sends message to FCM
func (f *Client) Send(message *Message) (*Response, error) {
data, err := json.Marshal(message)
if err != nil {
return &Response{}, err
}
req, err := http.NewRequest(MethodPOST, ServerURL, bytes.NewBuffer(data))
if err != nil {
return &Response{}, err
}
req.Header.Set("Authorization", f.authorization())
req.Header.Set("Content-Type", "application/json")
resp, err := f.HTTPClient.Do(req)
if err != nil {
return &Response{}, err
}
defer resp.Body.Close()
response := &Response{StatusCode: resp.StatusCode}
if resp.StatusCode >= 500 {
response.RetryAfter = resp.Header.Get(HeaderRetryAfter)
}
if resp.StatusCode != 200 {
return response, fmt.Errorf("fcm status code(%d)", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return response, err
}
if err := json.Unmarshal(body, &response); err != nil {
return response, err
}
if err := f.Failed(response); err != nil {
return response, err
}
response.Ok = true
return response, nil
}
// Failed method indicates if the server couldn't process
// the request in time.
func (f *Client) Failed(response *Response) error {
for _, response := range response.Results {
if retryableErrors[response.Error] {
return fmt.Errorf("fcm push error(%s)", response.Error)
}
}
return nil
}

View File

@@ -0,0 +1,68 @@
package fcm
import (
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"unicode"
. "github.com/smartystreets/goconvey/convey"
)
const apiKey = "AIzaSyBtMplqJkuTIDyIx-CM74MoPHbxHCBcYYQ"
func TestPush(t *testing.T) {
Convey("test jpush", t, func() {
data := map[string]string{
"task_id": "123456",
// "scheme": model.Scheme(model.LinkTypeVideo, "123", model.PlatformAndroid, 390000),
"scheme": "bilibili://video/123",
}
client := NewClient(apiKey, 5*time.Second)
message := &Message{
// DryRun: true, // 如果是 true消息不会下发给用户用于测试
Data: data,
RegistrationIDs: []string{"fpICefK-jfE:APA91bHjZTxe503tpFoFMmXXX9LAiMmg7OwgTPYmTb8Ox-yF88umTQnmTQUGbALplxqre7R6v3d0-vSK5MyT4jFtSqklbY1GIaM4d8uZ0wJlwWrRWdBDeOJ4rlpvamd3aGyBlHKAH18N"},
Priority: PriorityHigh,
DelayWhileIdle: true,
Notification: Notification{
Title: "Hello",
Body: "World",
ClickAction: "com.bilibili.app.in.com.bilibili.push.FCM_MESSAGE",
},
CollapseKey: strings.TrimFunc("t123456", func(r rune) bool {
return !unicode.IsNumber(r)
}), // 值转成 int 传到客户端
TimeToLive: int(time.Hour.Seconds()),
Android: Android{Priority: PriorityHigh},
}
response, err := client.Send(message)
msgb, _ := json.Marshal(message)
fmt.Printf("msg(%s)", msgb)
So(err, ShouldNotBeNil)
if err != nil {
t.Errorf("fcm send response(%+v) error(%v)", response, err)
} else {
fmt.Println("Status Code :", response.StatusCode)
fmt.Println("Success :", response.Success)
fmt.Println("Fail :", response.Fail)
fmt.Println("Canonical_ids :", response.CanonicalIDs)
fmt.Println("Topic MsgId :", response.MsgID)
}
})
}
func Test_ClientFaild(t *testing.T) {
Convey("test jpush", t, func() {
client := NewClient(apiKey, 5*time.Second)
err := client.Failed(&Response{})
So(err, ShouldBeNil)
r := &Response{RetryAfter: "3m"}
_, err = r.GetRetryAfterTime()
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,220 @@
package fcm
import "time"
// Message represents fcm request message
type (
Message struct {
// Data parameter specifies the custom key-value pairs of the message's payload.
//
// For example, with data:{"score":"3x1"}:
//
// On iOS, if the message is sent via APNS, it represents the custom data fields.
// If it is sent via FCM connection server, it would be represented as key value dictionary
// in AppDelegate application:didReceiveRemoteNotification:.
// On Android, this would result in an intent extra named score with the string value 3x1.
// The key should not be a reserved word ("from" or any word starting with "google" or "gcm").
// Do not use any of the words defined in this table (such as collapse_key).
// Values in string types are recommended. You have to convert values in objects
// or other non-string data types (e.g., integers or booleans) to string.
//
Data interface{} `json:"data,omitempty"`
// To this parameter specifies the recipient of a message.
//
// The value must be a registration token, notification key, or topic.
// Do not set this field when sending to multiple topics. See Condition.
To string `json:"to,omitempty"`
// RegistrationIDs for all registration ids
// This parameter specifies a list of devices
// (registration tokens, or IDs) receiving a multicast message.
// It must contain at least 1 and at most 1000 registration tokens.
// Use this parameter only for multicast messaging, not for single recipients.
// Multicast messages (sending to more than 1 registration tokens)
// are allowed using HTTP JSON format only.
RegistrationIDs []string `json:"registration_ids,omitempty"`
// CollapseKey This parameter identifies a group of messages
// (e.g., with collapse_key: "Updates Available") that can be collapsed,
// so that only the last message gets sent when delivery can be resumed.
// This is intended to avoid sending too many of the same messages when the
// device comes back online or becomes active (see delay_while_idle).
CollapseKey string `json:"collapse_key,omitempty"`
// Priority Sets the priority of the message. Valid values are "normal" and "high."
// On iOS, these correspond to APNs priorities 5 and 10.
// By default, notification messages are sent with high priority, and data messages
// are sent with normal priority. Normal priority optimizes the client app's battery
// consumption and should be used unless immediate delivery is required. For messages
// with normal priority, the app may receive the message with unspecified delay.
// When a message is sent with high priority, it is sent immediately, and the app
// can wake a sleeping device and open a network connection to your server.
// For more information, see Setting the priority of a message.
Priority string `json:"priority,omitempty"`
// Notification parameter specifies the predefined, user-visible key-value pairs of
// the notification payload. See Notification payload support for detail.
// For more information about notification message and data message options, see
// Notification
Notification Notification `json:"notification,omitempty"`
// ContentAvailable On iOS, use this field to represent content-available
// in the APNS payload. When a notification or message is sent and this is set
// to true, an inactive client app is awoken. On Android, data messages wake
// the app by default. On Chrome, currently not supported.
ContentAvailable bool `json:"content_available,omitempty"`
// DelayWhenIdle When this parameter is set to true, it indicates that
// the message should not be sent until the device becomes active.
// The default value is false.
DelayWhileIdle bool `json:"delay_while_idle,omitempty"`
// TimeToLive This parameter specifies how long (in seconds) the message
// should be kept in FCM storage if the device is offline. The maximum time
// to live supported is 4 weeks, and the default value is 4 weeks.
// For more information, see
// https://firebase.google.com/docs/cloud-messaging/concept-options#ttl
TimeToLive int `json:"time_to_live,omitempty"`
// RestrictedPackageName This parameter specifies the package name of the
// application where the registration tokens must match in order to
// receive the message.
RestrictedPackageName string `json:"restricted_package_name,omitempty"`
// DryRun This parameter, when set to true, allows developers to test
// a request without actually sending a message.
// The default value is false
DryRun bool `json:"dry_run,omitempty"`
// Condition to set a logical expression of conditions that determine the message target
// This parameter specifies a logical expression of conditions that determine the message target.
// Supported condition: Topic, formatted as "'yourTopic' in topics". This value is case-insensitive.
// Supported operators: &&, ||. Maximum two operators per topic message supported.
Condition string `json:"condition,omitempty"`
// Currently for iOS 10+ devices only. On iOS, use this field to represent mutable-content in the APNS payload.
// When a notification is sent and this is set to true, the content of the notification can be modified before
// it is displayed, using a Notification Service app extension. This parameter will be ignored for Android and web.
MutableContent bool `json:"mutable_content,omitempty"`
Android Android `json:"android,omitempty"`
}
Android struct {
Priority string `json:"priority,omitempty"`
}
// Result Downstream result from FCM, sent in the "results" field of the Response packet
Result struct {
// String specifying a unique ID for each successfully processed message.
MessageID string `json:"message_id"`
// Optional string specifying the canonical registration token for the
// client app that the message was processed and sent to. Sender should
// use this value as the registration token for future requests.
// Otherwise, the messages might be rejected.
RegistrationID string `json:"registration_id"`
// String specifying the error that occurred when processing the message
// for the recipient. The possible values can be found in table 9 here:
// https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9
Error string `json:"error"`
}
// Response represents fcm response message - (tokens and topics)
Response struct {
Ok bool
StatusCode int
// MulticastID a unique ID (number) identifying the multicast message.
MulticastID int `json:"multicast_id"`
// Success number of messages that were processed without an error.
Success int `json:"success"`
// Fail number of messages that could not be processed.
Fail int `json:"failure"`
// CanonicalIDs number of results that contain a canonical registration token.
// A canonical registration ID is the registration token of the last registration
// requested by the client app. This is the ID that the server should use
// when sending messages to the device.
CanonicalIDs int `json:"canonical_ids"`
// Results Array of objects representing the status of the messages processed. The objects are listed in the same order as the request (i.e., for each registration ID in the request, its result is listed in the same index in the response).
// message_id: String specifying a unique ID for each successfully processed message.
// registration_id: Optional string specifying the canonical registration token for the client app that the message was processed and sent to. Sender should use this value as the registration token for future requests. Otherwise, the messages might be rejected.
// error: String specifying the error that occurred when processing the message for the recipient. The possible values can be found in table 9.
Results []Result `json:"results,omitempty"`
// The topic message ID when FCM has successfully received the request and will attempt to deliver to all subscribed devices.
MsgID int `json:"message_id,omitempty"`
// Error that occurred when processing the message. The possible values can be found in table 9.
Err string `json:"error,omitempty"`
// RetryAfter
RetryAfter string
}
// Notification notification message payload
Notification struct {
// Title indicates notification title. This field is not visible on iOS phones and tablets.
Title string `json:"title,omitempty"`
// Body indicates notification body text.
Body string `json:"body,omitempty"`
// Sound indicates a sound to play when the device receives a notification.
// Sound files can be in the main bundle of the client app or in the
// Library/Sounds folder of the app's data container.
// See the iOS Developer Library for more information.
// http://apple.co/2jaGqiE
Sound string `json:"sound,omitempty"`
// Badge indicates the badge on the client app home icon.
Badge string `json:"badge,omitempty"`
// Icon indicates notification icon. Sets value to myicon for drawable resource myicon.
// If you don't send this key in the request, FCM displays the launcher icon specified
// in your app manifest.
Icon string `json:"icon,omitempty"`
// Tag indicates whether each notification results in a new entry in the notification
// drawer on Android. If not set, each request creates a new notification.
// If set, and a notification with the same tag is already being shown,
// the new notification replaces the existing one in the notification drawer.
Tag string `json:"tag,omitempty"`
// Color indicates color of the icon, expressed in #rrggbb format
Color string `json:"color,omitempty"`
// ClickAction indicates the action associated with a user click on the notification.
// When this is set, an activity with a matching intent filter is launched when user
// clicks the notification.
ClickAction string `json:"click_action,omitempty"`
// BodyLockKey indicates the key to the body string for localization. Use the key in
// the app's string resources when populating this value.
BodyLocKey string `json:"body_loc_key,omitempty"`
// BodyLocArgs indicates the string value to replace format specifiers in the body
// string for localization. For more information, see Formatting and Styling.
BodyLocArgs string `json:"body_loc_args,omitempty"`
// TitleLocKey indicates the key to the title string for localization.
// Use the key in the app's string resources when populating this value.
TitleLocKey string `json:"title_loc_key,omitempty"`
// TitleLocArgs indicates the string value to replace format specifiers in the title string for
// localization. For more information, see
// https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
TitleLocArgs string `json:"title_loc_args,omitempty"`
}
)
// GetRetryAfterTime converts the retry after response header to a time.Duration
func (r *Response) GetRetryAfterTime() (time.Duration, error) {
return time.ParseDuration(r.RetryAfter)
}

View File

@@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"access_test.go",
"client_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"access.go",
"client.go",
"message.go",
],
importpath = "go-common/app/service/main/push/dao/huawei",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/stat: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,65 @@
package huawei
// http://developer.huawei.com/consumer/cn/service/hms/catalog/huaweipush.html?page=hmssdk_huaweipush_api_reference_s1
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
const (
_accessTokenURL = "https://login.vmall.com/oauth2/token"
_grantType = "client_credentials"
_respCodeSuccess = 0
)
// Access huawei access token.
type Access struct {
AppID string
Token string
Expire int64
}
type accessResponse struct {
Token string `json:"access_token"`
Expire int64 `json:"expires_in"` // Access Token的有效期以秒为单位
Scope string `json:"scope"` // Access Token的访问范围即用户实际授予的权限列表
Code int `json:"error"`
Desc string `json:"error_description"`
}
// NewAccess get token.
func NewAccess(clientID, clientSecret string) (a *Access, err error) {
params := url.Values{}
params.Add("grant_type", _grantType)
params.Add("client_id", clientID)
params.Add("client_secret", clientSecret)
res, err := http.PostForm(_accessTokenURL, params)
if err != nil {
return
}
defer res.Body.Close()
dc := json.NewDecoder(res.Body)
resp := new(accessResponse)
if err = dc.Decode(resp); err != nil {
return
}
if resp.Code == _respCodeSuccess {
a = &Access{
AppID: clientID,
Token: resp.Token,
Expire: time.Now().Unix() + resp.Expire,
}
return
}
err = fmt.Errorf("new access error, code(%d) description(%s)", resp.Code, resp.Desc)
return
}
// IsExpired judge that whether privilige expired.
func (a *Access) IsExpired() bool {
return a.Expire <= time.Now().Add(8*time.Hour).Unix() // 提前8小时过期for renew auth
}

View File

@@ -0,0 +1,32 @@
package huawei
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_NewAccess(t *testing.T) {
Convey("new access", t, func() {
ac, err := NewAccess("10125085", "iejq6hn3ds3d4neq1m21v443lmbm31gs")
if err != nil {
t.Errorf("new access error(%v)", err)
} else {
t.Log(ac.Token, ac.Expire)
}
})
}
func Test_AccessExpire(t *testing.T) {
Convey("access expire", t, func() {
ac := Access{Expire: time.Now().Add(-8 * time.Hour).Unix()}
if !ac.IsExpired() {
t.Errorf("access should be expire")
}
ac.Expire -= 10
if ac.IsExpired() {
t.Error("access should not be expire")
}
})
}

View File

@@ -0,0 +1,134 @@
package huawei
// http://developer.huawei.com/consumer/cn/service/hms/catalog/huaweipush.html?page=hmssdk_huaweipush_api_reference_s2
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go-common/library/log"
"go-common/library/stat"
"go-common/library/stat/prom"
)
const (
_pushURL = "https://api.push.hicloud.com/pushsend.do"
_nspSvc = "openpush.message.api.send"
_ver = "1" // current SDK version
// ResponseCodeSuccess success code
ResponseCodeSuccess = "80000000"
// ResponseCodeSomeTokenInvalid some tokens failed
ResponseCodeSomeTokenInvalid = "80100000"
// ResponseCodeAllTokenInvalid all tokens failed
ResponseCodeAllTokenInvalid = "80100002"
// ResponseCodeAllTokenInvalidNew .
ResponseCodeAllTokenInvalidNew = "80300007"
)
var (
// ErrLimit .
ErrLimit = errors.New("触发华为系统级流控")
)
// Client huawei push http client.
type Client struct {
Access *Access
HTTPClient *http.Client
Stats stat.Stat
SDKCtx string
Package string
}
// ver huawei push service version.
type ver struct {
Ver string `json:"ver"`
AppID string `json:"appId"`
}
// NewClient new huawei push HTTP client.
func NewClient(pkg string, a *Access, timeout time.Duration) *Client {
ctx, _ := json.Marshal(ver{Ver: _ver, AppID: a.AppID})
return &Client{
Access: a,
HTTPClient: &http.Client{Timeout: timeout},
Stats: prom.HTTPClient,
SDKCtx: string(ctx),
Package: pkg,
}
}
/*
Push push notifications.
access_token: 必选使用OAuth2进行鉴权时的ACCESSTOKEN
nsp_ts: 必选服务请求时间戳自GMT 时间 1970-1-1 0:0:0至今的秒数。如果传入的时间与服务器时间相 差5分钟以上服务器可能会拒绝请求。
nsp_svc: 必选, 本接口固定为openpush.message.api.send
device_token_list: 以半角逗号分隔的华为PUSHTOKEN列表单次最多只是1000个
expire_time: 格式ISO 8601[6]:2013-06-03T17:30采用本地时间精确到分钟
payload: 描述投递消息的JSON结构体描述PUSH消息的:类型、内容、显示、点击动作、报表统计和扩展信 息。具体参考下面的详细说明。
*/
func (c *Client) Push(payload *Message, tokens []string, expire time.Time) (response *Response, err error) {
now := time.Now()
if c.Stats != nil {
defer func() {
c.Stats.Timing(_pushURL, int64(time.Since(now)/time.Millisecond))
log.Info("huawei stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(_pushURL, "failed")
}
}()
}
pl, _ := payload.SetPkg(c.Package).JSON()
reqURL := _pushURL + "?nsp_ctx=" + url.QueryEscape(c.SDKCtx)
tokenStr, _ := json.Marshal(tokens)
params := url.Values{}
params.Add("access_token", c.Access.Token)
params.Add("nsp_ts", strconv.FormatInt(now.Unix(), 10))
params.Add("nsp_svc", _nspSvc)
params.Add("device_token_list", string(tokenStr))
params.Add("expire_time", expire.Format("2006-01-02T15:04"))
params.Add("payload", pl)
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(params.Encode()))
if err != nil {
log.Error("http.NewRequest() error(%v)", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Connection", "Keep-Alive")
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
if res.StatusCode == http.StatusServiceUnavailable {
return nil, ErrLimit
}
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("huawei Push http code(%d)", res.StatusCode)
return
}
response = &Response{}
nspStatus := res.Header.Get("NSP_STATUS")
if nspStatus != "" {
log.Error("push huawei system error, NSP_STATUS(%s)", nspStatus)
response.Code = nspStatus
response.Msg = "NSP_STATUS error"
return
}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
}
if err = json.Unmarshal(bs, &response); err != nil {
log.Error("json decode body(%s) error(%v)", string(bs), err)
}
return
}

View File

@@ -0,0 +1,32 @@
package huawei
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Push(t *testing.T) {
Convey("push huawei", t, func() {
// ac, err := NewAccess("10125085", "iejq6hn3ds3d4neq1m21v443lmbm31gs")
// if err != nil {
// t.Fatal(err)
// } else {
// t.Log(ac)
// }
// return
ac := &Access{
AppID: "10125085",
Token: "CFrF0b079efz2JUoDNBs1lwk9wtL4LfxExYqZvM3lAuDAeZcytQS3CPjYO6qMv9h+6FJoKrGIsQEwcKOmODdeg==",
Expire: 1522913725,
}
palyod := NewMessage().SetContent("huawei-content").SetTitle("huawei-title").SetCustomize("task_id", "123").SetCustomize("scheme", "bilibili://search/你好").SetIcon("http://pic.qiantucdn.com/58pic/12/38/18/13758PIC4GV.jpg")
c := NewClient("tv.danmaku.bili", ac, time.Minute)
// tokens := []string{"0866090037077934300001050400CN01"}
tokens := []string{"1", "2", ""}
res, err := c.Push(palyod, tokens, time.Now().Add(time.Hour))
So(err, ShouldBeNil)
t.Logf("huawei push res(%+v)", res)
})
}

View File

@@ -0,0 +1,179 @@
package huawei
import (
"encoding/json"
)
const (
// MsgTypePassthrough 消息类型:透传
MsgTypePassthrough = 1
// MsgTypeNotification 消息类型:通知栏消息
MsgTypeNotification = 3
// ActionTypeCustom 动作类型:自定义
ActionTypeCustom = 1
// ActionTypeURL 动作类型:打开URL
ActionTypeURL = 2
// ActionTypeAPP 动作类型:打开APP
ActionTypeAPP = 3
// CallbackTokenUninstalled 应用被卸载了
CallbackTokenUninstalled = 2
// CallbackTokenNotApply 终端安装了该应用但从未打开过未申请token所以不能展示
CallbackTokenNotApply = 5
// CallbackTokenInactive 非活跃设备,消息丢弃
CallbackTokenInactive = 10
)
// Response push response.
type Response struct {
Code string `json:"code"`
Msg string `json:"msg"`
Err string `json:"error"`
RequestID string `json:"requestId"`
}
// InvalidTokenResponse invalid tokens info in the push response.
type InvalidTokenResponse struct {
Success int `json:"success"`
Failure int `json:"failure"`
IllegalTokens []string `json:"illegal_tokens"`
}
// Message request message.
type Message struct {
Hps Hps `json:"hps"`
}
// Hps .
type Hps struct {
Msg Msg `json:"msg"`
Ext Ext `json:"ext"`
}
// Msg .
type Msg struct {
Type int `json:"type"`
Body Body `json:"body"`
Action Action `json:"action"`
}
// Body .
type Body struct {
Content string `json:"content"`
Title string `json:"title"`
}
// Action .
type Action struct {
Type int `json:"type"`
Param Param `json:"param"`
}
// Param .
type Param struct {
Intent string `json:"intent"`
AppPkgName string `json:"appPkgName"`
}
// Ext .
type Ext struct {
BiTag string `json:"biTag"`
Icon string `json:"icon"`
Customize []map[string]string `json:"customize"`
}
// Callback 华为推送回执(回调)
type Callback struct {
Statuses []*CallbackItem `json:"statuses"`
}
// CallbackItem http://developer.huawei.com/consumer/cn/service/hms/catalog/huaweipush_agent.html?page=hmssdk_huaweipush_devguide_server_agent#3.3 消息回执
type CallbackItem struct {
BiTag string `json:"biTag"`
AppID string `json:"appid"`
Token string `json:"token"`
Status int `json:"status"`
Timestamp int64 `json:"timestamp"`
}
// NewMessage get message.
func NewMessage() *Message {
return &Message{
Hps: Hps{
Msg: Msg{
Type: MsgTypeNotification, //1 透传异步消息, 3 系统通知栏异步消息 注意:2和4以后为保留后续扩展使用
Body: Body{
Content: "",
Title: "",
},
Action: Action{
Type: ActionTypeAPP, //1 自定义行为, 2 打开URL ,3 打开App
Param: Param{},
},
},
Ext: Ext{ //扩展信息含BI消息统计特定展示风格消息折叠。
BiTag: "Trump", // 设置消息标签如果带了这个标签会在回执中推送给CP用于检测某种类型消息的到达率和状态
},
},
}
}
// SetContent sets content.
func (m *Message) SetContent(content string) *Message {
m.Hps.Msg.Body.Content = content
return m
}
// SetTitle sets title.
func (m *Message) SetTitle(title string) *Message {
m.Hps.Msg.Body.Title = title
return m
}
// SetMsgType sets title.
func (m *Message) SetMsgType(typ int) *Message {
m.Hps.Msg.Type = typ
return m
}
// SetIntent sets intent.
func (m *Message) SetIntent(intent string) *Message {
m.Hps.Msg.Action.Param.Intent = intent
return m
}
// SetPkg sets app package name.
func (m *Message) SetPkg(pkg string) *Message {
m.Hps.Msg.Action.Param.AppPkgName = pkg
return m
}
// SetCustomize set ext info.
func (m *Message) SetCustomize(key, val string) *Message {
mp := map[string]string{key: val}
m.Hps.Ext.Customize = append(m.Hps.Ext.Customize, mp)
return m
}
// SetBiTag set biTag.
func (m *Message) SetBiTag(tag string) *Message {
m.Hps.Ext.BiTag = tag
return m
}
// SetIcon sets icon.
func (m *Message) SetIcon(url string) *Message {
m.Hps.Ext.Icon = url
return m
}
// JSON encode the message.
func (m *Message) JSON() (res string, err error) {
bytes, err := json.Marshal(m)
if err != nil {
return
}
res = string(bytes)
return
}

View File

@@ -0,0 +1,54 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"audience.go",
"callback.go",
"client.go",
"errcode.go",
"message.go",
"notice.go",
"option.go",
"payload.go",
"platform.go",
"report.go",
"schedule.go",
],
importpath = "go-common/app/service/main/push/dao/jpush",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/stat: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,48 @@
package jpush
const (
_audienceTag = "tag"
_audienceTagAnd = "tag_and"
_audienceAlias = "alias"
_audienceID = "registration_id"
_audienceAll = "all"
)
// Audience .
type Audience struct {
Object interface{}
audience map[string][]string
}
// All .
func (a *Audience) All() {
a.Object = _audienceAll
}
// SetID .
func (a *Audience) SetID(ids []string) {
a.set(_audienceID, ids)
}
// SetTag .
func (a *Audience) SetTag(tags []string) {
a.set(_audienceTag, tags)
}
// SetTagAnd .
func (a *Audience) SetTagAnd(tags []string) {
a.set(_audienceTagAnd, tags)
}
// SetAlias .
func (a *Audience) SetAlias(alias []string) {
a.set(_audienceAlias, alias)
}
func (a *Audience) set(key string, v []string) {
if a.Object == nil {
a.audience = map[string][]string{key: v}
a.Object = a.audience
}
a.audience[key] = v
}

View File

@@ -0,0 +1,77 @@
package jpush
const (
// CallbackTypeReceive 送达才回执
CallbackTypeReceive = callbackType(1)
// CallbackTypeClick 点击才回执
CallbackTypeClick = callbackType(2)
// CallbackTypeAll 送达和点击都回执
CallbackTypeAll = callbackType(3)
// StatusSwitchOn 回执时候通知栏开关状态:开
StatusSwitchOn = int(1)
// StatusSwitchOff 回执时候通知栏开关状态:关
StatusSwitchOff = int(2)
defaultCallbackURL = "https://api.bilibili.com/x/push/callback/jpush"
)
type callbackType int
// CallbackReq 消息回执请求体
type CallbackReq struct {
// URL 接受回执数据的URL
URL string `json:"url"`
// Type 需要的回执类型
Type callbackType `json:"type"`
// Params 携带的自定义参数
Params map[string]string `json:"params"`
}
// NewCallbackReq new Callback
func NewCallbackReq() *CallbackReq {
return &CallbackReq{
URL: defaultCallbackURL,
Type: CallbackTypeReceive,
Params: make(map[string]string),
}
}
// SetURL 设置接收回执的URL
func (cb *CallbackReq) SetURL(url string) {
if url == "" {
return
}
cb.URL = url
}
// SetType 设置需要回执的类型
func (cb *CallbackReq) SetType(typ callbackType) {
cb.Type = typ
}
// SetParam 设置自定义参数
func (cb *CallbackReq) SetParam(m map[string]string) {
if m == nil {
return
}
cb.Params = m
}
// CallbackReply 消息回执接收体
type CallbackReply struct {
// Token device token
Token string `json:"registration_id"`
// Platform android or ios
Platform string `json:"platform"`
// Time 消息送达或点击的秒级时间戳
Time int64 `json:"sent_time"`
// Switch 通知栏消息开关
Switch bool `json:"notification_state"`
// Type 送达或点击
Type callbackType `json:"callback_type"`
// Channel 下发通道
Channel int `json:"channel"`
// Params 自定义参数
Params map[string]string `json:"params"`
}

View File

@@ -0,0 +1,92 @@
package jpush
import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"time"
"go-common/library/stat"
"go-common/library/stat/prom"
)
const (
_charset = "UTF-8"
_contentTypeJSON = "application/json"
_pushURL = "https://api.jpush.cn/v3/push"
// _scheduleURL = "https://api.jpush.cn/v3/schedules"
// _reportURL = "https://report.jpush.cn/v3/received"
)
// PushResponse .
type PushResponse struct {
SendNo interface{} `json:"sendno,omitempty"`
MsgID interface{} `json:"msg_id,omitempty"`
IllegalTokens []string `json:"illegal_rids,omitempty"`
Retry bool // 是否需要重试请求
Error struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
// Client jpush http client.
type Client struct {
Auth string
Stats stat.Stat
Timeout time.Duration
}
// NewClient new client.
func NewClient(appKey, secret string, timeout time.Duration) *Client {
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(appKey+":"+secret))
return &Client{
Auth: auth,
Stats: prom.HTTPClient,
Timeout: timeout,
}
}
// Push push notification.
func (cli *Client) Push(payload *Payload) (res *PushResponse, err error) {
res = new(PushResponse)
if cli.Stats != nil {
now := time.Now()
defer func() {
cli.Stats.Timing(_pushURL, int64(time.Since(now)/time.Millisecond))
// log.Info("jpush stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
cli.Stats.Incr(_pushURL, "failed")
}
}()
}
bs, err := payload.ToBytes()
if err != nil {
return
}
req, _ := http.NewRequest("POST", _pushURL, bytes.NewBuffer(bs))
req.Header.Add("Charset", _charset)
req.Header.Add("Authorization", cli.Auth)
req.Header.Add("Content-Type", _contentTypeJSON)
client := &http.Client{Timeout: cli.Timeout}
resp, err := client.Do(req)
if err != nil {
res.Retry = true
return
}
defer resp.Body.Close()
r, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = json.Unmarshal(r, &res); err != nil {
return
}
if res.Error.Code == ErrRetry || res.Error.Code == ErrInternal {
res.Retry = true
}
return
}

View File

@@ -0,0 +1,47 @@
package jpush
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestPush(t *testing.T) {
Convey("test jpush", t, func() {
var (
ad Audience
notice Notice
plat = NewPlatform(PlatformAndroid)
payload = NewPayload()
cbr = NewCallbackReq()
an = &AndroidNotice{
Title: "test title",
Alert: "test alert",
AlertType: AndroidAlertTypeLight | AndroidAlertTypeSound, // 通知提醒类型
Extras: map[string]interface{}{
"task_id": "tid",
"scheme": "bili:///?type=bilivideo&avid=123",
},
}
)
// ad.SetID([]string{"190e35f7e068f4a19d1"})
ad.SetID([]string{""})
notice.SetAndroidNotice(an)
payload.SetPlatform(plat)
payload.SetAudience(&ad)
payload.SetNotice(&notice)
payload.Options.SetTimelive(1000)
payload.Options.SetReturnInvalidToken(true)
cbr.SetParam(map[string]string{"task": "tid"})
payload.SetCallbackReq(cbr)
// bs, err := payload.ToBytes()
// fmt.Printf("payload(%s) error(%v)", bs, err)
cli := NewClient("62396b3e57f0b2b4b2c7bf48", "588f56e1bedd3c6b46db4863", time.Second)
res, err := cli.Push(payload)
So(err, ShouldBeNil)
t.Logf("push result(%+v)", res)
})
}

View File

@@ -0,0 +1,11 @@
package jpush
const (
// ErrAllTokenInvalid 所有的token都无效
// ErrAllTokensInvalid = int(1060)
// ErrRetry 需要重试
ErrRetry = int(1030)
// ErrInternal 服务方内部错误
ErrInternal = int(1000)
)

View File

@@ -0,0 +1,33 @@
package jpush
// Message .
type Message struct {
Content string `json:"msg_content"`
Title string `json:"title,omitempty"`
ContentType string `json:"content_type,omitempty"`
Extras map[string]interface{} `json:"extras,omitempty"`
}
// SetContent .
func (m *Message) SetContent(c string) {
m.Content = c
}
// SetTitle .
func (m *Message) SetTitle(title string) {
m.Title = title
}
// SetContentType .
func (m *Message) SetContentType(t string) {
m.ContentType = t
}
// AddExtras .
func (m *Message) AddExtras(key string, value interface{}) {
if m.Extras == nil {
m.Extras = make(map[string]interface{})
}
m.Extras[key] = value
}

View File

@@ -0,0 +1,87 @@
package jpush
const (
// AndroidAlertTypeAll 全开
AndroidAlertTypeAll = -1
// AndroidAlertTypeNone 全关
AndroidAlertTypeNone = 0
// AndroidAlertTypeSound 开声音
AndroidAlertTypeSound = 1
// AndroidAlertTypeVibrate 开振动
AndroidAlertTypeVibrate = 2
// AndroidAlertTypeLight 开呼吸灯
AndroidAlertTypeLight = 4
// AndroidStyleDefault 默认通知栏样式
AndroidStyleDefault = 0
// AndroidStyleBigTxt big_text 字段大文本的形式展示
AndroidStyleBigTxt = 1
// AndroidStyleInbox inbox 字段 json 的每个 key 对应的 value 会被当作文本条目逐条展示
AndroidStyleInbox = 2
// AndroidStylePic big_pic_path 字段的图片URL展示成图片
AndroidStylePic = 3
)
// Notice .
type Notice struct {
Alert string `json:"alert,omitempty"`
Android *AndroidNotice `json:"android,omitempty"`
IOS *IOSNotice `json:"ios,omitempty"`
WINPhone *WinPhoneNotice `json:"winphone,omitempty"`
}
// AndroidNotice .
type AndroidNotice struct {
Alert string `json:"alert"`
Title string `json:"title,omitempty"`
AlertType int `json:"alert_type"`
BuilderID int `json:"builder_id,omitempty"`
Style int `json:"style,omitempty"`
BigPicPath string `json:"big_pic_path,omitempty"`
Extras map[string]interface{} `json:"extras,omitempty"`
}
// SetPic sets Android notice pic.
func (an *AndroidNotice) SetPic(pic string) {
an.Style = AndroidStylePic
an.BigPicPath = pic
}
// IOSNotice .
type IOSNotice struct {
Alert interface{} `json:"alert"`
Sound string `json:"sound,omitempty"`
Badge string `json:"badge,omitempty"`
ContentAvailable bool `json:"content-available,omitempty"`
MutableContent bool `json:"mutable-content,omitempty"`
Category string `json:"category,omitempty"`
Extras map[string]interface{} `json:"extras,omitempty"`
}
// WinPhoneNotice .
type WinPhoneNotice struct {
Alert string `json:"alert"`
Title string `json:"title,omitempty"`
OpenPage string `json:"_open_page,omitempty"`
Extras map[string]interface{} `json:"extras,omitempty"`
}
// SetAlert .
func (n *Notice) SetAlert(alert string) {
n.Alert = alert
}
// SetAndroidNotice .
func (n *Notice) SetAndroidNotice(an *AndroidNotice) {
n.Android = an
}
// SetIOSNotice .
func (n *Notice) SetIOSNotice(in *IOSNotice) {
n.IOS = in
}
// SetWinPhoneNotice .
func (n *Notice) SetWinPhoneNotice(wn *WinPhoneNotice) {
n.WINPhone = wn
}

View File

@@ -0,0 +1,41 @@
package jpush
// Option .
type Option struct {
SendNo int `json:"sendno,omitempty"`
TimeLive int `json:"time_to_live,omitempty"`
ApnsProduction bool `json:"apns_production"`
OverrideMsgID int64 `json:"override_msg_id,omitempty"`
BigPushDuration int `json:"big_push_duration,omitempty"`
ReturnInvalidToken bool `json:"return_invalid_rid,omitempty"` // 是否同步返回无效的token
}
// SetSendno .
func (o *Option) SetSendno(no int) {
o.SendNo = no
}
// SetTimelive .
func (o *Option) SetTimelive(timelive int) {
o.TimeLive = timelive
}
// SetOverrideMsgID .
func (o *Option) SetOverrideMsgID(id int64) {
o.OverrideMsgID = id
}
// SetApns .
func (o *Option) SetApns(apns bool) {
o.ApnsProduction = apns
}
// SetBigPushDuration .
func (o *Option) SetBigPushDuration(dur int) {
o.BigPushDuration = dur
}
// SetReturnInvalidToken .
func (o *Option) SetReturnInvalidToken(onoff bool) {
o.ReturnInvalidToken = onoff
}

View File

@@ -0,0 +1,61 @@
package jpush
import (
"encoding/json"
)
// Payload .
type Payload struct {
Platform interface{} `json:"platform"`
Audience interface{} `json:"audience"`
Notification interface{} `json:"notification,omitempty"`
Message interface{} `json:"message,omitempty"`
Options *Option `json:"options,omitempty"`
Callback *CallbackReq `json:"callback,omitempty"`
}
// NewPayload .
func NewPayload() *Payload {
return &Payload{
Options: &Option{},
}
}
// SetPlatform .
func (p *Payload) SetPlatform(plat *Platform) {
p.Platform = plat.OS
}
// SetAudience .
func (p *Payload) SetAudience(ad *Audience) {
p.Audience = ad.Object
}
// SetOptions .
func (p *Payload) SetOptions(o *Option) {
p.Options = o
}
// SetMessage .
func (p *Payload) SetMessage(m *Message) {
p.Message = m
}
// SetNotice .
func (p *Payload) SetNotice(notice *Notice) {
p.Notification = notice
}
// SetCallbackReq .
func (p *Payload) SetCallbackReq(cb *CallbackReq) {
p.Callback = cb
}
// ToBytes .
func (p *Payload) ToBytes() ([]byte, error) {
content, err := json.Marshal(p)
if err != nil {
return nil, err
}
return content, nil
}

View File

@@ -0,0 +1,34 @@
package jpush
const (
// PlatformIOS .
PlatformIOS = "ios"
// PlatformAndroid .
PlatformAndroid = "android"
// PlatformWinphone .
PlatformWinphone = "winphone"
// PlatformAll .
PlatformAll = "all"
)
// Platform .
type Platform struct {
OS interface{}
osArray []string
}
// NewPlatform .
func NewPlatform(os ...string) *Platform {
p := new(Platform)
for _, v := range os {
switch v {
case PlatformIOS, PlatformAndroid, PlatformWinphone:
p.osArray = append(p.osArray, v)
case PlatformAll:
p.OS = PlatformAll
return p
}
}
p.OS = p.osArray
return p
}

View File

@@ -0,0 +1 @@
package jpush

View File

@@ -0,0 +1 @@
package jpush

View File

@@ -0,0 +1,110 @@
// Code generated by $GOPATH/src/go-common/app/tool/cache/mc. DO NOT EDIT.
/*
Package dao is a generated mc cache package.
It is generated from:
type _mc interface {
//mc: -key=tokenKey -type=get
TokenCache(c context.Context, key string) (*model.Report, error)
//mc: -key=tokenKey -expire=d.mcReportExpire
AddTokenCache(c context.Context, key string, value *model.Report) error
//mc: -key=tokenKey -expire=d.mcReportExpire
AddTokensCache(c context.Context, values map[string]*model.Report) error
//mc: -key=tokenKey
DelTokenCache(c context.Context, key string) error
}
*/
package dao
import (
"context"
"fmt"
"go-common/app/service/main/push/model"
"go-common/library/cache/memcache"
"go-common/library/log"
"go-common/library/stat/prom"
)
var _ _mc
// TokenCache get data from mc
func (d *Dao) TokenCache(c context.Context, id string) (res *model.Report, err error) {
conn := d.mc.Get(c)
defer conn.Close()
key := tokenKey(id)
reply, err := conn.Get(key)
if err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
prom.BusinessErrCount.Incr("mc:TokenCache")
log.Errorv(c, log.KV("TokenCache", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
res = &model.Report{}
err = conn.Scan(reply, res)
if err != nil {
prom.BusinessErrCount.Incr("mc:TokenCache")
log.Errorv(c, log.KV("TokenCache", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}
// AddTokenCache Set data to mc
func (d *Dao) AddTokenCache(c context.Context, id string, val *model.Report) (err error) {
if val == nil {
return
}
conn := d.mc.Get(c)
defer conn.Close()
key := tokenKey(id)
item := &memcache.Item{Key: key, Object: val, Expiration: d.mcReportExpire, Flags: memcache.FlagJSON}
if err = conn.Set(item); err != nil {
prom.BusinessErrCount.Incr("mc:AddTokenCache")
log.Errorv(c, log.KV("AddTokenCache", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}
// AddTokensCache Set data to mc
func (d *Dao) AddTokensCache(c context.Context, values map[string]*model.Report) (err error) {
if len(values) == 0 {
return
}
conn := d.mc.Get(c)
defer conn.Close()
for id, val := range values {
key := tokenKey(id)
item := &memcache.Item{Key: key, Object: val, Expiration: d.mcReportExpire, Flags: memcache.FlagJSON}
if err = conn.Set(item); err != nil {
prom.BusinessErrCount.Incr("mc:AddTokensCache")
log.Errorv(c, log.KV("AddTokensCache", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
}
return
}
// DelTokenCache delete data from mc
func (d *Dao) DelTokenCache(c context.Context, id string) (err error) {
conn := d.mc.Get(c)
defer conn.Close()
key := tokenKey(id)
if err = conn.Delete(key); err != nil {
if err == memcache.ErrNotFound {
err = nil
return
}
prom.BusinessErrCount.Incr("mc:DelTokenCache")
log.Errorv(c, log.KV("DelTokenCache", fmt.Sprintf("%+v", err)), log.KV("key", key))
return
}
return
}

View File

@@ -0,0 +1,309 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"sync"
"go-common/app/service/main/push/model"
"go-common/library/cache/memcache"
"go-common/library/log"
"go-common/library/sync/errgroup"
)
const (
_prefixReport = "r_%d"
_prefixToken = "t_%s"
_bulkSize = 20
)
func reportKey(mid int64) string {
return fmt.Sprintf(_prefixReport, mid)
}
func tokenKey(token string) string {
return fmt.Sprintf(_prefixToken, token)
}
// 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.mcReportExpire}
err = conn.Set(&item)
return
}
// TokensCache get reports cache by tokens
func (d *Dao) TokensCache(ctx context.Context, tokens []string) (res map[string]*model.Report, missed []string, err error) {
res = make(map[string]*model.Report)
if len(tokens) == 0 {
return
}
var (
mutex sync.Mutex
allKeys []string
tokenMap = make(map[string]string, len(tokens))
)
for _, t := range tokens {
k := tokenKey(t)
allKeys = append(allKeys, k)
tokenMap[k] = t
}
keysLen := len(allKeys)
group, errCtx := errgroup.WithContext(ctx)
for i := 0; i < keysLen; i += _bulkSize {
var keys []string
if (i + _bulkSize) > keysLen {
keys = allKeys[i:]
} else {
keys = allKeys[i : i+_bulkSize]
}
group.Go(func() (err error) {
conn := d.mc.Get(errCtx)
defer conn.Close()
replys, err := conn.GetMulti(keys)
if err != nil {
PromError("mc:TokensCache GetMulti")
log.Error("conn.Gets(%v) error(%+v)", keys, err)
err = nil
return
}
for key, item := range replys {
r := &model.Report{}
if err = conn.Scan(item, &r); err != nil {
PromError("mc:TokensCache Scan")
log.Error("item.Scan(%s) error(%+v)", item.Value, err)
err = nil
continue
}
mutex.Lock()
res[tokenMap[key]] = r
delete(tokenMap, key)
mutex.Unlock()
}
return
})
}
group.Wait()
for _, t := range tokenMap {
missed = append(missed, t)
}
return
}
// ReportsCacheByMid gets reports cache by mid.
func (d *Dao) ReportsCacheByMid(c context.Context, mid int64) (res []*model.Report, err error) {
key := reportKey(mid)
conn := d.mc.Get(c)
defer conn.Close()
reply, err := conn.Get(key)
if err != nil {
if err == memcache.ErrNotFound {
res = nil
err = nil
missedCount.Add("report", 1)
return
}
PromError("mc:获取上报")
log.Error("conn.Get(%v) error(%v)", key, err)
return
}
rs := make(map[int64]map[string]*model.Report)
if err = conn.Scan(reply, &rs); err != nil {
PromError("mc:解析上报")
log.Error("item.Scan(%s) error(%v)", reply.Value, err)
return
}
for _, v := range rs {
for _, r := range v {
res = append(res, r)
}
}
cachedCount.Add("report", 1)
return
}
// ReportsCacheByMids get report cache by mids.
func (d *Dao) ReportsCacheByMids(c context.Context, mids []int64) (res map[int64][]*model.Report, missed []int64, err error) {
res = make(map[int64][]*model.Report, len(mids))
if len(mids) == 0 {
return
}
allKeys := make([]string, 0, len(mids))
midmap := make(map[string]int64, len(mids))
for _, mid := range mids {
k := reportKey(mid)
allKeys = append(allKeys, k)
midmap[k] = mid
}
group := errgroup.Group{}
mutex := sync.Mutex{}
keysLen := len(allKeys)
for i := 0; i < keysLen; i += _bulkSize {
var keys []string
if (i + _bulkSize) > keysLen {
keys = allKeys[i:]
} else {
keys = allKeys[i : i+_bulkSize]
}
group.Go(func() error {
conn := d.mc.Get(context.TODO())
defer conn.Close()
replys, err := conn.GetMulti(keys)
if err != nil {
PromError("mc:获取上报")
log.Error("conn.Gets(%v) error(%v)", keys, err)
return nil
}
for k, item := range replys {
rm := make(map[int64]map[string]*model.Report)
if err = conn.Scan(item, &rm); err != nil {
PromError("mc:解析上报")
log.Error("item.Scan(%s) error(%v)", item.Value, err)
continue
}
mutex.Lock()
mid := midmap[k]
for _, v := range rm {
for _, r := range v {
res[mid] = append(res[mid], r)
}
}
delete(midmap, k)
mutex.Unlock()
}
return nil
})
}
group.Wait()
missed = make([]int64, 0, len(midmap))
for _, mid := range midmap {
missed = append(missed, mid)
}
missedCount.Add("report", int64(len(missed)))
cachedCount.Add("report", int64(len(res)))
return
}
// AddReportsCacheByMids add report cache by mids.
func (d *Dao) AddReportsCacheByMids(c context.Context, mrs map[int64][]*model.Report) (err error) {
var (
bs []byte
urs map[int64]map[string]*model.Report
conn = d.mc.Get(c)
)
defer conn.Close()
for mid, rs := range mrs {
if urs, err = formatReport(rs); err != nil {
log.Error("d.AddReportsCacheByMids() formatReport() data(%v) error(%v)", rs, err)
continue
}
if bs, err = json.Marshal(urs); err != nil {
PromError("增加上报缓存json解析")
log.Error("json.Marshal(%v) error(%v)", mrs, err)
continue
}
k := reportKey(mid)
item := &memcache.Item{Key: k, Value: bs, Expiration: d.mcReportExpire}
if err = conn.Set(item); err != nil {
PromError("mc:批量添加上报")
log.Error("conn.Store(%s) error(%v)", k, err)
return
}
}
return
}
func formatReport(rs []*model.Report) (mrs map[int64]map[string]*model.Report, err error) {
mrs = make(map[int64]map[string]*model.Report)
for _, r := range rs {
if _, ok := mrs[r.APPID]; !ok {
mrs[r.APPID] = make(map[string]*model.Report)
}
mrs[r.APPID][r.DeviceToken] = r
}
return
}
// // AddReportCache add report cache.
// func (d *Dao) AddReportCache(c context.Context, r *model.Report) (err error) {
// conn := d.mc.Get(c)
// defer conn.Close()
// k := reportKey(r.Mid)
// reply, err := conn.Get(k)
// if err != nil {
// return
// }
// rs := make(map[int64]map[string]*model.Report)
// if err = conn.Scan(reply, &rs); err != nil {
// PromError("mc:解析上报")
// log.Error("reply.Scan(%s) error(%v)", reply.Value, err)
// return
// }
// if _, ok := rs[r.APPID]; !ok {
// rs[r.APPID] = make(map[string]*model.Report)
// }
// rs[r.APPID][r.DeviceToken] = r
// var bs []byte
// if bs, err = json.Marshal(rs); err != nil {
// PromError("增加上报缓存json解析")
// log.Error("json.Marshal(%v) error(%v)", rs, err)
// return
// }
// item := &memcache.Item{Key: k, Value: bs, Expiration: d.mcReportExpire}
// if err = conn.Set(item); err != nil {
// PromError("mc:添加上报")
// log.Error("conn.Store(%s) error(%v)", k, err)
// return
// }
// PromInfo("mc:新增上报缓存")
// return
// }
// DelReportCache delete report cache.
func (d *Dao) DelReportCache(c context.Context, mid int64, appID int64, deviceToken string) (err error) {
conn := d.mc.Get(c)
defer conn.Close()
k := reportKey(mid)
reply, err := conn.Get(k)
if err != nil {
if err == memcache.ErrNotFound {
missedCount.Incr("report")
err = nil
return
}
PromError("mc:删除上报")
log.Error("conn.Get(%v) error(%v)", k, err)
return
}
rs := make(map[int64]map[string]*model.Report)
if err = conn.Scan(reply, &rs); err != nil {
PromError("mc:解析上报")
log.Error("reply.Scan(%s) error(%v)", reply.Value, err)
return
}
if _, ok := rs[appID]; !ok {
return
}
if rs[appID][deviceToken] == nil {
return
}
delete(rs[appID], deviceToken)
var bs []byte
if bs, err = json.Marshal(rs); err != nil {
PromError("删除上报缓存 marshal")
log.Error("json.Marshal(%v) error(%v)", rs, err)
return
}
item := &memcache.Item{Key: k, Value: bs, Expiration: d.mcReportExpire}
if err = conn.Set(item); err != nil {
PromError("mc:删除上报缓存")
log.Error("conn.Store(%s) error(%v)", k, err)
return
}
PromInfo("mc:删除上报缓存")
return
}

View File

@@ -0,0 +1,151 @@
package dao
import (
"context"
"fmt"
"testing"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_PingMc(t *testing.T) {
Convey("ping mc", t, WithDao(func(d *Dao) {
err := d.pingMC(context.Background())
So(err, ShouldBeNil)
}))
}
func TestAddReportsCacheByMids(t *testing.T) {
Convey("add reports cache by mids", t, WithDao(func(d *Dao) {
var err error
mrs := map[int64][]*model.Report{
910819: {{
APPID: 1,
PlatformID: 1,
Mid: 910819,
DeviceToken: "dt1",
}, {
APPID: 2,
PlatformID: 2,
Mid: 910819,
DeviceToken: "dt2",
}},
123456: {{
APPID: 3,
PlatformID: 3,
Mid: 123456,
DeviceToken: "dt3",
}},
}
err = d.AddReportsCacheByMids(context.Background(), mrs)
So(err, ShouldBeNil)
}))
}
func Test_ReportCache(t *testing.T) {
Convey("reports cache", t, WithDao(func(d *Dao) {
var err error
mrs := map[int64][]*model.Report{
910819: {{
APPID: 1,
PlatformID: 1,
Mid: 910819,
DeviceToken: "dt1",
}, {
APPID: 2,
PlatformID: 2,
Mid: 910819,
DeviceToken: "dt2",
}},
123456: {{
APPID: 3,
PlatformID: 3,
Mid: 123456,
DeviceToken: "dt3",
}},
}
err = d.AddReportsCacheByMids(context.Background(), mrs)
So(err, ShouldBeNil)
// add report
// err = d.AddReportCache(context.Background(), &model.Report{APPID: 3, PlatformID: 3, Mid: 123456, DeviceToken: "dt4"})
// So(err, ShouldBeNil)
// err = d.AddReportCache(context.Background(), &model.Report{APPID: 4, PlatformID: 4, Mid: 123456, DeviceToken: "dt5"})
// So(err, ShouldBeNil)
// delete report
err = d.DelReportCache(context.Background(), 910819, 2, "dt2")
So(err, ShouldBeNil)
// get report
rs, missed, err := d.ReportsCacheByMids(context.Background(), []int64{910819, 123456})
_ = missed
So(len(rs), ShouldEqual, 2)
So(err, ShouldBeNil)
for mid, v := range rs {
for _, vv := range v {
fmt.Printf("mid(%d) %+v \n", mid, vv)
}
}
// report miss
rs, misses, err := d.ReportsCacheByMids(context.Background(), []int64{1000000, 2000000})
So(len(rs), ShouldEqual, 0)
So(len(misses), ShouldEqual, 2)
So(err, ShouldBeNil)
}))
}
func Test_TokenCache(t *testing.T) {
Convey("add token cache", t, WithDao(func(d *Dao) {
token := "testtoken"
r := &model.Report{
APPID: 1,
DeviceToken: token,
}
err := d.AddTokenCache(context.Background(), r.DeviceToken, r)
So(err, ShouldBeNil)
m := make(map[string]*model.Report, 0)
m[r.DeviceToken] = r
d.AddTokensCache(context.Background(), m)
So(err, ShouldBeNil)
Convey("token cache", func() {
r, err := d.TokenCache(context.Background(), token)
So(err, ShouldBeNil)
t.Logf("report(%+v)", r)
Convey("delete token cache", func() {
err = d.DelTokenCache(context.Background(), token)
So(err, ShouldBeNil)
})
})
}))
}
func Test_TokensCache(t *testing.T) {
Convey("tokens cache", t, WithDao(func(d *Dao) {
r := &model.Report{APPID: 1, DeviceToken: "testtoken1"}
err := d.AddTokenCache(context.Background(), r.DeviceToken, r)
So(err, ShouldBeNil)
r = &model.Report{APPID: 1, DeviceToken: "testtoken2"}
err = d.AddTokenCache(context.Background(), r.DeviceToken, r)
So(err, ShouldBeNil)
res, missed, err := d.TokensCache(context.Background(), []string{"testtoken1", "testtoken2", "testtoken3"})
So(err, ShouldBeNil)
t.Logf("tokens cache missed(%v)", missed)
for token, val := range res {
t.Logf("token(%s) value(%+v)", token, val)
}
}))
}
func Test_ReportsCacheByMid(t *testing.T) {
Convey("Test_ReportsCacheByMid", t, WithDao(func(d *Dao) {
_, err := d.ReportsCacheByMid(context.Background(), 123)
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"constant.go",
"notification.go",
],
importpath = "go-common/app/service/main/push/dao/mi",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/stat: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,225 @@
package mi
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"go-common/library/log"
"go-common/library/stat"
"go-common/library/stat/prom"
)
// Client Xiaomi http client.
type Client struct {
Header http.Header
HTTPClient *http.Client
Package string
URL string
Stats stat.Stat
}
// NewClient returns a Client with request header and auth.
func NewClient(pkg, auth string, timeout time.Duration) *Client {
header := http.Header{}
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Authorization", AuthPrefix+auth)
// transport := &http.Transport{
// Proxy: func(_ *http.Request) (*url.URL, error) {
// return url.Parse("http://10.28.10.11:80")
// },
// DialContext: (&net.Dialer{
// Timeout: 30 * time.Second,
// KeepAlive: 30 * time.Second,
// DualStack: true,
// }).DialContext,
// MaxIdleConns: 100,
// IdleConnTimeout: 90 * time.Second,
// ExpectContinueTimeout: 1 * time.Second,
// }
return &Client{
Header: header,
HTTPClient: &http.Client{Timeout: timeout},
// HTTPClient: &http.Client{Timeout: timeout, Transport: transport},
Package: pkg,
Stats: prom.HTTPClient,
}
}
// SetProductionURL sets Production URL.
func (c *Client) SetProductionURL(url string) {
c.URL = ProductionHost + url
}
// SetDevelopmentURL sets Production URL.
func (c *Client) SetDevelopmentURL(url string) {
c.URL = DevHost + url
}
// SetVipURL sets VIP URL.
func (c *Client) SetVipURL(url string) {
c.URL = VipHost + url
}
// SetStatusURL sets feedback URL.
func (c *Client) SetStatusURL() {
c.URL = ProductionHost + StatusURL
}
// Push sends a Notification to Xiaomi push service.
func (c *Client) Push(xm *XMMessage) (response *Response, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.URL, int64(time.Since(now)/time.Millisecond))
log.Info("mi stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.URL, "failed")
}
}()
}
var req *http.Request
if req, err = http.NewRequest(http.MethodPost, c.URL, bytes.NewBuffer([]byte(xm.xmuv.Encode()))); err != nil {
log.Error("http.NewRequest() error(%v)", err)
return
}
req.Header = c.Header
var res *http.Response
if res, err = c.HTTPClient.Do(req); err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &Response{}
var bs []byte
bs, err = ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}
// MockPush mock push.
func (c *Client) MockPush(xm *XMMessage) (response *Response, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.URL, int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.URL, "mi push mock")
}
}()
}
time.Sleep(200 * time.Millisecond)
response = &Response{Code: ResultCodeOk, Result: ResultOk}
return
}
// InvalidTokens get invalid tokens.
func (c *Client) InvalidTokens() (response *Response, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.URL, int64(time.Since(now)/time.Millisecond))
log.Info("mi invalidTokens timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.URL, "failed")
}
}()
}
req, err := http.NewRequest(http.MethodGet, feedbackHost+feedbackURI, nil)
if err != nil {
log.Error("http.NewRequest(%s) error(%v)", c.URL, err)
return
}
req.Header = c.Header
c.HTTPClient.Timeout = time.Minute
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &Response{}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}
// UninstalledTokens get uninstalled tokens.
func (c *Client) UninstalledTokens() (response *UninstalledResponse, err error) {
if c.Stats != nil {
now := time.Now()
defer func() {
c.Stats.Timing(c.URL, int64(time.Since(now)/time.Millisecond))
log.Info("mi UninstalledTokens timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(c.URL, "mi uninstalled tokens")
}
}()
}
req, err := http.NewRequest(http.MethodGet, emqHost+uninstalledURI+"?package_name="+c.Package, nil)
if err != nil {
log.Error("http.NewRequest(%s) error(%v)", c.URL, err)
return
}
req.Header = c.Header
c.HTTPClient.Timeout = time.Minute
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &UninstalledResponse{}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
return
}
for _, s := range response.Result {
s = strings.Replace(s, `\`, "", -1)
s = strings.TrimPrefix(s, `"`)
s = strings.TrimSuffix(s, `"`)
ud := UninstalledData{}
if e := json.Unmarshal([]byte(s), &ud); e != nil {
log.Error("json unmarshal(%s) error(%v)", s, e)
continue
}
if ud.Token == "" {
continue
}
response.Data = append(response.Data, ud.Token)
}
return
}

View File

@@ -0,0 +1,75 @@
package mi
import (
"fmt"
"strconv"
"strings"
"testing"
"time"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Push(t *testing.T) {
Convey("push mi", t, func() {
xmm := &XMMessage{
Payload: "bili:///?type=bililive&roomid=33886",
RestrictedPackageName: "tv.danmaku.bili",
PassThrough: 0, // 0 表示通知栏消息1 表示透传消息
Title: model.DefaultMessageTitle,
Description: "直播推荐",
NotifyType: NotifyTypeDefaultAll,
TaskID: "vdsfdfs", // 每次不能相同,相同的只会推一次
}
// 设置是否被覆盖,不同的数字,可显示多行
xmm.SetNotifyID(xmm.TaskID)
xmm.SetCallbackParam("1")
xmm.SetRegID("device token")
// xmm.SetRegID("qlRyXrBPQ8ZkTg3x46hvTz3g8Oe/Fyz93XnE5U2NxRk=")
// xmm.SetUserAccount("15678567,25668444")
client := NewClient("tv.danmaku.bili", "QlcVxtNh6j7BXBPXjcbGoQ==", time.Hour)
// client.SetProductionURL(AccountURL)
client.SetVipURL(RegURL)
resp, err := client.Push(xmm)
So(err, ShouldBeNil)
So(resp.Code, ShouldEqual, ResultCodeNoValidTargets)
if resp.Result == ResultOk {
tt := strings.Split(resp.Info, " ")
if len(tt) == 6 {
m, _ := strconv.Atoi(tt[4])
fmt.Println(m + 1)
}
}
t.Logf("push xiaomi res(%+v)", resp)
// success: &{Result:ok Reason: Code:0 Data:{ID:scm01b20510561935064bK List:[]} Description:成功 Info:Received push messages for 1 REGID}
// failed: &{Result:error Reason:No valid targets! Code:20301 Data:{ID: List:[]} Description:发送消息失败 Info:}
})
}
// 需要测的时候再打开因为失效token获取完了就没了
// func Test_InvalidTokens(t *testing.T) {
// client := NewClient("tv.danmaku.bili", "QlcVxtNh6j7BXBPXjcbGoQ==", time.Hour)
// client.SetFeedbackURL()
// resp, err := client.InvalidTokens()
// if err != nil {
// t.Log(err)
// t.FailNow()
// }
// t.Log(resp)
// }
// 需要测的时候再打开因为卸载token获取完了就没了
// func Test_UninstalledTokens(t *testing.T) {
// client := NewClient("tv.danmaku.bili", "QlcVxtNh6j7BXBPXjcbGoQ==", time.Hour)
// resp, err := client.UninstalledTokens()
// if err != nil {
// t.Log(err)
// t.FailNow()
// }
// t.Log(resp)
// }

View File

@@ -0,0 +1,94 @@
package mi
// Xiaomi push service document: https://dev.mi.com/doc/cat=35/index.html
const (
// VipHost VIP host.
VipHost = "https://vip.api.xmpush.xiaomi.com"
// DevHost dev host.
DevHost = "https://sandbox.xmpush.xiaomi.com"
// ProductionHost production host.
ProductionHost = "https://api.xmpush.xiaomi.com"
// feedbackHost host to get invalid token.
feedbackHost = "https://feedback.xmpush.xiaomi.com"
// emqHost message queue
emqHost = "https://emq.xmpush.xiaomi.com"
// AuthPrefix auth prefix.
AuthPrefix = "key="
// ResultOk result status.
ResultOk = "ok" // "ok" means success, "error" means failed.
// ResultError result status.
ResultError = "error"
// ResultCodeOk result status code.
ResultCodeOk = 0
// ResultCodeNoValidTargets no valid token.
ResultCodeNoValidTargets = 20301
// ResultCodeNoMsgInEmq no message in emq.
ResultCodeNoMsgInEmq = 80002
// RegURL 向某个regid或一组regid列表推送某条消息
RegURL = "/v3/message/regid"
// AccountURL 根据account发送消息到指定account上
AccountURL = "/v2/message/user_account"
// MultiRegIDURL 针对不同的regid推送不同的消息
MultiRegIDURL = "/v2/multi_messages/regids"
// MultiAliasURL 针对不同的aliases推送不同的消息
MultiAliasURL = "/v2/multi_messages/aliases"
// MultiUserAccountURL 针对不同的accounts推送不同的消息
MultiUserAccountURL = "/v2/multi_messages/user_accounts"
// AliasURL 根据alias发送消息到指定设备上
AliasURL = "/v3/message/alias"
// MultiPackageNameMultiTopicURL 根据topic发送消息到指定一组设备上
MultiPackageNameMultiTopicURL = "/v3/message/multi_topic"
// MultiTopicURL 根据topic发送消息到指定一组设备上
MultiTopicURL = "/v2/message/topic"
// MultiPackageNameAllURL 向所有设备推送某条消息
MultiPackageNameAllURL = "/v3/message/all"
// AllURL 向所有设备推送某条消息
AllURL = "/v2/message/all"
// TopicURL 向多个topic广播消息
TopicURL = "/v3/message/multi_topic"
// ScheduleJobExistURL 检测定时消息的任务是否存在
ScheduleJobExistURL = "/v2/schedule_job/exist"
// ScheduleJobDeleteURL 删除指定的定时消息
ScheduleJobDeleteURL = "/v2/schedule_job/delete"
// ScheduleJobDeleteByJobKeyURL 删除指定的定时消息
ScheduleJobDeleteByJobKeyURL = "/v3/schedule_job/delete"
// feedbackURI 获取无效token列表
feedbackURI = "/v1/feedback/fetch_invalid_regids"
// uninstalledURI 获取卸载token列表
uninstalledURI = "/app/uninstall/regid"
// StatusURL 追踪消息
StatusURL = "/v1/trace/message/status"
// NotifyTypeDefaultAll 包括下面三种(notify type 可以是以下几种的OR组合)
NotifyTypeDefaultAll = -1
// NotifyTypeDefaultNone 声音、振动、led灯全关
NotifyTypeDefaultNone = 0
// NotifyTypeDefaultSound 使用默认提示音提示
NotifyTypeDefaultSound = 1
// NotifyTypeDefaultVibration 使用默认震动提示
NotifyTypeDefaultVibration = 2
// NotifyTypeDefaultLight 使用默认led灯光提示
NotifyTypeDefaultLight = 4
// NotPassThrough 显示通知
NotPassThrough = 0
// PassThrough 静默推送
PassThrough = 1
// CallbackURL 客户端收到后回调
CallbackURL = "https://api.bilibili.com/x/push/callback/xiaomi"
// CallbackBarStatusEnable .
CallbackBarStatusEnable = 1
// CallbackBarStatusDisable .
CallbackBarStatusDisable = 2
// CallbackBarStatusUnknown .
CallbackBarStatusUnknown = 3
// CallbackBarStatusEnableStr .
CallbackBarStatusEnableStr = "Enable"
// CallbackBarStatusDisableStr .
CallbackBarStatusDisableStr = "Disable"
// CallbackBarStatusUnknownStr .
CallbackBarStatusUnknownStr = "Unknown"
)

View File

@@ -0,0 +1,166 @@
package mi
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"time"
)
// XMMessage define reference struct http://dev.xiaomi.com/doc/?p=533
type XMMessage struct {
Payload string // 消息的内容。
RestrictedPackageName string // App的包名。备注V2版本支持一个包名V3版本支持多包名中间用逗号分割
PassThrough int // pass_through的值可以为 0 表示通知栏消息1 表示透传消息
NotifyType int // 通知方式
Title string // 通知栏展示的通知的标题。
Description string // 通知栏展示的通知的描述。
TaskID string // 上报数据使用
xmuv url.Values // 含有本条消息所有属性的数组
}
func (xm *XMMessage) buildXMPostParam() {
xmuv := url.Values{}
xmuv.Set("payload", xm.Payload)
xmuv.Set("restricted_package_name", xm.RestrictedPackageName)
xmuv.Set("pass_through", strconv.Itoa(xm.PassThrough))
xmuv.Set("title", xm.Title)
xmuv.Set("description", xm.Description)
xmuv.Set("notify_type", strconv.Itoa(xm.NotifyType))
xmuv.Set("extra.task_id", xm.TaskID)
xmuv.Set("extra.jobkey", xm.TaskID)
xmuv.Set("extra.callback", CallbackURL)
xmuv.Set("extra.callback.type", "1") // 第三方所需要的回执类型。1:送达回执,2:点击回执,3:送达和点击回执,默认值为3。
xm.xmuv = xmuv
}
// SetNotifyID 可选项
// 默认情况下通知栏只显示一条推送消息。如果通知栏要显示多条推送消息需要针对不同的消息设置不同的notify_id相同notify_id的通知栏消息会覆盖之前的
// notify_id 0-4 同一个notifyId在通知栏只会保留一条
func (xm *XMMessage) SetNotifyID(notifyID string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("notify_id", notifyID)
}
// SetNotifyType sound / vibration / led light
func (xm *XMMessage) SetNotifyType(typ int) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("notify_type", strconv.Itoa(typ))
}
// SetTimeToLive 可选项
// 如果用户离线设置消息在服务器保存的时间单位ms。服务器默认最长保留两周。
// time_to_live 可选项当用户离线是消息保留时间默认两周单位ms
func (xm *XMMessage) SetTimeToLive(expire int64) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
timeToLive := (expire - time.Now().Unix()) * 1000
xm.xmuv.Set("time_to_live", fmt.Sprintf("%d", timeToLive))
}
// SetTimeToSend 可选项
// 定时发送消息。用自1970年1月1日以来00:00:00.0 UTC时间表示以毫秒为单位的时间。注仅支持七天内的定时消息。
func (xm *XMMessage) SetTimeToSend(timeToSend int64) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("time_to_send", fmt.Sprintf("%d", timeToSend))
}
// SetUserAccount 根据user_account发送消息给设置了该user_account的所有设备。可以提供多个user_accountuser_account之间用“,”分割。参数仅适用于“/message/user_account”HTTP API。
func (xm *XMMessage) SetUserAccount(UserAccount string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("user_account", UserAccount)
}
// SetUserAccounts 针对不同的userAccount推送不同的消息
// 根据user_accounts发送消息给设置了该user_account的所有设备。可以提供多个user_accountuser_account之间用“,”分割。
func (xm *XMMessage) SetUserAccounts(UserAccount string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("user_accounts", UserAccount)
}
// SetRegID 根据registration_id发送消息到指定设备上。可以提供多个registration_id发送给一组设备不同的registration_id之间用“,”分割。
func (xm *XMMessage) SetRegID(deviceToken string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("registration_id", deviceToken)
}
// SetTopic 根据topic发送消息给订阅了该topic的所有设备。参数仅适用于“/message/topic”HTTP API。
func (xm *XMMessage) SetTopic(UserAccount string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("topic", UserAccount)
}
// SetCallbackParam 把应用标识传过去,这样方便区分应用
func (xm *XMMessage) SetCallbackParam(p string) {
if xm.xmuv == nil {
xm.buildXMPostParam()
}
xm.xmuv.Set("extra.callback.param", p) // 可选字段。第三方自定义回执参数最大长度64个字节这里用来存应用ID
}
// Response push result.
type Response struct {
Result string `json:"result,omitempty"` //“result”: string”ok” 表示成功, “error” 表示失败。
Reason string `json:"reason,omitempty"` //reason: string如果失败reason失败原因详情。
Code int `json:"code,omitempty"` //“code”: integer0表示成功非0表示失败。
Data Data `json:"data,omitempty"` //“data”: string本身就是一个json字符串其中id字段的值就是消息的Id
Description string `json:"description,omitempty"` //“description”: string 对发送消息失败原因的解释。
Info string `json:"info,omitempty"` //“info”: string详细信息。
TraceID string `json:"trace_id,omitempty"` // trace id for xiaomi
}
// Data response data.
type Data struct {
ID string `json:"id,omitempty"`
List []string `json:"list,omitempty"` // for feedback
Data json.RawMessage `json:"data,omitempty"` // for status
}
// UninstalledResponse .
type UninstalledResponse struct {
Code int `json:"errorCode,omitempty"`
Reason string `json:"reason,omitempty"`
Result []string `json:"result,omitempty"`
Data []string
}
// UninstalledData .
type UninstalledData struct {
Token string `json:"regId"`
Ts int64 `json:"ts"`
// Alias []string `json:"alias"` // 用不上
}
// RegidCallback regid callback
type RegidCallback struct {
AppID string `json:"app_id"`
AppVer string `json:"app_version"`
AppPkg string `json:"app_pkg"`
AppSecret string `json:"app_secret"`
Regid string `json:"regid"`
}
// Callback 推送回执(回调)
type Callback struct {
Param string `json:"param"` // 开发者上传的自定义参数值。
BarStatus string `json:"barStatus"` // 消息送达时通知栏的状态。Enable:为用户允许此app展示通知栏消息, Disable:为通知栏消息已关闭, Unknown:通知栏状态未知。
Type int `json:"type"` // callback类型
Targets string `json:"targets"` // 一批alias或者regId列表之间是用逗号分割
Jobkey string `json:"jobkey"`
}

View File

@@ -0,0 +1,215 @@
package dao
import (
"context"
"database/sql"
"encoding/json"
"strconv"
"time"
"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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
_upadteTaskStatusSQL = "UPDATE push_tasks SET status=? WHERE id=?"
_upadteTaskProgressSQL = "UPDATE push_tasks SET progress=? WHERE id=?"
_taskByIDSQL = "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 FROM push_tasks WHERE id=?"
_taskByPlatformSQL = "SELECT id,job,type,app_id,business_id,platform_id,title,summary,link_type,link_value,build,sound,vibration,pass_through,mid_file,progress,push_time,expire_time,status,`group`,image_url FROM push_tasks WHERE status=? AND push_time<=? AND dtime=0 and platform_id=? and mtime>? LIMIT 1 FOR UPDATE"
// business
_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"
// auth
_authsSQL = "SELECT app_id,platform_id,name,`key`,value,bundle_id FROM push_auths WHERE dtime=0"
// callback
_addCallbackSQL = `INSERT INTO push_callbacks (task,app,platform,mid,buvid,token,pid,click,brand,extra) VALUES(?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE app=?,platform=?,mid=?,buvid=?,pid=?,click=?`
)
// AddTask adds task.
func (d *Dao) AddTask(c context.Context, t *model.Task) (id int64, err error) {
var (
res sql.Result
platform = model.JoinInts(t.Platform)
build, _ = json.Marshal(t.Build)
progress, _ = json.Marshal(t.Progress)
extra, _ = json.Marshal(t.Extra)
)
if res, err = d.addTaskStmt.Exec(c, 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
}
// TxTaskByPlatform gets prepared task by platform.
func (d *Dao) TxTaskByPlatform(tx *xsql.Tx, platform int) (t *model.Task, err error) {
var (
id int64
build string
progress string
now = time.Now()
since = now.Add(-7 * 24 * time.Hour)
)
t = &model.Task{Progress: &model.Progress{}}
if err = tx.QueryRow(_taskByPlatformSQL, model.TaskStatusPrepared, now, platform, since).Scan(&id, &t.Job, &t.Type, &t.APPID, &t.BusinessID, &t.PlatformID, &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); err != nil {
t = nil
if err == sql.ErrNoRows {
err = nil
return
}
log.Error("d.TxPreparedTask() QueryRow(%d,%v) error(%v)", platform, now, err)
PromError("mysql:按状态查询任务")
return
}
t.ID = strconv.FormatInt(id, 10)
t.Build = model.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进度")
}
}
return
}
// Task loads task by task id.
func (d *Dao) Task(c context.Context, taskID string) (t *model.Task, err error) {
var (
platform string
build string
progress string
id, _ = strconv.ParseInt(taskID, 10, 64)
)
t = &model.Task{Progress: &model.Progress{}}
if err = d.taskStmt.QueryRow(c, 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); err != nil {
if err == sql.ErrNoRows {
t = nil
err = nil
return
}
log.Error("d.taskStmt.QueryRow(%s) error(%v)", taskID, err)
PromError("mysql:按ID查询任务")
return
}
t.ID = strconv.FormatInt(id, 10)
t.Platform = model.SplitInts(platform)
t.Build = model.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进度")
}
}
if t.Progress == nil {
t.Progress = &model.Progress{}
}
return
}
// UpdateTaskStatus update task status.
func (d *Dao) UpdateTaskStatus(c context.Context, taskID string, status int8) (err error) {
id, _ := strconv.ParseInt(taskID, 10, 64)
if _, err = d.updateTaskStatusStmt.Exec(c, status, id); err != nil {
log.Error("d.updateTaskStatusStmt.Exec(%s,%d) error(%v)", taskID, status, err)
PromError("mysql:更新推送任务状态")
}
return
}
// TxUpdateTaskStatus updates task status by tx.
func (d *Dao) TxUpdateTaskStatus(tx *xsql.Tx, taskID string, status int8) (err error) {
id, _ := strconv.ParseInt(taskID, 10, 64)
if _, err = tx.Exec(_upadteTaskStatusSQL, status, id); err != nil {
log.Error("d.TxUpdateTaskStatus() Exec(%s,%d) error(%v)", taskID, status, err)
PromError("mysql:更新推送任务状态")
}
return
}
// UpdateTaskProgress updates task's progress.
func (d *Dao) UpdateTaskProgress(c context.Context, taskID string, p *model.Progress) (err error) {
var b []byte
if b, err = json.Marshal(p); err != nil {
log.Error("json.Marshal(%+v) error(%v)", p, err)
return
}
id, _ := strconv.ParseInt(taskID, 10, 64)
if _, err = d.updateTaskProgressStmt.Exec(c, string(b), id); err != nil {
log.Error("d.updateTaskProgress.Exec(%s,%+v) error(%v)", taskID, p, err)
PromError("mysql:更新推送任务进度")
}
return
}
// Businesses gets all business info.
func (d *Dao) Businesses(c context.Context) (res map[int64]*model.Business, err error) {
rows, err := d.businessesStmt.Query(c)
if err != nil {
log.Error("d.businessesStmt.Query() error(%v)", err)
PromError("mysql:查询业务方")
return
}
defer rows.Close()
res = make(map[int64]*model.Business)
for rows.Next() {
var (
silentTime string
b = &model.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 = model.ParseSilentTime(silentTime)
res[b.ID] = b
}
return
}
func (d *Dao) auths(c context.Context) (res []*model.Auth, err error) {
var rows *xsql.Rows
if rows, err = d.authsStmt.Query(c); err != nil {
log.Error("d.authsStmt.Query() error(%v)", err)
PromError("mysql:获取 auths")
return
}
defer rows.Close()
for rows.Next() {
a := &model.Auth{}
if err = rows.Scan(&a.APPID, &a.PlatformID, &a.Name, &a.Key, &a.Value, &a.BundleID); err != nil {
PromError("mysql:获取 auths Scan")
log.Error("d.auths() Scan() error(%v)", err)
return
}
res = append(res, a)
}
return
}
// AddCallback adds callback.
func (d *Dao) AddCallback(c context.Context, cb *model.Callback) (err error) {
var extra []byte
if cb.Extra != nil {
extra, _ = json.Marshal(cb.Extra)
}
if _, err = d.addCallbackStmt.Exec(c, cb.Task, cb.APP, cb.Platform, cb.Mid, cb.Buvid, cb.Token, cb.Pid, cb.Click, cb.Brand, string(extra),
cb.APP, cb.Platform, cb.Mid, cb.Buvid, cb.Pid, cb.Click); err != nil {
log.Error("d.AddCallback(%+v) error(%v)", cb, err)
PromError("mysql:新增callback")
return
}
PromInfo("mysql:新增callback")
return
}

View File

@@ -0,0 +1,209 @@
package dao
import (
"context"
"database/sql"
"fmt"
"time"
"go-common/app/service/main/push/model"
xsql "go-common/library/database/sql"
"go-common/library/log"
"go-common/library/xstr"
)
const (
_batch = 1000
_reportSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra,dtime FROM push_reports WHERE token_hash=?`
_reportByIDSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra FROM push_reports WHERE id=?`
_reportsSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra FROM push_reports WHERE token_hash IN(%s) AND dtime=0`
_lastReportIDSQL = `SELECT id FROM push_reports ORDER BY id DESC LIMIT 1`
_addReportSQL = `INSERT INTO push_reports (app_id,platform_id,mid,buvid,device_token,token_hash,build,time_zone,notify_switch,device_brand,device_model,os_version,extra) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`
_updateReportSQL = `UPDATE push_reports SET app_id=?,platform_id=?,mid=?,buvid=?,build=?,time_zone=?,notify_switch=?,device_brand=?,device_model=?,os_version=?,extra=?,mtime=?,dtime=0 WHERE id=?`
_delReportSQL = `UPDATE push_reports SET dtime=? WHERE token_hash=?`
_reportsByMidSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra FROM push_reports WHERE mid=? AND dtime=0`
_reportsByMidsSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra FROM push_reports WHERE mid IN (%s) AND dtime=0`
_reportsByIDSQL = `SELECT id,app_id,platform_id,mid,buvid,device_token,build,time_zone,notify_switch,device_brand,device_model,os_version,extra FROM push_reports WHERE id IN (%s) AND dtime=0`
)
// Report gets report by device_token.
func (d *Dao) Report(c context.Context, dt string) (r *model.Report, err error) {
r = &model.Report{}
th := model.HashToken(dt)
if err = d.reportStmt.QueryRow(c, th).Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid,
&r.DeviceToken, &r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra, &r.Dtime); err != nil {
if err == sql.ErrNoRows {
err = nil
r = nil
return
}
log.Error("d.reportStmt.QueryRow(%s) error(%v)", dt, err)
PromError("mysql:获取上报")
}
return
}
// ReportByID gets report by id.
func (d *Dao) ReportByID(c context.Context, id int64) (r *model.Report, err error) {
r = &model.Report{}
if err = d.reportByIDStmt.QueryRow(c, id).Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid,
&r.DeviceToken, &r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra); err != nil {
if err == sql.ErrNoRows {
err = nil
r = nil
return
}
log.Error("d.reportByIDStmt.QueryRow(%d) error(%v)", id, err)
PromError("mysql:获取上报")
}
return
}
// Reports gets reports by device_token.
func (d *Dao) Reports(c context.Context, dts []string) (rs []*model.Report, err error) {
var ths []int64
for _, dt := range dts {
ths = append(ths, model.HashToken(dt))
}
s := fmt.Sprintf(_reportsSQL, xstr.JoinInts(ths))
var rows *xsql.Rows
if rows, err = d.db.Query(c, s); err != nil {
log.Error("d.reports Query() error(%v)", err)
PromError("mysql:通过tokens获取上报Query")
return
}
defer rows.Close()
for rows.Next() {
r := &model.Report{}
if err = rows.Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid, &r.DeviceToken,
&r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra); err != nil {
log.Error("d.Reports Scan() error(%v)", err)
PromError("mysql:通过tokens获取上报Scan")
return
}
rs = append(rs, r)
}
return
}
// AddReport adds report.
func (d *Dao) AddReport(ctx context.Context, r *model.Report) (id int64, err error) {
th := model.HashToken(r.DeviceToken)
res, err := d.addReportStmt.Exec(ctx, r.APPID, r.PlatformID, r.Mid, r.Buvid, r.DeviceToken, th, r.Build, r.TimeZone, r.NotifySwitch, r.DeviceBrand, r.DeviceModel, r.OSVersion, r.Extra)
if err != nil {
log.Error("d.AddReport(%+v) error(%v)", r, err)
PromError("mysql:AddReport")
return
}
PromInfo("mysql:AddReport")
return res.LastInsertId()
}
// UpdateReport update report.
func (d *Dao) UpdateReport(ctx context.Context, r *model.Report) (err error) {
if _, err = d.updateReportStmt.Exec(ctx, r.APPID, r.PlatformID, r.Mid, r.Buvid, r.Build, r.TimeZone, r.NotifySwitch, r.DeviceBrand, r.DeviceModel, r.OSVersion, r.Extra, time.Now(), r.ID); err != nil {
log.Error("UpdateReport(%+v) error(%v)", r, err)
PromError("mysql:UpdateReport")
return
}
PromInfo("mysql:UpdateReport")
return
}
// DelReport delete report.
func (d *Dao) DelReport(c context.Context, dt string) (rows int64, err error) {
th := model.HashToken(dt)
now := time.Now().Unix()
var res sql.Result
if res, err = d.delReportStmt.Exec(c, now, th); err != nil {
log.Error("d.delReportStmt.Exec(%s,%d) error(%v)", dt, th, err)
PromError("mysql:删除上报")
return
}
return res.RowsAffected()
}
// ReportsByMid gets reports by mid.
func (d *Dao) ReportsByMid(c context.Context, mid int64) (res []*model.Report, err error) {
var rows *xsql.Rows
if rows, err = d.reportsByMidStmt.Query(c, mid); err != nil {
log.Error("d.reportsByMid Query() error(%v)", err)
PromError("mysql:通过mid获取上报Query")
return
}
defer rows.Close()
for rows.Next() {
r := &model.Report{}
if err = rows.Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid, &r.DeviceToken,
&r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra); err != nil {
log.Error("d.ReportsByMid Scan() error(%v)", err)
PromError("mysql:通过mid获取上报Scan")
return
}
res = append(res, r)
}
PromInfo("mysql:通过mid获取上报")
return
}
// ReportsByMids gets reports by mids.
func (d *Dao) ReportsByMids(c context.Context, mids []int64) (reports map[int64][]*model.Report, err error) {
reports = make(map[int64][]*model.Report)
for {
midsLen := len(mids)
if midsLen == 0 {
return
}
var part []int64
if midsLen >= _batch {
part = mids[:_batch]
} else {
part = mids[:]
}
mids = mids[len(part):]
s := fmt.Sprintf(_reportsByMidsSQL, xstr.JoinInts(part))
var rows *xsql.Rows
if rows, err = d.db.Query(c, s); err != nil {
log.Error("d.reportsByMids Query() error(%v)", err)
PromError("mysql:通过mids获取上报Query")
return
}
defer rows.Close()
for rows.Next() {
r := &model.Report{}
if err = rows.Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid, &r.DeviceToken,
&r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra); err != nil {
log.Error("d.ReportsByMids Scan() error(%v)", err)
PromError("mysql:通过mids获取上报Scan")
return
}
reports[r.Mid] = append(reports[r.Mid], r)
}
PromInfo("mysql:通过mids获取上报")
}
}
// ReportsByID gets reports by mids.
func (d *Dao) ReportsByID(c context.Context, ids []int64) (reports []*model.Report, err error) {
s := fmt.Sprintf(_reportsByIDSQL, xstr.JoinInts(ids))
var rows *xsql.Rows
if rows, err = d.db.Query(c, s); err != nil {
log.Error("d.reportsByID Query() error(%v)", err)
PromError("mysql:通过id获取上报Query")
return
}
defer rows.Close()
for rows.Next() {
r := &model.Report{}
if err = rows.Scan(&r.ID, &r.APPID, &r.PlatformID, &r.Mid, &r.Buvid, &r.DeviceToken,
&r.Build, &r.TimeZone, &r.NotifySwitch, &r.DeviceBrand, &r.DeviceModel, &r.OSVersion, &r.Extra); err != nil {
log.Error("d.ReportsByID Scan() error(%v)", err)
PromError("mysql:通过id获取上报Scan")
return
}
reports = append(reports, r)
}
PromInfo("mysql:通过id获取上报")
return
}

View File

@@ -0,0 +1,50 @@
package dao
import (
"context"
"database/sql"
"encoding/json"
"go-common/library/log"
)
const (
_settingSQL = "SELECT value FROM push_user_settings WHERE mid=? AND dtime=0"
_setSettingSQL = "INSERT INTO push_user_settings (mid,value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=?"
)
// SetSetting saves user notify settings.
func (d *Dao) SetSetting(c context.Context, mid int64, st map[int]int) (err error) {
bs, err := json.Marshal(st)
if err != nil {
log.Error("SetSetting(%d) json.Marshal(%v) error(%v)", mid, st, err)
return
}
if _, err = d.setSettingStmt.Exec(c, mid, string(bs), string(bs)); err != nil {
PromError("mysql:保存用户通知开关")
log.Error("d.SetSetting(%d,%s) error(%v)", mid, bs, err)
}
return
}
// Setting gets user push setting.
func (d *Dao) Setting(c context.Context, mid int64) (st map[int]int, err error) {
var v string
if err = d.settingStmt.QueryRow(c, mid).Scan(&v); err != nil {
if err == sql.ErrNoRows {
st = nil
err = nil
return
}
log.Error("d.settingStmt.Query() error(%v)", err)
PromError("mysql:获取用户通知开关配置")
return
}
if v == "" {
return
}
if err = json.Unmarshal([]byte(v), &st); err != nil {
log.Error("d.Setting(%d) json.Unmarshal(%s) error(%v)", mid, v, err)
}
return
}

View File

@@ -0,0 +1,224 @@
package dao
import (
"context"
"fmt"
"strconv"
"testing"
"time"
"go-common/app/service/main/push/model"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Task(t *testing.T) {
Convey("add task", t, WithDao(func(d *Dao) {
t := &model.Task{
Job: 1378138219873,
Type: model.TaskTypeBusiness,
APPID: 1,
BusinessID: 1,
Platform: []int{1, 2, 3},
Title: "test_tile",
Summary: "test_summary",
LinkType: model.LinkTypeBrowser,
LinkValue: "https://www.bilibili.com",
Build: map[int]*model.Build{2: {Build: 100, Condition: "gt"}},
Sound: 1,
Vibration: 1,
PassThrough: 1,
Progress: new(model.Progress),
MidFile: "xxx.txt",
PushTime: xtime.Time(1500000000),
ExpireTime: xtime.Time(1600000000),
Status: model.TaskStatusPrepared,
}
c := context.Background()
taskID, err := d.AddTask(c, t)
taskIDString := strconv.FormatInt(taskID, 10)
So(err, ShouldBeNil)
Convey("task info", func() {
task, err := d.Task(c, taskIDString)
So(err, ShouldBeNil)
task.ID = ""
So(task, ShouldResemble, t)
})
Convey("update task progress", func() {
p := &model.Progress{TokenTotal: 100}
err := d.UpdateTaskProgress(c, taskIDString, p)
So(err, ShouldBeNil)
task, err := d.Task(c, taskIDString)
So(err, ShouldBeNil)
So(task, ShouldNotBeEmpty)
So(task.Progress.TokenTotal, ShouldEqual, 100)
})
Convey("update task status", func() {
err := d.UpdateTaskStatus(c, taskIDString, model.TaskStatusDone)
So(err, ShouldBeNil)
task, err := d.Task(c, taskIDString)
So(err, ShouldBeNil)
So(task, ShouldNotBeEmpty)
So(task.Status, ShouldEqual, model.TaskStatusDone)
})
Convey("tx tokens by platform", func() {
tx, _ := d.BeginTx(context.Background())
_, err := d.TxTaskByPlatform(tx, model.PlatformIPad)
So(err, ShouldBeNil)
err = tx.Commit()
So(err, ShouldBeNil)
})
}))
}
func Test_Business(t *testing.T) {
Convey("get businesses", t, WithDao(func(d *Dao) {
res, err := d.Businesses(context.Background())
So(err, ShouldBeNil)
So(res, ShouldNotBeEmpty)
fmt.Println(res[1])
}))
}
func Test_Setting(t *testing.T) {
Convey("setting", t, WithDao(func(d *Dao) {
c := context.Background()
mid := int64(910819)
err := d.SetSetting(c, mid, model.Settings)
So(err, ShouldBeNil)
res, err := d.Setting(c, mid)
So(err, ShouldBeNil)
So(res, ShouldResemble, model.Settings)
}))
}
func Test_Report(t *testing.T) {
r := &model.Report{
APPID: model.APPIDBBPhone,
PlatformID: model.PlatformIPhone,
Mid: 910819,
Buvid: "b",
DeviceToken: strconv.FormatInt(time.Now().UnixNano(), 10),
Build: 2233,
TimeZone: 8,
NotifySwitch: model.SwitchOn,
DeviceBrand: "OPPO",
DeviceModel: "OPPO R9st",
OSVersion: "6.0.1",
}
c := context.Background()
Convey("report", t, WithDao(func(d *Dao) {
id, err := d.AddReport(c, r)
So(err, ShouldBeNil)
r.ID = id
rt, err := d.Report(c, r.DeviceToken)
So(err, ShouldBeNil)
So(rt, ShouldResemble, r)
_, err = d.ReportsByMid(c, r.Mid)
So(err, ShouldBeNil)
_, err = d.ReportsByMids(c, []int64{r.Mid})
So(err, ShouldBeNil)
rows, err := d.DelReport(c, r.DeviceToken)
So(err, ShouldBeNil)
So(rows, ShouldBeGreaterThan, 0)
res, err := d.ReportsByID(context.TODO(), []int64{1, 2, 3})
So(err, ShouldBeNil)
// t.Logf("ReportsByID res(%v)", res)
So(len(res), ShouldBeGreaterThan, 0)
res1, err := d.Reports(context.TODO(), []string{"742381013eb5fb21e003479d041369481ca861d41a9e489abe9d44c27dd43d74", "cidViiN2cwpUdlrQXXPJlyk47N69WDje3PA1+ISCGIA="})
So(err, ShouldBeNil)
t.Log(len(res1))
}))
}
func Test_UpdateReport(t *testing.T) {
Convey("update report", t, WithDao(func(d *Dao) {
ctx := context.Background()
r := &model.Report{
APPID: model.APPIDBBPhone,
PlatformID: model.PlatformIPhone,
Mid: 910819,
Buvid: "b",
DeviceToken: "dt",
Build: 2233,
TimeZone: 8,
NotifySwitch: model.SwitchOn,
DeviceBrand: "OPPO",
DeviceModel: "OPPO R9st",
OSVersion: "6.0.1",
}
_, err := d.db.Exec(context.Background(), "delete from push_reports where token_hash=?", model.HashToken(r.DeviceToken))
So(err, ShouldBeNil)
id, err := d.AddReport(ctx, r)
So(err, ShouldBeNil)
So(id, ShouldBeGreaterThan, 0)
r.ID = id
rt, err := d.Report(ctx, r.DeviceToken)
So(err, ShouldBeNil)
So(rt, ShouldResemble, r)
rt.APPID = 2
rt.PlatformID = 3
rt.NotifySwitch = model.SwitchOff
rt.Mid = 123
rt.Buvid = "buvidxxxx"
rt.Build = 1000000
rt.OSVersion = "x.x.x"
err = d.UpdateReport(ctx, rt)
So(err, ShouldBeNil)
rt2, err := d.Report(ctx, r.DeviceToken)
So(err, ShouldBeNil)
So(rt2, ShouldResemble, rt)
So(rt2, ShouldNotResemble, r)
}))
}
func Test_Callback(t *testing.T) {
Convey("add callback", t, WithDao(func(d *Dao) {
cb := &model.Callback{
Task: "task123",
APP: model.APPIDBBPhone,
Platform: model.PlatformXiaomi,
Mid: 91221505,
Pid: model.MobiAndroid,
Token: "token",
Buvid: "buvid",
Click: 1,
Extra: &model.CallbackExtra{
Status: 2,
},
}
err := d.AddCallback(context.TODO(), cb)
So(err, ShouldBeNil)
}))
}
func Test_ReportByID(t *testing.T) {
Convey("report by id", t, WithDao(func(d *Dao) {
r, err := d.ReportByID(context.TODO(), 1)
So(err, ShouldBeNil)
So(r, ShouldNotBeNil)
t.Logf("reportByID res(%+v)", r)
}))
}

View File

@@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["client_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/main/push/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"auth.go",
"client.go",
"define.go",
],
importpath = "go-common/app/service/main/push/dao/oppo",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/stat: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,63 @@
package oppo
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
// Auth oppo auth token.
type Auth struct {
Token string
Expire int64
}
type authResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Token string `json:"auth_token"`
CreateTime int64 `json:"create_time"` // auth token的授权时间单位为毫秒
} `json:"data"`
}
// NewAuth get auth token.
func NewAuth(key, secret string) (a *Auth, err error) {
tm := strconv.FormatInt(time.Now().UnixNano()/1000000, 10) // 用毫秒
params := url.Values{}
params.Add("app_key", key)
params.Add("timestamp", tm)
params.Add("sign", sign(key, secret, tm))
res, err := http.PostForm(_apiAuth, params)
if err != nil {
return
}
defer res.Body.Close()
dc := json.NewDecoder(res.Body)
resp := new(authResponse)
if err = dc.Decode(resp); err != nil {
return
}
if resp.Code == ResponseCodeSuccess {
a = &Auth{
Token: resp.Data.Token,
Expire: resp.Data.CreateTime/1000 + _authExpire,
}
return
}
err = fmt.Errorf("new access error, code(%d) description(%s)", resp.Code, resp.Message)
return
}
func sign(key, secret, timestamp string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(key+timestamp+secret)))
}
// IsExpired judge that whether privilige expired.
func (a *Auth) IsExpired() bool {
return a.Expire <= time.Now().Add(4*time.Hour).Unix() // 提前4小时过期for renew auth
}

View File

@@ -0,0 +1,184 @@
package oppo
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go-common/library/log"
"go-common/library/stat"
"go-common/library/stat/prom"
)
// Client huawei push http client.
type Client struct {
Auth *Auth
HTTPClient *http.Client
Stats stat.Stat
Activity string
}
// NewClient new huawei push HTTP client.
func NewClient(a *Auth, activity string, timeout time.Duration) *Client {
return &Client{
Auth: a,
HTTPClient: &http.Client{Timeout: timeout},
Stats: prom.HTTPClient,
Activity: activity,
}
}
// Message saves push message content.
func (c *Client) Message(m *Message) (response *Response, err error) {
now := time.Now()
if c.Stats != nil {
defer func() {
c.Stats.Timing(_apiMessage, int64(time.Since(now)/time.Millisecond))
log.Info("oppo message stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(_apiMessage, "failed")
}
}()
}
params := url.Values{}
params.Add("auth_token", c.Auth.Token)
params.Add("title", m.Title)
params.Add("content", m.Content)
params.Add("click_action_type", strconv.Itoa(m.ActionType))
params.Add("click_action_activity", c.Activity)
params.Add("action_parameters", m.ActionParams)
params.Add("off_line_ttl", strconv.Itoa(m.OfflineTTL))
params.Add("call_back_url", m.CallbackURL)
req, err := http.NewRequest(http.MethodPost, _apiMessage, strings.NewReader(params.Encode()))
if err != nil {
log.Error("http.NewRequest() error(%v)", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Connection", "Keep-Alive")
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &Response{}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}
// Push push notification.
func (c *Client) Push(msgID string, tokens []string) (response *Response, err error) {
now := time.Now()
if c.Stats != nil {
defer func() {
c.Stats.Timing(_apiPushBroadcast, int64(time.Since(now)/time.Millisecond))
log.Info("oppo push stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(_apiPushBroadcast, "failed")
}
}()
}
params := url.Values{}
params.Add("auth_token", c.Auth.Token)
params.Add("message_id", msgID)
params.Add("target_type", _pushTypeToken)
params.Add("registration_ids", strings.Join(tokens, ";"))
req, err := http.NewRequest(http.MethodPost, _apiPushBroadcast, strings.NewReader(params.Encode()))
if err != nil {
log.Error("http.NewRequest() error(%v)", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Connection", "Keep-Alive")
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &Response{}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}
// PushOne push single notification.
func (c *Client) PushOne(m *Message, token string) (response *Response, err error) {
now := time.Now()
if c.Stats != nil {
defer func() {
c.Stats.Timing(_apiPushUnicast, int64(time.Since(now)/time.Millisecond))
log.Info("oppo pushOne stats timing: %v", int64(time.Since(now)/time.Millisecond))
if err != nil {
c.Stats.Incr(_apiPushUnicast, "failed")
}
}()
}
m.ActionActivity = c.Activity
params := url.Values{}
msg, _ := json.Marshal(&struct {
TargetType string `json:"target_type"`
Token string `json:"registration_id"`
Notification *Message `json:"notification"`
}{
TargetType: _pushTypeToken,
Token: token,
Notification: m,
})
params.Add("auth_token", c.Auth.Token)
params.Add("message", string(msg))
req, err := http.NewRequest(http.MethodPost, _apiPushUnicast, strings.NewReader(params.Encode()))
if err != nil {
log.Error("http.NewRequest() error(%v)", err)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Connection", "Keep-Alive")
res, err := c.HTTPClient.Do(req)
if err != nil {
log.Error("HTTPClient.Do() error(%v)", err)
return
}
defer res.Body.Close()
response = &Response{}
bs, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Error("ioutil.ReadAll() error(%v)", err)
return
} else if len(bs) == 0 {
return
}
if e := json.Unmarshal(bs, &response); e != nil {
if e != io.EOF {
log.Error("json decode body(%s) error(%v)", string(bs), e)
}
}
return
}

View File

@@ -0,0 +1,79 @@
package oppo
import (
"encoding/json"
"testing"
"time"
"go-common/app/service/main/push/model"
"github.com/smartystreets/goconvey/convey"
)
var (
auth = &Auth{Token: "7826d9a0-f192-4402-8a2c-b0dffbdd94da", Expire: 1519336387}
cli = NewClient(auth, "com.bilibili.oppo.push.internal", time.Hour)
)
func init() {
auth, _ = NewAuth("UTlf5g2bAOSQA9aCqYiFQh3X", "Ppq0B4xk73augxMJbEBSyu9m")
}
func Test_AuthExpire(t *testing.T) {
convey.Convey("auth expire", t, func() {
a := Auth{Expire: time.Now().Add(-8 * time.Hour).Unix()}
if !a.IsExpired() {
t.Errorf("access should be expire")
}
if auth.IsExpired() {
t.Error("access should not be expire")
}
})
}
func Test_Message(t *testing.T) {
convey.Convey("message", t, func() {
params, _ := json.Marshal(map[string]string{
"task_id": "123",
"scheme": model.Scheme(1, "2", model.PlatformAndroid, model.UnknownBuild),
})
m := &Message{
Title: "this is title",
Content: "this is content",
ActionType: ActionTypeInner,
ActionParams: string(params),
OfflineTTL: 3600,
}
res, err := cli.Message(m)
convey.So(err, convey.ShouldBeNil)
t.Logf("message result(%+v)", res)
})
}
func Test_Pushs(t *testing.T) {
convey.Convey("pushs", t, func() {
res, err := cli.Push("5a72f82ba250c94f9f51540d", []string{"token1", "token2"})
convey.So(err, convey.ShouldBeNil)
t.Logf("push result(%+v)", res)
})
}
func Test_PushOne(t *testing.T) {
convey.Convey("push one", t, func() {
params, _ := json.Marshal(map[string]string{
"task_id": "123",
"scheme": model.Scheme(1, "2", model.PlatformAndroid, model.UnknownBuild),
})
m := &Message{
Title: "this is title",
Content: "this is content",
ActionType: ActionTypeInner,
ActionParams: string(params),
OfflineTTL: 3600,
// CallbackURL: oppo.CallbackURL(1, 123),
}
res, err := cli.PushOne(m, "") // baab653406d187af12daa9980c87f4e5
convey.So(err, convey.ShouldBeNil)
t.Logf("pushOne result(%+v)", res)
})
}

View File

@@ -0,0 +1,72 @@
package oppo
import "fmt"
const (
_host = "https://api.push.oppomobile.com"
_apiAuth = _host + "/server/v1/auth"
_apiMessage = _host + "/server/v1/message/notification/save_message_content" // 保存通知栏消息内容体
_apiPushUnicast = _host + "/server/v1/message/notification/unicast" // 单条推送
_apiPushBroadcast = _host + "/server/v1/message/notification/broadcast" // 批量推送
// _apiStatistics = _host + "/server/v1/message/statistics" // 推送统计
_callbackURL = "https://api.bilibili.com/x/push/callback/oppo"
_authExpire = 24 * 60 * 60 // auth token 过期秒数
// _pushTypeAll = "1" // 推送全部设备
_pushTypeToken = "2" // 按token推
// ResponseCodeServiceUnavalable service unavalable
ResponseCodeServiceUnavalable = -1
// ResponseCodeSuccess http normal response code
ResponseCodeSuccess = 0
// ResponseCodeInvalidToken invalid token response code
ResponseCodeInvalidToken = 10000
// ResponseCodeUnsubscribeToken unsubscribe token
ResponseCodeUnsubscribeToken = 10001
// ResponseCodeRepeatToken repeat token
ResponseCodeRepeatToken = 10004
// ActionTypeInner 打开应⽤内⻚activity的intentaction
ActionTypeInner = 1
)
// Message message content.
type Message struct {
Title string `json:"title"`
Content string `json:"content"`
ActionType int `json:"click_action_type"` // 0:启动应⽤; 1:打开应⽤内⻚(activity的intentaction); 2:打开⽹⻚; 4:打开应⽤内⻚(activity); [⾮必填默认值为0]
ActionActivity string `json:"click_action_activity"` // 应⽤内⻚地址【click_action_type 为1或4时必填⻓度500】
ActionURL string `json:"click_action_url"` // ⽹⻚地址【click_action_type为2 必填⻓度500】
ActionParams string `json:"action_parameters"` // 传递给应⽤的参数,json格式
OfflineTTL int `json:"off_line_ttl"` // 离线消息的存活时间 (默认3600s) (单位:秒), 【off_line值为true时必填最 ⻓10天】
CallbackURL string `json:"call_back_url"` // 应⽤接收消息到达回执的回调(仅⽀持registrationId或aliasName 两种推送⽅式)
}
// Response push response.
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
MsgID string `json:"message_id,omitempty"`
TaskID string `json:"task_id,omitempty"`
TokenInvalid []string `json:"10000"`
TokenUnsubscribe []string `json:"10001"`
TokenRepeat []string `json:"10004"`
} `json:"data"`
}
// Callback oppo callback.
type Callback struct {
MsgID string `json:"messageId"`
TaskID string `json:"taskId"`
Tokens string `json:"registrationIds"` // regId1, regid2
EventType string `json:"eventType"` // push_arrive
}
// CallbackURL gets callback URL.
func CallbackURL(app int64, task string) string {
return fmt.Sprintf("%s?app=%d&task=%s", _callbackURL, app, task)
}

View File

@@ -0,0 +1,358 @@
package dao
import (
"context"
"fmt"
"strconv"
"strings"
"sync/atomic"
"time"
"unicode"
"go-common/app/service/main/push/dao/apns2"
"go-common/app/service/main/push/dao/fcm"
"go-common/app/service/main/push/dao/huawei"
"go-common/app/service/main/push/dao/jpush"
"go-common/app/service/main/push/dao/mi"
"go-common/app/service/main/push/dao/oppo"
"go-common/app/service/main/push/model"
"go-common/library/log"
)
func fmtRoundIndex(appid int64, platform int) string {
return fmt.Sprintf("%d_%d", appid, platform)
}
func (d *Dao) roundIndex(appid int64, platform int) (int, error) {
i := fmtRoundIndex(appid, platform)
l := d.clientsLen[i]
if l == 0 {
log.Error("no client app(%d) platform(%d)", appid, platform)
PromError("push:no client")
return 0, errNoClinets
}
n := atomic.AddUint32(d.clientsIndex[i], 1)
if n%uint32(l) == 0 { // 把第一个client预留出来做一些其它类型请求工作
n = atomic.AddUint32(d.clientsIndex[i], 1)
}
return int(n % uint32(l)), nil
}
func logPushError(task string, platform int, tokens []string) {
for _, t := range tokens {
log.Error("push error, task(%s) platfrom(%d) token(%s)", task, platform, t)
}
}
func buildAPNS(info *model.PushInfo, item *model.PushItem) *apns2.Payload {
var aps apns2.Aps
if info.PassThrough == model.SwitchOn {
aps = apns2.Aps{
ContentAvailable: 1, // 必带字段,让程序处于后台时也可以获取到推送内容
}
} else {
aps = apns2.Aps{
Alert: apns2.Alert{
Title: info.Title,
Body: info.Summary,
},
Badge: 0,
MutableContent: 1,
}
if info.Sound == model.SwitchOn {
aps.Sound = "default" // 默认提示音
}
}
scheme := model.Scheme(info.LinkType, info.LinkValue, item.Platform, item.Build)
return &apns2.Payload{Aps: aps, URL: scheme, TaskID: info.TaskID, Token: item.Token, Image: info.ImageURL}
}
// PushIPhone .
func (d *Dao) PushIPhone(c context.Context, info *model.PushInfo, item *model.PushItem) (res *model.HTTPResponse, err error) {
var (
index int
response *apns2.Response
)
if index, err = d.roundIndex(info.APPID, item.Platform); err != nil {
return
}
if response, err = d.clientsIPhone[info.APPID][index].Push(item.Token, buildAPNS(info, item), int64(info.ExpireTime)); err != nil {
log.Error("push iPhone task(%s) mid(%d) token(%s) error", info.TaskID, item.Mid, item.Token)
PromError("push: 推送iPhone")
return
}
if response == nil {
return
}
res = &model.HTTPResponse{Code: response.StatusCode, Msg: response.Reason}
log.Info("push iPhone task(%s) mid(%d) token(%s) success", info.TaskID, item.Mid, item.Token)
return
}
// PushIPad .
func (d *Dao) PushIPad(c context.Context, info *model.PushInfo, item *model.PushItem) (res *model.HTTPResponse, err error) {
var (
index int
response *apns2.Response
)
if index, err = d.roundIndex(info.APPID, item.Platform); err != nil {
return
}
if response, err = d.clientsIPad[info.APPID][index].Push(item.Token, buildAPNS(info, item), int64(info.ExpireTime)); err != nil {
log.Error("push iPad task(%s) mid(%d) token(%s) error", info.TaskID, item.Mid, item.Token)
PromError("push:推送iPad")
return
}
if response == nil {
return
}
res = &model.HTTPResponse{Code: response.StatusCode, Msg: response.Reason}
log.Info("push iPad task(%s) mid(%d) token(%s) success", info.TaskID, item.Mid, item.Token)
return
}
// PushMi .
func (d *Dao) PushMi(c context.Context, info *model.PushInfo, scheme, tokens string) (res *model.HTTPResponse, err error) {
res = &model.HTTPResponse{}
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformXiaomi); err != nil {
return
}
passThrough := mi.NotPassThrough
if info.PassThrough == model.SwitchOn {
passThrough = mi.PassThrough
}
xmm := &mi.XMMessage{
Payload: scheme,
RestrictedPackageName: d.clientsMi[info.APPID][0].Package,
PassThrough: passThrough,
Title: info.Title,
Description: info.Summary,
NotifyType: mi.NotifyTypeDefaultNone,
TaskID: info.TaskID,
}
xmm.SetRegID(tokens)
xmm.SetNotifyID(info.TaskID)
xmm.SetTimeToLive(int64(info.ExpireTime))
xmm.SetCallbackParam(strconv.FormatInt(info.APPID, 10))
if info.Sound == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultSound)
}
if info.Vibration == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultVibration)
}
if info.Sound == model.SwitchOn && info.Vibration == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultAll)
}
var response *mi.Response
if response, err = d.clientsMi[info.APPID][index].Push(xmm); err != nil {
log.Error("push mi task(%s) resp(%+v) error(%v)", info.TaskID, response, err)
logPushError(info.TaskID, model.PlatformXiaomi, strings.Split(tokens, ","))
PromError("push:推送Xiaomi")
return
}
res.Code = response.Code
res.Msg = response.Reason
if response.Code == mi.ResultCodeOk {
res.Code = model.HTTPCodeOk
res.Msg = response.Info
}
log.Info("push mi task(%s) tokens(%d) result(%+v) traceid(%s) success", info.TaskID, len(tokens), res, response.TraceID)
return
}
// PushMiByMids .
func (d *Dao) PushMiByMids(c context.Context, info *model.PushInfo, scheme, mids string) (res *model.HTTPResponse, err error) {
if d.clientMiByMids[info.APPID] == nil {
return
}
res = &model.HTTPResponse{}
passThrough := mi.NotPassThrough
if info.PassThrough == model.SwitchOn {
passThrough = mi.PassThrough
}
xmm := &mi.XMMessage{
Payload: scheme,
RestrictedPackageName: d.clientMiByMids[info.APPID].Package,
PassThrough: passThrough,
Title: info.Title,
Description: info.Summary,
NotifyType: mi.NotifyTypeDefaultNone,
TaskID: info.TaskID,
}
xmm.SetUserAccount(mids)
xmm.SetNotifyID(info.TaskID)
xmm.SetTimeToLive(int64(info.ExpireTime))
xmm.SetCallbackParam(strconv.FormatInt(info.APPID, 10))
if info.Sound == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultSound)
}
if info.Vibration == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultVibration)
}
if info.Sound == model.SwitchOn && info.Vibration == model.SwitchOn {
xmm.SetNotifyType(mi.NotifyTypeDefaultAll)
}
var response *mi.Response
if response, err = d.clientMiByMids[info.APPID].Push(xmm); err != nil {
log.Error("d.PushMi(%s,%s,%s) error(%v)", info.TaskID, scheme, mids, err)
PromError("push:推送miByMids")
return
}
res.Code = response.Code
res.Msg = response.Reason
if response.Code == mi.ResultCodeOk {
res.Code = model.HTTPCodeOk
res.Msg = response.Info
}
return
}
// PushHuawei push huawei notifications.
func (d *Dao) PushHuawei(c context.Context, info *model.PushInfo, scheme string, tokens []string) (res *huawei.Response, err error) {
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformHuawei); err != nil {
return
}
payload := huawei.NewMessage().SetTitle(info.Title).SetContent(info.Summary).SetCustomize("task_id", info.TaskID).SetCustomize("scheme", scheme).SetBiTag(info.TaskID).SetIcon(info.ImageURL)
if info.PassThrough == model.SwitchOn {
payload.SetMsgType(huawei.MsgTypePassthrough)
}
expire := time.Unix(int64(info.ExpireTime), 0)
if res, err = d.clientsHuawei[info.APPID][index].Push(payload, tokens, expire); err != nil {
if err == huawei.ErrLimit {
return
}
log.Error("push huawei task(%s) resp(%+v) tokens(%v) error(%v)", info.TaskID, res, tokens, err)
logPushError(info.TaskID, model.PlatformHuawei, tokens)
return
}
log.Info("push huawei task(%s) tokens(%d) result(%+v) success", info.TaskID, len(tokens), res)
return
}
// OppoMessage saves oppo message content.
func (d *Dao) OppoMessage(c context.Context, info *model.PushInfo, m *oppo.Message) (res *oppo.Response, err error) {
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformOppo); err != nil {
return
}
if res, err = d.clientsOppo[info.APPID][index].Message(m); err != nil {
log.Error("save oppo message task(%s) result(%+v) error(%v)", info.TaskID, res, err)
return
}
log.Info("save oppo message task(%s) result(%+v) success", info.TaskID, res)
return
}
// PushOppo push oppo notifications.
func (d *Dao) PushOppo(c context.Context, info *model.PushInfo, msgID string, tokens []string) (res *oppo.Response, err error) {
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformOppo); err != nil {
return
}
if res, err = d.clientsOppo[info.APPID][index].Push(msgID, tokens); err != nil {
log.Error("push oppo task(%s) resp(%+v) error(%v)", info.TaskID, res, err)
logPushError(info.TaskID, model.PlatformOppo, tokens)
return
}
log.Info("push oppo task(%s) tokens(%d) result(%+v) success", info.TaskID, len(tokens), res)
return
}
// PushOppoOne push oppo notifications.
func (d *Dao) PushOppoOne(c context.Context, info *model.PushInfo, m *oppo.Message, token string) (res *oppo.Response, err error) {
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformOppo); err != nil {
return
}
if res, err = d.clientsOppo[info.APPID][index].PushOne(m, token); err != nil {
log.Error("push oppo one task(%s) token(%s) result(%+v) error(%v)", info.TaskID, token, res, err)
return
}
log.Info("push oppo one task(%s) token(%s) result(%+v) success", info.TaskID, token, res)
return
}
// PushJpush push huawei notifications.
func (d *Dao) PushJpush(c context.Context, info *model.PushInfo, scheme string, tokens []string) (res *jpush.PushResponse, err error) {
var (
index int
ad jpush.Audience
notice jpush.Notice
plat = jpush.NewPlatform(jpush.PlatformAndroid)
payload = jpush.NewPayload()
cbr = jpush.NewCallbackReq()
an = &jpush.AndroidNotice{
Title: info.Title,
Alert: info.Summary,
AlertType: jpush.AndroidAlertTypeNone,
Extras: map[string]interface{}{
"task_id": info.TaskID,
"scheme": scheme,
},
}
)
if index, err = d.roundIndex(info.APPID, model.PlatformJpush); err != nil {
return
}
if info.Sound == model.SwitchOn {
an.AlertType |= jpush.AndroidAlertTypeSound
}
if info.Vibration == model.SwitchOn {
an.AlertType |= jpush.AndroidAlertTypeVibrate
}
if info.Sound == model.SwitchOn && info.Vibration == model.SwitchOn {
an.AlertType = jpush.AndroidAlertTypeAll
}
ad.SetID(tokens)
notice.SetAndroidNotice(an)
payload.SetPlatform(plat)
payload.SetAudience(&ad)
payload.SetNotice(&notice)
payload.Options.SetTimelive(int(int64(info.ExpireTime) - time.Now().Unix()))
payload.Options.SetReturnInvalidToken(true)
cbr.SetParam(map[string]string{"task": info.TaskID, "appid": strconv.FormatInt(info.APPID, 10)})
payload.SetCallbackReq(cbr)
if res, err = d.clientsJpush[info.APPID][index].Push(payload); err != nil {
logPushError(info.TaskID, model.PlatformJpush, tokens)
log.Error("push jpush task(%s) tokens(%d) result(%+v) error(%v)", info.TaskID, len(tokens), res, err)
return
}
log.Info("push jpush task(%s) tokens(%d) result(%+v) success", info.TaskID, len(tokens), res)
return
}
// PushFCM .
func (d *Dao) PushFCM(ctx context.Context, info *model.PushInfo, scheme string, tokens []string) (res *fcm.Response, err error) {
var index int
if index, err = d.roundIndex(info.APPID, model.PlatformFCM); err != nil {
return
}
message := fcm.Message{
Data: map[string]string{
"task_id": info.TaskID,
"scheme": scheme,
},
RegistrationIDs: tokens,
Priority: fcm.PriorityHigh,
DelayWhileIdle: true,
Notification: fcm.Notification{
Title: info.Title,
Body: info.Summary,
ClickAction: "com.bilibili.app.in.com.bilibili.push.FCM_MESSAGE",
},
TimeToLive: int(int64(info.ExpireTime) - time.Now().Unix()),
CollapseKey: strings.TrimFunc(info.TaskID, func(r rune) bool {
return !unicode.IsNumber(r)
}), // 应客户端要求task_id 保证值转成 int 传到客户端
Android: fcm.Android{Priority: fcm.PriorityHigh},
}
if res, err = d.clientsFCM[info.APPID][index].Send(&message); err != nil {
log.Error("push fcm task(%s) tokens(%d) result(%+v) error(%v)", info.TaskID, len(tokens), res)
PromError("push: 推送fcm")
return
}
log.Info("push fcm task(%s) tokens(%d) result(%+v) error(%v)", info.TaskID, len(tokens), res)
return
}

View File

@@ -0,0 +1,86 @@
package dao
import (
"context"
"testing"
"go-common/app/service/main/push/dao/oppo"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
func Test_roundIndex(t *testing.T) {
Convey("ping redis", t, WithDao(func(d *Dao) {
err := d.pingRedis(context.Background())
PromChanLen("a", 1)
So(err, ShouldBeNil)
_, err = d.roundIndex(0, 0)
So(err, ShouldNotBeNil)
logPushError("asd", 1, []string{"asd"})
}))
}
func TestPushIPhone(t *testing.T) {
Convey("push ipad", t, WithDao(func(d *Dao) {
_, err := d.PushIPhone(context.Background(), &model.PushInfo{}, &model.PushItem{})
So(err, ShouldNotBeNil)
}))
}
func TestPushIPad(t *testing.T) {
Convey("push ipad", t, WithDao(func(d *Dao) {
_, err := d.PushIPad(context.Background(), &model.PushInfo{}, &model.PushItem{})
So(err, ShouldNotBeNil)
}))
}
func TestPushMi(t *testing.T) {
Convey("push mi", t, WithDao(func(d *Dao) {
_, err := d.PushMi(context.Background(), &model.PushInfo{}, "", "123")
So(err, ShouldNotBeNil)
}))
}
func TestPushMiByMids(t *testing.T) {
Convey("push mi by mids", t, WithDao(func(d *Dao) {
_, err := d.PushMiByMids(context.Background(), &model.PushInfo{}, "", "123")
So(err, ShouldBeNil)
}))
}
func TestPushHuawei(t *testing.T) {
Convey("push huawei", t, WithDao(func(d *Dao) {
_, err := d.PushHuawei(context.Background(), &model.PushInfo{}, "", []string{"123"})
So(err, ShouldNotBeNil)
}))
}
func TestPushOppo(t *testing.T) {
Convey("push oppo", t, WithDao(func(d *Dao) {
_, err := d.PushOppo(context.Background(), &model.PushInfo{}, "", []string{"123"})
So(err, ShouldNotBeNil)
}))
}
func TestPushOppoOne(t *testing.T) {
Convey("push oppo one", t, WithDao(func(d *Dao) {
_, err := d.PushOppoOne(context.Background(), &model.PushInfo{}, &oppo.Message{}, "123")
So(err, ShouldNotBeNil)
}))
}
func TestPushJpush(t *testing.T) {
Convey("push jpush", t, WithDao(func(d *Dao) {
_, err := d.PushJpush(context.Background(), &model.PushInfo{}, "", []string{"123"})
So(err, ShouldNotBeNil)
}))
}
func TestPushFCM(t *testing.T) {
Convey("push fcm", t, WithDao(func(d *Dao) {
_, err := d.PushFCM(context.Background(), &model.PushInfo{}, "", []string{"123"})
So(err, ShouldNotBeNil)
}))
}

View File

@@ -0,0 +1,18 @@
package dao
import (
"context"
"go-common/library/log"
)
// pingRedis ping redis.
func (d *Dao) pingRedis(c context.Context) (err error) {
conn := d.redis.Get(c)
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
}

View File

@@ -0,0 +1,15 @@
package dao
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_PingRedis(t *testing.T) {
Convey("ping redis", t, WithDao(func(d *Dao) {
err := d.pingRedis(context.Background())
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,120 @@
package dao
import (
"context"
"time"
"go-common/app/service/main/push/dao/mi"
"go-common/app/service/main/push/model"
"go-common/library/log"
)
// DelMiInvalid .
func (d *Dao) DelMiInvalid(c context.Context) (err error) {
log.Info("delete xiaomi invalid report start, apps(%d)", len(d.clientsMi))
for appid, clients := range d.clientsMi {
log.Info("clients info app(%d) len(%d)", appid, len(clients))
if len(clients) == 0 {
log.Warn("no clients app(%d)", appid)
continue
}
log.Info("del mi invalid start, app(%d)", appid)
var res *mi.Response
if res, err = clients[0].InvalidTokens(); err != nil {
log.Error("client.InvalidTokens() error(%v)", err)
PromError("report:获取小米无效token")
continue
}
if res == nil || len(res.Data.List) == 0 {
log.Warn("no tokens app(%d)", appid)
continue
}
if err = d.delInvalidMiReports(c, appid, res.Data.List); err != nil {
PromError("report:主动删除xiaomi无效上报")
continue
}
log.Info("already del mi invalid stop, app(%d) count(%d)", appid, len(res.Data.List))
}
PromInfo("report:主动删除xiaomi无效上报")
return
}
// DelMiUninstalled deletes mi uninstalled tokens.
func (d *Dao) DelMiUninstalled(c context.Context) (err error) {
log.Info("delete xiaomi uninstalled tokens start, apps(%d)", len(d.clientsMi))
for appid, clients := range d.clientsMi {
log.Info("clients info app(%d) len(%d)", appid, len(clients))
if len(clients) == 0 {
log.Warn("no clients app(%d)", appid)
continue
}
log.Info("del mi uninstalled tokens start, app(%d)", appid)
var res *mi.UninstalledResponse
if res, err = clients[0].UninstalledTokens(); err != nil {
log.Error("client.UninstalledTokens() error(%v)", err)
PromError("report:获取小米卸载token")
continue
}
if res.Code == mi.ResultCodeNoMsgInEmq {
log.Info("no tokens app(%d)", appid)
continue
}
if res.Code != mi.ResultCodeOk {
log.Error("get uninstalled tokens error resp(%+v)", res)
continue
}
if len(res.Data) == 0 {
log.Warn("no tokens app(%d)", appid)
continue
}
if err = d.delInvalidMiReports(c, appid, res.Data); err != nil {
PromError("report:主动删除xiaomi卸载token")
continue
}
log.Info("already del mi uninstalled stop, app(%d) count(%d)", appid, len(res.Data))
}
PromInfo("report:主动删除xiaomi卸载token")
return
}
func (d *Dao) delInvalidMiReports(c context.Context, appid int64, tokens []string) (err error) {
var rs []*model.Report
if rs, err = d.Reports(c, tokens); err != nil {
log.Error("d.Reports(%v) error(%v)", tokens, err)
return
} else if len(rs) == 0 {
log.Warn("reports can not be found by tokens(%d)", len(tokens))
log.Warn("reports can not be found by tokens(%v)", tokens)
return
}
for _, r := range rs {
log.Info("deleted invalid mi report, app(%d) mid(%d) token(%s)", appid, r.Mid, r.DeviceToken)
var (
i int
e error
retry = _retry
)
for i < retry {
if _, e = d.DelReport(c, r.DeviceToken); e == nil {
break
}
time.Sleep(time.Second)
i++
log.Warn("retry delete report, mid(%d) token(%s)", r.Mid, r.DeviceToken)
}
if e != nil || r.Mid <= 0 {
continue
}
i = 0
for i < retry {
if e = d.DelReportCache(c, r.Mid, r.APPID, r.DeviceToken); e == nil {
break
}
log.Warn("retry delete report cache, mid(%d) token(%s)", r.Mid, r.DeviceToken)
time.Sleep(time.Second)
i++
}
}
log.Info("del invalid mi report, app(%d) tokens(%d)", appid, len(rs))
return
}

View File

@@ -0,0 +1,29 @@
package dao
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_DelMiInvalid(t *testing.T) {
Convey("Test_DelMiInvalid", t, WithDao(func(d *Dao) {
err := d.DelMiInvalid(context.Background())
So(err, ShouldBeNil)
}))
}
func Test_DelMiUninstalled(t *testing.T) {
Convey("Test_DelMiUninstalled", t, WithDao(func(d *Dao) {
err := d.DelMiUninstalled(context.Background())
So(err, ShouldNotBeNil)
}))
}
func Test_delInvalidMiReports(t *testing.T) {
Convey("Test_delInvalidMiReports", t, WithDao(func(d *Dao) {
err := d.delInvalidMiReports(context.Background(), 1, []string{"test"})
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["model_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/smartystreets/goconvey/convey:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"brands.go",
"constants.go",
"functions.go",
"model.go",
"platforms.go",
"rpc.go",
"settings.go",
],
importpath = "go-common/app/service/main/push/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/log:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/dgryski/go-farm: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,37 @@
package model
import "strings"
const (
_brandOhters = 0
_brandXiaomi = 1
_brandHuawei = 2
_brandOppo = 3
_brandVivo = 4
_brandMeizu = 5
_brandSamsung = 6
)
// mapping 映射可以解决一个品牌对应多个品牌标识的问题
var brandMapping = map[string]int{
"xiaomi": _brandXiaomi,
"huawei": _brandHuawei,
"honor": _brandHuawei,
"oppo": _brandOppo,
"vivo": _brandVivo,
"meizu": _brandMeizu,
"samsung": _brandSamsung,
}
// DeviceBrand .
func DeviceBrand(s string) int {
s = strings.Trim(s, " ")
if s == "" {
return _brandOhters
}
s = strings.ToLower(s)
if v, ok := brandMapping[s]; ok {
return v
}
return _brandOhters
}

View File

@@ -0,0 +1,178 @@
package model
const (
// TempTaskPrefix used to separate from the DB tasks.
TempTaskPrefix = "t"
// APPIDBBPhone 哔哩哔哩动画
APPIDBBPhone = 1
// HTTPCodeOk http response normally.
HTTPCodeOk = 0
// SwitchOff off.
SwitchOff = 0
// SwitchOn on.
SwitchOn = 1
// DelMiFeedback feedback 删除 (无效token删除方式)
DelMiFeedback = 1
// DelMiUninstalled 卸载
DelMiUninstalled = 2
// DefaultMessageTitle .
DefaultMessageTitle = "哔哩哔哩消息"
// UnknownBuild 未知build号
UnknownBuild = 0
)
const (
// MobiAndroid mobi_app android
MobiAndroid = 1
// MobiIPhone mobi_app iPhone
MobiIPhone = 2
// MobiIPad mobi_app iPad
MobiIPad = 3
// MobiAndroidComic
MobiAndroidComic = 4
)
// task status
const (
// TaskStatusPending 待审核
TaskStatusPending = int8(-5)
// TaskStatusStop 主动停止
TaskStatusStop = int8(-4)
// TaskStatusDelay 延期
TaskStatusDelay = int8(-3)
// TaskStatusExpired 过期
TaskStatusExpired = int8(-2)
// TaskStatusFailed 失败
TaskStatusFailed = int8(-1)
// TaskStatusPrepared 未开始
TaskStatusPrepared = int8(0)
// TaskStatusDoing 进行中
TaskStatusDoing = int8(1)
// TaskStatusDone 已完成
TaskStatusDone = int8(2)
// TaskStatusPretreatmentPrepared 等待预处理,处理完后是按平台拆成任务(token形式)
TaskStatusPretreatmentPrepared = int8(3)
// TaskStatusPretreatmentDoing 预处理中
TaskStatusPretreatmentDoing = int8(4)
// TaskStatusPretreatmentDone 预处理完成
TaskStatusPretreatmentDone = int8(5)
// TaskStatusPretreatmentFailed 预处理失败
TaskStatusPretreatmentFailed = int8(6)
// TaskStatusWaitDataPlatform 等待从数据平台获取数据
TaskStatusWaitDataPlatform = int8(7)
)
// data platform
const (
// DpCondStatusNoFile 没有查询到文件
DpCondStatusNoFile = -3
// DpCondStatusPending 待审核
DpCondStatusPending = -2
// DpCondStatusFailed 失败的查询
DpCondStatusFailed = -1
// DpCondStatusPrepared 准备提交到数据平台的查询
DpCondStatusPrepared = 0
// DpCondStatusSubmitting 提交中
DpCondStatusSubmitting = 1
// DpCondStatusSubmitted 已经提交的查询
DpCondStatusSubmitted = 2
// DpCondStatusPolling 轮询任务看有没有生成文件
DpCondStatusPolling = 3
// DpCondStatusDownloading 正在下载文件
DpCondStatusDownloading = 4
// DpCondStatusDone 已经完成的查询
DpCondStatusDone = 5
// DpTaskTypeMid mid维度查询
DpTaskTypeMid = 1
// DptaskTypeToken token维度查询
DpTaskTypeToken = 2
)
const (
// TaskTypeAll 后台全量
TaskTypeAll = 1
// TaskTypePart 后台批量
TaskTypePart = 2
// TaskTypeBusiness 业务推送
TaskTypeBusiness = 3
// TaskTypeTokens 批量token推送
TaskTypeTokens = 4
// TaskTypeMngMid 后台按mid推送
TaskTypeMngMid = 5
// TaskTypeMngToken 后台按token推送
TaskTypeMngToken = 6
// TaskTypeStrategyMid 策略层按mid推送
TaskTypeStrategyMid = 7
// TaskTypeDataPlatformMid 通过mid维度从数据平台获取token
TaskTypeDataPlatformMid = 8
// TaskTypeDataPlatformToken 通过token维度从数据平台获取token
TaskTypeDataPlatformToken = 9
)
const (
// LinkTypeBangumi bangumi 协议链接类型
LinkTypeBangumi = int8(1)
// LinkTypeVideo 视频
LinkTypeVideo = int8(2)
// LinkTypeLive 直播
LinkTypeLive = int8(3)
// LinkTypeSplist 专题页
LinkTypeSplist = int8(4)
// LinkTypeSearch 搜索
LinkTypeSearch = int8(5)
// LinkTypeAuthor 个人空间
LinkTypeAuthor = int8(6)
// LinkTypeBrowser 浏览器
LinkTypeBrowser = int8(7)
// LinkTypeVipBuy 大会员购买页
LinkTypeVipBuy = int8(10)
// LinkTypeCustom 自定义协议内容
LinkTypeCustom = int8(11)
)
const (
// 定义参考http://syncsvn.bilibili.co/app/wiki/blob/master/Android-App-URI.md
// SchemeBangumiSeasonIOS 番剧详情 iPhoneiPadHD 支持番剧
SchemeBangumiSeasonIOS = "bilibili://bangumi/season/"
// SchemeBangumiSeasonAndroid .
SchemeBangumiSeasonAndroid = "bili:///?type=season&season_id="
// SchemeVideoIOS 视频详情页 iPhoneiPadHD 支持视频
SchemeVideoIOS = "bilibili://video/"
// SchemeVideoAndroid .
SchemeVideoAndroid = "bili:///?type=bilivideo&avid="
// SchemeLive 直播详情页, 支持 iOS 和 Android 新协议
SchemeLive = "bilibili://live/"
// SchemeLiveAndroid Android 老协议
SchemeLiveAndroid = "bili:///?type=bililive&roomid="
// SchemeSplist 专题页 iPhone, iPadHD, Android 支持专题
SchemeSplist = "bilibili://splist/"
// SchemeSearchIOS 搜索 iPhoneiPadHD 支持搜索
SchemeSearchIOS = "bilibili://search/?keyword="
// SchemeSearchAndroid .
SchemeSearchAndroid = "bilibili://search/"
// SchemeAuthorIOS 个人空间 iPhoneiPadHD 支持个人空间
SchemeAuthorIOS = "bilibili://user/"
// SchemeAuthorAndroid .
SchemeAuthorAndroid = "bilibili://author/"
// SchemeBrowserIOS 指定URL iPhoneiPadHD 支持H5
SchemeBrowserIOS = "bilibili://browser/?url="
// SchemeBrowserAndroid .
SchemeBrowserAndroid = "bili:///?type=weblink&url="
// SchemeVipBuy 大会员购买页
SchemeVipBuy = "bilibili://user_center/vip/buy/"
)

View File

@@ -0,0 +1,244 @@
package model
import (
"bytes"
"crypto/md5"
"encoding/json"
"fmt"
"math"
"net/url"
"strconv"
"strings"
"time"
"go-common/library/log"
"github.com/dgryski/go-farm"
)
// SplitInts splts string to int-slice by ,
func SplitInts(s string) (res []int) {
if s == "" {
return
}
ints := strings.Split(s, ",")
for _, v := range ints {
i, _ := strconv.Atoi(v)
res = append(res, i)
}
return
}
// JoinInts merges int slice to string.
func JoinInts(ints []int) string {
if len(ints) == 0 {
return ""
}
if len(ints) == 1 {
return strconv.Itoa(ints[0])
}
buf := bytes.Buffer{}
for _, v := range ints {
buf.WriteString(strconv.Itoa(v))
buf.WriteString(",")
}
if buf.Len() > 0 {
buf.Truncate(buf.Len() - 1)
}
return buf.String()
}
// ExistsInt judge if item in the ints.
func ExistsInt(ints []int, item int) (exists bool) {
for _, i := range ints {
if i == item {
return true
}
}
return false
}
// HashToken gets token's hash value.
func HashToken(token string) int64 {
return int64(farm.Hash64([]byte(token)) % math.MaxInt64)
}
// RealTime culculates real time by timezone.
func RealTime(reportZone int) time.Time {
now := time.Now()
_, offset := now.Zone()
return now.Add(time.Duration(reportZone-offset/3600) * time.Hour)
}
// Scheme gets uri scheme.
func Scheme(typ int8, val string, platform, build int) (uri string) {
switch typ {
case LinkTypeBangumi: // 番剧
if platform == PlatformAndroid {
uri = SchemeBangumiSeasonAndroid + val
} else {
uri = SchemeBangumiSeasonIOS + val
}
case LinkTypeVideo: // 视频
if platform == PlatformAndroid {
uri = SchemeVideoAndroid + val
} else {
uri = SchemeVideoIOS + val
}
case LinkTypeLive:
var (
param string
parts = strings.Split(val, ",") // 值可能为 1 或者 1,0
)
if len(parts) == 2 {
param = "?broadcast_type=" + parts[1]
}
uri = SchemeLive + parts[0] + param
if platform == PlatformAndroid && build < 5290000 {
uri = SchemeLiveAndroid + parts[0]
}
case LinkTypeSplist: // 专题
uri = SchemeSplist + val
case LinkTypeAuthor: // 个人空间
if platform == PlatformAndroid {
uri = SchemeAuthorAndroid + val
} else {
uri = SchemeAuthorIOS + val
}
case LinkTypeSearch: // 搜索
if platform == PlatformAndroid {
uri = SchemeSearchAndroid + val
} else {
uri = SchemeSearchIOS + val
}
case LinkTypeBrowser: // H5
if platform == PlatformAndroid {
uri = SchemeBrowserAndroid + url.QueryEscape(val)
} else {
// 容错逻辑,标准写法是 SchemeBrowserIOS + val且 val 需要业务方进行 urlencode
// 但是老客户端有bug客户端会强制encode客户端从 5.28 开始修了这个bug
// 版本覆盖完全后,可改成标准写法
uri = val
}
case LinkTypeVipBuy:
uri = SchemeVipBuy + val
case LinkTypeCustom:
uri = val
default:
uri = ""
}
return
}
// ParseBuild parses string to build struct.
func ParseBuild(s string) (builds map[int]*Build) {
builds = make(map[int]*Build)
if s == "" {
return
}
temp := make(map[string]*Build)
if err := json.Unmarshal([]byte(s), &temp); err != nil {
log.Error("json.Unmarshal(%s) error(%v)", s, err)
return
}
for plat, build := range temp {
p, _ := strconv.Atoi(plat)
builds[p] = build
}
return
}
// TempTaskID gen temporary task ID.
func TempTaskID() string {
n := time.Now().UnixNano()
m := md5.Sum([]byte(strconv.FormatInt(n, 10)))
return TempTaskPrefix + fmt.Sprintf("%x", m)[:8] // 要把taskid当作jobkey参数jobkey要求长度最多9位, 1位prefix+8位时间hash值前段
}
// JobName gen job name.
func JobName(timestamp int64, content, linkValue, group string) int64 {
s := []byte(fmt.Sprintf("%d%s%s%s%s", timestamp, time.Now().Format("20060102"), content, linkValue, group))
return int64(farm.Hash64(s) % math.MaxInt64)
}
// Hash gen hash value by solt.
func Hash(salt string) string {
s := salt + strconv.FormatInt(time.Now().UnixNano(), 10)
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}
// 免打扰时间默认值
const (
_defaultSilentBeginHour = 22
_defaultSilentEndHour = 8
_defaultSilentBeginMinute = 0
_defaultSilentEndMinute = 0
)
// ParseSilentTime .
func ParseSilentTime(s string) (st BusinessSilentTime) {
st = BusinessSilentTime{
BeginHour: _defaultSilentBeginHour,
EndHour: _defaultSilentEndHour,
BeginMinute: _defaultSilentBeginMinute,
EndMinute: _defaultSilentEndMinute,
}
s = strings.Trim(s, " ")
if s == "" {
return
}
r := strings.Split(s, "-")
if len(r) != 2 {
return
}
begin := strings.Split(r[0], ":")
if len(begin) == 2 {
st.BeginHour, _ = strconv.Atoi(begin[0])
st.BeginMinute, _ = strconv.Atoi(begin[1])
}
end := strings.Split(r[1], ":")
if len(end) == 2 {
st.EndHour, _ = strconv.Atoi(end[0])
st.EndMinute, _ = strconv.Atoi(end[1])
}
return st
}
// IsAndroid .
func IsAndroid(platformID int) bool {
m := map[int]bool{
PlatformIPhone: true,
PlatformIPad: true,
}
return !m[platformID]
}
// ValidateBuild checks token&platform valid.
func ValidateBuild(platform, build int, builds map[int]*Build) bool {
if len(builds) == 0 {
return true
}
if IsAndroid(platform) {
platform = PlatformAndroid
}
if builds[platform] == nil {
return true
}
c := builds[platform].Condition
b := builds[platform].Build
switch c {
case "gt":
return build > b
case "gte":
return build >= b
case "lt":
return build < b
case "lte":
return build <= b
case "eq":
return build == b
case "ne":
return build != b
}
return false
}

View File

@@ -0,0 +1,191 @@
package model
import (
xtime "go-common/library/time"
)
// Report APP report info.
type Report struct {
ID int64 `json:"id"`
APPID int64 `json:"app_id"` // application
PlatformID int `json:"platform_id"`
Mid int64 `json:"mid"`
Buvid string `json:"buvid"`
Build int `json:"build"`
TimeZone int `json:"time_zone"`
NotifySwitch int `json:"notify_switch"` // global notification switch
DeviceToken string `json:"device_token"`
DeviceBrand string `json:"device_brand"`
DeviceModel string `json:"device_model"`
OSVersion string `json:"os_version"`
Extra string `json:"extra"`
Dtime int64 `json:"dtime"`
}
// Task push task info.
type Task struct {
ID string `json:"id"` // task id
Job int64 `json:"job"` // 多个子任务拥有同一个 job name
Type int `json:"type"` // 任务类型 1:后台全量 2:后台批量 3:业务推送
APPID int64 `json:"app_id"`
BusinessID int64 `json:"business_id"`
PlatformID int `json:"platform_id"`
Platform []int `json:"platform"`
Title string `json:"title"`
Summary string `json:"summary"`
LinkType int8 `json:"link_type"`
LinkValue string `json:"link_value"`
Build map[int]*Build `json:"build"`
Sound int `json:"sound"`
Vibration int `json:"vibration"`
PassThrough int `json:"pass_through"`
MidFile string `json:"mid_file"`
Progress *Progress `json:"progress"`
PushTime xtime.Time `json:"push_time"`
ExpireTime xtime.Time `json:"expire_time"`
Status int8 `json:"status"`
Group string `json:"group"`
ImageURL string `json:"image_url"`
Extra *TaskExtra `json:"extra"`
}
// TaskExtra task extra.
type TaskExtra struct {
Group string `json:"group"`
Filename string `json:"filename,omitempty"` // 任务文件的名称(前端展示用)
}
// Build version limit.
type Build struct {
Build int `json:"build"`
Condition string `json:"condition"`
}
// Progress task push progress.
type Progress struct {
// total indicators
Status int8 `json:"st"` // 任务状态
MidTotal int64 `json:"mid_total"` // 任务接收到的mid总数
MidValid int64 `json:"mid_valid"` // 能查到token的mid数
MidMissed int64 `json:"mm"` // mid_missed 查不到token的mid数
MidMissedSuccess int64 `json:"mms"` // mid_missed_success 无效mid补偿推送成功的
MidMissedFailed int64 `json:"mmf"` // mid_missed_failed 无效mid补偿推送失败的
TokenTotal int64 `json:"token_total"` // 一共要推送的token数
TokenValid int64 `json:"token_valid"` // 有效token
TokenDelay int64 `json:"token_delay"` // 延迟推送的token
TokenSuccess int64 `json:"token_success"` // 推送成功的
TokenFailed int64 `json:"token_failed"` // 推送失败的
// brand indicators
Brands map[int]int64 `json:"brands"` // 各品牌统计数据
// server indicators
RetryTimes int64 `json:"retry"` // 重试次数
BeginTime xtime.Time `json:"btime"` // 开始时间
PushTime xtime.Time `json:"ptime"` // 开始推送时间
EndTime xtime.Time `json:"etime"` // 结束时间
}
// APP appication
type APP struct {
ID int64
Name string
PushLimitUser int
}
// Business business
type Business struct {
ID int64 `json:"id"`
APPID int64 `json:"app_id"`
Name string `json:"name"`
Desc string `json:"desc"`
Token string `json:"token"`
Sound int `json:"sound"`
Vibration int `json:"vibration"`
ReceiveSwitch int `json:"receive_switch"`
PushSwitch int `json:"push_switch"`
SilentTime BusinessSilentTime `json:"silent_time"`
PushLimitUser int `json:"push_limit_user"`
Whitelist int `json:"whitelist"`
}
// BusinessSilentTime .
type BusinessSilentTime struct {
BeginHour, EndHour int
BeginMinute, EndMinute int
}
// PushInfo push message.
type PushInfo struct {
Job int64
TaskID string
APPID int64
Title string
Summary string
LinkType int8
LinkValue string
PushTime xtime.Time
ExpireTime xtime.Time
PassThrough int
Sound int
Vibration int
ImageURL string
}
// PushItem push item.
type PushItem struct {
Platform int
Token string
Mid int64
Sound int
Vibration int
Build int
}
// PushChanItem push channel item.
type PushChanItem struct {
Info *PushInfo
Item *PushItem
}
// PushChanItems push channel item.
type PushChanItems struct {
Info *PushInfo
Items []*PushItem
}
// Auth cert or auth info.
type Auth struct {
APPID int64
PlatformID int
Name string // 第三方名称 for android例如 小米
Key string // android的包名 或 iOS的 cert key
Value string // android的 auth 或 iOS的 cert value
BundleID string // just for iOS
}
// HTTPResponse http response.
type HTTPResponse struct {
Code int
Msg string
}
// Callback push callback.
type Callback struct {
Task string
APP int64
Platform int
Mid int64
Pid int // mobi_app ID
Token string
Buvid string
Click uint8 // 是否被点击
Brand int
Extra *CallbackExtra
}
// CallbackExtra .
type CallbackExtra struct {
Status int `json:"st"`
Channel int `json:"chan"`
}

View File

@@ -0,0 +1,102 @@
package model
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestFuncs(t *testing.T) {
Convey("int string functions", t, func() {
Convey("SplitInts", func() {
res := SplitInts("1,2,3")
So(res, ShouldResemble, []int{1, 2, 3})
})
Convey("JoinInts", func() {
ints := []int{1, 2, 3}
res := JoinInts(ints)
So(res, ShouldEqual, "1,2,3")
})
Convey("existsInt", func() {
exists := ExistsInt([]int{}, 4)
So(exists, ShouldBeFalse)
ints := []int{1, 2, 3}
exists = ExistsInt(ints, 1)
So(exists, ShouldBeTrue)
exists = ExistsInt(ints, 4)
So(exists, ShouldBeFalse)
})
Convey("gen temp task id", func() {
id := TempTaskID()
So(len(id), ShouldEqual, 9)
})
Convey("gen job name", func() {
name := JobName(time.Now().UnixNano(), "123", "456", "g")
t.Logf("job name is: %d", name)
})
})
Convey("ParseBuild", t, func() {
buildString := `{"2":{"Build":100,"Condition":"gt"}}`
build := ParseBuild(buildString)
So(build, ShouldResemble, map[int]*Build{2: {Build: 100, Condition: "gt"}})
})
Convey("platform", t, func() {
plat := Platform("iphone", PushSDKApns)
So(plat, ShouldEqual, PlatformIPhone)
plat = Platform("ipad", PushSDKApns)
So(plat, ShouldEqual, PlatformIPad)
plat = Platform("whatever", PushSDKXiaomi)
So(plat, ShouldEqual, PlatformXiaomi)
})
Convey("parse silent time", t, func() {
st := ParseSilentTime("22:30-06:00")
So(st, ShouldResemble, BusinessSilentTime{
BeginHour: 22,
EndHour: 6,
BeginMinute: 30,
EndMinute: 0,
})
})
}
func TestValidateBuild(t *testing.T) {
builds := map[int]*Build{
1: {Build: 520000, Condition: "eq"},
2: {Build: 123456, Condition: "gt"},
}
Convey("ValidateBuild", t, func() {
b := ValidateBuild(2, 123455, builds)
So(b, ShouldBeFalse)
b = ValidateBuild(2, 123457, builds)
So(b, ShouldBeTrue)
b = ValidateBuild(4, 520001, builds)
So(b, ShouldBeFalse)
b = ValidateBuild(4, 519999, builds)
So(b, ShouldBeFalse)
b = ValidateBuild(4, 520000, builds)
So(b, ShouldBeTrue)
})
}
func TestScheme(t *testing.T) {
Convey("Scheme()", t, func() {
scheme := Scheme(LinkTypeLive, "1,0", PlatformAndroid, 5300000)
So(scheme, ShouldEqual, "bilibili://live/1?broadcast_type=0")
scheme = Scheme(LinkTypeLive, "1", PlatformAndroid, 5280000)
So(scheme, ShouldEqual, "bili:///?type=bililive&roomid=1")
scheme = Scheme(LinkTypeLive, "1,1", PlatformIPhone, 5300000)
So(scheme, ShouldEqual, "bilibili://live/1?broadcast_type=1")
scheme = Scheme(LinkTypeLive, "1,0", PlatformIPhone, 5280000)
So(scheme, ShouldEqual, "bilibili://live/1?broadcast_type=0")
scheme = Scheme(LinkTypeCustom, "custom_scheme", PlatformIPhone, 68)
So(scheme, ShouldEqual, "custom_scheme")
})
}

View File

@@ -0,0 +1,76 @@
package model
import "strings"
// PushSDK* for parameter 'push_sdk' in http report API.
const (
// PushSDKApns apns sdk.
PushSDKApns = 1
// PushSDKXiaomi mipush sdk.
PushSDKXiaomi = 2
// PushSDKHuawei huawei sdk.
PushSDKHuawei = 3
// PushSDKOppo oppo sdk.
PushSDKOppo = 5
// PushSDKJpush jpush sdk.
PushSDKJpush = 6
// PushSDKFCM fcm sdk
PushSDKFCM = 7
)
const (
// PlatformUnknown unknown.
PlatformUnknown = 0
// PlatformAndroid Android.
PlatformAndroid = 1
// PlatformIPhone iPhone.
PlatformIPhone = 2
// PlatformIPad iPad.
PlatformIPad = 3
// PlatformXiaomi mipush.
PlatformXiaomi = 4
// PlatformHuawei huawei.
PlatformHuawei = 5
// PlatformOppo oppo.
PlatformOppo = 8
// PlatformJpush jpush.
PlatformJpush = 9
// PlatformFCM fcm
PlatformFCM = 10
)
// Platforms all platform
var Platforms = []int{
PlatformIPhone,
PlatformIPad,
PlatformXiaomi,
PlatformHuawei,
PlatformOppo,
PlatformJpush,
PlatformFCM,
}
// Platform gets real platform.
func Platform(platform string, pushSDK int) int {
switch pushSDK {
case PushSDKApns:
platform = strings.ToLower(platform)
if strings.HasPrefix(platform, "iphone") {
return PlatformIPhone
} else if strings.HasPrefix(platform, "ipad") {
return PlatformIPad
}
case PushSDKXiaomi:
return PlatformXiaomi
case PushSDKHuawei:
return PlatformHuawei
case PushSDKOppo:
return PlatformOppo
case PushSDKJpush:
return PlatformJpush
case PushSDKFCM:
return PlatformFCM
}
// TODO add more brands
return PlatformUnknown
}

View File

@@ -0,0 +1,84 @@
package model
// ArgReport .
type ArgReport struct {
ID int64
APPID int64
PlatformID int
Mid int64
Buvid string
DeviceToken string
Build int
TimeZone int
NotifySwitch int
DeviceBrand string
DeviceModel string
OSVersion string
Extra string
RealIP string
}
// ArgReports .
type ArgReports struct {
Reports []*Report
}
// ArgUserReports .
type ArgUserReports struct {
Mid int64
Reports []*Report
}
// ArgToken .
type ArgToken struct {
Token string
RealIP string
}
// ArgMidToken .
type ArgMidToken struct {
Mid int64
Token string
RealIP string
}
// ArgDelInvalidReport .
type ArgDelInvalidReport struct {
Type int
RealIP string
}
// ArgMid .
type ArgMid struct {
Mid int64
RealIP string
}
// ArgSetting .
type ArgSetting struct {
Mid int64
Type int
Value int
RealIP string
}
// ArgCallback .
type ArgCallback struct {
Task string
APP int64
Platform int
Mid int64
Pid int // mobi_app ID
Token string
Buvid string
Click uint8 // 是否被点击
Extra *CallbackExtra
}
// ArgMidProgress .
type ArgMidProgress struct {
Task string
MidTotal int64
MidValid int64
RealIP string
}

View File

@@ -0,0 +1,14 @@
package model
const (
// UserSettingArchive up主新投稿提醒
UserSettingArchive = 1
// UserSettingLive 主播开播提醒
UserSettingLive = 2
)
// Settings .
var Settings = map[int]int{
UserSettingArchive: SwitchOn,
UserSettingLive: SwitchOn,
}

View File

@@ -0,0 +1,48 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["server_test.go"],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = [
"//app/service/main/push/api/gorpc: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 = ["server.go"],
importpath = "go-common/app/service/main/push/server/gorpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push/conf:go_default_library",
"//app/service/main/push/model:go_default_library",
"//app/service/main/push/service:go_default_library",
"//library/net/rpc:go_default_library",
"//library/net/rpc/context:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,158 @@
package gorpc
import (
"go-common/app/service/main/push/conf"
"go-common/app/service/main/push/model"
"go-common/app/service/main/push/service"
"go-common/library/net/rpc"
"go-common/library/net/rpc/context"
)
// RPC rpc.
type RPC struct {
s *service.Service
}
// New .
func New(c *conf.Config, s *service.Service) (svc *rpc.Server) {
r := &RPC{s: s}
svc = rpc.NewServer(c.RPCServer)
if err := svc.Register(r); err != nil {
panic(err)
}
return
}
// Ping checks connection success.
func (r *RPC) Ping(c context.Context, arg *struct{}, res *struct{}) (err error) {
return
}
// Auth check connection success.
func (r *RPC) Auth(c context.Context, arg *rpc.Auth, res *struct{}) (err error) {
return
}
// AddReport adds report by mid.
func (r *RPC) AddReport(c context.Context, arg *model.ArgReport, res *struct{}) (err error) {
report := &model.Report{
APPID: arg.APPID,
PlatformID: arg.PlatformID,
Mid: arg.Mid,
Buvid: arg.Buvid,
DeviceToken: arg.DeviceToken,
Build: arg.Build,
TimeZone: arg.TimeZone,
NotifySwitch: arg.NotifySwitch,
DeviceBrand: arg.DeviceBrand,
DeviceModel: arg.DeviceModel,
OSVersion: arg.OSVersion,
Extra: arg.Extra,
}
err = r.s.AddReport(c, report)
return
}
// DelInvalidReports deletes invalid reports.
func (r *RPC) DelInvalidReports(c context.Context, arg *model.ArgDelInvalidReport, res *struct{}) (err error) {
err = r.s.DelInvalidReports(c, arg.Type)
return
}
// DelReport deletes report.
func (r *RPC) DelReport(c context.Context, arg *model.ArgReport, res *struct{}) (err error) {
err = r.s.DelReport(c, arg.APPID, arg.Mid, arg.DeviceToken)
return
}
// AddCallback adds callback data.
func (r *RPC) AddCallback(c context.Context, arg *model.ArgCallback, res *struct{}) (err error) {
cb := &model.Callback{
Task: arg.Task,
APP: arg.APP,
Platform: arg.Platform,
Mid: arg.Mid,
Pid: arg.Pid,
Token: arg.Token,
Buvid: arg.Buvid,
Click: arg.Click,
Extra: arg.Extra,
}
err = r.s.AddCallback(c, cb)
return
}
// AddReportCache adds report cache.
func (r *RPC) AddReportCache(c context.Context, arg *model.ArgReport, res *struct{}) (err error) {
report := &model.Report{
ID: arg.ID,
APPID: arg.APPID,
PlatformID: arg.PlatformID,
Mid: arg.Mid,
Buvid: arg.Buvid,
DeviceToken: arg.DeviceToken,
Build: arg.Build,
TimeZone: arg.TimeZone,
NotifySwitch: arg.NotifySwitch,
DeviceBrand: arg.DeviceBrand,
DeviceModel: arg.DeviceModel,
OSVersion: arg.OSVersion,
Extra: arg.Extra,
}
err = r.s.AddReportCache(c, report)
return
}
// AddUserReportCache adds user report cache.
func (r *RPC) AddUserReportCache(c context.Context, arg *model.ArgUserReports, res *struct{}) (err error) {
err = r.s.AddUserReportCache(c, arg.Mid, arg.Reports)
return
}
// Setting gets user push switch setting.
func (r *RPC) Setting(c context.Context, arg *model.ArgMid, res *map[int]int) (err error) {
*res, err = r.s.Setting(c, arg.Mid)
return
}
// SetSetting sets user push switch setting.
func (r *RPC) SetSetting(c context.Context, arg *model.ArgSetting, res *struct{}) (err error) {
err = r.s.SetSetting(c, arg.Mid, arg.Type, arg.Value)
return
}
// AddMidProgress add mid count number to task progress field
func (r *RPC) AddMidProgress(c context.Context, arg *model.ArgMidProgress, res *struct{}) (err error) {
err = r.s.AddMidProgress(c, arg.Task, arg.MidTotal, arg.MidValid)
return
}
// AddTokenCache add token cache
func (r *RPC) AddTokenCache(ctx context.Context, arg *model.ArgReport, res *struct{}) (err error) {
report := &model.Report{
APPID: arg.APPID,
PlatformID: arg.PlatformID,
Mid: arg.Mid,
Buvid: arg.Buvid,
DeviceToken: arg.DeviceToken,
Build: arg.Build,
TimeZone: arg.TimeZone,
NotifySwitch: arg.NotifySwitch,
DeviceBrand: arg.DeviceBrand,
DeviceModel: arg.DeviceModel,
OSVersion: arg.OSVersion,
Extra: arg.Extra,
}
err = r.s.AddTokenCache(ctx, report)
return
}
// AddTokensCache add token cache
func (r *RPC) AddTokensCache(ctx context.Context, arg *model.ArgReports, res *struct{}) (err error) {
rs := make(map[string]*model.Report, len(arg.Reports))
for _, v := range arg.Reports {
rs[v.DeviceToken] = v
}
err = r.s.AddTokensCache(ctx, rs)
return
}

View File

@@ -0,0 +1,92 @@
package gorpc
import (
"context"
"testing"
pushsrv "go-common/app/service/main/push/api/gorpc"
"go-common/app/service/main/push/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
// _noArg = &struct{}{}
// _noRes = &struct{}{}
ctx = context.TODO()
)
func WithRPC(f func(client *pushsrv.Service)) func() {
return func() {
client := pushsrv.New(nil)
f(client)
}
}
func Test_AddReport(t *testing.T) {
Convey("AddReport", t, WithRPC(func(client *pushsrv.Service) {
arg := &model.ArgReport{
APPID: 1,
PlatformID: 1,
Mid: 1,
Buvid: "b",
DeviceToken: "d",
Build: 8080,
TimeZone: 8,
NotifySwitch: 1,
}
err := client.AddReport(ctx, arg)
So(err, ShouldBeNil)
}))
}
func Test_Setting(t *testing.T) {
Convey("get setting", t, WithRPC(func(client *pushsrv.Service) {
arg := &model.ArgMid{Mid: 88888888}
res, err := client.Setting(ctx, arg)
So(err, ShouldBeNil)
t.Logf("setting(%v)", res)
}))
Convey("set setting", t, WithRPC(func(client *pushsrv.Service) {
arg := &model.ArgSetting{Mid: 999999999, Type: model.UserSettingArchive, Value: model.SwitchOff}
err := client.SetSetting(ctx, arg)
So(err, ShouldBeNil)
argMid := &model.ArgMid{Mid: 999999999}
res, err := client.Setting(ctx, argMid)
So(err, ShouldBeNil)
t.Logf("setting(%v)", res)
}))
}
func TestAddUserReportCache(t *testing.T) {
Convey("AddUserReportCache", t, WithRPC(func(client *pushsrv.Service) {
arg := &model.ArgUserReports{Mid: 123456, Reports: []*model.Report{{
APPID: 1,
PlatformID: 1,
Mid: 123456,
DeviceToken: "dtrpc",
}}}
err := client.AddUserReportCache(context.Background(), arg)
So(err, ShouldBeNil)
}))
}
func TestAddTokensCache(t *testing.T) {
Convey("AddTokensCache", t, WithRPC(func(client *pushsrv.Service) {
arg := &model.ArgReports{Reports: []*model.Report{{
APPID: 1,
PlatformID: 1,
Mid: 123456,
DeviceToken: "dtrpc",
}, {
APPID: 1,
PlatformID: 1,
Mid: 123456,
DeviceToken: "dtrpc2",
}}}
err := client.AddTokensCache(context.Background(), arg)
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,34 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["server.go"],
importpath = "go-common/app/service/main/push/server/grpc",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/service/main/push/api/grpc/v1:go_default_library",
"//app/service/main/push/model:go_default_library",
"//app/service/main/push/service:go_default_library",
"//library/net/rpc/warden:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,213 @@
package grpc
import (
"context"
pb "go-common/app/service/main/push/api/grpc/v1"
"go-common/app/service/main/push/model"
"go-common/app/service/main/push/service"
"go-common/library/net/rpc/warden"
)
// New warden rpc server
func New(c *warden.ServerConfig, svc *service.Service) *warden.Server {
svr := warden.NewServer(c)
pb.RegisterPushServer(svr.Server(), &server{svc: svc})
ws, err := svr.Start()
if err != nil {
panic(err)
}
return ws
}
type server struct {
svc *service.Service
}
// AddReport 上报
func (s *server) AddReport(ctx context.Context, req *pb.AddReportRequest) (reply *pb.AddReportReply, err error) {
reply = new(pb.AddReportReply)
if req.Report == nil {
return
}
r := req.Report
err = s.svc.AddReport(ctx, &model.Report{
APPID: int64(r.APPID),
PlatformID: int(r.PlatformID),
Mid: r.Mid,
Buvid: r.Buvid,
DeviceToken: r.DeviceToken,
Build: int(r.Build),
TimeZone: int(r.TimeZone),
NotifySwitch: int(r.NotifySwitch),
DeviceBrand: r.DeviceBrand,
DeviceModel: r.DeviceModel,
OSVersion: r.OSVersion,
Extra: r.Extra,
})
return
}
// DelReport 删除上报
func (s *server) DelReport(ctx context.Context, req *pb.DelReportRequest) (reply *pb.DelReportReply, err error) {
reply = new(pb.DelReportReply)
err = s.svc.DelReport(ctx, int64(req.APPID), req.Mid, req.DeviceToken)
return
}
// DelInvalidReports 删除无效token
func (s *server) DelInvalidReports(ctx context.Context, req *pb.DelInvalidReportsRequest) (reply *pb.DelInvalidReportsReply, err error) {
reply = new(pb.DelInvalidReportsReply)
err = s.svc.DelInvalidReports(ctx, int(req.Type))
return
}
// AddReportCache 上报缓存
func (s *server) AddReportCache(ctx context.Context, req *pb.AddReportCacheRequest) (reply *pb.AddReportCacheReply, err error) {
reply = new(pb.AddReportCacheReply)
if req.Report == nil {
return
}
r := req.Report
err = s.svc.AddReportCache(ctx, &model.Report{
APPID: int64(r.APPID),
PlatformID: int(r.PlatformID),
Mid: r.Mid,
Buvid: r.Buvid,
DeviceToken: r.DeviceToken,
Build: int(r.Build),
TimeZone: int(r.TimeZone),
NotifySwitch: int(r.NotifySwitch),
DeviceBrand: r.DeviceBrand,
DeviceModel: r.DeviceModel,
OSVersion: r.OSVersion,
Extra: r.Extra,
})
return
}
// AddUserReportCache 用户的上报缓存
func (s *server) AddUserReportCache(ctx context.Context, req *pb.AddUserReportCacheRequest) (reply *pb.AddUserReportCacheReply, err error) {
reply = new(pb.AddUserReportCacheReply)
var reports []*model.Report
for _, r := range req.Reports {
reports = append(reports, &model.Report{
APPID: int64(r.APPID),
PlatformID: int(r.PlatformID),
Mid: r.Mid,
Buvid: r.Buvid,
DeviceToken: r.DeviceToken,
Build: int(r.Build),
TimeZone: int(r.TimeZone),
NotifySwitch: int(r.NotifySwitch),
DeviceBrand: r.DeviceBrand,
DeviceModel: r.DeviceModel,
OSVersion: r.OSVersion,
Extra: r.Extra,
})
}
err = s.svc.AddUserReportCache(ctx, req.Mid, reports)
return
}
// AddTokenCache token缓存
func (s *server) AddTokenCache(ctx context.Context, req *pb.AddTokenCacheRequest) (reply *pb.AddTokenCacheReply, err error) {
reply = new(pb.AddTokenCacheReply)
if req.Report == nil {
return
}
r := req.Report
err = s.svc.AddTokenCache(ctx, &model.Report{
APPID: int64(r.APPID),
PlatformID: int(r.PlatformID),
Mid: r.Mid,
Buvid: r.Buvid,
DeviceToken: r.DeviceToken,
Build: int(r.Build),
TimeZone: int(r.TimeZone),
NotifySwitch: int(r.NotifySwitch),
DeviceBrand: r.DeviceBrand,
DeviceModel: r.DeviceModel,
OSVersion: r.OSVersion,
Extra: r.Extra,
})
return
}
// AddTokensCache 批量token缓存
func (s *server) AddTokensCache(ctx context.Context, req *pb.AddTokensCacheRequest) (reply *pb.AddTokensCacheReply, err error) {
reply = new(pb.AddTokensCacheReply)
if len(req.Reports) == 0 {
return
}
reports := make(map[string]*model.Report, len(req.Reports))
for _, v := range req.Reports {
reports[v.DeviceToken] = &model.Report{
APPID: int64(v.APPID),
PlatformID: int(v.PlatformID),
Mid: v.Mid,
Buvid: v.Buvid,
DeviceToken: v.DeviceToken,
Build: int(v.Build),
TimeZone: int(v.TimeZone),
NotifySwitch: int(v.NotifySwitch),
DeviceBrand: v.DeviceBrand,
DeviceModel: v.DeviceModel,
OSVersion: v.OSVersion,
Extra: v.Extra,
}
}
err = s.svc.AddTokensCache(ctx, reports)
return
}
// AddCallback 回执
func (s *server) AddCallback(ctx context.Context, req *pb.AddCallbackRequest) (reply *pb.AddCallbackReply, err error) {
reply = new(pb.AddCallbackReply)
cb := &model.Callback{
Task: req.Task,
APP: req.APP,
Platform: int(req.Platform),
Mid: int64(req.Mid),
Pid: int(req.Pid),
Token: req.Token,
Buvid: req.Buvid,
Click: uint8(req.Click),
}
if req.Extra != nil {
cb.Extra = &model.CallbackExtra{
Status: int(req.Extra.Status),
Channel: int(req.Extra.Channel),
}
}
err = s.svc.AddCallback(ctx, cb)
return
}
// AddMidProgress 记录任务中mid数
func (s *server) AddMidProgress(ctx context.Context, req *pb.AddMidProgressRequest) (reply *pb.AddMidProgressReply, err error) {
reply = new(pb.AddMidProgressReply)
err = s.svc.AddMidProgress(ctx, req.Task, req.MidTotal, req.MidValid)
return
}
// Setting 获取用户业务开关配置
func (s *server) Setting(ctx context.Context, req *pb.SettingRequest) (reply *pb.SettingReply, err error) {
reply = new(pb.SettingReply)
res, err := s.svc.Setting(ctx, req.Mid)
if err != nil || res == nil {
return
}
reply.Settings = make(map[int32]int32, len(res))
for k, v := range res {
reply.Settings[int32(k)] = int32(v)
}
return
}
// SetSetting 设置用户业务开关
func (s *server) SetSetting(ctx context.Context, req *pb.SetSettingRequest) (reply *pb.SetSettingReply, err error) {
reply = new(pb.SetSettingReply)
err = s.svc.SetSetting(ctx, req.Mid, int(req.Type), int(req.Value))
return
}

View File

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

View File

@@ -0,0 +1,51 @@
package http
import (
"net/http"
"go-common/app/service/main/push/conf"
"go-common/app/service/main/push/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
pushSrv *service.Service
idfSrv *verify.Verify
)
// Init init http.
func Init(c *conf.Config, srv *service.Service) {
idfSrv = verify.New(c.Verify)
pushSrv = srv
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-service", bm.CORS())
{
g.POST("/single", idfSrv.Verify, singlePush)
// for 直播
g.POST("/setting/set", idfSrv.Verify, setSettingInternal)
// for 管理后台测试推送
g.POST("/push", idfSrv.Verify, push)
// for test
g.POST("/test/token", idfSrv.Verify, testToken)
// upload image
g.POST("/upimg", upimg)
}
}
func ping(c *bm.Context) {
if err := pushSrv.Ping(c); err != nil {
log.Error("push-service ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,283 @@
package http
import (
"context"
"strconv"
"time"
"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"
"go-common/library/xstr"
)
func push(c *bm.Context) {
params := c.Request.Form
appID, _ := strconv.ParseInt(params.Get("app_id"), 10, 64)
if appID < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("app_id is wrong: %s", params.Get("app_id"))
return
}
platform := params.Get("platform")
alertTitle := params.Get("alert_title")
if alertTitle != "" {
res, err := pushSrv.Filter(c, alertTitle)
if err == nil && res != alertTitle {
log.Error("alertTitle(%s) contains invalid content", alertTitle)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
}
alertBody := params.Get("alert_body")
if alertBody == "" {
c.JSON(nil, ecode.RequestErr)
log.Error("alert_body is empty")
return
}
res, err := pushSrv.Filter(c, alertBody)
if err == nil && res != alertBody {
log.Error("alertBody(%s) contains invalid content", alertBody)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
linkType, _ := strconv.Atoi(params.Get("link_type"))
if linkType < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("link_type is wrong: %s", params.Get("link_type"))
return
}
linkValue := params.Get("link_value")
expireTime, _ := strconv.ParseInt(params.Get("expire_time"), 10, 64)
if expireTime == 0 {
expireTime = time.Now().Add(7 * 24 * time.Hour).Unix()
}
builds := params.Get("builds")
sound, vibration := model.SwitchOn, model.SwitchOn
if params.Get("sound") != "" {
if sd, _ := strconv.Atoi(params.Get("sound")); sd == model.SwitchOff {
sound = model.SwitchOff
}
}
if params.Get("vibration") != "" {
if vr, _ := strconv.Atoi(params.Get("vibration")); vr == model.SwitchOff {
vibration = model.SwitchOff
}
}
passThrough, _ := strconv.Atoi(params.Get("pass_through"))
if passThrough != model.SwitchOn {
passThrough = model.SwitchOff
}
mid := params.Get("mid")
if mid == "" {
log.Error("mid is empty", mid)
c.JSON(nil, ecode.RequestErr)
return
}
mids, err := xstr.SplitInts(mid)
if err != nil {
log.Error("parse mid(%s) error(%v)", mid, err)
c.JSON(nil, ecode.RequestErr)
return
}
task := &model.Task{
ID: model.TempTaskID(),
Job: model.JobName(time.Now().UnixNano(), alertBody, linkValue, ""),
APPID: appID,
Platform: model.SplitInts(platform),
Title: alertTitle,
Summary: alertBody,
LinkType: int8(linkType),
LinkValue: linkValue,
PushTime: xtime.Time(time.Now().Unix()),
ExpireTime: xtime.Time(expireTime),
Build: model.ParseBuild(builds),
Sound: sound,
Vibration: vibration,
PassThrough: passThrough,
Group: params.Get("group"),
ImageURL: params.Get("image_url"),
}
go pushSrv.Pushs(context.Background(), task, mids)
c.JSON(nil, nil)
}
func singlePush(c *bm.Context) {
params := c.Request.Form
appID, _ := strconv.ParseInt(params.Get("app_id"), 10, 64)
if appID < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("app_id is wrong: %s", params.Get("app_id"))
return
}
businessID, _ := strconv.ParseInt(params.Get("business_id"), 10, 64)
if businessID < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("business_id is wrong: %s", params.Get("business_id"))
return
}
token := params.Get("token")
if token == "" {
c.JSON(nil, ecode.RequestErr)
log.Error("token is empty")
return
}
platform := params.Get("platform")
alertTitle := params.Get("alert_title")
if alertTitle != "" {
res, err := pushSrv.Filter(c, alertTitle)
if err == nil && res != alertTitle {
log.Error("alertTitle(%s) contains invalid content", alertTitle)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
}
alertBody := params.Get("alert_body")
if alertBody == "" {
c.JSON(nil, ecode.RequestErr)
log.Error("alert_body is empty")
return
}
res, err := pushSrv.Filter(c, alertBody)
if err == nil && res != alertBody {
log.Error("alertBody(%s) contains invalid content", alertBody)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
linkType, _ := strconv.Atoi(params.Get("link_type"))
if linkType < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("link_type is wrong: %s", params.Get("link_type"))
return
}
linkValue := params.Get("link_value")
expireTime, _ := strconv.ParseInt(params.Get("expire_time"), 10, 64)
if expireTime == 0 {
expireTime = time.Now().Add(7 * 24 * time.Hour).Unix()
}
builds := params.Get("builds")
sound, vibration := model.SwitchOn, model.SwitchOn
if params.Get("sound") != "" {
if sd, _ := strconv.Atoi(params.Get("sound")); sd == model.SwitchOff {
sound = model.SwitchOff
}
}
if params.Get("vibration") != "" {
if vr, _ := strconv.Atoi(params.Get("vibration")); vr == model.SwitchOff {
vibration = model.SwitchOff
}
}
passThrough, _ := strconv.Atoi(params.Get("pass_through"))
if passThrough != model.SwitchOn {
passThrough = model.SwitchOff
}
mid, _ := strconv.ParseInt(params.Get("mid"), 10, 64)
if mid < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("mid is wrong (%d)", mid)
return
}
task := &model.Task{
ID: model.TempTaskID(),
Job: model.JobName(time.Now().UnixNano(), alertBody, linkValue, ""),
BusinessID: businessID,
APPID: appID,
Platform: model.SplitInts(platform),
Title: alertTitle,
Summary: alertBody,
LinkType: int8(linkType),
LinkValue: linkValue,
PushTime: xtime.Time(time.Now().Unix()),
ExpireTime: xtime.Time(expireTime),
Build: model.ParseBuild(builds),
Sound: sound,
Vibration: vibration,
PassThrough: passThrough,
Group: params.Get("group"),
ImageURL: params.Get("image_url"),
}
go pushSrv.SinglePush(context.Background(), token, task, mid)
c.JSON(nil, nil)
}
func testToken(c *bm.Context) {
params := c.Request.Form
appID, _ := strconv.ParseInt(params.Get("app_id"), 10, 64)
if appID < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("app_id is wrong: %s", params.Get("app_id"))
return
}
alertTitle := params.Get("alert_title")
if alertTitle == "" {
alertTitle = model.DefaultMessageTitle
}
res, err := pushSrv.Filter(c, alertTitle)
if err == nil && res != alertTitle {
log.Error("alertTitle(%s) contains invalid content", alertTitle)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
alertBody := params.Get("alert_body")
if alertBody == "" {
c.JSON(nil, ecode.RequestErr)
log.Error("alert_body is empty")
return
}
res, err = pushSrv.Filter(c, alertBody)
if err == nil && res != alertBody {
log.Error("alertBody(%s) contains invalid content", alertBody)
c.JSON(nil, ecode.PushSensitiveWordsErr)
return
}
token := params.Get("token")
if token == "" {
c.JSON(nil, ecode.RequestErr)
log.Error("token is empty")
return
}
linkType, _ := strconv.Atoi(params.Get("link_type"))
if linkType < 1 {
c.JSON(nil, ecode.RequestErr)
log.Error("link_type is wrong: %s", params.Get("link_type"))
return
}
linkValue := params.Get("link_value")
expireTime, _ := strconv.ParseInt(params.Get("expire_time"), 10, 64)
if expireTime == 0 {
expireTime = time.Now().Add(7 * 24 * time.Hour).Unix()
}
sound, vibration := model.SwitchOn, model.SwitchOn
if params.Get("sound") != "" {
if sd, _ := strconv.Atoi(params.Get("sound")); sd == model.SwitchOff {
sound = model.SwitchOff
}
}
if params.Get("vibration") != "" {
if vr, _ := strconv.Atoi(params.Get("vibration")); vr == model.SwitchOff {
vibration = model.SwitchOff
}
}
passThrough, _ := strconv.Atoi(params.Get("pass_through"))
if passThrough != model.SwitchOn {
passThrough = model.SwitchOff
}
info := &model.PushInfo{
TaskID: model.TempTaskID(),
Job: model.JobName(time.Now().UnixNano(), alertBody, linkValue, ""),
APPID: appID,
Title: alertTitle,
Summary: alertBody,
LinkType: int8(linkType),
LinkValue: linkValue,
ExpireTime: xtime.Time(expireTime),
PassThrough: passThrough,
Sound: sound,
Vibration: vibration,
ImageURL: params.Get("image_url"),
}
go pushSrv.TestToken(context.Background(), info, token)
c.JSON(nil, nil)
}

View File

@@ -0,0 +1,38 @@
package http
import (
"strconv"
pushmdl "go-common/app/service/main/push/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
func setSettingInternal(c *bm.Context) {
var (
params = c.Request.Form
midStr = params.Get("mid")
typeStr = params.Get("type")
valStr = params.Get("value")
)
mid, _ := strconv.ParseInt(midStr, 10, 64)
if mid <= 0 {
log.Warn("mid(%s) is wrong", midStr)
c.JSON(nil, ecode.RequestErr)
return
}
typ, _ := strconv.Atoi(typeStr)
if _, ok := pushmdl.Settings[typ]; !ok {
log.Warn("type(%s) is wrong", typeStr)
c.JSON(nil, ecode.RequestErr)
return
}
val, _ := strconv.Atoi(valStr)
if val != pushmdl.SwitchOn && val != pushmdl.SwitchOff {
log.Warn("value(%s) is wrong", valStr)
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, pushSrv.SetSetting(c, mid, typ, val))
}

View File

@@ -0,0 +1,42 @@
package http
import (
"path/filepath"
"go-common/app/service/main/push/conf"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
var imgExts = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
}
func upimg(ctx *bm.Context) {
f, h, err := ctx.Request.FormFile("file")
if err != nil {
log.Error("upimg error(%v)", err)
ctx.JSON(nil, err)
return
}
defer f.Close()
if h.Size > conf.Conf.Push.UpimgMaxSize {
log.Error("filesize error name(%s) size(%d)", h.Filename, h.Size)
ctx.JSON(nil, ecode.PushServiceFileSizeErr)
return
}
if ok := imgExts[filepath.Ext(h.Filename)]; !ok {
log.Error("file ext error name(%s)", h.Filename)
ctx.JSON(nil, ecode.PushServiceFileExtErr)
return
}
url, err := pushSrv.Upimg(ctx, f)
if err != nil {
ctx.JSON(nil, err)
return
}
ctx.JSON(map[string]string{"url": url}, nil)
}

View File

@@ -0,0 +1,74 @@
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/conf:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache/redis:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"business.go",
"callback.go",
"progress.go",
"push.go",
"report.go",
"service.go",
"setting.go",
"task.go",
"upload.go",
],
importpath = "go-common/app/service/main/push/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/conf:go_default_library",
"//app/service/main/push/dao:go_default_library",
"//app/service/main/push/dao/apns2:go_default_library",
"//app/service/main/push/dao/fcm:go_default_library",
"//app/service/main/push/dao/huawei:go_default_library",
"//app/service/main/push/dao/jpush:go_default_library",
"//app/service/main/push/dao/oppo:go_default_library",
"//app/service/main/push/model:go_default_library",
"//library/cache:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/time:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,62 @@
package service
import (
"context"
"time"
"go-common/app/service/main/push/dao"
"go-common/app/service/main/push/model"
"go-common/library/ecode"
"go-common/library/log"
)
func (s *Service) loadBusinessproc() {
for {
if s.loadBusiness() != nil {
time.Sleep(100 * time.Millisecond)
continue
}
time.Sleep(time.Duration(s.c.Push.LoadBusinessInteval))
}
}
func (s *Service) loadBusiness() (err error) {
res, err := s.dao.Businesses(context.Background())
if err != nil {
log.Error("s.dao.Business() error(%v)", err)
return
}
if len(res) > 0 {
s.businesses = res
}
return
}
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 == model.SwitchOff {
log.Error("business was forbidden. business(%d) token(%s)", id, token)
dao.PromError("service:业务方被禁止推送")
return ecode.PushBizForbiddenErr
}
// 校验免打扰时间
now := time.Now()
hour := now.Hour()
minute := now.Minute()
if (hour >= b.SilentTime.BeginHour && minute >= b.SilentTime.BeginMinute) &&
(hour <= b.SilentTime.EndHour && minute <= b.SilentTime.EndMinute) {
log.Warn("in silent time, forbidden. business(%d) now(%v)", id, time.Now())
return ecode.PushSilenceErr
}
return nil
}

View File

@@ -0,0 +1,32 @@
package service
import (
"context"
"go-common/app/service/main/push/dao/huawei"
"go-common/app/service/main/push/model"
)
// AddCallback adds callback.
func (s *Service) AddCallback(c context.Context, cb *model.Callback) (err error) {
// db 中能取到优先从 db 取(客户端有时候在送达和点击时刻取 mid 或 buvid 时会有问题)
if r, e := s.dao.Report(c, cb.Token); e == nil && r != nil {
cb.Mid = r.Mid
cb.Buvid = r.Buvid
cb.APP = r.APPID
cb.Platform = r.PlatformID
cb.Brand = model.DeviceBrand(r.DeviceBrand)
}
if cb.Platform == model.PlatformHuawei && cb.Extra != nil &&
(cb.Extra.Status == huawei.CallbackTokenUninstalled || cb.Extra.Status == huawei.CallbackTokenNotApply || cb.Extra.Status == huawei.CallbackTokenInactive) {
s.reportCache.Save(func() {
if cb.Mid > 0 {
s.DelReport(context.TODO(), cb.APP, cb.Mid, cb.Token)
} else {
s.dao.DelReport(context.TODO(), cb.Token)
}
})
}
err = s.dao.AddCallback(c, cb)
return
}

View File

@@ -0,0 +1,116 @@
package service
import (
"context"
"go-common/app/service/main/push/model"
"go-common/library/log"
xtime "go-common/library/time"
)
func (s *Service) setChCounter(taskID string, v int64) {
s.pmu.Lock()
s.chCounter[taskID] += v
s.pmu.Unlock()
}
func (s *Service) chCounterVal(taskID string) int64 {
s.pmu.RLock()
defer s.pmu.RUnlock()
return s.chCounter[taskID]
}
const (
_pgStatus = iota
_pgBeginTime
_pgEndTime
_pgPushTime
_pgMidTotal
_pgMidValid
_pgMidMissed
_pgMidMissedSuccess
_pgMidMissedFailed
_pgTokenTotal
_pgTokenSuccess
_pgTokenValid
_pgTokenFailed
_pgTokenDelay
_pgRetryTimes
)
func (s *Service) updateProgressproc() {
defer s.waiter.Done()
for {
f, ok := <-s.progressCh
if !ok {
log.Info("updateProgressproc exit")
return
}
f()
}
}
// AddMidProgress .
func (s *Service) AddMidProgress(ctx context.Context, task string, midTotal, midValid int64) error {
p := &model.Progress{MidTotal: midTotal, MidValid: midValid}
return s.dao.UpdateTaskProgress(ctx, task, p)
}
func (s *Service) setProgress(taskID string, typ int, v int64) {
s.ppmu.Lock()
defer s.ppmu.Unlock()
if s.progress[taskID] == nil {
s.progress[taskID] = &model.Progress{Brands: make(map[int]int64)}
}
s.setBaseProgress(taskID, typ, v)
}
func (s *Service) setBaseProgress(taskID string, typ int, v int64) {
switch typ {
case _pgStatus:
s.progress[taskID].Status = int8(v)
case _pgBeginTime:
s.progress[taskID].BeginTime = xtime.Time(v)
case _pgEndTime:
s.progress[taskID].EndTime = xtime.Time(v)
case _pgPushTime:
s.progress[taskID].PushTime = xtime.Time(v)
case _pgRetryTimes:
s.progress[taskID].RetryTimes += v
case _pgMidTotal:
s.progress[taskID].MidTotal += v
case _pgMidValid:
s.progress[taskID].MidValid += v
case _pgMidMissed:
s.progress[taskID].MidMissed += v
case _pgMidMissedSuccess:
s.progress[taskID].MidMissedSuccess += v
case _pgMidMissedFailed:
s.progress[taskID].MidMissedFailed += v
case _pgTokenTotal:
s.progress[taskID].TokenTotal += v
case _pgTokenSuccess:
s.progress[taskID].TokenSuccess += v
case _pgTokenValid:
s.progress[taskID].TokenValid += v
case _pgTokenFailed:
s.progress[taskID].TokenFailed += v
case _pgTokenDelay:
s.progress[taskID].TokenDelay += v
}
}
func (s *Service) setBrandProgress(taskID string, brand int, v int64) {
s.ppmu.Lock()
defer s.ppmu.Unlock()
if s.progress[taskID] == nil {
s.progress[taskID] = &model.Progress{Brands: make(map[int]int64)}
}
s.progress[taskID].Brands[brand] += v
}
func (s *Service) fetchProgress(taskID string) *model.Progress {
s.ppmu.RLock()
defer s.ppmu.RUnlock()
return s.progress[taskID]
}

View File

@@ -0,0 +1,866 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"math/rand"
"strconv"
"strings"
"time"
"go-common/app/service/main/push/dao"
"go-common/app/service/main/push/dao/apns2"
"go-common/app/service/main/push/dao/fcm"
"go-common/app/service/main/push/dao/huawei"
"go-common/app/service/main/push/dao/jpush"
"go-common/app/service/main/push/dao/oppo"
"go-common/app/service/main/push/model"
"go-common/library/log"
"go-common/library/sync/errgroup"
"go-common/library/xstr"
)
const (
_pushLimitAndroid = 1000
)
func (s *Service) pushAPNSproc() {
for {
v, ok := <-s.apnsCh
if !ok {
log.Info("apnsCh has been closed.")
return
}
s.pushIOS(v.Info, v.Item)
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func (s *Service) pushMiproc() {
for {
v, ok := <-s.miCh
if !ok {
log.Info("miCh has been closed.")
return
}
for scheme, items := range dispatchByScheme(v.Info.LinkType, v.Info.LinkValue, v.Items) {
s.pushMi(v.Info, scheme, items)
}
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func (s *Service) pushHuaweiproc() {
for {
v, ok := <-s.huaweiCh
if !ok {
log.Info("huaweiCh has been closed.")
return
}
for scheme, items := range dispatchByScheme(v.Info.LinkType, v.Info.LinkValue, v.Items) {
s.pushHuawei(v.Info, scheme, items)
}
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func (s *Service) pushOppoproc() {
for {
v, ok := <-s.oppoCh
if !ok {
log.Info("oppoCh has been closed.")
return
}
s.pushOppoOne(v.Info, v.Item)
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func (s *Service) pushJpushproc() {
for {
v, ok := <-s.jpushCh
if !ok {
log.Info("jpushCh has been closed.")
return
}
for scheme, items := range dispatchByScheme(v.Info.LinkType, v.Info.LinkValue, v.Items) {
s.pushJpush(v.Info, scheme, items)
}
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func (s *Service) pushFCMproc() {
for {
v, ok := <-s.fcmCh
if !ok {
log.Info("fcmCh has been closed")
return
}
for scheme, items := range dispatchByScheme(v.Info.LinkType, v.Info.LinkValue, v.Items) {
s.pushFCM(v.Info, scheme, items)
}
s.setChCounter(v.Info.TaskID, -1)
time.Sleep(time.Millisecond)
}
}
func dispatchByScheme(linkType int8, linkValue string, items []*model.PushItem) (res map[string][]*model.PushItem) {
var scheme string
res = make(map[string][]*model.PushItem)
for _, item := range items {
scheme = model.Scheme(linkType, linkValue, model.PlatformAndroid, item.Build)
res[scheme] = append(res[scheme], item)
}
return
}
func (s *Service) pushInfo(task *model.Task) *model.PushInfo {
info := &model.PushInfo{
Job: task.Job,
APPID: task.APPID,
TaskID: task.ID,
Title: task.Title,
Summary: task.Summary,
LinkType: task.LinkType,
LinkValue: task.LinkValue,
PushTime: task.PushTime,
ExpireTime: task.ExpireTime,
Sound: task.Sound,
Vibration: task.Vibration,
PassThrough: s.c.Push.PassThrough,
// PassThrough: task.PassThrough,
ImageURL: task.ImageURL,
}
if info.Title == "" {
info.Title = model.DefaultMessageTitle
}
return info
}
// Pushs push some mids.
func (s *Service) Pushs(c context.Context, task *model.Task, mids []int64) (err error) {
if len(mids) == 0 {
log.Warn("s.Pushs(%d) no mids", task.ID)
dao.PromInfo("push:没有mid")
return
}
s.pushByPart(c, task, mids)
return
}
// SinglePush push one mid.
func (s *Service) SinglePush(c context.Context, token string, task *model.Task, mid int64) (err error) {
if err = s.checkBusiness(task.BusinessID, token); err != nil {
return
}
var rs map[int][]*model.PushItem
if rs, err = s.tokensByMid(task, mid); err != nil {
return
}
if len(rs) == 0 {
log.Warn("no tokens. mid(%d) task(%+v)", mid, task)
return
}
info := s.pushInfo(task)
for p, v := range rs {
s.pushByPlatform(info, p, v)
}
for {
if s.chCounterVal(task.ID) == 0 {
log.Info("Pushs done. task(%+v) result(%+v)", task, s.fetchProgress(task.ID))
return
}
time.Sleep(10 * time.Millisecond)
}
}
// TestToken for test via push token.
func (s *Service) TestToken(c context.Context, info *model.PushInfo, token string) (err error) {
r, err := s.dao.Report(context.Background(), token)
if err != nil {
return
}
if r == nil {
log.Warn("test token(%s) not exist in db", token)
return
}
var (
res *model.HTTPResponse
item = &model.PushItem{
Platform: r.PlatformID,
Mid: r.Mid,
Token: token,
Sound: info.Sound,
Vibration: info.Vibration,
Build: r.Build,
}
androidScheme = model.Scheme(info.LinkType, info.LinkValue, model.PlatformAndroid, item.Build)
)
switch item.Platform {
case model.PlatformIPhone, model.PlatformIPad:
if item.Platform == model.PlatformIPhone {
res, err = s.dao.PushIPhone(c, info, item)
} else {
res, err = s.dao.PushIPad(c, info, item)
}
if err == nil && res.Code != apns2.StatusCodeSuccess {
err = errors.New(res.Msg)
}
case model.PlatformXiaomi:
res, err = s.dao.PushMi(c, info, androidScheme, token)
if err == nil && res.Code != model.HTTPCodeOk {
err = errors.New(res.Msg)
}
case model.PlatformHuawei:
var resp *huawei.Response
resp, err = s.dao.PushHuawei(c, info, androidScheme, []string{token})
if err == nil && resp.Code != huawei.ResponseCodeSuccess {
err = errors.New(resp.Msg)
}
log.Info("huawei s.TestToken(%+v,%s) result(%+v)", info, token, resp)
case model.PlatformOppo:
err = s.pushOppoOne(info, item)
log.Info("oppo pushOne s.TestToken(%+v,%s)", info, token)
case model.PlatformJpush:
_, err = s.dao.PushJpush(c, info, androidScheme, []string{token})
case model.PlatformFCM:
_, err = s.dao.PushFCM(c, info, androidScheme, []string{token})
log.Info("fcm s.dao.PushFCM(%+v,%s)", info, token)
default:
err = errors.New("平台类型错误")
}
if err != nil {
log.Error("s.TestToken(%+v,%s) error(%v)", info, token, err)
return
}
log.Info("s.TestToken(%+v,%s) result(%+v)", info, token, res)
return
}
func (s *Service) pushByPart(c context.Context, task *model.Task, mids []int64) (err error) {
var (
counter int
group = errgroup.Group{}
info = s.pushInfo(task)
)
for {
l := len(mids)
if l == 0 {
break
}
n := s.c.Push.PushPartSize
if l < n {
n = l
}
part := mids[:n]
mids = mids[n:]
group.Go(func() error {
var rs map[int][]*model.PushItem
var missed []int64
if rs, missed, err = s.tokensByMids(c, task, part); err != nil {
return nil
}
log.Info("missed mid task(%s) %s", task.ID, xstr.JoinInts(missed))
for p, v := range rs {
s.pushByPlatform(info, p, v)
}
_ = missed
return nil
})
time.Sleep(time.Duration(s.c.Push.PushPartInterval))
counter++
if counter > s.c.Push.PushPartChanSize {
group.Wait()
counter = 0
}
}
if counter > 0 {
group.Wait()
}
for {
if s.chCounterVal(task.ID) == 0 {
log.Info("Pushs done. task(%+v) result(%+v)", task, s.fetchProgress(task.ID))
return nil
}
time.Sleep(10 * time.Millisecond)
}
}
func (s *Service) pushByPlatform(info *model.PushInfo, platform int, rs []*model.PushItem) {
switch platform {
case model.PlatformIPhone, model.PlatformIPad:
for _, v := range rs {
s.apnsCh <- &model.PushChanItem{Info: info, Item: v}
dao.PromChanLen("apns_chan_len", int64(len(s.apnsCh)))
s.setChCounter(info.TaskID, 1)
}
return
case model.PlatformOppo:
for _, v := range rs {
s.oppoCh <- &model.PushChanItem{Info: info, Item: v}
dao.PromChanLen("oppo_chan_len", int64(len(s.oppoCh)))
s.setChCounter(info.TaskID, 1)
}
return
}
n := _pushLimitAndroid
if platform == model.PlatformHuawei {
n = s.c.Android.PushHuaweiPart
}
for len(rs) > 0 {
if n > len(rs) {
n = len(rs)
}
part := rs[:n]
rs = rs[n:]
switch platform {
case model.PlatformHuawei:
s.huaweiCh <- &model.PushChanItems{Info: info, Items: part}
dao.PromChanLen("huawei_chan_len", int64(len(s.huaweiCh)))
case model.PlatformJpush:
s.jpushCh <- &model.PushChanItems{Info: info, Items: part}
dao.PromChanLen("jpush_chan_len", int64(len(s.jpushCh)))
case model.PlatformFCM:
s.fcmCh <- &model.PushChanItems{Info: info, Items: part}
dao.PromChanLen("fcm_chan_len", int64(len(s.fcmCh)))
default:
s.miCh <- &model.PushChanItems{Info: info, Items: part}
dao.PromChanLen("mi_chan_len", int64(len(s.miCh)))
}
s.setChCounter(info.TaskID, 1)
}
}
func (s *Service) pushMi(info *model.PushInfo, scheme string, items []*model.PushItem) (err error) {
all := len(items)
// ts := make([]string, all)
tokenBuf := bytes.Buffer{}
for _, item := range items {
tokenBuf.WriteString(item.Token)
tokenBuf.WriteString(",")
// ts = append(ts, item.Token)
}
if tokenBuf.Len() == 0 {
return nil
}
tokenBuf.Truncate(tokenBuf.Len() - 1)
tokens := tokenBuf.String()
ctx := context.Background()
var res *model.HTTPResponse
for c := 0; c <= s.c.Push.RetryTimes; c++ {
res, err = s.dao.PushMi(ctx, info, scheme, tokens)
if err == nil {
break
}
dao.PromInfo("retry push mi")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
}
if err != nil || res.Code != model.HTTPCodeOk {
s.setProgress(info.TaskID, _pgTokenFailed, int64(all))
return
}
s.setProgress(info.TaskID, _pgTokenSuccess, int64(all))
s.cache.Save(func() { logPushed(info.TaskID, model.PlatformXiaomi, items) })
var success int
msg := strings.Split(res.Msg, " ")
if len(msg) == 6 {
success, _ = strconv.Atoi(msg[4])
} else {
log.Warn("push mi result msg(%s)", res.Msg)
}
s.setProgress(info.TaskID, _pgTokenValid, int64(success))
return
}
func (s *Service) pushIOS(info *model.PushInfo, item *model.PushItem) (err error) {
var (
res *model.HTTPResponse
ctx = context.Background()
)
for c := 0; c <= s.c.Push.RetryTimes; c++ {
if item.Platform == model.PlatformIPhone {
res, err = s.dao.PushIPhone(ctx, info, item)
} else {
res, err = s.dao.PushIPad(ctx, info, item)
}
if err == nil {
break
}
dao.PromInfo("retry push iOS")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
}
if err != nil || res == nil {
s.setProgress(info.TaskID, _pgTokenFailed, 1)
return
}
s.setProgress(info.TaskID, _pgTokenSuccess, 1)
if res.Code == apns2.StatusCodeSuccess {
s.setProgress(info.TaskID, _pgTokenValid, 1)
} else if res.Code == apns2.StatusCodeNoActive || res.Code == apns2.StatusCodeNotForTopic {
log.Warn("invalid token. mid(%d) token(%s) response(%+v)", item.Mid, item.Token, res)
s.reportCache.Save(func() { s.DelReport(context.TODO(), info.APPID, item.Mid, item.Token) })
} else {
log.Error("apns response(%+v)", res)
}
return
}
func (s *Service) pushHuawei(info *model.PushInfo, scheme string, items []*model.PushItem) (err error) {
var (
tokens []string
res *huawei.Response
all = int64(len(items))
)
for _, item := range items {
tokens = append(tokens, item.Token)
}
for c := 0; c <= s.c.Push.RetryTimes; c++ {
if res, err = s.dao.PushHuawei(context.TODO(), info, scheme, tokens); err == nil {
break
}
dao.PromInfo("retry push huawei")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
if err == huawei.ErrLimit {
time.Sleep(time.Duration(rand.Int63n(1000)) * time.Millisecond)
}
}
if err != nil {
dao.PromError("push:推送华为")
s.setProgress(info.TaskID, _pgTokenFailed, all)
if err == huawei.ErrLimit {
for _, t := range tokens {
log.Error("push huawei task(%s) token(%s) error(%v)", info.TaskID, t, err)
}
}
return
}
s.setProgress(info.TaskID, _pgTokenSuccess, all)
s.cache.Save(func() { logPushed(info.TaskID, model.PlatformHuawei, items) })
switch res.Code {
case huawei.ResponseCodeSuccess:
s.setProgress(info.TaskID, _pgTokenValid, all)
log.Info("push huawei success task(%s) success(%d)", info.TaskID, all)
case huawei.ResponseCodeSomeTokenInvalid:
itr := &huawei.InvalidTokenResponse{}
if err = json.Unmarshal([]byte(res.Msg), itr); err != nil {
log.Error("json.Unmarshal() error(%v)", err)
return
}
s.setProgress(info.TaskID, _pgTokenValid, int64(itr.Success))
log.Warn("push huawei success task(%s) failed(%d) illegal(%v)", info.TaskID, itr.Failure, itr.IllegalTokens)
if len(itr.IllegalTokens) == 0 {
return
}
s.reportCache.Save(func() {
m := make(map[string]int64, all)
for _, i := range items {
m[i.Token] = i.Mid
}
for _, t := range itr.IllegalTokens {
s.DelReport(context.TODO(), info.APPID, m[t], t)
}
})
case huawei.ResponseCodeAllTokenInvalid, huawei.ResponseCodeAllTokenInvalidNew:
s.cache.Save(func() {
for _, i := range items {
s.DelReport(context.TODO(), info.APPID, i.Mid, i.Token)
}
})
log.Error("push huawei failed task(%s) failed(%d) illegal(%v)", info.TaskID, all, tokens)
default:
log.Error("huawei push response task(%s) error(%v)", info.TaskID, res)
}
return
}
func (s *Service) pushOppoOne(info *model.PushInfo, item *model.PushItem) (err error) {
params, _ := json.Marshal(map[string]string{
"task_id": info.TaskID,
"scheme": model.Scheme(info.LinkType, info.LinkValue, model.PlatformAndroid, item.Build),
})
m := &oppo.Message{
Title: info.Title,
Content: info.Summary,
ActionType: oppo.ActionTypeInner,
ActionParams: string(params),
OfflineTTL: int(int64(info.ExpireTime) - time.Now().Unix()),
CallbackURL: oppo.CallbackURL(info.APPID, info.TaskID),
}
var res *oppo.Response
for c := 0; c <= s.c.Push.RetryTimes; c++ {
if res, err = s.dao.PushOppoOne(context.TODO(), info, m, item.Token); err == nil {
break
}
dao.PromInfo("retry push oppo")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
}
if err != nil || res == nil {
s.setProgress(info.TaskID, _pgTokenFailed, 1)
dao.PromError("push:推送oppo")
log.Error("oppo push response task(%s) error(%v)", info.TaskID, res)
return
}
s.setProgress(info.TaskID, _pgTokenSuccess, 1)
if res.Code == oppo.ResponseCodeInvalidToken || res.Code == oppo.ResponseCodeUnsubscribeToken || res.Code == oppo.ResponseCodeRepeatToken {
if item.Mid > 0 {
s.reportCache.Save(func() { s.DelReport(context.TODO(), info.APPID, item.Mid, item.Token) })
}
return
}
s.setProgress(info.TaskID, _pgTokenValid, 1)
return
}
// func (s *Service) pushOppo(info *model.PushInfo, items []*model.PushItem) (err error) {
// params, _ := json.Marshal(map[string]string{
// "task_id": info.TaskID,
// "scheme": model.Scheme(info.LinkType, info.LinkValue, model.PlatformAndroid),
// })
// m := &oppo.Message{
// Title: info.Title,
// Content: info.Summary,
// ActionType: oppo.ActionTypeInner,
// ActionParams: string(params),
// OfflineTTL: int(int64(info.ExpireTime) - time.Now().Unix()),
// CallbackURL: oppo.CallbackURL(info.APPID, info.TaskID),
// }
// res, err := s.dao.OppoMessage(context.TODO(), info, m)
// if err != nil || res == nil || res.Data.MsgID == "" {
// return
// }
// var (
// ts []string
// all = int64(len(items))
// tm = make(map[string]int64, all)
// )
// for _, i := range items {
// ts = append(ts, i.Token)
// tm[i.Token] = i.Mid
// }
// for c := 0; c <= s.c.Push.RetryTimes; c++ {
// if res, err = s.dao.PushOppo(context.TODO(), info, res.Data.MsgID, ts); err == nil {
// break
// }
// dao.PromInfo("retry push oppo")
// s.setProgress(info.TaskID, _pgRetryTimes, 1, model.PlatformOppo)
// }
// if err != nil || res == nil || res.Code != oppo.ResponseCodeSuccess {
// s.setProgress(info.TaskID, _pgTokenFailed, all, model.PlatformOppo)
// dao.PromError("push:推送oppo")
// log.Error("oppo push response task(%s) error(%v)", info.TaskID, res)
// if res.Code == oppo.ResponseCodeInvalidToken {
// s.delOppoReports(tm, info.APPID, ts)
// }
// return
// }
// s.setProgress(info.TaskID, _pgTokenSuccess, all, model.PlatformOppo)
// s.cache.Save(func() { logPushed(model.PlatformOppo, items) })
// var (
// invalid = len(res.Data.TokenInvalid)
// unsubscribe = len(res.Data.TokenUnsubscribe)
// repeat = len(res.Data.TokenRepeat)
// valid = int(all) - invalid - unsubscribe - repeat
// )
// if invalid+unsubscribe+repeat > 0 {
// if invalid > 0 {
// s.delOppoReports(tm, info.APPID, res.Data.TokenInvalid)
// }
// if unsubscribe > 0 {
// s.delOppoReports(tm, info.APPID, res.Data.TokenUnsubscribe)
// }
// if repeat > 0 {
// s.delOppoReports(tm, info.APPID, res.Data.TokenRepeat)
// }
// }
// s.setProgress(info.TaskID, _pgTokenValid, int64(valid), model.PlatformOppo)
// s.cache.Save(func() { s.dao.AddTokensCache(context.TODO(), info.TaskID, ts) })
// log.Info("push oppo success task(%s) success(%d)", info.TaskID, all)
// return
// }
// func (s *Service) delOppoReports(m map[string]int64, appid int64, tokens []string) {
// if len(tokens) == 0 {
// return
// }
// s.reportCache.Save(func() {
// for _, t := range tokens {
// s.DelReport(context.TODO(), appid, m[t], t)
// }
// })
// }
func (s *Service) pushJpush(info *model.PushInfo, scheme string, items []*model.PushItem) (err error) {
var (
tokens []string
all = int64(len(items))
valid = all
resp *jpush.PushResponse
)
for _, item := range items {
tokens = append(tokens, item.Token)
}
for c := 0; c <= s.c.Push.RetryTimes; c++ {
if resp, err = s.dao.PushJpush(context.TODO(), info, scheme, tokens); err == nil && !resp.Retry {
break
}
dao.PromInfo("retry push jpush")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
}
if err != nil {
dao.PromError("push:推送极光")
s.setProgress(info.TaskID, _pgTokenFailed, all)
return
}
if resp.Error.Code != 0 {
s.setProgress(info.TaskID, _pgTokenFailed, all)
log.Error("jpush task(%s) tokens(%d) invalid code(%+v)", info.TaskID, len(tokens), resp)
return
}
valid -= int64(len(resp.IllegalTokens))
s.setProgress(info.TaskID, _pgTokenSuccess, all)
s.cache.Save(func() { logPushed(info.TaskID, model.PlatformJpush, items) })
s.setProgress(info.TaskID, _pgTokenValid, valid)
log.Info("push jpush success task(%s) success(%d)", info.TaskID, all)
return
}
func (s *Service) pushFCM(info *model.PushInfo, scheme string, items []*model.PushItem) (err error) {
var (
tokens []string
all = int64(len(items))
valid = all
resp *fcm.Response
)
for _, item := range items {
tokens = append(tokens, item.Token)
}
for c := 0; c <= s.c.Push.RetryTimes; c++ {
if resp, err = s.dao.PushFCM(context.Background(), info, scheme, tokens); err == nil {
break
}
// 报错的情况下,如果 RetryAfter 有值才需要重试
if resp != nil && resp.RetryAfter == "" {
break
}
if d, e := resp.GetRetryAfterTime(); e == nil {
time.Sleep(d)
}
dao.PromInfo("retry push fcm")
s.setProgress(info.TaskID, _pgRetryTimes, 1)
}
if err != nil {
dao.PromError("push:push fcm")
s.setProgress(info.TaskID, _pgTokenFailed, all)
return
}
s.setProgress(info.TaskID, _pgTokenSuccess, all)
s.cache.Save(func() { logPushed(info.TaskID, model.PlatformFCM, items) })
valid -= int64(resp.Fail)
s.setProgress(info.TaskID, _pgTokenValid, valid)
log.Info("push fcm success task(%s) success(%d)", info.TaskID, all)
return
}
func (s *Service) tokensByMid(task *model.Task, mid int64) (res map[int][]*model.PushItem, err error) {
var rs []*model.Report
if rs, err = s.dao.ReportsCacheByMid(context.TODO(), mid); err != nil {
if rs, err = s.dao.ReportsByMid(context.TODO(), mid); err != nil {
return
}
}
if len(rs) == 0 {
return
}
p := map[int64][]*model.Report{mid: rs}
res = s.distribute(task, p)
return
}
func (s *Service) tokensByMids(c context.Context, task *model.Task, mids []int64) (res map[int][]*model.PushItem, missed []int64, err error) {
rs, missed, err := s.dao.ReportsCacheByMids(c, mids)
if err != nil {
dao.PromInfo("report:查缓存失败回源")
if rs, err = s.dao.ReportsByMids(c, mids); err != nil {
return
}
}
res = s.distribute(task, rs)
return
}
func (s *Service) distribute(task *model.Task, rs map[int64][]*model.Report) (res map[int][]*model.PushItem) {
var (
validMid int64
validMidPlat = make(map[int]map[int64]bool)
buildCount = len(task.Build)
// platformCount = len(task.Platform)
brands = make(map[string]int64)
)
res = make(map[int][]*model.PushItem)
for mid, rr := range rs {
var valid bool
for _, r := range rr {
if r.APPID != task.APPID {
// log.Info("task(%s) token(%s) mid(%d) app not match", task.ID, r.DeviceToken, mid)
continue
}
if r.NotifySwitch == model.SwitchOff {
// log.Info("task(%s) token(%s) mid(%d) switchoff", task.ID, r.DeviceToken, mid)
continue
}
realTime := model.RealTime(r.TimeZone)
if realTime.Unix() > int64(task.ExpireTime) {
// log.Info("task(%s) token(%s) mid(%d) expire_time(%s) expired", task.ID, r.DeviceToken, mid, task.ExpireTime)
continue
}
// if platformCount > 0 && !validatePlatform(r.PlatformID, task.Platform) {
// // log.Info("task(%s) token(%s) mid(%d) platform forbid", task.ID, r.DeviceToken, mid)
// continue
// }
if buildCount > 0 && !validateBuild(r.PlatformID, r.Build, task.Build) {
// log.Info("task(%s) token(%s) mid(%d) build forbid", task.ID, r.DeviceToken, mid)
continue
}
p := &model.PushItem{Platform: r.PlatformID, Token: r.DeviceToken, Mid: mid, Sound: task.Sound, Vibration: task.Vibration}
res[r.PlatformID] = append(res[r.PlatformID], p)
valid = true
brands[r.DeviceBrand]++
if _, ok := validMidPlat[r.PlatformID]; !ok {
validMidPlat[r.PlatformID] = make(map[int64]bool)
}
validMidPlat[r.PlatformID][r.Mid] = true
}
if valid {
validMid++
}
}
s.setProgress(task.ID, _pgMidValid, validMid)
for br, v := range brands {
s.setBrandProgress(task.ID, model.DeviceBrand(br), v)
}
return
}
// func validatePlatform(platform int, set []int) bool {
// for _, v := range set {
// if v == platform {
// return true
// }
// }
// if platform == model.PlatformIPhone || platform == model.PlatformIPad {
// return false
// }
// if platform == model.PlatformAndroid {
// return true
// }
// return false
// }
func logPushed(task string, platform int, items []*model.PushItem) {
for _, v := range items {
log.Info("push done, task(%s) platform(%d) mid(%d) token(%s)", task, platform, v.Mid, v.Token)
}
}
func validateBuild(platform, build int, builds map[int]*model.Build) bool {
if len(builds) == 0 {
return true
}
if builds[platform] == nil {
return true
}
c := builds[platform].Condition
b := builds[platform].Build
switch c {
case "gt":
return build > b
case "gte":
return build >= b
case "lt":
return build < b
case "lte":
return build <= b
case "eq":
return build == b
case "ne":
return build != b
}
return false
}
func (s *Service) pushTokens(task *model.Task) (err error) {
bs, err := ioutil.ReadFile(task.MidFile)
if err != nil {
log.Error("ioutil.ReadFile(%s) error(v)", task.MidFile, err)
return
}
var (
counter int
group = errgroup.Group{}
info = s.pushInfo(task)
tokens = strings.Split(string(bs), "\n")
)
for {
l := len(tokens)
if l == 0 {
break
}
n := s.c.Push.PushPartSize
if l < n {
n = l
}
part := tokens[:n]
tokens = tokens[n:]
group.Go(func() error {
res, missed, e := s.dao.TokensCache(context.Background(), part)
if e != nil {
log.Error("s.dao.TokensCache error(%v)", err)
}
if len(missed) > 0 {
log.Info("task(%s) tokens cache missed(%d)", task.ID, len(missed))
}
var rs []*model.PushItem
brands := make(map[int]int64)
for _, t := range part {
build := model.UnknownBuild
if v, ok := res[t]; ok {
build = v.Build
brands[model.DeviceBrand(v.DeviceBrand)]++
}
rs = append(rs, &model.PushItem{Platform: task.PlatformID, Token: t, Sound: task.Sound, Vibration: task.Vibration, Build: build})
}
log.Info("push tokens task(%+v) tokens(%d)", task, len(rs))
s.setProgress(task.ID, _pgTokenTotal, int64(len(rs)))
for k, v := range brands {
s.setBrandProgress(task.ID, k, v)
}
s.pushByPlatform(info, task.PlatformID, rs)
return nil
})
time.Sleep(time.Duration(s.c.Push.PushPartInterval))
counter++
if counter > s.c.Push.PushPartChanSize {
group.Wait()
counter = 0
}
}
if counter > 0 {
group.Wait()
}
for {
if s.chCounterVal(task.ID) == 0 {
log.Info("Pushs done. task(%+v) result(%+v)", task, s.fetchProgress(task.ID))
return nil
}
time.Sleep(10 * time.Millisecond)
}
}

View File

@@ -0,0 +1,117 @@
package service
import (
"context"
"go-common/app/service/main/push/model"
"go-common/library/log"
)
// AddReport add report.
func (s *Service) AddReport(ctx context.Context, r *model.Report) (err error) {
var old *model.Report
if old, err = s.dao.Report(ctx, r.DeviceToken); err != nil {
return
}
if old != nil {
r.ID = old.ID
if err = s.dao.UpdateReport(ctx, r); err != nil {
return
}
} else {
if r.ID, err = s.dao.AddReport(ctx, r); err != nil {
return
}
}
if r.NotifySwitch == model.SwitchOn {
s.reportCache.Save(func() { s.dao.AddTokenCache(context.Background(), r.DeviceToken, r) })
if r.Mid > 0 {
s.reportCache.Save(func() { s.AddReportCache(context.Background(), r) })
}
}
if old != nil && old.Dtime == 0 && old.Mid > 0 && (old.Mid != r.Mid || r.NotifySwitch == model.SwitchOff) {
s.reportCache.Save(func() { s.dao.DelReportCache(context.TODO(), old.Mid, old.APPID, old.DeviceToken) })
}
return
}
// AddReportCache add report cache.
func (s *Service) AddReportCache(c context.Context, r *model.Report) (err error) {
res, err := s.dao.ReportsCacheByMid(c, r.Mid)
if err != nil {
return
}
if len(res) == 0 {
if res, err = s.dao.ReportsByMid(c, r.Mid); err != nil {
return
}
}
if len(res) == 0 {
return
}
m := make(map[string]*model.Report)
for _, v := range res {
m[v.DeviceToken] = v
}
m[r.DeviceToken] = r
var rs []*model.Report
for _, v := range m {
rs = append(rs, v)
}
mrs := map[int64][]*model.Report{r.Mid: rs}
return s.dao.AddReportsCacheByMids(c, mrs)
}
// AddUserReportCache add user report cache.
func (s *Service) AddUserReportCache(c context.Context, mid int64, rs []*model.Report) (err error) {
mrs := map[int64][]*model.Report{mid: rs}
return s.dao.AddReportsCacheByMids(c, mrs)
}
// AddTokenCache add token cache.
func (s *Service) AddTokenCache(ctx context.Context, r *model.Report) (err error) {
err = s.dao.AddTokenCache(ctx, r.DeviceToken, r)
return
}
// AddTokensCache add token cache.
func (s *Service) AddTokensCache(ctx context.Context, rs map[string]*model.Report) (err error) {
for k := range rs {
log.Info("AddTokensCache token(%s)", k)
}
err = s.dao.AddTokensCache(ctx, rs)
return
}
// DelReport delelte report & its cache.
func (s *Service) DelReport(c context.Context, appID, mid int64, token string) (err error) {
if _, err = s.dao.DelReport(c, token); err != nil {
log.Error("delete token(%s) error(%v)", token, err)
return
}
log.Info("delete report app(%d) mid(%d) token(%s)", appID, mid, token)
s.reportCache.Save(func() {
if err = s.dao.DelTokenCache(context.Background(), token); err != nil {
log.Error("s.dao.DelTokeCache(%s) error(%v)", token, err)
}
if mid > 0 {
if err = s.dao.DelReportCache(context.Background(), mid, appID, token); err != nil {
log.Error("s.dao.DelReportCache(%d,%d,%s) error(%v)", mid, appID, token, err)
}
}
})
return
}
// DelInvalidReports deletes invalid reports.
func (s *Service) DelInvalidReports(c context.Context, tp int) (err error) {
switch tp {
case model.DelMiFeedback:
s.reportCache.Save(func() { s.dao.DelMiInvalid(context.TODO()) })
case model.DelMiUninstalled:
s.cache.Save(func() { s.dao.DelMiUninstalled(context.TODO()) })
default:
log.Error("delete invalid reports type error(%d)", tp)
}
return
}

View File

@@ -0,0 +1,178 @@
package service
import (
"context"
"math/rand"
"strconv"
"sync"
"time"
filterrpc "go-common/app/service/main/filter/rpc/client"
"go-common/app/service/main/push/conf"
"go-common/app/service/main/push/dao"
"go-common/app/service/main/push/model"
"go-common/library/cache"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
// Service push service.
type Service struct {
c *conf.Config
dao *dao.Dao
cache *cache.Cache
waiter sync.WaitGroup
filterRPC *filterrpc.Service
reportCache *cache.Cache
progress map[string]*model.Progress
businesses map[int64]*model.Business
apnsCh chan *model.PushChanItem
miCh, huaweiCh chan *model.PushChanItems
jpushCh chan *model.PushChanItems
fcmCh chan *model.PushChanItems
oppoCh chan *model.PushChanItem
chCounter map[string]int64
pmu, ppmu sync.RWMutex // progress mutex / progress proc mutex
closed bool
httpclient *bm.Client
progressCh chan func()
}
// New creates a push service instance.
func New(c *conf.Config) *Service {
rand.Seed(time.Now().UnixNano())
s := &Service{
c: c,
dao: dao.New(c),
filterRPC: filterrpc.New(c.FilterRPC),
cache: cache.New(1, 1024000),
reportCache: cache.New(1, 1024000),
progress: make(map[string]*model.Progress),
businesses: make(map[int64]*model.Business),
apnsCh: make(chan *model.PushChanItem, c.Push.PushChanSizeAPNS),
miCh: make(chan *model.PushChanItems, c.Push.PushChanSizeMi),
huaweiCh: make(chan *model.PushChanItems, c.Push.PushChanSizeHuawei),
jpushCh: make(chan *model.PushChanItems, c.Push.PushChanSizeJpush),
oppoCh: make(chan *model.PushChanItem, c.Push.PushChanSizeOppo),
fcmCh: make(chan *model.PushChanItems, c.Push.PushChanSizeFCM),
chCounter: make(map[string]int64),
httpclient: bm.NewClient(c.HTTPClient),
progressCh: make(chan func(), c.Push.UpdateTaskProgressProc),
}
s.loadBusiness()
go s.loadBusinessproc()
go s.loadTaskproc()
s.waiter.Add(1)
go s.updateTaskProgressproc()
for i := 0; i < s.c.Push.PushGoroutinesAPNS; i++ {
go s.pushAPNSproc()
}
for i := 0; i < s.c.Push.PushGoroutinesMi; i++ {
go s.pushMiproc()
}
for i := 0; i < s.c.Push.PushGoroutinesHuawei; i++ {
go s.pushHuaweiproc()
}
for i := 0; i < s.c.Push.PushGoroutinesOppo; i++ {
go s.pushOppoproc()
}
for i := 0; i < s.c.Push.PushGoroutinesJpush; i++ {
go s.pushJpushproc()
}
for i := 0; i < s.c.Push.PushGoroutinesFCM; i++ {
go s.pushFCMproc()
}
for i := 0; i < s.c.Push.UpdateTaskProgressProc; i++ {
s.waiter.Add(1)
go s.updateProgressproc()
}
return s
}
func (s *Service) loadTaskproc() {
if !s.c.Push.PickUpTask {
log.Warn("service do not pick up new tasks from database")
return
}
for _, v := range model.Platforms {
s.waiter.Add(1)
go func(platform int) {
defer s.waiter.Done()
for {
if s.closed {
return
}
task, err := s.pickNewTask(platform)
if err != nil {
time.Sleep(5 * time.Second)
continue
}
if task != nil {
s.handleTask(task)
}
time.Sleep(time.Duration(s.c.Push.LoadTaskInteval))
}
}(v)
}
}
func (s *Service) updateTaskProgressproc() {
defer s.waiter.Done()
for {
if s.closed {
close(s.progressCh)
return
}
time.Sleep(time.Duration(s.c.Push.UpdateTaskProgressInteval))
dao.PromChanLen("progress len", int64(len(s.progress)))
s.updateTaskProgress()
}
}
func (s *Service) updateTaskProgress() {
progress := make(map[string]*model.Progress)
s.ppmu.Lock()
for id, p := range s.progress {
brands := make(map[int]int64)
for key, data := range p.Brands {
brands[key] = data
}
pNew := *p
pNew.Brands = brands
progress[id] = &pNew
}
s.ppmu.Unlock()
for id, p := range progress {
id := id
p := p
if i, _ := strconv.ParseInt(id, 10, 64); i == 0 {
continue
}
s.progressCh <- func() { s.dao.UpdateTaskProgress(context.Background(), id, p) }
if p.Status == model.TaskStatusDone || p.Status == model.TaskStatusFailed || p.Status == model.TaskStatusExpired {
s.ppmu.Lock()
delete(s.progress, id)
s.ppmu.Unlock()
}
}
}
// Close closes service.
func (s *Service) Close() {
s.closed = true
s.waiter.Wait()
close(s.apnsCh)
close(s.miCh)
close(s.huaweiCh)
close(s.oppoCh)
close(s.jpushCh)
s.dao.Close()
}
// Ping checks service.
func (s *Service) Ping(c context.Context) (err error) {
err = s.dao.Ping(c)
return
}

Some files were not shown because too many files have changed in this diff Show More