Files
ftb-project-management/apps/web/lib/version-data-scope.ts
2e595c7e72
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
refactor(data): 收口关系表运行时数据源
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读
- 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移
- 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-07-09 14:59:49 +08:00

202 lines
6.1 KiB
TypeScript

import type { Bug } from './bug';
import type { DevTask } from './dev-task';
import type { OvertimeRecord } from './overtime';
import type { Requirement } from './requirement';
import type { TestCase } from './test-case';
import type { VersionPlan } from './version-plan';
export interface VersionDataScope {
requirements: Requirement[];
requirementIds: string[];
requirementIdSet: Set<string>;
plans: VersionPlan[];
plansByType: Record<VersionPlan['type'], VersionPlan[]>;
devTasks: DevTask[];
testCases: TestCase[];
bugs: Bug[];
overtimeRecords: OvertimeRecord[];
}
interface VersionDataScopeInput {
versionId: string;
requirements: Requirement[];
plans: VersionPlan[];
devTasks: DevTask[];
testCases: TestCase[];
bugs: Bug[];
overtimeRecords?: OvertimeRecord[];
}
interface VersionDataScopeMapInput extends Omit<VersionDataScopeInput, 'versionId'> {
versionIds: string[];
}
export function buildVersionDataScope(input: VersionDataScopeInput): VersionDataScope {
const {
versionId,
requirements,
plans,
devTasks,
testCases,
bugs,
overtimeRecords = [],
} = input;
const scopedRequirements: Requirement[] = [];
const requirementIds: string[] = [];
const requirementIdSet = new Set<string>();
for (const requirement of requirements) {
if (requirement.versionId !== versionId) continue;
scopedRequirements.push(requirement);
requirementIds.push(requirement.id);
requirementIdSet.add(requirement.id);
}
const scopedPlans: VersionPlan[] = [];
const plansByType: Record<VersionPlan['type'], VersionPlan[]> = {
research: [],
product: [],
ui: [],
};
for (const plan of plans) {
if (plan.versionId !== versionId) continue;
scopedPlans.push(plan);
plansByType[plan.type].push(plan);
}
return {
requirements: scopedRequirements,
requirementIds,
requirementIdSet,
plans: scopedPlans,
plansByType,
devTasks: devTasks.filter((task) => task.versionId === versionId || (!task.versionId && requirementIdSet.has(task.requirementId))),
testCases: testCases.filter((testCase) => testCase.versionId === versionId),
bugs: bugs.filter((bug) => bug.versionId === versionId),
overtimeRecords: overtimeRecords.filter((record) => record.versionId === versionId),
};
}
export function hasVersionDataScopeRows(scope: VersionDataScope | null | undefined): scope is VersionDataScope {
if (!scope) return false;
return (
scope.requirements.length > 0 ||
scope.plans.length > 0 ||
scope.devTasks.length > 0 ||
scope.testCases.length > 0 ||
scope.bugs.length > 0
);
}
export function selectVersionDataScope(input: {
storeScope: VersionDataScope | null;
v22Scope?: VersionDataScope | null;
}): VersionDataScope | null {
const { storeScope, v22Scope } = input;
if (hasVersionDataScopeRows(v22Scope)) {
const requirements = overlayRowsById(v22Scope.requirements, storeScope?.requirements ?? []);
const plans = overlayRowsById(v22Scope.plans, storeScope?.plans ?? []);
return {
...v22Scope,
requirements,
requirementIds: requirements.map((item) => item.id),
requirementIdSet: new Set(requirements.map((item) => item.id)),
plans,
plansByType: groupPlansByType(plans),
devTasks: overlayRowsById(v22Scope.devTasks, storeScope?.devTasks ?? []),
testCases: overlayRowsById(v22Scope.testCases, storeScope?.testCases ?? []),
bugs: overlayRowsById(v22Scope.bugs, storeScope?.bugs ?? []),
overtimeRecords: storeScope?.overtimeRecords ?? v22Scope.overtimeRecords,
};
}
return storeScope;
}
export function buildVersionDataScopeMap(input: VersionDataScopeMapInput): Record<string, VersionDataScope> {
const scopes: Record<string, VersionDataScope> = {};
const targetVersionIds = new Set(input.versionIds);
for (const versionId of input.versionIds) {
scopes[versionId] = createEmptyScope();
}
const requirementVersionMap = new Map<string, string>();
for (const requirement of input.requirements) {
const versionId = requirement.versionId;
if (!versionId) continue;
requirementVersionMap.set(requirement.id, versionId);
const scope = scopes[versionId];
if (!scope) continue;
scope.requirements.push(requirement);
scope.requirementIds.push(requirement.id);
scope.requirementIdSet.add(requirement.id);
}
for (const plan of input.plans) {
const scope = scopes[plan.versionId];
if (!scope) continue;
scope.plans.push(plan);
scope.plansByType[plan.type].push(plan);
}
for (const task of input.devTasks) {
const versionId = task.versionId || requirementVersionMap.get(task.requirementId);
if (!versionId || !targetVersionIds.has(versionId)) continue;
scopes[versionId]?.devTasks.push(task);
}
for (const testCase of input.testCases) {
scopes[testCase.versionId]?.testCases.push(testCase);
}
for (const bug of input.bugs) {
scopes[bug.versionId]?.bugs.push(bug);
}
for (const record of input.overtimeRecords ?? []) {
if (!record.versionId) continue;
scopes[record.versionId]?.overtimeRecords.push(record);
}
return scopes;
}
function createEmptyScope(): VersionDataScope {
return {
requirements: [],
requirementIds: [],
requirementIdSet: new Set<string>(),
plans: [],
plansByType: {
research: [],
product: [],
ui: [],
},
devTasks: [],
testCases: [],
bugs: [],
overtimeRecords: [],
};
}
function overlayRowsById<T extends { id: string }>(baseRows: T[], overlayRows: T[]): T[] {
if (baseRows.length === 0) return baseRows;
const overlayById = new Map(overlayRows.map((item) => [item.id, item]));
const seen = new Set<string>();
const merged = baseRows.map((item) => {
seen.add(item.id);
return overlayById.get(item.id) ?? item;
});
for (const item of overlayRows) {
if (!seen.has(item.id)) merged.push(item);
}
return merged;
}
function groupPlansByType(plans: VersionPlan[]): Record<VersionPlan['type'], VersionPlan[]> {
return {
research: plans.filter((plan) => plan.type === 'research'),
product: plans.filter((plan) => plan.type === 'product'),
ui: plans.filter((plan) => plan.type === 'ui'),
};
}