feat(backend): 添加 Swagger 文档并优化认证接口
- 在 .env 文件中添加 ENABLE_SWAGGER 环境变量 - 在 app.js 中集成 Swagger UI - 重构 auth 路由,添加请求参数验证 - 更新 API 文档,遵循 OpenAPI 3.0 规范 -优化认证接口的错误处理和响应格式
This commit is contained in:
@@ -5,6 +5,8 @@ const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const xss = require('xss-clean')
|
||||
const hpp = require('hpp')
|
||||
const swaggerUi = require('swagger-ui-express')
|
||||
const swaggerSpec = require('./config/swagger')
|
||||
|
||||
console.log('🔧 初始化Express应用...')
|
||||
|
||||
@@ -70,6 +72,12 @@ app.use(hpp({ // 防止参数污染
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static('uploads'))
|
||||
|
||||
// Swagger文档路由
|
||||
if (process.env.NODE_ENV === 'development' || process.env.ENABLE_SWAGGER === 'true') {
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
|
||||
console.log('📚 Swagger文档已启用: http://localhost:3001/api-docs')
|
||||
}
|
||||
|
||||
// 健康检查路由
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
|
||||
133
backend/src/config/swagger.js
Normal file
133
backend/src/config/swagger.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const swaggerJsdoc = require('swagger-jsdoc')
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '结伴客API文档',
|
||||
version: '1.0.0',
|
||||
description: '结伴客小程序后端API接口文档'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3001/api/v1',
|
||||
description: '开发环境服务器'
|
||||
},
|
||||
{
|
||||
url: 'https://your-domain.com/api/v1',
|
||||
description: '生产环境服务器'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '用户ID'
|
||||
},
|
||||
openid: {
|
||||
type: 'string',
|
||||
description: '微信openid'
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
description: '用户名'
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
description: '昵称'
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
description: '头像URL'
|
||||
},
|
||||
gender: {
|
||||
type: 'string',
|
||||
enum: ['male', 'female', 'other'],
|
||||
description: '性别'
|
||||
},
|
||||
birthday: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
description: '生日'
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: '手机号'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: '邮箱'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'banned'],
|
||||
description: '用户状态'
|
||||
},
|
||||
level: {
|
||||
type: 'integer',
|
||||
description: '用户等级'
|
||||
},
|
||||
points: {
|
||||
type: 'integer',
|
||||
description: '用户积分'
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '创建时间'
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '更新时间'
|
||||
}
|
||||
}
|
||||
},
|
||||
ApiResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '请求是否成功'
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
description: '状态码'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '响应消息'
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: '响应数据'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./src/routes/*.js',
|
||||
'./src/controllers/*.js'
|
||||
]
|
||||
}
|
||||
|
||||
const specs = swaggerJsdoc(options)
|
||||
|
||||
module.exports = specs
|
||||
@@ -1,33 +1,329 @@
|
||||
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 { body } = require('express-validator')
|
||||
const authController = require('../controllers/authControllerMySQL')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 用户注册
|
||||
router.post('/register', catchAsync(register))
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Auth
|
||||
* description: 用户认证相关接口
|
||||
*/
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', catchAsync(login))
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/register:
|
||||
* post:
|
||||
* summary: 用户注册
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* example: testuser
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* example: password123
|
||||
* nickname:
|
||||
* type: string
|
||||
* description: 昵称
|
||||
* example: 测试用户
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱
|
||||
* example: test@example.com
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* example: 13800000000
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 注册成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* token:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post(
|
||||
'/register',
|
||||
[
|
||||
body('username').notEmpty().withMessage('用户名不能为空'),
|
||||
body('password').isLength({ min: 6 }).withMessage('密码长度不能少于6位')
|
||||
],
|
||||
authController.register
|
||||
)
|
||||
|
||||
// 微信登录
|
||||
router.post('/wechat-login', catchAsync(wechatLogin))
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/login:
|
||||
* post:
|
||||
* summary: 用户登录
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名/邮箱/手机号
|
||||
* example: testuser
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* example: password123
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 登录成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* token:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 用户名或密码错误
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post(
|
||||
'/login',
|
||||
[
|
||||
body('username').notEmpty().withMessage('用户名不能为空'),
|
||||
body('password').notEmpty().withMessage('密码不能为空')
|
||||
],
|
||||
authController.login
|
||||
)
|
||||
|
||||
// 获取当前用户信息(需要认证)
|
||||
router.get('/me', authenticate, catchAsync(getCurrentUser))
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/me:
|
||||
* get:
|
||||
* summary: 获取当前用户信息
|
||||
* tags: [Auth]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/me', authController.getCurrentUser)
|
||||
|
||||
// 更新用户信息(需要认证)
|
||||
router.put('/profile', authenticate, catchAsync(updateProfile))
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/profile:
|
||||
* put:
|
||||
* summary: 更新用户个人信息
|
||||
* tags: [Auth]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* nickname:
|
||||
* type: string
|
||||
* description: 昵称
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: 头像URL
|
||||
* gender:
|
||||
* type: string
|
||||
* enum: [male, female, other]
|
||||
* description: 性别
|
||||
* birthday:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 生日
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.put('/profile', authController.updateProfile)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/password:
|
||||
* put:
|
||||
* summary: 修改密码
|
||||
* tags: [Auth]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - currentPassword
|
||||
* - newPassword
|
||||
* properties:
|
||||
* currentPassword:
|
||||
* type: string
|
||||
* description: 当前密码
|
||||
* newPassword:
|
||||
* type: string
|
||||
* description: 新密码
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 修改成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 当前密码错误
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.put(
|
||||
'/password',
|
||||
[
|
||||
body('currentPassword').notEmpty().withMessage('当前密码不能为空'),
|
||||
body('newPassword').isLength({ min: 6 }).withMessage('新密码长度不能少于6位')
|
||||
],
|
||||
authController.changePassword
|
||||
)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth/wechat:
|
||||
* post:
|
||||
* summary: 微信登录/注册
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - code
|
||||
* properties:
|
||||
* code:
|
||||
* type: string
|
||||
* description: 微信授权码
|
||||
* userInfo:
|
||||
* type: object
|
||||
* description: 微信用户信息
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 登录成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* user:
|
||||
* $ref: '#/components/schemas/User'
|
||||
* token:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post('/wechat', authController.wechatLogin)
|
||||
|
||||
// 修改密码(需要认证)
|
||||
router.put('/password', authenticate, catchAsync(changePassword))
|
||||
|
||||
module.exports = router
|
||||
@@ -96,8 +96,8 @@ const startServer = async () => {
|
||||
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(`🔴 Redis: ${redisConfig.isConnected ? '已连接' : '未连接'}`)
|
||||
console.log(`🐰 RabbitMQ: ${rabbitMQConfig.isConnected ? '已连接' : '未连接'}`)
|
||||
console.log('========================================\n')
|
||||
})
|
||||
|
||||
@@ -130,7 +130,7 @@ const startServer = async () => {
|
||||
}
|
||||
|
||||
// 关闭RabbitMQ连接
|
||||
if (rabbitMQConfig.isConnected()) {
|
||||
if (rabbitMQConfig.isConnected) {
|
||||
console.log('🔐 关闭RabbitMQ连接...')
|
||||
await rabbitMQConfig.close()
|
||||
console.log('✅ RabbitMQ连接已关闭')
|
||||
|
||||
Reference in New Issue
Block a user