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
+71 -71
View File
@@ -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 == "" {