Files
smartmate/backend/services/taskclassforum/dao/forum.go
2026-05-05 10:44:33 +08:00

467 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dao
import (
"context"
"errors"
"strings"
"time"
forummodel "github.com/LoveLosita/smartflow/backend/services/taskclassforum/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ForumDAO 承载计划广场私有表的持久化访问。
//
// 职责边界:
// 1. 只访问 forum_* 表,不直接读写旧 task_classes / task_items
// 2. 只做查询、事务和基础状态更新,不组装前端 DTO
// 3. 业务规则由 sv 层控制DAO 仅提供必要的数据原子操作。
type ForumDAO struct {
db *gorm.DB
}
func NewForumDAO(db *gorm.DB) *ForumDAO {
return &ForumDAO{db: db}
}
func (dao *ForumDAO) WithTx(tx *gorm.DB) *ForumDAO {
return &ForumDAO{db: tx}
}
// GormDB 返回当前 DAO 绑定的 GORM 句柄。
//
// 职责边界:
// 1. 只提供给需要和 forum 业务事务同提交的基础设施使用,例如 outbox 入队;
// 2. 不鼓励业务层绕过 DAO 任意读写 forum_* 表;
// 3. 若当前 DAO 来自 WithTx返回值就是同一个事务句柄。
func (dao *ForumDAO) GormDB() *gorm.DB {
if dao == nil {
return nil
}
return dao.db
}
// Transaction 在一个数据库事务内执行计划广场写操作。
func (dao *ForumDAO) Transaction(ctx context.Context, fn func(txDAO *ForumDAO) error) error {
return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return fn(dao.WithTx(tx))
})
}
type ListPostsQuery struct {
Page int
PageSize int
Sort string
Keyword string
Tag string
}
// CreatePostSnapshot 在同一事务中写帖子、模板和模板条目。
func (dao *ForumDAO) CreatePostSnapshot(ctx context.Context, post *forummodel.ForumPost, template *forummodel.ForumPostTemplate, items []forummodel.ForumPostTemplateItem) error {
return dao.Transaction(ctx, func(txDAO *ForumDAO) error {
if err := txDAO.db.Create(post).Error; err != nil {
return err
}
template.PostID = post.ID
if err := txDAO.db.Create(template).Error; err != nil {
return err
}
for i := range items {
items[i].PostID = post.ID
items[i].TemplateID = template.ID
}
if len(items) > 0 {
if err := txDAO.db.Create(&items).Error; err != nil {
return err
}
}
return nil
})
}
func (dao *ForumDAO) FindPostByIdempotencyKey(ctx context.Context, userID uint64, key string) (*forummodel.ForumPost, error) {
var post forummodel.ForumPost
err := dao.db.WithContext(ctx).
Where("author_user_id = ? AND idempotency_key = ?", userID, key).
First(&post).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &post, nil
}
func (dao *ForumDAO) ListPosts(ctx context.Context, query ListPostsQuery) ([]forummodel.ForumPost, int64, error) {
db := dao.db.WithContext(ctx).
Model(&forummodel.ForumPost{}).
Where("status = ? AND deleted_at IS NULL", forummodel.ForumPostStatusPublished)
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%"
db = db.Where("title LIKE ? OR summary LIKE ?", like, like)
}
if tag := strings.TrimSpace(query.Tag); tag != "" {
db = db.Where("JSON_CONTAINS(tags_json, JSON_QUOTE(?))", tag)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "created_at DESC"
switch strings.TrimSpace(query.Sort) {
case "likes":
orderBy = "like_count DESC, created_at DESC"
case "imports":
orderBy = "import_count DESC, created_at DESC"
}
var posts []forummodel.ForumPost
err := db.Order(orderBy).
Offset((query.Page - 1) * query.PageSize).
Limit(query.PageSize).
Find(&posts).Error
if err != nil {
return nil, 0, err
}
return posts, total, nil
}
func (dao *ForumDAO) ListPublishedTagJSONs(ctx context.Context) ([]string, error) {
var rows []struct {
TagsJSON string
}
err := dao.db.WithContext(ctx).
Model(&forummodel.ForumPost{}).
Select("tags_json").
Where("status = ? AND deleted_at IS NULL", forummodel.ForumPostStatusPublished).
Find(&rows).Error
if err != nil {
return nil, err
}
result := make([]string, 0, len(rows))
for _, row := range rows {
result = append(result, row.TagsJSON)
}
return result, nil
}
func (dao *ForumDAO) FindPublishedPost(ctx context.Context, postID uint64) (*forummodel.ForumPost, error) {
var post forummodel.ForumPost
err := dao.db.WithContext(ctx).
Where("id = ? AND status = ? AND deleted_at IS NULL", postID, forummodel.ForumPostStatusPublished).
First(&post).Error
if err != nil {
return nil, err
}
return &post, nil
}
func (dao *ForumDAO) LockPublishedPost(ctx context.Context, postID uint64) (*forummodel.ForumPost, error) {
var post forummodel.ForumPost
err := dao.db.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ? AND status = ? AND deleted_at IS NULL", postID, forummodel.ForumPostStatusPublished).
First(&post).Error
if err != nil {
return nil, err
}
return &post, nil
}
func (dao *ForumDAO) FindTemplateByPostID(ctx context.Context, postID uint64) (*forummodel.ForumPostTemplate, error) {
var template forummodel.ForumPostTemplate
err := dao.db.WithContext(ctx).Where("post_id = ?", postID).First(&template).Error
if err != nil {
return nil, err
}
return &template, nil
}
func (dao *ForumDAO) ListTemplateItemsByPostID(ctx context.Context, postID uint64) ([]forummodel.ForumPostTemplateItem, error) {
var items []forummodel.ForumPostTemplateItem
err := dao.db.WithContext(ctx).
Where("post_id = ?", postID).
Order("item_order ASC").
Find(&items).Error
return items, err
}
func (dao *ForumDAO) FindTemplatesByPostIDs(ctx context.Context, postIDs []uint64) (map[uint64]forummodel.ForumPostTemplate, error) {
var templates []forummodel.ForumPostTemplate
err := dao.db.WithContext(ctx).Where("post_id IN ?", postIDs).Find(&templates).Error
if err != nil {
return nil, err
}
result := make(map[uint64]forummodel.ForumPostTemplate, len(templates))
for _, template := range templates {
result[template.PostID] = template
}
return result, nil
}
func (dao *ForumDAO) CountTemplateItemsByPostIDs(ctx context.Context, postIDs []uint64) (map[uint64]int, error) {
var rows []struct {
PostID uint64
Count int
}
err := dao.db.WithContext(ctx).
Model(&forummodel.ForumPostTemplateItem{}).
Select("post_id, COUNT(*) AS count").
Where("post_id IN ?", postIDs).
Group("post_id").
Find(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint64]int, len(rows))
for _, row := range rows {
result[row.PostID] = row.Count
}
return result, nil
}
func (dao *ForumDAO) LikedPostIDSet(ctx context.Context, userID uint64, postIDs []uint64) (map[uint64]bool, error) {
var likes []forummodel.ForumLike
err := dao.db.WithContext(ctx).
Select("post_id").
Where("user_id = ? AND post_id IN ? AND status = ?", userID, postIDs, forummodel.ForumLikeStatusActive).
Find(&likes).Error
if err != nil {
return nil, err
}
result := make(map[uint64]bool, len(likes))
for _, like := range likes {
result[like.PostID] = true
}
return result, nil
}
func (dao *ForumDAO) ImportedPostIDSet(ctx context.Context, userID uint64, postIDs []uint64) (map[uint64]bool, error) {
var imports []forummodel.ForumImport
err := dao.db.WithContext(ctx).
Select("post_id").
Where("user_id = ? AND post_id IN ? AND status = ?", userID, postIDs, forummodel.ForumImportStatusImported).
Find(&imports).Error
if err != nil {
return nil, err
}
result := make(map[uint64]bool, len(imports))
for _, item := range imports {
result[item.PostID] = true
}
return result, nil
}
func (dao *ForumDAO) FindLike(ctx context.Context, postID uint64, userID uint64) (*forummodel.ForumLike, error) {
var like forummodel.ForumLike
err := dao.db.WithContext(ctx).Where("post_id = ? AND user_id = ?", postID, userID).First(&like).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &like, nil
}
func (dao *ForumDAO) CreateLike(ctx context.Context, like *forummodel.ForumLike) error {
return dao.db.WithContext(ctx).Create(like).Error
}
func (dao *ForumDAO) ActivateLike(ctx context.Context, likeID uint64) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumLike{}).
Where("id = ?", likeID).
Updates(map[string]any{
"status": forummodel.ForumLikeStatusActive,
"canceled_at": nil,
"updated_at": time.Now(),
}).Error
}
func (dao *ForumDAO) CancelLike(ctx context.Context, likeID uint64, now time.Time) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumLike{}).
Where("id = ?", likeID).
Updates(map[string]any{
"status": forummodel.ForumLikeStatusCanceled,
"canceled_at": &now,
"updated_at": now,
}).Error
}
func (dao *ForumDAO) AddPostCounter(ctx context.Context, postID uint64, column string, delta int64) error {
expr := "CASE WHEN " + column + " + ? < 0 THEN 0 ELSE " + column + " + ? END"
return dao.db.WithContext(ctx).
Model(&forummodel.ForumPost{}).
Where("id = ?", postID).
UpdateColumn(column, gorm.Expr(expr, delta, delta)).Error
}
func (dao *ForumDAO) FindCommentByID(ctx context.Context, commentID uint64) (*forummodel.ForumComment, error) {
var comment forummodel.ForumComment
err := dao.db.WithContext(ctx).Where("id = ?", commentID).First(&comment).Error
if err != nil {
return nil, err
}
return &comment, nil
}
func (dao *ForumDAO) LockCommentByID(ctx context.Context, commentID uint64) (*forummodel.ForumComment, error) {
var comment forummodel.ForumComment
err := dao.db.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", commentID).
First(&comment).Error
if err != nil {
return nil, err
}
return &comment, nil
}
func (dao *ForumDAO) FindCommentByIdempotencyKey(ctx context.Context, userID uint64, key string) (*forummodel.ForumComment, error) {
var comment forummodel.ForumComment
err := dao.db.WithContext(ctx).
Where("user_id = ? AND idempotency_key = ?", userID, key).
First(&comment).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &comment, nil
}
func (dao *ForumDAO) CreateComment(ctx context.Context, comment *forummodel.ForumComment) error {
return dao.db.WithContext(ctx).Create(comment).Error
}
func (dao *ForumDAO) CountRootComments(ctx context.Context, postID uint64) (int64, error) {
var total int64
err := dao.db.WithContext(ctx).
Model(&forummodel.ForumComment{}).
Where("post_id = ? AND parent_comment_id IS NULL", postID).
Count(&total).Error
return total, err
}
func (dao *ForumDAO) ListRootComments(ctx context.Context, postID uint64, page int, pageSize int, sort string) ([]forummodel.ForumComment, error) {
orderBy := "created_at ASC"
if strings.TrimSpace(sort) == "latest" {
orderBy = "created_at DESC"
}
var comments []forummodel.ForumComment
err := dao.db.WithContext(ctx).
Where("post_id = ? AND parent_comment_id IS NULL", postID).
Order(orderBy).
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&comments).Error
return comments, err
}
func (dao *ForumDAO) ListCommentsByPostID(ctx context.Context, postID uint64) ([]forummodel.ForumComment, error) {
var comments []forummodel.ForumComment
err := dao.db.WithContext(ctx).
Where("post_id = ?", postID).
Order("created_at ASC").
Find(&comments).Error
return comments, err
}
func (dao *ForumDAO) SoftDeleteComment(ctx context.Context, commentID uint64, now time.Time) error {
tx := dao.db.WithContext(ctx).
Model(&forummodel.ForumComment{}).
Where("id = ? AND status = ?", commentID, forummodel.ForumCommentStatusVisible).
Updates(map[string]any{
"status": forummodel.ForumCommentStatusDeleted,
"deleted_at": &now,
"updated_at": now,
})
return tx.Error
}
func (dao *ForumDAO) FindImport(ctx context.Context, postID uint64, userID uint64) (*forummodel.ForumImport, error) {
var item forummodel.ForumImport
err := dao.db.WithContext(ctx).Where("post_id = ? AND user_id = ?", postID, userID).First(&item).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &item, nil
}
func (dao *ForumDAO) FindImportByIdempotencyKey(ctx context.Context, userID uint64, key string) (*forummodel.ForumImport, error) {
var item forummodel.ForumImport
err := dao.db.WithContext(ctx).
Where("user_id = ? AND idempotency_key = ?", userID, key).
First(&item).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &item, nil
}
func (dao *ForumDAO) CreateImport(ctx context.Context, item *forummodel.ForumImport) error {
return dao.db.WithContext(ctx).Create(item).Error
}
func (dao *ForumDAO) UpdateImportProcessing(ctx context.Context, importID uint64, title string, now time.Time) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumImport{}).
Where("id = ?", importID).
Updates(map[string]any{
"target_title": title,
"status": forummodel.ForumImportStatusPending,
"last_error": nil,
"updated_at": now,
}).Error
}
func (dao *ForumDAO) FinalizeImport(ctx context.Context, importID uint64, newTaskClassID uint64, targetTitle string, now time.Time) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumImport{}).
Where("id = ?", importID).
Updates(map[string]any{
"new_task_class_id": &newTaskClassID,
"target_title": targetTitle,
"status": forummodel.ForumImportStatusImported,
"last_error": nil,
"updated_at": now,
}).Error
}
func (dao *ForumDAO) MarkImportFailed(ctx context.Context, importID uint64, message string, now time.Time) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumImport{}).
Where("id = ?", importID).
Updates(map[string]any{
"status": forummodel.ForumImportStatusFailed,
"last_error": &message,
"updated_at": now,
}).Error
}
func (dao *ForumDAO) MarkImportFailedAfterTaskClassCreated(ctx context.Context, importID uint64, newTaskClassID uint64, targetTitle string, message string, now time.Time) error {
return dao.db.WithContext(ctx).
Model(&forummodel.ForumImport{}).
Where("id = ?", importID).
Updates(map[string]any{
"new_task_class_id": &newTaskClassID,
"target_title": targetTitle,
"status": forummodel.ForumImportStatusFailed,
"last_error": &message,
"updated_at": now,
}).Error
}