修改管理后台

This commit is contained in:
shenquanyi
2025-09-12 20:08:42 +08:00
parent 39d61c6f9b
commit 80a24c2d60
286 changed files with 75316 additions and 9452 deletions

View File

@@ -65,6 +65,9 @@ publicRoutes.put('/:id/status', alertController.updateAlert);
* 500:
* description: 服务器错误
*/
// 根据养殖场名称搜索预警
router.get('/search', verifyToken, alertController.searchAlertsByFarmName);
router.get('/', (req, res) => {
// 从请求头获取token
const authHeader = req.headers['authorization'];

View File

@@ -1,462 +1,241 @@
/**
* 动物路由
* @file animals.js
* @description 定义动物相关的API路由
* 动物信息路由
*/
const express = require('express');
const router = express.Router();
const animalController = require('../controllers/animalController');
const { verifyToken } = require('../middleware/auth');
const jwt = require('jsonwebtoken');
const { Op } = require('sequelize');
const Animal = require('../models/Animal');
const { verifyToken, requirePermission } = require('../middleware/auth');
// 公开API路由,不需要验证token
// 公开路由,不需要认证
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取所有动物数据
// 公开API获取所有动物列表
publicRoutes.get('/', async (req, res) => {
try {
// 尝试从数据库获取数据
const { Animal, Farm } = require('../models');
const animals = await Animal.findAll({
include: [{ model: Farm, as: 'farm', attributes: ['id', 'name'] }]
});
const { IotCattle } = require('../models');
res.status(200).json({
// 获取所有牛只档案数据
const animals = await IotCattle.findAll({
attributes: [
'id', 'org_id', 'ear_number', 'sex', 'strain', 'varieties', 'cate',
'birth_weight', 'birthday', 'pen_id', 'into_time', 'parity', 'source',
'source_day', 'source_weight', 'weight', 'event', 'event_time',
'lactation_day', 'semen_num', 'is_wear', 'batch_id', 'imgs',
'is_ele_auth', 'is_qua_auth', 'is_delete', 'is_out', 'create_uid',
'create_time', 'algebra', 'colour', 'info_weight', 'descent',
'is_vaccin', 'is_insemination', 'is_insure', 'is_mortgage',
'update_time', 'breed_bull_time', 'level', 'six_weight',
'eighteen_weight', 'twelve_day_weight', 'eighteen_day_weight',
'xxiv_day_weight', 'semen_breed_imgs', 'sell_status',
'weight_calculate_time', 'day_of_birthday', 'user_id'
],
where: {
is_delete: 0, // 只获取未删除的记录
is_out: 0 // 只获取未出栏的记录
},
order: [['create_time', 'DESC']]
});
res.json({
success: true,
data: animals,
source: 'database'
message: '获取动物列表成功'
});
} catch (error) {
console.error('从数据库获取动物列表失败,使用模拟数据:', error.message);
// 数据库不可用时返回模拟数据
const mockAnimals = [
{ id: 1, name: '牛001', type: '肉牛', breed: '西门塔尔牛', age: 2, weight: 450, status: 'healthy', farmId: 1, farm: { id: 1, name: '宁夏农场1' } },
{ id: 2, name: '牛002', type: '肉牛', breed: '安格斯牛', age: 3, weight: 500, status: 'healthy', farmId: 1, farm: { id: 1, name: '宁夏农场1' } },
{ id: 3, name: '羊001', type: '肉羊', breed: '小尾寒羊', age: 1, weight: 70, status: 'sick', farmId: 2, farm: { id: 2, name: '宁夏农场2' } }
];
res.status(200).json({
success: true,
data: mockAnimals,
source: 'mock',
message: '数据库不可用,使用模拟数据'
console.error('获取动物列表失败:', error);
res.status(500).json({
success: false,
message: '获取动物列表失败',
error: error.message
});
}
});
/**
* @swagger
* tags:
* name: Animals
* description: 动物管理API
*/
/**
* @swagger
* /api/animals:
* get:
* summary: 获取所有动物
* tags: [Animals]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 成功获取动物列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/Animal'
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/', (req, res) => {
// 从请求头获取token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失'
});
}
// 获取动物绑定信息
router.get('/binding-info/:collarNumber', async (req, res) => {
try {
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
const { collarNumber } = req.params;
// 将用户信息添加到请求对象中
req.user = decoded;
console.log(`查询项圈编号 ${collarNumber} 的动物绑定信息`);
// 调用控制器方法获取数据
animalController.getAllAnimals(req, res);
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
// 使用新的绑定API逻辑
const { IotJbqClient, IotCattle, Farm, CattlePen, CattleBatch } = require('../models');
// 查询耳标信息
const jbqDevice = await IotJbqClient.findOne({
where: { cid: collarNumber },
attributes: [
'id', 'cid', 'aaid', 'org_id', 'uid', 'time', 'uptime', 'sid',
'walk', 'y_steps', 'r_walk', 'lat', 'lon', 'gps_state', 'voltage',
'temperature', 'temperature_two', 'state', 'type', 'sort', 'ver',
'weight', 'start_time', 'run_days', 'zenowalk', 'zenotime',
'is_read', 'read_end_time', 'bank_userid', 'bank_item_id',
'bank_house', 'bank_lanwei', 'bank_place', 'is_home',
'distribute_time', 'bandge_status', 'is_wear', 'is_temperature',
'source_id', 'expire_time'
]
});
if (!jbqDevice) {
return res.json({
success: false,
message: '访问令牌无效'
message: '未找到指定的耳标设备',
data: null
});
}
// 查询绑定的牛只档案信息(简化版本,不使用关联查询)
const cattleInfo = await IotCattle.findOne({
where: { earNumber: collarNumber },
attributes: [
'id', 'orgId', 'earNumber', 'sex', 'strain', 'varieties', 'cate',
'birthWeight', 'birthday', 'penId', 'intoTime', 'parity', 'source',
'sourceDay', 'sourceWeight', 'weight', 'event', 'eventTime',
'lactationDay', 'semenNum', 'isWear', 'batchId', 'imgs',
'isEleAuth', 'isQuaAuth', 'isDelete', 'isOut', 'createUid',
'createTime', 'algebra', 'colour', 'infoWeight', 'descent',
'isVaccin', 'isInsemination', 'isInsure', 'isMortgage',
'updateTime', 'breedBullTime', 'level', 'sixWeight',
'eighteenWeight', 'twelveDayWeight', 'eighteenDayWeight',
'xxivDayWeight', 'semenBreedImgs', 'sellStatus',
'weightCalculateTime', 'dayOfBirthday'
]
});
if (!cattleInfo) {
return res.json({
success: false,
message: '该耳标未绑定动物,暂无绑定信息',
data: null
});
}
// 返回模拟数据
const mockAnimals = [
{ id: 1, name: '牛001', type: '肉牛', breed: '西门塔尔牛', age: 2, weight: 450, status: 'healthy', farmId: 1, farm: { id: 1, name: '示例养殖场1' } },
{ id: 2, name: '牛002', type: '肉牛', breed: '安格斯牛', age: 3, weight: 500, status: 'healthy', farmId: 1, farm: { id: 1, name: '示例养殖场1' } },
{ id: 3, name: '羊001', type: '肉羊', breed: '小尾寒羊', age: 1, weight: 70, status: 'sick', farmId: 2, farm: { id: 2, name: '示例养殖场2' } }
];
res.status(200).json({
success: true,
data: mockAnimals
});
}
});
/**
* @swagger
* /api/animals/{id}:
* get:
* summary: 获取单个动物
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 动物ID
* responses:
* 200:
* description: 成功获取动物详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/Animal'
* 401:
* description: 未授权
* 404:
* description: 动物不存在
* 500:
* description: 服务器错误
*/
router.get('/:id', (req, res) => {
// 从请求头获取token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失'
});
}
try {
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
// 将用户信息添加到请求对象中
req.user = decoded;
// 调用控制器方法获取数据
animalController.getAnimalById(req, res);
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '访问令牌无效'
});
}
// 返回模拟数据
const animalId = parseInt(req.params.id);
const mockAnimal = {
id: animalId,
name: `动物${animalId}`,
type: animalId % 2 === 0 ? '肉牛' : '肉羊',
breed: animalId % 2 === 0 ? '西门塔尔牛' : '小尾寒羊',
age: Math.floor(Math.random() * 5) + 1,
weight: animalId % 2 === 0 ? Math.floor(Math.random() * 200) + 400 : Math.floor(Math.random() * 50) + 50,
status: Math.random() > 0.7 ? 'sick' : 'healthy',
farmId: Math.ceil(animalId / 3),
farm: { id: Math.ceil(animalId / 3), name: `示例养殖场${Math.ceil(animalId / 3)}` }
// 格式化数据以匹配前端UI需求
const bindingInfo = {
// 基础信息
basicInfo: {
collarNumber: jbqDevice.cid,
category: cattleInfo.cate || '奶牛',
calvingCount: cattleInfo.parity || 0,
earTag: cattleInfo.earNumber,
animalType: cattleInfo.sex === 1 ? '公牛' : cattleInfo.sex === 2 ? '母牛' : '未知',
breed: cattleInfo.varieties || '荷斯坦',
sourceType: cattleInfo.source || '自繁'
},
// 出生信息
birthInfo: {
birthDate: cattleInfo.birthday ? new Date(cattleInfo.birthday * 1000).toISOString().split('T')[0] : '',
birthWeight: cattleInfo.birthWeight ? parseFloat(cattleInfo.birthWeight).toFixed(2) : '0.00',
weaningWeight: cattleInfo.infoWeight ? parseFloat(cattleInfo.infoWeight).toFixed(2) : '0.00',
rightTeatCount: '',
entryDate: cattleInfo.intoTime ? new Date(cattleInfo.intoTime * 1000).toISOString().split('T')[0] : '',
weaningAge: 0,
leftTeatCount: ''
},
// 族谱信息
pedigreeInfo: {
fatherId: cattleInfo.descent || 'F001',
motherId: 'M001',
grandfatherId: 'GF001',
grandmotherId: 'GM001',
bloodline: cattleInfo.algebra || '纯种',
generation: 'F3'
},
// 保险信息
insuranceInfo: {
policyNumber: 'INS2024001',
insuranceCompany: '中国平安',
coverageAmount: '50000',
premium: '500',
startDate: '2024-01-01',
endDate: '2024-12-31',
status: cattleInfo.isInsure ? '有效' : '未投保'
},
// 贷款信息
loanInfo: {
loanNumber: 'LOAN2024001',
bankName: '中国农业银行',
loanAmount: '100000',
interestRate: '4.5%',
loanDate: '2024-01-01',
maturityDate: '2025-01-01',
status: cattleInfo.isMortgage ? '正常' : '无贷款'
},
// 设备信息
deviceInfo: {
deviceId: jbqDevice.id,
batteryLevel: jbqDevice.voltage,
temperature: jbqDevice.temperature,
status: jbqDevice.state === 1 ? '在线' : '离线',
lastUpdate: jbqDevice.uptime ? new Date(jbqDevice.uptime * 1000).toISOString() : '',
location: jbqDevice.lat && jbqDevice.lon ? `${jbqDevice.lat}, ${jbqDevice.lon}` : '无定位'
},
// 农场信息
farmInfo: {
farmName: '未知农场',
farmAddress: '',
penName: '未知栏舍',
batchName: '未知批次'
}
};
res.status(200).json({
res.json({
success: true,
data: mockAnimal
message: '获取绑定信息成功',
data: bindingInfo
});
} catch (error) {
console.error('获取动物绑定信息失败:', error);
res.status(500).json({
success: false,
message: '获取绑定信息失败: ' + error.message,
data: null
});
}
});
/**
* @swagger
* /api/animals:
* post:
* summary: 创建动物
* tags: [Animals]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - type
* - count
* - farmId
* properties:
* type:
* type: string
* description: 动物类型
* count:
* type: integer
* description: 数量
* farmId:
* type: integer
* description: 所属养殖场ID
* health_status:
* type: string
* enum: [healthy, sick, quarantine]
* description: 健康状态
* last_inspection:
* type: string
* format: date-time
* description: 最近检查时间
* notes:
* type: string
* description: 备注
* responses:
* 201:
* description: 动物创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 动物创建成功
* data:
* $ref: '#/components/schemas/Animal'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 养殖场不存在
* 500:
* description: 服务器错误
*/
router.post('/', verifyToken, animalController.createAnimal);
/**
* @swagger
* /api/animals/{id}:
* put:
* summary: 更新动物
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 动物ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* type:
* type: string
* description: 动物类型
* count:
* type: integer
* description: 数量
* farmId:
* type: integer
* description: 所属养殖场ID
* health_status:
* type: string
* enum: [healthy, sick, quarantine]
* description: 健康状态
* last_inspection:
* type: string
* format: date-time
* description: 最近检查时间
* notes:
* type: string
* description: 备注
* responses:
* 200:
* description: 动物更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 动物更新成功
* data:
* $ref: '#/components/schemas/Animal'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 动物不存在或养殖场不存在
* 500:
* description: 服务器错误
*/
router.put('/:id', verifyToken, animalController.updateAnimal);
/**
* @swagger
* /api/animals/{id}:
* delete:
* summary: 删除动物
* tags: [Animals]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: 动物ID
* responses:
* 200:
* description: 动物删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: 动物删除成功
* 401:
* description: 未授权
* 404:
* description: 动物不存在
* 500:
* description: 服务器错误
*/
router.delete('/:id', verifyToken, animalController.deleteAnimal);
/**
* @swagger
* /api/animals/stats/type:
* get:
* summary: 按类型统计动物数量
* tags: [Animals]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 成功获取动物类型统计
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* type: object
* properties:
* type:
* type: string
* example: 牛
* total:
* type: integer
* example: 5000
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/stats/type', (req, res) => {
// 从请求头获取token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失'
});
}
// 获取所有动物列表
router.get('/', async (req, res) => {
try {
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
const { page = 1, limit = 10, search = '' } = req.query;
const offset = (page - 1) * limit;
// 将用户信息添加到请求对象中
req.user = decoded;
// 调用控制器方法获取数据
animalController.getAnimalStatsByType(req, res);
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '访问令牌无效'
});
const whereConditions = {};
if (search) {
whereConditions[Op.or] = [
{ collar_number: { [Op.like]: `%${search}%` } },
{ ear_tag: { [Op.like]: `%${search}%` } }
];
}
// 返回模拟数据
const mockStats = [
{ type: '肉牛', total: 5280 },
{ type: '奶牛', total: 2150 },
{ type: '肉羊', total: 8760 },
{ type: '奶羊', total: 1430 },
{ type: '猪', total: 12500 }
];
const { count, rows } = await Animal.findAndCountAll({
where: whereConditions,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
res.status(200).json({
res.json({
success: true,
data: mockStats
data: rows,
total: count,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: count,
pages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('获取动物列表失败:', error);
res.status(500).json({
success: false,
message: '获取动物列表失败: ' + error.message,
data: null
});
}
});

View File

@@ -1,9 +1,11 @@
const express = require('express');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { User, Role, UserRole } = require('../models');
const { User, Role, UserRole, Permission, MenuPermission } = require('../models');
const { Op } = require('sequelize');
const { verifyToken, checkRole } = require('../middleware/auth');
const { loginAttemptsLimiter, recordLoginFailure, loginRateLimiter } = require('../middleware/security');
const { getRolePermissions, getAccessibleMenus } = require('../config/permissions');
const router = express.Router();
const { body, validationResult } = require('express-validator');
@@ -117,6 +119,9 @@ const { body, validationResult } = require('express-validator');
* description: 服务器错误
*/
router.post('/login',
loginRateLimiter, // 登录频率限制
loginAttemptsLimiter, // 登录失败次数限制
recordLoginFailure, // 记录登录失败
[
body('username').notEmpty().withMessage('用户名不能为空'),
body('password').isLength({ min: 6 }).withMessage('密码长度至少6位')
@@ -145,15 +150,22 @@ router.post('/login',
let user;
try {
// 查找用户(根据用户名或邮箱)
user = await User.findOne({
where: {
[Op.or]: [
{ username: username },
{ email: username }
]
}
});
// 查找用户(根据用户名或邮箱)
console.log('查找用户:', username);
user = await User.findOne({
where: {
[Op.or]: [
{ username: username },
{ email: username }
]
},
include: [{
model: Role,
as: 'role',
attributes: ['id', 'name', 'description']
}]
});
console.log('查询结果:', user ? '用户找到' : '用户未找到');
} catch (error) {
console.log('数据库查询失败,使用测试用户数据');
// 数据库连接失败时的测试用户数据
@@ -182,21 +194,32 @@ router.post('/login',
}
// 比较密码
console.log('开始密码验证...');
console.log('输入密码:', password);
console.log('存储密码哈希:', user.password.substring(0, 30) + '...');
let isPasswordValid;
if (user.password.startsWith('$2b$')) {
if (user.password.startsWith('$2b$') || user.password.startsWith('$2a$')) {
// 使用bcrypt比较
console.log('使用bcrypt比较密码');
isPasswordValid = await bcrypt.compare(password, user.password);
console.log('bcrypt比较结果:', isPasswordValid);
} else {
// 直接比较(用于测试数据)
console.log('使用明文比较密码');
isPasswordValid = password === user.password;
console.log('明文比较结果:', isPasswordValid);
}
if (!isPasswordValid) {
console.log('密码验证失败');
return res.status(401).json({
success: false,
message: '用户名或密码错误'
});
}
console.log('密码验证成功');
// 生成 JWT token
const token = jwt.sign(
@@ -216,11 +239,43 @@ router.post('/login',
email: user.email
};
// 获取用户权限信息 - 从数据库获取实际权限
const userWithPermissions = await User.findByPk(user.id, {
include: [{
model: Role,
as: 'role',
include: [{
model: Permission,
as: 'permissions',
through: { attributes: [] },
attributes: ['permission_key']
}, {
model: MenuPermission,
as: 'menuPermissions',
through: { attributes: [] },
attributes: ['menu_key']
}]
}]
});
// 从数据库获取功能权限
const userPermissions = userWithPermissions.role && userWithPermissions.role.permissions
? userWithPermissions.role.permissions.map(p => p.permission_key)
: [];
// 从数据库获取菜单权限
const accessibleMenus = userWithPermissions.role && userWithPermissions.role.menuPermissions
? userWithPermissions.role.menuPermissions.map(m => m.menu_key)
: [];
res.json({
success: true,
message: '登录成功',
token,
user: userData
user: userData,
role: user.role,
permissions: userPermissions,
accessibleMenus: accessibleMenus
});
} catch (error) {
console.error('登录错误:', error);
@@ -443,9 +498,8 @@ router.get('/me', async (req, res) => {
attributes: ['id', 'username', 'email'],
include: [{
model: Role,
as: 'roles', // 添加as属性指定关联别名
attributes: ['name', 'description'],
through: { attributes: [] } // 不包含中间表字段
as: 'role', // 使用正确的关联别名
attributes: ['name', 'description']
}]
});
} catch (dbError) {
@@ -527,135 +581,7 @@ router.get('/me', async (req, res) => {
* 500:
* description: 服务器错误
*/
router.get('/roles', async (req, res) => {
try {
// 用于测试500错误的参数
if (req.query.forceError === 'true') {
throw new Error('强制触发服务器错误');
}
// 为了测试如果请求中包含test=true参数则返回模拟数据
if (req.query.test === 'true') {
return res.status(200).json({
success: true,
roles: [
{ id: 0, name: 'string', description: 'string' }
]
});
}
// 为了测试403权限不足的情况
if (req.query.testForbidden === 'true') {
return res.status(403).json({
success: false,
message: '权限不足'
});
}
// 从请求头获取token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: '未授权'
});
}
// 验证token
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
} catch (err) {
if (err instanceof jwt.JsonWebTokenError || err instanceof jwt.TokenExpiredError) {
return res.status(401).json({
success: false,
message: '未授权'
});
}
// 如果是其他错误,返回模拟数据
return res.json({
success: true,
roles: [
{ id: 1, name: 'admin', description: '管理员' },
{ id: 2, name: 'user', description: '普通用户' },
{ id: 3, name: 'guest', description: '访客' }
]
});
}
// 检查用户角色
let userRoles;
try {
const user = await User.findByPk(decoded.id, {
include: [{
model: Role,
as: 'roles', // 添加as属性指定关联别名
attributes: ['name']
}]
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户或角色不存在'
});
}
userRoles = user.roles.map(role => role.name);
// 检查用户是否具有admin角色
if (!userRoles.includes('admin')) {
return res.status(403).json({
success: false,
message: '权限不足'
});
}
} catch (dbError) {
console.log('数据库连接失败,使用模拟数据');
// 返回模拟数据
return res.json({
success: true,
roles: [
{ id: 1, name: 'admin', description: '管理员' },
{ id: 2, name: 'user', description: '普通用户' },
{ id: 3, name: 'guest', description: '访客' }
]
});
}
// 获取所有角色
let roles;
try {
roles = await Role.findAll({
attributes: ['id', 'name', 'description']
});
} catch (dbError) {
console.log('数据库连接失败,使用模拟数据');
// 返回模拟数据
return res.json({
success: true,
roles: [
{ id: 1, name: 'admin', description: '管理员' },
{ id: 2, name: 'user', description: '普通用户' },
{ id: 3, name: 'guest', description: '访客' }
]
});
}
res.json({
success: true,
roles
});
} catch (error) {
console.error('获取角色列表错误:', error);
res.status(500).json({
success: false,
message: '服务器错误'
});
}
});
/**
* @swagger
@@ -1131,6 +1057,58 @@ router.delete('/users/:userId/roles/:roleId', async (req, res) => {
* 401:
* description: Token无效或已过期
*/
/**
* @swagger
* /api/auth/roles:
* get:
* summary: 获取所有角色列表
* tags: [Authentication]
* responses:
* 200:
* description: 获取角色列表成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* description:
* type: string
* 500:
* description: 服务器错误
*/
router.get('/roles', async (req, res) => {
try {
const roles = await Role.findAll({
attributes: ['id', 'name', 'description'],
order: [['id', 'ASC']]
});
res.json({
success: true,
data: roles,
roles: roles // 兼容性,同时提供两种格式
});
} catch (error) {
console.error('获取角色列表错误:', error);
res.status(500).json({
success: false,
message: '获取角色列表失败',
error: error.message
});
}
});
router.get('/validate', verifyToken, async (req, res) => {
try {
// 如果能到达这里说明token是有效的
@@ -1138,7 +1116,7 @@ router.get('/validate', verifyToken, async (req, res) => {
attributes: ['id', 'username', 'email', 'status'],
include: [{
model: Role,
as: 'roles',
as: 'role',
attributes: ['id', 'name']
}]
});
@@ -1150,6 +1128,10 @@ router.get('/validate', verifyToken, async (req, res) => {
});
}
// 获取用户权限信息
const userPermissions = user.role ? getRolePermissions(user.role.name) : [];
const accessibleMenus = getAccessibleMenus(userPermissions);
res.json({
success: true,
message: 'Token有效',
@@ -1158,7 +1140,9 @@ router.get('/validate', verifyToken, async (req, res) => {
username: user.username,
email: user.email,
status: user.status,
roles: user.roles
role: user.role,
permissions: userPermissions,
accessibleMenus: accessibleMenus
}
});
} catch (error) {

319
backend/routes/backup.js Normal file
View File

@@ -0,0 +1,319 @@
/**
* 备份管理路由
* @file backup.js
* @description 处理数据备份和恢复请求
*/
const express = require('express');
const { body } = require('express-validator');
const { verifyToken, checkRole } = require('../middleware/auth');
const backupController = require('../controllers/backupController');
const router = express.Router();
/**
* @swagger
* tags:
* name: Backup
* description: 数据备份管理相关接口
*/
/**
* @swagger
* /api/backup/create:
* post:
* summary: 创建数据备份
* tags: [Backup]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* type:
* type: string
* enum: [full, daily, weekly, monthly]
* description: 备份类型
* default: full
* description:
* type: string
* description: 备份描述
* responses:
* 200:
* description: 备份创建成功
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiResponse'
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器错误
*/
router.post('/create',
verifyToken,
checkRole(['admin']),
[
body('type').optional().isIn(['full', 'daily', 'weekly', 'monthly']).withMessage('备份类型无效'),
body('description').optional().isLength({ max: 255 }).withMessage('描述长度不能超过255字符')
],
backupController.createBackup
);
/**
* @swagger
* /api/backup/list:
* get:
* summary: 获取备份列表
* tags: [Backup]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: type
* schema:
* type: string
* enum: [full, daily, weekly, monthly]
* description: 备份类型过滤
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/list', verifyToken, checkRole(['admin']), backupController.getBackupList);
/**
* @swagger
* /api/backup/stats:
* get:
* summary: 获取备份统计信息
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/stats', verifyToken, checkRole(['admin']), backupController.getBackupStats);
/**
* @swagger
* /api/backup/health:
* get:
* summary: 获取备份系统健康状态
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/health', verifyToken, checkRole(['admin']), backupController.getBackupHealth);
/**
* @swagger
* /api/backup/{id}:
* delete:
* summary: 删除备份
* tags: [Backup]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 备份ID
* responses:
* 200:
* description: 删除成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 备份不存在
*/
router.delete('/:id', verifyToken, checkRole(['admin']), backupController.deleteBackup);
/**
* @swagger
* /api/backup/{id}/restore:
* post:
* summary: 恢复数据备份
* tags: [Backup]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 备份ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - confirm
* properties:
* confirm:
* type: boolean
* description: 确认恢复操作
* responses:
* 200:
* description: 恢复成功
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 恢复失败
*/
router.post('/:id/restore',
verifyToken,
checkRole(['admin']),
[
body('confirm').isBoolean().withMessage('confirm必须是布尔值')
],
backupController.restoreBackup
);
/**
* @swagger
* /api/backup/{id}/download:
* get:
* summary: 下载备份文件
* tags: [Backup]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: 备份ID
* responses:
* 200:
* description: 文件下载
* content:
* application/zip:
* schema:
* type: string
* format: binary
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 备份文件不存在
*/
router.get('/:id/download', verifyToken, checkRole(['admin']), backupController.downloadBackup);
/**
* @swagger
* /api/backup/cleanup:
* post:
* summary: 清理过期备份
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 清理成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/cleanup', verifyToken, checkRole(['admin']), backupController.cleanupBackups);
/**
* @swagger
* /api/backup/schedule/start:
* post:
* summary: 启动自动备份调度
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 启动成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/schedule/start', verifyToken, checkRole(['admin']), backupController.startScheduler);
/**
* @swagger
* /api/backup/schedule/stop:
* post:
* summary: 停止自动备份调度
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 停止成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/schedule/stop', verifyToken, checkRole(['admin']), backupController.stopScheduler);
/**
* @swagger
* /api/backup/schedule/status:
* get:
* summary: 获取自动备份调度状态
* tags: [Backup]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/schedule/status', verifyToken, checkRole(['admin']), backupController.getSchedulerStatus);
module.exports = router;

24
backend/routes/binding.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* 绑定信息路由
* @file binding.js
* @description 处理耳标与牛只档案绑定的相关路由
*/
const express = require('express');
const router = express.Router();
const bindingController = require('../controllers/bindingController');
const { verifyToken } = require('../middleware/auth');
// 获取耳标绑定信息
router.get('/info/:cid', verifyToken, bindingController.getBindingInfo);
// 获取绑定状态统计
router.get('/stats', verifyToken, bindingController.getBindingStats);
// 手动绑定耳标与牛只档案
router.post('/bind', verifyToken, bindingController.bindCattle);
// 解绑耳标与牛只档案
router.delete('/unbind/:cid', verifyToken, bindingController.unbindCattle);
module.exports = router;

View File

@@ -0,0 +1,349 @@
const express = require('express');
const router = express.Router();
const cattleBatchController = require('../controllers/cattleBatchController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 所有路由都需要认证
router.use(verifyToken);
/**
* @swagger
* /api/cattle-batches:
* get:
* summary: 获取批次列表
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: status
* schema:
* type: string
* enum: [进行中, 已完成, 已暂停]
* description: 状态筛选
* - in: query
* name: type
* schema:
* type: string
* enum: [育成批次, 繁殖批次, 育肥批次, 隔离批次, 治疗批次]
* description: 类型筛选
* responses:
* 200:
* description: 成功获取批次列表
*/
router.get('/', requirePermission('cattle:batches:view'), cattleBatchController.getBatches);
/**
* @swagger
* /api/cattle-batches/{id}:
* get:
* summary: 获取批次详情
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* responses:
* 200:
* description: 成功获取批次详情
* 404:
* description: 批次不存在
*/
router.get('/:id', requirePermission('cattle:batches:view'), cattleBatchController.getBatchById);
/**
* @swagger
* /api/cattle-batches:
* post:
* summary: 创建批次
* tags: [批次管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 批次名称
* code:
* type: string
* description: 批次编号
* type:
* type: string
* enum: [育成批次, 繁殖批次, 育肥批次, 隔离批次, 治疗批次]
* description: 批次类型
* startDate:
* type: string
* format: date
* description: 开始日期
* expectedEndDate:
* type: string
* format: date
* description: 预计结束日期
* targetCount:
* type: integer
* description: 目标牛只数量
* manager:
* type: string
* description: 负责人
* remark:
* type: string
* description: 备注
* responses:
* 201:
* description: 成功创建批次
* 400:
* description: 请求参数错误
*/
router.post('/', requirePermission('cattle:batches:create'), cattleBatchController.createBatch);
/**
* @swagger
* /api/cattle-batches/{id}:
* put:
* summary: 更新批次
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 批次名称
* code:
* type: string
* description: 批次编号
* type:
* type: string
* enum: [育成批次, 繁殖批次, 育肥批次, 隔离批次, 治疗批次]
* description: 批次类型
* startDate:
* type: string
* format: date
* description: 开始日期
* expectedEndDate:
* type: string
* format: date
* description: 预计结束日期
* actualEndDate:
* type: string
* format: date
* description: 实际结束日期
* targetCount:
* type: integer
* description: 目标牛只数量
* currentCount:
* type: integer
* description: 当前牛只数量
* manager:
* type: string
* description: 负责人
* status:
* type: string
* enum: [进行中, 已完成, 已暂停]
* description: 状态
* remark:
* type: string
* description: 备注
* responses:
* 200:
* description: 成功更新批次
* 404:
* description: 批次不存在
*/
router.put('/:id', requirePermission('cattle:batches:update'), cattleBatchController.updateBatch);
/**
* @swagger
* /api/cattle-batches/{id}:
* delete:
* summary: 删除批次
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* responses:
* 200:
* description: 成功删除批次
* 404:
* description: 批次不存在
* 400:
* description: 批次中还有牛只,无法删除
*/
router.delete('/:id', requirePermission('cattle:batches:delete'), cattleBatchController.deleteBatch);
/**
* @swagger
* /api/cattle-batches/batch-delete:
* post:
* summary: 批量删除批次
* tags: [批次管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 批次ID数组
* responses:
* 200:
* description: 成功批量删除批次
* 400:
* description: 请求参数错误或批次中还有牛只
*/
router.post('/batch-delete', requirePermission('cattle:batches:delete'), cattleBatchController.batchDeleteBatches);
/**
* @swagger
* /api/cattle-batches/{id}/animals:
* get:
* summary: 获取批次中的牛只
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* responses:
* 200:
* description: 成功获取批次牛只
* 404:
* description: 批次不存在
*/
router.get('/:id/animals', requirePermission('cattle:batches:view'), cattleBatchController.getBatchAnimals);
/**
* @swagger
* /api/cattle-batches/{id}/animals:
* post:
* summary: 添加牛只到批次
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* animalIds:
* type: array
* items:
* type: integer
* description: 牛只ID数组
* responses:
* 200:
* description: 成功添加牛只到批次
* 404:
* description: 批次不存在
* 400:
* description: 部分牛只已在该批次中
*/
router.post('/:id/animals', requirePermission('cattle:batches:update'), cattleBatchController.addAnimalsToBatch);
/**
* @swagger
* /api/cattle-batches/{id}/animals/{animalId}:
* delete:
* summary: 从批次中移除牛只
* tags: [批次管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 批次ID
* - in: path
* name: animalId
* required: true
* schema:
* type: integer
* description: 牛只ID
* responses:
* 200:
* description: 成功从批次中移除牛只
* 404:
* description: 牛只不在该批次中
*/
router.delete('/:id/animals/:animalId', requirePermission('cattle:batches:update'), cattleBatchController.removeAnimalFromBatch);
module.exports = router;

View File

@@ -0,0 +1,290 @@
const express = require('express');
const router = express.Router();
const cattleExitRecordController = require('../controllers/cattleExitRecordController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 所有路由都需要认证
router.use(verifyToken);
/**
* @swagger
* /api/cattle-exit-records:
* get:
* summary: 获取离栏记录列表
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: exitReason
* schema:
* type: string
* enum: [出售, 死亡, 淘汰, 转场, 其他]
* description: 离栏原因筛选
* - in: query
* name: status
* schema:
* type: string
* enum: [已确认, 待确认, 已取消]
* description: 状态筛选
* - in: query
* name: dateRange
* schema:
* type: array
* items:
* type: string
* format: date
* description: 日期范围
* responses:
* 200:
* description: 成功获取离栏记录列表
*/
router.get('/', requirePermission('cattle:exit:view'), cattleExitRecordController.getExitRecords);
/**
* @swagger
* /api/cattle-exit-records/{id}:
* get:
* summary: 获取离栏记录详情
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 离栏记录ID
* responses:
* 200:
* description: 成功获取离栏记录详情
* 404:
* description: 离栏记录不存在
*/
router.get('/:id', requirePermission('cattle:exit:view'), cattleExitRecordController.getExitRecordById);
/**
* @swagger
* /api/cattle-exit-records:
* post:
* summary: 创建离栏记录
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* animalId:
* type: integer
* description: 动物ID
* exitDate:
* type: string
* format: date
* description: 离栏日期
* exitReason:
* type: string
* enum: [出售, 死亡, 淘汰, 转场, 其他]
* description: 离栏原因
* originalPenId:
* type: integer
* description: 原栏舍ID
* destination:
* type: string
* description: 去向
* disposalMethod:
* type: string
* enum: [屠宰, 转售, 掩埋, 焚烧, 其他]
* description: 处理方式
* handler:
* type: string
* description: 处理人员
* status:
* type: string
* enum: [已确认, 待确认, 已取消]
* description: 状态
* remark:
* type: string
* description: 备注
* responses:
* 201:
* description: 成功创建离栏记录
* 400:
* description: 请求参数错误
*/
router.post('/', requirePermission('cattle:exit:create'), cattleExitRecordController.createExitRecord);
/**
* @swagger
* /api/cattle-exit-records/{id}:
* put:
* summary: 更新离栏记录
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 离栏记录ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* exitDate:
* type: string
* format: date
* description: 离栏日期
* exitReason:
* type: string
* enum: [出售, 死亡, 淘汰, 转场, 其他]
* description: 离栏原因
* destination:
* type: string
* description: 去向
* disposalMethod:
* type: string
* enum: [屠宰, 转售, 掩埋, 焚烧, 其他]
* description: 处理方式
* handler:
* type: string
* description: 处理人员
* status:
* type: string
* enum: [已确认, 待确认, 已取消]
* description: 状态
* remark:
* type: string
* description: 备注
* responses:
* 200:
* description: 成功更新离栏记录
* 404:
* description: 离栏记录不存在
*/
router.put('/:id', requirePermission('cattle:exit:update'), cattleExitRecordController.updateExitRecord);
/**
* @swagger
* /api/cattle-exit-records/{id}:
* delete:
* summary: 删除离栏记录
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 离栏记录ID
* responses:
* 200:
* description: 成功删除离栏记录
* 404:
* description: 离栏记录不存在
*/
router.delete('/:id', requirePermission('cattle:exit:delete'), cattleExitRecordController.deleteExitRecord);
/**
* @swagger
* /api/cattle-exit-records/batch-delete:
* post:
* summary: 批量删除离栏记录
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 离栏记录ID数组
* responses:
* 200:
* description: 成功批量删除离栏记录
* 400:
* description: 请求参数错误
*/
router.post('/batch-delete', requirePermission('cattle:exit:delete'), cattleExitRecordController.batchDeleteExitRecords);
/**
* @swagger
* /api/cattle-exit-records/{id}/confirm:
* post:
* summary: 确认离栏记录
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 离栏记录ID
* responses:
* 200:
* description: 成功确认离栏记录
* 404:
* description: 离栏记录不存在
* 400:
* description: 记录已确认,无需重复操作
*/
router.post('/:id/confirm', requirePermission('cattle:exit:update'), cattleExitRecordController.confirmExitRecord);
/**
* @swagger
* /api/cattle-exit-records/available-animals:
* get:
* summary: 获取可用的牛只列表
* tags: [离栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* responses:
* 200:
* description: 成功获取可用牛只列表
*/
router.get('/available-animals', requirePermission('cattle:exit:view'), cattleExitRecordController.getAvailableAnimals);
module.exports = router;

View File

@@ -0,0 +1,248 @@
const express = require('express');
const router = express.Router();
const cattlePenController = require('../controllers/cattlePenController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取栏舍列表
publicRoutes.get('/', cattlePenController.getPens);
// 公开获取栏舍详情
publicRoutes.get('/:id', cattlePenController.getPenById);
// 所有其他路由都需要认证
router.use(verifyToken);
/**
* @swagger
* /api/cattle-pens:
* get:
* summary: 获取栏舍列表
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: status
* schema:
* type: string
* enum: [启用, 停用]
* description: 状态筛选
* - in: query
* name: type
* schema:
* type: string
* enum: [育成栏, 产房, 配种栏, 隔离栏, 治疗栏]
* description: 类型筛选
* responses:
* 200:
* description: 成功获取栏舍列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* list:
* type: array
* items:
* $ref: '#/components/schemas/CattlePen'
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* message:
* type: string
*/
router.get('/', requirePermission('cattle:pens:view'), cattlePenController.getPens);
/**
* @swagger
* /api/cattle-pens/{id}:
* get:
* summary: 获取栏舍详情
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* responses:
* 200:
* description: 成功获取栏舍详情
* 404:
* description: 栏舍不存在
*/
router.get('/:id', requirePermission('cattle:pens:view'), cattlePenController.getPenById);
/**
* @swagger
* /api/cattle-pens:
* post:
* summary: 创建栏舍
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CattlePenInput'
* responses:
* 201:
* description: 成功创建栏舍
* 400:
* description: 请求参数错误
*/
router.post('/', requirePermission('cattle:pens:create'), cattlePenController.createPen);
/**
* @swagger
* /api/cattle-pens/{id}:
* put:
* summary: 更新栏舍
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CattlePenInput'
* responses:
* 200:
* description: 成功更新栏舍
* 404:
* description: 栏舍不存在
*/
router.put('/:id', requirePermission('cattle:pens:update'), cattlePenController.updatePen);
/**
* @swagger
* /api/cattle-pens/{id}:
* delete:
* summary: 删除栏舍
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* responses:
* 200:
* description: 成功删除栏舍
* 404:
* description: 栏舍不存在
* 400:
* description: 栏舍中还有牛只,无法删除
*/
router.delete('/:id', requirePermission('cattle:pens:delete'), cattlePenController.deletePen);
/**
* @swagger
* /api/cattle-pens/batch-delete:
* post:
* summary: 批量删除栏舍
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 栏舍ID数组
* responses:
* 200:
* description: 成功批量删除栏舍
* 400:
* description: 请求参数错误或栏舍中还有牛只
*/
router.post('/batch-delete', requirePermission('cattle:pens:delete'), cattlePenController.batchDeletePens);
/**
* @swagger
* /api/cattle-pens/{id}/animals:
* get:
* summary: 获取栏舍中的牛只
* tags: [栏舍管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* responses:
* 200:
* description: 成功获取栏舍牛只
* 404:
* description: 栏舍不存在
*/
router.get('/:id/animals', requirePermission('cattle:pens:view'), cattlePenController.getPenAnimals);
module.exports = router;

View File

@@ -0,0 +1,258 @@
const express = require('express');
const router = express.Router();
const cattleTransferRecordController = require('../controllers/cattleTransferRecordController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 所有路由都需要认证
router.use(verifyToken);
/**
* @swagger
* /api/cattle-transfer-records:
* get:
* summary: 获取转栏记录列表
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: fromPen
* schema:
* type: integer
* description: 转出栏舍ID
* - in: query
* name: toPen
* schema:
* type: integer
* description: 转入栏舍ID
* - in: query
* name: dateRange
* schema:
* type: array
* items:
* type: string
* format: date
* description: 日期范围
* responses:
* 200:
* description: 成功获取转栏记录列表
*/
router.get('/', requirePermission('cattle:transfer:view'), cattleTransferRecordController.getTransferRecords);
/**
* @swagger
* /api/cattle-transfer-records/{id}:
* get:
* summary: 获取转栏记录详情
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 转栏记录ID
* responses:
* 200:
* description: 成功获取转栏记录详情
* 404:
* description: 转栏记录不存在
*/
router.get('/:id', requirePermission('cattle:transfer:view'), cattleTransferRecordController.getTransferRecordById);
/**
* @swagger
* /api/cattle-transfer-records:
* post:
* summary: 创建转栏记录
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* animalId:
* type: integer
* description: 动物ID
* fromPenId:
* type: integer
* description: 转出栏舍ID
* toPenId:
* type: integer
* description: 转入栏舍ID
* transferDate:
* type: string
* format: date
* description: 转栏日期
* reason:
* type: string
* enum: [正常调栏, 疾病治疗, 配种需要, 产房准备, 隔离观察, 其他]
* description: 转栏原因
* operator:
* type: string
* description: 操作人员
* status:
* type: string
* enum: [已完成, 进行中]
* description: 状态
* remark:
* type: string
* description: 备注
* responses:
* 201:
* description: 成功创建转栏记录
* 400:
* description: 请求参数错误
*/
router.post('/', requirePermission('cattle:transfer:create'), cattleTransferRecordController.createTransferRecord);
/**
* @swagger
* /api/cattle-transfer-records/{id}:
* put:
* summary: 更新转栏记录
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 转栏记录ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* fromPenId:
* type: integer
* description: 转出栏舍ID
* toPenId:
* type: integer
* description: 转入栏舍ID
* transferDate:
* type: string
* format: date
* description: 转栏日期
* reason:
* type: string
* enum: [正常调栏, 疾病治疗, 配种需要, 产房准备, 隔离观察, 其他]
* description: 转栏原因
* operator:
* type: string
* description: 操作人员
* status:
* type: string
* enum: [已完成, 进行中]
* description: 状态
* remark:
* type: string
* description: 备注
* responses:
* 200:
* description: 成功更新转栏记录
* 404:
* description: 转栏记录不存在
*/
router.put('/:id', requirePermission('cattle:transfer:update'), cattleTransferRecordController.updateTransferRecord);
/**
* @swagger
* /api/cattle-transfer-records/{id}:
* delete:
* summary: 删除转栏记录
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 转栏记录ID
* responses:
* 200:
* description: 成功删除转栏记录
* 404:
* description: 转栏记录不存在
*/
router.delete('/:id', requirePermission('cattle:transfer:delete'), cattleTransferRecordController.deleteTransferRecord);
/**
* @swagger
* /api/cattle-transfer-records/batch-delete:
* post:
* summary: 批量删除转栏记录
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 转栏记录ID数组
* responses:
* 200:
* description: 成功批量删除转栏记录
* 400:
* description: 请求参数错误
*/
router.post('/batch-delete', requirePermission('cattle:transfer:delete'), cattleTransferRecordController.batchDeleteTransferRecords);
/**
* @swagger
* /api/cattle-transfer-records/available-animals:
* get:
* summary: 获取可用的牛只列表
* tags: [转栏记录管理]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* responses:
* 200:
* description: 成功获取可用牛只列表
*/
router.get('/available-animals', requirePermission('cattle:transfer:view'), cattleTransferRecordController.getAvailableAnimals);
module.exports = router;

View File

@@ -71,6 +71,9 @@ publicRoutes.get('/', deviceController.getAllDevices);
*/
router.get('/', verifyToken, deviceController.getAllDevices);
// 根据设备名称搜索设备
router.get('/search', verifyToken, deviceController.searchDevicesByName);
/**
* @swagger
* /api/devices/{id}:

View File

@@ -0,0 +1,460 @@
/**
* 电子围栏坐标点路由
* 处理围栏坐标点的API请求
*/
const express = require('express');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
const electronicFencePointController = require('../controllers/electronicFencePointController');
const router = express.Router();
// 应用认证中间件
router.use(verifyToken);
/**
* @swagger
* tags:
* name: ElectronicFencePoints
* description: 电子围栏坐标点管理
*/
/**
* @swagger
* /api/electronic-fence-points/fence/{fenceId}:
* get:
* summary: 获取围栏的所有坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fenceId
* required: true
* schema:
* type: integer
* description: 围栏ID
* - in: query
* name: point_type
* schema:
* type: string
* enum: [corner, control, marker]
* description: 坐标点类型
* - in: query
* name: is_active
* schema:
* type: boolean
* description: 是否激活
* responses:
* 200:
* description: 获取成功
* 404:
* description: 围栏不存在
* 500:
* description: 服务器错误
*/
router.get('/fence/:fenceId',
requirePermission('smart_fence:view'),
electronicFencePointController.getFencePoints
);
/**
* @swagger
* /api/electronic-fence-points/{id}:
* get:
* summary: 获取坐标点详情
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 坐标点ID
* responses:
* 200:
* description: 获取成功
* 404:
* description: 坐标点不存在
* 500:
* description: 服务器错误
*/
router.get('/:id',
requirePermission('smart_fence:view'),
electronicFencePointController.getPointById
);
/**
* @swagger
* /api/electronic-fence-points:
* post:
* summary: 创建坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - fence_id
* - point_order
* - longitude
* - latitude
* properties:
* fence_id:
* type: integer
* description: 围栏ID
* point_order:
* type: integer
* description: 坐标点顺序
* longitude:
* type: number
* description: 经度
* latitude:
* type: number
* description: 纬度
* point_type:
* type: string
* enum: [corner, control, marker]
* default: corner
* description: 坐标点类型
* description:
* type: string
* description: 坐标点描述
* responses:
* 201:
* description: 创建成功
* 400:
* description: 请求参数错误
* 404:
* description: 围栏不存在
* 500:
* description: 服务器错误
*/
router.post('/',
requirePermission('smart_fence:create'),
electronicFencePointController.createPoint
);
/**
* @swagger
* /api/electronic-fence-points/batch:
* post:
* summary: 批量创建坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - fence_id
* - points
* properties:
* fence_id:
* type: integer
* description: 围栏ID
* points:
* type: array
* items:
* type: object
* properties:
* lng:
* type: number
* description: 经度
* lat:
* type: number
* description: 纬度
* type:
* type: string
* enum: [corner, control, marker]
* description: 坐标点类型
* description:
* type: string
* description: 坐标点描述
* responses:
* 201:
* description: 创建成功
* 400:
* description: 请求参数错误
* 404:
* description: 围栏不存在
* 500:
* description: 服务器错误
*/
router.post('/batch',
requirePermission('smart_fence:create'),
electronicFencePointController.createPoints
);
/**
* @swagger
* /api/electronic-fence-points/{id}:
* put:
* summary: 更新坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 坐标点ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* point_order:
* type: integer
* description: 坐标点顺序
* longitude:
* type: number
* description: 经度
* latitude:
* type: number
* description: 纬度
* point_type:
* type: string
* enum: [corner, control, marker]
* description: 坐标点类型
* description:
* type: string
* description: 坐标点描述
* is_active:
* type: boolean
* description: 是否激活
* responses:
* 200:
* description: 更新成功
* 404:
* description: 坐标点不存在
* 500:
* description: 服务器错误
*/
router.put('/:id',
requirePermission('smart_fence:update'),
electronicFencePointController.updatePoint
);
/**
* @swagger
* /api/electronic-fence-points/fence/{fenceId}:
* put:
* summary: 更新围栏的所有坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fenceId
* required: true
* schema:
* type: integer
* description: 围栏ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - points
* properties:
* points:
* type: array
* items:
* type: object
* properties:
* lng:
* type: number
* description: 经度
* lat:
* type: number
* description: 纬度
* type:
* type: string
* enum: [corner, control, marker]
* description: 坐标点类型
* description:
* type: string
* description: 坐标点描述
* responses:
* 200:
* description: 更新成功
* 400:
* description: 请求参数错误
* 404:
* description: 围栏不存在
* 500:
* description: 服务器错误
*/
router.put('/fence/:fenceId',
requirePermission('smart_fence:update'),
electronicFencePointController.updateFencePoints
);
/**
* @swagger
* /api/electronic-fence-points/{id}:
* delete:
* summary: 删除坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 坐标点ID
* responses:
* 200:
* description: 删除成功
* 404:
* description: 坐标点不存在
* 500:
* description: 服务器错误
*/
router.delete('/:id',
requirePermission('smart_fence:delete'),
electronicFencePointController.deletePoint
);
/**
* @swagger
* /api/electronic-fence-points/fence/{fenceId}:
* delete:
* summary: 删除围栏的所有坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fenceId
* required: true
* schema:
* type: integer
* description: 围栏ID
* responses:
* 200:
* description: 删除成功
* 500:
* description: 服务器错误
*/
router.delete('/fence/:fenceId',
requirePermission('smart_fence:delete'),
electronicFencePointController.deleteFencePoints
);
/**
* @swagger
* /api/electronic-fence-points/fence/{fenceId}/bounds:
* get:
* summary: 获取围栏边界框
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fenceId
* required: true
* schema:
* type: integer
* description: 围栏ID
* responses:
* 200:
* description: 获取成功
* 404:
* description: 围栏没有坐标点
* 500:
* description: 服务器错误
*/
router.get('/fence/:fenceId/bounds',
requirePermission('smart_fence:view'),
electronicFencePointController.getFenceBounds
);
/**
* @swagger
* /api/electronic-fence-points/search:
* get:
* summary: 搜索坐标点
* tags: [ElectronicFencePoints]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: fence_id
* schema:
* type: integer
* description: 围栏ID
* - in: query
* name: point_type
* schema:
* type: string
* enum: [corner, control, marker]
* description: 坐标点类型
* - in: query
* name: longitude_min
* schema:
* type: number
* description: 最小经度
* - in: query
* name: longitude_max
* schema:
* type: number
* description: 最大经度
* - in: query
* name: latitude_min
* schema:
* type: number
* description: 最小纬度
* - in: query
* name: latitude_max
* schema:
* type: number
* description: 最大纬度
* - in: query
* name: description
* schema:
* type: string
* description: 描述关键词
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* responses:
* 200:
* description: 搜索成功
* 500:
* description: 服务器错误
*/
router.get('/search',
requirePermission('smart_fence:view'),
electronicFencePointController.searchPoints
);
module.exports = router;

View File

@@ -0,0 +1,390 @@
const express = require('express')
const router = express.Router()
const electronicFenceController = require('../controllers/electronicFenceController')
const { verifyToken } = require('../middleware/auth')
const { requirePermission } = require('../middleware/permission')
// 应用认证中间件
router.use(verifyToken)
/**
* @swagger
* tags:
* name: ElectronicFence
* description: 电子围栏管理
*/
/**
* @swagger
* /api/electronic-fence:
* get:
* summary: 获取围栏列表
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: type
* schema:
* type: string
* enum: [collector, grazing, safety]
* description: 围栏类型
* - in: query
* name: status
* schema:
* type: string
* enum: [grazing, not_grazing]
* description: 放牧状态
* - in: query
* name: farm_id
* schema:
* type: integer
* description: 农场ID
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* type:
* type: string
* coordinates:
* type: array
* items:
* type: object
* properties:
* lng:
* type: number
* lat:
* type: number
* grazingStatus:
* type: string
* insideCount:
* type: integer
* outsideCount:
* type: integer
* total:
* type: integer
* page:
* type: integer
* limit:
* type: integer
*/
router.get('/',
requirePermission('smart_fence:view'),
electronicFenceController.getFences
)
/**
* @swagger
* /api/electronic-fence/{id}:
* get:
* summary: 获取围栏详情
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 围栏ID
* responses:
* 200:
* description: 获取成功
* 404:
* description: 围栏不存在
*/
router.get('/:id',
requirePermission('smart_fence:view'),
electronicFenceController.getFenceById
)
/**
* @swagger
* /api/electronic-fence:
* post:
* summary: 创建围栏
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - coordinates
* properties:
* name:
* type: string
* description: 围栏名称
* type:
* type: string
* enum: [collector, grazing, safety]
* default: collector
* description: 围栏类型
* description:
* type: string
* description: 围栏描述
* coordinates:
* type: array
* items:
* type: object
* properties:
* lng:
* type: number
* lat:
* type: number
* description: 围栏坐标点数组
* farm_id:
* type: integer
* description: 关联农场ID
* responses:
* 201:
* description: 创建成功
* 400:
* description: 参数错误
*/
router.post('/',
requirePermission('smart_fence:create'),
electronicFenceController.createFence
)
/**
* @swagger
* /api/electronic-fence/{id}:
* put:
* summary: 更新围栏
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 围栏ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* type:
* type: string
* enum: [collector, grazing, safety]
* description:
* type: string
* coordinates:
* type: array
* items:
* type: object
* properties:
* lng:
* type: number
* lat:
* type: number
* grazing_status:
* type: string
* enum: [grazing, not_grazing]
* responses:
* 200:
* description: 更新成功
* 404:
* description: 围栏不存在
*/
router.put('/:id',
requirePermission('smart_fence:update'),
electronicFenceController.updateFence
)
/**
* @swagger
* /api/electronic-fence/{id}:
* delete:
* summary: 删除围栏
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 围栏ID
* responses:
* 200:
* description: 删除成功
* 404:
* description: 围栏不存在
*/
router.delete('/:id',
requirePermission('smart_fence:delete'),
electronicFenceController.deleteFence
)
/**
* @swagger
* /api/electronic-fence/{id}/stats:
* put:
* summary: 更新围栏统计信息
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 围栏ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* inside_count:
* type: integer
* description: 安全区域内动物数量
* outside_count:
* type: integer
* description: 安全区域外动物数量
* responses:
* 200:
* description: 更新成功
* 404:
* description: 围栏不存在
*/
router.put('/:id/stats',
requirePermission('smart_fence:update'),
electronicFenceController.updateFenceStats
)
/**
* @swagger
* /api/electronic-fence/{id}/check-point:
* get:
* summary: 检查点是否在围栏内
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 围栏ID
* - in: query
* name: lng
* required: true
* schema:
* type: number
* description: 经度
* - in: query
* name: lat
* required: true
* schema:
* type: number
* description: 纬度
* responses:
* 200:
* description: 检查完成
* 400:
* description: 参数错误
* 404:
* description: 围栏不存在
*/
router.get('/:id/check-point',
requirePermission('smart_fence:view'),
electronicFenceController.checkPointInFence
)
/**
* @swagger
* /api/electronic-fence/stats/overview:
* get:
* summary: 获取围栏统计概览
* tags: [ElectronicFence]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* totalFences:
* type: integer
* totalInside:
* type: integer
* totalOutside:
* type: integer
* byType:
* type: array
* items:
* type: object
* properties:
* type:
* type: string
* count:
* type: integer
* total_inside:
* type: integer
* total_outside:
* type: integer
*/
router.get('/stats/overview',
requirePermission('smart_fence:view'),
electronicFenceController.getFenceStats
)
module.exports = router

View File

@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const farmController = require('../controllers/farmController');
const searchLogger = require('../middleware/search-logger');
/**
* @swagger
@@ -25,6 +26,9 @@ const farmController = require('../controllers/farmController');
*/
router.get('/', farmController.getAllFarms);
// 根据养殖场名称搜索养殖场(添加搜索监听中间件)
router.get('/search', searchLogger, farmController.searchFarmsByName);
// 公共路由必须在参数路由之前定义
router.get('/public', farmController.getAllFarms);
@@ -136,7 +140,7 @@ router.delete('/:id', farmController.deleteFarm);
* 404:
* description: 养殖场不存在
*/
router.get('/:id/animals', farmController.getFarmAnimals);
// router.get('/:id/animals', farmController.getFarmAnimals); // 暂时注释,方法不存在
/**
* @swagger
@@ -157,7 +161,7 @@ router.get('/:id/animals', farmController.getFarmAnimals);
* 404:
* description: 养殖场不存在
*/
router.get('/:id/devices', farmController.getFarmDevices);
// router.get('/:id/devices', farmController.getFarmDevices); // 暂时注释,方法不存在
// 公共农场数据接口(保留兼容性)
module.exports = router;

View File

@@ -0,0 +1,24 @@
const express = require('express')
const router = express.Router()
const FormLogController = require('../controllers/formLogController')
const { verifyToken } = require('../middleware/auth')
// 添加表单日志
router.post('/add', verifyToken, (req, res) => FormLogController.addFormLog(req, res))
// 获取表单日志列表
router.get('/list', verifyToken, (req, res) => FormLogController.getFormLogs(req, res))
// 获取日志详情
router.get('/detail/:id', verifyToken, (req, res) => FormLogController.getFormLogDetail(req, res))
// 删除日志
router.delete('/delete/:id', verifyToken, (req, res) => FormLogController.deleteFormLog(req, res))
// 批量删除日志
router.delete('/batch-delete', verifyToken, (req, res) => FormLogController.batchDeleteFormLogs(req, res))
// 获取日志统计信息
router.get('/stats', verifyToken, (req, res) => FormLogController.getFormLogStats(req, res))
module.exports = router

View File

@@ -0,0 +1,94 @@
/**
* 牛只档案路由 - 基于iot_cattle表
*/
const express = require('express');
const multer = require('multer');
const router = express.Router();
const iotCattleController = require('../controllers/iotCattleController');
const { verifyToken, checkRole } = require('../middleware/auth');
// 配置文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/') // 上传文件保存目录
},
filename: function (req, file, cb) {
// 生成唯一文件名
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + '.' + file.originalname.split('.').pop());
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 限制文件大小为10MB
},
fileFilter: function (req, file, cb) {
// 只允许Excel文件
const allowedTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('只允许上传Excel文件(.xlsx或.xls格式)'), false);
}
}
});
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取牛只档案列表
publicRoutes.get('/', iotCattleController.getCattleArchives);
// 公开获取单个牛只档案详情
publicRoutes.get('/:id', iotCattleController.getCattleArchiveById);
// 公开下载导入模板
publicRoutes.get('/import/template', iotCattleController.downloadImportTemplate);
// 公开获取栏舍列表
publicRoutes.get('/pens/list', iotCattleController.getPens);
// 公开获取批次列表
publicRoutes.get('/batches/list', iotCattleController.getBatches);
// 获取牛只档案列表
router.get('/', verifyToken, iotCattleController.getCattleArchives);
// 获取单个牛只档案详情
router.get('/:id', verifyToken, iotCattleController.getCattleArchiveById);
// 创建牛只档案
router.post('/', verifyToken, checkRole(['admin', 'manager']), iotCattleController.createCattleArchive);
// 更新牛只档案
router.put('/:id', verifyToken, checkRole(['admin', 'manager']), iotCattleController.updateCattleArchive);
// 删除牛只档案
router.delete('/:id', verifyToken, checkRole(['admin', 'manager']), iotCattleController.deleteCattleArchive);
// 批量删除牛只档案
router.delete('/', verifyToken, checkRole(['admin', 'manager']), iotCattleController.batchDeleteCattleArchives);
// 获取农场列表
router.get('/farms/list', verifyToken, iotCattleController.getFarms);
// 获取栏舍列表
router.get('/pens/list', verifyToken, iotCattleController.getPens);
// 获取批次列表
router.get('/batches/list', verifyToken, iotCattleController.getBatches);
// 导入牛只档案数据
router.post('/import', verifyToken, checkRole(['admin', 'manager']), upload.single('file'), iotCattleController.importCattleArchives);
// 下载导入模板
router.get('/import/template', verifyToken, iotCattleController.downloadImportTemplate);
module.exports = router;

317
backend/routes/menus.js Normal file
View File

@@ -0,0 +1,317 @@
/**
* 菜单管理路由
* @file menus.js
* @description 定义菜单管理相关的API路由
*/
const express = require('express');
const router = express.Router();
const menuController = require('../controllers/menuController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取角色菜单权限(用于权限管理)
publicRoutes.get('/role/:roleId', menuController.getRoleMenus);
// 所有其他路由都需要认证
router.use(verifyToken);
// 需要认证的菜单API
router.get('/', menuController.getAllMenus);
router.get('/:id', menuController.getMenuById);
/**
* @swagger
* components:
* schemas:
* MenuPermission:
* type: object
* properties:
* id:
* type: integer
* description: 菜单ID
* name:
* type: string
* description: 菜单名称
* path:
* type: string
* description: 菜单路径
* component:
* type: string
* description: 组件路径
* icon:
* type: string
* description: 图标
* parent_id:
* type: integer
* description: 父菜单ID
* sort_order:
* type: integer
* description: 排序
* status:
* type: boolean
* description: 状态
* description:
* type: string
* description: 描述
* permission_code:
* type: string
* description: 权限代码
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/
/**
* @swagger
* /api/menus:
* get:
* summary: 获取菜单列表
* tags: [Menus]
* parameters:
* - in: query
* name: roleId
* schema:
* type: integer
* description: 角色ID可选
* responses:
* 200:
* description: 成功获取菜单列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/MenuPermission'
* message:
* type: string
*/
router.get('/', requirePermission('menu:view'), menuController.getAllMenus);
/**
* @swagger
* /api/menus/{id}:
* get:
* summary: 获取菜单详情
* tags: [Menus]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 菜单ID
* responses:
* 200:
* description: 成功获取菜单详情
* 404:
* description: 菜单不存在
*/
router.get('/:id', requirePermission('menu:view'), menuController.getMenuById);
/**
* @swagger
* /api/menus:
* post:
* summary: 创建菜单
* tags: [Menus]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - path
* properties:
* name:
* type: string
* description: 菜单名称
* path:
* type: string
* description: 菜单路径
* component:
* type: string
* description: 组件路径
* icon:
* type: string
* description: 图标
* parent_id:
* type: integer
* description: 父菜单ID
* sort_order:
* type: integer
* description: 排序
* status:
* type: boolean
* description: 状态
* description:
* type: string
* description: 描述
* permission_code:
* type: string
* description: 权限代码
* responses:
* 201:
* description: 菜单创建成功
* 400:
* description: 请求参数错误
*/
router.post('/', requirePermission('menu:create'), menuController.createMenu);
/**
* @swagger
* /api/menus/{id}:
* put:
* summary: 更新菜单
* tags: [Menus]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 菜单ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 菜单名称
* path:
* type: string
* description: 菜单路径
* component:
* type: string
* description: 组件路径
* icon:
* type: string
* description: 图标
* parent_id:
* type: integer
* description: 父菜单ID
* sort_order:
* type: integer
* description: 排序
* status:
* type: boolean
* description: 状态
* description:
* type: string
* description: 描述
* permission_code:
* type: string
* description: 权限代码
* responses:
* 200:
* description: 菜单更新成功
* 404:
* description: 菜单不存在
*/
router.put('/:id', requirePermission('menu:update'), menuController.updateMenu);
/**
* @swagger
* /api/menus/{id}:
* delete:
* summary: 删除菜单
* tags: [Menus]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 菜单ID
* responses:
* 200:
* description: 菜单删除成功
* 404:
* description: 菜单不存在
*/
router.delete('/:id', requirePermission('menu:delete'), menuController.deleteMenu);
/**
* @swagger
* /api/menus/role/{roleId}:
* get:
* summary: 获取角色的菜单权限
* tags: [Menus]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* description: 角色ID
* responses:
* 200:
* description: 成功获取角色菜单权限
* 404:
* description: 角色不存在
*/
router.get('/role/:roleId', requirePermission('menu:view'), menuController.getRoleMenus);
/**
* @swagger
* /api/menus/role/{roleId}:
* post:
* summary: 设置角色的菜单权限
* tags: [Menus]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* description: 角色ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* menuIds:
* type: array
* items:
* type: integer
* description: 菜单ID数组
* responses:
* 200:
* description: 设置角色菜单权限成功
* 404:
* description: 角色不存在
*/
router.post('/role/:roleId', requirePermission('menu:assign'), menuController.setRoleMenus);
module.exports = router;

View File

@@ -0,0 +1,586 @@
/**
* 操作日志路由
* @file operationLogs.js
* @description 定义操作日志相关的API路由
*/
const express = require('express');
const router = express.Router();
const { body, query, param } = require('express-validator');
const operationLogController = require('../controllers/operationLogController');
const auth = require('../middleware/auth');
const { hasPermission } = require('../config/permissions');
// 验证中间件
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '请求参数验证失败',
errors: errors.array()
});
}
next();
};
/**
* @swagger
* components:
* schemas:
* OperationLog:
* type: object
* properties:
* id:
* type: integer
* description: 主键ID
* user_id:
* type: integer
* description: 操作用户ID
* username:
* type: string
* description: 操作用户名
* user_role:
* type: string
* description: 操作用户角色
* operation_type:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* module_name:
* type: string
* description: 操作模块名称
* table_name:
* type: string
* description: 操作的数据表名
* record_id:
* type: integer
* description: 操作的记录ID
* operation_desc:
* type: string
* description: 操作描述
* old_data:
* type: object
* description: 操作前的数据
* new_data:
* type: object
* description: 操作后的数据
* ip_address:
* type: string
* description: 操作IP地址
* user_agent:
* type: string
* description: 用户代理信息
* request_url:
* type: string
* description: 请求URL
* request_method:
* type: string
* description: 请求方法
* response_status:
* type: integer
* description: 响应状态码
* execution_time:
* type: integer
* description: 执行时间(毫秒)
* error_message:
* type: string
* description: 错误信息
* created_at:
* type: string
* format: date-time
* description: 创建时间
*/
/**
* @swagger
* /api/operation-logs:
* get:
* summary: 获取操作日志列表
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 20
* description: 每页记录数
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: operationType
* schema:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称
* - in: query
* name: tableName
* schema:
* type: string
* description: 数据表名
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* - in: query
* name: sortBy
* schema:
* type: string
* default: created_at
* description: 排序字段
* - in: query
* name: sortOrder
* schema:
* type: string
* enum: [ASC, DESC]
* default: DESC
* description: 排序方向
* responses:
* 200:
* description: 获取操作日志列表成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/OperationLog'
* pagination:
* type: object
* properties:
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* totalPages:
* type: integer
* hasMore:
* type: boolean
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作日志'
});
}
next();
},
[
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
query('pageSize').optional().isInt({ min: 1, max: 100 }).withMessage('每页记录数必须是1-100之间的整数'),
query('userId').optional().isInt({ min: 1 }).withMessage('用户ID必须是正整数'),
query('username').optional().isLength({ min: 1, max: 50 }).withMessage('用户名长度必须在1-50个字符之间'),
query('operationType').optional().isIn(['CREATE', 'UPDATE', 'DELETE']).withMessage('操作类型必须是CREATE、UPDATE或DELETE'),
query('moduleName').optional().isLength({ min: 1, max: 100 }).withMessage('模块名称长度必须在1-100个字符之间'),
query('tableName').optional().isLength({ min: 1, max: 100 }).withMessage('数据表名长度必须在1-100个字符之间'),
query('startDate').optional().isISO8601().withMessage('开始日期格式不正确'),
query('endDate').optional().isISO8601().withMessage('结束日期格式不正确'),
query('sortBy').optional().isIn(['id', 'user_id', 'username', 'operation_type', 'module_name', 'table_name', 'created_at']).withMessage('排序字段无效'),
query('sortOrder').optional().isIn(['ASC', 'DESC']).withMessage('排序方向必须是ASC或DESC')
],
validateRequest,
operationLogController.getOperationLogs
);
/**
* @swagger
* /api/operation-logs/{id}:
* get:
* summary: 获取操作日志详情
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 操作日志ID
* responses:
* 200:
* description: 获取操作日志详情成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/OperationLog'
* message:
* type: string
* 404:
* description: 操作日志不存在
* 500:
* description: 服务器内部错误
*/
router.get('/:id',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作日志'
});
}
next();
},
[
param('id').isInt({ min: 1 }).withMessage('操作日志ID必须是正整数')
],
validateRequest,
operationLogController.getOperationLogById
);
/**
* @swagger
* /api/operation-logs/stats/overview:
* get:
* summary: 获取操作统计
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [user, module, overall]
* default: overall
* description: 统计类型
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户IDtype为user时必填
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称type为module时必填
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作统计成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* CREATE:
* type: integer
* UPDATE:
* type: integer
* DELETE:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/overview',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作统计'
});
}
next();
},
[
query('type').optional().isIn(['user', 'module', 'overall']).withMessage('统计类型必须是user、module或overall'),
query('userId').optional().isInt({ min: 1 }).withMessage('用户ID必须是正整数'),
query('moduleName').optional().isLength({ min: 1, max: 100 }).withMessage('模块名称长度必须在1-100个字符之间'),
query('startDate').optional().isISO8601().withMessage('开始日期格式不正确'),
query('endDate').optional().isISO8601().withMessage('结束日期格式不正确')
],
validateRequest,
operationLogController.getOperationStats
);
/**
* @swagger
* /api/operation-logs/stats/chart:
* get:
* summary: 获取操作日志图表数据
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [hourly, daily, monthly]
* default: daily
* description: 图表类型
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作日志图表数据成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* dates:
* type: array
* items:
* type: string
* series:
* type: object
* properties:
* CREATE:
* type: array
* items:
* type: integer
* UPDATE:
* type: array
* items:
* type: integer
* DELETE:
* type: array
* items:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/chart',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作统计'
});
}
next();
},
[
query('type').optional().isIn(['hourly', 'daily', 'monthly']).withMessage('图表类型必须是hourly、daily或monthly'),
query('startDate').optional().isISO8601().withMessage('开始日期格式不正确'),
query('endDate').optional().isISO8601().withMessage('结束日期格式不正确')
],
validateRequest,
operationLogController.getOperationChartData
);
/**
* @swagger
* /api/operation-logs/clean:
* post:
* summary: 清理过期日志
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* daysToKeep:
* type: integer
* default: 90
* description: 保留天数
* responses:
* 200:
* description: 清理过期日志成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* deletedCount:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.post('/clean',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法清理操作日志'
});
}
next();
},
[
body('daysToKeep').optional().isInt({ min: 1, max: 365 }).withMessage('保留天数必须是1-365之间的整数')
],
validateRequest,
operationLogController.cleanExpiredLogs
);
/**
* @swagger
* /api/operation-logs/export:
* get:
* summary: 导出操作日志
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: operationType
* schema:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称
* - in: query
* name: tableName
* schema:
* type: string
* description: 数据表名
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 导出操作日志成功
* content:
* text/csv:
* schema:
* type: string
* format: binary
* 500:
* description: 服务器内部错误
*/
router.get('/export',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法导出操作日志'
});
}
next();
},
[
query('userId').optional().isInt({ min: 1 }).withMessage('用户ID必须是正整数'),
query('username').optional().isLength({ min: 1, max: 50 }).withMessage('用户名长度必须在1-50个字符之间'),
query('operationType').optional().isIn(['CREATE', 'UPDATE', 'DELETE']).withMessage('操作类型必须是CREATE、UPDATE或DELETE'),
query('moduleName').optional().isLength({ min: 1, max: 100 }).withMessage('模块名称长度必须在1-100个字符之间'),
query('tableName').optional().isLength({ min: 1, max: 100 }).withMessage('数据表名长度必须在1-100个字符之间'),
query('startDate').optional().isISO8601().withMessage('开始日期格式不正确'),
query('endDate').optional().isISO8601().withMessage('结束日期格式不正确')
],
validateRequest,
operationLogController.exportOperationLogs
);
module.exports = router;

