import { getCheckInTasks } from '@/data/check-in' import { CHECK_IN_BADGES, CHECK_IN_POINTS, type CheckInBadge, type CheckInProfile, type CheckInRecord, cloneCheckInProfile, createCheckInRecordId, createEmptyCheckInProfile, normalizeCheckInProfile, } from '@/domain/check-in' export const CHECK_IN_STORAGE_KEY = 'guangming:check-in-profile' const CHECK_IN_STORAGE_SCHEMA_VERSION = 2 const LEGACY_CHECK_IN_STORAGE_SCHEMA_VERSION = 1 const envelopeKeys = new Set(['schemaVersion', 'savedAt', 'payload']) interface CheckInStorageEnvelope { schemaVersion: typeof CHECK_IN_STORAGE_SCHEMA_VERSION savedAt: string payload: CheckInProfile } export class CheckInStorageError extends Error { constructor(message: string) { super(message) this.name = 'CheckInStorageError' } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function readLegacyTimestamp(value: unknown): string | null { if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) return null return new Date(value).toISOString() } function migrateLegacyProfile(value: unknown): CheckInProfile | null { if (!isRecord(value) || !Array.isArray(value.checkIns)) return null const enabledPoiIds = new Set(getCheckInTasks().map(task => task.poiId)) const uniquePoiIds = new Set() const checkIns: CheckInRecord[] = [] for (const entry of value.checkIns) { if (!isRecord(entry)) return null const poiId = typeof entry.poiId === 'string' ? entry.poiId.trim() : '' const poiName = typeof entry.poiName === 'string' ? entry.poiName.trim() : '' const checkedInAt = readLegacyTimestamp(entry.checkedInAt) if (!poiId || !poiName || !checkedInAt) return null if (!enabledPoiIds.has(poiId) || uniquePoiIds.has(poiId)) continue uniquePoiIds.add(poiId) checkIns.push({ id: createCheckInRecordId(poiId, checkedInAt), poiId, poiName, checkedInAt, pointsAwarded: CHECK_IN_POINTS, }) } checkIns.sort((left, right) => right.checkedInAt.localeCompare(left.checkedInAt)) const badges: CheckInBadge[] = CHECK_IN_BADGES .filter(definition => checkIns.length >= definition.requiredCheckIns) .map((definition) => { const thresholdRecord = checkIns[checkIns.length - definition.requiredCheckIns] return { code: definition.code, obtainedAt: thresholdRecord.checkedInAt } }) return normalizeCheckInProfile({ points: checkIns.length * CHECK_IN_POINTS, badges, checkIns, }) } function readValidatedProfile(value: Record): { profile: CheckInProfile, migrated: boolean } | null { if (value.schemaVersion === CHECK_IN_STORAGE_SCHEMA_VERSION) { const profile = normalizeCheckInProfile(value.payload) return profile ? { profile, migrated: false } : null } if (value.schemaVersion === LEGACY_CHECK_IN_STORAGE_SCHEMA_VERSION) { const profile = migrateLegacyProfile(value.payload) return profile ? { profile, migrated: true } : null } return null } export function loadCheckInProfile(): CheckInProfile { let value: unknown try { value = uni.getStorageSync(CHECK_IN_STORAGE_KEY) } catch (error) { const reason = error instanceof Error && error.message ? `:${error.message}` : '' throw new CheckInStorageError(`无法读取本机打卡记录${reason}`) } if (value === undefined || value === null || value === '') return createEmptyCheckInProfile() if (!isRecord(value) || Object.keys(value).some(key => !envelopeKeys.has(key)) || typeof value.savedAt !== 'string' || !Number.isFinite(Date.parse(value.savedAt))) { throw new CheckInStorageError('本机打卡记录异常,请先在“我的打卡”中清除后重试') } const result = readValidatedProfile(value) if (!result) throw new CheckInStorageError('本机打卡记录异常,请先在“我的打卡”中清除后重试') if (result.migrated) saveCheckInProfile(result.profile) return result.profile } export function saveCheckInProfile(profile: CheckInProfile): void { const normalized = normalizeCheckInProfile(profile) if (!normalized) throw new Error('打卡记录格式无效,未写入本机') const envelope: CheckInStorageEnvelope = { schemaVersion: CHECK_IN_STORAGE_SCHEMA_VERSION, savedAt: new Date().toISOString(), payload: cloneCheckInProfile(normalized), } try { uni.setStorageSync(CHECK_IN_STORAGE_KEY, envelope) } catch (error) { const reason = error instanceof Error && error.message ? `:${error.message}` : '' throw new Error(`无法保存本机打卡记录${reason}`) } } export function clearCheckInProfile(): void { try { uni.removeStorageSync(CHECK_IN_STORAGE_KEY) } catch (error) { const reason = error instanceof Error && error.message ? `:${error.message}` : '' throw new Error(`无法清除本机打卡记录${reason}`) } }