添加消息推送

This commit is contained in:
邹宗楠
2023-03-18 11:05:47 +08:00
parent bf5efa6faa
commit ffd4920ebb
8 changed files with 452 additions and 5 deletions

View File

@@ -0,0 +1,72 @@
package uinapp
import (
"git.rosy.net.cn/baseapi/utils"
"git.rosy.net.cn/jx-callback/globals"
"testing"
"time"
)
var (
api = NewUinappApi(appId, appKey, appSecret, masterSecret)
ios = "22d3c42f10cab7765c04ee5aee5ba068"
android = "747bda2a61059284405e32a575a03c8f"
)
func TestGetToken(t *testing.T) {
err := api.GetUinAppToken()
globals.SugarLogger.Debug("result := %v", err)
}
func TestSendMsgAndroid(t *testing.T) {
api.SendMsgByUinApp(SendMsgReq{
RequestId: utils.Int64ToStr(time.Now().UnixNano()),
GroupName: "",
Audience: AudienceCid{CId: []string{android}},
Settings: SendMsgSetting{},
PushMessage: PushMessageDetail{
Notification: PushMessageDetailNotification{
Title: "测试消息推送[个推推送消息参数]",
Body: "吃早饭了没得[个推推送消息参数]",
BigText: "这是个什么东西[个推推送消息参数]",
BigImage: "",
Logo: "",
LogoUrl: "",
ChannelId: "",
ChannelName: "",
ChannelLevel: 0,
ClickType: "startapp",
Intent: "",
Url: "",
Payload: "",
NotifyId: 0,
RingName: "",
BadgeAddNum: 0,
ThreadId: "",
},
},
PushChannel: PushChannelDetail{
//Ios: IosApsDetail{},
Android: AndroidPushChannelDetail{Ups: AndroidPushChannelUps{
Notification: AndroidPushChannelNotification{
Title: "测试消息推送[厂商推送消息参数]",
Body: "吃早饭了没得[厂商推送消息参数]",
ClickType: "startapp",
Intent: "",
Url: "",
NotifyId: 0,
},
Transmission: "",
Revoke: struct {
OldTaskId string `json:"old_task_id"`
}{},
Options: struct {
Constraint string `json:"constraint"`
Key string `json:"key"`
Value interface{} `json:"value"`
}{},
}},
Mp: MpPushChannelDetail{},
},
})
}

View File

