467 lines
14 KiB
Go
467 lines
14 KiB
Go
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
|
||
}
|