feat(tenant): add brand publish-records list and harden enterprise site management

- Add GET /api/tenant/publish-records: brand-scoped paginated list that merges
  in-flight (queued/publishing) records ahead of history (success/failed/cancelled),
  enriched with article title and latest desktop task id; wire up
  service/handler/router/swagger/router_test
- Rework PublishManagementView to consume publish-records instead of desktop
  publish tasks; add publishRecordsApi + shared-types
  (ListPublishRecordsParams, PublishRecordListResponse)
- Enterprise site Update: distinguish explicit null from missing PATCH fields via
  EnterpriseSitePatchString, dedup site_url before write, verify RowsAffected,
  and map unique-constraint violations to a clear conflict
- MediaView: enterprise site edit/delete flows with favicon fallback handling
- Localize enterprise site / PBootCMS error messages to Chinese across the
  backend and the admin-web error map, plus legacy raw-message translation
- deploy: gate migration-coupled service rollouts behind RUN_MIGRATIONS unless
  ALLOW_SKIP_MIGRATIONS_FOR_APP_ROLLOUT=true; handle empty SERVICES safely

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:40:46 +08:00
parent 67be43319e
commit a44ed21967
15 changed files with 1418 additions and 764 deletions
@@ -33,7 +33,7 @@ import {
tenantAccountsApi,
} from '@/lib/api'
import { coverUploadRequired, deriveCoverFileName } from '@/lib/cover-requirements'
import { formatError } from '@/lib/errors'
import { formatError, formatStoredErrorMessage } from '@/lib/errors'
import {
accountInitial,
buildPublishAccountCards,
@@ -876,7 +876,8 @@ async function handleEnterprisePublishSuccess(
} else if (failed.length > 0) {
notification.error({
message: '企业站发布失败',
description: failed[0]?.error_message || '请检查插件、栏目和 PBootCMS API 配置。',
description:
formatStoredErrorMessage(failed[0]?.error_message) || '请检查插件、栏目和 PBootCMS API 配置。',
placement: 'topRight',
duration: 6,
})
@@ -1094,14 +1095,14 @@ function resolveEnterpriseSiteDisabledReason(
return '该企业站点已禁用。'
}
if (connectionStatus === 'error') {
return site.last_error?.trim() || '该企业站点连接异常,请先测试连通性。'
return formatStoredErrorMessage(site.last_error) || '该企业站点连接异常,请先测试连通性。'
}
return ''
}
function enterpriseSiteConnectionStatus(site: EnterpriseSiteConnection): string {
const status = String(site.status ?? '').trim()
const lastError = site.last_error?.trim()
const lastError = formatStoredErrorMessage(site.last_error)
const publishError = enterpriseSiteLatestPublishError(site)
if (status === 'error' && lastError && publishError && lastError === publishError) {
return 'connected'
@@ -1118,7 +1119,7 @@ function enterpriseSiteLatestPublishError(site: EnterpriseSiteConnection): strin
if (status !== 'failed' && status !== 'publish_failed') {
return ''
}
return record.error_message?.trim() || ''
return formatStoredErrorMessage(record.error_message)
}
function cmsTypeLabel(cmsType: string): string {