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:
2026-05-01 11:02:21 +08:00
parent 0f4310c345
commit 490c6c759d
15 changed files with 172 additions and 137 deletions
@@ -116,7 +116,7 @@ const deleteMutation = useMutation({
const columns = computed<TableColumnsType<ArticleListItem>>(() => [ const columns = computed<TableColumnsType<ArticleListItem>>(() => [
{ title: t("custom.article.titleColumn"), dataIndex: "title", key: "title", width: 340 }, { title: t("custom.article.titleColumn"), dataIndex: "title", key: "title", width: 340 },
{ title: t("custom.filters.promptRule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 160 }, { title: t("custom.filters.promptRule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 160 },
{ title: t("custom.schedule.platform"), dataIndex: "platforms", key: "platforms", width: 180 }, { title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 180 },
{ title: t("common.generateStatus"), dataIndex: "generate_status", key: "generate_status", width: 128 }, { title: t("common.generateStatus"), dataIndex: "generate_status", key: "generate_status", width: 128 },
{ title: t("common.publishStatus"), dataIndex: "publish_status", key: "publish_status", width: 128 }, { title: t("common.publishStatus"), dataIndex: "publish_status", key: "publish_status", width: 128 },
{ title: t("common.wordCount"), dataIndex: "word_count", key: "word_count", width: 100 }, { title: t("common.wordCount"), dataIndex: "word_count", key: "word_count", width: 100 },
@@ -319,8 +319,8 @@ onBeforeUnmount(() => {
<template v-else-if="column.key === 'prompt_rule_name'"> <template v-else-if="column.key === 'prompt_rule_name'">
{{ record.prompt_rule_name || "--" }} {{ record.prompt_rule_name || "--" }}
</template> </template>
<template v-else-if="column.key === 'platforms'"> <template v-else-if="column.key === 'auto_publish_platforms'">
{{ formatPublishPlatformList(record.platforms) }} {{ formatPublishPlatformList(record.auto_publish_platforms) }}
</template> </template>
<template v-else-if="column.key === 'generate_status'"> <template v-else-if="column.key === 'generate_status'">
<ArticleGenerateStatus :status="record.generate_status" /> <ArticleGenerateStatus :status="record.generate_status" />
@@ -80,7 +80,7 @@ const columns = computed<TableColumnsType<InstantTaskItem>>(() => [
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 }, { title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 }, { title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
{ title: t("custom.instant.executionTime"), dataIndex: "execution_time", key: "execution_time", width: 170 }, { title: t("custom.instant.executionTime"), dataIndex: "execution_time", key: "execution_time", width: 170 },
{ title: t("custom.schedule.platform"), dataIndex: "platforms", key: "platforms", width: 220 }, { title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 220 },
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 }, { title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 }, { title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
{ title: t("common.actions"), key: "actions", width: 88, fixed: "right", align: "right" }, { title: t("common.actions"), key: "actions", width: 88, fixed: "right", align: "right" },
@@ -235,8 +235,8 @@ onBeforeUnmount(() => {
<template v-else-if="column.key === 'execution_time'"> <template v-else-if="column.key === 'execution_time'">
{{ formatDateTime(record.execution_time) }} {{ formatDateTime(record.execution_time) }}
</template> </template>
<template v-else-if="column.key === 'platforms'"> <template v-else-if="column.key === 'auto_publish_platforms'">
{{ formatPublishPlatformList(record.platforms) }} {{ formatPublishPlatformList(record.auto_publish_platforms) }}
</template> </template>
<template v-else-if="column.key === 'generate_count'"> <template v-else-if="column.key === 'generate_count'">
{{ record.generate_count || 1 }} {{ record.generate_count || 1 }}
@@ -21,9 +21,6 @@ import {
publishStateLabel, publishStateLabel,
type PublishAccountCard, type PublishAccountCard,
} from "@/lib/publish-account-cards"; } from "@/lib/publish-account-cards";
import {
normalizePublishPlatformIds,
} from "@/lib/publish-platforms";
const props = defineProps<{ const props = defineProps<{
open: boolean; open: boolean;
@@ -93,10 +90,8 @@ watch(
const platformMap = computed(() => buildPublishPlatformMap(platformsQuery.data.value ?? [])); const platformMap = computed(() => buildPublishPlatformMap(platformsQuery.data.value ?? []));
const articlePlatformIds = computed(() => normalizePublishPlatformIds(detailQuery.data.value?.platforms ?? []));
const accountCards = computed<PublishAccountCard[]>(() => const accountCards = computed<PublishAccountCard[]>(() =>
buildPublishAccountCards(accountsQuery.data.value ?? [], platformMap.value, articlePlatformIds.value), buildPublishAccountCards(accountsQuery.data.value ?? [], platformMap.value),
); );
watch( watch(
@@ -137,10 +132,7 @@ watchEffect(() => {
return; return;
} }
const preferredPlatforms = new Set(articlePlatformIds.value); selectedAccountIds.value = [];
selectedAccountIds.value = accountCards.value
.filter((card) => card.selectable && preferredPlatforms.has(card.platformId))
.map((card) => card.id);
selectionHydrated.value = true; selectionHydrated.value = true;
}); });
@@ -237,7 +229,6 @@ async function persistCoverIfNeeded(detail: ArticleDetail): Promise<ArticleDetai
return articlesApi.update(detail.id, { return articlesApi.update(detail.id, {
title: detail.title?.trim() || t("article.untitled"), title: detail.title?.trim() || t("article.untitled"),
markdown_content: detail.markdown_content ?? "", markdown_content: detail.markdown_content ?? "",
platforms: normalizePublishPlatformIds(detail.platforms ?? []),
cover_asset_url: nextUrl || null, cover_asset_url: nextUrl || null,
cover_image_asset_id: coverImageAssetId.value, cover_image_asset_id: coverImageAssetId.value,
}); });
@@ -16,6 +16,7 @@ import ScheduleTaskModal from "@/components/ScheduleTaskModal.vue";
import { promptRulesApi, schedulesApi } from "@/lib/api"; import { promptRulesApi, schedulesApi } from "@/lib/api";
import { formatCronExecutionTime, formatDateTime } from "@/lib/display"; import { formatCronExecutionTime, formatDateTime } from "@/lib/display";
import { formatError } from "@/lib/errors"; import { formatError } from "@/lib/errors";
import { formatPublishPlatformList } from "@/lib/publish-platforms";
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useI18n(); const { t } = useI18n();
@@ -84,7 +85,7 @@ const columns = computed<TableColumnsType<ScheduleTask>>(() => [
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 }, { title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 }, { title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
{ title: t("custom.instant.executionTime"), dataIndex: "cron_expr", key: "cron_expr", width: 170 }, { title: t("custom.instant.executionTime"), dataIndex: "cron_expr", key: "cron_expr", width: 170 },
{ title: t("custom.schedule.autoPublish"), dataIndex: "auto_publish", key: "auto_publish", width: 160 }, { title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 200 },
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 }, { title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 }, { title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
{ title: t("common.actions"), key: "actions", width: 140, fixed: "right", align: "right" }, { title: t("common.actions"), key: "actions", width: 140, fixed: "right", align: "right" },
@@ -222,11 +223,11 @@ function handleTableChange(nextPage: number, nextPageSize: number): void {
<template v-else-if="column.key === 'cron_expr'"> <template v-else-if="column.key === 'cron_expr'">
{{ formatCronExecutionTime(record.cron_expr) }} {{ formatCronExecutionTime(record.cron_expr) }}
</template> </template>
<template v-else-if="column.key === 'auto_publish'"> <template v-else-if="column.key === 'auto_publish_platforms'">
<a-tag :color="record.auto_publish ? 'blue' : 'default'"> <a-tag :color="record.auto_publish ? 'blue' : 'default'">
{{ {{
record.auto_publish record.auto_publish
? t("custom.schedule.autoPublishAccountCount", { count: record.publish_account_ids?.length ?? 0 }) ? formatPublishPlatformList(record.auto_publish_platforms)
: t("custom.schedule.manualPublish") : t("custom.schedule.manualPublish")
}} }}
</a-tag> </a-tag>
+1 -1
View File
@@ -1290,9 +1290,9 @@ const enUS = {
name: "Task name", name: "Task name",
rule: "Prompt rule", rule: "Prompt rule",
cron: "Frequency", cron: "Frequency",
autoPublishPlatforms: "Auto publish platforms",
autoPublish: "Auto publish", autoPublish: "Auto publish",
manualPublish: "Manual publish", manualPublish: "Manual publish",
autoPublishAccountCount: "Auto · {count} accounts",
nextRun: "Next run", nextRun: "Next run",
startAt: "Start time", startAt: "Start time",
endAt: "End time", endAt: "End time",
+1 -1
View File
@@ -1300,9 +1300,9 @@ const zhCN = {
name: "任务名称", name: "任务名称",
rule: "关联规则", rule: "关联规则",
cron: "执行频率", cron: "执行频率",
autoPublishPlatforms: "自动发布平台",
autoPublish: "自动发布", autoPublish: "自动发布",
manualPublish: "手动发布", manualPublish: "手动发布",
autoPublishAccountCount: "自动 · {count} 个账号",
nextRun: "下次执行", nextRun: "下次执行",
startAt: "开始时间", startAt: "开始时间",
endAt: "结束时间", endAt: "结束时间",
+1 -1
View File
@@ -161,7 +161,7 @@ const recentTemplateArticles = computed<ArticleListItem[]>(() =>
prompt_rule_id: null, prompt_rule_id: null,
prompt_rule_name: null, prompt_rule_name: null,
generation_mode: article.generation_mode, generation_mode: article.generation_mode,
platforms: [], auto_publish_platforms: [],
generate_status: article.generate_status, generate_status: article.generate_status,
publish_status: article.publish_status, publish_status: article.publish_status,
title: article.title, title: article.title,
+4 -4
View File
@@ -557,7 +557,7 @@ export interface ArticleListItem {
prompt_rule_id: number | null; prompt_rule_id: number | null;
prompt_rule_name: string | null; prompt_rule_name: string | null;
generation_mode: string | null; generation_mode: string | null;
platforms: string[]; auto_publish_platforms: string[];
generate_status: string; generate_status: string;
publish_status: string; publish_status: string;
title: string | null; title: string | null;
@@ -582,7 +582,7 @@ export interface ArticleDetail {
template_id: number | null; template_id: number | null;
template_name: string | null; template_name: string | null;
generation_mode: string | null; generation_mode: string | null;
platforms: string[]; auto_publish_platforms: string[];
cover_asset_url: string | null; cover_asset_url: string | null;
cover_image_asset_id: number | null; cover_image_asset_id: number | null;
generate_status: string; generate_status: string;
@@ -632,7 +632,6 @@ export interface GenerateImitationResponse {
export interface UpdateArticleRequest { export interface UpdateArticleRequest {
title: string; title: string;
markdown_content: string; markdown_content: string;
platforms?: string[];
cover_asset_url?: string | null; cover_asset_url?: string | null;
referenced_image_asset_ids?: number[]; referenced_image_asset_ids?: number[];
cover_image_asset_id?: number | null; cover_image_asset_id?: number | null;
@@ -1448,6 +1447,7 @@ export interface ScheduleTask {
cron_expr: string; cron_expr: string;
auto_publish: boolean; auto_publish: boolean;
publish_account_ids: string[]; publish_account_ids: string[];
auto_publish_platforms: string[];
cover_asset_url: string | null; cover_asset_url: string | null;
cover_image_asset_id: number | null; cover_image_asset_id: number | null;
enable_web_search: boolean; enable_web_search: boolean;
@@ -1534,7 +1534,7 @@ export interface InstantTaskItem {
name: string; name: string;
status: string; status: string;
execution_time: string | null; execution_time: string | null;
platforms: string[]; auto_publish_platforms: string[];
generate_count: number; generate_count: number;
created_at: string; created_at: string;
} }
+12 -6
View File
@@ -115,7 +115,7 @@ type ArticleListItem struct {
PromptRuleID *int64 `json:"prompt_rule_id"` PromptRuleID *int64 `json:"prompt_rule_id"`
PromptRuleName *string `json:"prompt_rule_name"` PromptRuleName *string `json:"prompt_rule_name"`
GenerationMode *string `json:"generation_mode"` GenerationMode *string `json:"generation_mode"`
Platforms []string `json:"platforms"` AutoPublishPlatforms []string `json:"auto_publish_platforms"`
GenerateStatus string `json:"generate_status"` GenerateStatus string `json:"generate_status"`
PublishStatus string `json:"publish_status"` PublishStatus string `json:"publish_status"`
Title *string `json:"title"` Title *string `json:"title"`
@@ -147,7 +147,7 @@ type ArticleDetailResponse struct {
TemplateID *int64 `json:"template_id"` TemplateID *int64 `json:"template_id"`
TemplateName *string `json:"template_name"` TemplateName *string `json:"template_name"`
GenerationMode *string `json:"generation_mode"` GenerationMode *string `json:"generation_mode"`
Platforms []string `json:"platforms"` AutoPublishPlatforms []string `json:"auto_publish_platforms"`
CoverAssetURL *string `json:"cover_asset_url"` CoverAssetURL *string `json:"cover_asset_url"`
CoverImageAssetID *int64 `json:"cover_image_asset_id"` CoverImageAssetID *int64 `json:"cover_image_asset_id"`
GenerateStatus string `json:"generate_status"` GenerateStatus string `json:"generate_status"`
@@ -355,7 +355,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
} }
item.SourceType = dbSourceType item.SourceType = dbSourceType
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON) 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) item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
items = append(items, item) items = append(items, item)
} }
@@ -407,7 +411,11 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
_ = json.Unmarshal(wizardStateJSON, &item.WizardState) _ = json.Unmarshal(wizardStateJSON, &item.WizardState)
} }
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON) 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.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON) item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON) item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON)
@@ -495,7 +503,6 @@ func (s *ArticleService) Create(ctx context.Context, req CreateArticleRequest) (
type UpdateArticleRequest struct { type UpdateArticleRequest struct {
Title string `json:"title"` Title string `json:"title"`
MarkdownContent string `json:"markdown_content"` MarkdownContent string `json:"markdown_content"`
Platforms []string `json:"platforms"`
CoverAssetURL *string `json:"cover_asset_url"` CoverAssetURL *string `json:"cover_asset_url"`
ReferencedImageAssetIDs []int64 `json:"referenced_image_asset_ids"` ReferencedImageAssetIDs []int64 `json:"referenced_image_asset_ids"`
CoverImageAssetID NullableInt64Input `json:"cover_image_asset_id"` 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( nextWizardStateJSON, err := mergeArticleWizardState(
wizardStateJSON, wizardStateJSON,
title, title,
req.Platforms,
req.CoverAssetURL, req.CoverAssetURL,
resolvedCoverImageAssetInput, resolvedCoverImageAssetInput,
) )
@@ -31,16 +31,16 @@ type InstantTaskListParams struct {
} }
type InstantTaskResponse struct { type InstantTaskResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ArticleID *int64 `json:"article_id"` ArticleID *int64 `json:"article_id"`
PromptRuleID *int64 `json:"prompt_rule_id"` PromptRuleID *int64 `json:"prompt_rule_id"`
PromptRuleName *string `json:"prompt_rule_name"` PromptRuleName *string `json:"prompt_rule_name"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
ExecutionTime *string `json:"execution_time"` ExecutionTime *string `json:"execution_time"`
Platforms []string `json:"platforms"` AutoPublishPlatforms []string `json:"auto_publish_platforms"`
GenerateCount int `json:"generate_count"` GenerateCount int `json:"generate_count"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
} }
type InstantTaskListResponse struct { type InstantTaskListResponse struct {
@@ -123,7 +123,11 @@ func (s *InstantTaskService) List(ctx context.Context, params InstantTaskListPar
item.ExecutionTime = stringPtrFromDBValue(executionAt) item.ExecutionTime = stringPtrFromDBValue(executionAt)
item.CreatedAt = fmt.Sprintf("%v", createdAt) 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 item.GenerateCount = 1
if len(inputParamsJSON) > 0 { if len(inputParamsJSON) > 0 {
@@ -343,7 +343,7 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
coverImageAssetID.Set = true coverImageAssetID.Set = true
coverImageAssetID.Value = &rawCoverImageAssetID coverImageAssetID.Value = &rawCoverImageAssetID
} }
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, nil, coverAssetURL, coverImageAssetID) wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, coverAssetURL, coverImageAssetID)
tx, err := s.pool.Begin(ctx) tx, err := s.pool.Begin(ctx)
if err != nil { if err != nil {
+71 -71
View File
@@ -1,9 +1,12 @@
package app package app
import ( import (
"context"
"encoding/json" "encoding/json"
"strconv" "strconv"
"strings" "strings"
"github.com/jackc/pgx/v5"
) )
func normalizePlatformIDs(platformIDs []string) []string { func normalizePlatformIDs(platformIDs []string) []string {
@@ -31,52 +34,79 @@ func normalizePlatformIDs(platformIDs []string) []string {
return normalized return normalized
} }
func parsePlatformIDs(value string) []string { type scheduleAutoPublishPlatformQuerier interface {
if strings.TrimSpace(value) == "" { Query(context.Context, string, ...interface{}) (pgx.Rows, error)
return nil
}
return normalizePlatformIDs(strings.Split(value, ","))
} }
func serializePlatformIDs(platformIDs []string) string { func loadScheduleAutoPublishPlatforms(
normalized := normalizePlatformIDs(platformIDs) ctx context.Context,
if len(normalized) == 0 { db scheduleAutoPublishPlatformQuerier,
return "" tenantID int64,
} inputParamsJSON []byte,
return strings.Join(normalized, ",") ) ([]string, error) {
} if db == nil || tenantID <= 0 || len(inputParamsJSON) == 0 {
return []string{}, nil
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
} }
var payload map[string]interface{} var payload map[string]interface{}
if err := json.Unmarshal(raw, &payload); err != nil { if err := json.Unmarshal(inputParamsJSON, &payload); err != nil {
return nil return []string{}, nil
}
if !extractBool(payload, "schedule_auto_publish") {
return []string{}, nil
} }
if platforms := normalizePlatformsFromValue(payload["platforms"]); len(platforms) > 0 { accountIDs := normalizeSchedulePublishAccountIDs(extractStringList(payload["schedule_publish_account_ids"], 64))
return platforms if len(accountIDs) == 0 {
} return []string{}, nil
if platforms := normalizePlatformsFromValue(payload["target_platforms"]); len(platforms) > 0 {
return platforms
} }
if targetPlatform, ok := payload["target_platform"].(string); ok { return loadPublishAccountPlatformsByIDs(ctx, db, tenantID, accountIDs)
return parsePlatformIDs(targetPlatform) }
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 { func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
@@ -146,51 +176,21 @@ func extractInt64FromJSONPayload(raw []byte, key string) (int64, bool) {
} }
} }
func normalizePlatformsFromValue(raw interface{}) []string { func mergeArticleWizardState(raw []byte, title string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
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) {
state := make(map[string]interface{}) state := make(map[string]interface{})
if len(raw) > 0 { if len(raw) > 0 {
_ = json.Unmarshal(raw, &state) _ = json.Unmarshal(raw, &state)
} }
delete(state, "platforms")
delete(state, "target_platforms")
delete(state, "target_platform")
trimmedTitle := strings.TrimSpace(title) trimmedTitle := strings.TrimSpace(title)
if trimmedTitle != "" { if trimmedTitle != "" {
state["title"] = 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 { if coverAssetURL != nil {
trimmedCoverURL := strings.TrimSpace(*coverAssetURL) trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
if trimmedCoverURL == "" { if trimmedCoverURL == "" {
@@ -54,26 +54,27 @@ type ScheduleTaskRequest struct {
} }
type ScheduleTaskResponse struct { type ScheduleTaskResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
WorkspaceID *int64 `json:"workspace_id"` WorkspaceID *int64 `json:"workspace_id"`
PromptRuleID int64 `json:"prompt_rule_id"` PromptRuleID int64 `json:"prompt_rule_id"`
PromptRuleName *string `json:"prompt_rule_name"` PromptRuleName *string `json:"prompt_rule_name"`
BrandID *int64 `json:"brand_id"` BrandID *int64 `json:"brand_id"`
BrandName *string `json:"brand_name"` BrandName *string `json:"brand_name"`
Name string `json:"name"` Name string `json:"name"`
CronExpr string `json:"cron_expr"` CronExpr string `json:"cron_expr"`
AutoPublish bool `json:"auto_publish"` AutoPublish bool `json:"auto_publish"`
PublishAccountIDs []string `json:"publish_account_ids"` PublishAccountIDs []string `json:"publish_account_ids"`
CoverAssetURL *string `json:"cover_asset_url"` AutoPublishPlatforms []string `json:"auto_publish_platforms"`
CoverImageAssetID *int64 `json:"cover_image_asset_id"` CoverAssetURL *string `json:"cover_asset_url"`
EnableWebSearch bool `json:"enable_web_search"` CoverImageAssetID *int64 `json:"cover_image_asset_id"`
GenerateCount int `json:"generate_count"` EnableWebSearch bool `json:"enable_web_search"`
StartAt *string `json:"start_at"` GenerateCount int `json:"generate_count"`
EndAt *string `json:"end_at"` StartAt *string `json:"start_at"`
NextRunAt *string `json:"next_run_at"` EndAt *string `json:"end_at"`
Status string `json:"status"` NextRunAt *string `json:"next_run_at"`
CreatedAt string `json:"created_at"` Status string `json:"status"`
UpdatedAt string `json:"updated_at"` CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} }
type ScheduleTaskListParams struct { type ScheduleTaskListParams struct {
@@ -206,7 +207,7 @@ func (s *ScheduleTaskService) Create(ctx context.Context, req ScheduleTaskReques
return &ScheduleTaskResponse{ return &ScheduleTaskResponse{
ID: id, WorkspaceID: &workspaceID, PromptRuleID: req.PromptRuleID, BrandID: req.BrandID, ID: id, WorkspaceID: &workspaceID, PromptRuleID: req.PromptRuleID, BrandID: req.BrandID,
Name: req.Name, CronExpr: req.CronExpr, AutoPublish: publishConfig.AutoPublish, 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, CoverImageAssetID: publishConfig.CoverImageAssetID, EnableWebSearch: enableWebSearch,
GenerateCount: generateCount, GenerateCount: generateCount,
Status: status, CreatedAt: fmt.Sprintf("%v", createdAt), Status: status, CreatedAt: fmt.Sprintf("%v", createdAt),
@@ -400,6 +401,9 @@ func (s *ScheduleTaskService) loadScheduleTasks(ctx context.Context, tenantID in
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
return nil, err
}
items = append(items, item) items = append(items, item)
} }
return &ScheduleTaskListResponse{Items: items, Total: total}, nil return &ScheduleTaskListResponse{Items: items, Total: total}, nil
@@ -425,9 +429,24 @@ func (s *ScheduleTaskService) loadScheduleTaskDetail(ctx context.Context, tenant
} }
return nil, false, err return nil, false, err
} }
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
return nil, false, err
}
return &item, true, nil 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 { func scanScheduleTaskResponse(scanner interface {
Scan(dest ...interface{}) error Scan(dest ...interface{}) error
}) (ScheduleTaskResponse, error) { }) (ScheduleTaskResponse, error) {
@@ -480,6 +499,7 @@ func (s *ScheduleTaskService) ensurePromptRuleExists(ctx context.Context, tenant
type scheduleAutoPublishConfig struct { type scheduleAutoPublishConfig struct {
AutoPublish bool AutoPublish bool
AccountIDs []string AccountIDs []string
PlatformIDs []string
AccountIDsJSON []byte AccountIDsJSON []byte
CoverAssetURL *string CoverAssetURL *string
CoverImageAssetID *int64 CoverImageAssetID *int64
@@ -514,6 +534,7 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
if err != nil { if err != nil {
return config, err return config, err
} }
config.PlatformIDs = normalizePlatformIDs(platformIDs)
if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" { if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" {
return config, response.ErrBadRequest(40025, "cover_required", "selected publish accounts require cover_asset_url") return config, response.ErrBadRequest(40025, "cover_required", "selected publish accounts require cover_asset_url")
} }
@@ -0,0 +1 @@
-- Legacy article target platform JSON keys are intentionally not restored.
@@ -0,0 +1,11 @@
UPDATE articles
SET wizard_state_json = wizard_state_json - 'platforms' - 'target_platforms' - 'target_platform',
updated_at = NOW()
WHERE wizard_state_json IS NOT NULL
AND wizard_state_json ?| ARRAY['platforms', 'target_platforms', 'target_platform'];
UPDATE generation_tasks
SET input_params_json = input_params_json - 'platforms' - 'target_platforms' - 'target_platform',
updated_at = NOW()
WHERE input_params_json IS NOT NULL
AND input_params_json ?| ARRAY['platforms', 'target_platforms', 'target_platform'];