feat(server): implement chat file TTL cleanup and enhance JPush vendor channel support
Add FileCleanupWorker for automatic expiration of chat files with configurable retention period and batch processing. Files uploaded via `/api/v1/uploads/files` are tracked in `uploaded_files` table with expiration timestamps, then deleted from S3 and database upon expiry. Expired file URLs are injected as `"expired": true` in message responses. Extend JPush push notification configuration with vendor-specific channel_id support (xiaomi, huawei, oppo, vivo, meizu, honor, fcm) for differentiating system vs chat notifications. Add iOS APNs thread-id grouping configuration for notification categorization.
This commit is contained in:
156
internal/pkg/jpush/vendor/converters.go
vendored
Normal file
156
internal/pkg/jpush/vendor/converters.go
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"with_you/internal/config"
|
||||
)
|
||||
|
||||
// resolveChannelID 按场景选择 channel_id:厂商专属值优先,空则回退到全局默认
|
||||
// scene 为 SceneSystem 时取 system 配置,其余取 chat 配置
|
||||
func resolveChannelID(scene, vendorSys, vendorChat, defaultSys, defaultChat string) string {
|
||||
if scene == SceneSystem {
|
||||
if vendorSys != "" {
|
||||
return vendorSys
|
||||
}
|
||||
return defaultSys
|
||||
}
|
||||
if vendorChat != "" {
|
||||
return vendorChat
|
||||
}
|
||||
return defaultChat
|
||||
}
|
||||
|
||||
// channelOnlyMap 仅 channel_id 厂商的通用转换逻辑(厂商空则回退全局默认)
|
||||
// 最终 channel_id 为空时返回 nil(不下发该厂商)
|
||||
func channelOnlyMap(scene, vendorSys, vendorChat, defaultSys, defaultChat string) map[string]any {
|
||||
cid := resolveChannelID(scene, vendorSys, vendorChat, defaultSys, defaultChat)
|
||||
if cid == "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{"channel_id": cid}
|
||||
}
|
||||
|
||||
// huaweiConverter 华为厂商转换器(仅 channel_id)
|
||||
type huaweiConverter struct{}
|
||||
|
||||
func (huaweiConverter) VendorName() string { return "huawei" }
|
||||
func (huaweiConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.Huawei.System, cfg.Vendor.Huawei.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// vivoConverter VIVO 厂商转换器(仅 channel_id)
|
||||
type vivoConverter struct{}
|
||||
|
||||
func (vivoConverter) VendorName() string { return "vivo" }
|
||||
func (vivoConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.VIVO.System, cfg.Vendor.VIVO.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// meizuConverter 魅族厂商转换器(仅 channel_id)
|
||||
type meizuConverter struct{}
|
||||
|
||||
func (meizuConverter) VendorName() string { return "meizu" }
|
||||
func (meizuConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.Meizu.System, cfg.Vendor.Meizu.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// honorConverter 荣耀厂商转换器(仅 channel_id)
|
||||
type honorConverter struct{}
|
||||
|
||||
func (honorConverter) VendorName() string { return "honor" }
|
||||
func (honorConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.Honor.System, cfg.Vendor.Honor.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// fcmConverter FCM 厂商转换器(仅 channel_id)
|
||||
type fcmConverter struct{}
|
||||
|
||||
func (fcmConverter) VendorName() string { return "fcm" }
|
||||
func (fcmConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.FCM.System, cfg.Vendor.FCM.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// xiaomiConverter 小米厂商转换器
|
||||
// 配置 mi_template_id 时,私信消息下发时携带 channel_id 及 mi_template_id + mi_template_param
|
||||
type xiaomiConverter struct{}
|
||||
|
||||
func (xiaomiConverter) VendorName() string { return "xiaomi" }
|
||||
|
||||
func (xiaomiConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
x := cfg.Vendor.Xiaomi
|
||||
cid := resolveChannelID(params.Scene, x.System, x.Chat, cfg.System, cfg.Chat)
|
||||
// 小米模板按场景选择
|
||||
templateID := x.MiChatTemplateID
|
||||
if params.Scene == SceneSystem {
|
||||
templateID = x.MiSystemTemplateID
|
||||
}
|
||||
|
||||
// 最终 channel_id 与模板均未配置则跳过
|
||||
if cid == "" && templateID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]any{}
|
||||
if cid != "" {
|
||||
m["channel_id"] = cid
|
||||
}
|
||||
if templateID != "" {
|
||||
m["mi_template_id"] = templateID
|
||||
// mi_template_param 为模板参数的 JSON 字符串
|
||||
m["mi_template_param"] = buildMiTemplateParam(params)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// buildMiTemplateParam 生成小米消息模板参数 JSON 字符串
|
||||
// 默认占位符:sender / title / content,可按实际小米模板占位符在集中处调整
|
||||
func buildMiTemplateParam(params MessageParams) string {
|
||||
param := map[string]any{
|
||||
"sender": params.SenderName,
|
||||
"title": params.Title,
|
||||
"content": params.Body,
|
||||
}
|
||||
b, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// oppoConverter OPPO 厂商转换器
|
||||
// 配置 private_msg_template_id 时,私信消息下发时携带 private_msg_template_id + private_title_parameters + private_content_parameters
|
||||
type oppoConverter struct{}
|
||||
|
||||
func (oppoConverter) VendorName() string { return "oppo" }
|
||||
|
||||
func (oppoConverter) Convert(params MessageParams, cfg config.ChannelConfig) map[string]any {
|
||||
o := cfg.Vendor.OPPO
|
||||
cid := resolveChannelID(params.Scene, o.System, o.Chat, cfg.System, cfg.Chat)
|
||||
// OPPO 私信模板按场景选择
|
||||
templateID := o.OppoChatPrivateTemplateID
|
||||
if params.Scene == SceneSystem {
|
||||
templateID = o.OppoSystemPrivateTemplateID
|
||||
}
|
||||
|
||||
if cid == "" && templateID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]any{}
|
||||
if cid != "" {
|
||||
m["channel_id"] = cid
|
||||
}
|
||||
if templateID != "" {
|
||||
m["private_msg_template_id"] = templateID
|
||||
// private_title_parameters / private_content_parameters 为 JSONObject
|
||||
m["private_title_parameters"] = map[string]any{
|
||||
"title": params.Title,
|
||||
}
|
||||
m["private_content_parameters"] = map[string]any{
|
||||
"content": params.Body,
|
||||
"sender": params.SenderName,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
151
internal/pkg/jpush/vendor/params.go
vendored
Normal file
151
internal/pkg/jpush/vendor/params.go
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
// Package vendor 提供厂商无关的消息参数抽象与各厂商推送字段转换。
|
||||
//
|
||||
// 设计要点:
|
||||
// - 调用方只需提供厂商无关的 MessageParams,不感知厂商差异。
|
||||
// - 由于无法预知目标设备的具体厂商,JPush 要求所有厂商私有配置全数下发,
|
||||
// 由 JPush 服务端按目标设备厂商路由。
|
||||
// - 双层架构:TextRenderer(基础层,产出 Android/iOS 通用通知)+
|
||||
// PrivateConverter(附加层,产出各厂商 third_party_channel 私有字段)。
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"with_you/internal/config"
|
||||
"with_you/internal/pkg/jpush"
|
||||
)
|
||||
|
||||
// 场景常量
|
||||
const (
|
||||
// SceneSystem 系统消息/通知场景
|
||||
SceneSystem = "system"
|
||||
// SceneChat 私聊/群聊消息场景
|
||||
SceneChat = "chat"
|
||||
)
|
||||
|
||||
// MessageParams 厂商无关的统一消息参数(超集,用不上的字段厂商可忽略)
|
||||
type MessageParams struct {
|
||||
// Scene 决定使用哪类 channel/模板:SceneSystem 或 SceneChat
|
||||
Scene string
|
||||
// Title 通知标题(如发送者昵称、群名:发送者)
|
||||
Title string
|
||||
// Body 通知正文(消息内容预览)
|
||||
Body string
|
||||
// SenderName 发送者昵称
|
||||
SenderName string
|
||||
// SenderID 发送者 ID
|
||||
SenderID string
|
||||
// ConversationName 会话名称(群聊为群名)
|
||||
ConversationName string
|
||||
// ConversationID 会话 ID
|
||||
ConversationID string
|
||||
// MessageType 消息类型标识(chat_message / 系统通知类型),填充到 extras.notification_type
|
||||
MessageType string
|
||||
// ActionTime 操作时间(RFC3339)
|
||||
ActionTime string
|
||||
// LargeIcon 大图标 URL(群头像/发送者头像)
|
||||
LargeIcon string
|
||||
// Extra 兜底扩展字段,合并进 android/ios 的 extras
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
// PushPayload 工厂统一产物,client 据此组装最终推送 payload
|
||||
type PushPayload struct {
|
||||
// Notification 含 android + ios,由 TextRenderer 产出
|
||||
Notification *jpush.Notification
|
||||
// TPC 各厂商私有字段(options.third_party_channel),由 PrivateConverter 产出
|
||||
TPC *jpush.ThirdPartyChannel
|
||||
}
|
||||
|
||||
// TextRenderer 通用文本渲染器(基础层)
|
||||
// 产出所有厂商共享的通用通知:Android 通用通知 + iOS 通用通知
|
||||
type TextRenderer interface {
|
||||
// RenderAndroid 渲染 Android 通用通知(所有 Android 厂商共享)
|
||||
RenderAndroid(params MessageParams, cfg config.ChannelConfig) *jpush.AndroidNotif
|
||||
// RenderIOS 渲染 iOS 通用通知(按场景设置 thread-id 分组)
|
||||
RenderIOS(params MessageParams, cfg config.ChannelConfig) *jpush.IOSNotif
|
||||
}
|
||||
|
||||
// PrivateConverter 厂商私有转换器(附加层)
|
||||
// 产出 third_party_channel.{vendor} 的私有字段(channel_id、模板等)
|
||||
type PrivateConverter interface {
|
||||
// VendorName 返回厂商标识,对应 third_party_channel 的 key(xiaomi/oppo/huawei/...)
|
||||
VendorName() string
|
||||
// Convert 根据消息参数 + 全局配置产出该厂商私有字段 map
|
||||
// 厂商专属 channel_id 为空时回退到全局 cfg.System/Chat
|
||||
// 返回 nil 表示该厂商无需下发(如最终 channel_id 与模板均未配置)
|
||||
Convert(params MessageParams, cfg config.ChannelConfig) map[string]any
|
||||
}
|
||||
|
||||
// Factory 持有通用渲染器与全部厂商私有转换器
|
||||
type Factory struct {
|
||||
renderer TextRenderer
|
||||
privateConverters []PrivateConverter
|
||||
}
|
||||
|
||||
// NewFactory 创建工厂,注册默认渲染器与全部厂商转换器
|
||||
func NewFactory() *Factory {
|
||||
return &Factory{
|
||||
renderer: newDefaultRenderer(),
|
||||
privateConverters: []PrivateConverter{
|
||||
xiaomiConverter{},
|
||||
huaweiConverter{},
|
||||
oppoConverter{},
|
||||
vivoConverter{},
|
||||
meizuConverter{},
|
||||
honorConverter{},
|
||||
fcmConverter{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFactoryWith 自定义渲染器与转换器创建工厂(便于测试)
|
||||
func NewFactoryWith(renderer TextRenderer, converters []PrivateConverter) *Factory {
|
||||
return &Factory{renderer: renderer, privateConverters: converters}
|
||||
}
|
||||
|
||||
// Build 根据统一参数与配置,组装完整的 PushPayload
|
||||
// 调用通用渲染器产出 Notification,调用各私有转换器产出 ThirdPartyChannel
|
||||
func (f *Factory) Build(params MessageParams, cfg config.ChannelConfig) *PushPayload {
|
||||
android := f.renderer.RenderAndroid(params, cfg)
|
||||
ios := f.renderer.RenderIOS(params, cfg)
|
||||
|
||||
notification := &jpush.Notification{
|
||||
Alert: params.Body,
|
||||
Android: android,
|
||||
IOS: ios,
|
||||
}
|
||||
|
||||
tpc := &jpush.ThirdPartyChannel{}
|
||||
for _, c := range f.privateConverters {
|
||||
m := c.Convert(params, cfg)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
switch c.VendorName() {
|
||||
case "xiaomi":
|
||||
tpc.Xiaomi = m
|
||||
case "huawei":
|
||||
tpc.Huawei = m
|
||||
case "oppo":
|
||||
tpc.OPPO = m
|
||||
case "vivo":
|
||||
tpc.VIVO = m
|
||||
case "meizu":
|
||||
tpc.Meizu = m
|
||||
case "honor":
|
||||
tpc.Honor = m
|
||||
case "fcm":
|
||||
tpc.FCM = m
|
||||
}
|
||||
}
|
||||
|
||||
// 全部厂商字段为空则置 nil,避免序列化出空的 third_party_channel
|
||||
if tpc.Xiaomi == nil && tpc.Huawei == nil && tpc.OPPO == nil &&
|
||||
tpc.VIVO == nil && tpc.Meizu == nil && tpc.Honor == nil && tpc.FCM == nil {
|
||||
tpc = nil
|
||||
}
|
||||
|
||||
return &PushPayload{
|
||||
Notification: notification,
|
||||
TPC: tpc,
|
||||
}
|
||||
}
|
||||
135
internal/pkg/jpush/vendor/payload_compat_test.go
vendored
Normal file
135
internal/pkg/jpush/vendor/payload_compat_test.go
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"with_you/internal/config"
|
||||
)
|
||||
|
||||
// TestPayloadEmptyConfigMatchesLegacy 验证:未配置任何 channel/模板时,
|
||||
// Factory 产出的推送 payload 与改动前 BuildNotification 行为一致。
|
||||
//
|
||||
// 改动前 BuildNotification(title, body, "message", extras) 产出:
|
||||
// notification: { alert, android:{alert,title,extras:{notification_type}+extras,priority:1}, ios:{alert:{title,body},sound:default,badge:+1,extras} }
|
||||
// options: { time_to_live: 86400, apns_production } // 无 third_party_channel
|
||||
func TestPayloadEmptyConfigMatchesLegacy(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{} // 全空配置
|
||||
params := MessageParams{
|
||||
Scene: SceneChat,
|
||||
Title: "张三",
|
||||
Body: "你好",
|
||||
MessageType: "message",
|
||||
Extra: map[string]any{
|
||||
"message_id": "m1",
|
||||
"conversation_id": "c1",
|
||||
"sender_id": "u1",
|
||||
"category": "chat",
|
||||
},
|
||||
}
|
||||
|
||||
payload := factory.Build(params, cfg)
|
||||
|
||||
// 1. TPC 必须为 nil(无 third_party_channel 下发,与改动前一致)
|
||||
if payload.TPC != nil {
|
||||
t.Fatalf("TPC should be nil when all channel config empty, got: %+v", payload.TPC)
|
||||
}
|
||||
|
||||
notif := payload.Notification
|
||||
if notif == nil || notif.Android == nil || notif.IOS == nil {
|
||||
t.Fatalf("notification/android/ios must not be nil")
|
||||
}
|
||||
|
||||
// 2. Android 字段对比
|
||||
if notif.Android.Alert != "你好" || notif.Android.Title != "张三" || notif.Android.Priority != 1 {
|
||||
t.Errorf("Android basic fields mismatch: %+v", notif.Android)
|
||||
}
|
||||
if notif.Android.ChannelID != "" {
|
||||
t.Errorf("Android.ChannelID = %q, want empty", notif.Android.ChannelID)
|
||||
}
|
||||
if notif.Android.Extras["notification_type"] != "message" || notif.Android.Extras["message_id"] != "m1" {
|
||||
t.Errorf("Android.Extras mismatch: %+v", notif.Android.Extras)
|
||||
}
|
||||
|
||||
// 3. iOS 字段对比
|
||||
if notif.IOS.Sound != "default" || notif.IOS.Badge != "+1" {
|
||||
t.Errorf("IOS basic fields mismatch: %+v", notif.IOS)
|
||||
}
|
||||
if notif.IOS.ThreadID != "" {
|
||||
t.Errorf("IOS.ThreadID = %q, want empty", notif.IOS.ThreadID)
|
||||
}
|
||||
if notif.IOS.Extras["notification_type"] != "message" {
|
||||
t.Errorf("IOS.Extras[notification_type] mismatch: %+v", notif.IOS.Extras)
|
||||
}
|
||||
|
||||
// 4. 序列化 JSON 确认厂商私有字段不出现
|
||||
jsonBytes, err := json.Marshal(notif)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
jsonStr := string(jsonBytes)
|
||||
for _, key := range []string{`"channel_id"`, `"thread-id"`, `"third_party_channel"`, `"mi_template"`, `"private_msg_template_id"`} {
|
||||
if strings.Contains(jsonStr, key) {
|
||||
t.Errorf("notification JSON should not contain %s when config empty, got: %s", key, jsonStr)
|
||||
}
|
||||
}
|
||||
t.Logf("notification JSON: %s", jsonStr)
|
||||
}
|
||||
|
||||
// TestPayloadWithChannelConfig 验证:配置 channel_id 后正确下发,且不影响通用通知字段
|
||||
func TestPayloadWithChannelConfig(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
System: "sys_channel",
|
||||
Chat: "chat_channel",
|
||||
}
|
||||
params := MessageParams{Scene: SceneChat, Title: "T", Body: "B", MessageType: "chat_message"}
|
||||
|
||||
payload := factory.Build(params, cfg)
|
||||
|
||||
// 通用通知仍正常
|
||||
if payload.Notification.Android.Title != "T" {
|
||||
t.Errorf("Android.Title = %q, want T", payload.Notification.Android.Title)
|
||||
}
|
||||
// Android channel_id 跟随场景(chat)
|
||||
if payload.Notification.Android.ChannelID != "chat_channel" {
|
||||
t.Errorf("Android.ChannelID = %q, want chat_channel", payload.Notification.Android.ChannelID)
|
||||
}
|
||||
// TPC 各厂商 channel_id 下发(华为/VIVO 等回退全局)
|
||||
if payload.TPC == nil {
|
||||
t.Fatal("TPC should not be nil when channel config present")
|
||||
}
|
||||
if payload.TPC.Huawei["channel_id"] != "chat_channel" {
|
||||
t.Errorf("Huawei channel_id = %v, want chat_channel", payload.TPC.Huawei["channel_id"])
|
||||
}
|
||||
t.Logf("TPC JSON: %s", mustJSON(payload.TPC))
|
||||
}
|
||||
|
||||
// TestPayloadXiaomiTemplateOptional 验证:仅配 channel_id 不配模板时,小米不下发模板字段
|
||||
func TestPayloadXiaomiTemplateOptional(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
Xiaomi: config.VendorChannelID{Chat: "mi_chat"},
|
||||
},
|
||||
}
|
||||
params := MessageParams{Scene: SceneChat, Title: "T", Body: "B"}
|
||||
|
||||
payload := factory.Build(params, cfg)
|
||||
if payload.TPC == nil || payload.TPC.Xiaomi == nil {
|
||||
t.Fatal("Xiaomi TPC should exist")
|
||||
}
|
||||
if _, ok := payload.TPC.Xiaomi["mi_template_id"]; ok {
|
||||
t.Error("Xiaomi should NOT contain mi_template_id when template not configured")
|
||||
}
|
||||
if payload.TPC.Xiaomi["channel_id"] != "mi_chat" {
|
||||
t.Errorf("Xiaomi channel_id = %v, want mi_chat", payload.TPC.Xiaomi["channel_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
}
|
||||
61
internal/pkg/jpush/vendor/text_renderer.go
vendored
Normal file
61
internal/pkg/jpush/vendor/text_renderer.go
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"with_you/internal/config"
|
||||
"with_you/internal/pkg/jpush"
|
||||
)
|
||||
|
||||
// defaultRenderer 默认通用文本渲染器
|
||||
type defaultRenderer struct{}
|
||||
|
||||
func newDefaultRenderer() *defaultRenderer {
|
||||
return &defaultRenderer{}
|
||||
}
|
||||
|
||||
// mergeExtras 合并基础 extras 与额外 extras,返回新 map
|
||||
func mergeExtras(messageType string, extra map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(extra)+1)
|
||||
if messageType != "" {
|
||||
out["notification_type"] = messageType
|
||||
}
|
||||
for k, v := range extra {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RenderAndroid 渲染 Android 通用通知
|
||||
// 所有 Android 厂商共享:alert/title/channel_id/extras/large_icon/priority
|
||||
func (r *defaultRenderer) RenderAndroid(params MessageParams, cfg config.ChannelConfig) *jpush.AndroidNotif {
|
||||
channelID := cfg.Chat
|
||||
if params.Scene == SceneSystem {
|
||||
channelID = cfg.System
|
||||
}
|
||||
return &jpush.AndroidNotif{
|
||||
Alert: params.Body,
|
||||
Title: params.Title,
|
||||
ChannelID: channelID,
|
||||
LargeIcon: params.LargeIcon,
|
||||
Extras: mergeExtras(params.MessageType, params.Extra),
|
||||
Priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// RenderIOS 渲染 iOS 通用通知
|
||||
// iOS 走 APNs,通过 thread-id 实现按场景的通知分组
|
||||
func (r *defaultRenderer) RenderIOS(params MessageParams, cfg config.ChannelConfig) *jpush.IOSNotif {
|
||||
threadID := cfg.IOS.ChatThreadID
|
||||
if params.Scene == SceneSystem {
|
||||
threadID = cfg.IOS.SystemThreadID
|
||||
}
|
||||
return &jpush.IOSNotif{
|
||||
Alert: map[string]any{
|
||||
"title": params.Title,
|
||||
"body": params.Body,
|
||||
},
|
||||
Sound: "default",
|
||||
Badge: "+1",
|
||||
ThreadID: threadID,
|
||||
Extras: mergeExtras(params.MessageType, params.Extra),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user