feat: enhance schedule publish account handling and caching mechanisms
Frontend CI / Frontend (push) Successful in 3m1s
Backend CI / Backend (push) Failing after 6m51s

This commit is contained in:
2026-06-21 09:11:37 +08:00
parent c1b06dab18
commit 37d75597f9
6 changed files with 125 additions and 54 deletions
@@ -323,6 +323,12 @@ watch(
{ immediate: true }, { immediate: true },
) )
watch(
schedulePublishAccounts,
() => syncSchedulePublishAccountSelection(),
{ immediate: true },
)
watch(currentBrandId, () => { watch(currentBrandId, () => {
form.promptRuleId = undefined form.promptRuleId = undefined
}) })
@@ -495,6 +501,7 @@ function hydrateForm(): void {
normalizeClock(task.random_window_start, randomDefaultScheduleTime())) normalizeClock(task.random_window_start, randomDefaultScheduleTime()))
: randomDefaultScheduleTime() : randomDefaultScheduleTime()
selectDefaultSubscriptionPrompt() selectDefaultSubscriptionPrompt()
syncSchedulePublishAccountSelection()
} }
function selectDefaultSubscriptionPrompt(): void { function selectDefaultSubscriptionPrompt(): void {
@@ -518,7 +525,9 @@ function buildSchedulePayload() {
const coverMode = coverEnabled ? form.coverMode : 'specific' const coverMode = coverEnabled ? form.coverMode : 'specific'
const coverAssetUrl = coverEnabled && coverMode === 'specific' ? form.coverAssetUrl.trim() : '' const coverAssetUrl = coverEnabled && coverMode === 'specific' ? form.coverAssetUrl.trim() : ''
const publishAccountIds = const publishAccountIds =
form.autoPublish && form.publishTargetTab === 'media_accounts' ? form.publishAccountIds : [] form.autoPublish && form.publishTargetTab === 'media_accounts'
? currentSchedulePublishAccountIds()
: []
const publishEnterpriseSiteTargets = const publishEnterpriseSiteTargets =
form.autoPublish && form.publishTargetTab === 'enterprise_sites' form.autoPublish && form.publishTargetTab === 'enterprise_sites'
? buildScheduleEnterpriseSiteTargets() ? buildScheduleEnterpriseSiteTargets()
@@ -610,6 +619,25 @@ function canToggleSchedulePublishAccount(account: PublishAccountCard): boolean {
return account.selectable || isSchedulePublishAccountSelected(account.id) return account.selectable || isSchedulePublishAccountSelected(account.id)
} }
function currentSchedulePublishAccountIds(): string[] {
return filterExistingSchedulePublishAccountIds(form.publishAccountIds, schedulePublishAccounts.value)
}
function syncSchedulePublishAccountSelection(): void {
if (accountsQuery.isPending.value || accountsQuery.isFetching.value) {
return
}
form.publishAccountIds = currentSchedulePublishAccountIds()
}
function filterExistingSchedulePublishAccountIds(
accountIds: string[],
accounts: PublishAccountCard[],
): string[] {
const existingIds = new Set(accounts.map((account) => account.id))
return accountIds.filter((id) => existingIds.has(id))
}
function buildScheduleEnterpriseSiteTargets(): ScheduleEnterpriseSiteTarget[] { function buildScheduleEnterpriseSiteTargets(): ScheduleEnterpriseSiteTarget[] {
return form.publishEnterpriseSiteIds return form.publishEnterpriseSiteIds
.map((siteId) => ({ .map((siteId) => ({
@@ -794,7 +822,7 @@ function validateForm(): boolean {
} }
if (isSchedule.value && form.autoPublish) { if (isSchedule.value && form.autoPublish) {
if (form.publishTargetTab === 'media_accounts' && form.publishAccountIds.length === 0) { if (form.publishTargetTab === 'media_accounts' && currentSchedulePublishAccountIds().length === 0) {
message.warning(t('custom.messages.missingPublishTargets')) message.warning(t('custom.messages.missingPublishTargets'))
return false return false
} }
@@ -469,6 +469,17 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
} }
task.PublishAccountIDs = decodeDueSchedulePublishAccountIDs(publishAccountIDsJSON) task.PublishAccountIDs = decodeDueSchedulePublishAccountIDs(publishAccountIDsJSON)
task.PublishEnterpriseSiteTargets = tenantapp.DecodeScheduleEnterpriseSiteTargets(publishEnterpriseSiteTargetsJSON) task.PublishEnterpriseSiteTargets = tenantapp.DecodeScheduleEnterpriseSiteTargets(publishEnterpriseSiteTargetsJSON)
if task.AutoPublish && len(task.PublishAccountIDs) > 0 {
accountIDs, _, err := tenantapp.LoadExistingSchedulePublishAccountIDsAndPlatformsByIDs(ctx, tx, task.TenantID, task.WorkspaceID, task.PublishAccountIDs)
if err != nil {
rows.Close()
return nil, err
}
task.PublishAccountIDs = accountIDs
}
if task.AutoPublish && len(task.PublishAccountIDs) == 0 && len(task.PublishEnterpriseSiteTargets) == 0 {
task.AutoPublish = false
}
task.CoverRandomFolderID = int64PtrFromInt8(coverRandomFolderID) task.CoverRandomFolderID = int64PtrFromInt8(coverRandomFolderID)
if task.CoverMode == "" { if task.CoverMode == "" {
task.CoverMode = tenantapp.ScheduleCoverModeSpecific task.CoverMode = tenantapp.ScheduleCoverModeSpecific
@@ -16,6 +16,7 @@ import (
goredis "github.com/redis/go-redis/v9" goredis "github.com/redis/go-redis/v9"
"github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/auth"
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq" "github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
"github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository" "github.com/geo-platform/tenant-api/internal/tenant/repository"
@@ -27,6 +28,7 @@ type DesktopAccountService struct {
pool *pgxpool.Pool pool *pgxpool.Pool
repo repository.DesktopAccountRepository repo repository.DesktopAccountRepository
clientRepo repository.DesktopClientRepository clientRepo repository.DesktopClientRepository
cache sharedcache.Cache
redis *goredis.Client redis *goredis.Client
rabbitMQ *rabbitmq.Client rabbitMQ *rabbitmq.Client
now func() time.Time now func() time.Time
@@ -49,6 +51,11 @@ func NewDesktopAccountService(
} }
} }
func (s *DesktopAccountService) WithCache(c sharedcache.Cache) *DesktopAccountService {
s.cache = c
return s
}
type DesktopAccountView struct { type DesktopAccountView struct {
ID string `json:"id"` ID string `json:"id"`
Platform string `json:"platform"` Platform string `json:"platform"`
@@ -389,6 +396,7 @@ func (s *DesktopAccountService) Tombstone(ctx context.Context, client *repositor
} }
view := s.buildDesktopAccountView(account, nil, nil, nil, nil) view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
invalidateScheduleTaskCaches(ctx, s.cache, client.TenantID, nil)
return &view, nil return &view, nil
} }
@@ -451,6 +459,7 @@ func (s *DesktopAccountService) DeleteForActor(ctx context.Context, actor auth.A
} }
view := s.buildDesktopAccountView(account, nil, nil, nil, nil) view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
invalidateScheduleTaskCaches(ctx, s.cache, actor.TenantID, nil)
return &DesktopAccountDeleteResult{ return &DesktopAccountDeleteResult{
Account: view, Account: view,
CancelledQueuedPublishTasks: cancelled, CancelledQueuedPublishTasks: cancelled,
+46 -11
View File
@@ -70,36 +70,71 @@ func loadPublishAccountPlatformsByIDs(
tenantID int64, tenantID int64,
accountIDs []string, accountIDs []string,
) ([]string, error) { ) ([]string, error) {
_, platformIDs, err := loadExistingPublishAccountIDsAndPlatformsByIDs(ctx, db, tenantID, 0, accountIDs)
return platformIDs, err
}
func LoadExistingSchedulePublishAccountIDsAndPlatformsByIDs(
ctx context.Context,
db scheduleAutoPublishPlatformQuerier,
tenantID int64,
workspaceID int64,
accountIDs []string,
) ([]string, []string, error) {
return loadExistingPublishAccountIDsAndPlatformsByIDs(ctx, db, tenantID, workspaceID, accountIDs)
}
func loadExistingPublishAccountIDsAndPlatformsByIDs(
ctx context.Context,
db scheduleAutoPublishPlatformQuerier,
tenantID int64,
workspaceID int64,
accountIDs []string,
) ([]string, []string, error) {
accountIDs = normalizeSchedulePublishAccountIDs(accountIDs) accountIDs = normalizeSchedulePublishAccountIDs(accountIDs)
if db == nil || tenantID <= 0 || len(accountIDs) == 0 { if db == nil || tenantID <= 0 || len(accountIDs) == 0 {
return []string{}, nil return []string{}, []string{}, nil
} }
rows, err := db.Query(ctx, ` rows, err := db.Query(ctx, `
SELECT DISTINCT platform_id SELECT DISTINCT desktop_id::text, platform_id
FROM platform_accounts FROM platform_accounts
WHERE tenant_id = $1 WHERE tenant_id = $1
AND desktop_id::text = ANY($2::text[]) AND desktop_id::text = ANY($2::text[])
AND ($3::bigint <= 0 OR workspace_id = $3)
AND deleted_at IS NULL AND deleted_at IS NULL
ORDER BY platform_id `, tenantID, accountIDs, workspaceID)
`, tenantID, accountIDs)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
defer rows.Close() defer rows.Close()
platformIDs := make([]string, 0, len(accountIDs)) platformsByAccount := make(map[string][]string, len(accountIDs))
existingByAccount := make(map[string]struct{}, len(accountIDs))
for rows.Next() { for rows.Next() {
var accountID string
var platformID string var platformID string
if err := rows.Scan(&platformID); err != nil { if err := rows.Scan(&accountID, &platformID); err != nil {
return nil, err return nil, nil, err
} }
platformIDs = append(platformIDs, platformID) platformsByAccount[accountID] = append(platformsByAccount[accountID], platformID)
existingByAccount[accountID] = struct{}{}
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, err return nil, nil, err
} }
return emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)), nil
existingAccountIDs := make([]string, 0, len(accountIDs))
platformIDs := make([]string, 0, len(accountIDs))
for _, accountID := range accountIDs {
if _, ok := existingByAccount[accountID]; !ok {
continue
}
existingAccountIDs = append(existingAccountIDs, accountID)
platformIDs = append(platformIDs, platformsByAccount[accountID]...)
}
return emptyStringSliceIfNil(existingAccountIDs), emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)), nil
} }
func emptyStringSliceIfNil(values []string) []string { func emptyStringSliceIfNil(values []string) []string {
@@ -625,10 +625,15 @@ func (s *ScheduleTaskService) populateScheduleAutoPublishPlatforms(ctx context.C
if item == nil || !item.AutoPublish { if item == nil || !item.AutoPublish {
return nil return nil
} }
platformIDs, err := loadPublishAccountPlatformsByIDs(ctx, s.pool, tenantID, item.PublishAccountIDs) workspaceID := int64(0)
if item.WorkspaceID != nil {
workspaceID = *item.WorkspaceID
}
accountIDs, platformIDs, err := loadExistingPublishAccountIDsAndPlatformsByIDs(ctx, s.pool, tenantID, workspaceID, item.PublishAccountIDs)
if err != nil { if err != nil {
return response.ErrInternal(50010, "query_failed", "failed to resolve schedule auto publish platforms") return response.ErrInternal(50010, "query_failed", "failed to resolve schedule auto publish platforms")
} }
item.PublishAccountIDs = emptyStringSliceIfNil(accountIDs)
item.AutoPublishPlatforms = platformIDs item.AutoPublishPlatforms = platformIDs
return nil return nil
} }
@@ -656,7 +661,7 @@ func (s *ScheduleTaskService) populateScheduleAutoPublishPlatformsBatch(ctx cont
accountIDs = append(accountIDs, accountID) accountIDs = append(accountIDs, accountID)
} }
rows, err := s.pool.Query(ctx, ` rows, err := s.pool.Query(ctx, `
SELECT desktop_id::text, platform_id SELECT DISTINCT desktop_id::text, platform_id
FROM platform_accounts FROM platform_accounts
WHERE tenant_id = $1 WHERE tenant_id = $1
AND desktop_id::text = ANY($2::text[]) AND desktop_id::text = ANY($2::text[])
@@ -684,14 +689,21 @@ func (s *ScheduleTaskService) populateScheduleAutoPublishPlatformsBatch(ctx cont
if !items[idx].AutoPublish { if !items[idx].AutoPublish {
continue continue
} }
existingAccountIDs := make([]string, 0, len(items[idx].PublishAccountIDs))
platformIDs := make([]string, 0, len(items[idx].PublishAccountIDs)) platformIDs := make([]string, 0, len(items[idx].PublishAccountIDs))
for _, accountID := range items[idx].PublishAccountIDs { for _, accountID := range items[idx].PublishAccountIDs {
normalized := normalizeSchedulePublishAccountIDs([]string{accountID}) normalized := normalizeSchedulePublishAccountIDs([]string{accountID})
if len(normalized) == 0 { if len(normalized) == 0 {
continue continue
} }
platformIDs = append(platformIDs, platformsByAccount[normalized[0]]...) accountPlatforms := platformsByAccount[normalized[0]]
if len(accountPlatforms) == 0 {
continue
}
existingAccountIDs = append(existingAccountIDs, normalized[0])
platformIDs = append(platformIDs, accountPlatforms...)
} }
items[idx].PublishAccountIDs = emptyStringSliceIfNil(existingAccountIDs)
items[idx].AutoPublishPlatforms = emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)) items[idx].AutoPublishPlatforms = emptyStringSliceIfNil(normalizePlatformIDs(platformIDs))
} }
return nil return nil
@@ -1135,6 +1147,18 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
if len(accountIDs) == 0 && len(enterpriseTargets) == 0 { if len(accountIDs) == 0 && len(enterpriseTargets) == 0 {
return config, response.ErrBadRequest(40024, "publish_targets_required", "开启自动发布后,请至少选择一个发布目标") return config, response.ErrBadRequest(40024, "publish_targets_required", "开启自动发布后,请至少选择一个发布目标")
} }
if len(accountIDs) > 0 {
existingAccountIDs, platformIDs, err := loadExistingPublishAccountIDsAndPlatformsByIDs(ctx, s.pool, actor.TenantID, actor.PrimaryWorkspaceID, accountIDs)
if err != nil {
return config, response.ErrInternal(50010, "publish_account_check_failed", "failed to validate publish accounts")
}
accountIDs = existingAccountIDs
config.AccountIDs = accountIDs
config.PlatformIDs = normalizePlatformIDs(platformIDs)
}
if len(accountIDs) == 0 && len(enterpriseTargets) == 0 {
return config, response.ErrBadRequest(40024, "publish_targets_required", "开启自动发布后,请至少选择一个发布目标")
}
if err := validateOptionalPositiveInt64(req.CoverImageAssetID, "cover_image_asset_id"); err != nil { if err := validateOptionalPositiveInt64(req.CoverImageAssetID, "cover_image_asset_id"); err != nil {
return config, err return config, err
} }
@@ -1152,12 +1176,7 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
} }
coverURL := strings.TrimSpace(stringPointerValue(req.CoverAssetURL)) coverURL := strings.TrimSpace(stringPointerValue(req.CoverAssetURL))
if len(accountIDs) > 0 { if len(accountIDs) > 0 {
platformIDs, err := s.loadPublishAccountPlatformIDs(ctx, actor.TenantID, actor.PrimaryWorkspaceID, accountIDs) if schedulePublishPlatformsRequireCover(config.PlatformIDs) && coverMode == ScheduleCoverModeSpecific && coverURL == "" {
if err != nil {
return config, err
}
config.PlatformIDs = normalizePlatformIDs(platformIDs)
if schedulePublishPlatformsRequireCover(platformIDs) && coverMode == ScheduleCoverModeSpecific && coverURL == "" {
return config, response.ErrBadRequest(40025, "cover_required", "已选媒体账号的平台要求封面图,请先设置封面") return config, response.ErrBadRequest(40025, "cover_required", "已选媒体账号的平台要求封面图,请先设置封面")
} }
} }
@@ -1281,37 +1300,6 @@ func (s *ScheduleTaskService) validateScheduleEnterpriseSiteTargets(ctx context.
return nil return nil
} }
func (s *ScheduleTaskService) loadPublishAccountPlatformIDs(ctx context.Context, tenantID, workspaceID int64, accountIDs []string) ([]string, error) {
rows, err := s.pool.Query(ctx, `
SELECT platform_id
FROM platform_accounts
WHERE tenant_id = $1
AND workspace_id = $2
AND desktop_id::text = ANY($3::text[])
AND deleted_at IS NULL
`, tenantID, workspaceID, accountIDs)
if err != nil {
return nil, response.ErrInternal(50010, "publish_account_check_failed", "failed to validate publish accounts")
}
defer rows.Close()
platformIDs := make([]string, 0, len(accountIDs))
for rows.Next() {
var platformID string
if err := rows.Scan(&platformID); err != nil {
return nil, response.ErrInternal(50010, "publish_account_check_failed", "failed to parse publish accounts")
}
platformIDs = append(platformIDs, strings.TrimSpace(platformID))
}
if err := rows.Err(); err != nil {
return nil, response.ErrInternal(50010, "publish_account_check_failed", "failed to iterate publish accounts")
}
if len(platformIDs) != len(accountIDs) {
return nil, response.ErrBadRequest(40027, "invalid_publish_accounts", "one or more publish accounts do not exist")
}
return platformIDs, nil
}
func schedulePublishPlatformsRequireCover(platformIDs []string) bool { func schedulePublishPlatformsRequireCover(platformIDs []string) bool {
for _, platformID := range platformIDs { for _, platformID := range platformIDs {
if publishPlatformRequiresCover(platformID) { if publishPlatformRequiresCover(platformID) {
@@ -25,7 +25,7 @@ func NewDesktopAccountHandler(a *bootstrap.App) *DesktopAccountHandler {
repository.NewDesktopClientRepository(a.DB), repository.NewDesktopClientRepository(a.DB),
a.Redis, a.Redis,
a.RabbitMQ, a.RabbitMQ,
), ).WithCache(a.Cache),
} }
} }