feat(schedule): support random cover source for scheduled and manual publish
Add a cover mode to schedule tasks so auto-publish can pick a random cover image instead of a fixed one, scoped to all images or a specific folder. - migration: add cover_mode / cover_random_scope / cover_random_folder_id columns and check constraints on schedule_tasks - backend: validate cover mode/scope/folder on create & update; the dispatch worker resolves a random active image (signed asset URL) at enqueue time - frontend: new CoverSourceSelector component wired into GenerateTaskDrawer and PublishArticleModal, plus coverSource i18n strings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/publicasset"
|
||||
sharedschedule "github.com/geo-platform/tenant-api/internal/shared/schedule"
|
||||
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
)
|
||||
@@ -60,6 +61,9 @@ type dueScheduleTask struct {
|
||||
PublishEnterpriseSiteTargets []tenantapp.ScheduleEnterpriseSiteTarget
|
||||
CoverAssetURL *string
|
||||
CoverImageAssetID *int64
|
||||
CoverMode string
|
||||
CoverRandomScope string
|
||||
CoverRandomFolderID *int64
|
||||
EnableWebSearch bool
|
||||
GenerateCount int
|
||||
StartAt *time.Time
|
||||
@@ -378,6 +382,7 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
|
||||
subscription_prompt_id, brand_id, name, cron_expr, schedule_days, schedule_time_mode,
|
||||
random_window_start::text, random_window_end::text, generation_input_json,
|
||||
auto_publish, publish_account_ids, publish_enterprise_site_targets, cover_asset_url, cover_image_asset_id,
|
||||
cover_mode, cover_random_scope, cover_random_folder_id,
|
||||
enable_web_search, generate_count, start_at, end_at, next_run_at
|
||||
FROM schedule_tasks
|
||||
WHERE deleted_at IS NULL
|
||||
@@ -410,6 +415,7 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
|
||||
scheduleDaysJSON []byte
|
||||
publishAccountIDsJSON []byte
|
||||
publishEnterpriseSiteTargetsJSON []byte
|
||||
coverRandomFolderID pgtype.Int8
|
||||
startAt pgtype.Timestamptz
|
||||
endAt pgtype.Timestamptz
|
||||
nextRunAt pgtype.Timestamptz
|
||||
@@ -435,6 +441,9 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
|
||||
&publishEnterpriseSiteTargetsJSON,
|
||||
&task.CoverAssetURL,
|
||||
&task.CoverImageAssetID,
|
||||
&task.CoverMode,
|
||||
&task.CoverRandomScope,
|
||||
&coverRandomFolderID,
|
||||
&task.EnableWebSearch,
|
||||
&task.GenerateCount,
|
||||
&startAt,
|
||||
@@ -460,6 +469,13 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
|
||||
}
|
||||
task.PublishAccountIDs = decodeDueSchedulePublishAccountIDs(publishAccountIDsJSON)
|
||||
task.PublishEnterpriseSiteTargets = tenantapp.DecodeScheduleEnterpriseSiteTargets(publishEnterpriseSiteTargetsJSON)
|
||||
task.CoverRandomFolderID = int64PtrFromInt8(coverRandomFolderID)
|
||||
if task.CoverMode == "" {
|
||||
task.CoverMode = tenantapp.ScheduleCoverModeSpecific
|
||||
}
|
||||
if task.CoverRandomScope == "" {
|
||||
task.CoverRandomScope = tenantapp.ScheduleCoverRandomScopeAll
|
||||
}
|
||||
if task.GenerateCount <= 0 {
|
||||
task.GenerateCount = 1
|
||||
}
|
||||
@@ -587,6 +603,10 @@ func (w *ScheduleDispatchWorker) dispatch(parent context.Context, task dueSchedu
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) enqueueDueGeneration(ctx context.Context, task dueScheduleTask, generateCount int) (*tenantapp.GenerateFromRuleResponse, error) {
|
||||
coverAssetURL, coverImageAssetID, err := w.resolveDueScheduleCover(ctx, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch task.TargetType {
|
||||
case "", tenantapp.ScheduleTargetPromptRule:
|
||||
if task.PromptRuleID == nil || *task.PromptRuleID <= 0 {
|
||||
@@ -603,8 +623,8 @@ func (w *ScheduleDispatchWorker) enqueueDueGeneration(ctx context.Context, task
|
||||
AutoPublish: task.AutoPublish,
|
||||
PublishAccountIDs: task.PublishAccountIDs,
|
||||
PublishEnterpriseSiteTargets: task.PublishEnterpriseSiteTargets,
|
||||
CoverAssetURL: task.CoverAssetURL,
|
||||
CoverImageAssetID: task.CoverImageAssetID,
|
||||
CoverAssetURL: coverAssetURL,
|
||||
CoverImageAssetID: coverImageAssetID,
|
||||
EnableWebSearch: task.EnableWebSearch,
|
||||
GenerateCount: generateCount,
|
||||
ScheduledFor: task.ScheduledFor,
|
||||
@@ -631,8 +651,8 @@ func (w *ScheduleDispatchWorker) enqueueDueGeneration(ctx context.Context, task
|
||||
AutoPublish: task.AutoPublish,
|
||||
PublishAccountIDs: task.PublishAccountIDs,
|
||||
PublishEnterpriseSiteTargets: task.PublishEnterpriseSiteTargets,
|
||||
CoverAssetURL: task.CoverAssetURL,
|
||||
CoverImageAssetID: task.CoverImageAssetID,
|
||||
CoverAssetURL: coverAssetURL,
|
||||
CoverImageAssetID: coverImageAssetID,
|
||||
GenerateCount: generateCount,
|
||||
ScheduledFor: task.ScheduledFor,
|
||||
})
|
||||
@@ -648,6 +668,82 @@ func (w *ScheduleDispatchWorker) enqueueDueGeneration(ctx context.Context, task
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) resolveDueScheduleCover(ctx context.Context, task dueScheduleTask) (*string, *int64, error) {
|
||||
if !task.AutoPublish {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if task.CoverMode != tenantapp.ScheduleCoverModeRandom {
|
||||
return task.CoverAssetURL, task.CoverImageAssetID, nil
|
||||
}
|
||||
image, err := w.pickRandomScheduleCoverImage(ctx, task)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
coverURL := buildScheduleCoverAssetURL(image.ObjectKey, w.assetTokenSecret())
|
||||
return &coverURL, &image.ID, nil
|
||||
}
|
||||
|
||||
type scheduleCoverImage struct {
|
||||
ID int64
|
||||
ObjectKey string
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) pickRandomScheduleCoverImage(ctx context.Context, task dueScheduleTask) (scheduleCoverImage, error) {
|
||||
if w == nil || w.pool == nil {
|
||||
return scheduleCoverImage{}, errors.New("schedule random cover lookup unavailable")
|
||||
}
|
||||
|
||||
var row pgxRow
|
||||
if task.CoverRandomScope == tenantapp.ScheduleCoverRandomScopeFolder {
|
||||
if task.CoverRandomFolderID == nil || *task.CoverRandomFolderID <= 0 {
|
||||
return scheduleCoverImage{}, errors.New("schedule random cover folder missing")
|
||||
}
|
||||
row = w.pool.QueryRow(ctx, `
|
||||
SELECT id, object_key
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND folder_id = $2
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY random()
|
||||
LIMIT 1
|
||||
`, task.TenantID, *task.CoverRandomFolderID)
|
||||
} else {
|
||||
row = w.pool.QueryRow(ctx, `
|
||||
SELECT id, object_key
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY random()
|
||||
LIMIT 1
|
||||
`, task.TenantID)
|
||||
}
|
||||
|
||||
var image scheduleCoverImage
|
||||
if err := row.Scan(&image.ID, &image.ObjectKey); err != nil {
|
||||
return scheduleCoverImage{}, errors.New("schedule random cover image not found")
|
||||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
type pgxRow interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) assetTokenSecret() string {
|
||||
if w != nil && w.configProvider != nil {
|
||||
if cfg := w.configProvider.Current(); cfg != nil {
|
||||
return strings.TrimSpace(cfg.JWT.Secret)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildScheduleCoverAssetURL(objectKey string, secret string) string {
|
||||
return "/api/public/assets/" + publicasset.SignObjectKey(objectKey, secret)
|
||||
}
|
||||
|
||||
func (w *ScheduleDispatchWorker) recordDispatchResult(ctx context.Context, task dueScheduleTask, generateCount int, failures []error) {
|
||||
if w == nil || w.pool == nil {
|
||||
return
|
||||
|
||||
@@ -14,6 +14,10 @@ import (
|
||||
const (
|
||||
ScheduleTargetPromptRule = "prompt_rule"
|
||||
ScheduleTargetKolSubscriptionPrompt = "kol_subscription_prompt"
|
||||
ScheduleCoverModeSpecific = "specific"
|
||||
ScheduleCoverModeRandom = "random"
|
||||
ScheduleCoverRandomScopeAll = "all"
|
||||
ScheduleCoverRandomScopeFolder = "folder"
|
||||
|
||||
scheduleKindDaily = "daily"
|
||||
scheduleKindWeekly = "weekly"
|
||||
|
||||
@@ -58,6 +58,9 @@ type ScheduleTaskRequest struct {
|
||||
PublishEnterpriseSiteTargets []ScheduleEnterpriseSiteTarget `json:"publish_enterprise_site_targets"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||
CoverMode string `json:"cover_mode"`
|
||||
CoverRandomScope string `json:"cover_random_scope"`
|
||||
CoverRandomFolderID *int64 `json:"cover_random_folder_id"`
|
||||
EnableWebSearch *bool `json:"enable_web_search"`
|
||||
GenerateCount *int `json:"generate_count"`
|
||||
StartAt *string `json:"start_at"`
|
||||
@@ -90,6 +93,9 @@ type ScheduleTaskResponse struct {
|
||||
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||
CoverMode string `json:"cover_mode"`
|
||||
CoverRandomScope string `json:"cover_random_scope"`
|
||||
CoverRandomFolderID *int64 `json:"cover_random_folder_id"`
|
||||
EnableWebSearch bool `json:"enable_web_search"`
|
||||
GenerateCount int `json:"generate_count"`
|
||||
StartAt *string `json:"start_at"`
|
||||
@@ -212,21 +218,27 @@ func (s *ScheduleTaskService) Create(ctx context.Context, req ScheduleTaskReques
|
||||
brand_id, name, cron_expr, schedule_kind, schedule_days, schedule_time_mode,
|
||||
random_window_start, random_window_end, generation_input_json,
|
||||
enable_web_search, generate_count, auto_publish, publish_account_ids,
|
||||
publish_enterprise_site_targets, cover_asset_url, cover_image_asset_id, start_at, end_at
|
||||
publish_enterprise_site_targets, cover_asset_url, cover_image_asset_id,
|
||||
cover_mode, cover_random_scope, cover_random_folder_id,
|
||||
start_at, end_at
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11::jsonb, $12,
|
||||
$13::time, $14::time, $15::jsonb,
|
||||
$16, $17, $18, $19::jsonb,
|
||||
$20::jsonb, $21, $22, $23::timestamptz, $24::timestamptz
|
||||
$20::jsonb, $21, $22,
|
||||
$23, $24, $25,
|
||||
$26::timestamptz, $27::timestamptz
|
||||
)
|
||||
RETURNING id, created_at, start_at, end_at, status
|
||||
`, actor.TenantID, actor.PrimaryWorkspaceID, actor.UserID, targetType, promptRuleID, subscriptionPromptID,
|
||||
req.BrandID, req.Name, plan.CronExpr, plan.ScheduleKind, encodeScheduleDayKeys(plan.ScheduleDays), plan.ScheduleTimeMode,
|
||||
plan.RandomWindowStart, plan.RandomWindowEnd, encodeScheduleGenerationInput(generationInput),
|
||||
enableWebSearch, generateCount, publishConfig.AutoPublish, publishConfig.AccountIDsJSON,
|
||||
publishConfig.EnterpriseTargetsJSON, publishConfig.CoverAssetURL, publishConfig.CoverImageAssetID, req.StartAt, req.EndAt).
|
||||
publishConfig.EnterpriseTargetsJSON, publishConfig.CoverAssetURL, publishConfig.CoverImageAssetID,
|
||||
publishConfig.CoverMode, publishConfig.CoverRandomScope, publishConfig.CoverRandomFolderID,
|
||||
req.StartAt, req.EndAt).
|
||||
Scan(&id, &createdAt, &startAt, &endAt, &status)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "create_failed", "failed to create schedule task")
|
||||
@@ -286,9 +298,11 @@ func (s *ScheduleTaskService) Create(ctx context.Context, req ScheduleTaskReques
|
||||
RandomWindowEnd: plan.RandomWindowEnd, GenerationInput: generationInput, AutoPublish: publishConfig.AutoPublish,
|
||||
PublishAccountIDs: publishConfig.AccountIDs, PublishEnterpriseSiteTargets: publishConfig.EnterpriseTargets,
|
||||
AutoPublishPlatforms: publishConfig.PlatformIDs, CoverAssetURL: publishConfig.CoverAssetURL,
|
||||
CoverImageAssetID: publishConfig.CoverImageAssetID, EnableWebSearch: enableWebSearch,
|
||||
GenerateCount: generateCount,
|
||||
Status: status, CreatedAt: timeStringFromDBValue(createdAt),
|
||||
CoverImageAssetID: publishConfig.CoverImageAssetID, CoverMode: publishConfig.CoverMode,
|
||||
CoverRandomScope: publishConfig.CoverRandomScope, CoverRandomFolderID: publishConfig.CoverRandomFolderID,
|
||||
EnableWebSearch: enableWebSearch,
|
||||
GenerateCount: generateCount,
|
||||
Status: status, CreatedAt: timeStringFromDBValue(createdAt),
|
||||
NextRunAt: timeStringPtr(nextRunAt),
|
||||
}, nil
|
||||
}
|
||||
@@ -350,17 +364,19 @@ func (s *ScheduleTaskService) Update(ctx context.Context, id int64, req Schedule
|
||||
workspace_id = $15, auto_publish = $16, publish_account_ids = $17::jsonb,
|
||||
publish_enterprise_site_targets = $18::jsonb,
|
||||
cover_asset_url = $19, cover_image_asset_id = $20,
|
||||
start_at = $21::timestamptz, end_at = $22::timestamptz, operator_id = $23,
|
||||
cover_mode = $21, cover_random_scope = $22, cover_random_folder_id = $23,
|
||||
start_at = $24::timestamptz, end_at = $25::timestamptz, operator_id = $26,
|
||||
last_dispatch_status = NULL, last_dispatch_error = NULL,
|
||||
consecutive_failures = 0,
|
||||
updated_at = NOW()
|
||||
WHERE id = $24 AND tenant_id = $25 AND brand_id = $26 AND deleted_at IS NULL
|
||||
WHERE id = $27 AND tenant_id = $28 AND brand_id = $29 AND deleted_at IS NULL
|
||||
RETURNING status, start_at, end_at
|
||||
`, targetType, promptRuleID, subscriptionPromptID, req.BrandID, req.Name, plan.CronExpr,
|
||||
plan.ScheduleKind, encodeScheduleDayKeys(plan.ScheduleDays), plan.ScheduleTimeMode,
|
||||
plan.RandomWindowStart, plan.RandomWindowEnd, encodeScheduleGenerationInput(generationInput),
|
||||
enableWebSearch, generateCount, actor.PrimaryWorkspaceID, publishConfig.AutoPublish,
|
||||
publishConfig.AccountIDsJSON, publishConfig.EnterpriseTargetsJSON, publishConfig.CoverAssetURL, publishConfig.CoverImageAssetID,
|
||||
publishConfig.CoverMode, publishConfig.CoverRandomScope, publishConfig.CoverRandomFolderID,
|
||||
req.StartAt, req.EndAt, actor.UserID, id, actor.TenantID, brandID).
|
||||
Scan(&status, &startAt, &endAt)
|
||||
if err != nil {
|
||||
@@ -503,7 +519,9 @@ func (s *ScheduleTaskService) loadScheduleTasks(ctx context.Context, tenantID, b
|
||||
st.cron_expr, st.schedule_kind, st.schedule_days, st.schedule_time_mode,
|
||||
st.random_window_start::text, st.random_window_end::text, st.generation_input_json,
|
||||
st.auto_publish, st.publish_account_ids, st.publish_enterprise_site_targets,
|
||||
st.cover_asset_url, st.cover_image_asset_id, st.enable_web_search, st.generate_count, st.start_at, st.end_at,
|
||||
st.cover_asset_url, st.cover_image_asset_id,
|
||||
st.cover_mode, st.cover_random_scope, st.cover_random_folder_id,
|
||||
st.enable_web_search, st.generate_count, st.start_at, st.end_at,
|
||||
st.next_run_at, st.status, st.last_run_at, st.last_dispatch_status,
|
||||
st.last_dispatch_error, st.consecutive_failures, st.created_at, st.updated_at,
|
||||
pr.name AS prompt_rule_name,
|
||||
@@ -558,7 +576,9 @@ func (s *ScheduleTaskService) loadScheduleTaskDetail(ctx context.Context, tenant
|
||||
st.cron_expr, st.schedule_kind, st.schedule_days, st.schedule_time_mode,
|
||||
st.random_window_start::text, st.random_window_end::text, st.generation_input_json,
|
||||
st.auto_publish, st.publish_account_ids, st.publish_enterprise_site_targets,
|
||||
st.cover_asset_url, st.cover_image_asset_id, st.enable_web_search, st.generate_count, st.start_at, st.end_at,
|
||||
st.cover_asset_url, st.cover_image_asset_id,
|
||||
st.cover_mode, st.cover_random_scope, st.cover_random_folder_id,
|
||||
st.enable_web_search, st.generate_count, st.start_at, st.end_at,
|
||||
st.next_run_at, st.status, st.last_run_at, st.last_dispatch_status,
|
||||
st.last_dispatch_error, st.consecutive_failures, st.created_at, st.updated_at,
|
||||
pr.name AS prompt_rule_name,
|
||||
@@ -936,7 +956,9 @@ func scanScheduleTaskResponse(scanner interface {
|
||||
&item.CronExpr, &item.ScheduleKind, &scheduleDaysJSON, &item.ScheduleTimeMode,
|
||||
&item.RandomWindowStart, &item.RandomWindowEnd, &generationInputJSON,
|
||||
&item.AutoPublish, &publishAccountIDsJSON, &publishEnterpriseSiteTargetsJSON,
|
||||
&item.CoverAssetURL, &item.CoverImageAssetID, &item.EnableWebSearch, &item.GenerateCount, &startAt, &endAt,
|
||||
&item.CoverAssetURL, &item.CoverImageAssetID,
|
||||
&item.CoverMode, &item.CoverRandomScope, &item.CoverRandomFolderID,
|
||||
&item.EnableWebSearch, &item.GenerateCount, &startAt, &endAt,
|
||||
&nextRunAt, &item.Status, &lastRunAt, &item.LastDispatchStatus,
|
||||
&item.LastDispatchError, &item.ConsecutiveFailures, &createdAt, &updatedAt,
|
||||
&item.PromptRuleName, &item.SubscriptionPromptName, &item.SubscriptionPackageName,
|
||||
@@ -961,6 +983,12 @@ func scanScheduleTaskResponse(scanner interface {
|
||||
if item.RandomWindowEnd == "" {
|
||||
item.RandomWindowEnd = defaultRandomWindowEnd
|
||||
}
|
||||
if item.CoverMode == "" {
|
||||
item.CoverMode = ScheduleCoverModeSpecific
|
||||
}
|
||||
if item.CoverRandomScope == "" {
|
||||
item.CoverRandomScope = ScheduleCoverRandomScopeAll
|
||||
}
|
||||
item.ScheduleDays = decodeScheduleDayKeys(scheduleDaysJSON)
|
||||
item.GenerationInput = decodeScheduleGenerationInput(generationInputJSON)
|
||||
item.PublishAccountIDs = decodeSchedulePublishAccountIDs(publishAccountIDsJSON)
|
||||
@@ -1075,6 +1103,9 @@ type scheduleAutoPublishConfig struct {
|
||||
EnterpriseTargetsJSON []byte
|
||||
CoverAssetURL *string
|
||||
CoverImageAssetID *int64
|
||||
CoverMode string
|
||||
CoverRandomScope string
|
||||
CoverRandomFolderID *int64
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, actor auth.Actor, req ScheduleTaskRequest) (scheduleAutoPublishConfig, error) {
|
||||
@@ -1086,6 +1117,8 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
AutoPublish: autoPublish,
|
||||
AccountIDs: accountIDs,
|
||||
EnterpriseTargets: enterpriseTargets,
|
||||
CoverMode: ScheduleCoverModeSpecific,
|
||||
CoverRandomScope: ScheduleCoverRandomScopeAll,
|
||||
}
|
||||
|
||||
if !autoPublish {
|
||||
@@ -1105,6 +1138,18 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
if err := validateOptionalPositiveInt64(req.CoverImageAssetID, "cover_image_asset_id"); err != nil {
|
||||
return config, err
|
||||
}
|
||||
coverMode, err := normalizeScheduleCoverMode(req.CoverMode)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
randomScope := ScheduleCoverRandomScopeAll
|
||||
var randomFolderID *int64
|
||||
if coverMode == ScheduleCoverModeRandom {
|
||||
randomScope, randomFolderID, err = s.normalizeScheduleCoverRandomSource(ctx, actor.TenantID, req.CoverRandomScope, req.CoverRandomFolderID)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
}
|
||||
coverURL := strings.TrimSpace(stringPointerValue(req.CoverAssetURL))
|
||||
if len(accountIDs) > 0 {
|
||||
platformIDs, err := s.loadPublishAccountPlatformIDs(ctx, actor.TenantID, actor.PrimaryWorkspaceID, accountIDs)
|
||||
@@ -1112,7 +1157,7 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
return config, err
|
||||
}
|
||||
config.PlatformIDs = normalizePlatformIDs(platformIDs)
|
||||
if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" {
|
||||
if schedulePublishPlatformsRequireCover(platformIDs) && coverMode == ScheduleCoverModeSpecific && coverURL == "" {
|
||||
return config, response.ErrBadRequest(40025, "cover_required", "已选媒体账号的平台要求封面图,请先设置封面")
|
||||
}
|
||||
}
|
||||
@@ -1133,13 +1178,67 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
|
||||
config.AccountIDsJSON = accountIDsJSON
|
||||
config.EnterpriseTargetsJSON = enterpriseTargetsJSON
|
||||
if coverURL != "" {
|
||||
config.CoverMode = coverMode
|
||||
config.CoverRandomScope = randomScope
|
||||
config.CoverRandomFolderID = randomFolderID
|
||||
if coverMode == ScheduleCoverModeSpecific && coverURL != "" {
|
||||
config.CoverAssetURL = &coverURL
|
||||
config.CoverImageAssetID = req.CoverImageAssetID
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func normalizeScheduleCoverMode(value string) (string, error) {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "", ScheduleCoverModeSpecific:
|
||||
return ScheduleCoverModeSpecific, nil
|
||||
case ScheduleCoverModeRandom:
|
||||
return ScheduleCoverModeRandom, nil
|
||||
default:
|
||||
return "", response.ErrBadRequest(40076, "invalid_cover_mode", "封面图模式不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Context, tenantID int64, scope string, folderID *int64) (string, *int64, error) {
|
||||
normalizedScope := strings.TrimSpace(scope)
|
||||
if normalizedScope == "" {
|
||||
normalizedScope = ScheduleCoverRandomScopeAll
|
||||
}
|
||||
switch normalizedScope {
|
||||
case ScheduleCoverRandomScopeAll:
|
||||
return ScheduleCoverRandomScopeAll, nil, nil
|
||||
case ScheduleCoverRandomScopeFolder:
|
||||
if folderID == nil || *folderID <= 0 {
|
||||
return "", nil, response.ErrBadRequest(40077, "cover_random_folder_required", "请选择随机封面图片文件夹")
|
||||
}
|
||||
if err := s.ensureImageFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return ScheduleCoverRandomScopeFolder, folderID, nil
|
||||
default:
|
||||
return "", nil, response.ErrBadRequest(40078, "invalid_cover_random_scope", "随机封面范围不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM image_folders
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
)
|
||||
`, folderID, tenantID).Scan(&exists); err != nil {
|
||||
return response.ErrInternal(50010, "query_failed", "failed to validate image folder")
|
||||
}
|
||||
if !exists {
|
||||
return response.ErrBadRequest(40079, "image_folder_not_found", "随机封面图片文件夹不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) validateScheduleEnterpriseSiteTargets(ctx context.Context, tenantID, workspaceID int64, targets []ScheduleEnterpriseSiteTarget) error {
|
||||
for _, target := range targets {
|
||||
var status string
|
||||
|
||||
@@ -23,6 +23,9 @@ type ScheduleTask struct {
|
||||
PublishAccountIDs []string
|
||||
CoverAssetURL *string
|
||||
CoverImageAssetID *int64
|
||||
CoverMode string
|
||||
CoverRandomScope string
|
||||
CoverRandomFolderID *int64
|
||||
EnableWebSearch bool
|
||||
GenerateCount int
|
||||
StartAt *time.Time
|
||||
|
||||
Reference in New Issue
Block a user