Merge remote-tracking branch 'origin/main'

This commit is contained in:
2025-09-12 13:15:03 +08:00
committed by aiotagro
28 changed files with 10237 additions and 1945 deletions

50
backend/.dockerignore Normal file
View File

@@ -0,0 +1,50 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.production.local
.env.development.local
.env.test.local
# Build directories
build
.cache
# OS generated files
Thumbs.db
.DS_Store
# Test coverage
coverage
*.lcov
.nyc_output
# Temporary files
*.tmp
*.temp
.tmp/
.temp/

23
backend/.env.docker Normal file
View File

@@ -0,0 +1,23 @@
# 数据库配置
DB_USERNAME=niumall_user
DB_PASSWORD=niumall_password
DB_NAME=niumall_prod
DB_HOST=db
DB_PORT=3306
# JWT配置
JWT_SECRET=your_secure_jwt_secret_key_change_this_in_production
JWT_EXPIRES_IN=24h
# Redis配置
REDIS_HOST=redis
REDIS_PORT=6379
# 服务器配置
PORT=4330
# 日志配置
LOG_LEVEL=info
# 环境
NODE_ENV=production

26
backend/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# 使用官方Node.js镜像作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖(生产环境)
RUN npm install --production
# 复制项目文件
COPY . .
# 创建日志目录
RUN mkdir -p logs
# 暴露应用端口
EXPOSE 4330
# 设置环境变量
ENV NODE_ENV=production
# 启动应用
CMD ["node", "app.js"]

189
backend/README_DOCKER.md Normal file
View File

@@ -0,0 +1,189 @@
# Docker部署指南
本文档提供了使用Docker部署活牛采购智能数字化系统后端服务的详细步骤和配置说明。
## 前提条件
在开始部署前,请确保您的系统已安装以下软件:
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/)
## 配置说明
### 环境变量配置
1. 复制`.env.docker`文件为`.env`
```bash
cp .env.docker .env
```
2. 根据您的实际需求修改`.env`文件中的配置项:
```env
# 数据库配置
DB_USERNAME=niumall_user # MySQL用户名
DB_PASSWORD=niumall_password # MySQL密码
DB_NAME=niumall_prod # MySQL数据库名
# JWT配置
JWT_SECRET=your_secure_jwt_secret_key_change_this_in_production # JWT密钥请务必在生产环境中更换为安全的密钥
# 其他配置保持默认即可
```
### 目录结构说明
部署相关的主要文件:
- `Dockerfile`: 定义如何构建Node.js应用镜像
- `docker-compose.yml`: 定义多容器应用的配置
- `.env.docker`: Docker环境变量配置示例
- `.env`: 您的实际环境变量配置(不在版本控制中)
## 部署步骤
### 首次部署
1. 确保您已完成上述环境变量配置
2. 在项目根目录(包含`docker-compose.yml`的目录)执行以下命令启动所有服务:
```bash
docker-compose up -d --build
```
此命令将:
- 构建Node.js应用镜像
- 拉取MySQL和Redis镜像
- 创建并启动所有容器
- 将容器连接到内部网络
3. 查看服务状态:
```bash
docker-compose ps
```
### 初始化数据库(如果需要)
如果您需要初始化数据库表结构和数据,可以使用以下命令运行数据迁移:
```bash
docker-compose exec backend npm run db:migrate
docker-compose exec backend npm run db:seed
```
## 常用命令
### 服务管理
- 启动所有服务:
```bash
docker-compose up -d
```
- 停止所有服务:
```bash
docker-compose down
```
- 重启所有服务:
```bash
docker-compose restart
```
- 查看服务日志:
```bash
docker-compose logs -f
```
- 查看特定服务日志:
```bash
docker-compose logs -f backend
```
### 进入容器
- 进入后端应用容器:
```bash
docker-compose exec backend sh
```
- 进入MySQL容器
```bash
docker-compose exec db mysql -u${DB_USERNAME} -p${DB_PASSWORD} ${DB_NAME}
```
### 数据库管理
- 运行数据迁移:
```bash
docker-compose exec backend npm run db:migrate
```
- 重置数据库(警告:这将删除所有数据):
```bash
docker-compose exec backend npm run db:reset
```
## 数据持久化
本配置使用Docker数据卷确保数据持久化
- `mysql-data`: 存储MySQL数据库数据
- `redis-data`: 存储Redis数据
- `./logs:/app/logs`: 映射日志目录到主机
## 服务访问
部署完成后,您可以通过以下地址访问服务:
- API服务`http://服务器IP:4330`
- 健康检查:`http://服务器IP:4330/health`
## 生产环境注意事项
1. **安全配置**
- 务必修改`.env`文件中的`JWT_SECRET`为安全的随机字符串
- 不要使用默认的数据库密码
- 考虑配置HTTPS代理如Nginx
2. **资源限制**
- 为生产环境配置适当的容器资源限制
- 调整MySQL和Redis的配置以优化性能
3. **备份策略**
- 定期备份MySQL数据卷
- 考虑实现日志轮转和清理策略
4. **监控**
- 配置适当的监控解决方案如Prometheus和Grafana
- 设置日志聚合系统
## 常见问题排查
1. **服务启动失败**
- 检查环境变量配置是否正确
- 查看容器日志获取详细错误信息
2. **数据库连接问题**
- 确认`DB_HOST`设置为`db`Docker Compose服务名称
- 检查数据库凭证是否正确
3. **端口冲突**
- 如果主机端口4330、3306或6379已被占用可以修改`docker-compose.yml`中的端口映射
## 更新部署
当代码更新后,执行以下命令更新部署:
```bash
docker-compose up -d --build backend
```
## 参考文档
- [Docker官方文档](https://docs.docker.com/)
- [Docker Compose官方文档](https://docs.docker.com/compose/)

View File

@@ -4,13 +4,15 @@ const helmet = require('helmet')
const morgan = require('morgan')
const rateLimit = require('express-rate-limit')
const compression = require('compression')
const swaggerJsdoc = require('swagger-jsdoc')
const swaggerUi = require('swagger-ui-express')
const path = require('path')
require('dotenv').config()
// 数据库连接
const { testConnection, syncModels } = require('./models')
// 导入Swagger配置
const { specs, swaggerUi } = require('./config/swagger')
const app = express()
// 中间件配置
@@ -21,74 +23,7 @@ app.use(morgan('combined')) // 日志
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
// Swagger 配置
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: '活牛采购智能数字化系统 API',
version: '1.0.0',
description: '活牛采购标准化操作流程系统接口文档',
contact: {
name: 'API支持',
email: 'support@niumall.com'
}
},
servers: [
{
url: 'http://localhost:4330/api',
description: '开发环境'
},
{
url: 'https://wapi.yunniushi.cn/api',
description: '生产环境'
}
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
ApiResponse: {
type: 'object',
properties: {
success: { type: 'boolean', description: '请求是否成功' },
message: { type: 'string', description: '提示信息' },
data: { type: 'object', description: '响应数据' },
timestamp: { type: 'string', format: 'date-time', description: '时间戳' }
}
},
PaginationParams: {
type: 'object',
properties: {
page: { type: 'integer', description: '当前页码' },
limit: { type: 'integer', description: '每页数量' },
sort: { type: 'string', description: '排序字段' },
order: { type: 'string', enum: ['asc', 'desc'], description: '排序方向' }
}
},
PaginatedResponse: {
type: 'object',
properties: {
items: { type: 'array', description: '数据列表' },
total: { type: 'integer', description: '总记录数' },
page: { type: 'integer', description: '当前页码' },
limit: { type: 'integer', description: '每页数量' },
totalPages: { type: 'integer', description: '总页数' }
}
}
}
}
},
apis: ['./routes/*.js', './models/*.js'] // API路由文件路径
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// 限流
const limiter = rateLimit({
@@ -111,6 +46,13 @@ app.get('/health', (req, res) => {
})
})
// 配置Swagger UI
app.use('/swagger', swaggerUi.serve, swaggerUi.setup(specs, {
explorer: true,
customCss: '.swagger-ui .topbar { background-color: #3B82F6; }',
customSiteTitle: 'NiuMall API 文档'
}))
// API 路由
app.use('/api/auth', require('./routes/auth'))
app.use('/api/users', require('./routes/users'))
@@ -120,6 +62,14 @@ app.use('/api/transport', require('./routes/transport'))
app.use('/api/finance', require('./routes/finance'))
app.use('/api/quality', require('./routes/quality'))
// 静态文件服务
app.use('/static', express.static('public'));
// API文档路由重定向
app.get('/docs', (req, res) => {
res.redirect('/swagger');
});
// 404 处理
app.use((req, res) => {
res.status(404).json({
@@ -161,7 +111,7 @@ const startServer = async () => {
console.log(`📱 运行环境: ${process.env.NODE_ENV || 'development'}`)
console.log(`🌐 访问地址: http://localhost:${PORT}`)
console.log(`📊 健康检查: http://localhost:${PORT}/health`)
console.log(`📚 API文档: http://localhost:${PORT}/api/docs`)
console.log(`📚 API文档: http://localhost:${PORT}/swagger`)
})
} catch (error) {
console.error('❌ 服务器启动失败:', error)

248
backend/config/swagger.js Normal file
View File

@@ -0,0 +1,248 @@
const swaggerJsdoc = require('swagger-jsdoc')
const swaggerUi = require('swagger-ui-express')
// Swagger 配置选项
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '活牛采购智能数字化系统 API文档',
description: '活牛采购智能数字化系统的后端API接口文档',
version: '1.0.0',
contact: {
name: 'NiuMall Team',
email: 'contact@niumall.com'
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT'
}
},
servers: [
{
url: 'http://localhost:4330',
description: '本地开发环境'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
// 通用响应格式
ApiResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: '请求是否成功'
},
message: {
type: 'string',
description: '返回消息'
},
data: {
type: 'object',
description: '返回数据',
nullable: true
}
}
},
// 分页数据格式
PagedResponse: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object'
},
description: '数据列表'
},
total: {
type: 'integer',
description: '总条数'
},
page: {
type: 'integer',
description: '当前页码'
},
pageSize: {
type: 'integer',
description: '每页条数'
},
totalPages: {
type: 'integer',
description: '总页数'
}
}
},
// 订单相关模型
Order: {
type: 'object',
properties: {
id: {
type: 'integer',
description: '订单ID'
},
orderNo: {
type: 'string',
description: '订单编号'
},
buyerId: {
type: 'integer',
description: '买方ID'
},
buyerName: {
type: 'string',
description: '买方名称'
},
supplierId: {
type: 'integer',
description: '供应商ID'
},
supplierName: {
type: 'string',
description: '供应商名称'
},
traderId: {
type: 'integer',
nullable: true,
description: '贸易商ID'
},
traderName: {
type: 'string',
nullable: true,
description: '贸易商名称'
},
cattleBreed: {
type: 'string',
description: '牛品种'
},
cattleCount: {
type: 'integer',
description: '牛数量'
},
expectedWeight: {
type: 'number',
description: '预计重量'
},
actualWeight: {
type: 'number',
nullable: true,
description: '实际重量'
},
unitPrice: {
type: 'number',
description: '单价'
},
totalAmount: {
type: 'number',
description: '总金额'
},
paidAmount: {
type: 'number',
description: '已支付金额'
},
remainingAmount: {
type: 'number',
description: '剩余金额'
},
status: {
type: 'string',
enum: ['pending', 'confirmed', 'preparing', 'shipping', 'delivered', 'accepted', 'completed', 'cancelled', 'refunded'],
description: '订单状态'
},
deliveryAddress: {
type: 'string',
description: '送货地址'
},
expectedDeliveryDate: {
type: 'string',
format: 'date',
description: '预计送达日期'
},
actualDeliveryDate: {
type: 'string',
format: 'date',
nullable: true,
description: '实际送达日期'
},
notes: {
type: 'string',
nullable: true,
description: '备注'
},
createdAt: {
type: 'string',
format: 'date-time',
description: '创建时间'
},
updatedAt: {
type: 'string',
format: 'date-time',
description: '更新时间'
}
}
},
// 用户相关模型
User: {
type: 'object',
properties: {
id: {
type: 'integer',
description: '用户ID'
},
username: {
type: 'string',
description: '用户名'
},
email: {
type: 'string',
format: 'email',
description: '邮箱'
},
phone: {
type: 'string',
description: '手机号'
},
user_type: {
type: 'string',
enum: ['admin', 'buyer', 'supplier', 'trader'],
description: '用户类型'
},
status: {
type: 'string',
enum: ['active', 'inactive', 'suspended'],
description: '用户状态'
},
createdAt: {
type: 'string',
format: 'date-time',
description: '创建时间'
},
updatedAt: {
type: 'string',
format: 'date-time',
description: '更新时间'
}
}
}
}
}
},
// 指定API文档的路径
apis: ['../routes/*.js']
}
// 初始化Swagger-jsdoc
const specs = swaggerJsdoc(options)
module.exports = {
specs,
swaggerUi
}

