本次提交包含以下修改: 1. 新增微信登录相关服务及页面逻辑,实现首次微信绑定手机号流程 2. 优化产品匹配卡片 UI,移除不必要状态显示 3. 调整首页 scroll-view 属性及 banner 背景样式 4. 完善积分规则校验逻辑,增加时区及策略格式验证 5. 更新协议草案文档,补充资料清单及功能说明 6. 修复分析结果路由路径,适配新的匹配结果页面 7. 调整品牌 Logo 安卓端字体对齐样式
159 lines
9.5 KiB
Plaintext
159 lines
9.5 KiB
Plaintext
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
|
|
}
|