Merge remote-tracking branch 'origin/main'
# Conflicts: # backend/.env.example # backend/package-lock.json # backend/package.json
This commit is contained in:
23
backend/.env
Normal file
23
backend/.env
Normal file
@@ -0,0 +1,23 @@
|
||||
# 数据库配置
|
||||
DB_HOST=129.211.213.226
|
||||
DB_PORT=9527
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=aiotAiot123!
|
||||
DB_NAME=jiebandata
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=niumall_jwt_secret_key_2024
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# 应用配置
|
||||
NODE_ENV=development
|
||||
PORT=3002
|
||||
API_PREFIX=/api
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
@@ -1,30 +1,104 @@
|
||||
# Backend 后端服务
|
||||
# Backend - 活牛采购智能数字化系统后端服务
|
||||
|
||||
## 技术栈
|
||||
- Node.js/Express/Nest.js
|
||||
- 数据库:MySQL/MongoDB/Redis
|
||||
- 消息队列:RabbitMQ/Kafka
|
||||
- 缓存:Redis
|
||||
- 文件存储:MinIO/阿里云OSS
|
||||
## 📋 项目概述
|
||||
|
||||
活牛采购智能数字化系统后端服务采用Node.js + Express框架构建,为前端应用、管理后台和小程序提供统一的API接口服务。系统采用微服务架构设计,支持高并发、高可用的业务处理。
|
||||
|
||||
**核心特性:**
|
||||
- 🛠️ **微服务架构**:模块化服务设计,易于维护和扩展
|
||||
- 🔐 **统一认证**:JWT + RBAC权限控制系统
|
||||
- 📊 **实时数据**:WebSocket实时数据推送
|
||||
- 📹 **文件管理**:支持大文件上传和视频处理
|
||||
- 💰 **支付集成**:支持多种支付方式
|
||||
- 📈 **监控日志**:完善的日志和监控体系
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类别 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| **运行时** | Node.js | ^18.17.0 | 服务器运行环境 |
|
||||
| **Web框架** | Express.js | ^4.18.0 | 轻量级Web框架 |
|
||||
| **数据库ORM** | Sequelize | ^6.32.0 | 关系型数据库ORM |
|
||||
| **数据库** | MySQL | ^8.0 | 主数据库 |
|
||||
| **缓存** | Redis | ^7.0 | 内存缓存和会话存储 |
|
||||
| **认证** | jsonwebtoken | ^9.0.0 | JWT认证 |
|
||||
| **参数验证** | joi | ^17.9.0 | 请求参数验证 |
|
||||
| **文件上传** | multer | ^1.4.5 | 文件上传中间件 |
|
||||
| **日志** | winston | ^3.10.0 | 日志管理 |
|
||||
| **实时通信** | Socket.io | ^4.7.0 | WebSocket实时通信 |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── services/ # 服务层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── config/ # 配置文件
|
||||
├── tests/ # 测试文件
|
||||
├── package.json
|
||||
└── README.md
|
||||
├── src/ # 源代码目录
|
||||
│ ├── app.js # 应用入口文件
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── services/ # 服务层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── routes/ # 路由定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── validators/ # 参数验证器
|
||||
│ ├── jobs/ # 后台任务
|
||||
│ └── database/ # 数据库相关
|
||||
├── tests/ # 测试文件
|
||||
├── docs/ # 文档目录
|
||||
├── uploads/ # 上传文件目录
|
||||
├── logs/ # 日志目录
|
||||
└── package.json # 项目依赖
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
1. 使用ES6+语法
|
||||
2. 遵循RESTful API设计规范
|
||||
3. 错误处理统一格式
|
||||
4. 日志记录规范
|
||||
5. 安全防护措施
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 18.0.0
|
||||
- MySQL >= 8.0
|
||||
- Redis >= 7.0
|
||||
- npm >= 8.0.0
|
||||
|
||||
### 数据库配置
|
||||
```bash
|
||||
# 数据库连接信息
|
||||
主机: 129.211.213.226
|
||||
端口: 9527
|
||||
用户名: root
|
||||
密码: aiotAiot123!
|
||||
数据库: jiebandata
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 环境配置
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.example .env.development
|
||||
|
||||
# 编辑配置文件
|
||||
vim .env.development
|
||||
```
|
||||
|
||||
### 数据库初始化
|
||||
```bash
|
||||
# 执行数据库迁移
|
||||
npm run db:migrate
|
||||
|
||||
# 执行数据填充
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
# 开发环境
|
||||
npm run dev
|
||||
|
||||
# 生产环境
|
||||
npm start
|
||||
```
|
||||
|
||||
服务将运行在 http://localhost:3001
|
||||
101
backend/app.js
Normal file
101
backend/app.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const helmet = require('helmet')
|
||||
const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const compression = require('compression')
|
||||
require('dotenv').config()
|
||||
|
||||
// 数据库连接
|
||||
const { testConnection, syncModels } = require('./models')
|
||||
|
||||
const app = express()
|
||||
|
||||
// 中间件配置
|
||||
app.use(helmet()) // 安全头
|
||||
app.use(cors()) // 跨域
|
||||
app.use(compression()) // 压缩
|
||||
app.use(morgan('combined')) // 日志
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
||||
|
||||
// 限流
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 分钟
|
||||
max: 100, // 限制每个IP最多100个请求
|
||||
message: {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后重试'
|
||||
}
|
||||
})
|
||||
app.use('/api', limiter)
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '服务运行正常',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
// API 路由
|
||||
app.use('/api/auth', require('./routes/auth'))
|
||||
app.use('/api/users', require('./routes/users'))
|
||||
app.use('/api/orders', require('./routes/orders'))
|
||||
app.use('/api/suppliers', require('./routes/suppliers'))
|
||||
app.use('/api/transport', require('./routes/transport'))
|
||||
app.use('/api/finance', require('./routes/finance'))
|
||||
app.use('/api/quality', require('./routes/quality'))
|
||||
|
||||
// 404 处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '接口不存在',
|
||||
path: req.path
|
||||
})
|
||||
})
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err)
|
||||
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
message: err.message || '服务器内部错误',
|
||||
timestamp: new Date().toISOString(),
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 测试数据库连接
|
||||
const dbConnected = await testConnection();
|
||||
if (!dbConnected) {
|
||||
console.error('❌ 数据库连接失败,服务器启动终止');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 同步数据库模型
|
||||
await syncModels();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 服务器启动成功`)
|
||||
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)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
startServer()
|
||||
58
backend/config/database.js
Normal file
58
backend/config/database.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// 数据库配置文件
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
development: {
|
||||
username: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || 'aiotAiot123!',
|
||||
database: process.env.DB_NAME || 'jiebandata',
|
||||
host: process.env.DB_HOST || '129.211.213.226',
|
||||
port: process.env.DB_PORT || 9527,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4',
|
||||
dateStrings: true,
|
||||
typeCast: true
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: console.log,
|
||||
pool: {
|
||||
max: 20,
|
||||
min: 0,
|
||||
acquire: 60000,
|
||||
idle: 10000
|
||||
}
|
||||
},
|
||||
test: {
|
||||
username: process.env.TEST_DB_USERNAME || 'root',
|
||||
password: process.env.TEST_DB_PASSWORD || 'aiotAiot123!',
|
||||
database: process.env.TEST_DB_NAME || 'jiebandata_test',
|
||||
host: process.env.TEST_DB_HOST || '129.211.213.226',
|
||||
port: process.env.TEST_DB_PORT || 9527,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4'
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: false
|
||||
},
|
||||
production: {
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4'
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 50,
|
||||
min: 5,
|
||||
acquire: 60000,
|
||||
idle: 10000
|
||||
}
|
||||
}
|
||||
};
|
||||
140
backend/models/index.js
Normal file
140
backend/models/index.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// 数据库连接和模型定义
|
||||
const { Sequelize } = require('sequelize');
|
||||
const config = require('../config/database.js');
|
||||
|
||||
// 根据环境变量选择配置
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const dbConfig = config[env];
|
||||
|
||||
// 创建Sequelize实例
|
||||
const sequelize = new Sequelize(
|
||||
dbConfig.database,
|
||||
dbConfig.username,
|
||||
dbConfig.password,
|
||||
{
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
dialect: dbConfig.dialect,
|
||||
dialectOptions: dbConfig.dialectOptions,
|
||||
timezone: dbConfig.timezone,
|
||||
logging: dbConfig.logging,
|
||||
pool: dbConfig.pool
|
||||
}
|
||||
);
|
||||
|
||||
// 测试数据库连接
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义模型
|
||||
const models = {
|
||||
sequelize,
|
||||
Sequelize,
|
||||
|
||||
// 用户模型(匹配实际数据库结构)
|
||||
User: sequelize.define('User', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
openid: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
nickname: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
avatar: {
|
||||
type: Sequelize.STRING(255)
|
||||
},
|
||||
gender: {
|
||||
type: Sequelize.ENUM('male', 'female', 'other')
|
||||
},
|
||||
birthday: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(20),
|
||||
unique: true
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING(100),
|
||||
unique: true
|
||||
},
|
||||
uuid: {
|
||||
type: Sequelize.STRING(36),
|
||||
unique: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}),
|
||||
|
||||
// 为了兼容现有API,创建一个简化版的用户模型
|
||||
ApiUser: sequelize.define('ApiUser', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password_hash: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING(100)
|
||||
},
|
||||
user_type: {
|
||||
type: Sequelize.ENUM('client', 'supplier', 'driver', 'staff', 'admin'),
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.ENUM('active', 'inactive', 'locked'),
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
tableName: 'api_users',
|
||||
timestamps: true
|
||||
})
|
||||
};
|
||||
|
||||
// 同步数据库模型
|
||||
const syncModels = async () => {
|
||||
try {
|
||||
// 只同步API用户表(如果不存在则创建)
|
||||
await models.ApiUser.sync({ alter: true });
|
||||
console.log('✅ API用户表同步成功');
|
||||
console.log('✅ 数据库模型同步完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库模型同步失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...models,
|
||||
testConnection,
|
||||
syncModels
|
||||
};
|
||||
172
backend/routes/auth.js
Normal file
172
backend/routes/auth.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 引入数据库模型
|
||||
const { ApiUser } = require('../models')
|
||||
|
||||
// 登录参数验证
|
||||
const loginSchema = Joi.object({
|
||||
username: Joi.string().min(2).max(50).required(),
|
||||
password: Joi.string().min(6).max(100).required()
|
||||
})
|
||||
|
||||
// 生成JWT token
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.user_type
|
||||
},
|
||||
process.env.JWT_SECRET || 'niumall-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
)
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = loginSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
const { username, password } = value
|
||||
|
||||
// 查找用户
|
||||
const user = await ApiUser.findOne({
|
||||
where: {
|
||||
[require('sequelize').Op.or]: [
|
||||
{ username: username },
|
||||
{ phone: username },
|
||||
{ email: username }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
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({
|
||||
success: false,
|
||||
message: '账户已被禁用,请联系管理员'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const user = await ApiUser.findByPk(req.user.id)
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户信息失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 用户登出
|
||||
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({
|
||||
success: false,
|
||||
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
|
||||
490
backend/routes/finance.js
Normal file
490
backend/routes/finance.js
Normal file
@@ -0,0 +1,490 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟财务数据
|
||||
let settlements = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
settlementCode: 'SET001',
|
||||
supplierName: '山东优质牲畜合作社',
|
||||
buyerName: '北京肉类加工有限公司',
|
||||
cattleCount: 50,
|
||||
unitPrice: 25000,
|
||||
totalAmount: 1250000,
|
||||
paymentMethod: 'bank_transfer',
|
||||
paymentStatus: 'paid',
|
||||
settlementDate: '2024-01-20',
|
||||
paymentDate: '2024-01-22',
|
||||
invoiceNumber: 'INV001',
|
||||
invoiceStatus: 'issued',
|
||||
taxAmount: 125000,
|
||||
actualPayment: 1125000,
|
||||
bankAccount: '1234567890123456789',
|
||||
bankName: '中国农业银行',
|
||||
createdAt: new Date('2024-01-20'),
|
||||
updatedAt: new Date('2024-01-22')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
settlementCode: 'SET002',
|
||||
supplierName: '内蒙古草原牲畜有限公司',
|
||||
buyerName: '天津屠宰加工厂',
|
||||
cattleCount: 80,
|
||||
unitPrice: 24000,
|
||||
totalAmount: 1920000,
|
||||
paymentMethod: 'cash',
|
||||
paymentStatus: 'pending',
|
||||
settlementDate: '2024-01-25',
|
||||
paymentDate: null,
|
||||
invoiceNumber: 'INV002',
|
||||
invoiceStatus: 'pending',
|
||||
taxAmount: 192000,
|
||||
actualPayment: 1728000,
|
||||
bankAccount: '9876543210987654321',
|
||||
bankName: '中国建设银行',
|
||||
createdAt: new Date('2024-01-25'),
|
||||
updatedAt: new Date('2024-01-25')
|
||||
}
|
||||
];
|
||||
|
||||
let payments = [
|
||||
{
|
||||
id: 1,
|
||||
settlementId: 1,
|
||||
paymentCode: 'PAY001',
|
||||
amount: 1125000,
|
||||
paymentMethod: 'bank_transfer',
|
||||
status: 'success',
|
||||
transactionId: 'TXN20240122001',
|
||||
paidAt: '2024-01-22T10:30:00Z',
|
||||
createdAt: new Date('2024-01-22T10:30:00Z')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const settlementCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
cattleCount: Joi.number().integer().min(1).required(),
|
||||
unitPrice: Joi.number().min(0).required(),
|
||||
paymentMethod: Joi.string().valid('bank_transfer', 'cash', 'check', 'online').required(),
|
||||
settlementDate: Joi.date().iso().required(),
|
||||
invoiceNumber: Joi.string().min(3).max(50)
|
||||
});
|
||||
|
||||
const paymentCreateSchema = Joi.object({
|
||||
settlementId: Joi.number().integer().required(),
|
||||
amount: Joi.number().min(0).required(),
|
||||
paymentMethod: Joi.string().valid('bank_transfer', 'cash', 'check', 'online').required(),
|
||||
transactionId: Joi.string().max(100)
|
||||
});
|
||||
|
||||
// 获取结算列表
|
||||
router.get('/settlements', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
paymentStatus,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredSettlements = [...settlements];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
settlement.settlementCode.includes(keyword) ||
|
||||
settlement.supplierName.includes(keyword) ||
|
||||
settlement.buyerName.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 支付状态筛选
|
||||
if (paymentStatus) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement => settlement.paymentStatus === paymentStatus);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
new Date(settlement.settlementDate) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
new Date(settlement.settlementDate) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedSettlements = filteredSettlements.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedSettlements,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredSettlements.length,
|
||||
totalPages: Math.ceil(filteredSettlements.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取结算列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取结算详情
|
||||
router.get('/settlements/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const settlement = settlements.find(s => s.id === parseInt(id));
|
||||
|
||||
if (!settlement) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '结算记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取相关支付记录
|
||||
const relatedPayments = payments.filter(p => p.settlementId === settlement.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...settlement,
|
||||
payments: relatedPayments
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取结算详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建结算记录
|
||||
router.post('/settlements', (req, res) => {
|
||||
try {
|
||||
const { error, value } = settlementCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const settlementCode = `SET${String(Date.now()).slice(-6)}`;
|
||||
const totalAmount = value.cattleCount * value.unitPrice;
|
||||
const taxAmount = totalAmount * 0.1; // 假设税率10%
|
||||
const actualPayment = totalAmount - taxAmount;
|
||||
|
||||
const newSettlement = {
|
||||
id: Math.max(...settlements.map(s => s.id)) + 1,
|
||||
...value,
|
||||
settlementCode,
|
||||
totalAmount,
|
||||
taxAmount,
|
||||
actualPayment,
|
||||
paymentStatus: 'pending',
|
||||
paymentDate: null,
|
||||
invoiceStatus: 'pending',
|
||||
supplierName: '供应商名称', // 实际应从订单获取
|
||||
buyerName: '采购商名称', // 实际应从订单获取
|
||||
bankAccount: '',
|
||||
bankName: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
settlements.push(newSettlement);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '结算记录创建成功',
|
||||
data: newSettlement
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建结算记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新结算状态
|
||||
router.put('/settlements/:id/status', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { paymentStatus, invoiceStatus } = req.body;
|
||||
|
||||
const settlementIndex = settlements.findIndex(s => s.id === parseInt(id));
|
||||
if (settlementIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '结算记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (paymentStatus) {
|
||||
settlements[settlementIndex].paymentStatus = paymentStatus;
|
||||
if (paymentStatus === 'paid') {
|
||||
settlements[settlementIndex].paymentDate = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (invoiceStatus) {
|
||||
settlements[settlementIndex].invoiceStatus = invoiceStatus;
|
||||
}
|
||||
|
||||
settlements[settlementIndex].updatedAt = new Date();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '结算状态更新成功',
|
||||
data: settlements[settlementIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新结算状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取支付记录列表
|
||||
router.get('/payments', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
settlementId,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
let filteredPayments = [...payments];
|
||||
|
||||
// 按结算单筛选
|
||||
if (settlementId) {
|
||||
filteredPayments = filteredPayments.filter(payment => payment.settlementId === parseInt(settlementId));
|
||||
}
|
||||
|
||||
// 按状态筛选
|
||||
if (status) {
|
||||
filteredPayments = filteredPayments.filter(payment => payment.status === status);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedPayments = filteredPayments.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedPayments,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredPayments.length,
|
||||
totalPages: Math.ceil(filteredPayments.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取支付记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建支付记录
|
||||
router.post('/payments', (req, res) => {
|
||||
try {
|
||||
const { error, value } = paymentCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const paymentCode = `PAY${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newPayment = {
|
||||
id: Math.max(...payments.map(p => p.id)) + 1,
|
||||
...value,
|
||||
paymentCode,
|
||||
status: 'processing',
|
||||
paidAt: null,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
payments.push(newPayment);
|
||||
|
||||
// 模拟支付处理
|
||||
setTimeout(() => {
|
||||
const paymentIndex = payments.findIndex(p => p.id === newPayment.id);
|
||||
if (paymentIndex !== -1) {
|
||||
payments[paymentIndex].status = 'success';
|
||||
payments[paymentIndex].paidAt = new Date().toISOString();
|
||||
payments[paymentIndex].transactionId = `TXN${Date.now()}`;
|
||||
|
||||
// 更新对应结算单状态
|
||||
const settlementIndex = settlements.findIndex(s => s.id === value.settlementId);
|
||||
if (settlementIndex !== -1) {
|
||||
settlements[settlementIndex].paymentStatus = 'paid';
|
||||
settlements[settlementIndex].paymentDate = new Date().toISOString().split('T')[0];
|
||||
settlements[settlementIndex].updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
}, 3000); // 3秒后处理完成
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '支付申请已提交',
|
||||
data: newPayment
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建支付记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取财务统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalSettlements = settlements.length;
|
||||
const paidCount = settlements.filter(s => s.paymentStatus === 'paid').length;
|
||||
const pendingCount = settlements.filter(s => s.paymentStatus === 'pending').length;
|
||||
|
||||
const totalAmount = settlements.reduce((sum, s) => sum + s.totalAmount, 0);
|
||||
const paidAmount = settlements
|
||||
.filter(s => s.paymentStatus === 'paid')
|
||||
.reduce((sum, s) => sum + s.actualPayment, 0);
|
||||
const pendingAmount = settlements
|
||||
.filter(s => s.paymentStatus === 'pending')
|
||||
.reduce((sum, s) => sum + s.actualPayment, 0);
|
||||
|
||||
const totalTaxAmount = settlements.reduce((sum, s) => sum + s.taxAmount, 0);
|
||||
|
||||
// 本月统计
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthlySettlements = settlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getMonth() === currentMonth && settleDate.getFullYear() === currentYear;
|
||||
});
|
||||
const monthlyAmount = monthlySettlements.reduce((sum, s) => sum + s.totalAmount, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSettlements,
|
||||
paidCount,
|
||||
pendingCount,
|
||||
totalAmount,
|
||||
paidAmount,
|
||||
pendingAmount,
|
||||
totalTaxAmount,
|
||||
monthlyAmount,
|
||||
paymentRate: totalSettlements > 0 ? Math.round((paidCount / totalSettlements) * 100) : 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取财务统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取财务报表
|
||||
router.get('/reports/monthly', (req, res) => {
|
||||
try {
|
||||
const { year = new Date().getFullYear(), month } = req.query;
|
||||
|
||||
let targetSettlements = settlements;
|
||||
|
||||
// 筛选指定年份
|
||||
targetSettlements = targetSettlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getFullYear() === parseInt(year);
|
||||
});
|
||||
|
||||
// 如果指定了月份,进一步筛选
|
||||
if (month) {
|
||||
targetSettlements = targetSettlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getMonth() === parseInt(month) - 1;
|
||||
});
|
||||
}
|
||||
|
||||
// 按月份分组统计
|
||||
const monthlyStats = {};
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
monthlyStats[i] = {
|
||||
month: i,
|
||||
settlementCount: 0,
|
||||
totalAmount: 0,
|
||||
paidAmount: 0,
|
||||
pendingAmount: 0
|
||||
};
|
||||
}
|
||||
|
||||
targetSettlements.forEach(settlement => {
|
||||
const settleMonth = new Date(settlement.settlementDate).getMonth() + 1;
|
||||
monthlyStats[settleMonth].settlementCount++;
|
||||
monthlyStats[settleMonth].totalAmount += settlement.totalAmount;
|
||||
|
||||
if (settlement.paymentStatus === 'paid') {
|
||||
monthlyStats[settleMonth].paidAmount += settlement.actualPayment;
|
||||
} else {
|
||||
monthlyStats[settleMonth].pendingAmount += settlement.actualPayment;
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
year: parseInt(year),
|
||||
monthlyStats: Object.values(monthlyStats)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取财务报表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
539
backend/routes/orders.js
Normal file
539
backend/routes/orders.js
Normal file
@@ -0,0 +1,539 @@
|
||||
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_STATUS = {
|
||||
PENDING: 'pending',
|
||||
CONFIRMED: 'confirmed',
|
||||
PREPARING: 'preparing',
|
||||
SHIPPING: 'shipping',
|
||||
DELIVERED: 'delivered',
|
||||
ACCEPTED: 'accepted',
|
||||
COMPLETED: 'completed',
|
||||
CANCELLED: 'cancelled',
|
||||
REFUNDED: 'refunded'
|
||||
}
|
||||
|
||||
// 验证模式
|
||||
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('')
|
||||
})
|
||||
|
||||
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))
|
||||
})
|
||||
|
||||
// 获取订单列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
orderNo,
|
||||
buyerId,
|
||||
supplierId,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query
|
||||
|
||||
let filteredOrders = [...orders]
|
||||
|
||||
// 订单号搜索
|
||||
if (orderNo) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.orderNo.includes(orderNo)
|
||||
)
|
||||
}
|
||||
|
||||
// 买方筛选
|
||||
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)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedOrders,
|
||||
total: total,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单列表失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取订单详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const order = orders.find(o => o.id === parseInt(id))
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单详情失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建订单
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = createOrderSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
status: ORDER_STATUS.PENDING,
|
||||
deliveryAddress,
|
||||
expectedDeliveryDate,
|
||||
actualDeliveryDate: null,
|
||||
notes: notes || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
orders.push(newOrder)
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '订单创建成功',
|
||||
data: newOrder
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新订单
|
||||
router.put('/:id', (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({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 更新订单信息
|
||||
orders[orderIndex] = {
|
||||
...orders[orderIndex],
|
||||
...value,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 如果更新了实际重量,重新计算总金额
|
||||
if (value.actualWeight && orders[orderIndex].unitPrice) {
|
||||
orders[orderIndex].totalAmount = value.actualWeight * orders[orderIndex].unitPrice
|
||||
orders[orderIndex].remainingAmount = orders[orderIndex].totalAmount - orders[orderIndex].paidAmount
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单更新成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除订单
|
||||
router.delete('/:id', (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.splice(orderIndex, 1)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 确认订单
|
||||
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
|
||||
548
backend/routes/quality.js
Normal file
548
backend/routes/quality.js
Normal file
@@ -0,0 +1,548 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟质量检测数据
|
||||
let qualityRecords = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
inspectionCode: 'QC001',
|
||||
inspectorName: '张检验员',
|
||||
inspectionDate: '2024-01-15',
|
||||
inspectionLocation: '山东省济南市历城区牲畜养殖基地',
|
||||
cattleCount: 50,
|
||||
samplingCount: 5,
|
||||
inspectionType: 'pre_transport',
|
||||
healthStatus: 'healthy',
|
||||
quarantineCertificate: 'QC001_certificate.pdf',
|
||||
vaccineRecords: [
|
||||
{
|
||||
vaccineName: '口蹄疫疫苗',
|
||||
vaccineDate: '2024-01-01',
|
||||
batchNumber: 'VAC20240101'
|
||||
}
|
||||
],
|
||||
diseaseTests: [
|
||||
{
|
||||
testName: '布鲁氏菌病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-10'
|
||||
},
|
||||
{
|
||||
testName: '结核病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-10'
|
||||
}
|
||||
],
|
||||
weightCheck: {
|
||||
averageWeight: 450,
|
||||
weightRange: '420-480',
|
||||
weightVariance: 15
|
||||
},
|
||||
qualityGrade: 'A',
|
||||
qualityScore: 95,
|
||||
issues: [],
|
||||
recommendations: [
|
||||
'建议继续保持当前饲养标准',
|
||||
'注意观察牲畜健康状况'
|
||||
],
|
||||
photos: [
|
||||
'inspection_001_1.jpg',
|
||||
'inspection_001_2.jpg'
|
||||
],
|
||||
status: 'passed',
|
||||
createdAt: new Date('2024-01-15'),
|
||||
updatedAt: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
inspectionCode: 'QC002',
|
||||
inspectorName: '李检验员',
|
||||
inspectionDate: '2024-01-16',
|
||||
inspectionLocation: '内蒙古呼和浩特市草原牧场',
|
||||
cattleCount: 80,
|
||||
samplingCount: 8,
|
||||
inspectionType: 'pre_transport',
|
||||
healthStatus: 'healthy',
|
||||
quarantineCertificate: 'QC002_certificate.pdf',
|
||||
vaccineRecords: [
|
||||
{
|
||||
vaccineName: '口蹄疫疫苗',
|
||||
vaccineDate: '2023-12-15',
|
||||
batchNumber: 'VAC20231215'
|
||||
}
|
||||
],
|
||||
diseaseTests: [
|
||||
{
|
||||
testName: '布鲁氏菌病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-12'
|
||||
}
|
||||
],
|
||||
weightCheck: {
|
||||
averageWeight: 480,
|
||||
weightRange: '450-520',
|
||||
weightVariance: 20
|
||||
},
|
||||
qualityGrade: 'A',
|
||||
qualityScore: 92,
|
||||
issues: [
|
||||
{
|
||||
type: 'minor',
|
||||
description: '个别牲畜体重偏轻',
|
||||
solution: '加强营养补充'
|
||||
}
|
||||
],
|
||||
recommendations: [
|
||||
'对体重偏轻的牲畜进行重点关注',
|
||||
'适当调整饲料配比'
|
||||
],
|
||||
photos: [
|
||||
'inspection_002_1.jpg',
|
||||
'inspection_002_2.jpg',
|
||||
'inspection_002_3.jpg'
|
||||
],
|
||||
status: 'passed',
|
||||
createdAt: new Date('2024-01-16'),
|
||||
updatedAt: new Date('2024-01-16')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const inspectionCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
inspectorName: Joi.string().min(2).max(50).required(),
|
||||
inspectionDate: Joi.date().iso().required(),
|
||||
inspectionLocation: Joi.string().min(5).max(200).required(),
|
||||
cattleCount: Joi.number().integer().min(1).required(),
|
||||
samplingCount: Joi.number().integer().min(1).required(),
|
||||
inspectionType: Joi.string().valid('pre_transport', 'during_transport', 'post_transport', 'arrival').required()
|
||||
});
|
||||
|
||||
const qualityResultSchema = Joi.object({
|
||||
healthStatus: Joi.string().valid('healthy', 'sick', 'quarantine').required(),
|
||||
qualityGrade: Joi.string().valid('A+', 'A', 'B+', 'B', 'C', 'D').required(),
|
||||
qualityScore: Joi.number().min(0).max(100).required(),
|
||||
weightCheck: Joi.object({
|
||||
averageWeight: Joi.number().min(0),
|
||||
weightRange: Joi.string(),
|
||||
weightVariance: Joi.number().min(0)
|
||||
}),
|
||||
diseaseTests: Joi.array().items(Joi.object({
|
||||
testName: Joi.string().required(),
|
||||
result: Joi.string().valid('positive', 'negative', 'inconclusive').required(),
|
||||
testDate: Joi.date().iso().required()
|
||||
})),
|
||||
issues: Joi.array().items(Joi.object({
|
||||
type: Joi.string().valid('critical', 'major', 'minor').required(),
|
||||
description: Joi.string().required(),
|
||||
solution: Joi.string()
|
||||
})),
|
||||
recommendations: Joi.array().items(Joi.string())
|
||||
});
|
||||
|
||||
// 获取质量检测列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
inspectionType,
|
||||
qualityGrade,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredRecords = [...qualityRecords];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
record.inspectionCode.includes(keyword) ||
|
||||
record.inspectorName.includes(keyword) ||
|
||||
record.inspectionLocation.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 检测类型筛选
|
||||
if (inspectionType) {
|
||||
filteredRecords = filteredRecords.filter(record => record.inspectionType === inspectionType);
|
||||
}
|
||||
|
||||
// 质量等级筛选
|
||||
if (qualityGrade) {
|
||||
filteredRecords = filteredRecords.filter(record => record.qualityGrade === qualityGrade);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredRecords = filteredRecords.filter(record => record.status === status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
new Date(record.inspectionDate) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
new Date(record.inspectionDate) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedRecords = filteredRecords.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedRecords,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredRecords.length,
|
||||
totalPages: Math.ceil(filteredRecords.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量检测列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量检测详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const record = qualityRecords.find(r => r.id === parseInt(id));
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量检测详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建质量检测记录
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = inspectionCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const inspectionCode = `QC${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newRecord = {
|
||||
id: Math.max(...qualityRecords.map(r => r.id)) + 1,
|
||||
...value,
|
||||
inspectionCode,
|
||||
healthStatus: 'pending',
|
||||
quarantineCertificate: '',
|
||||
vaccineRecords: [],
|
||||
diseaseTests: [],
|
||||
weightCheck: null,
|
||||
qualityGrade: '',
|
||||
qualityScore: 0,
|
||||
issues: [],
|
||||
recommendations: [],
|
||||
photos: [],
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
qualityRecords.push(newRecord);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '质量检测记录创建成功',
|
||||
data: newRecord
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建质量检测记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新质量检测结果
|
||||
router.put('/:id/result', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = qualityResultSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const recordIndex = qualityRecords.findIndex(r => r.id === parseInt(id));
|
||||
if (recordIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 根据检测结果确定状态
|
||||
let status = 'passed';
|
||||
if (value.healthStatus === 'sick' || value.qualityScore < 60) {
|
||||
status = 'failed';
|
||||
} else if (value.healthStatus === 'quarantine' || value.issues.some(issue => issue.type === 'critical')) {
|
||||
status = 'quarantine';
|
||||
}
|
||||
|
||||
qualityRecords[recordIndex] = {
|
||||
...qualityRecords[recordIndex],
|
||||
...value,
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '质量检测结果更新成功',
|
||||
data: qualityRecords[recordIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新质量检测结果失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上传检测照片
|
||||
router.post('/:id/photos', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { photos } = req.body;
|
||||
|
||||
if (!Array.isArray(photos) || photos.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '照片列表不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const recordIndex = qualityRecords.findIndex(r => r.id === parseInt(id));
|
||||
if (recordIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
qualityRecords[recordIndex].photos = [...qualityRecords[recordIndex].photos, ...photos];
|
||||
qualityRecords[recordIndex].updatedAt = new Date();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '照片上传成功',
|
||||
data: {
|
||||
photos: qualityRecords[recordIndex].photos
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '上传照片失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalInspections = qualityRecords.length;
|
||||
const passedCount = qualityRecords.filter(r => r.status === 'passed').length;
|
||||
const failedCount = qualityRecords.filter(r => r.status === 'failed').length;
|
||||
const quarantineCount = qualityRecords.filter(r => r.status === 'quarantine').length;
|
||||
const pendingCount = qualityRecords.filter(r => r.status === 'pending').length;
|
||||
|
||||
// 平均质量分数
|
||||
const completedRecords = qualityRecords.filter(r => r.qualityScore > 0);
|
||||
const averageScore = completedRecords.length > 0
|
||||
? completedRecords.reduce((sum, r) => sum + r.qualityScore, 0) / completedRecords.length
|
||||
: 0;
|
||||
|
||||
// 质量等级分布
|
||||
const gradeDistribution = qualityRecords
|
||||
.filter(r => r.qualityGrade)
|
||||
.reduce((dist, record) => {
|
||||
dist[record.qualityGrade] = (dist[record.qualityGrade] || 0) + 1;
|
||||
return dist;
|
||||
}, {});
|
||||
|
||||
// 检测类型分布
|
||||
const typeDistribution = qualityRecords.reduce((dist, record) => {
|
||||
dist[record.inspectionType] = (dist[record.inspectionType] || 0) + 1;
|
||||
return dist;
|
||||
}, {});
|
||||
|
||||
// 合格率
|
||||
const passRate = totalInspections > 0 ? Math.round((passedCount / totalInspections) * 100) : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalInspections,
|
||||
passedCount,
|
||||
failedCount,
|
||||
quarantineCount,
|
||||
pendingCount,
|
||||
averageScore: Math.round(averageScore * 10) / 10,
|
||||
passRate,
|
||||
gradeDistribution,
|
||||
typeDistribution
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量趋势报告
|
||||
router.get('/reports/trend', (req, res) => {
|
||||
try {
|
||||
const { period = 'month' } = req.query;
|
||||
|
||||
// 按时间分组统计
|
||||
const now = new Date();
|
||||
const trends = [];
|
||||
|
||||
if (period === 'month') {
|
||||
// 最近12个月
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const monthRecords = qualityRecords.filter(r => {
|
||||
const recordDate = new Date(r.inspectionDate);
|
||||
return recordDate.getMonth() === date.getMonth() &&
|
||||
recordDate.getFullYear() === date.getFullYear();
|
||||
});
|
||||
|
||||
const passed = monthRecords.filter(r => r.status === 'passed').length;
|
||||
const total = monthRecords.length;
|
||||
const averageScore = monthRecords.length > 0
|
||||
? monthRecords.reduce((sum, r) => sum + (r.qualityScore || 0), 0) / monthRecords.length
|
||||
: 0;
|
||||
|
||||
trends.push({
|
||||
period: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
totalInspections: total,
|
||||
passedCount: passed,
|
||||
passRate: total > 0 ? Math.round((passed / total) * 100) : 0,
|
||||
averageScore: Math.round(averageScore * 10) / 10
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
period,
|
||||
trends
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量趋势报告失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取检测标准配置
|
||||
router.get('/standards', (req, res) => {
|
||||
try {
|
||||
const standards = {
|
||||
weightStandards: {
|
||||
cattle: {
|
||||
min: 400,
|
||||
max: 600,
|
||||
optimal: 500
|
||||
}
|
||||
},
|
||||
healthRequirements: [
|
||||
{
|
||||
name: '口蹄疫疫苗',
|
||||
required: true,
|
||||
validityDays: 365
|
||||
},
|
||||
{
|
||||
name: '布鲁氏菌病检测',
|
||||
required: true,
|
||||
validityDays: 30
|
||||
},
|
||||
{
|
||||
name: '结核病检测',
|
||||
required: true,
|
||||
validityDays: 30
|
||||
}
|
||||
],
|
||||
gradingCriteria: {
|
||||
'A+': { minScore: 95, description: '优质级' },
|
||||
'A': { minScore: 85, description: '良好级' },
|
||||
'B+': { minScore: 75, description: '合格级' },
|
||||
'B': { minScore: 65, description: '基本合格级' },
|
||||
'C': { minScore: 50, description: '待改进级' },
|
||||
'D': { minScore: 0, description: '不合格级' }
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: standards
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取检测标准失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
406
backend/routes/suppliers.js
Normal file
406
backend/routes/suppliers.js
Normal file
@@ -0,0 +1,406 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟供应商数据
|
||||
let suppliers = [
|
||||
{
|
||||
id: 1,
|
||||
name: '山东优质牲畜合作社',
|
||||
code: 'SUP001',
|
||||
contact: '李经理',
|
||||
phone: '15888888888',
|
||||
address: '山东省济南市历城区牲畜养殖基地',
|
||||
businessLicense: 'SUP001_license.pdf',
|
||||
qualificationLevel: 'A',
|
||||
certifications: ['动物防疫合格证', '饲料生产许可证'],
|
||||
cattleTypes: ['肉牛', '奶牛'],
|
||||
capacity: 5000,
|
||||
rating: 4.8,
|
||||
cooperationStartDate: '2022-01-15',
|
||||
status: 'active',
|
||||
region: 'east',
|
||||
createdAt: new Date('2022-01-15'),
|
||||
updatedAt: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '内蒙古草原牲畜有限公司',
|
||||
code: 'SUP002',
|
||||
contact: '王总',
|
||||
phone: '13999999999',
|
||||
address: '内蒙古呼和浩特市草原牧场',
|
||||
businessLicense: 'SUP002_license.pdf',
|
||||
qualificationLevel: 'A+',
|
||||
certifications: ['有机认证', '绿色食品认证'],
|
||||
cattleTypes: ['草原牛', '黄牛'],
|
||||
capacity: 8000,
|
||||
rating: 4.9,
|
||||
cooperationStartDate: '2021-08-20',
|
||||
status: 'active',
|
||||
region: 'north',
|
||||
createdAt: new Date('2021-08-20'),
|
||||
updatedAt: new Date('2024-01-20')
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '四川高原牲畜养殖场',
|
||||
code: 'SUP003',
|
||||
contact: '张场长',
|
||||
phone: '18777777777',
|
||||
address: '四川省成都市高原养殖区',
|
||||
businessLicense: 'SUP003_license.pdf',
|
||||
qualificationLevel: 'B+',
|
||||
certifications: ['无公害产品认证'],
|
||||
cattleTypes: ['高原牛'],
|
||||
capacity: 3000,
|
||||
rating: 4.5,
|
||||
cooperationStartDate: '2022-06-10',
|
||||
status: 'active',
|
||||
region: 'southwest',
|
||||
createdAt: new Date('2022-06-10'),
|
||||
updatedAt: new Date('2024-01-10')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const supplierCreateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100).required(),
|
||||
code: Joi.string().min(3).max(20).required(),
|
||||
contact: Joi.string().min(2).max(50).required(),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
|
||||
address: Joi.string().min(5).max(200).required(),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C').required(),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1).required(),
|
||||
capacity: Joi.number().integer().min(1).required(),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central').required()
|
||||
});
|
||||
|
||||
const supplierUpdateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100),
|
||||
contact: Joi.string().min(2).max(50),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/),
|
||||
address: Joi.string().min(5).max(200),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C'),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1),
|
||||
capacity: Joi.number().integer().min(1),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central'),
|
||||
status: Joi.string().valid('active', 'inactive', 'suspended')
|
||||
});
|
||||
|
||||
// 获取供应商列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status = 'active'
|
||||
} = req.query;
|
||||
|
||||
let filteredSuppliers = [...suppliers];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier =>
|
||||
supplier.name.includes(keyword) ||
|
||||
supplier.code.includes(keyword) ||
|
||||
supplier.contact.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 区域筛选
|
||||
if (region) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.region === region);
|
||||
}
|
||||
|
||||
// 资质等级筛选
|
||||
if (qualificationLevel) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.qualificationLevel === qualificationLevel);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.status === status);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedSuppliers = filteredSuppliers.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedSuppliers,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredSuppliers.length,
|
||||
totalPages: Math.ceil(filteredSuppliers.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplier = suppliers.find(s => s.id === parseInt(id));
|
||||
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建供应商
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = supplierCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
// 检查编码是否重复
|
||||
const existingSupplier = suppliers.find(s => s.code === value.code);
|
||||
if (existingSupplier) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商编码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const newSupplier = {
|
||||
id: Math.max(...suppliers.map(s => s.id)) + 1,
|
||||
...value,
|
||||
businessLicense: '',
|
||||
certifications: [],
|
||||
rating: 0,
|
||||
cooperationStartDate: new Date().toISOString().split('T')[0],
|
||||
status: 'active',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
suppliers.push(newSupplier);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '供应商创建成功',
|
||||
data: newSupplier
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新供应商
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = supplierUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
if (supplierIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers[supplierIndex] = {
|
||||
...suppliers[supplierIndex],
|
||||
...value,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
data: suppliers[supplierIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除供应商
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
|
||||
if (supplierIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers.splice(supplierIndex, 1);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商统计信息
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalSuppliers = suppliers.length;
|
||||
const activeSuppliers = suppliers.filter(s => s.status === 'active').length;
|
||||
const averageRating = suppliers.reduce((sum, s) => sum + s.rating, 0) / totalSuppliers;
|
||||
const totalCapacity = suppliers.reduce((sum, s) => sum + s.capacity, 0);
|
||||
|
||||
// 按等级统计
|
||||
const levelStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.qualificationLevel] = (stats[supplier.qualificationLevel] || 0) + 1;
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
// 按区域统计
|
||||
const regionStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.region] = (stats[supplier.region] || 0) + 1;
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSuppliers,
|
||||
activeSuppliers,
|
||||
averageRating: Math.round(averageRating * 10) / 10,
|
||||
totalCapacity,
|
||||
levelStats,
|
||||
regionStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 批量操作
|
||||
router.post('/batch', (req, res) => {
|
||||
try {
|
||||
const { action, ids } = req.body;
|
||||
|
||||
if (!action || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数错误'
|
||||
});
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'active';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'deactivate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'inactive';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
suppliers = suppliers.filter(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
affectedCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不支持的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `批量${action}成功`,
|
||||
data: { affectedCount }
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量操作失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
467
backend/routes/transport.js
Normal file
467
backend/routes/transport.js
Normal file
@@ -0,0 +1,467 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟运输数据
|
||||
let transports = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
transportCode: 'TRP001',
|
||||
driverName: '张师傅',
|
||||
driverPhone: '13800001111',
|
||||
vehicleNumber: '鲁A12345',
|
||||
vehicleType: '厢式货车',
|
||||
startLocation: '山东省济南市历城区牲畜养殖基地',
|
||||
endLocation: '北京市朝阳区肉类加工厂',
|
||||
plannedDepartureTime: '2024-01-15T08:00:00Z',
|
||||
actualDepartureTime: '2024-01-15T08:30:00Z',
|
||||
estimatedArrivalTime: '2024-01-15T18:00:00Z',
|
||||
actualArrivalTime: null,
|
||||
distance: 450,
|
||||
status: 'in_transit',
|
||||
currentLocation: {
|
||||
lat: 36.8012,
|
||||
lng: 117.1120,
|
||||
address: '山东省济南市天桥区',
|
||||
updateTime: '2024-01-15T14:30:00Z'
|
||||
},
|
||||
route: [
|
||||
{ lat: 36.6512, lng: 117.1201, time: '2024-01-15T08:30:00Z' },
|
||||
{ lat: 36.7012, lng: 117.1001, time: '2024-01-15T10:30:00Z' },
|
||||
{ lat: 36.8012, lng: 117.1120, time: '2024-01-15T14:30:00Z' }
|
||||
],
|
||||
cattleCount: 50,
|
||||
temperature: 18,
|
||||
humidity: 65,
|
||||
alerts: [],
|
||||
createdAt: new Date('2024-01-15T08:00:00Z'),
|
||||
updatedAt: new Date('2024-01-15T14:30:00Z')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
transportCode: 'TRP002',
|
||||
driverName: '李师傅',
|
||||
driverPhone: '13800002222',
|
||||
vehicleNumber: '蒙B67890',
|
||||
vehicleType: '专用运牛车',
|
||||
startLocation: '内蒙古呼和浩特市草原牧场',
|
||||
endLocation: '天津市滨海新区屠宰场',
|
||||
plannedDepartureTime: '2024-01-16T06:00:00Z',
|
||||
actualDepartureTime: '2024-01-16T06:15:00Z',
|
||||
estimatedArrivalTime: '2024-01-16T20:00:00Z',
|
||||
actualArrivalTime: '2024-01-16T19:45:00Z',
|
||||
distance: 680,
|
||||
status: 'completed',
|
||||
currentLocation: {
|
||||
lat: 39.3434,
|
||||
lng: 117.3616,
|
||||
address: '天津市滨海新区',
|
||||
updateTime: '2024-01-16T19:45:00Z'
|
||||
},
|
||||
route: [
|
||||
{ lat: 40.8420, lng: 111.7520, time: '2024-01-16T06:15:00Z' },
|
||||
{ lat: 40.1420, lng: 114.7520, time: '2024-01-16T12:15:00Z' },
|
||||
{ lat: 39.3434, lng: 117.3616, time: '2024-01-16T19:45:00Z' }
|
||||
],
|
||||
cattleCount: 80,
|
||||
temperature: 15,
|
||||
humidity: 70,
|
||||
alerts: [
|
||||
{
|
||||
type: 'temperature',
|
||||
message: '车厢温度偏高',
|
||||
time: '2024-01-16T14:30:00Z',
|
||||
resolved: true
|
||||
}
|
||||
],
|
||||
createdAt: new Date('2024-01-16T06:00:00Z'),
|
||||
updatedAt: new Date('2024-01-16T19:45:00Z')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const transportCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
driverName: Joi.string().min(2).max(50).required(),
|
||||
driverPhone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
|
||||
vehicleNumber: Joi.string().min(5).max(20).required(),
|
||||
vehicleType: Joi.string().min(2).max(50).required(),
|
||||
startLocation: Joi.string().min(5).max(200).required(),
|
||||
endLocation: Joi.string().min(5).max(200).required(),
|
||||
plannedDepartureTime: Joi.date().iso().required(),
|
||||
estimatedArrivalTime: Joi.date().iso().required(),
|
||||
distance: Joi.number().min(1).required(),
|
||||
cattleCount: Joi.number().integer().min(1).required()
|
||||
});
|
||||
|
||||
const locationUpdateSchema = Joi.object({
|
||||
lat: Joi.number().min(-90).max(90).required(),
|
||||
lng: Joi.number().min(-180).max(180).required(),
|
||||
address: Joi.string().max(200),
|
||||
temperature: Joi.number().min(-50).max(50),
|
||||
humidity: Joi.number().min(0).max(100)
|
||||
});
|
||||
|
||||
const statusUpdateSchema = Joi.object({
|
||||
status: Joi.string().valid('pending', 'loading', 'in_transit', 'arrived', 'completed', 'cancelled').required(),
|
||||
actualTime: Joi.date().iso()
|
||||
});
|
||||
|
||||
// 获取运输列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredTransports = [...transports];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
transport.transportCode.includes(keyword) ||
|
||||
transport.driverName.includes(keyword) ||
|
||||
transport.vehicleNumber.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredTransports = filteredTransports.filter(transport => transport.status === status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
new Date(transport.plannedDepartureTime) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
new Date(transport.plannedDepartureTime) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedTransports = filteredTransports.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedTransports,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredTransports.length,
|
||||
totalPages: Math.ceil(filteredTransports.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transport = transports.find(t => t.id === parseInt(id));
|
||||
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建运输任务
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = transportCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportCode = `TRP${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newTransport = {
|
||||
id: Math.max(...transports.map(t => t.id)) + 1,
|
||||
...value,
|
||||
transportCode,
|
||||
actualDepartureTime: null,
|
||||
actualArrivalTime: null,
|
||||
status: 'pending',
|
||||
currentLocation: null,
|
||||
route: [],
|
||||
temperature: null,
|
||||
humidity: null,
|
||||
alerts: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
transports.push(newTransport);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '运输任务创建成功',
|
||||
data: newTransport
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建运输任务失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新位置信息
|
||||
router.post('/:id/location', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = locationUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportIndex = transports.findIndex(t => t.id === parseInt(id));
|
||||
if (transportIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
const locationData = {
|
||||
...value,
|
||||
updateTime: currentTime.toISOString()
|
||||
};
|
||||
|
||||
// 更新当前位置
|
||||
transports[transportIndex].currentLocation = locationData;
|
||||
|
||||
// 添加到路径轨迹
|
||||
transports[transportIndex].route.push({
|
||||
lat: value.lat,
|
||||
lng: value.lng,
|
||||
time: currentTime.toISOString()
|
||||
});
|
||||
|
||||
// 更新温度和湿度
|
||||
if (value.temperature !== undefined) {
|
||||
transports[transportIndex].temperature = value.temperature;
|
||||
}
|
||||
if (value.humidity !== undefined) {
|
||||
transports[transportIndex].humidity = value.humidity;
|
||||
}
|
||||
|
||||
transports[transportIndex].updatedAt = currentTime;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '位置更新成功',
|
||||
data: transports[transportIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新位置失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新运输状态
|
||||
router.put('/:id/status', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = statusUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportIndex = transports.findIndex(t => t.id === parseInt(id));
|
||||
if (transportIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
transports[transportIndex].status = value.status;
|
||||
|
||||
// 根据状态更新实际时间
|
||||
if (value.status === 'in_transit' && !transports[transportIndex].actualDepartureTime) {
|
||||
transports[transportIndex].actualDepartureTime = value.actualTime || currentTime.toISOString();
|
||||
} else if (value.status === 'completed' && !transports[transportIndex].actualArrivalTime) {
|
||||
transports[transportIndex].actualArrivalTime = value.actualTime || currentTime.toISOString();
|
||||
}
|
||||
|
||||
transports[transportIndex].updatedAt = currentTime;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '状态更新成功',
|
||||
data: transports[transportIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalTransports = transports.length;
|
||||
const inTransitCount = transports.filter(t => t.status === 'in_transit').length;
|
||||
const completedCount = transports.filter(t => t.status === 'completed').length;
|
||||
const pendingCount = transports.filter(t => t.status === 'pending').length;
|
||||
|
||||
// 平均运输时间(已完成的订单)
|
||||
const completedTransports = transports.filter(t => t.status === 'completed' && t.actualDepartureTime && t.actualArrivalTime);
|
||||
const averageTransitTime = completedTransports.length > 0
|
||||
? completedTransports.reduce((sum, t) => {
|
||||
const departureTime = new Date(t.actualDepartureTime);
|
||||
const arrivalTime = new Date(t.actualArrivalTime);
|
||||
return sum + (arrivalTime - departureTime);
|
||||
}, 0) / completedTransports.length / (1000 * 60 * 60) // 转换为小时
|
||||
: 0;
|
||||
|
||||
// 总运输距离
|
||||
const totalDistance = transports.reduce((sum, t) => sum + t.distance, 0);
|
||||
|
||||
// 总运输牲畜数量
|
||||
const totalCattleCount = transports.reduce((sum, t) => sum + t.cattleCount, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalTransports,
|
||||
inTransitCount,
|
||||
completedCount,
|
||||
pendingCount,
|
||||
averageTransitTime: Math.round(averageTransitTime * 10) / 10,
|
||||
totalDistance,
|
||||
totalCattleCount
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实时运输地图数据
|
||||
router.get('/map/realtime', (req, res) => {
|
||||
try {
|
||||
const activeTransports = transports
|
||||
.filter(t => t.status === 'in_transit' && t.currentLocation)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
transportCode: t.transportCode,
|
||||
driverName: t.driverName,
|
||||
vehicleNumber: t.vehicleNumber,
|
||||
currentLocation: t.currentLocation,
|
||||
destination: t.endLocation,
|
||||
cattleCount: t.cattleCount,
|
||||
estimatedArrivalTime: t.estimatedArrivalTime
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activeTransports
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取实时地图数据失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输轨迹
|
||||
router.get('/:id/route', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transport = transports.find(t => t.id === parseInt(id));
|
||||
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
transportCode: transport.transportCode,
|
||||
startLocation: transport.startLocation,
|
||||
endLocation: transport.endLocation,
|
||||
route: transport.route,
|
||||
currentLocation: transport.currentLocation
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输轨迹失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
336
backend/routes/users.js
Normal file
336
backend/routes/users.js
Normal file
@@ -0,0 +1,336 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 引入数据库模型
|
||||
const { ApiUser } = require('../models')
|
||||
const sequelize = require('sequelize')
|
||||
|
||||
// 验证模式
|
||||
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')
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, keyword, user_type, status } = req.query
|
||||
|
||||
// 构建查询条件
|
||||
const where = {}
|
||||
if (keyword) {
|
||||
where[sequelize.Op.or] = [
|
||||
{ username: { [sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ email: { [sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ phone: { [sequelize.Op.like]: `%${keyword}%` } }
|
||||
]
|
||||
}
|
||||
if (user_type) where.user_type = user_type
|
||||
if (status) where.status = status
|
||||
|
||||
// 分页查询
|
||||
const result = await ApiUser.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: result.rows,
|
||||
total: result.count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(result.count / parseInt(pageSize))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取用户详情
|
||||
router.get('/: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: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户详情失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建用户
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = createUserSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
const { username, email, phone, password, user_type, status } = value
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await ApiUser.findOne({
|
||||
where: {
|
||||
[sequelize.Op.or]: [
|
||||
{ username: username },
|
||||
{ email: email },
|
||||
{ phone: phone }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名、邮箱或手机号已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
const saltRounds = 10
|
||||
const password_hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
// 创建新用户
|
||||
const newUser = await ApiUser.create({
|
||||
username,
|
||||
email,
|
||||
phone: phone || '',
|
||||
password_hash,
|
||||
user_type,
|
||||
status,
|
||||
})
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '用户创建成功',
|
||||
data: newUser
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户
|
||||
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({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
await user.update(value)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户更新成功',
|
||||
data: user
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户
|
||||
router.delete('/: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: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
await user.destroy()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 批量删除用户
|
||||
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
|
||||
Reference in New Issue
Block a user