Add hashtag extraction and management in CreatePost and EditPost logic; improve error handling
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 53s

This commit is contained in:
2025-10-26 14:31:48 +08:00
parent 57d8286607
commit a8b2457762
4 changed files with 173 additions and 10 deletions

View File

@@ -43,6 +43,13 @@ func (l *CreatePostLogic) CreatePost(req *types.CreatePostReq) (resp *types.Crea
return nil, fmt.Errorf("invalid content format") return nil, fmt.Errorf("invalid content format")
} }
// Extract all hashtags from content
hashtags, err := utils.ExtractHashtagsFromContent(req.Content)
if err != nil {
l.Errorf("Failed to parse hashtags from content: %v", err)
return nil, fmt.Errorf("invalid content format")
}
// Migrate cover image from tmpfiles to files if provided // Migrate cover image from tmpfiles to files if provided
var coverID sql.NullInt64 var coverID sql.NullInt64
if req.CoverImageKey != "" { if req.CoverImageKey != "" {
@@ -72,6 +79,36 @@ func (l *CreatePostLogic) CreatePost(req *types.CreatePostReq) (resp *types.Crea
return nil, err return nil, err
} }
// Create or get hashtag IDs and link them to the post
for _, tagName := range hashtags {
var tagID int64
// Try to get existing hashtag
getTagQuery := `SELECT id FROM hashtags WHERE name = $1`
err := tx.QueryRowContext(l.ctx, getTagQuery, tagName).Scan(&tagID)
if err == sql.ErrNoRows {
// Hashtag doesn't exist, create it
createTagQuery := `INSERT INTO hashtags (name) VALUES ($1) RETURNING id`
err = tx.QueryRowContext(l.ctx, createTagQuery, tagName).Scan(&tagID)
if err != nil {
l.Errorf("Failed to create hashtag %s: %v", tagName, err)
continue
}
} else if err != nil {
l.Errorf("Failed to get hashtag %s: %v", tagName, err)
continue
}
// Link hashtag to post
linkQuery := `INSERT INTO post_hashtags (post_id, hashtag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`
_, err = tx.ExecContext(l.ctx, linkQuery, postID, tagID)
if err != nil {
l.Errorf("Failed to link hashtag %s to post: %v", tagName, err)
// Continue with other hashtags
}
}
// Commit transaction // Commit transaction
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
l.Errorf("Failed to commit transaction: %v", err) l.Errorf("Failed to commit transaction: %v", err)

View File

@@ -2,6 +2,7 @@ package logic
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"git.cialloo.com/CiallooWeb/Blog/app/internal/svc" "git.cialloo.com/CiallooWeb/Blog/app/internal/svc"
@@ -26,23 +27,57 @@ func NewDeletePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete
} }
func (l *DeletePostLogic) DeletePost(req *types.DeletePostReq) (resp *types.DeletePostResp, err error) { func (l *DeletePostLogic) DeletePost(req *types.DeletePostReq) (resp *types.DeletePostResp, err error) {
// Start a transaction
tx, err := l.svcCtx.DB.BeginTx(l.ctx, nil)
if err != nil {
l.Errorf("Failed to begin transaction: %v", err)
return nil, err
}
defer tx.Rollback()
// Get cover_id before deleting the post
var coverID sql.NullInt64
getCoverQuery := `SELECT cover_id FROM posts WHERE id = $1`
err = tx.QueryRowContext(l.ctx, getCoverQuery, req.PostId).Scan(&coverID)
if err != nil {
if err == sql.ErrNoRows {
l.Errorf("Post not found with id: %s", req.PostId)
return nil, fmt.Errorf("post not found")
}
l.Errorf("Failed to get post: %v", err)
return nil, err
}
// Delete hashtag associations (post_hashtags will be deleted by CASCADE)
deleteHashtagsQuery := `DELETE FROM post_hashtags WHERE post_id = $1`
_, err = tx.ExecContext(l.ctx, deleteHashtagsQuery, req.PostId)
if err != nil {
l.Errorf("Failed to delete hashtag associations: %v", err)
return nil, err
}
// Delete post from database // Delete post from database
query := `DELETE FROM posts WHERE id = $1` deleteQuery := `DELETE FROM posts WHERE id = $1`
result, err := l.svcCtx.DB.ExecContext(l.ctx, query, req.PostId) _, err = tx.ExecContext(l.ctx, deleteQuery, req.PostId)
if err != nil { if err != nil {
l.Errorf("Failed to delete post: %v", err) l.Errorf("Failed to delete post: %v", err)
return nil, err return nil, err
} }
// Check if post exists // Delete cover file if exists
rowsAffected, err := result.RowsAffected() if coverID.Valid {
deleteFileQuery := `DELETE FROM files WHERE id = $1`
_, err = tx.ExecContext(l.ctx, deleteFileQuery, coverID.Int64)
if err != nil { if err != nil {
l.Errorf("Failed to get rows affected: %v", err) l.Errorf("Failed to delete cover file: %v", err)
return nil, err // Don't fail the whole operation for this
} }
if rowsAffected == 0 { }
l.Errorf("Post not found with id: %s", req.PostId)
return nil, fmt.Errorf("post not found") // Commit transaction
if err = tx.Commit(); err != nil {
l.Errorf("Failed to commit transaction: %v", err)
return nil, err
} }
return &types.DeletePostResp{}, nil return &types.DeletePostResp{}, nil

View File

@@ -56,6 +56,13 @@ func (l *EditPostLogic) EditPost(req *types.EditPostReq) (resp *types.EditPostRe
return nil, fmt.Errorf("invalid content format") return nil, fmt.Errorf("invalid content format")
} }
// Extract all hashtags from content
hashtags, err := utils.ExtractHashtagsFromContent(req.Content)
if err != nil {
l.Errorf("Failed to parse hashtags from content: %v", err)
return nil, fmt.Errorf("invalid content format")
}
// Migrate new cover image from tmpfiles to files if provided // Migrate new cover image from tmpfiles to files if provided
var coverID sql.NullInt64 var coverID sql.NullInt64
if req.CoverImageKey != "" { if req.CoverImageKey != "" {
@@ -102,6 +109,45 @@ func (l *EditPostLogic) EditPost(req *types.EditPostReq) (resp *types.EditPostRe
} }
} }
// Update hashtags - remove old ones and add new ones
// First, remove all existing hashtag associations for this post
deleteHashtagsQuery := `DELETE FROM post_hashtags WHERE post_id = $1`
_, err = tx.ExecContext(l.ctx, deleteHashtagsQuery, req.PostId)
if err != nil {
l.Errorf("Failed to delete old hashtags: %v", err)
return nil, err
}
// Create or get hashtag IDs and link them to the post
for _, tagName := range hashtags {
var tagID int64
// Try to get existing hashtag
getTagQuery := `SELECT id FROM hashtags WHERE name = $1`
err := tx.QueryRowContext(l.ctx, getTagQuery, tagName).Scan(&tagID)
if err == sql.ErrNoRows {
// Hashtag doesn't exist, create it
createTagQuery := `INSERT INTO hashtags (name) VALUES ($1) RETURNING id`
err = tx.QueryRowContext(l.ctx, createTagQuery, tagName).Scan(&tagID)
if err != nil {
l.Errorf("Failed to create hashtag %s: %v", tagName, err)
continue
}
} else if err != nil {
l.Errorf("Failed to get hashtag %s: %v", tagName, err)
continue
}
// Link hashtag to post
linkQuery := `INSERT INTO post_hashtags (post_id, hashtag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`
_, err = tx.ExecContext(l.ctx, linkQuery, req.PostId, tagID)
if err != nil {
l.Errorf("Failed to link hashtag %s to post: %v", tagName, err)
// Continue with other hashtags
}
}
// Commit transaction // Commit transaction
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
l.Errorf("Failed to commit transaction: %v", err) l.Errorf("Failed to commit transaction: %v", err)

