Generating commit message...

This commit is contained in:
2025-08-30 14:33:49 +08:00
parent 4d469e95f0
commit 7f9bfbb381
99 changed files with 69225 additions and 35 deletions

54
backend/.env Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

52
backend/package.json Normal file
View 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
View 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

View 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
};

View 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
View 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;

View 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
}

View 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
View 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文档'
})
};

View 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
View 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
}

View 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;

View 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
View 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

View 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

View 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
}

View 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
View 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 };