| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074 |
- /**
- * API 工具类
- * 统一管理后端 API 调用
- */
- import { getUserId, getUserToken } from './storage';
- /** 后端返回 detail 为 { code: string } 或旧版字符串 */
- function getDetailCode(detail: unknown): string | undefined {
- if (detail && typeof detail === 'object' && 'code' in detail && typeof (detail as { code: unknown }).code === 'string') {
- return (detail as { code: string }).code;
- }
- if (typeof detail === 'string') return detail;
- return undefined;
- }
- /**
- * 解析 4xx/5xx 响应为带 code 的 ApiError,供前端用 t('error.' + code) 展示
- */
- export class ApiError extends Error {
- code: string;
- status: number;
- constructor(code: string, status: number, message?: string) {
- super(message ?? code);
- this.name = 'ApiError';
- this.code = code;
- this.status = status;
- }
- }
- export async function parseApiError(response: Response, fallbackCode = 'UNKNOWN'): Promise<ApiError> {
- let code = fallbackCode;
- const contentType = response.headers.get('content-type') || '';
- if (contentType.includes('application/json')) {
- try {
- const body = await response.json();
- const detail = body?.detail;
- const parsed = getDetailCode(detail);
- if (parsed) code = parsed;
- } catch {
- // ignore
- }
- }
- return new ApiError(code, response.status, response.statusText);
- }
- /**
- * 获取 API Key(从环境变量读取)
- */
- const getApiKey = (): string => {
- return import.meta.env.VITE_API_CLIENT_KEY || '';
- };
- /**
- * 获取统一的请求头(包含 API Key 与用户会话信息)
- */
- export const getApiHeaders = (): Record<string, string> => {
- const headers: Record<string, string> = {
- 'Content-Type': 'application/json',
- };
-
- const apiKey = getApiKey();
- if (apiKey) {
- headers['X-Client-Key'] = apiKey;
- }
- // 附加用户会话信息(用于后端基于 token 的 HTTP 鉴权)
- const userId = getUserId();
- const token = getUserToken();
- if (userId && token) {
- headers['X-User-Id'] = userId;
- headers['X-User-Token'] = token;
- }
-
- return headers;
- };
- /**
- * 会话确保逻辑:
- * - 若本地已有 userId + token,直接复用(依赖服务端滑动续期,不主动换 token)
- * - 若没有,则通过 /api/session/init 创建一次会话
- * - 使用单例 Promise 防抖,避免并发多次刷新导致 token 频繁变更
- */
- let ensureSessionPromise: Promise<{ userId: string; token: string }> | null = null;
- export const ensureSession = async (): Promise<{ userId: string; token: string }> => {
- // 已有未完成的会话初始化,直接复用
- if (ensureSessionPromise) {
- return ensureSessionPromise;
- }
- // 动态导入,避免循环依赖
- const { getUserId: loadUserId, getUserToken: loadUserToken, setUserId, setUserToken } = await import('./storage');
- const existingUserId = loadUserId();
- const existingToken = loadUserToken();
- // 本地已经有 userId + token,则认为会话有效,直接返回
- if (existingUserId && existingToken) {
- return { userId: existingUserId, token: existingToken };
- }
- // 没有有效会话时,串行化一次 /api/session/init 调用
- ensureSessionPromise = (async () => {
- const resp = await initSession(existingUserId || undefined);
- if (!resp.success || !resp.data?.token || !resp.data?.user_id) {
- throw new Error('Session init failed');
- }
- setUserId(resp.data.user_id);
- setUserToken(resp.data.token);
- return { userId: resp.data.user_id, token: resp.data.token };
- })();
- try {
- return await ensureSessionPromise;
- } finally {
- ensureSessionPromise = null;
- }
- };
- /**
- * 获取后端 API 基础 URL
- */
- export const getApiBaseUrl = (): string => {
- // 开发环境:使用相对路径,通过 Vite 代理访问后端
- // 这样可以避免 Safari 对自签名证书的跨域限制
- if (import.meta.env.DEV) {
- return ''; // 使用相对路径,Vite 会代理到后端
- }
-
- // 生产环境:使用完整 URL
- // 优先使用环境变量指定的 API 地址
- if (import.meta.env.VITE_API_BASE_URL) {
- return import.meta.env.VITE_API_BASE_URL;
- }
-
- // 从 WebSocket URL 推导 API 基础 URL
- const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
-
- // 将 ws:// 或 wss:// 转换为 http:// 或 https://
- const apiUrl = wsUrl
- .replace(/^wss:\/\//, 'https://')
- .replace(/^ws:\/\//, 'http://')
- .replace(/\/ws$/, '');
-
- return apiUrl;
- };
- /**
- * 上报页面访问(每次打开任意界面时调用,保存到后台数据库)
- * 不阻塞 UI,失败静默忽略。
- */
- export const reportPageVisit = (path: string): void => {
- const apiBaseUrl = getApiBaseUrl();
- const body = JSON.stringify({ path: path || '/' });
- fetch(`${apiBaseUrl}/api/page-visit`, {
- method: 'POST',
- headers: getApiHeaders(),
- body,
- }).catch(() => {});
- };
- export interface UserEventPayload {
- event_id: string;
- user_id: string;
- event_name: string;
- event_label?: string;
- event_time: string;
- session_id?: string;
- platform: 'ios' | 'android' | 'web';
- app_version?: string;
- timezone_offset_minutes?: number;
- properties?: Record<string, unknown>;
- }
- /**
- * 上报单条用户事件(user_events)
- * - 不抛异常,失败静默
- * - 开发环境会打印请求与响应状态,便于排查未落库问题
- */
- export const reportUserEvent = (payload: UserEventPayload): void => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/user-events`;
- const headers = getApiHeaders();
- if (import.meta.env.DEV) {
- const hasAuth = !!(headers['X-User-Id'] && headers['X-User-Token']);
- const keys = Object.keys(payload);
- const required = ['event_id', 'user_id', 'event_name', 'event_time', 'platform'];
- const missing = required.filter((k) => !(k in payload) || payload[k as keyof UserEventPayload] == null);
- if (missing.length) {
- console.warn('[user_events] 请求体缺少必填字段:', missing, '当前 keys:', keys);
- }
- console.log('[user_events] 上报:', payload.event_name, { hasAuth, user_id: payload.user_id, keys });
- }
- fetch(url, {
- method: 'POST',
- headers,
- body: JSON.stringify(payload),
- })
- .then(async (res) => {
- if (import.meta.env.DEV && !res.ok) {
- const body = await res.text();
- try {
- const parsed = JSON.parse(body);
- const details = Array.isArray(parsed?.detail) ? parsed.detail : parsed?.detail ? [parsed.detail] : [];
- const first = details[0];
- const loc = first?.loc;
- console.warn('[user_events] 上报失败:', res.status, payload.event_name, '缺失/错误字段 loc:', loc, 'msg:', first?.msg, '详情:', details);
- } catch {
- console.warn('[user_events] 上报失败:', res.status, res.statusText, payload.event_name, body?.slice(0, 200));
- }
- }
- })
- .catch((err) => {
- if (import.meta.env.DEV) {
- console.warn('[user_events] 请求异常:', err?.message ?? err, payload.event_name);
- }
- });
- };
- /**
- * 创建用户
- */
- export interface CreateUserResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- user_type: string;
- user_level: number;
- current_background_image_id?: string;
- created_at: string;
- };
- }
- export const createUser = async (username: string = ''): Promise<CreateUserResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/users/create`;
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CREATE_USER_FAILED');
- }
- const data = await response.json();
-
- // 同步背景图片到 localStorage(包括metadata中的透明度配置)
- if (data.success && data.data?.current_background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.current_background_image_id,
- data.data.current_background_image_metadata
- );
- }
-
- return data;
- } catch (error) {
- console.error('[API] 创建用户异常:', error);
-
- // 如果是网络错误,提供更友好的提示
- if (error instanceof TypeError && error.message.includes('fetch')) {
- throw new Error('网络请求失败,请检查后端服务是否运行');
- }
-
- throw error;
- }
- };
- /**
- * 初始化/刷新会话,返回 user_id + token
- */
- export interface InitSessionResponse {
- success: boolean;
- data: {
- user_id: string;
- token: string;
- expires_at: string;
- username?: string;
- user_level?: number;
- current_background_image_id?: string;
- current_background_image_metadata?: Record<string, any>;
- };
- }
- export const initSession = async (user_id?: string, username?: string): Promise<InitSessionResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/session/init`;
- const body: Record<string, string> = {};
- if (user_id) {
- body.user_id = user_id;
- }
- if (username) {
- body.username = username;
- }
- const response = await fetch(url, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'INIT_SESSION_FAILED');
- }
- return response.json();
- };
- /**
- * 获取用户信息
- */
- export interface GetUserResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- user_type: string;
- user_segment?: string;
- org_id?: string | null;
- is_test_user?: boolean;
- region?: string | null;
- bot_name?: string;
- total_speech_duration_ms?: number;
- user_level?: number;
- current_background_image_id?: string;
- current_background_image_metadata?: Record<string, any>;
- preferred_language?: 'zh' | 'en';
- created_at: string;
- };
- }
- export const getUserInfo = async (user_id: string): Promise<GetUserResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_USER_INFO_FAILED');
- }
- const data = await response.json();
-
- // 同步背景图片到 localStorage(包括metadata中的透明度配置)
- if (data.success && data.data?.current_background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.current_background_image_id,
- data.data.current_background_image_metadata
- );
- }
-
- return data;
- };
- /**
- * 更新用户名
- */
- export interface UpdateUsernameResponse {
- success: boolean;
- message: string;
- }
- export const updateUsername = async (user_id: string, username: string): Promise<UpdateUsernameResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/username`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_USERNAME_FAILED');
- }
- return response.json();
- };
- /**
- * 更新背景图片ID
- */
- export interface UpdateBackgroundImageIdResponse {
- success: boolean;
- message: string;
- }
- export const updateBackgroundImageId = async (
- user_id: string,
- background_image_id: string,
- ): Promise<UpdateBackgroundImageIdResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/background-image-id`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ background_image_id }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_BG_IMAGE_FAILED');
- }
- return response.json();
- };
- /**
- * 更新机器人名字
- */
- export interface UpdateBotNameResponse {
- success: boolean;
- message: string;
- }
- export const updateBotName = async (user_id: string, bot_name: string): Promise<UpdateBotNameResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/bot-name`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ bot_name }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_BOT_NAME_FAILED');
- }
- return response.json();
- };
- /**
- * 用户 UI 状态相关接口
- */
- export interface UserUIState {
- current_page: string | null;
- visited_routes: string[];
- has_shown_tap_to_chat: boolean;
- has_shown_cancel_tooltip: boolean;
- has_shown_send_tooltip: boolean;
- has_shown_progress_bar_tooltip: boolean;
- has_shown_mission_stone_modal: boolean;
- has_shown_conversation_stone_modal: boolean;
- has_clicked_mission_stone: boolean;
- has_clicked_conversation_stone: boolean;
- subtitle_on: boolean;
- avatar_c_unlocked: boolean;
- has_shared_avatar: boolean;
- current_avatar_id?: string | null;
- }
- export interface GetUserUIStateResponse {
- success: boolean;
- data: {
- user_id: string;
- } & UserUIState;
- }
- export const getUserUIState = async (user_id: string): Promise<GetUserUIStateResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_UI_STATE_FAILED');
- }
- return response.json();
- };
- export interface UpdateUserUIStateResponse {
- success: boolean;
- data: {
- user_id: string;
- } & UserUIState;
- }
- export type UpdateUserUIStatePayload = Partial<UserUIState>;
- export const updateUserUIState = async (
- user_id: string,
- payload: UpdateUserUIStatePayload,
- ): Promise<UpdateUserUIStateResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/ui-state`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify(payload),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'UPDATE_UI_STATE_FAILED');
- }
- return response.json();
- };
- /** 亲密度:heart_count 0-3,days_since_last_visit 距上次访问天数 */
- export interface GetIntimacyResponse {
- success: boolean;
- data: { heart_count: number; days_since_last_visit: number };
- }
- export const getIntimacy = async (user_id: string): Promise<GetIntimacyResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_INTIMACY_FAILED');
- }
- return response.json();
- };
- export interface LightUpIntimacyResponse {
- success: boolean;
- data: { heart_count: number };
- }
- export const lightUpIntimacy = async (user_id: string): Promise<LightUpIntimacyResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy/light-up`, {
- method: 'POST',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LIGHT_UP_INTIMACY_FAILED');
- }
- return response.json();
- };
- export interface GetIntimacyStatusResponse {
- success: boolean;
- data: {
- heart_count: number;
- can_light_up_today: boolean;
- };
- }
- export const getIntimacyStatus = async (user_id: string): Promise<GetIntimacyStatusResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/intimacy/status`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_INTIMACY_STATUS_FAILED');
- }
- return response.json();
- };
- /**
- * 注册/登录相关接口
- */
- // 手机号注册
- export interface RegisterPhoneResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- area_code?: string;
- phone: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const registerWithPhone = async (
- phone: string,
- username: string = '',
- user_id?: string | null,
- area_code?: string,
- ): Promise<RegisterPhoneResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const body: { phone: string; username: string; user_id?: string; area_code?: string } = { phone, username };
- if (user_id) {
- body.user_id = user_id;
- }
- if (area_code) {
- body.area_code = area_code;
- }
- const response = await fetch(`${apiBaseUrl}/api/auth/register/phone`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'REGISTER_FAILED');
- }
- return response.json();
- };
- // 邮箱注册
- export interface RegisterEmailResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- email: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const registerWithEmail = async (email: string, username: string = '', user_id?: string | null): Promise<RegisterEmailResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const body: { email: string; username: string; user_id?: string } = { email, username };
- if (user_id) {
- body.user_id = user_id;
- }
- const response = await fetch(`${apiBaseUrl}/api/auth/register/email`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'REGISTER_FAILED');
- }
- return response.json();
- };
- // 微信登录
- export interface LoginWechatResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- wechat_openid?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithWechat = async (openid: string, username: string = ''): Promise<LoginWechatResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/wechat`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ openid, username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // QQ登录
- export interface LoginQQResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- qq_openid?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithQQ = async (openid: string, username: string = ''): Promise<LoginQQResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/qq`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ openid, username }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 手机号登录
- export interface LoginPhoneResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- area_code?: string;
- phone: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithPhone = async (phone: string, area_code?: string): Promise<LoginPhoneResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/phone`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ phone, area_code }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 邮箱登录
- export interface LoginEmailResponse {
- success: boolean;
- data: {
- user_id: string;
- username: string;
- email: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- created_at: string;
- };
- }
- export const loginWithEmail = async (email: string): Promise<LoginEmailResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/login/email`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ email }),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'LOGIN_FAILED');
- }
- return response.json();
- };
- // 发送验证码
- export interface SendCodeRequest {
- area_code?: string; // 手机区号(如 +86)
- phone?: string;
- email?: string;
- send_type: 'domestic_sms' | 'international_sms' | 'email';
- }
- export interface SendCodeResponse {
- success: boolean;
- data: {
- code_id: string;
- message: string;
- };
- }
- export const sendCode = async (request: SendCodeRequest): Promise<SendCodeResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/send-code`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'SEND_CODE_FAILED');
- }
- return response.json();
- };
- // 验证验证码
- export interface VerifyCodeRequest {
- phone?: string;
- email?: string;
- code: string;
- send_type: 'domestic_sms' | 'international_sms' | 'email';
- }
- export interface VerifyCodeResponse {
- success: boolean;
- data: {
- valid: boolean;
- message: string;
- };
- }
- export const verifyCode = async (request: VerifyCodeRequest): Promise<VerifyCodeResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/verify-code`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'VERIFY_CODE_FAILED');
- }
- return response.json();
- };
- // V2.0:验证码通过后统一登录/注册
- export interface VerifyAndAuthRequest {
- type: 'email' | 'phone';
- code: string;
- email?: string;
- phone?: string;
- area_code?: string;
- preferred_language?: 'zh' | 'en';
- }
- export interface VerifyAndAuthResponse {
- success: boolean;
- data: {
- user_id: string;
- username?: string;
- email?: string;
- phone?: string;
- user_type: string;
- user_level?: number;
- token?: string;
- expires_at?: string;
- is_new_user: boolean;
- [key: string]: unknown;
- };
- }
- export const verifyAndAuth = async (request: VerifyAndAuthRequest): Promise<VerifyAndAuthResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/auth/verify-and-auth`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify(request),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CODE_INVALID');
- }
- return response.json();
- };
- // V2.0:获取「退出早期共创社区」选项(登录页回显)
- export const getCommunityChoice = async (userId: string): Promise<{ success: boolean; data: { opted_out?: boolean } }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/community-choice`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:更新「退出早期共创社区」选项
- export const updateCommunityChoice = async (userId: string, optedOut: boolean): Promise<{ success: boolean }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/community-choice`, {
- method: 'PUT',
- headers: getApiHeaders(),
- body: JSON.stringify({ opted_out: optedOut }),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:获取早期共创状态
- export const getEarlyCommunity = async (userId: string): Promise<{ success: boolean; data: { popup_shown?: boolean; email?: string | null } }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/early-community`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- // V2.0:提交早期共创(标记弹窗已展示,可选留邮箱)
- export const submitEarlyCommunity = async (userId: string, email?: string | null): Promise<{ success: boolean; data: Record<string, unknown> }> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${userId}/early-community`, {
- method: 'POST',
- headers: getApiHeaders(),
- body: JSON.stringify({ email: email || undefined }),
- });
- if (!response.ok) throw await parseApiError(response, 'SERVER_ERROR');
- return response.json();
- };
- /**
- * OAuth 授权相关接口
- */
- // 获取微信授权URL
- export const getWechatAuthUrl = (): string => {
- const apiBaseUrl = getApiBaseUrl();
- return `${apiBaseUrl}/api/auth/wechat/authorize`;
- };
- // 获取QQ授权URL
- export const getQQAuthUrl = (from?: string, user_id?: string | null): string => {
- const apiBaseUrl = getApiBaseUrl();
- const url = `${apiBaseUrl}/api/auth/qq/authorize`;
- const params = new URLSearchParams();
- if (from) {
- params.append('from', from);
- }
- if (user_id) {
- params.append('user_id', user_id);
- }
- const queryString = params.toString();
- return queryString ? `${url}?${queryString}` : url;
- };
- /**
- * 头像 GIF 配置
- */
- export interface AvatarGifSet {
- default: string;
- positive: string;
- negative: string;
- talking: string;
- }
- export interface AvatarGifConfigResponse {
- success: boolean;
- data: {
- avatar_default?: Partial<AvatarGifSet>;
- avatar_registered_cap?: Partial<AvatarGifSet>;
- };
- }
- export const getAvatarGifs = async (): Promise<AvatarGifConfigResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/avatar-gifs`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_AVATARS_FAILED');
- }
- return response.json();
- };
- /**
- * 分享图片
- */
- export interface ShareImageItem {
- id: string;
- url: string;
- title?: string;
- sort_order?: number;
- is_active?: boolean;
- }
- export interface GetShareImagesResponse {
- success: boolean;
- data: ShareImageItem[];
- }
- export const getShareImages = async (): Promise<GetShareImagesResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/share-images`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_CONFIG_FAILED');
- }
- return response.json();
- };
- /**
- * 领取分享奖励
- */
- export interface ClaimShareRewardResponse {
- success: boolean;
- data?: {
- user_id: string;
- background_image_id: string;
- background_image_metadata?: Record<string, any>;
- message: string;
- };
- message?: string;
- }
- export const claimShareReward = async (user_id: string): Promise<ClaimShareRewardResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/users/${user_id}/claim-share-reward`, {
- method: 'POST',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'CLAIM_SHARE_REWARD_FAILED');
- }
- const data = await response.json();
-
- // 如果成功,同步背景图片到 localStorage
- if (data.success && data.data?.background_image_id) {
- const { syncBackgroundImageToStorage } = await import('./storage');
- syncBackgroundImageToStorage(
- data.data.background_image_id,
- data.data.background_image_metadata || { opacity: 0.5 } // 使用后端返回的metadata,或默认透明度
- );
- }
-
- return data;
- };
- /**
- * 获取所有背景图片配置
- */
- export interface BackgroundImageConfig {
- image_id: string;
- image_name: string;
- image_url: string;
- thumbnail_url?: string;
- unlock_type: 'default' | 'level' | 'purchase' | 'mission';
- unlock_level?: number;
- unlock_mission_type?: string;
- display_order: number;
- is_active: boolean;
- description?: string;
- metadata?: Record<string, any>;
- created_at?: string;
- updated_at?: string;
- }
- export interface GetBackgroundImagesResponse {
- success: boolean;
- data: BackgroundImageConfig[];
- }
- export const getBackgroundImages = async (): Promise<GetBackgroundImagesResponse> => {
- const apiBaseUrl = getApiBaseUrl();
- const response = await fetch(`${apiBaseUrl}/api/background-images`, {
- method: 'GET',
- headers: getApiHeaders(),
- });
- if (!response.ok) {
- throw await parseApiError(response, 'GET_BG_CONFIG_FAILED');
- }
- return response.json();
- };
|