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

BIN
numerology.db Normal file

Binary file not shown.

1578
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,15 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "yes | pnpm install && vite",
"build": "yes | pnpm install && rm -rf node_modules/.vite-temp && tsc -b && vite build",
"build:prod": "yes | pnpm install && rm -rf node_modules/.vite-temp && tsc -b && BUILD_MODE=prod vite build",
"lint": "yes | pnpm install && eslint .",
"preview": "yes | pnpm install && vite preview"
"dev": "vite",
"dev:server": "node server/index.js",
"dev:full": "concurrently \"npm run dev\" \"npm run dev:server\"",
"build": "tsc -b && vite build",
"build:prod": "tsc -b && BUILD_MODE=prod vite build",
"lint": "eslint .",
"preview": "vite preview",
"server": "node server/index.js",
"start": "NODE_ENV=production node server/index.js"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
@@ -40,12 +44,18 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@supabase/supabase-js": "^2.55.0",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^12.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"cors": "^2.8.5",
"date-fns": "^3.0.0",
"embla-carousel-react": "^8.5.2",
"express": "^5.1.0",
"helmet": "^8.1.0",
"input-otp": "^1.4.2",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.364.0",
"next-themes": "^0.4.4",
"react": "^18.3.1",
@@ -69,6 +79,7 @@
"@types/react-router-dom": "^5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "10.4.20",
"concurrently": "^9.2.0",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
@@ -80,4 +91,4 @@
"vite": "^6.0.1",
"vite-plugin-source-info": "^1.0.0"
}
}
}

111
server/database.js Normal file
View File

@@ -0,0 +1,111 @@
import Database from 'better-sqlite3';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 创建数据库连接
const dbPath = path.join(__dirname, '..', 'numerology.db');
const db = new Database(dbPath);
// 启用外键约束
db.pragma('foreign_keys = ON');
// 创建用户表
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
full_name TEXT,
birth_date DATE,
birth_time TIME,
birth_place TEXT,
gender TEXT CHECK (gender IN ('male', 'female')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 创建分析记录表
db.exec(`
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
reading_type TEXT NOT NULL CHECK (reading_type IN ('bazi', 'ziwei', 'yijing', 'wuxing')),
name TEXT,
birth_date DATE,
birth_time TIME,
gender TEXT CHECK (gender IN ('male', 'female')),
birth_place TEXT,
input_data TEXT,
results TEXT,
analysis TEXT,
status TEXT DEFAULT 'completed' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// 创建索引
db.exec(`
CREATE INDEX IF NOT EXISTS idx_readings_user_id ON readings(user_id);
CREATE INDEX IF NOT EXISTS idx_readings_type ON readings(reading_type);
CREATE INDEX IF NOT EXISTS idx_readings_created_at ON readings(created_at DESC);
`);
// 数据库操作函数
export const dbOperations = {
// 用户相关操作
createUser: db.prepare(`
INSERT INTO users (email, password, full_name, birth_date, birth_time, birth_place, gender)
VALUES (?, ?, ?, ?, ?, ?, ?)
`),
getUserByEmail: db.prepare('SELECT * FROM users WHERE email = ?'),
getUserById: db.prepare('SELECT id, email, full_name, birth_date, birth_time, birth_place, gender, created_at FROM users WHERE id = ?'),
updateUser: db.prepare(`
UPDATE users
SET full_name = ?, birth_date = ?, birth_time = ?, birth_place = ?, gender = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`),
// 分析记录相关操作
createReading: db.prepare(`
INSERT INTO readings (user_id, reading_type, name, birth_date, birth_time, gender, birth_place, input_data, results, analysis)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
getReadingsByUserId: db.prepare(`
SELECT * FROM readings
WHERE user_id = ?
ORDER BY created_at DESC
`),
getReadingsByUserIdAndType: db.prepare(`
SELECT * FROM readings
WHERE user_id = ? AND reading_type = ?
ORDER BY created_at DESC
`),
getReadingById: db.prepare('SELECT * FROM readings WHERE id = ?'),
deleteReading: db.prepare('DELETE FROM readings WHERE id = ? AND user_id = ?'),
// 统计信息
getUserReadingCount: db.prepare('SELECT COUNT(*) as count FROM readings WHERE user_id = ?'),
getReadingCountByType: db.prepare('SELECT reading_type, COUNT(*) as count FROM readings WHERE user_id = ? GROUP BY reading_type')
};
// 优雅关闭数据库连接
process.on('exit', () => db.close());
process.on('SIGHUP', () => process.exit(128 + 1));
process.on('SIGINT', () => process.exit(128 + 2));
process.on('SIGTERM', () => process.exit(128 + 15));
export default db;

140
server/index.js Normal file
View File

@@ -0,0 +1,140 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import path from 'path';
import { fileURLToPath } from 'url';
// 导入路由
import authRoutes from './routes/auth.js';
import analysisRoutes from './routes/analysis.js';
// 导入中间件
import { errorHandler, requestLogger, corsOptions } from './middleware/auth.js';
// 导入数据库(确保数据库初始化)
import './database.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// 安全中间件
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
crossOriginEmbedderPolicy: false
}));
// CORS配置
app.use(cors(corsOptions));
// 请求解析中间件
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 请求日志
app.use(requestLogger);
// 健康检查端点
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development',
version: '1.0.0'
});
});
// API路由
app.use('/api/auth', authRoutes);
app.use('/api/analysis', analysisRoutes);
// 静态文件服务(用于前端构建文件)
if (process.env.NODE_ENV === 'production') {
const frontendPath = path.join(__dirname, '..', 'dist');
app.use(express.static(frontendPath));
// SPA路由处理
app.get('*', (req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'));
});
}
// 404处理
app.use((req, res) => {
res.status(404).json({
error: {
code: 'NOT_FOUND',
message: '请求的资源不存在'
}
});
});
// 错误处理中间件
app.use(errorHandler);
// 优雅关闭处理
const gracefulShutdown = (signal) => {
console.log(`\n收到 ${signal} 信号,开始优雅关闭...`);
server.close((err) => {
if (err) {
console.error('服务器关闭时发生错误:', err);
process.exit(1);
}
console.log('服务器已关闭');
process.exit(0);
});
// 强制关闭超时
setTimeout(() => {
console.error('强制关闭服务器');
process.exit(1);
}, 10000);
};
// 启动服务器
const server = app.listen(PORT, () => {
console.log(`\n🚀 三算命本地服务器启动成功!`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🔧 环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`⏰ 启动时间: ${new Date().toLocaleString('zh-CN')}`);
console.log(`\n📚 API文档:`);
console.log(` 认证相关: http://localhost:${PORT}/api/auth`);
console.log(` 分析相关: http://localhost:${PORT}/api/analysis`);
console.log(` 健康检查: http://localhost:${PORT}/health`);
console.log(`\n💡 提示: 按 Ctrl+C 停止服务器\n`);
});
// 监听关闭信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 未捕获异常处理
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
console.error('Promise:', promise);
process.exit(1);
});
export default app;