View File

@@ -2,6 +2,7 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"strings"
) )
// ExtractImageKeysFromContent parses the rich text JSON and extracts all image fileKey values // ExtractImageKeysFromContent parses the rich text JSON and extracts all image fileKey values
@@ -37,3 +38,47 @@ func extractImageKeys(data interface{}, keys *[]string) {
} }
} }
} }
// ExtractHashtagsFromContent parses the rich text JSON and extracts all hashtag text values
func ExtractHashtagsFromContent(content string) ([]string, error) {
var contentData map[string]interface{}
if err := json.Unmarshal([]byte(content), &contentData); err != nil {
return nil, err
}
var hashtags []string
hashtagSet := make(map[string]bool) // Use map to avoid duplicates
extractHashtags(contentData, hashtagSet)
// Convert map keys to slice
for tag := range hashtagSet {
hashtags = append(hashtags, tag)
}
return hashtags, nil
}
// extractHashtags recursively searches for hashtag nodes and collects their text values
func extractHashtags(data interface{}, hashtags map[string]bool) {
switch v := data.(type) {
case map[string]interface{}:
// Check if this is a hashtag node
if nodeType, ok := v["type"].(string); ok && nodeType == "hashtag" {
if text, ok := v["text"].(string); ok && text != "" {
// Remove the # prefix if present
tag := strings.TrimPrefix(text, "#")
if tag != "" {
hashtags[tag] = true
}
}
}
// Recurse into all map values
for _, value := range v {
extractHashtags(value, hashtags)
}
case []interface{}:
// Recurse into all array elements
for _, item := range v {
extractHashtags(item, hashtags)
}
}
}