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:
@@ -303,6 +303,13 @@ report:
|
||||
# APP_JPUSH_CHANNEL_XIAOMI_MI_SYSTEM_TEMPLATE_ID, APP_JPUSH_CHANNEL_XIAOMI_MI_CHAT_TEMPLATE_ID
|
||||
# OPPO 私信模板(可选,仅 OPPO,配置后下发私信时携带):
|
||||
# APP_JPUSH_CHANNEL_OPPO_SYSTEM_PRIVATE_TEMPLATE_ID, APP_JPUSH_CHANNEL_OPPO_CHAT_PRIVATE_TEMPLATE_ID
|
||||
# OPPO 消息分类(2024.11.20 新规,category 必传时 notify_level 才生效):
|
||||
# APP_JPUSH_CHANNEL_OPPO_SYSTEM_CATEGORY, APP_JPUSH_CHANNEL_OPPO_CHAT_CATEGORY
|
||||
# APP_JPUSH_CHANNEL_OPPO_SYSTEM_NOTIFY_LEVEL, APP_JPUSH_CHANNEL_OPPO_CHAT_NOTIFY_LEVEL
|
||||
# 荣耀通知栏消息智能分类(importance,NORMAL=服务通讯/LOW=资讯营销):
|
||||
# APP_JPUSH_CHANNEL_HONOR_SYSTEM_IMPORTANCE, APP_JPUSH_CHANNEL_HONOR_CHAT_IMPORTANCE
|
||||
# vivo 厂商消息场景标识(category,classification=1 时必须为系统消息类):
|
||||
# APP_JPUSH_CHANNEL_VIVO_SYSTEM_CATEGORY, APP_JPUSH_CHANNEL_VIVO_CHAT_CATEGORY
|
||||
# iOS 通知分组 thread-id:
|
||||
# APP_JPUSH_CHANNEL_IOS_CHAT_THREAD_ID, APP_JPUSH_CHANNEL_IOS_SYSTEM_THREAD_ID
|
||||
jpush:
|
||||
@@ -319,20 +326,25 @@ jpush:
|
||||
system: "" # 系统消息/通知默认 channel_id
|
||||
chat: "" # 私聊/群聊消息默认 channel_id
|
||||
vendor:
|
||||
xiaomi:
|
||||
system: ""
|
||||
chat: ""
|
||||
mi_system_template_id: "" # 小米系统消息模板 id
|
||||
mi_chat_template_id: "" # 小米私聊消息模板 id
|
||||
# xiaomi: 默认值已在代码内置(system=153609, chat=153608, mi_system_template_id=P10761, mi_chat_template_id=M10289)
|
||||
# 不在此声明,由 config.go 的 viper.SetDefault 提供,环境变量可覆盖
|
||||
xiaomi: {}
|
||||
huawei: { system: "", chat: "" }
|
||||
# OPPO: category/notify_level 默认值已在代码内置(chat=IM/16, system=ACCOUNT/2),不在此声明
|
||||
oppo:
|
||||
system: ""
|
||||
chat: ""
|
||||
oppo_system_private_template_id: "" # OPPO 系统消息私信模板 id
|
||||
oppo_chat_private_template_id: "" # OPPO 私聊消息私信模板 id
|
||||
vivo: { system: "", chat: "" }
|
||||
oppo_system_private_template_id: "" # OPPO 系统消息私信模板 id(需厂商后台注册)
|
||||
oppo_chat_private_template_id: "" # OPPO 私聊消息私信模板 id(需厂商后台注册)
|
||||
# vivo: category 默认值已在代码内置(chat=IM, system=ACCOUNT),不在此声明
|
||||
vivo:
|
||||
system: ""
|
||||
chat: ""
|
||||
meizu: { system: "", chat: "" }
|
||||
honor: { system: "", chat: "" }
|
||||
# 荣耀: importance 默认值已在代码内置(chat=NORMAL, system=LOW),不在此声明
|
||||
honor:
|
||||
system: ""
|
||||
chat: ""
|
||||
fcm: { system: "", chat: "" }
|
||||
# iOS APNs 通知分组(按 thread-id 区分系统/私聊通知,不配置则不分组)
|
||||
ios:
|
||||
|
||||
@@ -198,8 +198,8 @@ func Load(configPath string) (*Config, error) {
|
||||
// 各厂商 channel_id 通过环境变量配置(见下方 BindEnv)
|
||||
viper.SetDefault("jpush.channel.system", "")
|
||||
viper.SetDefault("jpush.channel.chat", "")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.system", "")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.chat", "")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.system", "153609")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.chat", "153608")
|
||||
viper.SetDefault("jpush.channel.vendor.huawei.system", "")
|
||||
viper.SetDefault("jpush.channel.vendor.huawei.chat", "")
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.system", "")
|
||||
@@ -213,11 +213,29 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.SetDefault("jpush.channel.vendor.fcm.system", "")
|
||||
viper.SetDefault("jpush.channel.vendor.fcm.chat", "")
|
||||
// 小米/OPPO 模板 id 默认值
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_system_template_id", "")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_chat_template_id", "")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_system_template_id", "P10761")
|
||||
viper.SetDefault("jpush.channel.vendor.xiaomi.mi_chat_template_id", "M10289")
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_system_private_template_id", "")
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_chat_private_template_id", "")
|
||||
// iOS 通知分组 thread-id 默认值
|
||||
// OPPO 消息分类默认值(category / notify_level)
|
||||
// OPPO 消息分类默认值(2024.11.20 新规)
|
||||
// chat: category=IM(即时通讯), notify_level=16(通知栏+锁屏+横幅+震动+铃声,强提醒)
|
||||
// system: category=ACCOUNT(账号动态/互动通知), notify_level=2(通知栏+锁屏,避免打扰)
|
||||
// 注:使用 notify_level 时 category 必传,此处两者同时配置
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_system_category", "ACCOUNT")
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_chat_category", "IM")
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_system_notify_level", 2)
|
||||
viper.SetDefault("jpush.channel.vendor.oppo.oppo_chat_notify_level", 16)
|
||||
// 荣耀 importance 默认值
|
||||
// chat: NORMAL(服务与通讯,及时送达), system: LOW(资讯营销,避免打扰)
|
||||
// 注:classification 优先级更高,会覆盖 importance
|
||||
viper.SetDefault("jpush.channel.vendor.honor.honor_system_importance", "LOW")
|
||||
viper.SetDefault("jpush.channel.vendor.honor.honor_chat_importance", "NORMAL")
|
||||
// vivo category 默认值(classification=1 时必须为系统消息类)
|
||||
// chat: IM(即时通讯), system: ACCOUNT(账号动态/互动通知)
|
||||
viper.SetDefault("jpush.channel.vendor.vivo.vivo_system_category", "ACCOUNT")
|
||||
viper.SetDefault("jpush.channel.vendor.vivo.vivo_chat_category", "IM")
|
||||
// iOS 通知分组 thread-id 默认值(留空,不分组)
|
||||
viper.SetDefault("jpush.channel.ios.chat_thread_id", "")
|
||||
viper.SetDefault("jpush.channel.ios.system_thread_id", "")
|
||||
// JPush 厂商 channel_id 环境变量显式绑定
|
||||
@@ -244,6 +262,17 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.BindEnv("jpush.channel.vendor.xiaomi.mi_chat_template_id", "APP_JPUSH_CHANNEL_XIAOMI_MI_CHAT_TEMPLATE_ID")
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_system_private_template_id", "APP_JPUSH_CHANNEL_OPPO_SYSTEM_PRIVATE_TEMPLATE_ID")
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_chat_private_template_id", "APP_JPUSH_CHANNEL_OPPO_CHAT_PRIVATE_TEMPLATE_ID")
|
||||
// OPPO category / notify_level 环境变量绑定
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_system_category", "APP_JPUSH_CHANNEL_OPPO_SYSTEM_CATEGORY")
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_chat_category", "APP_JPUSH_CHANNEL_OPPO_CHAT_CATEGORY")
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_system_notify_level", "APP_JPUSH_CHANNEL_OPPO_SYSTEM_NOTIFY_LEVEL")
|
||||
viper.BindEnv("jpush.channel.vendor.oppo.oppo_chat_notify_level", "APP_JPUSH_CHANNEL_OPPO_CHAT_NOTIFY_LEVEL")
|
||||
// 荣耀 importance 环境变量绑定
|
||||
viper.BindEnv("jpush.channel.vendor.honor.honor_system_importance", "APP_JPUSH_CHANNEL_HONOR_SYSTEM_IMPORTANCE")
|
||||
viper.BindEnv("jpush.channel.vendor.honor.honor_chat_importance", "APP_JPUSH_CHANNEL_HONOR_CHAT_IMPORTANCE")
|
||||
// vivo category 环境变量绑定
|
||||
viper.BindEnv("jpush.channel.vendor.vivo.vivo_system_category", "APP_JPUSH_CHANNEL_VIVO_SYSTEM_CATEGORY")
|
||||
viper.BindEnv("jpush.channel.vendor.vivo.vivo_chat_category", "APP_JPUSH_CHANNEL_VIVO_CHAT_CATEGORY")
|
||||
// iOS thread-id 环境变量绑定
|
||||
viper.BindEnv("jpush.channel.ios.chat_thread_id", "APP_JPUSH_CHANNEL_IOS_CHAT_THREAD_ID")
|
||||
viper.BindEnv("jpush.channel.ios.system_thread_id", "APP_JPUSH_CHANNEL_IOS_SYSTEM_THREAD_ID")
|
||||
|
||||
@@ -60,4 +60,24 @@ type VendorChannelID struct {
|
||||
// 见:OPUSH 私信模版校验能力接入说明
|
||||
OppoSystemPrivateTemplateID string `mapstructure:"oppo_system_private_template_id"` // 系统消息私信模板 id
|
||||
OppoChatPrivateTemplateID string `mapstructure:"oppo_chat_private_template_id"` // 私聊消息私信模板 id
|
||||
|
||||
// OPPO 消息分类(可选,2024.11.20 新规)
|
||||
// category 消息场景标识,使用 notify_level 时必传;notify_level 提醒等级
|
||||
// 取值:category 如 IM/ACCOUNT;notify_level 1=通知栏, 2=通知栏+锁屏, 16=通知栏+锁屏+横幅+震动+铃声
|
||||
OppoSystemCategory string `mapstructure:"oppo_system_category"` // 系统消息 category(如 ACCOUNT)
|
||||
OppoChatCategory string `mapstructure:"oppo_chat_category"` // 私聊消息 category(如 IM)
|
||||
OppoSystemNotifyLevel int `mapstructure:"oppo_system_notify_level"` // 系统消息提醒等级(如 2)
|
||||
OppoChatNotifyLevel int `mapstructure:"oppo_chat_notify_level"` // 私聊消息提醒等级(如 16)
|
||||
|
||||
// 荣耀通知栏消息智能分类(可选,importance 字段)
|
||||
// 取值:"NORMAL"=服务与通讯,"LOW"=资讯营销
|
||||
// 注:classification 优先级更高,会覆盖 importance 设置的值
|
||||
HonorSystemImportance string `mapstructure:"honor_system_importance"` // 系统消息 importance(如 LOW)
|
||||
HonorChatImportance string `mapstructure:"honor_chat_importance"` // 私聊消息 importance(如 NORMAL)
|
||||
|
||||
// vivo 厂商消息场景标识(可选,category 字段)
|
||||
// 用于标识消息类型,确定提醒方式;classification=1 时 category 必须为系统消息类
|
||||
// 不携带 category 会默认按运营消息下发,受频控限制
|
||||
VivoSystemCategory string `mapstructure:"vivo_system_category"` // 系统消息 category(如 ACCOUNT 账号动态)
|
||||
VivoChatCategory string `mapstructure:"vivo_chat_category"` // 私聊消息 category(如 IM 即时通讯)
|
||||
}
|
||||
|
||||
82
internal/config/jpush_default_test.go
Normal file
82
internal/config/jpush_default_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestXiaomiDefaultValuesFromViper 验证:config.yaml 中小米字段留空时,
|
||||
// viper.SetDefault 设置的默认值(153608/153609 + M10289/P10761)能正确填充到 Config 结构体。
|
||||
// 这证明:不配置环境变量/yaml 值时,代码内置默认值生效。
|
||||
func TestXiaomiDefaultValuesFromViper(t *testing.T) {
|
||||
// 加载仓库内的 config.yaml(小米字段已留空,依赖代码默认值)
|
||||
yamlPath := filepath.Join("..", "..", "configs", "config.yaml")
|
||||
cfg, err := Load(yamlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load config failed: %v", err)
|
||||
}
|
||||
|
||||
x := cfg.JPush.Channel.Vendor.Xiaomi
|
||||
if x.System != "153609" {
|
||||
t.Errorf("xiaomi.system default = %q, want 153609", x.System)
|
||||
}
|
||||
if x.Chat != "153608" {
|
||||
t.Errorf("xiaomi.chat default = %q, want 153608", x.Chat)
|
||||
}
|
||||
if x.MiSystemTemplateID != "P10761" {
|
||||
t.Errorf("xiaomi.mi_system_template_id default = %q, want P10761", x.MiSystemTemplateID)
|
||||
}
|
||||
if x.MiChatTemplateID != "M10289" {
|
||||
t.Errorf("xiaomi.mi_chat_template_id default = %q, want M10289", x.MiChatTemplateID)
|
||||
}
|
||||
t.Logf("小米默认值生效: system=%s chat=%s mi_sys=%s mi_chat=%s",
|
||||
x.System, x.Chat, x.MiSystemTemplateID, x.MiChatTemplateID)
|
||||
}
|
||||
|
||||
// TestVendorDefaultValuesFromViper 验证:OPPO/vivo/荣耀的分类字段在 yaml 留空时,
|
||||
// viper.SetDefault 设置的默认值能正确填充到 Config 结构体。
|
||||
// channel_id 类字段需厂商后台注册,不在默认值范围(留空)。
|
||||
func TestVendorDefaultValuesFromViper(t *testing.T) {
|
||||
yamlPath := filepath.Join("..", "..", "configs", "config.yaml")
|
||||
cfg, err := Load(yamlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load config failed: %v", err)
|
||||
}
|
||||
|
||||
// OPPO: chat=IM/16, system=ACCOUNT/2
|
||||
oppo := cfg.JPush.Channel.Vendor.OPPO
|
||||
if oppo.OppoChatCategory != "IM" {
|
||||
t.Errorf("oppo.oppo_chat_category default = %q, want IM", oppo.OppoChatCategory)
|
||||
}
|
||||
if oppo.OppoSystemCategory != "ACCOUNT" {
|
||||
t.Errorf("oppo.oppo_system_category default = %q, want ACCOUNT", oppo.OppoSystemCategory)
|
||||
}
|
||||
if oppo.OppoChatNotifyLevel != 16 {
|
||||
t.Errorf("oppo.oppo_chat_notify_level default = %d, want 16", oppo.OppoChatNotifyLevel)
|
||||
}
|
||||
if oppo.OppoSystemNotifyLevel != 2 {
|
||||
t.Errorf("oppo.oppo_system_notify_level default = %d, want 2", oppo.OppoSystemNotifyLevel)
|
||||
}
|
||||
|
||||
// vivo: chat=IM, system=ACCOUNT
|
||||
vivo := cfg.JPush.Channel.Vendor.VIVO
|
||||
if vivo.VivoChatCategory != "IM" {
|
||||
t.Errorf("vivo.vivo_chat_category default = %q, want IM", vivo.VivoChatCategory)
|
||||
}
|
||||
if vivo.VivoSystemCategory != "ACCOUNT" {
|
||||
t.Errorf("vivo.vivo_system_category default = %q, want ACCOUNT", vivo.VivoSystemCategory)
|
||||
}
|
||||
|
||||
// 荣耀: chat=NORMAL, system=LOW
|
||||
honor := cfg.JPush.Channel.Vendor.Honor
|
||||
if honor.HonorChatImportance != "NORMAL" {
|
||||
t.Errorf("honor.honor_chat_importance default = %q, want NORMAL", honor.HonorChatImportance)
|
||||
}
|
||||
if honor.HonorSystemImportance != "LOW" {
|
||||
t.Errorf("honor.honor_system_importance default = %q, want LOW", honor.HonorSystemImportance)
|
||||
}
|
||||
|
||||
t.Logf("OPPO: chat=%s/%d system=%s/%d", oppo.OppoChatCategory, oppo.OppoChatNotifyLevel, oppo.OppoSystemCategory, oppo.OppoSystemNotifyLevel)
|
||||
t.Logf("vivo: chat=%s system=%s", vivo.VivoChatCategory, vivo.VivoSystemCategory)
|
||||
t.Logf("荣耀: chat=%s system=%s", honor.HonorChatImportance, honor.HonorSystemImportance)
|
||||
}
|
||||
@@ -664,8 +664,9 @@ func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
pageSize := c.DefaultQuery("page_size", "20")
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
users, err := h.userService.GetFollowingList(c.Request.Context(), userID, page, pageSize)
|
||||
users, err := h.userService.GetFollowingList(c.Request.Context(), userID, page, pageSize, keyword)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, "failed to get following list")
|
||||
return
|
||||
@@ -710,8 +711,9 @@ func (h *UserHandler) GetFollowersList(c *gin.Context) {
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
pageSize := c.DefaultQuery("page_size", "20")
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
users, err := h.userService.GetFollowersList(c.Request.Context(), userID, page, pageSize)
|
||||
users, err := h.userService.GetFollowersList(c.Request.Context(), userID, page, pageSize, keyword)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, "failed to get followers list")
|
||||
return
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
21
internal/pkg/jpush/vendor/params.go
vendored
21
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 通用文本渲染器(基础层)
|
||||
@@ -126,5 +138,14 @@ func (f *Factory) Build(params MessageParams, cfg config.ChannelConfig) *PushPay
|
||||
return &PushPayload{
|
||||
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"])
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ type UserRepository interface {
|
||||
Update(user *model.User) error
|
||||
Delete(id string) error
|
||||
List(page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowers(userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowing(userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowers(userID, keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowing(userID, keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||||
CreateFollow(follow *model.Follow) error
|
||||
DeleteFollow(followerID, followingID string) error
|
||||
IsFollowing(followerID, followingID string) (bool, error)
|
||||
@@ -137,35 +137,49 @@ func (r *userRepository) List(page, pageSize int) ([]*model.User, int64, error)
|
||||
}
|
||||
|
||||
// GetFollowers 获取用户粉丝
|
||||
func (r *userRepository) GetFollowers(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
func (r *userRepository) GetFollowers(userID, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("following_id = ?", userID).Select("follower_id")
|
||||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||||
query := r.db.Model(&model.User{}).Where("id IN (?)", subQuery)
|
||||
if keyword != "" {
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
p := "%" + keyword + "%"
|
||||
query = query.Where("(username ILIKE ? OR nickname ILIKE ?)", p, p)
|
||||
} else {
|
||||
p := "%" + strings.ToLower(keyword) + "%"
|
||||
query = query.Where("(LOWER(username) LIKE ? OR LOWER(nickname) LIKE ?)", p, p)
|
||||
}
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?)", subQuery).
|
||||
Order("created_at DESC, id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&users).Error
|
||||
err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
// GetFollowing 获取用户关注
|
||||
func (r *userRepository) GetFollowing(userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
func (r *userRepository) GetFollowing(userID, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
var users []*model.User
|
||||
var total int64
|
||||
|
||||
subQuery := r.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
|
||||
r.db.Model(&model.User{}).Where("id IN (?)", subQuery).Count(&total)
|
||||
query := r.db.Model(&model.User{}).Where("id IN (?)", subQuery)
|
||||
if keyword != "" {
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
p := "%" + keyword + "%"
|
||||
query = query.Where("(username ILIKE ? OR nickname ILIKE ?)", p, p)
|
||||
} else {
|
||||
p := "%" + strings.ToLower(keyword) + "%"
|
||||
query = query.Where("(LOWER(username) LIKE ? OR LOWER(nickname) LIKE ?)", p, p)
|
||||
}
|
||||
}
|
||||
query.Count(&total)
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
err := r.db.Where("id IN (?)", subQuery).
|
||||
Order("created_at DESC, id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&users).Error
|
||||
err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&users).Error
|
||||
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ func (s *pushServiceImpl) pushViaJPush(ctx context.Context, device *model.Device
|
||||
}
|
||||
|
||||
payload := s.buildPayload(params)
|
||||
_, err := s.jpushClient.PushByRegistrationIDs([]string{device.PushToken}, payload.Notification, payload.TPC)
|
||||
_, err := s.jpushClient.PushByRegistrationIDs([]string{device.PushToken}, payload.Notification, payload.Classification, payload.TPC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jpush push failed: %w", err)
|
||||
}
|
||||
@@ -373,7 +373,7 @@ func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, params
|
||||
}
|
||||
|
||||
payload := s.buildPayload(params)
|
||||
_, err := s.jpushClient.PushByRegistrationIDs(regIDs, payload.Notification, payload.TPC)
|
||||
_, err := s.jpushClient.PushByRegistrationIDs(regIDs, payload.Notification, payload.Classification, payload.TPC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jpush batch push failed: %w", err)
|
||||
}
|
||||
@@ -819,7 +819,9 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
|
||||
largeIcon = groupAvatar
|
||||
}
|
||||
params := vendor.MessageParams{
|
||||
Scene: s.resolveChannelScene(message),
|
||||
// PushChatMessage 处理的是会话上下文内的消息(含群内通知),
|
||||
// 统一走 chat channel,保证会话内通知体验一致
|
||||
Scene: vendor.SceneChat,
|
||||
Title: title,
|
||||
Body: content,
|
||||
SenderName: sender.Name,
|
||||
@@ -831,7 +833,7 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
|
||||
Extra: extras,
|
||||
}
|
||||
payload := s.buildPayload(params)
|
||||
if _, pushErr := s.jpushClient.PushByRegistrationIDs(regIDs, payload.Notification, payload.TPC); pushErr != nil {
|
||||
if _, pushErr := s.jpushClient.PushByRegistrationIDs(regIDs, payload.Notification, payload.Classification, payload.TPC); pushErr != nil {
|
||||
zap.L().Error("jpush push chat message failed",
|
||||
zap.String("userID", userID),
|
||||
zap.String("conversationID", conversationID),
|
||||
@@ -932,6 +934,7 @@ func (s *pushServiceImpl) PushSystemNotification(ctx context.Context, userID str
|
||||
return ids
|
||||
}(),
|
||||
payload.Notification,
|
||||
payload.Classification,
|
||||
payload.TPC,
|
||||
)
|
||||
if pushErr == nil {
|
||||
|
||||
@@ -42,10 +42,10 @@ type UserService interface {
|
||||
ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error
|
||||
|
||||
// 关注相关
|
||||
GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowingList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
||||
GetFollowersList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
||||
GetFollowers(ctx context.Context, userID, keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowing(ctx context.Context, userID, keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||||
GetFollowingList(ctx context.Context, userID, page, pageSize, keyword string) ([]*model.User, error)
|
||||
GetFollowersList(ctx context.Context, userID, page, pageSize, keyword string) ([]*model.User, error)
|
||||
FollowUser(ctx context.Context, followerID, followeeID string) error
|
||||
UnfollowUser(ctx context.Context, followerID, followeeID string) error
|
||||
GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
|
||||
@@ -475,13 +475,13 @@ func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) erro
|
||||
}
|
||||
|
||||
// GetFollowers 获取粉丝
|
||||
func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
return s.userRepo.GetFollowers(userID, page, pageSize)
|
||||
func (s *userServiceImpl) GetFollowers(ctx context.Context, userID, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
return s.userRepo.GetFollowers(userID, keyword, page, pageSize)
|
||||
}
|
||||
|
||||
// GetFollowing 获取关注
|
||||
func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
return s.userRepo.GetFollowing(userID, page, pageSize)
|
||||
func (s *userServiceImpl) GetFollowing(ctx context.Context, userID, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||
return s.userRepo.GetFollowing(userID, keyword, page, pageSize)
|
||||
}
|
||||
|
||||
// FollowUser 关注用户
|
||||
@@ -597,7 +597,7 @@ func (s *userServiceImpl) IsBlockedBatch(ctx context.Context, blockerID string,
|
||||
}
|
||||
|
||||
// GetFollowingList 获取关注列表(字符串参数版本)
|
||||
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
||||
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize, keyword string) ([]*model.User, error) {
|
||||
pageInt := 1
|
||||
pageSizeInt := 20
|
||||
if page != "" {
|
||||
@@ -615,12 +615,12 @@ func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pa
|
||||
}
|
||||
}
|
||||
|
||||
users, _, err := s.userRepo.GetFollowing(userID, pageInt, pageSizeInt)
|
||||
users, _, err := s.userRepo.GetFollowing(userID, keyword, pageInt, pageSizeInt)
|
||||
return users, err
|
||||
}
|
||||
|
||||
// GetFollowersList 获取粉丝列表(字符串参数版本)
|
||||
func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
||||
func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize, keyword string) ([]*model.User, error) {
|
||||
pageInt := 1
|
||||
pageSizeInt := 20
|
||||
if page != "" {
|
||||
@@ -638,7 +638,7 @@ func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pa
|
||||
}
|
||||
}
|
||||
|
||||
users, _, err := s.userRepo.GetFollowers(userID, pageInt, pageSizeInt)
|
||||
users, _, err := s.userRepo.GetFollowers(userID, keyword, pageInt, pageSizeInt)
|
||||
return users, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user