feat: 完成从Supabase到本地化架构的迁移\n\n- 添加本地SQLite数据库支持\n- 实现本地认证系统(JWT + bcrypt)\n- 创建Express.js API服务器\n- 实现完整的命理分析算法\n- 替换Supabase客户端为本地API客户端\n- 保持前端接口兼容性\n- 添加本地服务器启动脚本

This commit is contained in:
patdelphi
2025-08-18 09:41:38 +08:00
parent 5e87725cde
commit e806c216af
13 changed files with 3808 additions and 27 deletions

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { User } from '@supabase/supabase-js';
import { User } from '../lib/localApi';
import { supabase } from '../lib/supabase';
interface AuthContextType {
@@ -25,19 +25,26 @@ export function AuthProvider({ children }: AuthProviderProps) {
async function loadUser() {
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
setUser(user);
const response = await supabase.auth.getUser();
if (response.data?.user) {
setUser(response.data.user);
} else {
setUser(null);
}
} catch (error) {
console.error('加载用户信息失败:', error);
setUser(null);
} finally {
setLoading(false);
}
}
loadUser();
// Set up auth listener - KEEP SIMPLE, avoid any async operations in callback
// Set up auth listener - 本地API版本
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
// NEVER use any async operations in callback
setUser(session?.user || null);
setLoading(false);
}
);
@@ -46,18 +53,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
// Auth methods
async function signIn(email: string, password: string) {
return await supabase.auth.signInWithPassword({ email, password });
const response = await supabase.auth.signInWithPassword({ email, password });
if (response.data?.user) {
setUser(response.data.user);
}
return response;
}
async function signUp(email: string, password: string) {
return await supabase.auth.signUp({
email,
password,
});
const response = await supabase.auth.signUp({ email, password });
if (response.data?.user) {
setUser(response.data.user);
}
return response;
}
async function signOut() {
return await supabase.auth.signOut();
const response = await supabase.auth.signOut();
setUser(null);
return response;
}
return (

322
src/lib/localApi.ts Normal file
View File

@@ -0,0 +1,322 @@
// 本地API客户端替换Supabase
const API_BASE_URL = 'http://localhost:3001/api';
// 存储token的key
const TOKEN_KEY = 'numerology_token';
// API响应类型
interface ApiResponse<T = any> {
data?: T;
error?: {
code: string;
message: string;
};
message?: string;
}
// 用户类型
export interface User {
id: number;
email: string;
fullName?: string;
birthDate?: string;
birthTime?: string;
birthPlace?: string;
gender?: 'male' | 'female';
createdAt: string;
}
// 认证响应类型
interface AuthResponse {
user: User;
token: string;
}
// 分析记录类型
export interface Reading {
id: number;
type: 'bazi' | 'ziwei' | 'yijing' | 'wuxing';
name?: string;
birthDate?: string;
birthTime?: string;
gender?: 'male' | 'female';
birthPlace?: string;
status: string;
createdAt: string;
results?: any;
analysis?: any;
}
class LocalApiClient {
private baseUrl: string;
constructor(baseUrl: string = API_BASE_URL) {
this.baseUrl = baseUrl;
}
// 获取存储的token
private getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
// 设置token
private setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
// 清除token
private clearToken(): void {
localStorage.removeItem(TOKEN_KEY);
}
// 通用请求方法
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const token = this.getToken();
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
return { error: data.error || { code: 'UNKNOWN_ERROR', message: '请求失败' } };
}
return data;
} catch (error) {
console.error('API请求错误:', error);
return {
error: {
code: 'NETWORK_ERROR',
message: '网络连接失败,请检查本地服务器是否启动'
}
};
}
}
// 认证相关方法
auth = {
// 用户注册
signUp: async (userData: {
email: string;
password: string;
fullName?: string;
birthDate?: string;
birthTime?: string;
birthPlace?: string;
gender?: 'male' | 'female';
}): Promise<ApiResponse<AuthResponse>> => {
const response = await this.request<AuthResponse>('/auth/signup', {
method: 'POST',
body: JSON.stringify(userData),
});
if (response.data?.token) {
this.setToken(response.data.token);
}
return response;
},
// 用户登录
signInWithPassword: async (credentials: {
email: string;
password: string;
}): Promise<ApiResponse<AuthResponse>> => {
const response = await this.request<AuthResponse>('/auth/signin', {
method: 'POST',
body: JSON.stringify(credentials),
});
if (response.data?.token) {
this.setToken(response.data.token);
}
return response;
},
// 用户登出
signOut: async (): Promise<ApiResponse> => {
const response = await this.request('/auth/signout', {
method: 'POST',
});
this.clearToken();
return response;
},
// 获取当前用户
getUser: async (): Promise<ApiResponse<{ user: User }>> => {
return await this.request<{ user: User }>('/auth/user');
},
// 验证token
verifyToken: async (token?: string): Promise<ApiResponse<{ user: User; valid: boolean }>> => {
return await this.request<{ user: User; valid: boolean }>('/auth/verify', {
method: 'POST',
body: JSON.stringify({ token: token || this.getToken() }),
});
},
// 更新用户信息
updateUser: async (userData: Partial<User>): Promise<ApiResponse<{ user: User }>> => {
return await this.request<{ user: User }>('/auth/user', {
method: 'PUT',
body: JSON.stringify(userData),
});
},
// 监听认证状态变化模拟Supabase的onAuthStateChange
onAuthStateChange: (callback: (event: string, session: { user: User } | null) => void) => {
// 简单实现检查token是否存在
const checkAuth = async () => {
const token = this.getToken();
if (token) {
const response = await this.auth.verifyToken(token);
if (response.data?.valid && response.data.user) {
callback('SIGNED_IN', { user: response.data.user });
} else {
this.clearToken();
callback('SIGNED_OUT', null);
}
} else {
callback('SIGNED_OUT', null);
}
};
// 立即检查一次
checkAuth();
// 返回取消订阅的函数
return {
data: {
subscription: {
unsubscribe: () => {
// 本地实现不需要取消订阅
}
}
}
};
}
};
// 分析功能相关方法
functions = {
// 调用分析函数
invoke: async (functionName: string, options: { body: any }): Promise<ApiResponse> => {
const endpointMap: { [key: string]: string } = {
'bazi-analyzer': '/analysis/bazi',
'ziwei-analyzer': '/analysis/ziwei',
'yijing-analyzer': '/analysis/yijing',
'bazi-wuxing-analysis': '/analysis/wuxing',
'bazi-details': '/analysis/bazi',
'reading-history': '/analysis/history'
};
const endpoint = endpointMap[functionName];
if (!endpoint) {
return {
error: {
code: 'FUNCTION_NOT_FOUND',
message: `未知的分析函数: ${functionName}`
}
};
}
// 特殊处理历史记录请求
if (functionName === 'reading-history') {
if (options.body.action === 'delete') {
return await this.request(`${endpoint}/${options.body.readingId}`, {
method: 'DELETE'
});
} else {
const queryParams = new URLSearchParams();
if (options.body.type) queryParams.append('type', options.body.type);
if (options.body.limit) queryParams.append('limit', options.body.limit.toString());
if (options.body.offset) queryParams.append('offset', options.body.offset.toString());
return await this.request(`${endpoint}?${queryParams.toString()}`);
}
}
return await this.request(endpoint, {
method: 'POST',
body: JSON.stringify(options.body),
});
}
};
// 数据库操作模拟Supabase的数据库操作
from = (table: string) => {
return {
select: (columns: string = '*') => ({
eq: (column: string, value: any) => ({
single: async () => {
// 根据表名和操作类型调用相应的API
if (table === 'user_profiles') {
return await this.auth.getUser();
}
return { data: null, error: null };
}
}),
order: (column: string, options?: { ascending: boolean }) => ({
limit: (count: number) => ({
async all() {
if (table === 'numerology_readings') {
const response = await this.functions.invoke('reading-history', {
body: { limit: count }
});
return { data: response.data?.readings || [], error: response.error };
}
return { data: [], error: null };
}
})
})
}),
update: (data: any) => ({
eq: (column: string, value: any) => ({
select: () => ({
single: async () => {
if (table === 'user_profiles') {
return await this.auth.updateUser(data);
}
return { data: null, error: null };
}
})
})
}),
insert: (data: any) => ({
select: () => ({
single: async () => {
// 插入操作通常通过分析API完成
return { data: null, error: null };
}
})
})
};
};
}
// 创建全局实例
export const localApi = new LocalApiClient();
// 导出兼容Supabase的接口
export const supabase = localApi;
// 默认导出
export default localApi;

View File

@@ -1,10 +1,8 @@
import { createClient } from '@supabase/supabase-js'
// 本地化改造使用本地API替代Supabase
import { localApi } from './localApi';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
// 导出本地API客户端保持与原Supabase客户端相同的接口
export const supabase = localApi;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables')
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// 为了向后兼容,也可以导出为默认
export default localApi;