feat(jpush): add vendor-specific message classification support for push notifications
All checks were successful
Build Backend / build (push) Successful in 3m4s
Build Backend / build-docker (push) Successful in 1m15s

- 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:
lafay
2026-06-18 19:41:32 +08:00
parent 5b83f98fb8
commit 821e066446
13 changed files with 637 additions and 66 deletions

View File

@@ -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
# 荣耀通知栏消息智能分类importanceNORMAL=服务通讯/LOW=资讯营销):
# APP_JPUSH_CHANNEL_HONOR_SYSTEM_IMPORTANCE, APP_JPUSH_CHANNEL_HONOR_CHAT_IMPORTANCE
# vivo 厂商消息场景标识categoryclassification=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:

View File

@@ -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")

View File

@@ -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/ACCOUNTnotify_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 即时通讯)
}

View 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)
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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.classification0=运营消息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
}

View File

@@ -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()
// 场景1chat 用 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"])
}
// 场景2system 用 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)

View 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), &param); 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"])
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}