Version: 0.9.78.dev.260506
This commit is contained in:
146
backend/services/taskclassforum/dao/cache.go
Normal file
146
backend/services/taskclassforum/dao/cache.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
forumcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclassforum"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
const commentTreeCacheTTL = 2 * time.Minute
|
||||
|
||||
type commentTreeCachePayload struct {
|
||||
Items []forumcontracts.ForumCommentNode `json:"items"`
|
||||
Page forumcontracts.PageResult `json:"page"`
|
||||
}
|
||||
|
||||
// CommentTreeCache 承载计划广场评论树的 Redis 缓存能力。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责评论树读模型的 JSON 缓存和版本号失效,不读写 MySQL;
|
||||
// 2. 不计算当前用户是否可删除评论,避免把用户视角写进共享缓存;
|
||||
// 3. Redis 异常向上返回,由 service 层决定是否降级回源 DB。
|
||||
type CommentTreeCache struct {
|
||||
client *redis.Client
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func NewCommentTreeCache(client *redis.Client) *CommentTreeCache {
|
||||
return &CommentTreeCache{
|
||||
client: client,
|
||||
ttl: commentTreeCacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
func commentTreeVersionKey(postID uint64) string {
|
||||
return fmt.Sprintf("forum:comments:%d:version", postID)
|
||||
}
|
||||
|
||||
func commentTreeDataKey(postID uint64, version int64, sort string, page int, pageSize int) string {
|
||||
return fmt.Sprintf(
|
||||
"forum:comments:%d:v%d:sort:%s:page:%d:size:%d",
|
||||
postID,
|
||||
version,
|
||||
strings.TrimSpace(sort),
|
||||
page,
|
||||
pageSize,
|
||||
)
|
||||
}
|
||||
|
||||
// GetCommentTree 读取指定帖子、排序和分页维度下的评论树缓存。
|
||||
//
|
||||
// 返回值语义:
|
||||
// 1. hit=true 表示命中缓存,items/page 可直接用于返回前的用户视角补全;
|
||||
// 2. hit=false 且 error=nil 表示未命中,调用方应回源 DB;
|
||||
// 3. error 非空表示 Redis 或 JSON 异常,调用方应记录日志并回源 DB。
|
||||
func (c *CommentTreeCache) GetCommentTree(ctx context.Context, postID uint64, page int, pageSize int, sort string) ([]forumcontracts.ForumCommentNode, forumcontracts.PageResult, bool, error) {
|
||||
if c == nil || c.client == nil {
|
||||
return nil, forumcontracts.PageResult{}, false, errors.New("评论树缓存未初始化")
|
||||
}
|
||||
version, err := c.currentCommentTreeVersion(ctx, postID)
|
||||
if err != nil {
|
||||
return nil, forumcontracts.PageResult{}, false, err
|
||||
}
|
||||
|
||||
raw, err := c.client.Get(ctx, commentTreeDataKey(postID, version, sort, page, pageSize)).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, forumcontracts.PageResult{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, forumcontracts.PageResult{}, false, err
|
||||
}
|
||||
|
||||
var payload commentTreeCachePayload
|
||||
if err = json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return nil, forumcontracts.PageResult{}, false, err
|
||||
}
|
||||
if payload.Items == nil {
|
||||
payload.Items = []forumcontracts.ForumCommentNode{}
|
||||
}
|
||||
return payload.Items, payload.Page, true, nil
|
||||
}
|
||||
|
||||
// SetCommentTree 写入指定帖子、排序和分页维度下的评论树缓存。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先读取当前版本号,保证写入 key 与后续读取 key 一致;
|
||||
// 2. 再序列化去个性化后的评论树,避免缓存里带入某个用户的 can_delete;
|
||||
// 3. 最后写入短 TTL,让版本失效失败时也能靠自然过期兜底。
|
||||
func (c *CommentTreeCache) SetCommentTree(ctx context.Context, postID uint64, page int, pageSize int, sort string, items []forumcontracts.ForumCommentNode, pageResult forumcontracts.PageResult) error {
|
||||
if c == nil || c.client == nil {
|
||||
return errors.New("评论树缓存未初始化")
|
||||
}
|
||||
version, err := c.currentCommentTreeVersion(ctx, postID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if items == nil {
|
||||
items = []forumcontracts.ForumCommentNode{}
|
||||
}
|
||||
data, err := json.Marshal(commentTreeCachePayload{
|
||||
Items: items,
|
||||
Page: pageResult,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.client.Set(ctx, commentTreeDataKey(postID, version, sort, page, pageSize), data, c.ttl).Err()
|
||||
}
|
||||
|
||||
// BumpCommentTreeVersion 递增帖子评论树版本号,让旧分页缓存自然失效。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只做版本递增,不扫描删除旧 data key,避免写评论时阻塞 Redis;
|
||||
// 2. 旧 data key 依赖短 TTL 自动回收;
|
||||
// 3. 当 version key 不存在时 INCR 会从 1 开始,能够让默认 v0 缓存失效。
|
||||
func (c *CommentTreeCache) BumpCommentTreeVersion(ctx context.Context, postID uint64) error {
|
||||
if c == nil || c.client == nil {
|
||||
return errors.New("评论树缓存未初始化")
|
||||
}
|
||||
return c.client.Incr(ctx, commentTreeVersionKey(postID)).Err()
|
||||
}
|
||||
|
||||
func (c *CommentTreeCache) currentCommentTreeVersion(ctx context.Context, postID uint64) (int64, error) {
|
||||
raw, err := c.client.Get(ctx, commentTreeVersionKey(postID)).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
version, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if version < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
70
backend/services/taskclassforum/dao/connect.go
Normal file
70
backend/services/taskclassforum/dao/connect.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox"
|
||||
forummodel "github.com/LoveLosita/smartflow/backend/services/taskclassforum/model"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// OpenDBFromConfig 创建计划广场服务自己的数据库句柄,并迁移本服务私有表。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只迁移 forum_* 表和本服务 outbox 表,不迁移 task_classes / task_items,避免抢占 task-class 拆分线;
|
||||
// 2. 不负责装配 legacy TaskClass adapter,adapter 在服务实现阶段单独注入;
|
||||
// 3. 返回 *gorm.DB 供本服务 DAO 复用,调用方负责进程生命周期。
|
||||
func OpenDBFromConfig() (*gorm.DB, error) {
|
||||
host := viper.GetString("database.host")
|
||||
port := viper.GetString("database.port")
|
||||
user := viper.GetString("database.user")
|
||||
password := viper.GetString("database.password")
|
||||
dbname := viper.GetString("database.dbname")
|
||||
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
user, password, host, port, dbname,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = AutoMigrate(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// AutoMigrate 只迁移计划广场服务拥有的表。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 先创建帖子、模板、条目、点赞、评论、导入记录表;
|
||||
// 2. 再按 service catalog 创建 taskclass-forum outbox 表,为后续论坛自身异步事件预留稳定目录;
|
||||
// 3. 迁移期论坛奖励事件直接写 token-store outbox 表,发布端也兜底创建目标表,避免独立启动顺序导致奖励漏表;
|
||||
// 4. 唯一约束交给 GORM tag 生成,保证点赞和导入幂等有数据库兜底;
|
||||
// 5. 失败时直接返回错误,避免服务在 schema 不完整时继续启动。
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("taskclassforum auto migrate failed: db is nil")
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&forummodel.ForumPost{},
|
||||
&forummodel.ForumPostTemplate{},
|
||||
&forummodel.ForumPostTemplateItem{},
|
||||
&forummodel.ForumLike{},
|
||||
&forummodel.ForumComment{},
|
||||
&forummodel.ForumImport{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("auto migrate taskclassforum tables failed: %w", err)
|
||||
}
|
||||
if err := outboxinfra.AutoMigrateServiceTable(db, outboxinfra.ServiceTaskClassForum); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := outboxinfra.AutoMigrateServiceTable(db, outboxinfra.ServiceTokenStore); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
466
backend/services/taskclassforum/dao/forum.go
Normal file
466
backend/services/taskclassforum/dao/forum.go
Normal file
@@ -0,0 +1,466 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user