View File

@@ -0,0 +1,65 @@
# Docker Compose配置文件 - 活牛采购智能数字化系统后端
# 全局配置
x-common-config:
&common-config
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 服务定义
services:
# 后端Node.js应用服务
backend:
<<: *common-config
build:
context: .
dockerfile: Dockerfile
container_name: niumall-backend
restart: unless-stopped
ports:
- "4330:4330"
environment:
# 数据库配置 - 使用外部数据库
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
# Redis配置 - 使用外部Redis
REDIS_HOST: ${DB_HOST}
REDIS_PORT: 6379
# 服务器配置
PORT: 4330
NODE_ENV: production
# JWT配置
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 24h
# 日志配置
LOG_LEVEL: info
volumes:
- ./logs:/app/logs
depends_on:
# 不再依赖内部数据库和Redis容器改为使用外部服务
wait-for-db:
condition: service_completed_successfully
# 等待数据库连接测试服务
wait-for-db:
image: busybox:latest
container_name: wait-for-db
command: ["sh", "-c", "sleep 5 && echo '数据库连接等待完成'"]
environment:
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
# 数据卷配置
volumes:
logs:
driver: local

131
backend/middleware/auth.js Normal file
View File

@@ -0,0 +1,131 @@
const jwt = require('jsonwebtoken')
/**
* @swagger
* components:
* securitySchemes:
* bearerAuth:
* type: http
* scheme: bearer
* bearerFormat: JWT
* security:
* - bearerAuth: []
* responses:
* UnauthorizedError:
* description: 未授权访问
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* message:
* type: string
* example: 未授权访问
* ForbiddenError:
* description: 权限不足
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* message:
* type: string
* example: 权限不足
*/
// 从环境变量或配置中获取JWT密钥
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'
/**
* JWT认证中间件
* 验证请求中的JWT token确认用户身份
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express下一个中间件函数
*/
const authenticateJWT = (req, res, next) => {
// 从Authorization头中获取token
const authHeader = req.headers.authorization
if (!authHeader) {
return res.status(401).json({
success: false,
message: '未提供认证令牌'
})
}
// 提取token格式Bearer <token>
const token = authHeader.split(' ')[1]
if (!token) {
return res.status(401).json({
success: false,
message: '无效的认证令牌格式'
})
}
try {
// 验证token
const decoded = jwt.verify(token, JWT_SECRET)
// 将解码的用户信息附加到请求对象中
req.user = decoded
// 继续处理请求
next()
} catch (error) {
console.error('JWT验证失败:', error)
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '认证令牌已过期'
})
}
return res.status(401).json({
success: false,
message: '无效的认证令牌'
})
}
}
/**
* 用户角色授权中间件
* 检查用户是否具有指定角色
* @param {Array<string>} allowedRoles - 允许访问的角色列表
* @returns {Function} Express中间件函数
*/
const authorizeRoles = (...allowedRoles) => {
return (req, res, next) => {
// 确保用户已通过认证
if (!req.user) {
return res.status(401).json({
success: false,
message: '未授权访问'
})
}
// 检查用户角色是否在允许列表中
if (!allowedRoles.includes(req.user.user_type)) {
return res.status(403).json({
success: false,
message: '权限不足'
})
}
// 用户具有所需角色,继续处理请求
next()
}
}
module.exports = {
authenticateJWT,
authorizeRoles
}

View File

