feat(admin): add admin endpoint to list user devices with JPush registration IDs
- Add AdminDeviceTokenResponse DTO including push_token (RegistrationID) for push debugging - Add DeviceTokenToAdminResponse and DeviceTokensToAdminResponse converters - Inject PushService into AdminUserHandler for retrieving user devices - Register GET /api/v1/admin/users/:id/devices route for querying target user's device tokens
This commit is contained in:
@@ -627,13 +627,9 @@ func (s *chatServiceImpl) inlinePush(ctx context.Context, conv *model.Conversati
|
||||
}
|
||||
allTargetIDs = append(allTargetIDs, p.UserID)
|
||||
}
|
||||
var offlineTargetIDs []string
|
||||
if s.wsHub != nil && len(allTargetIDs) > 0 {
|
||||
_, offlineTargetIDs = s.wsHub.FilterOnline(allTargetIDs)
|
||||
} else {
|
||||
offlineTargetIDs = allTargetIDs
|
||||
}
|
||||
if len(offlineTargetIDs) > 0 {
|
||||
// 直接对所有参与者推送(不按 WS 在线过滤):
|
||||
// 多设备场景下某端在线不代表所有端在线,由 PushChatMessage 推送到用户所有移动设备
|
||||
if len(allTargetIDs) > 0 {
|
||||
go func(ids []string, sender ChatMessageSender, gid string, gavatar string) {
|
||||
for _, uid := range ids {
|
||||
if pushErr := s.pushSvc.PushChatMessage(context.Background(), uid, conversationID, &sender, convType, convName, message, gid, gavatar); pushErr != nil {
|
||||
@@ -644,7 +640,7 @@ func (s *chatServiceImpl) inlinePush(ctx context.Context, conv *model.Conversati
|
||||
)
|
||||
}
|
||||
}
|
||||
}(offlineTargetIDs, sender, groupID, groupAvatar)
|
||||
}(allTargetIDs, sender, groupID, groupAvatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ type PushService interface {
|
||||
PushToUser(ctx context.Context, userID string, message *model.Message, priority int) error
|
||||
|
||||
// 聊天消息推送(含免打扰检查)
|
||||
// 该方法会:1. 检查用户是否在线(在线则跳过);2. 检查会话免打扰(免打扰则跳过);3. 通过 JPush 推送
|
||||
// 该方法会:1. 不给发送者自己推送;2. 检查会话免打扰(免打扰则跳过);3. 通过 JPush 推送到用户所有移动设备
|
||||
// 不检查 WS 在线状态:多设备场景下直接推送到所有活跃移动设备
|
||||
PushChatMessage(ctx context.Context, userID string, conversationID string, sender *ChatMessageSender, convType model.ConversationType, convName string, message *model.Message, groupID string, groupAvatar string) error
|
||||
|
||||
// 系统消息推送
|
||||
@@ -382,12 +383,27 @@ func (s *pushServiceImpl) pushViaJPushBatch(devices []*model.DeviceToken, params
|
||||
}
|
||||
|
||||
// RegisterDevice 注册设备
|
||||
// 处理同设备多账号场景:registration_id(pushToken)是设备级标识,
|
||||
// 同一物理设备登录新账号时 pushToken 不变,需先失效其他用户对该 pushToken 的关联,
|
||||
// 避免给一个账号推送时同设备的其他账号也收到通知。
|
||||
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)
|
||||
}
|
||||
|
||||
// 失效其他用户/设备对同一 pushToken 的关联(防止跨账号误推)
|
||||
if pushToken != "" {
|
||||
if dErr := s.deviceRepo.DeactivateByPushToken(pushToken); dErr != nil {
|
||||
// 失效失败不阻断注册流程,仅记录日志
|
||||
zap.L().Warn("deactivate devices by push token failed",
|
||||
zap.String("userID", userID),
|
||||
zap.String("deviceID", deviceID),
|
||||
zap.Error(dErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deviceToken := &model.DeviceToken{
|
||||
UserID: userID,
|
||||
DeviceID: deviceID,
|
||||
@@ -739,14 +755,16 @@ func (s *pushServiceImpl) doRetry() int {
|
||||
}
|
||||
|
||||
// PushChatMessage 推送聊天消息给离线用户
|
||||
// 检查用户在线状态和免打扰设置,仅对离线且未免打扰的用户通过 JPush 推送
|
||||
// PushChatMessage 推送聊天消息给用户的所有移动设备
|
||||
// 不检查 WebSocket 在线状态:多设备场景下某端在线不代表所有端都在线,
|
||||
// 直接推送到用户所有活跃移动设备,避免 PC 在线时手机收不到通知
|
||||
func (s *pushServiceImpl) PushChatMessage(ctx context.Context, userID string, conversationID string, sender *ChatMessageSender, convType model.ConversationType, convName string, message *model.Message, groupID string, groupAvatar string) error {
|
||||
// 1. 用户在线则跳过(已通过 WebSocket 收到消息)
|
||||
if s.wsHub != nil && s.wsHub.HasClients(userID) {
|
||||
// 0. 不给消息发送者自己推送(纵深防御,上游应已排除)
|
||||
if sender != nil && userID == sender.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 检查免打扰状态
|
||||
// 1. 检查免打扰状态
|
||||
participant, err := s.messageRepo.GetParticipant(conversationID, userID)
|
||||
if err == nil && participant.NotificationMuted {
|
||||
zap.L().Debug("push chat message skipped: notification muted",
|
||||
|
||||
@@ -342,14 +342,9 @@ func (w *PushWorker) handleMsg(streamMsg redis.XMessage) error {
|
||||
allTargetIDs = append(allTargetIDs, p.UserID)
|
||||
}
|
||||
|
||||
var offlineTargetIDs []string
|
||||
if w.publisher != nil && len(allTargetIDs) > 0 {
|
||||
_, offlineTargetIDs = w.publisher.FilterOnline(allTargetIDs)
|
||||
} else {
|
||||
offlineTargetIDs = allTargetIDs
|
||||
}
|
||||
|
||||
for _, uid := range offlineTargetIDs {
|
||||
// 直接对所有参与者推送(不按 WS 在线过滤):
|
||||
// 多设备场景下某端在线不代表所有端在线,由 PushChatMessage 推送到用户所有移动设备
|
||||
for _, uid := range allTargetIDs {
|
||||
if pushErr := w.pushSvc.PushChatMessage(ctx, uid, pushMsg.ConversationID, &sender, conv.Type, convName, message, groupID, groupAvatar); pushErr != nil {
|
||||
zap.L().Debug("push worker: JPush chat message failed",
|
||||
zap.String("userID", uid),
|
||||
|
||||
Reference in New Issue
Block a user