forked from zhouruizhe/gmTouringMiniApp
150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
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}`)
|
||
}
|
||
}
|