- 新增 userExpReader 最小接口(ISP),PostService 通过它读取用户经验值
- PostService 新增 CanCreatePost(userID uint) bool,封装 LV0 投稿权限判断
- StudioController 替换内联 c.Get("exp") 检查为调用 ctrl.postService.CanCreatePost(uid)
- 更新 DI 注入链:NewPostService 增加 userRepo 参数
176 lines
5.4 KiB
Go
176 lines
5.4 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/config"
|
||
"metazone.cc/metalab/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// PostService 帖子业务逻辑
|
||
type PostService struct {
|
||
repo postStore
|
||
ss *config.SiteSettings
|
||
notifier postNotifier
|
||
userRepo userExpReader
|
||
}
|
||
|
||
// NewPostService 构造函数
|
||
func NewPostService(repo postStore, ss *config.SiteSettings, notifier postNotifier, userRepo userExpReader) *PostService {
|
||
return &PostService{
|
||
repo: repo,
|
||
ss: ss,
|
||
notifier: notifier,
|
||
userRepo: userRepo,
|
||
}
|
||
}
|
||
|
||
// auditEnabled 审核是否开启
|
||
func (s *PostService) auditEnabled() bool {
|
||
return s.ss.IsAuditEnabled()
|
||
}
|
||
|
||
// findPost 按 ID 查找帖子,区分"未找到"和"数据库错误"
|
||
func (s *PostService) findPost(id uint) (*model.Post, error) {
|
||
post, err := s.repo.FindByID(id)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, common.ErrPostNotFound
|
||
}
|
||
return nil, fmt.Errorf("查询帖子失败: %w", err)
|
||
}
|
||
return post, nil
|
||
}
|
||
|
||
// findPostWithAuthor 按 ID 查找帖子(含作者名),区分"未找到"和"数据库错误"
|
||
func (s *PostService) findPostWithAuthor(id uint) (*model.Post, error) {
|
||
post, err := s.repo.FindByIDWithAuthor(id)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, common.ErrPostNotFound
|
||
}
|
||
return nil, fmt.Errorf("查询帖子失败: %w", err)
|
||
}
|
||
return post, nil
|
||
}
|
||
|
||
// Create 创建帖子
|
||
// body 为 Vditor 输出的 Markdown,后端直接存储不做 HTML 消毒
|
||
// MD 是纯文本,无 XSS 注入风险;前端渲染时由 Vditor 转 HTML
|
||
func (s *PostService) Create(userID uint, title, body string) (*model.Post, error) {
|
||
status := model.PostStatusApproved
|
||
if s.auditEnabled() {
|
||
status = model.PostStatusDraft
|
||
}
|
||
|
||
post := &model.Post{
|
||
Title: title,
|
||
Body: body,
|
||
Excerpt: generateExcerpt(body),
|
||
UserID: userID,
|
||
Status: status,
|
||
AllowComment: true,
|
||
}
|
||
if err := s.repo.Create(post); err != nil {
|
||
return nil, err
|
||
}
|
||
return post, nil
|
||
}
|
||
|
||
// GetByID 按 ID 获取帖子(含作者名,仅 approved 公开可见)
|
||
func (s *PostService) GetByID(id uint) (*model.Post, error) {
|
||
return s.findPostWithAuthor(id)
|
||
}
|
||
|
||
// listPageable 分页查询公共逻辑:参数规范化 + nil 转空切片
|
||
func (s *PostService) listPageable(page, pageSize int, fn func(offset, limit int) ([]model.Post, int64, error)) ([]model.Post, int64, error) {
|
||
p := common.Pagination{Page: page, PageSize: pageSize}
|
||
p.DefaultPagination()
|
||
posts, total, err := fn(p.Offset(), p.PageSize)
|
||
if posts == nil {
|
||
posts = []model.Post{}
|
||
}
|
||
return posts, total, err
|
||
}
|
||
|
||
// List 公开帖子列表(仅 approved)
|
||
func (s *PostService) List(keyword string, page, pageSize int) ([]model.Post, int64, error) {
|
||
return s.listPageable(page, pageSize, func(offset, limit int) ([]model.Post, int64, error) {
|
||
return s.repo.FindPageable(keyword, offset, limit)
|
||
})
|
||
}
|
||
|
||
// ListAdmin 管理后台帖子列表(全状态)
|
||
func (s *PostService) ListAdmin(keyword, status string, page, pageSize int) ([]model.Post, int64, error) {
|
||
return s.listPageable(page, pageSize, func(offset, limit int) ([]model.Post, int64, error) {
|
||
return s.repo.FindAdminPageable(keyword, status, offset, limit)
|
||
})
|
||
}
|
||
|
||
// ListByUser 查询某用户指定状态的帖子(空 status=全状态)
|
||
func (s *PostService) ListByUser(userID uint, status string, page, pageSize int) ([]model.Post, int64, error) {
|
||
return s.listPageable(page, pageSize, func(offset, limit int) ([]model.Post, int64, error) {
|
||
return s.repo.FindByUserIDAndStatus(userID, status, offset, limit)
|
||
})
|
||
}
|
||
|
||
// StudioOverview 创作中心数据概览
|
||
type StudioOverview struct {
|
||
TotalPosts int64 `json:"total_posts"`
|
||
DraftCount int64 `json:"draft_count"`
|
||
PendingCount int64 `json:"pending_count"`
|
||
ApprovedCount int64 `json:"approved_count"`
|
||
RejectedCount int64 `json:"rejected_count"`
|
||
}
|
||
|
||
// GetOverview 获取创作中心数据概览(单次 GROUP BY 查询)
|
||
func (s *PostService) GetOverview(userID uint) (*StudioOverview, error) {
|
||
totalPosts, draftCount, pendingCount, approvedCount, rejectedCount, err := s.repo.GetOverviewByUserID(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &StudioOverview{
|
||
TotalPosts: totalPosts,
|
||
DraftCount: draftCount,
|
||
PendingCount: pendingCount,
|
||
ApprovedCount: approvedCount,
|
||
RejectedCount: rejectedCount,
|
||
}, nil
|
||
}
|
||
|
||
// IsPostAccessible 检查用户是否有权限访问/操作帖子(作者或 moderator+)
|
||
func (s *PostService) IsPostAccessible(userID uint, role string, postUserID uint) bool {
|
||
return userID == postUserID || model.HasMinRole(role, model.RoleModerator)
|
||
}
|
||
|
||
// CanCreatePost 检查用户是否可以创建帖子(LV0 用户不允许投稿)
|
||
func (s *PostService) CanCreatePost(userID uint) bool {
|
||
user, err := s.userRepo.FindByID(userID)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return user.Exp >= 1
|
||
}
|
||
|
||
// RecordRead 记录已登录用户阅读文章(已登录去重 + 防刷冷却)
|
||
func (s *PostService) RecordRead(userID, postID uint) {
|
||
firstRead, err := s.repo.RecordRead(userID, postID)
|
||
if err != nil || !firstRead {
|
||
return
|
||
}
|
||
_ = s.repo.IncrementViewsCount(postID)
|
||
}
|
||
|
||
// RecordGuestRead 记录访客阅读文章(Cookie 标识去重 + 防刷冷却)
|
||
func (s *PostService) RecordGuestRead(visitorID string, postID uint) {
|
||
firstRead, err := s.repo.RecordGuestRead(visitorID, postID)
|
||
if err != nil || !firstRead {
|
||
return
|
||
}
|
||
_ = s.repo.IncrementViewsCount(postID)
|
||
}
|