feat(article): expose auto_publish_platforms derived from publish accounts
Replace the freeform `platforms` field on articles/tasks (parsed out of wizard_state/input_params JSON) with `auto_publish_platforms`, resolved by joining schedule publish accounts to platform_accounts. The migration strips the legacy keys from existing rows so the new field is the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,7 +115,7 @@ type ArticleListItem struct {
|
||||
PromptRuleID *int64 `json:"prompt_rule_id"`
|
||||
PromptRuleName *string `json:"prompt_rule_name"`
|
||||
GenerationMode *string `json:"generation_mode"`
|
||||
Platforms []string `json:"platforms"`
|
||||
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
Title *string `json:"title"`
|
||||
@@ -147,7 +147,7 @@ type ArticleDetailResponse struct {
|
||||
TemplateID *int64 `json:"template_id"`
|
||||
TemplateName *string `json:"template_name"`
|
||||
GenerationMode *string `json:"generation_mode"`
|
||||
Platforms []string `json:"platforms"`
|
||||
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
@@ -355,7 +355,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
||||
}
|
||||
item.SourceType = dbSourceType
|
||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
||||
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, tenantID, inputParamsJSON)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "query_failed", "failed to resolve article auto publish platforms")
|
||||
}
|
||||
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||
items = append(items, item)
|
||||
}
|
||||
@@ -407,7 +411,11 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
|
||||
_ = json.Unmarshal(wizardStateJSON, &item.WizardState)
|
||||
}
|
||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
||||
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, tenantID, inputParamsJSON)
|
||||
if err != nil {
|
||||
return nil, false, response.ErrInternal(50010, "query_failed", "failed to resolve article auto publish platforms")
|
||||
}
|
||||
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||
item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
|
||||
item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON)
|
||||
@@ -495,7 +503,6 @@ func (s *ArticleService) Create(ctx context.Context, req CreateArticleRequest) (
|
||||
type UpdateArticleRequest struct {
|
||||
Title string `json:"title"`
|
||||
MarkdownContent string `json:"markdown_content"`
|
||||
Platforms []string `json:"platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
ReferencedImageAssetIDs []int64 `json:"referenced_image_asset_ids"`
|
||||
CoverImageAssetID NullableInt64Input `json:"cover_image_asset_id"`
|
||||
@@ -577,7 +584,6 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
|
||||
nextWizardStateJSON, err := mergeArticleWizardState(
|
||||
wizardStateJSON,
|
||||
title,
|
||||
req.Platforms,
|
||||
req.CoverAssetURL,
|
||||
resolvedCoverImageAssetInput,
|
||||
)
|
||||
|
||||
@@ -31,16 +31,16 @@ type InstantTaskListParams struct {
|
||||
}
|
||||
|
||||
type InstantTaskResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
ArticleID *int64 `json:"article_id"`
|
||||
PromptRuleID *int64 `json:"prompt_rule_id"`
|
||||
PromptRuleName *string `json:"prompt_rule_name"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
ExecutionTime *string `json:"execution_time"`
|
||||
Platforms []string `json:"platforms"`
|
||||
GenerateCount int `json:"generate_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
ArticleID *int64 `json:"article_id"`
|
||||
PromptRuleID *int64 `json:"prompt_rule_id"`
|
||||
PromptRuleName *string `json:"prompt_rule_name"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
ExecutionTime *string `json:"execution_time"`
|
||||
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||
GenerateCount int `json:"generate_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type InstantTaskListResponse struct {
|
||||
@@ -123,7 +123,11 @@ func (s *InstantTaskService) List(ctx context.Context, params InstantTaskListPar
|
||||
|
||||
item.ExecutionTime = stringPtrFromDBValue(executionAt)
|
||||
item.CreatedAt = fmt.Sprintf("%v", createdAt)
|
||||
item.Platforms = resolveArticlePlatforms(nil, inputParamsJSON)
|
||||
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, actor.TenantID, inputParamsJSON)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "query_failed", "failed to resolve instant task auto publish platforms")
|
||||
}
|
||||
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||
item.GenerateCount = 1
|
||||
|
||||
if len(inputParamsJSON) > 0 {
|
||||
|
||||
@@ -343,7 +343,7 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
coverImageAssetID.Set = true
|
||||
coverImageAssetID.Value = &rawCoverImageAssetID
|
||||
}
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, nil, coverAssetURL, coverImageAssetID)
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, coverAssetURL, coverImageAssetID)
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func normalizePlatformIDs(platformIDs []string) []string {
|
||||
@@ -31,52 +34,79 @@ func normalizePlatformIDs(platformIDs []string) []string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func parsePlatformIDs(value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return normalizePlatformIDs(strings.Split(value, ","))
|
||||
type scheduleAutoPublishPlatformQuerier interface {
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
}
|
||||
|
||||
func serializePlatformIDs(platformIDs []string) string {
|
||||
normalized := normalizePlatformIDs(platformIDs)
|
||||
if len(normalized) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(normalized, ",")
|
||||
}
|
||||
|
||||
func resolveArticlePlatforms(wizardStateJSON, inputParamsJSON []byte) []string {
|
||||
if platforms := extractPlatformsFromJSONPayload(wizardStateJSON); len(platforms) > 0 {
|
||||
return platforms
|
||||
}
|
||||
if platforms := extractPlatformsFromJSONPayload(inputParamsJSON); len(platforms) > 0 {
|
||||
return platforms
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func extractPlatformsFromJSONPayload(raw []byte) []string {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
func loadScheduleAutoPublishPlatforms(
|
||||
ctx context.Context,
|
||||
db scheduleAutoPublishPlatformQuerier,
|
||||
tenantID int64,
|
||||
inputParamsJSON []byte,
|
||||
) ([]string, error) {
|
||||
if db == nil || tenantID <= 0 || len(inputParamsJSON) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return nil
|
||||
if err := json.Unmarshal(inputParamsJSON, &payload); err != nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
if !extractBool(payload, "schedule_auto_publish") {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
if platforms := normalizePlatformsFromValue(payload["platforms"]); len(platforms) > 0 {
|
||||
return platforms
|
||||
}
|
||||
if platforms := normalizePlatformsFromValue(payload["target_platforms"]); len(platforms) > 0 {
|
||||
return platforms
|
||||
accountIDs := normalizeSchedulePublishAccountIDs(extractStringList(payload["schedule_publish_account_ids"], 64))
|
||||
if len(accountIDs) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
if targetPlatform, ok := payload["target_platform"].(string); ok {
|
||||
return parsePlatformIDs(targetPlatform)
|
||||
return loadPublishAccountPlatformsByIDs(ctx, db, tenantID, accountIDs)
|
||||
}
|
||||
|
||||
func loadPublishAccountPlatformsByIDs(
|
||||
ctx context.Context,
|
||||
db scheduleAutoPublishPlatformQuerier,
|
||||
tenantID int64,
|
||||
accountIDs []string,
|
||||
) ([]string, error) {
|
||||
accountIDs = normalizeSchedulePublishAccountIDs(accountIDs)
|
||||
if db == nil || tenantID <= 0 || len(accountIDs) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil
|
||||
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT DISTINCT platform_id
|
||||
FROM platform_accounts
|
||||
WHERE tenant_id = $1
|
||||
AND desktop_id::text = ANY($2::text[])
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY platform_id
|
||||
`, tenantID, accountIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
platformIDs := make([]string, 0, len(accountIDs))
|
||||
for rows.Next() {
|
||||
var platformID string
|
||||
if err := rows.Scan(&platformID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platformIDs = append(platformIDs, platformID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)), nil
|
||||
}
|
||||
|
||||
func emptyStringSliceIfNil(values []string) []string {
|
||||
if values == nil {
|
||||
return []string{}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
|
||||
@@ -146,51 +176,21 @@ func extractInt64FromJSONPayload(raw []byte, key string) (int64, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePlatformsFromValue(raw interface{}) []string {
|
||||
switch typed := raw.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case string:
|
||||
return parsePlatformIDs(typed)
|
||||
case []string:
|
||||
return normalizePlatformIDs(typed)
|
||||
case []interface{}:
|
||||
values := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
value, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return normalizePlatformIDs(values)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mergeArticleWizardState(raw []byte, title string, platforms []string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
|
||||
func mergeArticleWizardState(raw []byte, title string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
|
||||
state := make(map[string]interface{})
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &state)
|
||||
}
|
||||
|
||||
delete(state, "platforms")
|
||||
delete(state, "target_platforms")
|
||||
delete(state, "target_platform")
|
||||
|
||||
trimmedTitle := strings.TrimSpace(title)
|
||||
if trimmedTitle != "" {
|
||||
state["title"] = trimmedTitle
|
||||
}
|
||||
|
||||
if platforms != nil {
|
||||
normalized := normalizePlatformIDs(platforms)
|
||||
if len(normalized) == 0 {
|
||||
delete(state, "platforms")
|
||||
delete(state, "target_platform")
|
||||
} else {
|
||||
state["platforms"] = normalized
|
||||
state["target_platform"] = strings.Join(normalized, ",")
|
||||
}
|
||||
}
|
||||
|
||||
if coverAssetURL != nil {
|
||||
trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
|
||||
if trimmedCoverURL == "" {
|
||||
|
||||
@@ -54,26 +54,27 @@ type ScheduleTaskRequest struct {
|
||||
}
|
||||
|
||||
type ScheduleTaskResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
WorkspaceID *int64 `json:"workspace_id"`
|
||||
PromptRuleID int64 `json:"prompt_rule_id"`
|
||||
PromptRuleName *string `json:"prompt_rule_name"`
|
||||
BrandID *int64 `json:"brand_id"`
|
||||
BrandName *string `json:"brand_name"`
|
||||
Name string `json:"name"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
AutoPublish bool `json:"auto_publish"`
|
||||
PublishAccountIDs []string `json:"publish_account_ids"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||
EnableWebSearch bool `json:"enable_web_search"`
|
||||
GenerateCount int `json:"generate_count"`
|
||||
StartAt *string `json:"start_at"`
|
||||
EndAt *string `json:"end_at"`
|
||||
NextRunAt *string `json:"next_run_at"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
WorkspaceID *int64 `json:"workspace_id"`
|
||||
PromptRuleID int64 `json:"prompt_rule_id"`
|
||||
PromptRuleName *string `json:"prompt_rule_name"`
|
||||
BrandID *int64 `json:"brand_id"`
|
||||
BrandName *string `json:"brand_name"`
|
||||
Name string `json:"name"`
|
||||
CronExpr string `json:"cron_expr"`
|
||||
AutoPublish bool `json:"auto_publish"`
|
||||
PublishAccountIDs []string `json:"publish_account_ids"`
|
||||
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||
EnableWebSearch bool `json:"enable_web_search"`
|
||||
GenerateCount int `json:"generate_count"`
|
||||
StartAt *string `json:"start_at"`
|
||||
EndAt *string `json:"end_at"`
|
||||
NextRunAt *string `json:"next_run_at"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ScheduleTaskListParams struct {
|
||||
@@ -206,7 +207,7 @@ func (s *ScheduleTaskService) Create(ctx context.Context, req ScheduleTaskReques
|
||||
return &ScheduleTaskResponse{
|
||||
ID: id, WorkspaceID: &workspaceID, PromptRuleID: req.PromptRuleID, BrandID: req.BrandID,
|
||||
Name: req.Name, CronExpr: req.CronExpr, AutoPublish: publishConfig.AutoPublish,
|
||||
PublishAccountIDs: publishConfig.AccountIDs, CoverAssetURL: publishConfig.CoverAssetURL,
|
||||
PublishAccountIDs: publishConfig.AccountIDs, AutoPublishPlatforms: publishConfig.PlatformIDs, CoverAssetURL: publishConfig.CoverAssetURL,
|
||||
CoverImageAssetID: publishConfig.CoverImageAssetID, EnableWebSearch: enableWebSearch,
|
||||
GenerateCount: generateCount,
|
||||
Status: status, CreatedAt: fmt.Sprintf("%v", createdAt),
|
||||
@@ -400,6 +401,9 @@ func (s *ScheduleTaskService) loadScheduleTasks(ctx context.Context, tenantID in
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return &ScheduleTaskListResponse{Items: items, Total: total}, nil
|
||||
@@ -425,9 +429,24 @@ func (s *ScheduleTaskService) loadScheduleTaskDetail(ctx context.Context, tenant
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &item, true, nil
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) populateScheduleAutoPublishPlatforms(ctx context.Context, tenantID int64, item *ScheduleTaskResponse) error {
|
||||
if item == nil || !item.AutoPublish {
|
||||
return nil
|
||||
}
|
||||
platformIDs, err := loadPublishAccountPlatformsByIDs(ctx, s.pool, tenantID, item.PublishAccountIDs)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50010, "query_failed", "failed to resolve schedule auto publish platforms")
|
||||
}
|
||||
item.AutoPublishPlatforms = platformIDs
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanScheduleTaskResponse(scanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}) (ScheduleTaskResponse, error) {
|
||||
@@ -480,6 +499,7 @@ func (s *ScheduleTaskService) ensurePromptRuleExists(ctx context.Context, tenant
|
||||
type scheduleAutoPublishConfig struct {
|
||||
AutoPublish bool
|
||||
AccountIDs []string
|
||||
PlatformIDs []string
|
||||
AccountIDsJSON []byte
|
||||
CoverAssetURL *string
|
||||
CoverImageAssetID *int64
|
||||
@@ -514,6 +534,7 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
config.PlatformIDs = normalizePlatformIDs(platformIDs)
|
||||
if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" {
|
||||
return config, response.ErrBadRequest(40025, "cover_required", "selected publish accounts require cover_asset_url")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user