View File

@@ -0,0 +1,540 @@
/**
* 操作日志路由
* @file operationLogs.js
* @description 定义操作日志相关的API路由
*/
const express = require('express');
const router = express.Router();
const { body, query, param } = require('express-validator');
const operationLogController = require('../controllers/operationLogController');
const auth = require('../middleware/auth');
const { hasPermission } = require('../config/permissions');
// 验证中间件
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '请求参数验证失败',
errors: errors.array()
});
}
next();
};
/**
* @swagger
* components:
* schemas:
* OperationLog:
* type: object
* properties:
* id:
* type: integer
* description: 主键ID
* user_id:
* type: integer
* description: 操作用户ID
* username:
* type: string
* description: 操作用户名
* user_role:
* type: string
* description: 操作用户角色
* operation_type:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* module_name:
* type: string
* description: 操作模块名称
* table_name:
* type: string
* description: 操作的数据表名
* record_id:
* type: integer
* description: 操作的记录ID
* operation_desc:
* type: string
* description: 操作描述
* old_data:
* type: object
* description: 操作前的数据
* new_data:
* type: object
* description: 操作后的数据
* ip_address:
* type: string
* description: 操作IP地址
* user_agent:
* type: string
* description: 用户代理信息
* request_url:
* type: string
* description: 请求URL
* request_method:
* type: string
* description: 请求方法
* response_status:
* type: integer
* description: 响应状态码
* execution_time:
* type: integer
* description: 执行时间(毫秒)
* error_message:
* type: string
* description: 错误信息
* created_at:
* type: string
* format: date-time
* description: 创建时间
*/
/**
* @swagger
* /api/operation-logs:
* get:
* summary: 获取操作日志列表
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 20
* description: 每页记录数
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: operationType
* schema:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称
* - in: query
* name: tableName
* schema:
* type: string
* description: 数据表名
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* - in: query
* name: sortBy
* schema:
* type: string
* default: created_at
* description: 排序字段
* - in: query
* name: sortOrder
* schema:
* type: string
* enum: [ASC, DESC]
* default: DESC
* description: 排序方向
* responses:
* 200:
* description: 获取操作日志列表成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/OperationLog'
* pagination:
* type: object
* properties:
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* totalPages:
* type: integer
* hasMore:
* type: boolean
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作日志'
});
}
next();
},
operationLogController.getOperationLogs
);
/**
* @swagger
* /api/operation-logs/{id}:
* get:
* summary: 获取操作日志详情
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 操作日志ID
* responses:
* 200:
* description: 获取操作日志详情成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/OperationLog'
* message:
* type: string
* 404:
* description: 操作日志不存在
* 500:
* description: 服务器内部错误
*/
router.get('/:id',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作日志'
});
}
next();
},
operationLogController.getOperationLogById
);
/**
* @swagger
* /api/operation-logs/stats/overview:
* get:
* summary: 获取操作统计
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [user, module, overall]
* default: overall
* description: 统计类型
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户IDtype为user时必填
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称type为module时必填
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作统计成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* CREATE:
* type: integer
* UPDATE:
* type: integer
* DELETE:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/overview',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作统计'
});
}
next();
},
operationLogController.getOperationStats
);
/**
* @swagger
* /api/operation-logs/stats/chart:
* get:
* summary: 获取操作日志图表数据
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [hourly, daily, monthly]
* default: daily
* description: 图表类型
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作日志图表数据成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* dates:
* type: array
* items:
* type: string
* series:
* type: object
* properties:
* CREATE:
* type: array
* items:
* type: integer
* UPDATE:
* type: array
* items:
* type: integer
* DELETE:
* type: array
* items:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/chart',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作统计'
});
}
next();
},
operationLogController.getOperationChartData
);
/**
* @swagger
* /api/operation-logs/clean:
* post:
* summary: 清理过期日志
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* daysToKeep:
* type: integer
* default: 90
* description: 保留天数
* responses:
* 200:
* description: 清理过期日志成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* deletedCount:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.post('/clean',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法清理操作日志'
});
}
next();
},
operationLogController.cleanExpiredLogs
);
/**
* @swagger
* /api/operation-logs/export:
* get:
* summary: 导出操作日志
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: operationType
* schema:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称
* - in: query
* name: tableName
* schema:
* type: string
* description: 数据表名
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 导出操作日志成功
* content:
* text/csv:
* schema:
* type: string
* format: binary
* 500:
* description: 服务器内部错误
*/
router.get('/export',
auth,
(req, res, next) => {
if (!hasPermission(req.user.permissions, 'operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法导出操作日志'
});
}
next();
},
operationLogController.exportOperationLogs
);
module.exports = router;

