2026-07-06 12:43:40 +08:00

213 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
// 空批量请求返回空 mapgateway 可以直接输出空 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")
}