const { MeterReading, Bill, Room, Renter, Rental } = require('../models'); const { Op } = require('sequelize'); const response = require('../utils/response'); // 生成账单编号 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); response.success(res, '获取成功', { list: formattedReadings, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }); } catch (error) { console.error('获取抄表记录列表失败:', error); response.serverError(res, '获取抄表记录列表失败', error); } }; // 获取单个抄表记录 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 response.notFound(res, '抄表记录不存在'); } response.success(res, '获取成功', formatMeterReadingData(reading)); } catch (error) { console.error('获取抄表记录详情失败:', error); response.serverError(res, '获取抄表记录详情失败', error); } }; // 创建抄表记录(事务处理 + 自动创建账单) const createMeterReading = async (req, res) => { const transaction = await require('../config/db').transaction(); 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; // 参数校验 if (currentReading < previousReading) { await transaction.rollback(); return response.badRequest(res, '本期读数不能小于上期读数'); } // 创建抄表记录 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 }, { transaction }); // 自动创建关联账单 const categoryMap = { 'water': '水费', 'electricity': '电费', 'gas': '燃气费' }; const bill = await Bill.create({ billNo: generateBillNo(), roomId, renterId: renterId || null, rentalId: rentalId || null, type: 'income', category: meterType, receivableAmount: amount, receivedAmount: 0, status: 'unpaid', billMonth, billDate: readingDate || new Date(), settlementType: 'normal', sourceType: 'meter_reading', sourceId: meterReading.id, remark: `${categoryMap[meterType] || meterType}账单 - ${billMonth}`, tenantId, createBy: req.user.id, updateBy: req.user.id, isDeleted: 0 }, { transaction }); // 更新抄表记录的账单ID await meterReading.update({ billId: bill.id }, { transaction }); // 提交事务 await transaction.commit(); response.created(res, '抄表记录创建成功,已自动生成账单', { meterReading: formatMeterReadingData(meterReading), bill: { id: bill.id, billNo: bill.billNo, receivableAmount: bill.receivableAmount, status: bill.status } }); } catch (error) { await transaction.rollback(); console.error('创建抄表记录失败:', error); if (error.name === 'SequelizeUniqueConstraintError') { const field = error.errors[0]?.path || '未知字段'; if (field === 'uk_room_type_month') { return response.error(res, '该房间本月已存在此类型的抄表记录,请勿重复创建', 409); } return response.error(res, `数据重复: ${field} 必须唯一`, 409); } if (error.name === 'SequelizeValidationError') { const messages = error.errors.map(e => `${e.path}: ${e.message}`).join(', '); return response.badRequest(res, `Validation error: ${messages}`); } response.serverError(res, '创建抄表记录失败', error); } }; // 更新抄表记录(事务处理 + 同步更新账单 + 账单状态校验) const updateMeterReading = async (req, res) => { const transaction = await require('../config/db').transaction(); 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 }, transaction }); if (!meterReading) { await transaction.rollback(); return response.notFound(res, '抄表记录不存在'); } // 校验参数 if (currentReading < previousReading) { await transaction.rollback(); return response.badRequest(res, '本期读数不能小于上期读数'); } // 重新计算用量和金额 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 }, { transaction }); // 同步更新关联账单(需校验账单状态) if (meterReading.billId) { const bill = await Bill.findOne({ where: { id: meterReading.billId, isDeleted: 0 }, transaction }); if (bill) { // 账单已部分收款或已结清,不允许修改金额 if (bill.status === 'partial' || bill.status === 'paid') { await transaction.rollback(); return response.badRequest(res, `该抄表记录对应的账单已${bill.status === 'paid' ? '结清' : '部分收款'},不允许修改金额`); } const categoryMap = { 'water': '水费', 'electricity': '电费', 'gas': '燃气费' }; await bill.update({ receivableAmount: amount, remark: `${categoryMap[meterReading.meterType] || meterReading.meterType}账单 - ${bill.billMonth}`, updateBy: req.user.id }, { transaction }); } } // 提交事务 await transaction.commit(); response.success(res, '更新成功', { meterReading: formatMeterReadingData(meterReading), bill: meterReading.billId ? { id: meterReading.billId, receivableAmount: amount, status: bill?.status } : null }); } catch (error) { await transaction.rollback(); console.error('更新抄表记录失败:', error); response.serverError(res, '更新抄表记录失败', error); } }; // 删除抄表记录(同时删除关联账单) 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 response.notFound(res, '抄表记录不存在'); } // 软删除抄表记录 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 }); } } response.success(res, '抄表记录及关联账单删除成功'); } catch (error) { console.error('删除抄表记录失败:', error); response.serverError(res, '删除抄表记录失败', error); } }; // 获取房间历史抄表记录 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); response.success(res, '获取成功', { list: formattedReadings, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }); } catch (error) { console.error('获取房间抄表记录失败:', error); response.serverError(res, '获取房间抄表记录失败', error); } }; // 获取最新读数(用于创建时自动填充上期读数) const getLatestReading = async (req, res) => { const { roomId, meterType } = req.query; if (!roomId) { return response.badRequest(res, 'roomId不能为空'); } try { const latestReading = await MeterReading.findOne({ where: { roomId, meterType, tenantId: req.user.tenantId, isDeleted: 0 }, order: [['billMonth', 'DESC']] }); response.success(res, '获取成功', latestReading ? { previousReading: latestReading.currentReading, billMonth: latestReading.billMonth, unitPrice: latestReading.unitPrice } : null); } catch (error) { console.error('获取最新读数失败:', error); response.serverError(res, '获取最新读数失败', error); } }; module.exports = { getAllMeterReadings, getMeterReadingById, createMeterReading, updateMeterReading, deleteMeterReading, getRoomMeterReadings, getLatestReading };