217
server/middleware/auth.js Normal file
View File

@@ -0,0 +1,217 @@
import { authService } from '../services/authService.js';
/**
* JWT认证中间件
*/
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: '缺少访问令牌'
}
});
}
try {
const decoded = authService.verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({
error: {
code: 'FORBIDDEN',
message: '无效的访问令牌'
}
});
}
};
/**
* 可选认证中间件(用于可选登录的接口)
*/
export const optionalAuth = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
try {
const decoded = authService.verifyToken(token);
req.user = decoded;
} catch (error) {
// 忽略token验证错误继续执行
req.user = null;
}
} else {
req.user = null;
}
next();
};
/**
* 错误处理中间件
*/
export const errorHandler = (err, req, res, next) => {
console.error('API Error:', err);
// 数据库错误
if (err.code && err.code.startsWith('SQLITE_')) {
return res.status(500).json({
error: {
code: 'DATABASE_ERROR',
message: '数据库操作失败'
}
});
}
// 验证错误
if (err.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: err.message
}
});
}
// JWT错误
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: '无效的访问令牌'
}
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: {
code: 'TOKEN_EXPIRED',
message: '访问令牌已过期'
}
});
}
// 默认错误
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: err.message || '内部服务器错误'
}
});
};
/**
* 请求日志中间件
*/
export const requestLogger = (req, res, next) => {
const start = Date.now();
const { method, url, ip } = req;
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
console.log(`${new Date().toISOString()} - ${method} ${url} - ${statusCode} - ${duration}ms - ${ip}`);
});
next();
};
/**
* 输入验证中间件
*/
export const validateInput = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: {
code: 'INVALID_INPUT',
message: error.details[0].message
}
});
}
next();
};
};
/**
* 速率限制中间件(简单实现)
*/
const rateLimitStore = new Map();
export const rateLimit = (options = {}) => {
const {
windowMs = 15 * 60 * 1000, // 15分钟
max = 100, // 最大请求数
message = '请求过于频繁,请稍后再试'
} = options;
return (req, res, next) => {
const key = req.ip || req.connection.remoteAddress;
const now = Date.now();
if (!rateLimitStore.has(key)) {
rateLimitStore.set(key, { count: 1, resetTime: now + windowMs });
return next();
}
const record = rateLimitStore.get(key);
if (now > record.resetTime) {
// 重置计数
record.count = 1;
record.resetTime = now + windowMs;
return next();
}
if (record.count >= max) {
return res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message
}
});
}
record.count++;
next();
};
};
/**
* CORS中间件配置
*/
export const corsOptions = {
origin: function (origin, callback) {
// 允许的域名列表
const allowedOrigins = [
'http://localhost:5173',
'http://localhost:3000',
'http://127.0.0.1:5173',
'http://127.0.0.1:3000'
];
// 开发环境允许所有来源
if (process.env.NODE_ENV === 'development') {
return callback(null, true);
}
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('不允许的CORS来源'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};

349
server/routes/analysis.js Normal file
View File

@@ -0,0 +1,349 @@
import express from 'express';
import { numerologyService } from '../services/numerologyService.js';
import { authenticateToken, rateLimit } from '../middleware/auth.js';
const router = express.Router();
// 应用认证中间件
router.use(authenticateToken);
// 应用速率限制
router.use(rateLimit({ max: 50, windowMs: 15 * 60 * 1000 })); // 15分钟内最多50次请求
/**
* 八字命理分析
* POST /api/analysis/bazi
*/
router.post('/bazi', async (req, res, next) => {
try {
const { name, birthDate, birthTime, gender, birthPlace } = req.body;
// 参数验证
if (!birthDate) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '出生日期不能为空'
}
});
}
// 日期格式验证
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(birthDate)) {
return res.status(400).json({
error: {
code: 'INVALID_DATE_FORMAT',
message: '日期格式应为YYYY-MM-DD'
}
});
}
// 时间格式验证(可选)
if (birthTime) {
const timeRegex = /^\d{2}:\d{2}$/;
if (!timeRegex.test(birthTime)) {
return res.status(400).json({
error: {
code: 'INVALID_TIME_FORMAT',
message: '时间格式应为HH:MM'
}
});
}
}
const result = await numerologyService.analyzeBazi(req.user.userId, {
name,
birthDate,
birthTime,
gender,
birthPlace
});
res.json({
data: result,
message: '八字分析完成'
});
} catch (error) {
next(error);
}
});
/**
* 紫微斗数分析
* POST /api/analysis/ziwei
*/
router.post('/ziwei', async (req, res, next) => {
try {
const { name, birthDate, birthTime, gender, birthPlace } = req.body;
// 参数验证
if (!birthDate) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '出生日期不能为空'
}
});
}
// 日期格式验证
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(birthDate)) {
return res.status(400).json({
error: {
code: 'INVALID_DATE_FORMAT',
message: '日期格式应为YYYY-MM-DD'
}
});
}
const result = await numerologyService.analyzeZiwei(req.user.userId, {
name,
birthDate,
birthTime,
gender,
birthPlace
});
res.json({
data: result,
message: '紫微斗数分析完成'
});
} catch (error) {
next(error);
}
});
/**
* 易经占卜分析
* POST /api/analysis/yijing
*/
router.post('/yijing', async (req, res, next) => {
try {
const { question, method } = req.body;
// 参数验证
if (!question) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '占卜问题不能为空'
}
});
}
if (question.length > 200) {
return res.status(400).json({
error: {
code: 'QUESTION_TOO_LONG',
message: '问题长度不能超过200字符'
}
});
}
const result = await numerologyService.analyzeYijing(req.user.userId, {
question,
method: method || '梅花易数时间起卦法'
});
res.json({
data: result,
message: '易经占卜分析完成'
});
} catch (error) {
next(error);
}
});
/**
* 五行分析
* POST /api/analysis/wuxing
*/
router.post('/wuxing', async (req, res, next) => {
try {
const { name, birthDate, birthTime, gender } = req.body;
// 参数验证
if (!birthDate) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '出生日期不能为空'
}
});
}
// 日期格式验证
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(birthDate)) {
return res.status(400).json({
error: {
code: 'INVALID_DATE_FORMAT',
message: '日期格式应为YYYY-MM-DD'
}
});
}
const result = await numerologyService.analyzeWuxing(req.user.userId, {
name,
birthDate,
birthTime,
gender
});
res.json({
data: result,
message: '五行分析完成'
});
} catch (error) {
next(error);
}
});
/**
* 获取分析历史记录
* GET /api/analysis/history
*/
router.get('/history', async (req, res, next) => {
try {
const { type, limit = 20, offset = 0 } = req.query;
// 验证分析类型
if (type && !['bazi', 'ziwei', 'yijing', 'wuxing'].includes(type)) {
return res.status(400).json({
error: {
code: 'INVALID_TYPE',
message: '无效的分析类型'
}
});
}
const history = await numerologyService.getReadingHistory(req.user.userId, type);
// 简单的分页处理
const startIndex = parseInt(offset);
const endIndex = startIndex + parseInt(limit);
const paginatedHistory = history.slice(startIndex, endIndex);
res.json({
data: {
readings: paginatedHistory,
total: history.length,
hasMore: endIndex < history.length
},
message: '获取分析历史成功'
});
} catch (error) {
next(error);
}
});
/**
* 获取单个分析记录详情
* GET /api/analysis/history/:id
*/
router.get('/history/:id', async (req, res, next) => {
try {
const { id } = req.params;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
error: {
code: 'INVALID_ID',
message: '无效的记录ID'
}
});
}
const history = await numerologyService.getReadingHistory(req.user.userId);
const reading = history.find(r => r.id === parseInt(id));
if (!reading) {
return res.status(404).json({
error: {
code: 'READING_NOT_FOUND',
message: '分析记录不存在'
}
});
}
res.json({
data: { reading },
message: '获取分析记录成功'
});
} catch (error) {
next(error);
}
});
/**
* 删除分析记录
* DELETE /api/analysis/history/:id
*/
router.delete('/history/:id', async (req, res, next) => {
try {
const { id } = req.params;
if (!id || isNaN(parseInt(id))) {
return res.status(400).json({
error: {
code: 'INVALID_ID',
message: '无效的记录ID'
}
});
}
const success = await numerologyService.deleteReading(req.user.userId, parseInt(id));
if (!success) {
return res.status(404).json({
error: {
code: 'READING_NOT_FOUND',
message: '分析记录不存在或无权删除'
}
});
}
res.json({
message: '分析记录删除成功'
});
} catch (error) {
next(error);
}
});
/**
* 获取用户分析统计信息
* GET /api/analysis/stats
*/
router.get('/stats', async (req, res, next) => {
try {
const history = await numerologyService.getReadingHistory(req.user.userId);
const stats = {
total: history.length,
byType: {
bazi: history.filter(r => r.type === 'bazi').length,
ziwei: history.filter(r => r.type === 'ziwei').length,
yijing: history.filter(r => r.type === 'yijing').length,
wuxing: history.filter(r => r.type === 'wuxing').length
},
recent: history.slice(0, 5).map(r => ({
id: r.id,
type: r.type,
name: r.name,
createdAt: r.createdAt
}))
};
res.json({
data: stats,
message: '获取统计信息成功'
});
} catch (error) {
next(error);
}
});
export default router;

