feat: 对接稿件审核通知系统 + 消息页面系统消息区域
- 新增通知类型 post_approved / post_rejected
- PostService.Approve/Reject 完成后通知作者(通知失败不影响主流程)
- NotificationService 新增 NotifyPostApproved/NotifyPostRejected 方法
- 消息页面顶部增加系统消息标识和分隔线,之下为全部消息列表
- 稿件审核通过通知卡片点击跳转到 /posts/{id} 页面
- 稿件审核拒绝通知卡片点击跳转到 /studio/posts 创作中心
- 新增 postNotifier 接口遵循 ISP 原则
This commit is contained in:
@ -2,8 +2,10 @@ package model
|
||||
|
||||
// 消息/通知类型常量
|
||||
const (
|
||||
NotifyAuditApproved = "audit_approved"
|
||||
NotifyAuditRejected = "audit_rejected"
|
||||
NotifyAuditApproved = "audit_approved"
|
||||
NotifyAuditRejected = "audit_rejected"
|
||||
NotifyPostApproved = "post_approved"
|
||||
NotifyPostRejected = "post_rejected"
|
||||
)
|
||||
|
||||
// Notification 消息/通知模型
|
||||
@ -20,8 +22,10 @@ type Notification struct {
|
||||
|
||||
// NotifyTypeNames 通知类型 → 中文名
|
||||
var NotifyTypeNames = map[string]string{
|
||||
NotifyAuditApproved: "系统通知",
|
||||
NotifyAuditRejected: "系统通知",
|
||||
NotifyAuditApproved: "资料审核",
|
||||
NotifyAuditRejected: "资料审核",
|
||||
NotifyPostApproved: "稿件审核",
|
||||
NotifyPostRejected: "稿件审核",
|
||||
}
|
||||
|
||||
// NotificationListResult 消息列表查询结果
|
||||
|
||||
@ -32,7 +32,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
)
|
||||
|
||||
postRepo := repository.NewPostRepo(db)
|
||||
postService := service.NewPostService(postRepo, siteSettings)
|
||||
postService := service.NewPostService(postRepo, siteSettings, notificationService)
|
||||
shortcodeSvc := service.NewShortcodeService()
|
||||
postCtrl := controller.NewPostController(postService, shortcodeSvc)
|
||||
adminPostCtrl := adminCtrl.NewAdminPostController(postService)
|
||||
|
||||
@ -110,3 +110,20 @@ func (s *NotificationService) NotifyAuditRejected(userID uint, auditType, reason
|
||||
content,
|
||||
&relatedID)
|
||||
}
|
||||
|
||||
// NotifyPostApproved 稿件审核通过通知
|
||||
func (s *NotificationService) NotifyPostApproved(userID uint, postTitle string, postID uint) {
|
||||
title := "稿件审核通过"
|
||||
content := "你的稿件《" + postTitle + "》已通过审核,现已公开发布"
|
||||
_ = s.Create(userID, model.NotifyPostApproved, title, content, &postID)
|
||||
}
|
||||
|
||||
// NotifyPostRejected 稿件审核拒绝通知
|
||||
func (s *NotificationService) NotifyPostRejected(userID uint, postTitle, reason string, postID uint) {
|
||||
title := "稿件审核未通过"
|
||||
content := "你的稿件《" + postTitle + "》未通过审核"
|
||||
if reason != "" {
|
||||
content += ",理由:" + reason
|
||||
}
|
||||
_ = s.Create(userID, model.NotifyPostRejected, title, content, &postID)
|
||||
}
|
||||
|
||||
@ -75,15 +75,17 @@ func generateExcerpt(md string) string {
|
||||
|
||||
// PostService 帖子业务逻辑
|
||||
type PostService struct {
|
||||
repo postStore
|
||||
ss *config.SiteSettings
|
||||
repo postStore
|
||||
ss *config.SiteSettings
|
||||
notifier postNotifier
|
||||
}
|
||||
|
||||
// NewPostService 构造函数
|
||||
func NewPostService(repo postStore, ss *config.SiteSettings) *PostService {
|
||||
func NewPostService(repo postStore, ss *config.SiteSettings, notifier postNotifier) *PostService {
|
||||
return &PostService{
|
||||
repo: repo,
|
||||
ss: ss,
|
||||
repo: repo,
|
||||
ss: ss,
|
||||
notifier: notifier,
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,8 +265,10 @@ func (s *PostService) Approve(postID uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
wasRevision := post.PendingBody != ""
|
||||
|
||||
// 修订审核:将待审内容覆盖到正式字段
|
||||
if post.PendingBody != "" {
|
||||
if wasRevision {
|
||||
post.Title = post.PendingTitle
|
||||
post.Body = post.PendingBody
|
||||
post.Excerpt = generateExcerpt(post.PendingBody)
|
||||
@ -272,7 +276,14 @@ func (s *PostService) Approve(postID uint) error {
|
||||
post.PendingBody = ""
|
||||
post.Status = model.PostStatusApproved
|
||||
post.RejectReason = ""
|
||||
return s.repo.Update(post)
|
||||
if err := s.repo.Update(post); err != nil {
|
||||
return err
|
||||
}
|
||||
// 通知作者修订审核通过
|
||||
if s.notifier != nil {
|
||||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 首次审核通过
|
||||
@ -281,7 +292,14 @@ func (s *PostService) Approve(postID uint) error {
|
||||
}
|
||||
post.Status = model.PostStatusApproved
|
||||
post.RejectReason = ""
|
||||
return s.repo.Update(post)
|
||||
if err := s.repo.Update(post); err != nil {
|
||||
return err
|
||||
}
|
||||
// 通知作者审核通过
|
||||
if s.notifier != nil {
|
||||
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject 退回:pending/approved → rejected;修订退回则清空 Pending 保持 approved
|
||||
@ -299,13 +317,27 @@ func (s *PostService) Reject(postID uint, reason string) error {
|
||||
post.PendingTitle = ""
|
||||
post.PendingBody = ""
|
||||
post.RejectReason = reason
|
||||
return s.repo.Update(post)
|
||||
if err := s.repo.Update(post); err != nil {
|
||||
return err
|
||||
}
|
||||
// 通知作者修订被退回
|
||||
if s.notifier != nil {
|
||||
s.notifier.NotifyPostRejected(post.UserID, post.Title, reason, post.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 普通退回(首次审核)
|
||||
post.Status = model.PostStatusRejected
|
||||
post.RejectReason = reason
|
||||
return s.repo.Update(post)
|
||||
if err := s.repo.Update(post); err != nil {
|
||||
return err
|
||||
}
|
||||
// 通知作者审核被退回
|
||||
if s.notifier != nil {
|
||||
s.notifier.NotifyPostRejected(post.UserID, post.Title, reason, post.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock 锁定:设置 IsLocked 标志,禁止编辑,不改变帖子发布状态
|
||||
|
||||
@ -53,4 +53,10 @@ type spaceUserStore interface {
|
||||
// spacePostStore SpaceService 所需的最小帖子仓储接口(ISP:1 个方法)
|
||||
type spacePostStore interface {
|
||||
FindByUserID(userID uint, offset, limit int) ([]model.Post, int64, error)
|
||||
}
|
||||
|
||||
// postNotifier PostService 所需的通知服务接口(ISP:2 个方法)
|
||||
type postNotifier interface {
|
||||
NotifyPostApproved(userID uint, postTitle string, postID uint)
|
||||
NotifyPostRejected(userID uint, postTitle, reason string, postID uint)
|
||||
}
|
||||
@ -21,6 +21,15 @@
|
||||
</aside>
|
||||
|
||||
<div class="msg-main">
|
||||
<!-- 系统消息区域 -->
|
||||
<div class="msg-system-section">
|
||||
<div class="msg-system-header">
|
||||
<svg class="msg-system-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
系统消息
|
||||
</div>
|
||||
<div class="msg-system-divider"></div>
|
||||
</div>
|
||||
|
||||
{{if .Messages}}
|
||||
<div class="msg-toolbar">
|
||||
{{if .UnreadCount}}
|
||||
@ -29,7 +38,18 @@
|
||||
</div>
|
||||
<div class="msg-list">
|
||||
{{range $i, $m := .Messages}}
|
||||
<div class="msg-card{{if not $m.IsRead}} unread{{end}}" data-id="{{$m.ID}}">
|
||||
{{$link := ""}}
|
||||
{{if or (eq $m.NotifyType "post_approved") (eq $m.NotifyType "post_rejected")}}
|
||||
{{if eq $m.NotifyType "post_approved"}}
|
||||
{{$link = printf "/posts/%d" $m.RelatedID}}
|
||||
{{else}}
|
||||
{{$link = "/studio/posts"}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if $link}}
|
||||
<a href="{{$link}}" class="msg-card-link">
|
||||
{{end}}
|
||||
<div class="msg-card{{if not $m.IsRead}} unread{{end}}" data-id="{{$m.ID}}" data-type="{{$m.NotifyType}}" data-link="{{$link}}">
|
||||
<div class="msg-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
</div>
|
||||
@ -45,6 +65,9 @@
|
||||
<span class="msg-badge"></span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $link}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@ -82,7 +105,10 @@
|
||||
var cards = document.querySelectorAll('.msg-card.unread');
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
card.addEventListener('click', function (e) {
|
||||
var link = card.getAttribute('data-link');
|
||||
// 如果是可跳转的通知(post 审核),不阻止默认行为,只标记已读
|
||||
// 标记已读通过 fetch 发送,不阻止链接跳转
|
||||
var id = card.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
var xhr = new XMLHttpRequest();
|
||||
@ -96,6 +122,15 @@
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
|
||||
// 如果有链接,在标记已读后跳转(非 post 通知直接标记已读)
|
||||
if (link) {
|
||||
e.preventDefault();
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
window.location.href = link;
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
})(cards[i]);
|
||||
}
|
||||
|
||||
@ -100,6 +100,43 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ---------- System Message Section ---------- */
|
||||
.msg-system-section {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.msg-system-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
font-size: .95rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
padding: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.msg-system-icon {
|
||||
color: var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-system-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, var(--color-border), transparent);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Message Card Link ---------- */
|
||||
.msg-card-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.msg-card-link:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ---------- Toolbar ---------- */
|
||||
.msg-toolbar {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user