@@ -0,0 +1,180 @@
package uinapp
// AppIDdqxDP9HwHc5B1dd2db4ek2
// AppKeyIogz63Q1HL7R4dB8F4Kbn5
// AppSecretwX4n8IusDn7PfxkGmypzp5
// MasterSecretJ7pU14SwCC7SoUntXJghj8
const (
appKey = "Iogz63Q1HL7R4dB8F4Kbn5"
masterSecret = "J7pU14SwCC7SoUntXJghj8"
appId = "dqxDP9HwHc5B1dd2db4ek2"
appSecret = "wX4n8IusDn7PfxkGmypzp5"
BaseUrl = "https://restapi.getui.com/v2/" // 基础路由
GetTokenInterface = "/auth" // 获取token接口
PushMsgByCid = "push/single/cid" // 通过cId发送消息
)
// AppTokenRes 获取头肯返回值
type AppTokenRes struct {
Msg string `json:"msg"`
Code int `json:"code"`
Data struct {
ExpireTime string `json:"expire_time"`
Token string `json:"token"`
} `json:"data"`
}
// AudienceCid 推送目标用户
type AudienceCid struct {
CId []string `json:"cid"` // 设备CID只能一个
}
// SendMsgSetting 推送条件设置详细解释见下方settings说明
type SendMsgSetting struct {
Ttl int64 `json:"ttl"` // 消息离线时间设置
Strategy struct {
DefaultSetting int64 `json:"default"` //1: 表示该消息在用户在线时推送个推通道,用户离线时推送厂商通道; 2: 表示该消息只通过厂商通道策略下发,不考虑用户是否在线; 3: 表示该消息只通过个推通道下发,不考虑用户是否在线; 4: 表示该消息优先从厂商通道下发,若消息内容在厂商通道代发失败后会从个推通道下发。
Ios int64 `json:"ios"`
Hw int64 `json:"hw"`
Ho int64 `json:"ho"`
Xm int64 `json:"xm"`
Xmg int64 `json:"xmg"`
Vv int64 `json:"vv"`
Op int64 `json:"op"`
Opg int64 `json:"opg"`
Mz int64 `json:"mz"`
St int64 `json:"st"`
Wx int64 `json:"wx"`
} `json:"strategy"` // 厂商通道策略
}
// PushMessageDetailNotification 个推安卓消息
type PushMessageDetailNotification struct {
Title string `json:"title"` // 通知消息标题
Body string `json:"body"` // 通知消息内容
BigText string `json:"big_text"` // 长文本消息内容,通知消息+长文本样式与big_image二选一两个都填写时报错长度 ≤ 512字
BigImage string `json:"big_image"` // 大图的URL地址通知消息+大图样式, 与big_text二选一两个都填写时报错URL长度 ≤ 1024字
Logo string `json:"logo"` // 通知的图标名称,包含后缀名
LogoUrl string `json:"logo_url"` // 通知图标URL地址
ChannelId string `json:"channel_id"` // 通知渠道id
ChannelName string `json:"channel_name"` // 通知渠道名称
ChannelLevel int64 `json:"channel_level"` // 设置通知渠道重要性
ClickType string `json:"click_type"` // 点击通知后续动作 intent打开应用内特定页面 url打开网页地址 payload自定义消息内容启动应用 payload_custom自定义消息内容不启动应用 startapp打开应用首页 none纯通知无后续动作
Intent string `json:"intent"` // click_type为intent时必填 点击通知打开应用特定页面
Url string `json:"url"` // click_type为url时必填 点击通知栏消息时,唤起系统默认浏览器打开此链接。必须填写可访问的链接
Payload string `json:"payload"` // click_type为payload/payload_custom时必填 点击通知时,附加自定义透传消息
NotifyId int64 `json:"notify_id"` // 覆盖任务时会使用到该字段两条消息的notify_id相同新的消息会覆盖老的消息
RingName string `json:"ring_name"` // 自定义铃声,请填写文件名,不包含后缀名(需要在客户端开发时嵌入),个推通道下发有效
BadgeAddNum int64 `json:"badge_add_num"` // 角标, 必须大于0, 个推通道下发有效
ThreadId string `json:"thread_id"` // 消息折叠分组设置成相同thread_id的消息会被折叠仅支持个推渠道下发的安卓消息。目前与iOS的thread_id设置无关安卓和iOS需要分别设置。
}
// PushMessageDetail 个推推送消息参数
type PushMessageDetail struct {
Duration string `json:"duration"` // 手机端通知展示时间段格式为毫秒时间戳段两个时间的时间差必须大于10分钟
Notification PushMessageDetailNotification `json:"notification"` // 通知消息内容仅支持安卓系统iOS系统不展示个推通知消息与transmission、revoke三选一都填写时报错
Transmission string // 纯透传消息内容安卓和iOS均支持与notification、revoke 三选一,都填写时报错,长度 ≤ 3072字
Revoke struct {
OldTaskId string `json:"old_task_id"` // 需要撤回的taskId
Force bool `json:"force"` // 在没有找到对应的taskId是否把对应appId下所有的通知都撤回
} `json:"revoke"` // 撤回消息时使用(仅撤回个推通道消息)
}
// IosPushChannelAlert 厂推消息通知内容
type IosPushChannelAlert struct {
Title string `json:"title"` // 通知消息标题
Body string `json:"body"` // 通知消息内容
ActionLocKey string `json:"action-loc-key"` // 用于多语言支持指定执行按钮所使用的Localizable.strings
LocKey string `json:"loc-key"` // 用于多语言支持指定Localizable.strings文件中相应的key
LocArgs []string `json:"loc-args"` // 如果loc-key中使用了占位符则在loc-args中指定各参数
LaunchImage string `json:"launch-image"` // 指定启动界面图片名
TitleLocKey string `json:"title-loc-key"` // (用于多语言支持对于标题指定执行按钮所使用的Localizable.strings,仅支持iOS8.2以上版本
TitleLocArgs []string `json:"title-loc-args"` // 对于标题,如果loc-key中使用的占位符则在loc-args中指定各参数,仅支持iOS8.2以上版本
Subtitle string `json:"subtitle"` // 通知子标题,仅支持iOS8.2以上版本
SubtitleLocKey string `json:"subtitle-loc-key"` // 当前本地化文件中的子标题字符串的关键字,仅支持iOS8.2以上版本
SubtitleLocArgs []string `json:"subtitle-loc-args"` // 当前本地化子标题内容中需要置换的变量参数 ,仅支持iOS8.2以上版本
}
// IosPushChannelAps 推送通知消息内容
type IosPushChannelAps struct {
Alert IosPushChannelAlert `json:"alert"` // 通知消息
ContentAvailable int64 `json:"content-available"` // 0表示普通通知消息(默认为0) 1表示静默推送(无通知栏消息)静默推送时不需要填写其他参数。苹果建议1小时最多推送3条静默消息
Sound string // 自定义铃声设置为包含后缀名的完整文件名示例值ring.mp3 系统铃声设置为default无声设置为com.gexin.ios.silence或不填
Category string `json:"category"` // 在客户端通知栏触发特定的action和button显示
ThreadId string `json:"thread-id"` // ios的远程通知通过该属性对通知进行分组仅支持iOS 12.0以上版本
Timestamp string `json:"timestamp"` // type为liveactivity时必填
Event string `json:"event"` // type为liveactivity时必填且值为update
ContentState struct{} `json:"content-state"` // type为liveactivity时必填
}
type IosApsDetail struct {
NotifyType string `json:"type"` // notify默认通知消息 voipvoip语音推送notifyapns通知消息liveactivity灵动岛推送(不支持p12证书)
Aps IosPushChannelAps `json:"aps"` // 推送通知消息内容
AutoBadge string `json:""` // 用于计算icon上显示的数字还可以实现显示数字的自动增减如“+1”、 “-1”、 “1” 等计算结果将覆盖badge
Payload string `json:"payload"` // 增加自定义的数据
Multimedia []struct { // 多媒体设置
Url string `json:"url"` // 多媒体资源地址
TypeMultimedia int64 `json:"type"` // 资源类型1.图片2.音频3.视频)
OnlyWifi bool `json:"only_wifi"` // 是否只在wifi环境下加载如果设置成true,但未使用wifi时会展示成普通通知
} `json:"multimedia"` // 多媒体设置
ApnsCollapseId string `json:"apns-collapse-id"` // 使用相同的apns-collapse-id可以覆盖之前的消息
}
type AndroidPushChannelUps struct {
Notification AndroidPushChannelNotification `json:"notification"` // 通知消息内容与transmission、revoke三选一都填写时报错。若希望客户端离线时直接在系统通知栏中展示通知栏消息推荐使用此参数。
Transmission string `json:"transmission"` // 透传消息内容与notification、revoke 三选一,都填写时报错,长度 ≤ 3072字
Revoke struct {
OldTaskId string `json:"old_task_id"` // 需要撤回的taskId
} // 撤回消息时使用(仅撤回厂商通道消息支持的厂商有小米、VIVO)与notification、transmission三选一都填写时报错(消息撤回请勿填写策略参数)
Options struct {
Constraint string `json:"constraint"` // 扩展内容对应厂商通道设置如ALL,HW,HO,XM(小米国内),XMG(小米海外),VV,OP,OPG(OPPO海外),MZ,UPS(UPS的参数会影响UPS下面的所有机型比如ST,SN等等)
Key string `json:"key"` // 厂商内容扩展字段
Value interface{} `json:"value"` // value的设置根据key值决定
} // 第三方厂商扩展内容
}
// AndroidPushChannelNotification 安卓厂推通知消息
type AndroidPushChannelNotification struct {
Title string `json:"title"` // 通知栏标题(长度建议取最小集)
Body string `json:"body"` // 通知栏内容(长度建议取最小集)
ClickType string `json:"click_type"` // 点击通知后续动作, 目前支持以下后续动作intent打开应用内特定页面(厂商都支持)url打开网页地址(厂商都支持;华为/荣耀要求https协议且游戏类应用不支持打开网页地址)startapp打开应用首页(厂商都支持)
Intent string `json:"intent"` // click_type为intent时必填
Url string `json:"url"` // click_type为url时必填
NotifyId int64 `json:"notify_id"` // 消息覆盖使用两条消息的notify_id相同新的消息会覆盖老的消息
}
// AndroidPushChannelDetail 安卓厂推
type AndroidPushChannelDetail struct {
Ups AndroidPushChannelUps `json:"ups"` // android厂商通道推送消息内容
}
// MpPushChannelDetail 小程序厂推
type MpPushChannelDetail struct {
Wx struct {
TemplateId string `json:"template_id"` // 所需下发的订阅模板id
Page string `json:"page"` // 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,示例index?foo=bar。该字段不填则模板无跳转
MiniProgramState string `json:"mini_program_state"` // 跳转小程序类型developer开发版 trial体验版formal正式版
Lang string `json:"lang"` // 进入小程序查看”的语言类型: zh_CN简体中文en_US英文zh_HK繁体中文zh_TW繁体中文
Data interface{} `json:"data"` // 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }的Json对象
} `json:"wx"` // 微信小程序通道推送消息内容
}
// PushChannelDetail 厂推消息说明
type PushChannelDetail struct {
Ios IosApsDetail `json:"ios"` // ios通道推送消息内容
Android AndroidPushChannelDetail `json:"android"` // android通道推送消息内容
Mp MpPushChannelDetail `json:"mp"` // miniProgram通道推送消息内容(只支持透传消息)
}
// SendMsgReq 发送msg
type SendMsgReq struct {
RequestId string `json:"request_id"` // 请求唯一标识号10-32位之间如果request_id重复会导致消息丢失
GroupName string `json:"group_name"` // 任务组名称
Audience AudienceCid `json:"audience"` // 推送目标用户
Settings SendMsgSetting `json:"settings"` // 推送条件设置
PushMessage PushMessageDetail `json:"push_message"` // 个推推送消息参数
PushChannel PushChannelDetail `json:"push_channel"` // 厂商推送消息参数包含ios消息参数android厂商消息参数
}

