添加 IntelliJ IDEA 项目配置文件
This commit is contained in:
194
backend/routes/auth.js
Normal file
194
backend/routes/auth.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 模拟用户数据
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
role: 'admin',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'buyer',
|
||||
email: 'buyer@example.com',
|
||||
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
role: 'buyer',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'trader',
|
||||
email: 'trader@example.com',
|
||||
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
role: 'trader',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
|
||||
// 登录参数验证
|
||||
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.role
|
||||
},
|
||||
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 = users.find(u => u.username === username || u.email === username)
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password)
|
||||
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.role,
|
||||
status: user.status
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticateToken, (req, res) => {
|
||||
const user = users.find(u => u.id === 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.role,
|
||||
status: user.status
|
||||
},
|
||||
permissions: getUserPermissions(user.role)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 用户登出
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
function getUserPermissions(role) {
|
||||
const permissions = {
|
||||
admin: ['*'], // 管理员拥有所有权限
|
||||
buyer: ['order:read', 'order:create', 'order:update', 'supplier:read'],
|
||||
trader: ['order:read', 'order:update', 'supplier:read', 'supplier:create', 'supplier:update', 'transport:read'],
|
||||
supplier: ['order:read', 'quality:read', 'quality:create', 'quality:update'],
|
||||
driver: ['transport:read', 'transport:update']
|
||||
}
|
||||
|
||||
return permissions[role] || []
|
||||
}
|
||||
|
||||
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;
|
||||
359
backend/routes/users.js
Normal file
359
backend/routes/users.js
Normal file
@@ -0,0 +1,359 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 模拟用户数据
|
||||
let users = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'buyer01',
|
||||
email: 'buyer01@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'buyer',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'supplier01',
|
||||
email: 'supplier01@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'supplier',
|
||||
status: 'inactive',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
updatedAt: '2024-01-03T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
// 验证模式
|
||||
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(),
|
||||
role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver').required(),
|
||||
status: Joi.string().valid('active', 'inactive').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(''),
|
||||
role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver'),
|
||||
status: Joi.string().valid('active', 'inactive', 'banned')
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, keyword, role, status } = req.query
|
||||
|
||||
let filteredUsers = [...users]
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredUsers = filteredUsers.filter(user =>
|
||||
user.username.includes(keyword) ||
|
||||
user.email.includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 角色筛选
|
||||
if (role) {
|
||||
filteredUsers = filteredUsers.filter(user => user.role === role)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredUsers = filteredUsers.filter(user => user.status === status)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const total = filteredUsers.length
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + parseInt(pageSize)
|
||||
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedUsers,
|
||||
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 user = users.find(u => u.id === parseInt(id))
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
})
|
||||
} catch (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, role, status } = value
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (users.find(u => u.username === username)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (users.find(u => u.email === email)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮箱已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const newUser = {
|
||||
id: Math.max(...users.map(u => u.id)) + 1,
|
||||
username,
|
||||
email,
|
||||
phone: phone || '',
|
||||
role,
|
||||
status,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
users.push(newUser)
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '用户创建成功',
|
||||
data: newUser
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const userIndex = users.findIndex(u => u.id === parseInt(id))
|
||||
|
||||
if (userIndex === -1) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
users[userIndex] = {
|
||||
...users[userIndex],
|
||||
...value,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户更新成功',
|
||||
data: users[userIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const userIndex = users.findIndex(u => u.id === parseInt(id))
|
||||
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
users.splice(userIndex, 1)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 批量删除用户
|
||||
router.delete('/batch', (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的用户ID列表'
|
||||
})
|
||||
}
|
||||
|
||||
users = users.filter(user => !ids.includes(user.id))
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 个用户`
|
||||
})
|
||||
} catch (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 userIndex = users.findIndex(u => u.id === parseInt(id))
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '密码长度不能少于6位'
|
||||
})
|
||||
}
|
||||
|
||||
// 在实际项目中,这里会对密码进行加密
|
||||
users[userIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '重置密码失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户状态
|
||||
router.put('/:id/status', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { status } = req.body
|
||||
|
||||
const userIndex = users.findIndex(u => u.id === parseInt(id))
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!['active', 'inactive', 'banned'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的用户状态'
|
||||
})
|
||||
}
|
||||
|
||||
users[userIndex].status = status
|
||||
users[userIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户状态更新成功',
|
||||
data: users[userIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户状态失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user