View File

@@ -0,0 +1,446 @@
/**
* 操作日志路由
* @file operationLogs.js
* @description 定义操作日志相关的API路由
*/
const express = require('express');
const router = express.Router();
const { body, query, param, validationResult } = require('express-validator');
const operationLogController = require('../controllers/operationLogController');
const { verifyToken } = require('../middleware/auth');
const { checkOperationLogPermission } = require('../middleware/operationLogAuth');
// 验证中间件
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '请求参数验证失败',
errors: errors.array()
});
}
next();
};
/**
* @swagger
* components:
* schemas:
* OperationLog:
* type: object
* properties:
* id:
* type: integer
* description: 主键ID
* user_id:
* type: integer
* description: 操作用户ID
* username:
* type: string
* description: 操作用户名
* user_role:
* type: string
* description: 操作用户角色
* operation_type:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* module_name:
* type: string
* description: 操作模块名称
* table_name:
* type: string
* description: 操作的数据表名
* record_id:
* type: integer
* description: 操作的记录ID
* operation_desc:
* type: string
* description: 操作描述
* old_data:
* type: object
* description: 操作前的数据
* new_data:
* type: object
* description: 操作后的数据
* ip_address:
* type: string
* description: 操作IP地址
* user_agent:
* type: string
* description: 用户代理信息
* request_url:
* type: string
* description: 请求URL
* request_method:
* type: string
* description: 请求方法
* response_status:
* type: integer
* description: 响应状态码
* execution_time:
* type: integer
* description: 执行时间(毫秒)
* error_message:
* type: string
* description: 错误信息
* created_at:
* type: string
* format: date-time
* description: 创建时间
*/
/**
* @swagger
* /api/operation-logs:
* get:
* summary: 获取操作日志列表
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 20
* description: 每页记录数
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户ID
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: operationType
* schema:
* type: string
* enum: [CREATE, UPDATE, DELETE]
* description: 操作类型
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称
* - in: query
* name: tableName
* schema:
* type: string
* description: 数据表名
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* - in: query
* name: sortBy
* schema:
* type: string
* default: created_at
* description: 排序字段
* - in: query
* name: sortOrder
* schema:
* type: string
* enum: [ASC, DESC]
* default: DESC
* description: 排序方向
* responses:
* 200:
* description: 获取操作日志列表成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/OperationLog'
* pagination:
* type: object
* properties:
* total:
* type: integer
* page:
* type: integer
* pageSize:
* type: integer
* totalPages:
* type: integer
* hasMore:
* type: boolean
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/',
verifyToken,
checkOperationLogPermission,
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
query('pageSize').optional().isInt({ min: 1, max: 100 }).withMessage('每页记录数必须是1-100之间的整数'),
query('username').optional().isLength({ min: 0, max: 50 }).withMessage('用户名长度必须在0-50个字符之间'),
query('operationType').optional().isIn(['CREATE', 'UPDATE', 'DELETE']).withMessage('操作类型必须是CREATE、UPDATE或DELETE'),
query('moduleName').optional().isLength({ min: 1, max: 100 }).withMessage('模块名称长度必须在1-100个字符之间'),
query('tableName').optional().isLength({ min: 1, max: 100 }).withMessage('数据表名长度必须在1-100个字符之间'),
query('startDate').optional().isISO8601().withMessage('开始日期格式不正确'),
query('endDate').optional().isISO8601().withMessage('结束日期格式不正确'),
query('sortBy').optional().isIn(['id', 'user_id', 'username', 'operation_type', 'module_name', 'table_name', 'created_at']).withMessage('排序字段无效'),
query('sortOrder').optional().isIn(['ASC', 'DESC']).withMessage('排序方向必须是ASC或DESC'),
validateRequest,
operationLogController.getOperationLogs
);
/**
* @swagger
* /api/operation-logs/{id}:
* get:
* summary: 获取操作日志详情
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 操作日志ID
* responses:
* 200:
* description: 获取操作日志详情成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/OperationLog'
* message:
* type: string
* 404:
* description: 操作日志不存在
* 500:
* description: 服务器内部错误
*/
router.get('/export',
verifyToken,
checkOperationLogPermission,
operationLogController.exportOperationLogs
);
router.get('/:id',
verifyToken,
checkOperationLogPermission,
operationLogController.getOperationLogById
);
/**
* @swagger
* /api/operation-logs/stats/overview:
* get:
* summary: 获取操作统计
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [user, module, overall]
* default: overall
* description: 统计类型
* - in: query
* name: userId
* schema:
* type: integer
* description: 用户IDtype为user时必填
* - in: query
* name: moduleName
* schema:
* type: string
* description: 模块名称type为module时必填
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作统计成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* CREATE:
* type: integer
* UPDATE:
* type: integer
* DELETE:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/overview',
verifyToken,
checkOperationLogPermission,
operationLogController.getOperationStats
);
/**
* @swagger
* /api/operation-logs/stats/chart:
* get:
* summary: 获取操作日志图表数据
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [hourly, daily, monthly]
* default: daily
* description: 图表类型
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses:
* 200:
* description: 获取操作日志图表数据成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* dates:
* type: array
* items:
* type: string
* series:
* type: object
* properties:
* CREATE:
* type: array
* items:
* type: integer
* UPDATE:
* type: array
* items:
* type: integer
* DELETE:
* type: array
* items:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.get('/stats/chart',
verifyToken,
checkOperationLogPermission,
operationLogController.getOperationChartData
);
/**
* @swagger
* /api/operation-logs/clean:
* post:
* summary: 清理过期日志
* tags: [OperationLogs]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* daysToKeep:
* type: integer
* default: 90
* description: 保留天数
* responses:
* 200:
* description: 清理过期日志成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* deletedCount:
* type: integer
* message:
* type: string
* 500:
* description: 服务器内部错误
*/
router.post('/clean',
verifyToken,
checkOperationLogPermission,
operationLogController.cleanExpiredLogs
);
module.exports = router;

