feat(v2.2): 完成高频读取热路径
This commit is contained in:
481
apps/web/lib/v22-api.ts
Normal file
481
apps/web/lib/v22-api.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
import { api } from './api';
|
||||
import type { Bug, BugSeverity, BugStatus } from './bug';
|
||||
import type { DevTask, DevTaskStatus, Reference } from './dev-task';
|
||||
import type { Priority } from './derive';
|
||||
import type { Requirement, RequirementStatus, SourceType } from './requirement';
|
||||
import type { TestCase, TestCaseStatus } from './test-case';
|
||||
import type { VersionDataScope } from './version-data-scope';
|
||||
import type {
|
||||
PlanTask,
|
||||
VersionPlan,
|
||||
VersionPlanLog,
|
||||
VersionPlanRequirementCoverage,
|
||||
} from './version-plan';
|
||||
|
||||
type V22DateValue = string | Date | null | undefined;
|
||||
|
||||
interface V22VersionRow {
|
||||
id: string;
|
||||
productId: string;
|
||||
projectId?: string | null;
|
||||
name: string;
|
||||
releaseDate?: V22DateValue;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22RequirementRow {
|
||||
id: string;
|
||||
productId: string;
|
||||
projectId?: string | null;
|
||||
versionId?: string | null;
|
||||
code: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
priority?: number | string | null;
|
||||
type?: string | null;
|
||||
sourceType?: string | null;
|
||||
sourceTarget?: string | null;
|
||||
platform?: string | null;
|
||||
creatorId?: string | null;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22VersionPlanRow {
|
||||
id: string;
|
||||
versionId: string;
|
||||
type: string;
|
||||
title: string;
|
||||
status?: string | null;
|
||||
ownerId?: string | null;
|
||||
expectedStartAt?: V22DateValue;
|
||||
expectedEndAt?: V22DateValue;
|
||||
actualStartAt?: V22DateValue;
|
||||
completedAt?: V22DateValue;
|
||||
resultUrl?: string | null;
|
||||
requirementCoverage?: unknown;
|
||||
logs?: unknown;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22DevTaskRow {
|
||||
id: string;
|
||||
versionId: string;
|
||||
requirementId?: string | null;
|
||||
categoryId?: string | null;
|
||||
code: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
priority?: number | string | null;
|
||||
assigneeId?: string | null;
|
||||
creatorId?: string | null;
|
||||
isBlocked?: boolean | null;
|
||||
blockReason?: string | null;
|
||||
expectedStartAt?: V22DateValue;
|
||||
expectedEndAt?: V22DateValue;
|
||||
startDate?: V22DateValue;
|
||||
completedAt?: V22DateValue;
|
||||
estimateHours?: number | null;
|
||||
aiEstimateHours?: number | null;
|
||||
references?: unknown;
|
||||
aiDraft?: boolean | null;
|
||||
aiDraftAt?: V22DateValue;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22TestCaseRow {
|
||||
id: string;
|
||||
versionId: string;
|
||||
requirementId?: string | null;
|
||||
categoryId?: string | null;
|
||||
code: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
roundNo?: number | null;
|
||||
priority?: number | string | null;
|
||||
assigneeId?: string | null;
|
||||
creatorId?: string | null;
|
||||
plannedTestAt?: V22DateValue;
|
||||
plannedEndAt?: V22DateValue;
|
||||
startedAt?: V22DateValue;
|
||||
completedAt?: V22DateValue;
|
||||
estimateHours?: number | null;
|
||||
aiEstimateHours?: number | null;
|
||||
references?: unknown;
|
||||
aiDraft?: boolean | null;
|
||||
aiDraftAt?: V22DateValue;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22BugRow {
|
||||
id: string;
|
||||
versionId: string;
|
||||
testCaseId?: string | null;
|
||||
code: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
severity?: string | null;
|
||||
priority?: number | string | null;
|
||||
assigneeId?: string | null;
|
||||
reporterId?: string | null;
|
||||
plannedFixAt?: V22DateValue;
|
||||
resolvedAt?: V22DateValue;
|
||||
closedAt?: V22DateValue;
|
||||
resolution?: string | null;
|
||||
createdAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}
|
||||
|
||||
interface V22VersionDetailResponse {
|
||||
version: V22VersionRow;
|
||||
requirements: V22RequirementRow[];
|
||||
versionPlans: V22VersionPlanRow[];
|
||||
devTasks: V22DevTaskRow[];
|
||||
testCases: V22TestCaseRow[];
|
||||
bugs: V22BugRow[];
|
||||
}
|
||||
|
||||
export interface V22VersionDetailData {
|
||||
version: V22VersionRow;
|
||||
scope: VersionDataScope;
|
||||
}
|
||||
|
||||
export interface V22RequirementsQuery {
|
||||
productId: string;
|
||||
projectId?: string;
|
||||
versionId?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
type?: string;
|
||||
q?: string;
|
||||
sort?: string;
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface V22RequirementsPage {
|
||||
items: Requirement[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
export interface V22WorkspaceData {
|
||||
versionPlans: VersionPlan[];
|
||||
devTasks: DevTask[];
|
||||
testCases: TestCase[];
|
||||
bugs: Bug[];
|
||||
}
|
||||
|
||||
export interface V22XiaobaoWarningQuery {
|
||||
userId?: string;
|
||||
manager?: boolean;
|
||||
}
|
||||
|
||||
export interface V22XiaobaoWarningSummary {
|
||||
versionId: string;
|
||||
riskLevel: string;
|
||||
riskScore: number;
|
||||
confidence: number;
|
||||
forecastReleaseDate?: string;
|
||||
riskSignature: string;
|
||||
summary: unknown;
|
||||
dirty?: boolean;
|
||||
recomputedAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export async function loadV22VersionDetailData(versionId: string): Promise<V22VersionDetailData> {
|
||||
const response = await api.get<V22VersionDetailResponse>(
|
||||
`/v2.2/versions/${encodeURIComponent(versionId)}/detail-data`,
|
||||
);
|
||||
return {
|
||||
version: response.version,
|
||||
scope: buildScope(response),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadV22RequirementsPage(query: V22RequirementsQuery): Promise<V22RequirementsPage> {
|
||||
const response = await api.get<{ items: V22RequirementRow[]; nextCursor?: string }>(
|
||||
`/v2.2/requirements?${queryString(query)}`,
|
||||
);
|
||||
return {
|
||||
items: response.items.map(mapRequirement),
|
||||
nextCursor: response.nextCursor,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadV22WorkspaceData(userId: string): Promise<V22WorkspaceData> {
|
||||
const response = await api.get<{
|
||||
versionPlans: V22VersionPlanRow[];
|
||||
devTasks: V22DevTaskRow[];
|
||||
testCases: V22TestCaseRow[];
|
||||
bugs: V22BugRow[];
|
||||
}>(`/v2.2/workspace?${queryString({ userId })}`);
|
||||
return {
|
||||
versionPlans: response.versionPlans.map(mapVersionPlan),
|
||||
devTasks: response.devTasks.map(mapDevTask),
|
||||
testCases: response.testCases.map(mapTestCase),
|
||||
bugs: response.bugs.map(mapBug),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadV22XiaobaoWarnings(
|
||||
query: V22XiaobaoWarningQuery,
|
||||
): Promise<V22XiaobaoWarningSummary[]> {
|
||||
const response = await api.get<Array<Omit<V22XiaobaoWarningSummary, 'forecastReleaseDate' | 'recomputedAt' | 'updatedAt'> & {
|
||||
forecastReleaseDate?: V22DateValue;
|
||||
recomputedAt?: V22DateValue;
|
||||
updatedAt?: V22DateValue;
|
||||
}>>(`/v2.2/xiaobao-warning?${queryString({
|
||||
userId: query.userId,
|
||||
manager: query.manager ? 'true' : undefined,
|
||||
})}`);
|
||||
return response.map((row) => ({
|
||||
...row,
|
||||
forecastReleaseDate: optionalIso(row.forecastReleaseDate),
|
||||
recomputedAt: optionalIso(row.recomputedAt),
|
||||
updatedAt: optionalIso(row.updatedAt),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildScope(response: V22VersionDetailResponse): VersionDataScope {
|
||||
const requirements = response.requirements.map(mapRequirement);
|
||||
const requirementIds = requirements.map((requirement) => requirement.id);
|
||||
const requirementIdSet = new Set(requirementIds);
|
||||
const plans = response.versionPlans.map(mapVersionPlan);
|
||||
const plansByType: VersionDataScope['plansByType'] = { research: [], product: [], ui: [] };
|
||||
for (const plan of plans) {
|
||||
plansByType[plan.type].push(plan);
|
||||
}
|
||||
|
||||
return {
|
||||
requirements,
|
||||
requirementIds,
|
||||
requirementIdSet,
|
||||
plans,
|
||||
plansByType,
|
||||
devTasks: response.devTasks.map(mapDevTask),
|
||||
testCases: response.testCases.map(mapTestCase),
|
||||
bugs: response.bugs.map(mapBug),
|
||||
overtimeRecords: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mapRequirement(row: V22RequirementRow): Requirement {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
title: row.title,
|
||||
description: row.description ?? '',
|
||||
productId: row.productId,
|
||||
projectId: row.projectId ?? '',
|
||||
versionId: row.versionId ?? undefined,
|
||||
sourceType: toSourceType(row.sourceType),
|
||||
sourceTarget: row.sourceTarget ?? '',
|
||||
platforms: splitCsv(row.platform),
|
||||
typeId: row.type ?? '',
|
||||
status: toRequirementStatus(row.status),
|
||||
priority: toPriority(row.priority),
|
||||
effort: 'M',
|
||||
creator: row.creatorId ?? '',
|
||||
createdAt: requiredIso(row.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
function mapVersionPlan(row: V22VersionPlanRow): VersionPlan {
|
||||
const coverage = asArray<VersionPlanRequirementCoverage>(row.requirementCoverage);
|
||||
return {
|
||||
id: row.id,
|
||||
versionId: row.versionId,
|
||||
type: toPlanType(row.type),
|
||||
title: row.title,
|
||||
owner: row.ownerId ?? '',
|
||||
startTime: requiredIso(row.expectedStartAt),
|
||||
endTime: requiredIso(row.expectedEndAt),
|
||||
status: toPlanStatus(row.status),
|
||||
tasks: [],
|
||||
completedRequirementIds: coverage
|
||||
.filter((item) => item?.status === 'completed')
|
||||
.map((item) => item.requirementId)
|
||||
.filter(Boolean),
|
||||
linkedRequirementIds: coverage.map((item) => item?.requirementId).filter(Boolean),
|
||||
requirementCoverage: coverage,
|
||||
logs: asArray<VersionPlanLog>(row.logs),
|
||||
resultUrl: row.resultUrl ?? undefined,
|
||||
actualStartAt: optionalIso(row.actualStartAt),
|
||||
createdAt: requiredIso(row.createdAt),
|
||||
completedAt: optionalIso(row.completedAt),
|
||||
addedBy: row.ownerId ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function mapDevTask(row: V22DevTaskRow): DevTask {
|
||||
return {
|
||||
id: row.id,
|
||||
taskNo: row.code,
|
||||
versionId: row.versionId,
|
||||
requirementId: row.requirementId ?? '',
|
||||
title: row.title,
|
||||
description: row.description ?? '',
|
||||
categoryId: row.categoryId ?? '',
|
||||
assigneeId: row.assigneeId ?? '',
|
||||
priority: toPriority(row.priority),
|
||||
expectedStartAt: requiredIso(row.expectedStartAt),
|
||||
expectedEndAt: requiredIso(row.expectedEndAt),
|
||||
estimateHours: row.estimateHours ?? undefined,
|
||||
aiEstimateHours: row.aiEstimateHours ?? undefined,
|
||||
actualStartAt: optionalIso(row.startDate),
|
||||
actualEndAt: optionalIso(row.completedAt),
|
||||
status: toDevTaskStatus(row.status),
|
||||
isBlocked: row.isBlocked ?? false,
|
||||
blockReason: row.blockReason ?? undefined,
|
||||
references: asArray<Reference>(row.references),
|
||||
aiDraft: row.aiDraft ?? false,
|
||||
aiDraftAt: optionalIso(row.aiDraftAt),
|
||||
createdBy: row.creatorId ?? '',
|
||||
createdAt: requiredIso(row.createdAt),
|
||||
updatedAt: requiredIso(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
function mapTestCase(row: V22TestCaseRow): TestCase {
|
||||
return {
|
||||
id: row.id,
|
||||
caseNo: row.code,
|
||||
versionId: row.versionId,
|
||||
requirementId: row.requirementId ?? undefined,
|
||||
roundNo: row.roundNo ?? 1,
|
||||
title: row.title,
|
||||
description: row.description ?? '',
|
||||
categoryId: row.categoryId ?? '',
|
||||
priority: toPriority(row.priority),
|
||||
assigneeId: row.assigneeId ?? undefined,
|
||||
status: toTestCaseStatus(row.status),
|
||||
estimateHours: row.estimateHours ?? undefined,
|
||||
aiEstimateHours: row.aiEstimateHours ?? undefined,
|
||||
plannedTestAt: optionalIso(row.plannedTestAt),
|
||||
plannedEndAt: optionalIso(row.plannedEndAt),
|
||||
startedAt: optionalIso(row.startedAt),
|
||||
completedAt: optionalIso(row.completedAt),
|
||||
references: asArray<Reference>(row.references),
|
||||
aiDraft: row.aiDraft ?? false,
|
||||
aiDraftAt: optionalIso(row.aiDraftAt),
|
||||
createdBy: row.creatorId ?? '',
|
||||
createdAt: requiredIso(row.createdAt),
|
||||
updatedAt: requiredIso(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
function mapBug(row: V22BugRow): Bug {
|
||||
return {
|
||||
id: row.id,
|
||||
bugNo: row.code,
|
||||
versionId: row.versionId,
|
||||
testCaseId: row.testCaseId ?? '',
|
||||
title: row.title,
|
||||
description: row.description ?? '',
|
||||
severity: toBugSeverity(row.severity),
|
||||
priority: toPriority(row.priority),
|
||||
reportedBy: row.reporterId ?? '',
|
||||
assigneeId: row.assigneeId ?? '',
|
||||
status: toBugStatus(row.status),
|
||||
resolvedAt: optionalIso(row.resolvedAt),
|
||||
closedAt: optionalIso(row.closedAt),
|
||||
resolution: row.resolution ?? undefined,
|
||||
plannedFixAt: optionalIso(row.plannedFixAt),
|
||||
createdAt: requiredIso(row.createdAt),
|
||||
updatedAt: requiredIso(row.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
function queryString<T extends object>(values: T): string {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
if (value === undefined || value === '') return;
|
||||
params.set(key, String(value));
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function requiredIso(value: V22DateValue): string {
|
||||
return optionalIso(value) ?? '';
|
||||
}
|
||||
|
||||
function optionalIso(value: V22DateValue): string | undefined {
|
||||
if (!value) return undefined;
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return `${trimmed}T00:00:00.000Z`;
|
||||
const parsed = new Date(trimmed);
|
||||
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function splitCsv(value?: string | null): string[] {
|
||||
if (!value) return [];
|
||||
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function asArray<T>(value: unknown): T[] {
|
||||
return Array.isArray(value) ? value as T[] : [];
|
||||
}
|
||||
|
||||
function toPriority(value: number | string | null | undefined): Priority {
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (/^P[0-4]$/.test(normalized)) return normalized as Priority;
|
||||
const parsed = Number(normalized);
|
||||
if (Number.isFinite(parsed)) return toPriority(parsed);
|
||||
}
|
||||
const rank = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 2;
|
||||
return `P${Math.max(0, Math.min(4, rank))}` as Priority;
|
||||
}
|
||||
|
||||
function toSourceType(value?: string | null): SourceType {
|
||||
const allowed: SourceType[] = ['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management'];
|
||||
return allowed.includes(value as SourceType) ? value as SourceType : 'internal';
|
||||
}
|
||||
|
||||
function toRequirementStatus(value?: string | null): RequirementStatus {
|
||||
const allowed: RequirementStatus[] = ['pending_review', 'adopted', 'rejected', 'planned', 'developing', 'testing', 'released', 'closed'];
|
||||
return allowed.includes(value as RequirementStatus) ? value as RequirementStatus : 'pending_review';
|
||||
}
|
||||
|
||||
function toPlanType(value: string): VersionPlan['type'] {
|
||||
return value === 'research' || value === 'ui' ? value : 'product';
|
||||
}
|
||||
|
||||
function toPlanStatus(value?: string | null): VersionPlan['status'] {
|
||||
if (value === 'in_progress' || value === 'completed') return value;
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
function toDevTaskStatus(value?: string | null): DevTaskStatus {
|
||||
if (value === 'in_progress' || value === 'testing' || value === 'submitted') return value;
|
||||
return 'todo';
|
||||
}
|
||||
|
||||
function toTestCaseStatus(value?: string | null): TestCaseStatus {
|
||||
if (value === 'running' || value === 'passed' || value === 'failed' || value === 'blocked') return value;
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
function toBugStatus(value?: string | null): BugStatus {
|
||||
if (value === 'fixing' || value === 'fixed' || value === 'verifying' || value === 'closed' || value === 'rejected') {
|
||||
return value;
|
||||
}
|
||||
return 'open';
|
||||
}
|
||||
|
||||
function toBugSeverity(value?: string | null): BugSeverity {
|
||||
if (value === 'critical' || value === 'major' || value === 'trivial') return value;
|
||||
return 'minor';
|
||||
}
|
||||
Reference in New Issue
Block a user