mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-28 05:33:11 +08:00
feat: 完成从Supabase到本地化架构的迁移\n\n- 添加本地SQLite数据库支持\n- 实现本地认证系统(JWT + bcrypt)\n- 创建Express.js API服务器\n- 实现完整的命理分析算法\n- 替换Supabase客户端为本地API客户端\n- 保持前端接口兼容性\n- 添加本地服务器启动脚本
This commit is contained in:
111
server/database.js
Normal file
111
server/database.js
Normal 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
140
server/index.js
Normal 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
217
server/middleware/auth.js
Normal 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
349
server/routes/analysis.js
Normal 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
231
server/routes/auth.js
Normal 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;
|
||||
176
server/services/authService.js
Normal file
176
server/services/authService.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
638
server/services/numerologyService.js
Normal file
638
server/services/numerologyService.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user