refactor(data): 收口关系表运行时数据源
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:
2026-07-09 14:59:49 +08:00
parent 1e6fb0c7aa
commit 2e595c7e72
98 changed files with 2938 additions and 1441 deletions

View File

@@ -18,7 +18,7 @@ import {
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { DEFAULT_ROLE_PERMISSIONS, DEFAULT_ROLE_PRESETS } from '@/lib/permissions';
import { mergePresetRolePermissions } from '@/lib/role-permission-migration';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS, SERVER_DATA_SAVE_ERROR_EVENT } from '@/lib/server-data';
const PRESET_DEPARTMENTS: Department[] = [
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
@@ -37,6 +37,19 @@ const MOCK_MEMBERS: Member[] = [
];
let lastMembersFetchAt = 0;
type StoredMemberConfig = {
departments?: Department[];
members?: Member[];
roles?: RoleItem[];
passwordRule?: PasswordRule;
};
type MemberConfigState = {
departments: Department[];
roles: RoleItem[];
passwordRule: PasswordRule;
};
interface MemberState {
departments: Department[];
members: Member[];
@@ -57,13 +70,13 @@ interface MemberState {
deleteRole: (id: string) => void;
}
async function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
await saveServerData('members', state);
async function saveStored(state: MemberConfigState) {
await saveServerData('members', { departments: state.departments, members: [], roles: state.roles, passwordRule: state.passwordRule });
}
async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> {
async function loadStored(): Promise<StoredMemberConfig | null> {
try {
return await loadServerData<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }>('members');
return await loadServerData<StoredMemberConfig>('members');
} catch {}
return null;
}
@@ -86,7 +99,8 @@ export const useMemberStore = create<MemberState>((set, get) => ({
lastMembersFetchAt = Date.now();
if (cached) {
// 迁移:旧 RoleItem 没 permissions 字段,补默认值
const migratedRoles = cached.roles.map((r) => {
const configuredRoles = Array.isArray(cached.roles) ? cached.roles : PRESET_ROLES;
const migratedRoles = configuredRoles.map((r) => {
if (!('permissions' in r) || !Array.isArray((r as any).permissions)) {
return { ...r, permissions: DEFAULT_ROLE_PERMISSIONS[r.id] ?? [] };
}
@@ -98,23 +112,21 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const roles = rolesWithNewDefaults.some((r) => r.id === SYSTEM_ADMIN_ROLE_ID)
? rolesWithNewDefaults.map((r) => r.id === SYSTEM_ADMIN_ROLE_ID ? { ...adminRole, ...r, isSystem: true, permissions: DEFAULT_ROLE_PERMISSIONS[SYSTEM_ADMIN_ROLE_ID] } : r)
: [adminRole, ...rolesWithNewDefaults];
const rolesChanged =
permissionMerged.changed ||
roles.length !== rolesWithNewDefaults.length ||
roles.some((role, index) => JSON.stringify(role) !== JSON.stringify(rolesWithNewDefaults[index]));
const sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : cached.members;
const sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : get().members;
const ensured = ensureSystemAdminMember(sourceMembers);
const usernameEnsured = ensureMemberUsernames(ensured.members);
const next = {
departments: cached.departments,
departments: Array.isArray(cached.departments) ? cached.departments : PRESET_DEPARTMENTS,
members: usernameEnsured.members,
roles,
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
};
set({ ...next, loaded: true });
if (ensured.changed || usernameEnsured.changed || rolesChanged) void saveStored(next).catch(() => {});
} else {
set({ members: domainMembers && domainMembers.length > 0 ? domainMembers : get().members, loaded: true });
const sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : get().members;
const ensured = ensureSystemAdminMember(sourceMembers);
const usernameEnsured = ensureMemberUsernames(ensured.members);
set({ members: usernameEnsured.members, loaded: true });
}
},
@@ -122,7 +134,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const previous = get().passwordRule;
set({ passwordRule: rule });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule }),
save: () => saveStored({ departments: get().departments, roles: get().roles, passwordRule: rule }),
expected: rule,
getCurrent: () => get().passwordRule,
rollback: () => set({ passwordRule: previous }),
@@ -135,7 +147,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const departments = [...previous, dept];
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
@@ -146,7 +158,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const departments = previous.map((d) => d.id === id ? { ...d, ...data } : d);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
@@ -157,7 +169,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const departments = previous.filter((d) => d.id !== id && d.parentId !== id);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
@@ -182,8 +194,9 @@ export const useMemberStore = create<MemberState>((set, get) => ({
set({
members: get().members.map((item) => (item.id === member.id ? { ...member, ...created } : item)),
});
} catch {
await saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
} catch (error) {
notifyMemberSaveFailure(error);
throw error;
}
},
expected: members,
@@ -200,12 +213,9 @@ export const useMemberStore = create<MemberState>((set, get) => ({
try {
await updateMemberDomain(id, safeData);
} catch (error) {
try {
await saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule });
} catch {
set({ members: previousMembers });
throw error;
}
set({ members: previousMembers });
notifyMemberSaveFailure(error);
throw error;
}
},
deleteMember: (id) => {
@@ -217,8 +227,9 @@ export const useMemberStore = create<MemberState>((set, get) => ({
save: async () => {
try {
await deleteMemberDomain(id);
} catch {
await saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
} catch (error) {
notifyMemberSaveFailure(error);
throw error;
}
},
expected: members,
@@ -238,7 +249,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const roles = [...previous, role];
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments: get().departments, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
@@ -257,7 +268,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
});
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments: get().departments, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
@@ -269,10 +280,22 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const roles = previous.filter((r) => r.id !== id);
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
save: () => saveStored({ departments: get().departments, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
}));
function notifyMemberSaveFailure(error: unknown) {
if (typeof window === 'undefined') return;
window.dispatchEvent(
new CustomEvent(SERVER_DATA_SAVE_ERROR_EVENT, {
detail: {
key: 'members',
message: error instanceof Error && error.message ? error.message : '成员/账号保存失败,请检查 API 服务',
},
}),
);
}