Generating commit message...
This commit is contained in:
54
backend/.env
Normal file
54
backend/.env
Normal file
@@ -0,0 +1,54 @@
|
||||
# 服务器配置
|
||||
NODE_ENV=development
|
||||
PORT=3001
|
||||
HOST=0.0.0.0
|
||||
|
||||
# MySQL数据库配置
|
||||
DB_HOST=192.168.0.240
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=aiot$Aiot123
|
||||
DB_NAME=jiebandata
|
||||
|
||||
# 测试环境数据库
|
||||
TEST_DB_HOST=192.168.0.240
|
||||
TEST_DB_PORT=3306
|
||||
TEST_DB_USER=root
|
||||
TEST_DB_PASSWORD=aiot$Aiot123
|
||||
TEST_DB_NAME=jiebandata
|
||||
|
||||
# 生产环境数据库
|
||||
PROD_DB_HOST=129.211.213.226
|
||||
PROD_DB_PORT=9527
|
||||
PROD_DB_USER=root
|
||||
PROD_DB_PASSWORD=aiotAiot123!
|
||||
PROD_DB_NAME=jiebandata
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# RabbitMQ配置
|
||||
RABBITMQ_HOST=localhost
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USERNAME=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
RABBITMQ_VHOST=/
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRE=7d
|
||||
|
||||
# 微信配置
|
||||
WECHAT_APPID=your-wechat-appid
|
||||
WECHAT_SECRET=your-wechat-secret
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
|
||||
|
||||
# 调试配置
|
||||
DEBUG=jiebanke:*
|
||||
LOG_LEVEL=info
|
||||
39
backend/.env.example
Normal file
39
backend/.env.example
Normal file
@@ -0,0 +1,39 @@
|
||||
# 服务器配置
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
|
||||
# 数据库配置
|
||||
MONGODB_URI=mongodb://localhost:27017/jiebanke
|
||||
MONGODB_URI_TEST=mongodb://localhost:27017/jiebanke_test
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRE=7d
|
||||
|
||||
# 微信配置
|
||||
WECHAT_APPID=your-wechat-appid
|
||||
WECHAT_SECRET=your-wechat-secret
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif
|
||||
|
||||
# 邮件配置(可选)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-email-password
|
||||
|
||||
# Redis配置(可选)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# 第三方API配置
|
||||
MAP_API_KEY=your-map-api-key
|
||||
SMS_API_KEY=your-sms-api-key
|
||||
|
||||
# 调试配置
|
||||
DEBUG=jiebanke:*
|
||||
LOG_LEVEL=info
|
||||
202
backend/README.md
Normal file
202
backend/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 结伴客后端服务
|
||||
|
||||
基于 Node.js + Express + MongoDB 的后端 API 服务。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 用户认证系统(JWT)
|
||||
- ✅ 微信登录集成
|
||||
- ✅ RESTful API 设计
|
||||
- ✅ 数据验证和清洗
|
||||
- ✅ 错误处理中间件
|
||||
- ✅ 请求频率限制
|
||||
- ✅ 安全防护(CORS, Helmet, XSS防护)
|
||||
- ✅ MongoDB 数据库集成
|
||||
- ✅ 环境配置管理
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 16+
|
||||
- MongoDB 4.4+
|
||||
- npm 或 yarn
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 环境配置
|
||||
|
||||
1. 复制环境变量文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 编辑 `.env` 文件,配置你的环境变量:
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017/jiebanke
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 开发模式(带热重载)
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm start
|
||||
|
||||
# 调试模式
|
||||
npm run debug
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行测试
|
||||
npm test
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 运行端到端测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 认证接口
|
||||
|
||||
#### 用户注册
|
||||
```
|
||||
POST /api/v1/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123",
|
||||
"nickname": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"phone": "13800138000"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
```
|
||||
POST /api/v1/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取当前用户信息
|
||||
```
|
||||
GET /api/v1/auth/me
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 微信登录
|
||||
```
|
||||
POST /api/v1/auth/wechat-login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"code": "微信授权码",
|
||||
"userInfo": {
|
||||
"nickName": "微信用户",
|
||||
"avatarUrl": "https://...",
|
||||
"gender": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── routes/ # 路由定义
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── app.js # Express应用配置
|
||||
│ └── server.js # 服务器入口
|
||||
├── tests/ # 测试文件
|
||||
├── .env.example # 环境变量示例
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 用户表 (users)
|
||||
- 用户基本信息
|
||||
- 认证信息
|
||||
- 统计信息
|
||||
- 第三方登录信息
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. 创建数据模型 (`src/models/`)
|
||||
2. 创建控制器 (`src/controllers/`)
|
||||
3. 创建路由 (`src/routes/`)
|
||||
4. 在 `app.js` 中注册路由
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 使用 ESLint 进行代码检查
|
||||
- 遵循 JavaScript Standard Style
|
||||
- 使用 async/await 处理异步操作
|
||||
- 使用错误处理中间件
|
||||
|
||||
## 部署
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t jiebanke-backend .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 3000:3000 --env-file .env jiebanke-backend
|
||||
```
|
||||
|
||||
### PM2 部署
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
pm2 start ecosystem.config.js
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **MongoDB 连接失败**
|
||||
- 检查 MongoDB 服务是否运行
|
||||
- 检查连接字符串是否正确
|
||||
|
||||
2. **JWT 验证失败**
|
||||
- 检查 JWT_SECRET 环境变量
|
||||
|
||||
3. **CORS 错误**
|
||||
- 检查前端域名是否在 CORS 白名单中
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请查看日志文件或联系开发团队。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
94
backend/config/env.js
Normal file
94
backend/config/env.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// 环境配置
|
||||
const path = require('path')
|
||||
require('dotenv').config({ path: path.join(__dirname, '../../.env') })
|
||||
|
||||
const config = {
|
||||
// 开发环境
|
||||
development: {
|
||||
port: process.env.PORT || 3000,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_dev',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'dev-jwt-secret-key-2024',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d'
|
||||
},
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD || ''
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:9000',
|
||||
credentials: true
|
||||
}
|
||||
},
|
||||
|
||||
// 测试环境
|
||||
test: {
|
||||
port: process.env.PORT || 3001,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke_test',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'test-jwt-secret-key-2024',
|
||||
expiresIn: '1h',
|
||||
refreshExpiresIn: '7d'
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 2 * 1024 * 1024, // 2MB
|
||||
allowedTypes: ['image/jpeg', 'image/png']
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境
|
||||
production: {
|
||||
port: process.env.PORT || 3000,
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI,
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET,
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '1d',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
|
||||
},
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST,
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD
|
||||
},
|
||||
upload: {
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/webp']
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'https://your-domain.com',
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
const getConfig = () => {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
return config[env]
|
||||
}
|
||||
|
||||
module.exports = getConfig()
|
||||
6418
backend/package-lock.json
generated
Normal file
6418
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
backend/package.json
Normal file
52
backend/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "jiebanke-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "结伴客小程序后端API服务",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"migrate": "node src/utils/migrate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mini-program",
|
||||
"api",
|
||||
"express",
|
||||
"mongodb"
|
||||
],
|
||||
"author": "jiebanke-team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"amqplib": "^0.10.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-mongo-sanitize": "^2.2.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"hpp": "^0.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^8.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"redis": "^5.8.2",
|
||||
"winston": "^3.11.0",
|
||||
"xss-clean": "^0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
100
backend/src/app.js
Normal file
100
backend/src/app.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const helmet = require('helmet')
|
||||
const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const xss = require('xss-clean')
|
||||
const hpp = require('hpp')
|
||||
|
||||
console.log('🔧 初始化Express应用...')
|
||||
|
||||
const { globalErrorHandler, notFound } = require('./utils/errors')
|
||||
|
||||
// 路由导入
|
||||
const authRoutes = require('./routes/auth')
|
||||
// 其他路由将在这里导入
|
||||
|
||||
const app = express()
|
||||
|
||||
console.log('✅ Express应用初始化完成')
|
||||
|
||||
// 安全中间件
|
||||
app.use(helmet())
|
||||
|
||||
// CORS配置
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['https://your-domain.com']
|
||||
: ['http://localhost:9000', 'http://localhost:3000'],
|
||||
credentials: true
|
||||
}))
|
||||
|
||||
// 请求日志
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(morgan('dev'))
|
||||
} else {
|
||||
app.use(morgan('combined'))
|
||||
}
|
||||
|
||||
// 请求频率限制
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
max: process.env.NODE_ENV === 'production' ? 100 : 1000, // 生产环境100次,开发环境1000次
|
||||
message: {
|
||||
success: false,
|
||||
code: 429,
|
||||
message: '请求过于频繁,请稍后再试',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
app.use('/api', limiter)
|
||||
|
||||
// 请求体解析
|
||||
app.use(express.json({ limit: '10kb' }))
|
||||
app.use(express.urlencoded({ extended: true, limit: '10kb' }))
|
||||
|
||||
// 数据清洗
|
||||
app.use(xss()) // 防止XSS攻击
|
||||
app.use(hpp({ // 防止参数污染
|
||||
whitelist: [
|
||||
'page',
|
||||
'pageSize',
|
||||
'sort',
|
||||
'fields',
|
||||
'price',
|
||||
'rating',
|
||||
'distance'
|
||||
]
|
||||
}))
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static('uploads'))
|
||||
|
||||
// 健康检查路由
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
})
|
||||
})
|
||||
|
||||
// API路由
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
// 其他API路由将在这里添加
|
||||
// app.use('/api/v1/users', userRoutes)
|
||||
// app.use('/api/v1/travel', travelRoutes)
|
||||
// app.use('/api/v1/animals', animalRoutes)
|
||||
// app.use('/api/v1/flowers', flowerRoutes)
|
||||
// app.use('/api/v1/orders', orderRoutes)
|
||||
|
||||
// 404处理
|
||||
app.use('*', notFound)
|
||||
|
||||
// 全局错误处理
|
||||
app.use(globalErrorHandler)
|
||||
|
||||
console.log('✅ 应用配置完成')
|
||||
|
||||
module.exports = app
|
||||
73
backend/src/config/database.js
Normal file
73
backend/src/config/database.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || '192.168.0.240',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'aiot$Aiot123',
|
||||
database: process.env.DB_NAME || 'jiebandata',
|
||||
connectionLimit: 10,
|
||||
// 移除无效的配置选项 acquireTimeout 和 timeout
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
// 连接池配置
|
||||
waitForConnections: true,
|
||||
queueLimit: 0
|
||||
};
|
||||
|
||||
// 创建连接池
|
||||
const pool = mysql.createPool(dbConfig);
|
||||
|
||||
// 测试数据库连接
|
||||
async function testConnection() {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
console.log('✅ MySQL数据库连接成功');
|
||||
connection.release();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ MySQL数据库连接失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
async function query(sql, params = []) {
|
||||
try {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('数据库查询错误:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行事务
|
||||
async function transaction(callback) {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const result = await callback(connection);
|
||||
await connection.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭连接池
|
||||
async function closePool() {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
query,
|
||||
transaction,
|
||||
testConnection,
|
||||
closePool
|
||||
};
|
||||
203
backend/src/config/rabbitmq.js
Normal file
203
backend/src/config/rabbitmq.js
Normal file
@@ -0,0 +1,203 @@
|
||||
const amqp = require('amqplib');
|
||||
|
||||
class RabbitMQConfig {
|
||||
constructor() {
|
||||
this.connection = null;
|
||||
this.channel = null;
|
||||
this.isConnected = false;
|
||||
this.exchanges = new Map();
|
||||
this.queues = new Map();
|
||||
}
|
||||
|
||||
// 获取连接URL
|
||||
getConnectionUrl() {
|
||||
const host = process.env.RABBITMQ_HOST || 'localhost';
|
||||
const port = process.env.RABBITMQ_PORT || 5672;
|
||||
const username = process.env.RABBITMQ_USERNAME || 'guest';
|
||||
const password = process.env.RABBITMQ_PASSWORD || 'guest';
|
||||
const vhost = process.env.RABBITMQ_VHOST || '/';
|
||||
|
||||
return `amqp://${username}:${password}@${host}:${port}/${vhost}`;
|
||||
}
|
||||
|
||||
// 连接RabbitMQ
|
||||
async connect() {
|
||||
if (this.isConnected) {
|
||||
return { connection: this.connection, channel: this.channel };
|
||||
}
|
||||
|
||||
try {
|
||||
const url = this.getConnectionUrl();
|
||||
this.connection = await amqp.connect(url);
|
||||
|
||||
this.connection.on('error', (err) => {
|
||||
console.error('RabbitMQ连接错误:', err);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.connection.on('close', () => {
|
||||
console.log('❌ RabbitMQ连接关闭');
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.channel = await this.connection.createChannel();
|
||||
|
||||
this.channel.on('error', (err) => {
|
||||
console.error('RabbitMQ通道错误:', err);
|
||||
});
|
||||
|
||||
this.channel.on('close', () => {
|
||||
console.log('❌ RabbitMQ通道关闭');
|
||||
});
|
||||
|
||||
this.isConnected = true;
|
||||
console.log('✅ RabbitMQ连接成功');
|
||||
|
||||
// 声明默认交换器
|
||||
await this.setupDefaultExchanges();
|
||||
|
||||
return { connection: this.connection, channel: this.channel };
|
||||
} catch (error) {
|
||||
console.error('RabbitMQ连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认交换器
|
||||
async setupDefaultExchanges() {
|
||||
const exchanges = [
|
||||
{ name: 'jiebanke.direct', type: 'direct', durable: true },
|
||||
{ name: 'jiebanke.topic', type: 'topic', durable: true },
|
||||
{ name: 'jiebanke.fanout', type: 'fanout', durable: true },
|
||||
{ name: 'jiebanke.delay', type: 'x-delayed-message', durable: true, arguments: { 'x-delayed-type': 'direct' } }
|
||||
];
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
await this.channel.assertExchange(exchange.name, exchange.type, {
|
||||
durable: exchange.durable,
|
||||
arguments: exchange.arguments
|
||||
});
|
||||
this.exchanges.set(exchange.name, exchange);
|
||||
}
|
||||
}
|
||||
|
||||
// 声明队列
|
||||
async assertQueue(queueName, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const queueOptions = {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-message-ttl': 86400000, // 24小时消息过期时间
|
||||
...options.arguments
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
const queue = await this.channel.assertQueue(queueName, queueOptions);
|
||||
this.queues.set(queueName, queue);
|
||||
return queue;
|
||||
}
|
||||
|
||||
// 绑定队列到交换器
|
||||
async bindQueue(queueName, exchangeName, routingKey = '') {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
await this.channel.bindQueue(queueName, exchangeName, routingKey);
|
||||
console.log(`✅ 队列 ${queueName} 绑定到交换器 ${exchangeName},路由键: ${routingKey}`);
|
||||
}
|
||||
|
||||
// 发布消息
|
||||
async publish(exchangeName, routingKey, message, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const messageBuffer = Buffer.from(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
data: message
|
||||
}));
|
||||
|
||||
const publishOptions = {
|
||||
persistent: true,
|
||||
contentType: 'application/json',
|
||||
...options
|
||||
};
|
||||
|
||||
return this.channel.publish(exchangeName, routingKey, messageBuffer, publishOptions);
|
||||
}
|
||||
|
||||
// 消费消息
|
||||
async consume(queueName, callback, options = {}) {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
const consumeOptions = {
|
||||
noAck: false,
|
||||
...options
|
||||
};
|
||||
|
||||
return this.channel.consume(queueName, async (msg) => {
|
||||
try {
|
||||
if (msg !== null) {
|
||||
const content = JSON.parse(msg.content.toString());
|
||||
await callback(content, msg);
|
||||
this.channel.ack(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('消息处理错误:', error);
|
||||
this.channel.nack(msg, false, false); // 不重新入队
|
||||
}
|
||||
}, consumeOptions);
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('RabbitMQ未连接');
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
host: process.env.RABBITMQ_HOST || 'localhost',
|
||||
port: process.env.RABBITMQ_PORT || 5672,
|
||||
connected: this.isConnected
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
host: process.env.RABBITMQ_HOST || 'localhost',
|
||||
port: process.env.RABBITMQ_PORT || 5672,
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 优雅关闭
|
||||
async close() {
|
||||
try {
|
||||
if (this.channel) {
|
||||
await this.channel.close();
|
||||
}
|
||||
if (this.connection) {
|
||||
await this.connection.close();
|
||||
}
|
||||
this.isConnected = false;
|
||||
console.log('✅ RabbitMQ连接已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭RabbitMQ连接时出错:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局RabbitMQ实例
|
||||
const rabbitMQConfig = new RabbitMQConfig();
|
||||
|
||||
module.exports = rabbitMQConfig;
|
||||
119
backend/src/config/redis.js
Normal file
119
backend/src/config/redis.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const redis = require('redis');
|
||||
|
||||
class RedisConfig {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
// 创建Redis客户端
|
||||
createClient() {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
reconnectStrategy: (retries) => {
|
||||
const delay = Math.min(retries * 100, 3000);
|
||||
console.log(`Redis连接重试第${retries + 1}次,延迟${delay}ms`);
|
||||
return delay;
|
||||
}
|
||||
},
|
||||
password: process.env.REDIS_PASSWORD || null,
|
||||
database: process.env.REDIS_DB || 0
|
||||
};
|
||||
|
||||
// 移除空配置项
|
||||
if (!redisConfig.password) delete redisConfig.password;
|
||||
|
||||
this.client = redis.createClient(redisConfig);
|
||||
|
||||
// 错误处理
|
||||
this.client.on('error', (err) => {
|
||||
console.error('Redis错误:', err);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
// 连接成功
|
||||
this.client.on('connect', () => {
|
||||
console.log('✅ Redis连接中...');
|
||||
});
|
||||
|
||||
// 准备就绪
|
||||
this.client.on('ready', () => {
|
||||
this.isConnected = true;
|
||||
console.log('✅ Redis连接就绪');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
this.client.on('end', () => {
|
||||
this.isConnected = false;
|
||||
console.log('❌ Redis连接断开');
|
||||
});
|
||||
|
||||
// 重连
|
||||
this.client.on('reconnecting', () => {
|
||||
console.log('🔄 Redis重新连接中...');
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// 连接Redis
|
||||
async connect() {
|
||||
if (this.client && this.isConnected) {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
// 开发环境下,如果Redis未配置,则不连接
|
||||
if (process.env.NODE_ENV === 'development' &&
|
||||
(!process.env.REDIS_HOST || process.env.REDIS_HOST === 'localhost')) {
|
||||
console.log('⚠️ 开发环境未配置Redis,跳过连接');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.createClient();
|
||||
await this.client.connect();
|
||||
return this.client;
|
||||
} catch (error) {
|
||||
console.error('Redis连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
async disconnect() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
this.isConnected = false;
|
||||
console.log('✅ Redis连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取客户端状态
|
||||
getStatus() {
|
||||
return {
|
||||
isConnected: this.isConnected,
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379
|
||||
};
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis未连接');
|
||||
}
|
||||
await this.client.ping();
|
||||
return { status: 'healthy', ...this.getStatus() };
|
||||
} catch (error) {
|
||||
return { status: 'unhealthy', error: error.message, ...this.getStatus() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局Redis实例
|
||||
const redisConfig = new RedisConfig();
|
||||
|
||||
module.exports = redisConfig;
|
||||
266
backend/src/controllers/authController.js
Normal file
266
backend/src/controllers/authController.js
Normal file
@@ -0,0 +1,266 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User } = require('../models/User')
|
||||
const { AppError } = require('../utils/errors')
|
||||
const { success } = require('../utils/response')
|
||||
|
||||
// 生成JWT Token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRE || '7d' }
|
||||
)
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
const register = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, nickname, email, phone } = req.body
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({
|
||||
$or: [
|
||||
{ username },
|
||||
{ email: email || null },
|
||||
{ phone: phone || null }
|
||||
]
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.username === username) {
|
||||
throw new AppError('用户名已存在', 400)
|
||||
}
|
||||
if (existingUser.email === email) {
|
||||
throw new AppError('邮箱已存在', 400)
|
||||
}
|
||||
if (existingUser.phone === phone) {
|
||||
throw new AppError('手机号已存在', 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const user = new User({
|
||||
username,
|
||||
password,
|
||||
nickname: nickname || username,
|
||||
email,
|
||||
phone
|
||||
})
|
||||
|
||||
await user.save()
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
res.status(201).json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '注册成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
const login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('用户名和密码不能为空', 400)
|
||||
}
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
const user = await User.findOne({
|
||||
$or: [
|
||||
{ username },
|
||||
{ email: username },
|
||||
{ phone: username }
|
||||
]
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isActive()) {
|
||||
throw new AppError('账户已被禁用', 403)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await user.comparePassword(password)
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError('密码错误', 401)
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '登录成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.userId)
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON()
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateProfile = async (req, res, next) => {
|
||||
try {
|
||||
const { nickname, avatar, gender, birthday } = req.body
|
||||
const updates = {}
|
||||
|
||||
if (nickname !== undefined) updates.nickname = nickname
|
||||
if (avatar !== undefined) updates.avatar = avatar
|
||||
if (gender !== undefined) updates.gender = gender
|
||||
if (birthday !== undefined) updates.birthday = birthday
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.userId,
|
||||
updates,
|
||||
{ new: true, runValidators: true }
|
||||
)
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
message: '个人信息更新成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw new AppError('当前密码和新密码不能为空', 400)
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new AppError('新密码长度不能少于6位', 400)
|
||||
}
|
||||
|
||||
const user = await User.findById(req.userId)
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404)
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await user.comparePassword(currentPassword)
|
||||
if (!isCurrentPasswordValid) {
|
||||
throw new AppError('当前密码错误', 401)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.password = newPassword
|
||||
await user.save()
|
||||
|
||||
res.json(success({
|
||||
message: '密码修改成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录/注册
|
||||
const wechatLogin = async (req, res, next) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body
|
||||
|
||||
if (!code) {
|
||||
throw new AppError('微信授权码不能为空', 400)
|
||||
}
|
||||
|
||||
// 这里应该调用微信API获取openid和unionid
|
||||
// 模拟获取微信用户信息
|
||||
const wechatUserInfo = {
|
||||
openid: `mock_openid_${Date.now()}`,
|
||||
unionid: `mock_unionid_${Date.now()}`,
|
||||
nickname: userInfo?.nickName || '微信用户',
|
||||
avatar: userInfo?.avatarUrl || '',
|
||||
gender: userInfo?.gender === 1 ? 'male' : userInfo?.gender === 2 ? 'female' : 'unknown'
|
||||
}
|
||||
|
||||
// 查找是否已存在微信用户
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ wechatOpenid: wechatUserInfo.openid },
|
||||
{ wechatUnionid: wechatUserInfo.unionid }
|
||||
]
|
||||
})
|
||||
|
||||
if (user) {
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
} else {
|
||||
// 创建新用户(微信注册)
|
||||
user = new User({
|
||||
username: `wx_${wechatUserInfo.openid.slice(-8)}`,
|
||||
password: Math.random().toString(36).slice(-8), // 随机密码
|
||||
nickname: wechatUserInfo.nickname,
|
||||
avatar: wechatUserInfo.avatar,
|
||||
gender: wechatUserInfo.gender,
|
||||
wechatOpenid: wechatUserInfo.openid,
|
||||
wechatUnionid: wechatUserInfo.unionid
|
||||
})
|
||||
await user.save()
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user._id)
|
||||
|
||||
res.json(success({
|
||||
user: user.toJSON(),
|
||||
token,
|
||||
message: '微信登录成功'
|
||||
}))
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
}
|
||||
260
backend/src/controllers/authControllerMySQL.js
Normal file
260
backend/src/controllers/authControllerMySQL.js
Normal file
@@ -0,0 +1,260 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const UserMySQL = require('../models/UserMySQL');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { success } = require('../utils/response');
|
||||
|
||||
// 生成JWT Token
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRE || '7d' }
|
||||
);
|
||||
};
|
||||
|
||||
// 用户注册
|
||||
const register = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password, nickname, email, phone } = req.body;
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (await UserMySQL.isUsernameExists(username)) {
|
||||
throw new AppError('用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (email && await UserMySQL.isEmailExists(email)) {
|
||||
throw new AppError('邮箱已存在', 400);
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (phone && await UserMySQL.isPhoneExists(phone)) {
|
||||
throw new AppError('手机号已存在', 400);
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// 创建新用户
|
||||
const userId = await UserMySQL.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
nickname: nickname || username,
|
||||
email,
|
||||
phone
|
||||
});
|
||||
|
||||
// 获取用户信息
|
||||
const user = await UserMySQL.findById(userId);
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(userId);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(userId);
|
||||
|
||||
res.status(201).json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '注册成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 用户登录
|
||||
const login = async (req, res, next) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
let user = await UserMySQL.findByUsername(username);
|
||||
if (!user) {
|
||||
user = await UserMySQL.findByEmail(username);
|
||||
}
|
||||
if (!user) {
|
||||
user = await UserMySQL.findByPhone(username);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!UserMySQL.isActive(user)) {
|
||||
throw new AppError('账户已被禁用', 403);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError('密码错误', 401);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '登录成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async (req, res, next) => {
|
||||
try {
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user)
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
const updateProfile = async (req, res, next) => {
|
||||
try {
|
||||
const { nickname, avatar, gender, birthday } = req.body;
|
||||
const updates = {};
|
||||
|
||||
if (nickname !== undefined) updates.nickname = nickname;
|
||||
if (avatar !== undefined) updates.avatar = avatar;
|
||||
if (gender !== undefined) updates.gender = gender;
|
||||
if (birthday !== undefined) updates.birthday = birthday;
|
||||
|
||||
const success = await UserMySQL.update(req.userId, updates);
|
||||
if (!success) {
|
||||
throw new AppError('更新失败', 400);
|
||||
}
|
||||
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
message: '个人信息更新成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改密码
|
||||
const changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw new AppError('当前密码和新密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new AppError('新密码长度不能少于6位', 400);
|
||||
}
|
||||
|
||||
const user = await UserMySQL.findById(req.userId);
|
||||
if (!user) {
|
||||
throw new AppError('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!isCurrentPasswordValid) {
|
||||
throw new AppError('当前密码错误', 401);
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// 更新密码
|
||||
await UserMySQL.updatePassword(req.userId, hashedPassword);
|
||||
|
||||
res.json(success({
|
||||
message: '密码修改成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 微信登录/注册
|
||||
const wechatLogin = async (req, res, next) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body;
|
||||
|
||||
if (!code) {
|
||||
throw new AppError('微信授权码不能为空', 400);
|
||||
}
|
||||
|
||||
// 这里应该调用微信API获取openid和unionid
|
||||
// 模拟获取微信用户信息
|
||||
const wechatUserInfo = {
|
||||
openid: `mock_openid_${Date.now()}`,
|
||||
unionid: `mock_unionid_${Date.now()}`,
|
||||
nickname: userInfo?.nickName || '微信用户',
|
||||
avatar: userInfo?.avatarUrl || '',
|
||||
gender: userInfo?.gender === 1 ? 'male' : userInfo?.gender === 2 ? 'female' : 'unknown'
|
||||
};
|
||||
|
||||
// 查找是否已存在微信用户
|
||||
let user = await UserMySQL.findByWechatOpenid(wechatUserInfo.openid);
|
||||
|
||||
if (user) {
|
||||
// 更新最后登录时间
|
||||
await UserMySQL.updateLastLogin(user.id);
|
||||
} else {
|
||||
// 创建新用户(微信注册)
|
||||
const randomPassword = Math.random().toString(36).slice(-8);
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, 12);
|
||||
|
||||
const userId = await UserMySQL.create({
|
||||
username: `wx_${wechatUserInfo.openid.slice(-8)}`,
|
||||
password: hashedPassword,
|
||||
nickname: wechatUserInfo.nickname,
|
||||
avatar: wechatUserInfo.avatar,
|
||||
gender: wechatUserInfo.gender,
|
||||
wechatOpenid: wechatUserInfo.openid,
|
||||
wechatUnionid: wechatUserInfo.unionid
|
||||
});
|
||||
|
||||
user = await UserMySQL.findById(userId);
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
res.json(success({
|
||||
user: UserMySQL.sanitize(user),
|
||||
token,
|
||||
message: '微信登录成功'
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
};
|
||||
298
backend/src/docs/swagger.js
Normal file
298
backend/src/docs/swagger.js
Normal file
@@ -0,0 +1,298 @@
|
||||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
|
||||
// Swagger 配置选项
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '结伴客系统 API',
|
||||
version: '1.0.0',
|
||||
description: '结伴客系统 - 旅行结伴与动物认领平台',
|
||||
contact: {
|
||||
name: '技术支持',
|
||||
email: 'support@jiebanke.com',
|
||||
url: 'https://www.jiebanke.com'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000/api/v1',
|
||||
description: '开发环境'
|
||||
},
|
||||
{
|
||||
url: 'https://api.jiebanke.com/api/v1',
|
||||
description: '生产环境'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
// 通用响应模型
|
||||
ApiResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '请求是否成功'
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
description: '状态码'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '消息描述'
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: '业务数据'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '响应时间戳'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 错误响应模型
|
||||
ErrorResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
example: false
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
example: 400
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: '请求参数错误'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
example: '详细错误信息'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 用户模型
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
example: 1
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
example: 'testuser'
|
||||
},
|
||||
nickname: {
|
||||
type: '极速版string',
|
||||
example: '测试用户'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
example: 'test@example.com'
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
example: '13800138000'
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
example: 'https://example.com/avatar.jpg'
|
||||
},
|
||||
gender: {
|
||||
type: 'string',
|
||||
enum: ['male', 'female', 'unknown'],
|
||||
example: 'male'
|
||||
},
|
||||
birthday: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
example: '1990-01-01'
|
||||
},
|
||||
points: {
|
||||
type: 'integer',
|
||||
example: 1000
|
||||
},
|
||||
level: {
|
||||
type: 'integer',
|
||||
example: 3
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'banned'],
|
||||
example: 'active'
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 分页模型
|
||||
Pagination: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {
|
||||
type: 'integer',
|
||||
example: 100
|
||||
},
|
||||
page: {
|
||||
type: 'integer',
|
||||
example: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: 'integer',
|
||||
example: 20
|
||||
},
|
||||
totalPages: {
|
||||
type: 'integer',
|
||||
example: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
// 通用分页参数
|
||||
PageParam: {
|
||||
in: 'query',
|
||||
name: 'page',
|
||||
schema: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
default: 1
|
||||
},
|
||||
description: '页码'
|
||||
},
|
||||
PageSizeParam: {
|
||||
in: 'query',
|
||||
name: 'pageSize',
|
||||
schema: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 20
|
||||
},
|
||||
description: '每页数量'
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
// 通用响应
|
||||
UnauthorizedError: {
|
||||
description: '未授权访问',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 401,
|
||||
message: '未授权访问',
|
||||
error: 'Token已过期或无效',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ForbiddenError: {
|
||||
description: '禁止访问',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 403,
|
||||
message: '禁止访问',
|
||||
error: '权限不足',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NotFoundError: {
|
||||
description: '资源不存在',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '资源不存在',
|
||||
error: '请求的资源不存在',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ValidationError: {
|
||||
description: '参数验证错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '参数验证错误',
|
||||
error: '用户名必须为4-20个字符',
|
||||
timestamp: '2025-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
BearerAuth: []
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./src/routes/*.js',
|
||||
'./src/controllers/*.js',
|
||||
'./src/models/*.js'
|
||||
]
|
||||
};
|
||||
|
||||
const specs = swaggerJsdoc(options);
|
||||
|
||||
module.exports = {
|
||||
swaggerUi,
|
||||
specs,
|
||||
serve: swaggerUi.serve,
|
||||
setup: swaggerUi.setup(specs, {
|
||||
explorer: true,
|
||||
customCss: '.swagger-ui .topbar { display: none }',
|
||||
customSiteTitle: '结伴客系统 API文档'
|
||||
})
|
||||
};
|
||||
108
backend/src/middleware/auth.js
Normal file
108
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User } = require('../models/User')
|
||||
const { AppError } = require('../utils/errors')
|
||||
|
||||
// JWT认证中间件
|
||||
const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token
|
||||
|
||||
// 从Authorization头获取token
|
||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
||||
token = req.headers.authorization.split(' ')[1]
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return next(new AppError('访问被拒绝,请提供有效的token', 401))
|
||||
}
|
||||
|
||||
// 验证token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
|
||||
|
||||
// 查找用户
|
||||
const user = await User.findById(decoded.userId)
|
||||
if (!user) {
|
||||
return next(new AppError('用户不存在', 404))
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (!user.isActive()) {
|
||||
return next(new AppError('账户已被禁用', 403))
|
||||
}
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
req.userId = user._id
|
||||
req.user = user
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return next(new AppError('无效的token', 401))
|
||||
}
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return next(new AppError('token已过期', 401))
|
||||
}
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选认证中间件(不强制要求认证)
|
||||
const optionalAuthenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token
|
||||
|
||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
||||
token = req.headers.authorization.split(' ')[1]
|
||||
}
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key')
|
||||
const user = await User.findById(decoded.userId)
|
||||
|
||||
if (user && user.isActive()) {
|
||||
req.userId = user._id
|
||||
req.user = user
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
// 忽略token验证错误,继续处理请求
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员权限检查
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AppError('请先登录', 401))
|
||||
}
|
||||
|
||||
// 这里可以根据实际需求定义管理员权限
|
||||
// 例如:检查用户角色或权限级别
|
||||
if (req.user.level < 2) { // 假设2级以上为管理员
|
||||
return next(new AppError('权限不足,需要管理员权限', 403))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// VIP权限检查
|
||||
const requireVip = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AppError('请先登录', 401))
|
||||
}
|
||||
|
||||
if (!req.user.isVip()) {
|
||||
return next(new AppError('需要VIP权限', 403))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
optionalAuthenticate,
|
||||
requireAdmin,
|
||||
requireVip
|
||||
}
|
||||
212
backend/src/models/User.js
Normal file
212
backend/src/models/User.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const mongoose = require('mongoose')
|
||||
const bcrypt = require('bcryptjs')
|
||||
|
||||
// 用户等级枚举
|
||||
const UserLevel = {
|
||||
NORMAL: 1, // 普通用户
|
||||
VIP: 2, // VIP用户
|
||||
SUPER_VIP: 3 // 超级VIP
|
||||
}
|
||||
|
||||
// 用户状态枚举
|
||||
const UserStatus = {
|
||||
ACTIVE: 'active', // 活跃
|
||||
INACTIVE: 'inactive', // 非活跃
|
||||
BANNED: 'banned' // 封禁
|
||||
}
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
// 基础信息
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
minlength: 3,
|
||||
maxlength: 20,
|
||||
match: /^[a-zA-Z0-9_]+$/
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
match: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
sparse: true,
|
||||
trim: true,
|
||||
match: /^1[3-9]\d{9}$/
|
||||
},
|
||||
|
||||
// 个人信息
|
||||
nickname: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 20
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
enum: ['male', 'female', 'unknown'],
|
||||
default: 'unknown'
|
||||
},
|
||||
birthday: Date,
|
||||
|
||||
// 账户信息
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
enum: Object.values(UserLevel),
|
||||
default: UserLevel.NORMAL
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: Object.values(UserStatus),
|
||||
default: UserStatus.ACTIVE
|
||||
},
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
|
||||
// 第三方登录
|
||||
wechatOpenid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
wechatUnionid: {
|
||||
type: String,
|
||||
sparse: true
|
||||
},
|
||||
|
||||
// 统计信息
|
||||
travelCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
animalAdoptCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
flowerOrderCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 时间戳
|
||||
lastLoginAt: Date,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: {
|
||||
transform: function(doc, ret) {
|
||||
delete ret.password
|
||||
delete ret.wechatOpenid
|
||||
delete ret.wechatUnionid
|
||||
return ret
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 索引 (移除与字段定义中 unique: true 和 sparse: true 重复的索引)
|
||||
userSchema.index({ createdAt: -1 })
|
||||
userSchema.index({ points: -1 })
|
||||
|
||||
// 密码加密中间件
|
||||
userSchema.pre('save', async function(next) {
|
||||
if (!this.isModified('password')) return next()
|
||||
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(12)
|
||||
this.password = await bcrypt.hash(this.password, salt)
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
// 比较密码方法
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
return bcrypt.compare(candidatePassword, this.password)
|
||||
}
|
||||
|
||||
// 检查用户状态是否活跃
|
||||
userSchema.methods.isActive = function() {
|
||||
return this.status === UserStatus.ACTIVE
|
||||
}
|
||||
|
||||
// 检查是否为VIP用户
|
||||
userSchema.methods.isVip = function() {
|
||||
return this.level >= UserLevel.VIP
|
||||
}
|
||||
|
||||
// 添加积分
|
||||
userSchema.methods.addPoints = function(points) {
|
||||
this.points += points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 扣除积分
|
||||
userSchema.methods.deductPoints = function(points) {
|
||||
if (this.points < points) {
|
||||
throw new Error('积分不足')
|
||||
}
|
||||
this.points -= points
|
||||
return this.save()
|
||||
}
|
||||
|
||||
// 静态方法:根据用户名查找用户
|
||||
userSchema.statics.findByUsername = function(username) {
|
||||
return this.findOne({ username })
|
||||
}
|
||||
|
||||
// 静态方法:根据邮箱查找用户
|
||||
userSchema.statics.findByEmail = function(email) {
|
||||
return this.findOne({ email })
|
||||
}
|
||||
|
||||
// 静态方法:根据手机号查找用户
|
||||
userSchema.statics.findByPhone = function(phone) {
|
||||
return this.findOne({ phone })
|
||||
}
|
||||
|
||||
// 虚拟字段:用户等级名称
|
||||
userSchema.virtual('levelName').get(function() {
|
||||
const levelNames = {
|
||||
[UserLevel.NORMAL]: '普通用户',
|
||||
[UserLevel.VIP]: 'VIP用户',
|
||||
[UserLevel.SUPER_VIP]: '超级VIP'
|
||||
}
|
||||
return levelNames[this.level] || '未知'
|
||||
})
|
||||
|
||||
const User = mongoose.model('User', userSchema)
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
UserLevel,
|
||||
UserStatus
|
||||
}
|
||||
160
backend/src/models/UserMySQL.js
Normal file
160
backend/src/models/UserMySQL.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { query, transaction } = require('../config/database');
|
||||
|
||||
class UserMySQL {
|
||||
// 创建用户
|
||||
static async create(userData) {
|
||||
const {
|
||||
openid,
|
||||
nickname,
|
||||
avatar = '',
|
||||
gender = 'other',
|
||||
birthday = null,
|
||||
phone = null,
|
||||
email = null
|
||||
} = userData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO users (
|
||||
openid, nickname, avatar, gender, birthday, phone, email,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const params = [
|
||||
openid,
|
||||
nickname,
|
||||
avatar,
|
||||
gender,
|
||||
birthday,
|
||||
phone,
|
||||
email
|
||||
];
|
||||
|
||||
const result = await query(sql, params);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// 根据ID查找用户
|
||||
static async findById(id) {
|
||||
const sql = 'SELECT * FROM users WHERE id = ?';
|
||||
const rows = await query(sql, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据openid查找用户
|
||||
static async findByOpenid(openid) {
|
||||
const sql = 'SELECT * FROM users WHERE openid = ?';
|
||||
const rows = await query(sql, [openid]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
static async findByPhone(phone) {
|
||||
const sql = 'SELECT * FROM users WHERE phone = ?';
|
||||
const rows = await query(sql, [phone]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 根据邮箱查找用户
|
||||
static async findByEmail(email) {
|
||||
const sql = 'SELECT * FROM users WHERE email = ?';
|
||||
const rows = await query(sql, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
static async update(id, updates) {
|
||||
const allowedFields = ['nickname', 'avatar', 'gender', 'birthday', 'phone', 'email'];
|
||||
const setClauses = [];
|
||||
const params = [];
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(key) && value !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setClauses.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`;
|
||||
const result = await query(sql, params);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
static async updatePassword(id, newPassword) {
|
||||
const sql = 'UPDATE users SET password = ?, updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [newPassword, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
static async updateLastLogin(id) {
|
||||
const sql = 'UPDATE users SET updated_at = NOW() WHERE id = ?';
|
||||
const result = await query(sql, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 检查openid是否已存在
|
||||
static async isOpenidExists(openid, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE openid = ?';
|
||||
const params = [openid];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
static async isEmailExists(email, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE email = ?';
|
||||
const params = [email];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
static async isPhoneExists(phone, excludeId = null) {
|
||||
let sql = 'SELECT COUNT(*) as count FROM users WHERE phone = ?';
|
||||
const params = [phone];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const rows = await query(sql, params);
|
||||
return rows[0].count > 0;
|
||||
}
|
||||
|
||||
// 检查用户名是否已已存在 (根据openid检查)
|
||||
static async isUsernameExists(username, excludeId = null) {
|
||||
return await this.isOpenidExists(username, excludeId);
|
||||
}
|
||||
|
||||
// 安全返回用户信息(去除敏感信息)
|
||||
static sanitize(user) {
|
||||
if (!user) return null;
|
||||
|
||||
const { password, ...safeUser } = user;
|
||||
return safeUser;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserMySQL;
|
||||
33
backend/src/routes/auth.js
Normal file
33
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const express = require('express')
|
||||
const { catchAsync } = require('../utils/errors')
|
||||
const { authenticate, optionalAuthenticate } = require('../middleware/auth')
|
||||
const {
|
||||
register,
|
||||
login,
|
||||
getCurrentUser,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
wechatLogin
|
||||
} = require('../controllers/authControllerMySQL')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 用户注册
|
||||
router.post('/register', catchAsync(register))
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', catchAsync(login))
|
||||
|
||||
// 微信登录
|
||||
router.post('/wechat-login', catchAsync(wechatLogin))
|
||||
|
||||
// 获取当前用户信息(需要认证)
|
||||
router.get('/me', authenticate, catchAsync(getCurrentUser))
|
||||
|
||||
// 更新用户信息(需要认证)
|
||||
router.put('/profile', authenticate, catchAsync(updateProfile))
|
||||
|
||||
// 修改密码(需要认证)
|
||||
router.put('/password', authenticate, catchAsync(changePassword))
|
||||
|
||||
module.exports = router
|
||||
172
backend/src/server.js
Normal file
172
backend/src/server.js
Normal file
@@ -0,0 +1,172 @@
|
||||
require('dotenv').config()
|
||||
const app = require('./app')
|
||||
const { testConnection } = require('./config/database')
|
||||
const redisConfig = require('./config/redis')
|
||||
const rabbitMQConfig = require('./config/rabbitmq')
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
const HOST = process.env.HOST || '0.0.0.0'
|
||||
|
||||
// 显示启动横幅
|
||||
console.log('========================================')
|
||||
console.log('🚀 服务器启动中...')
|
||||
console.log(`📅 时间: ${new Date().toISOString()}`)
|
||||
console.log(`📌 版本: 1.0.0`)
|
||||
console.log('========================================\n')
|
||||
|
||||
// 显示环境信息
|
||||
console.log('🔍 环境配置:')
|
||||
console.log(`🔹 NODE_ENV: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`🔹 PORT: ${PORT}`)
|
||||
console.log(`🔹 HOST: ${HOST}`)
|
||||
console.log(`🔹 DATABASE_URL: ${process.env.DATABASE_URL ? '已配置' : '未配置'}`)
|
||||
console.log(`🔹 REDIS_URL: ${process.env.REDIS_URL ? '已配置' : '未配置'}`)
|
||||
console.log(`🔹 RABBITMQ_URL: ${process.env.RABBITMQ_URL ? '已配置' : '未配置'}\n`)
|
||||
|
||||
// 优雅关闭处理
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未捕获的异常:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 未处理的Promise拒绝:')
|
||||
console.error(`🔹 消息: ${err.message}`)
|
||||
console.error(`🔹 堆栈: ${err.stack}`)
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
console.log('\n========================================')
|
||||
console.log('🔍 正在初始化服务...')
|
||||
console.log('========================================\n')
|
||||
|
||||
console.log('🔍 测试数据库连接...')
|
||||
// 测试数据库连接
|
||||
await testConnection()
|
||||
console.log('✅ 数据库连接测试成功')
|
||||
console.log('📌 数据库连接池配置:', {
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER
|
||||
})
|
||||
|
||||
// 连接Redis(可选)
|
||||
try {
|
||||
console.log('\n🔍 初始化Redis连接...')
|
||||
console.log(`📌 Redis配置: ${process.env.REDIS_URL || '使用默认配置'}`)
|
||||
await redisConfig.connect()
|
||||
console.log('✅ Redis连接成功')
|
||||
const info = await redisConfig.getInfo()
|
||||
console.log('📌 Redis服务器信息:', info.server)
|
||||
console.log('📌 Redis内存信息:', info.memory)
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Redis连接失败,继续以无缓存模式运行')
|
||||
console.warn(`🔹 错误详情: ${error.message}`)
|
||||
}
|
||||
|
||||
// 连接RabbitMQ(可选)
|
||||
try {
|
||||
console.log('\n🔍 初始化RabbitMQ连接...')
|
||||
console.log(`📌 RabbitMQ配置: ${process.env.RABBITMQ_URL || '使用默认配置'}`)
|
||||
await rabbitMQConfig.connect()
|
||||
console.log('✅ RabbitMQ连接成功')
|
||||
const connInfo = rabbitMQConfig.getConnectionInfo()
|
||||
console.log('📌 RabbitMQ连接信息:', connInfo)
|
||||
} catch (error) {
|
||||
console.warn('⚠️ RabbitMQ连接失败,继续以无消息队列模式运行')
|
||||
console.warn(`🔹 错误详情: ${error.message}`)
|
||||
}
|
||||
|
||||
// 启动HTTP服务器
|
||||
console.log('\n🔍 启动HTTP服务器...')
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
console.log('========================================')
|
||||
console.log('✅ 服务器启动成功!')
|
||||
console.log(`🚀 访问地址: http://${HOST}:${PORT}`)
|
||||
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`⏰ 启动时间: ${new Date().toLocaleString()}`)
|
||||
console.log('💾 数据库: MySQL')
|
||||
console.log(`🔴 Redis: ${redisConfig.isConnected() ? '已连接' : '未连接'}`)
|
||||
console.log(`🐰 RabbitMQ: ${rabbitMQConfig.isConnected() ? '已连接' : '未连接'}`)
|
||||
console.log('========================================\n')
|
||||
})
|
||||
|
||||
// 优雅关闭
|
||||
const gracefulShutdown = async (signal) => {
|
||||
console.log('\n========================================')
|
||||
console.log(`🛑 收到 ${signal} 信号,开始优雅关闭流程...`)
|
||||
console.log(`⏰ 时间: ${new Date().toLocaleString()}`)
|
||||
console.log('========================================\n')
|
||||
|
||||
// 设置超时计时器
|
||||
const shutdownTimer = setTimeout(() => {
|
||||
console.error('========================================')
|
||||
console.error('❌ 关闭操作超时,强制退出')
|
||||
console.error('========================================')
|
||||
process.exit(1)
|
||||
}, 10000)
|
||||
|
||||
try {
|
||||
// 关闭HTTP服务器
|
||||
console.log('🔐 关闭HTTP服务器...')
|
||||
await new Promise((resolve) => server.close(resolve))
|
||||
console.log('✅ HTTP服务器已关闭')
|
||||
|
||||
// 关闭Redis连接
|
||||
if (redisConfig.isConnected()) {
|
||||
console.log('🔐 关闭Redis连接...')
|
||||
await redisConfig.disconnect()
|
||||
console.log('✅ Redis连接已关闭')
|
||||
}
|
||||
|
||||
// 关闭RabbitMQ连接
|
||||
if (rabbitMQConfig.isConnected()) {
|
||||
console.log('🔐 关闭RabbitMQ连接...')
|
||||
await rabbitMQConfig.close()
|
||||
console.log('✅ RabbitMQ连接已关闭')
|
||||
}
|
||||
|
||||
console.log('\n========================================')
|
||||
console.log('👋 服务器已完全关闭')
|
||||
console.log('========================================')
|
||||
|
||||
clearTimeout(shutdownTimer)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error('========================================')
|
||||
console.error('❌ 关闭过程中发生错误:', error.message)
|
||||
console.error('========================================')
|
||||
clearTimeout(shutdownTimer)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册关闭信号
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是直接运行此文件,则启动服务器
|
||||
if (require.main === module) {
|
||||
console.log('\n🔧 启动模式: 直接运行')
|
||||
console.log(`📌 调用堆栈: ${new Error().stack.split('\n')[1].trim()}`)
|
||||
console.log('🔄 开始启动服务器...\n')
|
||||
startServer()
|
||||
}
|
||||
|
||||
module.exports = app
|
||||
96
backend/src/utils/database.js
Normal file
96
backend/src/utils/database.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.mongoose = mongoose
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
const mongodbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/jiebanke'
|
||||
await this.mongoose.connect(mongodbUri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
this.isConnected = true
|
||||
console.log('✅ MongoDB连接成功')
|
||||
|
||||
// 监听连接事件
|
||||
this.mongoose.connection.on('error', (error) => {
|
||||
console.error('❌ MongoDB连接错误:', error)
|
||||
this.isConnected = false
|
||||
})
|
||||
|
||||
this.mongoose.connection.on('disconnected', () => {
|
||||
console.warn('⚠️ MongoDB连接断开')
|
||||
this.isConnected = false
|
||||
})
|
||||
|
||||
this.mongoose.connection.on('reconnected', () => {
|
||||
console.log('🔁 MongoDB重新连接成功')
|
||||
this.isConnected = true
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB连接失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (!this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.mongoose.disconnect()
|
||||
this.isConnected = false
|
||||
console.log('✅ MongoDB连接已关闭')
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB断开连接失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
async healthCheck() {
|
||||
try {
|
||||
await this.mongoose.connection.db.admin().ping()
|
||||
return { status: 'healthy', connected: this.isConnected }
|
||||
} catch (error) {
|
||||
return { status: 'unhealthy', connected: this.isConnected, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.isConnected,
|
||||
readyState: this.mongoose.connection.readyState,
|
||||
host: this.mongoose.connection.host,
|
||||
name: this.mongoose.connection.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const database = new Database()
|
||||
|
||||
// 进程退出时关闭数据库连接
|
||||
process.on('SIGINT', async () => {
|
||||
await database.disconnect()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await database.disconnect()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
module.exports = database
|
||||
79
backend/src/utils/errors.js
Normal file
79
backend/src/utils/errors.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// 自定义应用错误类
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message)
|
||||
this.statusCode = statusCode
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'
|
||||
this.isOperational = true
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
}
|
||||
}
|
||||
|
||||
// 异步错误处理包装器
|
||||
const catchAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
// 404错误处理
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new AppError(`无法找到 ${req.originalUrl}`, 404)
|
||||
next(error)
|
||||
}
|
||||
|
||||
// 全局错误处理中间件
|
||||
const globalErrorHandler = (err, req, res, next) => {
|
||||
err.statusCode = err.statusCode || 500
|
||||
err.status = err.status || 'error'
|
||||
err.message = err.message || '服务器内部错误'
|
||||
|
||||
// 开发环境详细错误信息
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
error: err,
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
})
|
||||
} else {
|
||||
// 生产环境简化错误信息
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MongoDB重复键错误处理
|
||||
const handleDuplicateFieldsDB = (err) => {
|
||||
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0]
|
||||
const message = `字段值 ${value} 已存在,请使用其他值`
|
||||
return new AppError(message, 400)
|
||||
}
|
||||
|
||||
// MongoDB验证错误处理
|
||||
const handleValidationErrorDB = (err) => {
|
||||
const errors = Object.values(err.errors).map(el => el.message)
|
||||
const message = `输入数据无效: ${errors.join('. ')}`
|
||||
return new AppError(message, 400)
|
||||
}
|
||||
|
||||
// JWT错误处理
|
||||
const handleJWTError = () =>
|
||||
new AppError('无效的token,请重新登录', 401)
|
||||
|
||||
const handleJWTExpiredError = () =>
|
||||
new AppError('token已过期,请重新登录', 401)
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
catchAsync,
|
||||
notFound,
|
||||
globalErrorHandler,
|
||||
handleDuplicateFieldsDB,
|
||||
handleValidationErrorDB,
|
||||
handleJWTError,
|
||||
handleJWTExpiredError
|
||||
}
|
||||
70
backend/src/utils/response.js
Normal file
70
backend/src/utils/response.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// 成功响应格式
|
||||
const success = (data = null, message = '操作成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页响应格式
|
||||
const paginate = (data, pagination, message = '获取成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message,
|
||||
data: {
|
||||
list: data,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
totalPages: Math.ceil(pagination.total / pagination.pageSize)
|
||||
}
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 错误响应格式
|
||||
const error = (message = '操作失败', code = 400, errors = null) => {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
message,
|
||||
errors,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建成功响应
|
||||
const created = (data = null, message = '创建成功') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 201,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// 无内容响应
|
||||
const noContent = (message = '无内容') => {
|
||||
return {
|
||||
success: true,
|
||||
code: 204,
|
||||
message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
paginate,
|
||||
error,
|
||||
created,
|
||||
noContent
|
||||
}
|
||||
103
backend/test-api.js
Normal file
103
backend/test-api.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const http = require('http');
|
||||
|
||||
// 测试健康检查接口
|
||||
function testHealthCheck() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 3000,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('✅ 健康检查接口测试成功');
|
||||
console.log('状态码:', res.statusCode);
|
||||
console.log('响应:', JSON.parse(data));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('❌ 健康检查接口测试失败:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// 测试认证接口
|
||||
function testAuthAPI() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const postData = JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'testpass123'
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 3000,
|
||||
path: '/api/v1/auth/login',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('\n✅ 认证接口测试成功');
|
||||
console.log('状态码:', res.statusCode);
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
console.log('响应:', response);
|
||||
} catch (e) {
|
||||
console.log('原始响应:', data);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('❌ 认证接口测试失败:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('🚀 开始测试API接口...\n');
|
||||
|
||||
try {
|
||||
await testHealthCheck();
|
||||
await testAuthAPI();
|
||||
console.log('\n🎉 所有测试完成!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此文件,则执行测试
|
||||
if (require.main === module) {
|
||||
runTests();
|
||||
}
|
||||
|
||||
module.exports = { runTests };
|
||||
Reference in New Issue
Block a user