Merge remote-tracking branch 'origin/main'

# Conflicts:
#	backend/.env.example
#	backend/package-lock.json
#	backend/package.json
This commit is contained in:
2025-09-10 20:11:54 +08:00
60 changed files with 19804 additions and 126 deletions

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

View File

@@ -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
View 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()

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