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 ( import (
"strconv" "strconv"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
package handler package handler
import ( import (
"strconv"
"with_you/internal/dto" "with_you/internal/dto"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/service" "with_you/internal/service"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -149,4 +149,4 @@ func parsePageParams(c *gin.Context) (int, int) {
pageSize = 20 pageSize = 20
} }
return page, pageSize return page, pageSize
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package handler package handler
import ( import (
"with_you/internal/dto" "with_you/internal/dto"
@@ -67,7 +67,7 @@ func (h *PushHandler) UnregisterDevice(c *gin.Context) {
return return
} }
err := h.pushService.UnregisterDevice(c.Request.Context(), deviceID) err := h.pushService.UnregisterDevice(c.Request.Context(), userID, deviceID)
if err != nil { if err != nil {
response.InternalServerError(c, "failed to unregister device") response.InternalServerError(c, "failed to unregister device")
return return
@@ -141,14 +141,14 @@ func (h *PushHandler) UpdateDeviceToken(c *gin.Context) {
} }
var req struct { var req struct {
PushToken string `json:"push_token"` PushToken string `json:"push_token"`
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error()) response.BadRequest(c, err.Error())
return 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 { if err != nil {
response.InternalServerError(c, "failed to update device token") response.InternalServerError(c, "failed to update device token")
return return

View File

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

View File

@@ -1,4 +1,4 @@
package handler package handler
import ( import (
"with_you/internal/dto" "with_you/internal/dto"
@@ -58,4 +58,4 @@ func (h *ReportHandler) Create(c *gin.Context) {
} }
response.Success(c, dto.ConvertReportToResponse(report)) response.Success(c, dto.ConvertReportToResponse(report))
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -217,4 +217,4 @@ func (h *TradeHandler) Unfavorite(c *gin.Context) {
return return
} }
response.Success(c, dto.TradeFavoriteResponse{Favorited: false}) response.Success(c, dto.TradeFavoriteResponse{Favorited: false})
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -510,11 +510,8 @@ func (r *postRepository) Search(keyword string, page, pageSize int) ([]*model.Po
// 搜索标题和内容 // 搜索标题和内容
if keyword != "" { if keyword != "" {
if r.db.Dialector.Name() == "postgres" { if r.db.Dialector.Name() == "postgres" {
// PostgreSQL 使用全文检索表达式,为 pg_trgm/GIN 索引升级预留路径 searchPattern := "%" + keyword + "%"
query = query.Where( query = query.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
keyword,
)
} else { } else {
searchPattern := "%" + keyword + "%" searchPattern := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern) 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 query.Keyword != "" {
if r.db.Dialector.Name() == "postgres" { if r.db.Dialector.Name() == "postgres" {
db = db.Where( searchPattern := "%" + query.Keyword + "%"
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)", db = db.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
query.Keyword,
)
} else { } else {
searchPattern := "%" + query.Keyword + "%" searchPattern := "%" + query.Keyword + "%"
db = db.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern) 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 keyword != "" {
if r.db.Dialector.Name() == "postgres" { if r.db.Dialector.Name() == "postgres" {
// PostgreSQL 使用全文检索表达式 searchPattern := "%" + keyword + "%"
query = query.Where( query = query.Where("title ILIKE ? OR content ILIKE ?", searchPattern, searchPattern)
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
keyword,
)
} else { } else {
searchPattern := "%" + keyword + "%" searchPattern := "%" + keyword + "%"
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern) 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 keyword != "" {
if r.db.Dialector.Name() == "postgres" { if r.db.Dialector.Name() == "postgres" {
searchPattern := "%" + keyword + "%"
query = query.Where( query = query.Where(
"to_tsvector('simple', COALESCE(username, '') || ' ' || COALESCE(nickname, '') || ' ' || COALESCE(bio, '')) @@ plainto_tsquery('simple', ?)", "username ILIKE ? OR nickname ILIKE ? OR bio ILIKE ?",
keyword, searchPattern, searchPattern, searchPattern,
) )
} else { } else {
// SQLite: 使用 LOWER() 实现大小写不敏感的模糊搜索 // SQLite: 使用 LOWER() 实现大小写不敏感的模糊搜索

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package service package service
import ( import (
"errors" "errors"
@@ -171,4 +171,4 @@ func (s *materialServiceImpl) DeleteMaterial(id string) error {
func (s *materialServiceImpl) IncrementDownloadCount(id string) error { func (s *materialServiceImpl) IncrementDownloadCount(id string) error {
return s.fileRepo.IncrementDownloadCount(id) return s.fileRepo.IncrementDownloadCount(id)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,10 @@ type SetupService interface {
} }
type setupService struct { type setupService struct {
userRepo repository.UserRepository userRepo repository.UserRepository
roleRepo repository.RoleRepository roleRepo repository.RoleRepository
casbinSvc CasbinService casbinSvc CasbinService
setupSecret string setupSecret string
} }
func NewSetupService( func NewSetupService(
@@ -60,4 +60,4 @@ func (s *setupService) SetupSuperAdmin(ctx context.Context, userID, setupSecret
} }
return s.casbinSvc.AddRoleForUser(ctx, user.ID, model.RoleSuperAdmin) return s.casbinSvc.AddRoleForUser(ctx, user.ID, model.RoleSuperAdmin)
} }

View File

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

View File

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

View File

@@ -219,4 +219,4 @@ func (s *tradeService) Unfavorite(ctx context.Context, userID string, id string)
} }
_ = s.tradeRepo.DecrementFavorites(ctx, id) _ = s.tradeRepo.DecrementFavorites(ctx, id)
return nil return nil
} }

View File

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

View File

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

View File

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

View File

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