// 后端 API 客户端:fetch 封装 + token 注入 + 超时 + 错误语义化 import { SETTING_KEYS } from '../constants'; import { getSetting } from '../database/ledgerDB'; import type { AuthUser, FamilyInfo, FamilyMember, LedgerInfo, PushResult, SyncChanges } from './serverTypes'; export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.status = status; } } // 当前配置(server_url + token),未配置返回 null export async function getSyncConfig(): Promise<{ baseUrl: string; token: string } | null> { const [url, token] = await Promise.all([ getSetting(SETTING_KEYS.SYNC_SERVER_URL), getSetting(SETTING_KEYS.SYNC_TOKEN), ]); if (!url || !token) return null; return { baseUrl: url.replace(/\/+$/, ''), token }; } // 底层请求(默认 10s 超时,可传更短的探测超时) async function request( baseUrl: string, path: string, init: { method: string; body?: unknown; token?: string; timeoutMs?: number }, ): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), init.timeoutMs ?? 10_000); try { const res = await fetch(`${baseUrl}${path}`, { method: init.method, headers: { 'Content-Type': 'application/json', ...(init.token ? { Authorization: `Bearer ${init.token}` } : {}), }, body: init.body !== undefined ? JSON.stringify(init.body) : undefined, signal: controller.signal, }); const text = await res.text(); const data = text ? JSON.parse(text) : {}; if (!res.ok) { throw new ApiError(data.error ?? `请求失败(${res.status})`, res.status); } return data as T; } catch (e) { if (e instanceof ApiError) throw e; if ((e as Error).name === 'AbortError') throw new ApiError('连接超时,请检查网络', 0); throw new ApiError('无法连接服务器', 0); } finally { clearTimeout(timer); } } // ===== 认证 ===== export async function apiRegister(baseUrl: string, username: string, password: string, displayName?: string) { return request<{ token: string; user: AuthUser }>(baseUrl, '/api/auth/register', { method: 'POST', body: { username, password, displayName }, }); } export async function apiLogin(baseUrl: string, username: string, password: string) { return request<{ token: string; user: AuthUser }>(baseUrl, '/api/auth/login', { method: 'POST', body: { username, password }, }); } export async function apiMe(baseUrl: string, token: string) { return request<{ user: AuthUser }>(baseUrl, '/api/me', { method: 'GET', token }); } // ===== 家庭 ===== export async function apiCreateFamily(baseUrl: string, token: string, name: string) { return request<{ family: FamilyInfo; user: AuthUser }>(baseUrl, '/api/family', { method: 'POST', body: { name }, token, }); } export async function apiJoinFamily(baseUrl: string, token: string, inviteCode: string) { return request<{ family: FamilyInfo; user: AuthUser }>(baseUrl, '/api/family/join', { method: 'POST', body: { inviteCode: inviteCode.trim().toUpperCase() }, token, }); } export async function apiGetFamily(baseUrl: string, token: string) { return request<{ family: FamilyInfo | null }>(baseUrl, '/api/family', { method: 'GET', token }); } export async function apiFamilyMembers(baseUrl: string, token: string) { return request<{ members: FamilyMember[] }>(baseUrl, '/api/family/members', { method: 'GET', token }); } export async function apiRegenerateInvite(baseUrl: string, token: string) { return request<{ inviteCode: string }>(baseUrl, '/api/family/invite/regenerate', { method: 'POST', token }); } export async function apiLeaveFamily(baseUrl: string, token: string) { return request<{ ok: boolean }>(baseUrl, '/api/family/leave', { method: 'POST', token }); } // owner 移除成员(v0.5) export async function apiRemoveMember(baseUrl: string, token: string, userId: number) { return request<{ ok: boolean }>(baseUrl, `/api/family/members/${userId}`, { method: 'DELETE', token }); } // 修改自己的昵称/头像(v0.5) export async function apiUpdateMe( baseUrl: string, token: string, patch: { displayName?: string; avatarEmoji?: string }, ) { return request<{ user: AuthUser }>(baseUrl, '/api/me', { method: 'PUT', body: patch, token }); } // 修改密码(server 0.5.6):成功返回本端新 token;其它设备的旧 token 因 token_version 递增立即失效 export async function apiChangePassword( baseUrl: string, token: string, currentPassword: string, newPassword: string, ) { return request<{ ok: boolean; token: string }>(baseUrl, '/api/auth/password', { method: 'POST', body: { currentPassword, newPassword }, token, }); } // 注销账号(server 0.5.4 就有接口,安卓端此前一直没有入口): // 需带当前密码复核;创建者注销=解散家庭账本,普通成员=解绑(本机副本保留) export async function apiDeleteAccount(baseUrl: string, token: string, password: string) { return request<{ ok: boolean; dissolvedFamily: boolean; unboundMembers: number }>(baseUrl, '/api/me', { method: 'DELETE', body: { password }, token, }); } // 当前用户的账本列表(个人账本 + 家庭账本) export async function apiGetLedgers(baseUrl: string, token: string) { return request<{ ledgers: LedgerInfo[] }>(baseUrl, '/api/ledgers', { method: 'GET', token }); } // ===== 同步 ===== export async function apiSyncPull(baseUrl: string, token: string, since: number, ledgerId: number) { return request<{ serverTime: number; changes: SyncChanges }>(baseUrl, '/api/sync/pull', { method: 'POST', body: { since, ledgerId }, token, }); } export async function apiSyncPush( baseUrl: string, token: string, changes: Partial, ledgerId: number, ) { return request(baseUrl, '/api/sync/push', { method: 'POST', body: { ...changes, ledgerId }, token, }); } // 健康检查(配置服务器地址时探测可达性;同步页用它刷新「服务器可达」状态,默认 5s 短超时) export async function apiHealth(baseUrl: string, timeoutMs = 5_000) { // version 自服务端 0.5.8 起返回;旧服务端没这个字段,客户端按「未知」处理即可 return request<{ ok: boolean; version?: string; time: number }>(baseUrl, '/api/health', { method: 'GET', timeoutMs, }); }