import { getSession, requestJson } from '@/services/api.uts' export type PointsSummary = { balance: number, serverDate: string, timezone: string, signedToday: boolean, checkInEnabled: boolean, ruleVersion: string | null, checkInReward: number | null, balanceState: string } export type PointsRules = { status: string, ruleVersion: string | null, checkInReward: number | null, timezone: string, publishedRules: UTSJSONObject | null } export type PointsTask = { task: string, status: string, reward: number | null, ruleVersion: string | null } export type PointsLedgerItem = { ledgerId: string, delta: number, balanceBefore: number, balanceAfter: number, businessType: string, businessDate: string | null, ruleVersion: string | null, createdAt: string | null } export type PointsLedgerPage = { items: PointsLedgerItem[], snapshotId: string, nextBeforeId: string | null } export type PointsTaskPage = { serverDate: string, timezone: string, items: PointsTask[] } export type CheckInRequest = { requestId: string, expectedRuleVersion: string, expectedDate: string } export type CheckInReceipt = { ledgerId: string, delta: number, balanceAfter: number, signedDate: string, ruleVersion: string, replayed: boolean } const text = (data: UTSJSONObject, key: string): string => data.getString(key) ?? '' const nullableText = (data: UTSJSONObject, key: string): string | null => data.getString(key) const integer = (data: UTSJSONObject, key: string): number => { const value = data.getNumber(key) if (value == null || !Number.isInteger(value)) throw new Error('积分响应格式无效') return value } const nullableInteger = (data: UTSJSONObject, key: string): number | null => { const value = data.getNumber(key) if (value != null && (!Number.isInteger(value) || value < 1 || value > 1000)) throw new Error('积分奖励格式无效') return value } const requiredDate = (value: string): string => { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new Error('积分日期格式无效') return value } const validId = (value: string, allowZero: boolean): boolean => { if (!(allowZero ? /^(?:0|[1-9][0-9]{0,9})$/.test(value) : /^[1-9][0-9]{0,9}$/.test(value))) return false return !(value.length == 10 && value > '4294967295') } const requiredId = (value: string): string => { if (!validId(value, false)) throw new Error('积分流水编号无效') return value } const snapshotId = (value: string): string => { if (!validId(value, true)) throw new Error('积分快照编号无效') return value } const optionalId = (value: string | null): string | null => value == null || value.length == 0 ? null : requiredId(value) const ensureSession = (): void => { if (getSession() == null) throw new Error('请先登录后查看积分') } const ensureStatus = (value: string): string => { if (!['draft', 'published', 'paused'].includes(value)) throw new Error('积分规则状态无效') return value } const ensureTaskStatus = (value: string): string => { if (!['available', 'completed', 'unavailable'].includes(value)) throw new Error('积分任务状态无效') return value } function summaryFrom(data: UTSJSONObject): PointsSummary { const serverDate = requiredDate(text(data, 'serverDate')) const ruleVersion = nullableText(data, 'ruleVersion') const reward = nullableInteger(data, 'checkInReward') const balanceState = text(data, 'balanceState') if (!['normal', 'anomaly'].includes(balanceState)) throw new Error('积分余额状态无效') const signedToday = data.getBoolean('signedToday') const checkInEnabled = data.getBoolean('checkInEnabled') if (signedToday == null || checkInEnabled == null) throw new Error('积分响应格式无效') return { balance: integer(data, 'balance'), serverDate, timezone: text(data, 'timezone'), signedToday, checkInEnabled, ruleVersion, checkInReward: reward, balanceState } } export async function fetchPointsSummary(): Promise { ensureSession() return summaryFrom(await requestJson('/points', 'GET', {})) } export async function fetchPointsRules(): Promise { ensureSession() const data = await requestJson('/points/rules', 'GET', {}) const status = ensureStatus(text(data, 'status')) const ruleVersion = nullableText(data, 'ruleVersion') const checkInReward = nullableInteger(data, 'checkInReward') const timezone = text(data, 'timezone') const published = data['publishedRules'] if (timezone != 'Asia/Shanghai') throw new Error('积分规则时区无效') if (status == 'draft') { if (data['ruleVersion'] !== null || data['checkInReward'] !== null || published !== null) throw new Error('积分规则状态不一致') return { status, ruleVersion: null, checkInReward: null, timezone, publishedRules: null } } if (ruleVersion == null || !/^[a-z][a-z0-9-]{1,63}$/.test(ruleVersion) || checkInReward == null || published == null || typeof published != 'object' || Array.isArray(published)) throw new Error('积分规则格式无效') const policy = published as UTSJSONObject // 展示的权益必须与服务端已实现的策略一致,未知策略不能套用一期说明。 if (policy.getNumber('dailyLimit') != 1 || policy.getBoolean('makeUpAllowed') != false || policy.getBoolean('consecutiveBonusEnabled') != false || policy.getBoolean('profileRewardEnabled') != false || policy.getBoolean('invitationRewardEnabled') != false || policy.getString('expiryPolicy') != 'none' || policy.getString('levelPolicy') != 'unchanged' || policy.getString('benefitsPolicy') != 'none') throw new Error('积分规则暂不支持,请稍后重试') return { status, ruleVersion, checkInReward, timezone, publishedRules: policy } } function ledgerFrom(data: UTSJSONObject): PointsLedgerItem { const businessDate = nullableText(data, 'businessDate') const businessType = text(data, 'businessType') if (businessDate != null) requiredDate(businessDate) if (!['checkin', 'legacy'].includes(businessType)) throw new Error('积分流水类型无效') return { ledgerId: requiredId(text(data, 'ledgerId')), delta: integer(data, 'delta'), balanceBefore: integer(data, 'balanceBefore'), balanceAfter: integer(data, 'balanceAfter'), businessType, businessDate, ruleVersion: nullableText(data, 'ruleVersion'), createdAt: nullableText(data, 'createdAt') } } export async function fetchPointsLedger(limit: number = 50, snapshot: string = '', before: string = ''): Promise { ensureSession() if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw new Error('积分流水分页参数无效') const payload: UTSJSONObject = { limit: limit.toString() } if (snapshot.length > 0) payload['snapshotId'] = snapshotId(snapshot) if (before.length > 0) payload['beforeId'] = requiredId(before) const data = await requestJson('/points/ledger', 'GET', payload) const rows = data['items'] if (rows == null || !Array.isArray(rows)) throw new Error('积分流水格式无效') return { items: (rows as UTSJSONObject[]).map((row: UTSJSONObject): PointsLedgerItem => ledgerFrom(row)), snapshotId: snapshotId(text(data, 'snapshotId')), nextBeforeId: optionalId(data.getString('nextBeforeId')) } } export async function fetchPointsTasks(): Promise { ensureSession() const data = await requestJson('/points/tasks', 'GET', {}) const rows = data['items'] if (rows == null || !Array.isArray(rows)) throw new Error('积分任务格式无效') return { serverDate: requiredDate(text(data, 'serverDate')), timezone: text(data, 'timezone'), items: (rows as UTSJSONObject[]).map((row: UTSJSONObject): PointsTask => ({ task: text(row, 'task'), status: ensureTaskStatus(text(row, 'status')), reward: nullableInteger(row, 'reward'), ruleVersion: nullableText(row, 'ruleVersion') })) } } export function createCheckInRequest(summary: PointsSummary): CheckInRequest { if (!summary.checkInEnabled || summary.signedToday || summary.ruleVersion == null || summary.checkInReward == null) throw new Error('签到活动暂不可用') let value = '' for (let index = 0; index < 32; index++) value += Math.floor(Math.random() * 16).toString(16) return { requestId: 'pts_' + value, expectedRuleVersion: summary.ruleVersion, expectedDate: summary.serverDate } } export async function submitPointsCheckIn(input: CheckInRequest): Promise { ensureSession() if (!/^pts_[0-9a-f]{32}$/.test(input.requestId) || !/^[a-z][a-z0-9-]{1,63}$/.test(input.expectedRuleVersion)) throw new Error('签到请求参数无效') requiredDate(input.expectedDate) const data = await requestJson('/points/check-in', 'POST', { requestId: input.requestId, expectedRuleVersion: input.expectedRuleVersion, expectedDate: input.expectedDate }) const replayed = data.getBoolean('replayed') if (replayed == null) throw new Error('签到回执格式无效') const receipt: CheckInReceipt = { ledgerId: requiredId(text(data, 'ledgerId')), delta: integer(data, 'delta'), balanceAfter: integer(data, 'balanceAfter'), signedDate: requiredDate(text(data, 'signedDate')), ruleVersion: text(data, 'ruleVersion'), replayed } if (receipt.delta < 1 || receipt.delta > 1000 || receipt.balanceAfter < 0 || receipt.balanceAfter > 2147483647) throw new Error('签到回执格式无效') if (receipt.signedDate != input.expectedDate || receipt.ruleVersion != input.expectedRuleVersion) throw new Error('签到回执与请求不一致') return receipt }