package models import ( "crypto/md5" "encoding/hex" "fmt" "strings" "time" "gorm.io/gorm" ) // Comment status constants. const ( CommentPending = 0 // 待审核 CommentApproved = 1 // 已通过 CommentRejected = 2 // 已拒绝(软拒绝;后台仍可查看,但前台不再显示) ) // Comment represents one reader-submitted comment on an article. Comments may // be nested via ParentID and authored by either a logged-in user (UserID) or // an anonymous visitor identified by a long-lived GuestToken cookie. type Comment struct { ID uint `gorm:"primarykey" json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` ArticleID uint `gorm:"not null;index" json:"article_id"` ParentID *uint `gorm:"index" json:"parent_id,omitempty"` // Authorship: logged-in users get UserID; anonymous visitors get a random // GuestToken stored in a cookie so they can see their own pending/private // comments on subsequent page loads. UserID *uint `gorm:"index" json:"user_id,omitempty"` GuestToken string `gorm:"size:64;index" json:"-"` AuthorName string `gorm:"not null;size:64" json:"author_name"` Email string `gorm:"not null;size:255" json:"-"` EmailHash string `gorm:"size:64;index" json:"email_hash"` Website string `gorm:"size:255" json:"website,omitempty"` Content string `gorm:"type:text;not null" json:"content"` IsPrivate bool `gorm:"default:false;index" json:"is_private"` Status int `gorm:"default:0;index" json:"status"` IPAddress string `gorm:"size:64" json:"-"` UserAgent string `gorm:"size:512" json:"-"` Article Article `gorm:"foreignKey:ArticleID" json:"-"` } // TableName overrides the default GORM table name. func (Comment) TableName() string { return "comments" } // HashEmail returns the md5 hash of a lowercase, trimmed email address. This // is the form expected by Gravatar. func HashEmail(email string) string { sum := md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))) return hex.EncodeToString(sum[:]) } // GravatarURL returns a Gravatar avatar URL for this comment's email hash. // Falls back to the "identicon" default avatar when no Gravatar exists. func (c *Comment) GravatarURL(size int) string { if size <= 0 { size = 64 } hash := c.EmailHash if hash == "" { hash = HashEmail(c.Email) } return fmt.Sprintf("https://www.gravatar.com/avatar/%s?s=%d&d=identicon", hash, size) } // MaskedEmail returns the email with the local part partially obscured, for // admin-side listings that should hint at identity without exposing the // full address. func (c *Comment) MaskedEmail() string { email := c.Email at := strings.LastIndex(email, "@") if at <= 0 { return email } local := email[:at] host := email[at:] if len(local) <= 2 { return string(local[0]) + "***" + host } return string(local[0]) + "***" + string(local[len(local)-1]) + host } // AuthorInitial returns an uppercase first character of AuthorName for use as // a text-based avatar placeholder when Gravatar is disabled. Returns "?" when // the name is empty. func (c *Comment) AuthorInitial() string { name := strings.TrimSpace(c.AuthorName) if name == "" { return "?" } r := []rune(name) return strings.ToUpper(string(r[0])) }