213 lines
7.7 KiB
Go
213 lines
7.7 KiB
Go
package user
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
invitedomain "hyapp/services/user-service/internal/domain/invite"
|
||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||
)
|
||
|
||
const (
|
||
defaultUserIDPageSize = 500
|
||
maxUserIDPageSize = 5000
|
||
)
|
||
|
||
// GetUser 查询单个用户主状态。
|
||
func (s *Service) GetUser(ctx context.Context, userID int64) (userdomain.User, error) {
|
||
if userID <= 0 {
|
||
// service 层统一做参数校验,transport 只负责协议适配。
|
||
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
|
||
if s.userRepository == nil {
|
||
// repository 缺失代表应用装配不完整。
|
||
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
|
||
user, err := s.userRepository.GetUser(ctx, userID)
|
||
if err != nil {
|
||
return userdomain.User{}, err
|
||
}
|
||
|
||
// 查询用户时顺带执行靓号懒过期,返回给上游的是当前有效展示号。
|
||
return s.refreshExpiredUser(ctx, user, "")
|
||
}
|
||
|
||
// GetInviteAttribution 按被邀请用户读取邀请关系;没有关系时返回 Found=false,调用方按普通非邀请充值处理。
|
||
func (s *Service) GetInviteAttribution(ctx context.Context, invitedUserID int64) (invitedomain.Attribution, error) {
|
||
if invitedUserID <= 0 {
|
||
return invitedomain.Attribution{}, xerr.New(xerr.InvalidArgument, "invited_user_id is required")
|
||
}
|
||
if s.userRepository == nil {
|
||
return invitedomain.Attribution{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
return s.userRepository.GetInviteAttribution(ctx, invitedUserID)
|
||
}
|
||
|
||
// GetMyProfileStats 读取我的页顶部统计计数;统计事实必须由专门 read model 维护。
|
||
func (s *Service) GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error) {
|
||
if userID <= 0 {
|
||
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||
}
|
||
if s.userRepository == nil {
|
||
return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
return s.userRepository.GetMyProfileStats(ctx, userID)
|
||
}
|
||
|
||
// BatchGetUsers 批量查询用户主状态。
|
||
func (s *Service) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {
|
||
if len(userIDs) == 0 {
|
||
// 空批量请求返回空 map,避免调用方处理 nil。
|
||
return map[int64]userdomain.User{}, nil
|
||
}
|
||
|
||
for _, userID := range userIDs {
|
||
if userID <= 0 {
|
||
// 任一非法 ID 都让整个批量请求失败,避免部分成功掩盖输入错误。
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_ids contains invalid value")
|
||
}
|
||
}
|
||
|
||
if s.userRepository == nil {
|
||
// 批量查询同样不能绕过 repository。
|
||
return nil, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
|
||
users, err := s.userRepository.BatchGetUsers(ctx, userIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for userID, user := range users {
|
||
// 批量返回前逐个刷新过期靓号,保证身份投影一致。
|
||
refreshed, err := s.refreshExpiredUser(ctx, user, "")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
users[userID] = refreshed
|
||
}
|
||
|
||
return users, nil
|
||
}
|
||
|
||
// BatchGetRoomBasicUsers 批量查询房间首屏最小用户资料。
|
||
func (s *Service) BatchGetRoomBasicUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.RoomBasicUser, error) {
|
||
if len(userIDs) == 0 {
|
||
// 空批量请求返回空 map,gateway 可以直接输出空 profiles。
|
||
return map[int64]userdomain.RoomBasicUser{}, nil
|
||
}
|
||
for _, userID := range userIDs {
|
||
if userID <= 0 {
|
||
// room-service 只接受正 user_id;这里继续对调用方输入做边界收敛。
|
||
return nil, xerr.New(xerr.InvalidArgument, "user_ids contains invalid value")
|
||
}
|
||
}
|
||
if s.userRepository == nil {
|
||
// 房间热路径也不能绕过 owner repository;无存储时明确返回不可用。
|
||
return nil, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
return s.userRepository.BatchGetRoomBasicUsers(ctx, userIDs)
|
||
}
|
||
|
||
// ListUserIDs returns target user IDs for low-frequency backend fanout tasks.
|
||
func (s *Service) ListUserIDs(ctx context.Context, filter userdomain.UserIDPageFilter) ([]int64, int64, bool, error) {
|
||
if s.userRepository == nil {
|
||
return nil, 0, false, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
filter.TargetScope = strings.TrimSpace(filter.TargetScope)
|
||
filter.Country = userdomain.NormalizeCountryCode(filter.Country)
|
||
filter.PageSize = normalizeUserIDPageSize(filter.PageSize)
|
||
if filter.CursorUserID < 0 {
|
||
return nil, 0, false, xerr.New(xerr.InvalidArgument, "cursor_user_id is invalid")
|
||
}
|
||
switch filter.TargetScope {
|
||
case userdomain.UserIDTargetAllActiveUsers, userdomain.UserIDTargetAllRegistered:
|
||
case userdomain.UserIDTargetRegion:
|
||
if filter.RegionID <= 0 {
|
||
return nil, 0, false, xerr.New(xerr.InvalidArgument, "region_id is required")
|
||
}
|
||
case userdomain.UserIDTargetCountry:
|
||
if !userdomain.ValidCountryCode(filter.Country) {
|
||
return nil, 0, false, xerr.New(xerr.InvalidArgument, "country is invalid")
|
||
}
|
||
default:
|
||
return nil, 0, false, xerr.New(xerr.InvalidArgument, "target_scope is invalid")
|
||
}
|
||
|
||
userIDs, err := s.userRepository.ListUserIDs(ctx, filter)
|
||
if err != nil {
|
||
return nil, 0, false, err
|
||
}
|
||
nextCursor := filter.CursorUserID
|
||
if len(userIDs) > 0 {
|
||
nextCursor = userIDs[len(userIDs)-1]
|
||
}
|
||
return userIDs, nextCursor, len(userIDs) < filter.PageSize, nil
|
||
}
|
||
|
||
func normalizeUserIDPageSize(value int) int {
|
||
if value <= 0 {
|
||
return defaultUserIDPageSize
|
||
}
|
||
if value > maxUserIDPageSize {
|
||
return maxUserIDPageSize
|
||
}
|
||
return value
|
||
}
|
||
|
||
// CreateUser 创建用户主记录,并同时分配 active display_user_id。
|
||
// 该方法供注册和三方首次登录后续接入,避免出现没有短号的用户。
|
||
func (s *Service) CreateUser(ctx context.Context) (userdomain.User, userdomain.Identity, error) {
|
||
if s.userRepository == nil {
|
||
// 没有持久化能力时不能创建用户。
|
||
return userdomain.User{}, userdomain.Identity{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||
}
|
||
|
||
for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ {
|
||
// user_id 和 display_user_id 都在每次尝试中重新生成,降低冲突概率。
|
||
userID := s.idGenerator.NewInt64()
|
||
nowMs := s.now().UnixMilli()
|
||
displayUserID := s.displayUserIDAllocator.NextDisplayUserID(userID, attempt)
|
||
if !userdomain.ValidDisplayUserID(displayUserID) {
|
||
// allocator 理论上应生成合法短号;非法候选直接跳过本次尝试。
|
||
continue
|
||
}
|
||
|
||
createdUser := userdomain.User{
|
||
AppCode: appcode.FromContext(ctx),
|
||
UserID: userID,
|
||
DefaultDisplayUserID: displayUserID,
|
||
CurrentDisplayUserID: displayUserID,
|
||
CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||
Status: userdomain.StatusActive,
|
||
CreatedAtMs: nowMs,
|
||
UpdatedAtMs: nowMs,
|
||
}
|
||
identity := userdomain.Identity{
|
||
AppCode: appcode.FromContext(ctx),
|
||
UserID: userID,
|
||
DisplayUserID: displayUserID,
|
||
DefaultDisplayUserID: displayUserID,
|
||
DisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||
Status: userdomain.DisplayUserIDStatusActive,
|
||
}
|
||
|
||
err := s.userRepository.CreateUserWithIdentity(ctx, createdUser, identity)
|
||
if err == nil {
|
||
// repository 事务成功后,用户和默认短号一定同时存在。
|
||
return createdUser, identity, nil
|
||
}
|
||
if !xerr.IsCode(err, xerr.DisplayUserIDExists) {
|
||
// 非短号冲突错误不能重试,否则可能掩盖真实存储异常。
|
||
return userdomain.User{}, userdomain.Identity{}, err
|
||
}
|
||
}
|
||
|
||
// 有限重试耗尽后返回稳定 reason,调用方可以提示稍后重试。
|
||
return userdomain.User{}, userdomain.Identity{}, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
|
||
}
|