refactor: unify code formatting and improve push/search implementations
All checks were successful
Build Backend / build (push) Successful in 3m54s
Build Backend / build-docker (push) Successful in 1m9s

- Remove BOM from all Go source files
- Standardize import ordering and whitespace across handlers and services
- Replace PostgreSQL full-text search with ILIKE pattern matching in post and user repositories
- Enhance JPush client with TLS transport, rate limit monitoring, and simplified API
- Refactor PushService to include userID in device management methods
- Add MaxDevicesPerUser limit and extract helper functions for push payload construction
This commit is contained in:
lafay
2026-04-28 14:53:04 +08:00
parent 179e468131
commit 67a5660952
67 changed files with 356 additions and 351 deletions

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,10 +1,10 @@
package handler
package handler
import (
"strconv"
"with_you/internal/dto"
"with_you/internal/pkg/response"
"with_you/internal/service"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"with_you/internal/model"
@@ -120,4 +120,3 @@ func (h *ChannelHandler) AdminDelete(c *gin.Context) {
}
response.Success(c, gin.H{"success": true})
}

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"errors"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"errors"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"context"
@@ -940,7 +940,7 @@ func (h *MessageHandler) HandleSetConversationNotificationMuted(c *gin.Context)
}
response.SuccessWithMessage(c, "conversation notification_muted status updated", gin.H{
"conversation_id": conversationID,
"conversation_id": conversationID,
"notification_muted": req.NotificationMuted,
})
}

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"with_you/internal/dto"
@@ -67,7 +67,7 @@ func (h *PushHandler) UnregisterDevice(c *gin.Context) {
return
}
err := h.pushService.UnregisterDevice(c.Request.Context(), deviceID)
err := h.pushService.UnregisterDevice(c.Request.Context(), userID, deviceID)
if err != nil {
response.InternalServerError(c, "failed to unregister device")
return
@@ -148,7 +148,7 @@ func (h *PushHandler) UpdateDeviceToken(c *gin.Context) {
return
}
err := h.pushService.UpdateDeviceToken(c.Request.Context(), deviceID, req.PushToken)
err := h.pushService.UpdateDeviceToken(c.Request.Context(), userID, deviceID, req.PushToken)
if err != nil {
response.InternalServerError(c, "failed to update device token")
return

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"with_you/internal/dto"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"errors"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"errors"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"github.com/gin-gonic/gin"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package handler
package handler
import (
"errors"

View File

@@ -2,10 +2,12 @@ package jpush
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
@@ -16,8 +18,8 @@ import (
const (
PushAPIBaseURL = "https://api.jpush.cn/v3"
DeviceAPIBaseURL = "https://device.jpush.cn/v3"
ReportAPIBaseURL = "https://report.jpush.cn/v3"
DeviceAPIBaseURL = "https://device.jpush.cn/v3"
ReportAPIBaseURL = "https://report.jpush.cn/v3"
)
type Client struct {
@@ -26,14 +28,25 @@ type Client struct {
production bool
httpClient *http.Client
logger *zap.Logger
lastRateLimit *RateLimitInfo
}
func NewClient(appKey, masterSecret string, production bool, logger *zap.Logger) *Client {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
return &Client{
appKey: appKey,
masterSecret: masterSecret,
production: production,
httpClient: &http.Client{Timeout: 30 * time.Second},
httpClient: &http.Client{Timeout: 30 * time.Second, Transport: transport},
logger: logger,
}
}
@@ -79,9 +92,9 @@ type IOSNotif struct {
}
type PushOptions struct {
TimeToLive int `json:"time_to_live,omitempty"`
ApnsProduction bool `json:"apns_production"`
ApnsCollapseID string `json:"apns_collapse_id,omitempty"`
TimeToLive int `json:"time_to_live,omitempty"`
ApnsProduction bool `json:"apns_production"`
ApnsCollapseID string `json:"apns_collapse_id,omitempty"`
}
type PushResponse struct {
@@ -96,7 +109,7 @@ type RateLimitInfo struct {
}
type APIError struct {
ErrCode int `json:"code"`
ErrCode int `json:"code"`
ErrMessage string `json:"message"`
ErrDetail struct {
Code int `json:"code"`
@@ -199,6 +212,14 @@ func (c *Client) Push(payload map[string]any) (*PushResponse, *RateLimitInfo, er
return nil, rateLimit, err
}
c.lastRateLimit = rateLimit
if rateLimit != nil && rateLimit.Limit > 0 && rateLimit.Remaining < rateLimit.Limit/10 {
c.logger.Warn("jpush rate limit approaching",
zap.Int("remaining", rateLimit.Remaining),
zap.Int("limit", rateLimit.Limit),
)
}
result, parseErr := c.parseResponse(resp, respBody)
if parseErr != nil {
c.logger.Error("jpush push response parse failed",
@@ -214,7 +235,7 @@ func (c *Client) Push(payload map[string]any) (*PushResponse, *RateLimitInfo, er
return result, rateLimit, parseErr
}
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification, extras map[string]any) (*PushResponse, error) {
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification) (*PushResponse, error) {
if len(registrationIDs) == 0 {
return nil, fmt.Errorf("registration IDs cannot be empty")
}
@@ -228,8 +249,6 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
zap.String("alert", notification.Alert),
)
mergeExtras(notification, extras)
payload := map[string]any{
"platform": "all",
"audience": map[string]any{"registration_id": registrationIDs},
@@ -256,7 +275,7 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
return result, nil
}
func (c *Client) PushByAliases(aliases []string, notification *Notification, extras map[string]any) (*PushResponse, error) {
func (c *Client) PushByAliases(aliases []string, notification *Notification) (*PushResponse, error) {
if len(aliases) == 0 {
return nil, fmt.Errorf("aliases cannot be empty")
}
@@ -270,8 +289,6 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification, ext
zap.String("alert", notification.Alert),
)
mergeExtras(notification, extras)
payload := map[string]any{
"platform": "all",
"audience": map[string]any{"alias": aliases},
@@ -295,7 +312,7 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification, ext
zap.Int("count", len(aliases)),
zap.String("msg_id", result.MsgID),
)
return result, err
return result, nil
}
func (c *Client) PushAll(notification *Notification) (*PushResponse, error) {
@@ -325,29 +342,7 @@ func (c *Client) PushAll(notification *Notification) (*PushResponse, error) {
c.logger.Info("jpush push all success",
zap.String("msg_id", result.MsgID),
)
return result, err
}
func mergeExtras(notification *Notification, extras map[string]any) {
if extras == nil {
return
}
if notification.Android != nil {
if notification.Android.Extras == nil {
notification.Android.Extras = make(map[string]any)
}
for k, v := range extras {
notification.Android.Extras[k] = v
}
}
if notification.IOS != nil {
if notification.IOS.Extras == nil {
notification.IOS.Extras = make(map[string]any)
}
for k, v := range extras {
notification.IOS.Extras[k] = v
}
}
return result, nil
}
func BuildNotification(title, body string, notificationType string, extras map[string]any) *Notification {
@@ -394,7 +389,7 @@ func (c *Client) GetDeviceInfo(registrationID string) (*DeviceInfo, error) {
zap.String("registration_id", registrationID),
)
path := DeviceAPIBaseURL + "/v3/devices/" + url.PathEscape(registrationID)
path := DeviceAPIBaseURL + "/devices/" + url.PathEscape(registrationID)
resp, body, _, err := c.doRequest("GET", path, nil)
if err != nil {
return nil, err
@@ -427,7 +422,7 @@ func (c *Client) SetDeviceAlias(registrationID string, alias string) error {
zap.String("alias", alias),
)
path := DeviceAPIBaseURL + "/v3/devices/" + url.PathEscape(registrationID)
path := DeviceAPIBaseURL + "/devices/" + url.PathEscape(registrationID)
payload := map[string]any{
"alias": alias,
}
@@ -461,7 +456,7 @@ func (c *Client) SetDeviceTags(registrationID string, addTags, removeTags []stri
zap.Strings("remove_tags", removeTags),
)
path := DeviceAPIBaseURL + "/v3/devices/" + url.PathEscape(registrationID)
path := DeviceAPIBaseURL + "/devices/" + url.PathEscape(registrationID)
tags := map[string]any{}
if len(addTags) > 0 {
tags["add"] = addTags
@@ -499,7 +494,7 @@ func (c *Client) GetAliasRegistrationIDs(alias string) ([]string, error) {
zap.String("alias", alias),
)
path := DeviceAPIBaseURL + "/v3/aliases/" + url.PathEscape(alias)
path := DeviceAPIBaseURL + "/aliases/" + url.PathEscape(alias)
resp, body, _, err := c.doRequest("GET", path, nil)
if err != nil {
return nil, err
@@ -532,7 +527,7 @@ func (c *Client) DeleteAlias(alias string) error {
zap.String("alias", alias),
)
path := DeviceAPIBaseURL + "/v3/aliases/" + url.PathEscape(alias)
path := DeviceAPIBaseURL + "/aliases/" + url.PathEscape(alias)
resp, body, _, err := c.doRequest("DELETE", path, nil)
if err != nil {
return err
@@ -559,7 +554,7 @@ func (c *Client) RemoveAliasDevices(alias string, registrationIDs []string) erro
zap.Int("count", len(registrationIDs)),
)
path := DeviceAPIBaseURL + "/v3/aliases/" + url.PathEscape(alias)
path := DeviceAPIBaseURL + "/aliases/" + url.PathEscape(alias)
payload := map[string]any{
"registration_ids": map[string]any{
"remove": registrationIDs,

View File

@@ -510,11 +510,8 @@ func (r *postRepository) Search(keyword string, page, pageSize int) ([]*model.Po
// 搜索标题和内容
if keyword != "" {
if r.db.Dialector.Name() == "postgres" {
// PostgreSQL 使用全文检索表达式,为 pg_trgm/GIN 索引升级预留路径
query = query.Where(
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
keyword,
)
searchPattern := "%" + keyword + "%"
query = query.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
} else {
searchPattern := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
@@ -880,10 +877,8 @@ func (r *postRepository) AdminList(query AdminPostListQuery) ([]*model.Post, int
// 关键词搜索(标题或内容)
if query.Keyword != "" {
if r.db.Dialector.Name() == "postgres" {
db = db.Where(
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
query.Keyword,
)
searchPattern := "%" + query.Keyword + "%"
db = db.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
} else {
searchPattern := "%" + query.Keyword + "%"
db = db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
@@ -1130,11 +1125,8 @@ func (r *postRepository) SearchPostsByCursor(ctx context.Context, keyword string
// 搜索标题和内容
if keyword != "" {
if r.db.Dialector.Name() == "postgres" {
// PostgreSQL 使用全文检索表达式
query = query.Where(
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
keyword,
)
searchPattern := "%" + keyword + "%"
query = query.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
} else {
searchPattern := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)

View File

@@ -439,9 +439,10 @@ func (r *userRepository) Search(keyword string, page, pageSize int) ([]*model.Us
// 搜索用户名、昵称、简介
if keyword != "" {
if r.db.Dialector.Name() == "postgres" {
searchPattern := "%" + keyword + "%"
query = query.Where(
"to_tsvector('simple', COALESCE(username, '') || ' ' || COALESCE(nickname, '') || ' ' || COALESCE(bio, '')) @@ plainto_tsquery('simple', ?)",
keyword,
"username ILIKE ? OR nickname ILIKE ? OR bio ILIKE ?",
searchPattern, searchPattern, searchPattern,
)
} else {
// SQLite: 使用 LOWER() 实现大小写不敏感的模糊搜索

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"
@@ -26,15 +26,15 @@ type AdminReportService interface {
// adminReportServiceImpl 管理端举报服务实现
type adminReportServiceImpl struct {
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
notifyRepo repository.SystemNotificationRepository
pushService PushService
txManager repository.TransactionManager
logService OperationLogService
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
notifyRepo repository.SystemNotificationRepository
pushService PushService
txManager repository.TransactionManager
logService OperationLogService
}
// NewAdminReportService 创建管理端举报服务
@@ -50,15 +50,15 @@ func NewAdminReportService(
logService OperationLogService,
) AdminReportService {
return &adminReportServiceImpl{
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
notifyRepo: notifyRepo,
pushService: pushService,
txManager: txManager,
logService: logService,
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
notifyRepo: notifyRepo,
pushService: pushService,
txManager: txManager,
logService: logService,
}
}
@@ -405,9 +405,9 @@ func (s *adminReportServiceImpl) notifyReporter(ctx context.Context, report *mod
// 创建通知
notification := &model.SystemNotification{
ReceiverID: report.ReporterID,
Type: notifyType,
Title: title,
Content: content,
Type: notifyType,
Title: title,
Content: content,
ExtraData: &model.SystemNotificationExtra{
ActorIDStr: report.ReporterID,
TargetID: report.TargetID,

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"cmp"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"time"
@@ -126,4 +126,3 @@ func (s *channelServiceImpl) Delete(id string) error {
s.invalidateChannelListCache()
return nil
}

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"
@@ -27,7 +27,7 @@ type ChatService interface {
GetConversationList(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error)
GetConversationByID(ctx context.Context, conversationID string, userID string) (*model.Conversation, error)
DeleteConversationForSelf(ctx context.Context, conversationID string, userID string) error
SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error
SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error
SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error
// 消息操作

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"cmp"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"cmp"
@@ -24,24 +24,24 @@ type hotRankScored struct {
// HotRankWorker 定时在候选池上重算热门分,写入 Redis ZSET仅 TopN及 top_idsRedis+本地缓存)
type HotRankWorker struct {
cfg *config.Config
postRepo repository.PostRepository
cfg *config.Config
postRepo repository.PostRepository
channelRepo repository.ChannelRepository
c cache.Cache
mu sync.Mutex
stopOnce sync.Once
stopCh chan struct{}
doneCh chan struct{}
c cache.Cache
mu sync.Mutex
stopOnce sync.Once
stopCh chan struct{}
doneCh chan struct{}
}
func NewHotRankWorker(cfg *config.Config, postRepo repository.PostRepository, channelRepo repository.ChannelRepository, c cache.Cache) *HotRankWorker {
return &HotRankWorker{
cfg: cfg,
postRepo: postRepo,
cfg: cfg,
postRepo: postRepo,
channelRepo: channelRepo,
c: c,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
c: c,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}

View File

@@ -1,8 +1,8 @@
package service
package service
import (
"with_you/internal/pkg/jwt"
"time"
"with_you/internal/pkg/jwt"
)
// JWTService JWT服务

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"errors"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -25,13 +25,15 @@ const (
DefaultExpiredTime = 24 * time.Hour
// PushQueueSize 推送队列大小
PushQueueSize = 1000
// MaxDevicesPerUser 每个用户最大设备数
MaxDevicesPerUser = 10
)
// ChatMessageSender 聊天消息发送者信息
type ChatMessageSender struct {
ID string
Name string
Avatar string
ID string
Name string
Avatar string
}
// PushPriority 推送优先级
@@ -62,8 +64,8 @@ type PushService interface {
// 设备管理
RegisterDevice(ctx context.Context, userID string, deviceID string, deviceType model.DeviceType, pushToken string) error
UnregisterDevice(ctx context.Context, deviceID string) error
UpdateDeviceToken(ctx context.Context, deviceID string, newPushToken string) error
UnregisterDevice(ctx context.Context, userID string, deviceID string) error
UpdateDeviceToken(ctx context.Context, userID string, deviceID string, newPushToken string) error
// 推送记录管理
CreatePushRecord(ctx context.Context, userID string, messageID string, channel model.PushChannel) (*model.PushRecord, error)
@@ -135,48 +137,15 @@ func (s *pushServiceImpl) PushToUser(ctx context.Context, userID string, message
}
// 用户不在线,尝试通过极光推送直接发送
if s.jpushClient != nil {
if s.isJPushAvailable() {
devices, err := s.deviceRepo.GetActiveByUserID(userID)
if err == nil && len(devices) > 0 {
mobileDevices := make([]*model.DeviceToken, 0)
for _, d := range devices {
if d.SupportsMobilePush() && d.PushToken != "" {
mobileDevices = append(mobileDevices, d)
}
}
mobileDevices := getMobileDevices(devices)
if len(mobileDevices) > 0 {
title := "新消息"
content := dto.ExtractTextContentFromModel(message.Segments)
if message.IsSystemMessage() || message.Category == model.CategoryNotification {
if message.ExtraData != nil && message.ExtraData.ActorName != "" {
title = message.ExtraData.ActorName
} else {
title = "系统通知"
}
}
extras := map[string]any{
"message_id": message.ID,
"conversation_id": message.ConversationID,
"sender_id": message.SenderID,
"category": string(message.Category),
}
if message.ExtraData != nil {
extras["actor_name"] = message.ExtraData.ActorName
extras["avatar_url"] = message.ExtraData.AvatarURL
extras["target_id"] = message.ExtraData.TargetID
extras["target_type"] = message.ExtraData.TargetType
}
pushErr := s.pushViaJPushBatch(mobileDevices, title, content, extras)
payload := buildMessagePayload(message)
pushErr := s.pushViaJPushBatch(mobileDevices, payload.Title, payload.Content, payload.Extras)
if pushErr == nil {
for _, device := range mobileDevices {
record, rerr := s.CreatePushRecord(ctx, userID, message.ID, model.PushChannelJPush)
if rerr == nil {
record.DeviceToken = device.PushToken
record.DeviceType = string(device.DeviceType)
record.MarkPushed()
s.pushRepo.Update(record)
}
}
s.batchCreatePushedRecords(ctx, userID, message.ID, mobileDevices)
return nil
}
}
@@ -247,11 +216,10 @@ func (s *pushServiceImpl) pushViaWebSocket(ctx context.Context, userID string, m
}
// 构建普通聊天消息的 WebSocket 消息 - 使用新的 WSEventResponse 格式
// 获取会话类型 (private/group)
// 根据 Category 推断 detailType群聊会话需要查询 Conversation 表获取准确类型
detailType := "private"
if message.ConversationID != "" {
// 从会话中获取类型,需要查询数据库或从消息中判断
// 这里暂时默认为 privategroup 类型需要额外逻辑
if message.Category == model.CategoryNotification || message.IsSystemMessage() {
detailType = "system"
}
// 直接使用 message.Segments
@@ -275,9 +243,50 @@ func (s *pushServiceImpl) pushViaWebSocket(ctx context.Context, userID string, m
return true
}
// messagePayload 推送消息的标题、内容和附加数据
type messagePayload struct {
Title string
Content string
Extras map[string]any
}
// buildMessagePayload 从消息中构建推送所需的标题、内容和附加数据
func buildMessagePayload(message *model.Message) messagePayload {
_ = message.Decrypt()
title := "新消息"
content := dto.ExtractTextContentFromModel(message.Segments)
if message.IsSystemMessage() || message.Category == model.CategoryNotification {
if message.ExtraData != nil && message.ExtraData.ActorName != "" {
title = message.ExtraData.ActorName
} else {
title = "系统通知"
}
}
extras := map[string]any{
"message_id": message.ID,
"conversation_id": message.ConversationID,
"sender_id": message.SenderID,
"category": string(message.Category),
}
if message.ExtraData != nil {
extras["actor_name"] = message.ExtraData.ActorName
extras["avatar_url"] = message.ExtraData.AvatarURL
extras["target_id"] = message.ExtraData.TargetID
extras["target_type"] = message.ExtraData.TargetType
}
return messagePayload{
Title: title,
Content: content,
Extras: extras,
}
}
// pushViaJPush 通过极光推送发送通知
func (s *pushServiceImpl) pushViaJPush(ctx context.Context, device *model.DeviceToken, title, content string, extras map[string]any) error {
if s.jpushClient == nil {
if !s.isJPushAvailable() {
return errors.New("jpush client not configured")
}
@@ -286,7 +295,7 @@ func (s *pushServiceImpl) pushViaJPush(ctx context.Context, device *model.Device
}
notification := jpush.BuildNotification(title, content, "message", extras)
_, err := s.jpushClient.PushByRegistrationIDs([]string{device.PushToken}, notification, extras)
_, err := s.jpushClient.PushByRegistrationIDs([]string{device.PushToken}, notification)
if err != nil {
return fmt.Errorf("jpush push failed: %w", err)
}
@@ -294,9 +303,37 @@ func (s *pushServiceImpl) pushViaJPush(ctx context.Context, device *model.Device
return nil
}
// batchCreatePushedRecords 批量创建已推送成功的记录
func (s *pushServiceImpl) batchCreatePushedRecords(ctx context.Context, userID string, messageID string, devices []*model.DeviceToken) {
expiredAt := time.Now().Add(DefaultExpiredTime)
records := make([]*model.PushRecord, 0, len(devices))
for _, device := range devices {
now := time.Now()
record := &model.PushRecord{
UserID: userID,
MessageID: messageID,
PushChannel: model.PushChannelJPush,
PushStatus: model.PushStatusPushed,
DeviceToken: device.PushToken,
DeviceType: string(device.DeviceType),
MaxRetry: MaxRetryCount,
ExpiredAt: &expiredAt,
PushedAt: &now,
}
records = append(records, record)
}
if err := s.pushRepo.BatchCreate(records); err != nil {
zap.L().Error("batch create pushed records failed",
zap.String("userID", userID),
zap.String("messageID", messageID),
zap.Error(err),
)
}
}
// pushViaJPushBatch 批量通过极光推送发送通知
func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, title, content string, extras map[string]any) error {
if s.jpushClient == nil {
if !s.isJPushAvailable() {
return errors.New("jpush client not configured")
}
@@ -312,7 +349,7 @@ func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, title,
}
notification := jpush.BuildNotification(title, content, "message", extras)
_, err := s.jpushClient.PushByRegistrationIDs(regIDs, notification, extras)
_, err := s.jpushClient.PushByRegistrationIDs(regIDs, notification)
if err != nil {
return fmt.Errorf("jpush batch push failed: %w", err)
}
@@ -322,6 +359,11 @@ func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, title,
// RegisterDevice 注册设备
func (s *pushServiceImpl) RegisterDevice(ctx context.Context, userID string, deviceID string, deviceType model.DeviceType, pushToken string) error {
existing, err := s.deviceRepo.GetByUserID(userID)
if err == nil && len(existing) >= MaxDevicesPerUser {
return fmt.Errorf("exceeded maximum device limit (%d)", MaxDevicesPerUser)
}
deviceToken := &model.DeviceToken{
UserID: userID,
DeviceID: deviceID,
@@ -335,16 +377,26 @@ func (s *pushServiceImpl) RegisterDevice(ctx context.Context, userID string, dev
}
// UnregisterDevice 注销设备
func (s *pushServiceImpl) UnregisterDevice(ctx context.Context, deviceID string) error {
func (s *pushServiceImpl) UnregisterDevice(ctx context.Context, userID string, deviceID string) error {
device, err := s.deviceRepo.GetByDeviceID(deviceID)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
if device.UserID != userID {
return fmt.Errorf("device does not belong to current user")
}
return s.deviceRepo.Deactivate(deviceID)
}
// UpdateDeviceToken 更新设备Token
func (s *pushServiceImpl) UpdateDeviceToken(ctx context.Context, deviceID string, newPushToken string) error {
func (s *pushServiceImpl) UpdateDeviceToken(ctx context.Context, userID string, deviceID string, newPushToken string) error {
deviceToken, err := s.deviceRepo.GetByDeviceID(deviceID)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
if deviceToken.UserID != userID {
return fmt.Errorf("device does not belong to current user")
}
deviceToken.PushToken = newPushToken
deviceToken.Activate()
@@ -417,58 +469,19 @@ func (s *pushServiceImpl) processPushTask(task *pushTask) {
return
}
// 提取消息标题和内容
_ = task.message.Decrypt()
title := "新消息"
content := dto.ExtractTextContentFromModel(task.message.Segments)
if task.message.IsSystemMessage() || task.message.Category == model.CategoryNotification {
if task.message.ExtraData != nil && task.message.ExtraData.ActorName != "" {
title = task.message.ExtraData.ActorName
} else {
title = "系统通知"
}
}
extras := map[string]any{
"message_id": task.message.ID,
"conversation_id": task.message.ConversationID,
"sender_id": task.message.SenderID,
"category": string(task.message.Category),
}
if task.message.ExtraData != nil {
extras["actor_name"] = task.message.ExtraData.ActorName
extras["avatar_url"] = task.message.ExtraData.AvatarURL
extras["target_id"] = task.message.ExtraData.TargetID
extras["target_type"] = task.message.ExtraData.TargetType
}
payload := buildMessagePayload(task.message)
// 尝试使用极光推送(统一推送到所有移动设备)
mobileDevices := make([]*model.DeviceToken, 0)
for _, device := range devices {
if device.SupportsMobilePush() && device.PushToken != "" {
mobileDevices = append(mobileDevices, device)
}
}
mobileDevices := getMobileDevices(devices)
if len(mobileDevices) > 0 {
// 优先使用极光推送
if s.jpushClient != nil {
err := s.pushViaJPushBatch(mobileDevices, title, content, extras)
if s.isJPushAvailable() {
err := s.pushViaJPushBatch(mobileDevices, payload.Title, payload.Content, payload.Extras)
if err == nil {
// 极光推送成功,为每个设备创建推送记录
for _, device := range mobileDevices {
record, rerr := s.CreatePushRecord(ctx, task.userID, task.message.ID, model.PushChannelJPush)
if rerr != nil {
continue
}
record.DeviceToken = device.PushToken
record.DeviceType = string(device.DeviceType)
record.MarkPushed()
s.pushRepo.Update(record)
}
s.batchCreatePushedRecords(ctx, task.userID, task.message.ID, mobileDevices)
return
}
// 极光推送失败,记录错误,逐个设备重试
}
// 极光推送不可用或批量推送失败,逐个设备处理
@@ -482,8 +495,8 @@ func (s *pushServiceImpl) processPushTask(task *pushTask) {
record.DeviceType = string(device.DeviceType)
var pushErr error
if s.jpushClient != nil {
pushErr = s.pushViaJPush(ctx, device, title, content, extras)
if s.isJPushAvailable() {
pushErr = s.pushViaJPush(ctx, device, payload.Title, payload.Content, payload.Extras)
} else {
pushErr = errors.New("jpush not configured")
}
@@ -510,9 +523,29 @@ func (s *pushServiceImpl) getChannelForDevice(device *model.DeviceToken) model.P
}
}
// retryFailedPushes 重试失败的推送
// isJPushAvailable 检查极光推送是否可用
func (s *pushServiceImpl) isJPushAvailable() bool {
return s.jpushClient != nil
}
// getMobileDevices 从设备列表中筛选出支持手机推送的活跃设备
func getMobileDevices(devices []*model.DeviceToken) []*model.DeviceToken {
mobileDevices := make([]*model.DeviceToken, 0, len(devices))
for _, d := range devices {
if d.SupportsMobilePush() && d.PushToken != "" {
mobileDevices = append(mobileDevices, d)
}
}
return mobileDevices
}
// retryFailedPushes 重试失败的推送(使用指数退避)
func (s *pushServiceImpl) retryFailedPushes() {
ticker := time.NewTicker(5 * time.Minute)
baseInterval := 5 * time.Minute
maxInterval := 30 * time.Minute
currentInterval := baseInterval
ticker := time.NewTicker(currentInterval)
defer ticker.Stop()
for {
@@ -520,21 +553,31 @@ func (s *pushServiceImpl) retryFailedPushes() {
case <-s.stopChan:
return
case <-ticker.C:
s.doRetry()
retryCount := s.doRetry()
if retryCount > 0 {
currentInterval = baseInterval
} else {
currentInterval = currentInterval * 2
if currentInterval > maxInterval {
currentInterval = maxInterval
}
}
ticker.Reset(currentInterval)
}
}
}
// doRetry 执行重试
func (s *pushServiceImpl) doRetry() {
// doRetry 执行重试,返回本次成功重试的数量
func (s *pushServiceImpl) doRetry() int {
ctx := context.Background()
// 获取失败待重试的推送
records, err := s.pushRepo.GetFailedPushesForRetry(100)
if err != nil {
return
return 0
}
retriedCount := 0
for _, record := range records {
// 检查是否过期
if record.IsExpired() {
@@ -555,6 +598,7 @@ func (s *pushServiceImpl) doRetry() {
if s.pushViaWebSocket(ctx, record.UserID, message) {
record.MarkDelivered()
s.pushRepo.Update(record)
retriedCount++
continue
}
@@ -568,40 +612,21 @@ func (s *pushServiceImpl) doRetry() {
}
var pushErr error
if s.jpushClient != nil && device.SupportsMobilePush() && device.PushToken != "" {
_ = message.Decrypt()
title := "新消息"
content := dto.ExtractTextContentFromModel(message.Segments)
if message.IsSystemMessage() || message.Category == model.CategoryNotification {
if message.ExtraData != nil && message.ExtraData.ActorName != "" {
title = message.ExtraData.ActorName
} else {
title = "系统通知"
}
}
extras := map[string]any{
"message_id": message.ID,
"conversation_id": message.ConversationID,
"sender_id": message.SenderID,
"category": string(message.Category),
}
if message.ExtraData != nil {
extras["actor_name"] = message.ExtraData.ActorName
extras["avatar_url"] = message.ExtraData.AvatarURL
extras["target_id"] = message.ExtraData.TargetID
extras["target_type"] = message.ExtraData.TargetType
}
pushErr = s.pushViaJPush(ctx, device, title, content, extras)
if s.isJPushAvailable() && device.SupportsMobilePush() && device.PushToken != "" {
payload := buildMessagePayload(message)
pushErr = s.pushViaJPush(ctx, device, payload.Title, payload.Content, payload.Extras)
}
if pushErr != nil {
record.MarkFailed(pushErr.Error())
} else {
record.MarkPushed()
retriedCount++
}
s.pushRepo.Update(record)
}
}
return retriedCount
}
// PushChatMessage 推送聊天消息给离线用户
@@ -623,7 +648,7 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
}
// 3. 获取用户的移动设备
if s.jpushClient == nil {
if !s.isJPushAvailable() {
return nil
}
devices, err := s.deviceRepo.GetActiveByUserID(userID)
@@ -631,16 +656,16 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
return nil
}
var regIDs []string
for _, d := range devices {
if d.SupportsMobilePush() && d.PushToken != "" {
regIDs = append(regIDs, d.PushToken)
}
}
if len(regIDs) == 0 {
mobileDevices := getMobileDevices(devices)
if len(mobileDevices) == 0 {
return nil
}
var regIDs []string
for _, d := range mobileDevices {
regIDs = append(regIDs, d.PushToken)
}
// 4. 构建通知内容
title := sender.Name
if title == "" {
@@ -655,13 +680,13 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
}
extras := map[string]any{
"notification_type": "chat_message",
"message_id": message.ID,
"conversation_id": message.ConversationID,
"message_id": message.ID,
"conversation_id": message.ConversationID,
"conversation_type": string(convType),
"sender_id": message.SenderID,
"sender_name": sender.Name,
"sender_avatar": sender.Avatar,
"category": string(message.Category),
"sender_id": message.SenderID,
"sender_name": sender.Name,
"sender_avatar": sender.Avatar,
"category": string(message.Category),
}
if message.Category == model.CategoryNotification && message.ExtraData != nil {
title = message.ExtraData.ActorName
@@ -679,7 +704,7 @@ func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, co
if sender.Avatar != "" {
notification.Android.LargeIcon = sender.Avatar
}
if _, pushErr := s.jpushClient.PushByRegistrationIDs(regIDs, notification, extras); pushErr != nil {
if _, pushErr := s.jpushClient.PushByRegistrationIDs(regIDs, notification); pushErr != nil {
zap.L().Error("jpush push chat message failed",
zap.String("userID", userID),
zap.String("conversationID", conversationID),
@@ -717,13 +742,13 @@ func (s *pushServiceImpl) pushSystemViaWebSocket(ctx context.Context, userID str
}
sysMsg := map[string]any{
"type": "notification",
"type": "notification",
"system_type": msgType,
"title": title,
"content": content,
"is_read": false,
"extra_data": data,
"created_at": time.Now().Format("2006-01-02T15:04:05Z07:00"),
"title": title,
"content": content,
"is_read": false,
"extra_data": data,
"created_at": time.Now().Format("2006-01-02T15:04:05Z07:00"),
}
s.wsHub.PublishToUserOnline(userID, "system_notification", sysMsg)
return true
@@ -737,15 +762,10 @@ func (s *pushServiceImpl) PushSystemNotification(ctx context.Context, userID str
}
// 用户不在线,尝试极光推送
if s.jpushClient != nil {
if s.isJPushAvailable() {
devices, err := s.deviceRepo.GetActiveByUserID(userID)
if err == nil && len(devices) > 0 {
mobileDevices := make([]*model.DeviceToken, 0)
for _, d := range devices {
if d.SupportsMobilePush() && d.PushToken != "" {
mobileDevices = append(mobileDevices, d)
}
}
mobileDevices := getMobileDevices(devices)
if len(mobileDevices) > 0 {
content := notification.Content
title := notification.Title
@@ -777,7 +797,6 @@ func (s *pushServiceImpl) PushSystemNotification(ctx context.Context, userID str
return ids
}(),
jpushNotif,
extras,
)
if pushErr == nil {
return nil

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"
@@ -22,16 +22,16 @@ type ReportService interface {
// reportServiceImpl 举报服务实现
type reportServiceImpl struct {
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
notifyRepo repository.SystemNotificationRepository
pushService PushService
txManager repository.TransactionManager
logService OperationLogService
config *config.ReportConfig
reportRepo repository.ReportRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
messageRepo repository.MessageRepository
userRepo repository.UserRepository
notifyRepo repository.SystemNotificationRepository
pushService PushService
txManager repository.TransactionManager
logService OperationLogService
config *config.ReportConfig
}
// NewReportService 创建举报服务
@@ -52,16 +52,16 @@ func NewReportService(
reportConfig.AutoHideThreshold = 3
}
return &reportServiceImpl{
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
notifyRepo: notifyRepo,
pushService: pushService,
txManager: txManager,
logService: logService,
config: reportConfig,
reportRepo: reportRepo,
postRepo: postRepo,
commentRepo: commentRepo,
messageRepo: messageRepo,
userRepo: userRepo,
notifyRepo: notifyRepo,
pushService: pushService,
txManager: txManager,
logService: logService,
config: reportConfig,
}
}

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -13,10 +13,10 @@ type SetupService interface {
}
type setupService struct {
userRepo repository.UserRepository
roleRepo repository.RoleRepository
casbinSvc CasbinService
setupSecret string
userRepo repository.UserRepository
roleRepo repository.RoleRepository
casbinSvc CasbinService
setupSecret string
}
func NewSetupService(

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"errors"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"

View File

@@ -1,4 +1,4 @@
package service
package service
import (
"context"