View File

@@ -0,0 +1,37 @@
package uinapp
import (
"errors"
"git.rosy.net.cn/baseapi/utils"
"net/http"
)
type SendMsgRes struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data struct {
TaskId struct {
Cid string `json:"cid"`
}
} `json:"data"`
}
func (a *API) SendMsgByUinApp(parma SendMsgReq) error {
if err := a.CheckTokenIsExpire(); err != nil {
return err
}
result, err := a.AccessAPI(BaseUrl+a.appId, PushMsgByCid, http.MethodPost, utils.Struct2MapByJson(parma))
if err != nil {
return err
}
var sendMsgRes *SendMsgRes
if err := utils.Map2StructByJson(result, &sendMsgRes, false); err != nil {
return err
}
if sendMsgRes.Code != 200 {
return errors.New(sendMsgRes.Msg)
}
return nil
}

View File

@@ -0,0 +1,131 @@
package uinapp
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.rosy.net.cn/baseapi/platformapi"
"git.rosy.net.cn/baseapi/utils"
"net/http"
"strings"
"sync"
"time"
)
type API struct {
appId string `json:"app_id"`
appKey string `json:"app_key"`
appSecret string `json:"app_secret"`
masterSecret string `json:"master_secret"`
token string `json:"token"`
expireTime string `json:"expire_time"`
locker sync.RWMutex
client *http.Client
config *platformapi.APIConfig
}
func NewUinappApi(appId, appKey, appSecret, masterSecret string, config ...*platformapi.APIConfig) *API {
curConfig := platformapi.DefAPIConfig
if len(config) > 0 {
curConfig = *config[0]
}
if appId == "" || masterSecret == "" {
return nil
}
return &API{
appId: appId,
appKey: appKey,
appSecret: appSecret,
masterSecret: masterSecret,
token: "",
expireTime: "",
locker: sync.RWMutex{},
client: &http.Client{Timeout: curConfig.ClientTimeout},
config: &curConfig,
}
}
func (a *API) AccessAPI(baseUrl, actionApi, method string, bizParams map[string]interface{}) (retVal map[string]interface{}, err error) {
// 序列化
data, err := json.Marshal(bizParams)
if err != nil {
return nil, err
}
// 全路径请求参数
fullURL := utils.GenerateGetURL(baseUrl, actionApi, nil)
// 发送请求
sendUrl := func() *http.Request {
var request *http.Request
if http.MethodPost == method {
request, _ = http.NewRequest(http.MethodPost, fullURL, strings.NewReader(string(data)))
} else {
request, _ = http.NewRequest(http.MethodGet, utils.GenerateGetURL(baseUrl, actionApi, bizParams), nil)
}
if actionApi != GetTokenInterface {
request.Header.Set("token", a.token)
}
request.Header.Set("Content-Type", "application/json;charset=utf-8")
return request
}
// 数据解析
dataMarshal := func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) {
if jsonResult1 == nil {
return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil")
}
if err != nil {
return "", err
}
if utils.MustInterface2Int64(jsonResult1["code"]) != 200 {
errLevel = platformapi.ErrLevelGeneralFail
err = utils.NewErrorCode(jsonResult1["msg"].(string), utils.Int64ToStr(utils.MustInterface2Int64(jsonResult1["code"])))
}
retVal = jsonResult1
return errLevel, err
}
err = platformapi.AccessPlatformAPIWithRetry(a.client, sendUrl, a.config, dataMarshal)
return retVal, err
}
func (a *API) signParam(timestamp string) (sig string) {
sig = a.appKey + timestamp + a.masterSecret
signature := sha256.Sum256([]byte(sig))
return hex.EncodeToString(signature[:])
}
// GetUinAppToken 获取token
func (a *API) GetUinAppToken() error {
timestamp := utils.Int64ToStr(time.Now().UnixNano() / 1e6)
sign := a.signParam(timestamp)
result, err := a.AccessAPI(BaseUrl+a.appId, GetTokenInterface, http.MethodPost, map[string]interface{}{"timestamp": timestamp, "sign": sign, "appkey": a.appKey})
fmt.Println(err.Error())
if err != nil && !strings.Contains(err.Error(), "success") {
return err
}
var appToken *AppTokenRes
if err := utils.Map2StructByJson(result, &appToken, false); err != nil {
return err
}
if appToken.Code != 0 && appToken.Msg != "success" {
return errors.New(appToken.Msg)
}
a.token = appToken.Data.Token
a.expireTime = appToken.Data.ExpireTime
return nil
}
// CheckTokenIsExpire 校验头肯是否过期
// 注鉴权接口每分钟最大调用量为100次每天最大调用量为10万次建议开发者妥善管理token以免达到限制影响推送.感觉不做缓存也够用了!
func (a *API) CheckTokenIsExpire() error {
// 没有token或者token过期了
if a.token == "" || a.expireTime == "" || utils.Str2Int64(a.expireTime) < (time.Now().UnixNano()/1e6) {
return a.GetUinAppToken()
}
return nil
}