@@ -22,6 +22,9 @@ const sequelize = new Sequelize(
}
);
// 导入模型定义
const defineOrder = require('./order.js');
// 测试数据库连接
const testConnection = async () => {
try {
@@ -118,15 +121,23 @@ const models = {
}, {
tableName: 'api_users',
timestamps: true
})
}),
// 订单模型
Order: defineOrder(sequelize)
};
// 同步数据库模型
const syncModels = async () => {
try {
// 同步API用户表如果不存在则创建
// 同步API用户表如果不存在则创建
await models.ApiUser.sync({ alter: true });
console.log('✅ API用户表同步成功');
// 同步订单表(如果不存在则创建)
await models.Order.sync({ alter: true });
console.log('✅ 订单表同步成功');
console.log('✅ 数据库模型同步完成');
} catch (error) {
console.error('❌ 数据库模型同步失败:', error);

132
backend/models/order.js Normal file
View File

@@ -0,0 +1,132 @@
// 订单模型定义
const { Sequelize } = require('sequelize');
module.exports = (sequelize) => {
const Order = sequelize.define('Order', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
orderNo: {
type: Sequelize.STRING(20),
allowNull: false,
unique: true,
comment: '订单编号'
},
buyerId: {
type: Sequelize.INTEGER,
allowNull: false,
comment: '买方ID'
},
buyerName: {
type: Sequelize.STRING(100),
allowNull: false,
comment: '买方名称'
},
supplierId: {
type: Sequelize.INTEGER,
allowNull: false,
comment: '供应商ID'
},
supplierName: {
type: Sequelize.STRING(100),
allowNull: false,
comment: '供应商名称'
},
traderId: {
type: Sequelize.INTEGER,
allowNull: true,
comment: '贸易商ID'
},
traderName: {
type: Sequelize.STRING(100),
allowNull: true,
comment: '贸易商名称'
},
cattleBreed: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '牛的品种'
},
cattleCount: {
type: Sequelize.INTEGER,
allowNull: false,
comment: '牛的数量'
},
expectedWeight: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false,
comment: '预计总重量'
},
actualWeight: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
comment: '实际总重量'
},
unitPrice: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false,
comment: '单价'
},
totalAmount: {
type: Sequelize.DECIMAL(15, 2),
allowNull: false,
comment: '总金额'
},
paidAmount: {
type: Sequelize.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0,
comment: '已支付金额'
},
remainingAmount: {
type: Sequelize.DECIMAL(15, 2),
allowNull: false,
comment: '剩余金额'
},
status: {
type: Sequelize.ENUM(
'pending', 'confirmed', 'preparing', 'shipping',
'delivered', 'accepted', 'completed', 'cancelled', 'refunded'
),
allowNull: false,
defaultValue: 'pending',
comment: '订单状态'
},
deliveryAddress: {
type: Sequelize.STRING(200),
allowNull: false,
comment: '收货地址'
},
expectedDeliveryDate: {
type: Sequelize.DATE,
allowNull: false,
comment: '预计交货日期'
},
actualDeliveryDate: {
type: Sequelize.DATE,
allowNull: true,
comment: '实际交货日期'
},
notes: {
type: Sequelize.TEXT,
allowNull: true,
comment: '备注信息'
}
}, {
tableName: 'orders',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ fields: ['orderNo'] },
{ fields: ['buyerId'] },
{ fields: ['supplierId'] },
{ fields: ['status'] },
{ fields: ['created_at'] }
]
});
return Order;
};

4114
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,10 @@
"name": "niumall-backend",
"version": "1.0.0",
"description": "活牛采购智能数字化系统 - 后端服务",
"main": "src/app.js",
"main": "src/main.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"start": "node src/main.js",
"dev": "nodemon src/main.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
@@ -49,8 +49,11 @@
"redis": "^4.6.7",
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"uuid": "^9.0.0",
"winston": "^3.10.0"
"winston": "^3.10.0",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^8.45.0",

View File

