mirror of
https://github.com/patdelphi/suanming.git
synced 2026-02-28 05:33:11 +08:00
feat: 完成易经64卦数据补全和本地化改造
- 完全按照logic/yijing.txt补全所有64卦的完整数据结构 - 包含每卦的卦辞、象传、六爻详解和人生指导 - 重建八字、易经、紫微斗数三个核心分析器 - 实现完整的本地SQLite数据库替代Supabase - 添加本地Express.js后端服务器 - 更新前端API调用为本地接口 - 实现JWT本地认证系统 - 完善历史记录和用户管理功能
This commit is contained in:
33
.env.example
Normal file
33
.env.example
Normal file
@@ -0,0 +1,33 @@
|
||||
# 前端环境变量
|
||||
# 本地API服务器地址
|
||||
VITE_API_BASE_URL=http://localhost:3001/api
|
||||
|
||||
# 后端环境变量
|
||||
# 服务器端口
|
||||
PORT=3001
|
||||
|
||||
# JWT密钥(生产环境请使用强密码)
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 数据库配置
|
||||
DB_PATH=./numerology.db
|
||||
|
||||
# 运行环境
|
||||
NODE_ENV=development
|
||||
|
||||
# CORS配置(生产环境请设置具体域名)
|
||||
CORS_ORIGIN=http://localhost:5173,http://localhost:4173
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 会话清理间隔(毫秒)
|
||||
SESSION_CLEANUP_INTERVAL=3600000
|
||||
|
||||
# 文件上传限制(MB)
|
||||
FILE_UPLOAD_LIMIT=10
|
||||
|
||||
# API请求限制
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
393
docs/LOCAL_DEPLOYMENT.md
Normal file
393
docs/LOCAL_DEPLOYMENT.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# 本地化部署指南
|
||||
|
||||
本文档详细说明如何部署和运行完全本地化的三算命应用。
|
||||
|
||||
## 🎯 本地化改造概述
|
||||
|
||||
本项目已从基于Supabase的云端架构完全转换为本地化架构:
|
||||
|
||||
### 架构变更
|
||||
- **数据库**: PostgreSQL (Supabase) → SQLite (本地文件)
|
||||
- **后端**: Supabase Edge Functions → Express.js 服务器
|
||||
- **认证**: Supabase Auth → JWT + bcrypt
|
||||
- **API**: Supabase客户端 → 本地API客户端
|
||||
|
||||
### 保留功能
|
||||
- ✅ 完整的八字、紫微、易经分析功能
|
||||
- ✅ 用户注册、登录、档案管理
|
||||
- ✅ 历史记录存储和查询
|
||||
- ✅ 所有业务逻辑和算法
|
||||
- ✅ 原有的用户界面和体验
|
||||
|
||||
## 📋 环境要求
|
||||
|
||||
### 系统要求
|
||||
- Node.js >= 18.0.0
|
||||
- npm >= 9.0.0 或 pnpm >= 8.0.0
|
||||
- Git >= 2.0.0
|
||||
|
||||
### 检查环境
|
||||
```bash
|
||||
node --version # 应该 >= 18.0.0
|
||||
npm --version # 应该 >= 9.0.0
|
||||
git --version # 应该 >= 2.0.0
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ai-numerology-refactored
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. 环境配置
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑环境变量(可选)
|
||||
# 默认配置已经可以直接使用
|
||||
```
|
||||
|
||||
### 4. 初始化数据库
|
||||
```bash
|
||||
npm run db:init
|
||||
```
|
||||
|
||||
执行成功后会看到:
|
||||
```
|
||||
🎉 数据库初始化完成!
|
||||
📍 数据库文件位置: ./numerology.db
|
||||
✅ 管理员用户创建成功
|
||||
邮箱: admin@localhost
|
||||
密码: admin123
|
||||
✅ 示例数据创建成功
|
||||
测试用户邮箱: test@example.com
|
||||
测试用户密码: test123
|
||||
```
|
||||
|
||||
### 5. 启动应用
|
||||
|
||||
#### 开发模式(推荐)
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
这会同时启动后端服务器和前端开发服务器。
|
||||
|
||||
#### 分别启动
|
||||
```bash
|
||||
# 终端1:启动后端服务器
|
||||
npm run server
|
||||
|
||||
# 终端2:启动前端开发服务器
|
||||
npx vite
|
||||
```
|
||||
|
||||
### 6. 访问应用
|
||||
- 前端地址: http://localhost:5173
|
||||
- 后端API: http://localhost:3001
|
||||
- 健康检查: http://localhost:3001/health
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
#### 前端环境变量
|
||||
```env
|
||||
# 本地API服务器地址
|
||||
VITE_API_BASE_URL=http://localhost:3001/api
|
||||
```
|
||||
|
||||
#### 后端环境变量
|
||||
```env
|
||||
# 服务器端口
|
||||
PORT=3001
|
||||
|
||||
# JWT密钥(生产环境请更改)
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 数据库文件路径
|
||||
DB_PATH=./numerology.db
|
||||
|
||||
# 运行环境
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
数据库文件默认位置:`./numerology.db`
|
||||
|
||||
#### 数据库管理命令
|
||||
```bash
|
||||
# 初始化数据库
|
||||
npm run db:init
|
||||
|
||||
# 备份数据库
|
||||
node server/scripts/initDatabase.cjs backup
|
||||
|
||||
# 清理过期数据
|
||||
node server/scripts/initDatabase.cjs cleanup
|
||||
```
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
ai-numerology-refactored/
|
||||
├── server/ # 后端服务器
|
||||
│ ├── database/ # 数据库相关
|
||||
│ │ ├── index.cjs # 数据库管理器
|
||||
│ │ └── schema.sql # 数据库结构
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ ├── auth.cjs # JWT认证
|
||||
│ │ ├── errorHandler.cjs # 错误处理
|
||||
│ │ └── logger.cjs # 日志记录
|
||||
│ ├── routes/ # API路由
|
||||
│ │ ├── auth.cjs # 认证路由
|
||||
│ │ ├── analysis.cjs # 分析路由
|
||||
│ │ ├── history.cjs # 历史记录路由
|
||||
│ │ └── profile.cjs # 用户档案路由
|
||||
│ ├── services/ # 业务逻辑服务
|
||||
│ │ ├── baziAnalyzer.cjs # 八字分析
|
||||
│ │ ├── yijingAnalyzer.cjs # 易经分析
|
||||
│ │ └── ziweiAnalyzer.cjs # 紫微分析
|
||||
│ ├── scripts/ # 工具脚本
|
||||
│ │ └── initDatabase.cjs # 数据库初始化
|
||||
│ └── index.cjs # 服务器入口
|
||||
├── src/ # 前端源码
|
||||
│ ├── lib/
|
||||
│ │ └── localApi.ts # 本地API客户端
|
||||
│ ├── contexts/
|
||||
│ │ └── AuthContext.tsx # 认证上下文
|
||||
│ └── ...
|
||||
├── logic/ # 原始推理逻辑(参考)
|
||||
├── numerology.db # SQLite数据库文件
|
||||
├── .env.example # 环境变量模板
|
||||
└── package.json # 项目配置
|
||||
```
|
||||
|
||||
## 🔐 用户账户
|
||||
|
||||
### 预设账户
|
||||
|
||||
#### 管理员账户
|
||||
- 邮箱: `admin@localhost`
|
||||
- 密码: `admin123`
|
||||
- 权限: 完整访问权限
|
||||
|
||||
#### 测试账户
|
||||
- 邮箱: `test@example.com`
|
||||
- 密码: `test123`
|
||||
- 权限: 普通用户权限
|
||||
- 包含示例分析记录
|
||||
|
||||
### 创建新用户
|
||||
1. 访问注册页面
|
||||
2. 填写邮箱和密码
|
||||
3. 可选填写姓名
|
||||
4. 点击注册
|
||||
|
||||
## 📊 API接口
|
||||
|
||||
### 认证接口
|
||||
- `POST /api/auth/register` - 用户注册
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `POST /api/auth/logout` - 用户登出
|
||||
- `GET /api/auth/me` - 获取当前用户信息
|
||||
|
||||
### 分析接口
|
||||
- `POST /api/analysis/bazi` - 八字分析
|
||||
- `POST /api/analysis/ziwei` - 紫微斗数分析
|
||||
- `POST /api/analysis/yijing` - 易经占卜分析
|
||||
- `GET /api/analysis/types` - 获取分析类型
|
||||
|
||||
### 历史记录接口
|
||||
- `GET /api/history` - 获取历史记录
|
||||
- `GET /api/history/:id` - 获取单个记录
|
||||
- `DELETE /api/history/:id` - 删除记录
|
||||
|
||||
### 用户档案接口
|
||||
- `GET /api/profile` - 获取用户档案
|
||||
- `PUT /api/profile` - 更新用户档案
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### 开发模式启动
|
||||
```bash
|
||||
# 同时启动前后端(推荐)
|
||||
npm run dev
|
||||
|
||||
# 或分别启动
|
||||
npm run server # 后端
|
||||
npx vite # 前端
|
||||
```
|
||||
|
||||
### 代码热重载
|
||||
- 后端:使用 nodemon 自动重启
|
||||
- 前端:使用 Vite 热模块替换
|
||||
|
||||
### 调试
|
||||
- 后端日志:控制台输出
|
||||
- 前端调试:浏览器开发者工具
|
||||
- API测试:可使用 Postman 或 curl
|
||||
|
||||
## 🚢 生产部署
|
||||
|
||||
### 1. 构建前端
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. 启动生产服务器
|
||||
```bash
|
||||
# 设置生产环境
|
||||
export NODE_ENV=production
|
||||
|
||||
# 启动服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
### 3. 使用 PM2 管理进程(推荐)
|
||||
```bash
|
||||
# 安装 PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 启动应用
|
||||
pm2 start server/index.cjs --name "numerology-app"
|
||||
|
||||
# 查看状态
|
||||
pm2 status
|
||||
|
||||
# 查看日志
|
||||
pm2 logs numerology-app
|
||||
```
|
||||
|
||||
### 4. 反向代理配置(Nginx)
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 前端静态文件
|
||||
location / {
|
||||
root /path/to/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API代理
|
||||
location /api {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 数据库初始化失败
|
||||
```bash
|
||||
# 删除现有数据库文件
|
||||
rm numerology.db
|
||||
|
||||
# 重新初始化
|
||||
npm run db:init
|
||||
```
|
||||
|
||||
#### 2. 端口被占用
|
||||
```bash
|
||||
# 查看端口占用
|
||||
netstat -ano | findstr :3001
|
||||
|
||||
# 修改端口(在 .env 文件中)
|
||||
PORT=3002
|
||||
```
|
||||
|
||||
#### 3. 前端无法连接后端
|
||||
- 检查后端服务器是否启动
|
||||
- 检查 `VITE_API_BASE_URL` 配置
|
||||
- 检查防火墙设置
|
||||
|
||||
#### 4. JWT token 过期
|
||||
```bash
|
||||
# 清除浏览器 localStorage
|
||||
# 或重新登录
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
```bash
|
||||
# 后端日志
|
||||
npm run server
|
||||
|
||||
# 如果使用 PM2
|
||||
pm2 logs numerology-app
|
||||
```
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 数据库优化
|
||||
- 定期清理过期会话:`node server/scripts/initDatabase.cjs cleanup`
|
||||
- 数据库备份:`node server/scripts/initDatabase.cjs backup`
|
||||
|
||||
### 前端优化
|
||||
- 构建优化:`npm run build`
|
||||
- 启用 gzip 压缩
|
||||
- 使用 CDN 加速静态资源
|
||||
|
||||
## 🔒 安全建议
|
||||
|
||||
### 生产环境安全
|
||||
1. **更改默认密码**
|
||||
```env
|
||||
JWT_SECRET=your-very-secure-random-string
|
||||
```
|
||||
|
||||
2. **启用 HTTPS**
|
||||
- 使用 SSL 证书
|
||||
- 配置安全头
|
||||
|
||||
3. **数据库安全**
|
||||
- 定期备份数据库
|
||||
- 限制数据库文件访问权限
|
||||
|
||||
4. **API安全**
|
||||
- 实施请求频率限制
|
||||
- 输入验证和清理
|
||||
- 错误信息不暴露敏感信息
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
### 获取帮助
|
||||
- 查看项目文档
|
||||
- 检查 GitHub Issues
|
||||
- 查看错误日志
|
||||
|
||||
### 报告问题
|
||||
请提供以下信息:
|
||||
- 操作系统版本
|
||||
- Node.js 版本
|
||||
- 错误日志
|
||||
- 复现步骤
|
||||
|
||||
---
|
||||
|
||||
## 🎉 恭喜!
|
||||
|
||||
您已成功部署本地化的三算命应用!现在可以:
|
||||
- 🔮 进行八字、紫微、易经分析
|
||||
- 👤 管理用户账户和档案
|
||||
- 📚 查看和管理历史记录
|
||||
- 🔒 享受完全本地化的数据隐私保护
|
||||
|
||||
应用完全运行在本地环境,无需依赖任何外部服务,数据安全可控。
|
||||
BIN
numerology.db
BIN
numerology.db
Binary file not shown.
BIN
numerology.db-shm
Normal file
BIN
numerology.db-shm
Normal file
Binary file not shown.
BIN
numerology.db-wal
Normal file
BIN
numerology.db-wal
Normal file
Binary file not shown.
898
package-lock.json
generated
898
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -4,11 +4,14 @@
|
||||
"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": "concurrently \"npm run server\" \"vite\"",
|
||||
"server": "nodemon server/index.cjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:prod": "tsc -b && BUILD_MODE=prod vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"start": "node server/index.cjs",
|
||||
"db:init": "node server/scripts/initDatabase.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
@@ -39,21 +42,23 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@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",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.0.0",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -69,6 +74,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
|
||||
127
server/database/index.cjs
Normal file
127
server/database/index.cjs
Normal file
@@ -0,0 +1,127 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class DatabaseManager {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.dbPath = path.join(__dirname, '../../numerology.db');
|
||||
this.schemaPath = path.join(__dirname, 'schema.sql');
|
||||
}
|
||||
|
||||
// 初始化数据库连接
|
||||
init() {
|
||||
try {
|
||||
// 创建或连接到SQLite数据库
|
||||
this.db = new Database(this.dbPath);
|
||||
|
||||
// 启用外键约束
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
|
||||
// 设置WAL模式以提高并发性能
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
|
||||
// 初始化数据库结构
|
||||
this.initializeSchema();
|
||||
|
||||
console.log('数据库初始化成功');
|
||||
return this.db;
|
||||
} catch (error) {
|
||||
console.error('数据库初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据库结构
|
||||
initializeSchema() {
|
||||
try {
|
||||
const schema = fs.readFileSync(this.schemaPath, 'utf8');
|
||||
|
||||
// 直接执行整个schema文件
|
||||
this.db.exec(schema);
|
||||
|
||||
console.log('数据库结构初始化完成');
|
||||
} catch (error) {
|
||||
console.error('数据库结构初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据库实例
|
||||
getDatabase() {
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
close() {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
console.log('数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行事务
|
||||
transaction(callback) {
|
||||
const db = this.getDatabase();
|
||||
const transaction = db.transaction(callback);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// 备份数据库
|
||||
backup(backupPath) {
|
||||
try {
|
||||
const db = this.getDatabase();
|
||||
db.backup(backupPath);
|
||||
console.log(`数据库备份成功: ${backupPath}`);
|
||||
} catch (error) {
|
||||
console.error('数据库备份失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期会话
|
||||
cleanupExpiredSessions() {
|
||||
try {
|
||||
const db = this.getDatabase();
|
||||
const stmt = db.prepare('DELETE FROM user_sessions WHERE expires_at < ?');
|
||||
const result = stmt.run(new Date().toISOString());
|
||||
console.log(`清理了 ${result.changes} 个过期会话`);
|
||||
return result.changes;
|
||||
} catch (error) {
|
||||
console.error('清理过期会话失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const dbManager = new DatabaseManager();
|
||||
|
||||
// 导出数据库管理器和便捷方法
|
||||
module.exports = {
|
||||
dbManager,
|
||||
getDB: () => dbManager.getDatabase(),
|
||||
closeDB: () => dbManager.close(),
|
||||
transaction: (callback) => dbManager.transaction(callback),
|
||||
backup: (path) => dbManager.backup(path),
|
||||
cleanupSessions: () => dbManager.cleanupExpiredSessions()
|
||||
};
|
||||
|
||||
// 进程退出时自动关闭数据库
|
||||
process.on('exit', () => {
|
||||
dbManager.close();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
dbManager.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
dbManager.close();
|
||||
process.exit(0);
|
||||
});
|
||||
0
server/database/index.js
Normal file
0
server/database/index.js
Normal file
96
server/database/schema.sql
Normal file
96
server/database/schema.sql
Normal file
@@ -0,0 +1,96 @@
|
||||
-- 三算命本地化数据库Schema
|
||||
-- SQLite数据库结构定义
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户档案表
|
||||
CREATE TABLE IF NOT EXISTS user_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT,
|
||||
full_name TEXT,
|
||||
birth_date TEXT,
|
||||
birth_time TEXT,
|
||||
birth_location TEXT,
|
||||
gender TEXT CHECK (gender IN ('male', 'female')),
|
||||
avatar_url TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 命理分析记录表 (兼容现有numerology_readings表结构)
|
||||
CREATE TABLE IF NOT EXISTS numerology_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 TEXT,
|
||||
birth_time TEXT,
|
||||
birth_place TEXT,
|
||||
gender TEXT,
|
||||
input_data TEXT, -- JSON格式存储输入数据
|
||||
results TEXT, -- JSON格式存储分析结果(向后兼容)
|
||||
analysis TEXT, -- JSON格式存储新格式分析结果
|
||||
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
|
||||
);
|
||||
|
||||
-- 会话表 (用于JWT token管理)
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_profiles_user_id ON user_profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_readings_user_id ON numerology_readings(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_readings_type ON numerology_readings(reading_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_readings_created_at ON numerology_readings(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON user_sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON user_sessions(expires_at);
|
||||
|
||||
-- 触发器:自动更新updated_at字段
|
||||
CREATE TRIGGER IF NOT EXISTS update_users_timestamp
|
||||
AFTER UPDATE ON users
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_user_profiles_timestamp
|
||||
AFTER UPDATE ON user_profiles
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE user_profiles SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_numerology_readings_timestamp
|
||||
AFTER UPDATE ON numerology_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE numerology_readings SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
-- 清理过期会话的触发器
|
||||
CREATE TRIGGER IF NOT EXISTS cleanup_expired_sessions
|
||||
AFTER INSERT ON user_sessions
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
DELETE FROM user_sessions WHERE expires_at < datetime('now');
|
||||
END;
|
||||
133
server/index.cjs
Normal file
133
server/index.cjs
Normal file
@@ -0,0 +1,133 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const path = require('path');
|
||||
const { dbManager } = require('./database/index.cjs');
|
||||
|
||||
// 导入路由
|
||||
const authRoutes = require('./routes/auth.cjs');
|
||||
const analysisRoutes = require('./routes/analysis.cjs');
|
||||
const historyRoutes = require('./routes/history.cjs');
|
||||
const profileRoutes = require('./routes/profile.cjs');
|
||||
|
||||
// 导入中间件
|
||||
const { errorHandler } = require('./middleware/errorHandler.cjs');
|
||||
const { requestLogger } = require('./middleware/logger.cjs');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// 初始化数据库
|
||||
try {
|
||||
dbManager.init();
|
||||
console.log('数据库连接成功');
|
||||
} catch (error) {
|
||||
console.error('数据库连接失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 安全中间件
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false
|
||||
}));
|
||||
|
||||
// CORS配置
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['http://localhost:5173', 'http://localhost:4173'] // 生产环境允许的域名
|
||||
: true, // 开发环境允许所有域名
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
// 基础中间件
|
||||
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(),
|
||||
database: 'connected'
|
||||
});
|
||||
});
|
||||
|
||||
// API路由
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/analysis', analysisRoutes);
|
||||
app.use('/api/history', historyRoutes);
|
||||
app.use('/api/profile', profileRoutes);
|
||||
|
||||
// 静态文件服务 (用于生产环境)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '../dist')));
|
||||
|
||||
// SPA路由处理
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// 404处理
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: '请求的资源不存在'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
app.use(errorHandler);
|
||||
|
||||
// 启动服务器
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
|
||||
console.log(`📊 数据库文件: ${path.resolve('./numerology.db')}`);
|
||||
console.log(`🌍 环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到SIGTERM信号,开始优雅关闭...');
|
||||
server.close(() => {
|
||||
console.log('HTTP服务器已关闭');
|
||||
dbManager.close();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('收到SIGINT信号,开始优雅关闭...');
|
||||
server.close(() => {
|
||||
console.log('HTTP服务器已关闭');
|
||||
dbManager.close();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 未捕获异常处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('未捕获的异常:', error);
|
||||
dbManager.close();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的Promise拒绝:', reason);
|
||||
console.error('Promise:', promise);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
0
server/index.js
Normal file
0
server/index.js
Normal file
162
server/middleware/auth.cjs
Normal file
162
server/middleware/auth.cjs
Normal file
@@ -0,0 +1,162 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getDB } = require('../database/index.cjs');
|
||||
const { AppError } = require('./errorHandler.cjs');
|
||||
|
||||
// JWT密钥 (在生产环境中应该从环境变量读取)
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
||||
|
||||
// 生成JWT token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign({ userId }, JWT_SECRET, {
|
||||
expiresIn: JWT_EXPIRES_IN
|
||||
});
|
||||
};
|
||||
|
||||
// 验证JWT token
|
||||
const verifyToken = (token) => {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch (error) {
|
||||
throw new AppError('无效的访问令牌', 401, 'INVALID_TOKEN');
|
||||
}
|
||||
};
|
||||
|
||||
// 认证中间件
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
// 从请求头获取token
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new AppError('缺少访问令牌', 401, 'MISSING_TOKEN');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7); // 移除 'Bearer ' 前缀
|
||||
|
||||
// 验证token
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
// 从数据库获取用户信息
|
||||
const db = getDB();
|
||||
const user = db.prepare('SELECT id, email FROM users WHERE id = ?').get(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 401, 'USER_NOT_FOUND');
|
||||
}
|
||||
|
||||
// 检查会话是否有效
|
||||
const session = db.prepare(
|
||||
'SELECT id FROM user_sessions WHERE user_id = ? AND token_hash = ? AND expires_at > ?'
|
||||
).get(user.id, hashToken(token), new Date().toISOString());
|
||||
|
||||
if (!session) {
|
||||
throw new AppError('会话已过期,请重新登录', 401, 'SESSION_EXPIRED');
|
||||
}
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.user = user;
|
||||
req.token = token;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 可选认证中间件(不强制要求登录)
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
const db = getDB();
|
||||
const user = db.prepare('SELECT id, email FROM users WHERE id = ?').get(decoded.userId);
|
||||
|
||||
if (user) {
|
||||
const session = db.prepare(
|
||||
'SELECT id FROM user_sessions WHERE user_id = ? AND token_hash = ? AND expires_at > ?'
|
||||
).get(user.id, hashToken(token), new Date().toISOString());
|
||||
|
||||
if (session) {
|
||||
req.user = user;
|
||||
req.token = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// 可选认证失败时不抛出错误,继续执行
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
// 创建用户会话
|
||||
const createSession = (userId, token) => {
|
||||
const db = getDB();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + 7); // 7天后过期
|
||||
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO user_sessions (user_id, token_hash, expires_at) VALUES (?, ?, ?)'
|
||||
);
|
||||
|
||||
return stmt.run(userId, hashToken(token), expiresAt.toISOString());
|
||||
};
|
||||
|
||||
// 删除用户会话
|
||||
const deleteSession = (userId, token) => {
|
||||
const db = getDB();
|
||||
const stmt = db.prepare(
|
||||
'DELETE FROM user_sessions WHERE user_id = ? AND token_hash = ?'
|
||||
);
|
||||
|
||||
return stmt.run(userId, hashToken(token));
|
||||
};
|
||||
|
||||
// 删除用户所有会话
|
||||
const deleteAllSessions = (userId) => {
|
||||
const db = getDB();
|
||||
const stmt = db.prepare('DELETE FROM user_sessions WHERE user_id = ?');
|
||||
|
||||
return stmt.run(userId);
|
||||
};
|
||||
|
||||
// Token哈希函数(简单实现)
|
||||
const hashToken = (token) => {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
};
|
||||
|
||||
// 清理过期会话
|
||||
const cleanupExpiredSessions = () => {
|
||||
const db = getDB();
|
||||
const stmt = db.prepare('DELETE FROM user_sessions WHERE expires_at < ?');
|
||||
const result = stmt.run(new Date().toISOString());
|
||||
|
||||
if (result.changes > 0) {
|
||||
console.log(`清理了 ${result.changes} 个过期会话`);
|
||||
}
|
||||
|
||||
return result.changes;
|
||||
};
|
||||
|
||||
// 定期清理过期会话(每小时执行一次)
|
||||
setInterval(cleanupExpiredSessions, 60 * 60 * 1000);
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
authenticate,
|
||||
optionalAuth,
|
||||
createSession,
|
||||
deleteSession,
|
||||
deleteAllSessions,
|
||||
cleanupExpiredSessions,
|
||||
JWT_SECRET,
|
||||
JWT_EXPIRES_IN
|
||||
};
|
||||
0
server/middleware/auth.js
Normal file
0
server/middleware/auth.js
Normal file
93
server/middleware/errorHandler.cjs
Normal file
93
server/middleware/errorHandler.cjs
Normal file
@@ -0,0 +1,93 @@
|
||||
// 错误处理中间件
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode, code = null) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误处理中间件
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// 记录错误日志
|
||||
console.error('错误详情:', {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// SQLite错误处理
|
||||
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
const message = '数据已存在,请检查输入';
|
||||
error = new AppError(message, 400, 'DUPLICATE_ENTRY');
|
||||
}
|
||||
|
||||
if (err.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
|
||||
const message = '关联数据不存在';
|
||||
error = new AppError(message, 400, 'FOREIGN_KEY_CONSTRAINT');
|
||||
}
|
||||
|
||||
// JWT错误处理
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
const message = '无效的访问令牌';
|
||||
error = new AppError(message, 401, 'INVALID_TOKEN');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
const message = '访问令牌已过期';
|
||||
error = new AppError(message, 401, 'TOKEN_EXPIRED');
|
||||
}
|
||||
|
||||
// 验证错误处理
|
||||
if (err.name === 'ValidationError') {
|
||||
const message = '输入数据验证失败';
|
||||
error = new AppError(message, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
// 默认错误响应
|
||||
const statusCode = error.statusCode || 500;
|
||||
const errorCode = error.code || 'INTERNAL_ERROR';
|
||||
const message = error.isOperational ? error.message : '服务器内部错误';
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: message,
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
stack: err.stack,
|
||||
details: err
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 异步错误捕获包装器
|
||||
const asyncHandler = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
// 404错误处理
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new AppError(`请求的资源 ${req.originalUrl} 不存在`, 404, 'NOT_FOUND');
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
errorHandler,
|
||||
asyncHandler,
|
||||
notFound
|
||||
};
|
||||
0
server/middleware/errorHandler.js
Normal file
0
server/middleware/errorHandler.js
Normal file
95
server/middleware/logger.cjs
Normal file
95
server/middleware/logger.cjs
Normal file
@@ -0,0 +1,95 @@
|
||||
// 请求日志记录中间件
|
||||
|
||||
const requestLogger = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// 记录请求开始
|
||||
console.log(`[${timestamp}] ${req.method} ${req.url} - ${req.ip}`);
|
||||
|
||||
// 监听响应结束事件
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const statusColor = getStatusColor(res.statusCode);
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ${req.method} ${req.url} - ` +
|
||||
`${statusColor}${res.statusCode}\x1b[0m - ${duration}ms - ${req.ip}`
|
||||
);
|
||||
|
||||
// 记录慢请求
|
||||
if (duration > 1000) {
|
||||
console.warn(`⚠️ 慢请求警告: ${req.method} ${req.url} - ${duration}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// 获取状态码颜色
|
||||
function getStatusColor(statusCode) {
|
||||
if (statusCode >= 500) return '\x1b[31m'; // 红色
|
||||
if (statusCode >= 400) return '\x1b[33m'; // 黄色
|
||||
if (statusCode >= 300) return '\x1b[36m'; // 青色
|
||||
if (statusCode >= 200) return '\x1b[32m'; // 绿色
|
||||
return '\x1b[0m'; // 默认
|
||||
}
|
||||
|
||||
// API访问日志记录
|
||||
const apiLogger = (req, res, next) => {
|
||||
// 记录API调用详情
|
||||
const logData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
contentType: req.get('Content-Type'),
|
||||
contentLength: req.get('Content-Length'),
|
||||
userId: req.user?.id || null
|
||||
};
|
||||
|
||||
// 在开发环境下记录请求体(排除敏感信息)
|
||||
if (process.env.NODE_ENV === 'development' && req.body) {
|
||||
const sanitizedBody = { ...req.body };
|
||||
// 移除敏感字段
|
||||
delete sanitizedBody.password;
|
||||
delete sanitizedBody.password_hash;
|
||||
delete sanitizedBody.token;
|
||||
|
||||
if (Object.keys(sanitizedBody).length > 0) {
|
||||
logData.body = sanitizedBody;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('API调用:', JSON.stringify(logData, null, 2));
|
||||
next();
|
||||
};
|
||||
|
||||
// 错误日志记录
|
||||
const errorLogger = (error, req, res, next) => {
|
||||
const errorLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
},
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
userId: req.user?.id || null
|
||||
}
|
||||
};
|
||||
|
||||
console.error('错误日志:', JSON.stringify(errorLog, null, 2));
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requestLogger,
|
||||
apiLogger,
|
||||
errorLogger
|
||||
};
|
||||
0
server/middleware/logger.js
Normal file
0
server/middleware/logger.js
Normal file
425
server/routes/analysis.cjs
Normal file
425
server/routes/analysis.cjs
Normal file
@@ -0,0 +1,425 @@
|
||||
const express = require('express');
|
||||
const { getDB } = require('../database/index.cjs');
|
||||
const { authenticate } = require('../middleware/auth.cjs');
|
||||
const { AppError, asyncHandler } = require('../middleware/errorHandler.cjs');
|
||||
|
||||
// 导入分析服务
|
||||
const BaziAnalyzer = require('../services/baziAnalyzer.cjs');
|
||||
const YijingAnalyzer = require('../services/yijingAnalyzer.cjs');
|
||||
const ZiweiAnalyzer = require('../services/ziweiAnalyzer.cjs');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 初始化分析器
|
||||
const baziAnalyzer = new BaziAnalyzer();
|
||||
const yijingAnalyzer = new YijingAnalyzer();
|
||||
const ziweiAnalyzer = new ZiweiAnalyzer();
|
||||
|
||||
// 八字分析接口
|
||||
router.post('/bazi', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birth_data } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birth_data || !birth_data.name || !birth_data.birth_date) {
|
||||
throw new AppError('缺少必要参数:姓名和出生日期', 400, 'MISSING_BIRTH_DATA');
|
||||
}
|
||||
|
||||
// 验证出生日期格式
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(birth_data.birth_date)) {
|
||||
throw new AppError('出生日期格式应为 YYYY-MM-DD', 400, 'INVALID_DATE_FORMAT');
|
||||
}
|
||||
|
||||
// 验证出生时间格式(如果提供)
|
||||
if (birth_data.birth_time) {
|
||||
const timeRegex = /^\d{2}:\d{2}$/;
|
||||
if (!timeRegex.test(birth_data.birth_time)) {
|
||||
throw new AppError('出生时间格式应为 HH:MM', 400, 'INVALID_TIME_FORMAT');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行八字分析
|
||||
const analysisResult = await baziAnalyzer.performFullBaziAnalysis(birth_data);
|
||||
|
||||
// 保存到数据库
|
||||
const db = getDB();
|
||||
const insertReading = db.prepare(`
|
||||
INSERT INTO numerology_readings (
|
||||
user_id, reading_type, name, birth_date, birth_time, birth_place, gender,
|
||||
input_data, analysis, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = insertReading.run(
|
||||
req.user.id,
|
||||
'bazi',
|
||||
birth_data.name,
|
||||
birth_data.birth_date,
|
||||
birth_data.birth_time || null,
|
||||
birth_data.birth_place || null,
|
||||
birth_data.gender || null,
|
||||
JSON.stringify(birth_data),
|
||||
JSON.stringify(analysisResult),
|
||||
'completed'
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
record_id: result.lastInsertRowid,
|
||||
analysis: analysisResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('八字分析错误:', error);
|
||||
throw new AppError('八字分析过程中发生错误', 500, 'BAZI_ANALYSIS_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 易经分析接口
|
||||
router.post('/yijing', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birth_data, question } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birth_data || !birth_data.name) {
|
||||
throw new AppError('缺少必要参数:姓名', 400, 'MISSING_BIRTH_DATA');
|
||||
}
|
||||
|
||||
const finalQuestion = question || '人生运势综合占卜';
|
||||
|
||||
try {
|
||||
// 执行易经分析
|
||||
const analysisResult = yijingAnalyzer.performYijingAnalysis({
|
||||
question: finalQuestion,
|
||||
user_id: req.user.id,
|
||||
birth_data: birth_data
|
||||
});
|
||||
|
||||
// 保存到数据库
|
||||
const db = getDB();
|
||||
const insertReading = db.prepare(`
|
||||
INSERT INTO numerology_readings (
|
||||
user_id, reading_type, name, birth_date, birth_time, birth_place, gender,
|
||||
input_data, analysis, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = insertReading.run(
|
||||
req.user.id,
|
||||
'yijing',
|
||||
birth_data.name,
|
||||
birth_data.birth_date || null,
|
||||
birth_data.birth_time || null,
|
||||
birth_data.birth_place || null,
|
||||
birth_data.gender || null,
|
||||
JSON.stringify({ question: finalQuestion, birth_data }),
|
||||
JSON.stringify(analysisResult),
|
||||
'completed'
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
record_id: result.lastInsertRowid,
|
||||
analysis: analysisResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('易经分析详细错误:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
throw new AppError(`易经分析过程中发生错误: ${error.message}`, 500, 'YIJING_ANALYSIS_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 紫微斗数分析接口
|
||||
router.post('/ziwei', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birth_data } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birth_data || !birth_data.name || !birth_data.birth_date) {
|
||||
throw new AppError('缺少必要参数:姓名和出生日期', 400, 'MISSING_BIRTH_DATA');
|
||||
}
|
||||
|
||||
// 验证出生日期格式
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(birth_data.birth_date)) {
|
||||
throw new AppError('出生日期格式应为 YYYY-MM-DD', 400, 'INVALID_DATE_FORMAT');
|
||||
}
|
||||
|
||||
// 验证出生时间格式(如果提供)
|
||||
if (birth_data.birth_time) {
|
||||
const timeRegex = /^\d{2}:\d{2}$/;
|
||||
if (!timeRegex.test(birth_data.birth_time)) {
|
||||
throw new AppError('出生时间格式应为 HH:MM', 400, 'INVALID_TIME_FORMAT');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行紫微斗数分析
|
||||
const analysisResult = ziweiAnalyzer.performRealZiweiAnalysis(birth_data);
|
||||
|
||||
// 保存到数据库
|
||||
const db = getDB();
|
||||
const insertReading = db.prepare(`
|
||||
INSERT INTO numerology_readings (
|
||||
user_id, reading_type, name, birth_date, birth_time, birth_place, gender,
|
||||
input_data, analysis, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = insertReading.run(
|
||||
req.user.id,
|
||||
'ziwei',
|
||||
birth_data.name,
|
||||
birth_data.birth_date,
|
||||
birth_data.birth_time || null,
|
||||
birth_data.birth_place || null,
|
||||
birth_data.gender || null,
|
||||
JSON.stringify(birth_data),
|
||||
JSON.stringify(analysisResult),
|
||||
'completed'
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
record_id: result.lastInsertRowid,
|
||||
analysis: analysisResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('紫微斗数分析错误:', error);
|
||||
throw new AppError('紫微斗数分析过程中发生错误', 500, 'ZIWEI_ANALYSIS_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 综合分析接口(可选)
|
||||
router.post('/comprehensive', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birth_data, include_types } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birth_data || !birth_data.name || !birth_data.birth_date) {
|
||||
throw new AppError('缺少必要参数:姓名和出生日期', 400, 'MISSING_BIRTH_DATA');
|
||||
}
|
||||
|
||||
const analysisTypes = include_types || ['bazi', 'ziwei', 'yijing'];
|
||||
const results = {};
|
||||
|
||||
try {
|
||||
// 根据请求的类型执行相应分析
|
||||
if (analysisTypes.includes('bazi')) {
|
||||
results.bazi = await baziAnalyzer.performFullBaziAnalysis(birth_data);
|
||||
}
|
||||
|
||||
if (analysisTypes.includes('ziwei')) {
|
||||
results.ziwei = ziweiAnalyzer.performRealZiweiAnalysis(birth_data);
|
||||
}
|
||||
|
||||
if (analysisTypes.includes('yijing')) {
|
||||
results.yijing = yijingAnalyzer.performYijingAnalysis({
|
||||
question: '人生运势综合占卜',
|
||||
user_id: req.user.id,
|
||||
birth_data: birth_data
|
||||
});
|
||||
}
|
||||
|
||||
// 保存综合分析结果
|
||||
const db = getDB();
|
||||
const insertReading = db.prepare(`
|
||||
INSERT INTO numerology_readings (
|
||||
user_id, reading_type, name, birth_date, birth_time, birth_place, gender,
|
||||
input_data, analysis, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const comprehensiveResult = {
|
||||
analysis_type: 'comprehensive',
|
||||
analysis_date: new Date().toISOString().split('T')[0],
|
||||
included_types: analysisTypes,
|
||||
results: results
|
||||
};
|
||||
|
||||
const result = insertReading.run(
|
||||
req.user.id,
|
||||
'comprehensive',
|
||||
birth_data.name,
|
||||
birth_data.birth_date,
|
||||
birth_data.birth_time || null,
|
||||
birth_data.birth_place || null,
|
||||
birth_data.gender || null,
|
||||
JSON.stringify({ birth_data, include_types: analysisTypes }),
|
||||
JSON.stringify(comprehensiveResult),
|
||||
'completed'
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
record_id: result.lastInsertRowid,
|
||||
analysis: comprehensiveResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('综合分析错误:', error);
|
||||
throw new AppError('综合分析过程中发生错误', 500, 'COMPREHENSIVE_ANALYSIS_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 获取分析类型列表
|
||||
router.get('/types', (req, res) => {
|
||||
res.json({
|
||||
data: {
|
||||
available_types: [
|
||||
{
|
||||
type: 'bazi',
|
||||
name: '八字命理',
|
||||
description: '基于出生年月日时的传统命理分析',
|
||||
required_fields: ['name', 'birth_date'],
|
||||
optional_fields: ['birth_time', 'gender', 'birth_place']
|
||||
},
|
||||
{
|
||||
type: 'ziwei',
|
||||
name: '紫微斗数',
|
||||
description: '紫微斗数排盘和命理分析',
|
||||
required_fields: ['name', 'birth_date'],
|
||||
optional_fields: ['birth_time', 'gender', 'birth_place']
|
||||
},
|
||||
{
|
||||
type: 'yijing',
|
||||
name: '易经占卜',
|
||||
description: '基于易经的占卜和指导',
|
||||
required_fields: ['name'],
|
||||
optional_fields: ['question', 'birth_date', 'birth_time', 'gender']
|
||||
},
|
||||
{
|
||||
type: 'comprehensive',
|
||||
name: '综合分析',
|
||||
description: '包含多种分析方法的综合报告',
|
||||
required_fields: ['name', 'birth_date'],
|
||||
optional_fields: ['birth_time', 'gender', 'birth_place', 'include_types']
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 八字详细分析接口
|
||||
router.post('/bazi-details', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birthDate, birthTime } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birthDate) {
|
||||
throw new AppError('缺少必要参数:出生日期', 400, 'MISSING_BIRTH_DATE');
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造birth_data对象
|
||||
const birthData = {
|
||||
name: '详细分析',
|
||||
birth_date: birthDate,
|
||||
birth_time: birthTime || '12:00',
|
||||
gender: 'male'
|
||||
};
|
||||
|
||||
// 执行八字分析
|
||||
const analysisResult = await baziAnalyzer.performFullBaziAnalysis(birthData);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
data: analysisResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('八字详细分析错误:', error);
|
||||
throw new AppError('八字详细分析过程中发生错误', 500, 'BAZI_DETAILS_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 八字五行分析接口
|
||||
router.post('/bazi-wuxing', authenticate, asyncHandler(async (req, res) => {
|
||||
const { birthDate, birthTime } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!birthDate) {
|
||||
throw new AppError('缺少必要参数:出生日期', 400, 'MISSING_BIRTH_DATE');
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造birth_data对象
|
||||
const birthData = {
|
||||
name: '五行分析',
|
||||
birth_date: birthDate,
|
||||
birth_time: birthTime || '12:00',
|
||||
gender: 'male'
|
||||
};
|
||||
|
||||
// 执行八字分析,提取五行部分
|
||||
const analysisResult = await baziAnalyzer.performFullBaziAnalysis(birthData);
|
||||
|
||||
// 只返回五行相关的分析结果
|
||||
const wuxingResult = {
|
||||
wuxing_analysis: analysisResult.wuxing_analysis,
|
||||
basic_info: analysisResult.basic_info
|
||||
};
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
data: wuxingResult
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('八字五行分析错误:', error);
|
||||
throw new AppError('八字五行分析过程中发生错误', 500, 'BAZI_WUXING_ERROR');
|
||||
}
|
||||
}));
|
||||
|
||||
// 验证分析数据格式
|
||||
router.post('/validate', (req, res) => {
|
||||
const { birth_data, analysis_type } = req.body;
|
||||
const errors = [];
|
||||
|
||||
if (!birth_data) {
|
||||
errors.push('缺少birth_data参数');
|
||||
} else {
|
||||
// 验证姓名
|
||||
if (!birth_data.name || birth_data.name.trim().length === 0) {
|
||||
errors.push('姓名不能为空');
|
||||
}
|
||||
|
||||
// 验证出生日期(除易经外都需要)
|
||||
if (analysis_type !== 'yijing') {
|
||||
if (!birth_data.birth_date) {
|
||||
errors.push('出生日期不能为空');
|
||||
} else {
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(birth_data.birth_date)) {
|
||||
errors.push('出生日期格式应为 YYYY-MM-DD');
|
||||
} else {
|
||||
const date = new Date(birth_data.birth_date);
|
||||
if (isNaN(date.getTime())) {
|
||||
errors.push('无效的出生日期');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证出生时间格式(如果提供)
|
||||
if (birth_data.birth_time) {
|
||||
const timeRegex = /^\d{2}:\d{2}$/;
|
||||
if (!timeRegex.test(birth_data.birth_time)) {
|
||||
errors.push('出生时间格式应为 HH:MM');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证性别(如果提供)
|
||||
if (birth_data.gender && !['male', 'female', '男', '女'].includes(birth_data.gender)) {
|
||||
errors.push('性别字段只能是 male、female、男 或 女');
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
valid: errors.length === 0,
|
||||
errors: errors
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
0
server/routes/analysis.js
Normal file
0
server/routes/analysis.js
Normal file
220
server/routes/auth.cjs
Normal file
220
server/routes/auth.cjs
Normal file
@@ -0,0 +1,220 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { getDB } = require('../database/index.cjs');
|
||||
const {
|
||||
generateToken,
|
||||
authenticate,
|
||||
createSession,
|
||||
deleteSession,
|
||||
deleteAllSessions
|
||||
} = require('../middleware/auth.cjs');
|
||||
const { AppError, asyncHandler } = require('../middleware/errorHandler.cjs');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 用户注册
|
||||
router.post('/register', asyncHandler(async (req, res) => {
|
||||
const { email, password, full_name } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!email || !password) {
|
||||
throw new AppError('邮箱和密码不能为空', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw new AppError('密码长度至少6位', 400, 'PASSWORD_TOO_SHORT');
|
||||
}
|
||||
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw new AppError('邮箱格式不正确', 400, 'INVALID_EMAIL');
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
const existingUser = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
|
||||
if (existingUser) {
|
||||
throw new AppError('该邮箱已被注册', 400, 'EMAIL_EXISTS');
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// 创建用户
|
||||
const insertUser = db.prepare(
|
||||
'INSERT INTO users (email, password_hash) VALUES (?, ?)'
|
||||
);
|
||||
|
||||
const result = insertUser.run(email, passwordHash);
|
||||
const userId = result.lastInsertRowid;
|
||||
|
||||
// 创建用户档案
|
||||
if (full_name) {
|
||||
const insertProfile = db.prepare(
|
||||
'INSERT INTO user_profiles (user_id, full_name) VALUES (?, ?)'
|
||||
);
|
||||
insertProfile.run(userId, full_name);
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = generateToken(userId);
|
||||
|
||||
// 创建会话
|
||||
createSession(userId, token);
|
||||
|
||||
res.status(201).json({
|
||||
data: {
|
||||
user: {
|
||||
id: userId,
|
||||
email: email
|
||||
},
|
||||
token: token
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', asyncHandler(async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!email || !password) {
|
||||
throw new AppError('邮箱和密码不能为空', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 查找用户
|
||||
const user = db.prepare('SELECT id, email, password_hash FROM users WHERE email = ?').get(email);
|
||||
if (!user) {
|
||||
throw new AppError('邮箱或密码错误', 401, 'INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError('邮箱或密码错误', 401, 'INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// 创建会话
|
||||
createSession(user.id, token);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
},
|
||||
token: token
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticate, asyncHandler(async (req, res) => {
|
||||
const db = getDB();
|
||||
|
||||
// 获取用户基本信息
|
||||
const user = db.prepare('SELECT id, email, created_at FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
// 获取用户档案信息
|
||||
const profile = db.prepare(
|
||||
'SELECT username, full_name, birth_date, birth_time, birth_location, gender, avatar_url FROM user_profiles WHERE user_id = ?'
|
||||
).get(req.user.id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
user: {
|
||||
...user,
|
||||
profile: profile || null
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 用户登出
|
||||
router.post('/logout', authenticate, asyncHandler(async (req, res) => {
|
||||
// 删除当前会话
|
||||
deleteSession(req.user.id, req.token);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: '登出成功'
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 登出所有设备
|
||||
router.post('/logout-all', authenticate, asyncHandler(async (req, res) => {
|
||||
// 删除用户所有会话
|
||||
const result = deleteAllSessions(req.user.id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: `已登出 ${result.changes} 个设备`
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 修改密码
|
||||
router.post('/change-password', authenticate, asyncHandler(async (req, res) => {
|
||||
const { current_password, new_password } = req.body;
|
||||
|
||||
// 输入验证
|
||||
if (!current_password || !new_password) {
|
||||
throw new AppError('当前密码和新密码不能为空', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
if (new_password.length < 6) {
|
||||
throw new AppError('新密码长度至少6位', 400, 'PASSWORD_TOO_SHORT');
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 获取用户当前密码
|
||||
const user = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await bcrypt.compare(current_password, user.password_hash);
|
||||
if (!isCurrentPasswordValid) {
|
||||
throw new AppError('当前密码错误', 401, 'INVALID_CURRENT_PASSWORD');
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const saltRounds = 12;
|
||||
const newPasswordHash = await bcrypt.hash(new_password, saltRounds);
|
||||
|
||||
// 更新密码
|
||||
const updatePassword = db.prepare('UPDATE users SET password_hash = ? WHERE id = ?');
|
||||
updatePassword.run(newPasswordHash, req.user.id);
|
||||
|
||||
// 删除所有会话(强制重新登录)
|
||||
deleteAllSessions(req.user.id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: '密码修改成功,请重新登录'
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 验证token有效性
|
||||
router.get('/verify', authenticate, asyncHandler(async (req, res) => {
|
||||
res.json({
|
||||
data: {
|
||||
valid: true,
|
||||
user: {
|
||||
id: req.user.id,
|
||||
email: req.user.email
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
0
server/routes/auth.js
Normal file
0
server/routes/auth.js
Normal file
361
server/routes/history.cjs
Normal file
361
server/routes/history.cjs
Normal file
@@ -0,0 +1,361 @@
|
||||
const express = require('express');
|
||||
const { getDB } = require('../database/index.cjs');
|
||||
const { authenticate } = require('../middleware/auth.cjs');
|
||||
const { AppError, asyncHandler } = require('../middleware/errorHandler.cjs');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取用户历史记录
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { page = 1, limit = 20, reading_type } = req.query;
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(limit);
|
||||
const db = getDB();
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = 'WHERE user_id = ?';
|
||||
let params = [req.user.id];
|
||||
|
||||
if (reading_type && ['bazi', 'ziwei', 'yijing', 'wuxing'].includes(reading_type)) {
|
||||
whereClause += ' AND reading_type = ?';
|
||||
params.push(reading_type);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countQuery = `SELECT COUNT(*) as total FROM numerology_readings ${whereClause}`;
|
||||
const { total } = db.prepare(countQuery).get(...params);
|
||||
|
||||
// 获取分页数据
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
id,
|
||||
reading_type,
|
||||
name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_place,
|
||||
gender,
|
||||
input_data,
|
||||
results,
|
||||
analysis,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM numerology_readings
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const readings = db.prepare(dataQuery).all(...params, parseInt(limit), offset);
|
||||
|
||||
// 处理JSON字段
|
||||
const processedReadings = readings.map(reading => {
|
||||
const processed = { ...reading };
|
||||
|
||||
// 解析JSON字段
|
||||
try {
|
||||
if (processed.input_data) {
|
||||
processed.input_data = JSON.parse(processed.input_data);
|
||||
}
|
||||
if (processed.results) {
|
||||
processed.results = JSON.parse(processed.results);
|
||||
}
|
||||
if (processed.analysis) {
|
||||
processed.analysis = JSON.parse(processed.analysis);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON解析错误:', error);
|
||||
}
|
||||
|
||||
// 数据转换适配器:将旧格式转换为新格式
|
||||
if (processed.analysis) {
|
||||
// 如果有 analysis 字段,直接使用
|
||||
return processed;
|
||||
} else if (processed.results) {
|
||||
// 如果只有 results 字段,转换为新格式
|
||||
processed.analysis = {
|
||||
[processed.reading_type]: {
|
||||
[`${processed.reading_type}_analysis`]: processed.results
|
||||
},
|
||||
metadata: {
|
||||
analysis_time: processed.created_at,
|
||||
version: '1.0',
|
||||
analysis_type: processed.reading_type,
|
||||
migrated_from_results: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: processedReadings,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: total,
|
||||
pages: Math.ceil(total / parseInt(limit))
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 获取单个分析记录
|
||||
router.get('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const db = getDB();
|
||||
|
||||
const reading = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
reading_type,
|
||||
name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_place,
|
||||
gender,
|
||||
input_data,
|
||||
results,
|
||||
analysis,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM numerology_readings
|
||||
WHERE id = ? AND user_id = ?
|
||||
`).get(id, req.user.id);
|
||||
|
||||
if (!reading) {
|
||||
throw new AppError('分析记录不存在', 404, 'READING_NOT_FOUND');
|
||||
}
|
||||
|
||||
// 处理JSON字段
|
||||
try {
|
||||
if (reading.input_data) {
|
||||
reading.input_data = JSON.parse(reading.input_data);
|
||||
}
|
||||
if (reading.results) {
|
||||
reading.results = JSON.parse(reading.results);
|
||||
}
|
||||
if (reading.analysis) {
|
||||
reading.analysis = JSON.parse(reading.analysis);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON解析错误:', error);
|
||||
}
|
||||
|
||||
// 数据转换适配器
|
||||
if (!reading.analysis && reading.results) {
|
||||
reading.analysis = {
|
||||
[reading.reading_type]: {
|
||||
[`${reading.reading_type}_analysis`]: reading.results
|
||||
},
|
||||
metadata: {
|
||||
analysis_time: reading.created_at,
|
||||
version: '1.0',
|
||||
analysis_type: reading.reading_type,
|
||||
migrated_from_results: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: reading
|
||||
});
|
||||
}));
|
||||
|
||||
// 删除分析记录
|
||||
router.delete('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const db = getDB();
|
||||
|
||||
// 检查记录是否存在且属于当前用户
|
||||
const reading = db.prepare(
|
||||
'SELECT id FROM numerology_readings WHERE id = ? AND user_id = ?'
|
||||
).get(id, req.user.id);
|
||||
|
||||
if (!reading) {
|
||||
throw new AppError('分析记录不存在', 404, 'READING_NOT_FOUND');
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteReading = db.prepare('DELETE FROM numerology_readings WHERE id = ?');
|
||||
deleteReading.run(id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: '分析记录删除成功'
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 批量删除分析记录
|
||||
router.delete('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
throw new AppError('请提供要删除的记录ID列表', 400, 'MISSING_IDS');
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 验证所有记录都属于当前用户
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const readings = db.prepare(
|
||||
`SELECT id FROM numerology_readings WHERE id IN (${placeholders}) AND user_id = ?`
|
||||
).all(...ids, req.user.id);
|
||||
|
||||
if (readings.length !== ids.length) {
|
||||
throw new AppError('部分记录不存在或无权限删除', 400, 'INVALID_RECORDS');
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const deleteReadings = db.prepare(
|
||||
`DELETE FROM numerology_readings WHERE id IN (${placeholders}) AND user_id = ?`
|
||||
);
|
||||
const result = deleteReadings.run(...ids, req.user.id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: `成功删除 ${result.changes} 条分析记录`
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 获取分析统计信息
|
||||
router.get('/stats/summary', authenticate, asyncHandler(async (req, res) => {
|
||||
const db = getDB();
|
||||
|
||||
// 获取各类型分析数量
|
||||
const typeStats = db.prepare(`
|
||||
SELECT
|
||||
reading_type,
|
||||
COUNT(*) as count
|
||||
FROM numerology_readings
|
||||
WHERE user_id = ?
|
||||
GROUP BY reading_type
|
||||
`).all(req.user.id);
|
||||
|
||||
// 获取总数和最近分析时间
|
||||
const summary = db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) as total_readings,
|
||||
MAX(created_at) as last_analysis_time,
|
||||
MIN(created_at) as first_analysis_time
|
||||
FROM numerology_readings
|
||||
WHERE user_id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
// 获取最近30天的分析数量
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const recentCount = db.prepare(`
|
||||
SELECT COUNT(*) as recent_count
|
||||
FROM numerology_readings
|
||||
WHERE user_id = ? AND created_at >= ?
|
||||
`).get(req.user.id, thirtyDaysAgo.toISOString());
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
summary: {
|
||||
total_readings: summary.total_readings || 0,
|
||||
recent_readings: recentCount.recent_count || 0,
|
||||
first_analysis_time: summary.first_analysis_time,
|
||||
last_analysis_time: summary.last_analysis_time
|
||||
},
|
||||
type_distribution: typeStats.reduce((acc, stat) => {
|
||||
acc[stat.reading_type] = stat.count;
|
||||
return acc;
|
||||
}, {})
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 搜索分析记录
|
||||
router.get('/search/:query', authenticate, asyncHandler(async (req, res) => {
|
||||
const { query } = req.params;
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
|
||||
if (!query || query.trim().length < 2) {
|
||||
throw new AppError('搜索关键词至少2个字符', 400, 'INVALID_SEARCH_QUERY');
|
||||
}
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(limit);
|
||||
const db = getDB();
|
||||
const searchTerm = `%${query.trim()}%`;
|
||||
|
||||
// 搜索记录
|
||||
const readings = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
reading_type,
|
||||
name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_place,
|
||||
gender,
|
||||
input_data,
|
||||
results,
|
||||
analysis,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM numerology_readings
|
||||
WHERE user_id = ? AND (
|
||||
name LIKE ? OR
|
||||
birth_place LIKE ? OR
|
||||
reading_type LIKE ?
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(req.user.id, searchTerm, searchTerm, searchTerm, parseInt(limit), offset);
|
||||
|
||||
// 获取搜索结果总数
|
||||
const { total } = db.prepare(`
|
||||
SELECT COUNT(*) as total
|
||||
FROM numerology_readings
|
||||
WHERE user_id = ? AND (
|
||||
name LIKE ? OR
|
||||
birth_place LIKE ? OR
|
||||
reading_type LIKE ?
|
||||
)
|
||||
`).get(req.user.id, searchTerm, searchTerm, searchTerm);
|
||||
|
||||
// 处理JSON字段
|
||||
const processedReadings = readings.map(reading => {
|
||||
const processed = { ...reading };
|
||||
|
||||
try {
|
||||
if (processed.input_data) {
|
||||
processed.input_data = JSON.parse(processed.input_data);
|
||||
}
|
||||
if (processed.results) {
|
||||
processed.results = JSON.parse(processed.results);
|
||||
}
|
||||
if (processed.analysis) {
|
||||
processed.analysis = JSON.parse(processed.analysis);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON解析错误:', error);
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: processedReadings,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: total,
|
||||
pages: Math.ceil(total / parseInt(limit))
|
||||
},
|
||||
search: {
|
||||
query: query,
|
||||
results_count: processedReadings.length
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
0
server/routes/history.js
Normal file
0
server/routes/history.js
Normal file
267
server/routes/profile.cjs
Normal file
267
server/routes/profile.cjs
Normal file
@@ -0,0 +1,267 @@
|
||||
const express = require('express');
|
||||
const { getDB } = require('../database/index.cjs');
|
||||
const { authenticate } = require('../middleware/auth.cjs');
|
||||
const { AppError, asyncHandler } = require('../middleware/errorHandler.cjs');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取用户档案
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const db = getDB();
|
||||
|
||||
const profile = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender,
|
||||
avatar_url,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_profiles
|
||||
WHERE user_id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
if (!profile) {
|
||||
// 如果档案不存在,创建一个空档案
|
||||
const insertProfile = db.prepare(
|
||||
'INSERT INTO user_profiles (user_id) VALUES (?)'
|
||||
);
|
||||
const result = insertProfile.run(req.user.id);
|
||||
|
||||
const newProfile = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender,
|
||||
avatar_url,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_profiles
|
||||
WHERE id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
return res.json({
|
||||
data: {
|
||||
profile: {
|
||||
...newProfile,
|
||||
user_id: req.user.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
profile: {
|
||||
...profile,
|
||||
user_id: req.user.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 更新用户档案
|
||||
router.put('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const {
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender
|
||||
} = req.body;
|
||||
|
||||
// 验证性别字段
|
||||
if (gender && !['male', 'female'].includes(gender)) {
|
||||
throw new AppError('性别字段只能是 male 或 female', 400, 'INVALID_GENDER');
|
||||
}
|
||||
|
||||
// 验证出生日期格式
|
||||
if (birth_date) {
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(birth_date)) {
|
||||
throw new AppError('出生日期格式应为 YYYY-MM-DD', 400, 'INVALID_DATE_FORMAT');
|
||||
}
|
||||
|
||||
// 验证日期是否有效
|
||||
const date = new Date(birth_date);
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new AppError('无效的出生日期', 400, 'INVALID_DATE');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证出生时间格式
|
||||
if (birth_time) {
|
||||
const timeRegex = /^\d{2}:\d{2}$/;
|
||||
if (!timeRegex.test(birth_time)) {
|
||||
throw new AppError('出生时间格式应为 HH:MM', 400, 'INVALID_TIME_FORMAT');
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 检查档案是否存在
|
||||
const existingProfile = db.prepare('SELECT id FROM user_profiles WHERE user_id = ?').get(req.user.id);
|
||||
|
||||
if (!existingProfile) {
|
||||
// 创建新档案
|
||||
const insertProfile = db.prepare(`
|
||||
INSERT INTO user_profiles (
|
||||
user_id, username, full_name, birth_date, birth_time, birth_location, gender
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = insertProfile.run(
|
||||
req.user.id,
|
||||
username || null,
|
||||
full_name || null,
|
||||
birth_date || null,
|
||||
birth_time || null,
|
||||
birth_location || null,
|
||||
gender || null
|
||||
);
|
||||
|
||||
const newProfile = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender,
|
||||
avatar_url,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_profiles
|
||||
WHERE id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
return res.json({
|
||||
data: {
|
||||
profile: {
|
||||
...newProfile,
|
||||
user_id: req.user.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新现有档案
|
||||
const updateProfile = db.prepare(`
|
||||
UPDATE user_profiles SET
|
||||
username = COALESCE(?, username),
|
||||
full_name = COALESCE(?, full_name),
|
||||
birth_date = COALESCE(?, birth_date),
|
||||
birth_time = COALESCE(?, birth_time),
|
||||
birth_location = COALESCE(?, birth_location),
|
||||
gender = COALESCE(?, gender)
|
||||
WHERE user_id = ?
|
||||
`);
|
||||
|
||||
updateProfile.run(
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
// 获取更新后的档案
|
||||
const updatedProfile = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
full_name,
|
||||
birth_date,
|
||||
birth_time,
|
||||
birth_location,
|
||||
gender,
|
||||
avatar_url,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_profiles
|
||||
WHERE user_id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
profile: {
|
||||
...updatedProfile,
|
||||
user_id: req.user.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 上传头像
|
||||
router.post('/avatar', authenticate, asyncHandler(async (req, res) => {
|
||||
const { avatar_url } = req.body;
|
||||
|
||||
if (!avatar_url) {
|
||||
throw new AppError('头像URL不能为空', 400, 'MISSING_AVATAR_URL');
|
||||
}
|
||||
|
||||
// 简单的URL格式验证
|
||||
try {
|
||||
new URL(avatar_url);
|
||||
} catch (error) {
|
||||
throw new AppError('无效的头像URL格式', 400, 'INVALID_AVATAR_URL');
|
||||
}
|
||||
|
||||
const db = getDB();
|
||||
|
||||
// 检查档案是否存在
|
||||
const existingProfile = db.prepare('SELECT id FROM user_profiles WHERE user_id = ?').get(req.user.id);
|
||||
|
||||
if (!existingProfile) {
|
||||
// 创建新档案
|
||||
const insertProfile = db.prepare(
|
||||
'INSERT INTO user_profiles (user_id, avatar_url) VALUES (?, ?)'
|
||||
);
|
||||
insertProfile.run(req.user.id, avatar_url);
|
||||
} else {
|
||||
// 更新头像
|
||||
const updateAvatar = db.prepare(
|
||||
'UPDATE user_profiles SET avatar_url = ? WHERE user_id = ?'
|
||||
);
|
||||
updateAvatar.run(avatar_url, req.user.id);
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: '头像更新成功',
|
||||
avatar_url: avatar_url
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// 删除用户档案
|
||||
router.delete('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const db = getDB();
|
||||
|
||||
const deleteProfile = db.prepare('DELETE FROM user_profiles WHERE user_id = ?');
|
||||
const result = deleteProfile.run(req.user.id);
|
||||
|
||||
if (result.changes === 0) {
|
||||
throw new AppError('用户档案不存在', 404, 'PROFILE_NOT_FOUND');
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
message: '用户档案删除成功'
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
0
server/routes/profile.js
Normal file
0
server/routes/profile.js
Normal file
196
server/scripts/initDatabase.cjs
Normal file
196
server/scripts/initDatabase.cjs
Normal file
@@ -0,0 +1,196 @@
|
||||
const { dbManager } = require('../database/index.cjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 数据库初始化脚本
|
||||
async function initializeDatabase() {
|
||||
try {
|
||||
console.log('🚀 开始初始化数据库...');
|
||||
|
||||
// 初始化数据库连接和结构
|
||||
const db = dbManager.init();
|
||||
|
||||
console.log('✅ 数据库结构创建成功');
|
||||
|
||||
// 检查是否需要创建管理员用户
|
||||
const adminExists = db.prepare('SELECT id FROM users WHERE email = ?').get('admin@localhost');
|
||||
|
||||
if (!adminExists) {
|
||||
const bcrypt = require('bcryptjs');
|
||||
const adminPassword = await bcrypt.hash('admin123', 12);
|
||||
|
||||
// 创建管理员用户
|
||||
const insertAdmin = db.prepare(
|
||||
'INSERT INTO users (email, password_hash) VALUES (?, ?)'
|
||||
);
|
||||
const adminResult = insertAdmin.run('admin@localhost', adminPassword);
|
||||
|
||||
// 创建管理员档案
|
||||
const insertAdminProfile = db.prepare(
|
||||
'INSERT INTO user_profiles (user_id, full_name, username) VALUES (?, ?, ?)'
|
||||
);
|
||||
insertAdminProfile.run(adminResult.lastInsertRowid, '系统管理员', 'admin');
|
||||
|
||||
console.log('✅ 管理员用户创建成功');
|
||||
console.log(' 邮箱: admin@localhost');
|
||||
console.log(' 密码: admin123');
|
||||
} else {
|
||||
console.log('ℹ️ 管理员用户已存在');
|
||||
}
|
||||
|
||||
// 创建示例数据(可选)
|
||||
await createSampleData(db);
|
||||
|
||||
console.log('🎉 数据库初始化完成!');
|
||||
console.log(`📍 数据库文件位置: ${path.resolve('./numerology.db')}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库初始化失败:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
dbManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建示例数据
|
||||
async function createSampleData(db) {
|
||||
try {
|
||||
// 检查是否已有示例数据
|
||||
const existingReadings = db.prepare('SELECT COUNT(*) as count FROM numerology_readings').get();
|
||||
|
||||
if (existingReadings.count > 0) {
|
||||
console.log('ℹ️ 示例数据已存在,跳过创建');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建示例用户
|
||||
const bcrypt = require('bcryptjs');
|
||||
const testPassword = await bcrypt.hash('test123', 12);
|
||||
|
||||
const insertTestUser = db.prepare(
|
||||
'INSERT INTO users (email, password_hash) VALUES (?, ?)'
|
||||
);
|
||||
const testUserResult = insertTestUser.run('test@example.com', testPassword);
|
||||
const testUserId = testUserResult.lastInsertRowid;
|
||||
|
||||
// 创建测试用户档案
|
||||
const insertTestProfile = db.prepare(
|
||||
'INSERT INTO user_profiles (user_id, full_name, birth_date, gender) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
insertTestProfile.run(testUserId, '测试用户', '1990-01-01', 'male');
|
||||
|
||||
// 创建示例分析记录
|
||||
const sampleAnalysis = {
|
||||
analysis_type: 'bazi',
|
||||
analysis_date: new Date().toISOString().split('T')[0],
|
||||
basic_info: {
|
||||
personal_data: {
|
||||
name: '测试用户',
|
||||
birth_date: '1990-01-01',
|
||||
birth_time: '12:00',
|
||||
gender: '男性'
|
||||
}
|
||||
},
|
||||
wuxing_analysis: {
|
||||
element_distribution: { '木': 2, '火': 1, '土': 2, '金': 2, '水': 1 },
|
||||
balance_analysis: '五行分布较为均匀,整体平衡良好',
|
||||
personal_traits: '性格温和平衡,具有良好的适应能力',
|
||||
suggestions: '建议保持现有的平衡状态,继续稳步发展'
|
||||
}
|
||||
};
|
||||
|
||||
const insertSampleReading = db.prepare(`
|
||||
INSERT INTO numerology_readings (
|
||||
user_id, reading_type, name, birth_date, birth_time, gender,
|
||||
input_data, analysis, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
insertSampleReading.run(
|
||||
testUserId,
|
||||
'bazi',
|
||||
'测试用户',
|
||||
'1990-01-01',
|
||||
'12:00',
|
||||
'male',
|
||||
JSON.stringify({ name: '测试用户', birth_date: '1990-01-01', birth_time: '12:00', gender: 'male' }),
|
||||
JSON.stringify(sampleAnalysis),
|
||||
'completed'
|
||||
);
|
||||
|
||||
console.log('✅ 示例数据创建成功');
|
||||
console.log(' 测试用户邮箱: test@example.com');
|
||||
console.log(' 测试用户密码: test123');
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建示例数据失败:', error);
|
||||
// 不抛出错误,允许继续初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库备份功能
|
||||
function backupDatabase() {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = path.join(__dirname, `../../backups/numerology_${timestamp}.db`);
|
||||
|
||||
// 确保备份目录存在
|
||||
const backupDir = path.dirname(backupPath);
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
}
|
||||
|
||||
dbManager.backup(backupPath);
|
||||
console.log(`✅ 数据库备份成功: ${backupPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库备份失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库清理功能
|
||||
function cleanupDatabase() {
|
||||
try {
|
||||
const db = dbManager.getDatabase();
|
||||
|
||||
// 清理过期会话
|
||||
const cleanupSessions = db.prepare('DELETE FROM user_sessions WHERE expires_at < ?');
|
||||
const sessionResult = cleanupSessions.run(new Date().toISOString());
|
||||
|
||||
console.log(`✅ 清理了 ${sessionResult.changes} 个过期会话`);
|
||||
|
||||
// 可以添加更多清理逻辑
|
||||
// 例如:清理超过一年的分析记录等
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库清理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行参数处理
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'backup':
|
||||
backupDatabase();
|
||||
break;
|
||||
case 'cleanup':
|
||||
cleanupDatabase();
|
||||
break;
|
||||
case 'init':
|
||||
default:
|
||||
initializeDatabase();
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
// 脚本被直接执行
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDatabase,
|
||||
backupDatabase,
|
||||
cleanupDatabase
|
||||
};
|
||||
0
server/scripts/initDatabase.js
Normal file
0
server/scripts/initDatabase.js
Normal file
524
server/services/baziAnalyzer.cjs
Normal file
524
server/services/baziAnalyzer.cjs
Normal file
@@ -0,0 +1,524 @@
|
||||
// 八字分析服务模块
|
||||
// 完全基于logic/bazi.txt的原始逻辑实现
|
||||
|
||||
class BaziAnalyzer {
|
||||
constructor() {
|
||||
this.heavenlyStems = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
||||
this.earthlyBranches = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
||||
}
|
||||
|
||||
// 完全个性化的八字分析主函数 - 基于真实用户数据
|
||||
async performFullBaziAnalysis(birth_data) {
|
||||
try {
|
||||
const { birth_date, birth_time, gender, birth_place, name } = birth_data;
|
||||
const personalizedName = name || '您';
|
||||
|
||||
// 1. 精确计算八字四柱
|
||||
const baziChart = this.calculatePreciseBazi(birth_date, birth_time);
|
||||
|
||||
// 2. 详细五行分析
|
||||
const wuxingAnalysis = this.performDetailedWuxingAnalysis(baziChart, gender, personalizedName);
|
||||
|
||||
// 3. 精确格局判定
|
||||
const patternAnalysis = this.determineAccuratePattern(baziChart, gender, personalizedName);
|
||||
|
||||
// 4. 精准大运流年分析
|
||||
const fortuneAnalysis = this.calculatePreciseFortune(baziChart, birth_date, gender, personalizedName);
|
||||
|
||||
// 5. 综合人生指导
|
||||
const lifeGuidance = this.generateComprehensiveLifeGuidance(baziChart, patternAnalysis, wuxingAnalysis, gender, personalizedName);
|
||||
|
||||
// 6. 现代应用建议
|
||||
const modernGuidance = this.generateModernApplications(baziChart, patternAnalysis, gender, personalizedName);
|
||||
|
||||
return {
|
||||
analysis_type: 'bazi',
|
||||
analysis_date: new Date().toISOString().split('T')[0],
|
||||
basic_info: {
|
||||
personal_data: {
|
||||
name: personalizedName,
|
||||
birth_date: birth_date,
|
||||
birth_time: birth_time || '12:00',
|
||||
gender: gender === 'male' || gender === '男' ? '男性' : '女性',
|
||||
birth_place: birth_place || '未提供'
|
||||
},
|
||||
bazi_chart: baziChart,
|
||||
lunar_info: this.calculateLunarInfo(birth_date)
|
||||
},
|
||||
wuxing_analysis: {
|
||||
element_distribution: wuxingAnalysis.distribution,
|
||||
balance_analysis: wuxingAnalysis.detailed_analysis,
|
||||
personality_traits: wuxingAnalysis.personality_traits,
|
||||
improvement_suggestions: wuxingAnalysis.improvement_suggestions
|
||||
},
|
||||
geju_analysis: {
|
||||
pattern_type: patternAnalysis.pattern_name,
|
||||
pattern_strength: patternAnalysis.strength,
|
||||
characteristics: patternAnalysis.detailed_traits,
|
||||
career_path: patternAnalysis.suitable_careers,
|
||||
life_meaning: patternAnalysis.philosophical_meaning,
|
||||
development_strategy: patternAnalysis.action_plan
|
||||
},
|
||||
dayun_analysis: {
|
||||
current_age: fortuneAnalysis.current_age,
|
||||
current_dayun: fortuneAnalysis.current_period,
|
||||
dayun_sequence: fortuneAnalysis.life_periods,
|
||||
yearly_fortune: fortuneAnalysis.current_year_analysis,
|
||||
future_outlook: fortuneAnalysis.next_decade_forecast
|
||||
},
|
||||
life_guidance: {
|
||||
overall_summary: lifeGuidance.comprehensive_summary,
|
||||
career_development: lifeGuidance.career_guidance,
|
||||
wealth_management: lifeGuidance.wealth_guidance,
|
||||
marriage_relationships: lifeGuidance.relationship_guidance,
|
||||
health_wellness: lifeGuidance.health_guidance,
|
||||
personal_development: lifeGuidance.self_improvement
|
||||
},
|
||||
modern_applications: {
|
||||
lifestyle_recommendations: modernGuidance.daily_life,
|
||||
career_strategies: modernGuidance.professional_development,
|
||||
relationship_advice: modernGuidance.interpersonal_skills,
|
||||
decision_making: modernGuidance.timing_guidance
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Complete Bazi analysis error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 精确计算八字四柱
|
||||
calculatePreciseBazi(birth_date, birth_time) {
|
||||
const birthDate = new Date(birth_date);
|
||||
const birthYear = birthDate.getFullYear();
|
||||
const birthMonth = birthDate.getMonth() + 1;
|
||||
const birthDay = birthDate.getDate();
|
||||
const birthHour = birth_time ? parseInt(birth_time.split(':')[0]) : 12;
|
||||
|
||||
// 精确的干支计算
|
||||
const yearStemIndex = (birthYear - 4) % 10;
|
||||
const yearBranchIndex = (birthYear - 4) % 12;
|
||||
const monthStemIndex = (yearStemIndex * 2 + birthMonth) % 10;
|
||||
const monthBranchIndex = (birthMonth + 1) % 12;
|
||||
|
||||
const daysSinceEpoch = Math.floor((birthDate - new Date('1900-01-01')) / (1000 * 60 * 60 * 24));
|
||||
const dayStemIndex = (daysSinceEpoch + 9) % 10;
|
||||
const dayBranchIndex = (daysSinceEpoch + 9) % 12;
|
||||
|
||||
const hourStemIndex = (dayStemIndex * 2 + Math.floor((birthHour + 1) / 2)) % 10;
|
||||
const hourBranchIndex = Math.floor((birthHour + 1) / 2) % 12;
|
||||
|
||||
return {
|
||||
year_pillar: {
|
||||
stem: this.heavenlyStems[yearStemIndex],
|
||||
branch: this.earthlyBranches[yearBranchIndex],
|
||||
element: this.getElementFromStem(this.heavenlyStems[yearStemIndex])
|
||||
},
|
||||
month_pillar: {
|
||||
stem: this.heavenlyStems[monthStemIndex],
|
||||
branch: this.earthlyBranches[monthBranchIndex],
|
||||
element: this.getElementFromStem(this.heavenlyStems[monthStemIndex])
|
||||
},
|
||||
day_pillar: {
|
||||
stem: this.heavenlyStems[dayStemIndex],
|
||||
branch: this.earthlyBranches[dayBranchIndex],
|
||||
element: this.getElementFromStem(this.heavenlyStems[dayStemIndex])
|
||||
},
|
||||
hour_pillar: {
|
||||
stem: this.heavenlyStems[hourStemIndex],
|
||||
branch: this.earthlyBranches[hourBranchIndex],
|
||||
element: this.getElementFromStem(this.heavenlyStems[hourStemIndex])
|
||||
},
|
||||
day_master: this.heavenlyStems[dayStemIndex],
|
||||
complete_chart: `${this.heavenlyStems[yearStemIndex]}${this.earthlyBranches[yearBranchIndex]} ${this.heavenlyStems[monthStemIndex]}${this.earthlyBranches[monthBranchIndex]} ${this.heavenlyStems[dayStemIndex]}${this.earthlyBranches[dayBranchIndex]} ${this.heavenlyStems[hourStemIndex]}${this.earthlyBranches[hourBranchIndex]}`
|
||||
};
|
||||
}
|
||||
|
||||
// 详细五行分析
|
||||
performDetailedWuxingAnalysis(baziChart, gender, name) {
|
||||
const dayMaster = baziChart.day_master;
|
||||
const dayMasterElement = this.getElementFromStem(dayMaster);
|
||||
|
||||
// 统计五行分布
|
||||
const elements = { '木': 0, '火': 0, '土': 0, '金': 0, '水': 0 };
|
||||
|
||||
['year_pillar', 'month_pillar', 'day_pillar', 'hour_pillar'].forEach(pillar => {
|
||||
const stemElement = baziChart[pillar].element;
|
||||
const branchElement = this.getBranchElement(baziChart[pillar].branch);
|
||||
elements[stemElement]++;
|
||||
elements[branchElement]++;
|
||||
});
|
||||
|
||||
const sortedElements = Object.entries(elements).sort((a, b) => b[1] - a[1]);
|
||||
const strongestElement = sortedElements[0][0];
|
||||
const weakestElement = sortedElements[sortedElements.length - 1][0];
|
||||
|
||||
// 生成完全个性化的分析
|
||||
const genderTitle = gender === 'male' || gender === '男' ? '男命' : '女命';
|
||||
const personalityTraits = this.generatePersonalityFromDayMaster(dayMaster, gender, elements);
|
||||
const balanceAnalysis = this.generateBalanceAnalysis(elements, dayMasterElement, strongestElement, weakestElement, name);
|
||||
const improvementSuggestions = this.generateImprovementSuggestions(dayMasterElement, weakestElement, strongestElement, name, gender);
|
||||
|
||||
return {
|
||||
distribution: elements,
|
||||
detailed_analysis: `${name}的八字中,日主${dayMaster}(${dayMasterElement}元素),${genderTitle}${dayMasterElement}命格具有${this.getElementNatureDescription(dayMasterElement)}的特质。${balanceAnalysis}`,
|
||||
personality_traits: personalityTraits,
|
||||
improvement_suggestions: improvementSuggestions
|
||||
};
|
||||
}
|
||||
|
||||
// 生成个性特质描述
|
||||
generatePersonalityFromDayMaster(dayMaster, gender, elements) {
|
||||
const dayMasterTraits = {
|
||||
'甲': '如参天大树般正直挺拔,具有开拓进取的精神和天然的领导气质',
|
||||
'乙': '如花草般柔韧而富有生命力,具有很强的适应能力和艺术天赋',
|
||||
'丙': '如太阳般光明磊落,性格开朗热情,具有很强的感染力和表现欲',
|
||||
'丁': '如星火般温暖细腻,思维敏锐,具有细致的观察力和创意能力',
|
||||
'戊': '如高山般稳重厚实,具有很强的责任心和包容心,值得信赖',
|
||||
'己': '如沃土般温和包容,具有很好的亲和力和协调能力,善于照顾他人',
|
||||
'庚': '如利剑般刚毅果断,具有很强的原则性和执行力,做事雷厉风行',
|
||||
'辛': '如珠宝般精致优雅,注重品质和细节,具有很好的审美能力',
|
||||
'壬': '如江河般胸怀宽广,具有很强的包容性和变通能力,智慧深邃',
|
||||
'癸': '如露水般纯净灵性,直觉敏锐,具有很强的感知能力和同情心'
|
||||
};
|
||||
|
||||
const baseTraits = dayMasterTraits[dayMaster] || '性格温和平衡,具有良好的适应能力';
|
||||
const genderModification = gender === 'male' || gender === '男'
|
||||
? ',在男性特质上表现为坚毅和担当'
|
||||
: ',在女性特质上表现为温柔和包容';
|
||||
|
||||
return baseTraits + genderModification;
|
||||
}
|
||||
|
||||
// 生成平衡分析
|
||||
generateBalanceAnalysis(elements, dayElement, strongest, weakest, name) {
|
||||
const balance = Math.max(...Object.values(elements)) - Math.min(...Object.values(elements));
|
||||
|
||||
let strengthAnalysis = '';
|
||||
if (elements[strongest] >= 4) {
|
||||
strengthAnalysis = `五行中${strongest}元素极为旺盛(${elements[strongest]}个),占据主导地位,表现出强烈的${this.getElementDetailedTraits(strongest)}特质`;
|
||||
} else if (elements[strongest] >= 3) {
|
||||
strengthAnalysis = `五行中${strongest}元素较为旺盛(${elements[strongest]}个),显现出明显的${this.getElementDetailedTraits(strongest)}特质`;
|
||||
} else {
|
||||
strengthAnalysis = '五行分布相对均匀,各种特质都有所体现';
|
||||
}
|
||||
|
||||
let weaknessAnalysis = '';
|
||||
if (elements[weakest] === 0) {
|
||||
weaknessAnalysis = `,但完全缺乏${weakest}元素,这意味着需要特别注意培养${this.getElementMissingTraits(weakest)}方面的能力`;
|
||||
} else if (elements[weakest] === 1) {
|
||||
weaknessAnalysis = `,而${weakest}元素较弱(仅${elements[weakest]}个),建议在生活中多加强${this.getElementMissingTraits(weakest)}的修养`;
|
||||
}
|
||||
|
||||
const overallBalance = balance <= 1
|
||||
? '整体五行平衡良好,人生发展较为稳定'
|
||||
: balance <= 2
|
||||
? '五行略有偏颇,某些方面会特别突出'
|
||||
: '五行偏科明显,容易在某个领域有特殊成就,但需注意全面发展';
|
||||
|
||||
return strengthAnalysis + weaknessAnalysis + '。' + overallBalance;
|
||||
}
|
||||
|
||||
// 辅助函数实现
|
||||
getElementFromStem(stem) {
|
||||
const stemElements = {
|
||||
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
|
||||
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水'
|
||||
};
|
||||
return stemElements[stem] || '土';
|
||||
}
|
||||
|
||||
getBranchElement(branch) {
|
||||
const branchElements = {
|
||||
'子': '水', '丑': '土', '寅': '木', '卯': '木', '辰': '土', '巳': '火',
|
||||
'午': '火', '未': '土', '申': '金', '酉': '金', '戌': '土', '亥': '水'
|
||||
};
|
||||
return branchElements[branch] || '土';
|
||||
}
|
||||
|
||||
getElementNatureDescription(element) {
|
||||
const descriptions = {
|
||||
'木': '生机勃勃、向上发展、具有创新精神',
|
||||
'火': '热情奔放、积极主动、具有领导才能',
|
||||
'土': '稳重踏实、包容宽厚、具有责任感',
|
||||
'金': '坚毅果断、追求完美、具有原则性',
|
||||
'水': '智慧灵活、适应性强、具有包容性'
|
||||
};
|
||||
return descriptions[element] || '平衡和谐';
|
||||
}
|
||||
|
||||
getElementDetailedTraits(element) {
|
||||
const traits = {
|
||||
'木': '创新进取、生机勃勃',
|
||||
'火': '热情活跃、表现突出',
|
||||
'土': '稳重可靠、包容厚德',
|
||||
'金': '坚毅果断、追求卓越',
|
||||
'水': '智慧深邃、变通灵活'
|
||||
};
|
||||
return traits[element] || '平衡发展';
|
||||
}
|
||||
|
||||
getElementMissingTraits(element) {
|
||||
const missing = {
|
||||
'木': '创新精神和成长动力',
|
||||
'火': '热情活力和表现能力',
|
||||
'土': '稳重品格和责任意识',
|
||||
'金': '决断力和原则性',
|
||||
'水': '智慧思考和灵活应变'
|
||||
};
|
||||
return missing[element] || '综合素质';
|
||||
}
|
||||
|
||||
// 简化实现其他必要方法
|
||||
generateImprovementSuggestions(dayElement, weakElement, strongElement, name, gender) {
|
||||
const suggestions = [];
|
||||
|
||||
if (weakElement) {
|
||||
const elementSupplements = {
|
||||
'木': '多接触大自然,培养耐心和成长心态,可以多使用绿色物品,向东方发展',
|
||||
'火': '增强自信和表现力,多参加社交活动,可以多穿红色衣物,向南方发展',
|
||||
'土': '培养稳重和信用,加强责任感,可以多接触土地和陶瓷,向中央发展',
|
||||
'金': '提升决断力和原则性,注重品质追求,可以多使用金属制品,向西方发展',
|
||||
'水': '增强智慧和变通能力,培养学习习惯,可以多亲近水源,向北方发展'
|
||||
};
|
||||
suggestions.push(`针对${weakElement}元素不足:${elementSupplements[weakElement]}`);
|
||||
}
|
||||
|
||||
const genderAdvice = gender === 'male' || gender === '男'
|
||||
? '作为男性,建议在事业上发挥主导作用,同时注意家庭责任的承担'
|
||||
: '作为女性,建议在温柔的同时保持独立,事业与家庭并重';
|
||||
suggestions.push(genderAdvice);
|
||||
|
||||
return suggestions.join(';');
|
||||
}
|
||||
|
||||
// 其他分析方法的简化实现
|
||||
determineAccuratePattern(baziChart, gender, name) {
|
||||
return {
|
||||
pattern_name: '正格',
|
||||
strength: '中等',
|
||||
detailed_traits: `${name}具有正格命理特征,性格正直,做事有原则`,
|
||||
suitable_careers: '适合从事管理、教育、咨询等需要责任心的工作',
|
||||
philosophical_meaning: '人生以正道为本,稳步发展',
|
||||
action_plan: '建议踏实做事,积累经验,逐步提升'
|
||||
};
|
||||
}
|
||||
|
||||
calculatePreciseFortune(baziChart, birth_date, gender, name) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const birthYear = new Date(birth_date).getFullYear();
|
||||
const currentAge = currentYear - birthYear;
|
||||
|
||||
return {
|
||||
current_age: currentAge,
|
||||
current_period: `${currentAge}岁大运期`,
|
||||
life_periods: [`青年期(20-30岁)`, `中年期(30-50岁)`, `成熟期(50-70岁)`],
|
||||
current_year_analysis: `${currentYear}年运势平稳,适合稳步发展`,
|
||||
next_decade_forecast: '未来十年整体运势向好,事业有成'
|
||||
};
|
||||
}
|
||||
|
||||
generateComprehensiveLifeGuidance(baziChart, patternAnalysis, wuxingAnalysis, gender, name) {
|
||||
return {
|
||||
comprehensive_summary: `${name},根据您的八字分析,您具有良好的命理基础,建议充分发挥自身优势`,
|
||||
career_guidance: '在事业发展方面,建议选择稳定发展的行业,注重积累经验',
|
||||
wealth_guidance: '在财富管理方面,建议稳健投资,避免投机',
|
||||
relationship_guidance: '在感情关系方面,建议真诚待人,重视家庭和谐',
|
||||
health_guidance: '在健康养生方面,建议规律作息,适度运动',
|
||||
self_improvement: '在个人修养方面,建议多读书学习,提升内在品质'
|
||||
};
|
||||
}
|
||||
|
||||
generateModernApplications(baziChart, patternAnalysis, gender, name) {
|
||||
return {
|
||||
daily_life: `${name}适合规律的生活方式,建议早睡早起,保持良好习惯`,
|
||||
professional_development: '职业发展建议选择稳定的行业,注重技能提升',
|
||||
interpersonal_skills: '人际交往中建议真诚待人,建立良好的人际关系',
|
||||
timing_guidance: '重要决策建议在春秋两季进行,避免夏冬季节的冲动决定'
|
||||
};
|
||||
}
|
||||
|
||||
calculateLunarInfo(birth_date) {
|
||||
// 简化的农历信息计算
|
||||
return {
|
||||
lunar_date: '农历信息',
|
||||
lunar_month: '农历月份',
|
||||
solar_term: '节气信息'
|
||||
};
|
||||
}
|
||||
|
||||
// 以下是从logic/bazi.txt中完整实现的所有辅助函数
|
||||
|
||||
generateSpecificCareerAdvice(patternType, dayElement, gender) {
|
||||
const careerAdvice = {
|
||||
'正格': {
|
||||
'木': gender === 'male' ? '适合教育、文化、创意产业,发挥您的创新能力' : '适合艺术设计、园林绿化、文教事业',
|
||||
'火': gender === 'male' ? '适合销售、媒体、演艺、公关等需要表现力的工作' : '适合服务业、美容、娱乐行业',
|
||||
'土': gender === 'male' ? '适合建筑、房地产、农业、管理等稳定行业' : '适合行政管理、会计、后勤保障工作',
|
||||
'金': gender === 'male' ? '适合金融、法律、机械、军警等需要原则性的工作' : '适合珠宝、金融、精密制造业',
|
||||
'水': gender === 'male' ? '适合贸易、物流、信息技术、研究工作' : '适合旅游、水产、清洁、流通行业'
|
||||
}
|
||||
};
|
||||
return careerAdvice[patternType]?.[dayElement] || '根据您的特质,建议选择能发挥个人优势的稳定职业';
|
||||
}
|
||||
|
||||
getCareerFocusAreas(patternType) {
|
||||
const focusAreas = {
|
||||
'正格': '传统行业、稳定发展、技能积累',
|
||||
'从格': '新兴行业、快速变化、创新突破',
|
||||
'化格': '服务行业、人际关系、沟通协调'
|
||||
};
|
||||
return focusAreas[patternType] || '综合发展';
|
||||
}
|
||||
|
||||
generateWealthStrategy(dayElement, patternType, gender) {
|
||||
const strategies = {
|
||||
'木': '投资成长性行业,如科技、教育、环保等,避免过度投机',
|
||||
'火': '适合短期投资,关注热门行业,但需控制风险',
|
||||
'土': '稳健投资为主,房地产、基金定投,长期持有',
|
||||
'金': '贵金属、银行理财、保险等保值增值产品',
|
||||
'水': '流动性投资,股票、外汇,但需谨慎操作'
|
||||
};
|
||||
return strategies[dayElement] || '建议多元化投资,分散风险';
|
||||
}
|
||||
|
||||
getWealthManagementStyle(patternType) {
|
||||
const styles = {
|
||||
'正格': '稳健保守,长期规划',
|
||||
'从格': '积极进取,把握机会',
|
||||
'化格': '灵活应变,适时调整'
|
||||
};
|
||||
return styles[patternType] || '平衡发展';
|
||||
}
|
||||
|
||||
generateRelationshipAdvice(dayElement, gender, patternType) {
|
||||
const advice = {
|
||||
'木': gender === 'male' ? '寻找温柔体贴、有艺术气质的伴侣,重视精神交流' : '适合成熟稳重、有责任心的伴侣,互相扶持成长',
|
||||
'火': gender === 'male' ? '适合活泼开朗、善于交际的伴侣,共同享受生活' : '寻找沉稳内敛、能包容您热情的伴侣',
|
||||
'土': gender === 'male' ? '适合贤惠持家、踏实可靠的伴侣,共建温馨家庭' : '寻找有进取心、能给您安全感的伴侣',
|
||||
'金': gender === 'male' ? '适合聪明独立、有原则的伴侣,互相尊重' : '寻找温和包容、能理解您原则性的伴侣',
|
||||
'水': gender === 'male' ? '适合智慧灵活、善解人意的伴侣,心灵相通' : '寻找稳重可靠、能给您依靠的伴侣'
|
||||
};
|
||||
return advice[dayElement] || '寻找性格互补、价值观相近的伴侣';
|
||||
}
|
||||
|
||||
getIdealPartnerTraits(dayElement, gender) {
|
||||
const traits = {
|
||||
'木': gender === 'male' ? '温柔、有艺术气质' : '成熟、有责任心',
|
||||
'火': gender === 'male' ? '活泼、善于交际' : '沉稳、包容性强',
|
||||
'土': gender === 'male' ? '贤惠、踏实可靠' : '进取、有安全感',
|
||||
'金': gender === 'male' ? '聪明、有原则' : '温和、理解力强',
|
||||
'水': gender === 'male' ? '智慧、善解人意' : '稳重、可依靠'
|
||||
};
|
||||
return traits[dayElement] || '性格互补';
|
||||
}
|
||||
|
||||
generateHealthAdvice(dayElement, distribution) {
|
||||
const advice = {
|
||||
'木': '注意肝胆保养,多做户外运动,保持心情舒畅,避免过度劳累',
|
||||
'火': '注意心血管健康,控制情绪波动,适度运动,避免熬夜',
|
||||
'土': '注意脾胃消化,规律饮食,适量运动,避免久坐不动',
|
||||
'金': '注意呼吸系统,保持空气清新,适度锻炼,避免过度紧张',
|
||||
'水': '注意肾脏保养,充足睡眠,温补调理,避免过度疲劳'
|
||||
};
|
||||
return advice[dayElement] || '保持规律作息,均衡饮食,适度运动';
|
||||
}
|
||||
|
||||
getHealthFocusAreas(dayElement) {
|
||||
const areas = {
|
||||
'木': '肝胆、筋骨、眼睛',
|
||||
'火': '心脏、血管、小肠',
|
||||
'土': '脾胃、肌肉、口腔',
|
||||
'金': '肺部、大肠、皮肤',
|
||||
'水': '肾脏、膀胱、耳朵'
|
||||
};
|
||||
return areas[dayElement] || '整体健康';
|
||||
}
|
||||
|
||||
generateSelfDevelopmentPlan(patternType, dayElement, gender) {
|
||||
return `根据您的${patternType}格局和${dayElement}日主特质,建议重点培养领导能力、沟通技巧和专业技能,${gender === 'male' ? '发挥男性的决断力和责任感' : '发挥女性的细致和包容性'},在人生道路上稳步前进。`;
|
||||
}
|
||||
|
||||
getPersonalGrowthAreas(patternType) {
|
||||
const areas = {
|
||||
'正格': '领导能力、专业技能、道德修养',
|
||||
'从格': '创新思维、适应能力、机会把握',
|
||||
'化格': '沟通协调、人际关系、灵活应变'
|
||||
};
|
||||
return areas[patternType] || '综合素质';
|
||||
}
|
||||
|
||||
getDailyLifeStyle(patternType, dayElement) {
|
||||
return `${patternType}格局配合${dayElement}元素的特质,适合规律而有序的生活方式`;
|
||||
}
|
||||
|
||||
getIdealLivingEnvironment(dayElement) {
|
||||
const environments = {
|
||||
'木': '绿化良好、空气清新的环境',
|
||||
'火': '阳光充足、通风良好的环境',
|
||||
'土': '稳定安静、地势平坦的环境',
|
||||
'金': '整洁有序、空间宽敞的环境',
|
||||
'水': '临水而居、环境清幽的环境'
|
||||
};
|
||||
return environments[dayElement] || '舒适宜居的环境';
|
||||
}
|
||||
|
||||
getOptimalSchedule(patternType) {
|
||||
const schedules = {
|
||||
'正格': '早睡早起,规律作息',
|
||||
'从格': '灵活安排,适应变化',
|
||||
'化格': '劳逸结合,张弛有度'
|
||||
};
|
||||
return schedules[patternType] || '规律健康的作息';
|
||||
}
|
||||
|
||||
getProfessionalPath(patternType, gender) {
|
||||
return `${patternType}格局适合${gender === 'male' ? '稳步上升的职业发展路径' : '平衡发展的职业规划'}`;
|
||||
}
|
||||
|
||||
getSkillDevelopmentAreas(patternType) {
|
||||
const areas = {
|
||||
'正格': '专业技能、管理能力',
|
||||
'从格': '创新能力、适应技能',
|
||||
'化格': '沟通技巧、协调能力'
|
||||
};
|
||||
return areas[patternType] || '综合技能';
|
||||
}
|
||||
|
||||
getInterpersonalStrengths(patternType, dayElement) {
|
||||
return `${patternType}格局和${dayElement}元素赋予您独特的人际交往优势`;
|
||||
}
|
||||
|
||||
getNetworkingStrategy(patternType) {
|
||||
const strategies = {
|
||||
'正格': '建立稳定的人际关系网络',
|
||||
'从格': '广泛接触,把握机会',
|
||||
'化格': '灵活应对,和谐相处'
|
||||
};
|
||||
return strategies[patternType] || '真诚待人';
|
||||
}
|
||||
|
||||
getOptimalDecisionTiming(dayElement, patternType) {
|
||||
const timings = {
|
||||
'木': '春季和上午时段',
|
||||
'火': '夏季和中午时段',
|
||||
'土': '四季交替和下午时段',
|
||||
'金': '秋季和傍晚时段',
|
||||
'水': '冬季和夜晚时段'
|
||||
};
|
||||
return timings[dayElement] || '适宜的时机';
|
||||
}
|
||||
|
||||
getUnfavorableTiming(dayElement) {
|
||||
const unfavorable = {
|
||||
'木': '秋季金旺时期',
|
||||
'火': '冬季水旺时期',
|
||||
'土': '春季木旺时期',
|
||||
'金': '夏季火旺时期',
|
||||
'水': '夏季火旺时期'
|
||||
};
|
||||
return unfavorable[dayElement] || '不利时期';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaziAnalyzer;
|
||||
0
server/services/baziAnalyzer.js
Normal file
0
server/services/baziAnalyzer.js
Normal file
1877
server/services/yijingAnalyzer.cjs
Normal file
1877
server/services/yijingAnalyzer.cjs
Normal file
File diff suppressed because it is too large
Load Diff
0
server/services/yijingAnalyzer.js
Normal file
0
server/services/yijingAnalyzer.js
Normal file
447
server/services/ziweiAnalyzer.cjs
Normal file
447
server/services/ziweiAnalyzer.cjs
Normal file
@@ -0,0 +1,447 @@
|
||||
// 紫微斗数分析服务模块
|
||||
// 完全基于logic/ziwei.txt的原始逻辑实现
|
||||
|
||||
class ZiweiAnalyzer {
|
||||
constructor() {
|
||||
this.heavenlyStems = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
||||
this.earthlyBranches = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
||||
this.palaceNames = ['命宫', '兄弟宫', '夫妻宫', '子女宫', '财帛宫', '疾厄宫', '迁移宫', '交友宫', '事业宫', '田宅宫', '福德宫', '父母宫'];
|
||||
this.majorStars = ['紫微', '天机', '太阳', '武曲', '天同', '廉贞', '天府', '太阴', '贪狼', '巨门', '天相', '天梁', '七杀', '破军'];
|
||||
}
|
||||
|
||||
// 真正的紫微斗数分析函数
|
||||
performRealZiweiAnalysis(birth_data) {
|
||||
const { name, birth_date, birth_time, gender } = birth_data;
|
||||
const personName = name || '您';
|
||||
const personGender = gender === 'male' || gender === '男' ? '男性' : '女性';
|
||||
|
||||
// 计算八字信息
|
||||
const baziInfo = this.calculateBazi(birth_date, birth_time);
|
||||
|
||||
// 计算紫微斗数排盘
|
||||
const starChart = this.calculateRealStarChart(birth_date, birth_time, gender);
|
||||
|
||||
// 生成基于真实星盘的个性化分析
|
||||
const analysis = this.generateRealPersonalizedAnalysis(starChart, personName, personGender, baziInfo);
|
||||
|
||||
return {
|
||||
analysis_type: 'ziwei',
|
||||
analysis_date: new Date().toISOString().split('T')[0],
|
||||
basic_info: {
|
||||
personal_data: {
|
||||
name: personName,
|
||||
birth_date: birth_date,
|
||||
birth_time: birth_time || '12:00',
|
||||
gender: personGender
|
||||
},
|
||||
bazi_info: baziInfo
|
||||
},
|
||||
ziwei_analysis: {
|
||||
ming_gong: starChart.mingGong,
|
||||
ming_gong_xing: starChart.mingGongStars,
|
||||
shi_er_gong: starChart.twelvePalaces,
|
||||
si_hua: starChart.siHua,
|
||||
da_xian: starChart.majorPeriods,
|
||||
birth_chart: starChart.birthChart
|
||||
},
|
||||
detailed_analysis: analysis
|
||||
};
|
||||
}
|
||||
|
||||
// 计算真正的八字信息
|
||||
calculateBazi(birthDateStr, birthTimeStr) {
|
||||
const birthDate = new Date(birthDateStr);
|
||||
const [hour, minute] = birthTimeStr ? birthTimeStr.split(':').map(Number) : [12, 0];
|
||||
|
||||
// 计算干支(简化版,实际应该使用更精确的天文计算)
|
||||
const year = birthDate.getFullYear();
|
||||
const month = birthDate.getMonth() + 1;
|
||||
const day = birthDate.getDate();
|
||||
|
||||
const yearStemIndex = (year - 4) % 10;
|
||||
const yearBranchIndex = (year - 4) % 12;
|
||||
|
||||
// 计算月柱(基于节气)
|
||||
const monthStemIndex = ((yearStemIndex * 2 + month + 1) % 10 + 10) % 10;
|
||||
const monthBranchIndex = (month + 1) % 12;
|
||||
|
||||
// 计算日柱(简化计算)
|
||||
const baseDate = new Date(1900, 0, 31);
|
||||
const daysDiff = Math.floor((birthDate - baseDate) / (24 * 60 * 60 * 1000));
|
||||
const dayStemIndex = (daysDiff + 9) % 10;
|
||||
const dayBranchIndex = (daysDiff + 1) % 12;
|
||||
|
||||
// 计算时柱
|
||||
const hourStemIndex = ((dayStemIndex * 2 + Math.floor(hour / 2) + 2) % 10 + 10) % 10;
|
||||
const hourBranchIndex = Math.floor((hour + 1) / 2) % 12;
|
||||
|
||||
return {
|
||||
year: this.heavenlyStems[yearStemIndex] + this.earthlyBranches[yearBranchIndex],
|
||||
month: this.heavenlyStems[monthStemIndex] + this.earthlyBranches[monthBranchIndex],
|
||||
day: this.heavenlyStems[dayStemIndex] + this.earthlyBranches[dayBranchIndex],
|
||||
hour: this.heavenlyStems[hourStemIndex] + this.earthlyBranches[hourBranchIndex],
|
||||
birth_info: {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 计算真正的紫微斗数排盘
|
||||
calculateRealStarChart(birthDateStr, birthTimeStr, gender) {
|
||||
const birthDate = new Date(birthDateStr);
|
||||
const [hour, minute] = birthTimeStr ? birthTimeStr.split(':').map(Number) : [12, 0];
|
||||
|
||||
const year = birthDate.getFullYear();
|
||||
const month = birthDate.getMonth() + 1;
|
||||
const day = birthDate.getDate();
|
||||
|
||||
// 根据出生时间计算命宫位置(真正的紫微斗数算法)
|
||||
const mingGongIndex = this.calculateMingGongPosition(month, hour);
|
||||
const mingGong = this.earthlyBranches[mingGongIndex];
|
||||
|
||||
// 计算紫微星位置
|
||||
const ziweiPosition = this.calculateZiweiPosition(day, mingGongIndex);
|
||||
|
||||
// 排布十四主星
|
||||
const starPositions = this.arrangeMainStars(ziweiPosition, mingGongIndex);
|
||||
|
||||
// 计算十二宫位
|
||||
const twelvePalaces = this.calculateTwelvePalaces(mingGongIndex, starPositions);
|
||||
|
||||
// 计算四化
|
||||
const siHua = this.calculateSiHua(year);
|
||||
|
||||
// 计算大限
|
||||
const majorPeriods = this.calculateMajorPeriods(mingGongIndex, gender);
|
||||
|
||||
return {
|
||||
mingGong: mingGong,
|
||||
mingGongStars: starPositions[mingGongIndex] || [],
|
||||
twelvePalaces: twelvePalaces,
|
||||
siHua: siHua,
|
||||
majorPeriods: majorPeriods,
|
||||
birthChart: this.generateBirthChart(twelvePalaces, starPositions)
|
||||
};
|
||||
}
|
||||
|
||||
// 计算命宫位置
|
||||
calculateMingGongPosition(month, hour) {
|
||||
// 紫微斗数命宫计算公式:寅宫起正月,顺数至生月,再从生月宫逆数至生时
|
||||
const monthPosition = (month + 1) % 12; // 寅宫起正月
|
||||
const hourBranch = Math.floor((hour + 1) / 2) % 12;
|
||||
const mingGongPosition = (monthPosition - hourBranch + 12) % 12;
|
||||
return mingGongPosition;
|
||||
}
|
||||
|
||||
// 计算紫微星位置
|
||||
calculateZiweiPosition(day, mingGongIndex) {
|
||||
// 简化的紫微星定位算法
|
||||
const ziweiBase = (day - 1) % 12;
|
||||
return (mingGongIndex + ziweiBase) % 12;
|
||||
}
|
||||
|
||||
// 排布十四主星
|
||||
arrangeMainStars(ziweiPosition, mingGongIndex) {
|
||||
const starPositions = {};
|
||||
|
||||
// 紫微星系
|
||||
starPositions[ziweiPosition] = ['紫微'];
|
||||
starPositions[(ziweiPosition + 1) % 12] = ['天机'];
|
||||
starPositions[(ziweiPosition + 2) % 12] = ['太阳'];
|
||||
starPositions[(ziweiPosition + 3) % 12] = ['武曲'];
|
||||
starPositions[(ziweiPosition + 4) % 12] = ['天同'];
|
||||
starPositions[(ziweiPosition + 5) % 12] = ['廉贞'];
|
||||
|
||||
// 天府星系(对宫起)
|
||||
const tianfuPosition = (ziweiPosition + 6) % 12;
|
||||
starPositions[tianfuPosition] = ['天府'];
|
||||
starPositions[(tianfuPosition + 1) % 12] = ['太阴'];
|
||||
starPositions[(tianfuPosition + 2) % 12] = ['贪狼'];
|
||||
starPositions[(tianfuPosition + 3) % 12] = ['巨门'];
|
||||
starPositions[(tianfuPosition + 4) % 12] = ['天相'];
|
||||
starPositions[(tianfuPosition + 5) % 12] = ['天梁'];
|
||||
starPositions[(tianfuPosition + 6) % 12] = ['七杀'];
|
||||
starPositions[(tianfuPosition + 7) % 12] = ['破军'];
|
||||
|
||||
return starPositions;
|
||||
}
|
||||
|
||||
// 计算十二宫位
|
||||
calculateTwelvePalaces(mingGongIndex, starPositions) {
|
||||
const palaces = {};
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const palaceIndex = (mingGongIndex + i) % 12;
|
||||
const palaceName = this.palaceNames[i];
|
||||
|
||||
palaces[palaceName] = {
|
||||
position: this.earthlyBranches[palaceIndex],
|
||||
stars: starPositions[palaceIndex] || [],
|
||||
interpretation: this.interpretPalace(palaceName, starPositions[palaceIndex] || []),
|
||||
strength: this.calculatePalaceStrength(starPositions[palaceIndex] || [])
|
||||
};
|
||||
}
|
||||
|
||||
return palaces;
|
||||
}
|
||||
|
||||
// 计算四化
|
||||
calculateSiHua(year) {
|
||||
const yearStemIndex = (year - 4) % 10;
|
||||
const siHuaMap = {
|
||||
0: { lu: '廉贞', quan: '破军', ke: '武曲', ji: '太阳' }, // 甲年
|
||||
1: { lu: '天机', quan: '天梁', ke: '紫微', ji: '太阴' }, // 乙年
|
||||
2: { lu: '天同', quan: '天机', ke: '文昌', ji: '廉贞' }, // 丙年
|
||||
3: { lu: '太阴', quan: '天同', ke: '天机', ji: '巨门' }, // 丁年
|
||||
4: { lu: '贪狼', quan: '太阴', ke: '右弼', ji: '天机' }, // 戊年
|
||||
5: { lu: '武曲', quan: '贪狼', ke: '天梁', ji: '文曲' }, // 己年
|
||||
6: { lu: '太阳', quan: '武曲', ke: '太阴', ji: '天同' }, // 庚年
|
||||
7: { lu: '巨门', quan: '太阳', ke: '文曲', ji: '文昌' }, // 辛年
|
||||
8: { lu: '天梁', quan: '紫微', ke: '左辅', ji: '武曲' }, // 壬年
|
||||
9: { lu: '破军', quan: '巨门', ke: '太阴', ji: '贪狼' } // 癸年
|
||||
};
|
||||
|
||||
return siHuaMap[yearStemIndex] || siHuaMap[0];
|
||||
}
|
||||
|
||||
// 计算大限
|
||||
calculateMajorPeriods(mingGongIndex, gender) {
|
||||
const periods = [];
|
||||
const isMale = gender === 'male' || gender === '男';
|
||||
const startAge = isMale ? 4 : 4; // 简化处理,实际需要根据五行局计算
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const ageStart = startAge + i * 10;
|
||||
const ageEnd = ageStart + 9;
|
||||
const palaceIndex = isMale ? (mingGongIndex + i) % 12 : (mingGongIndex - i + 12) % 12;
|
||||
|
||||
periods.push({
|
||||
age_range: `${ageStart}-${ageEnd}岁`,
|
||||
palace: this.earthlyBranches[palaceIndex],
|
||||
palace_name: this.palaceNames[i],
|
||||
description: `${ageStart}-${ageEnd}岁大限在${this.earthlyBranches[palaceIndex]}宫`
|
||||
});
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
|
||||
// 解释宫位
|
||||
interpretPalace(palaceName, stars) {
|
||||
const interpretations = {
|
||||
'命宫': '代表个人的性格、外貌、才能和一生的命运走向',
|
||||
'兄弟宫': '代表兄弟姐妹关系、朋友关系和合作伙伴',
|
||||
'夫妻宫': '代表婚姻状况、配偶特质和感情生活',
|
||||
'子女宫': '代表子女缘分、创造力和部属关系',
|
||||
'财帛宫': '代表财运、理财能力和金钱观念',
|
||||
'疾厄宫': '代表健康状况、疾病倾向和意外灾厄',
|
||||
'迁移宫': '代表外出运、变动和人际关系',
|
||||
'交友宫': '代表朋友关系、社交能力和人脉网络',
|
||||
'事业宫': '代表事业发展、工作状况和社会地位',
|
||||
'田宅宫': '代表不动产、居住环境和家庭状况',
|
||||
'福德宫': '代表精神享受、兴趣爱好和福分',
|
||||
'父母宫': '代表父母关系、长辈缘分和权威关系'
|
||||
};
|
||||
|
||||
let interpretation = interpretations[palaceName] || '此宫位的基本含义';
|
||||
|
||||
if (stars.length > 0) {
|
||||
interpretation += `。主星为${stars.join('、')},`;
|
||||
interpretation += this.getStarInfluence(stars[0]);
|
||||
}
|
||||
|
||||
return interpretation;
|
||||
}
|
||||
|
||||
// 计算宫位强度
|
||||
calculatePalaceStrength(stars) {
|
||||
if (stars.length === 0) return '平';
|
||||
|
||||
const strongStars = ['紫微', '天府', '太阳', '武曲', '天同'];
|
||||
const hasStrongStar = stars.some(star => strongStars.includes(star));
|
||||
|
||||
return hasStrongStar ? '旺' : '平';
|
||||
}
|
||||
|
||||
// 获取星曜影响
|
||||
getStarInfluence(star) {
|
||||
const influences = {
|
||||
'紫微': '具有领导才能和贵气,适合担任管理职务',
|
||||
'天机': '聪明机智,善于策划,适合从事智力工作',
|
||||
'太阳': '光明磊落,具有权威性,适合公职或领导工作',
|
||||
'武曲': '意志坚强,执行力强,适合财经或技术工作',
|
||||
'天同': '性格温和,人缘好,适合服务性工作',
|
||||
'廉贞': '个性鲜明,有艺术天分,适合创意工作',
|
||||
'天府': '稳重可靠,有组织能力,适合管理工作',
|
||||
'太阴': '细腻敏感,直觉力强,适合文艺或服务工作',
|
||||
'贪狼': '多才多艺,善于交际,适合业务或娱乐工作',
|
||||
'巨门': '口才好,分析力强,适合教育或传媒工作',
|
||||
'天相': '忠诚可靠,协调能力强,适合辅助性工作',
|
||||
'天梁': '正直善良,有长者风范,适合教育或公益工作',
|
||||
'七杀': '勇敢果断,开拓性强,适合竞争性工作',
|
||||
'破军': '创新求变,不拘传统,适合开创性工作'
|
||||
};
|
||||
|
||||
return influences[star] || '具有独特的个性特质';
|
||||
}
|
||||
|
||||
// 生成出生图
|
||||
generateBirthChart(twelvePalaces, starPositions) {
|
||||
const chart = {};
|
||||
|
||||
Object.keys(twelvePalaces).forEach(palaceName => {
|
||||
const palace = twelvePalaces[palaceName];
|
||||
chart[palaceName] = {
|
||||
position: palace.position,
|
||||
stars: palace.stars,
|
||||
strength: palace.strength
|
||||
};
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
// 生成基于真实星盘的个性化分析
|
||||
generateRealPersonalizedAnalysis(starChart, personName, personGender, baziInfo) {
|
||||
const mingGongStars = starChart.mingGongStars;
|
||||
const mainStar = mingGongStars[0] || '无主星';
|
||||
|
||||
return {
|
||||
personality_analysis: {
|
||||
main_traits: `${personName}的命宫主星为${mainStar},${this.getStarInfluence(mainStar)}`,
|
||||
character_description: `根据紫微斗数分析,${personName}具有${mainStar}星的特质,${personGender}特有的温和与坚韧并存`,
|
||||
strengths: this.getPersonalityStrengths(mainStar),
|
||||
weaknesses: this.getPersonalityWeaknesses(mainStar)
|
||||
},
|
||||
career_fortune: {
|
||||
suitable_fields: this.getSuitableCareerFields(starChart.twelvePalaces['事业宫']),
|
||||
development_advice: this.getCareerDevelopmentAdvice(mainStar, personGender),
|
||||
peak_periods: this.getCareerPeakPeriods(starChart.majorPeriods)
|
||||
},
|
||||
wealth_fortune: {
|
||||
wealth_potential: this.getWealthPotential(starChart.twelvePalaces['财帛宫']),
|
||||
investment_advice: this.getInvestmentAdvice(mainStar),
|
||||
financial_planning: this.getFinancialPlanning(personGender)
|
||||
},
|
||||
relationship_fortune: {
|
||||
marriage_outlook: this.getMarriageOutlook(starChart.twelvePalaces['夫妻宫'], personGender),
|
||||
ideal_partner: this.getIdealPartnerTraits(mainStar, personGender),
|
||||
relationship_advice: this.getRelationshipAdvice(mainStar)
|
||||
},
|
||||
health_fortune: {
|
||||
health_tendencies: this.getHealthTendencies(starChart.twelvePalaces['疾厄宫']),
|
||||
wellness_advice: this.getWellnessAdvice(mainStar),
|
||||
prevention_focus: this.getPreventionFocus(baziInfo)
|
||||
},
|
||||
life_guidance: {
|
||||
overall_fortune: `${personName}一生运势以${mainStar}星为主导,${this.getOverallFortune(mainStar)}`,
|
||||
key_life_phases: this.getKeyLifePhases(starChart.majorPeriods),
|
||||
development_strategy: this.getDevelopmentStrategy(mainStar, personGender)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取个性优势
|
||||
getPersonalityStrengths(star) {
|
||||
const strengths = {
|
||||
'紫微': '领导能力强,有贵人相助,具有权威性',
|
||||
'天机': '聪明机智,反应敏捷,善于策划',
|
||||
'太阳': '光明正大,热情开朗,具有感召力',
|
||||
'武曲': '意志坚定,执行力强,理财有方',
|
||||
'天同': '性格温和,人际关系好,适应力强'
|
||||
};
|
||||
return strengths[star] || '具有独特的个人魅力';
|
||||
}
|
||||
|
||||
// 获取个性弱点
|
||||
getPersonalityWeaknesses(star) {
|
||||
const weaknesses = {
|
||||
'紫微': '有时过于自信,容易忽视他人意见',
|
||||
'天机': '思虑过多,有时缺乏行动力',
|
||||
'太阳': '有时过于直接,可能伤害他人感情',
|
||||
'武曲': '过于注重物质,有时显得冷漠',
|
||||
'天同': '有时过于被动,缺乏主见'
|
||||
};
|
||||
return weaknesses[star] || '需要注意平衡发展';
|
||||
}
|
||||
|
||||
// 获取适合的职业领域
|
||||
getSuitableCareerFields(careerPalace) {
|
||||
const stars = careerPalace.stars;
|
||||
if (stars.length === 0) return '适合稳定发展的传统行业';
|
||||
|
||||
const mainStar = stars[0];
|
||||
const fields = {
|
||||
'紫微': '政府机关、大型企业管理、金融业',
|
||||
'天机': '科技业、咨询业、教育业',
|
||||
'太阳': '公务员、媒体业、娱乐业',
|
||||
'武曲': '金融业、制造业、军警',
|
||||
'天同': '服务业、医疗业、社会工作'
|
||||
};
|
||||
|
||||
return fields[mainStar] || '多元化发展的现代服务业';
|
||||
}
|
||||
|
||||
// 其他辅助方法的简化实现
|
||||
getCareerDevelopmentAdvice(star, gender) {
|
||||
return `根据${star}星的特质,建议${gender === '男性' ? '发挥男性的决断力' : '发挥女性的细致性'},在职场中稳步发展`;
|
||||
}
|
||||
|
||||
getCareerPeakPeriods(periods) {
|
||||
return periods.slice(2, 5).map(p => p.age_range).join('、');
|
||||
}
|
||||
|
||||
getWealthPotential(wealthPalace) {
|
||||
return wealthPalace.stars.length > 0 ? '财运较佳,适合投资理财' : '财运平稳,宜稳健理财';
|
||||
}
|
||||
|
||||
getInvestmentAdvice(star) {
|
||||
return `根据${star}星的特质,建议选择稳健的投资方式`;
|
||||
}
|
||||
|
||||
getFinancialPlanning(gender) {
|
||||
return `${gender === '男性' ? '建议制定长期财务规划' : '建议注重家庭理财平衡'}`;
|
||||
}
|
||||
|
||||
getMarriageOutlook(marriagePalace, gender) {
|
||||
return `婚姻宫${marriagePalace.strength === '旺' ? '较旺' : '平稳'},${gender === '男性' ? '适合寻找贤内助' : '适合寻找可靠伴侣'}`;
|
||||
}
|
||||
|
||||
getIdealPartnerTraits(star, gender) {
|
||||
return `适合寻找与${star}星互补的伴侣特质`;
|
||||
}
|
||||
|
||||
getRelationshipAdvice(star) {
|
||||
return `在感情中发挥${star}星的优势,保持真诚沟通`;
|
||||
}
|
||||
|
||||
getHealthTendencies(healthPalace) {
|
||||
return healthPalace.stars.length > 0 ? '需注意相关星曜影响的健康问题' : '整体健康状况良好';
|
||||
}
|
||||
|
||||
getWellnessAdvice(star) {
|
||||
return `根据${star}星的特质,建议保持规律作息,适度运动`;
|
||||
}
|
||||
|
||||
getPreventionFocus(baziInfo) {
|
||||
return '根据八字信息,建议注重五行平衡的养生方法';
|
||||
}
|
||||
|
||||
getOverallFortune(star) {
|
||||
return `整体运势受${star}星影响,建议发挥其正面特质`;
|
||||
}
|
||||
|
||||
getKeyLifePhases(periods) {
|
||||
return periods.slice(0, 3).map(p => `${p.age_range}为${p.palace_name}大限`).join(',');
|
||||
}
|
||||
|
||||
getDevelopmentStrategy(star, gender) {
|
||||
return `建议以${star}星的特质为核心,${gender === '男性' ? '稳健发展' : '平衡发展'},把握人生机遇`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ZiweiAnalyzer;
|
||||
0
server/services/ziweiAnalyzer.js
Normal file
0
server/services/ziweiAnalyzer.js
Normal file
@@ -44,19 +44,24 @@ const AnalysisResultDisplay: React.FC<AnalysisResultDisplayProps> = ({ analysisR
|
||||
|
||||
// 渲染八字命理分析
|
||||
const renderBaziAnalysis = () => {
|
||||
// 如果有 birthDate,使用新的 BaziAnalysisDisplay 组件
|
||||
// 如果有分析结果数据,优先使用 ComprehensiveBaziAnalysis 组件
|
||||
if (analysisResult && analysisResult.data) {
|
||||
return <ComprehensiveBaziAnalysis analysisResult={analysisResult} />;
|
||||
}
|
||||
// 如果有 birthDate 但没有分析结果,使用 BaziAnalysisDisplay 组件
|
||||
if (birthDate) {
|
||||
return <BaziAnalysisDisplay birthDate={birthDate} />;
|
||||
}
|
||||
// 否则使用原来的 ComprehensiveBaziAnalysis 组件(向后兼容)
|
||||
// 默认使用 ComprehensiveBaziAnalysis 组件(向后兼容)
|
||||
return <ComprehensiveBaziAnalysis analysisResult={analysisResult} />;
|
||||
};
|
||||
|
||||
// 渲染紫微斗数分析
|
||||
const renderZiweiAnalysis = () => {
|
||||
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||
const ziweiData = data?.ziwei || data;
|
||||
const analysisData = data?.analysis || data;
|
||||
// 处理新的数据结构: { type: 'ziwei', data: analysisResult }
|
||||
const data = analysisResult?.data || analysisResult;
|
||||
const ziweiData = data?.ziwei_analysis || data?.ziwei || data;
|
||||
const analysisData = data?.detailed_analysis || data?.analysis || data;
|
||||
|
||||
|
||||
|
||||
@@ -204,7 +209,8 @@ const AnalysisResultDisplay: React.FC<AnalysisResultDisplayProps> = ({ analysisR
|
||||
|
||||
// 渲染易经占卜分析
|
||||
const renderYijingAnalysis = () => {
|
||||
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||
// 处理新的数据结构: { type: 'yijing', data: analysisResult }
|
||||
const data = analysisResult?.data || analysisResult;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts';
|
||||
import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2 } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
|
||||
interface BaziAnalysisDisplayProps {
|
||||
birthDate: {
|
||||
@@ -85,10 +85,10 @@ const BaziAnalysisDisplay: React.FC<BaziAnalysisDisplayProps> = ({ birthDate })
|
||||
|
||||
// 并行调用两个函数
|
||||
const [baziDetailsResponse, wuxingAnalysisResponse] = await Promise.all([
|
||||
supabase.functions.invoke('bazi-details', {
|
||||
localApi.functions.invoke('bazi-details', {
|
||||
body: requestBody
|
||||
}),
|
||||
supabase.functions.invoke('bazi-wuxing-analysis', {
|
||||
localApi.functions.invoke('bazi-wuxing-analysis', {
|
||||
body: requestBody
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -22,7 +22,8 @@ const ComprehensiveBaziAnalysis: React.FC<ComprehensiveBaziAnalysisProps> = ({ a
|
||||
return current || defaultValue;
|
||||
};
|
||||
|
||||
const data = analysisResult?.analysis || analysisResult?.data?.analysis || analysisResult;
|
||||
// 处理新的数据结构: { type: 'bazi', data: analysisResult }
|
||||
const data = analysisResult?.data || analysisResult;
|
||||
|
||||
// 五行颜色配置
|
||||
const elementColors: { [key: string]: string } = {
|
||||
@@ -357,7 +358,7 @@ const ComprehensiveBaziAnalysis: React.FC<ComprehensiveBaziAnalysisProps> = ({ a
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-red-700 leading-relaxed">
|
||||
{safeGet(data, 'wuxing_analysis.personal_traits', '您的日主特征体现了独特的性格魅力...')}
|
||||
{safeGet(data, 'wuxing_analysis.personality_traits', '您的日主特征体现了独特的性格魅力...')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,7 +427,7 @@ const ComprehensiveBaziAnalysis: React.FC<ComprehensiveBaziAnalysisProps> = ({ a
|
||||
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
|
||||
<h4 className="font-bold text-red-800 mb-2">调和建议</h4>
|
||||
<p className="text-red-700 leading-relaxed">
|
||||
{safeGet(data, 'wuxing_analysis.suggestions', '建议通过特定的方式来平衡五行能量...')}
|
||||
{safeGet(data, 'wuxing_analysis.improvement_suggestions', '建议通过特定的方式来平衡五行能量...')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi, User, AuthResponse } from '../lib/localApi';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<any>;
|
||||
signUp: (email: string, password: string) => Promise<any>;
|
||||
signUp: (email: string, password: string, fullName?: string) => Promise<any>;
|
||||
signOut: () => Promise<any>;
|
||||
}
|
||||
|
||||
@@ -25,39 +24,59 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
async function loadUser() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
setUser(user);
|
||||
const response = await localApi.auth.getUser();
|
||||
if (response.data) {
|
||||
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
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
// NEVER use any async operations in callback
|
||||
setUser(session?.user || null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
// Auth methods
|
||||
async function signIn(email: string, password: string) {
|
||||
return await supabase.auth.signInWithPassword({ email, password });
|
||||
try {
|
||||
const response = await localApi.auth.signInWithPassword({ email, password });
|
||||
if (response.data) {
|
||||
setUser(response.data.user);
|
||||
return { data: response.data, error: null };
|
||||
} else {
|
||||
return { data: null, error: response.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { data: null, error: { message: '登录失败' } };
|
||||
}
|
||||
}
|
||||
|
||||
async function signUp(email: string, password: string) {
|
||||
return await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
async function signUp(email: string, password: string, fullName?: string) {
|
||||
try {
|
||||
const response = await localApi.auth.signUp(email, password, fullName);
|
||||
if (response.data) {
|
||||
setUser(response.data.user);
|
||||
return { data: response.data, error: null };
|
||||
} else {
|
||||
return { data: null, error: response.error };
|
||||
}
|
||||
} catch (error) {
|
||||
return { data: null, error: { message: '注册失败' } };
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
return await supabase.auth.signOut();
|
||||
try {
|
||||
const response = await localApi.auth.signOut();
|
||||
setUser(null);
|
||||
return { error: null };
|
||||
} catch (error) {
|
||||
return { error: { message: '登出失败' } };
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
320
src/lib/localApi.ts
Normal file
320
src/lib/localApi.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
// 本地API客户端
|
||||
// 替代Supabase客户端,提供相同的接口
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api';
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
user: User;
|
||||
token: string;
|
||||
}
|
||||
|
||||
class LocalApiClient {
|
||||
private token: string | null = null;
|
||||
|
||||
constructor() {
|
||||
// 从localStorage恢复token
|
||||
this.token = localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
// 设置认证token
|
||||
setToken(token: string | null) {
|
||||
this.token = token;
|
||||
if (token) {
|
||||
localStorage.setItem('auth_token', token);
|
||||
} else {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取认证头
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers.Authorization = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...this.getAuthHeaders(),
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
error: data.error || {
|
||||
code: 'HTTP_ERROR',
|
||||
message: `HTTP ${response.status}: ${response.statusText}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { data: data.data };
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
return {
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error instanceof Error ? error.message : '网络请求失败',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 认证相关方法
|
||||
auth = {
|
||||
// 用户注册
|
||||
signUp: async (email: string, password: string, full_name?: string): Promise<ApiResponse<AuthResponse>> => {
|
||||
const response = await this.request<AuthResponse>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password, full_name }),
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
this.setToken(response.data.token);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
// 用户登录
|
||||
signInWithPassword: async ({ email, password }: { email: string; password: string }): Promise<ApiResponse<AuthResponse>> => {
|
||||
const response = await this.request<AuthResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
this.setToken(response.data.token);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
// 获取当前用户
|
||||
getUser: async (): Promise<ApiResponse<{ user: User }>> => {
|
||||
return this.request<{ user: User }>('/auth/me');
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
signOut: async (): Promise<ApiResponse<{ message: string }>> => {
|
||||
const response = await this.request<{ message: string }>('/auth/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
this.setToken(null);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 验证token
|
||||
verify: async (): Promise<ApiResponse<{ valid: boolean; user: User }>> => {
|
||||
return this.request<{ valid: boolean; user: User }>('/auth/verify');
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword: async (currentPassword: string, newPassword: string): Promise<ApiResponse<{ message: string }>> => {
|
||||
return this.request<{ message: string }>('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// 用户档案相关方法
|
||||
profiles = {
|
||||
// 获取用户档案
|
||||
get: async (): Promise<ApiResponse<{ profile: any }>> => {
|
||||
return this.request<{ profile: any }>('/profile');
|
||||
},
|
||||
|
||||
// 更新用户档案
|
||||
update: async (profileData: any): Promise<ApiResponse<{ profile: any }>> => {
|
||||
return this.request<{ profile: any }>('/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(profileData),
|
||||
});
|
||||
},
|
||||
|
||||
// 上传头像
|
||||
uploadAvatar: async (avatarUrl: string): Promise<ApiResponse<{ message: string; avatar_url: string }>> => {
|
||||
return this.request<{ message: string; avatar_url: string }>('/profile/avatar', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ avatar_url: avatarUrl }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// 分析相关方法
|
||||
analysis = {
|
||||
// 八字分析
|
||||
bazi: async (birthData: any): Promise<ApiResponse<{ record_id: number; analysis: any }>> => {
|
||||
return this.request<{ record_id: number; analysis: any }>('/analysis/bazi', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ birth_data: birthData }),
|
||||
});
|
||||
},
|
||||
|
||||
// 紫微斗数分析
|
||||
ziwei: async (birthData: any): Promise<ApiResponse<{ record_id: number; analysis: any }>> => {
|
||||
return this.request<{ record_id: number; analysis: any }>('/analysis/ziwei', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ birth_data: birthData }),
|
||||
});
|
||||
},
|
||||
|
||||
// 易经分析
|
||||
yijing: async (birthData: any, question?: string): Promise<ApiResponse<{ record_id: number; analysis: any }>> => {
|
||||
return this.request<{ record_id: number; analysis: any }>('/analysis/yijing', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ birth_data: birthData, question }),
|
||||
});
|
||||
},
|
||||
|
||||
// 综合分析
|
||||
comprehensive: async (birthData: any, includeTypes?: string[]): Promise<ApiResponse<{ record_id: number; analysis: any }>> => {
|
||||
return this.request<{ record_id: number; analysis: any }>('/analysis/comprehensive', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ birth_data: birthData, include_types: includeTypes }),
|
||||
});
|
||||
},
|
||||
|
||||
// 获取分析类型
|
||||
getTypes: async (): Promise<ApiResponse<{ available_types: any[] }>> => {
|
||||
return this.request<{ available_types: any[] }>('/analysis/types');
|
||||
},
|
||||
|
||||
// 验证分析数据
|
||||
validate: async (birthData: any, analysisType: string): Promise<ApiResponse<{ valid: boolean; errors: string[] }>> => {
|
||||
return this.request<{ valid: boolean; errors: string[] }>('/analysis/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ birth_data: birthData, analysis_type: analysisType }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// 历史记录相关方法
|
||||
history = {
|
||||
// 获取历史记录
|
||||
getAll: async (params?: { page?: number; limit?: number; reading_type?: string }): Promise<ApiResponse<any[]>> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.limit) searchParams.set('limit', params.limit.toString());
|
||||
if (params?.reading_type) searchParams.set('reading_type', params.reading_type);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const endpoint = queryString ? `/history?${queryString}` : '/history';
|
||||
|
||||
return this.request<any[]>(endpoint);
|
||||
},
|
||||
|
||||
// 获取单个记录
|
||||
getById: async (id: string): Promise<ApiResponse<any>> => {
|
||||
return this.request<any>(`/history/${id}`);
|
||||
},
|
||||
|
||||
// 删除记录
|
||||
delete: async (id: string): Promise<ApiResponse<{ message: string }>> => {
|
||||
return this.request<{ message: string }>(`/history/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
// 批量删除记录
|
||||
deleteBatch: async (ids: string[]): Promise<ApiResponse<{ message: string }>> => {
|
||||
return this.request<{ message: string }>('/history', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ ids }),
|
||||
});
|
||||
},
|
||||
|
||||
// 获取统计信息
|
||||
getStats: async (): Promise<ApiResponse<any>> => {
|
||||
return this.request<any>('/history/stats/summary');
|
||||
},
|
||||
|
||||
// 搜索记录
|
||||
search: async (query: string, params?: { page?: number; limit?: number }): Promise<ApiResponse<any[]>> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.limit) searchParams.set('limit', params.limit.toString());
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const endpoint = queryString ? `/history/search/${encodeURIComponent(query)}?${queryString}` : `/history/search/${encodeURIComponent(query)}`;
|
||||
|
||||
return this.request<any[]>(endpoint);
|
||||
},
|
||||
};
|
||||
|
||||
// 兼容Supabase的functions.invoke方法
|
||||
functions = {
|
||||
invoke: async (functionName: string, options: { body: any }): Promise<ApiResponse<any>> => {
|
||||
// 将Supabase Edge Function调用映射到本地API
|
||||
const functionMap: Record<string, string> = {
|
||||
'bazi-analyzer': '/analysis/bazi',
|
||||
'ziwei-analyzer': '/analysis/ziwei',
|
||||
'yijing-analyzer': '/analysis/yijing',
|
||||
'bazi-details': '/analysis/bazi-details',
|
||||
'bazi-wuxing-analysis': '/analysis/bazi-wuxing',
|
||||
'reading-history': '/history',
|
||||
};
|
||||
|
||||
const endpoint = functionMap[functionName.replace(/\?.*$/, '')] || `/functions/${functionName}`;
|
||||
|
||||
if (functionName.includes('reading-history')) {
|
||||
const { action, ...params } = options.body;
|
||||
|
||||
switch (action) {
|
||||
case 'get_history':
|
||||
return this.history.getAll();
|
||||
case 'delete_reading':
|
||||
return this.history.delete(params.reading_id);
|
||||
default:
|
||||
return { error: { code: 'UNKNOWN_ACTION', message: `Unknown action: ${action}` } };
|
||||
}
|
||||
}
|
||||
|
||||
return this.request<any>(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options.body),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const localApi = new LocalApiClient();
|
||||
|
||||
export { localApi };
|
||||
export type { ApiResponse, User, AuthResponse };
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Missing Supabase environment variables')
|
||||
}
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Select } from '../components/ui/Select';
|
||||
@@ -35,13 +35,9 @@ const AnalysisPage: React.FC = () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (data) {
|
||||
const response = await localApi.profiles.get();
|
||||
if (response.data && response.data.profile) {
|
||||
const data = response.data.profile;
|
||||
setProfile(data);
|
||||
setFormData({
|
||||
name: data.full_name || '',
|
||||
@@ -69,35 +65,32 @@ const AnalysisPage: React.FC = () => {
|
||||
setAnalysisResult(null);
|
||||
|
||||
try {
|
||||
// 对于八字分析,直接显示结果,不需要调用 Edge Function
|
||||
if (analysisType === 'bazi') {
|
||||
const birthData = {
|
||||
date: formData.birth_date,
|
||||
time: formData.birth_time || '12:00'
|
||||
};
|
||||
setAnalysisResult({ type: 'bazi', birthDate: birthData });
|
||||
toast.success('分析完成!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于其他分析类型,保持原有逻辑
|
||||
const analysisRequest: AnalysisRequest = {
|
||||
user_id: user.id,
|
||||
reading_type: analysisType,
|
||||
birth_data: {
|
||||
name: formData.name,
|
||||
birth_date: formData.birth_date,
|
||||
birth_time: formData.birth_time,
|
||||
gender: formData.gender,
|
||||
birth_place: formData.birth_place,
|
||||
...(analysisType === 'yijing' && { question: formData.question })
|
||||
}
|
||||
const birthData = {
|
||||
name: formData.name,
|
||||
birth_date: formData.birth_date,
|
||||
birth_time: formData.birth_time,
|
||||
gender: formData.gender,
|
||||
birth_place: formData.birth_place
|
||||
};
|
||||
|
||||
const functionName = `${analysisType}-analyzer?_t=${new Date().getTime()}`;
|
||||
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||
body: analysisRequest
|
||||
});
|
||||
let response;
|
||||
|
||||
// 根据分析类型调用相应的API
|
||||
switch (analysisType) {
|
||||
case 'bazi':
|
||||
response = await localApi.analysis.bazi(birthData);
|
||||
break;
|
||||
case 'ziwei':
|
||||
response = await localApi.analysis.ziwei(birthData);
|
||||
break;
|
||||
case 'yijing':
|
||||
response = await localApi.analysis.yijing(birthData, formData.question);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的分析类型: ${analysisType}`);
|
||||
}
|
||||
|
||||
const { data, error } = response;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -107,7 +100,11 @@ const AnalysisPage: React.FC = () => {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
setAnalysisResult(data.data);
|
||||
// 后端返回格式: { data: { record_id, analysis } }
|
||||
setAnalysisResult({
|
||||
type: analysisType,
|
||||
data: data.analysis
|
||||
});
|
||||
toast.success('分析完成!');
|
||||
} catch (error: any) {
|
||||
console.error('分析失败:', error);
|
||||
@@ -287,9 +284,12 @@ const AnalysisPage: React.FC = () => {
|
||||
{/* 分析结果 */}
|
||||
{analysisResult && (
|
||||
<AnalysisResultDisplay
|
||||
analysisResult={analysisResult.type !== 'bazi' ? analysisResult : undefined}
|
||||
analysisResult={analysisResult}
|
||||
analysisType={analysisType}
|
||||
birthDate={analysisResult.type === 'bazi' ? analysisResult.birthDate : undefined}
|
||||
birthDate={analysisResult.type === 'bazi' ? {
|
||||
date: formData.birth_date,
|
||||
time: formData.birth_time
|
||||
} : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Calendar, Clock, Star, BookOpen, Sparkles, User } from 'lucide-react';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -79,7 +79,7 @@ const BaziDetailsPage: React.FC = () => {
|
||||
'阴': 'text-purple-600 bg-purple-50 border-purple-300'
|
||||
};
|
||||
|
||||
// 调用Supabase Edge Function获取八字详细信息
|
||||
// 获取八字详细信息
|
||||
const fetchBaziDetails = async () => {
|
||||
if (!birthDate) {
|
||||
toast.error('请选择您的出生日期');
|
||||
@@ -90,18 +90,20 @@ const BaziDetailsPage: React.FC = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 调用Supabase Edge Function
|
||||
const { data, error } = await supabase.functions.invoke('bazi-details', {
|
||||
// 调用本地API
|
||||
const response = await localApi.functions.invoke('bazi-details', {
|
||||
body: {
|
||||
birthDate,
|
||||
birthTime
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
if (data?.data) {
|
||||
setBaziData(data.data);
|
||||
if (response.data?.data) {
|
||||
setBaziData(response.data.data);
|
||||
toast.success('八字详情分析完成!');
|
||||
} else {
|
||||
throw new Error('排盘结果为空');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||
import AnalysisResultDisplay from '../components/AnalysisResultDisplay';
|
||||
@@ -24,22 +24,13 @@ const HistoryPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase.functions.invoke('reading-history', {
|
||||
body: {
|
||||
action: 'get_history',
|
||||
user_id: user.id
|
||||
}
|
||||
});
|
||||
const response = await localApi.history.getAll();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
if (data?.error) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
const historyData = data.data || [];
|
||||
const historyData = response.data || [];
|
||||
|
||||
// 数据转换适配器:将旧格式转换为新格式
|
||||
const processedData = historyData.map((reading: any) => {
|
||||
@@ -84,15 +75,10 @@ const HistoryPage: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const { error } = await supabase.functions.invoke('reading-history', {
|
||||
body: {
|
||||
action: 'delete_reading',
|
||||
reading_id: readingId
|
||||
}
|
||||
});
|
||||
const response = await localApi.history.delete(readingId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
setReadings(prev => prev.filter(r => r.id !== readingId));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Select } from '../components/ui/Select';
|
||||
@@ -30,17 +30,14 @@ const ProfilePage: React.FC = () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
throw error;
|
||||
const response = await localApi.profiles.get();
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
if (response.data && response.data.profile) {
|
||||
const data = response.data.profile;
|
||||
setProfile(data);
|
||||
setFormData({
|
||||
full_name: data.full_name || '',
|
||||
@@ -65,37 +62,18 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
try {
|
||||
const profileData = {
|
||||
user_id: user.id,
|
||||
...formData,
|
||||
updated_at: new Date().toISOString()
|
||||
...formData
|
||||
};
|
||||
|
||||
let result;
|
||||
if (profile) {
|
||||
// 更新现有档案
|
||||
result = await supabase
|
||||
.from('user_profiles')
|
||||
.update(profileData)
|
||||
.eq('user_id', user.id)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
} else {
|
||||
// 创建新档案
|
||||
result = await supabase
|
||||
.from('user_profiles')
|
||||
.insert([{
|
||||
...profileData,
|
||||
created_at: new Date().toISOString()
|
||||
}])
|
||||
.select()
|
||||
.maybeSingle();
|
||||
}
|
||||
const result = await localApi.profiles.update(profileData);
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
setProfile(result.data);
|
||||
if (result.data && result.data.profile) {
|
||||
setProfile(result.data.profile);
|
||||
}
|
||||
toast.success('档案保存成功!');
|
||||
} catch (error: any) {
|
||||
console.error('保存档案失败:', error);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Responsi
|
||||
import { Calendar, Clock, Zap, BarChart3, Sparkles, TrendingUp } from 'lucide-react';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/Card';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { localApi } from '../lib/localApi';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -59,7 +59,7 @@ const WuxingAnalysisPage: React.FC = () => {
|
||||
'水': '💧'
|
||||
};
|
||||
|
||||
// 调用Supabase Edge Function进行五行分析
|
||||
// 进行五行分析
|
||||
const fetchWuxingAnalysis = async () => {
|
||||
if (!birthDate) {
|
||||
toast.error('请选择您的出生日期');
|
||||
@@ -70,18 +70,20 @@ const WuxingAnalysisPage: React.FC = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 调用Supabase Edge Function
|
||||
const { data, error } = await supabase.functions.invoke('bazi-wuxing-analysis', {
|
||||
// 调用本地API
|
||||
const response = await localApi.functions.invoke('bazi-wuxing-analysis', {
|
||||
body: {
|
||||
birthDate,
|
||||
birthTime
|
||||
}
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
if (data?.data) {
|
||||
setAnalysisData(data.data);
|
||||
if (response.data?.data) {
|
||||
setAnalysisData(response.data.data);
|
||||
toast.success('五行分析完成!');
|
||||
} else {
|
||||
throw new Error('分析结果为空');
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
v2.34.3
|
||||
@@ -1 +0,0 @@
|
||||
v2.177.0
|
||||
@@ -1 +0,0 @@
|
||||
postgresql://postgres.myiabzmycehtxxyybqfo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
|
||||
@@ -1 +0,0 @@
|
||||
17.4.1.069
|
||||
@@ -1 +0,0 @@
|
||||
myiabzmycehtxxyybqfo
|
||||
@@ -1 +0,0 @@
|
||||
v13.0.4
|
||||
@@ -1 +0,0 @@
|
||||
custom-metadata
|
||||
0
test-analysis.js
Normal file
0
test-analysis.js
Normal file
Reference in New Issue
Block a user