From ba0b198eb8ba767dcdf586ae8a4f75c1a572d97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E6=9E=97=E9=93=96?= Date: Mon, 31 Aug 2026 14:38:23 +0800 Subject: [PATCH] feat: hourly sync work review dashboard data --- .../first-pass/projects/route.test.ts | 7 + .../core-metrics/first-pass/projects/route.ts | 7 +- .../core-metrics/first-pass/route.ts | 7 +- .../department-dashboard/CoreMetricDetail.tsx | 14 +- .../CoreMetricsDashboard.test.tsx | 4 +- .../CoreMetricsDashboard.tsx | 15 +- instrumentation.ts | 10 ++ lib/brand-center-dashboard-access.ts | 33 ++++ lib/work-review-dashboard-sync.ts | 165 ++++++++++++++++++ lib/work-review-metrics.ts | 4 + lib/work-review-service.ts | 10 +- 11 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 lib/brand-center-dashboard-access.ts create mode 100644 lib/work-review-dashboard-sync.ts diff --git a/app/api/department-dashboard/core-metrics/first-pass/projects/route.test.ts b/app/api/department-dashboard/core-metrics/first-pass/projects/route.test.ts index 47d7201..c03593a 100644 --- a/app/api/department-dashboard/core-metrics/first-pass/projects/route.test.ts +++ b/app/api/department-dashboard/core-metrics/first-pass/projects/route.test.ts @@ -11,6 +11,13 @@ jest.mock('@/lib/auth', () => ({ authOptions: {}, })) +jest.mock('@/lib/brand-center-dashboard-access', () => ({ + BrandCenterDashboardAccessError: class BrandCenterDashboardAccessError extends Error { + status = 403 + }, + assertBrandCenterDashboardAccess: jest.fn(async () => undefined), +})) + jest.mock('@/lib/work-review-service', () => { const actual = jest.requireActual('@/lib/work-review-service') return { diff --git a/app/api/department-dashboard/core-metrics/first-pass/projects/route.ts b/app/api/department-dashboard/core-metrics/first-pass/projects/route.ts index f481f87..5171ed1 100644 --- a/app/api/department-dashboard/core-metrics/first-pass/projects/route.ts +++ b/app/api/department-dashboard/core-metrics/first-pass/projects/route.ts @@ -1,6 +1,10 @@ import { getServerSession } from 'next-auth' import { NextResponse } from 'next/server' import { authOptions } from '@/lib/auth' +import { + assertBrandCenterDashboardAccess, + BrandCenterDashboardAccessError, +} from '@/lib/brand-center-dashboard-access' import { PerformanceSurveyApiError } from '@/lib/performance-survey-service' import { getFirstPassReviewProjects, @@ -27,6 +31,7 @@ function parsePositiveInteger(value: string | null, fallback: number) { export async function GET(request: Request) { try { const session = await getServerSession(authOptions) + await assertBrandCenterDashboardAccess(session) const url = new URL(request.url) const page = parsePositiveInteger(url.searchParams.get('page'), 1) const pageSize = Math.min(parsePositiveInteger(url.searchParams.get('pageSize'), 20), 50) @@ -36,7 +41,7 @@ export async function GET(request: Request) { const data = await getFirstPassReviewProjects(session, { page, pageSize, query }) return NextResponse.json({ success: true, data }, { headers: PRIVATE_HEADERS }) } catch (error) { - if (error instanceof WorkReviewApiError || error instanceof PerformanceSurveyApiError) { + if (error instanceof BrandCenterDashboardAccessError || error instanceof WorkReviewApiError || error instanceof PerformanceSurveyApiError) { return NextResponse.json( { success: false, error: error.message }, { status: error.status, headers: PRIVATE_HEADERS }, diff --git a/app/api/department-dashboard/core-metrics/first-pass/route.ts b/app/api/department-dashboard/core-metrics/first-pass/route.ts index a4b2eff..8927f98 100644 --- a/app/api/department-dashboard/core-metrics/first-pass/route.ts +++ b/app/api/department-dashboard/core-metrics/first-pass/route.ts @@ -1,6 +1,10 @@ import { getServerSession } from 'next-auth' import { NextResponse } from 'next/server' import { authOptions } from '@/lib/auth' +import { + assertBrandCenterDashboardAccess, + BrandCenterDashboardAccessError, +} from '@/lib/brand-center-dashboard-access' import { PerformanceSurveyApiError } from '@/lib/performance-survey-service' import { WorkReviewApiError, @@ -18,10 +22,11 @@ const PRIVATE_HEADERS = { export async function GET() { try { const session = await getServerSession(authOptions) + await assertBrandCenterDashboardAccess(session) const data = await getFirstPassReviewSummary(session) return NextResponse.json({ success: true, data }, { headers: PRIVATE_HEADERS }) } catch (error) { - if (error instanceof WorkReviewApiError || error instanceof PerformanceSurveyApiError) { + if (error instanceof BrandCenterDashboardAccessError || error instanceof WorkReviewApiError || error instanceof PerformanceSurveyApiError) { return NextResponse.json( { success: false, error: error.message }, { status: error.status, headers: PRIVATE_HEADERS }, diff --git a/components/department-dashboard/CoreMetricDetail.tsx b/components/department-dashboard/CoreMetricDetail.tsx index 8f1f80c..12c9166 100644 --- a/components/department-dashboard/CoreMetricDetail.tsx +++ b/components/department-dashboard/CoreMetricDetail.tsx @@ -325,11 +325,15 @@ export default function CoreMetricDetail({ metric, query, projectInsights, month ? monthlyLaunches.summary.lastSyncedAt ? `每小时增量同步 · ${formatDate(monthlyLaunches.summary.lastSyncedAt, true)}` : monthlyLaunches.summary.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步' - : hasOnTimeDeliverySource - ? onTimeDelivery.summary.lastSyncedAt - ? `每小时增量同步 · ${formatDate(onTimeDelivery.summary.lastSyncedAt, true)}` - : onTimeDelivery.summary.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步' - : hasFirstPassSource ? '打开页面时读取' : '每日同步待接入'} + : hasOnTimeDeliverySource + ? onTimeDelivery.summary.lastSyncedAt + ? `每小时增量同步 · ${formatDate(onTimeDelivery.summary.lastSyncedAt, true)}` + : onTimeDelivery.summary.syncStatus === 'error' ? '首轮同步失败' : '等待首轮同步' + : hasFirstPassSource + ? firstPassPage.summary.lastSyncedAt + ? `每小时增量同步 · ${formatDate(firstPassPage.summary.lastSyncedAt, true)}` + : firstPassPage.summary.syncStatus === 'error' ? '增量同步失败' : '等待首轮同步' + : '每日同步待接入'}

diff --git a/components/department-dashboard/CoreMetricsDashboard.test.tsx b/components/department-dashboard/CoreMetricsDashboard.test.tsx index 51c5059..108c4b0 100644 --- a/components/department-dashboard/CoreMetricsDashboard.test.tsx +++ b/components/department-dashboard/CoreMetricsDashboard.test.tsx @@ -56,6 +56,8 @@ describe('CoreMetricsDashboard', () => { rate: 73.3, statusBreakdown: { approved: 11, revisionRequired: 7, pendingReview: 10, unknown: 2 }, readAt: '2026-08-14T02:00:00.000Z', + syncStatus: 'ready', + lastSyncedAt: '2026-08-14T02:00:00.000Z', }, }, })) as jest.Mock @@ -72,7 +74,7 @@ describe('CoreMetricsDashboard', () => { expect(screen.queryByRole('complementary', { name: '数据接入说明' })).not.toBeInTheDocument() expect(await screen.findByText('73.3%')).toBeInTheDocument() expect(screen.getByText('V1通过 11 / 已通过 15')).toBeInTheDocument() - expect(screen.getByText('实时读取')).toBeInTheDocument() + expect(screen.getByText('作品过稿后台')).toBeInTheDocument() expect(screen.getAllByText('每小时同步').length).toBeGreaterThan(0) expect(screen.getAllByText('示例值')).toHaveLength(2) diff --git a/components/department-dashboard/CoreMetricsDashboard.tsx b/components/department-dashboard/CoreMetricsDashboard.tsx index 20fda13..f262983 100644 --- a/components/department-dashboard/CoreMetricsDashboard.tsx +++ b/components/department-dashboard/CoreMetricsDashboard.tsx @@ -188,7 +188,9 @@ export default function CoreMetricsDashboard() { : '等待首轮同步' : onTimeDelivery.status === 'error' ? '读取失败' : '正在读取' : isFirstPass - ? firstPass.status === 'success' ? '实时读取' : firstPass.status === 'error' ? '读取失败' : '正在读取' + ? firstPass.status === 'success' + ? firstPass.data.syncStatus === 'ready' ? '每小时同步' : firstPass.data.syncStatus === 'error' ? '同步异常' : '正在同步' + : firstPass.status === 'error' ? '读取失败' : '正在读取' : '示例值' const value = isOngoingWorkflow ? ongoingWorkflows.status === 'success' ? String(ongoingWorkflows.data.total) : '—' @@ -230,6 +232,11 @@ export default function CoreMetricsDashboard() { V1通过 {firstPass.data.firstPassApproved} / 已通过 {firstPass.data.totalApproved}

)} + {isFirstPass && firstPass.status === 'success' && firstPass.data.lastSyncedAt && ( +

+ 最近同步 {formatSyncTime(firstPass.data.lastSyncedAt)} +

+ )} {isFirstPass && firstPass.status === 'loading' && (

正在连接作品过稿

)} @@ -266,12 +273,12 @@ export default function CoreMetricsDashboard() {