feat: implement article version content storage and reconstruction logic; enhance article repository with version management

This commit is contained in:
2026-04-07 22:21:16 +08:00
parent d0f0271346
commit 41f8e0621e
9 changed files with 324 additions and 48 deletions
+40 -24
View File
@@ -21,6 +21,7 @@ import (
"github.com/geo-platform/tenant-api/internal/shared/middleware"
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type ArticleService struct {
@@ -161,7 +162,7 @@ func (s *ArticleService) List(ctx context.Context, params ArticleListParams) (*A
// List
listQuery := `SELECT a.id, a.source_type, a.template_id, a.prompt_rule_id, a.generate_status, a.publish_status, a.created_at,
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, av.markdown_content, COALESCE(av.word_count, 0), av.source_label, t.template_name, pr.name, a.wizard_state_json, gt.input_params_json
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, COALESCE(av.word_count, 0), av.source_label, t.template_name, pr.name, a.wizard_state_json, gt.input_params_json
FROM articles a
LEFT JOIN article_versions av ON av.id = a.current_version_id
LEFT JOIN article_templates t ON t.id = a.template_id
@@ -236,17 +237,15 @@ func (s *ArticleService) List(ctx context.Context, params ArticleListParams) (*A
for rows.Next() {
var item ArticleListItem
var dbSourceType string
var markdownContent *string
var wizardStateJSON []byte
var inputParamsJSON []byte
if err := rows.Scan(&item.ID, &dbSourceType, &item.TemplateID, &item.PromptRuleID, &item.GenerateStatus, &item.PublishStatus, &item.CreatedAt,
&item.Title, &item.GenerationErrorMessage, &markdownContent, &item.WordCount, &item.SourceLabel, &item.TemplateName, &item.PromptRuleName, &wizardStateJSON, &inputParamsJSON); err != nil {
&item.Title, &item.GenerationErrorMessage, &item.WordCount, &item.SourceLabel, &item.TemplateName, &item.PromptRuleName, &wizardStateJSON, &inputParamsJSON); err != nil {
return nil, response.ErrInternal(50010, "scan_failed", err.Error())
}
item.SourceType = dbSourceType
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
item.WordCount = resolveWordCount(markdownContent, item.WordCount)
items = append(items, item)
}
if items == nil {
@@ -281,12 +280,13 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe
actor := auth.MustActor(ctx)
var d ArticleDetailResponse
var dbSourceType string
var currentVersionID *int64
var wizardStateJSON []byte
var inputParamsJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT a.id, a.source_type, a.template_id, a.generate_status, a.publish_status, a.created_at,
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), av.html_content, av.markdown_content, COALESCE(av.word_count, 0), av.source_label, av.version_no,
SELECT a.id, a.source_type, a.template_id, a.current_version_id, a.generate_status, a.publish_status, a.created_at,
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), COALESCE(av.word_count, 0), av.source_label, av.version_no,
t.template_name, gt.error_message, a.wizard_state_json, gt.input_params_json
FROM articles a
LEFT JOIN article_versions av ON av.id = a.current_version_id
@@ -299,12 +299,20 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe
LIMIT 1
) gt ON true
WHERE a.id = $1 AND a.tenant_id = $2 AND a.deleted_at IS NULL
`, id, actor.TenantID).Scan(&d.ID, &dbSourceType, &d.TemplateID, &d.GenerateStatus, &d.PublishStatus, &d.CreatedAt,
&d.Title, &d.HTMLContent, &d.MarkdownContent, &d.WordCount, &d.SourceLabel, &d.VersionNo, &d.TemplateName, &d.GenerationErrorMessage, &wizardStateJSON, &inputParamsJSON)
`, id, actor.TenantID).Scan(&d.ID, &dbSourceType, &d.TemplateID, &currentVersionID, &d.GenerateStatus, &d.PublishStatus, &d.CreatedAt,
&d.Title, &d.WordCount, &d.SourceLabel, &d.VersionNo, &d.TemplateName, &d.GenerationErrorMessage, &wizardStateJSON, &inputParamsJSON)
if err != nil {
return nil, response.ErrNotFound(40411, "article_not_found", "article not found")
}
d.SourceType = dbSourceType
if currentVersionID != nil {
content, err := repository.LoadArticleVersionContent(ctx, s.pool, d.ID, *currentVersionID)
if err != nil {
return nil, response.ErrInternal(50011, "article_version_reconstruct_failed", "failed to reconstruct article version")
}
d.HTMLContent = content.HTMLContent
d.MarkdownContent = content.MarkdownContent
}
if len(wizardStateJSON) > 0 {
d.WizardState = map[string]interface{}{}
_ = json.Unmarshal(wizardStateJSON, &d.WizardState)
@@ -341,12 +349,17 @@ func (s *ArticleService) Create(ctx context.Context, req CreateArticleRequest) (
}
sourceLabel := "自由创作"
var versionID int64
if err := tx.QueryRow(ctx, `
INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label)
VALUES ($1, 1, $2, NULL, '', 0, $3)
RETURNING id
`, articleID, title, sourceLabel).Scan(&versionID); err != nil {
articleRepo := repository.NewArticleRepository(tx)
versionID, err := articleRepo.CreateArticleVersion(ctx, repository.CreateArticleVersionInput{
ArticleID: articleID,
VersionNo: 1,
Title: title,
HTMLContent: "",
MarkdownContent: "",
WordCount: 0,
SourceLabel: sourceLabel,
})
if err != nil {
return nil, response.ErrInternal(50014, "create_failed", "failed to create article version")
}
@@ -426,12 +439,17 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
return nil, response.ErrInternal(50012, "update_failed", "failed to update article metadata")
}
var versionID int64
if err := tx.QueryRow(ctx, `
INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label)
VALUES ($1, $2, $3, NULL, $4, $5, $6)
RETURNING id
`, id, nextVersionNo, title, markdown, wordCount, sourceLabel).Scan(&versionID); err != nil {
articleRepo := repository.NewArticleRepository(tx)
versionID, err := articleRepo.CreateArticleVersion(ctx, repository.CreateArticleVersionInput{
ArticleID: id,
VersionNo: nextVersionNo,
Title: title,
HTMLContent: "",
MarkdownContent: markdown,
WordCount: wordCount,
SourceLabel: sourceLabel,
})
if err != nil {
return nil, response.ErrInternal(50012, "update_failed", "failed to create article version")
}
@@ -527,7 +545,7 @@ func (s *ArticleService) Versions(ctx context.Context, articleID int64) ([]Versi
}
rows, err := s.pool.Query(ctx, `
SELECT id, version_no, title, markdown_content, word_count, source_label, created_at
SELECT id, version_no, title, word_count, source_label, created_at
FROM article_versions WHERE article_id = $1 ORDER BY version_no DESC
`, articleID)
if err != nil {
@@ -538,11 +556,9 @@ func (s *ArticleService) Versions(ctx context.Context, articleID int64) ([]Versi
var versions []VersionItem
for rows.Next() {
var v VersionItem
var markdownContent *string
if err := rows.Scan(&v.ID, &v.VersionNo, &v.Title, &markdownContent, &v.WordCount, &v.SourceLabel, &v.CreatedAt); err != nil {
if err := rows.Scan(&v.ID, &v.VersionNo, &v.Title, &v.WordCount, &v.SourceLabel, &v.CreatedAt); err != nil {
return nil, response.ErrInternal(50010, "scan_failed", err.Error())
}
v.WordCount = resolveWordCount(markdownContent, v.WordCount)
versions = append(versions, v)
}
if versions == nil {