import { AssessmentProfile, AssessmentRecord, EncryptedAssessmentRecord, MindScopeBackup, } from '@/lib/assessment-types'; import { clearDeviceRecordKey, decryptDeviceRecord, encryptRecordForDevice, isEncryptedRecord, } from '@/lib/record-crypto'; import { buildScoreSummary } from '@/lib/score-summary'; const DB_NAME = 'mindscope'; const DB_VERSION = 1; const PROFILE_STORE = 'profiles'; const RECORD_STORE = 'records'; const ACTIVE_PROFILE_KEY = 'mindscope_active_profile'; function createId(prefix: string) { return `${prefix}_${Date.now()}_${crypto.randomUUID()}`; } function openDatabase(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(PROFILE_STORE)) { db.createObjectStore(PROFILE_STORE, { keyPath: 'id' }); } if (!db.objectStoreNames.contains(RECORD_STORE)) { const records = db.createObjectStore(RECORD_STORE, { keyPath: 'id' }); records.createIndex('profileId', 'profileId'); records.createIndex('questionnaireId', 'questionnaireId'); } }; }); } function requestResult(request: IDBRequest): Promise { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function normalizeRecord(record: AssessmentRecord): AssessmentRecord { if (record.questionnaireVersion && record.scoreVersion && record.scoreSummary?.questionnaireVersion) { return record; } const scoreSummary = buildScoreSummary( record.questionnaireId, record.answers.map((answer) => answer.value), ); return { ...record, questionnaireVersion: record.questionnaireVersion || scoreSummary.questionnaireVersion, scoreVersion: record.scoreVersion || scoreSummary.scoreVersion, scoreSummary: { ...scoreSummary, ...record.scoreSummary, questionnaireVersion: record.scoreSummary?.questionnaireVersion || scoreSummary.questionnaireVersion, scoreVersion: record.scoreSummary?.scoreVersion || scoreSummary.scoreVersion, min: record.scoreSummary?.min ?? scoreSummary.min, max: record.scoreSummary?.max ?? scoreSummary.max, reverseItems: record.scoreSummary?.reverseItems || scoreSummary.reverseItems, direction: record.scoreSummary?.direction || scoreSummary.direction, highScoreMeaning: record.scoreSummary?.highScoreMeaning || scoreSummary.highScoreMeaning, thresholds: record.scoreSummary?.thresholds || scoreSummary.thresholds, scoringStatus: record.scoreSummary?.scoringStatus || scoreSummary.scoringStatus, }, }; } export function getActiveProfileId() { return localStorage.getItem(ACTIVE_PROFILE_KEY); } export function setActiveProfileId(profileId: string) { localStorage.setItem(ACTIVE_PROFILE_KEY, profileId); window.dispatchEvent(new CustomEvent('mindscope-profile-change')); } export async function getProfiles(): Promise { const db = await openDatabase(); const profiles = await requestResult( db.transaction(PROFILE_STORE, 'readonly').objectStore(PROFILE_STORE).getAll(), ); db.close(); return profiles.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); } export async function createProfile(name: string): Promise { const now = new Date().toISOString(); const profile: AssessmentProfile = { id: createId('profile'), name: name.trim(), createdAt: now, updatedAt: now, }; const db = await openDatabase(); await requestResult( db.transaction(PROFILE_STORE, 'readwrite').objectStore(PROFILE_STORE).add(profile), ); db.close(); return profile; } export async function ensureActiveProfile(): Promise { const profiles = await getProfiles(); const activeId = getActiveProfileId(); const active = profiles.find((profile) => profile.id === activeId); if (active) return active; const profile = profiles[0] || (await createProfile('我')); setActiveProfileId(profile.id); return profile; } export async function renameProfile(profileId: string, name: string) { const db = await openDatabase(); const store = db.transaction(PROFILE_STORE, 'readwrite').objectStore(PROFILE_STORE); const profile = await requestResult(store.get(profileId)); if (profile) { await requestResult( store.put({ ...profile, name: name.trim(), updatedAt: new Date().toISOString() }), ); } db.close(); } export async function deleteProfile(profileId: string) { const db = await openDatabase(); const transaction = db.transaction([PROFILE_STORE, RECORD_STORE], 'readwrite'); transaction.objectStore(PROFILE_STORE).delete(profileId); const index = transaction.objectStore(RECORD_STORE).index('profileId'); const recordKeys = await requestResult(index.getAllKeys(profileId)); recordKeys.forEach((key) => transaction.objectStore(RECORD_STORE).delete(key)); await new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); db.close(); } export async function addAssessmentRecord( record: Omit, ): Promise { const completeRecord = { ...record, id: createId('record') }; const encrypted = await encryptRecordForDevice(completeRecord); const db = await openDatabase(); await requestResult( db.transaction(RECORD_STORE, 'readwrite').objectStore(RECORD_STORE).add(encrypted), ); db.close(); return completeRecord; } export async function updateRecordAnalysis(recordId: string, analysisText: string) { const db = await openDatabase(); const store = db.transaction(RECORD_STORE, 'readwrite').objectStore(RECORD_STORE); const stored = await requestResult(store.get(recordId)); const record = stored ? await decryptDeviceRecord(stored) : undefined; if (record && record.analysisText !== analysisText) { await requestResult(store.put(await encryptRecordForDevice({ ...record, analysisText }))); } db.close(); } export async function getRecords(profileId?: string): Promise { const db = await openDatabase(); const store = db.transaction(RECORD_STORE, 'readonly').objectStore(RECORD_STORE); const storedRecords = profileId ? await requestResult(store.index('profileId').getAll(profileId)) : await requestResult(store.getAll()); db.close(); const typedRecords = storedRecords as Array; const records = (await Promise.all(typedRecords.map(decryptDeviceRecord))).map(normalizeRecord); const plaintextRecords = typedRecords.filter((record) => !isEncryptedRecord(record)) as AssessmentRecord[]; if (plaintextRecords.length) { await migratePlaintextRecords(plaintextRecords); } return records.sort((a, b) => b.completedAt.localeCompare(a.completedAt)); } async function migratePlaintextRecords(records: AssessmentRecord[]) { const db = await openDatabase(); const store = db.transaction(RECORD_STORE, 'readwrite').objectStore(RECORD_STORE); for (const record of records) { await requestResult(store.put(await encryptRecordForDevice(record))); } db.close(); } export async function deleteRecord(recordId: string) { const db = await openDatabase(); await requestResult( db.transaction(RECORD_STORE, 'readwrite').objectStore(RECORD_STORE).delete(recordId), ); db.close(); } export async function exportBackup(): Promise { return { format: 'mindscope-backup', version: 1, exportedAt: new Date().toISOString(), profiles: await getProfiles(), records: await getRecords(), }; } export async function importBackup(backup: MindScopeBackup) { if (backup.format !== 'mindscope-backup' || backup.version !== 1) { throw new Error('不支持的备份格式'); } const db = await openDatabase(); const transaction = db.transaction([PROFILE_STORE, RECORD_STORE], 'readwrite'); backup.profiles.forEach((profile) => transaction.objectStore(PROFILE_STORE).put(profile)); for (const record of backup.records) { transaction.objectStore(RECORD_STORE).put(await encryptRecordForDevice(record)); } await new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); db.close(); } export async function clearAllAssessmentData() { const db = await openDatabase(); const transaction = db.transaction([PROFILE_STORE, RECORD_STORE], 'readwrite'); transaction.objectStore(PROFILE_STORE).clear(); transaction.objectStore(RECORD_STORE).clear(); await new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); db.close(); localStorage.removeItem(ACTIVE_PROFILE_KEY); clearDeviceRecordKey(); }