feat(backend): 开发订单管理和供应商管理功能

- 新增订单管理页面,实现订单列表展示、搜索、分页等功能
- 新增供应商管理页面,实现供应商列表展示、搜索、分页等功能- 添加订单和供应商相关模型及数据库迁移
- 实现订单状态更新和供应商信息编辑功能
- 优化后端路由结构,移除不必要的代码
This commit is contained in:
ylweng
2025-09-18 23:51:25 +08:00
parent 8637c05970
commit 5b6b50b60b
21 changed files with 3593 additions and 400 deletions

View File

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

View File

@@ -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: '批量操作失败',