rentease-backend-new/controllers/meterReadingController.js

443 lines
12 KiB
JavaScript
Raw Normal View History

2026-04-20 06:43:09 +00:00
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
};