From bc6e9357eaacba11aad10ec8aa71b3a50a17a5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=82?= Date: Mon, 6 Jul 2026 11:30:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(workspace):=20=E5=85=9C=E5=BA=95=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=9C=AC=E5=9C=B0=E5=BF=AB=E8=AF=BB=E7=A9=BA=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v22-query/v22-query.service.spec.ts | 54 +++++++++++++ .../modules/v22-query/v22-query.service.ts | 77 ++++++++++++++++++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/v22-query/v22-query.service.spec.ts b/apps/server/src/modules/v22-query/v22-query.service.spec.ts index a42d150..69fc4c5 100644 --- a/apps/server/src/modules/v22-query/v22-query.service.spec.ts +++ b/apps/server/src/modules/v22-query/v22-query.service.spec.ts @@ -21,6 +21,9 @@ function buildPrismaMock() { bug: { findMany: jest.fn(), }, + appData: { + findMany: jest.fn(), + }, xiaobaoRiskSummary: { findMany: jest.fn(), }, @@ -123,6 +126,57 @@ describe('V22QueryService', () => { }); }); + it('falls back to AppData workspace rows when relation tables are empty', async () => { + const prisma = buildPrismaMock(); + prisma.versionPlan.findMany.mockResolvedValue([]); + prisma.devTask.findMany.mockResolvedValue([]); + prisma.testCase.findMany.mockResolvedValue([]); + prisma.bug.findMany.mockResolvedValue([]); + prisma.appData.findMany.mockResolvedValue([ + { + key: 'products-overview', + value: [{ + id: 'product-1', + name: 'FTB', + projects: [{ id: 'project-1', name: '案例学习' }], + versions: [{ id: 'version-1', name: '案例学习V1.3', projectId: 'project-1' }], + }], + }, + { + key: 'members', + value: { + members: [{ id: 'm-8', name: '超级管理员', username: 'admin', email: '', createdAt: '2026-07-06' }], + }, + }, + { + key: 'version-plans', + value: [{ + id: 'plan-1', + versionId: 'version-1', + type: 'product', + title: '案例学习 V1.3 产品方案', + owner: '超级管理员', + status: 'in_progress', + startTime: '2026-07-06T09:00:00.000Z', + endTime: '2026-07-07T18:00:00.000Z', + createdAt: '2026-07-06T09:00:00.000Z', + }], + }, + ]); + const service = new V22QueryService(prisma as any); + + const result = await service.getWorkspaceData('m-8'); + + expect(result.versionPlans).toEqual([ + expect.objectContaining({ + id: 'plan-1', + ownerId: 'm-8', + title: '案例学习 V1.3 产品方案', + }), + ]); + expect(prisma.appData.findMany).toHaveBeenCalled(); + }); + it('loads Xiaobao warning summaries either globally for managers or by user-owned version ids', async () => { const prisma = buildPrismaMock(); prisma.versionPlan.findMany.mockResolvedValue([{ versionId: 'version-1' }]); diff --git a/apps/server/src/modules/v22-query/v22-query.service.ts b/apps/server/src/modules/v22-query/v22-query.service.ts index bade2a2..5d59eb8 100644 --- a/apps/server/src/modules/v22-query/v22-query.service.ts +++ b/apps/server/src/modules/v22-query/v22-query.service.ts @@ -1,5 +1,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; +import { APP_DATA_KEYS } from '../data/data-keys'; +import { mapAppDataToV22Rows } from '../migration/app-data-v22.mapper'; interface RequirementQuery { productId?: string; @@ -103,7 +105,7 @@ export class V22QueryService { }; } - async getWorkspaceData(userId: string) { + async getWorkspaceData(userId: string): Promise { const normalizedUserId = userId.trim(); if (!normalizedUserId) throw new BadRequestException('userId is required'); @@ -126,9 +128,39 @@ export class V22QueryService { }), ]); + const relationResult = { versionPlans, devTasks, testCases, bugs }; + if (hasWorkspaceRows(relationResult)) return relationResult; + + const fallback = await this.getWorkspaceDataFromAppData(normalizedUserId); + if (hasWorkspaceRows(fallback)) return fallback; + return { versionPlans, devTasks, testCases, bugs }; } + private async getWorkspaceDataFromAppData(userId: string): Promise { + const rows = await this.prisma.appData.findMany({ + where: { key: { in: [...APP_DATA_KEYS] } }, + select: { key: true, value: true }, + }); + const snapshot = Object.fromEntries(rows.map((row) => [row.key, row.value])); + const mapped = mapAppDataToV22Rows(snapshot); + + return { + versionPlans: mapped.versionPlans + .filter((plan) => plan.ownerId === userId && plan.status !== 'completed') + .sort(comparePlanRows), + devTasks: mapped.devTasks + .filter((task) => task.assigneeId === userId && task.status !== 'submitted') + .sort(comparePriorityThenUpdatedRows), + testCases: mapped.testCases + .filter((testCase) => testCase.assigneeId === userId && !['passed', 'failed', 'blocked'].includes(testCase.status)) + .sort(comparePriorityThenUpdatedRows), + bugs: mapped.bugs + .filter((bug) => bug.assigneeId === userId && ['open', 'fixing', 'fixed', 'verifying'].includes(bug.status)) + .sort(comparePriorityThenUpdatedRows), + }; + } + async getXiaobaoWarnings(query: XiaobaoWarningQuery) { if (query.manager === 'true') { return this.prisma.xiaobaoRiskSummary.findMany({ @@ -174,6 +206,49 @@ export class V22QueryService { } } +type WorkspaceRows = { + versionPlans: unknown[]; + devTasks: unknown[]; + testCases: unknown[]; + bugs: unknown[]; +}; + +type PriorityUpdatedRow = { + priority?: number; + updatedAt?: string; +}; + +type PlanSortRow = { + expectedEndAt?: string; + updatedAt?: string; +}; + +function hasWorkspaceRows(rows: WorkspaceRows): boolean { + return rows.versionPlans.length > 0 + || rows.devTasks.length > 0 + || rows.testCases.length > 0 + || rows.bugs.length > 0; +} + +function comparePlanRows(a: PlanSortRow, b: PlanSortRow): number { + const endDiff = dateSortValue(a.expectedEndAt, Number.POSITIVE_INFINITY) + - dateSortValue(b.expectedEndAt, Number.POSITIVE_INFINITY); + if (endDiff !== 0) return endDiff; + return dateSortValue(b.updatedAt, 0) - dateSortValue(a.updatedAt, 0); +} + +function comparePriorityThenUpdatedRows(a: PriorityUpdatedRow, b: PriorityUpdatedRow): number { + const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0); + if (priorityDiff !== 0) return priorityDiff; + return dateSortValue(b.updatedAt, 0) - dateSortValue(a.updatedAt, 0); +} + +function dateSortValue(value: string | undefined, fallback: number): number { + if (!value) return fallback; + const time = new Date(value).getTime(); + return Number.isFinite(time) ? time : fallback; +} + function parseLimit(raw?: string): number { const parsed = raw ? Number(raw) : 50; if (!Number.isFinite(parsed)) return 50;