@@ -0,0 +1,984 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NiuMall API 文档</title>
<style>
/* 基础样式 */
:root {
--primary: #3B82F6;
--secondary: #10B981;
--accent: #8B5CF6;
--danger: #EF4444;
--warning: #F59E0B;
--info: #60A5FA;
--dark: #1E293B;
--light: #F1F5F9;
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-500: #6B7280;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
--blue-100: #DBEAFE;
--blue-50: #EFF6FF;
--blue-800: #1E40AF;
--green-100: #D1FAE5;
--green-50: #ECFDF5;
--green-600: #059669;
--green-800: #065F46;
--red-50: #FEF2F2;
--red-600: #DC2626;
--yellow-50: #FFFBEB;
--yellow-100: #FEF3C7;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--gray-50);
min-height: 100vh;
color: var(--gray-900);
line-height: 1.6;
}
/* 布局样式 */
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 0 16px;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.lg:flex-row {
flex-direction: row;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.gap-8 {
gap: 32px;
}
.shrink-0 {
flex-shrink: 0;
}
.lg:w-64 {
width: 256px;
}
.flex-1 {
flex: 1;
}
/* 卡片和阴影 */
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.shadow-card {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded-xl {
border-radius: 12px;
}
.rounded-lg {
border-radius: 8px;
}
.rounded {
border-radius: 4px;
}
.rounded-full {
border-radius: 9999px;
}
/* 内边距和外边距 */
.p-4 {
padding: 16px;
}
.p-6 {
padding: 24px;
}
.py-6 {
padding-top: 24px;
padding-bottom: 24px;
}
.px-4 {
padding-left: 16px;
padding-right: 16px;
}
.px-6 {
padding-left: 24px;
padding-right: 24px;
}
.py-8 {
padding-top: 32px;
padding-bottom: 32px;
}
.mt-1 {
margin-top: 4px;
}
.mt-4 {
margin-top: 16px;
}
.mt-8 {
margin-top: 32px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-8 {
margin-bottom: 32px;
}
.mb-12 {
margin-bottom: 48px;
}
.ml-2 {
margin-left: 8px;
}
.mr-2 {
margin-right: 8px;
}
/* 文本样式 */
.text-white {
color: white;
}
.text-primary {
color: var(--primary);
}
.text-secondary {
color: var(--secondary);
}
.text-accent {
color: var(--accent);
}
.text-dark {
color: var(--dark);
}
.text-gray-100 {
color: var(--gray-100);
}
.text-gray-400 {
color: var(--gray-400);
}
.text-gray-500 {
color: var(--gray-500);
}
.text-gray-700 {
color: var(--gray-700);
}
.text-gray-800 {
color: var(--gray-800);
}
.text-blue-100 {
color: var(--blue-100);
}
.text-blue-800 {
color: var(--blue-800);
}
.text-green-600 {
color: var(--green-600);
}
.text-green-800 {
color: var(--green-800);
}
.text-red-600 {
color: var(--red-600);
}
.text-yellow-100 {
color: var(--yellow-100);
}
.text-gray-300 {
color: var(--gray-300);
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.text-xs {
font-size: 12px;
}
.text-sm {
font-size: 14px;
}
.text-base {
font-size: 16px;
}
.text-lg {
font-size: 18px;
}
.text-xl {
font-size: 20px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.uppercase {
text-transform: uppercase;
}
.tracking-wider {
letter-spacing: 0.05em;
}
.text-shadow {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 背景色 */
.bg-primary {
background-color: var(--primary);
}
.bg-secondary {
background-color: var(--secondary);
}
.bg-accent {
background-color: var(--accent);
}
.bg-dark {
background-color: var(--dark);
}
.bg-gray-50 {
background-color: var(--gray-50);
}
.bg-gray-100 {
background-color: var(--gray-100);
}
.bg-gray-800 {
background-color: var(--gray-800);
}
.bg-white {
background-color: white;
}
.bg-blue-50 {
background-color: var(--blue-50);
}
.bg-blue-100 {
background-color: var(--blue-100);
}
.bg-green-100 {
background-color: var(--green-100);
}
.bg-green-50 {
background-color: var(--green-50);
}
.bg-red-50 {
background-color: var(--red-50);
}
.bg-yellow-50 {
background-color: var(--yellow-50);
}
.bg-yellow-100 {
background-color: var(--yellow-100);
}
.bg-info {
background-color: var(--info);
}
.bg-danger {
background-color: var(--danger);
}
.bg-warning {
background-color: var(--warning);
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--primary), var(--accent));
}
/* 导航和链接 */
a {
color: inherit;
text-decoration: none;
transition: all 0.2s ease;
}
a:hover {
color: var(--primary);
}
/* 表格样式 */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px 16px;
text-align: left;
border-bottom: 1px solid var(--gray-200);
}
th {
font-weight: 600;
color: var(--gray-500);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* 代码和预格式化文本 */
pre {
overflow-x: auto;
padding: 16px;
border-radius: 8px;
background-color: var(--gray-800);
color: var(--gray-100);
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--gray-100);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--gray-400);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--gray-500);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.lg:flex-row {
flex-direction: column;
}
.lg:w-64 {
width: 100%;
}
}
/* 工具类 */
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.border-l-4 {
border-left: 4px solid;
}
.pl-4 {
padding-left: 16px;
}
.sticky {
position: sticky;
top: 16px;
}
/* 图标替代方案 */
.icon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
vertical-align: middle;
}
/* 导航高亮 */
.nav-link.active {
background-color: var(--blue-50);
color: var(--primary);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<header class="bg-gradient-to-r from-primary to-accent text-white shadow-lg">
<div class="container mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row md:justify-between md:items-center">
<div>
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-shadow">NiuMall API 文档</h1>
<p class="mt-1 text-blue-100">版本: 1.0.0 | 最后更新: 2025-09-12</p>
</div>
<div class="mt-4 md:mt-0">
<div class="flex items-center space-x-2 bg-white/20 px-4 py-2 rounded-lg backdrop-blur-sm">
<span class="icon" style="background: white; width: 20px; height: 20px; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 18H5c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1h14c.55 0 1 .45 1 1v14c0 .55-.45 1-1 1z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span>
<span>运行环境: Production</span>
</div>
</div>
</div>
</div>
</header>
<div class="container mx-auto px-4 py-8 sm:px-6 lg:px-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- 侧边导航 -->
<aside class="lg:w-64 shrink-0">
<nav class="bg-white rounded-xl shadow-card p-4 sticky top-4">
<h2 class="text-lg font-bold text-dark mb-4 flex items-center">
<span class="icon" style="background: linear-gradient(45deg, var(--primary), var(--accent)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 接口分类
</h2>
<ul class="space-y-1">
<li>
<a href="#auth" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--primary), var(--accent)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 15c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm0-2c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm0-10C7.03 3 3 7.03 3 12c0 5.52 3.84 10.74 9 12 5.16-1.26 9-6.48 9-12 0-4.97-4.03-9-9-9zm0 16.5c-3.59 0-6.5-2.91-6.5-6.5s2.91-6.5 6.5-6.5 6.5 2.91 6.5 6.5-2.91 6.5-6.5 6.5z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 认证管理
</a>
</li>
<li>
<a href="#users" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--secondary), var(--primary)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 用户管理
</a>
</li>
<li>
<a href="#orders" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--warning), var(--primary)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 订单管理
</a>
</li>
<li>
<a href="#finance" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--info), var(--primary)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 财务管理
</a>
</li>
<li>
<a href="#suppliers" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--accent), var(--primary)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19 6H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H5V8h14v8zm-4-6h-4v2h4v-2zm0 4h-4v2h4v-2z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 供应商管理
</a>
</li>
<li>
<a href="#transport" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--secondary), var(--accent)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 19.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V10.5h2.5zM18 19.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 运输管理
</a>
</li>
<li>
<a href="#quality" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--warning), var(--accent)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 质量管理
</a>
</li>
<li>
<a href="#system" class="block px-4 py-2 rounded-lg hover:bg-blue-50 hover:text-primary">
<span class="icon" style="background: linear-gradient(45deg, var(--dark), var(--primary)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 系统接口
</a>
</li>
</ul>
<div class="mt-8">
<h2 class="text-lg font-bold text-dark mb-4 flex items-center">
<span class="icon" style="background: linear-gradient(45deg, var(--primary), var(--accent)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 文档说明
</h2>
<div class="bg-blue-50 p-4 rounded-lg">
<p class="text-sm text-blue-800">
本API文档提供了NiuMall系统所有后端接口的详细说明。所有接口均返回统一的JSON格式包含success、message和data字段。
</p>
</div>
</div>
</nav>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<!-- 认证管理 -->
<section id="auth" class="mb-12">
<div class="bg-white rounded-xl shadow-card overflow-hidden">
<div class="bg-primary text-white px-6 py-4">
<h2 class="text-xl font-bold flex items-center">
<span class="icon" style="background: white; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 15c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm0-2c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm0-10C7.03 3 3 7.03 3 12c0 5.52 3.84 10.74 9 12 5.16-1.26 9-6.48 9-12 0-4.97-4.03-9-9-9zm0 16.5c-3.59 0-6.5-2.91-6.5-6.5s2.91-6.5 6.5-6.5 6.5 2.91 6.5 6.5-2.91 6.5-6.5 6.5z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 认证管理
</h2>
<p class="text-blue-100 text-sm mt-1">认证相关接口,用于用户登录获取访问令牌</p>
</div>
<div class="p-6">
<!-- 登录接口 -->
<div class="mb-8 border-l-4 border-primary pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">用户登录</h3>
<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded font-medium">POST</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/api/auth/login</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">请求参数</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参数名</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">必填</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-4 py-2 font-medium">username</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-red-600"></td>
<td class="px-4 py-2">用户名/手机号/邮箱</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">password</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-red-600"></td>
<td class="px-4 py-2">密码</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">请求示例</h4>
<div class="bg-gray-800 text-gray-100 p-4 rounded-lg text-sm overflow-x-auto">
<pre>{"username": "admin", "password": "123456"}</pre>
</div>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">响应示例</h4>
<div class="bg-gray-800 text-gray-100 p-4 rounded-lg text-sm overflow-x-auto">
<pre>{"success": true, "message": "登录成功", "data": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 86400, "user": {"id": 1, "username": "admin", "email": "admin@example.com", "role": "admin", "status": "active"}}}</pre>
</div>
</div>
<div>
<h4 class="text-sm font-semibold text-gray-700 mb-2">响应状态码</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
<div class="bg-green-50 px-3 py-2 rounded-lg flex items-center">
<span class="bg-green-500 text-white text-xs w-6 h-6 flex items-center justify-center rounded-full mr-2">200</span>
<span>登录成功</span>
</div>
<div class="bg-red-50 px-3 py-2 rounded-lg flex items-center">
<span class="bg-red-500 text-white text-xs w-6 h-6 flex items-center justify-center rounded-full mr-2">401</span>
<span>用户名或密码错误</span>
</div>
<div class="bg-yellow-50 px-3 py-2 rounded-lg flex items-center">
<span class="bg-yellow-500 text-white text-xs w-6 h-6 flex items-center justify-center rounded-full mr-2">403</span>
<span>账户已被禁用</span>
</div>
<div class="bg-blue-50 px-3 py-2 rounded-lg flex items-center">
<span class="bg-blue-500 text-white text-xs w-6 h-6 flex items-center justify-center rounded-full mr-2">500</span>
<span>服务器内部错误</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 用户管理 -->
<section id="users" class="mb-12">
<div class="bg-white rounded-xl shadow-card overflow-hidden">
<div class="bg-secondary text-white px-6 py-4">
<h2 class="text-xl font-bold flex items-center">
<span class="icon" style="background: white; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 用户管理
</h2>
<p class="text-green-100 text-sm mt-1">用户相关接口,用于管理系统用户信息</p>
</div>
<div class="p-6">
<!-- 获取用户列表 -->
<div class="mb-8 border-l-4 border-secondary pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">获取用户列表</h3>
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded font-medium">GET</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/api/users</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">查询参数</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参数名</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">必填</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-4 py-2 font-medium">page</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">页码默认为1</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">pageSize</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">每页条数默认为20</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">keyword</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">关键词搜索(用户名、邮箱、手机号)</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">user_type</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">用户类型筛选</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">status</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">用户状态筛选</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">响应示例</h4>
<div class="bg-gray-800 text-gray-100 p-4 rounded-lg text-sm overflow-x-auto">
<pre>{"success": true, "data": {"items": [{"id": 1, "username": "admin", "email": "admin@example.com", "phone": "13800138000", "user_type": "admin", "status": "active", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}], "total": 10, "page": 1, "pageSize": 20, "totalPages": 1}}</pre>
</div>
</div>
</div>
<!-- 获取用户详情 -->
<div class="mb-8 border-l-4 border-secondary pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">获取用户详情</h3>
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded font-medium">GET</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/api/users/:id</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">路径参数</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参数名</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">必填</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-4 py-2 font-medium">id</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-red-600"></td>
<td class="px-4 py-2">用户ID</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">响应示例</h4>
<div class="bg-gray-800 text-gray-100 p-4 rounded-lg text-sm overflow-x-auto">
<pre>{"success": true, "data": {"id": 1, "username": "admin", "email": "admin@example.com", "phone": "13800138000", "user_type": "admin", "status": "active", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}}</pre>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 订单管理 -->
<section id="orders" class="mb-12">
<div class="bg-white rounded-xl shadow-card overflow-hidden">
<div class="bg-warning text-white px-6 py-4">
<h2 class="text-xl font-bold flex items-center">
<span class="icon" style="background: white; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 订单管理
</h2>
<p class="text-yellow-100 text-sm mt-1">订单相关接口,用于管理商品订单</p>
</div>
<div class="p-6">
<!-- 获取订单列表 -->
<div class="mb-8 border-l-4 border-warning pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">获取订单列表</h3>
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded font-medium">GET</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/api/orders</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">查询参数</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参数名</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">必填</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-4 py-2 font-medium">page</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">页码默认为1</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">pageSize</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">每页条数默认为20</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">orderNo</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">订单号搜索</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">buyerId</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">买方ID筛选</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">supplierId</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">供应商ID筛选</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">status</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">订单状态筛选</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 财务管理 -->
<section id="finance" class="mb-12">
<div class="bg-white rounded-xl shadow-card overflow-hidden">
<div class="bg-info text-white px-6 py-4">
<h2 class="text-xl font-bold flex items-center">
<span class="icon" style="background: white; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 财务管理
</h2>
<p class="text-blue-100 text-sm mt-1">财务相关接口,用于管理结算和支付</p>
</div>
<div class="p-6">
<!-- 获取结算列表 -->
<div class="mb-8 border-l-4 border-info pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">获取结算列表</h3>
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded font-medium">GET</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/api/finance/settlements</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">查询参数</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参数名</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">必填</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-4 py-2 font-medium">page</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">页码默认为1</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">pageSize</td>
<td class="px-4 py-2">number</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">每页条数默认为20</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">keyword</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">关键词搜索</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium">paymentStatus</td>
<td class="px-4 py-2">string</td>
<td class="px-4 py-2 text-green-600"></td>
<td class="px-4 py-2">支付状态筛选</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 系统接口 -->
<section id="system" class="mb-12">
<div class="bg-white rounded-xl shadow-card overflow-hidden">
<div class="bg-dark text-white px-6 py-4">
<h2 class="text-xl font-bold flex items-center">
<span class="icon" style="background: white; mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 系统接口
</h2>
<p class="text-gray-300 text-sm mt-1">系统级接口,用于监控和维护</p>
</div>
<div class="p-6">
<!-- 健康检查 -->
<div class="mb-8 border-l-4 border-dark pl-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-dark">健康检查</h3>
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded font-medium">GET</span>
<span class="bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded">/health</span>
</div>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-700 mb-2">响应示例</h4>
<div class="bg-gray-800 text-gray-100 p-4 rounded-lg text-sm overflow-x-auto">
<pre>{"success": true, "message": "服务运行正常", "timestamp": "2025-09-12T04:47:48.209Z", "version": "1.0.0"}</pre>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
</div>
<footer class="bg-dark text-gray-400 py-8 mt-12">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<p>NiuMall API 文档 &copy; 2025</p>
</div>
<div class="flex space-x-4">
<a href="#" class="hover:text-white">
<span class="icon" style="background: var(--gray-400); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 帮助中心
</a>
<a href="#" class="hover:text-white">
<span class="icon" style="background: var(--gray-400); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 联系我们
</a>
<a href="#" class="hover:text-white">
<span class="icon" style="background: var(--gray-400); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z'/></svg>'); mask-size: contain; mask-repeat: no-repeat;"></span> 完整文档
</a>
</div>
</div>
</div>
</footer>
<script src="/static/scripts/docs.js"></script>
</body>
</html>

