202 lines
4.9 KiB
Go
202 lines
4.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/contentstats"
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type CreateArticleInput struct {
|
|
TenantID int64
|
|
SourceType string
|
|
TemplateID *int64
|
|
KolPromptID *int64
|
|
}
|
|
|
|
type CreateArticleVersionInput struct {
|
|
ArticleID int64
|
|
VersionNo int
|
|
Title string
|
|
HTMLContent string
|
|
MarkdownContent string
|
|
WordCount int
|
|
SourceLabel string
|
|
}
|
|
|
|
type ArticleRepository interface {
|
|
CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error)
|
|
CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error)
|
|
UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error
|
|
UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error
|
|
}
|
|
|
|
type articleRepository struct {
|
|
db generated.DBTX
|
|
q generated.Querier
|
|
}
|
|
|
|
func NewArticleRepository(db generated.DBTX) ArticleRepository {
|
|
return &articleRepository{db: db, q: newQuerier(db)}
|
|
}
|
|
|
|
func (r *articleRepository) CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error) {
|
|
return r.q.CreateArticle(ctx, generated.CreateArticleParams{
|
|
TenantID: input.TenantID,
|
|
SourceType: input.SourceType,
|
|
TemplateID: pgInt8(input.TemplateID),
|
|
KolPromptID: pgInt8(input.KolPromptID),
|
|
})
|
|
}
|
|
|
|
func NextArticleVersionNo(ctx context.Context, db generated.DBTX, articleID int64) (int, error) {
|
|
var nextVersionNo int
|
|
err := db.QueryRow(ctx, `
|
|
SELECT COALESCE(MAX(version_no), 0) + 1
|
|
FROM article_versions
|
|
WHERE article_id = $1
|
|
`, articleID).Scan(&nextVersionNo)
|
|
return nextVersionNo, err
|
|
}
|
|
|
|
func (r *articleRepository) CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error) {
|
|
if input.VersionNo <= 0 {
|
|
nextVersionNo, err := NextArticleVersionNo(ctx, r.db, input.ArticleID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
input.VersionNo = nextVersionNo
|
|
}
|
|
|
|
previous, err := loadLatestArticleVersionContent(ctx, r.db, input.ArticleID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
storedHTML, storedMarkdown := buildArticleVersionDiffStorage(*previous, ArticleVersionContent{
|
|
HTMLContent: stringPtr(input.HTMLContent),
|
|
MarkdownContent: stringPtr(input.MarkdownContent),
|
|
})
|
|
|
|
var versionID int64
|
|
err = r.db.QueryRow(ctx, `
|
|
INSERT INTO article_versions (
|
|
article_id,
|
|
version_no,
|
|
title,
|
|
html_content,
|
|
markdown_content,
|
|
word_count,
|
|
source_label,
|
|
plaintext_snapshot
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id
|
|
`,
|
|
input.ArticleID,
|
|
input.VersionNo,
|
|
pgText(&input.Title),
|
|
pgText(storedHTML),
|
|
pgText(storedMarkdown),
|
|
input.WordCount,
|
|
pgText(&input.SourceLabel),
|
|
strings.TrimSpace(input.Title+"\n"+ExtractArticlePlaintext(input.MarkdownContent)),
|
|
).Scan(&versionID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return versionID, nil
|
|
}
|
|
|
|
func ExtractArticlePlaintext(markdown string) string {
|
|
markdown = strings.TrimSpace(markdown)
|
|
if markdown == "" {
|
|
return ""
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.Grow(len(markdown))
|
|
inFence := false
|
|
lines := strings.Split(markdown, "\n")
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
|
|
inFence = !inFence
|
|
continue
|
|
}
|
|
if inFence {
|
|
builder.WriteString(trimmed)
|
|
builder.WriteByte('\n')
|
|
continue
|
|
}
|
|
line = stripMarkdownLine(line)
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
builder.WriteString(line)
|
|
builder.WriteByte('\n')
|
|
}
|
|
return strings.TrimSpace(builder.String())
|
|
}
|
|
|
|
func stripMarkdownLine(line string) string {
|
|
replacer := strings.NewReplacer(
|
|
"#", " ",
|
|
"*", " ",
|
|
"_", " ",
|
|
"`", " ",
|
|
">", " ",
|
|
"[", " ",
|
|
"]", " ",
|
|
"(", " ",
|
|
")", " ",
|
|
"!", " ",
|
|
"|", " ",
|
|
)
|
|
line = replacer.Replace(line)
|
|
line = strings.TrimLeft(line, "-+0123456789. \t")
|
|
return strings.Join(strings.Fields(line), " ")
|
|
}
|
|
|
|
func CountArticlePlaintextWords(markdown string) int {
|
|
return contentstats.CountWords(ExtractArticlePlaintext(markdown))
|
|
}
|
|
|
|
func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error {
|
|
tag, err := r.db.Exec(ctx, `
|
|
UPDATE articles
|
|
SET current_version_id = $1,
|
|
updated_at = NOW()
|
|
WHERE id = $2
|
|
AND tenant_id = $3
|
|
AND deleted_at IS NULL
|
|
`, pgInt8(&versionID), articleID, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *articleRepository) UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error {
|
|
tag, err := r.db.Exec(ctx, `
|
|
UPDATE articles
|
|
SET generate_status = $1,
|
|
updated_at = NOW()
|
|
WHERE id = $2
|
|
AND tenant_id = $3
|
|
AND deleted_at IS NULL
|
|
`, status, articleID, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|