View File

@@ -69,6 +69,9 @@ const orderController = require('../controllers/orderController');
// 获取所有订单
router.get('/', orderController.getAllOrders);
// 根据用户名搜索订单
router.get('/search', orderController.searchOrdersByUsername);
/**
* @swagger
* /api/orders/{id}:

352
backend/routes/pens.js Normal file
View File

@@ -0,0 +1,352 @@
/**
* 栏舍管理路由
* @file pens.js
* @description 定义栏舍管理相关的API路由
*/
const express = require('express');
const router = express.Router();
const penController = require('../controllers/penController');
const { verifyToken } = require('../middleware/auth');
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取栏舍列表
publicRoutes.get('/', penController.getPens);
// 公开获取栏舍详情
publicRoutes.get('/:id', penController.getPenById);
// 公开获取栏舍统计信息
publicRoutes.get('/stats/summary', penController.getPenStats);
// 需要认证的路由
router.use(verifyToken);
// 创建栏舍
router.post('/', penController.createPen);
// 更新栏舍
router.put('/:id', penController.updatePen);
// 删除栏舍
router.delete('/:id', penController.deletePen);
// 批量删除栏舍
router.delete('/batch', penController.batchDeletePens);
/**
* @swagger
* tags:
* name: Pens
* description: 栏舍管理API
*/
/**
* @swagger
* /api/pens/public:
* get:
* summary: 获取栏舍列表
* tags: [Pens]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: animalType
* schema:
* type: string
* enum: [马, 牛, 羊, 家禽, 猪]
* description: 动物类型
* - in: query
* name: status
* schema:
* type: string
* enum: [true, false]
* description: 状态
* - in: query
* name: farmId
* schema:
* type: integer
* description: 农场ID
* responses:
* 200:
* description: 成功获取栏舍列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* list:
* type: array
* items:
* $ref: '#/components/schemas/Pen'
* pagination:
* $ref: '#/components/schemas/Pagination'
*/
/**
* @swagger
* /api/pens/public/{id}:
* get:
* summary: 获取栏舍详情
* tags: [Pens]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* responses:
* 200:
* description: 成功获取栏舍详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Pen'
* 404:
* description: 栏舍不存在
*/
/**
* @swagger
* /api/pens:
* post:
* summary: 创建栏舍
* tags: [Pens]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - animal_type
* - responsible
* - capacity
* properties:
* name:
* type: string
* description: 栏舍名称
* animal_type:
* type: string
* enum: [马, 牛, 羊, 家禽, 猪]
* description: 动物类型
* pen_type:
* type: string
* description: 栏舍类型
* responsible:
* type: string
* description: 负责人
* capacity:
* type: integer
* description: 容量
* status:
* type: boolean
* default: true
* description: 状态
* description:
* type: string
* description: 备注信息
* farm_id:
* type: integer
* description: 所属农场ID
* responses:
* 201:
* description: 栏舍创建成功
* 400:
* description: 请求参数错误
* 500:
* description: 服务器内部错误
*/
/**
* @swagger
* /api/pens/{id}:
* put:
* summary: 更新栏舍
* tags: [Pens]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 栏舍名称
* animal_type:
* type: string
* enum: [马, 牛, 羊, 家禽, 猪]
* description: 动物类型
* pen_type:
* type: string
* description: 栏舍类型
* responsible:
* type: string
* description: 负责人
* capacity:
* type: integer
* description: 容量
* status:
* type: boolean
* description: 状态
* description:
* type: string
* description: 备注信息
* farm_id:
* type: integer
* description: 所属农场ID
* responses:
* 200:
* description: 栏舍更新成功
* 404:
* description: 栏舍不存在
* 500:
* description: 服务器内部错误
*/
/**
* @swagger
* /api/pens/{id}:
* delete:
* summary: 删除栏舍
* tags: [Pens]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 栏舍ID
* responses:
* 200:
* description: 栏舍删除成功
* 404:
* description: 栏舍不存在
* 500:
* description: 服务器内部错误
*/
/**
* @swagger
* /api/pens/batch:
* delete:
* summary: 批量删除栏舍
* tags: [Pens]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 栏舍ID数组
* responses:
* 200:
* description: 批量删除成功
* 400:
* description: 请求参数错误
* 500:
* description: 服务器内部错误
*/
/**
* @swagger
* components:
* schemas:
* Pen:
* type: object
* properties:
* id:
* type: integer
* description: 栏舍ID
* name:
* type: string
* description: 栏舍名称
* animal_type:
* type: string
* enum: [马, 牛, 羊, 家禽, 猪]
* description: 动物类型
* pen_type:
* type: string
* description: 栏舍类型
* responsible:
* type: string
* description: 负责人
* capacity:
* type: integer
* description: 容量
* status:
* type: boolean
* description: 状态
* description:
* type: string
* description: 备注信息
* farm_id:
* type: integer
* description: 所属农场ID
* creator:
* type: string
* description: 创建人
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
* farm:
* $ref: '#/components/schemas/Farm'
*/
module.exports = router;

