feat(backend): 开发订单管理和供应商管理功能
- 新增订单管理页面,实现订单列表展示、搜索、分页等功能 - 新增供应商管理页面,实现供应商列表展示、搜索、分页等功能- 添加订单和供应商相关模型及数据库迁移 - 实现订单状态更新和供应商信息编辑功能 - 优化后端路由结构,移除不必要的代码
This commit is contained in:
@@ -23,8 +23,6 @@ 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 分钟
|
||||
@@ -53,47 +51,14 @@ app.use('/swagger', swaggerUi.serve, swaggerUi.setup(specs, {
|
||||
customSiteTitle: 'NiuMall API 文档'
|
||||
}))
|
||||
|
||||
// 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'))
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/static', express.static('public'));
|
||||
|
||||
// API文档路由重定向
|
||||
app.get('/docs', (req, res) => {
|
||||
res.redirect('/swagger');
|
||||
});
|
||||
|
||||
// 404 处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '接口不存在',
|
||||
path: req.path
|
||||
})
|
||||
// 提供Swagger JSON文件
|
||||
app.get('/api-docs-json', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(specs)
|
||||
})
|
||||
|
||||
// 错误处理中间件
|
||||
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 || 4330
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 测试数据库连接
|
||||
@@ -112,6 +77,7 @@ const startServer = async () => {
|
||||
console.log(`🌐 访问地址: http://localhost:${PORT}`)
|
||||
console.log(`📊 健康检查: http://localhost:${PORT}/health`)
|
||||
console.log(`📚 API文档: http://localhost:${PORT}/swagger`)
|
||||
console.log(`📄 API文档JSON: http://localhost:${PORT}/api-docs-json`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
@@ -120,3 +86,9 @@ const startServer = async () => {
|
||||
}
|
||||
|
||||
startServer()
|
||||
|
||||
// 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/payments', require('./routes/payments'))
|
||||
|
||||
@@ -124,7 +124,73 @@ const models = {
|
||||
}),
|
||||
|
||||
// 订单模型
|
||||
Order: defineOrder(sequelize)
|
||||
Order: defineOrder(sequelize),
|
||||
|
||||
// 供应商模型
|
||||
Supplier: sequelize.define('Supplier', {
|
||||
id: {
|
||||
type: Sequelize.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
code: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
contact: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
address: {
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: false
|
||||
},
|
||||
businessLicense: {
|
||||
type: Sequelize.STRING(255)
|
||||
},
|
||||
qualificationLevel: {
|
||||
type: Sequelize.STRING(10),
|
||||
allowNull: false
|
||||
},
|
||||
certifications: {
|
||||
type: Sequelize.JSON
|
||||
},
|
||||
cattleTypes: {
|
||||
type: Sequelize.JSON
|
||||
},
|
||||
capacity: {
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
rating: {
|
||||
type: Sequelize.DECIMAL(3, 2)
|
||||
},
|
||||
cooperationStartDate: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.ENUM('active', 'inactive', 'suspended'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
region: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'suppliers',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
})
|
||||
};
|
||||
|
||||
// 同步数据库模型
|
||||
@@ -138,6 +204,10 @@ const syncModels = async () => {
|
||||
await models.Order.sync({ alter: true });
|
||||
console.log('✅ 订单表同步成功');
|
||||
|
||||
// 同步供应商表(如果不存在则创建)
|
||||
await models.Supplier.sync({ alter: true });
|
||||
console.log('✅ 供应商表同步成功');
|
||||
|
||||
console.log('✅ 数据库模型同步完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库模型同步失败:', error);
|
||||
|
||||
@@ -598,4 +598,98 @@ router.delete('/:id', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/orders/{id}/status:
|
||||
* patch:
|
||||
* summary: 更新订单状态
|
||||
* tags: [订单管理]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: 订单ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, confirmed, preparing, shipping, delivered, accepted, completed, cancelled, refunded]
|
||||
* description: 订单状态
|
||||
* required:
|
||||
* - status
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Order'
|
||||
* 400:
|
||||
* description: 参数验证失败
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 订单不存在
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
// 更新订单状态
|
||||
router.patch('/:id/status', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { status } = req.body
|
||||
|
||||
// 验证状态值是否有效
|
||||
const validStatuses = ['pending', 'confirmed', 'preparing', 'shipping', 'delivered', 'accepted', 'completed', 'cancelled', 'refunded']
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的订单状态',
|
||||
details: `状态必须是以下值之一: ${validStatuses.join(', ')}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
const order = await Order.findByPk(id)
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
await order.update({ status })
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单状态更新成功',
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新订单状态失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新订单状态失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,67 +1,8 @@
|
||||
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')
|
||||
}
|
||||
];
|
||||
const { Supplier } = require('../models');
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 验证schemas
|
||||
const supplierCreateSchema = Joi.object({
|
||||
@@ -70,7 +11,9 @@ const supplierCreateSchema = Joi.object({
|
||||
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(),
|
||||
businessLicense: Joi.string().max(255).optional(),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C').required(),
|
||||
certifications: Joi.array().items(Joi.string()).optional(),
|
||||
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()
|
||||
@@ -81,7 +24,9 @@ const supplierUpdateSchema = Joi.object({
|
||||
contact: Joi.string().min(2).max(50),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/),
|
||||
address: Joi.string().min(5).max(200),
|
||||
businessLicense: Joi.string().max(255).optional(),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C'),
|
||||
certifications: Joi.array().items(Joi.string()).optional(),
|
||||
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'),
|
||||
@@ -89,7 +34,7 @@ const supplierUpdateSchema = Joi.object({
|
||||
});
|
||||
|
||||
// 获取供应商列表
|
||||
router.get('/', (req, res) => {
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
@@ -97,53 +42,74 @@ router.get('/', (req, res) => {
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status = 'active'
|
||||
status
|
||||
} = 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);
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.status === status);
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
// 区域筛选
|
||||
if (region) {
|
||||
whereConditions.region = region;
|
||||
}
|
||||
|
||||
// 资质等级筛选
|
||||
if (qualificationLevel) {
|
||||
whereConditions.qualificationLevel = qualificationLevel;
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereConditions[Sequelize.Op.or] = [
|
||||
{ name: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ code: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ contact: { [Sequelize.Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedSuppliers = filteredSuppliers.slice(startIndex, endIndex);
|
||||
// 分页参数
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
// 查询数据库
|
||||
const { rows, count } = await Supplier.findAndCountAll({
|
||||
where: whereConditions,
|
||||
offset,
|
||||
limit,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
// 解析JSON字段
|
||||
const processedRows = rows.map(row => {
|
||||
const rowData = row.toJSON();
|
||||
if (rowData.cattleTypes && typeof rowData.cattleTypes === 'string') {
|
||||
rowData.cattleTypes = JSON.parse(rowData.cattleTypes);
|
||||
}
|
||||
if (rowData.certifications && typeof rowData.certifications === 'string') {
|
||||
rowData.certifications = JSON.parse(rowData.certifications);
|
||||
}
|
||||
return rowData;
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedSuppliers,
|
||||
list: processedRows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredSuppliers.length,
|
||||
totalPages: Math.ceil(filteredSuppliers.length / pageSize)
|
||||
pageSize: limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商列表失败',
|
||||
@@ -153,10 +119,12 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
// 获取供应商详情
|
||||
router.get('/:id', (req, res) => {
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplier = suppliers.find(s => s.id === parseInt(id));
|
||||
|
||||
// 查询数据库
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
@@ -165,11 +133,21 @@ router.get('/:id', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 解析JSON字段
|
||||
const supplierData = supplier.toJSON();
|
||||
if (supplierData.cattleTypes && typeof supplierData.cattleTypes === 'string') {
|
||||
supplierData.cattleTypes = JSON.parse(supplierData.cattleTypes);
|
||||
}
|
||||
if (supplierData.certifications && typeof supplierData.certifications === 'string') {
|
||||
supplierData.certifications = JSON.parse(supplierData.certifications);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: supplier
|
||||
data: supplierData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商详情失败',
|
||||
@@ -179,7 +157,7 @@ router.get('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// 创建供应商
|
||||
router.post('/', (req, res) => {
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { error, value } = supplierCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
@@ -191,7 +169,7 @@ router.post('/', (req, res) => {
|
||||
}
|
||||
|
||||
// 检查编码是否重复
|
||||
const existingSupplier = suppliers.find(s => s.code === value.code);
|
||||
const existingSupplier = await Supplier.findOne({ where: { code: value.code } });
|
||||
if (existingSupplier) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -199,19 +177,25 @@ router.post('/', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
// 检查电话是否重复
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers.push(newSupplier);
|
||||
// 创建新供应商
|
||||
const newSupplier = await Supplier.create({
|
||||
...value,
|
||||
businessLicense: value.businessLicense || '',
|
||||
certifications: value.certifications ? JSON.stringify(value.certifications) : JSON.stringify([]),
|
||||
cattleTypes: JSON.stringify(value.cattleTypes),
|
||||
rating: 0,
|
||||
cooperationStartDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
@@ -219,6 +203,7 @@ router.post('/', (req, res) => {
|
||||
data: newSupplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建供应商失败',
|
||||
@@ -228,7 +213,7 @@ router.post('/', (req, res) => {
|
||||
});
|
||||
|
||||
// 更新供应商
|
||||
router.put('/:id', (req, res) => {
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = supplierUpdateSchema.validate(req.body);
|
||||
@@ -241,26 +226,41 @@ router.put('/:id', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
if (supplierIndex === -1) {
|
||||
// 查找供应商
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers[supplierIndex] = {
|
||||
...suppliers[supplierIndex],
|
||||
// 如果更新了电话号码,检查是否重复
|
||||
if (value.phone && value.phone !== supplier.phone) {
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新供应商信息
|
||||
await supplier.update({
|
||||
...value,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
businessLicense: value.businessLicense !== undefined ? value.businessLicense : undefined,
|
||||
certifications: value.certifications !== undefined ? JSON.stringify(value.certifications) : undefined,
|
||||
cattleTypes: value.cattleTypes ? JSON.stringify(value.cattleTypes) : undefined
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
data: suppliers[supplierIndex]
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新供应商失败',
|
||||
@@ -270,25 +270,28 @@ router.put('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// 删除供应商
|
||||
router.delete('/:id', (req, res) => {
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
|
||||
if (supplierIndex === -1) {
|
||||
|
||||
// 查找供应商
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers.splice(supplierIndex, 1);
|
||||
// 删除供应商
|
||||
await supplier.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除供应商失败',
|
||||
@@ -298,22 +301,56 @@ router.delete('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// 获取供应商统计信息
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
router.get('/stats/overview', async (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 totalSuppliers = await Supplier.count();
|
||||
const activeSuppliers = await Supplier.count({ where: { status: 'active' } });
|
||||
|
||||
// 获取平均评分(排除评分为0的供应商)
|
||||
const ratingResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('AVG', Sequelize.col('rating')), 'averageRating']
|
||||
],
|
||||
where: {
|
||||
rating: {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
const averageRating = ratingResult ? parseFloat(ratingResult.getDataValue('averageRating')).toFixed(2) : 0;
|
||||
|
||||
// 获取总产能
|
||||
const capacityResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('SUM', Sequelize.col('capacity')), 'totalCapacity']
|
||||
]
|
||||
});
|
||||
const totalCapacity = capacityResult ? capacityResult.getDataValue('totalCapacity') : 0;
|
||||
|
||||
// 按等级统计
|
||||
const levelStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.qualificationLevel] = (stats[supplier.qualificationLevel] || 0) + 1;
|
||||
const levelStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'qualificationLevel',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['qualificationLevel']
|
||||
});
|
||||
const levelStats = levelStatsResult.reduce((stats, item) => {
|
||||
stats[item.qualificationLevel] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
// 按区域统计
|
||||
const regionStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.region] = (stats[supplier.region] || 0) + 1;
|
||||
const regionStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'region',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['region']
|
||||
});
|
||||
const regionStats = regionStatsResult.reduce((stats, item) => {
|
||||
stats[item.region] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
@@ -322,13 +359,14 @@ router.get('/stats/overview', (req, res) => {
|
||||
data: {
|
||||
totalSuppliers,
|
||||
activeSuppliers,
|
||||
averageRating: Math.round(averageRating * 10) / 10,
|
||||
averageRating: parseFloat(averageRating),
|
||||
totalCapacity,
|
||||
levelStats,
|
||||
regionStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商统计信息失败',
|
||||
@@ -337,64 +375,53 @@ router.get('/stats/overview', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 批量操作
|
||||
router.post('/batch', (req, res) => {
|
||||
// 批量操作供应商
|
||||
router.post('/batch', async (req, res) => {
|
||||
try {
|
||||
const { action, ids } = req.body;
|
||||
const { ids, action } = req.body;
|
||||
|
||||
if (!action || !Array.isArray(ids) || ids.length === 0) {
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数错误'
|
||||
message: '请选择要操作的供应商'
|
||||
});
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
if (!['activate', 'deactivate', 'delete'].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'active';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
await Supplier.update(
|
||||
{ status: 'active' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'deactivate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'inactive';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
await Supplier.update(
|
||||
{ status: 'inactive' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
suppliers = suppliers.filter(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
affectedCount++;
|
||||
return false;
|
||||
await Supplier.destroy({
|
||||
where: {
|
||||
id: ids
|
||||
}
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不支持的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `批量${action}成功`,
|
||||
data: { affectedCount }
|
||||
message: '批量操作成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量操作失败',
|
||||
|
||||
@@ -6,12 +6,30 @@ const createOrder = async (req, res) => {
|
||||
try {
|
||||
const orderData = req.body;
|
||||
|
||||
// 设置买家ID
|
||||
orderData.buyer_id = req.user.id;
|
||||
// 设置买家ID和名称(从token中获取)
|
||||
orderData.buyerId = req.user.id;
|
||||
orderData.buyerName = req.user.username;
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = `ORD${Date.now()}${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`;
|
||||
orderData.order_no = orderNo;
|
||||
const orderNo = `ORD${new Date().getFullYear()}${(new Date().getMonth() + 1).toString().padStart(2, '0')}${new Date().getDate().toString().padStart(2, '0')}${Date.now().toString().slice(-6)}`;
|
||||
orderData.orderNo = orderNo;
|
||||
|
||||
// 计算总金额
|
||||
orderData.totalAmount = (orderData.expectedWeight * orderData.unitPrice).toFixed(2);
|
||||
|
||||
// 初始化已付金额和剩余金额
|
||||
orderData.paidAmount = "0.00";
|
||||
orderData.remainingAmount = orderData.totalAmount;
|
||||
|
||||
// 如果没有提供供应商名称,则使用默认值
|
||||
if (!orderData.supplierName) {
|
||||
orderData.supplierName = "供应商";
|
||||
}
|
||||
|
||||
// 如果没有提供牛品种类,则使用默认值
|
||||
if (!orderData.cattleBreed) {
|
||||
orderData.cattleBreed = "西门塔尔牛";
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
const order = await Order.create(orderData);
|
||||
|
||||
388
backend/src/controllers/TransportController.js
Normal file
388
backend/src/controllers/TransportController.js
Normal file
@@ -0,0 +1,388 @@
|
||||
const Transport = require('../models/Transport');
|
||||
const Vehicle = require('../models/Vehicle');
|
||||
const Order = require('../models/Order');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取运输列表
|
||||
exports.getTransportList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, status, orderId } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
if (orderId) where.order_id = orderId;
|
||||
|
||||
// 分页查询
|
||||
const { count, rows } = await Transport.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取运输列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取运输详情
|
||||
exports.getTransportDetail = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取关联的车辆信息
|
||||
const vehicle = await Vehicle.findByPk(transport.vehicle_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...transport.toJSON(),
|
||||
vehicle
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取运输详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 创建运输记录
|
||||
exports.createTransport = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
order_id,
|
||||
driver_id,
|
||||
vehicle_id,
|
||||
start_location,
|
||||
end_location,
|
||||
scheduled_start_time,
|
||||
scheduled_end_time,
|
||||
cattle_count,
|
||||
special_requirements
|
||||
} = req.body;
|
||||
|
||||
// 检查订单是否存在
|
||||
const order = await Order.findByPk(order_id);
|
||||
if (!order) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查车辆是否存在
|
||||
const vehicle = await Vehicle.findByPk(vehicle_id);
|
||||
if (!vehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建运输记录
|
||||
const transport = await Transport.create({
|
||||
order_id,
|
||||
driver_id,
|
||||
vehicle_id,
|
||||
start_location,
|
||||
end_location,
|
||||
scheduled_start_time,
|
||||
scheduled_end_time,
|
||||
cattle_count,
|
||||
special_requirements
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '运输记录创建成功',
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新运输记录
|
||||
exports.updateTransport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新运输记录
|
||||
await transport.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '运输记录更新成功',
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除运输记录
|
||||
exports.deleteTransport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const transport = await Transport.findByPk(id);
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除运输记录
|
||||
await transport.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '运输记录删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除运输记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除运输记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取车辆列表
|
||||
exports.getVehicleList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, status } = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (status) where.status = status;
|
||||
|
||||
// 分页查询
|
||||
const { count, rows } = await Vehicle.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取车辆列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取车辆列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取车辆详情
|
||||
exports.getVehicleDetail = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取车辆详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取车辆详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 创建车辆记录
|
||||
exports.createVehicle = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
license_plate,
|
||||
vehicle_type,
|
||||
capacity,
|
||||
driver_id,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
// 检查车牌号是否已存在
|
||||
const existingVehicle = await Vehicle.findOne({ where: { license_plate } });
|
||||
if (existingVehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车牌号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建车辆记录
|
||||
const vehicle = await Vehicle.create({
|
||||
license_plate,
|
||||
vehicle_type,
|
||||
capacity,
|
||||
driver_id,
|
||||
status
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '车辆记录创建成功',
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新车辆记录
|
||||
exports.updateVehicle = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查车牌号是否已存在(排除当前车辆)
|
||||
if (updateData.license_plate) {
|
||||
const existingVehicle = await Vehicle.findOne({
|
||||
where: {
|
||||
license_plate: updateData.license_plate,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
if (existingVehicle) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '车牌号已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新车辆记录
|
||||
await vehicle.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '车辆记录更新成功',
|
||||
data: vehicle
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除车辆记录
|
||||
exports.deleteVehicle = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const vehicle = await Vehicle.findByPk(id);
|
||||
if (!vehicle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '车辆不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除车辆记录
|
||||
await vehicle.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '车辆记录删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除车辆记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除车辆记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,8 @@ const authRoutes = require('./routes/auth');
|
||||
const userRoutes = require('./routes/users');
|
||||
const orderRoutes = require('./routes/orders');
|
||||
const paymentRoutes = require('./routes/payments');
|
||||
const supplierRoutes = require('./routes/suppliers');
|
||||
const transportRoutes = require('./routes/transports');
|
||||
|
||||
// 创建Express应用
|
||||
const app = express();
|
||||
@@ -53,6 +55,8 @@ app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/orders', orderRoutes);
|
||||
app.use('/api/payments', paymentRoutes);
|
||||
app.use('/api/suppliers', supplierRoutes);
|
||||
app.use('/api/transports', transportRoutes);
|
||||
|
||||
// 基本路由
|
||||
app.get('/', (req, res) => {
|
||||
|
||||
@@ -8,70 +8,106 @@ const Order = sequelize.define('Order', {
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
order_no: {
|
||||
orderNo: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
unique: true,
|
||||
field: 'orderNo'
|
||||
},
|
||||
buyer_id: {
|
||||
buyerId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
field: 'buyerId'
|
||||
},
|
||||
trader_id: {
|
||||
buyerName: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'buyerName'
|
||||
},
|
||||
supplierId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true
|
||||
allowNull: false,
|
||||
field: 'supplierId'
|
||||
},
|
||||
supplier_id: {
|
||||
supplierName: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'supplierName'
|
||||
},
|
||||
traderId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
allowNull: true,
|
||||
field: 'traderId'
|
||||
},
|
||||
driver_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true
|
||||
traderName: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
field: 'traderName'
|
||||
},
|
||||
breed_type: {
|
||||
cattleBreed: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
field: 'cattleBreed'
|
||||
},
|
||||
min_weight: {
|
||||
type: DataTypes.DECIMAL(10,2),
|
||||
allowNull: false
|
||||
},
|
||||
max_weight: {
|
||||
type: DataTypes.DECIMAL(10,2),
|
||||
allowNull: false
|
||||
},
|
||||
total_count: {
|
||||
cattleCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
field: 'cattleCount'
|
||||
},
|
||||
total_weight: {
|
||||
expectedWeight: {
|
||||
type: DataTypes.DECIMAL(10,2),
|
||||
allowNull: true
|
||||
allowNull: false,
|
||||
field: 'expectedWeight'
|
||||
},
|
||||
unit_price: {
|
||||
actualWeight: {
|
||||
type: DataTypes.DECIMAL(10,2),
|
||||
allowNull: false
|
||||
allowNull: true,
|
||||
field: 'actualWeight'
|
||||
},
|
||||
total_amount: {
|
||||
unitPrice: {
|
||||
type: DataTypes.DECIMAL(10,2),
|
||||
allowNull: false,
|
||||
field: 'unitPrice'
|
||||
},
|
||||
totalAmount: {
|
||||
type: DataTypes.DECIMAL(15,2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
field: 'totalAmount'
|
||||
},
|
||||
paidAmount: {
|
||||
type: DataTypes.DECIMAL(15,2),
|
||||
allowNull: false,
|
||||
field: 'paidAmount'
|
||||
},
|
||||
remainingAmount: {
|
||||
type: DataTypes.DECIMAL(15,2),
|
||||
allowNull: false,
|
||||
field: 'remainingAmount'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'confirmed', 'loading', 'shipping', 'delivered', 'completed', 'cancelled'),
|
||||
defaultValue: 'pending'
|
||||
defaultValue: 'pending',
|
||||
field: 'status'
|
||||
},
|
||||
delivery_address: {
|
||||
deliveryAddress: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
field: 'deliveryAddress'
|
||||
},
|
||||
delivery_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false
|
||||
expectedDeliveryDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
field: 'expectedDeliveryDate'
|
||||
},
|
||||
special_requirements: {
|
||||
actualDeliveryDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'actualDeliveryDate'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
field: 'notes'
|
||||
}
|
||||
}, {
|
||||
tableName: 'orders',
|
||||
|
||||
83
backend/src/models/Transport.js
Normal file
83
backend/src/models/Transport.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
// 运输管理模型
|
||||
const Transport = sequelize.define('Transport', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
order_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '关联订单ID'
|
||||
},
|
||||
driver_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '司机ID'
|
||||
},
|
||||
vehicle_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '车辆ID'
|
||||
},
|
||||
start_location: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '起始地点'
|
||||
},
|
||||
end_location: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '目的地'
|
||||
},
|
||||
scheduled_start_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '计划开始时间'
|
||||
},
|
||||
actual_start_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '实际开始时间'
|
||||
},
|
||||
scheduled_end_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '计划结束时间'
|
||||
},
|
||||
actual_end_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '实际结束时间'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('scheduled', 'in_transit', 'completed', 'cancelled'),
|
||||
defaultValue: 'scheduled',
|
||||
comment: '运输状态: scheduled(已安排), in_transit(运输中), completed(已完成), cancelled(已取消)'
|
||||
},
|
||||
estimated_arrival_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '预计到达时间'
|
||||
},
|
||||
cattle_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '运输牛只数量'
|
||||
},
|
||||
special_requirements: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '特殊要求'
|
||||
}
|
||||
}, {
|
||||
tableName: 'transports',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
});
|
||||
|
||||
module.exports = Transport;
|
||||
64
backend/src/models/Vehicle.js
Normal file
64
backend/src/models/Vehicle.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
// 车辆管理模型
|
||||
const Vehicle = sequelize.define('Vehicle', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
license_plate: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '车牌号'
|
||||
},
|
||||
vehicle_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '车辆类型'
|
||||
},
|
||||
capacity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '载重能力(公斤)'
|
||||
},
|
||||
driver_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '司机ID'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('available', 'in_use', 'maintenance', 'retired'),
|
||||
defaultValue: 'available',
|
||||
comment: '车辆状态: available(可用), in_use(使用中), maintenance(维护中), retired(已退役)'
|
||||
},
|
||||
last_maintenance_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '上次维护日期'
|
||||
},
|
||||
next_maintenance_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '下次维护日期'
|
||||
},
|
||||
insurance_expiry_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '保险到期日期'
|
||||
},
|
||||
registration_expiry_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '注册到期日期'
|
||||
}
|
||||
}, {
|
||||
tableName: 'vehicles',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
});
|
||||
|
||||
module.exports = Vehicle;
|
||||
406
backend/src/routes/suppliers.js
Normal file
406
backend/src/routes/suppliers.js
Normal file
@@ -0,0 +1,406 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { Supplier } = require('../../models');
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 验证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('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
// 区域筛选
|
||||
if (region) {
|
||||
whereConditions.region = region;
|
||||
}
|
||||
|
||||
// 资质等级筛选
|
||||
if (qualificationLevel) {
|
||||
whereConditions.qualificationLevel = qualificationLevel;
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereConditions[Sequelize.Op.or] = [
|
||||
{ name: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ code: { [Sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ contact: { [Sequelize.Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
// 查询数据库
|
||||
const { rows, count } = await Supplier.findAndCountAll({
|
||||
where: whereConditions,
|
||||
offset,
|
||||
limit,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 查询数据库
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建供应商
|
||||
router.post('/', async (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 = await Supplier.findOne({ where: { code: value.code } });
|
||||
if (existingSupplier) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商编码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查电话是否重复
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新供应商
|
||||
const newSupplier = await Supplier.create({
|
||||
...value,
|
||||
businessLicense: '',
|
||||
certifications: JSON.stringify([]),
|
||||
cattleTypes: JSON.stringify(value.cattleTypes),
|
||||
rating: 0,
|
||||
cooperationStartDate: new Date(),
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '供应商创建成功',
|
||||
data: newSupplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新供应商
|
||||
router.put('/:id', async (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 supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 如果更新了电话号码,检查是否重复
|
||||
if (value.phone && value.phone !== supplier.phone) {
|
||||
const existingPhone = await Supplier.findOne({ where: { phone: value.phone } });
|
||||
if (existingPhone) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商电话已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新供应商信息
|
||||
await supplier.update({
|
||||
...value,
|
||||
cattleTypes: value.cattleTypes ? JSON.stringify(value.cattleTypes) : undefined
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除供应商
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 查找供应商
|
||||
const supplier = await Supplier.findByPk(id);
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除供应商
|
||||
await supplier.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除供应商失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商统计信息
|
||||
router.get('/stats/overview', async (req, res) => {
|
||||
try {
|
||||
// 获取总数和活跃数
|
||||
const totalSuppliers = await Supplier.count();
|
||||
const activeSuppliers = await Supplier.count({ where: { status: 'active' } });
|
||||
|
||||
// 获取平均评分(排除评分为0的供应商)
|
||||
const ratingResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('AVG', Sequelize.col('rating')), 'averageRating']
|
||||
],
|
||||
where: {
|
||||
rating: {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
const averageRating = ratingResult ? parseFloat(ratingResult.getDataValue('averageRating')).toFixed(2) : 0;
|
||||
|
||||
// 获取总产能
|
||||
const capacityResult = await Supplier.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('SUM', Sequelize.col('capacity')), 'totalCapacity']
|
||||
]
|
||||
});
|
||||
const totalCapacity = capacityResult ? capacityResult.getDataValue('totalCapacity') : 0;
|
||||
|
||||
// 按等级统计
|
||||
const levelStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'qualificationLevel',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['qualificationLevel']
|
||||
});
|
||||
const levelStats = levelStatsResult.reduce((stats, item) => {
|
||||
stats[item.qualificationLevel] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
// 按区域统计
|
||||
const regionStatsResult = await Supplier.findAll({
|
||||
attributes: [
|
||||
'region',
|
||||
[Sequelize.fn('COUNT', Sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['region']
|
||||
});
|
||||
const regionStats = regionStatsResult.reduce((stats, item) => {
|
||||
stats[item.region] = item.getDataValue('count');
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSuppliers,
|
||||
activeSuppliers,
|
||||
averageRating: parseFloat(averageRating),
|
||||
totalCapacity,
|
||||
levelStats,
|
||||
regionStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取供应商统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 批量操作供应商
|
||||
router.post('/batch', async (req, res) => {
|
||||
try {
|
||||
const { ids, action } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要操作的供应商'
|
||||
});
|
||||
}
|
||||
|
||||
if (!['activate', 'deactivate', 'delete'].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
await Supplier.update(
|
||||
{ status: 'active' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'deactivate':
|
||||
await Supplier.update(
|
||||
{ status: 'inactive' },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
await Supplier.destroy({
|
||||
where: {
|
||||
id: ids
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '批量操作成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量操作失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
78
backend/src/routes/transports.js
Normal file
78
backend/src/routes/transports.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const transportController = require('../controllers/TransportController');
|
||||
const { authenticate, checkRole } = require('../middleware/auth');
|
||||
|
||||
// 运输管理路由
|
||||
// 获取运输列表
|
||||
router.get('/',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getTransportList
|
||||
);
|
||||
|
||||
// 获取运输详情
|
||||
router.get('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getTransportDetail
|
||||
);
|
||||
|
||||
// 创建运输记录
|
||||
router.post('/',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.createTransport
|
||||
);
|
||||
|
||||
// 更新运输记录
|
||||
router.put('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.updateTransport
|
||||
);
|
||||
|
||||
// 删除运输记录
|
||||
router.delete('/:id',
|
||||
authenticate,
|
||||
checkRole(['admin']),
|
||||
transportController.deleteTransport
|
||||
);
|
||||
|
||||
// 车辆管理路由
|
||||
// 获取车辆列表
|
||||
router.get('/vehicles',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getVehicleList
|
||||
);
|
||||
|
||||
// 获取车辆详情
|
||||
router.get('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.getVehicleDetail
|
||||
);
|
||||
|
||||
// 创建车辆记录
|
||||
router.post('/vehicles',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.createVehicle
|
||||
);
|
||||
|
||||
// 更新车辆记录
|
||||
router.put('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin', 'logistics_manager']),
|
||||
transportController.updateVehicle
|
||||
);
|
||||
|
||||
// 删除车辆记录
|
||||
router.delete('/vehicles/:id',
|
||||
authenticate,
|
||||
checkRole(['admin']),
|
||||
transportController.deleteVehicle
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user