const { MeterReading, Bill, Room, Renter, Rental } = require('../models'); const { Op } = require('sequelize'); // 生成账单编号 const generateBillNo = () => { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const random = Math.floor(1000 + Math.random() * 9000); return `B${year}${month}${day}${random}`; }; // 格式化时间 const formatDate = (date) => { if (!date) return null; const dateObj = date instanceof Date ? date : new Date(date); const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000); return beijingDate.toISOString().split('T')[0]; }; const formatDateTime = (date) => { if (!date) return null; const dateObj = date instanceof Date ? date : new Date(date); const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000); return beijingDate.toISOString().replace('T', ' ').slice(0, 19); }; // 格式化抄表数据 const formatMeterReadingData = (reading) => { const formatted = { ...reading.toJSON(), createTime: formatDateTime(reading.createTime), updateTime: formatDateTime(reading.updateTime), readingDate: formatDate(reading.readingDate) }; if (formatted.Room) { formatted.roomNumber = formatted.Room.roomNumber; formatted.apartmentName = formatted.Room.Apartment?.name; } if (formatted.Renter) { formatted.renterName = formatted.Renter.name; } if (formatted.Bill) { formatted.billStatus = formatted.Bill.status; formatted.receivedAmount = formatted.Bill.receivedAmount; } return formatted; }; // 获取所有抄表记录 const getAllMeterReadings = async (req, res) => { try { const { roomId, meterType, billMonth, page = 1, pageSize = 10 } = req.query; const where = { tenantId: req.user.tenantId, isDeleted: 0 }; if (roomId) { where.roomId = roomId; } if (meterType) { where.meterType = meterType; } if (billMonth) { where.billMonth = billMonth; } const offset = (page - 1) * pageSize; const { count, rows } = await MeterReading.findAndCountAll({ where, include: [ { model: Room, attributes: ['roomNumber'], include: [{ model: require('../models').Apartment, attributes: ['name'] }] }, { model: Renter, attributes: ['name', 'phone'] }, { model: Bill, as: 'bill', required: false, where: { isDeleted: 0 }, attributes: ['status', 'receivedAmount'] } ], limit: parseInt(pageSize), offset: parseInt(offset), order: [['createTime', 'DESC']] }); const formattedReadings = rows.map(formatMeterReadingData); res.status(200).json({ code: 200, data: formattedReadings, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }); } catch (error) { console.error('获取抄表记录列表失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; // 获取单个抄表记录 const getMeterReadingById = async (req, res) => { try { const { id } = req.params; const reading = await MeterReading.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, include: [ { model: Room, attributes: ['roomNumber'], include: [{ model: require('../models').Apartment, attributes: ['name'] }] }, { model: Renter, attributes: ['name', 'phone'] }, { model: Rental, attributes: ['startDate', 'endDate'] }, { model: Bill, as: 'bill', required: false, where: { isDeleted: 0 } } ] }); if (!reading) { return res.status(404).json({ code: 404, error: '抄表记录不存在' }); } res.status(200).json({ code: 200, data: formatMeterReadingData(reading), message: 'success' }); } catch (error) { console.error('获取抄表记录详情失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; // 创建抄表记录(同时创建账单) const createMeterReading = async (req, res) => { try { const { roomId, renterId, rentalId, meterType, previousReading, currentReading, unitPrice, billMonth, readingDate, remark } = req.body; const usage = parseFloat(currentReading) - parseFloat(previousReading); const amount = usage * parseFloat(unitPrice); const tenantId = req.user.tenantId; // 创建抄表记录 const meterReading = await MeterReading.create({ roomId, renterId: renterId || null, rentalId: rentalId || null, meterType, previousReading, currentReading, usage, unitPrice, amount, billMonth, readingDate: readingDate || new Date(), remark, tenantId, createBy: req.user.id, updateBy: req.user.id, isDeleted: 0 }); // 自动创建关联账单 const categoryMap = { 'water': 'water', 'electricity': 'electricity', 'gas': 'gas' }; const bill = await Bill.create({ billNo: generateBillNo(), roomId, renterId: renterId || null, rentalId: rentalId || null, type: 'income', category: categoryMap[meterType], receivableAmount: amount, receivedAmount: 0, status: 'unpaid', billMonth, billDate: readingDate || new Date(), sourceType: 'meter_reading', sourceId: meterReading.id, remark: `${meterType === 'water' ? '水费' : meterType === 'electricity' ? '电费' : '燃气费'} - ${billMonth}`, tenantId, createBy: req.user.id, updateBy: req.user.id, isDeleted: 0 }); // 更新抄表记录的账单ID await meterReading.update({ billId: bill.id }); res.status(201).json({ code: 201, data: { meterReading: formatMeterReadingData(meterReading), bill: { id: bill.id, billNo: bill.billNo, status: bill.status } }, message: '抄表记录创建成功,已自动生成账单' }); } catch (error) { console.error('创建抄表记录失败:', error); // 处理唯一约束错误 if (error.name === 'SequelizeUniqueConstraintError') { const field = error.errors[0]?.path || '未知字段'; if (field === 'uk_room_type_month') { return res.status(409).json({ code: 409, error: '该房间本月已存在此类型的抄表记录,请勿重复创建' }); } return res.status(409).json({ code: 409, error: `数据重复: ${field} 必须唯一` }); } // 输出详细的验证错误信息 if (error.name === 'SequelizeValidationError') { const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', '); console.error('验证错误详情:', messages); return res.status(400).json({ code: 400, error: `Validation error: ${messages}` }); } res.status(500).json({ code: 500, error: error.message }); } }; // 更新抄表记录 const updateMeterReading = async (req, res) => { try { const { id } = req.params; const { previousReading, currentReading, unitPrice, readingDate, remark } = req.body; const meterReading = await MeterReading.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 } }); if (!meterReading) { return res.status(404).json({ code: 404, error: '抄表记录不存在' }); } // 重新计算用量和金额 const usage = parseFloat(currentReading) - parseFloat(previousReading); const amount = usage * parseFloat(unitPrice); await meterReading.update({ previousReading, currentReading, usage, unitPrice, amount, readingDate: readingDate || meterReading.readingDate, remark, updateBy: req.user.id }); // 同步更新关联账单金额 if (meterReading.billId) { const bill = await Bill.findByPk(meterReading.billId); if (bill && bill.isDeleted === 0) { await bill.update({ receivableAmount: amount, updateBy: req.user.id }); } } res.status(200).json({ code: 200, data: formatMeterReadingData(meterReading), message: '更新成功' }); } catch (error) { console.error('更新抄表记录失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; // 删除抄表记录(同时删除关联账单) const deleteMeterReading = async (req, res) => { try { const { id } = req.params; const meterReading = await MeterReading.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 } }); if (!meterReading) { return res.status(404).json({ code: 404, error: '抄表记录不存在' }); } // 软删除抄表记录 await meterReading.update({ isDeleted: 1, updateBy: req.user.id }); // 同步软删除关联账单 if (meterReading.billId) { const bill = await Bill.findByPk(meterReading.billId); if (bill && bill.isDeleted === 0) { await bill.update({ isDeleted: 1, updateBy: req.user.id }); } } res.status(200).json({ code: 200, message: '抄表记录及关联账单删除成功' }); } catch (error) { console.error('删除抄表记录失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; // 获取房间历史抄表记录 const getRoomMeterReadings = async (req, res) => { try { const { roomId } = req.params; const { meterType, page = 1, pageSize = 10 } = req.query; const where = { roomId, tenantId: req.user.tenantId, isDeleted: 0 }; if (meterType) { where.meterType = meterType; } const offset = (page - 1) * pageSize; const { count, rows } = await MeterReading.findAndCountAll({ where, include: [ { model: Bill, as: 'bill', required: false, where: { isDeleted: 0 }, attributes: ['status', 'receivedAmount'] } ], limit: parseInt(pageSize), offset: parseInt(offset), order: [['billMonth', 'DESC']] }); const formattedReadings = rows.map(formatMeterReadingData); res.status(200).json({ code: 200, data: formattedReadings, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }); } catch (error) { console.error('获取房间抄表记录失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; // 获取最新读数(用于创建时自动填充上期读数) const getLatestReading = async (req, res) => { try { const { roomId, meterType } = req.query; const latestReading = await MeterReading.findOne({ where: { roomId, meterType, tenantId: req.user.tenantId, isDeleted: 0 }, order: [['billMonth', 'DESC']] }); res.status(200).json({ code: 200, data: latestReading ? { previousReading: latestReading.currentReading, billMonth: latestReading.billMonth } : null }); } catch (error) { console.error('获取最新读数失败:', error); res.status(500).json({ code: 500, error: error.message }); } }; module.exports = { getAllMeterReadings, getMeterReadingById, createMeterReading, updateMeterReading, deleteMeterReading, getRoomMeterReadings, getLatestReading };