Files
gmTouringMiniApp/src/services/check-in/storage.ts
T

150 lines
4.9 KiB
TypeScript
Raw 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.
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<string, unknown> {
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<string>()
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<string, unknown>): { 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}`)
}
}