feat: sync account deletion lifecycle

Sync telesrv 73f5c91 (feat(account): implement unified account deletion lifecycle).

Skipped telesrv docs changes per public sync rules.
This commit is contained in:
A 2026-07-19 20:35:58 +08:00
parent 96a419b565
commit edb7057757
32 changed files with 3236 additions and 88 deletions

View file

@ -174,7 +174,12 @@ func DefaultAccountReactionSettings() AccountReactionSettings {
}
// DefaultAccountTTLDays 是账号自毁默认期限(无显式设置时)。与历史固定回显一致。
const DefaultAccountTTLDays = 365
const (
DefaultAccountTTLDays = 365
// MaxAccountTTLDays prevents an untrusted int32 TL value from producing an
// out-of-range PostgreSQL interval/timestamp during deadline maintenance.
MaxAccountTTLDays = 3650
)
// GlobalPrivacy 是 globalPrivacySettings 的业务层表达(账号级隐私开关)。
// DisallowedGifts 依赖礼物资产模型(当前未实现),故不建模、保持默认。
@ -212,7 +217,7 @@ func DefaultAccountSettings() AccountSettings {
// NormalizedTTLDays 返回钳制后的账号自毁期限(0/越界回落默认)。
func (s AccountSettings) NormalizedTTLDays() int {
if s.AccountTTLDays <= 0 {
if s.AccountTTLDays <= 0 || s.AccountTTLDays > MaxAccountTTLDays {
return DefaultAccountTTLDays
}
return s.AccountTTLDays

View file

@ -0,0 +1,99 @@
package domain
import (
"errors"
"time"
)
var (
ErrAccountDeleted = errors.New("account deleted")
ErrAccountDeletionForbidden = errors.New("account deletion forbidden")
ErrAccountDeletionHashInvalid = errors.New("account deletion hash invalid")
ErrAccountDeletionNotPending = errors.New("account deletion not pending")
)
// AccountDeletionSource is the single audited reason attached to a user
// tombstone. Different entry points share one execution and cleanup path.
type AccountDeletionSource string
const (
AccountDeletionManual AccountDeletionSource = "manual"
AccountDeletionForgotPassword AccountDeletionSource = "forgot_password"
AccountDeletionTOSDecline AccountDeletionSource = "tos_decline"
AccountDeletionPasswordResetExpiry AccountDeletionSource = "password_reset_expiry"
AccountDeletionAccountTTL AccountDeletionSource = "account_ttl"
AccountDeletionFreezeExpiry AccountDeletionSource = "freeze_expiry"
)
type AccountDeletionRequestState string
const (
AccountDeletionPending AccountDeletionRequestState = "pending"
AccountDeletionCancelled AccountDeletionRequestState = "cancelled"
AccountDeletionExecuted AccountDeletionRequestState = "executed"
)
// AccountDeletionRequest represents the seven-day 2FA confirmation window.
// ConfirmHashDigest is SHA-256(raw link token); the raw token is only included
// in the durable service message and is never persisted as a credential.
type AccountDeletionRequest struct {
ID int64
UserID int64
RequesterAuthKeyID [8]byte
State AccountDeletionRequestState
Reason string
ConfirmHashDigest [32]byte
RequestedAt time.Time
ExecuteAt time.Time
CompletedAt time.Time
}
type AccountDeletionSnapshot struct {
User User
HasPassword bool
PasswordUpdatedAt time.Time
Pending *AccountDeletionRequest
}
type ScheduleAccountDeletion struct {
UserID int64
RequesterAuthKeyID [8]byte
Reason string
ConfirmHashDigest [32]byte
ServiceMessage string
RequestedAt time.Time
ExecuteAt time.Time
}
type AccountDeletionResult struct {
User User
Changed bool
RevokedAuthorizations []Authorization
}
type AccountDeleteKind string
const (
AccountDeleteImmediate AccountDeleteKind = "immediate"
AccountDeleteDelayed AccountDeleteKind = "delayed"
)
type AccountDeleteOutcome struct {
Kind AccountDeleteKind
WaitSeconds int
ExecuteAt time.Time
Deletion AccountDeletionResult
}
type AccountDeletionCandidate struct {
UserID int64
Source AccountDeletionSource
DueAt time.Time
}
type AccountDeletionNotification struct {
ID int64
TargetUserID int64
DeletedUserID int64
Attempts int
}

View file

@ -1,5 +1,7 @@
package domain
import "time"
// UserIDSequenceBase 是普通用户 ID 的起始值。
//
// 取 2026-06-01 00:00:00 Asia/Shanghai 的 Unix 秒级时间戳。
@ -63,6 +65,15 @@ type User struct {
PhotoHasVideo bool
LastSeenAt int
Status UserStatus
// Deleted is the durable tombstone state. Deleted users remain addressable by
// ID so historical messages can render "Deleted Account", but all profile
// and reusable identity fields are cleared at the store boundary.
Deleted bool
DeletedAt int64
DeletionSource AccountDeletionSource
DeletionReason string
CreatedAt time.Time
AccountDeleteAt time.Time
}
// PremiumActiveAt 报告用户在 now(Unix 秒)时刻是否为有效会员。
@ -81,6 +92,25 @@ func (u User) EmojiStatusActiveAt(now int64) bool {
return u.EmojiStatusUntil == 0 || int64(u.EmojiStatusUntil) > now
}
// DeletedTombstone strips every viewer-dependent or personally identifying
// field while preserving the immutable id and lifecycle audit facts.
func (u User) DeletedTombstone() User {
if !u.Deleted {
return u
}
return User{
ID: u.ID,
AccessHash: u.AccessHash,
Deleted: true,
DeletedAt: u.DeletedAt,
DeletionSource: u.DeletionSource,
DeletionReason: u.DeletionReason,
CreatedAt: u.CreatedAt,
AccountDeleteAt: u.AccountDeleteAt,
Status: UserStatus{Kind: UserStatusEmpty},
}
}
// UserStatusKind is a protocol-neutral account presence state.
type UserStatusKind int