231
server/routes/auth.js Normal file
View File

@@ -0,0 +1,231 @@
import express from 'express';
import { authService } from '../services/authService.js';
import { authenticateToken, rateLimit } from '../middleware/auth.js';
const router = express.Router();
// 应用速率限制
router.use(rateLimit({ max: 20, windowMs: 15 * 60 * 1000 })); // 15分钟内最多20次请求
/**
* 用户注册
* POST /api/auth/signup
*/
router.post('/signup', async (req, res, next) => {
try {
const { email, password, fullName, birthDate, birthTime, birthPlace, gender } = req.body;
// 基本验证
if (!email || !password) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '邮箱和密码不能为空'
}
});
}
// 邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
error: {
code: 'INVALID_EMAIL',
message: '邮箱格式不正确'
}
});
}
// 密码强度验证
if (password.length < 6) {
return res.status(400).json({
error: {
code: 'WEAK_PASSWORD',
message: '密码长度至少6位'
}
});
}
const result = await authService.signUp({
email,
password,
fullName,
birthDate,
birthTime,
birthPlace,
gender
});
res.status(201).json({
data: result,
message: '注册成功'
});
} catch (error) {
next(error);
}
});
/**
* 用户登录
* POST /api/auth/signin
*/
router.post('/signin', async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
error: {
code: 'MISSING_PARAMETERS',
message: '邮箱和密码不能为空'
}
});
}
const result = await authService.signIn(email, password);
res.json({
data: result,
message: '登录成功'
});
} catch (error) {
if (error.message.includes('邮箱或密码错误')) {
return res.status(401).json({
error: {
code: 'INVALID_CREDENTIALS',
message: error.message
}
});
}
next(error);
}
});
/**
* 获取当前用户信息
* GET /api/auth/user
*/
router.get('/user', authenticateToken, async (req, res, next) => {
try {
const user = await authService.getUserById(req.user.userId);
res.json({
data: { user },
message: '获取用户信息成功'
});
} catch (error) {
next(error);
}
});
/**
* 更新用户信息
* PUT /api/auth/user
*/
router.put('/user', authenticateToken, async (req, res, next) => {
try {
const { fullName, birthDate, birthTime, birthPlace, gender } = req.body;
const updatedUser = await authService.updateUser(req.user.userId, {
fullName,
birthDate,
birthTime,
birthPlace,
gender
});
res.json({
data: { user: updatedUser },
message: '用户信息更新成功'
});
} catch (error) {
next(error);
}
});
/**
* 验证token
* POST /api/auth/verify
*/
router.post('/verify', async (req, res, next) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
error: {
code: 'MISSING_TOKEN',
message: 'Token不能为空'
}
});
}
const decoded = authService.verifyToken(token);
const user = await authService.getUserById(decoded.userId);
res.json({
data: { user, valid: true },
message: 'Token验证成功'
});
} catch (error) {
res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Token无效或已过期'
}
});
}
});
/**
* 用户登出(客户端处理,服务端记录日志)
* POST /api/auth/signout
*/
router.post('/signout', authenticateToken, async (req, res) => {
try {
// 这里可以添加登出日志记录
console.log(`用户 ${req.user.userId}${new Date().toISOString()} 登出`);
res.json({
message: '登出成功'
});
} catch (error) {
res.status(500).json({
error: {
code: 'SIGNOUT_ERROR',
message: '登出失败'
}
});
}
});
/**
* 刷新token可选功能
* POST /api/auth/refresh
*/
router.post('/refresh', authenticateToken, async (req, res, next) => {
try {
const user = await authService.getUserById(req.user.userId);
// 生成新的token
const jwt = await import('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
const newToken = jwt.default.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
data: {
user,
token: newToken
},
message: 'Token刷新成功'
});
} catch (error) {
next(error);
}
});
export default router;

