Files
nxxmdata/backend/controllers/iotCattleController.js
2025-11-17 09:18:31 +08:00

1299 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Op } = require('sequelize');
const XLSX = require('xlsx');
const path = require('path');
const IotCattle = require('../models/IotCattle');
const Farm = require('../models/Farm');
const CattlePen = require('../models/CattlePen');
const CattleBatch = require('../models/CattleBatch');
const CattleType = require('../models/CattleType');
const CattleUser = require('../models/CattleUser');
/**
* 计算月龄基于birthday时间戳
*/
const calculateAgeInMonths = (birthday) => {
if (!birthday) return 0;
const now = Math.floor(Date.now() / 1000); // 当前时间戳(秒)
const birthTimestamp = parseInt(birthday);
if (isNaN(birthTimestamp)) return 0;
const ageInSeconds = now - birthTimestamp;
const ageInMonths = Math.floor(ageInSeconds / (30 * 24 * 60 * 60)); // 按30天一个月计算
return Math.max(0, ageInMonths);
};
/**
* 类别中文映射(与前端保持一致)
*/
const getCategoryName = (cate) => {
const categoryMap = {
1: '犊牛',
2: '育成母牛',
3: '架子牛',
4: '青年牛',
5: '基础母牛',
6: '育肥牛'
};
return categoryMap[cate] || '未知';
};
/**
* 类别名称到ID的映射用于导入
*/
const getCategoryId = (name) => {
const categoryMap = {
'犊牛': 1,
'育成母牛': 2,
'架子牛': 3,
'青年牛': 4,
'基础母牛': 5,
'育肥牛': 6
};
return categoryMap[name] || null;
};
/**
* 性别名称到ID的映射用于导入
*/
const getSexId = (name) => {
const sexMap = {
'公': 1,
'公牛': 1,
'母': 2,
'母牛': 2
};
return sexMap[name] || null;
};
/**
* 来源名称到ID的映射用于导入
*/
const getSourceId = (name) => {
const sourceMap = {
'购买': 1,
'自繁': 2,
'放生': 3,
'合作社': 4,
'入股': 5
};
return sourceMap[name] || null;
};
/**
* 血统纯度名称到ID的映射用于导入
*/
const getDescentId = (name) => {
const descentMap = {
'纯血': 1,
'纯种': 1,
'杂交': 2,
'杂交一代': 2,
'杂交二代': 3,
'杂交三代': 4
};
return descentMap[name] || null;
};
/**
* 日期字符串转换为时间戳(秒)
*/
const dateToTimestamp = (dateStr) => {
if (!dateStr) return null;
// 支持多种日期格式2023-01-01, 2023/01/01, 2023-1-1
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
return null;
}
return Math.floor(date.getTime() / 1000);
};
/**
* 获取栏舍、批次、品种和用途名称
*/
const getPenBatchTypeAndUserNames = async (cattleList) => {
// 获取所有唯一的栏舍ID、批次ID、品种ID和品系ID用途
const penIds = [...new Set(cattleList.map(cattle => cattle.penId).filter(id => id))];
const batchIds = [...new Set(cattleList.map(cattle => cattle.batchId).filter(id => id && id > 0))];
const typeIds = [...new Set(cattleList.map(cattle => cattle.varieties).filter(id => id))];
const strainIds = [...new Set(cattleList.map(cattle => cattle.strain).filter(id => id))];
// 查询栏舍名称
const penNames = {};
if (penIds.length > 0) {
const pens = await CattlePen.findAll({
where: { id: penIds },
attributes: ['id', 'name']
});
pens.forEach(pen => {
penNames[pen.id] = pen.name;
});
}
// 查询批次名称
const batchNames = {};
if (batchIds.length > 0) {
const batches = await CattleBatch.findAll({
where: { id: batchIds },
attributes: ['id', 'name']
});
batches.forEach(batch => {
batchNames[batch.id] = batch.name;
});
}
// 查询品种名称
const typeNames = {};
if (typeIds.length > 0) {
const types = await CattleType.findAll({
where: { id: typeIds },
attributes: ['id', 'name']
});
types.forEach(type => {
typeNames[type.id] = type.name;
});
}
// 查询用途名称基于strain字段
const userNames = {};
if (strainIds.length > 0) {
const users = await CattleUser.findAll({
where: { id: strainIds },
attributes: ['id', 'name']
});
users.forEach(user => {
userNames[user.id] = user.name;
});
}
return { penNames, batchNames, typeNames, userNames };
};
/**
* 牛只档案控制器 - 基于iot_cattle表
*/
class IotCattleController {
/**
* 获取牛只档案列表
*/
async getCattleArchives(req, res) {
try {
const {
page = 1,
pageSize = 10,
search = '',
farmId = '',
penId = '',
batchId = ''
} = req.query;
console.log('=== 后端接收搜索请求 ===');
console.log('请求时间:', new Date().toISOString());
console.log('请求参数:', { page, pageSize, search, farmId, penId, batchId });
console.log('请求来源:', req.ip);
console.log('User-Agent:', req.get('User-Agent'));
const offset = (page - 1) * pageSize;
const whereConditions = {};
// 搜索条件
if (search) {
// 尝试将搜索词转换为数字,如果成功则按数字搜索,否则按字符串搜索
const searchNumber = parseInt(search);
if (!isNaN(searchNumber)) {
// 数字搜索:精确匹配耳号
whereConditions[Op.or] = [
{ earNumber: searchNumber },
{ strain: { [Op.like]: `%${search}%` } }
];
} else {
// 字符串搜索:模糊匹配
whereConditions[Op.or] = [
{ strain: { [Op.like]: `%${search}%` } }
];
}
console.log('=== 搜索条件构建 ===');
console.log('搜索关键词:', search);
console.log('搜索条件对象:', JSON.stringify(whereConditions, null, 2));
}
// 农场筛选
if (farmId) {
whereConditions.orgId = farmId;
console.log('添加农场筛选条件:', farmId);
}
// 栏舍筛选
if (penId) {
whereConditions.penId = penId;
console.log('添加栏舍筛选条件:', penId);
}
// 批次筛选
if (batchId) {
whereConditions.batchId = batchId;
console.log('添加批次筛选条件:', batchId);
}
console.log('=== 最终查询条件 ===');
console.log('完整查询条件:', JSON.stringify(whereConditions, null, 2));
console.log('分页参数:', { offset, limit: pageSize });
// 先获取总数
console.log('=== 开始数据库查询 ===');
console.log('查询时间:', new Date().toISOString());
const countStartTime = Date.now();
const totalCount = await IotCattle.count({
where: whereConditions
});
const countEndTime = Date.now();
console.log('=== 总数查询完成 ===');
console.log('查询耗时:', countEndTime - countStartTime, 'ms');
console.log('总记录数:', totalCount);
// 获取分页数据
const dataStartTime = Date.now();
const rows = await IotCattle.findAll({
where: whereConditions,
attributes: [
'id', 'earNumber', 'sex', 'strain', 'varieties', 'cate',
'birthWeight', 'birthday', 'penId', 'batchId', 'orgId',
'weight', 'parity', 'weightCalculateTime', 'dayOfBirthday',
'intoTime', 'source', 'sourceDay', 'sourceWeight',
'event', 'eventTime', 'lactationDay', 'semenNum', 'isWear',
'imgs', 'isEleAuth', 'isQuaAuth', 'isDelete', 'isOut',
'createUid', 'createTime', 'algebra', 'colour', 'infoWeight',
'descent', 'isVaccin', 'isInsemination', 'isInsure', 'isMortgage',
'updateTime', 'breedBullTime', 'sixWeight', 'eighteenWeight',
'twelveDayWeight', 'eighteenDayWeight', 'xxivDayWeight',
'semenBreedImgs', 'sellStatus'
],
limit: parseInt(pageSize),
offset: parseInt(offset),
order: [['id', 'ASC']] // 升序排序
});
const dataEndTime = Date.now();
console.log('=== 数据查询完成 ===');
console.log('查询耗时:', dataEndTime - dataStartTime, 'ms');
console.log('查询到记录数:', rows.length);
console.log('记录详情:', rows.map(row => ({
id: row.id,
earNumber: row.earNumber,
sex: row.sex,
varieties: row.varieties
})));
// 获取栏舍、批次、品种和用途名称
const { penNames, batchNames, typeNames, userNames } = await getPenBatchTypeAndUserNames(rows);
// 格式化数据基于iot_cattle表字段映射
const formattedData = rows.map(cattle => ({
id: cattle.id,
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
sex: cattle.sex, // 映射iot_cattle.sex
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称用于显示
strainId: cattle.strain, // 原始ID用于编辑和提交
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称用于显示
varietiesId: cattle.varieties, // 原始ID用于编辑和提交
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文用于显示
cateId: cattle.cate, // 原始ID用于编辑和提交
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
birthday: cattle.birthday, // 映射iot_cattle.birthday
intoTime: cattle.intoTime,
parity: cattle.parity,
source: cattle.source,
sourceDay: cattle.sourceDay,
sourceWeight: cattle.sourceWeight,
ageInMonths: calculateAgeInMonths(cattle.birthday), // 从iot_cattle.birthday计算月龄
physiologicalStage: cattle.parity || 0, // 使用parity作为生理阶段
currentWeight: cattle.weight || 0, // 使用weight作为当前体重
weightCalculateTime: cattle.weightCalculateTime,
dayOfBirthday: cattle.dayOfBirthday,
farmName: `农场ID:${cattle.orgId}`, // 暂时显示ID后续可优化
penName: cattle.penId ? (penNames[cattle.penId] || `栏舍ID:${cattle.penId}`) : '未分配栏舍', // 映射栏舍名称
batchName: cattle.batchId === 0 ? '未分配批次' : (batchNames[cattle.batchId] || `批次ID:${cattle.batchId}`), // 映射批次名称
farmId: cattle.orgId, // 映射iot_cattle.org_id
penId: cattle.penId, // 映射iot_cattle.pen_id
batchId: cattle.batchId // 映射iot_cattle.batch_id
}));
console.log('=== 数据格式化完成 ===');
console.log('格式化后数据条数:', formattedData.length);
console.log('格式化后数据示例:', formattedData.slice(0, 2));
const responseData = {
success: true,
data: {
list: formattedData,
pagination: {
current: parseInt(page),
pageSize: parseInt(pageSize),
total: totalCount,
pages: Math.ceil(totalCount / parseInt(pageSize))
}
},
message: '获取牛只档案列表成功'
};
console.log('=== 准备返回响应 ===');
console.log('响应时间:', new Date().toISOString());
console.log('响应数据大小:', JSON.stringify(responseData).length, 'bytes');
console.log('分页信息:', responseData.data.pagination);
res.json(responseData);
} catch (error) {
console.error('获取牛只档案列表失败:', error);
res.status(500).json({
success: false,
message: '获取牛只档案列表失败',
error: error.message
});
}
}
/**
* 获取单个牛只档案详情
*/
async getCattleArchiveById(req, res) {
try {
const { id } = req.params;
console.log('=== 获取牛只档案详情 ===');
console.log('请求时间:', new Date().toISOString());
console.log('档案ID:', id);
console.log('请求来源:', req.ip);
console.log('用户信息:', req.user ? { id: req.user.id, username: req.user.username } : '未登录');
console.log('User-Agent:', req.get('User-Agent'));
const cattle = await IotCattle.findByPk(id, {
include: [
{
model: Farm,
as: 'farm',
attributes: ['id', 'name', 'location']
},
{
model: CattlePen,
as: 'pen',
attributes: ['id', 'name', 'code']
},
{
model: CattleBatch,
as: 'batch',
attributes: ['id', 'name', 'code']
}
]
});
if (!cattle) {
console.log('牛只档案不存在ID:', id);
return res.status(404).json({
success: false,
message: '牛只档案不存在'
});
}
console.log('找到牛只档案:', {
id: cattle.id,
earNumber: cattle.earNumber,
orgId: cattle.orgId,
penId: cattle.penId,
batchId: cattle.batchId
});
// 获取栏舍、批次、品种和用途名称
const cattleList = [cattle];
const { penNames, batchNames, typeNames, userNames } = await getPenBatchTypeAndUserNames(cattleList);
// 格式化数据基于iot_cattle表字段映射
const formattedData = {
id: cattle.id,
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
sex: cattle.sex, // 映射iot_cattle.sex
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称用于显示
strainId: cattle.strain, // 原始ID用于编辑和提交
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称用于显示
varietiesId: cattle.varieties, // 原始ID用于编辑和提交
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文用于显示
cateId: cattle.cate, // 原始ID用于编辑和提交
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
birthday: cattle.birthday, // 映射iot_cattle.birthday
intoTime: cattle.intoTime,
parity: cattle.parity,
source: cattle.source,
sourceDay: cattle.sourceDay,
sourceWeight: cattle.sourceWeight,
ageInMonths: calculateAgeInMonths(cattle.birthday), // 从iot_cattle.birthday计算月龄
physiologicalStage: cattle.parity || 0, // 使用parity作为生理阶段
currentWeight: cattle.weight || 0, // 使用weight作为当前体重
weightCalculateTime: cattle.weightCalculateTime,
dayOfBirthday: cattle.dayOfBirthday,
farmName: `农场ID:${cattle.orgId}`, // 暂时显示ID后续可优化
penName: cattle.penId ? (penNames[cattle.penId] || `栏舍ID:${cattle.penId}`) : '未分配栏舍', // 映射栏舍名称
batchName: cattle.batchId === 0 ? '未分配批次' : (batchNames[cattle.batchId] || `批次ID:${cattle.batchId}`), // 映射批次名称
farmId: cattle.orgId, // 映射iot_cattle.org_id
penId: cattle.penId, // 映射iot_cattle.pen_id
batchId: cattle.batchId // 映射iot_cattle.batch_id
};
console.log('=== 返回格式化后的数据 ===');
console.log('格式化数据示例:', {
id: formattedData.id,
earNumber: formattedData.earNumber,
farmId: formattedData.farmId,
penId: formattedData.penId,
batchId: formattedData.batchId
});
res.json({
success: true,
data: formattedData,
message: '获取牛只档案详情成功'
});
} catch (error) {
console.error('=== 获取牛只档案详情失败 ===');
console.error('错误时间:', new Date().toISOString());
console.error('档案ID:', req.params.id);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
res.status(500).json({
success: false,
message: '获取牛只档案详情失败',
error: error.message
});
}
}
/**
* 创建牛只档案
*/
async createCattleArchive(req, res) {
try {
const {
earNumber,
sex,
strain,
varieties,
cate,
birthWeight,
birthday,
penId,
intoTime,
parity,
source,
sourceDay,
sourceWeight,
orgId,
batchId
} = req.body;
// 验证必填字段
if (!earNumber || !sex || !strain || !varieties || !cate || !birthWeight || !birthday || !orgId) {
return res.status(400).json({
success: false,
message: '缺少必填字段'
});
}
// 检查耳标号是否已存在
const existingCattle = await IotCattle.findOne({
where: { earNumber: earNumber }
});
if (existingCattle) {
return res.status(400).json({
success: false,
message: '耳标号已存在'
});
}
const cattleData = {
earNumber: parseInt(earNumber),
sex: parseInt(sex),
strain: parseInt(strain),
varieties: parseInt(varieties),
cate: parseInt(cate),
birthWeight: parseFloat(birthWeight),
birthday: parseInt(birthday),
penId: penId ? parseInt(penId) : 0,
intoTime: intoTime ? parseInt(intoTime) : 0,
parity: parity ? parseInt(parity) : 0,
source: source ? parseInt(source) : 0,
sourceDay: sourceDay ? parseInt(sourceDay) : 0,
sourceWeight: sourceWeight ? parseFloat(sourceWeight) : 0,
weight: req.body.currentWeight ? parseFloat(req.body.currentWeight) : 0,
event: req.body.event || 1,
eventTime: req.body.eventTime || Math.floor(Date.now() / 1000),
lactationDay: req.body.lactationDay || 0,
semenNum: req.body.semenNum || '',
isWear: req.body.isWear || 0,
imgs: req.body.imgs || '',
isEleAuth: req.body.isEleAuth || 0,
isQuaAuth: req.body.isQuaAuth || 0,
isDelete: 0,
isOut: 0,
createUid: req.user ? req.user.id : 1,
createTime: Math.floor(Date.now() / 1000),
algebra: req.body.algebra || 0,
colour: req.body.colour || '',
infoWeight: req.body.infoWeight ? parseFloat(req.body.infoWeight) : 0,
descent: req.body.descent || 0,
isVaccin: req.body.isVaccin || 0,
isInsemination: req.body.isInsemination || 0,
isInsure: req.body.isInsure || 0,
isMortgage: req.body.isMortgage || 0,
updateTime: Math.floor(Date.now() / 1000),
breedBullTime: req.body.breedBullTime || 0,
level: req.body.level || 0,
sixWeight: req.body.sixWeight ? parseFloat(req.body.sixWeight) : 0,
eighteenWeight: req.body.eighteenWeight ? parseFloat(req.body.eighteenWeight) : 0,
twelveDayWeight: req.body.twelveDayWeight ? parseFloat(req.body.twelveDayWeight) : 0,
eighteenDayWeight: req.body.eighteenDayWeight ? parseFloat(req.body.eighteenDayWeight) : 0,
xxivDayWeight: req.body.xxivDayWeight ? parseFloat(req.body.xxivDayWeight) : 0,
semenBreedImgs: req.body.semenBreedImgs || '',
sellStatus: req.body.sellStatus || 100,
orgId: parseInt(orgId),
batchId: batchId ? parseInt(batchId) : 0
};
const cattle = await IotCattle.create(cattleData);
res.status(201).json({
success: true,
data: cattle,
message: '创建牛只档案成功'
});
} catch (error) {
console.error('创建牛只档案失败:', error);
res.status(500).json({
success: false,
message: '创建牛只档案失败',
error: error.message
});
}
}
/**
* 更新牛只档案
*/
async updateCattleArchive(req, res) {
try {
const { id } = req.params;
const updateData = req.body;
const cattle = await IotCattle.findByPk(id);
if (!cattle) {
return res.status(404).json({
success: false,
message: '牛只档案不存在'
});
}
// 如果更新耳标号,检查是否重复
if (updateData.earNumber && updateData.earNumber !== cattle.earNumber) {
const existingCattle = await IotCattle.findOne({
where: {
earNumber: updateData.earNumber,
id: { [Op.ne]: id }
}
});
if (existingCattle) {
return res.status(400).json({
success: false,
message: '耳标号已存在'
});
}
}
// 转换数据类型,只更新实际提交的字段
const processedData = {};
// 辅助函数:安全转换为整数,如果转换失败则返回原值(不更新该字段)
const safeParseInt = (value) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = parseInt(value);
return isNaN(parsed) ? undefined : parsed;
};
// 辅助函数:安全转换为浮点数,如果转换失败则返回原值(不更新该字段)
const safeParseFloat = (value) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = parseFloat(value);
return isNaN(parsed) ? undefined : parsed;
};
// 只更新实际提交的字段,如果字段值无效则跳过(保持原有值)
if (updateData.hasOwnProperty('earNumber')) {
const parsed = safeParseInt(updateData.earNumber);
if (parsed !== undefined) processedData.earNumber = parsed;
}
if (updateData.hasOwnProperty('sex')) {
const parsed = safeParseInt(updateData.sex);
if (parsed !== undefined) processedData.sex = parsed;
}
if (updateData.hasOwnProperty('strain')) {
const parsed = safeParseInt(updateData.strain);
if (parsed !== undefined) processedData.strain = parsed;
}
if (updateData.hasOwnProperty('varieties')) {
const parsed = safeParseInt(updateData.varieties);
if (parsed !== undefined) processedData.varieties = parsed;
}
if (updateData.hasOwnProperty('cate')) {
const parsed = safeParseInt(updateData.cate);
if (parsed !== undefined) processedData.cate = parsed;
}
if (updateData.hasOwnProperty('birthWeight')) {
const parsed = safeParseFloat(updateData.birthWeight);
if (parsed !== undefined) processedData.birthWeight = parsed;
}
if (updateData.hasOwnProperty('birthday')) {
const parsed = safeParseInt(updateData.birthday);
if (parsed !== undefined) processedData.birthday = parsed;
}
if (updateData.hasOwnProperty('penId')) {
const parsed = safeParseInt(updateData.penId);
if (parsed !== undefined) processedData.penId = parsed;
}
if (updateData.hasOwnProperty('intoTime')) {
const parsed = safeParseInt(updateData.intoTime);
if (parsed !== undefined) processedData.intoTime = parsed;
}
if (updateData.hasOwnProperty('parity')) {
const parsed = safeParseInt(updateData.parity);
if (parsed !== undefined) processedData.parity = parsed;
}
if (updateData.hasOwnProperty('source')) {
const parsed = safeParseInt(updateData.source);
if (parsed !== undefined) processedData.source = parsed;
}
if (updateData.hasOwnProperty('sourceDay')) {
const parsed = safeParseInt(updateData.sourceDay);
if (parsed !== undefined) processedData.sourceDay = parsed;
}
if (updateData.hasOwnProperty('sourceWeight')) {
const parsed = safeParseFloat(updateData.sourceWeight);
if (parsed !== undefined) processedData.sourceWeight = parsed;
}
if (updateData.hasOwnProperty('orgId')) {
const parsed = safeParseInt(updateData.orgId);
if (parsed !== undefined) processedData.orgId = parsed;
}
if (updateData.hasOwnProperty('batchId')) {
const parsed = safeParseInt(updateData.batchId);
if (parsed !== undefined) processedData.batchId = parsed;
}
await cattle.update(processedData);
res.json({
success: true,
data: cattle,
message: '更新牛只档案成功'
});
} catch (error) {
console.error('更新牛只档案失败:', error);
res.status(500).json({
success: false,
message: '更新牛只档案失败',
error: error.message
});
}
}
/**
* 删除牛只档案
*/
async deleteCattleArchive(req, res) {
try {
const { id } = req.params;
const cattle = await IotCattle.findByPk(id);
if (!cattle) {
return res.status(404).json({
success: false,
message: '牛只档案不存在'
});
}
await cattle.destroy();
res.json({
success: true,
message: '删除牛只档案成功'
});
} catch (error) {
console.error('删除牛只档案失败:', error);
res.status(500).json({
success: false,
message: '删除牛只档案失败',
error: error.message
});
}
}
/**
* 批量删除牛只档案
*/
async batchDeleteCattleArchives(req, res) {
try {
const { ids } = req.body;
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要删除的牛只档案'
});
}
const deletedCount = await IotCattle.destroy({
where: {
id: {
[Op.in]: ids
}
}
});
res.json({
success: true,
data: { deletedCount },
message: `成功删除 ${deletedCount} 个牛只档案`
});
} catch (error) {
console.error('批量删除牛只档案失败:', error);
res.status(500).json({
success: false,
message: '批量删除牛只档案失败',
error: error.message
});
}
}
/**
* 获取农场列表(用于下拉选择)
*/
async getFarms(req, res) {
try {
const farms = await Farm.findAll({
attributes: ['id', 'name', 'location'],
order: [['name', 'ASC']]
});
res.json({
success: true,
data: farms,
message: '获取农场列表成功'
});
} catch (error) {
console.error('获取农场列表失败:', error);
res.status(500).json({
success: false,
message: '获取农场列表失败',
error: error.message
});
}
}
/**
* 获取栏舍列表(用于下拉选择)
*/
async getPens(req, res) {
try {
const { farmId } = req.query;
const where = {};
if (farmId) {
where.farmId = farmId;
}
const pens = await CattlePen.findAll({
where,
attributes: ['id', 'name', 'code', 'farmId'],
order: [['name', 'ASC']]
});
res.json({
success: true,
data: pens,
message: '获取栏舍列表成功'
});
} catch (error) {
console.error('获取栏舍列表失败:', error);
res.status(500).json({
success: false,
message: '获取栏舍列表失败',
error: error.message
});
}
}
/**
* 获取批次列表(用于下拉选择)
*/
async getBatches(req, res) {
try {
const { farmId } = req.query;
const where = {};
if (farmId) {
where.farmId = farmId;
}
const batches = await CattleBatch.findAll({
where,
attributes: ['id', 'name', 'code', 'farmId'],
order: [['name', 'ASC']]
});
res.json({
success: true,
data: batches,
message: '获取批次列表成功'
});
} catch (error) {
console.error('获取批次列表失败:', error);
res.status(500).json({
success: false,
message: '获取批次列表失败',
error: error.message
});
}
}
/**
* 导入牛只档案数据
*/
async importCattleArchives(req, res) {
try {
console.log('=== 开始导入牛只档案数据 ===');
if (!req.file) {
return res.status(400).json({
success: false,
message: '请选择要导入的文件'
});
}
const file = req.file;
console.log('上传文件信息:', {
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size
});
// 检查文件类型
const allowedTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel'
];
if (!allowedTypes.includes(file.mimetype)) {
return res.status(400).json({
success: false,
message: '请上传Excel文件(.xlsx或.xls格式)'
});
}
// 解析Excel文件
const workbook = XLSX.readFile(file.path);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet);
console.log(`解析到 ${data.length} 行数据`);
if (data.length === 0) {
return res.status(400).json({
success: false,
message: 'Excel文件中没有数据'
});
}
// 获取所有品种和品系(品类)的映射关系
const cattleTypes = await CattleType.findAll({ attributes: ['id', 'name'] });
const typeNameToId = {};
cattleTypes.forEach(type => {
typeNameToId[type.name] = type.id;
});
const cattleUsers = await CattleUser.findAll({ attributes: ['id', 'name'] });
const userNameToId = {};
cattleUsers.forEach(user => {
userNameToId[user.name] = user.id;
});
// 获取所有栏舍和批次的映射关系
const pens = await CattlePen.findAll({ attributes: ['id', 'name'] });
const penNameToId = {};
pens.forEach(pen => {
penNameToId[pen.name] = pen.id;
});
const batches = await CattleBatch.findAll({ attributes: ['id', 'name'] });
const batchNameToId = {};
batches.forEach(batch => {
batchNameToId[batch.name] = batch.id;
});
// 获取默认农场ID从请求中获取或使用第一个农场
let defaultOrgId = req.body.orgId || req.query.orgId;
if (!defaultOrgId) {
const firstFarm = await Farm.findOne({ order: [['id', 'ASC']] });
defaultOrgId = firstFarm ? firstFarm.id : null;
}
if (!defaultOrgId) {
return res.status(400).json({
success: false,
message: '请指定所属农场'
});
}
const errors = [];
const successData = [];
// 处理每一行数据
for (let i = 0; i < data.length; i++) {
const row = data[i];
const rowNum = i + 2; // Excel行号从2开始第1行是表头
try {
// 验证必填字段
if (!row['耳号']) {
errors.push({ row: rowNum, field: '耳号', message: '耳号不能为空' });
continue;
}
// 映射字段
const earNumber = String(row['耳号']).trim();
// 检查耳号是否已存在
const existingCattle = await IotCattle.findOne({
where: { earNumber: parseInt(earNumber) }
});
if (existingCattle) {
errors.push({ row: rowNum, field: '耳号', message: `耳号 ${earNumber} 已存在` });
continue;
}
// 品类strain- 从名称查找ID必填
const strainName = row['品类'] ? String(row['品类']).trim() : '';
if (!strainName) {
errors.push({ row: rowNum, field: '品类', message: '品类不能为空' });
continue;
}
const strainId = userNameToId[strainName];
if (!strainId) {
errors.push({ row: rowNum, field: '品类', message: `品类 "${strainName}" 不存在` });
continue;
}
// 品种varieties- 从名称查找ID必填
const varietiesName = row['品种'] ? String(row['品种']).trim() : '';
if (!varietiesName) {
errors.push({ row: rowNum, field: '品种', message: '品种不能为空' });
continue;
}
const varietiesId = typeNameToId[varietiesName];
if (!varietiesId) {
errors.push({ row: rowNum, field: '品种', message: `品种 "${varietiesName}" 不存在` });
continue;
}
// 生理阶段cate必填
const cateName = row['生理阶段'] ? String(row['生理阶段']).trim() : '';
if (!cateName) {
errors.push({ row: rowNum, field: '生理阶段', message: '生理阶段不能为空' });
continue;
}
const cateId = getCategoryId(cateName);
if (!cateId) {
errors.push({ row: rowNum, field: '生理阶段', message: `生理阶段 "${cateName}" 无效` });
continue;
}
// 性别sex必填
const sexName = row['性别'] ? String(row['性别']).trim() : '';
if (!sexName) {
errors.push({ row: rowNum, field: '性别', message: '性别不能为空' });
continue;
}
const sexId = getSexId(sexName);
if (!sexId) {
errors.push({ row: rowNum, field: '性别', message: `性别 "${sexName}" 无效,应为"公"或"母"` });
continue;
}
// 来源source必填
const sourceName = row['来源'] ? String(row['来源']).trim() : '';
if (!sourceName) {
errors.push({ row: rowNum, field: '来源', message: '来源不能为空' });
continue;
}
const sourceId = getSourceId(sourceName);
if (!sourceId) {
errors.push({ row: rowNum, field: '来源', message: `来源 "${sourceName}" 无效` });
continue;
}
// 血统纯度descent
const descentName = row['血统纯度'] ? String(row['血统纯度']).trim() : '';
const descentId = descentName ? getDescentId(descentName) : 0;
// 栏舍penId- 从名称查找ID
const penName = row['栏舍'] ? String(row['栏舍']).trim() : '';
const penId = penName ? (penNameToId[penName] || null) : null;
// 所属批次batchId- 从名称查找ID
const batchName = row['所属批次'] ? String(row['所属批次']).trim() : '';
const batchId = batchName ? (batchNameToId[batchName] || null) : null;
// 已产胎次parity
const parity = row['已产胎次'] ? parseInt(row['已产胎次']) || 0 : 0;
// 出生日期birthday必填
const birthdayStr = row['出生日期(格式必须为2023-01-01)'] || row['出生日期'] || '';
if (!birthdayStr) {
errors.push({ row: rowNum, field: '出生日期', message: '出生日期不能为空' });
continue;
}
const birthday = dateToTimestamp(birthdayStr);
if (!birthday) {
errors.push({ row: rowNum, field: '出生日期', message: `出生日期格式错误: "${birthdayStr}"格式应为2023-01-01` });
continue;
}
// 现估重weight必填
const currentWeightStr = row['现估重(公斤)'] || row['现估重'] || '';
if (!currentWeightStr) {
errors.push({ row: rowNum, field: '现估重(公斤)', message: '现估重(公斤)不能为空' });
continue;
}
const currentWeight = parseFloat(currentWeightStr);
if (isNaN(currentWeight) || currentWeight < 0) {
errors.push({ row: rowNum, field: '现估重(公斤)', message: `现估重(公斤)格式错误: "${currentWeightStr}"` });
continue;
}
// 代数algebra
const algebra = row['代数'] ? parseInt(row['代数']) || 0 : 0;
// 入场日期intoTime
const intoTimeStr = row['入场日期(格式必须为2023-01-01)'] || row['入场日期'] || '';
const intoTime = intoTimeStr ? dateToTimestamp(intoTimeStr) : null;
if (intoTimeStr && !intoTime) {
errors.push({ row: rowNum, field: '入场日期', message: `入场日期格式错误: "${intoTimeStr}"` });
continue;
}
// 出生体重birthWeight
const birthWeight = row['出生体重'] ? parseFloat(row['出生体重']) || 0 : 0;
// 冻精编号semenNum
const semenNum = row['冻精编号'] ? String(row['冻精编号']).trim() : '';
// 构建插入数据
const cattleData = {
orgId: parseInt(defaultOrgId),
earNumber: parseInt(earNumber),
sex: sexId,
strain: strainId || 0,
varieties: varietiesId || 0,
cate: cateId || 0,
birthWeight: birthWeight,
birthday: birthday || 0,
penId: penId || 0,
intoTime: intoTime || 0,
parity: parity,
source: sourceId || 0,
sourceDay: 0,
sourceWeight: 0,
weight: currentWeight,
event: 1,
eventTime: Math.floor(Date.now() / 1000),
lactationDay: 0,
semenNum: semenNum,
isWear: 0,
imgs: '',
isEleAuth: 0,
isQuaAuth: 0,
isDelete: 0,
isOut: 0,
createUid: req.user ? req.user.id : 1,
createTime: Math.floor(Date.now() / 1000),
algebra: algebra,
colour: '',
infoWeight: 0,
descent: descentId || 0,
isVaccin: 0,
isInsemination: 0,
isInsure: 0,
isMortgage: 0,
updateTime: Math.floor(Date.now() / 1000),
breedBullTime: 0,
level: 0,
sixWeight: 0,
eighteenWeight: 0,
twelveDayWeight: 0,
eighteenDayWeight: 0,
xxivDayWeight: 0,
semenBreedImgs: '',
sellStatus: 100,
batchId: batchId || 0
};
// 插入数据库
await IotCattle.create(cattleData);
successData.push({ row: rowNum, earNumber: earNumber });
} catch (error) {
console.error(`处理第 ${rowNum} 行数据失败:`, error);
errors.push({
row: rowNum,
field: '数据',
message: `处理失败: ${error.message}`
});
}
}
const importedCount = successData.length;
console.log(`导入完成: 成功 ${importedCount} 条,失败 ${errors.length}`);
res.json({
success: true,
message: `导入完成: 成功 ${importedCount} 条,失败 ${errors.length}`,
importedCount: importedCount,
errorCount: errors.length,
errors: errors,
successData: successData
});
} catch (error) {
console.error('导入牛只档案数据失败:', error);
res.status(500).json({
success: false,
message: '导入失败',
error: error.message
});
}
}
/**
* 下载导入模板
*/
async downloadImportTemplate(req, res) {
try {
console.log('=== 下载牛只档案导入模板 ===');
// 创建模板数据 - 按照图片格式16列
const templateData = [
{
'耳号': '202308301035',
'品类': '肉用型牛',
'品种': '蒙古牛',
'生理阶段': '犊牛',
'性别': '公',
'血统纯度': '纯血',
'栏舍': '牛舍-20230819',
'所属批次': '230508357',
'已产胎次': '0',
'来源': '购买',
'现估重(公斤)': '50',
'代数': '0',
'出生日期(格式必须为2023-01-01)': '2023-08-30',
'入场日期(格式必须为2023-01-01)': '2023-08-30',
'出生体重': '50.00',
'冻精编号': '51568'
},
{
'耳号': '202308301036',
'品类': '肉用型牛',
'品种': '蒙古牛',
'生理阶段': '犊牛',
'性别': '母',
'血统纯度': '杂交',
'栏舍': '牛舍-20230819',
'所属批次': '230508357',
'已产胎次': '1',
'来源': '购买',
'现估重(公斤)': '50',
'代数': '1',
'出生日期(格式必须为2023-01-01)': '2023-08-30',
'入场日期(格式必须为2023-01-01)': '2023-08-30',
'出生体重': '45.00',
'冻精编号': '51568'
}
];
// 使用ExportUtils生成Excel文件按照图片中的列顺序
const ExportUtils = require('../utils/exportUtils');
const result = await ExportUtils.exportToExcelWithStyle(templateData, [
{ title: '耳号', dataIndex: '耳号', key: 'earNumber', width: 15, required: true },
{ title: '品类', dataIndex: '品类', key: 'strain', width: 12, required: true },
{ title: '品种', dataIndex: '品种', key: 'varieties', width: 12, required: true },
{ title: '生理阶段', dataIndex: '生理阶段', key: 'cate', width: 12, required: true },
{ title: '性别', dataIndex: '性别', key: 'sex', width: 8, required: true },
{ title: '血统纯度', dataIndex: '血统纯度', key: 'descent', width: 12, required: false },
{ title: '栏舍', dataIndex: '栏舍', key: 'penName', width: 15, required: false },
{ title: '所属批次', dataIndex: '所属批次', key: 'batchName', width: 15, required: false },
{ title: '已产胎次', dataIndex: '已产胎次', key: 'parity', width: 10, required: false },
{ title: '来源', dataIndex: '来源', key: 'source', width: 10, required: true },
{ title: '现估重(公斤)', dataIndex: '现估重(公斤)', key: 'currentWeight', width: 12, required: true },
{ title: '代数', dataIndex: '代数', key: 'algebra', width: 8, required: false },
{ title: '出生日期(格式必须为2023-01-01)', dataIndex: '出生日期(格式必须为2023-01-01)', key: 'birthday', width: 25, required: true },
{ title: '入场日期(格式必须为2023-01-01)', dataIndex: '入场日期(格式必须为2023-01-01)', key: 'intoTime', width: 25, required: false },
{ title: '出生体重', dataIndex: '出生体重', key: 'birthWeight', width: 12, required: false },
{ title: '冻精编号', dataIndex: '冻精编号', key: 'semenNum', width: 12, required: false }
], '牛只档案导入模板');
if (result.success) {
// 使用Express的res.download方法
res.download(result.filePath, '牛只档案导入模板.xlsx', (err) => {
if (err) {
console.error('文件下载失败:', err);
if (!res.headersSent) {
res.status(500).json({
success: false,
message: '文件下载失败',
error: err.message
});
}
} else {
// 下载成功后删除临时文件
const fs = require('fs');
fs.unlink(result.filePath, (err) => {
if (err) console.error('删除临时文件失败:', err);
});
}
});
} else {
res.status(500).json({
success: false,
message: '生成模板文件失败',
error: result.message
});
}
} catch (error) {
console.error('下载导入模板失败:', error);
res.status(500).json({
success: false,
message: '下载模板失败',
error: error.message
});
}
}
}
module.exports = new IotCattleController();