const { Op } = require('sequelize'); 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] || '未知'; }; /** * 获取栏舍、批次、品种和用途名称 */ 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为用途名称 varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称 cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文 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; 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) { return res.status(404).json({ success: false, message: '牛只档案不存在' }); } // 格式化数据(基于iot_cattle表字段映射) const formattedData = { id: cattle.id, earNumber: cattle.earNumber, // 映射iot_cattle.ear_number sex: cattle.sex, // 映射iot_cattle.sex strain: cattle.strain, // 映射iot_cattle.strain varieties: cattle.varieties, // 映射iot_cattle.varieties(单个记录不需要名称映射) cate: cattle.cate, // 映射iot_cattle.cate 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 ? `栏舍ID:${cattle.penId}` : '未分配栏舍', // 暂时显示ID,后续可优化 batchName: cattle.batchId === 0 ? '未分配批次' : `批次ID:${cattle.batchId}`, // 暂时显示ID,后续可优化 farmId: cattle.orgId, // 映射iot_cattle.org_id penId: cattle.penId, // 映射iot_cattle.pen_id batchId: cattle.batchId // 映射iot_cattle.batch_id }; res.json({ success: true, data: formattedData, message: '获取牛只档案详情成功' }); } catch (error) { console.error('获取牛只档案详情失败:', error); 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 = {}; if (updateData.earNumber) processedData.earNumber = parseInt(updateData.earNumber); if (updateData.sex) processedData.sex = parseInt(updateData.sex); if (updateData.strain) processedData.strain = parseInt(updateData.strain); if (updateData.varieties) processedData.varieties = parseInt(updateData.varieties); if (updateData.cate) processedData.cate = parseInt(updateData.cate); if (updateData.birthWeight) processedData.birthWeight = parseFloat(updateData.birthWeight); if (updateData.birthday) processedData.birthday = parseInt(updateData.birthday); if (updateData.penId) processedData.penId = parseInt(updateData.penId); if (updateData.intoTime) processedData.intoTime = parseInt(updateData.intoTime); if (updateData.parity) processedData.parity = parseInt(updateData.parity); if (updateData.source) processedData.source = parseInt(updateData.source); if (updateData.sourceDay) processedData.sourceDay = parseInt(updateData.sourceDay); if (updateData.sourceWeight) processedData.sourceWeight = parseFloat(updateData.sourceWeight); if (updateData.orgId) processedData.orgId = parseInt(updateData.orgId); if (updateData.batchId) processedData.batchId = parseInt(updateData.batchId); 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解析逻辑 // 由于没有安装xlsx库,先返回模拟数据 const importedCount = 0; const errors = []; // TODO: 实现Excel文件解析和数据库插入逻辑 // 1. 使用xlsx库解析Excel文件 // 2. 验证数据格式 // 3. 批量插入到数据库 // 4. 返回导入结果 res.json({ success: true, message: '导入功能开发中', importedCount, errors }); } catch (error) { console.error('导入牛只档案数据失败:', error); res.status(500).json({ success: false, message: '导入失败', error: error.message }); } } /** * 下载导入模板 */ async downloadImportTemplate(req, res) { try { console.log('=== 下载牛只档案导入模板 ==='); // 创建模板数据 - 按照截图格式 const templateData = [ { '耳标编号': '2105523006', '性别': '1为公牛2为母牛', '品系': '1:乳肉兼用', '品种': '1:西藏高山牦牛2:宁夏牛', '类别': '1:犊牛,2:育成母牛,3:架子牛,4:青年牛,5:基础母牛,6:育肥牛', '出生体重(kg)': '30', '出生日期': '格式必须为(2023-1-15)', '栏舍ID': '1', '入栏时间': '2023-01-20', '胎次': '0', '来源': '1', '来源天数': '5', '来源体重': '35.5', '当前体重': '450.0', '事件': '正常', '事件时间': '2023-01-20', '泌乳天数': '0', '精液编号': '', '是否佩戴': '1', '批次ID': '1' } ]; // 使用ExportUtils生成Excel文件 const ExportUtils = require('../utils/exportUtils'); const result = ExportUtils.exportToExcel(templateData, [ { title: '耳标编号', dataIndex: '耳标编号', key: 'earNumber' }, { title: '性别', dataIndex: '性别', key: 'sex' }, { title: '品系', dataIndex: '品系', key: 'strain' }, { title: '品种', dataIndex: '品种', key: 'varieties' }, { title: '类别', dataIndex: '类别', key: 'cate' }, { title: '出生体重(kg)', dataIndex: '出生体重(kg)', key: 'birthWeight' }, { title: '出生日期', dataIndex: '出生日期', key: 'birthday' }, { title: '栏舍ID', dataIndex: '栏舍ID', key: 'penId' }, { title: '入栏时间', dataIndex: '入栏时间', key: 'intoTime' }, { title: '胎次', dataIndex: '胎次', key: 'parity' }, { title: '来源', dataIndex: '来源', key: 'source' }, { title: '来源天数', dataIndex: '来源天数', key: 'sourceDay' }, { title: '来源体重', dataIndex: '来源体重', key: 'sourceWeight' }, { title: '当前体重', dataIndex: '当前体重', key: 'weight' }, { title: '事件', dataIndex: '事件', key: 'event' }, { title: '事件时间', dataIndex: '事件时间', key: 'eventTime' }, { title: '泌乳天数', dataIndex: '泌乳天数', key: 'lactationDay' }, { title: '精液编号', dataIndex: '精液编号', key: 'semenNum' }, { title: '是否佩戴', dataIndex: '是否佩戴', key: 'isWear' }, { title: '批次ID', dataIndex: '批次ID', key: 'batchId' } ], '牛只档案导入模板'); 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();