View File

@@ -0,0 +1,176 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { dbOperations } from '../database.js';
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
const JWT_EXPIRES_IN = '7d';
export const authService = {
/**
* 用户注册
*/
async signUp(userData) {
const { email, password, fullName, birthDate, birthTime, birthPlace, gender } = userData;
try {
// 检查用户是否已存在
const existingUser = dbOperations.getUserByEmail.get(email);
if (existingUser) {
throw new Error('用户已存在');
}
// 密码加密
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// 创建用户
const result = dbOperations.createUser.run(
email,
hashedPassword,
fullName || null,
birthDate || null,
birthTime || null,
birthPlace || null,
gender || null
);
// 获取创建的用户信息
const user = dbOperations.getUserById.get(result.lastInsertRowid);
// 生成JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
return {
user: {
id: user.id,
email: user.email,
fullName: user.full_name,
birthDate: user.birth_date,
birthTime: user.birth_time,
birthPlace: user.birth_place,
gender: user.gender,
createdAt: user.created_at
},
token
};
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('邮箱已被注册');
}
throw error;
}
},
/**
* 用户登录
*/
async signIn(email, password) {
try {
// 查找用户
const user = dbOperations.getUserByEmail.get(email);
if (!user) {
throw new Error('邮箱或密码错误');
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error('邮箱或密码错误');
}
// 生成JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
return {
user: {
id: user.id,
email: user.email,
fullName: user.full_name,
birthDate: user.birth_date,
birthTime: user.birth_time,
birthPlace: user.birth_place,
gender: user.gender,
createdAt: user.created_at
},
token
};
} catch (error) {
throw error;
}
},
/**
* 验证JWT token
*/
verifyToken(token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
return decoded;
} catch (error) {
throw new Error('无效的token');
}
},
/**
* 获取用户信息
*/
async getUserById(userId) {
try {
const user = dbOperations.getUserById.get(userId);
if (!user) {
throw new Error('用户不存在');
}
return {
id: user.id,
email: user.email,
fullName: user.full_name,
birthDate: user.birth_date,
birthTime: user.birth_time,
birthPlace: user.birth_place,
gender: user.gender,
createdAt: user.created_at
};
} catch (error) {
throw error;
}
},
/**
* 更新用户信息
*/
async updateUser(userId, userData) {
const { fullName, birthDate, birthTime, birthPlace, gender } = userData;
try {
// 检查用户是否存在
const existingUser = dbOperations.getUserById.get(userId);
if (!existingUser) {
throw new Error('用户不存在');
}
// 更新用户信息
dbOperations.updateUser.run(
fullName || existingUser.full_name,
birthDate || existingUser.birth_date,
birthTime || existingUser.birth_time,
birthPlace || existingUser.birth_place,
gender || existingUser.gender,
userId
);
// 返回更新后的用户信息
return await this.getUserById(userId);
} catch (error) {
throw error;
}
}
};

