Files

159 lines
9.5 KiB
Plaintext
Raw Permalink Normal View History

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<PointsSummary> {
ensureSession()
return summaryFrom(await requestJson('/points', 'GET', {}))
}
export async function fetchPointsRules(): Promise<PointsRules> {
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<PointsLedgerPage> {
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<PointsTaskPage> {
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<CheckInReceipt> {
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
}