450 lines
20 KiB
Go
450 lines
20 KiB
Go
// Package auth 实现 user-service 的认证、三方登录、密码登录和 refresh session 用例。
|
||
package auth
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"hyapp/pkg/idgen"
|
||
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||
invitedomain "hyapp/services/user-service/internal/domain/invite"
|
||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||
userservice "hyapp/services/user-service/internal/service/user"
|
||
)
|
||
|
||
const (
|
||
// hashAlgBcrypt 是当前密码 hash 算法标识。
|
||
hashAlgBcrypt = "bcrypt"
|
||
// tokenType 是对外 token 类型。
|
||
tokenType = "Bearer"
|
||
// loginPassword 是密码登录审计类型。
|
||
loginPassword = "password"
|
||
// loginThird 是三方登录审计类型。
|
||
loginThird = "third_party"
|
||
// loginRefresh 是 refresh token 轮换审计类型。
|
||
loginRefresh = "refresh"
|
||
// loginAccessResign 是不轮换 refresh token 的 access token 重签审计类型。
|
||
loginAccessResign = "access_resign"
|
||
// loginSetPass 是设置密码审计类型。
|
||
loginSetPass = "set_password"
|
||
// resultSuccess 是审计成功结果。
|
||
resultSuccess = "success"
|
||
// resultFailed 是审计失败结果。
|
||
resultFailed = "failed"
|
||
// revokeReasonRefreshRotated 表示 refresh token 已被成功轮换,旧 access token 必须立刻失效。
|
||
revokeReasonRefreshRotated = "REFRESH_ROTATED"
|
||
// revokeReasonUserLogout 表示用户主动登出,旧 access token 必须立刻失效。
|
||
revokeReasonUserLogout = "USER_LOGOUT"
|
||
)
|
||
|
||
// AuthRepository 是认证用例的持久化边界。
|
||
// 跨表写入仍按用例保留事务入口,例如三方首次登录要一次性创建用户、默认短号、三方身份和 session。
|
||
type AuthRepository interface {
|
||
// AllocateDisplayUserID 分配当前 App 下的随机外观默认短号;最终唯一性由创建事务里的展示号事实表兜底。
|
||
AllocateDisplayUserID(ctx context.Context, appCode string) (string, error)
|
||
// SetPassword 为已有用户首次写入密码身份。
|
||
SetPassword(ctx context.Context, account authdomain.PasswordAccount) error
|
||
// CreatePasswordUser 原子创建用户、默认短号、密码身份和可选首个 session;全站机器人会传空 session。
|
||
CreatePasswordUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, account authdomain.PasswordAccount, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) error
|
||
// FindPasswordByDisplayUserID 通过当前 active 展示号或 held 默认短号登录别名读取密码身份。
|
||
FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error)
|
||
// CreateSession 创建新的 refresh session。
|
||
CreateSession(ctx context.Context, session authdomain.Session) error
|
||
// FindActiveSessionByRefreshHash 通过 refresh token hash 查找未过期未吊销 session。
|
||
FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error)
|
||
// FindActiveSessionByID 通过 session_id 查找未过期未吊销 session,用于只重签 access token 的链路。
|
||
FindActiveSessionByID(ctx context.Context, sessionID string, nowMs int64) (authdomain.Session, error)
|
||
// ReplaceSession 原子吊销旧 session 并创建新 session。
|
||
ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error
|
||
// RevokeSession 按 session_id 或 refresh token hash 吊销 session。
|
||
RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error)
|
||
// RevokeSessionWithReason 按 session_id 幂等吊销 session,并持久化撤销来源。
|
||
RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error)
|
||
// RevokeUserDeviceSessionsForRisk 按风险源 session 定位同一设备后续 session,吊销 active session,并返回需要写入 denylist 的 session_id。
|
||
RevokeUserDeviceSessionsForRisk(ctx context.Context, sourceSessionID string, userID int64, revokedAtMs int64, reason string, requestID string, revokedBy string) ([]string, error)
|
||
// UpdateSessionHeartbeat 刷新当前 App 登录会话心跳,必须校验 session 仍属于当前用户且未过期未吊销。
|
||
UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error)
|
||
// FindThirdPartyIdentity 查找 provider + subject 的绑定。
|
||
FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
|
||
// CountUsersByRegisterDeviceID 统计当前 App 下已使用同一注册设备号的账号数量。
|
||
CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error)
|
||
// ListDisplayUserIDsByRegisterDeviceID 按原注册顺序列出同设备已注册账号短号,用于上限判断和失败提示。
|
||
ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error)
|
||
// CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。
|
||
CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) error
|
||
// CompleteThirdPartyRegistration 原子补齐旧三方 pending 用户资料并创建首个注册完成 session。
|
||
CompleteThirdPartyRegistration(ctx context.Context, command userdomain.CompleteOnboardingCommand, session authdomain.Session) (userdomain.User, error)
|
||
// GetRegisterRiskConfig 读取当前 App 的注册风控配置。
|
||
GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, error)
|
||
// UpsertRegisterRiskConfig 保存当前 App 的注册风控配置。
|
||
UpsertRegisterRiskConfig(ctx context.Context, config authdomain.RegisterRiskConfig) (authdomain.RegisterRiskConfig, error)
|
||
// RecordLoginAudit 记录登录审计,主链路不会因为审计失败而失败。
|
||
RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error
|
||
// CreateLoginIPRiskJob 创建登录后异步 IP 风控任务,失败只影响复核能力。
|
||
CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
|
||
// ClaimLoginIPRiskJobs 按 DB 租约认领待处理风控任务。
|
||
ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error)
|
||
// CompleteLoginIPRiskJob 写入任务终态。
|
||
CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
|
||
// CreateLoginIPRiskDecision 持久化 IP 风控决策审计。
|
||
CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error
|
||
// ListEnabledLoginRiskCountryBlocks 返回当前 App 的动态屏蔽国家词表。
|
||
ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error)
|
||
// ListEnabledLoginRiskIPWhitelist 返回当前 App 的登录地区屏蔽 IP 白名单。
|
||
ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error)
|
||
// ListEnabledLoginRiskUserWhitelist 返回当前 App 的登录地区屏蔽用户白名单。
|
||
ListEnabledLoginRiskUserWhitelist(ctx context.Context) ([]authdomain.LoginRiskUserWhitelist, error)
|
||
// MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。
|
||
MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error
|
||
}
|
||
|
||
// UserRepository 为 auth service 提供登录签发前的用户快照读取。
|
||
// Auth service 不关心用户创建和短号修改细节,只需要判断用户是否可登录以及签发当前展示号。
|
||
type UserRepository interface {
|
||
// GetUser 返回登录签发前需要的用户快照。
|
||
GetUser(ctx context.Context, userID int64) (userdomain.User, error)
|
||
}
|
||
|
||
// IdentityRepository 为 auth service 提供靓号懒过期能力。
|
||
// 过期恢复仍由 identity repository 的事务完成,避免 auth service 手动改 users 和短号表。
|
||
type IdentityRepository interface {
|
||
// ExpirePrettyDisplayUserID 原子恢复过期靓号,避免 token 签发过期展示号。
|
||
ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error)
|
||
}
|
||
|
||
// ThirdPartyVerifier 把 provider credential 校验成稳定 provider_subject。
|
||
// gateway 不接触 provider secret,三方校验必须停留在 user-service。
|
||
type ThirdPartyVerifier interface {
|
||
// Verify 校验 provider credential,并返回 provider 内稳定身份。
|
||
Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error)
|
||
}
|
||
|
||
// RegistrationRewardCommand 是三方首次注册成功后发往 activity-service 的幂等奖励命令。
|
||
type RegistrationRewardCommand struct {
|
||
AppCode string
|
||
RequestID string
|
||
UserID int64
|
||
CommandID string
|
||
}
|
||
|
||
// RegistrationRewardIssuer 隔离 auth service 和 activity protobuf,避免认证领域直接依赖外部 RPC 细节。
|
||
type RegistrationRewardIssuer interface {
|
||
IssueRegistrationReward(ctx context.Context, command RegistrationRewardCommand) error
|
||
}
|
||
|
||
// RegistrationLevelBadgeCommand 是三方首次注册成功后发往 activity-service 的等级徽章发放命令。
|
||
type RegistrationLevelBadgeCommand struct {
|
||
AppCode string
|
||
RequestID string
|
||
UserID int64
|
||
CommandID string
|
||
}
|
||
|
||
// RegistrationLevelBadgeIssuer 隔离注册等级徽章发放 RPC;失败不能回滚用户注册主事务。
|
||
type RegistrationLevelBadgeIssuer interface {
|
||
IssueRegistrationLevelBadges(ctx context.Context, command RegistrationLevelBadgeCommand) error
|
||
}
|
||
|
||
// IMAccountImporter 把 user-service 用户导入腾讯 IM 账号体系。
|
||
type IMAccountImporter interface {
|
||
ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error
|
||
}
|
||
|
||
// Config 保存认证底座需要的稳定参数。
|
||
type Config struct {
|
||
// Issuer 写入 access token 的 iss claim。
|
||
Issuer string
|
||
// AccessTokenTTLSec 是 App 登录 access token 有效期。
|
||
AccessTokenTTLSec int64
|
||
// RefreshTokenTTLSec 是服务端 refresh session 有效期。
|
||
RefreshTokenTTLSec int64
|
||
// SigningAlg 当前只支持 HS256。
|
||
SigningAlg string
|
||
// SigningSecret 是 HS256 签名密钥。
|
||
SigningSecret string
|
||
}
|
||
|
||
// LoginRiskPolicy 保存 IP 风控 worker 的判定和缓存策略。
|
||
type LoginRiskPolicy struct {
|
||
Enabled bool
|
||
UnsupportedCountries []string
|
||
BlockedTTL time.Duration
|
||
AllowedTTL time.Duration
|
||
UnknownTTL time.Duration
|
||
ProviderTimeout time.Duration
|
||
ProviderNames []string
|
||
DenylistTTL time.Duration
|
||
}
|
||
|
||
// IPDecisionCache 是 Redis 风控快查缓存和 session denylist 的最小接口。
|
||
type IPDecisionCache interface {
|
||
GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error)
|
||
SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error
|
||
SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error
|
||
GetIPWhitelist(ctx context.Context, appCode string) ([]string, bool, error)
|
||
SetIPWhitelist(ctx context.Context, appCode string, ips []string, ttl time.Duration) error
|
||
GetUserWhitelist(ctx context.Context, appCode string) ([]int64, bool, error)
|
||
SetUserWhitelist(ctx context.Context, appCode string, userIDs []int64, ttl time.Duration) error
|
||
}
|
||
|
||
// RegisterRiskConfigCache 是注册风控配置的 Redis 快取边界。
|
||
// 该缓存只做读放大优化,MySQL 仍是配置的持久事实来源。
|
||
type RegisterRiskConfigCache interface {
|
||
GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, bool, error)
|
||
SetRegisterRiskConfig(ctx context.Context, config authdomain.RegisterRiskConfig, ttl time.Duration) error
|
||
}
|
||
|
||
// IPDecision 是 Redis 中 IP 决策 JSON 的领域投影。
|
||
type IPDecision struct {
|
||
Decision string `json:"decision"`
|
||
Country string `json:"country"`
|
||
Source string `json:"source"`
|
||
CheckedAtMs int64 `json:"checked_at_ms"`
|
||
ExpiresAtMs int64 `json:"expires_at_ms"`
|
||
}
|
||
|
||
// IPGeoProvider 表达一个按优先级调用的 IP Geo 来源。
|
||
type IPGeoProvider interface {
|
||
Name() string
|
||
ResolveCountry(ctx context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error)
|
||
}
|
||
|
||
// IPGeoResult 是 provider 返回的标准化结果。
|
||
type IPGeoResult struct {
|
||
CountryCode string
|
||
Confidence int
|
||
}
|
||
|
||
// Service 承载登录注册用例。
|
||
type Service struct {
|
||
// cfg 保存 token 签发配置。
|
||
cfg Config
|
||
// authRepository 负责密码身份、session、三方绑定和登录审计。
|
||
authRepository AuthRepository
|
||
// userRepository 提供登录签发前的用户主状态读取。
|
||
userRepository UserRepository
|
||
// identityRepository 提供靓号懒过期恢复事务。
|
||
identityRepository IdentityRepository
|
||
// countryRegionRepository 提供三方首次注册时的国家主数据校验和区域解析。
|
||
countryRegionRepository userservice.CountryRegionRepository
|
||
// idGenerator 生成三方首次登录时的新 user_id。
|
||
idGenerator userservice.IDGenerator
|
||
// displayUserIDAllocator 生成三方首次登录时的默认短号候选。
|
||
displayUserIDAllocator userservice.DisplayUserIDAllocator
|
||
// thirdPartyVerifier 把 provider credential 校验成稳定 subject。
|
||
thirdPartyVerifier ThirdPartyVerifier
|
||
// now 是服务时钟,测试可注入固定时间。
|
||
now func() time.Time
|
||
// allocateMaxAttempts 是默认短号冲突重试上限。
|
||
allocateMaxAttempts int
|
||
// loginRiskPolicy 控制登录 IP 风控 worker 的判定和 TTL。
|
||
loginRiskPolicy LoginRiskPolicy
|
||
// ipDecisionCache 读写 Redis IP 决策和 session denylist,nil 时按 fail-open 处理。
|
||
ipDecisionCache IPDecisionCache
|
||
// registerRiskConfigCache 缓存注册风控配置,后台修改后会立即刷新同一个 Redis key。
|
||
registerRiskConfigCache RegisterRiskConfigCache
|
||
// ipGeoProviders 按顺序提供 IP 国家解析能力。
|
||
ipGeoProviders []IPGeoProvider
|
||
// registrationRewardIssuer 在注册事务提交后异步式触发奖励;失败只记录日志,不回滚用户注册。
|
||
registrationRewardIssuer RegistrationRewardIssuer
|
||
// registrationLevelBadgeIssuer 在注册事务提交后补发三条等级轨道的 1 级徽章。
|
||
registrationLevelBadgeIssuer RegistrationLevelBadgeIssuer
|
||
// imAccountImporter 在用户创建成功后补齐腾讯 IM 账号;UserSig 入口仍会兜底补偿历史账号。
|
||
imAccountImporter IMAccountImporter
|
||
}
|
||
|
||
// Option 调整 auth service 的依赖和策略。
|
||
type Option func(*Service)
|
||
|
||
// New 创建认证服务。
|
||
func New(cfg Config, options ...Option) *Service {
|
||
// 默认依赖只保证可构造,生产必须通过 app 层注入 repository 和真实配置。
|
||
svc := &Service{
|
||
cfg: cfg,
|
||
idGenerator: idgen.NewInt64Generator(0),
|
||
displayUserIDAllocator: userservice.NumericDisplayUserIDAllocator{},
|
||
thirdPartyVerifier: NewStaticThirdPartyVerifier([]string{"wechat"}),
|
||
now: time.Now,
|
||
allocateMaxAttempts: 8,
|
||
loginRiskPolicy: DefaultLoginRiskPolicy(),
|
||
}
|
||
|
||
for _, option := range options {
|
||
// option 只替换依赖和策略,不执行 IO。
|
||
option(svc)
|
||
}
|
||
|
||
return svc
|
||
}
|
||
|
||
func DefaultLoginRiskPolicy() LoginRiskPolicy {
|
||
return LoginRiskPolicy{
|
||
Enabled: true,
|
||
UnsupportedCountries: []string{"CN"},
|
||
BlockedTTL: 30 * 24 * time.Hour,
|
||
AllowedTTL: 7 * 24 * time.Hour,
|
||
UnknownTTL: 30 * time.Minute,
|
||
ProviderTimeout: 800 * time.Millisecond,
|
||
DenylistTTL: 7*24*time.Hour + time.Minute,
|
||
}
|
||
}
|
||
|
||
func WithLoginRiskPolicy(policy LoginRiskPolicy) Option {
|
||
return func(s *Service) {
|
||
defaults := DefaultLoginRiskPolicy()
|
||
if policy.BlockedTTL <= 0 {
|
||
policy.BlockedTTL = defaults.BlockedTTL
|
||
}
|
||
if policy.AllowedTTL <= 0 {
|
||
policy.AllowedTTL = defaults.AllowedTTL
|
||
}
|
||
if policy.UnknownTTL <= 0 {
|
||
policy.UnknownTTL = defaults.UnknownTTL
|
||
}
|
||
if policy.ProviderTimeout <= 0 {
|
||
policy.ProviderTimeout = defaults.ProviderTimeout
|
||
}
|
||
if policy.DenylistTTL <= 0 {
|
||
policy.DenylistTTL = defaults.DenylistTTL
|
||
if s.cfg.AccessTokenTTLSec > 0 {
|
||
policy.DenylistTTL = time.Duration(s.cfg.AccessTokenTTLSec+60) * time.Second
|
||
}
|
||
}
|
||
s.loginRiskPolicy = policy
|
||
}
|
||
}
|
||
|
||
func WithIPDecisionCache(cache IPDecisionCache) Option {
|
||
return func(s *Service) {
|
||
s.ipDecisionCache = cache
|
||
}
|
||
}
|
||
|
||
func WithRegisterRiskConfigCache(cache RegisterRiskConfigCache) Option {
|
||
return func(s *Service) {
|
||
s.registerRiskConfigCache = cache
|
||
}
|
||
}
|
||
|
||
func WithIPGeoProviders(providers []IPGeoProvider) Option {
|
||
return func(s *Service) {
|
||
s.ipGeoProviders = providers
|
||
}
|
||
}
|
||
|
||
// WithRegistrationRewardIssuer 注入注册奖励发放 client;nil 表示本环境不启用该集成。
|
||
func WithRegistrationRewardIssuer(issuer RegistrationRewardIssuer) Option {
|
||
return func(s *Service) {
|
||
if issuer != nil {
|
||
s.registrationRewardIssuer = issuer
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithRegistrationLevelBadgeIssuer 注入注册等级徽章发放 client;nil 表示本环境不启用该集成。
|
||
func WithRegistrationLevelBadgeIssuer(issuer RegistrationLevelBadgeIssuer) Option {
|
||
return func(s *Service) {
|
||
if issuer != nil {
|
||
s.registrationLevelBadgeIssuer = issuer
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithIMAccountImporter 注入腾讯 IM 账号导入边界;nil 表示当前环境不触发真实 IM 副作用。
|
||
func WithIMAccountImporter(importer IMAccountImporter) Option {
|
||
return func(s *Service) {
|
||
if importer != nil {
|
||
s.imAccountImporter = importer
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithAuthRepository 注入纯认证持久化实现。
|
||
func WithAuthRepository(repository AuthRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// nil 不覆盖现有配置,避免测试误传导致 service 不可用。
|
||
s.authRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithUserRepository 注入用户快照读取实现。
|
||
func WithUserRepository(repository UserRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// auth service 只读取用户快照,不直接创建或修改用户资料。
|
||
s.userRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithIdentityRepository 注入短号懒过期实现。
|
||
func WithIdentityRepository(repository IdentityRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// identityRepository 只负责过期靓号恢复事务。
|
||
s.identityRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithCountryRegionRepository 注入国家和区域解析 repository。
|
||
func WithCountryRegionRepository(repository userservice.CountryRegionRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
s.countryRegionRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithIDGenerator 注入 user_id 发号器。
|
||
func WithIDGenerator(generator userservice.IDGenerator) Option {
|
||
return func(s *Service) {
|
||
if generator != nil {
|
||
// 三方首次登录需要生成新用户 ID。
|
||
s.idGenerator = generator
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithDisplayUserIDAllocator 注入短号候选生成器。
|
||
func WithDisplayUserIDAllocator(allocator userservice.DisplayUserIDAllocator) Option {
|
||
return func(s *Service) {
|
||
if allocator != nil {
|
||
// allocator 只生成候选,唯一性由 repository 事务保证。
|
||
s.displayUserIDAllocator = allocator
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithThirdPartyVerifier 注入三方 provider 校验器。
|
||
func WithThirdPartyVerifier(verifier ThirdPartyVerifier) Option {
|
||
return func(s *Service) {
|
||
if verifier != nil {
|
||
// verifier 可替换为真实三方 SDK 实现。
|
||
s.thirdPartyVerifier = verifier
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithClock 注入时间函数,保证 session 轮换测试稳定。
|
||
func WithClock(now func() time.Time) Option {
|
||
return func(s *Service) {
|
||
if now != nil {
|
||
// 固定时钟用于 session 过期和 refresh 轮换测试。
|
||
s.now = now
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithDisplayUserIDAllocateMaxAttempts 配置默认短号分配重试次数。
|
||
func WithDisplayUserIDAllocateMaxAttempts(maxAttempts int) Option {
|
||
return func(s *Service) {
|
||
if maxAttempts > 0 {
|
||
// 只接受正数,避免三方注册永远无法分配默认短号。
|
||
s.allocateMaxAttempts = maxAttempts
|
||
}
|
||
}
|
||
}
|