View File

@@ -0,0 +1,638 @@
import { dbOperations } from '../database.js';
// 天干地支数据
const HEAVENLY_STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const EARTHLY_BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const ZODIAC_ANIMALS = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
// 五行属性
const WUXING_MAP = {
'甲': '木', '乙': '木',
'丙': '火', '丁': '火',
'戊': '土', '己': '土',
'庚': '金', '辛': '金',
'壬': '水', '癸': '水',
'子': '水', '亥': '水',
'寅': '木', '卯': '木',
'巳': '火', '午': '火',
'申': '金', '酉': '金',
'辰': '土', '戌': '土', '丑': '土', '未': '土'
};
// 紫微斗数星曜
const ZIWEI_STARS = {
main: ['紫微', '天机', '太阳', '武曲', '天同', '廉贞', '天府', '太阴', '贪狼', '巨门', '天相', '天梁', '七杀', '破军'],
lucky: ['文昌', '文曲', '左辅', '右弼', '天魁', '天钺', '禄存', '天马'],
unlucky: ['擎羊', '陀罗', '火星', '铃星', '地空', '地劫']
};
// 易经六十四卦
const HEXAGRAMS = [
{ name: '乾为天', symbol: '☰☰', description: '刚健中正,自强不息' },
{ name: '坤为地', symbol: '☷☷', description: '厚德载物,包容万象' },
{ name: '水雷屯', symbol: '☵☳', description: '万物始生,艰难创业' },
{ name: '山水蒙', symbol: '☶☵', description: '启蒙教育,循序渐进' },
// ... 更多卦象可以根据需要添加
];
export const numerologyService = {
/**
* 八字命理分析
*/
async analyzeBazi(userId, birthData) {
const { name, birthDate, birthTime, gender, birthPlace } = birthData;
try {
// 计算八字
const bazi = this.calculateBazi(birthDate, birthTime);
// 五行分析
const wuxing = this.analyzeWuxing(bazi);
// 生成分析结果
const analysis = this.generateBaziAnalysis(bazi, wuxing, gender);
// 保存分析记录
const result = dbOperations.createReading.run(
userId,
'bazi',
name,
birthDate,
birthTime,
gender,
birthPlace,
JSON.stringify(birthData),
JSON.stringify({ bazi, wuxing }),
JSON.stringify(analysis)
);
return {
recordId: result.lastInsertRowid,
analysis: {
bazi,
wuxing,
analysis
}
};
} catch (error) {
throw new Error(`八字分析失败: ${error.message}`);
}
},
/**
* 紫微斗数分析
*/
async analyzeZiwei(userId, birthData) {
const { name, birthDate, birthTime, gender, birthPlace } = birthData;
try {
// 计算紫微斗数
const ziwei = this.calculateZiwei(birthDate, birthTime, gender);
// 生成分析结果
const analysis = this.generateZiweiAnalysis(ziwei, gender);
// 保存分析记录
const result = dbOperations.createReading.run(
userId,
'ziwei',
name,
birthDate,
birthTime,
gender,
birthPlace,
JSON.stringify(birthData),
JSON.stringify({ ziwei }),
JSON.stringify(analysis)
);
return {
recordId: result.lastInsertRowid,
analysis: {
ziwei,
analysis
}
};
} catch (error) {
throw new Error(`紫微斗数分析失败: ${error.message}`);
}
},
/**
* 易经占卜分析
*/
async analyzeYijing(userId, divinationData) {
const { question, method } = divinationData;
try {
// 生成卦象
const hexagram = this.generateHexagram();
// 生成分析结果
const analysis = this.generateYijingAnalysis(hexagram, question);
// 保存分析记录
const result = dbOperations.createReading.run(
userId,
'yijing',
null,
null,
null,
null,
null,
JSON.stringify(divinationData),
JSON.stringify({ hexagram }),
JSON.stringify(analysis)
);
return {
recordId: result.lastInsertRowid,
analysis
};
} catch (error) {
throw new Error(`易经占卜分析失败: ${error.message}`);
}
},
/**
* 五行分析
*/
async analyzeWuxing(userId, birthData) {
const { name, birthDate, birthTime, gender } = birthData;
try {
// 计算八字
const bazi = this.calculateBazi(birthDate, birthTime);
// 五行分析
const wuxing = this.analyzeWuxing(bazi);
// 生成建议
const recommendations = this.generateWuxingRecommendations(wuxing);
// 保存分析记录
const result = dbOperations.createReading.run(
userId,
'wuxing',
name,
birthDate,
birthTime,
gender,
null,
JSON.stringify(birthData),
JSON.stringify({ wuxing }),
JSON.stringify({ recommendations })
);
return {
recordId: result.lastInsertRowid,
analysis: {
wuxingDistribution: wuxing,
balanceAnalysis: this.analyzeWuxingBalance(wuxing),
recommendations
}
};
} catch (error) {
throw new Error(`五行分析失败: ${error.message}`);
}
},
/**
* 计算八字
*/
calculateBazi(birthDate, birthTime) {
const date = new Date(birthDate + 'T' + (birthTime || '12:00'));
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
// 简化的八字计算(实际应用中需要更复杂的算法)
const yearStem = HEAVENLY_STEMS[(year - 4) % 10];
const yearBranch = EARTHLY_BRANCHES[(year - 4) % 12];
const monthStem = HEAVENLY_STEMS[(month - 1) % 10];
const monthBranch = EARTHLY_BRANCHES[(month - 1) % 12];
const dayStem = HEAVENLY_STEMS[(day - 1) % 10];
const dayBranch = EARTHLY_BRANCHES[(day - 1) % 12];
const hourStem = HEAVENLY_STEMS[Math.floor(hour / 2) % 10];
const hourBranch = EARTHLY_BRANCHES[Math.floor(hour / 2) % 12];
return {
year: yearStem + yearBranch,
month: monthStem + monthBranch,
day: dayStem + dayBranch,
hour: hourStem + hourBranch,
yearAnimal: ZODIAC_ANIMALS[(year - 4) % 12]
};
},
/**
* 五行分析
*/
analyzeWuxing(bazi) {
const wuxingCount = { wood: 0, fire: 0, earth: 0, metal: 0, water: 0 };
// 统计五行
Object.values(bazi).forEach(pillar => {
if (typeof pillar === 'string' && pillar.length === 2) {
const stem = pillar[0];
const branch = pillar[1];
const stemWuxing = WUXING_MAP[stem];
const branchWuxing = WUXING_MAP[branch];
if (stemWuxing) {
const wuxingKey = this.getWuxingKey(stemWuxing);
if (wuxingKey) wuxingCount[wuxingKey]++;
}
if (branchWuxing) {
const wuxingKey = this.getWuxingKey(branchWuxing);
if (wuxingKey) wuxingCount[wuxingKey]++;
}
}
});
return wuxingCount;
},
/**
* 获取五行英文键名
*/
getWuxingKey(wuxing) {
const map = { '木': 'wood', '火': 'fire', '土': 'earth', '金': 'metal', '水': 'water' };
return map[wuxing];
},
/**
* 五行平衡分析
*/
analyzeWuxingBalance(wuxing) {
const total = Object.values(wuxing).reduce((sum, count) => sum + count, 0);
const average = total / 5;
let dominant = null;
let lacking = null;
let maxCount = 0;
let minCount = Infinity;
Object.entries(wuxing).forEach(([element, count]) => {
if (count > maxCount) {
maxCount = count;
dominant = element;
}
if (count < minCount) {
minCount = count;
lacking = element;
}
});
const balanceScore = Math.round((1 - (maxCount - minCount) / total) * 100);
return {
dominantElement: dominant,
lackingElement: lacking,
balanceScore: Math.max(0, Math.min(100, balanceScore))
};
},
/**
* 生成八字分析
*/
generateBaziAnalysis(bazi, wuxing, gender) {
return {
character: this.generateCharacterAnalysis(bazi, wuxing, gender),
career: this.generateCareerAnalysis(bazi, wuxing),
wealth: this.generateWealthAnalysis(bazi, wuxing),
health: this.generateHealthAnalysis(bazi, wuxing),
relationships: this.generateRelationshipAnalysis(bazi, wuxing, gender)
};
},
/**
* 生成性格分析
*/
generateCharacterAnalysis(bazi, wuxing, gender) {
const traits = [];
// 根据日干分析性格
const dayStem = bazi.day[0];
switch (dayStem) {
case '甲':
traits.push('性格刚直,有领导才能,喜欢挑战');
break;
case '乙':
traits.push('性格温和,适应能力强,善于合作');
break;
case '丙':
traits.push('性格开朗,热情洋溢,富有创造力');
break;
case '丁':
traits.push('性格细腻,思维敏锐,注重细节');
break;
default:
traits.push('性格特点需要结合具体情况分析');
}
// 根据五行平衡分析性格
const balance = this.analyzeWuxingBalance(wuxing);
if (balance.dominantElement === 'wood') {
traits.push('木旺之人,性格积极向上,富有生命力');
} else if (balance.dominantElement === 'fire') {
traits.push('火旺之人,性格热情奔放,行动力强');
}
return traits.join('');
},
/**
* 生成事业分析
*/
generateCareerAnalysis(bazi, wuxing) {
const advice = [];
const balance = this.analyzeWuxingBalance(wuxing);
if (balance.dominantElement === 'wood') {
advice.push('适合从事教育、文化、林业等与木相关的行业');
} else if (balance.dominantElement === 'fire') {
advice.push('适合从事能源、娱乐、餐饮等与火相关的行业');
} else if (balance.dominantElement === 'earth') {
advice.push('适合从事房地产、农业、建筑等与土相关的行业');
} else if (balance.dominantElement === 'metal') {
advice.push('适合从事金融、机械、汽车等与金相关的行业');
} else if (balance.dominantElement === 'water') {
advice.push('适合从事航运、水利、贸易等与水相关的行业');
}
return advice.join('');
},
/**
* 生成财运分析
*/
generateWealthAnalysis(bazi, wuxing) {
return '财运需要通过努力获得,建议理性投资,稳健理财';
},
/**
* 生成健康分析
*/
generateHealthAnalysis(bazi, wuxing) {
const balance = this.analyzeWuxingBalance(wuxing);
const advice = [];
if (balance.lackingElement === 'wood') {
advice.push('注意肝胆健康,多接触绿色植物');
} else if (balance.lackingElement === 'fire') {
advice.push('注意心脏健康,保持乐观心态');
}
return advice.length > 0 ? advice.join('') : '身体健康状况良好,注意均衡饮食和适量运动';
},
/**
* 生成感情分析
*/
generateRelationshipAnalysis(bazi, wuxing, gender) {
return '感情运势平稳,建议真诚待人,珍惜缘分';
},
/**
* 计算紫微斗数
*/
calculateZiwei(birthDate, birthTime, gender) {
const date = new Date(birthDate + 'T' + (birthTime || '12:00'));
const hour = date.getHours();
// 简化的紫微斗数计算
const mingGongIndex = Math.floor(hour / 2);
const mingGong = EARTHLY_BRANCHES[mingGongIndex];
// 随机分配主星(实际应用中需要复杂的计算)
const mainStars = this.getRandomStars(ZIWEI_STARS.main, 2);
const luckyStars = this.getRandomStars(ZIWEI_STARS.lucky, 3);
const unluckyStars = this.getRandomStars(ZIWEI_STARS.unlucky, 2);
// 生成十二宫位
const twelvePalaces = this.generateTwelvePalaces(mingGongIndex);
// 四化飞星
const siHua = this.generateSiHua();
return {
mingGong,
mingGongXing: mainStars,
shiErGong: twelvePalaces,
siHua,
birthChart: {
mingGongPosition: mingGong,
mainStars,
luckyStars,
unluckyStars
}
};
},
/**
* 随机获取星曜
*/
getRandomStars(starArray, count) {
const shuffled = [...starArray].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
},
/**
* 生成十二宫位
*/
generateTwelvePalaces(mingGongIndex) {
const palaces = ['命宫', '兄弟宫', '夫妻宫', '子女宫', '财帛宫', '疾厄宫', '迁移宫', '交友宫', '事业宫', '田宅宫', '福德宫', '父母宫'];
const result = {};
palaces.forEach((palace, index) => {
const branchIndex = (mingGongIndex + index) % 12;
result[palace] = {
branch: EARTHLY_BRANCHES[branchIndex],
mainStars: this.getRandomStars(ZIWEI_STARS.main, 1),
interpretation: `${palace}的详细解读内容`
};
});
return result;
},
/**
* 生成四化飞星
*/
generateSiHua() {
return {
huaLu: { star: '廉贞', meaning: '财禄亨通,运势顺遂' },
huaQuan: { star: '破军', meaning: '权力地位,事业有成' },
huaKe: { star: '武曲', meaning: '贵人相助,学业有成' },
huaJi: { star: '太阳', meaning: '需要谨慎,防范风险' }
};
},
/**
* 生成紫微斗数分析
*/
generateZiweiAnalysis(ziwei, gender) {
return {
character: {
overview: '根据命宫主星分析,您的性格特点突出',
personalityTraits: '具有领导能力,做事果断,富有责任感'
},
career: {
suitableIndustries: ['管理', '金融', '教育'],
careerAdvice: '适合从事需要决策和领导的工作'
},
wealth: {
wealthPattern: '财运稳定,通过努力可以获得不错的收入'
},
health: {
constitution: '体质较好,注意劳逸结合',
wellnessAdvice: '保持规律作息,适量运动'
},
relationships: {
marriageFortune: '感情运势平稳,婚姻美满',
spouseCharacteristics: '伴侣性格温和,相处和谐'
}
};
},
/**
* 生成卦象
*/
generateHexagram() {
const randomIndex = Math.floor(Math.random() * HEXAGRAMS.length);
const hexagram = HEXAGRAMS[randomIndex];
return {
name: hexagram.name,
symbol: hexagram.symbol,
description: hexagram.description,
upperTrigram: hexagram.symbol.substring(0, 1),
lowerTrigram: hexagram.symbol.substring(1, 2)
};
},
/**
* 生成易经分析
*/
generateYijingAnalysis(hexagram, question) {
return {
basicInfo: {
divinationData: {
question,
method: '梅花易数时间起卦法',
divinationTime: new Date().toISOString()
},
hexagramInfo: {
mainHexagram: hexagram.name,
hexagramDescription: hexagram.description,
upperTrigram: hexagram.upperTrigram,
lowerTrigram: hexagram.lowerTrigram,
detailedInterpretation: `${hexagram.name}卦象显示${hexagram.description}`
}
},
detailedAnalysis: {
hexagramAnalysis: {
primaryMeaning: '此卦象征着新的开始和机遇',
judgment: '吉',
image: '天行健,君子以自强不息'
},
changingLinesAnalysis: {
changingLinePosition: '六二',
lineMeaning: '见龙在田,利见大人'
},
changingHexagram: {
name: '天风姤',
meaning: '变化中蕴含新的机遇',
transformationInsight: '顺应变化,把握时机'
}
},
lifeGuidance: {
overallFortune: '整体运势向好,宜积极进取',
careerGuidance: '事业发展顺利,可以大胆尝试',
relationshipGuidance: '人际关系和谐,感情稳定',
wealthGuidance: '财运亨通,投资需谨慎'
},
divinationWisdom: {
keyMessage: '天道酬勤,自强不息',
actionAdvice: '保持积极心态,勇于面对挑战',
philosophicalInsight: '变化是永恒的,适应变化才能成功'
}
};
},
/**
* 生成五行建议
*/
generateWuxingRecommendations(wuxing) {
const balance = this.analyzeWuxingBalance(wuxing);
const recommendations = {
colors: [],
directions: [],
careerFields: [],
lifestyleAdvice: ''
};
if (balance.lackingElement === 'wood') {
recommendations.colors = ['绿色', '青色'];
recommendations.directions = ['东方'];
recommendations.careerFields = ['教育', '文化', '林业'];
recommendations.lifestyleAdvice = '多接触自然,种植绿色植物';
} else if (balance.lackingElement === 'fire') {
recommendations.colors = ['红色', '橙色'];
recommendations.directions = ['南方'];
recommendations.careerFields = ['能源', '娱乐', '餐饮'];
recommendations.lifestyleAdvice = '保持乐观心态,多参加社交活动';
}
return recommendations;
},
/**
* 获取用户分析历史
*/
async getReadingHistory(userId, type = null) {
try {
let readings;
if (type) {
readings = dbOperations.getReadingsByUserIdAndType.all(userId, type);
} else {
readings = dbOperations.getReadingsByUserId.all(userId);
}
return readings.map(reading => ({
id: reading.id,
type: reading.reading_type,
name: reading.name,
birthDate: reading.birth_date,
birthTime: reading.birth_time,
gender: reading.gender,
birthPlace: reading.birth_place,
status: reading.status,
createdAt: reading.created_at,
results: reading.results ? JSON.parse(reading.results) : null,
analysis: reading.analysis ? JSON.parse(reading.analysis) : null
}));
} catch (error) {
throw new Error(`获取分析历史失败: ${error.message}`);
}
},
/**
* 删除分析记录
*/
async deleteReading(userId, readingId) {
try {
const result = dbOperations.deleteReading.run(readingId, userId);
return result.changes > 0;
} catch (error) {
throw new Error(`删除分析记录失败: ${error.message}`);
}
}
};

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;