feat: sync monoforum and collectible emoji status

Sync telesrv bd15657 (feat(account): implement collectible emoji status).

Skipped telesrv docs changes per public sync rules.
This commit is contained in:
A 2026-07-19 20:37:10 +08:00
parent edb7057757
commit 0c99ae0a9d
91 changed files with 4061 additions and 693 deletions

View file

@ -16,6 +16,73 @@ type PeerColor struct {
BackgroundEmojiID int64
}
// EmojiStatusCollectible is the immutable projection needed to render a
// collectible gift as an emoji status. The source of truth remains the owned
// UniqueStarGift; users store an immutable snapshot so every user projection,
// online update and offline difference observes the same shape without an
// RPC-layer lookup.
type EmojiStatusCollectible struct {
CollectibleID int64 `json:"collectible_id"`
DocumentID int64 `json:"document_id"`
Title string `json:"title"`
Slug string `json:"slug"`
PatternDocumentID int64 `json:"pattern_document_id"`
CenterColor int `json:"center_color"`
EdgeColor int `json:"edge_color"`
PatternColor int `json:"pattern_color"`
TextColor int `json:"text_color"`
}
// Empty reports whether no collectible status is present.
func (s EmojiStatusCollectible) Empty() bool {
return s == (EmojiStatusCollectible{})
}
// Valid enforces the complete collectible status shape. Partial snapshots
// are forbidden because clients would otherwise render a gradient without its
// model/pattern or be unable to resolve the collectible link.
func (s EmojiStatusCollectible) Valid() bool {
if s.CollectibleID <= 0 || s.DocumentID <= 0 || s.PatternDocumentID <= 0 ||
s.Title == "" || s.Slug == "" {
return false
}
for _, color := range []int{s.CenterColor, s.EdgeColor, s.PatternColor, s.TextColor} {
if color < 0 || color > 0xffffff {
return false
}
}
return true
}
// UserEmojiStatus is the protocol-neutral mutation value accepted by the user
// service/store boundary. Exactly one of a normal document or a complete
// collectible snapshot may be active; the zero value clears the status.
type UserEmojiStatus struct {
DocumentID int64 `json:"document_id"`
Until int `json:"until,omitempty"`
Collectible EmojiStatusCollectible `json:"collectible,omitempty"`
}
func (s UserEmojiStatus) Empty() bool {
return s.DocumentID == 0 && s.Collectible.Empty()
}
func (s UserEmojiStatus) Valid() bool {
if s.Until < 0 {
return false
}
if s.Empty() {
return s.Until == 0
}
if s.DocumentID <= 0 {
return false
}
if s.Collectible.Empty() {
return true
}
return s.Collectible.Valid() && s.DocumentID == s.Collectible.DocumentID
}
// Empty reports whether no explicit color/profile color state is set.
func (c PeerColor) Empty() bool {
return !c.HasColor && c.BackgroundEmojiID == 0
@ -47,9 +114,11 @@ type User struct {
PremiumUntil int
// EmojiStatusDocumentID / EmojiStatusUntil 是用户自定义 emoji status
//premium 专属account.updateEmojiStatus。DocumentID==0 表示未设置;
// Until==0 表示永久。
EmojiStatusDocumentID int64
EmojiStatusUntil int
// Until==0 表示永久。EmojiStatusCollectible 非零时 DocumentID 必须等于
// collectible 的 model document id。
EmojiStatusDocumentID int64
EmojiStatusUntil int
EmojiStatusCollectible EmojiStatusCollectible
// Birthday 是用户公开生日account.updateBirthday。零值表示未设置。
Birthday Birthday
// PersonalChannelID 是资料页展示的「个人频道」account.updatePersonalChannel
@ -86,12 +155,21 @@ func (u User) PremiumActiveAt(now int64) bool {
// 已设置且未过期Until==0 表示永久。emoji status 是 premium 专属,到期
// 降级后即便列仍有残值也不再下发。
func (u User) EmojiStatusActiveAt(now int64) bool {
if !u.PremiumActiveAt(now) || u.EmojiStatusDocumentID == 0 {
if !u.PremiumActiveAt(now) || !u.EmojiStatus().Valid() || u.EmojiStatusDocumentID == 0 {
return false
}
return u.EmojiStatusUntil == 0 || int64(u.EmojiStatusUntil) > now
}
// EmojiStatus returns the complete status snapshot carried by this user.
func (u User) EmojiStatus() UserEmojiStatus {
return UserEmojiStatus{
DocumentID: u.EmojiStatusDocumentID,
Until: u.EmojiStatusUntil,
Collectible: u.EmojiStatusCollectible,
}
}
// DeletedTombstone strips every viewer-dependent or personally identifying
// field while preserving the immutable id and lifecycle audit facts.
func (u User) DeletedTombstone() User {