thread-master/apps/backend/internal/module/member/domain/member.go

147 lines
6.3 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 domain
import (
"strings"
"time"
)
// Member is the person profile (stand-alone user_info)uid 為數字主鍵。
type Member struct {
UID int64 `bson:"uid" json:"uid"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
Email string `bson:"email" json:"email"`
Phone string `bson:"phone,omitempty" json:"phone,omitempty"`
DisplayName string `bson:"display_name" json:"display_name"`
Nickname string `bson:"nickname,omitempty" json:"nickname,omitempty"`
FullName string `bson:"full_name,omitempty" json:"full_name,omitempty"`
Roles []string `bson:"roles" json:"roles"`
Status string `bson:"status" json:"status"`
EmailVerified bool `bson:"email_verified" json:"email_verified"`
EmailVerifiedAt *int64 `bson:"email_verified_at,omitempty" json:"email_verified_at,omitempty"`
PhoneVerified bool `bson:"phone_verified" json:"phone_verified"`
PhoneVerifiedAt *int64 `bson:"phone_verified_at,omitempty" json:"phone_verified_at,omitempty"`
InviteCode string `bson:"invite_code,omitempty" json:"invite_code,omitempty"`
ParentUID *int64 `bson:"parent_uid,omitempty" json:"parent_uid,omitempty"`
Bio string `bson:"bio,omitempty" json:"bio,omitempty"`
Timezone string `bson:"timezone,omitempty" json:"timezone,omitempty"`
AvatarURL string `bson:"avatar_url,omitempty" json:"avatar_url,omitempty"`
NotifyEmail bool `bson:"notify_email,omitempty" json:"notify_email,omitempty"`
GenderCode int8 `bson:"gender_code,omitempty" json:"gender_code,omitempty"`
Birthdate int64 `bson:"birthdate,omitempty" json:"birthdate,omitempty"` // YYYYMMDD
National string `bson:"national,omitempty" json:"national,omitempty"`
Address string `bson:"address,omitempty" json:"address,omitempty"`
PostCode string `bson:"post_code,omitempty" json:"post_code,omitempty"`
PreferredLanguage string `bson:"preferred_language,omitempty" json:"preferred_language,omitempty"`
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
// Primary password for platform email login (also mirrored on Identity).
// Must be JSON-tagged: monc Redis cache marshals with encoding/json;
// json:"-" strips the hash and makes warm-cache login always fail.
PasswordHash string `bson:"password_hash,omitempty" json:"password_hash,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
func (m Member) IsAdmin() bool {
for _, r := range m.Roles {
if r == RoleAdmin {
return true
}
}
return false
}
func (m Member) IsSuspended() bool {
return m.Status == StatusSuspended
}
// Identity binds a login channel to a member (stand-alone account + account_uid).
type Identity struct {
ID string `bson:"_id,omitempty" json:"id,omitempty"`
UID int64 `bson:"uid" json:"uid"`
LoginID string `bson:"login_id" json:"login_id"` // email / phone / oauth subject
Platform string `bson:"platform" json:"platform"` // platform|google|line|apple
AccountType string `bson:"account_type" json:"account_type"`
// json tag required for monc cache (see Member.PasswordHash).
PasswordHash string `bson:"password_hash,omitempty" json:"password_hash,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
// UserSettings — AI/placement overrides
type UserSettings struct {
UID int64 `bson:"uid" json:"uid"`
Provider string `bson:"provider" json:"provider"`
Model string `bson:"model" json:"model"`
APIKeyConfigured bool `bson:"api_key_configured" json:"api_key_configured"`
ResearchAPIKeyConfigured bool `bson:"research_api_key_configured" json:"research_api_key_configured"`
// APIKey — legacy single key (migrated into ProviderAPIKeys on save)
APIKey string `bson:"api_key,omitempty" json:"-"`
// ProviderAPIKeys — per-provider BYOK: "xai" | "opencode-go"
ProviderAPIKeys map[string]string `bson:"provider_api_keys,omitempty" json:"-"`
ExaAPIKey string `bson:"exa_api_key,omitempty" json:"-"`
WebSearchProvider string `bson:"web_search_provider" json:"web_search_provider"`
ExpandStrategy string `bson:"expand_strategy" json:"expand_strategy"`
ExaAPIKeyConfigured bool `bson:"exa_api_key_configured" json:"exa_api_key_configured"`
DevModeEnabled bool `bson:"dev_mode_enabled" json:"dev_mode_enabled"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
func DefaultSettings(uid int64) *UserSettings {
return &UserSettings{
UID: uid, Provider: "xai", Model: "grok-3",
WebSearchProvider: "exa", ExpandStrategy: "hybrid",
ProviderAPIKeys: map[string]string{},
}
}
// KeyForProvider returns BYOK for provider (legacy APIKey fallback for current provider).
func (s *UserSettings) KeyForProvider(provider string) string {
if s == nil {
return ""
}
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "" {
provider = s.Provider
}
if s.ProviderAPIKeys != nil {
if k := strings.TrimSpace(s.ProviderAPIKeys[provider]); k != "" {
return k
}
}
// legacy single key only applies to currently selected provider
if provider == s.Provider || s.Provider == "" {
return strings.TrimSpace(s.APIKey)
}
return ""
}
// SetKeyForProvider stores BYOK under provider map.
func (s *UserSettings) SetKeyForProvider(provider, key string) {
if s == nil {
return
}
provider = strings.ToLower(strings.TrimSpace(provider))
if s.ProviderAPIKeys == nil {
s.ProviderAPIKeys = map[string]string{}
}
key = strings.TrimSpace(key)
if key == "" {
delete(s.ProviderAPIKeys, provider)
} else {
s.ProviderAPIKeys[provider] = key
}
// keep legacy field in sync for current provider
if provider == s.Provider {
s.APIKey = key
s.APIKeyConfigured = key != ""
s.ResearchAPIKeyConfigured = key != ""
}
}
// HasKeyForProvider — UI configured flag.
func (s *UserSettings) HasKeyForProvider(provider string) bool {
return strings.TrimSpace(s.KeyForProvider(provider)) != ""
}
func NowNano() int64 { return time.Now().UTC().UnixNano() }