rentease-backend-new/controllers/meterReadingController.js

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