更新技术实施方案和PRD文档版本历史
This commit is contained in:
50
backend/.dockerignore
Normal file
50
backend/.dockerignore
Normal 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
23
backend/.env.docker
Normal 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
26
backend/Dockerfile
Normal 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
189
backend/README_DOCKER.md
Normal 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/)
|
||||
@@ -4,8 +4,6 @@ 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')
|
||||
require('dotenv').config()
|
||||
|
||||
// 数据库连接
|
||||
@@ -21,74 +19,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({
|
||||
@@ -120,6 +51,9 @@ app.use('/api/transport', require('./routes/transport'))
|
||||
app.use('/api/finance', require('./routes/finance'))
|
||||
app.use('/api/quality', require('./routes/quality'))
|
||||
|
||||
// API文档路由
|
||||
app.use('/docs', express.static('public/api-docs.html'))
|
||||
|
||||
// 404 处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
@@ -161,7 +95,6 @@ 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`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
|
||||
248
backend/config/swagger.js
Normal file
248
backend/config/swagger.js
Normal 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
|
||||
}
|
||||
65
backend/docker-compose.yml
Normal file
65
backend/docker-compose.yml
Normal 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
|
||||
@@ -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
132
backend/models/order.js
Normal 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;
|
||||
};
|
||||
4128
backend/package-lock.json
generated
4128
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,8 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
562
backend/public/api-docs.html
Normal file
562
backend/public/api-docs.html
Normal file
@@ -0,0 +1,562 @@
|
||||
<!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>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#10B981',
|
||||
accent: '#8B5CF6',
|
||||
danger: '#EF4444',
|
||||
warning: '#F59E0B',
|
||||
info: '#60A5FA',
|
||||
dark: '#1E293B',
|
||||
light: '#F1F5F9'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.text-shadow {
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.transition-all-300 {
|
||||
transition: all 300ms ease-in-out;
|
||||
}
|
||||
.shadow-card {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
</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">
|
||||
<i class="fa fa-server text-xl"></i>
|
||||
<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">
|
||||
<i class="fa fa-navicon mr-2"></i> 接口分类
|
||||
</h2>
|
||||
<ul class="space-y-1">
|
||||
<li>
|
||||
<a href="#auth" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-lock mr-2"></i> 认证管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#users" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-users mr-2"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#orders" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-shopping-cart mr-2"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#finance" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-money mr-2"></i> 财务管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#suppliers" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-building-o mr-2"></i> 供应商管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#transport" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-truck mr-2"></i> 运输管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#quality" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-check-circle mr-2"></i> 质量管理
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#system" class="block px-4 py-2 rounded-lg transition-all-300 hover:bg-blue-50 hover:text-primary">
|
||||
<i class="fa fa-cog mr-2"></i> 系统接口
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-bold text-dark mb-4 flex items-center">
|
||||
<i class="fa fa-book mr-2"></i> 文档说明
|
||||
</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">
|
||||
<i class="fa fa-lock mr-2"></i> 认证管理
|
||||
</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">
|
||||
<i class="fa fa-users mr-2"></i> 用户管理
|
||||
</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">
|
||||
<i class="fa fa-shopping-cart mr-2"></i> 订单管理
|
||||
</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">
|
||||
<i class="fa fa-money mr-2"></i> 财务管理
|
||||
</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">
|
||||
<i class="fa fa-cog mr-2"></i> 系统接口
|
||||
</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 文档 © 2025</p>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<a href="#" class="hover:text-white transition-all-300">
|
||||
<i class="fa fa-question-circle"></i> 帮助中心
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-all-300">
|
||||
<i class="fa fa-envelope"></i> 联系我们
|
||||
</a>
|
||||
<a href="#" class="hover:text-white transition-all-300">
|
||||
<i class="fa fa-book"></i> 完整文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// 平滑滚动
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 导航高亮
|
||||
window.addEventListener('scroll', () => {
|
||||
const sections = document.querySelectorAll('section');
|
||||
let current = '';
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionTop = section.offsetTop;
|
||||
const sectionHeight = section.clientHeight;
|
||||
if (pageYOffset >= (sectionTop - 100)) {
|
||||
current = section.getAttribute('id');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('nav a').forEach(link => {
|
||||
link.classList.remove('bg-blue-50', 'text-primary');
|
||||
if (link.getAttribute('href') === `#${current}`) {
|
||||
link.classList.add('bg-blue-50', 'text-primary');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,59 +2,8 @@ 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 ORDER_STATUS = {
|
||||
@@ -72,8 +21,11 @@ const ORDER_STATUS = {
|
||||
// 验证模式
|
||||
const createOrderSchema = Joi.object({
|
||||
buyerId: Joi.number().integer().positive().required(),
|
||||
buyerName: Joi.string().min(1).max(100).required(),
|
||||
supplierId: Joi.number().integer().positive().required(),
|
||||
traderId: Joi.number().integer().positive(),
|
||||
supplierName: Joi.string().min(1).max(100).required(),
|
||||
traderId: Joi.number().integer().positive().allow(null),
|
||||
traderName: Joi.string().min(1).max(100).allow(null, ''),
|
||||
cattleBreed: Joi.string().min(1).max(50).required(),
|
||||
cattleCount: Joi.number().integer().positive().required(),
|
||||
expectedWeight: Joi.number().positive().required(),
|
||||
@@ -84,20 +36,39 @@ const createOrderSchema = Joi.object({
|
||||
})
|
||||
|
||||
const updateOrderSchema = Joi.object({
|
||||
buyerId: Joi.number().integer().positive(),
|
||||
buyerName: Joi.string().min(1).max(100),
|
||||
supplierId: Joi.number().integer().positive(),
|
||||
supplierName: Joi.string().min(1).max(100),
|
||||
traderId: Joi.number().integer().positive().allow(null),
|
||||
traderName: Joi.string().min(1).max(100).allow(null, ''),
|
||||
cattleBreed: Joi.string().min(1).max(50),
|
||||
cattleCount: Joi.number().integer().positive(),
|
||||
expectedWeight: Joi.number().positive(),
|
||||
actualWeight: Joi.number().positive(),
|
||||
actualWeight: Joi.number().positive().allow(null),
|
||||
unitPrice: Joi.number().positive(),
|
||||
totalAmount: Joi.number().positive(),
|
||||
paidAmount: Joi.number().positive(),
|
||||
remainingAmount: Joi.number().positive(),
|
||||
deliveryAddress: Joi.string().min(1).max(200),
|
||||
expectedDeliveryDate: Joi.date().iso(),
|
||||
actualDeliveryDate: Joi.date().iso(),
|
||||
actualDeliveryDate: Joi.date().iso().allow(null),
|
||||
notes: Joi.string().max(500).allow(''),
|
||||
status: Joi.string().valid(...Object.values(ORDER_STATUS))
|
||||
})
|
||||
|
||||
// 生成订单编号
|
||||
const generateOrderNo = () => {
|
||||
const date = new Date()
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const random = Math.floor(100 + Math.random() * 900) // 生成3位随机数
|
||||
return `ORD${year}${month}${day}${random}`
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
router.get('/', (req, res) => {
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
@@ -110,64 +81,63 @@ router.get('/', (req, res) => {
|
||||
endDate
|
||||
} = req.query
|
||||
|
||||
let filteredOrders = [...orders]
|
||||
const where = {}
|
||||
|
||||
// 订单号搜索
|
||||
if (orderNo) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.orderNo.includes(orderNo)
|
||||
)
|
||||
where.orderNo = { [require('sequelize').Op.like]: `%${orderNo}%` }
|
||||
}
|
||||
|
||||
// 买方筛选
|
||||
if (buyerId) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.buyerId === parseInt(buyerId)
|
||||
)
|
||||
where.buyerId = parseInt(buyerId)
|
||||
}
|
||||
|
||||
// 供应商筛选
|
||||
if (supplierId) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.supplierId === parseInt(supplierId)
|
||||
)
|
||||
where.supplierId = parseInt(supplierId)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredOrders = filteredOrders.filter(order => order.status === status)
|
||||
where.status = status
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (startDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) >= new Date(startDate)
|
||||
)
|
||||
where.created_at = {
|
||||
...where.created_at,
|
||||
[require('sequelize').Op.gte]: new Date(startDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) <= new Date(endDate)
|
||||
)
|
||||
where.created_at = {
|
||||
...where.created_at,
|
||||
[require('sequelize').Op.lte]: new Date(endDate)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const total = filteredOrders.length
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + parseInt(pageSize)
|
||||
const paginatedOrders = filteredOrders.slice(startIndex, endIndex)
|
||||
// 查询数据库
|
||||
const { count, rows } = await Order.findAndCountAll({
|
||||
where,
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize)
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedOrders,
|
||||
total: total,
|
||||
items: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单列表失败'
|
||||
@@ -176,10 +146,10 @@ router.get('/', (req, res) => {
|
||||
})
|
||||
|
||||
// 获取订单详情
|
||||
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 +163,7 @@ router.get('/:id', (req, res) => {
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单详情失败'
|
||||
@@ -200,8 +171,8 @@ router.get('/:id', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 创建订单
|
||||
router.post('/', (req, res) => {
|
||||
// 创建新订单
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = createOrderSchema.validate(req.body)
|
||||
@@ -212,61 +183,28 @@ router.post('/', (req, res) => {
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 计算总金额和剩余金额
|
||||
const totalAmount = value.expectedWeight * value.unitPrice
|
||||
const remainingAmount = totalAmount
|
||||
|
||||
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,
|
||||
// 创建订单
|
||||
const order = await Order.create({
|
||||
...value,
|
||||
orderNo: generateOrderNo(),
|
||||
totalAmount,
|
||||
remainingAmount,
|
||||
paidAmount: 0,
|
||||
remainingAmount: totalAmount,
|
||||
status: ORDER_STATUS.PENDING,
|
||||
deliveryAddress,
|
||||
expectedDeliveryDate,
|
||||
actualDeliveryDate: null,
|
||||
notes: notes || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
orders.push(newOrder)
|
||||
status: ORDER_STATUS.PENDING
|
||||
})
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '订单创建成功',
|
||||
data: newOrder
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建订单失败'
|
||||
@@ -275,17 +213,9 @@ router.post('/', (req, res) => {
|
||||
})
|
||||
|
||||
// 更新订单
|
||||
router.put('/:id', (req, res) => {
|
||||
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)
|
||||
@@ -296,26 +226,42 @@ router.put('/:id', (req, res) => {
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 更新订单信息
|
||||
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
|
||||
|
||||
// 如果更新了单价和重量,重新计算总金额
|
||||
if (value.unitPrice && value.expectedWeight) {
|
||||
value.totalAmount = value.expectedWeight * value.unitPrice
|
||||
} else if (value.unitPrice) {
|
||||
value.totalAmount = order.expectedWeight * value.unitPrice
|
||||
} else if (value.expectedWeight) {
|
||||
value.totalAmount = value.expectedWeight * order.unitPrice
|
||||
}
|
||||
|
||||
// 如果更新了总金额或已支付金额,重新计算剩余金额
|
||||
if (value.totalAmount || value.paidAmount) {
|
||||
const totalAmount = value.totalAmount || order.totalAmount
|
||||
const paidAmount = value.paidAmount || order.paidAmount
|
||||
value.remainingAmount = totalAmount - 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: '更新订单失败'
|
||||
@@ -324,25 +270,28 @@ router.put('/:id', (req, res) => {
|
||||
})
|
||||
|
||||
// 删除订单
|
||||
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,188 +299,115 @@ router.delete('/:id', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 确认订单
|
||||
router.put('/:id/confirm', (req, res) => {
|
||||
// 更新订单状态
|
||||
router.patch('/:id/status', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
const { status } = req.body
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (orders[orderIndex].status !== ORDER_STATUS.PENDING) {
|
||||
// 验证状态
|
||||
if (!status || !Object.values(ORDER_STATUS).includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只有待确认的订单才能确认'
|
||||
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) {
|
||||
// 查找订单
|
||||
const order = await Order.findByPk(id)
|
||||
if (!order) {
|
||||
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()
|
||||
|
||||
// 特殊状态处理
|
||||
if (status === ORDER_STATUS.COMPLETED && !order.actualDeliveryDate) {
|
||||
order.actualDeliveryDate = new Date()
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
order.status = status
|
||||
await order.save()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单取消成功',
|
||||
data: orders[orderIndex]
|
||||
message: '订单状态更新成功',
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新订单状态失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '取消订单失败'
|
||||
message: '更新订单状态失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 订单验收
|
||||
router.put('/:id/accept', (req, res) => {
|
||||
// 订单支付
|
||||
router.post('/:id/pay', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { actualWeight, notes } = req.body
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
const { amount } = req.body
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!actualWeight || actualWeight <= 0) {
|
||||
// 验证金额
|
||||
if (!amount || amount <= 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的实际重量'
|
||||
message: '支付金额必须大于0'
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
// 查找订单
|
||||
const order = await Order.findByPk(id)
|
||||
if (!order) {
|
||||
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 (order.status === ORDER_STATUS.CANCELLED || order.status === ORDER_STATUS.REFUNDED) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '订单已取消或已退款,无法支付'
|
||||
})
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) <= new Date(endDate)
|
||||
)
|
||||
|
||||
// 检查支付金额是否超过剩余金额
|
||||
if (amount > order.remainingAmount) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '支付金额不能超过剩余金额'
|
||||
})
|
||||
}
|
||||
|
||||
// 更新支付信息
|
||||
order.paidAmount += amount
|
||||
order.remainingAmount -= amount
|
||||
|
||||
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
|
||||
}, {})
|
||||
// 如果已全额支付,更新订单状态
|
||||
if (order.remainingAmount === 0) {
|
||||
order.status = ORDER_STATUS.COMPLETED
|
||||
if (!order.actualDeliveryDate) {
|
||||
order.actualDeliveryDate = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
await order.save()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statistics
|
||||
message: '支付成功',
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('订单支付失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单统计失败'
|
||||
message: '订单支付失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user