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 ¬ifPb.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 ¬ifPb.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) }