diff --git a/configs/config.yaml b/configs/config.yaml index 0d9d6d2..ded28c0 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -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: diff --git a/internal/config/config.go b/internal/config/config.go index a417394..c093d4f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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") diff --git a/internal/config/jpush.go b/internal/config/jpush.go index d604881..0d21f80 100644 --- a/internal/config/jpush.go +++ b/internal/config/jpush.go @@ -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 即时通讯) } diff --git a/internal/config/jpush_default_test.go b/internal/config/jpush_default_test.go new file mode 100644 index 0000000..e777d20 --- /dev/null +++ b/internal/config/jpush_default_test.go @@ -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) +} diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 2dacf27..6075b94 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -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 diff --git a/internal/pkg/jpush/client.go b/internal/pkg/jpush/client.go index 563456a..87b0305 100644 --- a/internal/pkg/jpush/client.go +++ b/internal/pkg/jpush/client.go @@ -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, } diff --git a/internal/pkg/jpush/vendor/converters.go b/internal/pkg/jpush/vendor/converters.go index 8046051..fc5584a 100644 --- a/internal/pkg/jpush/vendor/converters.go +++ b/internal/pkg/jpush/vendor/converters.go @@ -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 } diff --git a/internal/pkg/jpush/vendor/params.go b/internal/pkg/jpush/vendor/params.go index 4ee3ee7..2261191 100644 --- a/internal/pkg/jpush/vendor/params.go +++ b/internal/pkg/jpush/vendor/params.go @@ -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 +} diff --git a/internal/pkg/jpush/vendor/payload_compat_test.go b/internal/pkg/jpush/vendor/payload_compat_test.go index 22bec37..ea6fefe 100644 --- a/internal/pkg/jpush/vendor/payload_compat_test.go +++ b/internal/pkg/jpush/vendor/payload_compat_test.go @@ -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) diff --git a/internal/pkg/jpush/vendor/xiaomi_real_config_test.go b/internal/pkg/jpush/vendor/xiaomi_real_config_test.go new file mode 100644 index 0000000..65c44e3 --- /dev/null +++ b/internal/pkg/jpush/vendor/xiaomi_real_config_test.go @@ -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"]) + } +} diff --git a/internal/repository/user_repo.go b/internal/repository/user_repo.go index f88b98f..c92455c 100644 --- a/internal/repository/user_repo.go +++ b/internal/repository/user_repo.go @@ -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 } diff --git a/internal/service/push_service.go b/internal/service/push_service.go index 02139aa..5dd4e4b 100644 --- a/internal/service/push_service.go +++ b/internal/service/push_service.go @@ -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 { diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 060a23c..0e9a50f 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -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 }