topfans/backend/services/notificationService/service/user_device_service.go

145 lines
4.3 KiB
Go

package service
import (
"context"
"strings"
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
notifPb "github.com/topfans/backend/pkg/proto/notification"
"github.com/topfans/backend/services/notificationService/repository"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"gorm.io/gorm"
)
// ========== 设备注册服务 ==========
//
// UserDeviceService 处理客户端推送 token (cid) 的注册与注销。
// 调用方:gateway 通过 Dubbo RPC 转发 App 端的 RegisterDevice / UnregisterDevice。
//
// 行为约定:
// - RegisterDevice:按 cid upsert;同 cid 已存在则更新归属/platform/version。
// - UnregisterDevice:cid 非空时按 cid 注销;cid 为空时按 user_id 注销该用户所有设备(主动登出场景)。
// UserDeviceService 设备服务。
type UserDeviceService struct {
db *gorm.DB
repo *repository.UserDeviceRepository
}
// NewUserDeviceService 创建 UserDeviceService。
func NewUserDeviceService(db *gorm.DB) *UserDeviceService {
return &UserDeviceService{
db: db,
repo: repository.NewUserDeviceRepository(db),
}
}
// DB 返回底层 gorm.DB(供同包 NotificationService 调用查询 cids)。
func (s *UserDeviceService) DB() *gorm.DB { return s.db }
// RegisterDevice 注册/更新 cid。
func (s *UserDeviceService) RegisterDevice(
ctx context.Context,
userID int64,
req *notifPb.RegisterDeviceRequest,
) (*notifPb.RegisterDeviceResponse, error) {
if userID <= 0 {
return nil, appErrors.NewError(codes.InvalidArgument, "user_id is required")
}
if req == nil || strings.TrimSpace(req.Cid) == "" {
return nil, appErrors.NewError(codes.InvalidArgument, "cid is required")
}
// 平台号做白名单简单保护(后续可加更严格的枚举)
platform := strings.ToLower(strings.TrimSpace(req.Platform))
switch platform {
case "ios", "android", "harmony", "":
// 通过
default:
// 未知平台也允许写入,便于鸿蒙/小程序等新通道接入;只 warn
logger.Logger.Warn("register device with unknown platform",
zap.Int64("user_id", userID),
zap.String("platform", platform))
}
d, err := s.repo.UpsertByCID(ctx, nil,
strings.TrimSpace(req.Cid),
userID,
platform,
strings.TrimSpace(req.AppVersion),
strings.TrimSpace(req.DeviceModel),
)
if err != nil {
logger.Logger.Error("RegisterDevice upsert failed",
zap.Int64("user_id", userID),
zap.String("cid", req.Cid),
zap.Error(err))
return nil, appErrors.NewError(codes.Internal, "register device failed")
}
logger.Logger.Info("device registered",
zap.Int64("id", d.ID),
zap.Int64("user_id", userID),
zap.String("platform", d.Platform),
zap.String("cid", d.CID))
return &notifPb.RegisterDeviceResponse{
Base: appErrors.FormatSuccessResponse(),
Id: d.ID,
}, nil
}
// UnregisterDevice 注销 cid;cid 为空时注销 user 的所有设备。
func (s *UserDeviceService) UnregisterDevice(
ctx context.Context,
userID int64,
req *notifPb.UnregisterDeviceRequest,
) (*notifPb.UnregisterDeviceResponse, error) {
if userID <= 0 {
return nil, appErrors.NewError(codes.InvalidArgument, "user_id is required")
}
if req == nil {
return nil, appErrors.NewError(codes.InvalidArgument, "request is nil")
}
cid := strings.TrimSpace(req.Cid)
var affected int64
var err error
if cid != "" {
affected, err = s.repo.DeactivateByCID(ctx, nil, cid)
} else {
// cid 为空:注销该用户全部(批量 UPDATE)
res := s.db.WithContext(ctx).Exec(`
UPDATE public.user_devices
SET active = FALSE, updated_at = $1
WHERE user_id = $2 AND active = TRUE
`, time.Now().UnixMilli(), userID)
affected, err = res.RowsAffected, res.Error
}
if err != nil {
logger.Logger.Error("UnregisterDevice failed",
zap.Int64("user_id", userID),
zap.String("cid", cid),
zap.Error(err))
return nil, appErrors.NewError(codes.Internal, "unregister device failed")
}
logger.Logger.Info("device unregistered",
zap.Int64("user_id", userID),
zap.String("cid", cid),
zap.Int64("affected", affected))
return &notifPb.UnregisterDeviceResponse{
Base: appErrors.FormatSuccessResponse(),
Affected: int32(affected),
}, nil
}
// ListActiveCIDs 获取某用户的活跃 cid 列表(供 NotificationService 推送时调用)。
func (s *UserDeviceService) ListActiveCIDs(ctx context.Context, userID int64) ([]string, error) {
return s.repo.ListActiveCIDsByUserID(ctx, userID)
}