refactor(data): 收口关系表运行时数据源
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -9,8 +9,12 @@ import {
|
||||
updateRequirementByProductId,
|
||||
updateRequirementStatusByProductId,
|
||||
} from '@/lib/domain-api';
|
||||
import { api } from '@/lib/api';
|
||||
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
|
||||
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import { SERVER_DATA_CACHE_MS } from '@/lib/server-data';
|
||||
import { loadV22RequirementsPage, type V22RequirementsQuery } from '@/lib/v22-api';
|
||||
import { useAuthStore } from './useAuthStore';
|
||||
import { useMemberStore } from './useMemberStore';
|
||||
|
||||
interface RequirementState {
|
||||
requirements: Requirement[];
|
||||
@@ -19,7 +23,7 @@ interface RequirementState {
|
||||
platforms: DictItem[];
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
fetchRequirements: (options?: { force?: boolean }) => Promise<void>;
|
||||
fetchRequirements: (options?: { force?: boolean; productId?: string; projectId?: string; versionId?: string }) => Promise<void>;
|
||||
createRequirement: (data: Omit<Requirement, 'id' | 'code' | 'createdAt'>) => void;
|
||||
updateRequirement: (id: string, data: Partial<Requirement>) => void;
|
||||
deleteRequirement: (id: string, productId?: string) => void;
|
||||
@@ -62,11 +66,13 @@ const PRESET_PLATFORMS: DictItem[] = [
|
||||
const MOCK_REQUIREMENTS: Requirement[] = [];
|
||||
let lastRequirementsFetchAt = 0;
|
||||
|
||||
async function loadStored(): Promise<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] } | null> {
|
||||
try {
|
||||
return await loadServerData<{ requirements: Requirement[]; sourceTargets: SourceTarget[]; types: DictItem[]; platforms: DictItem[] }>('requirements');
|
||||
} catch {}
|
||||
return null;
|
||||
type RequirementGovernanceKind = 'requirement_type' | 'requirement_platform' | 'requirement_source';
|
||||
|
||||
interface GovernanceDictionaryRow {
|
||||
id: string;
|
||||
name: string;
|
||||
group?: string | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// --- Store ---
|
||||
@@ -81,19 +87,27 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
loaded: false,
|
||||
|
||||
fetchRequirements: async (options) => {
|
||||
if (!options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const cached = await loadStored();
|
||||
if (!options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const hasRelationScope = Boolean(options?.productId);
|
||||
if (!hasRelationScope && !options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
const [governed, scopedRequirements] = await Promise.all([
|
||||
loadRequirementGovernanceDictionaries().catch(() => null),
|
||||
hasRelationScope ? loadScopedRequirementsFromRelation(options).catch(() => null) : Promise.resolve(null),
|
||||
]);
|
||||
if (!hasRelationScope && !options?.force && get().loaded && Date.now() - lastRequirementsFetchAt < SERVER_DATA_CACHE_MS) return;
|
||||
lastRequirementsFetchAt = Date.now();
|
||||
if (cached) {
|
||||
set({ requirements: cached.requirements, sourceTargets: cached.sourceTargets, types: cached.types, platforms: cached.platforms, loaded: true });
|
||||
} else {
|
||||
set({ loaded: true });
|
||||
}
|
||||
set({
|
||||
requirements: scopedRequirements
|
||||
? mergeRequirementsForScope(get().requirements, options, scopedRequirements)
|
||||
: get().requirements,
|
||||
sourceTargets: governed?.sourceTargets ?? PRESET_SOURCE_TARGETS,
|
||||
types: governed?.types ?? PRESET_TYPES,
|
||||
platforms: governed?.platforms ?? PRESET_PLATFORMS,
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
|
||||
createRequirement: (data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { requirements } = get();
|
||||
const code = `REQ-${String(requirements.length + 1).padStart(3, '0')}`;
|
||||
const newReq: Requirement = {
|
||||
...data,
|
||||
@@ -106,17 +120,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
set({ requirements: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: async () => {
|
||||
try {
|
||||
const created = await createRequirementByProductId(newReq.productId, newReq);
|
||||
set({
|
||||
requirements: get().requirements.map((item) => (
|
||||
item.id === newReq.id ? { ...created, creator: newReq.creator || created.creator } : item
|
||||
)),
|
||||
loaded: true,
|
||||
});
|
||||
} catch {
|
||||
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
||||
}
|
||||
const created = await createRequirementByProductId(newReq.productId, newReq);
|
||||
set({
|
||||
requirements: get().requirements.map((item) => (
|
||||
item.id === newReq.id ? { ...created, creator: newReq.creator || created.creator } : item
|
||||
)),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().requirements,
|
||||
@@ -125,29 +135,24 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
updateRequirement: (id, data) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { requirements } = get();
|
||||
const current = requirements.find((r) => r.id === id);
|
||||
const productId = data.productId ?? current?.productId;
|
||||
const updated = requirements.map((r) => (r.id === id ? { ...r, ...data } : r));
|
||||
set({ requirements: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: async () => {
|
||||
try {
|
||||
if (!productId) throw new Error('missing requirement productId');
|
||||
const domainPatch = { ...data };
|
||||
const status = domainPatch.status;
|
||||
delete domainPatch.status;
|
||||
delete domainPatch.productId;
|
||||
delete domainPatch.productOwner;
|
||||
delete domainPatch.addedToVersionBy;
|
||||
if (hasDomainPatch(domainPatch)) {
|
||||
await updateRequirementByProductId(productId, id, domainPatch);
|
||||
}
|
||||
if (status) {
|
||||
await updateRequirementStatusByProductId(productId, id, status);
|
||||
}
|
||||
} catch {
|
||||
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
||||
if (!productId) throw new Error('missing requirement productId');
|
||||
const domainPatch = { ...data };
|
||||
const status = domainPatch.status;
|
||||
delete domainPatch.status;
|
||||
delete domainPatch.productId;
|
||||
delete domainPatch.addedToVersionBy;
|
||||
if (hasDomainPatch(domainPatch)) {
|
||||
await updateRequirementByProductId(productId, id, domainPatch);
|
||||
}
|
||||
if (status) {
|
||||
await updateRequirementStatusByProductId(productId, id, status);
|
||||
}
|
||||
},
|
||||
expected: updated,
|
||||
@@ -157,18 +162,14 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
deleteRequirement: (id, scopedProductId) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { requirements } = get();
|
||||
const productId = scopedProductId ?? requirements.find((r) => r.id === id)?.productId;
|
||||
const updated = requirements.filter((r) => r.id !== id);
|
||||
set({ requirements: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: async () => {
|
||||
try {
|
||||
if (!productId) throw new Error('missing requirement productId');
|
||||
await deleteRequirementByProductId(productId, id);
|
||||
} catch {
|
||||
await saveRequirementsFallback({ requirements: updated, sourceTargets, types, platforms });
|
||||
}
|
||||
if (!productId) throw new Error('missing requirement productId');
|
||||
await deleteRequirementByProductId(productId, id);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().requirements,
|
||||
@@ -178,12 +179,20 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
|
||||
// --- SourceTarget dict ---
|
||||
addSourceTarget: (name, sourceType) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: SourceTarget = { id: `st-${Date.now()}`, name, sourceType, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const { sourceTargets } = get();
|
||||
const updated = [...sourceTargets, item];
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
||||
save: async () => {
|
||||
const created = await createGovernanceDictionary('requirement_source', name, sourceType);
|
||||
set({
|
||||
sourceTargets: get().sourceTargets.map((target) => (
|
||||
target.id === item.id ? governanceRowToSourceTarget(created, sourceType) : target
|
||||
)),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().sourceTargets,
|
||||
rollback: () => set({ sourceTargets, loaded: true }),
|
||||
@@ -191,11 +200,14 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
updateSourceTarget: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { sourceTargets } = get();
|
||||
const current = sourceTargets.find((target) => target.id === id);
|
||||
const updated = sourceTargets.map((s) => (s.id === id ? { ...s, name } : s));
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
||||
save: async () => {
|
||||
await updateGovernanceDictionary('requirement_source', id, name, current?.sourceType ?? 'customer');
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().sourceTargets,
|
||||
rollback: () => set({ sourceTargets, loaded: true }),
|
||||
@@ -203,11 +215,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
deleteSourceTarget: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { sourceTargets } = get();
|
||||
const updated = sourceTargets.filter((s) => s.id !== id);
|
||||
set({ sourceTargets: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets: updated, types, platforms }),
|
||||
save: async () => {
|
||||
await deleteGovernanceDictionary('requirement_source', id);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().sourceTargets,
|
||||
rollback: () => set({ sourceTargets, loaded: true }),
|
||||
@@ -216,12 +230,18 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
|
||||
// --- Type dict ---
|
||||
addType: (name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: DictItem = { id: `type-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const { types } = get();
|
||||
const updated = [...types, item];
|
||||
set({ types: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
||||
save: async () => {
|
||||
const created = await createGovernanceDictionary('requirement_type', name);
|
||||
set({
|
||||
types: get().types.map((type) => (type.id === item.id ? governanceRowToDictItem(created) : type)),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().types,
|
||||
rollback: () => set({ types, loaded: true }),
|
||||
@@ -229,11 +249,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
updateType: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { types } = get();
|
||||
const updated = types.map((t) => (t.id === id ? { ...t, name } : t));
|
||||
set({ types: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
||||
save: async () => {
|
||||
await updateGovernanceDictionary('requirement_type', id, name);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().types,
|
||||
rollback: () => set({ types, loaded: true }),
|
||||
@@ -241,11 +263,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
deleteType: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { types } = get();
|
||||
const updated = types.filter((t) => t.id !== id);
|
||||
set({ types: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types: updated, platforms }),
|
||||
save: async () => {
|
||||
await deleteGovernanceDictionary('requirement_type', id);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().types,
|
||||
rollback: () => set({ types, loaded: true }),
|
||||
@@ -254,12 +278,18 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
|
||||
// --- Platform dict ---
|
||||
addPlatform: (name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const item: DictItem = { id: `platform-${Date.now()}`, name, createdAt: new Date().toISOString().slice(0, 10) };
|
||||
const { platforms } = get();
|
||||
const updated = [...platforms, item];
|
||||
set({ platforms: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
||||
save: async () => {
|
||||
const created = await createGovernanceDictionary('requirement_platform', name);
|
||||
set({
|
||||
platforms: get().platforms.map((platform) => (platform.id === item.id ? governanceRowToDictItem(created) : platform)),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().platforms,
|
||||
rollback: () => set({ platforms, loaded: true }),
|
||||
@@ -267,11 +297,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
updatePlatform: (id, name) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { platforms } = get();
|
||||
const updated = platforms.map((p) => (p.id === id ? { ...p, name } : p));
|
||||
set({ platforms: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
||||
save: async () => {
|
||||
await updateGovernanceDictionary('requirement_platform', id, name);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().platforms,
|
||||
rollback: () => set({ platforms, loaded: true }),
|
||||
@@ -279,11 +311,13 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
},
|
||||
|
||||
deletePlatform: (id) => {
|
||||
const { requirements, sourceTargets, types, platforms } = get();
|
||||
const { platforms } = get();
|
||||
const updated = platforms.filter((p) => p.id !== id);
|
||||
set({ platforms: updated, loaded: true });
|
||||
scheduleSaveWithOptimisticRollback({
|
||||
save: () => saveServerData('requirements', { requirements, sourceTargets, types, platforms: updated }),
|
||||
save: async () => {
|
||||
await deleteGovernanceDictionary('requirement_platform', id);
|
||||
},
|
||||
expected: updated,
|
||||
getCurrent: () => get().platforms,
|
||||
rollback: () => set({ platforms, loaded: true }),
|
||||
@@ -292,15 +326,114 @@ export const useRequirementStore = create<RequirementState>((set, get) => {
|
||||
};
|
||||
});
|
||||
|
||||
async function saveRequirementsFallback(data: {
|
||||
requirements: Requirement[];
|
||||
sourceTargets: SourceTarget[];
|
||||
types: DictItem[];
|
||||
platforms: DictItem[];
|
||||
}) {
|
||||
await saveServerData('requirements', data);
|
||||
}
|
||||
|
||||
function hasDomainPatch(data: Partial<Requirement>) {
|
||||
return Object.values(data).some((value) => value !== undefined);
|
||||
}
|
||||
|
||||
async function loadScopedRequirementsFromRelation(
|
||||
options?: { productId?: string; projectId?: string; versionId?: string },
|
||||
): Promise<Requirement[]> {
|
||||
if (!options?.productId) return [];
|
||||
const items: Requirement[] = [];
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
const query: V22RequirementsQuery = {
|
||||
productId: options.productId,
|
||||
projectId: options.projectId,
|
||||
versionId: options.versionId,
|
||||
sort: 'created_at_desc',
|
||||
limit: 500,
|
||||
cursor,
|
||||
};
|
||||
const page = await loadV22RequirementsPage(query);
|
||||
items.push(...page.items);
|
||||
cursor = page.nextCursor;
|
||||
} while (cursor);
|
||||
return items;
|
||||
}
|
||||
|
||||
function mergeRequirementsForScope(
|
||||
current: Requirement[],
|
||||
scope: { productId?: string; projectId?: string; versionId?: string } | undefined,
|
||||
scopedRequirements: Requirement[],
|
||||
) {
|
||||
if (!scope?.productId) return current;
|
||||
return [
|
||||
...current.filter((requirement) => !isRequirementInScope(requirement, scope)),
|
||||
...scopedRequirements,
|
||||
];
|
||||
}
|
||||
|
||||
function isRequirementInScope(
|
||||
requirement: Requirement,
|
||||
scope: { productId?: string; projectId?: string; versionId?: string },
|
||||
) {
|
||||
if (scope.versionId) return requirement.versionId === scope.versionId;
|
||||
if (scope.projectId) return requirement.productId === scope.productId && requirement.projectId === scope.projectId;
|
||||
return requirement.productId === scope.productId;
|
||||
}
|
||||
|
||||
async function loadRequirementGovernanceDictionaries() {
|
||||
const [types, platforms, sourceTargets] = await Promise.all([
|
||||
listGovernanceDictionary('requirement_type'),
|
||||
listGovernanceDictionary('requirement_platform'),
|
||||
listGovernanceDictionary('requirement_source'),
|
||||
]);
|
||||
return {
|
||||
types: types.length > 0 ? types.map(governanceRowToDictItem) : PRESET_TYPES,
|
||||
platforms: platforms.length > 0 ? platforms.map(governanceRowToDictItem) : PRESET_PLATFORMS,
|
||||
sourceTargets: sourceTargets.length > 0 ? sourceTargets.map((row) => governanceRowToSourceTarget(row)) : PRESET_SOURCE_TARGETS,
|
||||
};
|
||||
}
|
||||
|
||||
function listGovernanceDictionary(kind: RequirementGovernanceKind) {
|
||||
return api.get<GovernanceDictionaryRow[]>(`/governance/dictionaries?kind=${kind}`);
|
||||
}
|
||||
|
||||
function createGovernanceDictionary(kind: RequirementGovernanceKind, name: string, group?: string) {
|
||||
const { actorId, permissions } = getGovernanceActorContext();
|
||||
return api.post<GovernanceDictionaryRow>('/governance/dictionaries', { actorId, permissions, kind, name, group });
|
||||
}
|
||||
|
||||
function updateGovernanceDictionary(kind: RequirementGovernanceKind, id: string, name: string, group?: string) {
|
||||
const { actorId, permissions } = getGovernanceActorContext();
|
||||
return api.patch<GovernanceDictionaryRow>(`/governance/dictionaries/${kind}/${id}`, { actorId, permissions, kind, name, group });
|
||||
}
|
||||
|
||||
function deleteGovernanceDictionary(kind: RequirementGovernanceKind, id: string) {
|
||||
const { actorId, permissions } = getGovernanceActorContext();
|
||||
return api.deleteWithBody<GovernanceDictionaryRow>(`/governance/dictionaries/${kind}/${id}`, { actorId, permissions });
|
||||
}
|
||||
|
||||
function getGovernanceActorContext() {
|
||||
const user = useAuthStore.getState().user;
|
||||
const role = useMemberStore.getState().roles.find((item) => item.id === user?.roleId);
|
||||
return { actorId: user?.id ?? '', permissions: role?.permissions ?? [] };
|
||||
}
|
||||
|
||||
function governanceRowToDictItem(row: GovernanceDictionaryRow): DictItem {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
createdAt: normalizeGovernanceCreatedAt(row.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
function governanceRowToSourceTarget(row: GovernanceDictionaryRow, fallbackSourceType?: SourceType): SourceTarget {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
sourceType: normalizeGovernanceSourceType(row.group, fallbackSourceType),
|
||||
createdAt: normalizeGovernanceCreatedAt(row.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGovernanceCreatedAt(value?: string) {
|
||||
return value ? value.slice(0, 10) : new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function normalizeGovernanceSourceType(value?: string | null, fallback: SourceType = 'customer'): SourceType {
|
||||
return ['customer', 'internal', 'operation', 'aftersale', 'market', 'competitor', 'management'].includes(value ?? '')
|
||||
? value as SourceType
|
||||
: fallback;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user