feat(jpush): add vendor-specific message classification support for push notifications
- Add OPPO category/notify_level support (2024.11.20 regulation: category required when notify_level present) - Add Honor importance field (NORMAL=service/LOW=marketing) for notification classification - Add vivo category field (IM/ACCOUNT) for message scenario identification - Set Xiaomi channel/template defaults in code (system=153609, chat=153608, templates P10761/M10289) - Add classification option to JPush push payloads (0=operation, 1=system) - Update xiaomi template keywords mapping to match actual template placeholders - Add keyword search support for GetFollowers and GetFollowing endpoints - Bind new config fields to environment variables for OPPO/Honor/vivo
This commit is contained in:
@@ -292,7 +292,7 @@ func (c *Client) Push(payload map[string]any) (*PushResponse, *RateLimitInfo, er
|
||||
return result, rateLimit, parseErr
|
||||
}
|
||||
|
||||
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification, classification int, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
if len(registrationIDs) == 0 {
|
||||
return nil, fmt.Errorf("registration IDs cannot be empty")
|
||||
}
|
||||
@@ -310,7 +310,7 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
|
||||
"platform": "all",
|
||||
"audience": map[string]any{"registration_id": registrationIDs},
|
||||
"notification": notification,
|
||||
"options": c.buildOptions(tpc),
|
||||
"options": c.buildOptions(classification, tpc),
|
||||
}
|
||||
|
||||
var result *PushResponse
|
||||
@@ -337,7 +337,7 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) PushByAliases(aliases []string, notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
func (c *Client) PushByAliases(aliases []string, notification *Notification, classification int, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
if len(aliases) == 0 {
|
||||
return nil, fmt.Errorf("aliases cannot be empty")
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification, tpc
|
||||
"platform": "all",
|
||||
"audience": map[string]any{"alias": aliases},
|
||||
"notification": notification,
|
||||
"options": c.buildOptions(tpc),
|
||||
"options": c.buildOptions(classification, tpc),
|
||||
}
|
||||
|
||||
result, _, err := c.Push(payload)
|
||||
@@ -374,7 +374,7 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification, tpc
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) PushAll(notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
func (c *Client) PushAll(notification *Notification, classification int, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
c.logger.Info("jpush push all",
|
||||
zap.String("title", notification.Android.Title),
|
||||
zap.String("alert", notification.Alert),
|
||||
@@ -384,7 +384,7 @@ func (c *Client) PushAll(notification *Notification, tpc *ThirdPartyChannel) (*P
|
||||
"platform": "all",
|
||||
"audience": "all",
|
||||
"notification": notification,
|
||||
"options": c.buildOptions(tpc),
|
||||
"options": c.buildOptions(classification, tpc),
|
||||
}
|
||||
|
||||
result, _, err := c.Push(payload)
|
||||
@@ -402,9 +402,11 @@ func (c *Client) PushAll(notification *Notification, tpc *ThirdPartyChannel) (*P
|
||||
}
|
||||
|
||||
// buildOptions 构造 push payload 的 options 部分
|
||||
// 当 tpc 非空时加入 third_party_channel 字段,用于向各厂商下发 channel_id
|
||||
func (c *Client) buildOptions(tpc *ThirdPartyChannel) map[string]any {
|
||||
// classification 为消息分类(0=运营消息,1=系统消息)
|
||||
// tpc 非空时加入 third_party_channel 字段,用于向各厂商下发 channel_id
|
||||
func (c *Client) buildOptions(classification int, tpc *ThirdPartyChannel) map[string]any {
|
||||
opts := map[string]any{
|
||||
"classification": classification,
|
||||
"time_to_live": 86400,
|
||||
"apns_production": c.production,
|
||||
}
|
||||
|
||||
84
internal/pkg/jpush/vendor/converters.go
vendored
84
internal/pkg/jpush/vendor/converters.go
vendored
@@ -39,12 +39,32 @@ func (huaweiConverter) Convert(params MessageParams, cfg config.ChannelConfig) m
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.Huawei.System, cfg.Vendor.Huawei.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// vivoConverter VIVO 厂商转换器(仅 channel_id)
|
||||
// vivoConverter VIVO 厂商转换器
|
||||
// channel_id + category(厂商消息场景标识)
|
||||
// classification=1 时 category 必须为系统消息类,不携带 category 会默认按运营消息下发受频控限制
|
||||
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)
|
||||
v := cfg.Vendor.VIVO
|
||||
cid := resolveChannelID(params.Scene, v.System, v.Chat, cfg.System, cfg.Chat)
|
||||
category := v.VivoChatCategory
|
||||
if params.Scene == SceneSystem {
|
||||
category = v.VivoSystemCategory
|
||||
}
|
||||
|
||||
if cid == "" && category == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]any{}
|
||||
if cid != "" {
|
||||
m["channel_id"] = cid
|
||||
}
|
||||
if category != "" {
|
||||
m["category"] = category
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// meizuConverter 魅族厂商转换器(仅 channel_id)
|
||||
@@ -55,12 +75,33 @@ func (meizuConverter) Convert(params MessageParams, cfg config.ChannelConfig) ma
|
||||
return channelOnlyMap(params.Scene, cfg.Vendor.Meizu.System, cfg.Vendor.Meizu.Chat, cfg.System, cfg.Chat)
|
||||
}
|
||||
|
||||
// honorConverter 荣耀厂商转换器(仅 channel_id)
|
||||
// honorConverter 荣耀厂商转换器
|
||||
// channel_id + importance(通知栏消息智能分类)
|
||||
// importance 取值:"NORMAL"=服务与通讯,"LOW"=资讯营销
|
||||
// 注:classification 优先级更高,会覆盖 importance 设置的值
|
||||
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)
|
||||
h := cfg.Vendor.Honor
|
||||
cid := resolveChannelID(params.Scene, h.System, h.Chat, cfg.System, cfg.Chat)
|
||||
importance := h.HonorChatImportance
|
||||
if params.Scene == SceneSystem {
|
||||
importance = h.HonorSystemImportance
|
||||
}
|
||||
|
||||
if cid == "" && importance == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]any{}
|
||||
if cid != "" {
|
||||
m["channel_id"] = cid
|
||||
}
|
||||
if importance != "" {
|
||||
m["importance"] = importance
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// fcmConverter FCM 厂商转换器(仅 channel_id)
|
||||
@@ -104,12 +145,22 @@ func (xiaomiConverter) Convert(params MessageParams, cfg config.ChannelConfig) m
|
||||
}
|
||||
|
||||
// buildMiTemplateParam 生成小米消息模板参数 JSON 字符串
|
||||
// 默认占位符:sender / title / content,可按实际小米模板占位符在集中处调整
|
||||
// 占位符严格匹配小米模板定义的 keywords:
|
||||
// - 私聊模板(M10289 好友聊天): keywords1=title, keywords2=发送者, keywords3=内容
|
||||
// - 系统模板(P10761 系统消息): keywords1=标题, keywords2=内容
|
||||
func buildMiTemplateParam(params MessageParams) string {
|
||||
param := map[string]any{
|
||||
"sender": params.SenderName,
|
||||
"title": params.Title,
|
||||
"content": params.Body,
|
||||
var param map[string]any
|
||||
if params.Scene == SceneSystem {
|
||||
param = map[string]any{
|
||||
"keywords1": params.Title,
|
||||
"keywords2": params.Body,
|
||||
}
|
||||
} else {
|
||||
param = map[string]any{
|
||||
"keywords1": params.Title,
|
||||
"keywords2": params.SenderName,
|
||||
"keywords3": params.Body,
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
@@ -129,11 +180,15 @@ func (oppoConverter) Convert(params MessageParams, cfg config.ChannelConfig) map
|
||||
cid := resolveChannelID(params.Scene, o.System, o.Chat, cfg.System, cfg.Chat)
|
||||
// OPPO 私信模板按场景选择
|
||||
templateID := o.OppoChatPrivateTemplateID
|
||||
category := o.OppoChatCategory
|
||||
notifyLevel := o.OppoChatNotifyLevel
|
||||
if params.Scene == SceneSystem {
|
||||
templateID = o.OppoSystemPrivateTemplateID
|
||||
category = o.OppoSystemCategory
|
||||
notifyLevel = o.OppoSystemNotifyLevel
|
||||
}
|
||||
|
||||
if cid == "" && templateID == "" {
|
||||
if cid == "" && templateID == "" && category == "" && notifyLevel == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,5 +207,14 @@ func (oppoConverter) Convert(params MessageParams, cfg config.ChannelConfig) map
|
||||
"sender": params.SenderName,
|
||||
}
|
||||
}
|
||||
// OPPO 2024.11.20 新规:category 消息场景标识,notify_level 提醒等级
|
||||
// 文档约束:使用 notify_level 时 category 必传,否则 OPPO 校验失败
|
||||
// 这里做保护:notify_level 非零但 category 为空时跳过 notify_level
|
||||
if category != "" {
|
||||
m["category"] = category
|
||||
if notifyLevel != 0 {
|
||||
m["notify_level"] = notifyLevel
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
25
internal/pkg/jpush/vendor/params.go
vendored
25
internal/pkg/jpush/vendor/params.go
vendored
@@ -47,12 +47,24 @@ type MessageParams struct {
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
// 消息分类常量(对应 JPush options.classification)
|
||||
const (
|
||||
// ClassificationOperation 运营消息(0):营销/公告类,受厂商限速
|
||||
ClassificationOperation = 0
|
||||
// ClassificationSystem 系统消息(1):即时聊天/系统通知/互动通知,高优先级及时送达
|
||||
// 注:聊天即时消息也归为系统消息,避免被厂商运营消息限速影响送达
|
||||
ClassificationSystem = 1
|
||||
)
|
||||
|
||||
// PushPayload 工厂统一产物,client 据此组装最终推送 payload
|
||||
type PushPayload struct {
|
||||
// Notification 含 android + ios,由 TextRenderer 产出
|
||||
Notification *jpush.Notification
|
||||
// TPC 各厂商私有字段(options.third_party_channel),由 PrivateConverter 产出
|
||||
TPC *jpush.ThirdPartyChannel
|
||||
// Classification 消息分类(options.classification):0=运营消息,1=系统消息
|
||||
// 聊天和系统通知统一用 1,避免厂商运营消息限速
|
||||
Classification int
|
||||
}
|
||||
|
||||
// TextRenderer 通用文本渲染器(基础层)
|
||||
@@ -124,7 +136,16 @@ func (f *Factory) Build(params MessageParams, cfg config.ChannelConfig) *PushPay
|
||||
}
|
||||
|
||||
return &PushPayload{
|
||||
Notification: notification,
|
||||
TPC: tpc.Normalize(),
|
||||
Notification: notification,
|
||||
TPC: tpc.Normalize(),
|
||||
Classification: resolveClassification(params.Scene),
|
||||
}
|
||||
}
|
||||
|
||||
// resolveClassification 根据场景映射 JPush options.classification
|
||||
// 聊天即时消息与系统通知统一归为系统消息(1),避免厂商运营消息限速影响送达
|
||||
// 未来若引入营销/公告推送可扩展为运营消息(0)
|
||||
func resolveClassification(scene string) int {
|
||||
// system 与 chat 场景均使用系统消息分类
|
||||
return ClassificationSystem
|
||||
}
|
||||
|
||||
191
internal/pkg/jpush/vendor/payload_compat_test.go
vendored
191
internal/pkg/jpush/vendor/payload_compat_test.go
vendored
@@ -76,6 +76,26 @@ func TestPayloadEmptyConfigMatchesLegacy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
t.Logf("notification JSON: %s", jsonStr)
|
||||
|
||||
// 5. classification 必须为系统消息(1):聊天即时消息统一归系统消息避免限速
|
||||
if payload.Classification != ClassificationSystem {
|
||||
t.Errorf("Classification = %d, want %d (system)", payload.Classification, ClassificationSystem)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClassificationByScene 验证:system 与 chat 场景的 classification 均为 1(系统消息)
|
||||
// 聊天即时消息归为系统消息,避免厂商运营消息限速影响送达
|
||||
func TestClassificationByScene(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{}
|
||||
|
||||
for _, scene := range []string{SceneSystem, SceneChat} {
|
||||
params := MessageParams{Scene: scene, Title: "T", Body: "B"}
|
||||
payload := factory.Build(params, cfg)
|
||||
if payload.Classification != ClassificationSystem {
|
||||
t.Errorf("scene=%s: Classification = %d, want %d", scene, payload.Classification, ClassificationSystem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPayloadWithChannelConfig 验证:配置 channel_id 后正确下发,且不影响通用通知字段
|
||||
@@ -129,6 +149,177 @@ func TestPayloadXiaomiTemplateOptional(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOppoCategoryNotifyLevel 验证:OPPO category/notify_level 按场景区分,且 notify_level 必须伴 category
|
||||
func TestOppoCategoryNotifyLevel(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
|
||||
// 场景1:chat 用 IM + 16(强提醒)
|
||||
cfgChat := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
OPPO: config.VendorChannelID{
|
||||
Chat: "oppo_chat",
|
||||
OppoChatCategory: "IM",
|
||||
OppoChatNotifyLevel: 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfgChat)
|
||||
if payload.TPC == nil || payload.TPC.OPPO == nil {
|
||||
t.Fatal("OPPO TPC should exist for chat scene")
|
||||
}
|
||||
if payload.TPC.OPPO["category"] != "IM" {
|
||||
t.Errorf("OPPO chat category = %v, want IM", payload.TPC.OPPO["category"])
|
||||
}
|
||||
if payload.TPC.OPPO["notify_level"] != 16 {
|
||||
t.Errorf("OPPO chat notify_level = %v, want 16", payload.TPC.OPPO["notify_level"])
|
||||
}
|
||||
|
||||
// 场景2:system 用 ACCOUNT + 2(通知栏+锁屏)
|
||||
cfgSys := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
OPPO: config.VendorChannelID{
|
||||
System: "oppo_sys",
|
||||
OppoSystemCategory: "ACCOUNT",
|
||||
OppoSystemNotifyLevel: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfgSys)
|
||||
if payload.TPC.OPPO["category"] != "ACCOUNT" {
|
||||
t.Errorf("OPPO system category = %v, want ACCOUNT", payload.TPC.OPPO["category"])
|
||||
}
|
||||
if payload.TPC.OPPO["notify_level"] != 2 {
|
||||
t.Errorf("OPPO system notify_level = %v, want 2", payload.TPC.OPPO["notify_level"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOppoNotifyLevelRequiresCategory 验证:notify_level 配置但 category 未配置时,跳过 notify_level(避免 OPPO 校验失败)
|
||||
func TestOppoNotifyLevelRequiresCategory(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
OPPO: config.VendorChannelID{
|
||||
Chat: "oppo_chat",
|
||||
OppoChatNotifyLevel: 16, // 只配 notify_level,不配 category
|
||||
},
|
||||
},
|
||||
}
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC == nil || payload.TPC.OPPO == nil {
|
||||
t.Fatal("OPPO TPC should exist (has channel_id)")
|
||||
}
|
||||
if _, ok := payload.TPC.OPPO["notify_level"]; ok {
|
||||
t.Error("OPPO should NOT contain notify_level when category not configured")
|
||||
}
|
||||
if _, ok := payload.TPC.OPPO["category"]; ok {
|
||||
t.Error("OPPO should NOT contain category when not configured")
|
||||
}
|
||||
// channel_id 仍正常下发
|
||||
if payload.TPC.OPPO["channel_id"] != "oppo_chat" {
|
||||
t.Errorf("OPPO channel_id = %v, want oppo_chat", payload.TPC.OPPO["channel_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestHonorImportanceByScene 验证:荣耀 importance 按场景区分(chat=NORMAL, system=LOW)
|
||||
func TestHonorImportanceByScene(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
Honor: config.VendorChannelID{
|
||||
Chat: "honor_chat",
|
||||
System: "honor_sys",
|
||||
HonorChatImportance: "NORMAL",
|
||||
HonorSystemImportance: "LOW",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// chat 场景 -> NORMAL
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC == nil || payload.TPC.Honor == nil {
|
||||
t.Fatal("Honor TPC should exist for chat scene")
|
||||
}
|
||||
if payload.TPC.Honor["importance"] != "NORMAL" {
|
||||
t.Errorf("Honor chat importance = %v, want NORMAL", payload.TPC.Honor["importance"])
|
||||
}
|
||||
|
||||
// system 场景 -> LOW
|
||||
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC.Honor["importance"] != "LOW" {
|
||||
t.Errorf("Honor system importance = %v, want LOW", payload.TPC.Honor["importance"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestHonorImportanceOptional 验证:未配置 importance 时不下发,仅 channel_id
|
||||
func TestHonorImportanceOptional(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
Honor: config.VendorChannelID{Chat: "honor_chat"},
|
||||
},
|
||||
}
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC == nil || payload.TPC.Honor == nil {
|
||||
t.Fatal("Honor TPC should exist (has channel_id)")
|
||||
}
|
||||
if _, ok := payload.TPC.Honor["importance"]; ok {
|
||||
t.Error("Honor should NOT contain importance when not configured")
|
||||
}
|
||||
if payload.TPC.Honor["channel_id"] != "honor_chat" {
|
||||
t.Errorf("Honor channel_id = %v, want honor_chat", payload.TPC.Honor["channel_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestVivoCategoryByScene 验证:vivo category 按场景区分
|
||||
func TestVivoCategoryByScene(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
VIVO: config.VendorChannelID{
|
||||
Chat: "vivo_chat",
|
||||
System: "vivo_sys",
|
||||
VivoChatCategory: "IM",
|
||||
VivoSystemCategory: "ACCOUNT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// chat 场景 -> IM
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC == nil || payload.TPC.VIVO == nil {
|
||||
t.Fatal("VIVO TPC should exist for chat scene")
|
||||
}
|
||||
if payload.TPC.VIVO["category"] != "IM" {
|
||||
t.Errorf("VIVO chat category = %v, want IM", payload.TPC.VIVO["category"])
|
||||
}
|
||||
|
||||
// system 场景 -> ACCOUNT
|
||||
payload = factory.Build(MessageParams{Scene: SceneSystem, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC.VIVO["category"] != "ACCOUNT" {
|
||||
t.Errorf("VIVO system category = %v, want ACCOUNT", payload.TPC.VIVO["category"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestVivoCategoryOptional 验证:未配置 category 时不下发,仅 channel_id
|
||||
func TestVivoCategoryOptional(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
VIVO: config.VendorChannelID{Chat: "vivo_chat"},
|
||||
},
|
||||
}
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC == nil || payload.TPC.VIVO == nil {
|
||||
t.Fatal("VIVO TPC should exist (has channel_id)")
|
||||
}
|
||||
if _, ok := payload.TPC.VIVO["category"]; ok {
|
||||
t.Error("VIVO should NOT contain category when not configured")
|
||||
}
|
||||
if payload.TPC.VIVO["channel_id"] != "vivo_chat" {
|
||||
t.Errorf("VIVO channel_id = %v, want vivo_chat", payload.TPC.VIVO["channel_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
|
||||
131
internal/pkg/jpush/vendor/xiaomi_real_config_test.go
vendored
Normal file
131
internal/pkg/jpush/vendor/xiaomi_real_config_test.go
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"with_you/internal/config"
|
||||
)
|
||||
|
||||
// TestXiaomiConversion 验证:小米转换器对 channel_id/模板 id/keywords 映射的转换逻辑正确。
|
||||
// 注:默认值(153608/153609 + M10289/P10761)由 config.go 的 viper.SetDefault 提供,
|
||||
// 其生效性由 internal/config.TestXiaomiDefaultValuesFromViper 覆盖。
|
||||
// 本测试显式传入值,专注验证 vendor 转换逻辑(场景区分 + keywords 映射)。
|
||||
func TestXiaomiConversion(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
// 使用与代码默认值一致的配置(也可被环境变量覆盖为其他值)
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
Xiaomi: config.VendorChannelID{
|
||||
System: "153609",
|
||||
Chat: "153608",
|
||||
MiSystemTemplateID: "P10761",
|
||||
MiChatTemplateID: "M10289",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// === 场景1:私聊消息 ===
|
||||
chatParams := MessageParams{
|
||||
Scene: SceneChat,
|
||||
Title: "张三",
|
||||
Body: "你好,在吗?",
|
||||
SenderName: "张三",
|
||||
}
|
||||
chatPayload := factory.Build(chatParams, cfg)
|
||||
if chatPayload.TPC == nil || chatPayload.TPC.Xiaomi == nil {
|
||||
t.Fatal("Xiaomi TPC should exist for chat scene with default config")
|
||||
}
|
||||
|
||||
xiaomi := chatPayload.TPC.Xiaomi
|
||||
if xiaomi["channel_id"] != "153608" {
|
||||
t.Errorf("chat channel_id = %v, want 153608 (default)", xiaomi["channel_id"])
|
||||
}
|
||||
if xiaomi["mi_template_id"] != "M10289" {
|
||||
t.Errorf("chat mi_template_id = %v, want M10289 (default)", xiaomi["mi_template_id"])
|
||||
}
|
||||
|
||||
// 验证 mi_template_param 的 keywords 映射(M10289: keywords1=title, keywords2=发送者, keywords3=内容)
|
||||
paramStr, ok := xiaomi["mi_template_param"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("mi_template_param should be string, got %T", xiaomi["mi_template_param"])
|
||||
}
|
||||
var param map[string]any
|
||||
if err := json.Unmarshal([]byte(paramStr), ¶m); err != nil {
|
||||
t.Fatalf("mi_template_param not valid JSON: %v", err)
|
||||
}
|
||||
if param["keywords1"] != "张三" {
|
||||
t.Errorf("keywords1 = %v, want 张三", param["keywords1"])
|
||||
}
|
||||
if param["keywords2"] != "张三" {
|
||||
t.Errorf("keywords2 = %v, want 张三", param["keywords2"])
|
||||
}
|
||||
if param["keywords3"] != "你好,在吗?" {
|
||||
t.Errorf("keywords3 = %v, want 你好,在吗?", param["keywords3"])
|
||||
}
|
||||
t.Logf("chat 小米 payload: %s", mustJSON(xiaomi))
|
||||
|
||||
// === 场景2:系统消息 ===
|
||||
sysParams := MessageParams{
|
||||
Scene: SceneSystem,
|
||||
Title: "点赞通知",
|
||||
Body: "李四 赞了你的帖子",
|
||||
}
|
||||
sysPayload := factory.Build(sysParams, cfg)
|
||||
xiaomiSys := sysPayload.TPC.Xiaomi
|
||||
if xiaomiSys["channel_id"] != "153609" {
|
||||
t.Errorf("system channel_id = %v, want 153609 (default)", xiaomiSys["channel_id"])
|
||||
}
|
||||
if xiaomiSys["mi_template_id"] != "P10761" {
|
||||
t.Errorf("system mi_template_id = %v, want P10761 (default)", xiaomiSys["mi_template_id"])
|
||||
}
|
||||
|
||||
// 验证系统模板参数(P10761: keywords1=标题, keywords2=内容)
|
||||
sysParamStr := xiaomiSys["mi_template_param"].(string)
|
||||
var sysParam map[string]any
|
||||
json.Unmarshal([]byte(sysParamStr), &sysParam)
|
||||
if sysParam["keywords1"] != "点赞通知" {
|
||||
t.Errorf("system keywords1 = %v, want 点赞通知", sysParam["keywords1"])
|
||||
}
|
||||
if sysParam["keywords2"] != "李四 赞了你的帖子" {
|
||||
t.Errorf("system keywords2 = %v, want 李四 赞了你的帖子", sysParam["keywords2"])
|
||||
}
|
||||
if _, ok := sysParam["keywords3"]; ok {
|
||||
t.Error("system template should NOT have keywords3")
|
||||
}
|
||||
t.Logf("system 小米 payload: %s", mustJSON(xiaomiSys))
|
||||
|
||||
// === 验证完整 third_party_channel 结构 ===
|
||||
fullJSON := mustJSON(chatPayload.TPC)
|
||||
if !strings.Contains(fullJSON, `"channel_id":"153608"`) {
|
||||
t.Errorf("full TPC JSON missing channel_id 153608: %s", fullJSON)
|
||||
}
|
||||
if !strings.Contains(fullJSON, `"mi_template_id":"M10289"`) {
|
||||
t.Errorf("full TPC JSON missing mi_template_id M10289: %s", fullJSON)
|
||||
}
|
||||
t.Logf("完整 third_party_channel: %s", fullJSON)
|
||||
}
|
||||
|
||||
// TestXiaomiEnvOverride 验证:环境变量/配置值优先于代码默认值
|
||||
func TestXiaomiEnvOverride(t *testing.T) {
|
||||
factory := NewFactory()
|
||||
// 显式配置覆盖默认值(模拟环境变量覆盖)
|
||||
cfg := config.ChannelConfig{
|
||||
Vendor: config.VendorChannel{
|
||||
Xiaomi: config.VendorChannelID{
|
||||
System: "999999",
|
||||
Chat: "888888",
|
||||
MiChatTemplateID: "CUSTOM_TPL",
|
||||
MiSystemTemplateID: "SYS_TPL",
|
||||
},
|
||||
},
|
||||
}
|
||||
payload := factory.Build(MessageParams{Scene: SceneChat, Title: "T", Body: "B"}, cfg)
|
||||
if payload.TPC.Xiaomi["channel_id"] != "888888" {
|
||||
t.Errorf("override channel_id = %v, want 888888", payload.TPC.Xiaomi["channel_id"])
|
||||
}
|
||||
if payload.TPC.Xiaomi["mi_template_id"] != "CUSTOM_TPL" {
|
||||
t.Errorf("override mi_template_id = %v, want CUSTOM_TPL", payload.TPC.Xiaomi["mi_template_id"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user