feat(v2.4): 完成领域主写迁移

This commit is contained in:
2026-07-08 15:41:10 +08:00
parent 2358dbd0b5
commit da71128ccd
42 changed files with 1410 additions and 81 deletions

View File

@@ -9,6 +9,12 @@ import {
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';
@@ -72,7 +78,10 @@ export const useMemberStore = create<MemberState>((set, get) => ({
fetchMembers: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
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) {
@@ -93,7 +102,8 @@ export const useMemberStore = create<MemberState>((set, get) => ({
permissionMerged.changed ||
roles.length !== rolesWithNewDefaults.length ||
roles.some((role, index) => JSON.stringify(role) !== JSON.stringify(rolesWithNewDefaults[index]));
const ensured = ensureSystemAdminMember(cached.members);
const sourceMembers = domainMembers && domainMembers.length > 0 ? domainMembers : cached.members;
const ensured = ensureSystemAdminMember(sourceMembers);
const usernameEnsured = ensureMemberUsernames(ensured.members);
const next = {
departments: cached.departments,
@@ -104,7 +114,7 @@ export const useMemberStore = create<MemberState>((set, get) => ({
set({ ...next, loaded: true });
if (ensured.changed || usernameEnsured.changed || rolesChanged) void saveStored(next).catch(() => {});
} else {
set({ loaded: true });
set({ members: domainMembers && domainMembers.length > 0 ? domainMembers : get().members, loaded: true });
}
},
@@ -166,7 +176,16 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const members = [...previous, member];
set({ members });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }),
save: async () => {
try {
const created = await createMemberDomain(member, previous);
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 });
}
},
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),
@@ -179,10 +198,14 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const ensured = ensureSystemAdminMember(updatedMembers);
set({ members: ensured.members });
try {
await saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule });
await updateMemberDomain(id, safeData);
} catch (error) {
set({ members: previousMembers });
throw error;
try {
await saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule });
} catch {
set({ members: previousMembers });
throw error;
}
}
},
deleteMember: (id) => {
@@ -191,7 +214,13 @@ export const useMemberStore = create<MemberState>((set, get) => ({
const members = previous.filter((m) => m.id !== id);
set({ members });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }),
save: async () => {
try {
await deleteMemberDomain(id);
} catch {
await saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
}
},
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),

View File

@@ -3,6 +3,12 @@ import { create } from 'zustand';
import type { OvertimeRecord } from '@/lib/overtime';
import type { DictItem } from '@/lib/requirement';
import { calcDuration } from '@/lib/overtime';
import {
createOvertimeRecordDomain,
deleteOvertimeRecordDomain,
listOvertimeRecordsDomain,
updateOvertimeRecordDomain,
} from '@/lib/domain-api';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
@@ -52,10 +58,17 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
fetchRecords: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
const [cached, domainRecords] = await Promise.all([
loadStored(),
listOvertimeRecordsDomain().catch(() => null),
]);
if (!options?.force && get().loaded && Date.now() - lastOvertimeFetchAt < SERVER_DATA_CACHE_MS) return;
lastOvertimeFetchAt = Date.now();
set({ records: cached?.records ?? MOCK_RECORDS, reasons: cached?.reasons ?? PRESET_REASONS, loaded: true });
set({
records: domainRecords && domainRecords.length > 0 ? domainRecords : cached?.records ?? MOCK_RECORDS,
reasons: cached?.reasons ?? PRESET_REASONS,
loaded: true,
});
},
createRecord: (data) => {
@@ -70,7 +83,17 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
const records = [...previous, record];
set({ records, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('overtime', { records, reasons: get().reasons }),
save: async () => {
try {
const created = await createOvertimeRecordDomain(record);
set({
records: get().records.map((item) => (item.id === record.id ? { ...record, ...created } : item)),
loaded: true,
});
} catch {
await saveOvertimeFallback(records, get().reasons);
}
},
expected: records,
getCurrent: () => get().records,
rollback: () => set({ records: previous, loaded: true }),
@@ -89,7 +112,13 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
});
set({ records, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('overtime', { records, reasons: get().reasons }),
save: async () => {
try {
await updateOvertimeRecordDomain(id, records.find((record) => record.id === id) ?? {});
} catch {
await saveOvertimeFallback(records, get().reasons);
}
},
expected: records,
getCurrent: () => get().records,
rollback: () => set({ records: previous, loaded: true }),
@@ -101,7 +130,13 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
const records = previous.filter((r) => r.id !== id);
set({ records, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('overtime', { records, reasons: get().reasons }),
save: async () => {
try {
await deleteOvertimeRecordDomain(id);
} catch {
await saveOvertimeFallback(records, get().reasons);
}
},
expected: records,
getCurrent: () => get().records,
rollback: () => set({ records: previous, loaded: true }),
@@ -145,3 +180,7 @@ export const useOvertimeStore = create<OvertimeState>((set, get) => ({
});
},
}));
async function saveOvertimeFallback(records: OvertimeRecord[], reasons: DictItem[]) {
await saveServerData('overtime', { records, reasons });
}

View File

@@ -2,6 +2,12 @@
import { create } from 'zustand';
import type { TaskCategory, CategoryGroup } from '@/lib/task-category';
import { PRESET_CATEGORIES, normalizeTaskCategories, resolveAiTaskCategoryByName } from '@/lib/task-category';
import {
createTaskCategoryDomain,
deleteTaskCategoryDomain,
listTaskCategoriesDomain,
updateTaskCategoryDomain,
} from '@/lib/domain-api';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
@@ -30,7 +36,7 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
fetchCategories: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
const cached = await listTaskCategoriesDomain().catch(loadStored);
if (!options?.force && get().loaded && Date.now() - lastCategoriesFetchAt < SERVER_DATA_CACHE_MS) return;
lastCategoriesFetchAt = Date.now();
set({ categories: cached ? normalizeTaskCategories(cached) : PRESET_CATEGORIES, loaded: true });
@@ -50,7 +56,17 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
const updated = [...list, item];
set({ categories: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-categories', updated),
save: async () => {
try {
const created = await createTaskCategoryDomain(item);
set({
categories: get().categories.map((category) => (category.id === item.id ? { ...item, ...created } : category)),
loaded: true,
});
} catch {
await saveTaskCategoriesFallback(updated);
}
},
expected: updated,
getCurrent: () => get().categories,
rollback: () => set({ categories: list, loaded: true }),
@@ -63,7 +79,13 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
if (result.created) {
set({ categories: result.categories, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-categories', result.categories),
save: async () => {
try {
await createTaskCategoryDomain(result.category);
} catch {
await saveTaskCategoriesFallback(result.categories);
}
},
expected: result.categories,
getCurrent: () => get().categories,
rollback: () => set({ categories: previous, loaded: true }),
@@ -77,7 +99,13 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
const updated = previous.map((c) => (c.id === id ? { ...c, ...data } : c));
set({ categories: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-categories', updated),
save: async () => {
try {
await updateTaskCategoryDomain(id, data);
} catch {
await saveTaskCategoriesFallback(updated);
}
},
expected: updated,
getCurrent: () => get().categories,
rollback: () => set({ categories: previous, loaded: true }),
@@ -91,7 +119,13 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
const updated = previous.filter((c) => c.id !== id);
set({ categories: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-categories', updated),
save: async () => {
try {
await deleteTaskCategoryDomain(id);
} catch {
await saveTaskCategoriesFallback(updated);
}
},
expected: updated,
getCurrent: () => get().categories,
rollback: () => set({ categories: previous, loaded: true }),
@@ -99,3 +133,7 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
return true;
},
}));
async function saveTaskCategoriesFallback(categories: TaskCategory[]) {
await saveServerData('task-categories', categories);
}

View File

@@ -1,6 +1,11 @@
'use client';
import { create } from 'zustand';
import type { TaskWorklog } from '@/lib/task-worklog';
import {
createTaskWorklogDomain,
deleteTaskWorklogDomain,
listTaskWorklogsDomain,
} from '@/lib/domain-api';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
@@ -28,7 +33,7 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
fetchWorklogs: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastWorklogsFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
const cached = await listTaskWorklogsDomain().catch(loadStored);
if (!options?.force && get().loaded && Date.now() - lastWorklogsFetchAt < SERVER_DATA_CACHE_MS) return;
lastWorklogsFetchAt = Date.now();
set({ worklogs: cached ?? [], loaded: true });
@@ -44,7 +49,17 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
const updated = [...previous, item];
set({ worklogs: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-worklogs', updated),
save: async () => {
try {
const created = await createTaskWorklogDomain(item);
set({
worklogs: get().worklogs.map((worklog) => (worklog.id === item.id ? { ...item, ...created } : worklog)),
loaded: true,
});
} catch {
await saveTaskWorklogsFallback(updated);
}
},
expected: updated,
getCurrent: () => get().worklogs,
rollback: () => set({ worklogs: previous, loaded: true }),
@@ -56,7 +71,13 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
const updated = previous.filter((w) => w.id !== id);
set({ worklogs: updated, loaded: true });
scheduleSaveWithOptimisticRollback({
save: () => saveServerData('task-worklogs', updated),
save: async () => {
try {
await deleteTaskWorklogDomain(id);
} catch {
await saveTaskWorklogsFallback(updated);
}
},
expected: updated,
getCurrent: () => get().worklogs,
rollback: () => set({ worklogs: previous, loaded: true }),
@@ -69,3 +90,7 @@ export const useTaskWorklogStore = create<TaskWorklogState>((set, get) => ({
.reduce((sum, w) => sum + w.hours, 0);
},
}));
async function saveTaskWorklogsFallback(worklogs: TaskWorklog[]) {
await saveServerData('task-worklogs', worklogs);
}

View File

@@ -3,6 +3,12 @@ import { create } from 'zustand';
import { formatLocalDate } from '@/lib/format';
import { mergeWorkActivities, type WorkActivity, type WorkActivityDraft } from '@/lib/work-activity';
import { makeProgressNoteActivity, type ProgressNoteActivityInput } from '@/lib/work-activity-factory';
import {
createWorkActivityDomain,
deleteWorkActivityDomain,
listWorkActivitiesDomain,
} from '@/lib/domain-api';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
interface WorkActivityState {
@@ -52,13 +58,14 @@ export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
fetchActivities: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
const cached = await listWorkActivitiesDomain().catch(loadStored);
if (!options?.force && get().loaded && Date.now() - lastActivitiesFetchAt < SERVER_DATA_CACHE_MS) return;
lastActivitiesFetchAt = Date.now();
set({ activities: cached ?? [], loaded: true });
},
addActivity: (data) => {
const previous = get().activities;
const now = new Date();
const item: WorkActivity = {
...data,
@@ -66,9 +73,24 @@ export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
date: data.date ?? formatLocalDate(now),
occurredAt: data.occurredAt ?? now.toISOString(),
};
const updated = [...get().activities, item];
const updated = [...previous, item];
set({ activities: updated, loaded: true });
saveStored(updated, (activities) => set({ activities, loaded: true }));
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
const created = await createWorkActivityDomain(item);
set({
activities: get().activities.map((activity) => (activity.id === item.id ? { ...item, ...created } : activity)),
loaded: true,
});
} catch {
await saveStored(updated, (activities) => set({ activities, loaded: true }));
}
},
expected: updated,
getCurrent: () => get().activities,
rollback: () => set({ activities: previous, loaded: true }),
});
return item;
},
@@ -77,8 +99,20 @@ export const useWorkActivityStore = create<WorkActivityState>((set, get) => ({
},
deleteActivity: (id) => {
const updated = get().activities.filter((activity) => activity.id !== id);
const previous = get().activities;
const updated = previous.filter((activity) => activity.id !== id);
set({ activities: updated, loaded: true });
saveStored(updated, (activities) => set({ activities, loaded: true }), { mergeRemote: false });
scheduleSaveWithOptimisticRollback({
save: async () => {
try {
await deleteWorkActivityDomain(id);
} catch {
await saveStored(updated, (activities) => set({ activities, loaded: true }), { mergeRemote: false });
}
},
expected: updated,
getCurrent: () => get().activities,
rollback: () => set({ activities: previous, loaded: true }),
});
},
}));