feat(tenant/articles): aggregate publish status from per-account records
a.publish_status only stored a coarse summary so the article list/detail mis-reported as 失败 when one of several accounts failed. Compute the effective status via a LATERAL join over latest publish_records per platform_account_id and bucket into success / publishing / failed / partial_success. Wire the same expression into list filtering and detail queries, and let normalizePublishStatus / batch-status helpers retain partial_success end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,34 @@ var supportedArticleImageExtensions = map[string]string{
|
||||
var articleMarkdownImageAssetIDPattern = regexp.MustCompile(`data-asset-id=(?:"|')(\d+)(?:"|')`)
|
||||
var articleMarkdownImageAssetAttributePattern = regexp.MustCompile(`\s*data-asset-id=(?:"|')(\d+)(?:"|')`)
|
||||
|
||||
const articlePublishStatusAggregateJoin = `
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COUNT(*) AS record_count,
|
||||
BOOL_OR(latest.status_bucket = 'publishing') AS has_publishing,
|
||||
BOOL_OR(latest.status_bucket = 'success') AS has_success,
|
||||
BOOL_OR(latest.status_bucket = 'failed') AS has_failed
|
||||
FROM (
|
||||
SELECT DISTINCT ON (pr.platform_account_id)
|
||||
CASE
|
||||
WHEN pr.status IN ('success', 'published', 'publish_success') THEN 'success'
|
||||
WHEN pr.status IN ('publishing', 'pending', 'queued', 'running') THEN 'publishing'
|
||||
ELSE 'failed'
|
||||
END AS status_bucket
|
||||
FROM publish_records pr
|
||||
WHERE pr.tenant_id = a.tenant_id
|
||||
AND pr.article_id = a.id
|
||||
ORDER BY pr.platform_account_id, pr.created_at DESC, pr.id DESC
|
||||
) latest
|
||||
) publish_status_summary ON true`
|
||||
|
||||
const articleEffectivePublishStatusExpr = `CASE
|
||||
WHEN COALESCE(publish_status_summary.record_count, 0) = 0 THEN a.publish_status
|
||||
WHEN COALESCE(publish_status_summary.has_publishing, false) THEN 'publishing'
|
||||
WHEN COALESCE(publish_status_summary.has_success, false) AND COALESCE(publish_status_summary.has_failed, false) THEN 'partial_success'
|
||||
WHEN COALESCE(publish_status_summary.has_success, false) THEN 'success'
|
||||
ELSE 'failed'
|
||||
END`
|
||||
|
||||
type ArticleListParams struct {
|
||||
Page int
|
||||
PageSize int
|
||||
@@ -173,7 +201,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
WHERE article_id = a.id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) gt ON true
|
||||
) gt ON true`
|
||||
if params.PublishStatus != nil {
|
||||
countQuery += articlePublishStatusAggregateJoin
|
||||
}
|
||||
countQuery += `
|
||||
WHERE a.tenant_id = $1 AND a.deleted_at IS NULL`
|
||||
argIdx := 2
|
||||
|
||||
@@ -183,7 +215,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
argIdx++
|
||||
}
|
||||
if params.PublishStatus != nil {
|
||||
countQuery += ` AND a.publish_status = $` + strconv.Itoa(argIdx)
|
||||
countQuery += ` AND ` + articleEffectivePublishStatusExpr + ` = $` + strconv.Itoa(argIdx)
|
||||
countArgs = append(countArgs, *params.PublishStatus)
|
||||
argIdx++
|
||||
}
|
||||
@@ -232,7 +264,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
return nil, response.ErrInternal(50010, "query_failed", "failed to count articles")
|
||||
}
|
||||
|
||||
listQuery := `SELECT a.id, a.source_type, a.template_id, a.kol_prompt_id, a.prompt_rule_id, a.generate_status, a.publish_status, a.created_at,
|
||||
listQuery := `SELECT a.id, a.source_type, a.template_id, a.kol_prompt_id, a.prompt_rule_id, a.generate_status, ` + articleEffectivePublishStatusExpr + `, a.created_at,
|
||||
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,
|
||||
COALESCE(t.template_name, NULLIF(gt.input_params_json ->> 'prompt_name', ''), NULLIF(gt.input_params_json ->> 'package_name', '')), pr.name, a.wizard_state_json, gt.input_params_json
|
||||
FROM articles a
|
||||
@@ -246,6 +278,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) gt ON true
|
||||
` + articlePublishStatusAggregateJoin + `
|
||||
WHERE a.tenant_id = $1 AND a.deleted_at IS NULL`
|
||||
listArgs := []interface{}{tenantID}
|
||||
listIdx := 2
|
||||
@@ -256,7 +289,7 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
listIdx++
|
||||
}
|
||||
if params.PublishStatus != nil {
|
||||
listQuery += ` AND a.publish_status = $` + strconv.Itoa(listIdx)
|
||||
listQuery += ` AND ` + articleEffectivePublishStatusExpr + ` = $` + strconv.Itoa(listIdx)
|
||||
listArgs = append(listArgs, *params.PublishStatus)
|
||||
listIdx++
|
||||
}
|
||||
@@ -336,7 +369,7 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
|
||||
var wizardStateJSON []byte
|
||||
var inputParamsJSON []byte
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT a.id, a.source_type, a.template_id, a.current_version_id, a.generate_status, a.publish_status, a.created_at,
|
||||
SELECT a.id, a.source_type, a.template_id, a.current_version_id, a.generate_status, `+articleEffectivePublishStatusExpr+`, 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
|
||||
@@ -349,6 +382,7 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) gt ON true
|
||||
`+articlePublishStatusAggregateJoin+`
|
||||
WHERE a.id = $1 AND a.tenant_id = $2 AND a.deleted_at IS NULL
|
||||
`, articleID, tenantID).Scan(&item.ID, &dbSourceType, &item.TemplateID, ¤tVersionID, &item.GenerateStatus, &item.PublishStatus, &item.CreatedAt,
|
||||
&item.Title, &item.WordCount, &item.SourceLabel, &item.VersionNo, &item.TemplateName, &item.GenerationErrorMessage, &wizardStateJSON, &inputParamsJSON)
|
||||
|
||||
@@ -154,6 +154,8 @@ func normalizePublishStatus(status string) string {
|
||||
switch status {
|
||||
case "success", "published", "publish_success":
|
||||
return "success"
|
||||
case "partial_success":
|
||||
return "partial_success"
|
||||
case "publishing", "pending", "queued", "running":
|
||||
return "publishing"
|
||||
default:
|
||||
|
||||
@@ -63,3 +63,15 @@ func TestNormalizePublishRecordForResponseUsesBaijiahaoPublicURL(t *testing.T) {
|
||||
t.Fatalf("ExternalManageURL = %v, want original edit URL", item.ExternalManageURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePublishStatusKeepsPartialSuccess(t *testing.T) {
|
||||
if got := normalizePublishStatus("partial_success"); got != "partial_success" {
|
||||
t.Fatalf("normalizePublishStatus(partial_success) = %q, want partial_success", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishBatchStatusToArticleStatusKeepsPartialSuccess(t *testing.T) {
|
||||
if got := publishBatchStatusToArticleStatus("partial_success"); got != "partial_success" {
|
||||
t.Fatalf("publishBatchStatusToArticleStatus(partial_success) = %q, want partial_success", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user