Files
ftb-project-management/apps/web/stores/useMemberStore.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

302 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { create } from 'zustand';
import type { Department, Member, RoleItem, PasswordRule } from '@/lib/members';
import { DEFAULT_PASSWORD_RULE, ensureMemberUsernames, generatePassword, generateUsernameFromName } from '@/lib/members';
import {
SYSTEM_ADMIN_MEMBER,
SYSTEM_ADMIN_ROLE_ID,
canDeleteSystemMember,
ensureSystemAdminMember,
sanitizeSystemAdminPatch,
} from '@/lib/member-system';
import {
createMemberDomain,
deleteMemberDomain,
listMembersDomain,
updateMemberDomain,
} from '@/lib/domain-api';
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, SERVER_DATA_SAVE_ERROR_EVENT } from '@/lib/server-data';
const PRESET_DEPARTMENTS: Department[] = [
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
{ id: 'dept-2', name: '技术部', order: 2, createdAt: '2024-01-01' },
{ id: 'dept-2-1', name: '前端组', parentId: 'dept-2', order: 1, createdAt: '2024-01-01' },
{ id: 'dept-2-2', name: '后端组', parentId: 'dept-2', order: 2, createdAt: '2024-01-01' },
{ id: 'dept-2-3', name: '测试组', parentId: 'dept-2', order: 3, createdAt: '2024-01-01' },
{ id: 'dept-3', name: '设计部', order: 3, createdAt: '2024-01-01' },
{ id: 'dept-4', name: '运营部', order: 4, createdAt: '2024-01-01' },
];
const PRESET_ROLES: RoleItem[] = DEFAULT_ROLE_PRESETS;
const MOCK_MEMBERS: Member[] = [
SYSTEM_ADMIN_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[];
roles: RoleItem[];
passwordRule: PasswordRule;
loading: boolean;
loaded: boolean;
fetchMembers: (options?: { force?: boolean }) => Promise<void>;
updatePasswordRule: (rule: PasswordRule) => void;
createDepartment: (data: Omit<Department, 'id' | 'createdAt'>) => void;
updateDepartment: (id: string, data: Partial<Department>) => void;
deleteDepartment: (id: string) => void;
createMember: (data: Omit<Member, 'id' | 'createdAt'>) => void;
updateMember: (id: string, data: Partial<Member>) => Promise<void>;
deleteMember: (id: string) => void;
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
updateRole: (id: string, data: Partial<RoleItem>) => void;
deleteRole: (id: string) => void;
}
async function saveStored(state: MemberConfigState) {
await saveServerData('members', { departments: state.departments, members: [], roles: state.roles, passwordRule: state.passwordRule });
}
async function loadStored(): Promise<StoredMemberConfig | null> {
try {
return await loadServerData<StoredMemberConfig>('members');
} catch {}
return null;
}
export const useMemberStore = create<MemberState>((set, get) => ({
departments: PRESET_DEPARTMENTS,
members: MOCK_MEMBERS,
roles: PRESET_ROLES,
passwordRule: DEFAULT_PASSWORD_RULE,
loading: false,
loaded: false,
fetchMembers: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
const [cached, domainMembers] = await Promise.all([
loadStored(),
listMembersDomain().catch(() => null),
]);
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
lastMembersFetchAt = Date.now();
if (cached) {
// 迁移:旧 RoleItem 没 permissions 字段,补默认值
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] ?? [] };
}
return r;
});
const permissionMerged = mergePresetRolePermissions(migratedRoles);
const rolesWithNewDefaults = permissionMerged.roles;
const adminRole = PRESET_ROLES.find((r) => r.id === SYSTEM_ADMIN_ROLE_ID)!;
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 sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : get().members;
const ensured = ensureSystemAdminMember(sourceMembers);
const usernameEnsured = ensureMemberUsernames(ensured.members);
const next = {
departments: Array.isArray(cached.departments) ? cached.departments : PRESET_DEPARTMENTS,
members: usernameEnsured.members,
roles,
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
};
set({ ...next, loaded: true });
} else {
const sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : get().members;
const ensured = ensureSystemAdminMember(sourceMembers);
const usernameEnsured = ensureMemberUsernames(ensured.members);
set({ members: usernameEnsured.members, loaded: true });
}
},
updatePasswordRule: (rule) => {
const previous = get().passwordRule;
set({ passwordRule: rule });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, roles: get().roles, passwordRule: rule }),
expected: rule,
getCurrent: () => get().passwordRule,
rollback: () => set({ passwordRule: previous }),
});
},
createDepartment: (data) => {
const previous = get().departments;
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
const departments = [...previous, dept];
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
updateDepartment: (id, data) => {
const previous = get().departments;
const departments = previous.map((d) => d.id === id ? { ...d, ...data } : d);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
deleteDepartment: (id) => {
const previous = get().departments;
const departments = previous.filter((d) => d.id !== id && d.parentId !== id);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
createMember: (data) => {
const previous = get().members;
const member: Member = {
...data,
username: data.username || generateUsernameFromName(data.name, previous.map((m) => m.username ?? '')),
phone: data.phone ?? '',
id: `m-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
};
const members = [...previous, member];
set({ members });
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
const created = await createMemberDomain(member, previous);
set({
members: get().members.map((item) => (item.id === member.id ? { ...member, ...created } : item)),
});
} catch (error) {
notifyMemberSaveFailure(error);
throw error;
}
},
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),
});
},
updateMember: async (id, data) => {
const previousMembers = get().members;
const safeData = sanitizeSystemAdminPatch(id, data);
const updatedMembers = get().members.map((m) => m.id === id ? { ...m, ...safeData } : m);
const ensured = ensureSystemAdminMember(updatedMembers);
set({ members: ensured.members });
try {
await updateMemberDomain(id, safeData);
} catch (error) {
set({ members: previousMembers });
notifyMemberSaveFailure(error);
throw error;
}
},
deleteMember: (id) => {
if (!canDeleteSystemMember(id)) return;
const previous = get().members;
const members = previous.filter((m) => m.id !== id);
set({ members });
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
await deleteMemberDomain(id);
} catch (error) {
notifyMemberSaveFailure(error);
throw error;
}
},
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),
});
},
createRole: (data) => {
const previous = get().roles;
const role: RoleItem = {
...data,
id: `role-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
permissions: data.permissions ?? [],
};
const roles = [...previous, role];
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
updateRole: (id, data) => {
const previous = get().roles;
const roles = previous.map((r) => {
if (r.id !== id) return r;
// 超管不允许改 permissions防误操作锁死系统
if (r.id === 'role-admin' && data.permissions) {
const { permissions, ...rest } = data;
return { ...r, ...rest };
}
return { ...r, ...data };
});
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
deleteRole: (id) => {
if (id === SYSTEM_ADMIN_ROLE_ID) return;
const previous = get().roles;
const roles = previous.filter((r) => r.id !== id);
set({ roles });
scheduleSaveWithOptimisticRollback({
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 服务',
},
}),
);
}