// Package user 实现用户主数据、展示短号和临时靓号用例。 package user import ( "context" "time" "hyapp/pkg/idgen" userdomain "hyapp/services/user-service/internal/domain/user" ) // UserRepository 隔离用户主数据读写。 // 它只覆盖 users 主表视角和“创建用户+默认短号”的事务入口,避免查询用例依赖短号变更细节。 type UserRepository interface { // GetUser 查询单个用户主状态。 GetUser(ctx context.Context, userID int64) (userdomain.User, error) // BatchGetUsers 批量查询用户主状态,缺失用户不应返回占位对象。 BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) // CreateUserWithIdentity 原子创建用户和默认短号。 CreateUserWithIdentity(ctx context.Context, createdUser userdomain.User, identity userdomain.Identity) error // UpdateUserProfile 原子修改用户基础资料。 UpdateUserProfile(ctx context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error) // CompleteOnboarding 原子写注册页必填资料并标记 profile_completed。 CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error) // ChangeUserCountry 原子修改用户国家并写变更日志。 ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, error) } // AppRegistryRepository 负责把客户端包名解析成内部 app_code。 type AppRegistryRepository interface { ResolveApp(ctx context.Context, appCode string, packageName string, platform string) (userdomain.App, error) } // CountryRegionRepository 负责把用户选择的 country_code 解析成国家主数据和当前 active 区域。 // 注册和改国家都依赖这组同步读,避免未知国家码进入 users 主表。 type CountryRegionRepository interface { // ResolveEnabledCountryByCode 按国家码读取 App 可选国家;必须存在且 enabled=true。 ResolveEnabledCountryByCode(ctx context.Context, countryCode string) (userdomain.Country, bool, error) // ListRegistrationCountries 返回 App 注册页可选国家列表。 ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) // ResolveActiveRegionByCountry 按国家码读取当前 active 区域;无映射返回 ok=false。 ResolveActiveRegionByCountry(ctx context.Context, countryCode string) (userdomain.Region, bool, error) } // CountryAdminRepository 是国家管理 RPC 的持久化边界。 type CountryAdminRepository interface { CountryRegionRepository UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error) ListCountries(ctx context.Context, filter userdomain.CountryFilter) ([]userdomain.Country, error) } // RegionAdminRepository 是区域管理 RPC 的持久化边界。 type RegionAdminRepository interface { CountryRegionRepository UpdateRegion(ctx context.Context, command userdomain.UpdateRegionCommand) (userdomain.Region, error) ListRegions(ctx context.Context, filter userdomain.RegionFilter) ([]userdomain.Region, error) GetRegion(ctx context.Context, regionID int64) (userdomain.Region, error) ReplaceRegionCountries(ctx context.Context, command userdomain.ReplaceRegionCountriesCommand) (userdomain.Region, error) } // RegionRebuildRepository 负责消费 user_region_rebuild_tasks 并分页刷新 users.region_id。 type RegionRebuildRepository interface { // ProcessNextRegionRebuildTask 先持久化 claim,再处理一个待重算任务;没有任务时返回 NoTask=true。 ProcessNextRegionRebuildTask(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) (userdomain.RegionRebuildProcessResult, error) } // IdentityRepository 隔离 display_user_id 的归属、解析、变更和靓号租约。 // 用户 service 通过该接口表达短号用例,不直接感知 MySQL 的 identity/change_log/lease 表布局。 type IdentityRepository interface { // GetUserIdentity 按 user_id 读取当前 active display_user_id。 GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error) // ResolveDisplayUserID 把短号解析成 user_id,并按 nowMs 触发懒过期。 ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error) // ChangeDisplayUserID 原子修改默认短号并写变更日志。 ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) // ApplyPrettyDisplayUserID 原子创建靓号租约并覆盖当前展示号。 ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) // ExpirePrettyDisplayUserID 原子恢复默认短号。 ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error) } // IDGenerator 生成系统内部不可变 user_id。 type IDGenerator interface { // NewInt64 返回全局唯一 user_id。 NewInt64() int64 } // DisplayUserIDAllocator 为新用户生成默认短号候选。 type DisplayUserIDAllocator interface { // NextDisplayUserID 根据 user_id 和重试次数生成候选短号。 NextDisplayUserID(userID int64, attempt int) string } // Option 调整 user service 的运行时策略,测试可注入确定性发号器。 type Option func(*Service) // Service 承载用户主数据和短号用例。 // 字段按领域接口拆分,跨表事务仍保留在 repository 方法内,避免 service 层手动拼事务。 type Service struct { // appRegistryRepository 持有 App 注册表读取能力,gateway 解析 app_code 时使用。 appRegistryRepository AppRegistryRepository // userRepository 持有 users 主表视角的读写能力。 userRepository UserRepository // identityRepository 持有短号、靓号和变更日志事务能力。 identityRepository IdentityRepository // countryRegionRepository 持有国家主数据校验和国家到区域映射解析能力。 countryRegionRepository CountryRegionRepository // countryAdminRepository 持有国家管理事务能力。 countryAdminRepository CountryAdminRepository // regionAdminRepository 持有区域管理事务能力。 regionAdminRepository RegionAdminRepository // regionRebuildRepository 持有历史用户区域快照重算能力。 regionRebuildRepository RegionRebuildRepository // idGenerator 生成系统内部不可变 user_id。 idGenerator IDGenerator // displayUserIDAllocator 生成默认短号候选,唯一性由 repository 保证。 displayUserIDAllocator DisplayUserIDAllocator // now 是服务时钟,测试可注入固定时间。 now func() time.Time // allocateMaxAttempts 是默认短号冲突重试上限。 allocateMaxAttempts int // displayUserIDCooldown 是用户修改默认短号的冷却期。 displayUserIDCooldown time.Duration // countryChangeCooldown 是用户修改国家的滚动冷却期。 countryChangeCooldown time.Duration } // New 创建用户服务。 // 构造函数只强制要求 UserRepository;如果同一实现也支持 IdentityRepository,则自动接入短号用例。 func New(userRepository UserRepository, options ...Option) *Service { // 默认策略可独立跑通本地单测;生产 repository 由 app 层注入。 svc := &Service{ userRepository: userRepository, idGenerator: idgen.NewInt64Generator(0), displayUserIDAllocator: NumericDisplayUserIDAllocator{}, now: time.Now, allocateMaxAttempts: 8, displayUserIDCooldown: 30 * 24 * time.Hour, countryChangeCooldown: 30 * 24 * time.Hour, } for _, option := range options { // option 只改依赖或策略,不做 IO。 option(svc) } return svc } // WithAppRegistryRepository 注入 App 注册表 repository。 func WithAppRegistryRepository(repository AppRegistryRepository) Option { return func(s *Service) { if repository != nil { s.appRegistryRepository = repository } } } // WithCountryRegionRepository 注入国家/区域解析 repository。 func WithCountryRegionRepository(repository CountryRegionRepository) Option { return func(s *Service) { if repository != nil { // 注册和改国家不能在缺失主数据校验时静默写入自由文本。 s.countryRegionRepository = repository } } } // WithCountryAdminRepository 注入国家管理 repository。 func WithCountryAdminRepository(repository CountryAdminRepository) Option { return func(s *Service) { if repository != nil { s.countryAdminRepository = repository s.countryRegionRepository = repository } } } // WithRegionAdminRepository 注入区域管理 repository。 func WithRegionAdminRepository(repository RegionAdminRepository) Option { return func(s *Service) { if repository != nil { s.regionAdminRepository = repository s.countryRegionRepository = repository } } } // WithRegionRebuildRepository 注入历史用户区域重算 repository。 func WithRegionRebuildRepository(repository RegionRebuildRepository) Option { return func(s *Service) { if repository != nil { s.regionRebuildRepository = repository } } } // WithCountryChangeCooldown 配置用户国家修改冷却窗口。 func WithCountryChangeCooldown(cooldown time.Duration) Option { return func(s *Service) { if cooldown >= 0 { // cooldown 允许为 0,便于本地测试或运营临时关闭限制。 s.countryChangeCooldown = cooldown } } } // WithIdentityRepository 注入独立短号 repository。 // 该 option 支持后续把用户主数据和短号 identity 拆成不同实现,同时不改变现有 New(mysqlRepo) 调用。 func WithIdentityRepository(repository IdentityRepository) Option { return func(s *Service) { if repository != nil { // 允许将用户主数据和短号事务拆到不同实现。 s.identityRepository = repository } } } // WithIDGenerator 注入 user_id 发号器。 func WithIDGenerator(generator IDGenerator) Option { return func(s *Service) { if generator != nil { // nil generator 不覆盖默认发号器,避免构造出不可创建用户的 service。 s.idGenerator = generator } } } // WithDisplayUserIDAllocator 注入短号候选生成器。 func WithDisplayUserIDAllocator(allocator DisplayUserIDAllocator) Option { return func(s *Service) { if allocator != nil { // allocator 只负责候选生成,不负责唯一性判断。 s.displayUserIDAllocator = allocator } } } // WithClock 注入时间函数,保证冷却期测试不依赖真实时间。 func WithClock(now func() time.Time) Option { return func(s *Service) { if now != nil { // 固定时钟用于冷却期、靓号过期和创建时间测试。 s.now = now } } } // WithDisplayUserIDPolicy 配置分配重试和修改冷却期。 func WithDisplayUserIDPolicy(maxAttempts int, cooldown time.Duration) Option { return func(s *Service) { if maxAttempts > 0 { // 只接受正数重试次数,避免配置错误导致无法创建用户。 s.allocateMaxAttempts = maxAttempts } if cooldown >= 0 { // cooldown 允许为 0,便于测试和运营豁免策略。 s.displayUserIDCooldown = cooldown } } }