修改管理后台
This commit is contained in:
@@ -1,115 +1,215 @@
|
||||
/**
|
||||
* Animal 模型定义
|
||||
* @file Animal.js
|
||||
* @description 定义动物模型,用于数据库操作
|
||||
* 动物信息模型
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
/**
|
||||
* 动物模型
|
||||
* @typedef {Object} Animal
|
||||
* @property {number} id - 动物唯一标识
|
||||
* @property {string} type - 动物类型
|
||||
* @property {number} count - 数量
|
||||
* @property {number} farmId - 所属养殖场ID
|
||||
* @property {Date} created_at - 创建时间
|
||||
* @property {Date} updated_at - 更新时间
|
||||
*/
|
||||
class Animal extends BaseModel {
|
||||
/**
|
||||
* 获取动物所属的养殖场
|
||||
* @returns {Promise<Object>} 养殖场信息
|
||||
*/
|
||||
async getFarm() {
|
||||
return await this.getFarm();
|
||||
class Animal extends Model {
|
||||
// 获取动物类型文本
|
||||
getAnimalTypeText() {
|
||||
const typeMap = {
|
||||
1: '牛',
|
||||
2: '羊',
|
||||
3: '猪',
|
||||
4: '马'
|
||||
};
|
||||
return typeMap[this.animal_type] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动物数量
|
||||
* @param {Number} count 新数量
|
||||
* @returns {Promise<Boolean>} 更新结果
|
||||
*/
|
||||
async updateCount(count) {
|
||||
try {
|
||||
if (count < 0) {
|
||||
throw new Error('数量不能为负数');
|
||||
}
|
||||
|
||||
this.count = count;
|
||||
await this.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新动物数量失败:', error);
|
||||
return false;
|
||||
}
|
||||
// 获取品种文本
|
||||
getBreedText() {
|
||||
const breedMap = {
|
||||
1: '西藏高山牦牛',
|
||||
2: '荷斯坦奶牛',
|
||||
3: '西门塔尔牛',
|
||||
4: '安格斯牛',
|
||||
5: '小尾寒羊',
|
||||
6: '波尔山羊'
|
||||
};
|
||||
return breedMap[this.breed] || '未知品种';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新健康状态
|
||||
* @param {String} status 新状态
|
||||
* @returns {Promise<Boolean>} 更新结果
|
||||
*/
|
||||
async updateHealthStatus(status) {
|
||||
try {
|
||||
this.health_status = status;
|
||||
await this.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新健康状态失败:', error);
|
||||
return false;
|
||||
// 获取品类文本
|
||||
getCategoryText() {
|
||||
const categoryMap = {
|
||||
1: '乳肉兼用',
|
||||
2: '肉用',
|
||||
3: '乳用',
|
||||
4: '种用'
|
||||
};
|
||||
return categoryMap[this.category] || '未知品类';
|
||||
}
|
||||
|
||||
// 获取来源类型文本
|
||||
getSourceTypeText() {
|
||||
const sourceMap = {
|
||||
1: '合作社',
|
||||
2: '农户',
|
||||
3: '养殖场',
|
||||
4: '进口'
|
||||
};
|
||||
return sourceMap[this.source_type] || '未知来源';
|
||||
}
|
||||
|
||||
// 格式化出生日期
|
||||
getBirthDateFormatted() {
|
||||
if (this.birth_date) {
|
||||
const date = new Date(this.birth_date);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// 格式化入场日期
|
||||
getEntryDateFormatted() {
|
||||
if (this.entry_date) {
|
||||
const date = new Date(this.entry_date);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Animal模型
|
||||
Animal.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
comment: '动物ID'
|
||||
},
|
||||
type: {
|
||||
collar_number: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
comment: '项圈编号'
|
||||
},
|
||||
count: {
|
||||
ear_tag: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '动物耳号'
|
||||
},
|
||||
animal_type: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
defaultValue: 1,
|
||||
comment: '动物类型:1-牛,2-羊,3-猪,4-马'
|
||||
},
|
||||
breed: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '品种:1-西藏高山牦牛,2-荷斯坦奶牛,3-西门塔尔牛,4-安格斯牛,5-小尾寒羊,6-波尔山羊'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '品类:1-乳肉兼用,2-肉用,3-乳用,4-种用'
|
||||
},
|
||||
source_type: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '来源类型:1-合作社,2-农户,3-养殖场,4-进口'
|
||||
},
|
||||
birth_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '出生日期'
|
||||
},
|
||||
birth_weight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0.00,
|
||||
comment: '出生体重'
|
||||
},
|
||||
weaning_weight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0.00,
|
||||
comment: '断奶体重'
|
||||
},
|
||||
weaning_age: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '断奶日龄'
|
||||
},
|
||||
entry_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '入场日期'
|
||||
},
|
||||
calving_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '历史已产胎次'
|
||||
},
|
||||
left_teat_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '乳头数(左)'
|
||||
},
|
||||
right_teat_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '乳头数(右)'
|
||||
},
|
||||
farm_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '农场ID'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'farms',
|
||||
key: 'id'
|
||||
}
|
||||
defaultValue: 1,
|
||||
comment: '状态:1-正常,2-生病,3-死亡'
|
||||
},
|
||||
health_status: {
|
||||
type: DataTypes.ENUM('healthy', 'sick', 'quarantine', 'treatment'),
|
||||
defaultValue: 'healthy'
|
||||
},
|
||||
last_inspection: {
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '创建时间'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '更新时间'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'animals',
|
||||
modelName: 'Animal',
|
||||
tableName: 'animals',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['collar_number']
|
||||
},
|
||||
{
|
||||
fields: ['ear_tag']
|
||||
},
|
||||
{
|
||||
fields: ['farm_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 导出动物模型
|
||||
* @exports Animal
|
||||
*/
|
||||
module.exports = Animal;
|
||||
105
backend/models/CattleBatch.js
Normal file
105
backend/models/CattleBatch.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const CattleBatch = sequelize.define('CattleBatch', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '批次名称'
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '批次编号'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('育成批次', '繁殖批次', '育肥批次', '隔离批次', '治疗批次'),
|
||||
allowNull: false,
|
||||
comment: '批次类型'
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
field: 'start_date',
|
||||
comment: '开始日期'
|
||||
},
|
||||
expectedEndDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'expected_end_date',
|
||||
comment: '预计结束日期'
|
||||
},
|
||||
actualEndDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'actual_end_date',
|
||||
comment: '实际结束日期'
|
||||
},
|
||||
targetCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'target_count',
|
||||
comment: '目标牛只数量'
|
||||
},
|
||||
currentCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'current_count',
|
||||
comment: '当前牛只数量'
|
||||
},
|
||||
manager: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '负责人'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('进行中', '已完成', '已暂停'),
|
||||
allowNull: false,
|
||||
defaultValue: '进行中',
|
||||
comment: '状态'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
farmId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'farm_id',
|
||||
comment: '所属农场ID'
|
||||
}
|
||||
}, {
|
||||
tableName: 'cattle_batches',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '批次设置表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
CattleBatch.associate = (models) => {
|
||||
// 批次属于农场
|
||||
CattleBatch.belongsTo(models.Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 批次与动物的多对多关系
|
||||
CattleBatch.belongsToMany(models.Animal, {
|
||||
through: 'cattle_batch_animals',
|
||||
foreignKey: 'batch_id',
|
||||
otherKey: 'animal_id',
|
||||
as: 'animals'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CattleBatch;
|
||||
65
backend/models/CattleBatchAnimal.js
Normal file
65
backend/models/CattleBatchAnimal.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const CattleBatchAnimal = sequelize.define('CattleBatchAnimal', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
batchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'batch_id',
|
||||
comment: '批次ID'
|
||||
},
|
||||
animalId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'animal_id',
|
||||
comment: '动物ID'
|
||||
},
|
||||
addedDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'added_date',
|
||||
comment: '添加日期'
|
||||
},
|
||||
addedBy: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'added_by',
|
||||
comment: '添加人ID'
|
||||
}
|
||||
}, {
|
||||
tableName: 'cattle_batch_animals',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '批次牛只关联表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
CattleBatchAnimal.associate = (models) => {
|
||||
// 关联到批次
|
||||
CattleBatchAnimal.belongsTo(models.CattleBatch, {
|
||||
foreignKey: 'batchId',
|
||||
as: 'batch'
|
||||
});
|
||||
|
||||
// 关联到牛只
|
||||
CattleBatchAnimal.belongsTo(models.IotCattle, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'cattle'
|
||||
});
|
||||
|
||||
// 关联到添加人
|
||||
CattleBatchAnimal.belongsTo(models.User, {
|
||||
foreignKey: 'addedBy',
|
||||
as: 'adder'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CattleBatchAnimal;
|
||||
110
backend/models/CattleExitRecord.js
Normal file
110
backend/models/CattleExitRecord.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const CattleExitRecord = sequelize.define('CattleExitRecord', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
recordId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: 'record_id',
|
||||
comment: '记录编号'
|
||||
},
|
||||
animalId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'animal_id',
|
||||
comment: '动物ID'
|
||||
},
|
||||
exitDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
field: 'exit_date',
|
||||
comment: '离栏日期'
|
||||
},
|
||||
exitReason: {
|
||||
type: DataTypes.ENUM('出售', '死亡', '淘汰', '转场', '其他'),
|
||||
allowNull: false,
|
||||
field: 'exit_reason',
|
||||
comment: '离栏原因'
|
||||
},
|
||||
originalPenId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'original_pen_id',
|
||||
comment: '原栏舍ID'
|
||||
},
|
||||
destination: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '去向'
|
||||
},
|
||||
disposalMethod: {
|
||||
type: DataTypes.ENUM('屠宰', '转售', '掩埋', '焚烧', '其他'),
|
||||
allowNull: false,
|
||||
field: 'disposal_method',
|
||||
comment: '处理方式'
|
||||
},
|
||||
handler: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '处理人员'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('已确认', '待确认', '已取消'),
|
||||
allowNull: false,
|
||||
defaultValue: '待确认',
|
||||
comment: '状态'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
farmId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'farm_id',
|
||||
comment: '所属农场ID'
|
||||
},
|
||||
earNumber: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
field: 'ear_number',
|
||||
comment: '牛只耳号'
|
||||
}
|
||||
}, {
|
||||
tableName: 'cattle_exit_records',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '离栏记录表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
CattleExitRecord.associate = (models) => {
|
||||
// 离栏记录属于牛只
|
||||
CattleExitRecord.belongsTo(models.IotCattle, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'cattle'
|
||||
});
|
||||
|
||||
// 离栏记录属于原栏舍
|
||||
CattleExitRecord.belongsTo(models.CattlePen, {
|
||||
foreignKey: 'originalPenId',
|
||||
as: 'originalPen'
|
||||
});
|
||||
|
||||
// 离栏记录属于农场
|
||||
CattleExitRecord.belongsTo(models.Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CattleExitRecord;
|
||||
89
backend/models/CattlePen.js
Normal file
89
backend/models/CattlePen.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const CattlePen = sequelize.define('CattlePen', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '栏舍名称'
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '栏舍编号'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('育成栏', '产房', '配种栏', '隔离栏', '治疗栏'),
|
||||
allowNull: false,
|
||||
comment: '栏舍类型'
|
||||
},
|
||||
capacity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '栏舍容量'
|
||||
},
|
||||
currentCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'current_count',
|
||||
comment: '当前牛只数量'
|
||||
},
|
||||
area: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
comment: '面积(平方米)'
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '位置描述'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('启用', '停用'),
|
||||
allowNull: false,
|
||||
defaultValue: '启用',
|
||||
comment: '状态'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
farmId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'farm_id',
|
||||
comment: '所属农场ID'
|
||||
}
|
||||
}, {
|
||||
tableName: 'cattle_pens',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '栏舍设置表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
CattlePen.associate = (models) => {
|
||||
// 栏舍属于农场
|
||||
CattlePen.belongsTo(models.Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 栏舍有多个动物
|
||||
CattlePen.hasMany(models.Animal, {
|
||||
foreignKey: 'penId',
|
||||
as: 'animals'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CattlePen;
|
||||
110
backend/models/CattleTransferRecord.js
Normal file
110
backend/models/CattleTransferRecord.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const CattleTransferRecord = sequelize.define('CattleTransferRecord', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
recordId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: 'record_id',
|
||||
comment: '记录编号'
|
||||
},
|
||||
animalId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'animal_id',
|
||||
comment: '动物ID'
|
||||
},
|
||||
fromPenId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'from_pen_id',
|
||||
comment: '转出栏舍ID'
|
||||
},
|
||||
toPenId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'to_pen_id',
|
||||
comment: '转入栏舍ID'
|
||||
},
|
||||
transferDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
field: 'transfer_date',
|
||||
comment: '转栏日期'
|
||||
},
|
||||
reason: {
|
||||
type: DataTypes.ENUM('正常调栏', '疾病治疗', '配种需要', '产房准备', '隔离观察', '其他'),
|
||||
allowNull: false,
|
||||
comment: '转栏原因'
|
||||
},
|
||||
operator: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '操作人员'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('已完成', '进行中'),
|
||||
allowNull: false,
|
||||
defaultValue: '已完成',
|
||||
comment: '状态'
|
||||
},
|
||||
remark: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
farmId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'farm_id',
|
||||
comment: '所属农场ID'
|
||||
},
|
||||
earNumber: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
field: 'ear_number',
|
||||
comment: '牛只耳号'
|
||||
}
|
||||
}, {
|
||||
tableName: 'cattle_transfer_records',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '转栏记录表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
CattleTransferRecord.associate = (models) => {
|
||||
// 转栏记录属于牛只
|
||||
CattleTransferRecord.belongsTo(models.IotCattle, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'cattle'
|
||||
});
|
||||
|
||||
// 转栏记录属于转出栏舍
|
||||
CattleTransferRecord.belongsTo(models.CattlePen, {
|
||||
foreignKey: 'fromPenId',
|
||||
as: 'fromPen'
|
||||
});
|
||||
|
||||
// 转栏记录属于转入栏舍
|
||||
CattleTransferRecord.belongsTo(models.CattlePen, {
|
||||
foreignKey: 'toPenId',
|
||||
as: 'toPen'
|
||||
});
|
||||
|
||||
// 转栏记录属于农场
|
||||
CattleTransferRecord.belongsTo(models.Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = CattleTransferRecord;
|
||||
45
backend/models/CattleType.js
Normal file
45
backend/models/CattleType.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
/**
|
||||
* 牛只品种模型
|
||||
*/
|
||||
class CattleType extends BaseModel {
|
||||
static init(sequelize) {
|
||||
return super.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
comment: '品种ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '品种名称'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '品种描述'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'CattleType',
|
||||
tableName: 'cattle_type',
|
||||
comment: '牛只品种表',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
});
|
||||
}
|
||||
|
||||
static associate(models) {
|
||||
// 一个品种可以有多个牛只
|
||||
this.hasMany(models.IotCattle, {
|
||||
foreignKey: 'varieties',
|
||||
as: 'cattle'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CattleType;
|
||||
45
backend/models/CattleUser.js
Normal file
45
backend/models/CattleUser.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
/**
|
||||
* 牛只用途模型
|
||||
*/
|
||||
class CattleUser extends BaseModel {
|
||||
static init(sequelize) {
|
||||
return super.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
comment: '用途ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '用途名称'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '用途描述'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'CattleUser',
|
||||
tableName: 'cattle_user',
|
||||
comment: '牛只用途表',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
});
|
||||
}
|
||||
|
||||
static associate(models) {
|
||||
// 一个用途可以有多个牛只
|
||||
this.hasMany(models.IotCattle, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'cattle'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CattleUser;
|
||||
243
backend/models/ElectronicFence.js
Normal file
243
backend/models/ElectronicFence.js
Normal file
@@ -0,0 +1,243 @@
|
||||
const { DataTypes } = require('sequelize')
|
||||
const BaseModel = require('./BaseModel')
|
||||
const { sequelize } = require('../config/database-simple')
|
||||
|
||||
/**
|
||||
* 电子围栏模型
|
||||
*/
|
||||
class ElectronicFence extends BaseModel {
|
||||
static init(sequelize) {
|
||||
return super.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '围栏ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '围栏名称'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('collector', 'grazing', 'safety'),
|
||||
allowNull: false,
|
||||
defaultValue: 'collector',
|
||||
comment: '围栏类型: collector-采集器电子围栏, grazing-放牧围栏, safety-安全围栏'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '围栏描述'
|
||||
},
|
||||
coordinates: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
comment: '围栏坐标点数组'
|
||||
},
|
||||
center_lng: {
|
||||
type: DataTypes.DECIMAL(10, 7),
|
||||
allowNull: false,
|
||||
comment: '围栏中心经度'
|
||||
},
|
||||
center_lat: {
|
||||
type: DataTypes.DECIMAL(10, 7),
|
||||
allowNull: false,
|
||||
comment: '围栏中心纬度'
|
||||
},
|
||||
area: {
|
||||
type: DataTypes.DECIMAL(10, 4),
|
||||
allowNull: true,
|
||||
comment: '围栏面积(平方米)'
|
||||
},
|
||||
grazing_status: {
|
||||
type: DataTypes.ENUM('grazing', 'not_grazing'),
|
||||
allowNull: false,
|
||||
defaultValue: 'not_grazing',
|
||||
comment: '放牧状态: grazing-放牧中, not_grazing-未放牧'
|
||||
},
|
||||
inside_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '安全区域内动物数量'
|
||||
},
|
||||
outside_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '安全区域外动物数量'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否启用'
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '创建人ID'
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '更新人ID'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ElectronicFence',
|
||||
tableName: 'electronic_fences',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '电子围栏表',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_fence_name',
|
||||
fields: ['name']
|
||||
},
|
||||
{
|
||||
name: 'idx_fence_type',
|
||||
fields: ['type']
|
||||
},
|
||||
{
|
||||
name: 'idx_fence_center',
|
||||
fields: ['center_lng', 'center_lat']
|
||||
},
|
||||
{
|
||||
name: 'idx_fence_active',
|
||||
fields: ['is_active']
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义关联关系
|
||||
*/
|
||||
static associate(models) {
|
||||
// 围栏与农场关联(可选)
|
||||
this.belongsTo(models.Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取围栏类型文本
|
||||
*/
|
||||
getTypeText() {
|
||||
const typeMap = {
|
||||
'collector': '采集器电子围栏',
|
||||
'grazing': '放牧围栏',
|
||||
'safety': '安全围栏'
|
||||
}
|
||||
return typeMap[this.type] || '未知类型'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取放牧状态文本
|
||||
*/
|
||||
getGrazingStatusText() {
|
||||
const statusMap = {
|
||||
'grazing': '放牧中',
|
||||
'not_grazing': '未放牧'
|
||||
}
|
||||
return statusMap[this.grazing_status] || '未知状态'
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算围栏面积(简化计算)
|
||||
*/
|
||||
calculateArea() {
|
||||
if (!this.coordinates || this.coordinates.length < 3) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 使用Shoelace公式计算多边形面积
|
||||
let area = 0
|
||||
const coords = this.coordinates
|
||||
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
const j = (i + 1) % coords.length
|
||||
area += coords[i].lng * coords[j].lat
|
||||
area -= coords[j].lng * coords[i].lat
|
||||
}
|
||||
|
||||
// 转换为平方米(粗略计算)
|
||||
return Math.abs(area) * 111000 * 111000 / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算围栏中心点
|
||||
*/
|
||||
calculateCenter() {
|
||||
if (!this.coordinates || this.coordinates.length === 0) {
|
||||
return { lng: 0, lat: 0 }
|
||||
}
|
||||
|
||||
let lngSum = 0
|
||||
let latSum = 0
|
||||
|
||||
this.coordinates.forEach(coord => {
|
||||
lngSum += coord.lng
|
||||
latSum += coord.lat
|
||||
})
|
||||
|
||||
return {
|
||||
lng: lngSum / this.coordinates.length,
|
||||
lat: latSum / this.coordinates.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查点是否在围栏内
|
||||
*/
|
||||
isPointInside(lng, lat) {
|
||||
if (!this.coordinates || this.coordinates.length < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
let inside = false
|
||||
const coords = this.coordinates
|
||||
|
||||
for (let i = 0, j = coords.length - 1; i < coords.length; j = i++) {
|
||||
if (((coords[i].lat > lat) !== (coords[j].lat > lat)) &&
|
||||
(lng < (coords[j].lng - coords[i].lng) * (lat - coords[i].lat) / (coords[j].lat - coords[i].lat) + coords[i].lng)) {
|
||||
inside = !inside
|
||||
}
|
||||
}
|
||||
|
||||
return inside
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为前端格式
|
||||
*/
|
||||
toFrontendFormat() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
type: this.getTypeText(),
|
||||
description: this.description,
|
||||
coordinates: this.coordinates,
|
||||
center: {
|
||||
lng: this.center_lng,
|
||||
lat: this.center_lat
|
||||
},
|
||||
area: this.area,
|
||||
grazingStatus: this.getGrazingStatusText(),
|
||||
insideCount: this.inside_count,
|
||||
outsideCount: this.outside_count,
|
||||
isActive: this.is_active,
|
||||
createdAt: this.created_at,
|
||||
updatedAt: this.updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化模型
|
||||
ElectronicFence.init(sequelize)
|
||||
|
||||
module.exports = ElectronicFence
|
||||
298
backend/models/ElectronicFencePoint.js
Normal file
298
backend/models/ElectronicFencePoint.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 电子围栏坐标点模型
|
||||
* 用于存储围栏绘制过程中用户选定的经纬度坐标点
|
||||
*/
|
||||
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
class ElectronicFencePoint extends BaseModel {
|
||||
// 模型属性定义
|
||||
static attributes = {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '主键ID'
|
||||
},
|
||||
fence_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '关联的围栏ID',
|
||||
references: {
|
||||
model: 'electronic_fences',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
point_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '坐标点在围栏中的顺序(从0开始)'
|
||||
},
|
||||
longitude: {
|
||||
type: DataTypes.DECIMAL(10, 7),
|
||||
allowNull: false,
|
||||
comment: '经度'
|
||||
},
|
||||
latitude: {
|
||||
type: DataTypes.DECIMAL(10, 7),
|
||||
allowNull: false,
|
||||
comment: '纬度'
|
||||
},
|
||||
point_type: {
|
||||
type: DataTypes.ENUM('corner', 'control', 'marker'),
|
||||
allowNull: false,
|
||||
defaultValue: 'corner',
|
||||
comment: '坐标点类型:corner-拐角点,control-控制点,marker-标记点'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '坐标点描述信息'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否激活'
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '创建人ID'
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '更新人ID'
|
||||
}
|
||||
};
|
||||
|
||||
// 模型选项
|
||||
static options = {
|
||||
tableName: 'electronic_fence_points',
|
||||
comment: '电子围栏坐标点表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['fence_id']
|
||||
},
|
||||
{
|
||||
fields: ['fence_id', 'point_order']
|
||||
},
|
||||
{
|
||||
fields: ['longitude', 'latitude']
|
||||
},
|
||||
{
|
||||
fields: ['point_type']
|
||||
},
|
||||
{
|
||||
fields: ['is_active']
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
beforeCreate: (point, options) => {
|
||||
// 创建前钩子
|
||||
if (!point.point_order && point.point_order !== 0) {
|
||||
// 如果没有指定顺序,自动计算
|
||||
return ElectronicFencePoint.count({
|
||||
where: { fence_id: point.fence_id }
|
||||
}).then(count => {
|
||||
point.point_order = count;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 实例方法
|
||||
/**
|
||||
* 获取坐标点的经纬度对象
|
||||
* @returns {Object} 包含lng和lat的对象
|
||||
*/
|
||||
getCoordinates() {
|
||||
return {
|
||||
lng: parseFloat(this.longitude),
|
||||
lat: parseFloat(this.latitude)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置坐标点
|
||||
* @param {number} lng - 经度
|
||||
* @param {number} lat - 纬度
|
||||
*/
|
||||
setCoordinates(lng, lat) {
|
||||
this.longitude = lng;
|
||||
this.latitude = lat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为前端格式
|
||||
* @returns {Object} 前端使用的坐标点格式
|
||||
*/
|
||||
toFrontendFormat() {
|
||||
return {
|
||||
id: this.id,
|
||||
fenceId: this.fence_id,
|
||||
pointOrder: this.point_order,
|
||||
lng: parseFloat(this.longitude),
|
||||
lat: parseFloat(this.latitude),
|
||||
pointType: this.point_type,
|
||||
description: this.description,
|
||||
isActive: this.is_active,
|
||||
createdAt: this.created_at,
|
||||
updatedAt: this.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算到另一个点的距离(米)
|
||||
* @param {ElectronicFencePoint} otherPoint - 另一个坐标点
|
||||
* @returns {number} 距离(米)
|
||||
*/
|
||||
distanceTo(otherPoint) {
|
||||
const R = 6371000; // 地球半径(米)
|
||||
const lat1 = this.latitude * Math.PI / 180;
|
||||
const lat2 = otherPoint.latitude * Math.PI / 180;
|
||||
const deltaLat = (otherPoint.latitude - this.latitude) * Math.PI / 180;
|
||||
const deltaLng = (otherPoint.longitude - this.longitude) * Math.PI / 180;
|
||||
|
||||
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) *
|
||||
Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查坐标点是否在指定范围内
|
||||
* @param {number} centerLng - 中心点经度
|
||||
* @param {number} centerLat - 中心点纬度
|
||||
* @param {number} radius - 半径(米)
|
||||
* @returns {boolean} 是否在范围内
|
||||
*/
|
||||
isWithinRadius(centerLng, centerLat, radius) {
|
||||
const centerPoint = {
|
||||
longitude: centerLng,
|
||||
latitude: centerLat
|
||||
};
|
||||
return this.distanceTo(centerPoint) <= radius;
|
||||
}
|
||||
|
||||
// 静态方法
|
||||
/**
|
||||
* 根据围栏ID获取所有坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Array>} 坐标点数组
|
||||
*/
|
||||
static async getByFenceId(fenceId, options = {}) {
|
||||
const defaultOptions = {
|
||||
where: {
|
||||
fence_id: fenceId,
|
||||
is_active: true
|
||||
},
|
||||
order: [['point_order', 'ASC']]
|
||||
};
|
||||
|
||||
return this.findAll({
|
||||
...defaultOptions,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @param {Array} points - 坐标点数组
|
||||
* @param {Object} options - 创建选项
|
||||
* @returns {Promise<Array>} 创建的坐标点数组
|
||||
*/
|
||||
static async createPoints(fenceId, points, options = {}) {
|
||||
const pointsData = points.map((point, index) => ({
|
||||
fence_id: fenceId,
|
||||
point_order: index,
|
||||
longitude: point.lng,
|
||||
latitude: point.lat,
|
||||
point_type: point.type || 'corner',
|
||||
description: point.description || null,
|
||||
created_by: options.createdBy || null
|
||||
}));
|
||||
|
||||
return this.bulkCreate(pointsData, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新围栏的所有坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @param {Array} points - 新的坐标点数组
|
||||
* @param {Object} options - 更新选项
|
||||
* @returns {Promise<Array>} 更新后的坐标点数组
|
||||
*/
|
||||
static async updateFencePoints(fenceId, points, options = {}) {
|
||||
const transaction = options.transaction || await sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 删除现有坐标点
|
||||
await this.destroy({
|
||||
where: { fence_id: fenceId },
|
||||
transaction
|
||||
});
|
||||
|
||||
// 创建新的坐标点
|
||||
const newPoints = await this.createPoints(fenceId, points, {
|
||||
...options,
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
|
||||
return newPoints;
|
||||
} catch (error) {
|
||||
if (!options.transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取围栏的边界框
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @returns {Promise<Object>} 边界框对象
|
||||
*/
|
||||
static async getFenceBounds(fenceId) {
|
||||
const points = await this.getByFenceId(fenceId);
|
||||
|
||||
if (points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lngs = points.map(p => parseFloat(p.longitude));
|
||||
const lats = points.map(p => parseFloat(p.latitude));
|
||||
|
||||
return {
|
||||
minLng: Math.min(...lngs),
|
||||
maxLng: Math.max(...lngs),
|
||||
minLat: Math.min(...lats),
|
||||
maxLat: Math.max(...lats),
|
||||
center: {
|
||||
lng: (Math.min(...lngs) + Math.max(...lngs)) / 2,
|
||||
lat: (Math.min(...lats) + Math.max(...lats)) / 2
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化模型
|
||||
ElectronicFencePoint.init(ElectronicFencePoint.attributes, {
|
||||
...ElectronicFencePoint.options,
|
||||
sequelize,
|
||||
modelName: 'ElectronicFencePoint'
|
||||
});
|
||||
|
||||
module.exports = ElectronicFencePoint;
|
||||
125
backend/models/FormLog.js
Normal file
125
backend/models/FormLog.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const { DataTypes } = require('sequelize')
|
||||
const BaseModel = require('./BaseModel')
|
||||
|
||||
class FormLog extends BaseModel {
|
||||
static init(sequelize) {
|
||||
return super.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '日志ID'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '模块名称'
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'userId',
|
||||
comment: '用户ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
field: 'username',
|
||||
comment: '用户名'
|
||||
},
|
||||
sessionId: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
field: 'sessionId',
|
||||
comment: '会话ID'
|
||||
},
|
||||
userAgent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'userAgent',
|
||||
comment: '用户代理'
|
||||
},
|
||||
screenResolution: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
field: 'screenResolution',
|
||||
comment: '屏幕分辨率'
|
||||
},
|
||||
currentUrl: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'currentUrl',
|
||||
comment: '当前URL'
|
||||
},
|
||||
formData: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
field: 'formData',
|
||||
comment: '表单数据'
|
||||
},
|
||||
additionalData: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
field: 'additionalData',
|
||||
comment: '附加数据'
|
||||
},
|
||||
ipAddress: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: true,
|
||||
field: 'ipAddress',
|
||||
comment: 'IP地址'
|
||||
},
|
||||
timestamp: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'timestamp',
|
||||
comment: '时间戳'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('success', 'error', 'warning'),
|
||||
allowNull: false,
|
||||
defaultValue: 'success',
|
||||
field: 'status',
|
||||
comment: '状态'
|
||||
},
|
||||
errorMessage: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'errorMessage',
|
||||
comment: '错误信息'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'FormLog',
|
||||
tableName: 'form_logs',
|
||||
comment: '表单操作日志表',
|
||||
timestamps: false, // 禁用自动时间戳
|
||||
indexes: [
|
||||
{
|
||||
fields: ['module', 'action']
|
||||
},
|
||||
{
|
||||
fields: ['userId']
|
||||
},
|
||||
{
|
||||
fields: ['timestamp']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
static associate(models) {
|
||||
// 可以添加与其他模型的关联
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FormLog
|
||||
333
backend/models/IotCattle.js
Normal file
333
backend/models/IotCattle.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
const IotCattle = sequelize.define('IotCattle', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
field: 'id'
|
||||
},
|
||||
orgId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'org_id',
|
||||
comment: '组织ID'
|
||||
},
|
||||
earNumber: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
field: 'ear_number',
|
||||
comment: '耳标号'
|
||||
},
|
||||
sex: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'sex',
|
||||
comment: '性别'
|
||||
},
|
||||
strain: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'strain',
|
||||
comment: '品系'
|
||||
},
|
||||
varieties: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'varieties',
|
||||
comment: '品种'
|
||||
},
|
||||
cate: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'cate',
|
||||
comment: '类别'
|
||||
},
|
||||
birthWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'birth_weight',
|
||||
comment: '出生体重'
|
||||
},
|
||||
birthday: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'birthday',
|
||||
comment: '出生日期'
|
||||
},
|
||||
penId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'pen_id',
|
||||
comment: '栏舍ID'
|
||||
},
|
||||
intoTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'into_time',
|
||||
comment: '入栏时间'
|
||||
},
|
||||
parity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'parity',
|
||||
comment: '胎次'
|
||||
},
|
||||
source: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'source',
|
||||
comment: '来源'
|
||||
},
|
||||
sourceDay: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'source_day',
|
||||
comment: '来源天数'
|
||||
},
|
||||
sourceWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'source_weight',
|
||||
comment: '来源体重'
|
||||
},
|
||||
weight: {
|
||||
type: DataTypes.DOUBLE(11, 2),
|
||||
allowNull: false,
|
||||
field: 'weight',
|
||||
comment: '当前体重'
|
||||
},
|
||||
event: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'event',
|
||||
comment: '事件'
|
||||
},
|
||||
eventTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'event_time',
|
||||
comment: '事件时间'
|
||||
},
|
||||
lactationDay: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'lactation_day',
|
||||
comment: '泌乳天数'
|
||||
},
|
||||
semenNum: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
field: 'semen_num',
|
||||
comment: '精液编号'
|
||||
},
|
||||
isWear: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_wear',
|
||||
comment: '是否佩戴设备'
|
||||
},
|
||||
batchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'batch_id',
|
||||
comment: '批次ID'
|
||||
},
|
||||
imgs: {
|
||||
type: DataTypes.STRING(800),
|
||||
allowNull: false,
|
||||
field: 'imgs',
|
||||
comment: '图片'
|
||||
},
|
||||
isEleAuth: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_ele_auth',
|
||||
comment: '是否电子认证'
|
||||
},
|
||||
isQuaAuth: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_qua_auth',
|
||||
comment: '是否质量认证'
|
||||
},
|
||||
isDelete: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'is_delete',
|
||||
comment: '是否删除'
|
||||
},
|
||||
isOut: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'is_out',
|
||||
comment: '是否出栏'
|
||||
},
|
||||
createUid: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'create_uid',
|
||||
comment: '创建人ID'
|
||||
},
|
||||
createTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'create_time',
|
||||
comment: '创建时间'
|
||||
},
|
||||
algebra: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'algebra',
|
||||
comment: '代数'
|
||||
},
|
||||
colour: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
field: 'colour',
|
||||
comment: '毛色'
|
||||
},
|
||||
infoWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'info_weight',
|
||||
comment: '信息体重'
|
||||
},
|
||||
descent: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'descent',
|
||||
comment: '血统'
|
||||
},
|
||||
isVaccin: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_vaccin',
|
||||
comment: '是否接种疫苗'
|
||||
},
|
||||
isInsemination: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_insemination',
|
||||
comment: '是否人工授精'
|
||||
},
|
||||
isInsure: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_insure',
|
||||
comment: '是否投保'
|
||||
},
|
||||
isMortgage: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'is_mortgage',
|
||||
comment: '是否抵押'
|
||||
},
|
||||
updateTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'update_time',
|
||||
comment: '更新时间'
|
||||
},
|
||||
breedBullTime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'breed_bull_time',
|
||||
comment: '配种时间'
|
||||
},
|
||||
level: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
field: 'level',
|
||||
comment: '等级'
|
||||
},
|
||||
sixWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'six_weight',
|
||||
comment: '6月龄体重'
|
||||
},
|
||||
eighteenWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'eighteen_weight',
|
||||
comment: '18月龄体重'
|
||||
},
|
||||
twelveDayWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'twelve_day_weight',
|
||||
comment: '12日龄体重'
|
||||
},
|
||||
eighteenDayWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'eighteen_day_weight',
|
||||
comment: '18日龄体重'
|
||||
},
|
||||
xxivDayWeight: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
field: 'xxiv_day_weight',
|
||||
comment: '24日龄体重'
|
||||
},
|
||||
semenBreedImgs: {
|
||||
type: DataTypes.STRING(800),
|
||||
allowNull: false,
|
||||
field: 'semen_breed_imgs',
|
||||
comment: '精液配种图片'
|
||||
},
|
||||
sellStatus: {
|
||||
type: DataTypes.SMALLINT,
|
||||
allowNull: false,
|
||||
field: 'sell_status',
|
||||
comment: '销售状态'
|
||||
},
|
||||
weightCalculateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'weight_calculate_time',
|
||||
comment: '体重计算时间'
|
||||
},
|
||||
dayOfBirthday: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'day_of_birthday',
|
||||
comment: '出生天数'
|
||||
}
|
||||
}, {
|
||||
tableName: 'iot_cattle',
|
||||
timestamps: false, // iot_cattle表没有created_at和updated_at字段
|
||||
comment: '物联网牛只表'
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
IotCattle.associate = (models) => {
|
||||
// 关联到农场
|
||||
IotCattle.belongsTo(models.Farm, {
|
||||
foreignKey: 'orgId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 关联到批次
|
||||
IotCattle.belongsTo(models.CattleBatch, {
|
||||
foreignKey: 'batchId',
|
||||
as: 'batch'
|
||||
});
|
||||
|
||||
// 关联到栏舍
|
||||
IotCattle.belongsTo(models.CattlePen, {
|
||||
foreignKey: 'penId',
|
||||
as: 'pen'
|
||||
});
|
||||
|
||||
// 关联到围栏
|
||||
IotCattle.belongsTo(models.ElectronicFence, {
|
||||
foreignKey: 'fenceId',
|
||||
as: 'fence'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = IotCattle;
|
||||
391
backend/models/IotJbqClient.js
Normal file
391
backend/models/IotJbqClient.js
Normal file
@@ -0,0 +1,391 @@
|
||||
const { Model, DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class IotJbqClient extends Model {
|
||||
// 获取设备状态文本
|
||||
getStatusText() {
|
||||
const statusMap = {
|
||||
0: '离线',
|
||||
1: '在线',
|
||||
2: '报警',
|
||||
3: '维护'
|
||||
};
|
||||
return statusMap[this.state] || '未知';
|
||||
}
|
||||
|
||||
// 获取设备状态颜色
|
||||
getStatusColor() {
|
||||
const colorMap = {
|
||||
0: 'red', // 离线
|
||||
1: 'green', // 在线
|
||||
2: 'orange', // 报警
|
||||
3: 'blue' // 维护
|
||||
};
|
||||
return colorMap[this.state] || 'default';
|
||||
}
|
||||
|
||||
// 获取电量百分比
|
||||
getBatteryPercent() {
|
||||
const voltage = parseFloat(this.voltage) || 0;
|
||||
// 假设电压范围是0-100,转换为百分比
|
||||
return Math.min(100, Math.max(0, voltage));
|
||||
}
|
||||
|
||||
// 获取温度值
|
||||
getTemperatureValue() {
|
||||
return parseFloat(this.temperature) || 0;
|
||||
}
|
||||
|
||||
// 获取GPS状态文本
|
||||
getGpsStatusText() {
|
||||
const gpsMap = {
|
||||
'A': '有效',
|
||||
'V': '无效',
|
||||
'N': '无信号'
|
||||
};
|
||||
return gpsMap[this.gps_state] || '未知';
|
||||
}
|
||||
|
||||
// 获取最后更新时间
|
||||
getLastUpdateTime() {
|
||||
if (!this.uptime) return '-';
|
||||
const date = new Date(this.uptime * 1000);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取绑带状态文本
|
||||
getBandgeStatusText() {
|
||||
const statusMap = {
|
||||
0: '未绑定',
|
||||
1: '已绑定',
|
||||
2: '松动',
|
||||
3: '脱落'
|
||||
};
|
||||
return statusMap[this.bandge_status] || '未知';
|
||||
}
|
||||
|
||||
// 获取绑带状态颜色
|
||||
getBandgeStatusColor() {
|
||||
const colorMap = {
|
||||
0: 'default', // 未绑定
|
||||
1: 'green', // 已绑定
|
||||
2: 'orange', // 松动
|
||||
3: 'red' // 脱落
|
||||
};
|
||||
return colorMap[this.bandge_status] || 'default';
|
||||
}
|
||||
|
||||
// 检查是否有定位信息
|
||||
hasLocation() {
|
||||
return this.lat && this.lon && this.lat !== '0' && this.lon !== '0';
|
||||
}
|
||||
|
||||
// 获取耳标编号(使用aaid字段)
|
||||
getEartagNumber() {
|
||||
return this.aaid ? this.aaid.toString() : '-';
|
||||
}
|
||||
|
||||
// 获取被采集主机(使用sid字段)
|
||||
getHostId() {
|
||||
return this.sid || '-';
|
||||
}
|
||||
|
||||
// 获取总运动量
|
||||
getTotalMovement() {
|
||||
return this.walk || 0;
|
||||
}
|
||||
|
||||
// 获取当日运动量
|
||||
getDailyMovement() {
|
||||
return this.y_steps || 0;
|
||||
}
|
||||
|
||||
// 获取佩戴状态文本
|
||||
getWearStatusText() {
|
||||
return this.is_wear ? '已佩戴' : '未佩戴';
|
||||
}
|
||||
|
||||
// 获取佩戴状态颜色
|
||||
getWearStatusColor() {
|
||||
return this.is_wear ? 'green' : 'default';
|
||||
}
|
||||
|
||||
// 获取GPS信号等级
|
||||
getGpsSignalLevel() {
|
||||
const gpsState = this.gps_state;
|
||||
if (gpsState === 'A') {
|
||||
return '强';
|
||||
} else if (gpsState === 'V') {
|
||||
return '弱';
|
||||
} else {
|
||||
return '无';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IotJbqClient.init({
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '耳标设备ID'
|
||||
},
|
||||
org_id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '组织ID'
|
||||
},
|
||||
cid: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '设备CID'
|
||||
},
|
||||
aaid: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '耳标编号'
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '用户ID'
|
||||
},
|
||||
time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '时间戳'
|
||||
},
|
||||
uptime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '更新时间戳'
|
||||
},
|
||||
sid: {
|
||||
type: DataTypes.STRING(16),
|
||||
allowNull: false,
|
||||
comment: '被采集主机ID'
|
||||
},
|
||||
walk: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '总运动量'
|
||||
},
|
||||
y_steps: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '当日运动量'
|
||||
},
|
||||
r_walk: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '剩余运动量'
|
||||
},
|
||||
lat: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: '0',
|
||||
comment: '纬度'
|
||||
},
|
||||
lon: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: '0',
|
||||
comment: '经度'
|
||||
},
|
||||
gps_state: {
|
||||
type: DataTypes.STRING(5),
|
||||
allowNull: false,
|
||||
defaultValue: 'V',
|
||||
comment: 'GPS状态'
|
||||
},
|
||||
voltage: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true,
|
||||
comment: '电压/电量'
|
||||
},
|
||||
temperature: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true,
|
||||
comment: '温度'
|
||||
},
|
||||
temperature_two: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false,
|
||||
defaultValue: '0',
|
||||
comment: '温度2'
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.INTEGER(1),
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '设备状态:0-离线,1-在线,2-报警,3-维护'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: true,
|
||||
defaultValue: 1,
|
||||
comment: '设备类型'
|
||||
},
|
||||
sort: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: true,
|
||||
defaultValue: 4,
|
||||
comment: '排序'
|
||||
},
|
||||
ver: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false,
|
||||
defaultValue: '0',
|
||||
comment: '固件版本'
|
||||
},
|
||||
weight: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '重量'
|
||||
},
|
||||
start_time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '开始时间'
|
||||
},
|
||||
run_days: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 240,
|
||||
comment: '运行天数'
|
||||
},
|
||||
zenowalk: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '零步数'
|
||||
},
|
||||
zenotime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '零时间'
|
||||
},
|
||||
is_read: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '是否已读'
|
||||
},
|
||||
read_end_time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '读取结束时间'
|
||||
},
|
||||
bank_userid: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: true,
|
||||
comment: '银行用户ID'
|
||||
},
|
||||
bank_item_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '银行项目ID'
|
||||
},
|
||||
bank_house: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '银行房屋'
|
||||
},
|
||||
bank_lanwei: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '银行栏位'
|
||||
},
|
||||
bank_place: {
|
||||
type: DataTypes.TINYINT(2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '银行地点'
|
||||
},
|
||||
is_home: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '是否在家'
|
||||
},
|
||||
distribute_time: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '分发时间'
|
||||
},
|
||||
bandge_status: {
|
||||
type: DataTypes.INTEGER(5),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '绑带状态:0-未绑定,1-已绑定,2-松动,3-脱落'
|
||||
},
|
||||
is_wear: {
|
||||
type: DataTypes.TINYINT(1).UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否佩戴'
|
||||
},
|
||||
is_temperature: {
|
||||
type: DataTypes.TINYINT(1).UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否有温度'
|
||||
},
|
||||
source_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '来源ID'
|
||||
},
|
||||
expire_time: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '过期时间'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'IotJbqClient',
|
||||
tableName: 'iot_jbq_client',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ fields: ['org_id'] },
|
||||
{ fields: ['aaid'] },
|
||||
{ fields: ['uid'] },
|
||||
{ fields: ['uptime'] },
|
||||
{ fields: ['sid'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['is_wear'] },
|
||||
{ fields: ['source_id'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
IotJbqClient.associate = (models) => {
|
||||
// 与牛只档案的关联关系(通过cid和earNumber匹配)
|
||||
IotJbqClient.hasOne(models.IotCattle, {
|
||||
foreignKey: 'earNumber',
|
||||
sourceKey: 'cid',
|
||||
as: 'cattle'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = IotJbqClient;
|
||||
310
backend/models/IotJbqServer.js
Normal file
310
backend/models/IotJbqServer.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 智能主机设备模型
|
||||
* @file IotJbqServer.js
|
||||
* @description 智能主机设备数据模型,对应iot_jbq_server表
|
||||
*/
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class IotJbqServer extends Model {
|
||||
/**
|
||||
* 获取设备状态文本
|
||||
*/
|
||||
getStatusText() {
|
||||
const statusMap = {
|
||||
0: '离线',
|
||||
1: '在线',
|
||||
2: '报警',
|
||||
3: '维护'
|
||||
};
|
||||
return statusMap[this.state] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态颜色
|
||||
*/
|
||||
getStatusColor() {
|
||||
const colorMap = {
|
||||
0: 'red', // 离线
|
||||
1: 'green', // 在线
|
||||
2: 'orange', // 报警
|
||||
3: 'blue' // 维护
|
||||
};
|
||||
return colorMap[this.state] || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取GPS状态文本
|
||||
*/
|
||||
getGpsStatusText() {
|
||||
const gpsMap = {
|
||||
'A': '已定位',
|
||||
'V': '未定位'
|
||||
};
|
||||
return gpsMap[this.gps_state] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取GPS状态颜色
|
||||
*/
|
||||
getGpsStatusColor() {
|
||||
const colorMap = {
|
||||
'A': 'green', // 已定位
|
||||
'V': 'red' // 未定位
|
||||
};
|
||||
return colorMap[this.gps_state] || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电池电量百分比
|
||||
*/
|
||||
getBatteryPercent() {
|
||||
const voltage = parseFloat(this.voltage) || 0;
|
||||
// 假设电池电压范围是3.0V-4.2V
|
||||
if (voltage >= 4.2) return 100;
|
||||
if (voltage <= 3.0) return 0;
|
||||
return Math.round(((voltage - 3.0) / 1.2) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取温度数值
|
||||
*/
|
||||
getTemperatureValue() {
|
||||
return parseFloat(this.temperature) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号强度文本
|
||||
*/
|
||||
getSignalText() {
|
||||
const signal = parseInt(this.signa) || 0;
|
||||
if (signal >= 20) return '强';
|
||||
if (signal >= 10) return '中';
|
||||
if (signal >= 5) return '弱';
|
||||
return '无信号';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信号强度颜色
|
||||
*/
|
||||
getSignalColor() {
|
||||
const signal = parseInt(this.signa) || 0;
|
||||
if (signal >= 20) return 'green';
|
||||
if (signal >= 10) return 'orange';
|
||||
if (signal >= 5) return 'red';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否低电量
|
||||
*/
|
||||
isLowBattery() {
|
||||
return this.getBatteryPercent() < 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要维护
|
||||
*/
|
||||
needsMaintenance() {
|
||||
return this.state === 3 || this.isLowBattery() || this.gps_state === 'V';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后更新时间
|
||||
*/
|
||||
getLastUpdateTime() {
|
||||
if (this.uptime) {
|
||||
const date = new Date(this.uptime * 1000);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
return '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备编号(使用sid字段)
|
||||
*/
|
||||
getDeviceNumber() {
|
||||
return this.sid || '无编号';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断设备是否联网
|
||||
* 通过 simId 字段是否为空来判断联网状态
|
||||
*/
|
||||
isOnline() {
|
||||
return this.simId && this.simId.trim() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取联网状态文本
|
||||
*/
|
||||
getNetworkStatusText() {
|
||||
return this.isOnline() ? '已联网' : '未联网';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取联网状态颜色
|
||||
*/
|
||||
getNetworkStatusColor() {
|
||||
return this.isOnline() ? 'green' : 'red';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化模型
|
||||
IotJbqServer.init({
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '主机设备ID'
|
||||
},
|
||||
org_id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
comment: '组织ID'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: '',
|
||||
comment: '设备标题'
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '用户ID'
|
||||
},
|
||||
time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '设备时间戳'
|
||||
},
|
||||
uptime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '上传时间戳'
|
||||
},
|
||||
distribute_time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '分发时间'
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '设备状态'
|
||||
},
|
||||
sid: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
comment: '设备序列号'
|
||||
},
|
||||
gps_state: {
|
||||
type: DataTypes.STRING(5),
|
||||
allowNull: false,
|
||||
comment: 'GPS状态'
|
||||
},
|
||||
lat: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '纬度'
|
||||
},
|
||||
lon: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '经度'
|
||||
},
|
||||
signa: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '信号强度'
|
||||
},
|
||||
voltage: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false,
|
||||
comment: '电池电压'
|
||||
},
|
||||
temperature: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: false,
|
||||
comment: '设备温度'
|
||||
},
|
||||
ver: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '固件版本'
|
||||
},
|
||||
macsid: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: 'MAC序列号'
|
||||
},
|
||||
ctwing: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: 'CTWing信息'
|
||||
},
|
||||
bank_lanwei: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '栏位编号'
|
||||
},
|
||||
bank_house: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '栏舍编号'
|
||||
},
|
||||
bank_item_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '栏位项目ID'
|
||||
},
|
||||
fence_id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '围栏ID'
|
||||
},
|
||||
source_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '数据源ID'
|
||||
},
|
||||
simId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: 'SIM卡ID,用于判断联网状态'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'IotJbqServer',
|
||||
tableName: 'iot_jbq_server',
|
||||
timestamps: false, // 该表没有created_at和updated_at字段
|
||||
indexes: [
|
||||
{
|
||||
fields: ['uid']
|
||||
},
|
||||
{
|
||||
fields: ['sid']
|
||||
},
|
||||
{
|
||||
fields: ['state']
|
||||
},
|
||||
{
|
||||
fields: ['fence_id']
|
||||
},
|
||||
{
|
||||
fields: ['source_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = IotJbqServer;
|
||||
338
backend/models/IotXqClient.js
Normal file
338
backend/models/IotXqClient.js
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 智能项圈设备模型
|
||||
* @file IotXqClient.js
|
||||
* @description 智能项圈设备数据模型,对应iot_xq_client表
|
||||
*/
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class IotXqClient extends Model {
|
||||
/**
|
||||
* 获取设备状态文本
|
||||
*/
|
||||
getStatusText() {
|
||||
const statusMap = {
|
||||
0: '离线',
|
||||
1: '在线',
|
||||
2: '报警',
|
||||
3: '维护'
|
||||
};
|
||||
return statusMap[this.state] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态颜色
|
||||
*/
|
||||
getStatusColor() {
|
||||
const colorMap = {
|
||||
0: 'red', // 离线
|
||||
1: 'green', // 在线
|
||||
2: 'orange', // 报警
|
||||
3: 'blue' // 维护
|
||||
};
|
||||
return colorMap[this.state] || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取GPS信号强度等级(1-5星)
|
||||
*/
|
||||
getGpsSignalLevel() {
|
||||
const nsat = parseInt(this.nsat) || 0;
|
||||
if (nsat >= 8) return 5;
|
||||
if (nsat >= 6) return 4;
|
||||
if (nsat >= 4) return 3;
|
||||
if (nsat >= 2) return 2;
|
||||
if (nsat >= 1) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电池电量百分比
|
||||
*/
|
||||
getBatteryPercent() {
|
||||
const battery = parseFloat(this.battery) || 0;
|
||||
// 假设电池电压范围是3.0V-4.2V
|
||||
if (battery >= 4.2) return 100;
|
||||
if (battery <= 3.0) return 0;
|
||||
return Math.round(((battery - 3.0) / 1.2) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取体温数值
|
||||
*/
|
||||
getTemperatureValue() {
|
||||
return parseFloat(this.temperature) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否低电量
|
||||
*/
|
||||
isLowBattery() {
|
||||
return this.getBatteryPercent() < 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要维护
|
||||
*/
|
||||
needsMaintenance() {
|
||||
return this.state === 3 || !this.is_connect || this.isLowBattery();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后更新时间
|
||||
*/
|
||||
getLastUpdateTime() {
|
||||
if (this.uptime) {
|
||||
return new Date(this.uptime * 1000).toLocaleString('zh-CN');
|
||||
}
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化模型
|
||||
IotXqClient.init({
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '项圈设备ID'
|
||||
},
|
||||
org_id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '组织ID'
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '用户ID'
|
||||
},
|
||||
deviceId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '设备编号'
|
||||
},
|
||||
sn: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '设备序列号'
|
||||
},
|
||||
sort: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '排序'
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '设备状态'
|
||||
},
|
||||
longitude: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '经度'
|
||||
},
|
||||
latitude: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '纬度'
|
||||
},
|
||||
altitude: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '海拔'
|
||||
},
|
||||
gps_state: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: 'GPS状态'
|
||||
},
|
||||
nsat: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'GPS卫星数量'
|
||||
},
|
||||
rsrp: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '信号强度'
|
||||
},
|
||||
battery: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '电池电量'
|
||||
},
|
||||
temperature: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '体温'
|
||||
},
|
||||
steps: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '步数'
|
||||
},
|
||||
acc_x: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: 'X轴加速度'
|
||||
},
|
||||
acc_y: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: 'Y轴加速度'
|
||||
},
|
||||
acc_z: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: 'Z轴加速度'
|
||||
},
|
||||
bandge_status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '设备佩戴状态'
|
||||
},
|
||||
ver: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '固件版本'
|
||||
},
|
||||
time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '设备时间戳'
|
||||
},
|
||||
uptime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '上传时间戳'
|
||||
},
|
||||
distribute_time: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '分发时间'
|
||||
},
|
||||
zenowalk: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '零步数'
|
||||
},
|
||||
zenotime: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '零时间'
|
||||
},
|
||||
bank_item_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '栏位项目ID'
|
||||
},
|
||||
bank_house: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '栏舍编号'
|
||||
},
|
||||
bank_lanwei: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '栏位编号'
|
||||
},
|
||||
bank_place: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
comment: '位置编号'
|
||||
},
|
||||
is_home: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '是否在家'
|
||||
},
|
||||
fence_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '围栏ID'
|
||||
},
|
||||
y_steps: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '昨日步数'
|
||||
},
|
||||
is_wear: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否佩戴'
|
||||
},
|
||||
is_temperature: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否测温'
|
||||
},
|
||||
is_connect: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '是否连接'
|
||||
},
|
||||
source_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '数据源ID'
|
||||
},
|
||||
loctime: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
comment: '定位时间'
|
||||
},
|
||||
expire_time: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '过期时间'
|
||||
},
|
||||
subType: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '子类型'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'IotXqClient',
|
||||
tableName: 'iot_xq_client',
|
||||
timestamps: false, // 该表没有created_at和updated_at字段
|
||||
indexes: [
|
||||
{
|
||||
fields: ['uid']
|
||||
},
|
||||
{
|
||||
fields: ['sn']
|
||||
},
|
||||
{
|
||||
fields: ['ver']
|
||||
},
|
||||
{
|
||||
fields: ['fence_id']
|
||||
},
|
||||
{
|
||||
fields: ['source_id']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = IotXqClient;
|
||||
384
backend/models/MenuPermission.js
Normal file
384
backend/models/MenuPermission.js
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* 菜单权限模型
|
||||
* @file MenuPermission.js
|
||||
* @description 菜单权限配置数据模型
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class MenuPermission extends BaseModel {
|
||||
/**
|
||||
* 获取用户可访问的菜单
|
||||
* @param {Array} userRoles 用户角色数组
|
||||
* @param {Array} userPermissions 用户权限数组
|
||||
* @returns {Array} 菜单树结构
|
||||
*/
|
||||
static async getUserMenus(userRoles = [], userPermissions = []) {
|
||||
try {
|
||||
const allMenus = await this.findAll({
|
||||
where: {
|
||||
is_visible: true,
|
||||
is_enabled: true
|
||||
},
|
||||
order: [['sort_order', 'ASC']]
|
||||
});
|
||||
|
||||
// 过滤用户有权限的菜单
|
||||
const accessibleMenus = allMenus.filter(menu => {
|
||||
return this.checkMenuAccess(menu, userRoles, userPermissions);
|
||||
});
|
||||
|
||||
// 构建菜单树
|
||||
return this.buildMenuTree(accessibleMenus);
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查菜单访问权限
|
||||
* @param {Object} menu 菜单对象
|
||||
* @param {Array} userRoles 用户角色
|
||||
* @param {Array} userPermissions 用户权限
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
static checkMenuAccess(menu, userRoles, userPermissions) {
|
||||
// 解析所需角色
|
||||
let requiredRoles = [];
|
||||
if (menu.required_roles) {
|
||||
try {
|
||||
requiredRoles = JSON.parse(menu.required_roles);
|
||||
} catch (error) {
|
||||
console.error('解析菜单所需角色失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析所需权限
|
||||
let requiredPermissions = [];
|
||||
if (menu.required_permissions) {
|
||||
try {
|
||||
requiredPermissions = JSON.parse(menu.required_permissions);
|
||||
} catch (error) {
|
||||
console.error('解析菜单所需权限失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有配置权限要求,则默认允许访问
|
||||
if (requiredRoles.length === 0 && requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (requiredRoles.length > 0) {
|
||||
const hasRole = requiredRoles.some(role => userRoles.includes(role));
|
||||
if (!hasRole) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查具体权限
|
||||
if (requiredPermissions.length > 0) {
|
||||
const hasPermission = requiredPermissions.some(permission =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树结构
|
||||
* @param {Array} menus 菜单数组
|
||||
* @returns {Array} 菜单树
|
||||
*/
|
||||
static buildMenuTree(menus) {
|
||||
const menuMap = new Map();
|
||||
const roots = [];
|
||||
|
||||
// 创建菜单映射
|
||||
menus.forEach(menu => {
|
||||
menuMap.set(menu.id, {
|
||||
...menu.dataValues,
|
||||
children: []
|
||||
});
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
menus.forEach(menu => {
|
||||
const menuNode = menuMap.get(menu.id);
|
||||
|
||||
if (menu.parent_id && menuMap.has(menu.parent_id)) {
|
||||
const parent = menuMap.get(menu.parent_id);
|
||||
parent.children.push(menuNode);
|
||||
} else {
|
||||
roots.push(menuNode);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认菜单权限
|
||||
*/
|
||||
static async initDefaultMenus() {
|
||||
try {
|
||||
const defaultMenus = [
|
||||
// 主要功能模块
|
||||
{
|
||||
menu_key: 'home',
|
||||
menu_name: '首页',
|
||||
menu_path: '/',
|
||||
menu_type: 'page',
|
||||
icon: 'home-outlined',
|
||||
sort_order: 1,
|
||||
required_roles: '["user", "admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'dashboard',
|
||||
menu_name: '系统概览',
|
||||
menu_path: '/dashboard',
|
||||
menu_type: 'page',
|
||||
icon: 'dashboard-outlined',
|
||||
sort_order: 2,
|
||||
required_roles: '["user", "admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'monitor',
|
||||
menu_name: '实时监控',
|
||||
menu_path: '/monitor',
|
||||
menu_type: 'page',
|
||||
icon: 'line-chart-outlined',
|
||||
sort_order: 3,
|
||||
required_roles: '["user", "admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'analytics',
|
||||
menu_name: '数据分析',
|
||||
menu_path: '/analytics',
|
||||
menu_type: 'page',
|
||||
icon: 'bar-chart-outlined',
|
||||
sort_order: 4,
|
||||
required_roles: '["user", "admin", "manager"]'
|
||||
},
|
||||
|
||||
// 管理功能模块
|
||||
{
|
||||
menu_key: 'farms',
|
||||
menu_name: '养殖场管理',
|
||||
menu_path: '/farms',
|
||||
menu_type: 'page',
|
||||
icon: 'home-outlined',
|
||||
sort_order: 10,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'animals',
|
||||
menu_name: '动物管理',
|
||||
menu_path: '/animals',
|
||||
menu_type: 'page',
|
||||
icon: 'bug-outlined',
|
||||
sort_order: 11,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'devices',
|
||||
menu_name: '设备管理',
|
||||
menu_path: '/devices',
|
||||
menu_type: 'page',
|
||||
icon: 'desktop-outlined',
|
||||
sort_order: 12,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'alerts',
|
||||
menu_name: '预警管理',
|
||||
menu_path: '/alerts',
|
||||
menu_type: 'page',
|
||||
icon: 'alert-outlined',
|
||||
sort_order: 13,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
|
||||
// 业务功能模块
|
||||
{
|
||||
menu_key: 'products',
|
||||
menu_name: '产品管理',
|
||||
menu_path: '/products',
|
||||
menu_type: 'page',
|
||||
icon: 'shopping-outlined',
|
||||
sort_order: 20,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'orders',
|
||||
menu_name: '订单管理',
|
||||
menu_path: '/orders',
|
||||
menu_type: 'page',
|
||||
icon: 'shopping-cart-outlined',
|
||||
sort_order: 21,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'reports',
|
||||
menu_name: '报表管理',
|
||||
menu_path: '/reports',
|
||||
menu_type: 'page',
|
||||
icon: 'file-text-outlined',
|
||||
sort_order: 22,
|
||||
required_roles: '["admin", "manager"]'
|
||||
},
|
||||
|
||||
// 系统管理模块
|
||||
{
|
||||
menu_key: 'users',
|
||||
menu_name: '用户管理',
|
||||
menu_path: '/users',
|
||||
menu_type: 'page',
|
||||
icon: 'user-outlined',
|
||||
sort_order: 30,
|
||||
required_roles: '["admin"]'
|
||||
},
|
||||
{
|
||||
menu_key: 'system',
|
||||
menu_name: '系统管理',
|
||||
menu_path: '/system',
|
||||
menu_type: 'page',
|
||||
icon: 'setting-outlined',
|
||||
sort_order: 31,
|
||||
required_roles: '["admin"]'
|
||||
}
|
||||
];
|
||||
|
||||
for (const menuData of defaultMenus) {
|
||||
const existing = await this.findOne({
|
||||
where: { menu_key: menuData.menu_key }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await this.create(menuData);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('默认菜单权限初始化完成');
|
||||
} catch (error) {
|
||||
console.error('初始化默认菜单权限失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化MenuPermission模型
|
||||
MenuPermission.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '权限ID'
|
||||
},
|
||||
menu_key: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '菜单标识'
|
||||
},
|
||||
menu_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '菜单名称'
|
||||
},
|
||||
menu_path: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: true,
|
||||
comment: '菜单路径'
|
||||
},
|
||||
parent_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '父菜单ID'
|
||||
},
|
||||
menu_type: {
|
||||
type: DataTypes.ENUM('page', 'button', 'api'),
|
||||
allowNull: false,
|
||||
defaultValue: 'page',
|
||||
comment: '菜单类型'
|
||||
},
|
||||
required_roles: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '所需角色(JSON数组)'
|
||||
},
|
||||
required_permissions: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '所需权限(JSON数组)'
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '菜单图标'
|
||||
},
|
||||
sort_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序顺序'
|
||||
},
|
||||
is_visible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否可见'
|
||||
},
|
||||
is_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否启用'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '菜单描述'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'menu_permissions',
|
||||
modelName: 'MenuPermission',
|
||||
comment: '菜单权限表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['menu_key'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['parent_id']
|
||||
},
|
||||
{
|
||||
fields: ['menu_type']
|
||||
},
|
||||
{
|
||||
fields: ['sort_order']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 定义自关联关系
|
||||
MenuPermission.associate = function(models) {
|
||||
// 自关联:父子菜单关系
|
||||
MenuPermission.hasMany(MenuPermission, {
|
||||
as: 'children',
|
||||
foreignKey: 'parent_id'
|
||||
});
|
||||
|
||||
MenuPermission.belongsTo(MenuPermission, {
|
||||
as: 'parent',
|
||||
foreignKey: 'parent_id'
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = MenuPermission;
|
||||
303
backend/models/OperationLog.js
Normal file
303
backend/models/OperationLog.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 操作日志模型
|
||||
* @file OperationLog.js
|
||||
* @description 记录系统用户的操作日志,包括新增、编辑、删除操作
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
class OperationLog extends BaseModel {
|
||||
static init(sequelize) {
|
||||
return super.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '主键ID'
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '操作用户ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '操作用户名'
|
||||
},
|
||||
user_role: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '操作用户角色'
|
||||
},
|
||||
operation_type: {
|
||||
type: DataTypes.ENUM('CREATE', 'UPDATE', 'DELETE', 'READ', 'LOGIN', 'LOGOUT', 'EXPORT', 'IMPORT', 'BATCH_DELETE', 'BATCH_UPDATE'),
|
||||
allowNull: false,
|
||||
comment: '操作类型:CREATE-新增,UPDATE-编辑,DELETE-删除,READ-查看,LOGIN-登录,LOGOUT-登出,EXPORT-导出,IMPORT-导入,BATCH_DELETE-批量删除,BATCH_UPDATE-批量更新'
|
||||
},
|
||||
module_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '操作模块名称'
|
||||
},
|
||||
table_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '操作的数据表名'
|
||||
},
|
||||
record_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '操作的记录ID'
|
||||
},
|
||||
operation_desc: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
comment: '操作描述'
|
||||
},
|
||||
old_data: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '操作前的数据(编辑和删除时记录)'
|
||||
},
|
||||
new_data: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '操作后的数据(新增和编辑时记录)'
|
||||
},
|
||||
ip_address: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: true,
|
||||
comment: '操作IP地址'
|
||||
},
|
||||
user_agent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '用户代理信息'
|
||||
},
|
||||
request_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '请求URL'
|
||||
},
|
||||
request_method: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true,
|
||||
comment: '请求方法'
|
||||
},
|
||||
response_status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '响应状态码'
|
||||
},
|
||||
execution_time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '执行时间(毫秒)'
|
||||
},
|
||||
error_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息(如果有)'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '创建时间'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'operation_logs',
|
||||
comment: '操作日志表',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_user_id',
|
||||
fields: ['user_id']
|
||||
},
|
||||
{
|
||||
name: 'idx_operation_type',
|
||||
fields: ['operation_type']
|
||||
},
|
||||
{
|
||||
name: 'idx_module_name',
|
||||
fields: ['module_name']
|
||||
},
|
||||
{
|
||||
name: 'idx_table_name',
|
||||
fields: ['table_name']
|
||||
},
|
||||
{
|
||||
name: 'idx_created_at',
|
||||
fields: ['created_at']
|
||||
},
|
||||
{
|
||||
name: 'idx_user_operation',
|
||||
fields: ['user_id', 'operation_type']
|
||||
},
|
||||
{
|
||||
name: 'idx_module_operation',
|
||||
fields: ['module_name', 'operation_type']
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
* @param {Object} logData 日志数据
|
||||
* @returns {Promise<OperationLog>} 创建的日志记录
|
||||
*/
|
||||
static async recordOperation(logData) {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
username,
|
||||
userRole,
|
||||
operationType,
|
||||
moduleName,
|
||||
tableName,
|
||||
recordId,
|
||||
operationDesc,
|
||||
oldData,
|
||||
newData,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
requestUrl,
|
||||
requestMethod,
|
||||
responseStatus,
|
||||
executionTime,
|
||||
errorMessage
|
||||
} = logData;
|
||||
|
||||
return await this.create({
|
||||
user_id: userId,
|
||||
username: username,
|
||||
user_role: userRole,
|
||||
operation_type: operationType,
|
||||
module_name: moduleName,
|
||||
table_name: tableName,
|
||||
record_id: recordId,
|
||||
operation_desc: operationDesc,
|
||||
old_data: oldData,
|
||||
new_data: newData,
|
||||
ip_address: ipAddress,
|
||||
user_agent: userAgent,
|
||||
request_url: requestUrl,
|
||||
request_method: requestMethod,
|
||||
response_status: responseStatus,
|
||||
execution_time: executionTime,
|
||||
error_message: errorMessage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录操作日志失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户操作统计
|
||||
* @param {number} userId 用户ID
|
||||
* @param {string} startDate 开始日期
|
||||
* @param {string} endDate 结束日期
|
||||
* @returns {Promise<Object>} 统计结果
|
||||
*/
|
||||
static async getUserOperationStats(userId, startDate, endDate) {
|
||||
try {
|
||||
const whereClause = {
|
||||
user_id: userId
|
||||
};
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereClause.created_at = {
|
||||
[this.sequelize.Sequelize.Op.between]: [startDate, endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await this.findAll({
|
||||
where: whereClause,
|
||||
attributes: [
|
||||
'operation_type',
|
||||
[this.sequelize.fn('COUNT', this.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['operation_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats.reduce((acc, stat) => {
|
||||
acc[stat.operation_type] = parseInt(stat.count);
|
||||
return acc;
|
||||
}, {});
|
||||
} catch (error) {
|
||||
console.error('获取用户操作统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块操作统计
|
||||
* @param {string} moduleName 模块名称
|
||||
* @param {string} startDate 开始日期
|
||||
* @param {string} endDate 结束日期
|
||||
* @returns {Promise<Object>} 统计结果
|
||||
*/
|
||||
static async getModuleOperationStats(moduleName, startDate, endDate) {
|
||||
try {
|
||||
const whereClause = {
|
||||
module_name: moduleName
|
||||
};
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereClause.created_at = {
|
||||
[this.sequelize.Sequelize.Op.between]: [startDate, endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await this.findAll({
|
||||
where: whereClause,
|
||||
attributes: [
|
||||
'operation_type',
|
||||
[this.sequelize.fn('COUNT', this.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['operation_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats.reduce((acc, stat) => {
|
||||
acc[stat.operation_type] = parseInt(stat.count);
|
||||
return acc;
|
||||
}, {});
|
||||
} catch (error) {
|
||||
console.error('获取模块操作统计失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
* @param {number} daysToKeep 保留天数
|
||||
* @returns {Promise<number>} 删除的记录数
|
||||
*/
|
||||
static async cleanExpiredLogs(daysToKeep = 90) {
|
||||
try {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||||
|
||||
const deletedCount = await this.destroy({
|
||||
where: {
|
||||
created_at: {
|
||||
[this.sequelize.Sequelize.Op.lt]: cutoffDate
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`清理了 ${deletedCount} 条过期操作日志`);
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
console.error('清理过期日志失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OperationLog;
|
||||
@@ -121,15 +121,7 @@ Order.init({
|
||||
allowNull: false,
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
payment_status: {
|
||||
type: DataTypes.ENUM('unpaid', 'paid', 'refunded'),
|
||||
allowNull: false,
|
||||
defaultValue: 'unpaid'
|
||||
},
|
||||
shipping_address: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'orders',
|
||||
|
||||
@@ -128,9 +128,7 @@ OrderItem.init({
|
||||
sequelize,
|
||||
tableName: 'order_items',
|
||||
modelName: 'OrderItem',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: false
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
178
backend/models/Pen.js
Normal file
178
backend/models/Pen.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 栏舍模型
|
||||
* @file Pen.js
|
||||
* @description 栏舍信息数据模型
|
||||
*/
|
||||
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
class Pen extends Model {
|
||||
// 获取动物类型文本
|
||||
getAnimalTypeText() {
|
||||
return this.animal_type || '未知';
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText() {
|
||||
return this.status ? '开启' : '关闭';
|
||||
}
|
||||
|
||||
// 获取容量使用率(如果有当前动物数量的话)
|
||||
getCapacityUsageRate() {
|
||||
// 这里可以根据实际业务需求计算
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 检查容量是否足够
|
||||
isCapacitySufficient(requiredCapacity) {
|
||||
return (this.capacity - this.getCurrentAnimalCount()) >= requiredCapacity;
|
||||
}
|
||||
|
||||
// 获取当前动物数量(需要根据实际业务实现)
|
||||
getCurrentAnimalCount() {
|
||||
// 这里应该查询当前栏舍中的动物数量
|
||||
// 暂时返回0,实际实现时需要关联查询
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Pen.init({
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '栏舍ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '栏舍名称',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '栏舍名称不能为空'
|
||||
},
|
||||
len: {
|
||||
args: [1, 50],
|
||||
msg: '栏舍名称长度应在1-50个字符之间'
|
||||
}
|
||||
}
|
||||
},
|
||||
animal_type: {
|
||||
type: DataTypes.ENUM('马', '牛', '羊', '家禽', '猪'),
|
||||
allowNull: false,
|
||||
comment: '动物类型',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '动物类型不能为空'
|
||||
},
|
||||
isIn: {
|
||||
args: [['马', '牛', '羊', '家禽', '猪']],
|
||||
msg: '动物类型必须是:马、牛、羊、家禽、猪中的一个'
|
||||
}
|
||||
}
|
||||
},
|
||||
pen_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '栏舍类型',
|
||||
validate: {
|
||||
len: {
|
||||
args: [0, 50],
|
||||
msg: '栏舍类型长度不能超过50个字符'
|
||||
}
|
||||
}
|
||||
},
|
||||
responsible: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '负责人',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '负责人不能为空'
|
||||
},
|
||||
len: {
|
||||
args: [1, 20],
|
||||
msg: '负责人姓名长度应在1-20个字符之间'
|
||||
}
|
||||
}
|
||||
},
|
||||
capacity: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '容量',
|
||||
validate: {
|
||||
min: {
|
||||
args: 1,
|
||||
msg: '容量不能小于1'
|
||||
},
|
||||
max: {
|
||||
args: 10000,
|
||||
msg: '容量不能超过10000'
|
||||
},
|
||||
isInt: {
|
||||
msg: '容量必须是整数'
|
||||
}
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '状态:true-开启,false-关闭'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息',
|
||||
validate: {
|
||||
len: {
|
||||
args: [0, 1000],
|
||||
msg: '备注信息长度不能超过1000个字符'
|
||||
}
|
||||
}
|
||||
},
|
||||
farm_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
comment: '所属农场ID',
|
||||
references: {
|
||||
model: 'farms',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
creator: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'admin',
|
||||
comment: '创建人'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Pen',
|
||||
tableName: 'pens',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '栏舍管理表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['name']
|
||||
},
|
||||
{
|
||||
fields: ['animal_type']
|
||||
},
|
||||
{
|
||||
fields: ['farm_id']
|
||||
},
|
||||
{
|
||||
fields: ['status']
|
||||
},
|
||||
{
|
||||
fields: ['created_at']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Pen;
|
||||
66
backend/models/Permission.js
Normal file
66
backend/models/Permission.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 权限模型
|
||||
* @file Permission.js
|
||||
* @description 权限定义模型
|
||||
*/
|
||||
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
const Permission = sequelize.define('Permission', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '权限ID'
|
||||
},
|
||||
permission_key: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '权限标识'
|
||||
},
|
||||
permission_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '权限名称'
|
||||
},
|
||||
permission_desc: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '权限描述'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '所属模块'
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
}
|
||||
}, {
|
||||
tableName: 'permissions',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '权限定义表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['module'],
|
||||
name: 'idx_module'
|
||||
},
|
||||
{
|
||||
fields: ['action'],
|
||||
name: 'idx_action'
|
||||
},
|
||||
{
|
||||
fields: ['permission_key'],
|
||||
name: 'idx_permission_key'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Permission;
|
||||
@@ -15,8 +15,7 @@ const { sequelize } = require('../config/database-simple');
|
||||
* @property {string} description - 产品描述
|
||||
* @property {number} price - 产品价格(单位:分)
|
||||
* @property {number} stock - 库存数量
|
||||
* @property {string} image_url - 产品图片URL
|
||||
* @property {boolean} is_active - 是否激活
|
||||
* @property {string} status - 产品状态
|
||||
* @property {Date} created_at - 创建时间
|
||||
* @property {Date} updated_at - 更新时间
|
||||
*/
|
||||
@@ -28,7 +27,7 @@ class Product extends BaseModel {
|
||||
*/
|
||||
static async getActiveProducts(options = {}) {
|
||||
return await this.findAll({
|
||||
where: { is_active: true },
|
||||
where: { status: 'active' },
|
||||
...options
|
||||
});
|
||||
}
|
||||
@@ -93,31 +92,24 @@ Product.init({
|
||||
allowNull: true
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '单位:分'
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
},
|
||||
stock: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
defaultValue: 0
|
||||
},
|
||||
image_url: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
allowNull: true,
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'products',
|
||||
modelName: 'Product',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,6 +88,12 @@ Role.init({
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '角色状态:true-启用,false-禁用'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
54
backend/models/RoleMenuPermission.js
Normal file
54
backend/models/RoleMenuPermission.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 角色菜单权限关联模型
|
||||
* @file RoleMenuPermission.js
|
||||
* @description 角色和菜单权限的多对多关联表
|
||||
*/
|
||||
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
const BaseModel = require('./BaseModel');
|
||||
|
||||
const RoleMenuPermission = sequelize.define('RoleMenuPermission', {
|
||||
role_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
comment: '角色ID',
|
||||
references: {
|
||||
model: 'roles',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
menu_permission_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
comment: '菜单权限ID',
|
||||
references: {
|
||||
model: 'menu_permissions',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '创建时间'
|
||||
}
|
||||
}, {
|
||||
tableName: 'RoleMenuPermissions',
|
||||
timestamps: false, // 手动管理时间戳
|
||||
comment: '角色菜单权限关联表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['role_id'],
|
||||
name: 'idx_role_id'
|
||||
},
|
||||
{
|
||||
fields: ['menu_permission_id'],
|
||||
name: 'idx_menu_permission_id'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = RoleMenuPermission;
|
||||
@@ -93,17 +93,7 @@ class SensorData extends BaseModel {
|
||||
}
|
||||
|
||||
static associate(models) {
|
||||
// 传感器数据属于某个设备
|
||||
this.belongsTo(models.Device, {
|
||||
foreignKey: 'device_id',
|
||||
as: 'device'
|
||||
});
|
||||
|
||||
// 传感器数据属于某个养殖场
|
||||
this.belongsTo(models.Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
});
|
||||
// 关联关系已在index.js中定义,这里不需要重复定义
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
262
backend/models/SystemConfig.js
Normal file
262
backend/models/SystemConfig.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 系统配置模型
|
||||
* @file SystemConfig.js
|
||||
* @description 系统参数配置数据模型
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class SystemConfig extends BaseModel {
|
||||
/**
|
||||
* 获取配置值并自动转换类型
|
||||
* @param {string} key 配置键名
|
||||
* @returns {*} 转换后的配置值
|
||||
*/
|
||||
static async getValue(key) {
|
||||
try {
|
||||
const config = await this.findOne({
|
||||
where: { config_key: key }
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.parseValue(config.config_value, config.config_type);
|
||||
} catch (error) {
|
||||
console.error(`获取配置 ${key} 失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置值
|
||||
* @param {string} key 配置键名
|
||||
* @param {*} value 配置值
|
||||
* @param {number} userId 操作用户ID
|
||||
* @returns {Object} 配置对象
|
||||
*/
|
||||
static async setValue(key, value, userId = null) {
|
||||
try {
|
||||
const existingConfig = await this.findOne({
|
||||
where: { config_key: key }
|
||||
});
|
||||
|
||||
const stringValue = this.stringifyValue(value);
|
||||
const configType = this.detectType(value);
|
||||
|
||||
if (existingConfig) {
|
||||
await existingConfig.update({
|
||||
config_value: stringValue,
|
||||
config_type: configType,
|
||||
updated_by: userId
|
||||
});
|
||||
return existingConfig;
|
||||
} else {
|
||||
return await this.create({
|
||||
config_key: key,
|
||||
config_value: stringValue,
|
||||
config_type: configType,
|
||||
updated_by: userId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`设置配置 ${key} 失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开配置(前端可访问)
|
||||
* @returns {Object} 公开配置对象
|
||||
*/
|
||||
static async getPublicConfigs() {
|
||||
try {
|
||||
const configs = await this.findAll({
|
||||
where: { is_public: true },
|
||||
order: [['category', 'ASC'], ['sort_order', 'ASC']]
|
||||
});
|
||||
|
||||
const result = {};
|
||||
configs.forEach(config => {
|
||||
result[config.config_key] = this.parseValue(config.config_value, config.config_type);
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('获取公开配置失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类配置
|
||||
* @param {string} category 配置分类
|
||||
* @returns {Array} 配置列表
|
||||
*/
|
||||
static async getByCategory(category) {
|
||||
try {
|
||||
const configs = await this.findAll({
|
||||
where: { category },
|
||||
order: [['sort_order', 'ASC']]
|
||||
});
|
||||
|
||||
return configs.map(config => ({
|
||||
...config.dataValues,
|
||||
parsed_value: this.parseValue(config.config_value, config.config_type)
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`获取分类 ${category} 配置失败:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置值
|
||||
* @param {string} value 字符串值
|
||||
* @param {string} type 数据类型
|
||||
* @returns {*} 解析后的值
|
||||
*/
|
||||
static parseValue(value, type) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return Number(value);
|
||||
case 'boolean':
|
||||
return value === 'true' || value === '1' || value === 1;
|
||||
case 'json':
|
||||
return JSON.parse(value);
|
||||
case 'array':
|
||||
return JSON.parse(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`解析配置值失败: ${value}, type: ${type}`, error);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换值为字符串
|
||||
* @param {*} value 任意类型的值
|
||||
* @returns {string} 字符串值
|
||||
*/
|
||||
static stringifyValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测值的类型
|
||||
* @param {*} value 任意类型的值
|
||||
* @returns {string} 数据类型
|
||||
*/
|
||||
static detectType(value) {
|
||||
if (typeof value === 'number') {
|
||||
return 'number';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return 'boolean';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return 'array';
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return 'json';
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化SystemConfig模型
|
||||
SystemConfig.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '配置ID'
|
||||
},
|
||||
config_key: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '配置键名'
|
||||
},
|
||||
config_value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '配置值'
|
||||
},
|
||||
config_type: {
|
||||
type: DataTypes.ENUM('string', 'number', 'boolean', 'json', 'array'),
|
||||
allowNull: false,
|
||||
defaultValue: 'string',
|
||||
comment: '配置类型'
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'general',
|
||||
comment: '配置分类'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '配置描述'
|
||||
},
|
||||
is_public: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: '是否公开(前端可访问)'
|
||||
},
|
||||
is_editable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否可编辑'
|
||||
},
|
||||
sort_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序顺序'
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '最后更新人ID'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'system_configs',
|
||||
modelName: 'SystemConfig',
|
||||
comment: '系统配置表',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['config_key'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: ['category']
|
||||
},
|
||||
{
|
||||
fields: ['is_public']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = SystemConfig;
|
||||
@@ -4,9 +4,9 @@
|
||||
* @description 定义用户模型,用于数据库操作
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const bcrypt = require('bcrypt');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database-pool');
|
||||
const { sequelize } = require('../config/database-simple');
|
||||
|
||||
class User extends BaseModel {
|
||||
/**
|
||||
@@ -23,7 +23,58 @@ class User extends BaseModel {
|
||||
* @returns {Promise<Array>} 用户角色列表
|
||||
*/
|
||||
async getRoles() {
|
||||
return await this.getRoles();
|
||||
// 简化实现,直接返回当前角色
|
||||
try {
|
||||
const { Role } = require('./index');
|
||||
const role = await Role.findByPk(this.roles);
|
||||
return role ? [role] : [];
|
||||
} catch (error) {
|
||||
console.error('获取用户角色失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
* @returns {Promise<Array>} 用户权限列表
|
||||
*/
|
||||
async getPermissions() {
|
||||
try {
|
||||
const { getRolePermissions } = require('../config/permissions');
|
||||
const roles = await this.getRoles();
|
||||
|
||||
if (roles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取角色的所有权限
|
||||
const allPermissions = new Set();
|
||||
for (const role of roles) {
|
||||
const rolePermissions = getRolePermissions(role.name);
|
||||
rolePermissions.forEach(permission => allPermissions.add(permission));
|
||||
}
|
||||
|
||||
return Array.from(allPermissions);
|
||||
} catch (error) {
|
||||
console.error('获取用户权限失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否具有指定权限
|
||||
* @param {string|Array} permissions 权限名称或权限数组
|
||||
* @returns {Promise<boolean>} 是否有权限
|
||||
*/
|
||||
async hasPermission(permissions) {
|
||||
try {
|
||||
const { hasPermission } = require('../config/permissions');
|
||||
const userPermissions = await this.getPermissions();
|
||||
return hasPermission(userPermissions, permissions);
|
||||
} catch (error) {
|
||||
console.error('检查用户权限失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,16 +95,13 @@ class User extends BaseModel {
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
* @param {Number|Array} roleId 角色ID或角色ID数组
|
||||
* @param {Number} roleId 角色ID
|
||||
* @returns {Promise<Boolean>} 分配结果
|
||||
*/
|
||||
async assignRole(roleId) {
|
||||
try {
|
||||
if (Array.isArray(roleId)) {
|
||||
await this.addRoles(roleId);
|
||||
} else {
|
||||
await this.addRole(roleId);
|
||||
}
|
||||
this.roles = roleId;
|
||||
await this.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('分配角色失败:', error);
|
||||
@@ -63,16 +111,12 @@ class User extends BaseModel {
|
||||
|
||||
/**
|
||||
* 移除用户角色
|
||||
* @param {Number|Array} roleId 角色ID或角色ID数组
|
||||
* @returns {Promise<Boolean>} 移除结果
|
||||
*/
|
||||
async removeRole(roleId) {
|
||||
async removeRole() {
|
||||
try {
|
||||
if (Array.isArray(roleId)) {
|
||||
await this.removeRoles(roleId);
|
||||
} else {
|
||||
await this.removeRole(roleId);
|
||||
}
|
||||
this.roles = null;
|
||||
await this.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('移除角色失败:', error);
|
||||
@@ -122,6 +166,15 @@ User.init({
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true
|
||||
},
|
||||
roles: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: 2, // 默认为user角色ID
|
||||
references: {
|
||||
model: 'roles',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended'),
|
||||
defaultValue: 'active'
|
||||
|
||||
@@ -16,6 +16,28 @@ const Product = require('./Product');
|
||||
const Order = require('./Order');
|
||||
const OrderItem = require('./OrderItem');
|
||||
const SensorData = require('./SensorData');
|
||||
const SystemConfig = require('./SystemConfig');
|
||||
const MenuPermission = require('./MenuPermission');
|
||||
const RoleMenuPermission = require('./RoleMenuPermission');
|
||||
const Permission = require('./Permission');
|
||||
const IotXqClient = require('./IotXqClient');
|
||||
const IotJbqServer = require('./IotJbqServer');
|
||||
const IotJbqClient = require('./IotJbqClient');
|
||||
const ElectronicFence = require('./ElectronicFence');
|
||||
const ElectronicFencePoint = require('./ElectronicFencePoint');
|
||||
const Pen = require('./Pen');
|
||||
const CattlePen = require('./CattlePen');
|
||||
const CattleBatch = require('./CattleBatch');
|
||||
const CattleBatchAnimal = require('./CattleBatchAnimal');
|
||||
const CattleTransferRecord = require('./CattleTransferRecord');
|
||||
const CattleExitRecord = require('./CattleExitRecord');
|
||||
const IotCattle = require('./IotCattle');
|
||||
const CattleType = require('./CattleType');
|
||||
const CattleUser = require('./CattleUser');
|
||||
const FormLog = require('./FormLog');
|
||||
const OperationLog = require('./OperationLog');
|
||||
|
||||
// 注意:模型初始化在各自的模型文件中完成
|
||||
|
||||
// 建立模型之间的关联关系
|
||||
|
||||
@@ -31,6 +53,20 @@ Animal.belongsTo(Farm, {
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 养殖场与牛只的一对多关系
|
||||
Farm.hasMany(IotCattle, {
|
||||
foreignKey: 'orgId',
|
||||
as: 'cattle',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
IotCattle.belongsTo(Farm, {
|
||||
foreignKey: 'orgId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 牛只品种与牛只的一对多关系(在模型初始化后定义)
|
||||
|
||||
// 养殖场与设备的一对多关系
|
||||
Farm.hasMany(Device, {
|
||||
foreignKey: 'farm_id',
|
||||
@@ -91,31 +127,29 @@ SensorData.belongsTo(Farm, {
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 用户与角色的多对多关系
|
||||
User.belongsToMany(Role, {
|
||||
through: UserRole,
|
||||
foreignKey: 'user_id',
|
||||
otherKey: 'role_id',
|
||||
as: 'roles'
|
||||
// 用户与角色的直接关联关系(通过roles字段)
|
||||
User.belongsTo(Role, {
|
||||
foreignKey: 'roles',
|
||||
as: 'role'
|
||||
});
|
||||
Role.belongsToMany(User, {
|
||||
through: UserRole,
|
||||
foreignKey: 'role_id',
|
||||
otherKey: 'user_id',
|
||||
Role.hasMany(User, {
|
||||
foreignKey: 'roles',
|
||||
as: 'users'
|
||||
});
|
||||
|
||||
// 同步所有模型
|
||||
const syncModels = async (options = {}) => {
|
||||
try {
|
||||
await sequelize.sync(options);
|
||||
console.log('所有模型已同步到数据库');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('模型同步失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// 用户与角色的多对多关系(暂时注释掉,当前使用直接关联)
|
||||
// User.belongsToMany(Role, {
|
||||
// through: UserRole,
|
||||
// foreignKey: 'user_id',
|
||||
// otherKey: 'role_id',
|
||||
// as: 'userRoles'
|
||||
// });
|
||||
// Role.belongsToMany(User, {
|
||||
// through: UserRole,
|
||||
// foreignKey: 'role_id',
|
||||
// otherKey: 'user_id',
|
||||
// as: 'roleUsers'
|
||||
// });
|
||||
|
||||
// 用户与订单的一对多关系
|
||||
User.hasMany(Order, {
|
||||
@@ -153,6 +187,304 @@ OrderItem.belongsTo(Product, {
|
||||
as: 'product'
|
||||
});
|
||||
|
||||
// 菜单权限的自关联关系(已在associate方法中定义)
|
||||
|
||||
// 角色与菜单权限的多对多关系
|
||||
Role.belongsToMany(MenuPermission, {
|
||||
through: RoleMenuPermission,
|
||||
foreignKey: 'role_id',
|
||||
otherKey: 'menu_permission_id',
|
||||
as: 'menuPermissions'
|
||||
});
|
||||
|
||||
MenuPermission.belongsToMany(Role, {
|
||||
through: RoleMenuPermission,
|
||||
foreignKey: 'menu_permission_id',
|
||||
otherKey: 'role_id',
|
||||
as: 'roles'
|
||||
});
|
||||
|
||||
// 角色与权限的多对多关系
|
||||
Role.belongsToMany(Permission, {
|
||||
through: 'role_permissions',
|
||||
as: 'permissions',
|
||||
foreignKey: 'role_id',
|
||||
otherKey: 'permission_id'
|
||||
});
|
||||
|
||||
Permission.belongsToMany(Role, {
|
||||
through: 'role_permissions',
|
||||
as: 'roles',
|
||||
foreignKey: 'permission_id',
|
||||
otherKey: 'role_id'
|
||||
});
|
||||
|
||||
// 农场与栏舍的一对多关系
|
||||
Farm.hasMany(Pen, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'pens',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
Pen.belongsTo(Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 农场与电子围栏的一对多关系
|
||||
Farm.hasMany(ElectronicFence, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'electronicFences',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
ElectronicFence.belongsTo(Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 电子围栏与坐标点的一对多关系
|
||||
ElectronicFence.hasMany(ElectronicFencePoint, {
|
||||
foreignKey: 'fence_id',
|
||||
as: 'points',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
ElectronicFencePoint.belongsTo(ElectronicFence, {
|
||||
foreignKey: 'fence_id',
|
||||
as: 'fence'
|
||||
});
|
||||
|
||||
// 用户与坐标点的关联关系
|
||||
User.hasMany(ElectronicFencePoint, {
|
||||
foreignKey: 'created_by',
|
||||
as: 'createdFencePoints',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
ElectronicFencePoint.belongsTo(User, {
|
||||
foreignKey: 'created_by',
|
||||
as: 'creator'
|
||||
});
|
||||
|
||||
User.hasMany(ElectronicFencePoint, {
|
||||
foreignKey: 'updated_by',
|
||||
as: 'updatedFencePoints',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
ElectronicFencePoint.belongsTo(User, {
|
||||
foreignKey: 'updated_by',
|
||||
as: 'updater'
|
||||
});
|
||||
|
||||
// 农场与栏舍设置的一对多关系
|
||||
Farm.hasMany(CattlePen, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'cattlePens',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattlePen.belongsTo(Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 栏舍设置与动物的一对多关系
|
||||
CattlePen.hasMany(Animal, {
|
||||
foreignKey: 'pen_id',
|
||||
as: 'animals',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
Animal.belongsTo(CattlePen, {
|
||||
foreignKey: 'pen_id',
|
||||
as: 'pen'
|
||||
});
|
||||
|
||||
// 农场与批次设置的一对多关系
|
||||
Farm.hasMany(CattleBatch, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'cattleBatches',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleBatch.belongsTo(Farm, {
|
||||
foreignKey: 'farm_id',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 栏舍设置与牛只的一对多关系
|
||||
CattlePen.hasMany(IotCattle, {
|
||||
foreignKey: 'penId',
|
||||
as: 'cattle',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
IotCattle.belongsTo(CattlePen, {
|
||||
foreignKey: 'penId',
|
||||
as: 'pen'
|
||||
});
|
||||
|
||||
// 批次设置与牛只的一对多关系
|
||||
CattleBatch.hasMany(IotCattle, {
|
||||
foreignKey: 'batchId',
|
||||
as: 'cattle',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
IotCattle.belongsTo(CattleBatch, {
|
||||
foreignKey: 'batchId',
|
||||
as: 'batch'
|
||||
});
|
||||
|
||||
// 牛只与转栏记录的一对多关系
|
||||
IotCattle.hasMany(CattleTransferRecord, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'transferRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleTransferRecord.belongsTo(IotCattle, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'cattle'
|
||||
});
|
||||
|
||||
// 栏舍设置与转栏记录的一对多关系(转出)
|
||||
CattlePen.hasMany(CattleTransferRecord, {
|
||||
foreignKey: 'fromPenId',
|
||||
as: 'fromTransferRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleTransferRecord.belongsTo(CattlePen, {
|
||||
foreignKey: 'fromPenId',
|
||||
as: 'fromPen'
|
||||
});
|
||||
|
||||
// 栏舍设置与转栏记录的一对多关系(转入)
|
||||
CattlePen.hasMany(CattleTransferRecord, {
|
||||
foreignKey: 'toPenId',
|
||||
as: 'toTransferRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleTransferRecord.belongsTo(CattlePen, {
|
||||
foreignKey: 'toPenId',
|
||||
as: 'toPen'
|
||||
});
|
||||
|
||||
// 农场与转栏记录的一对多关系
|
||||
Farm.hasMany(CattleTransferRecord, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'transferRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleTransferRecord.belongsTo(Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 牛只与离栏记录的一对多关系
|
||||
IotCattle.hasMany(CattleExitRecord, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'exitRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleExitRecord.belongsTo(IotCattle, {
|
||||
foreignKey: 'animalId',
|
||||
as: 'cattle'
|
||||
});
|
||||
|
||||
// 栏舍设置与离栏记录的一对多关系
|
||||
CattlePen.hasMany(CattleExitRecord, {
|
||||
foreignKey: 'originalPenId',
|
||||
as: 'exitRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleExitRecord.belongsTo(CattlePen, {
|
||||
foreignKey: 'originalPenId',
|
||||
as: 'originalPen'
|
||||
});
|
||||
|
||||
// 农场与离栏记录的一对多关系
|
||||
Farm.hasMany(CattleExitRecord, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'exitRecords',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
CattleExitRecord.belongsTo(Farm, {
|
||||
foreignKey: 'farmId',
|
||||
as: 'farm'
|
||||
});
|
||||
|
||||
// 初始化所有模型
|
||||
const initModels = () => {
|
||||
// 初始化CattleType模型
|
||||
CattleType.init(sequelize);
|
||||
// 初始化CattleUser模型
|
||||
CattleUser.init(sequelize);
|
||||
// 初始化FormLog模型
|
||||
FormLog.init(sequelize);
|
||||
// 初始化OperationLog模型
|
||||
OperationLog.init(sequelize);
|
||||
};
|
||||
|
||||
// 初始化模型
|
||||
initModels();
|
||||
|
||||
// 在模型初始化后定义CattleType的关联关系
|
||||
CattleType.hasMany(IotCattle, {
|
||||
foreignKey: 'varieties',
|
||||
as: 'cattle',
|
||||
onDelete: 'RESTRICT',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
IotCattle.belongsTo(CattleType, {
|
||||
foreignKey: 'varieties',
|
||||
as: 'cattleType'
|
||||
});
|
||||
|
||||
// 在模型初始化后定义CattleUser的关联关系
|
||||
CattleUser.hasMany(IotCattle, {
|
||||
foreignKey: 'strain',
|
||||
as: 'cattle',
|
||||
onDelete: 'RESTRICT',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
IotCattle.belongsTo(CattleUser, {
|
||||
foreignKey: 'strain',
|
||||
as: 'cattleUser'
|
||||
});
|
||||
|
||||
// 用户与操作日志的一对多关系
|
||||
User.hasMany(OperationLog, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'operationLogs',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
OperationLog.belongsTo(User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user'
|
||||
});
|
||||
|
||||
// 同步所有模型
|
||||
const syncModels = async (options = {}) => {
|
||||
try {
|
||||
await sequelize.sync(options);
|
||||
console.log('所有模型已同步到数据库');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('模型同步失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
BaseModel,
|
||||
@@ -167,5 +499,67 @@ module.exports = {
|
||||
Order,
|
||||
OrderItem,
|
||||
SensorData,
|
||||
SystemConfig,
|
||||
MenuPermission,
|
||||
RoleMenuPermission,
|
||||
Permission,
|
||||
IotXqClient,
|
||||
IotJbqServer,
|
||||
IotJbqClient,
|
||||
ElectronicFence,
|
||||
ElectronicFencePoint,
|
||||
Pen,
|
||||
CattlePen,
|
||||
CattleBatch,
|
||||
CattleBatchAnimal,
|
||||
CattleTransferRecord,
|
||||
CattleExitRecord,
|
||||
IotCattle,
|
||||
CattleType,
|
||||
CattleUser,
|
||||
FormLog,
|
||||
OperationLog,
|
||||
syncModels
|
||||
};
|
||||
};
|
||||
|
||||
// 调用模型的associate方法建立关联关系
|
||||
const models = {
|
||||
Farm,
|
||||
Animal,
|
||||
Device,
|
||||
Alert,
|
||||
User,
|
||||
Role,
|
||||
UserRole,
|
||||
Product,
|
||||
Order,
|
||||
OrderItem,
|
||||
SensorData,
|
||||
SystemConfig,
|
||||
MenuPermission,
|
||||
RoleMenuPermission,
|
||||
Permission,
|
||||
IotXqClient,
|
||||
IotJbqServer,
|
||||
IotJbqClient,
|
||||
ElectronicFence,
|
||||
ElectronicFencePoint,
|
||||
Pen,
|
||||
CattlePen,
|
||||
CattleBatch,
|
||||
CattleBatchAnimal,
|
||||
CattleTransferRecord,
|
||||
CattleExitRecord,
|
||||
IotCattle,
|
||||
CattleType,
|
||||
CattleUser,
|
||||
FormLog,
|
||||
OperationLog
|
||||
};
|
||||
|
||||
// 建立关联关系(暂时禁用,避免冲突)
|
||||
// Object.keys(models).forEach(modelName => {
|
||||
// if (models[modelName].associate) {
|
||||
// models[modelName].associate(models);
|
||||
// }
|
||||
// });
|
||||
Reference in New Issue
Block a user