443 lines
12 KiB
JavaScript
443 lines
12 KiB
JavaScript
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
|
|
};
|