Documentation
¶
Overview ¶
Package comment 提供评论与回复树。
评论是本项目里唯一由**匿名访客**写入的用户内容,安全模型因此与文章正文相反: 正文信任已认证用户、原样保留 HTML;评论一律先全文转义再做有限的富化(见 render.go)。
依赖 content 模块的 posts 表与 mail 模块的发信服务:modules.go 中两者都必须先于本模块注册。
Index ¶
- Constants
- Variables
- func Count(nodes []*PublicView) int
- func Render(text string) string
- type Comment
- type ConfigFunc
- type DefaultChecker
- type Filter
- type Handler
- type Module
- func (m *Module) Delete(ctx context.Context, id int64) error
- func (m *Module) Migrations() fs.FS
- func (m *Module) Moderate(ctx context.Context, id int64, status Status) error
- func (m *Module) Name() string
- func (m *Module) Navigation() app.Navigation
- func (m *Module) Permissions() []app.Permission
- func (m *Module) Register(a *app.App) error
- func (m *Module) Routes(r app.Router)
- func (m *Module) Settings() []app.SettingGroup
- type Notifier
- type NotifyEvent
- type PostRef
- type PublicView
- type Settings
- type SpamChecker
- type SpamInput
- type Status
- type Store
- func (s *Store) AuthorEmail(ctx context.Context, userID int64) (string, error)
- func (s *Store) CountApproved(ctx context.Context, postID int64) (int, error)
- func (s *Store) CountPending(ctx context.Context, authorID int64) (int, error)
- func (s *Store) Create(ctx context.Context, c *Comment) error
- func (s *Store) Delete(ctx context.Context, id int64) error
- func (s *Store) Get(ctx context.Context, id int64) (*Comment, error)
- func (s *Store) LastByIP(ctx context.Context, ip string) (time.Time, error)
- func (s *Store) ListApproved(ctx context.Context, postID int64) ([]Comment, error)
- func (s *Store) Page(ctx context.Context, filter Filter, params api.PageParams) ([]Comment, int, error)
- func (s *Store) PostRef(ctx context.Context, id int64) (*PostRef, error)
- func (s *Store) SerializeByIP(ctx context.Context, ip string, fn func(ctx context.Context, tx *Store) error) error
- func (s *Store) UpdateStatus(ctx context.Context, id int64, status Status) error
- type Verdict
Constants ¶
const GroupComment = "comment"
GroupComment 是评论设置分组。
const Name = "comment"
Name 是模块名,也是迁移版本表的后缀(goose_db_version_comment)。
Variables ¶
var ( // ErrNotFound 表示评论不存在。 ErrNotFound = errors.New("评论不存在") // ErrPostNotFound 表示评论的目标内容不存在。 ErrPostNotFound = errors.New("评论的内容不存在") // ErrInvalid 表示字段违反了数据库约束。 ErrInvalid = errors.New("字段不合法") )
错误哨兵。处理器据此映射为 400 / 404。
Functions ¶
Types ¶
type Comment ¶
type Comment struct {
bun.BaseModel `bun:"table:comments,alias:cm"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
PostID int64 `bun:"post_id,notnull" json:"postId"`
ParentID *int64 `bun:"parent_id" json:"parentId" doc:"父评论 ID,顶层评论为 null"`
UserID *int64 `bun:"user_id" json:"userId" doc:"已登录用户的 ID;访客评论为 null"`
AuthorName string `bun:"author_name,notnull" json:"authorName"`
AuthorEmail string `bun:"author_email" json:"authorEmail" doc:"仅 Console 可见"`
AuthorURL string `bun:"author_url" json:"authorUrl"`
Content string `bun:"content,notnull" json:"content" doc:"访客提交的原文,纯文本"`
ContentHTML string `bun:"content_html" json:"contentHtml" doc:"转义并线性化后的展示版本"`
Status Status `bun:"status,notnull" json:"status" enum:"pending,approved,spam"`
IP string `bun:"ip" json:"ip" doc:"仅 Console 可见"`
UserAgent string `bun:"user_agent" json:"userAgent" doc:"仅 Console 可见"`
CreatedAt time.Time `bun:"created_at,nullzero" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at,nullzero" json:"updatedAt"`
// 以下为关联数据,由存储层按需填充,不是数据库列。
Post *PostRef `bun:"-" json:"post,omitempty"`
}
Comment 是一条评论。
字段分两类:带 json 标签的可以出现在 Console 响应里; 邮箱、IP 与 UA 是个人信息,只在 Console 平面出现,前台视图另有一套(见 PublicView)。
type ConfigFunc ¶
ConfigFunc 返回评论设置;由模块装配时注入设置服务,测试可传 nil 用缺省值。
type Filter ¶
type Filter struct {
Status Status
PostID int64
// AuthorID 非零时只返回该作者名下内容的评论,用于不带 _any 权限的调用者。
AuthorID int64
Q string
}
Filter 是后台列表的筛选条件。
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler 提供评论的 Console 与 Public 接口。
func NewHandler ¶
func NewHandler(store *Store, cfg ConfigFunc, spam SpamChecker, notify Notifier, events app.Events, logger *slog.Logger) *Handler
NewHandler 构造 Handler;notify 与 events 可为 nil,spam 为 nil 时用内置判定器。
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module 是评论模块。
func (*Module) Moderate ¶ added in v0.2.0
Moderate 改一条评论的状态,给插件这类内部调用方用:权限由调用方判定。 从别的状态改成通过时派发 comment.approved,与后台审核一致。
func (*Module) Navigation ¶
func (m *Module) Navigation() app.Navigation
Navigation 实现 app.NavigationProvider。
func (*Module) Permissions ¶
func (m *Module) Permissions() []app.Permission
Permissions 实现 app.PermissionProvider。
func (*Module) Settings ¶
func (m *Module) Settings() []app.SettingGroup
Settings 实现 app.SettingsProvider。
type Notifier ¶
type Notifier interface {
CommentCreated(ctx context.Context, event *NotifyEvent)
}
Notifier 在评论创建后决定要不要发通知。
实现必须立即返回:它跑在请求路径上,而发信要走后台队列。
type NotifyEvent ¶
type NotifyEvent struct {
Comment *Comment
Post *PostRef
// Settings 是判定时的评论设置,避免通知侧再读一次。
Settings Settings
// AutoApproved 为真表示该评论已直接展示,通知里不必再提「待审核」。
AutoApproved bool
// Parent 是被回复的评论,仅在这是一条回复时有值。
Parent *Comment
}
NotifyEvent 描述一次需要通知的评论事件。
type PostRef ¶
type PostRef struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Slug string `json:"slug"`
AuthorID int64 `json:"authorId"`
// Status 与 Visibility 供前台接口复刻正文的可见性规则。
Status string `json:"-"`
Visibility string `json:"-"`
}
PostRef 是评论所属内容的摘要,供后台列表直接显示与跳转。
func (*PostRef) PubliclyVisible ¶
PubliclyVisible 报告内容是否对访客可见,判据与主题前台的 publicFilter 完全一致: 已发布且公开。
这里刻意不接受「作者可见自己的私密内容」:Public 平面是访客平面, 草稿、私密与回收站中的内容一律不对外提供服务,其存在性也不该被探知。 作者要看自己未公开内容的评论,走 Console。
type PublicView ¶
type PublicView struct {
ID int64 `json:"id"`
ParentID *int64 `json:"parentId"`
AuthorName string `json:"authorName"`
AuthorURL string `json:"authorUrl"`
ContentHTML string `json:"contentHtml" doc:"已转义的展示 HTML,主题直出即可"`
IsAuthor bool `json:"isAuthor" doc:"是否为该内容的作者本人所发"`
CreatedAt time.Time `json:"createdAt"`
Children []*PublicView `json:"children" doc:"回复,按时间正序"`
}
PublicView 是前台可见的评论,**不含**邮箱、IP 与 UA。
单独建一个类型而不是给字段加 omitempty:前台响应少一个字段是功能问题, 多一个字段是数据泄漏,两者的严重程度不对等,故用类型把边界钉死。
func BuildTree ¶
func BuildTree(items []Comment, postAuthorID int64) []*PublicView
BuildTree 把平铺的评论按 parent_id 组装成回复树。
只保留能追溯到顶层的节点:父评论被删或被标垃圾时,其回复不该跑到顶层去展示。
type Settings ¶
type Settings struct {
Enabled bool `json:"enabled"`
AllowAnonymous bool `json:"allowAnonymous"`
RequireEmail bool `json:"requireEmail"`
RequireApproval bool `json:"requireApproval"`
MaxLength int `json:"maxLength"`
IntervalSeconds int `json:"intervalSeconds"`
MaxLinks int `json:"maxLinks"`
Blocklist string `json:"blocklist"`
NotifyNew bool `json:"notifyNew"`
NotifyReply bool `json:"notifyReply"`
NotifyTo string `json:"notifyTo"`
}
Settings 是 comment 分组的有效值。
type SpamChecker ¶
SpamChecker 判定一条评论是否为垃圾。
做成接口是为了给插件留位置:v1 只做频率限制、链接数与关键词, Akismet 一类的服务留给扩展实现,核心不引入任何外部依赖。
type SpamInput ¶
type SpamInput struct {
AuthorName string
AuthorURL string
Content string
// Honeypot 是表单里的隐藏字段:正常访客看不见也不会填,填了的必是机器人。
Honeypot string
// LastFromIP 是同一 IP 的上一条评论时间,零值表示没有。
LastFromIP time.Time
Now time.Time
}
SpamInput 是判定所需的信息。
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store 提供评论的持久化操作。
直接用 bun 读 posts 表而不经 content 模块:两者共用同一张表, 而模块之间只允许经 App 交互,不值得为一次标题查询引入跨模块依赖。
func (*Store) AuthorEmail ¶ added in v0.2.1
AuthorEmail 取内容作者的邮箱,新评论通知没填收件地址时发给他;账号停用或不存在时返回空串。
func (*Store) CountApproved ¶
CountApproved 统计某篇内容已通过的评论数。
func (*Store) CountPending ¶
CountPending 统计待审评论数,供 Console 侧显示待办角标。
func (*Store) LastByIP ¶
LastByIP 返回某 IP 最近一条评论的时间,供频率限制使用。
只算「已存在」的评论:把待审的也算上,同一 IP 反复提交就会被自己挡住, 而正常访客并不知道自己的评论在待审。
func (*Store) ListApproved ¶
ListApproved 返回某篇内容下已通过的**全部**评论,按时间正序。
不分页:回复树一旦被切开就没法正确组装,而单篇内容的评论量本就有上限。
func (*Store) Page ¶
func (s *Store) Page(ctx context.Context, filter Filter, params api.PageParams) ([]Comment, int, error)
Page 分页返回评论,可按状态、内容与作者筛选。
func (*Store) SerializeByIP ¶ added in v0.1.9
func (s *Store) SerializeByIP(ctx context.Context, ip string, fn func(ctx context.Context, tx *Store) error) error
SerializeByIP 在一个事务里执行 fn,同一来源 IP 的调用排队进行。
频率判定是「先查这个 IP 的上一条,再写这一条」:同一 IP 几乎同时提交的两条评论 会各自查到「没有上一条」而双双放行。按 IP 取事务级 advisory lock, 后一条要等前一条提交后才去查,多实例下同样成立。