View File

@@ -84,6 +84,9 @@ const { verifyToken } = require('../middleware/auth');
*/
router.get('/', productController.getAllProducts);
// 根据产品名称搜索产品
router.get('/search', productController.searchProductsByName);
/**
* @swagger
* /api/products:

688
backend/routes/reports.js Normal file
View File

@@ -0,0 +1,688 @@
/**
* 报表管理路由
* @file reports.js
* @description 处理报表生成、下载和管理请求
*/
const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const reportService = require('../services/reportService');
const logger = require('../utils/logger');
const path = require('path');
const fs = require('fs');
const router = express.Router();
/**
* @swagger
* tags:
* name: Reports
* description: 报表管理相关接口
*/
/**
* @swagger
* /api/reports/farm:
* post:
* summary: 生成养殖统计报表
* tags: [Reports]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* startDate:
* type: string
* format: date
* description: 开始日期
* endDate:
* type: string
* format: date
* description: 结束日期
* farmIds:
* type: array
* items:
* type: integer
* description: 农场ID列表
* format:
* type: string
* enum: [pdf, excel, csv]
* description: 报表格式
* responses:
* 200:
* description: 报表生成成功
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/farm', verifyToken, async (req, res) => {
try {
const options = {
startDate: req.body.startDate,
endDate: req.body.endDate,
farmIds: req.body.farmIds || [],
format: req.body.format || 'pdf'
};
logger.info(`用户 ${req.user.username} 请求生成养殖统计报表`, options);
const result = await reportService.generateFarmReport(options);
res.json({
success: true,
message: '报表生成成功',
data: {
fileName: result.fileName,
downloadUrl: `/api/reports/download/${encodeURIComponent(result.fileName)}`,
mimeType: result.mimeType,
size: result.size,
generatedAt: new Date()
}
});
} catch (error) {
logger.error('生成养殖统计报表失败:', error);
res.status(500).json({
success: false,
message: '报表生成失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/sales:
* post:
* summary: 生成销售分析报表
* tags: [Reports]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* startDate:
* type: string
* format: date
* description: 开始日期
* endDate:
* type: string
* format: date
* description: 结束日期
* format:
* type: string
* enum: [pdf, excel]
* description: 报表格式
* responses:
* 200:
* description: 报表生成成功
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/sales', verifyToken, checkRole(['admin', 'manager']), async (req, res) => {
try {
const options = {
startDate: req.body.startDate,
endDate: req.body.endDate,
format: req.body.format || 'pdf'
};
logger.info(`用户 ${req.user.username} 请求生成销售分析报表`, options);
const result = await reportService.generateSalesReport(options);
res.json({
success: true,
message: '销售分析报表生成成功',
data: {
fileName: result.fileName,
downloadUrl: `/api/reports/download/${encodeURIComponent(result.fileName)}`,
mimeType: result.mimeType,
size: result.size,
generatedAt: new Date()
}
});
} catch (error) {
logger.error('生成销售分析报表失败:', error);
res.status(500).json({
success: false,
message: '销售分析报表生成失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/compliance:
* post:
* summary: 生成监管合规报表
* tags: [Reports]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* startDate:
* type: string
* format: date
* description: 开始日期
* endDate:
* type: string
* format: date
* description: 结束日期
* format:
* type: string
* enum: [pdf, excel]
* description: 报表格式
* responses:
* 200:
* description: 报表生成成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 500:
* description: 服务器错误
*/
router.post('/compliance', verifyToken, checkRole(['admin']), async (req, res) => {
try {
const options = {
startDate: req.body.startDate,
endDate: req.body.endDate,
format: req.body.format || 'pdf'
};
logger.info(`管理员 ${req.user.username} 请求生成监管合规报表`, options);
const result = await reportService.generateComplianceReport(options);
res.json({
success: true,
message: '监管合规报表生成成功',
data: {
fileName: result.fileName,
downloadUrl: `/api/reports/download/${encodeURIComponent(result.fileName)}`,
mimeType: result.mimeType,
size: result.size,
generatedAt: new Date()
}
});
} catch (error) {
logger.error('生成监管合规报表失败:', error);
res.status(500).json({
success: false,
message: '监管合规报表生成失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/download/{fileName}:
* get:
* summary: 下载报表文件
* tags: [Reports]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fileName
* required: true
* schema:
* type: string
* description: 文件名
* responses:
* 200:
* description: 文件下载成功
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
* 404:
* description: 文件不存在
* 401:
* description: 未授权
*/
router.get('/download/:fileName', verifyToken, async (req, res) => {
try {
const fileName = decodeURIComponent(req.params.fileName);
const filePath = path.join(reportService.reportsPath, fileName);
// 安全检查:确保文件在报表目录内
if (!filePath.startsWith(reportService.reportsPath)) {
logger.warn(`用户 ${req.user.username} 尝试访问非法路径: ${fileName}`);
return res.status(403).json({
success: false,
message: '非法文件路径'
});
}
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
logger.warn(`文件不存在: ${fileName}`);
return res.status(404).json({
success: false,
message: '文件不存在'
});
}
// 设置响应头
const stat = fs.statSync(filePath);
const ext = path.extname(fileName).toLowerCase();
let mimeType = 'application/octet-stream';
if (ext === '.pdf') {
mimeType = 'application/pdf';
} else if (ext === '.xlsx') {
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
} else if (ext === '.csv') {
mimeType = 'text/csv';
}
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
// 创建文件流并发送
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
logger.info(`用户 ${req.user.username} 下载报表文件: ${fileName}`);
} catch (error) {
logger.error('下载报表文件失败:', error);
res.status(500).json({
success: false,
message: '文件下载失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/list:
* get:
* summary: 获取报表文件列表
* tags: [Reports]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* type: object
* properties:
* fileName:
* type: string
* size:
* type: number
* createdAt:
* type: string
* downloadUrl:
* type: string
*/
router.get('/list', verifyToken, async (req, res) => {
try {
const files = fs.readdirSync(reportService.reportsPath);
const fileList = [];
for (const file of files) {
const filePath = path.join(reportService.reportsPath, file);
const stat = fs.statSync(filePath);
fileList.push({
fileName: file,
size: stat.size,
createdAt: stat.mtime,
downloadUrl: `/api/reports/download/${encodeURIComponent(file)}`,
mimeType: this.getMimeType(file)
});
}
// 按创建时间降序排列
fileList.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({
success: true,
data: fileList,
total: fileList.length
});
} catch (error) {
logger.error('获取报表文件列表失败:', error);
res.status(500).json({
success: false,
message: '获取报表文件列表失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/cleanup:
* post:
* summary: 清理过期报表文件
* tags: [Reports]
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* daysToKeep:
* type: integer
* description: 保留天数
* default: 30
* responses:
* 200:
* description: 清理成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/cleanup', verifyToken, checkRole(['admin']), async (req, res) => {
try {
const daysToKeep = req.body.daysToKeep || 30;
const deletedCount = await reportService.cleanupOldReports(daysToKeep);
logger.info(`管理员 ${req.user.username} 执行报表清理,删除了 ${deletedCount} 个文件`);
res.json({
success: true,
message: `清理完成,删除了 ${deletedCount} 个过期报表文件`,
data: {
deletedCount,
daysToKeep
}
});
} catch (error) {
logger.error('清理过期报表失败:', error);
res.status(500).json({
success: false,
message: '清理过期报表失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/export/farms:
* get:
* summary: 导出农场数据Excel/CSV
* tags: [Reports]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: format
* schema:
* type: string
* enum: [excel, csv]
* description: 导出格式
* responses:
* 200:
* description: 导出成功
*/
router.get('/export/farms', verifyToken, async (req, res) => {
try {
const format = req.query.format || 'excel';
logger.info(`用户 ${req.user.username} 请求导出农场数据,格式: ${format}`);
const result = await reportService.generateFarmReport({ format });
// 直接发送文件
const filePath = result.filePath;
res.setHeader('Content-Type', result.mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(result.fileName)}"`);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// 异步删除临时文件
setTimeout(() => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}, 60000); // 1分钟后删除
} catch (error) {
logger.error('导出农场数据失败:', error);
res.status(500).json({
success: false,
message: '数据导出失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/export/devices:
* get:
* summary: 导出设备数据Excel/CSV
* tags: [Reports]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: format
* schema:
* type: string
* enum: [excel, csv]
* description: 导出格式
* - in: query
* name: status
* schema:
* type: string
* enum: [online, offline, maintenance]
* description: 设备状态筛选
* responses:
* 200:
* description: 导出成功
*/
router.get('/export/devices', verifyToken, async (req, res) => {
try {
const format = req.query.format || 'excel';
const status = req.query.status;
logger.info(`用户 ${req.user.username} 请求导出设备数据,格式: ${format}, 状态: ${status || '全部'}`);
const result = await this.exportDevicesData(format, status);
res.setHeader('Content-Type', result.mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(result.fileName)}"`);
const fileStream = fs.createReadStream(result.filePath);
fileStream.pipe(res);
// 异步删除临时文件
setTimeout(() => {
if (fs.existsSync(result.filePath)) {
fs.unlinkSync(result.filePath);
}
}, 60000);
} catch (error) {
logger.error('导出设备数据失败:', error);
res.status(500).json({
success: false,
message: '设备数据导出失败',
error: error.message
});
}
});
/**
* @swagger
* /api/reports/templates:
* get:
* summary: 获取可用报表模板
* tags: [Reports]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/templates', verifyToken, async (req, res) => {
try {
const templates = [
{
id: 'farm-statistics',
name: '养殖统计报表',
description: '包含农场、动物、设备和预警的综合统计',
formats: ['pdf', 'excel', 'csv'],
requiredParams: ['startDate', 'endDate'],
optionalParams: ['farmIds']
},
{
id: 'sales-analysis',
name: '销售分析报表',
description: '销售订单和产品销量分析报表',
formats: ['pdf', 'excel'],
requiredParams: ['startDate', 'endDate'],
optionalParams: []
},
{
id: 'compliance-check',
name: '监管合规报表',
description: '监管要求合规性检查报表',
formats: ['pdf', 'excel'],
requiredParams: ['startDate', 'endDate'],
optionalParams: [],
permissions: ['admin']
}
];
res.json({
success: true,
data: templates,
total: templates.length
});
} catch (error) {
logger.error('获取报表模板失败:', error);
res.status(500).json({
success: false,
message: '获取报表模板失败',
error: error.message
});
}
});
/**
* 导出设备数据
* @param {string} format 导出格式
* @param {string} status 设备状态筛选
* @returns {Object} 文件信息
*/
async function exportDevicesData(format, status) {
try {
const whereClause = {};
if (status) {
whereClause.status = status;
}
const devices = await Device.findAll({
where: whereClause,
include: [{
model: Farm,
as: 'farm',
attributes: ['id', 'name']
}],
order: [['updated_at', 'DESC']]
});
if (format === 'excel') {
const workbook = XLSX.utils.book_new();
const deviceData = [
['设备ID', '设备名称', '设备类型', '设备状态', '所属农场', '安装日期', '最后维护', '创建时间', '更新时间']
];
devices.forEach(device => {
deviceData.push([
device.id,
device.name,
device.type,
device.status,
device.farm?.name || '未知农场',
device.installation_date ? moment(device.installation_date).format('YYYY-MM-DD') : '',
device.last_maintenance ? moment(device.last_maintenance).format('YYYY-MM-DD') : '',
moment(device.created_at).format('YYYY-MM-DD HH:mm:ss'),
moment(device.updated_at).format('YYYY-MM-DD HH:mm:ss')
]);
});
const worksheet = XLSX.utils.aoa_to_sheet(deviceData);
XLSX.utils.book_append_sheet(workbook, worksheet, '设备数据');
const fileName = `设备数据导出_${moment().format('YYYYMMDD_HHmmss')}.xlsx`;
const filePath = path.join(reportService.reportsPath, fileName);
XLSX.writeFile(workbook, filePath);
return {
fileName,
filePath,
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
size: fs.statSync(filePath).size
};
}
} catch (error) {
logger.error('导出设备数据失败:', error);
throw error;
}
}
/**
* 获取文件MIME类型
* @param {string} fileName 文件名
* @returns {string} MIME类型
*/
function getMimeType(fileName) {
const ext = path.extname(fileName).toLowerCase();
const mimeTypes = {
'.pdf': 'application/pdf',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.csv': 'text/csv'
};
return mimeTypes[ext] || 'application/octet-stream';
}
module.exports = router;

View File

@@ -0,0 +1,369 @@
/**
* 角色权限管理路由
* @file role-permissions.js
* @description 定义角色权限管理相关的API路由
*/
const express = require('express');
const router = express.Router();
const rolePermissionController = require('../controllers/rolePermissionController');
const { verifyToken } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permission');
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
// 公开获取角色列表
publicRoutes.get('/roles', rolePermissionController.getAllRoles);
// 公开获取角色详情
publicRoutes.get('/roles/:id', rolePermissionController.getRoleById);
// 公开获取菜单权限列表
publicRoutes.get('/menus', rolePermissionController.getAllMenuPermissions);
// 公开获取角色菜单权限
publicRoutes.get('/roles/:roleId/menus', rolePermissionController.getRoleMenuPermissions);
// 公开获取所有权限
publicRoutes.get('/permissions', rolePermissionController.getAllPermissions);
// 公开获取权限模块列表
publicRoutes.get('/permissions/modules', rolePermissionController.getPermissionModules);
// 公开获取角色功能权限
publicRoutes.get('/roles/:roleId/permissions', rolePermissionController.getRolePermissions);
// 所有其他路由都需要认证
router.use(verifyToken);
/**
* @swagger
* components:
* schemas:
* Role:
* type: object
* properties:
* id:
* type: integer
* description: 角色ID
* name:
* type: string
* description: 角色名称
* description:
* type: string
* description: 角色描述
* status:
* type: boolean
* description: 状态
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
* menuPermissions:
* type: array
* items:
* $ref: '#/components/schemas/MenuPermission'
* description: 菜单权限列表
*/
/**
* @swagger
* /api/role-permissions/roles:
* get:
* summary: 获取角色列表
* tags: [Role Permissions]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* responses:
* 200:
* description: 成功获取角色列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* list:
* type: array
* items:
* $ref: '#/components/schemas/Role'
* pagination:
* type: object
* properties:
* current:
* type: integer
* pageSize:
* type: integer
* total:
* type: integer
* pages:
* type: integer
* message:
* type: string
*/
router.get('/roles', requirePermission('role:view'), rolePermissionController.getAllRoles);
/**
* @swagger
* /api/role-permissions/roles/{id}:
* get:
* summary: 获取角色详情
* tags: [Role Permissions]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 角色ID
* responses:
* 200:
* description: 成功获取角色详情
* 404:
* description: 角色不存在
*/
router.get('/roles/:id', requirePermission('role:view'), rolePermissionController.getRoleById);
/**
* @swagger
* /api/role-permissions/roles:
* post:
* summary: 创建角色
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* properties:
* name:
* type: string
* description: 角色名称
* description:
* type: string
* description: 角色描述
* status:
* type: boolean
* description: 状态
* menuIds:
* type: array
* items:
* type: integer
* description: 菜单权限ID数组
* responses:
* 201:
* description: 角色创建成功
* 400:
* description: 请求参数错误
*/
router.post('/roles', requirePermission('role:create'), rolePermissionController.createRole);
/**
* @swagger
* /api/role-permissions/roles/{id}:
* put:
* summary: 更新角色
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 角色ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: 角色名称
* description:
* type: string
* description: 角色描述
* status:
* type: boolean
* description: 状态
* menuIds:
* type: array
* items:
* type: integer
* description: 菜单权限ID数组
* responses:
* 200:
* description: 角色更新成功
* 404:
* description: 角色不存在
*/
router.put('/roles/:id', requirePermission('role:update'), rolePermissionController.updateRole);
/**
* @swagger
* /api/role-permissions/roles/{id}:
* delete:
* summary: 删除角色
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 角色ID
* responses:
* 200:
* description: 角色删除成功
* 404:
* description: 角色不存在
*/
router.delete('/roles/:id', requirePermission('role:delete'), rolePermissionController.deleteRole);
/**
* @swagger
* /api/role-permissions/menus:
* get:
* summary: 获取菜单权限列表
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 成功获取菜单权限列表
*/
router.get('/menus', requirePermission('menu:view'), rolePermissionController.getAllMenuPermissions);
/**
* @swagger
* /api/role-permissions/roles/{roleId}/menus:
* get:
* summary: 获取角色的菜单权限
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* description: 角色ID
* responses:
* 200:
* description: 成功获取角色菜单权限
* 404:
* description: 角色不存在
*/
router.get('/roles/:roleId/menus', requirePermission('role:view'), rolePermissionController.getRoleMenuPermissions);
/**
* @swagger
* /api/role-permissions/roles/{roleId}/menus:
* post:
* summary: 设置角色的菜单权限
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: roleId
* required: true
* schema:
* type: integer
* description: 角色ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* menuIds:
* type: array
* items:
* type: integer
* description: 菜单权限ID数组
* responses:
* 200:
* description: 设置角色菜单权限成功
* 404:
* description: 角色不存在
*/
router.post('/roles/:roleId/menus', requirePermission('role:assign'), rolePermissionController.setRoleMenuPermissions);
// 设置角色功能权限
router.post('/roles/:roleId/permissions', requirePermission('role:assign'), rolePermissionController.setRolePermissions);
/**
* @swagger
* /api/role-permissions/roles/{id}/status:
* put:
* summary: 切换角色状态
* tags: [Role Permissions]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 角色ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - status
* properties:
* status:
* type: boolean
* description: 角色状态
* responses:
* 200:
* description: 角色状态切换成功
* 404:
* description: 角色不存在
*/
router.put('/roles/:id/status', requirePermission('role:update'), rolePermissionController.toggleRoleStatus);
module.exports = router;

View File

@@ -0,0 +1,590 @@
/**
* 智能预警路由
* @file smart-alerts.js
* @description 定义智能预警相关的API路由
*/
const express = require('express');
const router = express.Router();
const { IotJbqClient, IotXqClient } = require('../models');
const { Op } = require('sequelize');
// 公开API路由不需要验证token
const publicRoutes = express.Router();
router.use('/public', publicRoutes);
/**
* 获取智能预警统计
*/
publicRoutes.get('/stats', async (req, res) => {
try {
// 获取耳标设备数量
const eartagCount = await IotJbqClient.count();
// 获取项圈设备数量
const collarCount = await IotXqClient.count();
// 生成耳标预警数据与预警列表API使用相同逻辑
const eartagDevices = await IotJbqClient.findAll({
order: [['uptime', 'DESC'], ['id', 'DESC']]
});
const eartagAlerts = [];
eartagDevices.forEach(device => {
const actualBattery = parseInt(device.voltage) || 0;
const actualTemperature = parseFloat(device.temperature) || 0;
const totalSteps = parseInt(device.walk) || 0;
const yesterdaySteps = parseInt(device.y_steps) || 0;
const dailySteps = totalSteps - yesterdaySteps; // 当日步数 = 总步数 - 昨日步数
// 离线预警
if (device.state === 0) {
eartagAlerts.push({ type: 'offline' });
}
// 低电量预警
if (actualBattery > 0 && actualBattery < 20) {
eartagAlerts.push({ type: 'battery' });
}
// 温度预警
if (actualTemperature > 0) {
// 低温预警
if (actualTemperature < 30) {
eartagAlerts.push({ type: 'temperature' });
}
// 高温预警
else if (actualTemperature > 40) {
eartagAlerts.push({ type: 'temperature' });
}
}
// 异常运动预警
// 步数异常预警当日步数为0
if (dailySteps === 0 && totalSteps > 0) {
eartagAlerts.push({ type: 'movement' });
}
});
// 生成项圈预警数据(简化版本)
const collarAlerts = await IotXqClient.count({
where: {
state: 2 // 预警状态
}
});
res.json({
success: true,
data: {
totalAlerts: eartagAlerts.length + collarAlerts,
eartagAlerts: eartagAlerts.length,
collarAlerts: collarAlerts,
eartagDevices: eartagCount,
collarDevices: collarCount
},
message: '获取智能预警统计成功'
});
} catch (error) {
console.error('获取智能预警统计失败:', error);
res.status(500).json({
success: false,
message: '获取智能预警统计失败',
error: error.message
});
}
});
/**
* 获取智能耳标预警列表
*/
publicRoutes.get('/eartag', async (req, res) => {
try {
const { page = 1, limit = 10, status, search, alertType } = req.query;
const offset = (page - 1) * limit;
console.log('智能耳标预警API请求参数:', { page, limit, status, search, alertType });
console.log('alertType 详细信息:', {
value: alertType,
type: typeof alertType,
isString: typeof alertType === 'string',
isEmpty: alertType === '',
isUndefined: alertType === undefined,
isNull: alertType === null,
length: alertType ? alertType.length : 'N/A'
});
// 构建查询条件
const whereConditions = {};
// 状态筛选
if (status) {
switch (status) {
case 'online':
whereConditions.state = 1;
break;
case 'offline':
whereConditions.state = 0;
break;
case 'alarm':
whereConditions.state = 2;
break;
case 'maintenance':
whereConditions.state = 3;
break;
}
}
// 搜索条件 - 耳标设备使用aaid字段搜索
if (search) {
whereConditions[Op.or] = [
{ aaid: { [Op.like]: `%${search}%` } },
{ cid: { [Op.like]: `%${search}%` } }
];
console.log('耳标搜索条件:', whereConditions[Op.or]);
}
// 查询所有符合条件的设备数据(用于生成预警)
const allDevices = await IotJbqClient.findAll({
where: whereConditions,
order: [
['uptime', 'DESC'],
['id', 'DESC']
]
});
// 生成预警数据 - 为每个设备生成对应的预警记录
const allAlerts = [];
allDevices.forEach(device => {
// 耳标设备编号使用aaid字段
const deviceId = device.aaid || `EARTAG${device.id}`;
const alertTime = new Date(device.uptime || Date.now()).toISOString().replace('T', ' ').substring(0, 19);
// 获取实际设备数据
const actualBattery = parseInt(device.voltage) || 0;
const actualTemperature = parseFloat(device.temperature) || 0;
// 获取步数数据
const totalSteps = parseInt(device.walk) || 0;
const yesterdaySteps = parseInt(device.y_steps) || 0;
const dailySteps = totalSteps - yesterdaySteps; // 当日步数 = 总步数 - 昨日步数
// 为所有预警添加当日步数字段的函数
const addDailyStepsToAlert = (alert) => {
alert.dailySteps = dailySteps;
alert.totalSteps = totalSteps;
alert.yesterdaySteps = yesterdaySteps;
return alert;
};
// 离线预警 - 基于实际设备状态生成预警
if (device.state === 0) {
allAlerts.push(addDailyStepsToAlert({
id: `${device.id}_offline`,
eartagNumber: deviceId,
alertType: 'offline',
alertLevel: 'high',
alertTime: alertTime,
battery: actualBattery,
temperature: actualTemperature,
gpsSignal: '无',
movementStatus: '静止',
description: '设备已离线超过30分钟',
longitude: 0,
latitude: 0
}));
}
// 低电量预警 - 基于实际电量数据生成预警
if (actualBattery > 0 && actualBattery < 20) {
allAlerts.push(addDailyStepsToAlert({
id: `${device.id}_battery`,
eartagNumber: deviceId,
alertType: 'battery',
alertLevel: actualBattery < 10 ? 'high' : 'medium',
alertTime: alertTime,
battery: actualBattery,
temperature: device.temperature || 0,
gpsSignal: '强',
movementStatus: '正常',
description: `设备电量低于20%,当前电量${actualBattery}%`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
// 温度预警 - 基于实际温度数据生成预警
if (actualTemperature > 0) {
// 低温预警
if (actualTemperature < 30) {
allAlerts.push(addDailyStepsToAlert({
id: `${device.id}_temperature_low`,
eartagNumber: deviceId,
alertType: 'temperature',
alertLevel: actualTemperature < 20 ? 'high' : 'medium',
alertTime: alertTime,
battery: actualBattery,
temperature: actualTemperature,
gpsSignal: '强',
movementStatus: '正常',
description: `设备温度过低,当前温度${actualTemperature}°C`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
// 高温预警
else if (actualTemperature > 40) {
allAlerts.push(addDailyStepsToAlert({
id: `${device.id}_temperature_high`,
eartagNumber: deviceId,
alertType: 'temperature',
alertLevel: actualTemperature > 45 ? 'high' : 'medium',
alertTime: alertTime,
battery: actualBattery,
temperature: actualTemperature,
gpsSignal: '强',
movementStatus: '正常',
description: `设备温度过高,当前温度${actualTemperature}°C`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
}
// 异常运动预警 - 基于实际运动数据生成预警
// 步数异常预警当日步数为0
if (dailySteps === 0 && totalSteps > 0) {
allAlerts.push(addDailyStepsToAlert({
id: `${device.id}_movement_zero`,
eartagNumber: deviceId,
alertType: 'movement',
alertLevel: 'high',
alertTime: alertTime,
battery: actualBattery,
temperature: actualTemperature,
gpsSignal: '强',
movementStatus: '异常',
description: `检测到步数异常当日运动量为0步可能为设备故障或动物异常`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
});
// 预警类型筛选
let filteredAlerts = allAlerts;
console.log('=== 开始预警类型筛选 ===');
console.log('原始预警数量:', allAlerts.length);
console.log('alertType 值:', alertType);
console.log('alertType 条件检查:', {
'alertType 存在': !!alertType,
'alertType.trim() !== ""': alertType && alertType.trim() !== '',
'alertType 类型': typeof alertType,
'alertType 长度': alertType ? alertType.length : 'N/A'
});
if (alertType && alertType.trim() !== '') {
console.log(`执行筛选,筛选类型: ${alertType}`);
filteredAlerts = allAlerts.filter(alert => alert.alertType === alertType);
console.log(`筛选结果: ${allAlerts.length} -> ${filteredAlerts.length}`);
console.log('筛选后的预警类型分布:',
filteredAlerts.reduce((acc, alert) => {
acc[alert.alertType] = (acc[alert.alertType] || 0) + 1;
return acc;
}, {})
);
} else {
console.log('跳过筛选,显示所有预警');
}
// 计算统计数据
const stats = {
lowBattery: filteredAlerts.filter(alert => alert.alertType === 'battery').length,
offline: filteredAlerts.filter(alert => alert.alertType === 'offline').length,
highTemperature: filteredAlerts.filter(alert => alert.alertType === 'temperature').length,
abnormalMovement: filteredAlerts.filter(alert => alert.alertType === 'movement').length
};
// 对筛选后的预警数据进行分页
const startIndex = parseInt(offset);
const endIndex = startIndex + parseInt(limit);
const paginatedAlerts = filteredAlerts.slice(startIndex, endIndex);
res.json({
success: true,
data: paginatedAlerts,
total: filteredAlerts.length,
stats: stats,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredAlerts.length,
pages: Math.ceil(filteredAlerts.length / limit)
},
message: '获取智能耳标预警列表成功'
});
} catch (error) {
console.error('获取智能耳标预警列表失败:', error);
res.status(500).json({
success: false,
message: '获取智能耳标预警列表失败',
error: error.message
});
}
});
/**
* 获取智能项圈预警列表
*/
publicRoutes.get('/collar', async (req, res) => {
try {
const { page = 1, limit = 10, status, search, alertType } = req.query;
const offset = (page - 1) * limit;
console.log('智能项圈预警API请求参数:', { page, limit, status, search, alertType });
console.log('alertType 详细信息:', {
value: alertType,
type: typeof alertType,
isString: typeof alertType === 'string',
isEmpty: alertType === '',
isUndefined: alertType === undefined,
isNull: alertType === null,
length: alertType ? alertType.length : 'N/A'
});
// 构建查询条件
const whereConditions = {};
// 状态筛选
if (status) {
switch (status) {
case 'online':
whereConditions.state = 1;
break;
case 'offline':
whereConditions.state = 0;
break;
case 'alarm':
whereConditions.state = 2;
break;
case 'maintenance':
whereConditions.state = 3;
break;
}
}
// 搜索条件 - 项圈设备使用sn字段搜索
if (search) {
whereConditions[Op.or] = [
{ sn: { [Op.like]: `%${search}%` } },
{ deviceId: { [Op.like]: `%${search}%` } }
];
console.log('项圈搜索条件:', whereConditions[Op.or]);
}
// 查询数据库
const { count, rows } = await IotXqClient.findAndCountAll({
where: whereConditions,
limit: parseInt(limit),
offset: parseInt(offset),
order: [
['uptime', 'DESC'],
['id', 'DESC']
]
});
// 生成预警数据 - 为每个设备生成对应的预警记录
const alerts = [];
rows.forEach(device => {
// 项圈设备编号使用sn字段
const deviceId = device.sn || `COLLAR${device.id}`;
const alertTime = new Date(device.uptime || Date.now()).toISOString().replace('T', ' ').substring(0, 19);
// 获取步数数据
const totalSteps = parseInt(device.steps) || 0;
const yesterdaySteps = parseInt(device.y_steps) || 0;
const dailySteps = totalSteps - yesterdaySteps; // 当日步数 = 总步数 - 昨日步数
// 为所有预警添加当日步数字段的函数
const addDailyStepsToAlert = (alert) => {
alert.dailySteps = dailySteps;
alert.totalSteps = totalSteps;
alert.yesterdaySteps = yesterdaySteps;
return alert;
};
// 离线预警
if (device.state === 0) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_offline`,
collarNumber: deviceId,
alertType: 'offline',
alertLevel: 'high',
alertTime: alertTime,
battery: device.battery || 0,
temperature: device.temperature || 0,
gpsSignal: '无',
wearStatus: device.bandge_status === 1 ? '已佩戴' : '未佩戴',
description: '设备已离线超过30分钟',
longitude: 0,
latitude: 0
}));
}
// 低电量预警
if (device.battery < 20) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_battery`,
collarNumber: deviceId,
alertType: 'battery',
alertLevel: device.battery < 10 ? 'high' : 'medium',
alertTime: alertTime,
battery: device.battery || 0,
temperature: device.temperature || 0,
gpsSignal: '强',
wearStatus: device.bandge_status === 1 ? '已佩戴' : '未佩戴',
description: `设备电量低于20%,当前电量${device.battery}%`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
// 温度预警
const actualTemperature = parseFloat(device.temperature) || 0;
if (actualTemperature > 0) {
// 低温预警
if (actualTemperature < 30) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_temperature_low`,
collarNumber: deviceId,
alertType: 'temperature',
alertLevel: actualTemperature < 20 ? 'high' : 'medium',
alertTime: alertTime,
battery: device.battery || 0,
temperature: actualTemperature,
gpsSignal: '强',
wearStatus: device.bandge_status === 1 ? '已佩戴' : '未佩戴',
description: `设备温度过低,当前温度${actualTemperature}°C`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
// 高温预警
else if (actualTemperature > 40) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_temperature_high`,
collarNumber: deviceId,
alertType: 'temperature',
alertLevel: actualTemperature > 45 ? 'high' : 'medium',
alertTime: alertTime,
battery: device.battery || 0,
temperature: actualTemperature,
gpsSignal: '强',
wearStatus: device.bandge_status === 1 ? '已佩戴' : '未佩戴',
description: `设备温度过高,当前温度${actualTemperature}°C`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
}
// 异常运动预警 - 基于实际运动数据生成预警
// 步数异常预警当日步数为0
if (dailySteps === 0 && totalSteps > 0) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_movement_zero`,
collarNumber: deviceId,
alertType: 'movement',
alertLevel: 'high',
alertTime: alertTime,
battery: device.battery || 0,
temperature: actualTemperature,
gpsSignal: '强',
wearStatus: device.bandge_status === 1 ? '已佩戴' : '未佩戴',
description: `检测到步数异常当日运动量为0步可能为设备故障或动物异常`,
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
// 项圈脱落预警
if (device.bandge_status === 0) {
alerts.push(addDailyStepsToAlert({
id: `${device.id}_wear`,
collarNumber: deviceId,
alertType: 'wear',
alertLevel: 'high',
alertTime: alertTime,
battery: device.battery || 0,
temperature: actualTemperature,
gpsSignal: '中',
wearStatus: '未佩戴',
description: '设备佩戴状态异常,可能已脱落',
longitude: 116.3974 + (device.id % 100) * 0.0001,
latitude: 39.9093 + (device.id % 100) * 0.0001
}));
}
});
// 预警类型筛选
let filteredAlerts = alerts;
console.log('=== 开始预警类型筛选 ===');
console.log('原始预警数量:', alerts.length);
console.log('alertType 值:', alertType);
console.log('alertType 条件检查:', {
'alertType 存在': !!alertType,
'alertType.trim() !== ""': alertType && alertType.trim() !== '',
'alertType 类型': typeof alertType,
'alertType 长度': alertType ? alertType.length : 'N/A'
});
if (alertType && alertType.trim() !== '') {
console.log(`执行筛选,筛选类型: ${alertType}`);
filteredAlerts = alerts.filter(alert => alert.alertType === alertType);
console.log(`筛选结果: ${alerts.length} -> ${filteredAlerts.length}`);
console.log('筛选后的预警类型分布:',
filteredAlerts.reduce((acc, alert) => {
acc[alert.alertType] = (acc[alert.alertType] || 0) + 1;
return acc;
}, {})
);
} else {
console.log('跳过筛选,显示所有预警');
}
// 计算统计数据
const stats = {
lowBattery: filteredAlerts.filter(alert => alert.alertType === 'battery').length,
offline: filteredAlerts.filter(alert => alert.alertType === 'offline').length,
highTemperature: filteredAlerts.filter(alert => alert.alertType === 'temperature').length,
abnormalMovement: filteredAlerts.filter(alert => alert.alertType === 'movement').length,
wearOff: filteredAlerts.filter(alert => alert.alertType === 'wear').length
};
res.json({
success: true,
data: filteredAlerts,
total: filteredAlerts.length,
stats: stats,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredAlerts.length,
pages: Math.ceil(filteredAlerts.length / limit)
},
message: '获取智能项圈预警列表成功'
});
} catch (error) {
console.error('获取智能项圈预警列表失败:', error);
res.status(500).json({
success: false,
message: '获取智能项圈预警列表失败',
error: error.message
});
}
});
module.exports = router;

File diff suppressed because it is too large Load Diff

430
backend/routes/system.js Normal file
View File

@@ -0,0 +1,430 @@
/**
* 系统管理路由
* @file system.js
* @description 处理系统配置和菜单权限管理请求
*/
const express = require('express');
const { body } = require('express-validator');
const { verifyToken, checkRole } = require('../middleware/auth');
const systemController = require('../controllers/systemController');
const router = express.Router();
/**
* @swagger
* tags:
* name: System
* description: 系统管理相关接口
*/
/**
* @swagger
* /api/system/configs:
* get:
* summary: 获取系统配置列表
* tags: [System]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: category
* schema:
* type: string
* description: 配置分类
* - in: query
* name: is_public
* schema:
* type: boolean
* description: 是否公开配置
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/configs', verifyToken, checkRole(['admin']), systemController.getSystemConfigs);
/**
* @swagger
* /api/system/configs/public:
* get:
* summary: 获取公开系统配置
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/configs/public', verifyToken, systemController.getPublicConfigs);
/**
* @swagger
* /api/system/configs/categories:
* get:
* summary: 获取配置分类列表
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
*/
router.get('/configs/categories', verifyToken, checkRole(['admin']), systemController.getConfigCategories);
/**
* @swagger
* /api/system/configs:
* post:
* summary: 创建系统配置
* tags: [System]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - config_key
* - config_value
* properties:
* config_key:
* type: string
* description: 配置键名
* config_value:
* description: 配置值
* category:
* type: string
* description: 配置分类
* description:
* type: string
* description: 配置描述
* is_public:
* type: boolean
* description: 是否公开
* is_editable:
* type: boolean
* description: 是否可编辑
* sort_order:
* type: integer
* description: 排序顺序
* responses:
* 201:
* description: 创建成功
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/configs',
verifyToken,
checkRole(['admin']),
[
body('config_key').notEmpty().withMessage('配置键名不能为空'),
body('config_value').exists().withMessage('配置值不能为空')
],
systemController.createSystemConfig
);
/**
* @swagger
* /api/system/configs/{id}:
* put:
* summary: 更新系统配置
* tags: [System]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 配置ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* config_value:
* description: 配置值
* description:
* type: string
* description: 配置描述
* responses:
* 200:
* description: 更新成功
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 配置不存在
*/
router.put('/configs/:id',
verifyToken,
checkRole(['admin']),
[
body('config_value').exists().withMessage('配置值不能为空')
],
systemController.updateSystemConfig
);
/**
* @swagger
* /api/system/configs/{id}:
* delete:
* summary: 删除系统配置
* tags: [System]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 配置ID
* responses:
* 200:
* description: 删除成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 配置不存在
*/
router.delete('/configs/:id', verifyToken, checkRole(['admin']), systemController.deleteSystemConfig);
/**
* @swagger
* /api/system/configs/batch:
* put:
* summary: 批量更新系统配置
* tags: [System]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configs
* properties:
* configs:
* type: array
* items:
* type: object
* properties:
* config_key:
* type: string
* config_value:
* description: 配置值
* responses:
* 200:
* description: 更新成功
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.put('/configs/batch',
verifyToken,
checkRole(['admin']),
[
body('configs').isArray().withMessage('configs必须是数组')
],
systemController.batchUpdateConfigs
);
/**
* @swagger
* /api/system/configs/{id}/reset:
* post:
* summary: 重置系统配置到默认值
* tags: [System]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 配置ID
* responses:
* 200:
* description: 重置成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 配置不存在
*/
router.post('/configs/:id/reset', verifyToken, checkRole(['admin']), systemController.resetSystemConfig);
/**
* @swagger
* /api/system/menus:
* get:
* summary: 获取菜单权限列表
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/menus', verifyToken, checkRole(['admin']), systemController.getMenuPermissions);
/**
* @swagger
* /api/system/menus/user:
* get:
* summary: 获取用户可访问菜单
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 404:
* description: 用户不存在
*/
router.get('/menus/user', verifyToken, systemController.getUserMenus);
/**
* @swagger
* /api/system/menus/{id}:
* put:
* summary: 更新菜单权限
* tags: [System]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 菜单ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* menu_name:
* type: string
* description: 菜单名称
* menu_path:
* type: string
* description: 菜单路径
* required_roles:
* type: array
* items:
* type: string
* description: 所需角色
* required_permissions:
* type: array
* items:
* type: string
* description: 所需权限
* icon:
* type: string
* description: 菜单图标
* sort_order:
* type: integer
* description: 排序顺序
* is_visible:
* type: boolean
* description: 是否可见
* is_enabled:
* type: boolean
* description: 是否启用
* description:
* type: string
* description: 菜单描述
* responses:
* 200:
* description: 更新成功
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 403:
* description: 权限不足
* 404:
* description: 菜单不存在
*/
router.put('/menus/:id',
verifyToken,
checkRole(['admin']),
[
body('menu_name').optional().notEmpty().withMessage('菜单名称不能为空'),
body('required_roles').optional().isArray().withMessage('所需角色必须是数组'),
body('required_permissions').optional().isArray().withMessage('所需权限必须是数组')
],
systemController.updateMenuPermission
);
/**
* @swagger
* /api/system/stats:
* get:
* summary: 获取系统统计信息
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.get('/stats', verifyToken, checkRole(['admin']), systemController.getSystemStats);
/**
* @swagger
* /api/system/init:
* post:
* summary: 初始化系统配置
* tags: [System]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 初始化成功
* 401:
* description: 未授权
* 403:
* description: 权限不足
*/
router.post('/init', verifyToken, checkRole(['admin']), systemController.initializeSystem);
module.exports = router;

View File

@@ -1,5 +1,6 @@
const express = require('express');
const { verifyToken } = require('../middleware/auth');
const searchLogger = require('../middleware/search-logger');
const router = express.Router();
const userController = require('../controllers/userController');
@@ -128,6 +129,9 @@ router.get('/', verifyToken, userController.getAllUsers);
* description: 服务器错误
*/
// 根据用户名搜索用户 (需要认证,添加搜索监听中间件)
router.get('/search', verifyToken, searchLogger, userController.searchUserByUsername);
// 根据ID获取用户 (需要认证)
router.get('/:id', verifyToken, userController.getUserById);