2026-04-27 02:29:42 +08:00

158 lines
6.2 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 实现用户主数据、展示短号和临时靓号用例。
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
}
// 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)
}
// Repository 是兼容旧构造方式的组合接口。
// 新代码应优先让 Service 字段依赖 UserRepository 和 IdentityRepository而不是把所有方法塞回一个贫血大接口。
type Repository interface {
UserRepository
IdentityRepository
}
// 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 {
// userRepository 持有 users 主表视角的读写能力。
userRepository UserRepository
// identityRepository 持有短号、靓号和变更日志事务能力。
identityRepository IdentityRepository
// idGenerator 生成系统内部不可变 user_id。
idGenerator IDGenerator
// displayUserIDAllocator 生成默认短号候选,唯一性由 repository 保证。
displayUserIDAllocator DisplayUserIDAllocator
// now 是服务时钟,测试可注入固定时间。
now func() time.Time
// allocateMaxAttempts 是默认短号冲突重试上限。
allocateMaxAttempts int
// displayUserIDCooldown 是用户修改默认短号的冷却期。
displayUserIDCooldown 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,
}
if identityRepository, ok := userRepository.(IdentityRepository); ok {
// MySQL 和 memory repository 同时实现短号接口时自动启用短号用例。
svc.identityRepository = identityRepository
}
for _, option := range options {
// option 只改依赖或策略,不做 IO。
option(svc)
}
return svc
}
// WithIdentityRepository 注入独立短号 repository。
// 该 option 支持后续把用户主数据和短号 identity 拆成不同实现,同时不改变现有 New(memory/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
}
}
}