View File

@@ -0,0 +1,61 @@
// 平滑滚动功能
function setupSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
// 使用简单的滚动实现避免依赖behavior: 'smooth'可能在某些浏览器中不支持
window.scrollTo({
top: targetElement.offsetTop - 20,
behavior: 'auto'
});
}
});
});
}
// 导航高亮功能
function setupNavHighlight() {
const sections = document.querySelectorAll('section');
const navLinks = document.querySelectorAll('nav a');
window.addEventListener('scroll', () => {
let currentSection = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (window.pageYOffset >= (sectionTop - 100)) {
currentSection = section.getAttribute('id');
}
});
navLinks.forEach(link => {
// 移除所有链接的高亮
link.style.backgroundColor = '';
link.style.color = '';
// 为当前部分的链接添加高亮
if (link.getAttribute('href') === `#${currentSection}`) {
link.style.backgroundColor = 'var(--blue-50)';
link.style.color = 'var(--primary)';
}
});
});
}
// 页面加载完成后执行初始化
function init() {
setupSmoothScroll();
setupNavHighlight();
}
// 检查DOM是否已经加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

View File

@@ -7,25 +7,101 @@ const router = express.Router()
// 引入数据库模型
const { ApiUser } = require('../models')
// 登录参数验证
// 引入认证中间件
const { authenticateJWT } = require('../middleware/auth')
/**
* @swagger
* components:
* schemas:
* LoginRequest:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名
* password:
* type: string
* description: 密码
* LoginResponse:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* token:
* type: string
* user:
* type: object
* properties:
* id:
* type: integer
* username:
* type: string
* email:
* type: string
* user_type:
* type: string
* status:
* type: string
*/
// 从环境变量或配置中获取JWT密钥
const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h'
// 验证模式
const loginSchema = Joi.object({
username: Joi.string().min(2).max(50).required(),
password: Joi.string().min(6).max(100).required()
username: Joi.string().required(),
password: Joi.string().required()
})
// 生成JWT token
// 生成JWT令牌
const generateToken = (user) => {
return jwt.sign(
{
id: user.id,
username: user.username,
role: user.user_type
email: user.email,
user_type: user.user_type
},
process.env.JWT_SECRET || 'niumall-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
JWT_SECRET,
{
expiresIn: JWT_EXPIRES_IN
}
)
}
/**
* @swagger
* /api/auth/login:
* post:
* summary: 用户登录
* tags: [认证管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginRequest'
* responses:
* 200:
* description: 登录成功
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginResponse'
* 400:
* description: 参数验证失败或用户名密码错误
* 401:
* description: 未授权或用户被禁用
* 500:
* description: 服务器内部错误
*/
// 用户登录
router.post('/login', async (req, res) => {
try {
@@ -38,94 +114,126 @@ router.post('/login', async (req, res) => {
details: error.details[0].message
})
}
const { username, password } = value
// 查找用户
const user = await ApiUser.findOne({
where: {
[require('sequelize').Op.or]: [
{ username: username },
{ phone: username },
[ApiUser.sequelize.Op.or]: [
{ username },
{ email: username }
]
}
});
})
if (!user) {
// 检查用户是否存在以及密码是否正确
if (!user || !(await bcrypt.compare(password, user.password_hash))) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
})
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password_hash)
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
})
}
// 检查用户状态
if (user.status !== 'active') {
return res.status(403).json({
return res.status(401).json({
success: false,
message: '账户已被禁用,请联系管理员'
message: '用户账号已被禁用'
})
}
// 生成token
// 生成JWT令牌
const token = generateToken(user)
// 准备返回的用户信息(不包含敏感数据)
const userInfo = {
id: user.id,
username: user.username,
email: user.email,
user_type: user.user_type,
status: user.status
}
res.json({
success: true,
message: '登录成功',
data: {
access_token: token,
token_type: 'Bearer',
expires_in: 86400, // 24小时
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.user_type,
status: user.status
}
}
token,
user: userInfo
})
} catch (error) {
console.error('登录失败:', error)
console.error('用户登录失败:', error)
res.status(500).json({
success: false,
message: '登录失败,请稍后试'
message: '登录失败,请稍后试'
})
}
})
/**
* @swagger
* /api/auth/me:
* get:
* summary: 获取当前用户信息
* tags: [认证管理]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* id:
* type: integer
* username:
* type: string
* email:
* type: string
* user_type:
* type: string
* status:
* type: string
* createdAt:
* type: string
* format: date-time
* updatedAt:
* type: string
* format: date-time
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 获取当前用户信息
router.get('/me', authenticateToken, async (req, res) => {
router.get('/me', authenticateJWT, async (req, res) => {
try {
const user = await ApiUser.findByPk(req.user.id)
const userId = req.user.id
// 根据ID查找用户
const user = await ApiUser.findByPk(userId, {
attributes: {
exclude: ['password_hash'] // 排除密码哈希等敏感信息
}
})
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
res.json({
success: true,
data: {
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.user_type,
status: user.status
}
}
data: user
})
} catch (error) {
console.error('获取用户信息失败:', error)
@@ -136,37 +244,49 @@ router.get('/me', authenticateToken, async (req, res) => {
}
})
/**
* @swagger
* /api/auth/logout:
* post:
* summary: 用户登出
* tags: [认证管理]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 登出成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 用户登出
router.post('/logout', authenticateToken, (req, res) => {
// 在实际项目中可以将token加入黑名单
res.json({
success: true,
message: '登出成功'
})
})
// JWT token验证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (!token) {
return res.status(401).json({
router.post('/logout', authenticateJWT, async (req, res) => {
try {
// 注意JWT是无状态的服务器端无法直接使token失效
// 登出操作主要由客户端完成如删除本地存储的token
// 这里只返回成功信息
res.json({
success: true,
message: '登出成功'
})
} catch (error) {
console.error('用户登出失败:', error)
res.status(500).json({
success: false,
message: '访问令牌缺失'
message: '登出失败,请稍后再试'
})
}
jwt.verify(token, process.env.JWT_SECRET || 'niumall-secret-key', (err, user) => {
if (err) {
return res.status(403).json({
success: false,
message: '访问令牌无效或已过期'
})
}
req.user = user
next()
})
}
})
module.exports = router

View File

@@ -2,172 +2,318 @@ const express = require('express')
const Joi = require('joi')
const router = express.Router()
// 模拟订单数据
let orders = [
{
id: 1,
orderNo: 'ORD20240101001',
buyerId: 2,
buyerName: '山东养殖场',
supplierId: 3,
supplierName: '河北供应商',
traderId: 1,
traderName: '北京贸易公司',
cattleBreed: '西门塔尔',
cattleCount: 50,
expectedWeight: 25000,
actualWeight: 24800,
unitPrice: 28.5,
totalAmount: 712500,
paidAmount: 200000,
remainingAmount: 512500,
status: 'shipping',
deliveryAddress: '山东省济南市某养殖场',
expectedDeliveryDate: '2024-01-15',
actualDeliveryDate: null,
notes: '优质西门塔尔牛',
createdAt: '2024-01-10T00:00:00Z',
updatedAt: '2024-01-12T00:00:00Z'
},
{
id: 2,
orderNo: 'ORD20240101002',
buyerId: 2,
buyerName: '山东养殖场',
supplierId: 4,
supplierName: '内蒙古牧场',
traderId: 1,
traderName: '北京贸易公司',
cattleBreed: '安格斯',
cattleCount: 30,
expectedWeight: 18000,
actualWeight: 18200,
unitPrice: 30.0,
totalAmount: 540000,
paidAmount: 540000,
remainingAmount: 0,
status: 'completed',
deliveryAddress: '山东省济南市某养殖场',
expectedDeliveryDate: '2024-01-08',
actualDeliveryDate: '2024-01-08',
notes: '',
createdAt: '2024-01-05T00:00:00Z',
updatedAt: '2024-01-08T00:00:00Z'
}
]
// 引入数据库模型
const { Order } = require('../models')
const sequelize = require('sequelize')
/**
* @swagger
* components:
* schemas:
* Order:
* type: object
* properties:
* id:
* type: integer
* description: 订单ID
* buyer_id:
* type: integer
* description: 买方ID
* buyer_name:
* type: string
* description: 买方名称
* supplier_id:
* type: integer
* description: 供应商ID
* supplier_name:
* type: string
* description: 供应商名称
* cow_breed:
* type: string
* description: 牛品种
* quantity:
* type: integer
* description: 数量
* weight:
* type: number
* format: float
* description: 总重量(kg)
* unit_price:
* type: number
* format: float
* description: 单价(元/kg)
* total_amount:
* type: number
* format: float
* description: 总金额(元)
* status:
* type: string
* enum: [pending, confirmed, delivered, completed, cancelled]
* description: 订单状态
* delivery_address:
* type: string
* description: 配送地址
* delivery_date:
* type: string
* format: date
* description: 配送日期
* payment_status:
* type: string
* enum: [unpaid, paid, partially_paid]
* description: 支付状态
* remark:
* type: string
* description: 备注
* createdAt:
* type: string
* format: date-time
* description: 创建时间
* updatedAt:
* type: string
* format: date-time
* description: 更新时间
* CreateOrderRequest:
* type: object
* required:
* - buyer_id
* - supplier_id
* - cow_breed
* - quantity
* - weight
* - unit_price
* - total_amount
* - delivery_address
* - delivery_date
* properties:
* buyer_id:
* type: integer
* description: 买方ID
* buyer_name:
* type: string
* description: 买方名称
* supplier_id:
* type: integer
* description: 供应商ID
* supplier_name:
* type: string
* description: 供应商名称
* cow_breed:
* type: string
* description: 牛品种
* quantity:
* type: integer
* description: 数量
* weight:
* type: number
* format: float
* description: 总重量(kg)
* unit_price:
* type: number
* format: float
* description: 单价(元/kg)
* total_amount:
* type: number
* format: float
* description: 总金额(元)
* delivery_address:
* type: string
* description: 配送地址
* delivery_date:
* type: string
* format: date
* description: 配送日期
* remark:
* type: string
* description: 备注
* UpdateOrderRequest:
* type: object
* properties:
* status:
* type: string
* enum: [pending, confirmed, delivered, completed, cancelled]
* description: 订单状态
* payment_status:
* type: string
* enum: [unpaid, paid, partially_paid]
* description: 支付状态
* delivery_address:
* type: string
* description: 配送地址
* delivery_date:
* type: string
* format: date
* description: 配送日期
* remark:
* type: string
* description: 备注
*/
// 订单状态枚举
const ORDER_STATUS = {
PENDING: 'pending',
CONFIRMED: 'confirmed',
PREPARING: 'preparing',
SHIPPING: 'shipping',
DELIVERED: 'delivered',
ACCEPTED: 'accepted',
COMPLETED: 'completed',
CANCELLED: 'cancelled',
REFUNDED: 'refunded'
PENDING: 'pending', // 待确认
CONFIRMED: 'confirmed', // 已确认
DELIVERED: 'delivered', // 已配送
COMPLETED: 'completed', // 已完成
CANCELLED: 'cancelled' // 已取消
}
// 支付状态枚举
const PAYMENT_STATUS = {
UNPAID: 'unpaid', // 未支付
PAID: 'paid', // 已支付
PARTIALLY_PAID: 'partially_paid' // 部分支付
}
// 验证模式
const createOrderSchema = Joi.object({
buyerId: Joi.number().integer().positive().required(),
supplierId: Joi.number().integer().positive().required(),
traderId: Joi.number().integer().positive(),
cattleBreed: Joi.string().min(1).max(50).required(),
cattleCount: Joi.number().integer().positive().required(),
expectedWeight: Joi.number().positive().required(),
unitPrice: Joi.number().positive().required(),
deliveryAddress: Joi.string().min(1).max(200).required(),
expectedDeliveryDate: Joi.date().iso().required(),
notes: Joi.string().max(500).allow('')
buyer_id: Joi.number().integer().required(),
buyer_name: Joi.string().allow(''),
supplier_id: Joi.number().integer().required(),
supplier_name: Joi.string().allow(''),
cow_breed: Joi.string().required(),
quantity: Joi.number().integer().min(1).required(),
weight: Joi.number().positive().required(),
unit_price: Joi.number().positive().required(),
total_amount: Joi.number().positive().required(),
delivery_address: Joi.string().required(),
delivery_date: Joi.date().required(),
remark: Joi.string().allow('')
})
const updateOrderSchema = Joi.object({
cattleBreed: Joi.string().min(1).max(50),
cattleCount: Joi.number().integer().positive(),
expectedWeight: Joi.number().positive(),
actualWeight: Joi.number().positive(),
unitPrice: Joi.number().positive(),
deliveryAddress: Joi.string().min(1).max(200),
expectedDeliveryDate: Joi.date().iso(),
actualDeliveryDate: Joi.date().iso(),
notes: Joi.string().max(500).allow(''),
status: Joi.string().valid(...Object.values(ORDER_STATUS))
status: Joi.string().valid(...Object.values(ORDER_STATUS)),
payment_status: Joi.string().valid(...Object.values(PAYMENT_STATUS)),
delivery_address: Joi.string(),
delivery_date: Joi.date(),
remark: Joi.string().allow('')
})
/**
* @swagger
* /api/orders:
* get:
* summary: 获取订单列表
* tags: [订单管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: 页码默认为1
* - in: query
* name: pageSize
* schema:
* type: integer
* description: 每页条数默认为20
* - in: query
* name: keyword
* schema:
* type: string
* description: 关键词搜索(买方名称、供应商名称、牛品种)
* - in: query
* name: status
* schema:
* type: string
* description: 订单状态筛选
* - in: query
* name: payment_status
* schema:
* type: string
* description: 支付状态筛选
* - in: query
* name: start_date
* schema:
* type: string
* format: date
* description: 开始日期筛选
* - in: query
* name: end_date
* schema:
* type: string
* format: date
* description: 结束日期筛选
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* items:
* type: array
* items:
* $ref: '#/components/schemas/Order'
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* totalPages:
* type: integer
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 获取订单列表
router.get('/', (req, res) => {
router.get('/', async (req, res) => {
try {
const {
page = 1,
pageSize = 20,
orderNo,
buyerId,
supplierId,
keyword,
status,
startDate,
endDate
payment_status,
start_date,
end_date
} = req.query
let filteredOrders = [...orders]
// 订单号搜索
if (orderNo) {
filteredOrders = filteredOrders.filter(order =>
order.orderNo.includes(orderNo)
)
// 构建查询条件
const where = {}
if (keyword) {
where[sequelize.Op.or] = [
{ buyer_name: { [sequelize.Op.like]: `%${keyword}%` } },
{ supplier_name: { [sequelize.Op.like]: `%${keyword}%` } },
{ cow_breed: { [sequelize.Op.like]: `%${keyword}%` } }
]
}
if (status) where.status = status
if (payment_status) where.payment_status = payment_status
if (start_date || end_date) {
where.createdAt = {}
if (start_date) where.createdAt[sequelize.Op.gte] = new Date(start_date)
if (end_date) where.createdAt[sequelize.Op.lte] = new Date(end_date)
}
// 买方筛选
if (buyerId) {
filteredOrders = filteredOrders.filter(order =>
order.buyerId === parseInt(buyerId)
)
}
// 供应商筛选
if (supplierId) {
filteredOrders = filteredOrders.filter(order =>
order.supplierId === parseInt(supplierId)
)
}
// 状态筛选
if (status) {
filteredOrders = filteredOrders.filter(order => order.status === status)
}
// 日期范围筛选
if (startDate) {
filteredOrders = filteredOrders.filter(order =>
new Date(order.createdAt) >= new Date(startDate)
)
}
if (endDate) {
filteredOrders = filteredOrders.filter(order =>
new Date(order.createdAt) <= new Date(endDate)
)
}
// 分页
const total = filteredOrders.length
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + parseInt(pageSize)
const paginatedOrders = filteredOrders.slice(startIndex, endIndex)
// 分页查询
const result = await Order.findAndCountAll({
where,
limit: parseInt(pageSize),
offset: (parseInt(page) - 1) * parseInt(pageSize),
order: [['createdAt', 'DESC']]
})
res.json({
success: true,
data: {
items: paginatedOrders,
total: total,
items: result.rows,
total: result.count,
page: parseInt(page),
pageSize: parseInt(pageSize),
totalPages: Math.ceil(total / pageSize)
totalPages: Math.ceil(result.count / parseInt(pageSize))
}
})
} catch (error) {
console.error('获取订单列表失败:', error)
res.status(500).json({
success: false,
message: '获取订单列表失败'
@@ -175,11 +321,46 @@ router.get('/', (req, res) => {
}
})
/**
* @swagger
* /api/orders/{id}:
* get:
* summary: 获取订单详情
* tags: [订单管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 订单ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Order'
* 401:
* description: 未授权
* 404:
* description: 订单不存在
* 500:
* description: 服务器内部错误
*/
// 获取订单详情
router.get('/:id', (req, res) => {
router.get('/:id', async (req, res) => {
try {
const { id } = req.params
const order = orders.find(o => o.id === parseInt(id))
const order = await Order.findByPk(id)
if (!order) {
return res.status(404).json({
@@ -193,6 +374,7 @@ router.get('/:id', (req, res) => {
data: order
})
} catch (error) {
console.error('获取订单详情失败:', error)
res.status(500).json({
success: false,
message: '获取订单详情失败'
@@ -200,10 +382,44 @@ router.get('/:id', (req, res) => {
}
})
// 创建订单
router.post('/', (req, res) => {
/**
* @swagger
* /api/orders:
* post:
* summary: 创建新订单
* tags: [订单管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateOrderRequest'
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* $ref: '#/components/schemas/Order'
* 400:
* description: 参数验证失败
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 创建新订单
router.post('/', async (req, res) => {
try {
// 参数验证
const { error, value } = createOrderSchema.validate(req.body)
if (error) {
return res.status(400).json({
@@ -213,60 +429,20 @@ router.post('/', (req, res) => {
})
}
const {
buyerId,
supplierId,
traderId,
cattleBreed,
cattleCount,
expectedWeight,
unitPrice,
deliveryAddress,
expectedDeliveryDate,
notes
} = value
// 生成订单号
const orderNo = `ORD${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(orders.length + 1).padStart(3, '0')}`
// 计算总金额
const totalAmount = expectedWeight * unitPrice
// 创建新订单
const newOrder = {
id: Math.max(...orders.map(o => o.id)) + 1,
orderNo,
buyerId,
buyerName: '买方名称', // 实际项目中需要从数据库获取
supplierId,
supplierName: '供应商名称', // 实际项目中需要从数据库获取
traderId: traderId || null,
traderName: traderId ? '贸易商名称' : null,
cattleBreed,
cattleCount,
expectedWeight,
actualWeight: null,
unitPrice,
totalAmount,
paidAmount: 0,
remainingAmount: totalAmount,
// 创建订单,默认状态为待确认
const order = await Order.create({
...value,
status: ORDER_STATUS.PENDING,
deliveryAddress,
expectedDeliveryDate,
actualDeliveryDate: null,
notes: notes || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
orders.push(newOrder)
payment_status: PAYMENT_STATUS.UNPAID
})
res.status(201).json({
success: true,
message: '订单创建成功',
data: newOrder
data: order
})
} catch (error) {
console.error('创建订单失败:', error)
res.status(500).json({
success: false,
message: '创建订单失败'
@@ -274,20 +450,55 @@ router.post('/', (req, res) => {
}
})
// 更新订单
router.put('/:id', (req, res) => {
/**
* @swagger
* /api/orders/{id}:
* put:
* summary: 更新订单信息
* tags: [订单管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 订单ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateOrderRequest'
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* $ref: '#/components/schemas/Order'
* 400:
* description: 参数验证失败
* 401:
* description: 未授权
* 404:
* description: 订单不存在
* 500:
* description: 服务器内部错误
*/
// 更新订单信息
router.put('/:id', async (req, res) => {
try {
const { id } = req.params
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
// 参数验证
const { error, value } = updateOrderSchema.validate(req.body)
if (error) {
return res.status(400).json({
@@ -297,25 +508,26 @@ router.put('/:id', (req, res) => {
})
}
// 更新订单信息
orders[orderIndex] = {
...orders[orderIndex],
...value,
updatedAt: new Date().toISOString()
// 查找订单
const order = await Order.findByPk(id)
if (!order) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
// 如果更新了实际重量,重新计算总金额
if (value.actualWeight && orders[orderIndex].unitPrice) {
orders[orderIndex].totalAmount = value.actualWeight * orders[orderIndex].unitPrice
orders[orderIndex].remainingAmount = orders[orderIndex].totalAmount - orders[orderIndex].paidAmount
}
// 更新订单信息
await order.update(value)
res.json({
success: true,
message: '订单更新成功',
data: orders[orderIndex]
data: order
})
} catch (error) {
console.error('更新订单失败:', error)
res.status(500).json({
success: false,
message: '更新订单失败'
@@ -323,26 +535,62 @@ router.put('/:id', (req, res) => {
}
})
/**
* @swagger
* /api/orders/{id}:
* delete:
* summary: 删除订单
* tags: [订单管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 订单ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 404:
* description: 订单不存在
* 500:
* description: 服务器内部错误
*/
// 删除订单
router.delete('/:id', (req, res) => {
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
const order = await Order.findByPk(id)
if (!order) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
orders.splice(orderIndex, 1)
await order.destroy()
res.json({
success: true,
message: '订单删除成功'
})
} catch (error) {
console.error('删除订单失败:', error)
res.status(500).json({
success: false,
message: '删除订单失败'
@@ -350,190 +598,4 @@ router.delete('/:id', (req, res) => {
}
})
// 确认订单
router.put('/:id/confirm', (req, res) => {
try {
const { id } = req.params
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
if (orders[orderIndex].status !== ORDER_STATUS.PENDING) {
return res.status(400).json({
success: false,
message: '只有待确认的订单才能确认'
})
}
orders[orderIndex].status = ORDER_STATUS.CONFIRMED
orders[orderIndex].updatedAt = new Date().toISOString()
res.json({
success: true,
message: '订单确认成功',
data: orders[orderIndex]
})
} catch (error) {
res.status(500).json({
success: false,
message: '确认订单失败'
})
}
})
// 取消订单
router.put('/:id/cancel', (req, res) => {
try {
const { id } = req.params
const { reason } = req.body
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
orders[orderIndex].status = ORDER_STATUS.CANCELLED
orders[orderIndex].notes = reason ? `取消原因: ${reason}` : '订单已取消'
orders[orderIndex].updatedAt = new Date().toISOString()
res.json({
success: true,
message: '订单取消成功',
data: orders[orderIndex]
})
} catch (error) {
res.status(500).json({
success: false,
message: '取消订单失败'
})
}
})
// 订单验收
router.put('/:id/accept', (req, res) => {
try {
const { id } = req.params
const { actualWeight, notes } = req.body
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
if (!actualWeight || actualWeight <= 0) {
return res.status(400).json({
success: false,
message: '请提供有效的实际重量'
})
}
orders[orderIndex].status = ORDER_STATUS.ACCEPTED
orders[orderIndex].actualWeight = actualWeight
orders[orderIndex].totalAmount = actualWeight * orders[orderIndex].unitPrice
orders[orderIndex].remainingAmount = orders[orderIndex].totalAmount - orders[orderIndex].paidAmount
orders[orderIndex].actualDeliveryDate = new Date().toISOString()
if (notes) {
orders[orderIndex].notes = notes
}
orders[orderIndex].updatedAt = new Date().toISOString()
res.json({
success: true,
message: '订单验收成功',
data: orders[orderIndex]
})
} catch (error) {
res.status(500).json({
success: false,
message: '订单验收失败'
})
}
})
// 完成订单
router.put('/:id/complete', (req, res) => {
try {
const { id } = req.params
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
if (orderIndex === -1) {
return res.status(404).json({
success: false,
message: '订单不存在'
})
}
orders[orderIndex].status = ORDER_STATUS.COMPLETED
orders[orderIndex].paidAmount = orders[orderIndex].totalAmount
orders[orderIndex].remainingAmount = 0
orders[orderIndex].updatedAt = new Date().toISOString()
res.json({
success: true,
message: '订单完成成功',
data: orders[orderIndex]
})
} catch (error) {
res.status(500).json({
success: false,
message: '完成订单失败'
})
}
})
// 获取订单统计数据
router.get('/statistics', (req, res) => {
try {
const { startDate, endDate } = req.query
let filteredOrders = [...orders]
if (startDate) {
filteredOrders = filteredOrders.filter(order =>
new Date(order.createdAt) >= new Date(startDate)
)
}
if (endDate) {
filteredOrders = filteredOrders.filter(order =>
new Date(order.createdAt) <= new Date(endDate)
)
}
const statistics = {
totalOrders: filteredOrders.length,
completedOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.COMPLETED).length,
pendingOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.PENDING).length,
cancelledOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.CANCELLED).length,
totalAmount: filteredOrders.reduce((sum, order) => sum + order.totalAmount, 0),
totalCattle: filteredOrders.reduce((sum, order) => sum + order.cattleCount, 0),
statusDistribution: Object.values(ORDER_STATUS).reduce((acc, status) => {
acc[status] = filteredOrders.filter(o => o.status === status).length
return acc
}, {})
}
res.json({
success: true,
data: statistics
})
} catch (error) {
res.status(500).json({
success: false,
message: '获取订单统计失败'
})
}
})
module.exports = router

View File

@@ -7,24 +7,176 @@ const router = express.Router()
const { ApiUser } = require('../models')
const sequelize = require('sequelize')
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* properties:
* id:
* type: integer
* description: 用户ID
* username:
* type: string
* description: 用户名
* email:
* type: string
* format: email
* description: 邮箱
* phone:
* type: string
* description: 手机号
* user_type:
* type: string
* enum: [admin, buyer, supplier, trader]
* description: 用户类型
* status:
* type: string
* enum: [active, inactive, suspended]
* description: 用户状态
* createdAt:
* type: string
* format: date-time
* description: 创建时间
* updatedAt:
* type: string
* format: date-time
* description: 更新时间
* CreateUserRequest:
* type: object
* required:
* - username
* - email
* - password
* - user_type
* properties:
* username:
* type: string
* description: 用户名
* email:
* type: string
* format: email
* description: 邮箱
* phone:
* type: string
* description: 手机号
* password:
* type: string
* description: 密码
* user_type:
* type: string
* enum: [admin, buyer, supplier, trader]
* description: 用户类型
* status:
* type: string
* enum: [active, inactive, suspended]
* description: 用户状态
* UpdateUserRequest:
* type: object
* properties:
* username:
* type: string
* description: 用户名
* email:
* type: string
* format: email
* description: 邮箱
* phone:
* type: string
* description: 手机号
* user_type:
* type: string
* enum: [admin, buyer, supplier, trader]
* description: 用户类型
* status:
* type: string
* enum: [active, inactive, suspended]
* description: 用户状态
*/
// 验证模式
const createUserSchema = Joi.object({
username: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
password: Joi.string().min(6).max(100).required(),
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin').required(),
status: Joi.string().valid('active', 'inactive', 'locked').default('active')
user_type: Joi.string().valid('admin', 'buyer', 'supplier', 'trader').required(),
status: Joi.string().valid('active', 'inactive', 'suspended').default('active')
})
const updateUserSchema = Joi.object({
username: Joi.string().min(2).max(50),
email: Joi.string().email(),
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin'),
status: Joi.string().valid('active', 'inactive', 'locked')
user_type: Joi.string().valid('admin', 'buyer', 'supplier', 'trader'),
status: Joi.string().valid('active', 'inactive', 'suspended')
})
/**
* @swagger
* /api/users:
* get:
* summary: 获取用户列表
* tags: [用户管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: 页码默认为1
* - in: query
* name: pageSize
* schema:
* type: integer
* description: 每页条数默认为20
* - in: query
* name: keyword
* schema:
* type: string
* description: 关键词搜索(用户名、邮箱、手机号)
* - in: query
* name: user_type
* schema:
* type: string
* description: 用户类型筛选
* - in: query
* name: status
* schema:
* type: string
* description: 用户状态筛选
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* items:
* type: array
* items:
* $ref: '#/components/schemas/User'
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* totalPages:
* type: integer
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 获取用户列表
router.get('/', async (req, res) => {
try {
@@ -69,10 +221,45 @@ router.get('/', async (req, res) => {
}
})
/**
* @swagger
* /api/users/{id}:
* get:
* summary: 获取用户详情
* tags: [用户管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 用户ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/User'
* 401:
* description: 未授权
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
// 获取用户详情
router.get('/:id', async (req, res) => {
try {
const { id } = req.params
const user = await ApiUser.findByPk(id)
if (!user) {
@@ -95,10 +282,44 @@ router.get('/:id', async (req, res) => {
}
})
// 创建用户
/**
* @swagger
* /api/users:
* post:
* summary: 创建新用户
* tags: [用户管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateUserRequest'
* responses:
* 201:
* description: 创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* $ref: '#/components/schemas/User'
* 400:
* description: 参数验证失败
* 401:
* description: 未授权
* 500:
* description: 服务器内部错误
*/
// 创建新用户
router.post('/', async (req, res) => {
try {
// 参数验证
const { error, value } = createUserSchema.validate(req.body)
if (error) {
return res.status(400).json({
@@ -110,13 +331,12 @@ router.post('/', async (req, res) => {
const { username, email, phone, password, user_type, status } = value
// 检查用户名是否已存在
// 检查用户名、邮箱是否已存在
const existingUser = await ApiUser.findOne({
where: {
[sequelize.Op.or]: [
{ username: username },
{ email: email },
{ phone: phone }
{ username },
{ email }
]
}
})
@@ -124,28 +344,31 @@ router.post('/', async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名邮箱或手机号已存在'
message: '用户名邮箱已被使用'
})
}
// 密码加密
const saltRounds = 10
const password_hash = await bcrypt.hash(password, saltRounds)
const hashedPassword = await bcrypt.hash(password, 10)
// 创建用户
const newUser = await ApiUser.create({
// 创建用户
const user = await ApiUser.create({
username,
email,
phone: phone || '',
password_hash,
phone,
password_hash: hashedPassword,
user_type,
status,
status
})
// 移除密码哈希,避免返回敏感信息
const userData = user.toJSON()
delete userData.password_hash
res.status(201).json({
success: true,
message: '用户创建成功',
data: newUser
data: userData
})
} catch (error) {
console.error('创建用户失败:', error)
@@ -156,20 +379,55 @@ router.post('/', async (req, res) => {
}
})
// 更新用户
/**
* @swagger
* /api/users/{id}:
* put:
* summary: 更新用户信息
* tags: [用户管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 用户ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateUserRequest'
* responses:
* 200:
* description: 更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* $ref: '#/components/schemas/User'
* 400:
* description: 参数验证失败
* 401:
* description: 未授权
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
// 更新用户信息
router.put('/:id', async (req, res) => {
try {
const { id } = req.params
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
// 参数验证
const { error, value } = updateUserSchema.validate(req.body)
if (error) {
return res.status(400).json({
@@ -179,6 +437,16 @@ router.put('/:id', async (req, res) => {
})
}
// 查找用户
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
// 更新用户信息
await user.update(value)
@@ -196,10 +464,45 @@ router.put('/:id', async (req, res) => {
}
})
/**
* @swagger
* /api/users/{id}:
* delete:
* summary: 删除用户
* tags: [用户管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 用户ID
* responses:
* 200:
* description: 删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 404:
* description: 用户不存在
* 500:
* description: 服务器内部错误
*/
// 删除用户
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params
const user = await ApiUser.findByPk(id)
if (!user) {
@@ -209,6 +512,8 @@ router.delete('/:id', async (req, res) => {
})
}
// 软删除或永久删除
// 如果需要软删除可以改为更新status为'inactive'
await user.destroy()
res.json({
@@ -224,113 +529,4 @@ router.delete('/:id', async (req, res) => {
}
})
// 批量删除用户
router.delete('/batch', async (req, res) => {
try {
const { ids } = req.body
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
success: false,
message: '请提供有效的用户ID列表'
})
}
await ApiUser.destroy({
where: {
id: ids
}
})
res.json({
success: true,
message: `成功删除 ${ids.length} 个用户`
})
} catch (error) {
console.error('批量删除用户失败:', error)
res.status(500).json({
success: false,
message: '批量删除用户失败'
})
}
})
// 重置用户密码
router.put('/:id/password', async (req, res) => {
try {
const { id } = req.params
const { password } = req.body
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
if (!password || password.length < 6) {
return res.status(400).json({
success: false,
message: '密码长度不能少于6位'
})
}
// 密码加密
const saltRounds = 10
const password_hash = await bcrypt.hash(password, saltRounds)
// 更新密码
await user.update({ password_hash })
res.json({
success: true,
message: '密码重置成功'
})
} catch (error) {
console.error('重置密码失败:', error)
res.status(500).json({
success: false,
message: '重置密码失败'
})
}
})
// 更新用户状态
router.put('/:id/status', async (req, res) => {
try {
const { id } = req.params
const { status } = req.body
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
if (!['active', 'inactive', 'locked'].includes(status)) {
return res.status(400).json({
success: false,
message: '无效的用户状态'
})
}
await user.update({ status })
res.json({
success: true,
message: '用户状态更新成功',
data: user
})
} catch (error) {
console.error('更新用户状态失败:', error)
res.status(500).json({
success: false,
message: '更新用户状态失败'
})
}
})
module.exports = router