From c32c81d1caa28e211430bf76e18df39b4952e53f Mon Sep 17 00:00:00 2001
From: freefire
Date: Sat, 29 Aug 2026 21:51:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20SSR=20=E8=AF=84=E8=AE=BA=E5=9B=9E?=
=?UTF-8?q?=E5=A4=8D=E4=B8=8E=20@=20=E6=8F=90=E5=8F=8A=E9=80=9A=E7=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
发评/审通接入 NotifyService,系统通知会话支持 kind 筛选。
Co-authored-by: Cursor
---
docs/rebuild-spec/02-features.md | 6 +--
docs/rebuild-spec/06-pages-ux.md | 8 +--
public/assets/site.css | 12 +++++
routers/setup.go | 1 +
routers/web/admin.go | 7 +++
routers/web/chrome.go | 1 +
routers/web/messages.go | 91 ++++++++++++++++++++++++++++----
routers/web/post.go | 10 ++++
templates/messages/list.tmpl | 1 +
templates/messages/thread.tmpl | 13 ++++-
web_src/css/site.css | 12 +++++
11 files changed, 143 insertions(+), 19 deletions(-)
diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md
index ae70e5e..0fa4b84 100644
--- a/docs/rebuild-spec/02-features.md
+++ b/docs/rebuild-spec/02-features.md
@@ -105,8 +105,8 @@
- [x] 编辑评论(时限);删除评论 — SSR 帖详情入口 + `/comments/:cid/edit|delete`;作者软删进回收站
- [x] 评论点赞 — `POST /post/:id/comments/:cid/like`
- [x] 评论举报 — SSR
-- [ ] @ 提及 → 通知
-- [ ] 回复提醒(站内信 + 可选邮件)
+- [x] @ 提及 → 通知 — SSR 发评/审通调用 `NotifyCommentMentions`
+- [x] 回复提醒(站内信 + 可选邮件)— SSR 发评/审通调用 `NotifyCommentPublished`
- [ ] 审核中 / 被拒评论可见性规则
- [x] 管理员:通过 / 拒绝待审评论 — SSR `/admin/moderation`(回收站/修订未迁)
@@ -119,7 +119,7 @@
- [x] 发送私信 — 表单 PRG;用户主页「发私信」入口
- [x] 未读数(可分私信 / 通知)— 列表分项 + 导航角标
- [x] 标记会话已读 / 通知已读 / 全部已读 — 打开会话自动已读;全部标已读
-- [ ] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;筛选 UI 未迁)
+- [x] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;`/messages/with/0?kind=` 筛选)
---
diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md
index 88ceecf..2e3e4ab 100644
--- a/docs/rebuild-spec/06-pages-ux.md
+++ b/docs/rebuild-spec/06-pages-ux.md
@@ -147,7 +147,7 @@
| 投票卡 | 选选项提交;显示百分比;作者可结束 |
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 |
| 抽奖卡 | 显示参与人数;开奖;中奖名单 |
-| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除 — SSR;@ 未迁 |
+| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除、回复/@ 通知 — SSR;嵌套树 UI 未做 |
| 修订 | 面板列出历史,可选对比 |
| 图片 | Lightbox 查看 |
@@ -155,10 +155,10 @@
## 5. 消息页
-- 左:会话列表(系统会话单独)
-- 右:消息时间线;发送框
+- 左:会话列表(系统会话单独)— SSR `/messages`;快捷链到回复/提及筛选
+- 右:消息时间线;发送框 — SSR `/messages/with/:peerId`
- 顶:未读角标(全局导航也可显示)
-- 通知筛选(kind)
+- 通知筛选(kind)— 系统会话 `?kind=reply|mention|…`
---
diff --git a/public/assets/site.css b/public/assets/site.css
index 4de600d..79e247f 100644
--- a/public/assets/site.css
+++ b/public/assets/site.css
@@ -698,6 +698,18 @@ body.j13-body {
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
.j13-msg-compose { margin-top: 1.25rem; }
+.j13-msg-kinds {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem 0.85rem;
+ margin: 0.5rem 0 0;
+ font-size: 0.9rem;
+}
+.j13-msg-kinds a { color: var(--j13-muted); }
+.j13-msg-kinds a.is-active {
+ color: var(--j13-accent);
+ font-weight: 600;
+}
/* 内容门控锁定壳 */
members-only,
diff --git a/routers/setup.go b/routers/setup.go
index b2897a1..a0dda64 100644
--- a/routers/setup.go
+++ b/routers/setup.go
@@ -108,6 +108,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
SitePage: sitePageSvc,
Report: reportSvc,
Captcha: captchaSvc,
+ Notify: notifySvc,
}, authMW)
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
diff --git a/routers/web/admin.go b/routers/web/admin.go
index 1749b08..0ef353d 100644
--- a/routers/web/admin.go
+++ b/routers/web/admin.go
@@ -357,6 +357,13 @@ func (d Deps) AdminCommentApprove(c *gin.Context) {
if err := d.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil {
ctx.SetFlash(err.Error())
} else {
+ if d.Notify != nil {
+ if comment, err := d.Comment.GetByID(uint(id)); err == nil {
+ comment.Status = models.ContentStatusPublished
+ d.Notify.AsyncNotifyCommentPublished(comment)
+ d.Notify.AsyncNotifyCommentMentions(comment)
+ }
+ }
ctx.SetFlash("评论已通过")
}
ctx.Redirect("/admin/moderation")
diff --git a/routers/web/chrome.go b/routers/web/chrome.go
index a0b1cdb..373597e 100644
--- a/routers/web/chrome.go
+++ b/routers/web/chrome.go
@@ -29,6 +29,7 @@ type Deps struct {
SitePage *services.SitePageService
Report *services.ReportService
Captcha *services.CaptchaService
+ Notify *services.NotifyService
}
// SitePageLink 导航/页脚站点单页链接
diff --git a/routers/web/messages.go b/routers/web/messages.go
index fefa57d..46c827e 100644
--- a/routers/web/messages.go
+++ b/routers/web/messages.go
@@ -6,6 +6,7 @@ import (
"strconv"
"strings"
+ "git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
@@ -52,6 +53,15 @@ type messagesThreadData struct {
OlderBefore uint
HasOlder bool
Draft string
+ KindFilter string
+ KindLinks []msgKindLink
+}
+
+type msgKindLink struct {
+ Kind string
+ Label string
+ URL string
+ Active bool
}
// MessagesList GET /messages
@@ -121,9 +131,22 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa
uid := ctx.UserID()
before, _ := strconv.ParseUint(beforeRaw, 10, 64)
size := 40
- list, total, err := d.Message.ListConversationMessages(services.ConversationMessagesQuery{
- UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before),
- })
+ kindFilter := ""
+ if peerID == 0 {
+ kindFilter = normalizeNotifyKind(c.Query("kind"))
+ }
+
+ var list []models.PrivateMessage
+ var total int64
+ var err error
+ if peerID == 0 && kindFilter != "" && kindFilter != "all" {
+ list, total, err = d.Message.ListNotifications(uid, 1, size, kindFilter)
+ _ = before // kind 筛选暂不分页 before
+ } else {
+ list, total, err = d.Message.ListConversationMessages(services.ConversationMessagesQuery{
+ UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before),
+ })
+ }
if err != nil {
ctx.SetFlash(err.Error())
ctx.Redirect("/messages")
@@ -156,21 +179,69 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa
})
}
hasOlder := false
- if oldest > 0 {
- if before == 0 {
- hasOlder = total > int64(len(list))
- } else {
- hasOlder = len(list) >= size
+ if peerID != 0 || kindFilter == "" || kindFilter == "all" {
+ if oldest > 0 {
+ if before == 0 {
+ hasOlder = total > int64(len(list))
+ } else {
+ hasOlder = len(list) >= size
+ }
}
+ } else {
+ hasOlder = false
}
chrome := d.chrome(ctx, peerName+" · 私信 · "+d.Settings.SiteBranding().Name, "", "")
chrome.Error = errMsg
- ctx.HTML(http.StatusOK, "messages/thread", messagesThreadData{
+ data := messagesThreadData{
PageChrome: chrome, PeerID: peerID, PeerName: peerName,
IsSystem: peerID == 0, CanReply: canReply, Messages: bubbles,
OlderBefore: oldest, HasOlder: hasOlder, Draft: draft,
- })
+ }
+ if peerID == 0 {
+ data.KindFilter = kindFilter
+ data.KindLinks = notifyKindLinks(kindFilter)
+ }
+ ctx.HTML(http.StatusOK, "messages/thread", data)
+}
+
+func normalizeNotifyKind(raw string) string {
+ k := strings.TrimSpace(strings.ToLower(raw))
+ switch k {
+ case "", "all":
+ return "all"
+ case models.MessageKindReply, models.MessageKindMention, models.MessageKindSystem,
+ models.MessageKindReject, models.MessageKindReportResult, models.MessageKindModeration:
+ return k
+ default:
+ return "all"
+ }
+}
+
+func notifyKindLinks(active string) []msgKindLink {
+ if active == "" {
+ active = "all"
+ }
+ items := []struct{ kind, label string }{
+ {"all", "全部"},
+ {models.MessageKindReply, "回复"},
+ {models.MessageKindMention, "提及"},
+ {models.MessageKindSystem, "系统"},
+ {models.MessageKindReject, "拒绝"},
+ {models.MessageKindReportResult, "举报结果"},
+ {models.MessageKindModeration, "审核"},
+ }
+ out := make([]msgKindLink, 0, len(items))
+ for _, it := range items {
+ url := "/messages/with/0"
+ if it.kind != "all" {
+ url += "?kind=" + it.kind
+ }
+ out = append(out, msgKindLink{
+ Kind: it.kind, Label: it.label, URL: url, Active: active == it.kind,
+ })
+ }
+ return out
}
// MessagesSend POST /messages/with/:peerId
diff --git a/routers/web/post.go b/routers/web/post.go
index afb7690..db02e29 100644
--- a/routers/web/post.go
+++ b/routers/web/post.go
@@ -201,6 +201,16 @@ func (d Deps) PostComment(c *gin.Context) {
ctx.Redirect(fmt.Sprintf("/post/%d#comments", id))
return
}
+ if d.Notify != nil && cm != nil {
+ switch cm.Status {
+ case models.ContentStatusPublished:
+ d.Notify.AsyncNotifyCommentPublished(cm)
+ d.Notify.AsyncNotifyCommentMentions(cm)
+ case models.ContentStatusPending:
+ ctx.SetFlash("评论已提交,审核通过后公开显示")
+ d.Notify.AsyncNotifyPendingComment(cm)
+ }
+ }
anchor := "#comments"
if cm != nil && cm.Floor > 0 {
anchor = fmt.Sprintf("#floor-%d", cm.Floor)
diff --git a/templates/messages/list.tmpl b/templates/messages/list.tmpl
index cd660aa..db499ae 100644
--- a/templates/messages/list.tmpl
+++ b/templates/messages/list.tmpl
@@ -14,6 +14,7 @@
{{end}}
+ 系统通知 · 回复 · 提及
{{if .Conversations}}
diff --git a/templates/messages/thread.tmpl b/templates/messages/thread.tmpl
index d2b3460..2c095e0 100644
--- a/templates/messages/thread.tmpl
+++ b/templates/messages/thread.tmpl
@@ -4,10 +4,19 @@
← 返回会话列表
{{.PeerName}}
- {{if .IsSystem}}系统通知(只读)
{{else}}查看主页
{{end}}
+ {{if .IsSystem}}
+ 系统通知(只读)
+ {{if .KindLinks}}
+
+ {{end}}
+ {{else}}查看主页
{{end}}
{{template "base/alert" .}}
- {{if .HasOlder}}
+ {{if and .HasOlder (or (not .IsSystem) (eq .KindFilter "all") (eq .KindFilter ""))}}
加载更早消息
{{end}}
diff --git a/web_src/css/site.css b/web_src/css/site.css
index 4de600d..79e247f 100644
--- a/web_src/css/site.css
+++ b/web_src/css/site.css
@@ -698,6 +698,18 @@ body.j13-body {
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
.j13-msg-compose { margin-top: 1.25rem; }
+.j13-msg-kinds {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem 0.85rem;
+ margin: 0.5rem 0 0;
+ font-size: 0.9rem;
+}
+.j13-msg-kinds a { color: var(--j13-muted); }
+.j13-msg-kinds a.is-active {
+ color: var(--j13-accent);
+ font-weight: 600;
+}
/* 内容门控锁定壳 */
members-only,