548 lines
14 KiB
JavaScript
548 lines
14 KiB
JavaScript
|
|
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;
|