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();